UMBRA/frontend/src/components/dashboard/UpcomingWidget.tsx
Kyle Pope bfe97fd749 Fix 5 testing bugs: priority badge width, recurring event errors, nested subtask UI, comment timestamps, close button
- Widen priority badge from w-10 to w-14 to fit "medium" text, add "none" case
- Guard against null end_datetime in event update validation
- Exclude current event from this_and_future DELETE to prevent 404
- Use Python-side datetime.now for comment timestamps (avoids UTC offset)
- Hide "Add subtask" button when viewing a subtask (prevents nested nesting)
- Add X close button to TaskDetailPanel header on desktop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 11:53:09 +08:00

79 lines
3.4 KiB
TypeScript

import { format } from 'date-fns';
import { CheckSquare, Calendar, Bell, ArrowRight } from 'lucide-react';
import type { UpcomingItem } from '@/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { cn } from '@/lib/utils';
interface UpcomingWidgetProps {
items: UpcomingItem[];
days?: number;
}
const typeConfig: Record<string, { icon: typeof CheckSquare; color: string; label: string }> = {
todo: { icon: CheckSquare, color: 'text-blue-400', label: 'TODO' },
event: { icon: Calendar, color: 'text-purple-400', label: 'EVENT' },
reminder: { icon: Bell, color: 'text-orange-400', label: 'REMINDER' },
};
export default function UpcomingWidget({ items, days = 7 }: UpcomingWidgetProps) {
return (
<Card className="h-full">
<CardHeader>
<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>
<CardContent>
{items.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-8">
Nothing upcoming
</p>
) : (
<ScrollArea className="max-h-[400px] -mr-2 pr-2">
<div className="space-y-0.5">
{items.map((item, index) => {
const config = typeConfig[item.type] || typeConfig.todo;
const Icon = config.icon;
return (
<div
key={`${item.type}-${item.id}-${index}`}
className="flex items-center gap-2 py-1.5 px-2 rounded-md hover:bg-card-elevated transition-colors duration-150"
>
<Icon className={cn('h-3.5 w-3.5 shrink-0', config.color)} />
<span className="text-sm font-medium truncate flex-1 min-w-0">{item.title}</span>
<span className="text-xs text-muted-foreground shrink-0 whitespace-nowrap tabular-nums w-[7rem] text-right">
{item.datetime
? format(new Date(item.datetime), 'MMM d, h:mm a')
: format(new Date(item.date), 'MMM d')}
</span>
<span className={cn('text-[9px] font-semibold uppercase tracking-wider shrink-0 w-14 text-right', config.color)}>
{config.label}
</span>
<span className={cn(
'text-[9px] font-semibold px-1.5 py-0.5 rounded shrink-0 w-14 text-center',
item.priority === 'high' ? 'bg-red-500/10 text-red-400' :
item.priority === 'medium' ? 'bg-yellow-500/10 text-yellow-400' :
item.priority === 'low' ? 'bg-green-500/10 text-green-400' :
item.priority === 'none' ? 'bg-gray-500/10 text-gray-400' :
'invisible'
)}>
{item.priority || ''}
</span>
</div>
);
})}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
);
}