
Some checks failed
GBCI / build (push) Failing after 10m45s
- Introduced a new CSS theme for Orange, featuring a modern color palette with distinct foreground and background colors. - Added an XTree Gold theme that emulates the classic 1980s DOS interface, complete with authentic colors and styles for file management elements. - Both themes include variables for customization and specific styles for various UI components such as cards, popovers, and menus.
976 lines
No EOL
37 KiB
TypeScript
976 lines
No EOL
37 KiB
TypeScript
"use client";
|
|
import React, { useState, useMemo } from 'react';
|
|
import {
|
|
Calendar, ChevronLeft, ChevronRight, Plus, Search, Settings,
|
|
Clock, Users, MapPin, Bell, Repeat, Mail, Phone, Video,
|
|
Edit3, Trash2, Copy, Forward, Reply, MoreHorizontal,
|
|
Filter, RefreshCw, Print, Import, Export, Share2,
|
|
ChevronDown, User, Building, Globe, AlertCircle,
|
|
CheckCircle2, X, Eye, EyeOff, Star, Flag, Tag
|
|
} from 'lucide-react';
|
|
import { cn } from "@/lib/utils";
|
|
|
|
// Sample calendar data
|
|
const sampleEvents = [
|
|
{
|
|
id: 1,
|
|
title: "Team Standup",
|
|
start: "2025-06-28T09:00:00",
|
|
end: "2025-06-28T09:30:00",
|
|
type: "meeting",
|
|
location: "Conference Room A",
|
|
attendees: ["john@company.com", "jane@company.com", "mike@company.com"],
|
|
description: "Daily team synchronization meeting",
|
|
priority: "high",
|
|
status: "accepted",
|
|
organizer: "sarah@company.com",
|
|
category: "work",
|
|
recurring: true
|
|
},
|
|
{
|
|
id: 2,
|
|
title: "Client Presentation",
|
|
start: "2025-06-28T14:00:00",
|
|
end: "2025-06-28T15:30:00",
|
|
type: "meeting",
|
|
location: "Board Room",
|
|
attendees: ["client@external.com", "manager@company.com"],
|
|
description: "Q2 results presentation to key client",
|
|
priority: "high",
|
|
status: "tentative",
|
|
organizer: "me@company.com",
|
|
category: "important",
|
|
recurring: false
|
|
},
|
|
{
|
|
id: 3,
|
|
title: "Lunch with Marketing Team",
|
|
start: "2025-06-28T12:00:00",
|
|
end: "2025-06-28T13:00:00",
|
|
type: "personal",
|
|
location: "Downtown Cafe",
|
|
attendees: ["marketing@company.com"],
|
|
description: "Monthly team lunch",
|
|
priority: "normal",
|
|
status: "accepted",
|
|
organizer: "marketing@company.com",
|
|
category: "social",
|
|
recurring: true
|
|
},
|
|
{
|
|
id: 4,
|
|
title: "Project Review",
|
|
start: "2025-06-29T10:00:00",
|
|
end: "2025-06-29T11:00:00",
|
|
type: "meeting",
|
|
location: "Online",
|
|
attendees: ["team@company.com"],
|
|
description: "Weekly project status review",
|
|
priority: "normal",
|
|
status: "accepted",
|
|
organizer: "me@company.com",
|
|
category: "work",
|
|
recurring: true
|
|
},
|
|
{
|
|
id: 5,
|
|
title: "Doctor Appointment",
|
|
start: "2025-06-30T15:00:00",
|
|
end: "2025-06-30T16:00:00",
|
|
type: "personal",
|
|
location: "Medical Center",
|
|
attendees: [],
|
|
description: "Annual checkup",
|
|
priority: "normal",
|
|
status: "accepted",
|
|
organizer: "me@company.com",
|
|
category: "personal",
|
|
recurring: false
|
|
}
|
|
];
|
|
|
|
const categoriesData = [
|
|
{ name: "Work", color: "blue", visible: true, count: 12 },
|
|
{ name: "Personal", color: "green", visible: true, count: 5 },
|
|
{ name: "Important", color: "red", visible: true, count: 3 },
|
|
{ name: "Social", color: "purple", visible: true, count: 8 },
|
|
{ name: "Travel", color: "orange", visible: false, count: 2 },
|
|
{ name: "Family", color: "pink", visible: true, count: 4 }
|
|
];
|
|
|
|
// Date utilities
|
|
const formatDate = (date) => {
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
}).format(date);
|
|
};
|
|
|
|
const formatTime = (dateString) => {
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
hour: 'numeric',
|
|
minute: '2-digit',
|
|
hour12: true
|
|
}).format(new Date(dateString));
|
|
};
|
|
|
|
const formatDateShort = (date) => {
|
|
return new Intl.DateTimeFormat('en-US', {
|
|
month: 'short',
|
|
day: 'numeric'
|
|
}).format(date);
|
|
};
|
|
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
DropdownMenuSeparator,
|
|
} from "@/components/ui/dropdown-menu";
|
|
import {
|
|
ContextMenu,
|
|
ContextMenuContent,
|
|
ContextMenuItem,
|
|
ContextMenuTrigger,
|
|
ContextMenuSeparator,
|
|
} from "@/components/ui/context-menu";
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipTrigger,
|
|
TooltipProvider,
|
|
} from "@/components/ui/tooltip";
|
|
import {
|
|
ResizableHandle,
|
|
ResizablePanel,
|
|
ResizablePanelGroup,
|
|
} from "@/components/ui/resizable";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import Footer from '../footer';
|
|
|
|
const CalendarSidebar = ({ isCollapsed, categories, onCategoryToggle, currentDate, onDateSelect }) => {
|
|
const [selectedCategories, setSelectedCategories] = useState(
|
|
categories.filter(cat => cat.visible).map(cat => cat.name)
|
|
);
|
|
|
|
const toggleCategory = (categoryName) => {
|
|
const newSelected = selectedCategories.includes(categoryName)
|
|
? selectedCategories.filter(name => name !== categoryName)
|
|
: [...selectedCategories, categoryName];
|
|
setSelectedCategories(newSelected);
|
|
onCategoryToggle(categoryName);
|
|
};
|
|
|
|
const generateCalendarDays = () => {
|
|
const today = new Date();
|
|
const currentMonth = today.getMonth();
|
|
const currentYear = today.getFullYear();
|
|
const firstDay = new Date(currentYear, currentMonth, 1);
|
|
const lastDay = new Date(currentYear, currentMonth + 1, 0);
|
|
const startDate = new Date(firstDay);
|
|
startDate.setDate(startDate.getDate() - firstDay.getDay());
|
|
|
|
const days = [];
|
|
for (let i = 0; i < 42; i++) {
|
|
const date = new Date(startDate);
|
|
date.setDate(startDate.getDate() + i);
|
|
days.push(date);
|
|
}
|
|
return days;
|
|
};
|
|
|
|
const calendarDays = generateCalendarDays();
|
|
const today = new Date();
|
|
|
|
const quickActions = [
|
|
{ icon: Plus, label: "New Event", action: "new-event" },
|
|
{ icon: Users, label: "New Meeting", action: "new-meeting" },
|
|
];
|
|
|
|
if (isCollapsed) {
|
|
return (
|
|
<div className="p-2 space-y-2 bg-card border-r border-border">
|
|
{quickActions.map((action, index) => (
|
|
<Tooltip key={index} delayDuration={0}>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="w-9 h-9 bg-secondary hover:bg-secondary/80">
|
|
<action.icon className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right" className="bg-popover text-popover-foreground border-border">
|
|
{action.label}
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-card border-r border-border w-[200px]">
|
|
{/* Quick Actions */}
|
|
<div className="p-3 border-b border-border bg-secondary">
|
|
<h3 className="text-sm font-semibold text-foreground mb-2 font-mono">Quick Actions</h3>
|
|
<div className="space-y-1">
|
|
{quickActions.map((action, index) => (
|
|
<Button
|
|
key={index}
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-start text-xs h-8 hover:bg-secondary/80 bg-secondary"
|
|
>
|
|
<action.icon className="h-3 w-3 mr-2" />
|
|
{action.label}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Mini Calendar */}
|
|
<div className="p-3 border-b border-border bg-secondary">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="text-sm font-semibold text-foreground font-mono">
|
|
{today.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
|
|
</h3>
|
|
<div className="flex gap-1">
|
|
<Button variant="ghost" size="icon" className="h-6 w-6 bg-secondary hover:bg-secondary/80">
|
|
<ChevronLeft className="h-3 w-3" />
|
|
</Button>
|
|
<Button variant="ghost" size="icon" className="h-6 w-6 bg-secondary hover:bg-secondary/80">
|
|
<ChevronRight className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-7 gap-1 text-xs font-mono">
|
|
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map(day => (
|
|
<div key={day} className="text-center text-foreground font-medium p-1">
|
|
{day}
|
|
</div>
|
|
))}
|
|
{calendarDays.map((date, index) => {
|
|
const isToday = date.toDateString() === today.toDateString();
|
|
const isCurrentMonth = date.getMonth() === today.getMonth();
|
|
return (
|
|
<button
|
|
key={index}
|
|
onClick={() => onDateSelect(date)}
|
|
className={cn(
|
|
"p-1 text-center rounded-none hover:bg-secondary/80 transition-colors font-mono",
|
|
isToday && "bg-primary text-primary-foreground hover:bg-primary",
|
|
!isCurrentMonth && "text-muted-foreground",
|
|
isCurrentMonth && !isToday && "text-foreground"
|
|
)}
|
|
>
|
|
{date.getDate()}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Categories */}
|
|
<div className="flex-1 p-3 bg-secondary">
|
|
<h3 className="text-sm font-semibold text-foreground mb-2 font-mono">My Calendars</h3>
|
|
<ScrollArea className="h-full">
|
|
<div className="space-y-1">
|
|
{categories.map((category) => (
|
|
<div
|
|
key={category.name}
|
|
className="flex items-center justify-between p-1 rounded-none hover:bg-secondary/80"
|
|
>
|
|
<button
|
|
onClick={() => toggleCategory(category.name)}
|
|
className="flex items-center gap-2 flex-1 text-left"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{category.visible ? (
|
|
<Eye className="h-3 w-3 text-foreground" />
|
|
) : (
|
|
<EyeOff className="h-3 w-3 text-muted-foreground" />
|
|
)}
|
|
<div
|
|
className={cn(
|
|
"w-3 h-3 rounded-none",
|
|
category.color === 'blue' && "bg-blue-500",
|
|
category.color === 'green' && "bg-green-500",
|
|
category.color === 'red' && "bg-red-500",
|
|
category.color === 'purple' && "bg-purple-500",
|
|
category.color === 'orange' && "bg-orange-500",
|
|
category.color === 'pink' && "bg-pink-500"
|
|
)}
|
|
/>
|
|
</div>
|
|
<span className={cn(
|
|
"text-xs font-mono",
|
|
category.visible ? "text-foreground" : "text-muted-foreground"
|
|
)}>
|
|
{category.name}
|
|
</span>
|
|
</button>
|
|
<span className="text-xs text-foreground font-mono">({category.count})</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const EventCard = ({ event, onAction }) => {
|
|
const getCategoryColor = (category) => {
|
|
const colors = {
|
|
work: "bg-blue-500/20 border-l-blue-500 text-foreground",
|
|
personal: "bg-green-500/20 border-l-green-500 text-foreground",
|
|
important: "bg-red-500/20 border-l-red-500 text-foreground",
|
|
social: "bg-purple-500/20 border-l-purple-500 text-foreground"
|
|
};
|
|
return colors[category] || "bg-secondary border-l-muted text-foreground";
|
|
};
|
|
|
|
const getPriorityIcon = (priority) => {
|
|
if (priority === 'high') return <Flag className="h-3 w-3 text-red-500" />;
|
|
return null;
|
|
};
|
|
|
|
const getStatusIcon = (status) => {
|
|
switch (status) {
|
|
case 'accepted': return <CheckCircle2 className="h-3 w-3 text-green-500" />;
|
|
case 'tentative': return <AlertCircle className="h-3 w-3 text-yellow-500" />;
|
|
case 'declined': return <X className="h-3 w-3 text-red-500" />;
|
|
default: return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<ContextMenu>
|
|
<ContextMenuTrigger asChild>
|
|
<div className={cn(
|
|
"p-2 rounded-none border-l-4 cursor-pointer hover:shadow-none transition-all mb-1 border border-border",
|
|
getCategoryColor(event.category)
|
|
)}>
|
|
<div className="flex items-start justify-between mb-1">
|
|
<div className="flex items-center gap-1">
|
|
{getPriorityIcon(event.priority)}
|
|
<h4 className="text-sm font-medium truncate font-mono">{event.title}</h4>
|
|
</div>
|
|
<div className="flex items-center gap-1">
|
|
{event.recurring && <Repeat className="h-3 w-3 text-foreground" />}
|
|
{getStatusIcon(event.status)}
|
|
</div>
|
|
</div>
|
|
<div className="text-xs text-foreground space-y-1 font-mono">
|
|
<div className="flex items-center gap-1">
|
|
<Clock className="h-3 w-3" />
|
|
{formatTime(event.start)} - {formatTime(event.end)}
|
|
</div>
|
|
{event.location && (
|
|
<div className="flex items-center gap-1">
|
|
<MapPin className="h-3 w-3" />
|
|
<span className="truncate">{event.location}</span>
|
|
</div>
|
|
)}
|
|
{event.attendees.length > 0 && (
|
|
<div className="flex items-center gap-1">
|
|
<Users className="h-3 w-3" />
|
|
<span>{event.attendees.length} attendees</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</ContextMenuTrigger>
|
|
<ContextMenuContent className="bg-popover text-popover-foreground border-border font-mono">
|
|
<ContextMenuItem onClick={() => onAction('open', event)} className="hover:bg-primary hover:text-primary-foreground">
|
|
<Eye className="h-4 w-4 mr-2" />
|
|
Open
|
|
</ContextMenuItem>
|
|
<ContextMenuItem onClick={() => onAction('edit', event)} className="hover:bg-primary hover:text-primary-foreground">
|
|
<Edit3 className="h-4 w-4 mr-2" />
|
|
Edit
|
|
</ContextMenuItem>
|
|
<ContextMenuSeparator className="border-t border-border" />
|
|
<ContextMenuItem onClick={() => onAction('forward', event)} className="hover:bg-primary hover:text-primary-foreground">
|
|
<Forward className="h-4 w-4 mr-2" />
|
|
Forward
|
|
</ContextMenuItem>
|
|
<ContextMenuItem onClick={() => onAction('copy', event)} className="hover:bg-primary hover:text-primary-foreground">
|
|
<Copy className="h-4 w-4 mr-2" />
|
|
Copy
|
|
</ContextMenuItem>
|
|
<ContextMenuSeparator className="border-t border-border" />
|
|
<ContextMenuItem onClick={() => onAction('delete', event)} className="text-destructive hover:bg-primary hover:text-primary-foreground">
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete
|
|
</ContextMenuItem>
|
|
</ContextMenuContent>
|
|
</ContextMenu>
|
|
);
|
|
};
|
|
|
|
const CalendarView = ({ view, events, currentDate, onEventAction }) => {
|
|
const filteredEvents = events.filter(event => {
|
|
const eventDate = new Date(event.start);
|
|
if (view === 'day') {
|
|
return eventDate.toDateString() === currentDate.toDateString();
|
|
} else if (view === 'week') {
|
|
const weekStart = new Date(currentDate);
|
|
weekStart.setDate(currentDate.getDate() - currentDate.getDay());
|
|
const weekEnd = new Date(weekStart);
|
|
weekEnd.setDate(weekStart.getDate() + 6);
|
|
return eventDate >= weekStart && eventDate <= weekEnd;
|
|
} else if (view === 'month') {
|
|
return eventDate.getMonth() === currentDate.getMonth() &&
|
|
eventDate.getFullYear() === currentDate.getFullYear();
|
|
}
|
|
return true;
|
|
});
|
|
|
|
if (view === 'day') {
|
|
const hours = Array.from({ length: 24 }, (_, i) => i);
|
|
const dayEvents = filteredEvents.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-background">
|
|
<div className="p-4 bg-card border-b border-border">
|
|
<h2 className="text-lg font-semibold text-foreground font-mono">
|
|
{formatDate(currentDate)}
|
|
</h2>
|
|
</div>
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-4 space-y-2">
|
|
{dayEvents.length > 0 ? (
|
|
dayEvents.map(event => (
|
|
<EventCard
|
|
key={event.id}
|
|
event={event}
|
|
onAction={onEventAction}
|
|
/>
|
|
))
|
|
) : (
|
|
<div className="text-center py-8 text-foreground font-mono">
|
|
<Calendar className="h-12 w-12 mx-auto mb-2 text-muted-foreground" />
|
|
<p>No events scheduled for this day</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (view === 'week') {
|
|
const weekStart = new Date(currentDate);
|
|
weekStart.setDate(currentDate.getDate() - currentDate.getDay());
|
|
const weekDays = Array.from({ length: 7 }, (_, i) => {
|
|
const day = new Date(weekStart);
|
|
day.setDate(weekStart.getDate() + i);
|
|
return day;
|
|
});
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-background">
|
|
<div className="p-4 bg-card border-b border-border">
|
|
<h2 className="text-lg font-semibold text-foreground font-mono">
|
|
Week of {formatDateShort(weekStart)} - {formatDateShort(weekDays[6])}
|
|
</h2>
|
|
</div>
|
|
<div className="flex-1 grid grid-cols-7 gap-1 p-2">
|
|
{weekDays.map((day, index) => {
|
|
const dayEvents = filteredEvents.filter(event =>
|
|
new Date(event.start).toDateString() === day.toDateString()
|
|
);
|
|
const isToday = day.toDateString() === new Date().toDateString();
|
|
|
|
return (
|
|
<div key={index} className="border border-border rounded-none bg-card min-h-[200px]">
|
|
<div className={cn(
|
|
"p-2 text-center border-b border-border text-sm font-medium font-mono",
|
|
isToday ? "bg-primary text-primary-foreground" : "bg-secondary text-foreground"
|
|
)}>
|
|
<div>{day.toLocaleDateString('en-US', { weekday: 'short' })}</div>
|
|
<div className="text-lg">{day.getDate()}</div>
|
|
</div>
|
|
<div className="p-1 space-y-1">
|
|
{dayEvents.map(event => (
|
|
<div
|
|
key={event.id}
|
|
className={cn(
|
|
"p-1 rounded-none text-xs border-l-2 cursor-pointer hover:shadow-none border border-border font-mono",
|
|
event.category === 'work' && "bg-blue-500/20 border-l-blue-500",
|
|
event.category === 'personal' && "bg-green-500/20 border-l-green-500",
|
|
event.category === 'important' && "bg-red-500/20 border-l-red-500",
|
|
event.category === 'social' && "bg-purple-500/20 border-l-purple-500"
|
|
)}
|
|
onClick={() => onEventAction('open', event)}
|
|
>
|
|
<div className="font-medium truncate">{event.title}</div>
|
|
<div className="text-foreground">{formatTime(event.start)}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Month view
|
|
const monthStart = new Date(currentDate.getFullYear(), currentDate.getMonth(), 1);
|
|
const monthEnd = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 0);
|
|
const calendarStart = new Date(monthStart);
|
|
calendarStart.setDate(calendarStart.getDate() - monthStart.getDay());
|
|
|
|
const calendarDays = Array.from({ length: 42 }, (_, i) => {
|
|
const day = new Date(calendarStart);
|
|
day.setDate(calendarStart.getDate() + i);
|
|
return day;
|
|
});
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-background">
|
|
<div className="p-4 bg-card border-b border-border">
|
|
<h2 className="text-lg font-semibold text-foreground font-mono">
|
|
{currentDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
|
|
</h2>
|
|
</div>
|
|
<div className="flex-1 grid grid-cols-7 gap-1 p-2">
|
|
{['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].map(day => (
|
|
<div key={day} className="p-2 text-center text-sm font-medium text-foreground bg-secondary border border-border font-mono">
|
|
{day.substring(0, 3)}
|
|
</div>
|
|
))}
|
|
{calendarDays.map((day, index) => {
|
|
const dayEvents = filteredEvents.filter(event =>
|
|
new Date(event.start).toDateString() === day.toDateString()
|
|
);
|
|
const isToday = day.toDateString() === new Date().toDateString();
|
|
const isCurrentMonth = day.getMonth() === currentDate.getMonth();
|
|
|
|
return (
|
|
<div key={index} className={cn(
|
|
"border border-border bg-card min-h-[100px] p-1",
|
|
!isCurrentMonth && "bg-secondary"
|
|
)}>
|
|
<div className={cn(
|
|
"text-sm font-medium mb-1 font-mono",
|
|
isToday ? "bg-primary text-primary-foreground rounded-none px-1" : "text-foreground",
|
|
!isCurrentMonth && "text-muted-foreground"
|
|
)}>
|
|
{day.getDate()}
|
|
</div>
|
|
<div className="space-y-1">
|
|
{dayEvents.slice(0, 3).map(event => (
|
|
<div
|
|
key={event.id}
|
|
className={cn(
|
|
"p-1 rounded-none text-xs border-l-2 cursor-pointer border border-border font-mono",
|
|
event.category === 'work' && "bg-blue-500/20 border-l-blue-500",
|
|
event.category === 'personal' && "bg-green-500/20 border-l-green-500",
|
|
event.category === 'important' && "bg-red-500/20 border-l-red-500",
|
|
event.category === 'social' && "bg-purple-500/20 border-l-purple-500"
|
|
)}
|
|
onClick={() => onEventAction('open', event)}
|
|
>
|
|
<div className="truncate font-medium">{event.title}</div>
|
|
</div>
|
|
))}
|
|
{dayEvents.length > 3 && (
|
|
<div className="text-xs text-foreground text-center font-mono">
|
|
+{dayEvents.length - 3} more
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const EventDetails = ({ event }) => {
|
|
if (!event) {
|
|
return (
|
|
<div className="flex flex-col items-center justify-center h-full text-center text-foreground bg-background font-mono">
|
|
<Calendar className="w-12 h-12 mb-4 text-muted-foreground" />
|
|
<div className="text-lg font-medium">No event selected</div>
|
|
<div className="text-sm">Select an event to view details</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col h-full bg-background border-l border-border">
|
|
{/* Header */}
|
|
<div className="p-4 border-b border-border bg-card">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="text-lg font-semibold text-foreground font-mono">{event.title}</h3>
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon" className="bg-secondary hover:bg-secondary/80">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="bg-popover text-popover-foreground border-border font-mono">
|
|
<DropdownMenuItem className="hover:bg-primary hover:text-primary-foreground">
|
|
<Edit3 className="h-4 w-4 mr-2" />
|
|
Edit
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="hover:bg-primary hover:text-primary-foreground">
|
|
<Forward className="h-4 w-4 mr-2" />
|
|
Forward
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="hover:bg-primary hover:text-primary-foreground">
|
|
<Reply className="h-4 w-4 mr-2" />
|
|
Reply
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator className="border-t border-border" />
|
|
<DropdownMenuItem className="text-destructive hover:bg-primary hover:text-primary-foreground">
|
|
<Trash2 className="h-4 w-4 mr-2" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
<Badge variant="outline" className={cn(
|
|
"rounded-none border border-border font-mono",
|
|
event.category === 'work' && "border-blue-500 text-foreground",
|
|
event.category === 'personal' && "border-green-500 text-foreground",
|
|
event.category === 'important' && "border-red-500 text-foreground",
|
|
event.category === 'social' && "border-purple-500 text-foreground"
|
|
)}>
|
|
{event.category}
|
|
</Badge>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<ScrollArea className="flex-1">
|
|
<div className="p-4 space-y-4 font-mono">
|
|
{/* Time */}
|
|
<div className="flex items-center gap-2">
|
|
<Clock className="h-4 w-4 text-foreground" />
|
|
<div>
|
|
<div className="font-medium">
|
|
{formatTime(event.start)} - {formatTime(event.end)}
|
|
</div>
|
|
<div className="text-sm text-foreground">
|
|
{new Date(event.start).toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric'
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Location */}
|
|
{event.location && (
|
|
<div className="flex items-center gap-2">
|
|
<MapPin className="h-4 w-4 text-foreground" />
|
|
<span className="text-foreground">{event.location}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Organizer */}
|
|
<div className="flex items-center gap-2">
|
|
<User className="h-4 w-4 text-foreground" />
|
|
<div>
|
|
<div className="font-medium text-foreground">Organizer</div>
|
|
<div className="text-sm text-foreground">{event.organizer}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Attendees */}
|
|
{event.attendees.length > 0 && (
|
|
<div className="flex items-start gap-2">
|
|
<Users className="h-4 w-4 text-foreground mt-1" />
|
|
<div className="flex-1">
|
|
<div className="font-medium mb-1 text-foreground">Attendees ({event.attendees.length})</div>
|
|
<div className="space-y-1">
|
|
{event.attendees.map((attendee, index) => (
|
|
<div key={index} className="text-sm text-foreground flex items-center gap-2">
|
|
<div className="w-2 h-2 bg-green-500 rounded-none"></div>
|
|
{attendee}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Description */}
|
|
{event.description && (
|
|
<div>
|
|
<div className="font-medium mb-2 text-foreground">Description</div>
|
|
<div className="text-sm text-foreground leading-relaxed">
|
|
{event.description}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Properties */}
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-foreground">Priority</span>
|
|
<Badge variant={event.priority === 'high' ? 'destructive' : 'secondary'} className="rounded-none border border-border font-mono">
|
|
{event.priority}
|
|
</Badge>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-foreground">Status</span>
|
|
<Badge variant={
|
|
event.status === 'accepted' ? 'default' :
|
|
event.status === 'tentative' ? 'secondary' : 'destructive'
|
|
} className="rounded-none border border-border font-mono">
|
|
{event.status}
|
|
</Badge>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-sm font-medium text-foreground">Recurring</span>
|
|
<Badge variant={event.recurring ? 'default' : 'secondary'} className="rounded-none border border-border font-mono">
|
|
{event.recurring ? 'Yes' : 'No'}
|
|
</Badge>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* Footer */}
|
|
<div className="p-4 border-t border-border flex justify-end gap-2">
|
|
<Button variant="outline" size="sm" className="rounded-none border border-border font-mono">
|
|
<X className="h-4 w-4 mr-2" />
|
|
Decline
|
|
</Button>
|
|
<Button variant="outline" size="sm" className="rounded-none border border-border font-mono">
|
|
<Clock className="h-4 w-4 mr-2" />
|
|
Tentative
|
|
</Button>
|
|
<Button size="sm" className="rounded-none border border-border bg-primary text-primary-foreground font-mono hover:bg-primary/90">
|
|
<CheckCircle2 className="h-4 w-4 mr-2" />
|
|
Accept
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const CalendarPage = () => {
|
|
const [currentDate, setCurrentDate] = useState(new Date());
|
|
const [selectedEvent, setSelectedEvent] = useState(null);
|
|
const [categories, setCategories] = useState(categoriesData);
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
|
|
|
const handleEventAction = (action, event) => {
|
|
switch (action) {
|
|
case 'open':
|
|
setSelectedEvent(event);
|
|
break;
|
|
case 'edit':
|
|
// Handle edit
|
|
break;
|
|
case 'delete':
|
|
// Handle delete
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
};
|
|
|
|
const handleCategoryToggle = (categoryName) => {
|
|
setCategories(categories.map(cat =>
|
|
cat.name === categoryName ? { ...cat, visible: !cat.visible } : cat
|
|
));
|
|
};
|
|
|
|
const handleDateSelect = (date) => {
|
|
setCurrentDate(date);
|
|
};
|
|
|
|
const shortcuts = [
|
|
// Calendar actions row (unique qkeys)
|
|
[
|
|
{ key: 'Q', label: 'New Event', action: () => console.log('New Event') },
|
|
{ key: 'W', label: 'New Meeting', action: () => console.log('New Meeting') },
|
|
{ key: 'E', label: 'Edit Event', action: () => console.log('Edit Event') },
|
|
{ key: 'R', label: 'Refresh', action: () => console.log('Refresh') },
|
|
{ key: 'T', label: 'Today', action: () => setCurrentDate(new Date()) },
|
|
{ key: 'Y', label: 'Delete Event', action: () => console.log('Delete Event') },
|
|
{ key: 'U', label: 'Duplicate', action: () => console.log('Duplicate Event') },
|
|
{ key: 'I', label: 'Invite', action: () => console.log('Invite Attendees') },
|
|
{ key: 'O', label: 'Open Event', action: () => console.log('Open Event') },
|
|
{ key: 'P', label: 'Print', action: () => console.log('Print Calendar') },
|
|
],
|
|
// Navigation/info row (unique qkeys)
|
|
[
|
|
{ key: 'A', label: 'Accept', action: () => console.log('Accept Invitation') },
|
|
{ key: 'S', label: 'Send', action: () => console.log('Send Invitation') },
|
|
{ key: 'D', label: 'Decline', action: () => console.log('Decline Invitation') },
|
|
{ key: 'G', label: 'Go to Date', action: () => console.log('Go to Date') },
|
|
{ key: 'H', label: 'Help', action: () => console.log('Help') },
|
|
{ key: 'J', label: 'Share', action: () => console.log('Share Calendar') },
|
|
{ key: 'K', label: 'Show/Hide Weekends', action: () => console.log('Toggle Weekends') },
|
|
{ key: 'L', label: 'List View', action: () => console.log('List View') },
|
|
{ key: 'Z', label: 'Month View', action: () => console.log('Month View') },
|
|
{ key: 'X', label: 'Week View', action: () => console.log('Week View') },
|
|
]
|
|
];
|
|
|
|
return (
|
|
<div className="flex flex-col h-[calc(100vh-50px)]">
|
|
<div className="flex h-screen bg-background font-mono">
|
|
{/* Sidebar */}
|
|
<CalendarSidebar
|
|
isCollapsed={sidebarCollapsed}
|
|
categories={categories}
|
|
onCategoryToggle={handleCategoryToggle}
|
|
currentDate={currentDate}
|
|
onDateSelect={handleDateSelect}
|
|
/>
|
|
|
|
{/* Main Content */}
|
|
<div className="flex-1 flex flex-col">
|
|
{/* Toolbar */}
|
|
<div className="p-2 border-b border-border flex items-center justify-between bg-card">
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
|
className="bg-secondary hover:bg-secondary/80"
|
|
>
|
|
{sidebarCollapsed ? (
|
|
<ChevronRight className="h-4 w-4" />
|
|
) : (
|
|
<ChevronLeft className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setCurrentDate(new Date())}
|
|
className="bg-secondary hover:bg-secondary/80 font-mono"
|
|
>
|
|
Today
|
|
</Button>
|
|
<div className="flex items-center gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const newDate = new Date(currentDate);
|
|
newDate.setDate(currentDate.getDate() - 1);
|
|
setCurrentDate(newDate);
|
|
}}
|
|
className="bg-secondary hover:bg-secondary/80"
|
|
>
|
|
<ChevronLeft className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
const newDate = new Date(currentDate);
|
|
newDate.setDate(currentDate.getDate() + 1);
|
|
setCurrentDate(newDate);
|
|
}}
|
|
className="bg-secondary hover:bg-secondary/80"
|
|
>
|
|
<ChevronRight className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
<div className="text-sm font-medium ml-2 text-foreground font-mono">
|
|
{formatDate(currentDate)}
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="outline" size="sm" className="bg-secondary hover:bg-secondary/80 border-border rounded-none font-mono">
|
|
<Settings className="h-4 w-4 mr-2" />
|
|
Settings
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end" className="bg-popover text-popover-foreground border-border font-mono">
|
|
<DropdownMenuItem className="hover:bg-primary hover:text-primary-foreground">
|
|
<RefreshCw className="h-4 w-4 mr-2" />
|
|
Refresh
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="hover:bg-primary hover:text-primary-foreground">
|
|
<Filter className="h-4 w-4 mr-2" />
|
|
Filters
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator className="border-t border-border" />
|
|
<DropdownMenuItem className="hover:bg-primary hover:text-primary-foreground">
|
|
<Print className="h-4 w-4 mr-2" />
|
|
Print
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem className="hover:bg-primary hover:text-primary-foreground">
|
|
<Share2 className="h-4 w-4 mr-2" />
|
|
Share
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Calendar Area */}
|
|
<div className="flex-1 flex">
|
|
{/* Three calendar views on the left */}
|
|
<div className="w-[60%] flex flex-col border-r border-border">
|
|
<div className="h-1/3 border-b border-border">
|
|
<CalendarView
|
|
view="day"
|
|
events={sampleEvents}
|
|
currentDate={currentDate}
|
|
onEventAction={handleEventAction}
|
|
/>
|
|
</div>
|
|
<div className="h-1/3 border-b border-border">
|
|
<CalendarView
|
|
view="week"
|
|
events={sampleEvents}
|
|
currentDate={currentDate}
|
|
onEventAction={handleEventAction}
|
|
/>
|
|
</div>
|
|
<div className="h-1/3">
|
|
<CalendarView
|
|
view="month"
|
|
events={sampleEvents}
|
|
currentDate={currentDate}
|
|
onEventAction={handleEventAction}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Event details on the right */}
|
|
<div className="w-[40%]">
|
|
<EventDetails event={selectedEvent} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Footer shortcuts={shortcuts} />
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CalendarPage; |