Kyle Pope 1d21caaa62 Dashboard personalization: preferred name, colored dots, smart briefing
- Add preferred_name column to settings model/schema with migration
- Settings page gets Profile card with name input (saves on blur/enter)
- Dashboard greeting now shows "Good evening, Kyle." when name is set
- WeekTimeline dots use event's actual color when available
- New DayBriefing component shows time-of-day-aware contextual summary

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

91 lines
2.9 KiB
TypeScript

import { useMemo } from 'react';
import { format, startOfWeek, addDays, isSameDay, isBefore, startOfDay } 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',
!item.color && (typeColors[item.type] || 'bg-muted-foreground')
)}
style={item.color ? { backgroundColor: item.color } : undefined}
/>
))}
{day.items.length > 4 && (
<span className="text-[9px] text-muted-foreground font-medium">
+{day.items.length - 4}
</span>
)}
</div>
</div>
))}
</div>
);
}