Landingpages als eigene Gattung, erster Typ: Mehrere Links
Die sechs noch offenen Typen haben kein fremdes Ziel, auf das man weiterleiten koennte — sie brauchen eine Seite. Dafuer bekommt der Typ-Vertrag `hasLandingPage`: /r/[slug] schickt solche Codes auf /s/[slug], statt `redirectTarget` zu befragen. Die oeffentlichen Seiten duerfen die Navigation der Anwendung nicht zeigen — wer einen Code scannt, hat mit „Verwaltung" nichts zu tun. Deshalb liegt das App-Chrome jetzt in der Routengruppe (app); das Wurzel-Layout traegt nur noch html und body. Die URLs aendern sich dadurch nicht.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Icon, type IconName } from "@/components/icons";
|
||||
import { Notice } from "@/components/ui";
|
||||
import { isAuthEnabled } from "@/lib/auth/actor";
|
||||
import { requirePagePermission } from "@/lib/auth/guard";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
export const metadata: Metadata = { title: "Verwaltung" };
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function Tile({
|
||||
href,
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
hint,
|
||||
}: {
|
||||
href: string;
|
||||
icon: IconName;
|
||||
title: string;
|
||||
description: string;
|
||||
hint: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="flex items-start gap-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm transition-all hover:border-indigo-300 hover:shadow-md"
|
||||
>
|
||||
<span className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-indigo-50 text-indigo-600">
|
||||
<Icon name={icon} className="size-6" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="font-semibold text-slate-900">{title}</span>
|
||||
<span className="mt-1 block text-sm text-slate-600">{description}</span>
|
||||
<span className="mt-2 block text-xs text-slate-500 tabular-nums">
|
||||
{hint}
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default async function AdminPage() {
|
||||
await requirePagePermission("instance:manage");
|
||||
|
||||
const [userCount, groupCount, codeCount, domainCount] = await Promise.all([
|
||||
prisma.user.count(),
|
||||
prisma.group.count(),
|
||||
prisma.qrCode.count(),
|
||||
prisma.domain.count(),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-4xl px-4 py-8 sm:px-6 sm:py-10">
|
||||
<div className="mb-7">
|
||||
<h1 className="text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">
|
||||
Verwaltung
|
||||
</h1>
|
||||
<p className="mt-2 text-slate-600">
|
||||
Benutzer, Gruppen und die verfügbaren QR-Typen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!isAuthEnabled() ? (
|
||||
<div className="mb-6">
|
||||
<Notice tone="info">
|
||||
<span className="font-medium">Die Anmeldung ist ausgeschaltet.</span>{" "}
|
||||
Jeder, der die Anwendung erreicht, darf alles. Zum Einschalten
|
||||
<code className="mx-1 rounded bg-white/70 px-1.5 py-0.5 font-mono text-xs">
|
||||
AUTH_ENABLED=true
|
||||
</code>
|
||||
in der <code className="font-mono text-xs">.env</code> setzen und
|
||||
<code className="mx-1 rounded bg-white/70 px-1.5 py-0.5 font-mono text-xs">
|
||||
docker compose up -d
|
||||
</code>
|
||||
ausführen. Beim nächsten Aufruf legst du den ersten Administrator
|
||||
an. Benutzer und Gruppen lassen sich schon jetzt vorbereiten.
|
||||
</Notice>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<Tile
|
||||
href="/verwaltung/benutzer"
|
||||
icon="contact"
|
||||
title="Benutzer"
|
||||
description="Konten anlegen, Passwörter zurücksetzen, deaktivieren."
|
||||
hint={`${userCount} ${userCount === 1 ? "Benutzer" : "Benutzer"} angelegt`}
|
||||
/>
|
||||
<Tile
|
||||
href="/verwaltung/gruppen"
|
||||
icon="list"
|
||||
title="Gruppen"
|
||||
description="Gruppen anlegen und Mitglieder mit Rollen zuordnen."
|
||||
hint={`${groupCount} ${groupCount === 1 ? "Gruppe" : "Gruppen"}, ${codeCount} ${codeCount === 1 ? "Code" : "Codes"}`}
|
||||
/>
|
||||
<Tile
|
||||
href="/verwaltung/domains"
|
||||
icon="globe"
|
||||
title="Domains"
|
||||
description="Adressen, unter denen Kurz-URLs ausgeliefert werden."
|
||||
hint={`${domainCount} ${domainCount === 1 ? "Domain" : "Domains"} registriert`}
|
||||
/>
|
||||
<Tile
|
||||
href="/verwaltung/typen"
|
||||
icon="qr"
|
||||
title="QR-Typen"
|
||||
description="Festlegen, welche Typen auf der Startseite erscheinen."
|
||||
hint="15 Typen insgesamt"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user