"use client"; import type { WidgetInstance, WidgetData } from "@/lib/api/schema"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { MoreVertical, RefreshCw, Pencil, Trash2, GripVertical, Clock, Shield, ImageIcon, StickyNote, Camera, Activity } from "lucide-react"; import { useWidgetData, useRefreshWidget } from "@/lib/api/hooks"; import { cn } from "@/lib/utils"; const widgetTypeIcons: Record = { clock: , pihole: , image: , memos: , immich: , }; export function WidgetCard({ widget, onEdit, onDelete, dragHandleProps, }: { widget: WidgetInstance; onEdit: (w: WidgetInstance) => void; onDelete: (id: string) => void; dragHandleProps?: React.HTMLAttributes; }) { const { data, isLoading, error } = useWidgetData(widget.id); const refreshMut = useRefreshWidget(); const handleRefresh = () => refreshMut.mutate(widget.id); const statusLabel = data?.status === "stale" ? "stale" : data?.status === "error" ? "error" : ""; const typeIcon = widgetTypeIcons[widget.type] || ; return (
{dragHandleProps && (
)}
{typeIcon}
{widget.title} {statusLabel && ( {statusLabel} )}
onEdit(widget)} className="gap-2 text-xs"> Edit onDelete(widget.id)}> Delete
{isLoading ? ( [LOADING...] ) : error || data?.status === "error" ? ( [ERROR: {data?.error || "Failed to load"}] ) : ( )} ); } function WidgetContent({ widget, data }: { widget: WidgetInstance; data?: WidgetData }) { switch (widget.type) { case "clock": return ; case "image": return ; case "pihole": return ; case "memos": return ; case "immich": return ; default: return Unknown widget type; } } function ClockContent({ config }: { config: Record; data?: WidgetData }) { const timezones = (config.timezones as string[]) || []; const now = new Date(); const localTime = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" }); const localDate = now.toLocaleDateString([], { weekday: "long", month: "long", day: "numeric" }); return (
{localTime}
{localDate}
{timezones.length > 0 && (
{timezones.map((tz) => { try { const t = new Date().toLocaleTimeString([], { timeZone: tz, hour: "2-digit", minute: "2-digit" }); return (
{tz.split("/").pop()?.replace("_", " ")} {t}
); } catch { return null; } })}
)}
); } function ImageContent({ config }: { config: Record }) { const imageUrl = config.imageUrl as string; const linkUrl = config.linkUrl as string | null; const img = ( Widget image { (e.target as HTMLImageElement).style.display = "none"; }} /> ); if (linkUrl) { return {img}; } return img; } function PiHoleContent({ data }: { data?: WidgetData }) { const d = data?.data as Record | undefined; if (!d) return No data; return (
Status
{String(d.status || "unknown")}
Blocked
{String(d.ads_blocked_today || "0")}
Queries
{String(d.dns_queries_today || "0")}
% Blocked
{String(d.ads_percentage_today || "0")}%
); } function MemosContent({ data }: { data?: WidgetData }) { const d = data?.data as Record | undefined; const memos = (d?.memos as Array>) || []; if (memos.length === 0) return No memos; return (
{memos.slice(0, 5).map((m, i) => (
{String(m.content || m.snippet || "")}
))}
); } function ImmichContent({ data }: { data?: WidgetData }) { const d = data?.data as Record | undefined; if (!d) return No data; return (
Photos
{String(d.photos || "0")}
Videos
{String(d.videos || "0")}
); }