feat(02-04): mount Schedule-X CalendarShell wired to TanStack Query + Zustand + hydrateEvents

- CalendarShell.tsx: all four views (day/week/month-grid/month-agenda), eventsService + eventModal plugins
- calendarId routing: 'shared' | String(ownerUserId) via hydrateEvents, matching buildCalendarConfig keys
- SX_FIRST_DAY_OF_WEEK=7 (Sunday, Temporal convention); initial range from Zustand default (A4 guard)
- onRangeUpdate updates Zustand range, triggering TanStack Query refetch on navigation
- App.tsx: replaces EventProof landing with CalendarShell; tokens only (no hardcoded hex/px)
This commit is contained in:
Lucas Berger
2026-06-05 10:40:07 -04:00
parent dabe2ccd57
commit b79f649ac5
2 changed files with 186 additions and 107 deletions
+2 -107
View File
@@ -1,110 +1,5 @@
import { useQuery } from '@tanstack/react-query'
import { fetchMe, type MeUser } from './api/client'
import { EventProof } from './components/EventProof'
interface HealthResponse {
ok: boolean
db: string
}
async function fetchHealth(): Promise<HealthResponse> {
const res = await fetch('/health')
if (!res.ok) {
throw new Error(`Health check failed: ${res.status}`)
}
return res.json() as Promise<HealthResponse>
}
function ColorSwatch({ color }: { color: string }) {
return (
<span
style={{
display: 'inline-block',
width: '1rem',
height: '1rem',
borderRadius: '50%',
background: color,
marginRight: '0.5rem',
verticalAlign: 'middle',
border: '1px solid rgba(0,0,0,0.1)',
}}
aria-label={`Color: ${color}`}
/>
)
}
function MemberBadge({ user }: { user: MeUser }) {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
padding: '0.75rem 1rem',
borderRadius: '8px',
background: '#f0f9ff',
border: `2px solid ${user.color}`,
marginBottom: '1rem',
}}
>
<ColorSwatch color={user.color} />
<span style={{ fontWeight: 600 }}>
{user.displayName ?? 'Member'}
</span>
</div>
)
}
import { CalendarShell } from './components/CalendarShell.js'
export default function App() {
const meQuery = useQuery({
queryKey: ['me'],
queryFn: fetchMe,
retry: false, // 401 triggers Authelia redirect — don't retry
staleTime: 5 * 60 * 1000, // 5 min — session persists via refresh rotation
})
const healthQuery = useQuery({
queryKey: ['health'],
queryFn: fetchHealth,
retry: 1,
refetchInterval: 30_000,
})
return (
<div style={{ fontFamily: 'system-ui, sans-serif', padding: '2rem', maxWidth: '480px', margin: '0 auto' }}>
<h1 style={{ fontSize: '1.5rem', marginBottom: '1.5rem' }}>FamilySync</h1>
{/* Authenticated member identity (AUTH-03) */}
{meQuery.isLoading && (
<div style={{ color: '#666', marginBottom: '1rem' }}>Loading...</div>
)}
{meQuery.isError && (
<div style={{ color: '#991b1b', marginBottom: '1rem', padding: '0.75rem', background: '#fee2e2', borderRadius: '8px' }}>
Sign-in required
</div>
)}
{meQuery.data && (
<MemberBadge user={meQuery.data.user} />
)}
{/* Broker proof: one cached Fastmail event (CAL-01) */}
<div style={{ marginBottom: '1rem' }}>
<EventProof />
</div>
{/* Stack health indicator (from Plan 01) */}
<div
style={{
padding: '1rem',
borderRadius: '8px',
background: healthQuery.isLoading ? '#f5f5f5' : healthQuery.isError ? '#fee2e2' : '#dcfce7',
color: healthQuery.isLoading ? '#666' : healthQuery.isError ? '#991b1b' : '#166534',
fontSize: '0.875rem',
}}
>
{healthQuery.isLoading && 'Checking stack...'}
{healthQuery.isError && 'stack: down'}
{healthQuery.data && `stack: ${healthQuery.data.ok && healthQuery.data.db === 'up' ? 'up' : 'down'}`}
</div>
</div>
)
return <CalendarShell />
}