- 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>
80 lines
2.9 KiB
TypeScript
80 lines
2.9 KiB
TypeScript
import { MapPin, StickyNote } from "lucide-react";
|
||
import type { CalendarShareData, EventShareData } from "../server/share-queries";
|
||
|
||
function formatEventTime(startAt: string, endAt: string, allDay: boolean): string {
|
||
const start = new Date(startAt);
|
||
const end = new Date(endAt);
|
||
if (allDay) {
|
||
return start.toLocaleDateString(undefined, { weekday: "short", month: "long", day: "numeric" });
|
||
}
|
||
const dateStr = start.toLocaleDateString(undefined, {
|
||
weekday: "short",
|
||
month: "long",
|
||
day: "numeric",
|
||
});
|
||
const startTime = start.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
||
const endTime = end.toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" });
|
||
return `${dateStr} · ${startTime}–${endTime}`;
|
||
}
|
||
|
||
export function EventSharedView({ data }: { data: EventShareData }) {
|
||
return (
|
||
<div className="mx-auto max-w-xl space-y-4 p-4">
|
||
<header className="space-y-1">
|
||
<p className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
||
{data.calendarName}
|
||
</p>
|
||
<h1 className="text-2xl font-semibold">{data.title}</h1>
|
||
<p className="text-sm text-muted-foreground">
|
||
{formatEventTime(data.startAt, data.endAt, data.allDay)}
|
||
</p>
|
||
</header>
|
||
{data.location && (
|
||
<div className="flex items-start gap-2 text-sm">
|
||
<MapPin className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
||
<span>{data.location}</span>
|
||
</div>
|
||
)}
|
||
{data.notes && (
|
||
<div className="flex items-start gap-2 text-sm">
|
||
<StickyNote className="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
||
<p className="whitespace-pre-wrap">{data.notes}</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function CalendarSharedView({ data }: { data: CalendarShareData }) {
|
||
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">
|
||
Upcoming events — next 90 days
|
||
</p>
|
||
</header>
|
||
{data.events.length === 0 ? (
|
||
<p className="text-sm text-muted-foreground">No upcoming events.</p>
|
||
) : (
|
||
<ul className="divide-y rounded-lg border bg-background">
|
||
{data.events.map((event) => (
|
||
<li key={event.id} className="flex flex-col gap-0.5 px-4 py-3">
|
||
<span className="font-medium">{event.title}</span>
|
||
<span className="text-xs text-muted-foreground">
|
||
{formatEventTime(event.startAt, event.endAt, event.allDay)}
|
||
</span>
|
||
{event.location && (
|
||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||
<MapPin className="size-3" />
|
||
{event.location}
|
||
</span>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|