959 lines
No EOL
36 KiB
TypeScript
959 lines
No EOL
36 KiB
TypeScript
"use client";
|
|
import React, { useState } from 'react';
|
|
import {
|
|
Calendar, ChevronLeft, ChevronRight, Plus, Settings,
|
|
Clock, Users, MapPin, Repeat,
|
|
Edit3, Trash2, Copy, Forward, Reply, MoreHorizontal,
|
|
Filter, RefreshCw, Share2,
|
|
User, AlertCircle,
|
|
CheckCircle2, X, Eye, EyeOff, Flag,
|
|
PrinterIcon} from 'lucide-react';
|
|
import { cn } from "@/lib/utils";
|
|
import Footer from '../footer';
|
|
|
|
// 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 {
|
|
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,
|
|
} from "@/components/ui/tooltip";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
|
|
const CalendarSidebar = ({ isCollapsed, categories, onCategoryToggle, 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 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">
|
|
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].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 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 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}
|
|
|
|
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">
|
|
<PrinterIcon 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; |