// Federated Dashboard — React + Tailwind + Font Awesome 6 Free (Canvas Preview JS) // Adds custom drag preview that follows the cursor with a blue (#0EA5E9 @ 50%) shadow. // Features: theme toggle, favorites, search (headings hidden while typing), drag & drop reordering, // custom floating preview, status pill, 4-col grid, centered Federated logo. import React, { useMemo, useState, useCallback, useEffect } from "react"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faStar as faStarRegular } from "@fortawesome/free-regular-svg-icons"; import { faSun, faMoon, faArrowUpRightFromSquare, faMagnifyingGlass, faStar as faStarSolid, faXmark, } from "@fortawesome/free-solid-svg-icons"; // ------------------------------------ // Brand Tokens // ------------------------------------ const BRAND_TOKENS = { primary: "#0F172A", // dark theme background accent: "#D67147", // favorites + focus ring + section label color surface: "#1E293B", }; const LIGHT_TOKENS = { primary: "#4D6F83", // requested light background surface: "#FFFFFF", // white logo tile in light }; // ------------------------------------ // Data // ------------------------------------ // Local asset files for app logos (place in public/assets/) const ASSETS = { panel_black: "/assets/panelblack.png", panel_white: "/assets/panelwhite.png", billing_black: "/assets/Billingblack.png", billing_white: "/assets/Billingwhite.png", jitsi: "/assets/jitsi.svg", element: "/assets/element.png", nextcloud: "/assets/nextcloud.svg", espocrm: "/assets/espocrm.svg", plane: "/assets/plane.svg", freescout: "/assets/freescout.png", roundcube: "/assets/roundcube.svg", bookstack: "/assets/bookstack.svg", baserow: "/assets/baserow.svg", gitea: "/assets/gitea.svg", powerdns: "/assets/powerdns.svg", wordpress: "/assets/Wordpress.png", vaultwarden: "/assets/Vaultwarden.svg", calcom_black: "/assets/calcomblack.svg", calcom_white: "/assets/calcomwhite.svg", federated_logo_light: "/assets/federated.png", federated_logo_dark: "/assets/federated.png", }; const APP_CATALOG = [ { id: "nextcloud", name: "Nextcloud", category: "Email, Files, Documents", panelManaged: true, logoUrl: ASSETS.nextcloud }, { id: "jitsi", name: "Jitsi (Meet)", category: "Video Chat", panelManaged: true, logoUrl: ASSETS.jitsi }, { id: "element", name: "Element", category: "Team Chat", panelManaged: true, logoUrl: ASSETS.element }, { id: "espocrm", name: "EspoCRM", category: "Customer Relationship Manager", panelManaged: true, logoUrl: ASSETS.espocrm }, { id: "plane", name: "Plane", category: "Project Management", panelManaged: false, logoUrl: ASSETS.plane }, { id: "freescout", name: "FreeScout", category: "Customer Help Desk", panelManaged: false, logoUrl: ASSETS.freescout }, { id: "vaultwarden", name: "Vaultwarden", category: "Passwords", panelManaged: false, logoUrl: ASSETS.vaultwarden }, { id: "roundcube", name: "Roundcube", category: "Web Mail", panelManaged: true, logoUrl: ASSETS.roundcube }, { id: "bookstack", name: "BookStack", category: "Wiki Knowledgebase", panelManaged: true, logoUrl: ASSETS.bookstack }, { id: "baserow", name: "Baserow", category: "Visual Databases", panelManaged: true, logoUrl: ASSETS.baserow }, { id: "gitea", name: "Gitea", category: "GIT Source Control", panelManaged: true, logoUrl: ASSETS.gitea }, { id: "wordpress", name: "WordPress", category: "Your Website", panelManaged: true, logoUrl: ASSETS.wordpress }, { id: "powerdns", name: "PowerDNS", category: "DNS Management", panelManaged: true, logoUrl: ASSETS.powerdns }, { id: "calcom", name: "Cal.com", category: "Scheduling", panelManaged: true, logoUrl: ASSETS.calcom_black }, { id: "panel", name: "Federated Panel", category: "Create & Manage Users", panelManaged: true, logoUrl: ASSETS.panel_black }, { id: "billing", name: "Billing", category: "Manage your subscription", panelManaged: false, logoUrl: ASSETS.billing_black }, ]; // ------------------------------------ // Hooks // ------------------------------------ function useDebouncedValue(value, delay = 150) { const [debounced, setDebounced] = useState(value); useEffect(() => { const t = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(t); }, [value, delay]); return debounced; } // ------------------------------------ // UI bits // ------------------------------------ const Pill = React.memo(function Pill({ managed }) { const pillBg = managed ? "bg-emerald-600 text-white ring-1 ring-emerald-300/30" : "bg-red-600 text-white ring-1 ring-red-300/30"; const label = managed ? "In Panel" : "In App"; return ( {label} ); }); const AppLogo = React.memo(function AppLogo({ app, theme }) { const bg = theme === "light" ? "bg-white" : "bg-black/20"; const themedSrc = app.id === "panel" ? (theme === "light" ? ASSETS.panel_black : ASSETS.panel_white) : app.id === "billing" ? (theme === "light" ? ASSETS.billing_black : ASSETS.billing_white) : app.id === "calcom" ? (theme === "light" ? ASSETS.calcom_black : ASSETS.calcom_white) : app.logoUrl; return (
{themedSrc ? ( {`${app.name} ) : null}
); }); // Drag-and-drop capable card const AppCard = React.memo(function AppCard({ app, favorite = false, theme = "light", onToggleFavorite, dnd, }) { const draggable = !!(dnd && dnd.enabled); const isDragging = draggable && dnd && dnd.draggingId === app.id; return (
{/* Brand link (top-left) */}
{app.brandUrl && ( i )}
{/* Status pill (bottom-right) */}
{/* Favorite toggle */}
{/* Title and purpose ABOVE the logo */}
{app.category}

{app.name}

{/* Logo area */}
); }); // ------------------------------------ // Page // ------------------------------------ export default function FederatedDashboard() { const [theme, setTheme] = useState("light"); const [query, setQuery] = useState(""); // Default favorite: Panel only const [favorites, setFavorites] = useState(new Set(["panel"])); // Orders for within-section reordering const initialFav = new Set(["panel"]); const [favoritesOrder, setFavoritesOrder] = useState(APP_CATALOG.map(a => a.id).filter(id => initialFav.has(id))); const [othersOrder, setOthersOrder] = useState(APP_CATALOG.map(a => a.id).filter(id => !initialFav.has(id))); // Drag state const [dragging, setDragging] = useState(null); // { id, from } const [dragPos, setDragPos] = useState(null); // { x, y } const [dragMeta, setDragMeta] = useState(null); // { offsetX, offsetY, width, height, app } // CSS vars const rootStyle = useMemo(() => ({ "--brand-primary": theme === "light" ? LIGHT_TOKENS.primary : BRAND_TOKENS.primary, "--brand-accent": BRAND_TOKENS.accent, "--brand-surface": theme === "light" ? LIGHT_TOKENS.surface : BRAND_TOKENS.surface, }), [theme]); // Index for search const APP_INDEX = useMemo( () => APP_CATALOG.map((a) => ({ ...a, _hay: `${a.name} ${a.category} ${a.id}`.toLowerCase() })), [] ); const APP_MAP = useMemo(() => { const m = new Map(); APP_INDEX.forEach(a => m.set(a.id, a)); return m; }, [APP_INDEX]); const debouncedQuery = useDebouncedValue(query, 150); const tokens = useMemo( () => debouncedQuery.toLowerCase().trim().split(/\s+/).filter(Boolean), [debouncedQuery] ); const isSearching = tokens.length > 0; const matches = useCallback((a) => { if (tokens.length === 0) return true; const h = a._hay; for (const t of tokens) if (!h.includes(t)) return false; return true; }, [tokens]); // Lists in display order, then filtered const favAll = useMemo(() => favoritesOrder.map(id => APP_MAP.get(id)).filter(Boolean), [favoritesOrder, APP_MAP]); const otherAll = useMemo(() => othersOrder.map(id => APP_MAP.get(id)).filter(Boolean), [othersOrder, APP_MAP]); const favList = useMemo(() => favAll.filter(matches), [favAll, matches]); const otherList = useMemo(() => otherAll.filter(matches), [otherAll, matches]); // Favorite toggle + sync orders const toggleFavorite = useCallback((id) => { setFavorites(prev => { const next = new Set(prev); const wasFav = next.has(id); if (wasFav) next.delete(id); else next.add(id); setFavoritesOrder(o => (wasFav ? o.filter(x => x !== id) : o.includes(id) ? o : [...o, id])); setOthersOrder(o => (wasFav ? (o.includes(id) ? o : [...o, id]) : o.filter(x => x !== id))); return next; }); }, []); // DnD handlers (within-section only) + floating preview tracking const onDragStart = useCallback((id, list) => (e) => { setDragging({ id, from: list }); const rect = e.currentTarget.getBoundingClientRect(); const offsetX = e.clientX - rect.left; const offsetY = e.clientY - rect.top; setDragMeta({ offsetX, offsetY, width: rect.width, height: rect.height, app: APP_MAP.get(id) }); setDragPos({ x: e.clientX, y: e.clientY }); e.dataTransfer.effectAllowed = "move"; // hide the native ghost image so only our custom preview shows const img = new Image(); img.src = "data:image/svg+xml;base64," + btoa(''); e.dataTransfer.setDragImage(img, 0, 0); }, [APP_MAP]); const onDragOver = useCallback((overId, list) => (e) => { if (!dragging || dragging.from !== list) return; e.preventDefault(); e.dataTransfer.dropEffect = "move"; }, [dragging]); // keep preview following cursor even when not over a specific card useEffect(() => { if (!dragging) return; const handle = (e) => { setDragPos({ x: e.clientX, y: e.clientY }); }; window.addEventListener("dragover", handle); return () => window.removeEventListener("dragover", handle); }, [dragging]); const onDrag = useCallback((e) => { if (!dragging) return; setDragPos({ x: e.clientX, y: e.clientY }); }, [dragging]); const onDrop = useCallback((overId, list) => (e) => { e.preventDefault(); if (!dragging || dragging.from !== list) return; if (dragging.id === overId) { setDragging(null); setDragMeta(null); setDragPos(null); return; } const arr = list === "fav" ? favoritesOrder.slice() : othersOrder.slice(); const fromIdx = arr.indexOf(dragging.id); const toIdx = arr.indexOf(overId); if (fromIdx < 0 || toIdx < 0) { setDragging(null); setDragMeta(null); setDragPos(null); return; } arr.splice(fromIdx, 1); arr.splice(toIdx, 0, dragging.id); if (list === "fav") setFavoritesOrder(arr); else setOthersOrder(arr); setDragging(null); setDragMeta(null); setDragPos(null); }, [dragging, favoritesOrder, othersOrder]); const onDropToEnd = useCallback((list) => (e) => { e.preventDefault(); if (!dragging || dragging.from !== list) return; const arr = list === "fav" ? favoritesOrder.slice() : othersOrder.slice(); const fromIdx = arr.indexOf(dragging.id); if (fromIdx < 0) { setDragging(null); setDragMeta(null); setDragPos(null); return; } arr.splice(fromIdx, 1); arr.push(dragging.id); if (list === "fav") setFavoritesOrder(arr); else setOthersOrder(arr); setDragging(null); setDragMeta(null); setDragPos(null); }, [dragging, favoritesOrder, othersOrder]); const onDragEnd = useCallback(() => { setDragging(null); setDragMeta(null); setDragPos(null); }, []); const clearSearch = useCallback(() => setQuery(""), []); return (
{/* Header */}
Federated Computer

Dashboard

{/* Search */}
setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") clearSearch(); }} placeholder="Search apps…" className="w-full rounded-xl border border-white/10 bg-white/5 py-3 pl-10 pr-10 outline-none placeholder:text-white/50 focus:ring-2 focus:ring-[var(--brand-accent)]" aria-label="Search apps" /> {query && ( )}
{isSearching ? (
{[...favList, ...otherList].map((app) => ( ))} {[...favList, ...otherList].length === 0 && (
No apps found for “{debouncedQuery}”.
)}
) : ( <> {/* Favorites */}
dragging && dragging.from === "fav" && e.preventDefault()} onDrop={onDropToEnd("fav")}>

Favorites

{favList.map((app) => ( ))} {favList.length === 0 && (
Click the star on any app to favorite it.
)}
{/* Your Apps (non-favorites) */}
dragging && dragging.from === "other" && e.preventDefault()} onDrop={onDropToEnd("other") }>

Your Apps

{otherList.map((app) => ( ))}
)} {/* Floating drag preview */} {dragging && dragMeta && dragPos && (
{dragMeta.app.category}

{dragMeta.app.name}

)} {/* Footer */}
); } // NOTE: Dev-only sanity checks were removed to maximize Canvas compatibility.