"use client"; import { useState, useEffect } from "react"; import type { Service, ServiceUrl } from "@/lib/api/schema"; import { Card } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSeparator } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { MoreVertical, ExternalLink, Pencil, Trash2, Globe, Home, Settings } from "lucide-react"; import { cn } from "@/lib/utils"; function getInitials(name: string) { const words = name.trim().split(/\s+/); if (words.length >= 2) return (words[0][0] + words[1][0]).toUpperCase(); return name.slice(0, 2).toUpperCase(); } function extractHost(url: string) { try { return new URL(url).hostname; } catch { return url; } } function getIconUrl(service: Service) { if (service.iconUrl) return service.iconUrl; if (service.iconAssetId) return `/uploads/icons/${service.iconAssetId}`; return null; } function kindIcon(kind: string) { switch (kind) { case "local": return ; case "external": return ; default: return ; } } function kindBadgeClass(kind: string) { switch (kind) { case "local": return "badge-local"; case "external": return "badge-external"; default: return "badge-custom"; } } function useServicePing(url: string | undefined) { const [status, setStatus] = useState<"up" | "down" | "unknown">("unknown"); useEffect(() => { if (!url) return; let cancelled = false; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), 5000); fetch(url, { method: "HEAD", mode: "no-cors", signal: controller.signal }) .then(() => { if (!cancelled) setStatus("up"); }) .catch(() => { if (!cancelled) setStatus("down"); }) .finally(() => clearTimeout(timer)); return () => { cancelled = true; controller.abort(); }; }, [url]); return status; } function StatusDot({ status }: { status: "up" | "down" | "unknown" }) { if (status === "unknown") return null; return ( ); } function UrlPickerDialog({ urls, open, onOpenChange, }: { urls: ServiceUrl[]; open: boolean; onOpenChange: (open: boolean) => void; }) { return ( Open App Choose which URL to open ); } export function ServiceCard({ service, onEdit, onDelete, }: { service: Service; onEdit: (s: Service) => void; onDelete: (id: string) => void; }) { const [pickerOpen, setPickerOpen] = useState(false); const handleClick = () => { if (service.urls.length === 1) { window.open(service.urls[0].url, "_blank", "noopener,noreferrer"); } else { setPickerOpen(true); } }; const iconSrc = getIconUrl(service); const primaryUrl = service.urls.find((u) => u.isPrimary) || service.urls[0]; const status = useServicePing(primaryUrl?.url); return ( <> {/* Accent line at top */}
{/* Icon container */}
{iconSrc ? ( {service.name} { (e.target as HTMLImageElement).style.display = "none"; (e.target as HTMLImageElement).nextElementSibling?.classList.remove("hidden"); }} /> ) : null}
{getInitials(service.name)}
{/* App name */} {service.name} {/* URL indicator */} {primaryUrl && ( {extractHost(primaryUrl.url)} )} {/* URL kind badges */} {service.urls.length > 1 && (
{service.urls.slice(0, 3).map((u) => ( {u.kind} ))}
)}
{/* Actions */}
e.stopPropagation()} > onEdit(service)} className="gap-2 text-xs"> Edit onDelete(service.id)}> Delete
{service.urls.length > 1 && ( )} ); }