feat: UX
This commit is contained in:
219
frontend/src/pages/Index.tsx
Normal file
219
frontend/src/pages/Index.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user