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>
This commit is contained in:
parent
c5adc316ef
commit
7ecc81f27e
@ -4,6 +4,9 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>UMBRA</title>
|
<title>UMBRA</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700&family=DM+Sans:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@ -20,37 +20,38 @@ export default function CalendarWidget({ events }: CalendarWidgetProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Calendar className="h-5 w-5" />
|
<div className="p-1.5 rounded-md bg-purple-500/10">
|
||||||
|
<Calendar className="h-4 w-4 text-purple-400" />
|
||||||
|
</div>
|
||||||
Today's Events
|
Today's Events
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">
|
<p className="text-sm text-muted-foreground text-center py-6">
|
||||||
No events scheduled for today
|
No events today
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
{events.map((event) => (
|
{events.map((event) => (
|
||||||
<div
|
<div
|
||||||
key={event.id}
|
key={event.id}
|
||||||
className="flex items-start gap-3 p-3 rounded-lg border bg-card hover:bg-accent/5 transition-colors"
|
className="flex items-start gap-3 p-3 rounded-lg border border-transparent hover:border-border/50 hover:bg-card-elevated transition-all duration-200"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="w-1 h-full min-h-[2rem] rounded-full"
|
className="w-1 h-full min-h-[2rem] rounded-full shrink-0"
|
||||||
style={{ backgroundColor: event.color || 'hsl(var(--primary))' }}
|
style={{ backgroundColor: event.color || 'hsl(var(--primary))' }}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-medium">{event.title}</p>
|
<p className="font-medium text-sm">{event.title}</p>
|
||||||
{!event.all_day && (
|
{!event.all_day ? (
|
||||||
<div className="flex items-center gap-1 text-sm text-muted-foreground mt-1">
|
<div className="flex items-center gap-1 text-xs text-muted-foreground mt-1">
|
||||||
<Clock className="h-3 w-3" />
|
<Clock className="h-3 w-3" />
|
||||||
{format(new Date(event.start_datetime), 'h:mm a')}
|
{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')}`}
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
{event.all_day && (
|
<p className="text-xs text-muted-foreground mt-1">All day</p>
|
||||||
<p className="text-sm text-muted-foreground mt-1">All day</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -8,9 +8,19 @@ import StatsWidget from './StatsWidget';
|
|||||||
import TodoWidget from './TodoWidget';
|
import TodoWidget from './TodoWidget';
|
||||||
import CalendarWidget from './CalendarWidget';
|
import CalendarWidget from './CalendarWidget';
|
||||||
import UpcomingWidget from './UpcomingWidget';
|
import UpcomingWidget from './UpcomingWidget';
|
||||||
|
import WeekTimeline from './WeekTimeline';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { DashboardSkeleton } from '@/components/ui/skeleton';
|
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() {
|
export default function DashboardPage() {
|
||||||
const { settings } = useSettings();
|
const { settings } = useSettings();
|
||||||
|
|
||||||
@ -36,11 +46,13 @@ export default function DashboardPage() {
|
|||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="border-b bg-card px-6 py-4">
|
<div className="px-6 py-6">
|
||||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
<div className="animate-pulse space-y-2">
|
||||||
<p className="text-muted-foreground mt-1">Welcome back. Here's your overview.</p>
|
<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 p-6">
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||||
<DashboardSkeleton />
|
<DashboardSkeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -57,46 +69,82 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="border-b bg-card px-6 py-4">
|
{/* Header — greeting + date */}
|
||||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
<div className="px-6 pt-6 pb-2">
|
||||||
<p className="text-muted-foreground mt-1">Welcome back. Here's your overview.</p>
|
<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>
|
||||||
|
|
||||||
<div className="flex-1 overflow-y-auto p-6">
|
<div className="flex-1 overflow-y-auto px-6 pb-6">
|
||||||
<div className="space-y-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
|
<StatsWidget
|
||||||
projectStats={data.project_stats}
|
projectStats={data.project_stats}
|
||||||
totalPeople={data.total_people}
|
totalPeople={data.total_people}
|
||||||
totalLocations={data.total_locations}
|
totalLocations={data.total_locations}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{upcomingData && upcomingData.items.length > 0 && (
|
|
||||||
<UpcomingWidget items={upcomingData.items} days={upcomingData.days} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
|
||||||
<TodoWidget todos={data.upcoming_todos} />
|
|
||||||
<CalendarWidget events={data.todays_events} />
|
|
||||||
</div>
|
</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 && (
|
{data.active_reminders.length > 0 && (
|
||||||
<Card>
|
<Card className="animate-slide-up" style={{ animationDelay: '150ms', animationFillMode: 'backwards' }}>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<Bell className="h-5 w-5" />
|
<div className="p-1.5 rounded-md bg-orange-500/10">
|
||||||
|
<Bell className="h-4 w-4 text-orange-400" />
|
||||||
|
</div>
|
||||||
Active Reminders
|
Active Reminders
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
{data.active_reminders.map((reminder) => (
|
{data.active_reminders.map((reminder) => (
|
||||||
<div
|
<div
|
||||||
key={reminder.id}
|
key={reminder.id}
|
||||||
className="flex items-center gap-3 p-3 rounded-lg border bg-card hover:bg-accent/5 transition-colors"
|
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"
|
||||||
>
|
>
|
||||||
<Bell className="h-4 w-4 text-accent" />
|
<div className="w-1 h-8 rounded-full bg-orange-400" />
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-medium">{reminder.title}</p>
|
<p className="font-medium text-sm">{reminder.title}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{format(new Date(reminder.remind_at), 'MMM d, yyyy h:mm a')}
|
{format(new Date(reminder.remind_at), 'MMM d, yyyy h:mm a')}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@ -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';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
|
|
||||||
interface StatsWidgetProps {
|
interface StatsWidgetProps {
|
||||||
@ -13,42 +13,52 @@ interface StatsWidgetProps {
|
|||||||
export default function StatsWidget({ projectStats, totalPeople, totalLocations }: StatsWidgetProps) {
|
export default function StatsWidget({ projectStats, totalPeople, totalLocations }: StatsWidgetProps) {
|
||||||
const statCards = [
|
const statCards = [
|
||||||
{
|
{
|
||||||
label: 'Total Projects',
|
label: 'PROJECTS',
|
||||||
value: projectStats.total,
|
value: projectStats.total,
|
||||||
icon: FolderKanban,
|
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,
|
value: projectStats.by_status['in_progress'] || 0,
|
||||||
icon: FolderKanban,
|
icon: TrendingUp,
|
||||||
color: 'text-purple-500',
|
color: 'text-purple-400',
|
||||||
|
glowBg: 'bg-purple-500/10',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'People',
|
label: 'PEOPLE',
|
||||||
value: totalPeople,
|
value: totalPeople,
|
||||||
icon: Users,
|
icon: Users,
|
||||||
color: 'text-green-500',
|
color: 'text-emerald-400',
|
||||||
|
glowBg: 'bg-emerald-500/10',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Locations',
|
label: 'LOCATIONS',
|
||||||
value: totalLocations,
|
value: totalLocations,
|
||||||
icon: MapPin,
|
icon: MapPin,
|
||||||
color: 'text-orange-500',
|
color: 'text-orange-400',
|
||||||
|
glowBg: 'bg-orange-500/10',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
|
||||||
{statCards.map((stat) => (
|
{statCards.map((stat) => (
|
||||||
<Card key={stat.label}>
|
<Card key={stat.label} className="bg-gradient-to-br from-accent/[0.03] to-transparent">
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div>
|
<div className="space-y-2">
|
||||||
<p className="text-sm font-medium text-muted-foreground">{stat.label}</p>
|
<p className="text-[11px] font-medium tracking-wider text-muted-foreground">
|
||||||
<p className="text-3xl font-bold mt-2">{stat.value}</p>
|
{stat.label}
|
||||||
|
</p>
|
||||||
|
<p className="font-heading text-3xl font-bold tabular-nums leading-none">
|
||||||
|
{stat.value}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`p-2 rounded-lg ${stat.glowBg}`}>
|
||||||
|
<stat.icon className={`h-5 w-5 ${stat.color}`} />
|
||||||
</div>
|
</div>
|
||||||
<stat.icon className={`h-8 w-8 ${stat.color}`} />
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { format, isPast } from 'date-fns';
|
import { format, isPast, endOfDay } from 'date-fns';
|
||||||
import { Calendar, CheckCircle2 } from 'lucide-react';
|
import { Calendar, CheckCircle2 } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
@ -17,9 +17,9 @@ interface TodoWidgetProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const priorityColors: Record<string, string> = {
|
const priorityColors: Record<string, string> = {
|
||||||
low: 'bg-green-500/10 text-green-500 border-green-500/20',
|
low: 'bg-green-500/10 text-green-400 border-green-500/20',
|
||||||
medium: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20',
|
medium: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20',
|
||||||
high: 'bg-red-500/10 text-red-500 border-red-500/20',
|
high: 'bg-red-500/10 text-red-400 border-red-500/20',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function TodoWidget({ todos }: TodoWidgetProps) {
|
export default function TodoWidget({ todos }: TodoWidgetProps) {
|
||||||
@ -27,41 +27,45 @@ export default function TodoWidget({ todos }: TodoWidgetProps) {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
<CheckCircle2 className="h-5 w-5" />
|
<div className="p-1.5 rounded-md bg-blue-500/10">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-blue-400" />
|
||||||
|
</div>
|
||||||
Upcoming Todos
|
Upcoming Todos
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{todos.length === 0 ? (
|
{todos.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">
|
<p className="text-sm text-muted-foreground text-center py-6">
|
||||||
No upcoming todos. You're all caught up!
|
All caught up.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-2">
|
||||||
{todos.slice(0, 5).map((todo) => {
|
{todos.slice(0, 5).map((todo) => {
|
||||||
const isOverdue = isPast(new Date(todo.due_date));
|
const isOverdue = isPast(endOfDay(new Date(todo.due_date)));
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={todo.id}
|
key={todo.id}
|
||||||
className="flex items-center gap-3 p-3 rounded-lg border bg-card hover:bg-accent/5 transition-colors"
|
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={cn(
|
||||||
|
'w-1 h-8 rounded-full shrink-0',
|
||||||
|
todo.priority === 'high' ? 'bg-red-400' :
|
||||||
|
todo.priority === 'medium' ? 'bg-yellow-400' : 'bg-green-400'
|
||||||
|
)} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-medium">{todo.title}</p>
|
<p className="font-medium text-sm truncate">{todo.title}</p>
|
||||||
<div className="flex items-center gap-2 mt-1">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
<div className={cn(
|
<div className={cn(
|
||||||
"flex items-center gap-1 text-xs",
|
"flex items-center gap-1 text-xs",
|
||||||
isOverdue ? "text-destructive" : "text-muted-foreground"
|
isOverdue ? "text-destructive" : "text-muted-foreground"
|
||||||
)}>
|
)}>
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
{format(new Date(todo.due_date), 'MMM d, yyyy')}
|
{format(new Date(todo.due_date), 'MMM d')}
|
||||||
{isOverdue && <span className="font-medium">(Overdue)</span>}
|
{isOverdue && <span className="font-medium">overdue</span>}
|
||||||
</div>
|
|
||||||
{todo.category && (
|
|
||||||
<Badge variant="outline" className="text-xs">{todo.category}</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge className={priorityColors[todo.priority] || priorityColors.medium}>
|
</div>
|
||||||
|
<Badge className={cn('text-[10px] shrink-0', priorityColors[todo.priority] || priorityColors.medium)}>
|
||||||
{todo.priority}
|
{todo.priority}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,72 +1,98 @@
|
|||||||
import { format } from 'date-fns';
|
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 type { UpcomingItem } from '@/types';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface UpcomingWidgetProps {
|
interface UpcomingWidgetProps {
|
||||||
items: UpcomingItem[];
|
items: UpcomingItem[];
|
||||||
days?: number;
|
days?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const priorityColors: Record<string, string> = {
|
const typeConfig: Record<string, { icon: typeof CheckSquare; color: string; borderColor: string; label: string }> = {
|
||||||
low: 'bg-green-500/10 text-green-500 border-green-500/20',
|
todo: {
|
||||||
medium: 'bg-yellow-500/10 text-yellow-500 border-yellow-500/20',
|
icon: CheckSquare,
|
||||||
high: 'bg-red-500/10 text-red-500 border-red-500/20',
|
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) {
|
export default function UpcomingWidget({ items, days = 7 }: UpcomingWidgetProps) {
|
||||||
const getIcon = (type: string) => {
|
|
||||||
switch (type) {
|
|
||||||
case 'todo':
|
|
||||||
return <CheckSquare className="h-5 w-5 text-blue-500" />;
|
|
||||||
case 'event':
|
|
||||||
return <Calendar className="h-5 w-5 text-purple-500" />;
|
|
||||||
case 'reminder':
|
|
||||||
return <Bell className="h-5 w-5 text-orange-500" />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="h-full">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Upcoming ({days} days)</CardTitle>
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<div className="p-1.5 rounded-md bg-accent/10">
|
||||||
|
<ArrowRight className="h-4 w-4 text-accent" />
|
||||||
|
</div>
|
||||||
|
Upcoming
|
||||||
|
</CardTitle>
|
||||||
|
<span className="text-xs text-muted-foreground">{days} days</span>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{items.length === 0 ? (
|
{items.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">
|
<p className="text-sm text-muted-foreground text-center py-8">
|
||||||
No upcoming items in the next few days
|
Nothing upcoming
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<ScrollArea className="h-[300px]">
|
<ScrollArea className="max-h-[400px] -mr-2 pr-2">
|
||||||
<div className="space-y-3">
|
<div className="space-y-1.5">
|
||||||
{items.map((item, index) => (
|
{items.map((item, index) => {
|
||||||
|
const config = typeConfig[item.type] || typeConfig.todo;
|
||||||
|
const Icon = config.icon;
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={`${item.type}-${item.id}-${index}`}
|
key={`${item.type}-${item.id}-${index}`}
|
||||||
className="flex items-start gap-3 p-3 rounded-lg border bg-card hover:bg-accent/5 transition-colors"
|
className={cn(
|
||||||
|
'flex items-start gap-3 p-3 rounded-lg',
|
||||||
|
'border-l-2 border border-transparent',
|
||||||
|
config.borderColor,
|
||||||
|
'hover:bg-card-elevated transition-all duration-200'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{getIcon(item.type)}
|
<Icon className={cn('h-4 w-4 mt-0.5 shrink-0', config.color)} />
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="font-medium">{item.title}</p>
|
<p className="font-medium text-sm">{item.title}</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<div className="flex items-center gap-2 mt-1">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
{item.datetime
|
{item.datetime
|
||||||
? format(new Date(item.datetime), 'MMM d, yyyy h:mm a')
|
? format(new Date(item.datetime), 'MMM d · h:mm a')
|
||||||
: format(new Date(item.date), 'MMM d, yyyy')}
|
: format(new Date(item.date), 'MMM d')}
|
||||||
</p>
|
</span>
|
||||||
|
<span className={cn('text-[10px] font-medium uppercase tracking-wider', config.color)}>
|
||||||
|
{config.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Badge variant="outline" className="capitalize">
|
|
||||||
{item.type}
|
|
||||||
</Badge>
|
|
||||||
{item.priority && (
|
{item.priority && (
|
||||||
<Badge className={priorityColors[item.priority]}>{item.priority}</Badge>
|
<span className={cn(
|
||||||
|
'text-[10px] font-medium px-1.5 py-0.5 rounded',
|
||||||
|
item.priority === 'high' ? 'bg-red-500/10 text-red-400' :
|
||||||
|
item.priority === 'medium' ? 'bg-yellow-500/10 text-yellow-400' :
|
||||||
|
'bg-green-500/10 text-green-400'
|
||||||
|
)}>
|
||||||
|
{item.priority}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
)}
|
)}
|
||||||
|
|||||||
89
frontend/src/components/dashboard/WeekTimeline.tsx
Normal file
89
frontend/src/components/dashboard/WeekTimeline.tsx
Normal file
@ -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<string, string> = {
|
||||||
|
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 (
|
||||||
|
<div className="flex items-stretch gap-2">
|
||||||
|
{days.map((day) => (
|
||||||
|
<div
|
||||||
|
key={day.key}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 flex flex-col items-center gap-1.5 rounded-lg py-3 px-2 transition-all duration-200 border',
|
||||||
|
day.isToday
|
||||||
|
? 'bg-accent/10 border-accent/30 shadow-[0_0_12px_hsl(var(--accent-color)/0.15)]'
|
||||||
|
: day.isPast
|
||||||
|
? 'border-transparent opacity-50'
|
||||||
|
: 'border-transparent hover:border-border/50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'text-[11px] font-medium uppercase tracking-wider',
|
||||||
|
day.isToday ? 'text-accent' : 'text-muted-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{day.dayName}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
'font-heading text-lg font-semibold leading-none',
|
||||||
|
day.isToday ? 'text-accent' : 'text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{day.dayNum}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1 mt-0.5 min-h-[8px]">
|
||||||
|
{day.items.slice(0, 4).map((item) => (
|
||||||
|
<div
|
||||||
|
key={`${item.type}-${item.id}`}
|
||||||
|
className={cn(
|
||||||
|
'w-1.5 h-1.5 rounded-full',
|
||||||
|
typeColors[item.type] || 'bg-muted-foreground'
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{day.items.length > 4 && (
|
||||||
|
<span className="text-[9px] text-muted-foreground font-medium">
|
||||||
|
+{day.items.length - 4}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -45,16 +45,16 @@ export default function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose
|
|||||||
|
|
||||||
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
|
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
|
||||||
cn(
|
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
|
isActive
|
||||||
? 'bg-accent text-accent-foreground'
|
? 'bg-accent/15 text-accent border-l-2 border-accent'
|
||||||
: 'text-muted-foreground hover:bg-accent/10 hover:text-accent'
|
: 'text-muted-foreground hover:bg-accent/10 hover:text-accent border-l-2 border-transparent'
|
||||||
);
|
);
|
||||||
|
|
||||||
const sidebarContent = (
|
const sidebarContent = (
|
||||||
<>
|
<>
|
||||||
<div className="flex h-16 items-center justify-between border-b px-4">
|
<div className="flex h-16 items-center justify-between border-b px-4">
|
||||||
{!collapsed && <h1 className="text-xl font-bold text-accent">UMBRA</h1>}
|
{!collapsed && <h1 className="font-heading text-xl font-bold tracking-tight text-accent">UMBRA</h1>}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
@ -120,7 +120,7 @@ export default function Sidebar({ collapsed, onToggle, mobileOpen, onMobileClose
|
|||||||
{/* Mobile overlay */}
|
{/* Mobile overlay */}
|
||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
<div className="fixed inset-0 z-40 md:hidden">
|
<div className="fixed inset-0 z-40 md:hidden">
|
||||||
<div className="absolute inset-0 bg-background/80" onClick={onMobileClose} />
|
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm" onClick={onMobileClose} />
|
||||||
<aside className="relative z-50 flex flex-col w-64 h-full bg-card border-r">
|
<aside className="relative z-50 flex flex-col w-64 h-full bg-card border-r">
|
||||||
{sidebarContent}
|
{sidebarContent}
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@ -5,7 +5,12 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
|
|||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('rounded-lg border bg-card text-card-foreground shadow-sm', className)}
|
className={cn(
|
||||||
|
'rounded-lg border bg-card text-card-foreground shadow-sm',
|
||||||
|
'transition-all duration-200',
|
||||||
|
'hover:shadow-lg hover:shadow-accent/5 hover:border-accent/20',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -14,7 +19,7 @@ Card.displayName = 'Card';
|
|||||||
|
|
||||||
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
|
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-5', className)} {...props} />
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
CardHeader.displayName = 'CardHeader';
|
CardHeader.displayName = 'CardHeader';
|
||||||
@ -23,7 +28,7 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
|
|||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<h3
|
<h3
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('text-2xl font-semibold leading-none tracking-tight', className)}
|
className={cn('font-heading text-lg font-semibold leading-none tracking-tight', className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@ -40,14 +45,14 @@ CardDescription.displayName = 'CardDescription';
|
|||||||
|
|
||||||
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn('p-6 pt-0', className)} {...props} />
|
<div ref={ref} className={cn('p-5 pt-0', className)} {...props} />
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
CardContent.displayName = 'CardContent';
|
CardContent.displayName = 'CardContent';
|
||||||
|
|
||||||
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div ref={ref} className={cn('flex items-center p-6 pt-0', className)} {...props} />
|
<div ref={ref} className={cn('flex items-center p-5 pt-0', className)} {...props} />
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
CardFooter.displayName = 'CardFooter';
|
CardFooter.displayName = 'CardFooter';
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
--foreground: 0 0% 98%;
|
--foreground: 0 0% 98%;
|
||||||
--card: 0 0% 5%;
|
--card: 0 0% 5%;
|
||||||
--card-foreground: 0 0% 98%;
|
--card-foreground: 0 0% 98%;
|
||||||
|
--card-elevated: 0 0% 7%;
|
||||||
--popover: 0 0% 5%;
|
--popover: 0 0% 5%;
|
||||||
--popover-foreground: 0 0% 98%;
|
--popover-foreground: 0 0% 98%;
|
||||||
--primary: var(--accent-h) var(--accent-s) var(--accent-l);
|
--primary: var(--accent-h) var(--accent-s) var(--accent-l);
|
||||||
@ -29,6 +30,14 @@
|
|||||||
--accent-h: 187;
|
--accent-h: 187;
|
||||||
--accent-s: 85.7%;
|
--accent-s: 85.7%;
|
||||||
--accent-l: 53.3%;
|
--accent-l: 53.3%;
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
--transition-fast: 150ms;
|
||||||
|
--transition-normal: 250ms;
|
||||||
|
|
||||||
|
/* Fonts */
|
||||||
|
--font-heading: 'Sora', sans-serif;
|
||||||
|
--font-body: 'DM Sans', sans-serif;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,8 +47,38 @@
|
|||||||
}
|
}
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
|
font-family: var(--font-body);
|
||||||
font-feature-settings: "rlig" 1, "calt" 1;
|
font-feature-settings: "rlig" 1, "calt" 1;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
font-family: var(--font-heading);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Scrollbar */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: hsl(0 0% 20%) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: hsl(0 0% 20%);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: hsl(var(--accent-color) / 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FullCalendar Dark Theme Overrides */
|
/* FullCalendar Dark Theme Overrides */
|
||||||
|
|||||||
@ -8,6 +8,10 @@ export default {
|
|||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
fontFamily: {
|
||||||
|
heading: ['Sora', 'sans-serif'],
|
||||||
|
body: ['DM Sans', 'sans-serif'],
|
||||||
|
},
|
||||||
colors: {
|
colors: {
|
||||||
border: 'hsl(var(--border))',
|
border: 'hsl(var(--border))',
|
||||||
input: 'hsl(var(--input))',
|
input: 'hsl(var(--input))',
|
||||||
@ -41,6 +45,7 @@ export default {
|
|||||||
card: {
|
card: {
|
||||||
DEFAULT: 'hsl(var(--card))',
|
DEFAULT: 'hsl(var(--card))',
|
||||||
foreground: 'hsl(var(--card-foreground))',
|
foreground: 'hsl(var(--card-foreground))',
|
||||||
|
elevated: 'hsl(var(--card-elevated))',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
borderRadius: {
|
borderRadius: {
|
||||||
@ -48,6 +53,20 @@ export default {
|
|||||||
md: 'calc(var(--radius) - 2px)',
|
md: 'calc(var(--radius) - 2px)',
|
||||||
sm: 'calc(var(--radius) - 4px)',
|
sm: 'calc(var(--radius) - 4px)',
|
||||||
},
|
},
|
||||||
|
keyframes: {
|
||||||
|
'fade-in': {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
'slide-up': {
|
||||||
|
'0%': { opacity: '0', transform: 'translateY(8px)' },
|
||||||
|
'100%': { opacity: '1', transform: 'translateY(0)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fade-in 0.3s ease-out',
|
||||||
|
'slide-up': 'slide-up 0.4s ease-out',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user