Task 5.25: Dashboard-System — GET /api/v1/dashboard/widgets, DashboardWidgetLoader, DashboardGrid with drag-and-drop, 3 example widgets (RecentContacts, TasksSummary, CalendarUpcoming), updated Dashboard.tsx, i18n, 6 tests
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* CalendarUpcomingWidget — shows next 3 upcoming events (Task 5.25).
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { listEntries, type CalendarEntry } from '@/api/calendar';
|
||||
import { formatDateTime } from '@/utils/date';
|
||||
import { Calendar, MapPin } from 'lucide-react';
|
||||
|
||||
export function CalendarUpcomingWidget() {
|
||||
const { t } = useTranslation();
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ['calendarEntries', 'upcoming'],
|
||||
queryFn: () => listEntries(),
|
||||
staleTime: 60 * 1000,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="animate-pulse space-y-2" data-testid="calendar-upcoming-loading">{[...Array(2)].map((_, i) => <div key={i} className="h-4 bg-secondary-100 rounded" />)}</div>;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <p className="text-sm text-secondary-500" data-testid="calendar-upcoming-error">{t('dashboard.widgetError')}</p>;
|
||||
}
|
||||
|
||||
const entries: CalendarEntry[] = data ?? [];
|
||||
const now = new Date();
|
||||
const upcoming = entries
|
||||
.filter((e: CalendarEntry) => new Date(e.start_at || e.due_date || '') >= now)
|
||||
.slice(0, 3);
|
||||
|
||||
if (upcoming.length === 0) {
|
||||
return <p className="text-sm text-secondary-500" data-testid="calendar-upcoming-empty">{t('dashboard.noUpcomingEvents')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3" data-testid="calendar-upcoming-widget">
|
||||
{upcoming.map((entry: CalendarEntry) => (
|
||||
<div key={entry.id} className="flex items-start gap-2 text-sm">
|
||||
<Calendar className="w-3.5 h-3.5 text-primary-500 mt-0.5" />
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-secondary-800">{entry.title || '—'}</div>
|
||||
<div className="text-secondary-500 text-xs">
|
||||
{formatDateTime(entry.start_at || entry.due_date) || ''}
|
||||
</div>
|
||||
{entry.location && (
|
||||
<div className="flex items-center gap-1 text-secondary-400 text-xs mt-0.5">
|
||||
<MapPin className="w-3 h-3" />
|
||||
{entry.location}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user