Refactor root admin dashboard UI
This commit is contained in:
@@ -1,27 +1,23 @@
|
|||||||
import { ArrowLeft, LogOut, Power } from "lucide-react";
|
import { Power } from "lucide-react";
|
||||||
import Link from "next/link";
|
|
||||||
import { revalidatePath } from "next/cache";
|
import { revalidatePath } from "next/cache";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { AppHeader } from "@/components/layout/app-header";
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { AppShell } from "@/components/layout/app-shell";
|
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
||||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
|
||||||
import { routing } from "@/i18n/routing";
|
import { routing } from "@/i18n/routing";
|
||||||
import { getLocalizedPath } from "@/lib/locale";
|
import { getLocalizedPath } from "@/lib/locale";
|
||||||
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
import { getMaintenanceMode, setMaintenanceMode } from "@/lib/app-config";
|
import { getMaintenanceMode, setMaintenanceMode } from "@/lib/app-config";
|
||||||
import { getRootNavigation } from "@/lib/root-navigation";
|
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
import { AppCard } from "@/components/ui/app-card";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { CardContent } from "@/components/ui/card";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const copy = {
|
const copy = {
|
||||||
title: "Wartungsmodus",
|
title: "Wartungsmodus",
|
||||||
subtitle: "Steuerung fuer den globalen Maintenance Status.",
|
subtitle: "Steuerung fuer den globalen Wartungsstatus.",
|
||||||
overview: "Uebersicht",
|
overview: "Uebersicht",
|
||||||
maintenance: "Wartungsmodus",
|
maintenance: "Wartungsmodus",
|
||||||
uiKit: "UI Kit",
|
uiKit: "UI Kit",
|
||||||
@@ -75,54 +71,23 @@ export default async function RootMaintenancePage() {
|
|||||||
redirect("/root/maintenance");
|
redirect("/root/maintenance");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarItems = getRootNavigation(copy, "maintenance");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<RootDashboardShell
|
||||||
sidebar={
|
copy={copy}
|
||||||
<AppSidebar
|
active="maintenance"
|
||||||
title={copy.title}
|
logoutAction={logoutAction}
|
||||||
description={copy.subtitle}
|
headerTitle={copy.title}
|
||||||
items={sidebarItems}
|
headerDescription={copy.subtitle}
|
||||||
footer={
|
headerActions={
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ThemeToggle ariaLabel="Theme wechseln" />
|
|
||||||
</div>
|
|
||||||
<Button asChild variant="outline" className="w-full justify-start">
|
|
||||||
<Link href={getLocalizedPath("de")}>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
{copy.backToSite}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<form action={logoutAction}>
|
|
||||||
<Button type="submit" variant="destructive" className="w-full justify-start">
|
|
||||||
<LogOut className="h-4 w-4" />
|
|
||||||
{copy.logout}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
header={
|
|
||||||
<AppHeader
|
|
||||||
title={copy.title}
|
|
||||||
description={copy.subtitle}
|
|
||||||
actions={
|
|
||||||
<Badge variant={maintenanceEnabled ? "warning" : "success"}>
|
<Badge variant={maintenanceEnabled ? "warning" : "success"}>
|
||||||
{maintenanceEnabled ? copy.maintenanceOn : copy.maintenanceOff}
|
{maintenanceEnabled ? copy.maintenanceOn : copy.maintenanceOff}
|
||||||
</Badge>
|
</Badge>
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
|
<MotionFade delay={0.1}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader>
|
<CardContent className="space-y-4 p-6">
|
||||||
<CardTitle className="text-xl">{copy.title}</CardTitle>
|
<p className="text-sm text-muted-foreground">{copy.maintenanceText}</p>
|
||||||
<CardDescription>{copy.maintenanceText}</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<form action={updateMaintenanceMode}>
|
<form action={updateMaintenanceMode}>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -136,6 +101,7 @@ export default async function RootMaintenancePage() {
|
|||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
</AppShell>
|
</MotionFade>
|
||||||
|
</RootDashboardShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,13 +47,13 @@ export async function createMediaAssetAction(formData: FormData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
revalidateMediaPages();
|
revalidateMediaPages();
|
||||||
redirect(withMessage("/root/media", "success", "Media asset created."));
|
redirect(withMessage("/root/media", "success", "Datei gespeichert."));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRedirectError(error)) {
|
if (isRedirectError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = error instanceof Error ? error.message : "Unable to create media asset.";
|
const message = error instanceof Error ? error.message : "Datei konnte nicht gespeichert werden.";
|
||||||
redirect(withMessage("/root/media", "error", message));
|
redirect(withMessage("/root/media", "error", message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,13 +67,13 @@ export async function deleteMediaAssetAction(formData: FormData) {
|
|||||||
const asset = await getMediaAssetById(assetId);
|
const asset = await getMediaAssetById(assetId);
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
redirect(withMessage("/root/media", "error", "Media asset not found."));
|
redirect(withMessage("/root/media", "error", "Datei nicht gefunden."));
|
||||||
}
|
}
|
||||||
|
|
||||||
const usageCount = await countMediaUsageReferences(asset.id);
|
const usageCount = await countMediaUsageReferences(asset.id);
|
||||||
|
|
||||||
if (usageCount > 0) {
|
if (usageCount > 0) {
|
||||||
redirect(withMessage("/root/media", "error", "Media asset is still in use."));
|
redirect(withMessage("/root/media", "error", "Datei wird noch verwendet."));
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.mediaAsset.delete({
|
await prisma.mediaAsset.delete({
|
||||||
@@ -90,13 +90,13 @@ export async function deleteMediaAssetAction(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
revalidateMediaPages();
|
revalidateMediaPages();
|
||||||
redirect(withMessage("/root/media", "success", "Media asset deleted."));
|
redirect(withMessage("/root/media", "success", "Datei geloescht."));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRedirectError(error)) {
|
if (isRedirectError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = error instanceof Error ? error.message : "Unable to delete media asset.";
|
const message = error instanceof Error ? error.message : "Datei konnte nicht geloescht werden.";
|
||||||
redirect(withMessage("/root/media", "error", message));
|
redirect(withMessage("/root/media", "error", message));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-13
@@ -4,6 +4,7 @@ import { ExternalLink, ImageIcon, Trash2 } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
import { AppCard } from "@/components/ui/app-card";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -27,6 +28,17 @@ const copy = {
|
|||||||
portfolio: "Portfolio",
|
portfolio: "Portfolio",
|
||||||
logout: "Ausloggen",
|
logout: "Ausloggen",
|
||||||
backToSite: "Zur Website",
|
backToSite: "Zur Website",
|
||||||
|
createTitle: "Neue Datei",
|
||||||
|
createDescription: "Datei hochladen oder externe URL zentral fuer spaetere Wiederverwendung speichern.",
|
||||||
|
label: "Bezeichnung",
|
||||||
|
kind: "Typ",
|
||||||
|
uploadFile: "Datei hochladen",
|
||||||
|
externalUrl: "Externe URL",
|
||||||
|
saveMedia: "Datei speichern",
|
||||||
|
open: "Oeffnen",
|
||||||
|
delete: "Loeschen",
|
||||||
|
usages: "Verwendungen",
|
||||||
|
noAssets: "Noch keine Dateien vorhanden.",
|
||||||
};
|
};
|
||||||
|
|
||||||
type RootMediaPageProps = {
|
type RootMediaPageProps = {
|
||||||
@@ -60,31 +72,36 @@ export default async function RootMediaPage({ searchParams }: RootMediaPageProps
|
|||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{searchParams?.success ? (
|
{searchParams?.success ? (
|
||||||
|
<MotionFade delay={0.1}>
|
||||||
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
|
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
|
||||||
{searchParams.success}
|
{searchParams.success}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{searchParams?.error ? (
|
{searchParams?.error ? (
|
||||||
|
<MotionFade delay={0.12}>
|
||||||
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
{searchParams.error}
|
{searchParams.error}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<MotionFade delay={0.15}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>New Media Asset</CardTitle>
|
<CardTitle>{copy.createTitle}</CardTitle>
|
||||||
<CardDescription>Upload a file or store an external URL for reuse across the site.</CardDescription>
|
<CardDescription>{copy.createDescription}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form action={createMediaAssetAction} className="grid gap-4 md:grid-cols-2">
|
<form action={createMediaAssetAction} className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="label">Label</Label>
|
<Label htmlFor="label">{copy.label}</Label>
|
||||||
<Input id="label" name="label" required />
|
<Input id="label" name="label" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="kind">Kind</Label>
|
<Label htmlFor="kind">{copy.kind}</Label>
|
||||||
<select
|
<select
|
||||||
id="kind"
|
id="kind"
|
||||||
name="kind"
|
name="kind"
|
||||||
@@ -97,34 +114,36 @@ export default async function RootMediaPage({ searchParams }: RootMediaPageProps
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-2 md:col-span-2">
|
||||||
<Label htmlFor="file">Upload File</Label>
|
<Label htmlFor="file">{copy.uploadFile}</Label>
|
||||||
<Input id="file" name="file" type="file" accept="image/*,.svg,.pdf" />
|
<Input id="file" name="file" type="file" accept="image/*,.svg,.pdf" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-2 md:col-span-2">
|
||||||
<Label htmlFor="externalUrl">External URL</Label>
|
<Label htmlFor="externalUrl">{copy.externalUrl}</Label>
|
||||||
<Input id="externalUrl" name="externalUrl" placeholder="https://example.com/image.jpg" />
|
<Input id="externalUrl" name="externalUrl" placeholder="https://example.com/image.jpg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
<ImageIcon className="h-4 w-4" />
|
<ImageIcon className="h-4 w-4" />
|
||||||
Save Media
|
{copy.saveMedia}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
{mediaAssets.map((asset) => (
|
{mediaAssets.map((asset, index) => (
|
||||||
<AppCard key={asset.id}>
|
<MotionFade key={asset.id} delay={0.18 + index * 0.04}>
|
||||||
|
<AppCard>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">{asset.label}</CardTitle>
|
<CardTitle className="text-base">{asset.label}</CardTitle>
|
||||||
<CardDescription className="flex flex-wrap gap-2">
|
<CardDescription className="flex flex-wrap gap-2">
|
||||||
<span>{asset.kind}</span>
|
<span>{asset.kind}</span>
|
||||||
<span>{asset.source}</span>
|
<span>{asset.source}</span>
|
||||||
<span>{asset.usages.length} usages</span>
|
<span>{asset.usages.length} {copy.usages}</span>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
@@ -143,7 +162,7 @@ export default async function RootMediaPage({ searchParams }: RootMediaPageProps
|
|||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Link href={asset.url} target="_blank" rel="noreferrer" className="inline-flex items-center gap-2 text-foreground">
|
<Link href={asset.url} target="_blank" rel="noreferrer" className="inline-flex items-center gap-2 text-foreground">
|
||||||
<ExternalLink className="h-4 w-4" />
|
<ExternalLink className="h-4 w-4" />
|
||||||
Open
|
{copy.open}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,20 +181,23 @@ export default async function RootMediaPage({ searchParams }: RootMediaPageProps
|
|||||||
<input type="hidden" name="assetId" value={asset.id} />
|
<input type="hidden" name="assetId" value={asset.id} />
|
||||||
<Button type="submit" variant="destructive" disabled={asset.usages.length > 0}>
|
<Button type="submit" variant="destructive" disabled={asset.usages.length > 0}>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
Delete
|
{copy.delete}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{mediaAssets.length === 0 ? (
|
{mediaAssets.length === 0 ? (
|
||||||
|
<MotionFade delay={0.2}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardContent className="p-6 text-sm text-muted-foreground">
|
<CardContent className="p-6 text-sm text-muted-foreground">
|
||||||
No media assets found yet.
|
{copy.noAssets}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</RootDashboardShell>
|
</RootDashboardShell>
|
||||||
|
|||||||
+252
-109
@@ -1,19 +1,37 @@
|
|||||||
import { ArrowLeft, ExternalLink, ImageIcon, LockKeyhole, LogOut } from "lucide-react";
|
import {
|
||||||
|
ExternalLink,
|
||||||
|
FolderKanban,
|
||||||
|
ImageIcon,
|
||||||
|
LayoutDashboard,
|
||||||
|
LockKeyhole,
|
||||||
|
LogOut,
|
||||||
|
ShieldAlert,
|
||||||
|
Shapes,
|
||||||
|
SwatchBook,
|
||||||
|
ArrowLeft,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { AppHeader } from "@/components/layout/app-header";
|
|
||||||
import { AppShell } from "@/components/layout/app-shell";
|
|
||||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
|
||||||
import { Container } from "@/components/layout/container";
|
import { Container } from "@/components/layout/container";
|
||||||
import { MotionFade } from "@/components/motion-fade";
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
|
import { DashboardCard } from "@/components/dashboard/dashboard-card";
|
||||||
|
import { DashboardLayout } from "@/components/dashboard/dashboard-layout";
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { getLocalizedPath } from "@/lib/locale";
|
import { Badge, type BadgeProps } from "@/components/ui/badge";
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CardContent, CardDescription, CardHeader } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/components/ui/table";
|
||||||
import {
|
import {
|
||||||
clearAdminSessionCookie,
|
clearAdminSessionCookie,
|
||||||
getAdminLockState,
|
getAdminLockState,
|
||||||
@@ -25,6 +43,12 @@ import {
|
|||||||
setAdminSessionCookie,
|
setAdminSessionCookie,
|
||||||
} from "@/lib/admin-auth";
|
} from "@/lib/admin-auth";
|
||||||
import { getMaintenanceMode } from "@/lib/app-config";
|
import { getMaintenanceMode } from "@/lib/app-config";
|
||||||
|
import { getLocalizedPath } from "@/lib/locale";
|
||||||
|
import { getAdminMediaAssets } from "@/lib/media";
|
||||||
|
import {
|
||||||
|
getAdminPortfolioCategories,
|
||||||
|
getAdminPortfolioProjects,
|
||||||
|
} from "@/lib/portfolio";
|
||||||
import { getRootNavigation } from "@/lib/root-navigation";
|
import { getRootNavigation } from "@/lib/root-navigation";
|
||||||
|
|
||||||
type RootPageProps = {
|
type RootPageProps = {
|
||||||
@@ -52,7 +76,7 @@ const copy = {
|
|||||||
media: "Media",
|
media: "Media",
|
||||||
portfolio: "Portfolio",
|
portfolio: "Portfolio",
|
||||||
portfolioTitle: "Portfolio",
|
portfolioTitle: "Portfolio",
|
||||||
portfolioDescription: "Kategorien, Projekte, Sections und Assets verwalten.",
|
portfolioDescription: "Kategorien, Projekte, Abschnitte und Dateien verwalten.",
|
||||||
portfolioAction: "Zum Portfolio",
|
portfolioAction: "Zum Portfolio",
|
||||||
mediaTitle: "Media Library",
|
mediaTitle: "Media Library",
|
||||||
mediaDescription: "Uploads, externe URLs und Verwendungsorte zentral verwalten.",
|
mediaDescription: "Uploads, externe URLs und Verwendungsorte zentral verwalten.",
|
||||||
@@ -68,6 +92,31 @@ const copy = {
|
|||||||
logout: "Ausloggen",
|
logout: "Ausloggen",
|
||||||
backToSite: "Zur Website",
|
backToSite: "Zur Website",
|
||||||
uiKit: "UI Kit",
|
uiKit: "UI Kit",
|
||||||
|
tableTitle: "Bereiche",
|
||||||
|
tableDescription: "Direkter Zugriff auf alle Verwaltungsbereiche mit aktuellem Status.",
|
||||||
|
tableSection: "Bereich",
|
||||||
|
tableSummary: "Zusammenfassung",
|
||||||
|
tableStatus: "Status",
|
||||||
|
tableAction: "Aktion",
|
||||||
|
maintenanceStatusOn: "Aktiv",
|
||||||
|
maintenanceStatusOff: "Inaktiv",
|
||||||
|
authStatusOk: "Bereit",
|
||||||
|
authStatusWarning: "Pruefen",
|
||||||
|
authCard: "Root Zugang",
|
||||||
|
authDescription: "Admin Passwort und Session Signatur sind gesetzt.",
|
||||||
|
authDescriptionWarning: "Eine oder mehrere Root Variablen fehlen und sollten geprueft werden.",
|
||||||
|
mediaCountDescription: "Verfuegbare Dateien fuer Portfolio und Inhalte.",
|
||||||
|
projectCountDescription: "Gespeicherte Portfolio Projekte im System.",
|
||||||
|
categoryCountDescription: "Portfolio Kategorien mit eigener Sortierung.",
|
||||||
|
systemStatusTitle: "Systemstatus",
|
||||||
|
systemStatusDescription: "Technischer Zustand des Root Bereichs und der wichtigsten Inhaltsbereiche.",
|
||||||
|
maintenanceLabel: "Wartung",
|
||||||
|
contentLabel: "Inhalte",
|
||||||
|
contentDescription: "Portfolio, Media und Kategorien sind direkt aus diesem Bereich erreichbar.",
|
||||||
|
publishedProjects: "Veroeffentlicht",
|
||||||
|
totalProjects: "Projekte",
|
||||||
|
totalCategories: "Kategorien",
|
||||||
|
totalMedia: "Dateien",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootPage({ searchParams }: RootPageProps) {
|
export default async function RootPage({ searchParams }: RootPageProps) {
|
||||||
@@ -77,7 +126,6 @@ export default async function RootPage({ searchParams }: RootPageProps) {
|
|||||||
);
|
);
|
||||||
const authenticated = isAdminAuthenticated();
|
const authenticated = isAdminAuthenticated();
|
||||||
const lockState = getAdminLockState();
|
const lockState = getAdminLockState();
|
||||||
const maintenanceEnabled = authenticated ? await getMaintenanceMode() : false;
|
|
||||||
|
|
||||||
async function loginAction(formData: FormData) {
|
async function loginAction(formData: FormData) {
|
||||||
"use server";
|
"use server";
|
||||||
@@ -114,7 +162,7 @@ export default async function RootPage({ searchParams }: RootPageProps) {
|
|||||||
return (
|
return (
|
||||||
<Container size="narrow" className="py-12">
|
<Container size="narrow" className="py-12">
|
||||||
<MotionFade>
|
<MotionFade>
|
||||||
<AppCard level={3}>
|
<Card className="border-border/70 bg-card/95 shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<p className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
<p className="inline-flex items-center gap-2 text-sm font-medium text-muted-foreground">
|
||||||
<LockKeyhole className="h-4 w-4 text-brand-primary" />
|
<LockKeyhole className="h-4 w-4 text-brand-primary" />
|
||||||
@@ -159,137 +207,232 @@ export default async function RootPage({ searchParams }: RootPageProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</Card>
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarItems = getRootNavigation(copy, "overview");
|
const [maintenanceEnabled, categories, projects, mediaAssets] = await Promise.all([
|
||||||
|
getMaintenanceMode(),
|
||||||
|
getAdminPortfolioCategories(),
|
||||||
|
getAdminPortfolioProjects(),
|
||||||
|
getAdminMediaAssets(),
|
||||||
|
]);
|
||||||
|
const publishedProjects = projects.filter((project) => project.isPublished).length;
|
||||||
|
const rootNavigation = getRootNavigation(copy, "overview");
|
||||||
|
const authHealthy = authConfigured && basicConfigured;
|
||||||
|
const sectionRows: Array<{
|
||||||
|
label: string;
|
||||||
|
summary: string;
|
||||||
|
status: string;
|
||||||
|
href: string;
|
||||||
|
badgeVariant: BadgeProps["variant"];
|
||||||
|
action: string;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
label: copy.maintenanceTitle,
|
||||||
|
summary: copy.maintenanceDescription,
|
||||||
|
status: maintenanceEnabled ? copy.maintenanceStatusOn : copy.maintenanceStatusOff,
|
||||||
|
href: "/root/maintenance",
|
||||||
|
badgeVariant: maintenanceEnabled ? "warning" : "success",
|
||||||
|
action: copy.maintenanceAction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: copy.mediaTitle,
|
||||||
|
summary: `${mediaAssets.length} ${copy.totalMedia}`,
|
||||||
|
status: copy.authStatusOk,
|
||||||
|
href: "/root/media",
|
||||||
|
badgeVariant: "outline" as const,
|
||||||
|
action: copy.mediaAction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: copy.portfolioTitle,
|
||||||
|
summary: `${projects.length} ${copy.totalProjects} / ${categories.length} ${copy.totalCategories}`,
|
||||||
|
status: `${publishedProjects} ${copy.publishedProjects}`,
|
||||||
|
href: "/root/portfolio",
|
||||||
|
badgeVariant: "outline" as const,
|
||||||
|
action: copy.portfolioAction,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: copy.uiKitTitle,
|
||||||
|
summary: copy.uiKitDescription,
|
||||||
|
status: copy.authStatusOk,
|
||||||
|
href: "/root/ui-kit",
|
||||||
|
badgeVariant: "outline" as const,
|
||||||
|
action: copy.uiKitAction,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<DashboardLayout
|
||||||
sidebar={
|
|
||||||
<AppSidebar
|
|
||||||
title={copy.title}
|
title={copy.title}
|
||||||
description={copy.subtitle}
|
description={copy.subtitle}
|
||||||
items={sidebarItems}
|
icon={LayoutDashboard}
|
||||||
footer={
|
items={rootNavigation}
|
||||||
<div className="space-y-2">
|
sidebarFooter={
|
||||||
<div className="flex items-center gap-2">
|
<>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant={maintenanceEnabled ? "default" : "outline"}
|
||||||
|
className="w-full justify-between"
|
||||||
|
>
|
||||||
|
<Link href="/root/maintenance">
|
||||||
|
{copy.maintenance}
|
||||||
|
<ShieldAlert className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline" className="w-full justify-between">
|
||||||
|
<Link href="/root/ui-kit">
|
||||||
|
{copy.uiKit}
|
||||||
|
<SwatchBook className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
headerActions={
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
{maintenanceEnabled ? (
|
||||||
|
<Button asChild variant="destructive" className="hidden sm:inline-flex">
|
||||||
|
<Link href="/root/maintenance">{copy.maintenanceVisitorsClosed}</Link>
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<ThemeToggle ariaLabel="Theme wechseln" />
|
<ThemeToggle ariaLabel="Theme wechseln" />
|
||||||
</div>
|
<Button asChild variant="outline" size="sm">
|
||||||
<Button asChild variant="outline" className="w-full justify-start">
|
|
||||||
<Link href={getLocalizedPath("de")}>
|
<Link href={getLocalizedPath("de")}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
{copy.backToSite}
|
{copy.backToSite}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<form action={logoutAction}>
|
<form action={logoutAction}>
|
||||||
<Button type="submit" variant="destructive" className="w-full justify-start">
|
<Button type="submit" variant="ghost" size="sm" className="text-destructive hover:bg-destructive/10 hover:text-destructive">
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
{copy.logout}
|
{copy.logout}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
|
||||||
}
|
|
||||||
header={
|
|
||||||
<AppHeader
|
|
||||||
title={copy.title}
|
|
||||||
description={copy.subtitle}
|
|
||||||
actions={
|
|
||||||
maintenanceEnabled ? (
|
|
||||||
<Button asChild variant="destructive">
|
|
||||||
<Link href="/root/maintenance">
|
|
||||||
{copy.maintenanceVisitorsClosed}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
) : null
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="grid gap-6">
|
<div className="space-y-6">
|
||||||
<section className="grid gap-4 lg:grid-cols-4">
|
<section className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<MotionFade delay={0.05}>
|
<MotionFade delay={0.05}>
|
||||||
<AppCard level={2}>
|
<DashboardCard
|
||||||
<CardHeader>
|
title={copy.maintenanceTitle}
|
||||||
<CardDescription>{copy.maintenanceTitle}</CardDescription>
|
value={maintenanceEnabled ? copy.maintenanceStatusOn : copy.maintenanceStatusOff}
|
||||||
</CardHeader>
|
description={copy.maintenanceDescription}
|
||||||
<CardContent className="space-y-4">
|
icon={ShieldAlert}
|
||||||
<p className="text-lg font-semibold text-foreground">
|
/>
|
||||||
{maintenanceEnabled ? copy.maintenanceVisitorsClosed : copy.maintenanceOpen}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
{copy.maintenanceDescription}
|
|
||||||
</p>
|
|
||||||
<Button asChild variant={maintenanceEnabled ? "destructive" : "outline"}>
|
|
||||||
<Link href="/root/maintenance">
|
|
||||||
{copy.maintenanceAction}
|
|
||||||
<ExternalLink className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</AppCard>
|
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
|
|
||||||
<MotionFade delay={0.1}>
|
<MotionFade delay={0.1}>
|
||||||
<AppCard level={2}>
|
<DashboardCard
|
||||||
<CardHeader>
|
title={copy.totalMedia}
|
||||||
<CardDescription>{copy.uiKitTitle}</CardDescription>
|
value={String(mediaAssets.length)}
|
||||||
</CardHeader>
|
description={copy.mediaCountDescription}
|
||||||
<CardContent className="space-y-4">
|
icon={ImageIcon}
|
||||||
<p className="text-lg font-semibold text-foreground">{copy.uiKitTitle}</p>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">{copy.uiKitDescription}</p>
|
|
||||||
<Button asChild variant="outline">
|
|
||||||
<Link href="/root/ui-kit">
|
|
||||||
{copy.uiKitAction}
|
|
||||||
<ExternalLink className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</AppCard>
|
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
|
|
||||||
<MotionFade delay={0.15}>
|
<MotionFade delay={0.15}>
|
||||||
<AppCard level={2}>
|
<DashboardCard
|
||||||
<CardHeader>
|
title={copy.totalProjects}
|
||||||
<CardDescription>{copy.portfolioTitle}</CardDescription>
|
value={String(projects.length)}
|
||||||
</CardHeader>
|
description={copy.projectCountDescription}
|
||||||
<CardContent className="space-y-4">
|
icon={FolderKanban}
|
||||||
<p className="text-lg font-semibold text-foreground">{copy.portfolioTitle}</p>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">{copy.portfolioDescription}</p>
|
|
||||||
<Button asChild variant="outline">
|
|
||||||
<Link href="/root/portfolio">
|
|
||||||
{copy.portfolioAction}
|
|
||||||
<ExternalLink className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</AppCard>
|
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
|
|
||||||
<MotionFade delay={0.2}>
|
<MotionFade delay={0.2}>
|
||||||
<AppCard level={2}>
|
<DashboardCard
|
||||||
<CardHeader>
|
title={copy.totalCategories}
|
||||||
<CardDescription>{copy.mediaTitle}</CardDescription>
|
value={String(categories.length)}
|
||||||
</CardHeader>
|
description={copy.categoryCountDescription}
|
||||||
<CardContent className="space-y-4">
|
icon={Shapes}
|
||||||
<p className="text-lg font-semibold text-foreground">{copy.mediaTitle}</p>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">{copy.mediaDescription}</p>
|
|
||||||
<Button asChild variant="outline">
|
|
||||||
<Link href="/root/media">
|
|
||||||
{copy.mediaAction}
|
|
||||||
<ImageIcon className="h-4 w-4" />
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
</CardContent>
|
|
||||||
</AppCard>
|
|
||||||
</MotionFade>
|
</MotionFade>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section className="grid gap-6 xl:grid-cols-[minmax(0,1.5fr)_minmax(320px,1fr)]">
|
||||||
|
<MotionFade delay={0.25}>
|
||||||
|
<Card className="border-border/70 bg-card/95 shadow-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{copy.tableTitle}</CardTitle>
|
||||||
|
<CardDescription>{copy.tableDescription}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>{copy.tableSection}</TableHead>
|
||||||
|
<TableHead>{copy.tableSummary}</TableHead>
|
||||||
|
<TableHead>{copy.tableStatus}</TableHead>
|
||||||
|
<TableHead className="text-right">{copy.tableAction}</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{sectionRows.map((row) => (
|
||||||
|
<TableRow key={row.href}>
|
||||||
|
<TableCell className="font-medium">{row.label}</TableCell>
|
||||||
|
<TableCell className="text-muted-foreground">{row.summary}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge variant={row.badgeVariant}>{row.status}</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<Button asChild variant="ghost" size="sm">
|
||||||
|
<Link href={row.href}>
|
||||||
|
{row.action}
|
||||||
|
<ExternalLink className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</MotionFade>
|
||||||
|
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<MotionFade delay={0.3}>
|
||||||
|
<Card className="border-border/70 bg-card/95 shadow-sm">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{copy.systemStatusTitle}</CardTitle>
|
||||||
|
<CardDescription>{copy.systemStatusDescription}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="rounded-lg border border-border bg-muted/40 p-4">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-foreground">{copy.authCard}</p>
|
||||||
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
|
{authHealthy ? copy.authDescription : copy.authDescriptionWarning}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</AppShell>
|
<Badge variant={authHealthy ? "success" : "warning"}>
|
||||||
|
{authHealthy ? copy.authStatusOk : copy.authStatusWarning}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-3 text-sm">
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-muted-foreground">{copy.maintenanceLabel}</span>
|
||||||
|
<Badge variant={maintenanceEnabled ? "warning" : "success"}>
|
||||||
|
{maintenanceEnabled ? copy.maintenanceStatusOn : copy.maintenanceStatusOff}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between gap-4">
|
||||||
|
<span className="text-muted-foreground">{copy.contentLabel}</span>
|
||||||
|
<span className="font-medium text-foreground">
|
||||||
|
{`${publishedProjects} ${copy.publishedProjects} / ${projects.length} ${copy.totalProjects}`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground">{copy.contentDescription}</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</MotionFade>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,12 +52,12 @@ function parseJsonArray(rawValue: FormDataEntryValue | null, key: string) {
|
|||||||
const parsed = JSON.parse(rawValue);
|
const parsed = JSON.parse(rawValue);
|
||||||
|
|
||||||
if (!Array.isArray(parsed)) {
|
if (!Array.isArray(parsed)) {
|
||||||
throw new Error(`${key} must be an array.`);
|
throw new Error(`${key} muss ein Array sein.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Invalid ${key} payload.`);
|
throw new Error(`Ungueltige ${key} Nutzdaten.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,17 +70,17 @@ function parseJsonObject(rawValue: FormDataEntryValue | null, key: string) {
|
|||||||
const parsed = JSON.parse(rawValue);
|
const parsed = JSON.parse(rawValue);
|
||||||
|
|
||||||
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
||||||
throw new Error(`${key} must be an object.`);
|
throw new Error(`${key} muss ein Objekt sein.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error(`Invalid ${key} payload.`);
|
throw new Error(`Ungueltige ${key} Nutzdaten.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseZodError(error: ZodError) {
|
function parseZodError(error: ZodError) {
|
||||||
return error.issues[0]?.message ?? "Validation failed.";
|
return error.issues[0]?.message ?? "Validierung fehlgeschlagen.";
|
||||||
}
|
}
|
||||||
|
|
||||||
async function revalidatePortfolioPages() {
|
async function revalidatePortfolioPages() {
|
||||||
@@ -135,7 +135,7 @@ export async function upsertCategoryAction(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await revalidatePortfolioPages();
|
await revalidatePortfolioPages();
|
||||||
redirect(withMessage(redirectPath, "success", "Category saved."));
|
redirect(withMessage(redirectPath, "success", "Kategorie gespeichert."));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRedirectError(error)) {
|
if (isRedirectError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -145,8 +145,8 @@ export async function upsertCategoryAction(formData: FormData) {
|
|||||||
error instanceof ZodError
|
error instanceof ZodError
|
||||||
? parseZodError(error)
|
? parseZodError(error)
|
||||||
: error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002"
|
: error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002"
|
||||||
? "Category slug must be unique."
|
? "Kategorie Slug muss eindeutig sein."
|
||||||
: "Unable to save category.";
|
: "Kategorie konnte nicht gespeichert werden.";
|
||||||
|
|
||||||
redirect(withMessage(redirectPath, "error", message));
|
redirect(withMessage(redirectPath, "error", message));
|
||||||
}
|
}
|
||||||
@@ -166,7 +166,7 @@ export async function deleteCategoryAction(formData: FormData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (projectCount > 0) {
|
if (projectCount > 0) {
|
||||||
redirect(withMessage(redirectPath, "error", "Cannot delete a category with projects."));
|
redirect(withMessage(redirectPath, "error", "Kategorie mit Projekten kann nicht geloescht werden."));
|
||||||
}
|
}
|
||||||
|
|
||||||
await prisma.category.delete({
|
await prisma.category.delete({
|
||||||
@@ -176,13 +176,13 @@ export async function deleteCategoryAction(formData: FormData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await revalidatePortfolioPages();
|
await revalidatePortfolioPages();
|
||||||
redirect(withMessage(redirectPath, "success", "Category deleted."));
|
redirect(withMessage(redirectPath, "success", "Kategorie geloescht."));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRedirectError(error)) {
|
if (isRedirectError(error)) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect(withMessage(redirectPath, "error", "Unable to delete category."));
|
redirect(withMessage(redirectPath, "error", "Kategorie konnte nicht geloescht werden."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +337,7 @@ export async function saveProjectAction(formData: FormData) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!assetSelection.url) {
|
if (!assetSelection.url) {
|
||||||
throw new Error("Each asset row needs either an existing file or a new upload.");
|
throw new Error("Jede Datei Zeile braucht eine vorhandene Datei oder einen neuen Upload.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (assetSelection.createdAssetId) {
|
if (assetSelection.createdAssetId) {
|
||||||
@@ -521,7 +521,7 @@ export async function saveProjectAction(formData: FormData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redirect(
|
redirect(
|
||||||
withMessage(`/root/portfolio/projects/${projectResult.project.id}`, "success", "Project saved."),
|
withMessage(`/root/portfolio/projects/${projectResult.project.id}`, "success", "Projekt gespeichert."),
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isRedirectError(error)) {
|
if (isRedirectError(error)) {
|
||||||
@@ -532,10 +532,10 @@ export async function saveProjectAction(formData: FormData) {
|
|||||||
error instanceof ZodError
|
error instanceof ZodError
|
||||||
? parseZodError(error)
|
? parseZodError(error)
|
||||||
: error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002"
|
: error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002"
|
||||||
? "Project slug must be unique."
|
? "Projekt Slug muss eindeutig sein."
|
||||||
: error instanceof Error
|
: error instanceof Error
|
||||||
? error.message
|
? error.message
|
||||||
: "Unable to save project.";
|
: "Projekt konnte nicht gespeichert werden.";
|
||||||
|
|
||||||
await removeManagedPaths(uploadedPaths);
|
await removeManagedPaths(uploadedPaths);
|
||||||
if (createdMediaAssetIds.length > 0) {
|
if (createdMediaAssetIds.length > 0) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
|
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
|
||||||
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
import { AppCard } from "@/components/ui/app-card";
|
||||||
@@ -17,9 +18,9 @@ import { deleteCategoryAction, upsertCategoryAction } from "../actions";
|
|||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
const locales = [
|
const locales = [
|
||||||
{ key: "Ar", label: "Arabic", hint: "الواجهة العربية" },
|
{ key: "Ar", label: "Arabisch", hint: "الواجهة العربية" },
|
||||||
{ key: "En", label: "English", hint: "English website" },
|
{ key: "En", label: "Englisch", hint: "English website" },
|
||||||
{ key: "De", label: "German", hint: "Deutsche Website" },
|
{ key: "De", label: "Deutsch", hint: "Deutsche Website" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
const copy = {
|
const copy = {
|
||||||
@@ -32,6 +33,13 @@ const copy = {
|
|||||||
portfolio: "Portfolio",
|
portfolio: "Portfolio",
|
||||||
logout: "Ausloggen",
|
logout: "Ausloggen",
|
||||||
backToSite: "Zur Website",
|
backToSite: "Zur Website",
|
||||||
|
sortOrder: "Sortierung",
|
||||||
|
active: "Aktiv",
|
||||||
|
saveCategory: "Kategorie speichern",
|
||||||
|
save: "Speichern",
|
||||||
|
delete: "Loeschen",
|
||||||
|
projects: "Projekte",
|
||||||
|
description: "Beschreibung",
|
||||||
};
|
};
|
||||||
|
|
||||||
type RootPortfolioCategoriesPageProps = {
|
type RootPortfolioCategoriesPageProps = {
|
||||||
@@ -65,22 +73,26 @@ export default async function RootPortfolioCategoriesPage({
|
|||||||
logoutAction={logoutAction}
|
logoutAction={logoutAction}
|
||||||
headerTitle={copy.title}
|
headerTitle={copy.title}
|
||||||
headerDescription={copy.subtitle}
|
headerDescription={copy.subtitle}
|
||||||
|
toolbar={<PortfolioSubnav active="categories" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PortfolioSubnav active="categories" />
|
|
||||||
|
|
||||||
{searchParams?.success ? (
|
{searchParams?.success ? (
|
||||||
|
<MotionFade delay={0.1}>
|
||||||
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
|
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
|
||||||
{searchParams.success}
|
{searchParams.success}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{searchParams?.error ? (
|
{searchParams?.error ? (
|
||||||
|
<MotionFade delay={0.12}>
|
||||||
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
{searchParams.error}
|
{searchParams.error}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<MotionFade delay={0.15}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Neue Kategorie</CardTitle>
|
<CardTitle>Neue Kategorie</CardTitle>
|
||||||
@@ -94,7 +106,7 @@ export default async function RootPortfolioCategoriesPage({
|
|||||||
<Input id="create-slug" name="slug" required />
|
<Input id="create-slug" name="slug" required />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="create-sortOrder">Sort Order</Label>
|
<Label htmlFor="create-sortOrder">{copy.sortOrder}</Label>
|
||||||
<Input id="create-sortOrder" name="sortOrder" type="number" min="0" defaultValue="0" required />
|
<Input id="create-sortOrder" name="sortOrder" type="number" min="0" defaultValue="0" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -117,7 +129,7 @@ export default async function RootPortfolioCategoriesPage({
|
|||||||
<Input id={`create-name-${locale.key}`} name={`name${locale.key}`} required />
|
<Input id={`create-name-${locale.key}`} name={`name${locale.key}`} required />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-2 md:col-span-2">
|
||||||
<Label htmlFor={`create-description-${locale.key}`}>{`Description ${locale.label}`}</Label>
|
<Label htmlFor={`create-description-${locale.key}`}>{`${copy.description} ${locale.label}`}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id={`create-description-${locale.key}`}
|
id={`create-description-${locale.key}`}
|
||||||
name={`description${locale.key}`}
|
name={`description${locale.key}`}
|
||||||
@@ -133,19 +145,21 @@ export default async function RootPortfolioCategoriesPage({
|
|||||||
|
|
||||||
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm md:col-span-2">
|
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm md:col-span-2">
|
||||||
<input type="checkbox" name="isActive" defaultChecked />
|
<input type="checkbox" name="isActive" defaultChecked />
|
||||||
Active
|
{copy.active}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Button type="submit">Save Category</Button>
|
<Button type="submit">{copy.saveCategory}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{categories.map((category) => (
|
{categories.map((category, index) => (
|
||||||
<AppCard key={category.id}>
|
<MotionFade key={category.id} delay={0.18 + index * 0.03}>
|
||||||
|
<AppCard>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<form action={upsertCategoryAction} className="grid gap-4 md:grid-cols-2">
|
<form action={upsertCategoryAction} className="grid gap-4 md:grid-cols-2">
|
||||||
<input type="hidden" name="id" value={category.id} />
|
<input type="hidden" name="id" value={category.id} />
|
||||||
@@ -157,7 +171,7 @@ export default async function RootPortfolioCategoriesPage({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`sortOrder-${category.id}`}>Sort Order</Label>
|
<Label htmlFor={`sortOrder-${category.id}`}>{copy.sortOrder}</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`sortOrder-${category.id}`}
|
id={`sortOrder-${category.id}`}
|
||||||
name="sortOrder"
|
name="sortOrder"
|
||||||
@@ -197,7 +211,7 @@ export default async function RootPortfolioCategoriesPage({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-2 md:col-span-2">
|
||||||
<Label htmlFor={`${descriptionKey}-${category.id}`}>{`Description ${locale.label}`}</Label>
|
<Label htmlFor={`${descriptionKey}-${category.id}`}>{`${copy.description} ${locale.label}`}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id={`${descriptionKey}-${category.id}`}
|
id={`${descriptionKey}-${category.id}`}
|
||||||
name={descriptionKey}
|
name={descriptionKey}
|
||||||
@@ -215,13 +229,13 @@ export default async function RootPortfolioCategoriesPage({
|
|||||||
|
|
||||||
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
|
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
|
||||||
<input type="checkbox" name="isActive" defaultChecked={category.isActive} />
|
<input type="checkbox" name="isActive" defaultChecked={category.isActive} />
|
||||||
Active
|
{copy.active}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="flex items-end justify-between gap-3">
|
<div className="flex items-end justify-between gap-3">
|
||||||
<p className="text-sm text-muted-foreground">{category.projectCount} projects</p>
|
<p className="text-sm text-muted-foreground">{category.projectCount} {copy.projects}</p>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Button type="submit">Save</Button>
|
<Button type="submit">{copy.save}</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -230,11 +244,12 @@ export default async function RootPortfolioCategoriesPage({
|
|||||||
<input type="hidden" name="id" value={category.id} />
|
<input type="hidden" name="id" value={category.id} />
|
||||||
<input type="hidden" name="redirectPath" value="/root/portfolio/categories" />
|
<input type="hidden" name="redirectPath" value="/root/portfolio/categories" />
|
||||||
<Button type="submit" variant="destructive" disabled={category.projectCount > 0}>
|
<Button type="submit" variant="destructive" disabled={category.projectCount > 0}>
|
||||||
Delete
|
{copy.delete}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default async function RootPortfolioPage() {
|
|||||||
logoutAction={logoutAction}
|
logoutAction={logoutAction}
|
||||||
headerTitle={copy.title}
|
headerTitle={copy.title}
|
||||||
headerDescription={copy.subtitle}
|
headerDescription={copy.subtitle}
|
||||||
|
toolbar={<PortfolioSubnav active="overview" />}
|
||||||
headerActions={
|
headerActions={
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
@@ -79,8 +80,6 @@ export default async function RootPortfolioPage() {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PortfolioSubnav active="overview" />
|
|
||||||
|
|
||||||
<section className="grid gap-4 md:grid-cols-3">
|
<section className="grid gap-4 md:grid-cols-3">
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
@@ -120,6 +119,7 @@ export default async function RootPortfolioPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid gap-4 lg:grid-cols-3">
|
<section className="grid gap-4 lg:grid-cols-3">
|
||||||
|
<MotionFade delay={0.18}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{copy.totalCategories}</CardTitle>
|
<CardTitle>{copy.totalCategories}</CardTitle>
|
||||||
@@ -131,11 +131,13 @@ export default async function RootPortfolioPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
|
|
||||||
|
<MotionFade delay={0.22}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{copy.totalProjects}</CardTitle>
|
<CardTitle>{copy.totalProjects}</CardTitle>
|
||||||
<CardDescription>Drafts, veroeffentlichte Projekte und flexible Sections pflegen.</CardDescription>
|
<CardDescription>Entwuerfe, veroeffentlichte Projekte und flexible Abschnitte pflegen.</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex gap-3">
|
<CardContent className="flex gap-3">
|
||||||
<Button asChild variant="outline">
|
<Button asChild variant="outline">
|
||||||
@@ -146,7 +148,9 @@ export default async function RootPortfolioPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
|
|
||||||
|
<MotionFade delay={0.26}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{copy.media}</CardTitle>
|
<CardTitle>{copy.media}</CardTitle>
|
||||||
@@ -161,6 +165,7 @@ export default async function RootPortfolioPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</RootDashboardShell>
|
</RootDashboardShell>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { PortfolioProjectForm } from "@/components/root/portfolio-project-form";
|
import { PortfolioProjectForm } from "@/components/root/portfolio-project-form";
|
||||||
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
|
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
|
||||||
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
||||||
@@ -27,6 +28,10 @@ const copy = {
|
|||||||
portfolio: "Portfolio",
|
portfolio: "Portfolio",
|
||||||
logout: "Ausloggen",
|
logout: "Ausloggen",
|
||||||
backToSite: "Zur Website",
|
backToSite: "Zur Website",
|
||||||
|
saveProject: "Projekt speichern",
|
||||||
|
dangerZone: "Gefahrenbereich",
|
||||||
|
dangerText: "Projektdaten werden aus der Datenbank entfernt. Hochgeladene Dateien bleiben auf dem Speicher erhalten.",
|
||||||
|
deleteProject: "Projekt loeschen",
|
||||||
};
|
};
|
||||||
|
|
||||||
type RootPortfolioProjectPageProps = {
|
type RootPortfolioProjectPageProps = {
|
||||||
@@ -72,47 +77,54 @@ export default async function RootPortfolioProjectPage({
|
|||||||
logoutAction={logoutAction}
|
logoutAction={logoutAction}
|
||||||
headerTitle={copy.title}
|
headerTitle={copy.title}
|
||||||
headerDescription={copy.subtitle}
|
headerDescription={copy.subtitle}
|
||||||
|
toolbar={<PortfolioSubnav active="projects" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PortfolioSubnav active="projects" />
|
|
||||||
|
|
||||||
{searchParams?.success ? (
|
{searchParams?.success ? (
|
||||||
|
<MotionFade delay={0.1}>
|
||||||
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
|
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
|
||||||
{searchParams.success}
|
{searchParams.success}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{searchParams?.error ? (
|
{searchParams?.error ? (
|
||||||
|
<MotionFade delay={0.12}>
|
||||||
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
{searchParams.error}
|
{searchParams.error}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<MotionFade delay={0.15}>
|
||||||
<PortfolioProjectForm
|
<PortfolioProjectForm
|
||||||
action={saveProjectAction}
|
action={saveProjectAction}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
mediaOptions={mediaOptions}
|
mediaOptions={mediaOptions}
|
||||||
project={project}
|
project={project}
|
||||||
redirectPath={`/root/portfolio/projects/${project.id}`}
|
redirectPath={`/root/portfolio/projects/${project.id}`}
|
||||||
submitLabel="Save Project"
|
submitLabel={copy.saveProject}
|
||||||
/>
|
/>
|
||||||
|
</MotionFade>
|
||||||
|
|
||||||
|
<MotionFade delay={0.2}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardContent className="flex items-center justify-between gap-4 p-6">
|
<CardContent className="flex items-center justify-between gap-4 p-6">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium text-foreground">Danger Zone</p>
|
<p className="text-sm font-medium text-foreground">{copy.dangerZone}</p>
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
<p className="mt-1 text-sm text-muted-foreground">
|
||||||
Project records are deleted from the database. Uploaded files stay on disk.
|
{copy.dangerText}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form action={deleteProjectAction}>
|
<form action={deleteProjectAction}>
|
||||||
<input type="hidden" name="id" value={project.id} />
|
<input type="hidden" name="id" value={project.id} />
|
||||||
<Button type="submit" variant="destructive">
|
<Button type="submit" variant="destructive">
|
||||||
Delete Project
|
{copy.deleteProject}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
</div>
|
</div>
|
||||||
</RootDashboardShell>
|
</RootDashboardShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { PortfolioProjectForm } from "@/components/root/portfolio-project-form";
|
import { PortfolioProjectForm } from "@/components/root/portfolio-project-form";
|
||||||
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
|
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
|
||||||
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
||||||
@@ -13,7 +14,7 @@ export const dynamic = "force-dynamic";
|
|||||||
|
|
||||||
const copy = {
|
const copy = {
|
||||||
title: "Neues Portfolio Projekt",
|
title: "Neues Portfolio Projekt",
|
||||||
subtitle: "Projekt mit Kategorie, Sections und Assets anlegen.",
|
subtitle: "Projekt mit Kategorie, Abschnitten und Dateien anlegen.",
|
||||||
overview: "Uebersicht",
|
overview: "Uebersicht",
|
||||||
maintenance: "Wartungsmodus",
|
maintenance: "Wartungsmodus",
|
||||||
uiKit: "UI Kit",
|
uiKit: "UI Kit",
|
||||||
@@ -56,23 +57,26 @@ export default async function RootNewPortfolioProjectPage({
|
|||||||
logoutAction={logoutAction}
|
logoutAction={logoutAction}
|
||||||
headerTitle={copy.title}
|
headerTitle={copy.title}
|
||||||
headerDescription={copy.subtitle}
|
headerDescription={copy.subtitle}
|
||||||
|
toolbar={<PortfolioSubnav active="projects" />}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PortfolioSubnav active="projects" />
|
|
||||||
|
|
||||||
{searchParams?.error ? (
|
{searchParams?.error ? (
|
||||||
|
<MotionFade delay={0.1}>
|
||||||
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
{searchParams.error}
|
{searchParams.error}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<MotionFade delay={0.15}>
|
||||||
<PortfolioProjectForm
|
<PortfolioProjectForm
|
||||||
action={saveProjectAction}
|
action={saveProjectAction}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
mediaOptions={mediaOptions}
|
mediaOptions={mediaOptions}
|
||||||
redirectPath="/root/portfolio/projects/new"
|
redirectPath="/root/portfolio/projects/new"
|
||||||
submitLabel="Create Project"
|
submitLabel="Projekt anlegen"
|
||||||
/>
|
/>
|
||||||
|
</MotionFade>
|
||||||
</div>
|
</div>
|
||||||
</RootDashboardShell>
|
</RootDashboardShell>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Plus } from "lucide-react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
|
import { PortfolioSubnav } from "@/components/root/portfolio-subnav";
|
||||||
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
||||||
import { AppCard } from "@/components/ui/app-card";
|
import { AppCard } from "@/components/ui/app-card";
|
||||||
@@ -27,6 +28,17 @@ const copy = {
|
|||||||
logout: "Ausloggen",
|
logout: "Ausloggen",
|
||||||
backToSite: "Zur Website",
|
backToSite: "Zur Website",
|
||||||
newProject: "Neues Projekt",
|
newProject: "Neues Projekt",
|
||||||
|
category: "Kategorie",
|
||||||
|
all: "Alle",
|
||||||
|
status: "Status",
|
||||||
|
draft: "Entwurf",
|
||||||
|
published: "Veroeffentlicht",
|
||||||
|
filter: "Filtern",
|
||||||
|
sort: "Sortierung",
|
||||||
|
previewSet: "Preview Link gesetzt",
|
||||||
|
previewMissing: "Kein Preview Link",
|
||||||
|
editProject: "Projekt bearbeiten",
|
||||||
|
empty: "Keine Projekte fuer die aktuellen Filter gefunden.",
|
||||||
};
|
};
|
||||||
|
|
||||||
type RootPortfolioProjectsPageProps = {
|
type RootPortfolioProjectsPageProps = {
|
||||||
@@ -71,36 +83,42 @@ export default async function RootPortfolioProjectsPage({
|
|||||||
logoutAction={logoutAction}
|
logoutAction={logoutAction}
|
||||||
headerTitle={copy.title}
|
headerTitle={copy.title}
|
||||||
headerDescription={copy.subtitle}
|
headerDescription={copy.subtitle}
|
||||||
|
toolbar={<PortfolioSubnav active="projects" />}
|
||||||
headerActions={
|
headerActions={
|
||||||
|
<MotionFade delay={0.04}>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href="/root/portfolio/projects/new">
|
<Link href="/root/portfolio/projects/new">
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
{copy.newProject}
|
{copy.newProject}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
</MotionFade>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<PortfolioSubnav active="projects" />
|
|
||||||
|
|
||||||
{searchParams?.success ? (
|
{searchParams?.success ? (
|
||||||
|
<MotionFade delay={0.1}>
|
||||||
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
|
<p className="rounded-nested border border-status-success/30 bg-status-success/10 px-4 py-3 text-sm text-status-success">
|
||||||
{searchParams.success}
|
{searchParams.success}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{searchParams?.error ? (
|
{searchParams?.error ? (
|
||||||
|
<MotionFade delay={0.12}>
|
||||||
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
<p className="rounded-nested border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||||
{searchParams.error}
|
{searchParams.error}
|
||||||
</p>
|
</p>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<MotionFade delay={0.15}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<form className="grid gap-4 md:grid-cols-3">
|
<form className="grid gap-4 md:grid-cols-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="category" className="text-sm font-medium text-foreground">
|
<label htmlFor="category" className="text-sm font-medium text-foreground">
|
||||||
Category
|
{copy.category}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="category"
|
id="category"
|
||||||
@@ -108,7 +126,7 @@ export default async function RootPortfolioProjectsPage({
|
|||||||
defaultValue={searchParams?.category ?? ""}
|
defaultValue={searchParams?.category ?? ""}
|
||||||
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
|
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
|
||||||
>
|
>
|
||||||
<option value="">All</option>
|
<option value="">{copy.all}</option>
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<option key={category.id} value={category.id}>
|
<option key={category.id} value={category.id}>
|
||||||
{category.name.de}
|
{category.name.de}
|
||||||
@@ -119,7 +137,7 @@ export default async function RootPortfolioProjectsPage({
|
|||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label htmlFor="status" className="text-sm font-medium text-foreground">
|
<label htmlFor="status" className="text-sm font-medium text-foreground">
|
||||||
Status
|
{copy.status}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="status"
|
id="status"
|
||||||
@@ -127,24 +145,26 @@ export default async function RootPortfolioProjectsPage({
|
|||||||
defaultValue={selectedStatus}
|
defaultValue={selectedStatus}
|
||||||
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
|
className="flex h-11 w-full rounded-pill border border-border bg-background px-4 text-sm text-foreground shadow-sm"
|
||||||
>
|
>
|
||||||
<option value="all">All</option>
|
<option value="all">{copy.all}</option>
|
||||||
<option value="draft">Draft</option>
|
<option value="draft">{copy.draft}</option>
|
||||||
<option value="published">Published</option>
|
<option value="published">{copy.published}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<Button type="submit" variant="outline">
|
<Button type="submit" variant="outline">
|
||||||
Filter
|
{copy.filter}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
|
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{projects.map((project) => (
|
{projects.map((project, index) => (
|
||||||
<AppCard key={project.id} interactive>
|
<MotionFade key={project.id} delay={0.18 + index * 0.03}>
|
||||||
|
<AppCard interactive>
|
||||||
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
<CardHeader className="flex flex-row items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>{getLocalizedValue(project.title, "de")}</CardTitle>
|
<CardTitle>{getLocalizedValue(project.title, "de")}</CardTitle>
|
||||||
@@ -154,34 +174,37 @@ export default async function RootPortfolioProjectsPage({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
|
||||||
<span className="rounded-pill border border-border px-3 py-1">
|
<span className="rounded-pill border border-border px-3 py-1">
|
||||||
{project.isPublished ? "Published" : "Draft"}
|
{project.isPublished ? copy.published : copy.draft}
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-pill border border-border px-3 py-1">
|
<span className="rounded-pill border border-border px-3 py-1">
|
||||||
{project.projectYear}
|
{project.projectYear}
|
||||||
</span>
|
</span>
|
||||||
<span className="rounded-pill border border-border px-3 py-1">
|
<span className="rounded-pill border border-border px-3 py-1">
|
||||||
Sort {project.sortOrder}
|
{copy.sort} {project.sortOrder}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="flex flex-wrap items-center justify-between gap-4">
|
<CardContent className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div className="space-y-1 text-sm text-muted-foreground">
|
<div className="space-y-1 text-sm text-muted-foreground">
|
||||||
<p>{project.slug}</p>
|
<p>{project.slug}</p>
|
||||||
<p>{project.previewUrl ? "Preview link set" : "No preview link"}</p>
|
<p>{project.previewUrl ? copy.previewSet : copy.previewMissing}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button asChild>
|
<Button asChild>
|
||||||
<Link href={`/root/portfolio/projects/${project.id}`}>Edit Project</Link>
|
<Link href={`/root/portfolio/projects/${project.id}`}>{copy.editProject}</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{projects.length === 0 ? (
|
{projects.length === 0 ? (
|
||||||
|
<MotionFade delay={0.18}>
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardContent className="p-6 text-sm text-muted-foreground">
|
<CardContent className="p-6 text-sm text-muted-foreground">
|
||||||
No projects match the selected filters.
|
{copy.empty}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
</MotionFade>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+11
-40
@@ -1,15 +1,8 @@
|
|||||||
import { ArrowLeft, LogOut } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
import { AppHeader } from "@/components/layout/app-header";
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { AppShell } from "@/components/layout/app-shell";
|
import { RootDashboardShell } from "@/components/root/root-dashboard-shell";
|
||||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
|
||||||
import { getLocalizedPath } from "@/lib/locale";
|
|
||||||
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
import { clearAdminSessionCookie, isAdminAuthenticated } from "@/lib/admin-auth";
|
||||||
import { getRootNavigation } from "@/lib/root-navigation";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { UiKitShowcase } from "@/components/ui/ui-kit-showcase";
|
import { UiKitShowcase } from "@/components/ui/ui-kit-showcase";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -40,39 +33,17 @@ export default async function RootUiKitPage() {
|
|||||||
redirect("/root");
|
redirect("/root");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sidebarItems = getRootNavigation(copy, "ui-kit");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppShell
|
<RootDashboardShell
|
||||||
sidebar={
|
copy={copy}
|
||||||
<AppSidebar
|
active="ui-kit"
|
||||||
title={copy.title}
|
logoutAction={logoutAction}
|
||||||
description={copy.subtitle}
|
headerTitle={copy.title}
|
||||||
items={sidebarItems}
|
headerDescription={copy.subtitle}
|
||||||
footer={
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<ThemeToggle ariaLabel="Theme wechseln" />
|
|
||||||
</div>
|
|
||||||
<Button asChild variant="outline" className="w-full justify-start">
|
|
||||||
<Link href={getLocalizedPath("de")}>
|
|
||||||
<ArrowLeft className="h-4 w-4" />
|
|
||||||
{copy.backToSite}
|
|
||||||
</Link>
|
|
||||||
</Button>
|
|
||||||
<form action={logoutAction}>
|
|
||||||
<Button type="submit" variant="destructive" className="w-full justify-start">
|
|
||||||
<LogOut className="h-4 w-4" />
|
|
||||||
{copy.logout}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
header={<AppHeader title={copy.title} description={copy.subtitle} />}
|
|
||||||
>
|
>
|
||||||
|
<MotionFade delay={0.1}>
|
||||||
<UiKitShowcase localeKey="de" />
|
<UiKitShowcase localeKey="de" />
|
||||||
</AppShell>
|
</MotionFade>
|
||||||
|
</RootDashboardShell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
||||||
|
type DashboardCardProps = {
|
||||||
|
title: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DashboardCard({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
description,
|
||||||
|
icon: Icon,
|
||||||
|
}: DashboardCardProps) {
|
||||||
|
return (
|
||||||
|
<Card className="border-border/70 bg-card/95 shadow-sm">
|
||||||
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-3">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<CardDescription>{title}</CardDescription>
|
||||||
|
<CardTitle className="text-3xl font-semibold tracking-tight">{value}</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-md border border-border bg-muted/60 p-2 text-muted-foreground">
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="text-sm text-muted-foreground">{description}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import { Menu, type LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { DashboardSidebar } from "@/components/dashboard/sidebar";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Sheet,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
} from "@/components/ui/sheet";
|
||||||
|
import type { RootNavItem } from "@/lib/root-navigation";
|
||||||
|
|
||||||
|
type DashboardLayoutProps = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon?: LucideIcon;
|
||||||
|
items: RootNavItem[];
|
||||||
|
sidebarFooter?: ReactNode;
|
||||||
|
headerActions?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DashboardLayout({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
icon: Icon,
|
||||||
|
items,
|
||||||
|
sidebarFooter,
|
||||||
|
headerActions,
|
||||||
|
children,
|
||||||
|
}: DashboardLayoutProps) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-muted/30">
|
||||||
|
<div className="grid min-h-screen lg:grid-cols-[280px_minmax(0,1fr)]">
|
||||||
|
<aside className="hidden border-r border-border/70 bg-sidebar/60 lg:block">
|
||||||
|
<DashboardSidebar items={items} footer={sidebarFooter} />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
|
<header className="sticky top-0 z-30 flex h-16 items-center gap-3 border-b border-border/70 bg-background/95 px-4 backdrop-blur lg:px-6">
|
||||||
|
<Sheet>
|
||||||
|
<SheetTrigger asChild>
|
||||||
|
<Button variant="outline" size="icon" className="lg:hidden">
|
||||||
|
<Menu className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</SheetTrigger>
|
||||||
|
<SheetContent side="left" className="p-0">
|
||||||
|
<SheetHeader className="sr-only">
|
||||||
|
<SheetTitle>{title}</SheetTitle>
|
||||||
|
<SheetDescription>{description}</SheetDescription>
|
||||||
|
</SheetHeader>
|
||||||
|
<DashboardSidebar items={items} footer={sidebarFooter} />
|
||||||
|
</SheetContent>
|
||||||
|
</Sheet>
|
||||||
|
|
||||||
|
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||||
|
{Icon ? (
|
||||||
|
<div className="hidden rounded-lg border border-border/70 bg-muted/50 p-2 text-muted-foreground sm:flex">
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-lg font-semibold text-foreground">{title}</p>
|
||||||
|
<p className="truncate text-sm text-muted-foreground">{description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{headerActions ? (
|
||||||
|
<div className="flex items-center gap-2">{headerActions}</div>
|
||||||
|
) : null}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 p-4 lg:p-6">{children}</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import { ShieldCheck } from "lucide-react";
|
||||||
|
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
type DashboardSidebarItem = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
active?: boolean;
|
||||||
|
children?: DashboardSidebarItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type DashboardSidebarProps = {
|
||||||
|
items: DashboardSidebarItem[];
|
||||||
|
footer?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DashboardSidebar({ items, footer }: DashboardSidebarProps) {
|
||||||
|
function renderItem(item: DashboardSidebarItem, nested = false) {
|
||||||
|
const Icon = item.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={item.href} className="space-y-1">
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition-colors",
|
||||||
|
item.active
|
||||||
|
? "bg-primary text-primary-foreground"
|
||||||
|
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||||
|
nested ? "ml-4" : undefined,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-4 w-4" />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
{item.children?.length ? item.children.map((child) => renderItem(child, true)) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full flex-col bg-sidebar/40">
|
||||||
|
<div className="flex items-center gap-3 px-4 py-5">
|
||||||
|
<div className="rounded-xl bg-primary/10 p-2 text-primary">
|
||||||
|
<ShieldCheck className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="truncate text-sm font-semibold text-foreground">Mohs Admin</p>
|
||||||
|
<p className="truncate text-xs text-muted-foreground">Website owner workspace</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="rounded-xl border border-border/70 bg-background/80 p-4">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-foreground">Private panel</p>
|
||||||
|
<p className="mt-1 text-xs text-muted-foreground">Single owner access</p>
|
||||||
|
</div>
|
||||||
|
<Badge variant="outline">Private</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator className="my-4" />
|
||||||
|
|
||||||
|
<div className="flex-1 space-y-6 overflow-y-auto px-4 pb-4">
|
||||||
|
<nav className="space-y-1">{items.map((item) => renderItem(item))}</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{footer ? (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<div className="space-y-2 p-4">{footer}</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -27,14 +27,13 @@ export function AppSidebar({
|
|||||||
items,
|
items,
|
||||||
footer,
|
footer,
|
||||||
}: AppSidebarProps) {
|
}: AppSidebarProps) {
|
||||||
return (
|
const portfolioItem = items.find((item) => item.href === "/root/portfolio");
|
||||||
<AppCard className="sticky top-6 overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
|
const systemOrder = ["/root", "/root/media", "/root/maintenance", "/root/ui-kit"];
|
||||||
<CardHeader className="pb-4">
|
const systemItems = items
|
||||||
<CardTitle className="text-base font-semibold">{title}</CardTitle>
|
.filter((item) => item.href !== "/root/portfolio")
|
||||||
<p className="text-sm text-sidebar-foreground/72">{description}</p>
|
.sort((left, right) => systemOrder.indexOf(left.href) - systemOrder.indexOf(right.href));
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="space-y-1 px-3 pb-3 pt-0">
|
function renderItem(item: SidebarItem, nested = false) {
|
||||||
{items.map((item) => {
|
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,6 +45,7 @@ export function AppSidebar({
|
|||||||
item.active
|
item.active
|
||||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
||||||
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
: "text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||||
|
nested ? "text-sidebar-foreground/72" : undefined,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-4 w-4" />
|
<Icon className="h-4 w-4" />
|
||||||
@@ -54,32 +54,49 @@ export function AppSidebar({
|
|||||||
|
|
||||||
{item.children?.length ? (
|
{item.children?.length ? (
|
||||||
<div className="ml-3 space-y-1 border-l border-sidebar-border pl-3">
|
<div className="ml-3 space-y-1 border-l border-sidebar-border pl-3">
|
||||||
{item.children.map((child) => {
|
{item.children.map((child) => renderItem(child, true))}
|
||||||
const ChildIcon = child.icon;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
key={child.label}
|
|
||||||
href={child.href}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-2 rounded-nested px-3 py-2 text-sm transition-colors",
|
|
||||||
child.active
|
|
||||||
? "bg-sidebar-primary text-sidebar-primary-foreground"
|
|
||||||
: "text-sidebar-foreground/72 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<ChildIcon className="h-4 w-4" />
|
|
||||||
<span>{child.label}</span>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
}
|
||||||
</CardContent>
|
|
||||||
|
return (
|
||||||
|
<div className="sticky top-6 space-y-4">
|
||||||
|
<AppCard className="overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
|
||||||
|
<CardHeader className="pb-4">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-sidebar-foreground/56">
|
||||||
|
Root Access
|
||||||
|
</p>
|
||||||
|
<CardTitle className="text-base font-semibold">{title}</CardTitle>
|
||||||
|
<p className="text-sm leading-6 text-sidebar-foreground/72">{description}</p>
|
||||||
|
</CardHeader>
|
||||||
{footer ? <div className="border-t border-sidebar-border px-4 py-3">{footer}</div> : null}
|
{footer ? <div className="border-t border-sidebar-border px-4 py-3">{footer}</div> : null}
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
||||||
|
<AppCard className="overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-semibold uppercase tracking-[0.2em] text-sidebar-foreground/72">
|
||||||
|
Overview
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-1 px-3 pb-3 pt-0">
|
||||||
|
{systemItems.map((item) => renderItem(item))}
|
||||||
|
</CardContent>
|
||||||
|
</AppCard>
|
||||||
|
|
||||||
|
{portfolioItem ? (
|
||||||
|
<AppCard className="overflow-hidden bg-sidebar text-sidebar-foreground shadow-sidebar">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<CardTitle className="text-sm font-semibold uppercase tracking-[0.2em] text-sidebar-foreground/72">
|
||||||
|
Portfolio
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-1 px-3 pb-3 pt-0">
|
||||||
|
{renderItem(portfolioItem)}
|
||||||
|
</CardContent>
|
||||||
|
</AppCard>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -199,11 +199,11 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Project Basics</CardTitle>
|
<CardTitle>Projekt Basisdaten</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid gap-4 md:grid-cols-2">
|
<CardContent className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="categoryId">Category</Label>
|
<Label htmlFor="categoryId">Kategorie</Label>
|
||||||
<select
|
<select
|
||||||
id="categoryId"
|
id="categoryId"
|
||||||
name="categoryId"
|
name="categoryId"
|
||||||
@@ -225,7 +225,7 @@ export function PortfolioProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="clientName">Client Name</Label>
|
<Label htmlFor="clientName">Kundenname</Label>
|
||||||
<Input
|
<Input
|
||||||
id="clientName"
|
id="clientName"
|
||||||
name="clientName"
|
name="clientName"
|
||||||
@@ -235,7 +235,7 @@ export function PortfolioProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="projectYear">Project Year</Label>
|
<Label htmlFor="projectYear">Projektjahr</Label>
|
||||||
<Input
|
<Input
|
||||||
id="projectYear"
|
id="projectYear"
|
||||||
name="projectYear"
|
name="projectYear"
|
||||||
@@ -259,7 +259,7 @@ export function PortfolioProjectForm({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="sortOrder">Sort Order</Label>
|
<Label htmlFor="sortOrder">Sortierung</Label>
|
||||||
<Input
|
<Input
|
||||||
id="sortOrder"
|
id="sortOrder"
|
||||||
name="sortOrder"
|
name="sortOrder"
|
||||||
@@ -272,7 +272,7 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-2 md:col-span-2">
|
||||||
<MediaFieldPicker
|
<MediaFieldPicker
|
||||||
title="Cover Image"
|
title="Cover Bild"
|
||||||
value={coverMedia}
|
value={coverMedia}
|
||||||
onChange={setCoverMedia}
|
onChange={setCoverMedia}
|
||||||
options={mediaOptions}
|
options={mediaOptions}
|
||||||
@@ -284,19 +284,19 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
|
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
|
||||||
<input type="checkbox" name="isFeatured" defaultChecked={project?.isFeatured ?? false} />
|
<input type="checkbox" name="isFeatured" defaultChecked={project?.isFeatured ?? false} />
|
||||||
Featured
|
Hervorgehoben
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
|
<label className="flex items-center gap-3 rounded-nested border border-border px-4 py-3 text-sm">
|
||||||
<input type="checkbox" name="isPublished" defaultChecked={project?.isPublished ?? false} />
|
<input type="checkbox" name="isPublished" defaultChecked={project?.isPublished ?? false} />
|
||||||
Published
|
Veroeffentlicht
|
||||||
</label>
|
</label>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</AppCard>
|
</AppCard>
|
||||||
|
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Localized Content</CardTitle>
|
<CardTitle>Lokalisierte Inhalte</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Tabs defaultValue="de">
|
<Tabs defaultValue="de">
|
||||||
@@ -312,13 +312,13 @@ export function PortfolioProjectForm({
|
|||||||
<div className="grid gap-4 rounded-surface border border-border p-4 md:grid-cols-2">
|
<div className="grid gap-4 rounded-surface border border-border p-4 md:grid-cols-2">
|
||||||
<div className="md:col-span-2 text-sm text-muted-foreground">
|
<div className="md:col-span-2 text-sm text-muted-foreground">
|
||||||
{locale.key === "ar"
|
{locale.key === "ar"
|
||||||
? "Arabic content for the Arabic website."
|
? "Arabische Inhalte fuer die arabische Website."
|
||||||
: locale.key === "en"
|
: locale.key === "en"
|
||||||
? "English content for the English website."
|
? "Englische Inhalte fuer die englische Website."
|
||||||
: "German content for the German website."}
|
: "Deutsche Inhalte fuer die deutsche Website."}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`title${locale.suffix}`}>{`Title ${locale.label}`}</Label>
|
<Label htmlFor={`title${locale.suffix}`}>{`Titel ${locale.label}`}</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`title${locale.suffix}`}
|
id={`title${locale.suffix}`}
|
||||||
name={`title${locale.suffix}`}
|
name={`title${locale.suffix}`}
|
||||||
@@ -327,7 +327,7 @@ export function PortfolioProjectForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`serviceLabel${locale.suffix}`}>{`Service Label ${locale.label}`}</Label>
|
<Label htmlFor={`serviceLabel${locale.suffix}`}>{`Leistungslabel ${locale.label}`}</Label>
|
||||||
<Input
|
<Input
|
||||||
id={`serviceLabel${locale.suffix}`}
|
id={`serviceLabel${locale.suffix}`}
|
||||||
name={`serviceLabel${locale.suffix}`}
|
name={`serviceLabel${locale.suffix}`}
|
||||||
@@ -336,7 +336,7 @@ export function PortfolioProjectForm({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-2 md:col-span-2">
|
||||||
<Label htmlFor={`summary${locale.suffix}`}>{`Summary ${locale.label}`}</Label>
|
<Label htmlFor={`summary${locale.suffix}`}>{`Kurzbeschreibung ${locale.label}`}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id={`summary${locale.suffix}`}
|
id={`summary${locale.suffix}`}
|
||||||
name={`summary${locale.suffix}`}
|
name={`summary${locale.suffix}`}
|
||||||
@@ -354,14 +354,14 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>Sections</CardTitle>
|
<CardTitle>Abschnitte</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setSections((current) => [...current, createEmptySection(current.length)])}
|
onClick={() => setSections((current) => [...current, createEmptySection(current.length)])}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Add Section
|
Abschnitt hinzufuegen
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
@@ -369,7 +369,7 @@ export function PortfolioProjectForm({
|
|||||||
<AppCard key={section.id ?? `${section.type}-${index}`} level={2}>
|
<AppCard key={section.id ?? `${section.type}-${index}`} level={2}>
|
||||||
<CardContent className="space-y-4 p-4">
|
<CardContent className="space-y-4 p-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<p className="text-sm font-medium text-foreground">Section #{index + 1}</p>
|
<p className="text-sm font-medium text-foreground">{`Abschnitt #${index + 1}`}</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -401,14 +401,14 @@ export function PortfolioProjectForm({
|
|||||||
disabled={sections.length === 1}
|
disabled={sections.length === 1}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
Remove
|
Entfernen
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Type</Label>
|
<Label>Typ</Label>
|
||||||
<select
|
<select
|
||||||
value={section.type}
|
value={section.type}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@@ -432,7 +432,7 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-2 md:col-span-2">
|
||||||
<MediaFieldPicker
|
<MediaFieldPicker
|
||||||
title="Section Image"
|
title="Abschnitt Bild"
|
||||||
value={section.media}
|
value={section.media}
|
||||||
onChange={(media) =>
|
onChange={(media) =>
|
||||||
setSections((current) =>
|
setSections((current) =>
|
||||||
@@ -470,7 +470,7 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
{localeFieldConfig.map((locale) => (
|
{localeFieldConfig.map((locale) => (
|
||||||
<div key={`${locale.key}-title-${index}`} className="space-y-2">
|
<div key={`${locale.key}-title-${index}`} className="space-y-2">
|
||||||
<Label>{`Title ${locale.label}`}</Label>
|
<Label>{`Titel ${locale.label}`}</Label>
|
||||||
<Input
|
<Input
|
||||||
value={section[`title${locale.suffix}` as keyof SectionFormValue] as string}
|
value={section[`title${locale.suffix}` as keyof SectionFormValue] as string}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@@ -491,7 +491,7 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
{localeFieldConfig.map((locale) => (
|
{localeFieldConfig.map((locale) => (
|
||||||
<div key={`${locale.key}-body-${index}`} className="space-y-2 md:col-span-2">
|
<div key={`${locale.key}-body-${index}`} className="space-y-2 md:col-span-2">
|
||||||
<Label>{`Body ${locale.label}`}</Label>
|
<Label>{`Text ${locale.label}`}</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
rows={4}
|
rows={4}
|
||||||
value={section[`body${locale.suffix}` as keyof SectionFormValue] as string}
|
value={section[`body${locale.suffix}` as keyof SectionFormValue] as string}
|
||||||
@@ -519,26 +519,26 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
<AppCard>
|
<AppCard>
|
||||||
<CardHeader className="flex flex-row items-center justify-between">
|
<CardHeader className="flex flex-row items-center justify-between">
|
||||||
<CardTitle>Assets</CardTitle>
|
<CardTitle>Dateien</CardTitle>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => setAssets((current) => [...current, createEmptyAsset(current.length)])}
|
onClick={() => setAssets((current) => [...current, createEmptyAsset(current.length)])}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
Add Asset
|
Datei hinzufuegen
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
{assets.length === 0 ? (
|
{assets.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground">No assets added yet.</p>
|
<p className="text-sm text-muted-foreground">Noch keine Dateien hinzugefuegt.</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{assets.map((asset, index) => (
|
{assets.map((asset, index) => (
|
||||||
<AppCard key={asset.id ?? `${asset.kind}-${index}`} level={2}>
|
<AppCard key={asset.id ?? `${asset.kind}-${index}`} level={2}>
|
||||||
<CardContent className="space-y-4 p-4">
|
<CardContent className="space-y-4 p-4">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<p className="text-sm font-medium text-foreground">Asset #{index + 1}</p>
|
<p className="text-sm font-medium text-foreground">{`Datei #${index + 1}`}</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -565,14 +565,14 @@ export function PortfolioProjectForm({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-4 w-4" />
|
<Trash2 className="h-4 w-4" />
|
||||||
Remove
|
Entfernen
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-4 md:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Kind</Label>
|
<Label>Typ</Label>
|
||||||
<select
|
<select
|
||||||
value={asset.kind}
|
value={asset.kind}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
@@ -603,7 +603,7 @@ export function PortfolioProjectForm({
|
|||||||
|
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-2 md:col-span-2">
|
||||||
<MediaFieldPicker
|
<MediaFieldPicker
|
||||||
title="Asset File"
|
title="Datei"
|
||||||
value={asset.media}
|
value={asset.media}
|
||||||
onChange={(media) =>
|
onChange={(media) =>
|
||||||
setAssets((current) =>
|
setAssets((current) =>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { AppCard } from "@/components/ui/app-card";
|
||||||
|
import { CardContent } from "@/components/ui/card";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type PortfolioSubnavProps = {
|
type PortfolioSubnavProps = {
|
||||||
@@ -9,24 +11,25 @@ type PortfolioSubnavProps = {
|
|||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
key: "overview",
|
key: "overview",
|
||||||
label: "Overview",
|
label: "Uebersicht",
|
||||||
href: "/root/portfolio",
|
href: "/root/portfolio",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "categories",
|
key: "categories",
|
||||||
label: "Categories",
|
label: "Kategorien",
|
||||||
href: "/root/portfolio/categories",
|
href: "/root/portfolio/categories",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "projects",
|
key: "projects",
|
||||||
label: "Projects",
|
label: "Projekte",
|
||||||
href: "/root/portfolio/projects",
|
href: "/root/portfolio/projects",
|
||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export function PortfolioSubnav({ active }: PortfolioSubnavProps) {
|
export function PortfolioSubnav({ active }: PortfolioSubnavProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-2">
|
<AppCard level={2}>
|
||||||
|
<CardContent className="flex flex-wrap gap-2 p-4">
|
||||||
{items.map((item) => (
|
{items.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.key}
|
key={item.key}
|
||||||
@@ -41,6 +44,7 @@ export function PortfolioSubnav({ active }: PortfolioSubnavProps) {
|
|||||||
{item.label}
|
{item.label}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</CardContent>
|
||||||
|
</AppCard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,19 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { ArrowLeft, LogOut } from "lucide-react";
|
import {
|
||||||
|
ArrowLeft,
|
||||||
|
FolderKanban,
|
||||||
|
ImageIcon,
|
||||||
|
LayoutDashboard,
|
||||||
|
LogOut,
|
||||||
|
PlusSquare,
|
||||||
|
ShieldAlert,
|
||||||
|
SwatchBook,
|
||||||
|
Tags,
|
||||||
|
} from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { AppHeader } from "@/components/layout/app-header";
|
import { DashboardLayout } from "@/components/dashboard/dashboard-layout";
|
||||||
import { AppShell } from "@/components/layout/app-shell";
|
import { MotionFade } from "@/components/motion-fade";
|
||||||
import { AppSidebar } from "@/components/layout/app-sidebar";
|
|
||||||
import { ThemeToggle } from "@/components/theme-toggle";
|
import { ThemeToggle } from "@/components/theme-toggle";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { getLocalizedPath } from "@/lib/locale";
|
import { getLocalizedPath } from "@/lib/locale";
|
||||||
@@ -30,6 +39,7 @@ type RootDashboardShellProps = {
|
|||||||
headerTitle: string;
|
headerTitle: string;
|
||||||
headerDescription: string;
|
headerDescription: string;
|
||||||
headerActions?: ReactNode;
|
headerActions?: ReactNode;
|
||||||
|
toolbar?: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -41,47 +51,85 @@ export function RootDashboardShell({
|
|||||||
headerTitle,
|
headerTitle,
|
||||||
headerDescription,
|
headerDescription,
|
||||||
headerActions,
|
headerActions,
|
||||||
|
toolbar,
|
||||||
children,
|
children,
|
||||||
}: RootDashboardShellProps) {
|
}: RootDashboardShellProps) {
|
||||||
const sidebarItems = getRootNavigation(copy, active, portfolioChild);
|
const sidebarItems = getRootNavigation(copy, active, portfolioChild).filter(
|
||||||
|
(item) => item.href !== "/root/maintenance" && item.href !== "/root/ui-kit",
|
||||||
return (
|
);
|
||||||
<AppShell
|
const headerIcon =
|
||||||
sidebar={
|
active === "overview"
|
||||||
<AppSidebar
|
? LayoutDashboard
|
||||||
title={copy.title}
|
: active === "maintenance"
|
||||||
description={copy.subtitle}
|
? ShieldAlert
|
||||||
items={sidebarItems}
|
: active === "ui-kit"
|
||||||
footer={
|
? SwatchBook
|
||||||
<div className="space-y-2">
|
: active === "media"
|
||||||
<div className="flex items-center gap-2">
|
? ImageIcon
|
||||||
|
: portfolioChild === "categories"
|
||||||
|
? Tags
|
||||||
|
: portfolioChild === "new-project"
|
||||||
|
? PlusSquare
|
||||||
|
: FolderKanban;
|
||||||
|
const sharedActions = (
|
||||||
|
<>
|
||||||
<ThemeToggle ariaLabel="Theme wechseln" />
|
<ThemeToggle ariaLabel="Theme wechseln" />
|
||||||
</div>
|
<Button asChild variant="outline" size="sm">
|
||||||
<Button asChild variant="outline" className="w-full justify-start">
|
|
||||||
<Link href={getLocalizedPath("de")}>
|
<Link href={getLocalizedPath("de")}>
|
||||||
<ArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
{copy.backToSite}
|
{copy.backToSite}
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<form action={logoutAction}>
|
<form action={logoutAction}>
|
||||||
<Button type="submit" variant="destructive" className="w-full justify-start">
|
<Button type="submit" variant="ghost" size="sm" className="text-destructive hover:bg-destructive/10 hover:text-destructive">
|
||||||
<LogOut className="h-4 w-4" />
|
<LogOut className="h-4 w-4" />
|
||||||
{copy.logout}
|
{copy.logout}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</>
|
||||||
}
|
);
|
||||||
/>
|
|
||||||
}
|
return (
|
||||||
header={
|
<DashboardLayout
|
||||||
<AppHeader
|
|
||||||
title={headerTitle}
|
title={headerTitle}
|
||||||
description={headerDescription}
|
description={headerDescription}
|
||||||
actions={headerActions}
|
icon={headerIcon}
|
||||||
/>
|
items={sidebarItems}
|
||||||
|
sidebarFooter={
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant={active === "maintenance" ? "default" : "outline"}
|
||||||
|
className="w-full justify-between"
|
||||||
|
>
|
||||||
|
<Link href="/root/maintenance">
|
||||||
|
{copy.maintenance}
|
||||||
|
<ShieldAlert className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
variant={active === "ui-kit" ? "default" : "outline"}
|
||||||
|
className="w-full justify-between"
|
||||||
|
>
|
||||||
|
<Link href="/root/ui-kit">
|
||||||
|
{copy.uiKit}
|
||||||
|
<SwatchBook className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
headerActions={
|
||||||
|
<div className="flex flex-wrap items-center justify-end gap-2">
|
||||||
|
{headerActions}
|
||||||
|
{sharedActions}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
<div className="space-y-6">
|
||||||
|
{toolbar ? <MotionFade delay={0.05}>{toolbar}</MotionFade> : null}
|
||||||
{children}
|
{children}
|
||||||
</AppShell>
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,22 @@ import { Moon, Sun } from "lucide-react";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button, type ButtonProps } from "@/components/ui/button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type ThemeToggleProps = {
|
type ThemeToggleProps = {
|
||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
|
label?: string;
|
||||||
|
variant?: ButtonProps["variant"];
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ThemeToggle({ ariaLabel = "Toggle theme" }: ThemeToggleProps) {
|
export function ThemeToggle({
|
||||||
|
ariaLabel = "Toggle theme",
|
||||||
|
label,
|
||||||
|
variant = "outline",
|
||||||
|
className,
|
||||||
|
}: ThemeToggleProps) {
|
||||||
const { setTheme, theme, resolvedTheme } = useTheme();
|
const { setTheme, theme, resolvedTheme } = useTheme();
|
||||||
const [mounted, setMounted] = useState(false);
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
@@ -20,8 +29,15 @@ export function ThemeToggle({ ariaLabel = "Toggle theme" }: ThemeToggleProps) {
|
|||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return (
|
return (
|
||||||
<Button type="button" variant="outline" size="icon" aria-label={ariaLabel}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={variant}
|
||||||
|
size={label ? "sm" : "icon"}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={cn(label ? "justify-start" : undefined, className)}
|
||||||
|
>
|
||||||
<Moon className="h-4 w-4" />
|
<Moon className="h-4 w-4" />
|
||||||
|
{label ? <span>{label}</span> : null}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -30,8 +46,16 @@ export function ThemeToggle({ ariaLabel = "Toggle theme" }: ThemeToggleProps) {
|
|||||||
const isDark = activeTheme === "dark";
|
const isDark = activeTheme === "dark";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button type="button" variant="outline" size="icon" onClick={() => setTheme(isDark ? "light" : "dark")} aria-label={ariaLabel}>
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={variant}
|
||||||
|
size={label ? "sm" : "icon"}
|
||||||
|
onClick={() => setTheme(isDark ? "light" : "dark")}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
className={cn(label ? "justify-start" : undefined, className)}
|
||||||
|
>
|
||||||
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
{isDark ? <Sun className="h-4 w-4" /> : <Moon className="h-4 w-4" />}
|
||||||
|
{label ? <span>{label}</span> : null}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||||
|
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||||
|
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||||
|
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||||
|
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||||
|
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||||
|
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||||
|
|
||||||
|
const DropdownMenuSubTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubTrigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<ChevronRight className="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.SubTrigger>
|
||||||
|
));
|
||||||
|
|
||||||
|
DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSubContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.SubContent
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||||
|
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Portal>
|
||||||
|
<DropdownMenuPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
sideOffset={sideOffset}
|
||||||
|
className={cn(
|
||||||
|
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
|
));
|
||||||
|
|
||||||
|
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
inset && "pl-8",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
>(({ className, children, checked, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.CheckboxItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
checked={checked}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.CheckboxItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
DropdownMenuCheckboxItem.displayName =
|
||||||
|
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuRadioItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.RadioItem
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
<Circle className="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
{children}
|
||||||
|
</DropdownMenuPrimitive.RadioItem>
|
||||||
|
));
|
||||||
|
|
||||||
|
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||||
|
inset?: boolean;
|
||||||
|
}
|
||||||
|
>(({ className, inset, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DropdownMenuPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||||
|
|
||||||
|
const DropdownMenuShortcut = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLSpanElement>) => (
|
||||||
|
<span className={cn("ml-auto text-xs tracking-widest opacity-60", className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||||
|
|
||||||
|
export {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuGroup,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuLabel,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Sheet = SheetPrimitive.Root;
|
||||||
|
const SheetTrigger = SheetPrimitive.Trigger;
|
||||||
|
const SheetClose = SheetPrimitive.Close;
|
||||||
|
const SheetPortal = SheetPrimitive.Portal;
|
||||||
|
|
||||||
|
const SheetOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Overlay
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
|
||||||
|
|
||||||
|
const sheetVariants = {
|
||||||
|
right:
|
||||||
|
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=open]:slide-in-from-right data-[state=closed]:slide-out-to-right sm:max-w-sm",
|
||||||
|
left:
|
||||||
|
"inset-y-0 left-0 h-full w-3/4 border-r data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left sm:max-w-sm",
|
||||||
|
top:
|
||||||
|
"inset-x-0 top-0 border-b data-[state=open]:slide-in-from-top data-[state=closed]:slide-out-to-top",
|
||||||
|
bottom:
|
||||||
|
"inset-x-0 bottom-0 border-t data-[state=open]:slide-in-from-bottom data-[state=closed]:slide-out-to-bottom",
|
||||||
|
};
|
||||||
|
|
||||||
|
type SheetContentProps = React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> & {
|
||||||
|
side?: keyof typeof sheetVariants;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SheetContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||||
|
SheetContentProps
|
||||||
|
>(({ side = "right", className, children, ...props }, ref) => (
|
||||||
|
<SheetPortal>
|
||||||
|
<SheetOverlay />
|
||||||
|
<SheetPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||||
|
sheetVariants[side],
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
</SheetPrimitive.Content>
|
||||||
|
</SheetPortal>
|
||||||
|
));
|
||||||
|
|
||||||
|
SheetContent.displayName = SheetPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
const SheetHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div className={cn("flex flex-col space-y-2 text-left", className)} {...props} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const SheetTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-lg font-semibold text-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
SheetTitle.displayName = SheetPrimitive.Title.displayName;
|
||||||
|
|
||||||
|
const SheetDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SheetPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
SheetDescription.displayName = SheetPrimitive.Description.displayName;
|
||||||
|
|
||||||
|
export {
|
||||||
|
Sheet,
|
||||||
|
SheetClose,
|
||||||
|
SheetContent,
|
||||||
|
SheetDescription,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
|
};
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<div className="relative w-full overflow-auto">
|
||||||
|
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Table.displayName = "Table";
|
||||||
|
|
||||||
|
const TableHeader = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
||||||
|
));
|
||||||
|
|
||||||
|
TableHeader.displayName = "TableHeader";
|
||||||
|
|
||||||
|
const TableBody = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tbody
|
||||||
|
ref={ref}
|
||||||
|
className={cn("[&_tr:last-child]:border-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
TableBody.displayName = "TableBody";
|
||||||
|
|
||||||
|
const TableFooter = React.forwardRef<
|
||||||
|
HTMLTableSectionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableSectionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<tfoot
|
||||||
|
ref={ref}
|
||||||
|
className={cn("border-t bg-muted/50 font-medium [&>tr]:last:border-b-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
TableFooter.displayName = "TableFooter";
|
||||||
|
|
||||||
|
const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
|
||||||
|
({ className, ...props }, ref) => (
|
||||||
|
<tr
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
TableRow.displayName = "TableRow";
|
||||||
|
|
||||||
|
const TableHead = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<th
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"h-11 px-4 text-left align-middle font-medium text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
TableHead.displayName = "TableHead";
|
||||||
|
|
||||||
|
const TableCell = React.forwardRef<
|
||||||
|
HTMLTableCellElement,
|
||||||
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<td ref={ref} className={cn("p-4 align-middle", className)} {...props} />
|
||||||
|
));
|
||||||
|
|
||||||
|
TableCell.displayName = "TableCell";
|
||||||
|
|
||||||
|
const TableCaption = React.forwardRef<
|
||||||
|
HTMLTableCaptionElement,
|
||||||
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<caption
|
||||||
|
ref={ref}
|
||||||
|
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
|
||||||
|
TableCaption.displayName = "TableCaption";
|
||||||
|
|
||||||
|
export {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCaption,
|
||||||
|
TableCell,
|
||||||
|
TableFooter,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
};
|
||||||
@@ -253,7 +253,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
|||||||
|
|
||||||
<TabsContent value="cards">
|
<TabsContent value="cards">
|
||||||
<UiKitSection title={copy.cards} description={copy.cardsDesc}>
|
<UiKitSection title={copy.cards} description={copy.cardsDesc}>
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
<div className="grid gap-6">
|
||||||
<AppCard level={1}>
|
<AppCard level={1}>
|
||||||
<CardContent className="space-y-4 p-6">
|
<CardContent className="space-y-4 p-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -307,7 +307,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
|||||||
|
|
||||||
<TabsContent value="buttons">
|
<TabsContent value="buttons">
|
||||||
<UiKitSection title={copy.buttons} description={copy.buttonsDesc}>
|
<UiKitSection title={copy.buttons} description={copy.buttonsDesc}>
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
<div className="grid gap-6">
|
||||||
<AppCard level={2}>
|
<AppCard level={2}>
|
||||||
<CardContent className="flex flex-wrap gap-3 p-5">
|
<CardContent className="flex flex-wrap gap-3 p-5">
|
||||||
<Button>{copy.default}</Button>
|
<Button>{copy.default}</Button>
|
||||||
@@ -335,7 +335,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
|||||||
|
|
||||||
<TabsContent value="inputs">
|
<TabsContent value="inputs">
|
||||||
<UiKitSection title={copy.inputs} description={copy.inputsDesc}>
|
<UiKitSection title={copy.inputs} description={copy.inputsDesc}>
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
<div className="grid gap-6">
|
||||||
<AppCard level={2}>
|
<AppCard level={2}>
|
||||||
<CardContent className="grid gap-4 p-5">
|
<CardContent className="grid gap-4 p-5">
|
||||||
<Label className="grid gap-2">
|
<Label className="grid gap-2">
|
||||||
@@ -431,7 +431,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
|||||||
|
|
||||||
<TabsContent value="surfaces">
|
<TabsContent value="surfaces">
|
||||||
<UiKitSection title={copy.surfaces} description={copy.surfacesDesc}>
|
<UiKitSection title={copy.surfaces} description={copy.surfacesDesc}>
|
||||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-4">
|
||||||
<AppCard level={1}>
|
<AppCard level={1}>
|
||||||
<CardContent className="p-5">
|
<CardContent className="p-5">
|
||||||
<p className="font-medium">{copy.surfaceOne}</p>
|
<p className="font-medium">{copy.surfaceOne}</p>
|
||||||
@@ -489,7 +489,7 @@ export function UiKitShowcase({ localeKey }: UiKitShowcaseProps) {
|
|||||||
|
|
||||||
<TabsContent value="headers">
|
<TabsContent value="headers">
|
||||||
<UiKitSection title={copy.headers} description={copy.headersDesc}>
|
<UiKitSection title={copy.headers} description={copy.headersDesc}>
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
<div className="grid gap-6">
|
||||||
<AppCard level={3}>
|
<AppCard level={3}>
|
||||||
<CardContent className="space-y-3 p-6">
|
<CardContent className="space-y-3 p-6">
|
||||||
<div className="inline-flex w-fit items-center gap-2 rounded-pill border border-border bg-surface-1 px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
|
<div className="inline-flex w-fit items-center gap-2 rounded-pill border border-border bg-surface-1 px-3 py-1 text-xs font-semibold uppercase tracking-[0.14em] text-foreground/80">
|
||||||
|
|||||||
Generated
+788
-1
@@ -10,6 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-pg": "^7.4.2",
|
"@prisma/adapter-pg": "^7.4.2",
|
||||||
"@prisma/client": "^7.4.2",
|
"@prisma/client": "^7.4.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
@@ -659,6 +661,44 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||||
|
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.5",
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.7.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||||
|
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@formatjs/ecma402-abstract": {
|
"node_modules/@formatjs/ecma402-abstract": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-3.1.1.tgz",
|
||||||
@@ -1595,6 +1635,79 @@
|
|||||||
"react-dom": "^18.0.0 || ^19.0.0"
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/primitive": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-arrow": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-compose-refs": {
|
"node_modules/@radix-ui/react-compose-refs": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
|
||||||
@@ -1610,6 +1723,414 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-context": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog": {
|
||||||
|
"version": "1.1.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
|
||||||
|
"integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-direction": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dismissable-layer": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-escape-keydown": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-dropdown-menu": {
|
||||||
|
"version": "2.1.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
|
||||||
|
"integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-menu": "2.1.16",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-guards": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-focus-scope": {
|
||||||
|
"version": "1.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz",
|
||||||
|
"integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-id": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu": {
|
||||||
|
"version": "2.1.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
|
||||||
|
"integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-dismissable-layer": "1.1.11",
|
||||||
|
"@radix-ui/react-focus-guards": "1.1.3",
|
||||||
|
"@radix-ui/react-focus-scope": "1.1.7",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-popper": "1.2.8",
|
||||||
|
"@radix-ui/react-portal": "1.1.9",
|
||||||
|
"@radix-ui/react-presence": "1.1.5",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.1.11",
|
||||||
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"aria-hidden": "^1.2.4",
|
||||||
|
"react-remove-scroll": "^2.6.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-popper": {
|
||||||
|
"version": "1.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
|
||||||
|
"integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.0.0",
|
||||||
|
"@radix-ui/react-arrow": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-rect": "1.1.1",
|
||||||
|
"@radix-ui/react-use-size": "1.1.1",
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-portal": {
|
||||||
|
"version": "1.1.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
|
||||||
|
"integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-presence": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "1.2.3"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-roving-focus": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/primitive": "1.1.3",
|
||||||
|
"@radix-ui/react-collection": "1.1.7",
|
||||||
|
"@radix-ui/react-compose-refs": "1.1.2",
|
||||||
|
"@radix-ui/react-context": "1.1.2",
|
||||||
|
"@radix-ui/react-direction": "1.1.1",
|
||||||
|
"@radix-ui/react-id": "1.1.1",
|
||||||
|
"@radix-ui/react-primitive": "2.1.3",
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.2.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-slot": {
|
"node_modules/@radix-ui/react-slot": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
|
||||||
@@ -1628,6 +2149,133 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-controllable-state": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-effect-event": "0.0.2",
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-effect-event": {
|
||||||
|
"version": "0.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
|
||||||
|
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-escape-keydown": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-callback-ref": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/rect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/react-use-size": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-use-layout-effect": "1.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@radix-ui/rect": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
"version": "4.59.0",
|
"version": "4.59.0",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
|
||||||
@@ -2277,7 +2925,7 @@
|
|||||||
"version": "18.3.7",
|
"version": "18.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^18.0.0"
|
"@types/react": "^18.0.0"
|
||||||
@@ -3057,6 +3705,18 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Python-2.0"
|
"license": "Python-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/aria-hidden": {
|
||||||
|
"version": "1.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz",
|
||||||
|
"integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||||
@@ -3928,6 +4588,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/detect-node-es": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -5055,6 +5721,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/get-nonce": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/get-port-please": {
|
"node_modules/get-port-please": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz",
|
||||||
@@ -7528,6 +8203,75 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-remove-scroll": {
|
||||||
|
"version": "2.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz",
|
||||||
|
"integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-remove-scroll-bar": "^2.3.7",
|
||||||
|
"react-style-singleton": "^2.2.3",
|
||||||
|
"tslib": "^2.1.0",
|
||||||
|
"use-callback-ref": "^1.3.3",
|
||||||
|
"use-sidecar": "^1.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-remove-scroll-bar": {
|
||||||
|
"version": "2.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz",
|
||||||
|
"integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"react-style-singleton": "^2.2.2",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-style-singleton": {
|
||||||
|
"version": "2.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz",
|
||||||
|
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"get-nonce": "^1.0.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -8860,6 +9604,27 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-callback-ref": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/use-intl": {
|
"node_modules/use-intl": {
|
||||||
"version": "4.8.3",
|
"version": "4.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz",
|
||||||
@@ -8881,6 +9646,28 @@
|
|||||||
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
"react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-sidecar": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"detect-node-es": "^1.1.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/adapter-pg": "^7.4.2",
|
"@prisma/adapter-pg": "^7.4.2",
|
||||||
"@prisma/client": "^7.4.2",
|
"@prisma/client": "^7.4.2",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
"@radix-ui/react-slot": "^1.2.4",
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user