UMBRA/frontend/src/components/dashboard/DashboardPage.tsx
Kyle Pope d99506c9e4 UI overhaul Stage 1: Dashboard redesign with refined dark luxury aesthetic
- Add Sora + DM Sans Google Fonts with heading/body font system
- New CSS variables for elevated surfaces, transitions, custom scrollbars
- Tailwind config: fade-in/slide-up animations, card-elevated color, font families
- Card component: hover glow, accent border on hover, smooth transitions
- New WeekTimeline component: 7-day horizontal strip with event dot indicators
- Dashboard: contextual time-of-day greeting, week timeline, redesigned 5-col layout
- Stats widget: accent-tinted gradients, icon glow backgrounds, uppercase labels
- Upcoming widget: colored left-border type indicators, unified timeline feed
- Calendar/Todo widgets: refined spacing, hover states, colored accent bars
- Sidebar: accent bar active state (border-l-2), backdrop-blur mobile overlay

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 01:35:01 +08:00

163 lines
5.9 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { format } from 'date-fns';
import { Bell } from 'lucide-react';
import api from '@/lib/api';
import type { DashboardData, UpcomingResponse } from '@/types';
import { useSettings } from '@/hooks/useSettings';
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();
const { data, isLoading } = useQuery({
queryKey: ['dashboard'],
queryFn: async () => {
const now = new Date();
const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
const { data } = await api.get<DashboardData>(`/dashboard?client_date=${today}`);
return data;
},
});
const { data: upcomingData } = useQuery({
queryKey: ['upcoming', settings?.upcoming_days],
queryFn: async () => {
const days = settings?.upcoming_days || 7;
const { data } = await api.get<UpcomingResponse>(`/upcoming?days=${days}`);
return data;
},
});
if (isLoading) {
return (
<div className="flex flex-col h-full">
<div className="px-6 py-6">
<div className="animate-pulse space-y-2">
<div className="h-8 w-48 rounded bg-muted" />
<div className="h-4 w-32 rounded bg-muted" />
</div>
</div>
<div className="flex-1 overflow-y-auto px-6 pb-6">
<DashboardSkeleton />
</div>
</div>
);
}
if (!data) {
return (
<div className="flex items-center justify-center h-full">
<div className="text-muted-foreground">Failed to load dashboard</div>
</div>
);
}
return (
<div className="flex flex-col h-full">
{/* Header — greeting + date */}
<div className="px-6 pt-6 pb-2">
<h1 className="font-heading text-3xl font-bold tracking-tight animate-fade-in">
{getGreeting()}
</h1>
<p className="text-muted-foreground text-sm mt-1">
{format(new Date(), 'EEEE, MMMM d, yyyy')}
</p>
</div>
<div className="flex-1 overflow-y-auto px-6 pb-6">
<div className="space-y-5">
{/* Week Timeline */}
{upcomingData && (
<div className="animate-slide-up">
<WeekTimeline items={upcomingData.items} />
</div>
)}
{/* Stats Row */}
<div className="animate-slide-up" style={{ animationDelay: '50ms', animationFillMode: 'backwards' }}>
<StatsWidget
projectStats={data.project_stats}
totalPeople={data.total_people}
totalLocations={data.total_locations}
/>
</div>
{/* Main Content — 2 columns */}
<div className="grid gap-5 lg:grid-cols-5 animate-slide-up" style={{ animationDelay: '100ms', animationFillMode: 'backwards' }}>
{/* Left: Upcoming feed (wider) */}
<div className="lg:col-span-3">
{upcomingData && upcomingData.items.length > 0 ? (
<UpcomingWidget items={upcomingData.items} days={upcomingData.days} />
) : (
<Card className="h-full">
<CardHeader>
<CardTitle>Upcoming</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground text-center py-8">
Nothing upcoming. Enjoy the quiet.
</p>
</CardContent>
</Card>
)}
</div>
{/* Right: Today's events + todos stacked */}
<div className="lg:col-span-2 space-y-5">
<CalendarWidget events={data.todays_events} />
<TodoWidget todos={data.upcoming_todos} />
</div>
</div>
{/* Active Reminders */}
{data.active_reminders.length > 0 && (
<Card className="animate-slide-up" style={{ animationDelay: '150ms', animationFillMode: 'backwards' }}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="p-1.5 rounded-md bg-orange-500/10">
<Bell className="h-4 w-4 text-orange-400" />
</div>
Active Reminders
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
{data.active_reminders.map((reminder) => (
<div
key={reminder.id}
className="flex items-center gap-3 p-3 rounded-lg border border-transparent hover:border-border/50 hover:bg-card-elevated transition-all duration-200"
>
<div className="w-1 h-8 rounded-full bg-orange-400" />
<div className="flex-1">
<p className="font-medium text-sm">{reminder.title}</p>
<p className="text-xs text-muted-foreground">
{format(new Date(reminder.remind_at), 'MMM d, yyyy h:mm a')}
</p>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
</div>
</div>
);
}