This commit is contained in:
Nikita Lokhmachev
2025-12-16 13:47:29 -05:00
commit 4a0db63a30
84 changed files with 12595 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
import { useState, useEffect, useMemo } from 'react';
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 { generateMockAvailability } from '@/data/mockData';
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 } from 'lucide-react';
const STORAGE_KEY = 'calendar-participants';
const SETTINGS_KEY = 'calendar-settings';
interface Settings {
showPartialAvailability: boolean;
}
const defaultSettings: Settings = {
showPartialAvailability: false,
};
const Index = () => {
const [participants, setParticipants] = useState<Participant[]>([]);
const [selectedParticipants, setSelectedParticipants] = useState<Participant[]>([]);
const [selectedSlot, setSelectedSlot] = useState<TimeSlot | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [settings, setSettings] = useState<Settings>(defaultSettings);
// Load participants from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
try {
setParticipants(JSON.parse(stored));
} catch (e) {
console.error('Failed to parse stored participants');
}
}
}, []);
// Load settings from localStorage on mount
useEffect(() => {
const stored = localStorage.getItem(SETTINGS_KEY);
if (stored) {
try {
setSettings({ ...defaultSettings, ...JSON.parse(stored) });
} catch (e) {
console.error('Failed to parse stored settings');
}
}
}, []);
// Save participants to localStorage when changed
useEffect(() => {
localStorage.setItem(STORAGE_KEY, JSON.stringify(participants));
}, [participants]);
// Save settings to localStorage when changed
useEffect(() => {
localStorage.setItem(SETTINGS_KEY, JSON.stringify(settings));
}, [settings]);
const handleAddParticipant = (data: { name: string; email: string; icsLink: string }) => {
const newParticipant: Participant = {
id: crypto.randomUUID(),
name: data.name,
email: data.email,
icsLink: data.icsLink,
connected: true,
};
setParticipants((prev) => [...prev, newParticipant]);
};
const handleRemoveParticipant = (id: string) => {
setParticipants((prev) => prev.filter((p) => p.id !== id));
setSelectedParticipants((prev) => prev.filter((p) => p.id !== id));
};
// Generate availability when participants change
const availabilitySlots = useMemo(() => {
return generateMockAvailability(selectedParticipants);
}, [selectedParticipants]);
const handleSlotSelect = (slot: TimeSlot) => {
setSelectedSlot(slot);
setIsModalOpen(true);
};
return (
<div className="min-h-screen bg-background">
<Header />
<main className="container max-w-5xl mx-auto px-4 py-8">
<Tabs defaultValue="schedule" className="space-y-6">
<TabsList className="grid w-full max-w-md mx-auto grid-cols-2">
<TabsTrigger value="participants" className="flex items-center gap-2">
<Users className="w-4 h-4" />
Participants
</TabsTrigger>
<TabsTrigger value="schedule" className="flex items-center gap-2">
<CalendarDays className="w-4 h-4" />
Schedule
</TabsTrigger>
</TabsList>
<TabsContent value="participants" className="animate-fade-in">
<div className="text-center mb-6">
<h2 className="text-3xl font-bold text-foreground mb-2">
Manage Participants
</h2>
<p className="text-muted-foreground">
Add team members with their calendar ICS links
</p>
</div>
<ParticipantManager
participants={participants}
onAddParticipant={handleAddParticipant}
onRemoveParticipant={handleRemoveParticipant}
/>
</TabsContent>
<TabsContent value="schedule" className="animate-fade-in">
<div className="space-y-8">
<div className="text-center relative">
<div className="absolute right-0 top-0">
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon">
<Settings className="w-5 h-5" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-72" align="end">
<div className="space-y-4">
<h4 className="font-medium">Settings</h4>
<div className="flex items-center justify-between gap-4">
<Label htmlFor="partial-availability" className="text-sm cursor-pointer">
Show partial availability
</Label>
<Switch
id="partial-availability"
checked={settings.showPartialAvailability}
onCheckedChange={(checked) =>
setSettings((prev) => ({ ...prev, showPartialAvailability: checked }))
}
/>
</div>
<p className="text-xs text-muted-foreground">
When enabled, shows time slots where only some participants are available.
</p>
</div>
</PopoverContent>
</Popover>
</div>
<h2 className="text-3xl font-bold text-foreground mb-2">
Schedule a Meeting
</h2>
<p className="text-muted-foreground">
Find the perfect time that works for everyone
</p>
</div>
{participants.length === 0 ? (
<div className="bg-card rounded-xl shadow-card p-8 text-center">
<Users className="w-12 h-12 mx-auto mb-4 text-muted-foreground opacity-50" />
<h3 className="text-lg font-medium text-foreground mb-2">No participants yet</h3>
<p className="text-muted-foreground">
Add participants in the Participants tab to start scheduling.
</p>
</div>
) : (
<>
<div className="bg-card rounded-xl shadow-card p-6">
<h3 className="text-lg font-semibold text-foreground mb-4">
Who's joining?
</h3>
<ParticipantSelector
participants={participants}
selectedParticipants={selectedParticipants}
onSelectionChange={setSelectedParticipants}
/>
</div>
<AvailabilityHeatmap
slots={availabilitySlots}
selectedParticipants={selectedParticipants}
onSlotSelect={handleSlotSelect}
showPartialAvailability={settings.showPartialAvailability}
/>
</>
)}
</div>
</TabsContent>
</Tabs>
</main>
<ScheduleModal
isOpen={isModalOpen}
onClose={() => {
setIsModalOpen(false);
setSelectedSlot(null);
}}
slot={selectedSlot}
participants={selectedParticipants}
/>
</div>
);
};
export default Index;