"use client"; import { use, useState, useEffect } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { toast } from "sonner"; import { CheckCircle2, ExternalLink, Loader2, Package } from "lucide-react"; import { cn } from "@/lib/utils"; // --------------------------------------------------------------------------- // Design package catalogue // --------------------------------------------------------------------------- interface DesignPackage { id: string; name: string; description: string; bestFor: string[]; url: string; tags: string[]; } const PACKAGES: DesignPackage[] = [ { id: "shadcn", name: "shadcn/ui", description: "Copy-paste components built on Radix UI primitives. Tailwind-styled, fully customisable — you own the code.", bestFor: ["App", "Admin", "Dashboard"], url: "https://ui.shadcn.com", tags: ["Tailwind", "Radix", "Headless"], }, { id: "daisy-ui", name: "DaisyUI", description: "Tailwind plugin that adds semantic class names (btn, badge, card). Fastest path from idea to styled UI.", bestFor: ["Website", "Prototype"], url: "https://daisyui.com", tags: ["Tailwind", "Plugin", "Themes"], }, { id: "hero-ui", name: "HeroUI", description: "Beautiful, accessible React components with smooth animations and dark mode built-in. Formerly NextUI.", bestFor: ["App", "Website", "Landing"], url: "https://heroui.com", tags: ["React", "Tailwind", "Animations"], }, { id: "mantine", name: "Mantine", description: "100+ fully-featured React components with hooks, forms, and charts. Best for data-heavy admin tools.", bestFor: ["Admin", "Dashboard", "Complex Apps"], url: "https://mantine.dev", tags: ["React", "CSS-in-JS", "Hooks"], }, { id: "headless-ui", name: "Headless UI", description: "Completely unstyled, accessible components by Tailwind Labs. Full creative control with Tailwind classes.", bestFor: ["Custom Design", "Brand-specific UI"], url: "https://headlessui.com", tags: ["Tailwind", "Unstyled", "Accessible"], }, { id: "tailwind-only", name: "Tailwind only", description: "No component library — pure Tailwind CSS with your own components. Maximum flexibility, zero opinions.", bestFor: ["Custom", "Marketing Site"], url: "https://tailwindcss.com", tags: ["Tailwind", "Custom", "Minimal"], }, ]; // --------------------------------------------------------------------------- // Package card // --------------------------------------------------------------------------- function PackageCard({ pkg, selected, saving, onSelect, }: { pkg: DesignPackage; selected: boolean; saving: boolean; onSelect: () => void; }) { return ( ); } // --------------------------------------------------------------------------- // App section // --------------------------------------------------------------------------- function AppSection({ appName, selectedPackageId, onSelect, }: { appName: string; selectedPackageId: string | undefined; onSelect: (appName: string, pkgId: string) => Promise; }) { const [saving, setSaving] = useState(null); const handleSelect = async (pkgId: string) => { if (pkgId === selectedPackageId) return; setSaving(pkgId); await onSelect(appName, pkgId); setSaving(null); }; const selectedPkg = PACKAGES.find((p) => p.id === selectedPackageId); return (

{appName}

{selectedPkg && (

Using {selectedPkg.name}

)}
{selectedPkg && ( e.stopPropagation()} > Docs )}
{PACKAGES.map((pkg) => ( handleSelect(pkg.id)} /> ))}
); } // --------------------------------------------------------------------------- // Page // --------------------------------------------------------------------------- export default function DesignPage({ params, }: { params: Promise<{ workspace: string; projectId: string }>; }) { const { projectId } = use(params); const [apps, setApps] = useState<{ name: string; path: string }[]>([]); const [designPackages, setDesignPackages] = useState>({}); const [giteaRepo, setGiteaRepo] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(`/api/projects/${projectId}/apps`) .then((r) => r.json()) .then((d) => { setApps(d.apps ?? []); setDesignPackages(d.designPackages ?? {}); setGiteaRepo(d.giteaRepo ?? null); }) .catch(() => toast.error("Failed to load apps")) .finally(() => setLoading(false)); }, [projectId]); const handleSelect = async (appName: string, packageId: string) => { try { const res = await fetch(`/api/projects/${projectId}/apps`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ appName, packageId }), }); if (!res.ok) throw new Error("Save failed"); setDesignPackages((prev) => ({ ...prev, [appName]: packageId })); const pkg = PACKAGES.find((p) => p.id === packageId); toast.success(`${appName} → ${pkg?.name ?? packageId}`); } catch { toast.error("Failed to save selection"); } }; if (loading) { return (
); } return (
{/* Header */}

Design packages

Each app in your Turborepo can use a different UI library — they never conflict.

{giteaRepo && ( )}
{/* App sections */} {apps.length === 0 ? (

No apps found

Push a Turborepo scaffold to your Gitea repo — apps in the{" "} apps/ directory will appear here.

) : (
{apps.map((app) => (
))}
)}
); }