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>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
d5deee9a46
commit
39aa5704f9
@@ -0,0 +1,79 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user