- 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>
163 lines
5.9 KiB
TypeScript
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>
|
|
);
|
|
}
|