import { useState, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { Header } from '@/components/Header'; import { ParticipantSelector } from '@/components/ParticipantSelector'; import { ParticipantManager } from '@/components/ParticipantManager'; import { AvailabilityHeatmap } from '@/components/AvailabilityHeatmap'; import { ScheduleModal } from '@/components/ScheduleModal'; import { Participant, TimeSlot } from '@/types/calendar'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { Button } from '@/components/ui/button'; import { Users, CalendarDays, Settings, RefreshCw } from 'lucide-react'; import { useToast } from '@/hooks/use-toast'; import { fetchParticipants, createParticipant, updateParticipant, deleteParticipant, fetchAvailability, syncCalendars, ParticipantAPI, } from '@/api/client'; const SETTINGS_KEY = 'calendar-settings'; interface SettingsState { showPartialAvailability: boolean; } const defaultSettings: SettingsState = { showPartialAvailability: false, }; function apiToParticipant(p: ParticipantAPI): Participant { return { id: p.id, name: p.name, email: p.email, timezone: p.timezone, icsLink: p.ics_url, connected: true, }; } interface IndexProps { defaultTab?: string; } const Index = ({ defaultTab = 'schedule' }: IndexProps) => { const navigate = useNavigate(); const location = useLocation(); const [activeTab, setActiveTab] = useState(defaultTab); const [participants, setParticipants] = useState([]); const [selectedParticipants, setSelectedParticipants] = useState([]); const [availabilitySlots, setAvailabilitySlots] = useState([]); const [selectedSlot, setSelectedSlot] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [settings, setSettings] = useState(defaultSettings); const [isLoading, setIsLoading] = useState(false); const [isSyncing, setIsSyncing] = useState(false); const { toast } = useToast(); useEffect(() => { // Sync internal state if prop changes (e.g. browser back button) setActiveTab(defaultTab); }, [defaultTab]); const handleTabChange = (value: string) => { setActiveTab(value); navigate(`/${value}`); }; useEffect(() => { const stored = localStorage.getItem(SETTINGS_KEY); if (stored) { try { setSettings({ ...defaultSettings, ...JSON.parse(stored) }); } catch (e) { console.error('Failed to parse stored settings'); } } }, []); useEffect(() => { localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings)); }, [settings]); useEffect(() => { loadParticipants(); }, []); useEffect(() => { if (selectedParticipants.length > 0) { loadAvailability(); } else { setAvailabilitySlots([]); } }, [selectedParticipants]); const loadParticipants = async () => { try { const data = await fetchParticipants(); setParticipants(data.map(apiToParticipant)); } catch (error) { toast({ title: 'Error loading participants', description: error instanceof Error ? error.message : 'Unknown error', variant: 'destructive', }); } }; const loadAvailability = async () => { setIsLoading(true); try { const ids = selectedParticipants.map((p) => p.id); const slots = await fetchAvailability(ids); setAvailabilitySlots(slots); } catch (error) { toast({ title: 'Error loading availability', description: error instanceof Error ? error.message : 'Unknown error', variant: 'destructive', }); } finally { setIsLoading(false); } }; const handleAddParticipant = async (data: { name: string; email: string; timezone: string; icsLink: string }) => { try { const created = await createParticipant({ name: data.name, email: data.email, timezone: data.timezone, ics_url: data.icsLink || undefined, }); setParticipants((prev) => [...prev, apiToParticipant(created)]); toast({ title: 'Participant added', description: `${data.name} has been added and calendar synced`, }); } catch (error) { toast({ title: 'Error adding participant', description: error instanceof Error ? error.message : 'Unknown error', variant: 'destructive', }); } }; const handleRemoveParticipant = async (id: string) => { try { await deleteParticipant(id); setParticipants((prev) => prev.filter((p) => p.id !== id)); setSelectedParticipants((prev) => prev.filter((p) => p.id !== id)); toast({ title: 'Participant removed', }); } catch (error) { toast({ title: 'Error removing participant', description: error instanceof Error ? error.message : 'Unknown error', variant: 'destructive', }); } }; const handleUpdateParticipant = async (id: string, data: { timezone?: string; ics_url?: string }) => { const updated = await updateParticipant(id, data); setParticipants((prev) => prev.map((p) => (p.id === id ? apiToParticipant(updated) : p)) ); setSelectedParticipants((prev) => prev.map((p) => (p.id === id ? apiToParticipant(updated) : p)) ); }; const handleSyncCalendars = async () => { setIsSyncing(true); try { await syncCalendars(); if (selectedParticipants.length > 0) { await loadAvailability(); } toast({ title: 'Calendars synced', description: 'All calendars have been refreshed', }); } catch (error) { toast({ title: 'Error syncing calendars', description: error instanceof Error ? error.message : 'Unknown error', variant: 'destructive', }); } finally { setIsSyncing(false); } }; const handleSlotSelect = (slot: TimeSlot) => { setSelectedSlot(slot); setIsModalOpen(true); }; return (
People Schedule

Manage People

Add team members with their calendar ICS links

Settings

setSettings((prev) => ({ ...prev, showPartialAvailability: checked })) } />

When enabled, shows time slots where only some participants are available.

Schedule a Meeting

Find the perfect time that works for everyone

{participants.length === 0 ? (

No participants yet

Add people in the People tab to start scheduling.

) : ( <>

Who's joining?

)}
{ setIsModalOpen(false); setSelectedSlot(null); }} slot={selectedSlot} participants={selectedParticipants} />
); }; export default Index;