Scale down all content text on mobile via .mobile-scale CSS class (excludes navbar/UMBRA title). Hide calendar event times in month view (Google Calendar style). Restructure CategoryFilterBar so categories display on a separate row when toggled instead of being hidden behind the search bar. Reduce dashboard widget density with hidden badges and tighter spacing on small screens. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useNavigate } from 'react-router-dom';
|
|
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 navigate = useNavigate();
|
|
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-1 sm:gap-2">
|
|
{days.map((day) => (
|
|
<div
|
|
key={day.key}
|
|
onClick={() => navigate('/calendar', { state: { date: day.key, view: 'timeGridDay' } })}
|
|
className={cn(
|
|
'flex-1 flex flex-col items-center gap-1 sm:gap-1.5 rounded-lg py-2 sm:py-3 px-1 sm:px-2 transition-all duration-200 border cursor-pointer',
|
|
day.isToday
|
|
? 'bg-accent/10 border-accent/30 shadow-[0_0_12px_hsl(var(--accent-color)/0.15)]'
|
|
: day.isPast
|
|
? 'border-transparent opacity-50 hover:opacity-75'
|
|
: 'border-transparent hover:border-border/50'
|
|
)}
|
|
>
|
|
<span
|
|
className={cn(
|
|
'text-[9px] sm:text-[11px] font-medium uppercase tracking-wider',
|
|
day.isToday ? 'text-accent' : 'text-muted-foreground'
|
|
)}
|
|
>
|
|
{day.dayName}
|
|
</span>
|
|
<span
|
|
className={cn(
|
|
'font-heading text-sm sm: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>
|
|
);
|
|
}
|