fix: tighten sharing and shadcn composition
CI / checks (push) Successful in 12m55s
CI / build (push) Successful in 15m16s

This commit is contained in:
ginnoir
2026-06-13 05:20:01 -05:00
parent e1774c802d
commit a312d4ce39
42 changed files with 1539 additions and 417 deletions
@@ -17,6 +17,7 @@ import type { CalView } from "@/modules/_core/themes";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
@@ -48,6 +49,10 @@ type EventDraft = {
type MobileView = "day" | "week" | "agenda";
const DEFAULT_COLOR = "#B85C3C";
const VISIBILITY_ITEMS = [
{ label: "Household", value: "household" },
{ label: "Private", value: "private" },
];
const VIEW_MAP: Record<CalView, string> = {
month: "dayGridMonth",
@@ -115,6 +120,10 @@ export function CalendarShell({
() => eventRows.filter((event) => visibleIds.has(event.calendarId)),
[eventRows, visibleIds],
);
const calendarSelectItems = useMemo(
() => calendarRows.map((calendar) => ({ label: calendar.name, value: calendar.id })),
[calendarRows],
);
function toggleCalendar(id: string) {
setVisibleIds((current) => {
@@ -343,6 +352,7 @@ export function CalendarShell({
onChange={(event) => updateCalendar(calendar, { color: event.target.value })}
/>
<Select
items={VISIBILITY_ITEMS}
value={calendar.visibility}
onValueChange={(value) =>
updateCalendar(calendar, { visibility: value as "private" | "household" })
@@ -354,8 +364,13 @@ export function CalendarShell({
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="household">Household</SelectItem>
<SelectItem value="private">Private</SelectItem>
<SelectGroup>
{VISIBILITY_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Button
@@ -391,6 +406,7 @@ export function CalendarShell({
onChange={(event) => setCalendarColorValue(event.target.value)}
/>
<Select
items={VISIBILITY_ITEMS}
value={calendarVisibility}
onValueChange={(value) =>
setCalendarVisibilityValue(value as "private" | "household")
@@ -402,8 +418,13 @@ export function CalendarShell({
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="household">Household</SelectItem>
<SelectItem value="private">Private</SelectItem>
<SelectGroup>
{VISIBILITY_ITEMS.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
@@ -444,6 +465,7 @@ export function CalendarShell({
/>
<Label htmlFor="event-calendar">Calendar</Label>
<Select
items={calendarSelectItems}
value={selectedEvent.calendarId}
onValueChange={(value) => setSelectedEvent({ ...selectedEvent, calendarId: value ?? "" })}
>
@@ -454,11 +476,13 @@ export function CalendarShell({
</SelectValue>
</SelectTrigger>
<SelectContent>
{calendarRows.map((calendar) => (
<SelectItem key={calendar.id} value={calendar.id}>
{calendar.name}
</SelectItem>
))}
<SelectGroup>
{calendarSelectItems.map((calendar) => (
<SelectItem key={calendar.value} value={calendar.value}>
{calendar.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<div className="grid gap-3 sm:grid-cols-2">
+6 -2
View File
@@ -2,6 +2,8 @@ import type { ModuleManifest, WidgetContext } from "../_core/module";
import { z } from "zod";
import { listCalendars, listEvents, searchCalendars, searchEvents } from "./server/queries";
import {
canShareCalendar,
canShareEvent,
loadCalendarForShare,
loadEventForShare,
type CalendarShareData,
@@ -150,7 +152,8 @@ const manifest: ModuleManifest = {
share: { canShare: true, defaultCapabilities: ["read"] },
search: { search: searchCalendars },
resolveUrl: (id) => `/calendar?id=${id}`,
loadForShare: (id) => loadCalendarForShare(id),
canShareEntity: canShareCalendar,
loadForShare: loadCalendarForShare,
renderSharedView: ({ data }) => <CalendarSharedView data={data as CalendarShareData} />,
renderActivity: (entry) => {
const name = entry.payload?.name as string | undefined;
@@ -166,7 +169,8 @@ const manifest: ModuleManifest = {
reminder: { canRemind: true },
search: { search: searchEvents },
resolveUrl: (id) => `/calendar/events/${id}`,
loadForShare: (id) => loadEventForShare(id),
canShareEntity: canShareEvent,
loadForShare: loadEventForShare,
renderSharedView: ({ data }) => <EventSharedView data={data as EventShareData} />,
renderActivity: (entry) => {
const title = entry.payload?.title as string | undefined;
+48 -4
View File
@@ -1,5 +1,6 @@
import { and, asc, eq, gte, lte } from "drizzle-orm";
import { db } from "@/lib/db";
import type { PublicShareContext, ShareContext } from "@/modules/_core/module";
import { calendarEvents, calendars } from "../schema";
export type EventSummary = {
@@ -21,11 +22,51 @@ export type CalendarShareData = {
export type EventShareData = EventSummary & { calendarName: string };
export async function loadCalendarForShare(id: string): Promise<CalendarShareData | null> {
function canSeeCalendarRow(
row: { householdId: string; ownerId: string; visibility: string },
ctx: ShareContext,
): boolean {
if (row.householdId !== ctx.householdId) return false;
return row.visibility === "household" || row.ownerId === ctx.userId;
}
export async function canShareCalendar(id: string, ctx: ShareContext): Promise<boolean> {
const [calendar] = await db
.select({
householdId: calendars.householdId,
ownerId: calendars.ownerId,
visibility: calendars.visibility,
})
.from(calendars)
.where(eq(calendars.id, id))
.limit(1);
return calendar ? canSeeCalendarRow(calendar, ctx) : false;
}
export async function canShareEvent(id: string, ctx: ShareContext): Promise<boolean> {
const [row] = await db
.select({
householdId: calendars.householdId,
ownerId: calendars.ownerId,
visibility: calendars.visibility,
})
.from(calendarEvents)
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
.where(eq(calendarEvents.id, id))
.limit(1);
return row ? canSeeCalendarRow(row, ctx) : false;
}
export async function loadCalendarForShare(
id: string,
ctx: PublicShareContext,
): Promise<CalendarShareData | null> {
const [calendar] = await db
.select({ id: calendars.id, name: calendars.name, color: calendars.color })
.from(calendars)
.where(eq(calendars.id, id))
.where(and(eq(calendars.id, id), eq(calendars.householdId, ctx.householdId)))
.limit(1);
if (!calendar) return null;
@@ -63,7 +104,10 @@ export async function loadCalendarForShare(id: string): Promise<CalendarShareDat
};
}
export async function loadEventForShare(id: string): Promise<EventShareData | null> {
export async function loadEventForShare(
id: string,
ctx: PublicShareContext,
): Promise<EventShareData | null> {
const [row] = await db
.select({
id: calendarEvents.id,
@@ -77,7 +121,7 @@ export async function loadEventForShare(id: string): Promise<EventShareData | nu
})
.from(calendarEvents)
.innerJoin(calendars, eq(calendarEvents.calendarId, calendars.id))
.where(eq(calendarEvents.id, id))
.where(and(eq(calendarEvents.id, id), eq(calendars.householdId, ctx.householdId)))
.limit(1);
if (!row) return null;