diff --git a/frontend/index.html b/frontend/index.html index 68b043b..1abf41b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,9 @@ UMBRA + + +
diff --git a/frontend/src/components/dashboard/CalendarWidget.tsx b/frontend/src/components/dashboard/CalendarWidget.tsx index b0c3975..1b4f61b 100644 --- a/frontend/src/components/dashboard/CalendarWidget.tsx +++ b/frontend/src/components/dashboard/CalendarWidget.tsx @@ -20,37 +20,38 @@ export default function CalendarWidget({ events }: CalendarWidgetProps) { - +
+ +
Today's Events
{events.length === 0 ? ( -

- No events scheduled for today +

+ No events today

) : ( -
+
{events.map((event) => (
-

{event.title}

- {!event.all_day && ( -
+

{event.title}

+ {!event.all_day ? ( +
{format(new Date(event.start_datetime), 'h:mm a')} - {event.end_datetime && ` - ${format(new Date(event.end_datetime), 'h:mm a')}`} + {event.end_datetime && ` โ€“ ${format(new Date(event.end_datetime), 'h:mm a')}`}
- )} - {event.all_day && ( -

All day

+ ) : ( +

All day

)}
diff --git a/frontend/src/components/dashboard/DashboardPage.tsx b/frontend/src/components/dashboard/DashboardPage.tsx index 5715bff..16ac44d 100644 --- a/frontend/src/components/dashboard/DashboardPage.tsx +++ b/frontend/src/components/dashboard/DashboardPage.tsx @@ -8,9 +8,19 @@ import StatsWidget from './StatsWidget'; import TodoWidget from './TodoWidget'; import CalendarWidget from './CalendarWidget'; import UpcomingWidget from './UpcomingWidget'; +import WeekTimeline from './WeekTimeline'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { DashboardSkeleton } from '@/components/ui/skeleton'; +function getGreeting(): string { + const hour = new Date().getHours(); + if (hour < 5) return 'Good night.'; + if (hour < 12) return 'Good morning.'; + if (hour < 17) return 'Good afternoon.'; + if (hour < 21) return 'Good evening.'; + return 'Good night.'; +} + export default function DashboardPage() { const { settings } = useSettings(); @@ -36,11 +46,13 @@ export default function DashboardPage() { if (isLoading) { return (
-
-

Dashboard

-

Welcome back. Here's your overview.

+
+
+
+
+
-
+
@@ -57,46 +69,82 @@ export default function DashboardPage() { return (
-
-

Dashboard

-

Welcome back. Here's your overview.

+ {/* Header โ€” greeting + date */} +
+

+ {getGreeting()} +

+

+ {format(new Date(), 'EEEE, MMMM d, yyyy')} +

-
-
- - - {upcomingData && upcomingData.items.length > 0 && ( - +
+
+ {/* Week Timeline */} + {upcomingData && ( +
+ +
)} -
- - + {/* Stats Row */} +
+
+ {/* Main Content โ€” 2 columns */} +
+ {/* Left: Upcoming feed (wider) */} +
+ {upcomingData && upcomingData.items.length > 0 ? ( + + ) : ( + + + Upcoming + + +

+ Nothing upcoming. Enjoy the quiet. +

+
+
+ )} +
+ + {/* Right: Today's events + todos stacked */} +
+ + +
+
+ + {/* Active Reminders */} {data.active_reminders.length > 0 && ( - + - +
+ +
Active Reminders
-
+
{data.active_reminders.map((reminder) => (
- +
-

{reminder.title}

+

{reminder.title}

{format(new Date(reminder.remind_at), 'MMM d, yyyy h:mm a')}

diff --git a/frontend/src/components/dashboard/StatsWidget.tsx b/frontend/src/components/dashboard/StatsWidget.tsx index 7204bdd..f9aff94 100644 --- a/frontend/src/components/dashboard/StatsWidget.tsx +++ b/frontend/src/components/dashboard/StatsWidget.tsx @@ -1,4 +1,4 @@ -import { FolderKanban, Users, MapPin } from 'lucide-react'; +import { FolderKanban, Users, MapPin, TrendingUp } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; interface StatsWidgetProps { @@ -13,42 +13,52 @@ interface StatsWidgetProps { export default function StatsWidget({ projectStats, totalPeople, totalLocations }: StatsWidgetProps) { const statCards = [ { - label: 'Total Projects', + label: 'PROJECTS', value: projectStats.total, icon: FolderKanban, - color: 'text-blue-500', + color: 'text-blue-400', + glowBg: 'bg-blue-500/10', }, { - label: 'In Progress', + label: 'IN PROGRESS', value: projectStats.by_status['in_progress'] || 0, - icon: FolderKanban, - color: 'text-purple-500', + icon: TrendingUp, + color: 'text-purple-400', + glowBg: 'bg-purple-500/10', }, { - label: 'People', + label: 'PEOPLE', value: totalPeople, icon: Users, - color: 'text-green-500', + color: 'text-emerald-400', + glowBg: 'bg-emerald-500/10', }, { - label: 'Locations', + label: 'LOCATIONS', value: totalLocations, icon: MapPin, - color: 'text-orange-500', + color: 'text-orange-400', + glowBg: 'bg-orange-500/10', }, ]; return ( -
+
{statCards.map((stat) => ( - - -
-
-

{stat.label}

-

{stat.value}

+ + +
+
+

+ {stat.label} +

+

+ {stat.value} +

+
+
+
-
diff --git a/frontend/src/components/dashboard/TodoWidget.tsx b/frontend/src/components/dashboard/TodoWidget.tsx index b281cf6..d9123ae 100644 --- a/frontend/src/components/dashboard/TodoWidget.tsx +++ b/frontend/src/components/dashboard/TodoWidget.tsx @@ -1,4 +1,4 @@ -import { format, isPast } from 'date-fns'; +import { format, isPast, endOfDay } from 'date-fns'; import { Calendar, CheckCircle2 } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; @@ -17,9 +17,9 @@ interface TodoWidgetProps { } const priorityColors: Record = { - low: 'bg-green-500/10 text-green-500 border-green-500/20', - medium: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20', - high: 'bg-red-500/10 text-red-500 border-red-500/20', + low: 'bg-green-500/10 text-green-400 border-green-500/20', + medium: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20', + high: 'bg-red-500/10 text-red-400 border-red-500/20', }; export default function TodoWidget({ todos }: TodoWidgetProps) { @@ -27,41 +27,45 @@ export default function TodoWidget({ todos }: TodoWidgetProps) { - +
+ +
Upcoming Todos
{todos.length === 0 ? ( -

- No upcoming todos. You're all caught up! +

+ All caught up.

) : ( -
+
{todos.slice(0, 5).map((todo) => { - const isOverdue = isPast(new Date(todo.due_date)); + const isOverdue = isPast(endOfDay(new Date(todo.due_date))); return (
+
-

{todo.title}

+

{todo.title}

- {format(new Date(todo.due_date), 'MMM d, yyyy')} - {isOverdue && (Overdue)} + {format(new Date(todo.due_date), 'MMM d')} + {isOverdue && overdue}
- {todo.category && ( - {todo.category} - )}
- + {todo.priority}
diff --git a/frontend/src/components/dashboard/UpcomingWidget.tsx b/frontend/src/components/dashboard/UpcomingWidget.tsx index dd06c84..bbc2d2b 100644 --- a/frontend/src/components/dashboard/UpcomingWidget.tsx +++ b/frontend/src/components/dashboard/UpcomingWidget.tsx @@ -1,72 +1,98 @@ import { format } from 'date-fns'; -import { CheckSquare, Calendar, Bell } from 'lucide-react'; +import { CheckSquare, Calendar, Bell, ArrowRight } from 'lucide-react'; import type { UpcomingItem } from '@/types'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; import { ScrollArea } from '@/components/ui/scroll-area'; +import { cn } from '@/lib/utils'; interface UpcomingWidgetProps { items: UpcomingItem[]; days?: number; } -const priorityColors: Record = { - low: 'bg-green-500/10 text-green-500 border-green-500/20', - medium: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20', - high: 'bg-red-500/10 text-red-500 border-red-500/20', +const typeConfig: Record = { + todo: { + icon: CheckSquare, + color: 'text-blue-400', + borderColor: 'border-l-blue-400', + label: 'Todo', + }, + event: { + icon: Calendar, + color: 'text-purple-400', + borderColor: 'border-l-purple-400', + label: 'Event', + }, + reminder: { + icon: Bell, + color: 'text-orange-400', + borderColor: 'border-l-orange-400', + label: 'Reminder', + }, }; export default function UpcomingWidget({ items, days = 7 }: UpcomingWidgetProps) { - const getIcon = (type: string) => { - switch (type) { - case 'todo': - return ; - case 'event': - return ; - case 'reminder': - return ; - default: - return null; - } - }; - return ( - + - Upcoming ({days} days) +
+ +
+ +
+ Upcoming +
+ {days} days +
{items.length === 0 ? (

- No upcoming items in the next few days + Nothing upcoming

) : ( - -
- {items.map((item, index) => ( -
- {getIcon(item.type)} -
-

{item.title}

-

- {item.datetime - ? format(new Date(item.datetime), 'MMM d, yyyy h:mm a') - : format(new Date(item.date), 'MMM d, yyyy')} -

-
-
- - {item.type} - + +
+ {items.map((item, index) => { + const config = typeConfig[item.type] || typeConfig.todo; + const Icon = config.icon; + return ( +
+ +
+

{item.title}

+
+ + {item.datetime + ? format(new Date(item.datetime), 'MMM d ยท h:mm a') + : format(new Date(item.date), 'MMM d')} + + + {config.label} + +
+
{item.priority && ( - {item.priority} + + {item.priority} + )}
-
- ))} + ); + })}
)} diff --git a/frontend/src/components/dashboard/WeekTimeline.tsx b/frontend/src/components/dashboard/WeekTimeline.tsx new file mode 100644 index 0000000..ea3d7af --- /dev/null +++ b/frontend/src/components/dashboard/WeekTimeline.tsx @@ -0,0 +1,89 @@ +import { useMemo } from 'react'; +import { format, startOfWeek, addDays, isSameDay, isBefore, startOfDay, formatISO } from 'date-fns'; +import type { UpcomingItem } from '@/types'; +import { cn } from '@/lib/utils'; + +interface WeekTimelineProps { + items: UpcomingItem[]; +} + +const typeColors: Record = { + todo: 'bg-blue-400', + event: 'bg-purple-400', + reminder: 'bg-orange-400', +}; + +export default function WeekTimeline({ items }: WeekTimelineProps) { + const today = useMemo(() => startOfDay(new Date()), []); + const weekStart = useMemo(() => startOfWeek(today, { weekStartsOn: 1 }), [today]); + + const days = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const date = addDays(weekStart, i); + const dayItems = items.filter((item) => { + const itemDate = item.datetime ? new Date(item.datetime) : new Date(item.date); + return isSameDay(startOfDay(itemDate), date); + }); + return { + date, + key: format(date, 'yyyy-MM-dd'), + dayName: format(date, 'EEE'), + dayNum: format(date, 'd'), + isToday: isSameDay(date, today), + isPast: isBefore(date, today), + items: dayItems, + }; + }); + }, [weekStart, today, items]); + + return ( +
+ {days.map((day) => ( +
+ + {day.dayName} + + + {day.dayNum} + +
+ {day.items.slice(0, 4).map((item) => ( +
+ ))} + {day.items.length > 4 && ( + + +{day.items.length - 4} + + )} +
+
+ ))} +
+ ); +} diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index 15418e6..da1e897 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -45,16 +45,16 @@ export default function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose const navLinkClass = ({ isActive }: { isActive: boolean }) => cn( - 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors', + 'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-all duration-200', isActive - ? 'bg-accent text-accent-foreground' - : 'text-muted-foreground hover:bg-accent/10 hover:text-accent' + ? 'bg-accent/15 text-accent border-l-2 border-accent' + : 'text-muted-foreground hover:bg-accent/10 hover:text-accent border-l-2 border-transparent' ); const sidebarContent = ( <>
- {!collapsed &&

UMBRA

} + {!collapsed &&

UMBRA

}