From 14fc085009191bdb9e1576c60a0d7109ae2882b5 Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Fri, 6 Mar 2026 15:38:52 +0800 Subject: [PATCH] =?UTF-8?q?Phase=205:=20Shared=20calendar=20polish=20?= =?UTF-8?q?=E2=80=94=20scoped=20polling,=20admin=20stats,=20dual=20panel?= =?UTF-8?q?=20fix,=20edge=20case=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Scope shared calendar polling to CalendarPage only (other consumers no longer poll) - Add admin sharing stats card (owned/member/invites sent/received) in UserDetailSection - Fix dual EventDetailPanel mount via JS media query breakpoint (replaces CSS hidden) - Auto-close panel + toast when shared calendar is removed while viewing Co-Authored-By: Claude Opus 4.6 --- .../components/admin/UserDetailSection.tsx | 23 +++++++- .../src/components/calendar/CalendarPage.tsx | 54 ++++++++++++------- frontend/src/hooks/useAdmin.ts | 18 +++++++ frontend/src/hooks/useCalendars.ts | 8 ++- 4 files changed, 80 insertions(+), 23 deletions(-) diff --git a/frontend/src/components/admin/UserDetailSection.tsx b/frontend/src/components/admin/UserDetailSection.tsx index 51261a4..26ab578 100644 --- a/frontend/src/components/admin/UserDetailSection.tsx +++ b/frontend/src/components/admin/UserDetailSection.tsx @@ -1,10 +1,10 @@ -import { X, User, ShieldCheck, Loader2 } from 'lucide-react'; +import { X, User, ShieldCheck, Share2, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Select } from '@/components/ui/select'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; -import { useAdminUserDetail, useUpdateRole, getErrorMessage } from '@/hooks/useAdmin'; +import { useAdminUserDetail, useAdminSharingStats, useUpdateRole, getErrorMessage } from '@/hooks/useAdmin'; import { getRelativeTime } from '@/lib/date-utils'; import { cn } from '@/lib/utils'; import type { UserRole } from '@/types'; @@ -57,6 +57,7 @@ function MfaBadge({ enabled, pending }: { enabled: boolean; pending: boolean }) export default function UserDetailSection({ userId, onClose }: UserDetailSectionProps) { const { data: user, isLoading, error } = useAdminUserDetail(userId); const updateRole = useUpdateRole(); + const { data: sharingStats } = useAdminSharingStats(userId); const handleRoleChange = async (newRole: UserRole) => { if (!user || newRole === user.role) return; @@ -218,6 +219,24 @@ export default function UserDetailSection({ userId, onClose }: UserDetailSection /> + + {/* Sharing Stats */} + + +
+
+ +
+ Sharing +
+
+ + + + + + +
); } diff --git a/frontend/src/components/calendar/CalendarPage.tsx b/frontend/src/components/calendar/CalendarPage.tsx index 51b15b2..cf5ad2a 100644 --- a/frontend/src/components/calendar/CalendarPage.tsx +++ b/frontend/src/components/calendar/CalendarPage.tsx @@ -42,7 +42,7 @@ export default function CalendarPage() { const [createDefaults, setCreateDefaults] = useState(null); const { settings } = useSettings(); - const { data: calendars = [], sharedData } = useCalendars(); + const { data: calendars = [], sharedData, allCalendarIds } = useCalendars({ pollingEnabled: true }); const [visibleSharedIds, setVisibleSharedIds] = useState>(new Set()); const calendarContainerRef = useRef(null); @@ -99,6 +99,15 @@ export default function CalendarPage() { const panelOpen = panelMode !== 'closed'; + // Track desktop breakpoint to prevent dual EventDetailPanel mount + const [isDesktop, setIsDesktop] = useState(() => window.matchMedia('(min-width: 1024px)').matches); + useEffect(() => { + const mql = window.matchMedia('(min-width: 1024px)'); + const handler = (e: MediaQueryListEvent) => setIsDesktop(e.matches); + mql.addEventListener('change', handler); + return () => mql.removeEventListener('change', handler); + }, []); + // Continuously resize calendar during panel open/close CSS transition useEffect(() => { let rafId: number; @@ -150,6 +159,15 @@ export default function CalendarPage() { const selectedEventPermission = selectedEvent ? permissionMap.get(selectedEvent.calendar_id) ?? null : null; const selectedEventIsShared = selectedEvent ? permissionMap.has(selectedEvent.calendar_id) && permissionMap.get(selectedEvent.calendar_id) !== 'owner' : false; + // Close panel if shared calendar was removed while viewing + useEffect(() => { + if (!selectedEvent || allCalendarIds.size === 0) return; + if (!allCalendarIds.has(selectedEvent.calendar_id)) { + handlePanelClose(); + toast.info('This calendar is no longer available'); + } + }, [allCalendarIds, selectedEvent]); + // Escape key closes detail panel useEffect(() => { if (!panelOpen) return; @@ -497,29 +515,27 @@ export default function CalendarPage() { {/* Detail panel (desktop) */} -
- -
+ {panelOpen && isDesktop && ( +
+ +
+ )} {/* Mobile detail panel overlay */} - {panelOpen && ( + {panelOpen && !isDesktop && (
({ + queryKey: ['admin', 'users', userId, 'sharing-stats'], + queryFn: async () => { + const { data } = await api.get(`/admin/users/${userId}/sharing-stats`); + return data; + }, + enabled: userId !== null, + }); +} + export function useAdminDashboard() { return useQuery({ queryKey: ['admin', 'dashboard'], diff --git a/frontend/src/hooks/useCalendars.ts b/frontend/src/hooks/useCalendars.ts index 825de5e..7bbfd3f 100644 --- a/frontend/src/hooks/useCalendars.ts +++ b/frontend/src/hooks/useCalendars.ts @@ -3,7 +3,11 @@ import { useQuery } from '@tanstack/react-query'; import api from '@/lib/api'; import type { Calendar, SharedCalendarMembership } from '@/types'; -export function useCalendars() { +interface UseCalendarsOptions { + pollingEnabled?: boolean; +} + +export function useCalendars({ pollingEnabled = false }: UseCalendarsOptions = {}) { const ownedQuery = useQuery({ queryKey: ['calendars'], queryFn: async () => { @@ -18,7 +22,7 @@ export function useCalendars() { const { data } = await api.get('/shared-calendars'); return data; }, - refetchInterval: 5_000, + refetchInterval: pollingEnabled ? 5_000 : false, staleTime: 3_000, });