- 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>
79 lines
3.4 KiB
TypeScript
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>
|
|
);
|
|
}
|