From bdae07fb7d2ff8d4a34bcaaf9a1d3645aa52994c Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Fri, 20 Feb 2026 14:28:39 +0800 Subject: [PATCH] Compact dashboard: single-line rows, multi-star, weather city - UpcomingWidget: single-line rows with icon/title/date/type/priority - CalendarWidget: whitespace-nowrap time ranges, no wrapping - TodoWidget: compact dot + title + date + badge on one line - Active Reminders: single-line with dot indicator - CountdownWidget: supports array of starred events - StatsWidget: shows city name in weather card - Dashboard API: returns starred_events array (up to 5) instead of single Co-Authored-By: Claude Opus 4.6 --- backend/app/routers/dashboard.py | 21 +++---- .../components/dashboard/CalendarWidget.tsx | 6 +- .../components/dashboard/CountdownWidget.tsx | 38 ++++++------ .../components/dashboard/DashboardPage.tsx | 20 +++---- .../src/components/dashboard/StatsWidget.tsx | 7 ++- .../src/components/dashboard/TodoWidget.tsx | 41 ++++++------- .../components/dashboard/UpcomingWidget.tsx | 58 ++++++------------- frontend/src/types/index.ts | 4 +- 8 files changed, 87 insertions(+), 108 deletions(-) diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index 3e51f42..10470b9 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -69,22 +69,23 @@ async def get_dashboard( ) total_incomplete_todos = total_incomplete_result.scalar() - # Next starred event (soonest future starred event) + # Starred events (upcoming, ordered by date) now = datetime.now() starred_query = select(CalendarEvent).where( CalendarEvent.is_starred == True, CalendarEvent.start_datetime > now - ).order_by(CalendarEvent.start_datetime.asc()).limit(1) + ).order_by(CalendarEvent.start_datetime.asc()).limit(5) starred_result = await db.execute(starred_query) - next_starred = starred_result.scalar_one_or_none() + starred_events = starred_result.scalars().all() - next_starred_event_data = None - if next_starred: - next_starred_event_data = { - "id": next_starred.id, - "title": next_starred.title, - "start_datetime": next_starred.start_datetime + starred_events_data = [ + { + "id": e.id, + "title": e.title, + "start_datetime": e.start_datetime } + for e in starred_events + ] return { "todays_events": [ @@ -122,7 +123,7 @@ async def get_dashboard( "by_status": projects_by_status }, "total_incomplete_todos": total_incomplete_todos, - "next_starred_event": next_starred_event_data + "starred_events": starred_events_data } diff --git a/frontend/src/components/dashboard/CalendarWidget.tsx b/frontend/src/components/dashboard/CalendarWidget.tsx index 01776dc..688a202 100644 --- a/frontend/src/components/dashboard/CalendarWidget.tsx +++ b/frontend/src/components/dashboard/CalendarWidget.tsx @@ -37,13 +37,13 @@ export default function CalendarWidget({ events }: CalendarWidgetProps) { {events.map((event) => (
- + {event.all_day ? 'All day' : `${format(new Date(event.start_datetime), 'h:mm a')} โ€“ ${format(new Date(event.end_datetime), 'h:mm a')}`} diff --git a/frontend/src/components/dashboard/CountdownWidget.tsx b/frontend/src/components/dashboard/CountdownWidget.tsx index 426414c..dc3f892 100644 --- a/frontend/src/components/dashboard/CountdownWidget.tsx +++ b/frontend/src/components/dashboard/CountdownWidget.tsx @@ -2,31 +2,33 @@ import { differenceInCalendarDays } from 'date-fns'; import { Star } from 'lucide-react'; interface CountdownWidgetProps { - event: { + events: Array<{ id: number; title: string; start_datetime: string; - }; + }>; } -export default function CountdownWidget({ event }: CountdownWidgetProps) { - const daysUntil = differenceInCalendarDays(new Date(event.start_datetime), new Date()); - if (daysUntil < 0) return null; - - const label = daysUntil === 0 - ? 'Today' - : daysUntil === 1 - ? '1 day' - : `${daysUntil} days`; +export default function CountdownWidget({ events }: CountdownWidgetProps) { + const visible = events.filter((e) => differenceInCalendarDays(new Date(e.start_datetime), new Date()) >= 0); + if (visible.length === 0) return null; return ( -
- - - {label} - {daysUntil > 0 ? ' until ' : ' โ€” '} - {event.title} - +
+ {visible.map((event) => { + const days = differenceInCalendarDays(new Date(event.start_datetime), new Date()); + const label = days === 0 ? 'Today' : days === 1 ? '1 day' : `${days} days`; + return ( +
+ + + {label} + {days > 0 ? ' until ' : ' โ€” '} + {event.title} + +
+ ); + })}
); } diff --git a/frontend/src/components/dashboard/DashboardPage.tsx b/frontend/src/components/dashboard/DashboardPage.tsx index 34ff68c..c38eda3 100644 --- a/frontend/src/components/dashboard/DashboardPage.tsx +++ b/frontend/src/components/dashboard/DashboardPage.tsx @@ -197,8 +197,8 @@ export default function DashboardPage() { {/* Right: Countdown + Today's events + todos stacked */}
- {data.next_starred_event && ( - + {data.starred_events.length > 0 && ( + )} @@ -217,19 +217,17 @@ export default function DashboardPage() { -
+
{data.active_reminders.map((reminder) => (
-
-
-

{reminder.title}

-

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

-
+
+ {reminder.title} + + {format(new Date(reminder.remind_at), 'MMM d, h:mm a')} +
))}
diff --git a/frontend/src/components/dashboard/StatsWidget.tsx b/frontend/src/components/dashboard/StatsWidget.tsx index 37bc1d1..1c87030 100644 --- a/frontend/src/components/dashboard/StatsWidget.tsx +++ b/frontend/src/components/dashboard/StatsWidget.tsx @@ -7,7 +7,7 @@ interface StatsWidgetProps { by_status: Record; }; totalIncompleteTodos: number; - weatherData?: { temp: number; description: string } | null; + weatherData?: { temp: number; description: string; city?: string } | null; } export default function StatsWidget({ projectStats, totalIncompleteTodos, weatherData }: StatsWidgetProps) { @@ -73,6 +73,11 @@ export default function StatsWidget({ projectStats, totalIncompleteTodos, weathe

{weatherData.description}

+ {weatherData.city && ( +

+ {weatherData.city} +

+ )} ) : ( <> diff --git a/frontend/src/components/dashboard/TodoWidget.tsx b/frontend/src/components/dashboard/TodoWidget.tsx index d9123ae..b81b29c 100644 --- a/frontend/src/components/dashboard/TodoWidget.tsx +++ b/frontend/src/components/dashboard/TodoWidget.tsx @@ -1,5 +1,5 @@ import { format, isPast, endOfDay } from 'date-fns'; -import { Calendar, CheckCircle2 } from 'lucide-react'; +import { CheckCircle2 } from 'lucide-react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; @@ -22,6 +22,12 @@ const priorityColors: Record = { high: 'bg-red-500/10 text-red-400 border-red-500/20', }; +const dotColors: Record = { + high: 'bg-red-400', + medium: 'bg-yellow-400', + low: 'bg-green-400', +}; + export default function TodoWidget({ todos }: TodoWidgetProps) { return ( @@ -39,33 +45,24 @@ export default function TodoWidget({ todos }: TodoWidgetProps) { All caught up.

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

{todo.title}

-
-
- - {format(new Date(todo.due_date), 'MMM d')} - {isOverdue && overdue} -
-
-
- +
+ {todo.title} + + {format(new Date(todo.due_date), 'MMM d')} + {isOverdue && ' overdue'} + + {todo.priority}
diff --git a/frontend/src/components/dashboard/UpcomingWidget.tsx b/frontend/src/components/dashboard/UpcomingWidget.tsx index bbc2d2b..4eb0b2b 100644 --- a/frontend/src/components/dashboard/UpcomingWidget.tsx +++ b/frontend/src/components/dashboard/UpcomingWidget.tsx @@ -10,25 +10,10 @@ interface UpcomingWidgetProps { days?: number; } -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', - }, +const typeConfig: Record = { + todo: { icon: CheckSquare, color: 'text-blue-400', label: 'TODO' }, + event: { icon: Calendar, color: 'text-purple-400', label: 'EVENT' }, + reminder: { icon: Bell, color: 'text-orange-400', label: 'REMINDER' }, }; export default function UpcomingWidget({ items, days = 7 }: UpcomingWidgetProps) { @@ -52,37 +37,28 @@ export default function UpcomingWidget({ items, days = 7 }: UpcomingWidgetProps)

) : ( -
+
{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.title} + + {item.datetime + ? format(new Date(item.datetime), 'MMM d, h:mm a') + : format(new Date(item.date), 'MMM d')} + + + {config.label} + {item.priority && ( ; }; total_incomplete_todos: number; - next_starred_event: { + starred_events: Array<{ id: number; title: string; start_datetime: string; - } | null; + }>; } export interface WeatherData {