UMBRA/frontend/src/components/calendar/SharedCalendarSection.tsx
Kyle Pope 4e3fd35040 Phase 3: Frontend core for shared calendars
Types: CalendarPermission, SharedCalendarMembership, CalendarMemberInfo,
CalendarInvite, EventLockInfo. Calendar type extended with is_shared.

Hooks: useCalendars extended with shared calendar polling (5s).
useSharedCalendars for member CRUD, invite responses, color updates.
useConnections cascade invalidation on disconnect.

New components: PermissionBadge, CalendarMemberSearch,
CalendarMemberRow, CalendarMemberList, SharedCalendarSection,
SharedCalendarSettings (non-owner dialog with color, members, leave).

Modified: CalendarForm (sharing toggle, member management for owners),
CalendarSidebar (shared calendars section with localStorage visibility),
CalendarPage (shared calendar ID integration in event filtering),
NotificationToaster (calendar_invite toast with accept/decline),
NotificationsPage (calendar_invite inline actions + type icons).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 04:59:13 +08:00

91 lines
2.9 KiB
TypeScript

import { useState } from 'react';
import { Pencil } from 'lucide-react';
import { Checkbox } from '@/components/ui/checkbox';
import type { SharedCalendarMembership } from '@/types';
import SharedCalendarSettings from './SharedCalendarSettings';
const STORAGE_KEY = 'umbra_shared_cal_visibility';
function loadVisibility(): Record<number, boolean> {
try {
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
} catch {
return {};
}
}
function saveVisibility(v: Record<number, boolean>) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(v));
}
interface SharedCalendarSectionProps {
memberships: SharedCalendarMembership[];
visibleSharedIds: Set<number>;
onVisibilityChange: (calendarId: number, visible: boolean) => void;
}
export default function SharedCalendarSection({
memberships,
visibleSharedIds,
onVisibilityChange,
}: SharedCalendarSectionProps) {
const [settingsFor, setSettingsFor] = useState<SharedCalendarMembership | null>(null);
if (memberships.length === 0) return null;
return (
<>
<div className="space-y-1.5">
<div className="px-2">
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">
Shared Calendars
</span>
</div>
<div className="space-y-0.5">
{memberships.map((m) => {
const color = m.local_color || m.calendar_color;
const isVisible = visibleSharedIds.has(m.calendar_id);
return (
<div
key={m.id}
className="group flex items-center gap-2.5 rounded-md px-2 py-1.5 hover:bg-card-elevated transition-colors duration-150"
>
<Checkbox
checked={isVisible}
onChange={() => onVisibilityChange(m.calendar_id, !isVisible)}
className="shrink-0"
style={{
accentColor: color,
borderColor: isVisible ? color : undefined,
backgroundColor: isVisible ? color : undefined,
}}
/>
<span
className="h-2.5 w-2.5 rounded-full shrink-0"
style={{ backgroundColor: color }}
/>
<span className="text-sm text-foreground truncate flex-1">{m.calendar_name}</span>
<button
onClick={() => setSettingsFor(m)}
className="opacity-0 group-hover:opacity-100 transition-opacity duration-150 text-muted-foreground hover:text-foreground"
>
<Pencil className="h-3.5 w-3.5" />
</button>
</div>
);
})}
</div>
</div>
{settingsFor && (
<SharedCalendarSettings
membership={settingsFor}
onClose={() => setSettingsFor(null)}
/>
)}
</>
);
}
export { STORAGE_KEY, loadVisibility, saveVisibility };