mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-04 12:32:58 +00:00
083373a24f
- Add Redis architecture implementation - Update browser extension functionality - Clean up deprecated files and documentation - Enhance backend handlers for auth, messages, search - Add new configuration options and settings - Update Docker and deployment configurations
1122 lines
45 KiB
TypeScript
1122 lines
45 KiB
TypeScript
import { createSignal, createEffect, onMount, For, Show } from 'solid-js'
|
|
import { DateRangePicker } from '@/components/ui/DateRangePicker';
|
|
import { ModalPortal } from '@/components/ui/ModalPortal';
|
|
import {
|
|
IconCalendar,
|
|
IconClock,
|
|
IconPlus,
|
|
IconChevronLeft,
|
|
IconChevronRight,
|
|
IconCheck,
|
|
IconX,
|
|
IconAlertTriangle,
|
|
IconFlag
|
|
} from '@tabler/icons-solidjs'
|
|
import { getMockCalendarEvents } from '@/lib/mockData';
|
|
import { isDemoMode as isDemoModeEnabled } from '@/lib/demo-mode';
|
|
|
|
interface CalendarEvent {
|
|
id: number
|
|
title: string
|
|
description?: string
|
|
start_time: string
|
|
end_time: string
|
|
type: 'task' | 'meeting' | 'deadline' | 'reminder' | 'habit'
|
|
priority: 'low' | 'medium' | 'high' | 'urgent'
|
|
location?: string
|
|
is_completed: boolean
|
|
is_all_day: boolean
|
|
task?: {
|
|
id: number
|
|
title: string
|
|
}
|
|
bookmark?: {
|
|
id: number
|
|
title: string
|
|
}
|
|
note?: {
|
|
id: number
|
|
title: string
|
|
}
|
|
}
|
|
|
|
interface NewEvent {
|
|
title: string
|
|
description: string
|
|
start_time: string
|
|
end_time: string
|
|
type: 'task' | 'meeting' | 'deadline' | 'reminder' | 'habit'
|
|
priority: 'low' | 'medium' | 'high' | 'urgent'
|
|
location: string
|
|
is_all_day: boolean
|
|
}
|
|
|
|
export function Calendar() {
|
|
const [upcomingEvents, setUpcomingEvents] = createSignal<CalendarEvent[]>([])
|
|
const [todayEvents, setTodayEvents] = createSignal<CalendarEvent[]>([])
|
|
const [deadlines, setDeadlines] = createSignal<CalendarEvent[]>([])
|
|
const [currentDate, setCurrentDate] = createSignal(new Date())
|
|
const [view, setView] = createSignal<'month' | 'week' | 'day'>('month')
|
|
const [showEventModal, setShowEventModal] = createSignal(false)
|
|
const [showTaskDetailModal, setShowTaskDetailModal] = createSignal(false)
|
|
const [selectedTask, setSelectedTask] = createSignal<CalendarEvent | null>(null)
|
|
const [currentTime, setCurrentTime] = createSignal(new Date())
|
|
const [mappedEvents, setMappedEvents] = createSignal<CalendarEvent[]>([])
|
|
|
|
const [newEvent, setNewEvent] = createSignal<NewEvent>({
|
|
title: '',
|
|
description: '',
|
|
start_time: '',
|
|
end_time: '',
|
|
type: 'reminder',
|
|
priority: 'medium',
|
|
location: '',
|
|
is_all_day: false
|
|
})
|
|
|
|
// Update current time every second
|
|
createEffect(() => {
|
|
const timer = setInterval(() => {
|
|
setCurrentTime(new Date())
|
|
}, 1000)
|
|
return () => clearInterval(timer)
|
|
})
|
|
|
|
// Fetch calendar data
|
|
const fetchCalendarData = async () => {
|
|
try {
|
|
const token = localStorage.getItem('token');
|
|
|
|
if (isDemoModeEnabled()) {
|
|
// Use mock data in demo mode
|
|
const mockEvents = getMockCalendarEvents();
|
|
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const weekFromNow = new Date();
|
|
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
|
|
|
// Map mock events to calendar events and store for calendar grid
|
|
const mappedEvents: CalendarEvent[] = mockEvents.map(event => ({
|
|
id: parseInt(event.id),
|
|
title: event.title,
|
|
description: event.description,
|
|
start_time: event.start,
|
|
end_time: event.end,
|
|
type: event.type === 'personal' ? 'reminder' : event.type as 'task' | 'meeting' | 'deadline' | 'reminder' | 'habit',
|
|
priority: 'medium' as 'low' | 'medium' | 'high' | 'urgent',
|
|
location: event.location,
|
|
is_completed: false,
|
|
is_all_day: event.allDay
|
|
}));
|
|
|
|
setMappedEvents(mappedEvents);
|
|
|
|
const todayEvents = mappedEvents.filter(event => {
|
|
const eventDate = new Date(event.start_time);
|
|
return eventDate.toDateString() === today.toDateString();
|
|
});
|
|
|
|
const upcomingEvents = mappedEvents.filter(event => {
|
|
const eventDate = new Date(event.start_time);
|
|
return eventDate >= today && eventDate <= weekFromNow;
|
|
});
|
|
|
|
const deadlines = mappedEvents.filter(event =>
|
|
event.type === 'deadline' && new Date(event.start_time) >= today
|
|
);
|
|
|
|
setTodayEvents(todayEvents);
|
|
setUpcomingEvents(upcomingEvents);
|
|
setDeadlines(deadlines);
|
|
return;
|
|
}
|
|
|
|
if (!token) {
|
|
setMappedEvents([]);
|
|
setTodayEvents([]);
|
|
setUpcomingEvents([]);
|
|
setDeadlines([]);
|
|
return;
|
|
}
|
|
|
|
const headers = {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
|
|
// Fetch all calendar data in parallel
|
|
const [upcomingRes, todayRes, deadlinesRes] = await Promise.all([
|
|
fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8080'}/api/v1/calendar/upcoming`, { headers }),
|
|
fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8080'}/api/v1/calendar/today`, { headers }),
|
|
fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8080'}/api/v1/calendar/deadlines`, { headers })
|
|
])
|
|
|
|
if (upcomingRes.ok) {
|
|
const upcomingData = await upcomingRes.json()
|
|
setUpcomingEvents(upcomingData.events || [])
|
|
}
|
|
|
|
if (todayRes.ok) {
|
|
const todayData = await todayRes.json()
|
|
setTodayEvents(todayData.events || [])
|
|
}
|
|
|
|
if (deadlinesRes.ok) {
|
|
const deadlinesData = await deadlinesRes.json()
|
|
setDeadlines(deadlinesData.deadlines || [])
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to fetch calendar data:', error)
|
|
if (isDemoModeEnabled()) {
|
|
const mockEvents = getMockCalendarEvents();
|
|
const today = new Date();
|
|
today.setHours(0, 0, 0, 0);
|
|
const weekFromNow = new Date();
|
|
weekFromNow.setDate(weekFromNow.getDate() + 7);
|
|
|
|
const mappedEvents: CalendarEvent[] = mockEvents.map(event => ({
|
|
id: parseInt(event.id),
|
|
title: event.title,
|
|
description: event.description,
|
|
start_time: event.start,
|
|
end_time: event.end,
|
|
type: event.type === 'personal' ? 'reminder' : event.type as 'task' | 'meeting' | 'deadline' | 'reminder' | 'habit',
|
|
priority: 'medium' as 'low' | 'medium' | 'high' | 'urgent',
|
|
location: event.location,
|
|
is_completed: false,
|
|
is_all_day: event.allDay
|
|
}));
|
|
|
|
setMappedEvents(mappedEvents);
|
|
|
|
const todayEvents = mappedEvents.filter(event => {
|
|
const eventDate = new Date(event.start_time);
|
|
return eventDate.toDateString() === today.toDateString();
|
|
});
|
|
|
|
const upcomingEvents = mappedEvents.filter(event => {
|
|
const eventDate = new Date(event.start_time);
|
|
return eventDate >= today && eventDate <= weekFromNow;
|
|
});
|
|
|
|
const deadlines = mappedEvents.filter(event =>
|
|
event.type === 'deadline' && new Date(event.start_time) >= today
|
|
);
|
|
|
|
setTodayEvents(todayEvents);
|
|
setUpcomingEvents(upcomingEvents);
|
|
setDeadlines(deadlines);
|
|
return;
|
|
}
|
|
|
|
setMappedEvents([]);
|
|
setTodayEvents([]);
|
|
setUpcomingEvents([]);
|
|
setDeadlines([]);
|
|
}
|
|
}
|
|
|
|
onMount(() => {
|
|
fetchCalendarData()
|
|
})
|
|
|
|
const createEvent = async () => {
|
|
try {
|
|
if (isDemoModeEnabled()) {
|
|
// Simulate event creation in demo mode
|
|
console.log('Creating event (demo mode):', newEvent());
|
|
setShowEventModal(false);
|
|
setNewEvent({
|
|
title: '',
|
|
description: '',
|
|
start_time: '',
|
|
end_time: '',
|
|
type: 'reminder',
|
|
priority: 'medium',
|
|
location: '',
|
|
is_all_day: false
|
|
});
|
|
fetchCalendarData();
|
|
return;
|
|
}
|
|
|
|
const token = localStorage.getItem('token')
|
|
if (!token) return
|
|
|
|
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8080'}/api/v1/calendar`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(newEvent())
|
|
})
|
|
|
|
if (response.ok) {
|
|
setShowEventModal(false)
|
|
setNewEvent({
|
|
title: '',
|
|
description: '',
|
|
start_time: '',
|
|
end_time: '',
|
|
type: 'reminder',
|
|
priority: 'medium',
|
|
location: '',
|
|
is_all_day: false
|
|
})
|
|
fetchCalendarData()
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to create event:', error)
|
|
// Fallback to demo mode behavior
|
|
setShowEventModal(false);
|
|
setNewEvent({
|
|
title: '',
|
|
description: '',
|
|
start_time: '',
|
|
end_time: '',
|
|
type: 'reminder',
|
|
priority: 'medium',
|
|
location: '',
|
|
is_all_day: false
|
|
});
|
|
fetchCalendarData();
|
|
}
|
|
}
|
|
|
|
const toggleEventCompletion = async (eventId: number) => {
|
|
try {
|
|
if (isDemoModeEnabled()) {
|
|
// Simulate event completion toggle in demo mode
|
|
console.log('Toggling event completion (demo mode):', eventId);
|
|
fetchCalendarData();
|
|
return;
|
|
}
|
|
|
|
const token = localStorage.getItem('token')
|
|
if (!token) return
|
|
|
|
const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:8080'}/api/v1/calendar/${eventId}/toggle-complete`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
|
|
if (response.ok) {
|
|
fetchCalendarData()
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to toggle event completion:', error)
|
|
// Fallback to demo mode behavior
|
|
fetchCalendarData();
|
|
}
|
|
}
|
|
|
|
const getPriorityColor = (priority: string) => {
|
|
switch (priority) {
|
|
case 'urgent': return 'text-primary'
|
|
case 'high': return 'text-primary'
|
|
case 'medium': return 'text-primary'
|
|
case 'low': return 'text-primary'
|
|
default: return 'text-primary'
|
|
}
|
|
}
|
|
|
|
const getTypeColor = (type: string) => {
|
|
switch (type) {
|
|
case 'task': return 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary'
|
|
case 'meeting': return 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary'
|
|
case 'deadline': return 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary'
|
|
case 'reminder': return 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary'
|
|
case 'habit': return 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary'
|
|
default: return 'bg-primary/10 text-primary dark:bg-primary/20 dark:text-primary'
|
|
}
|
|
}
|
|
|
|
const formatDateTime = (dateString: string) => {
|
|
const date = new Date(dateString)
|
|
return date.toLocaleString()
|
|
}
|
|
|
|
const formatTime = (dateString: string) => {
|
|
const date = new Date(dateString)
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
}
|
|
|
|
const openEventModal = (date: Date) => {
|
|
setNewEvent({
|
|
title: '',
|
|
description: '',
|
|
start_time: date.toISOString().slice(0, 16),
|
|
end_time: new Date(date.getTime() + 60 * 60 * 1000).toISOString().slice(0, 16),
|
|
type: 'reminder',
|
|
priority: 'medium',
|
|
location: '',
|
|
is_all_day: false
|
|
})
|
|
setShowEventModal(true)
|
|
}
|
|
|
|
const navigateMonth = (direction: number) => {
|
|
const newDate = new Date(currentDate())
|
|
newDate.setMonth(newDate.getMonth() + direction)
|
|
setCurrentDate(newDate)
|
|
}
|
|
|
|
return (
|
|
<div class="space-y-6">
|
|
{/* Header with Current Time */}
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-foreground mb-2">Calendar</h1>
|
|
<p class="text-muted-foreground">
|
|
{currentTime().toLocaleDateString('en-US', {
|
|
weekday: 'long',
|
|
year: 'numeric',
|
|
month: 'long',
|
|
day: 'numeric'
|
|
})}
|
|
<span class="ml-2 font-mono text-lg">
|
|
{currentTime().toLocaleTimeString()}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => openEventModal(new Date())}
|
|
class="inline-flex items-center gap-2 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 transition-colors"
|
|
>
|
|
<IconPlus class="size-4" />
|
|
New Event
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4 lg:gap-6">
|
|
{/* Calendar View */}
|
|
<div class="lg:col-span-2">
|
|
<div class="bg-card rounded-lg border border-border p-4 lg:p-6">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<div class="flex items-center gap-2">
|
|
<button
|
|
onClick={() => navigateMonth(-1)}
|
|
class="p-2 hover:bg-accent rounded-lg transition-colors"
|
|
>
|
|
<IconChevronLeft class="size-4" />
|
|
</button>
|
|
<h2 class="text-xl font-semibold">
|
|
{currentDate().toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
|
|
</h2>
|
|
<button
|
|
onClick={() => navigateMonth(1)}
|
|
class="p-2 hover:bg-accent rounded-lg transition-colors"
|
|
>
|
|
<IconChevronRight class="size-4" />
|
|
</button>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button
|
|
onClick={() => setView('month')}
|
|
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
|
|
view() === 'month'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'hover:bg-accent'
|
|
}`}
|
|
>
|
|
Month
|
|
</button>
|
|
<button
|
|
onClick={() => setView('week')}
|
|
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
|
|
view() === 'week'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'hover:bg-accent'
|
|
}`}
|
|
>
|
|
Week
|
|
</button>
|
|
<button
|
|
onClick={() => setView('day')}
|
|
class={`px-3 py-1 rounded-lg text-sm transition-colors ${
|
|
view() === 'day'
|
|
? 'bg-primary text-primary-foreground'
|
|
: 'hover:bg-accent'
|
|
}`}
|
|
>
|
|
Day
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Enhanced Calendar Grid with Events */}
|
|
<div class="grid grid-cols-7 gap-1 text-sm">
|
|
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
|
|
<div class="text-center text-sm font-medium text-muted-foreground p-2">
|
|
{day}
|
|
</div>
|
|
))}
|
|
{/* Calendar days with events */}
|
|
{Array.from({ length: 35 }, (_, i) => {
|
|
const date = new Date(currentDate().getFullYear(), currentDate().getMonth(), i - 2)
|
|
const isToday = date.toDateString() === new Date().toDateString()
|
|
const isCurrentMonth = date.getMonth() === currentDate().getMonth()
|
|
|
|
// Get events for this day
|
|
const dayEvents = mappedEvents().filter((event: CalendarEvent) => {
|
|
const eventDate = new Date(event.start_time);
|
|
return eventDate.toDateString() === date.toDateString();
|
|
}) || [];
|
|
|
|
return (
|
|
<div
|
|
onClick={() => openEventModal(date)}
|
|
class={`border border-border rounded-lg p-1 cursor-pointer hover:bg-accent transition-colors relative overflow-hidden h-24 flex flex-col ${
|
|
isToday ? 'bg-primary/10 border-primary' : ''
|
|
} ${!isCurrentMonth ? 'opacity-40' : ''}`}
|
|
>
|
|
<div class="text-sm font-medium shrink-0">{date.getDate()}</div>
|
|
{/* Event indicators */}
|
|
<div class="flex-1 overflow-hidden flex flex-col justify-start space-y-0.5 mt-1">
|
|
{dayEvents.slice(0, 3).map((event: CalendarEvent) => (
|
|
<div
|
|
class={`text-xs px-1 py-0.5 rounded truncate w-full cursor-pointer hover:opacity-80 transition-opacity leading-none ${
|
|
event.type === 'deadline'
|
|
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 border border-red-200 dark:border-red-800'
|
|
: event.type === 'meeting'
|
|
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
|
: event.type === 'task'
|
|
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
|
|
: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
|
}`}
|
|
style={`font-size: 9px; line-height: 1.2;`}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
setSelectedTask(event);
|
|
setShowTaskDetailModal(true);
|
|
}}
|
|
>
|
|
{event.title.length > 10 ? event.title.substring(0, 10) + '...' : event.title}
|
|
</div>
|
|
))}
|
|
{dayEvents.length > 3 && (
|
|
<div
|
|
class="text-xs text-muted-foreground font-medium cursor-pointer hover:text-primary transition-colors underline leading-none mt-0.5"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
// Show all events for this day
|
|
setSelectedTask({
|
|
id: date.getTime(),
|
|
title: `${date.toLocaleDateString()} - All Events`,
|
|
description: `Total events: ${dayEvents.length}`,
|
|
start_time: date.toISOString(),
|
|
end_time: date.toISOString(),
|
|
type: 'reminder' as const,
|
|
priority: 'medium' as const,
|
|
is_completed: false,
|
|
is_all_day: true
|
|
});
|
|
setShowTaskDetailModal(true);
|
|
}}
|
|
>
|
|
+{dayEvents.length - 3} more
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar */}
|
|
<div class="space-y-4 lg:space-y-6">
|
|
{/* Today's Events */}
|
|
<div class="bg-card rounded-lg border border-border p-4 lg:p-6">
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<IconClock class="size-5" />
|
|
Today's Events
|
|
</h3>
|
|
<div class="space-y-3 max-h-80 overflow-y-auto pr-2">
|
|
<Show
|
|
when={todayEvents().length > 0}
|
|
fallback={
|
|
<p class="text-muted-foreground text-sm">No events today</p>
|
|
}
|
|
>
|
|
<For each={todayEvents().slice(0, 10)}>
|
|
{(event) => (
|
|
<div class="p-3 bg-muted/50 rounded-lg cursor-pointer hover:bg-muted transition-colors"
|
|
onClick={() => {
|
|
setSelectedTask(event);
|
|
setShowTaskDetailModal(true);
|
|
}}>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class={`text-xs px-2 py-1 rounded-full ${getTypeColor(event.type)}`}>
|
|
{event.type}
|
|
</span>
|
|
<IconFlag class={`size-3 ${getPriorityColor(event.priority)}`} />
|
|
</div>
|
|
<h4 class="font-medium text-sm">{event.title}</h4>
|
|
<p class="text-xs text-muted-foreground mt-1">
|
|
{formatTime(event.start_time)} - {formatTime(event.end_time)}
|
|
</p>
|
|
{event.location && (
|
|
<p class="text-xs text-muted-foreground">{event.location}</p>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
toggleEventCompletion(event.id);
|
|
}}
|
|
class={`p-1 rounded hover:bg-accent transition-colors ${
|
|
event.is_completed ? 'text-green-500' : 'text-muted-foreground'
|
|
}`}
|
|
>
|
|
<IconCheck class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upcoming Events */}
|
|
<div class="bg-card rounded-lg border border-border p-4 lg:p-6">
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<IconCalendar class="size-5" />
|
|
Upcoming (7 Days)
|
|
</h3>
|
|
<div class="space-y-3 max-h-80 overflow-y-auto pr-2">
|
|
<Show
|
|
when={upcomingEvents().length > 0}
|
|
fallback={
|
|
<p class="text-muted-foreground text-sm">No upcoming events</p>
|
|
}
|
|
>
|
|
<For each={upcomingEvents().slice(0, 10)}>
|
|
{(event) => (
|
|
<div class="p-3 bg-muted/50 rounded-lg cursor-pointer hover:bg-muted transition-colors"
|
|
onClick={() => {
|
|
setSelectedTask(event);
|
|
setShowTaskDetailModal(true);
|
|
}}>
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class={`text-xs px-2 py-1 rounded-full ${getTypeColor(event.type)}`}>
|
|
{event.type}
|
|
</span>
|
|
<IconFlag class={`size-3 ${getPriorityColor(event.priority)}`} />
|
|
</div>
|
|
<h4 class="font-medium text-sm">{event.title}</h4>
|
|
<p class="text-xs text-muted-foreground">
|
|
{formatDateTime(event.start_time)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Deadlines */}
|
|
<div class="bg-card rounded-lg border border-border p-4 lg:p-6">
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<IconAlertTriangle class="size-5" />
|
|
Deadlines
|
|
</h3>
|
|
<div class="space-y-3 max-h-80 overflow-y-auto pr-2">
|
|
<Show
|
|
when={deadlines().length > 0}
|
|
fallback={
|
|
<p class="text-muted-foreground text-sm">No upcoming deadlines</p>
|
|
}
|
|
>
|
|
<For each={deadlines().slice(0, 10)}>
|
|
{(deadline) => (
|
|
<div class="p-3 bg-muted/50 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class={`text-xs px-2 py-1 rounded-full ${getTypeColor(deadline.type)}`}>
|
|
{deadline.type}
|
|
</span>
|
|
<IconFlag class={`size-3 ${getPriorityColor(deadline.priority)}`} />
|
|
</div>
|
|
<h4 class="font-medium text-sm">{deadline.title}</h4>
|
|
<p class="text-xs text-muted-foreground">
|
|
{formatDateTime(deadline.start_time)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Responsive layout for mobile */}
|
|
<div class="xl:hidden space-y-6">
|
|
{/* Today's Events - Mobile */}
|
|
<div class="bg-card rounded-lg border border-border p-4">
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<IconClock class="size-5" />
|
|
Today's Events
|
|
</h3>
|
|
<div class="space-y-3 max-h-60 overflow-y-auto">
|
|
<Show
|
|
when={todayEvents().length > 0}
|
|
fallback={
|
|
<p class="text-muted-foreground text-sm">No events today</p>
|
|
}
|
|
>
|
|
<For each={todayEvents().slice(0, 5)}>
|
|
{(event) => (
|
|
<div class="p-3 bg-muted/50 rounded-lg">
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class={`text-xs px-2 py-1 rounded-full ${getTypeColor(event.type)}`}>
|
|
{event.type}
|
|
</span>
|
|
<IconFlag class={`size-3 ${getPriorityColor(event.priority)}`} />
|
|
</div>
|
|
<h4 class="font-medium text-sm">{event.title}</h4>
|
|
<p class="text-xs text-muted-foreground mt-1">
|
|
{formatTime(event.start_time)} - {formatTime(event.end_time)}
|
|
</p>
|
|
{event.location && (
|
|
<p class="text-xs text-muted-foreground">{event.location}</p>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={() => toggleEventCompletion(event.id)}
|
|
class={`p-1 rounded hover:bg-accent transition-colors ${
|
|
event.is_completed ? 'text-green-500' : 'text-muted-foreground'
|
|
}`}
|
|
>
|
|
<IconCheck class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upcoming Events - Mobile */}
|
|
<div class="bg-card rounded-lg border border-border p-4">
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<IconCalendar class="size-5" />
|
|
Upcoming (7 Days)
|
|
</h3>
|
|
<div class="space-y-3 max-h-60 overflow-y-auto">
|
|
<Show
|
|
when={upcomingEvents().length > 0}
|
|
fallback={
|
|
<p class="text-muted-foreground text-sm">No upcoming events</p>
|
|
}
|
|
>
|
|
<For each={upcomingEvents().slice(0, 5)}>
|
|
{(event) => (
|
|
<div class="p-3 bg-muted/50 rounded-lg">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class={`text-xs px-2 py-1 rounded-full ${getTypeColor(event.type)}`}>
|
|
{event.type}
|
|
</span>
|
|
<IconFlag class={`size-3 ${getPriorityColor(event.priority)}`} />
|
|
</div>
|
|
<h4 class="font-medium text-sm">{event.title}</h4>
|
|
<p class="text-xs text-muted-foreground">
|
|
{formatDateTime(event.start_time)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Deadlines - Mobile */}
|
|
<div class="bg-card rounded-lg border border-border p-4">
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
<IconAlertTriangle class="size-5" />
|
|
Deadlines
|
|
</h3>
|
|
<div class="space-y-3 max-h-60 overflow-y-auto">
|
|
<Show
|
|
when={deadlines().length > 0}
|
|
fallback={
|
|
<p class="text-muted-foreground text-sm">No upcoming deadlines</p>
|
|
}
|
|
>
|
|
<For each={deadlines().slice(0, 5)}>
|
|
{(deadline) => (
|
|
<div class="p-3 bg-muted/50 border border-red-200 dark:border-red-800 rounded-lg">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class={`text-xs px-2 py-1 rounded-full ${getTypeColor(deadline.type)}`}>
|
|
{deadline.type}
|
|
</span>
|
|
<IconFlag class={`size-3 ${getPriorityColor(deadline.priority)}`} />
|
|
</div>
|
|
<h4 class="font-medium text-sm">{deadline.title}</h4>
|
|
<p class="text-xs text-muted-foreground">
|
|
{formatDateTime(deadline.start_time)}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Event Creation Modal */}
|
|
<Show when={showEventModal()}>
|
|
<ModalPortal>
|
|
<div
|
|
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
onClick={(e) => {
|
|
// Close modal only when clicking the backdrop, not the modal content
|
|
if (e.target === e.currentTarget) {
|
|
setShowEventModal(false);
|
|
}
|
|
}}
|
|
>
|
|
<div class="bg-card rounded-lg border border-border p-6 w-full max-w-md max-h-[90vh] overflow-y-auto">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">New Event</h3>
|
|
<button
|
|
onClick={() => setShowEventModal(false)}
|
|
class="p-1 hover:bg-accent rounded-lg transition-colors"
|
|
>
|
|
<IconX class="size-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Title</label>
|
|
<input
|
|
type="text"
|
|
value={newEvent().title}
|
|
onInput={(e) => setNewEvent({ ...newEvent(), title: e.currentTarget.value })}
|
|
class="w-full px-3 py-2 border border-border rounded-lg bg-background"
|
|
placeholder="Event title"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Description</label>
|
|
<textarea
|
|
value={newEvent().description}
|
|
onChange={(e) => setNewEvent({ ...newEvent(), description: e.target.value })}
|
|
placeholder="Enter event description"
|
|
rows={3}
|
|
class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring bg-background resize-none"
|
|
/>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Type</label>
|
|
<select
|
|
value={newEvent().type}
|
|
onChange={(e) => setNewEvent({ ...newEvent(), type: e.target.value as any })}
|
|
class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring bg-background"
|
|
>
|
|
<option value="task">Task</option>
|
|
<option value="meeting">Meeting</option>
|
|
<option value="deadline">Deadline</option>
|
|
<option value="reminder">Reminder</option>
|
|
<option value="habit">Habit</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Priority</label>
|
|
<select
|
|
value={newEvent().priority}
|
|
onChange={(e) => setNewEvent({ ...newEvent(), priority: e.target.value as any })}
|
|
class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring bg-background"
|
|
>
|
|
<option value="low">Low</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="high">High</option>
|
|
<option value="urgent">Urgent</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">Location</label>
|
|
<input
|
|
type="text"
|
|
value={newEvent().location}
|
|
onChange={(e) => setNewEvent({ ...newEvent(), location: e.target.value })}
|
|
placeholder="Enter location (optional)"
|
|
class="w-full px-3 py-2 border border-input rounded-md focus:outline-none focus:ring-2 focus:ring-ring bg-background"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="all-day"
|
|
checked={newEvent().is_all_day}
|
|
onChange={(e) => {
|
|
const isAllDay = e.target.checked;
|
|
setNewEvent({
|
|
...newEvent(),
|
|
is_all_day: isAllDay,
|
|
// Reset times when toggling all-day
|
|
start_time: isAllDay ? new Date(new Date().setHours(0, 0, 0, 0)).toISOString() : new Date().toISOString(),
|
|
end_time: isAllDay ? new Date(new Date().setHours(23, 59, 59, 999)).toISOString() : ''
|
|
});
|
|
}}
|
|
class="rounded border-border accent-primary"
|
|
/>
|
|
<label for="all-day" class="text-sm font-medium cursor-pointer">All day event</label>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 gap-4">
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">
|
|
{newEvent().is_all_day ? 'Event Date' : 'Start Time'}
|
|
</label>
|
|
<DateRangePicker
|
|
value={newEvent().start_time ? { start: new Date(newEvent().start_time), end: new Date(newEvent().end_time || newEvent().start_time) } : undefined}
|
|
onChange={(range) => {
|
|
if (range && range.start) {
|
|
if (newEvent().is_all_day) {
|
|
// For all-day events, set time to beginning of day
|
|
const startOfDay = new Date(range.start);
|
|
startOfDay.setHours(0, 0, 0, 0);
|
|
setNewEvent({ ...newEvent(), start_time: startOfDay.toISOString() });
|
|
} else {
|
|
setNewEvent({ ...newEvent(), start_time: range.start.toISOString() });
|
|
}
|
|
}
|
|
}}
|
|
placeholder={newEvent().is_all_day ? "Select event date" : "Select start time"}
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
|
|
{!newEvent().is_all_day && (
|
|
<div>
|
|
<label class="block text-sm font-medium mb-1">End Time</label>
|
|
<DateRangePicker
|
|
value={newEvent().start_time ? { start: new Date(newEvent().start_time), end: new Date(newEvent().end_time || newEvent().start_time) } : undefined}
|
|
onChange={(range) => {
|
|
if (range && range.start) {
|
|
setNewEvent({ ...newEvent(), end_time: range.end ? range.end.toISOString() : range.start.toISOString() });
|
|
}
|
|
}}
|
|
placeholder="Select end time"
|
|
class="w-full"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div class="flex gap-2 mt-4">
|
|
<button
|
|
onClick={() => setShowEventModal(false)}
|
|
class="flex-1 px-4 py-2 border border-border rounded-lg hover:bg-accent transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={createEvent}
|
|
disabled={!newEvent().title || !newEvent().start_time}
|
|
class="flex-1 px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
Create Event
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</ModalPortal>
|
|
</Show>
|
|
|
|
{/* Task Detail Modal */}
|
|
<Show when={showTaskDetailModal() && selectedTask()}>
|
|
<ModalPortal>
|
|
<div
|
|
class="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
|
|
onClick={(e) => {
|
|
if (e.target === e.currentTarget) {
|
|
setShowTaskDetailModal(false);
|
|
setSelectedTask(null);
|
|
}
|
|
}}
|
|
>
|
|
<div class="bg-card rounded-lg border border-border p-6 w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h3 class="text-lg font-semibold">Task Details</h3>
|
|
<button
|
|
onClick={() => {
|
|
setShowTaskDetailModal(false);
|
|
setSelectedTask(null);
|
|
}}
|
|
class="p-1 hover:bg-accent rounded-lg transition-colors"
|
|
>
|
|
<IconX class="size-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<Show when={selectedTask()}>
|
|
{(task) => (
|
|
<div class="space-y-4">
|
|
{/* Check if this is a "All Events" special task */}
|
|
{task().title.includes('All Events') ? (
|
|
<>
|
|
<div>
|
|
<h4 class="text-xl font-medium mb-2">{task().title}</h4>
|
|
<p class="text-sm text-muted-foreground mb-4">{task().description}</p>
|
|
</div>
|
|
|
|
<div class="space-y-3 max-h-60 overflow-y-auto">
|
|
{mappedEvents().filter((event: CalendarEvent) => {
|
|
const eventDate = new Date(event.start_time);
|
|
const taskDate = new Date(task().start_time);
|
|
return eventDate.toDateString() === taskDate.toDateString();
|
|
}).map((event: CalendarEvent) => (
|
|
<div class="p-3 bg-muted/50 rounded-lg cursor-pointer hover:bg-muted transition-colors"
|
|
onClick={() => {
|
|
setSelectedTask(event);
|
|
}}>
|
|
<div class="flex items-start justify-between">
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<span class={`text-xs px-2 py-1 rounded-full ${getTypeColor(event.type)}`}>
|
|
{event.type}
|
|
</span>
|
|
<IconFlag class={`size-3 ${getPriorityColor(event.priority)}`} />
|
|
</div>
|
|
<h5 class="font-medium text-sm">{event.title}</h5>
|
|
<p class="text-xs text-muted-foreground mt-1">
|
|
{formatTime(event.start_time)} - {formatTime(event.end_time)}
|
|
</p>
|
|
{event.location && (
|
|
<p class="text-xs text-muted-foreground">{event.location}</p>
|
|
)}
|
|
</div>
|
|
<button
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
toggleEventCompletion(event.id);
|
|
}}
|
|
class={`p-1 rounded hover:bg-accent transition-colors ${
|
|
event.is_completed ? 'text-green-500' : 'text-muted-foreground'
|
|
}`}
|
|
>
|
|
<IconCheck class="size-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
) : (
|
|
<>
|
|
<div>
|
|
<h4 class="text-xl font-medium mb-2">{task().title}</h4>
|
|
<div class="flex items-center gap-2 mb-3">
|
|
<span class={`text-xs px-2 py-1 rounded-full ${getTypeColor(task().type)}`}>
|
|
{task().type}
|
|
</span>
|
|
<IconFlag class={`size-3 ${getPriorityColor(task().priority)}`} />
|
|
<span class="text-xs text-muted-foreground">
|
|
{task().priority.charAt(0).toUpperCase() + task().priority.slice(1)} Priority
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{task().description && (
|
|
<div>
|
|
<h5 class="text-sm font-medium mb-1">Description</h5>
|
|
<p class="text-sm text-muted-foreground">{task().description}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h5 class="text-sm font-medium mb-1">Start Time</h5>
|
|
<p class="text-sm text-muted-foreground">
|
|
{formatDateTime(task().start_time)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h5 class="text-sm font-medium mb-1">End Time</h5>
|
|
<p class="text-sm text-muted-foreground">
|
|
{formatDateTime(task().end_time)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{task().location && (
|
|
<div>
|
|
<h5 class="text-sm font-medium mb-1">Location</h5>
|
|
<p class="text-sm text-muted-foreground">{task().location}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div class="flex items-center gap-2">
|
|
<span class="text-sm font-medium">Status:</span>
|
|
<span class={`text-sm ${task().is_completed ? 'text-green-500' : 'text-muted-foreground'}`}>
|
|
{task().is_completed ? 'Completed' : 'Pending'}
|
|
</span>
|
|
</div>
|
|
|
|
{task().is_all_day && (
|
|
<div class="flex items-center gap-2">
|
|
<IconCalendar class="size-4 text-muted-foreground" />
|
|
<span class="text-sm text-muted-foreground">All-day event</span>
|
|
</div>
|
|
)}
|
|
|
|
<div class="flex gap-3 pt-4">
|
|
<button
|
|
onClick={() => {
|
|
toggleEventCompletion(task().id);
|
|
setShowTaskDetailModal(false);
|
|
setSelectedTask(null);
|
|
}}
|
|
class={`flex-1 px-4 py-2 rounded-lg transition-colors ${
|
|
task().is_completed
|
|
? 'bg-orange-500 text-white hover:bg-orange-600'
|
|
: 'bg-green-500 text-white hover:bg-green-600'
|
|
}`}
|
|
>
|
|
{task().is_completed ? 'Mark as Incomplete' : 'Mark as Complete'}
|
|
</button>
|
|
<button
|
|
onClick={() => {
|
|
setShowTaskDetailModal(false);
|
|
setSelectedTask(null);
|
|
}}
|
|
class="flex-1 px-4 py-2 border border-border rounded-lg hover:bg-accent transition-colors"
|
|
>
|
|
Close
|
|
</button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
</ModalPortal>
|
|
</Show>
|
|
</div>
|
|
)
|
|
}
|