Files
famapp/src/modules/lists/components/shared-view.tsx
T
ginnoirandClaude Sonnet 4.6 39aa5704f9 Implement share viewer (task 31)
- Makes actorId nullable in activity_log (migration 0010) with onDelete:
  set null so audit history survives user deletion; adds logShareActivity
  for anonymous mutations
- Adds resolveShareToken return of householdId for downstream validation
- Adds renderSharedView to EntityTypeRegistration; each module implements
  loadForShare (no-session bare queries) and renderSharedView
- Calendar: CalendarSharedView (upcoming 90-day events) and
  EventSharedView (title/time/location/notes)
- Lists: ListSharedView (client component with optimistic toggles via
  toggleShareListItem server action in lists/server/share-actions.ts)
- Notes: NoteSharedView (title + body + updated date)
- /app/s/[token]/page.tsx: resolves token, dispatches to loadForShare +
  renderSharedView, friendly error for invalid/expired tokens, noindex
- Middleware /s/* exemption confirmed present (no change needed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-06 14:26:38 -05:00

105 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useOptimistic, useTransition } from "react";
import type { ListShareData, ListShareItem } from "../server/share-queries";
import { toggleShareListItem } from "../server/share-actions";
export function ListSharedView({
data,
canWrite,
token,
}: {
data: ListShareData;
canWrite: boolean;
token: string;
}) {
const [optimisticItems, setOptimisticItems] = useOptimistic(
data.items,
(current: ListShareItem[], { id, done }: { id: string; done: boolean }) =>
current.map((item) => (item.id === id ? { ...item, done } : item)),
);
const [, startTransition] = useTransition();
function toggle(item: ListShareItem) {
if (!canWrite) return;
const next = !item.done;
startTransition(async () => {
setOptimisticItems({ id: item.id, done: next });
await toggleShareListItem(token, item.id);
});
}
const open = optimisticItems.filter((i) => !i.done);
const done = optimisticItems.filter((i) => i.done);
return (
<div className="mx-auto max-w-xl space-y-4 p-4">
<header>
<h1 className="text-2xl font-semibold">{data.name}</h1>
<p className="text-sm text-muted-foreground capitalize">{data.type}</p>
</header>
{optimisticItems.length === 0 ? (
<p className="text-sm text-muted-foreground">This list is empty.</p>
) : (
<div className="space-y-4">
{open.length > 0 && (
<ul className="divide-y rounded-lg border bg-background">
{open.map((item) => (
<ItemRow key={item.id} item={item} canWrite={canWrite} onToggle={toggle} />
))}
</ul>
)}
{done.length > 0 && (
<details className="group">
<summary className="cursor-pointer select-none text-sm text-muted-foreground">
{done.length} completed
</summary>
<ul className="mt-2 divide-y rounded-lg border bg-background">
{done.map((item) => (
<ItemRow key={item.id} item={item} canWrite={canWrite} onToggle={toggle} />
))}
</ul>
</details>
)}
</div>
)}
</div>
);
}
function ItemRow({
item,
canWrite,
onToggle,
}: {
item: ListShareItem;
canWrite: boolean;
onToggle: (item: ListShareItem) => void;
}) {
return (
<li className="flex items-center gap-3 px-4 py-3">
{canWrite ? (
<input
type="checkbox"
aria-label={`Complete ${item.text}`}
className="size-5 accent-primary"
checked={item.done}
onChange={() => onToggle(item)}
/>
) : (
<span
aria-hidden
className={`size-5 shrink-0 rounded-sm border border-border ${item.done ? "bg-muted" : ""}`}
/>
)}
<span className={`text-sm ${item.done ? "text-muted-foreground line-through" : ""}`}>
{item.text}
{item.qty && (
<span className="ml-1 text-xs text-muted-foreground">×{item.qty}</span>
)}
</span>
</li>
);
}