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>
91 lines
2.9 KiB
TypeScript
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 };
|