import { useState, useEffect } from "react"; import { createRoute } from "@tanstack/react-router"; import { Route as appRoute } from "../_app"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { api, useApiQuery, useApiMutation } from "@/lib/api"; import { useRealtime } from "@/hooks/use-realtime"; import { Plus, Settings2, Trash2, ListTodo, Flame, FileText, FolderKanban, Calendar, Zap, TrendingUp, Target } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; import { Progress } from "@/components/ui/progress"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; import type { DashboardWidget, Task, Habit, Note, Project, CalendarEvent, PaginatedResponse } from "@/lib/types"; import { format, isToday, isPast, addDays, parseISO } from "date-fns"; const WIDGET_TYPES = [ { id: "tasks_due", label: "Tasks Due Today", icon: ListTodo, defaultW: 2, defaultH: 2 }, { id: "habits_today", label: "Habits Today", icon: Flame, defaultW: 2, defaultH: 2 }, { id: "recent_notes", label: "Recent Notes", icon: FileText, defaultW: 2, defaultH: 2 }, { id: "active_projects", label: "Active Projects", icon: FolderKanban, defaultW: 2, defaultH: 2 }, { id: "upcoming_events", label: "Upcoming Events", icon: Calendar, defaultW: 2, defaultH: 2 }, { id: "streak_counter", label: "Streak Counter", icon: Flame, defaultW: 1, defaultH: 1 }, { id: "quick_capture", label: "Quick Capture", icon: Zap, defaultW: 2, defaultH: 1 }, { id: "productivity_chart", label: "Productivity Chart", icon: TrendingUp, defaultW: 3, defaultH: 2 }, ] as const; function TasksDueWidget() { const { data } = useApiQuery>(["tasks-due"], "/tasks?limit=10&status=todo,in_progress&sort=dueDate"); const tasks = data?.items || []; const today = tasks.filter((t) => t.dueDate && isToday(parseISO(t.dueDate))); const overdue = tasks.filter((t) => t.dueDate && isPast(parseISO(t.dueDate)) && t.status !== "done"); return (
{today.length === 0 && overdue.length === 0 ? (

No tasks due today

) : ( <> {overdue.length > 0 && (

Overdue ({overdue.length})

{overdue.slice(0, 3).map((t) => (
{t.title}
))}
)} {today.length > 0 && (

Today ({today.length})

{today.slice(0, 5).map((t) => (
{t.title}
))}
)} )}
); } function HabitsTodayWidget() { const { data } = useApiQuery>(["habits-today"], "/habits?limit=20"); const habits = data?.items || []; const queryClient = useQueryClient(); const completeMutation = useMutation({ mutationFn: (id: string) => api.post("/habits/" + id + "/complete", {}), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["habits-today"] }), }); return (
{habits.length === 0 ? (

No habits yet

) : ( habits.slice(0, 6).map((h) => (
{h.name} {h.streakCount > 0 && ( {h.streakCount} )}
)) )}
); } function RecentNotesWidget() { const { data } = useApiQuery>(["recent-notes"], "/notes?limit=5&sort=-updated"); const notes = data?.items || []; return (
{notes.length === 0 ? (

No notes yet

) : ( notes.map((n) => (

{n.title}

{format(parseISO(n.updatedAt), "MMM d, HH:mm")}

)) )}
); } function ActiveProjectsWidget() { const { data } = useApiQuery>(["active-projects"], "/projects?limit=10&status=active"); const projects = data?.items || []; return (
{projects.length === 0 ? (

No active projects

) : ( projects.slice(0, 5).map((p) => (
{p.name} {p.progress || 0}%
)) )}
); } function UpcomingEventsWidget() { const { data } = useApiQuery>(["upcoming-events"], "/calendar/events?limit=20"); const events = data?.items || []; const now = new Date(); const weekFromNow = addDays(now, 7); const upcoming = events.filter((e) => { const start = parseISO(e.startTime); return start >= now && start <= weekFromNow; }).sort((a, b) => parseISO(a.startTime).getTime() - parseISO(b.startTime).getTime()); return (
{upcoming.length === 0 ? (

No upcoming events

) : ( upcoming.slice(0, 5).map((e) => (
{e.title} {format(parseISO(e.startTime), "MMM d, HH:mm")}
)) )}
); } function StreakCounterWidget() { const { data } = useApiQuery>(["streaks"], "/habits?limit=50"); const habits = data?.items || []; const bestStreak = Math.max(...habits.map((h) => h.streakCount || 0), 0); const totalActive = habits.filter((h) => h.streakCount > 0).length; return (
{bestStreak} Best streak
{totalActive} Active streaks
); } function QuickCaptureWidget() { const queryClient = useQueryClient(); const [text, setText] = useState(""); const [type, setType] = useState<"task" | "note">("task"); const createTask = useMutation({ mutationFn: (title: string) => api.post("/tasks", { title, status: "todo", priority: "medium" }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["tasks-due"] }); setText(""); }, }); const createNote = useMutation({ mutationFn: (title: string) => api.post("/notes", { title, content: "" }), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["recent-notes"] }); setText(""); }, }); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); e.stopPropagation(); if (!text.trim()) return; if (type === "task") createTask.mutate(text.trim()); else createNote.mutate(text.trim()); }; return (
setText(e.target.value)} className="h-9" />
); } function ProductivityChartWidget() { const { data } = useApiQuery(["productivity-chart"], "/analytics/productivity?range=30"); const stats = data; if (!stats) return

Loading...

; return (

{stats.totalTasks || 0}

Total

{stats.completedTasks || 0}

Done

{stats.taskCompletionRate || 0}%

Rate

Last {stats.period || 30} days

); } function StatsWidget() { const { data } = useApiQuery(["stats"], "/analytics/productivity?range=30"); const stats = data; if (!stats) return

Loading...

; return (

{stats.totalTasks || 0}

Total Tasks

{stats.completedTasks || 0}

Completed

{stats.taskCompletionRate || 0}%

Rate

); } function WidgetRenderer({ type }: { type: string }) { switch (type) { case "tasks_due": return ; case "habits_today": return ; case "recent_notes": return ; case "active_projects": return ; case "upcoming_events": return ; case "streak_counter": return ; case "quick_capture": return ; case "productivity_chart": return ; case "stats": return ; default: return

Unknown widget: {type}

; } } function WidgetCard({ widget, onConfigure, onDelete }: { widget: DashboardWidget; onConfigure: () => void; onDelete: () => void }) { const typeInfo = WIDGET_TYPES.find((t) => t.id === widget.type); const Icon = typeInfo?.icon || Target; return (
{widget.title || typeInfo?.label || widget.type}
); } function AddWidgetDialog({ open, onOpenChange, onAdd }: { open: boolean; onOpenChange: (open: boolean) => void; onAdd: (type: string) => void }) { return ( Add Widget
{WIDGET_TYPES.map((wt) => { const Icon = wt.icon; return ( ); })}
); } function ConfigureWidgetDialog({ widget, open, onOpenChange, onSave }: { widget: DashboardWidget | null; open: boolean; onOpenChange: (open: boolean) => void; onSave: (title: string, w: number, h: number) => void }) { const [title, setTitle] = useState(widget?.title || ""); const [w, setW] = useState(widget?.layout.w || 2); const [h, setH] = useState(widget?.layout.h || 2); useEffect(() => { if (widget) { setTitle(widget.title || ""); setW(widget.layout.w || 2); setH(widget.layout.h || 2); } }, [widget]); const handleSave = () => { onSave(title, w, h); onOpenChange(false); }; return ( Configure Widget
setTitle(e.target.value)} placeholder="Widget title" />
); } function DashboardPage() { const queryClient = useQueryClient(); const [addOpen, setAddOpen] = useState(false); const [configOpen, setConfigOpen] = useState(false); const [configWidget, setConfigWidget] = useState(null); useRealtime({ enabled: true }); const { data: widgetsData, isLoading } = useApiQuery<{ items: DashboardWidget[]; totalItems: number }>(["dashboard-widgets"], "/dashboard/widgets"); const widgets = widgetsData?.items || []; const createMutation = useMutation({ mutationFn: (data: any) => api.post("/dashboard/widgets", data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["dashboard-widgets"] }), }); const updateMutation = useMutation({ mutationFn: ({ id, data }: { id: string; data: any }) => api.patch("/dashboard/widgets/" + id, data), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["dashboard-widgets"] }), }); const deleteMutation = useMutation({ mutationFn: (id: string) => api.delete("/dashboard/widgets/" + id), onSuccess: () => queryClient.invalidateQueries({ queryKey: ["dashboard-widgets"] }), }); const handleAddWidget = (type: string) => { const typeInfo = WIDGET_TYPES.find((t) => t.id === type); createMutation.mutate({ type, title: typeInfo?.label || type, layout: { x: 0, y: widgets.length, w: typeInfo?.defaultW || 2, h: typeInfo?.defaultH || 2 } }); }; const handleConfigure = (widget: DashboardWidget) => { setConfigWidget(widget); setConfigOpen(true); }; const handleSaveConfig = (title: string, w: number, h: number) => { if (!configWidget) return; updateMutation.mutate({ id: configWidget.id, data: { title: title || null, layout: { ...configWidget.layout, w, h } } }); }; const handleDelete = (id: string) => { deleteMutation.mutate(id); }; return (

Dashboard

{isLoading ? (
Loading dashboard...
) : widgets.length === 0 ? (

Your dashboard is empty. Add some widgets to get started!

) : (
{widgets.map((w) => ( handleConfigure(w)} onDelete={() => handleDelete(w.id)} /> ))}
)}
); } export const Route = createRoute({ getParentRoute: () => appRoute, path: "/", component: DashboardPage, });