feat(t-paliad-054): /admin landing page indexing admin sub-pages
`/admin` was 404 — only `/admin/team` existed. Add a browseable index so
the admin area has a root, with the existing Team-Verwaltung tile alongside
greyed-out roadmap placeholders (Departments, Audit-Log, Email-Templates,
Feature-Flags) so admins see what's coming.
- internal/handlers/admin_users.go: handleAdminIndexPage serves
dist/admin.html. Same RequireAdminFunc gate as /admin/team — non-admins
get the standard 302 to /dashboard?forbidden=admin.
- internal/handlers/handlers.go: register GET /admin under the existing
admin-conditional block.
- frontend/src/admin.tsx + client/admin.ts: card grid built from the
shared .grid + .card landing-page pattern. .admin-card-soon dims the
placeholders + adds a "Kommt bald" badge so they read as roadmap, not
broken links.
- frontend/src/components/Sidebar.tsx: add Admin-Bereich (/admin) above
Team-Verwaltung in the existing admin group. Both items live in the
same display:none group that sidebar.ts reveals after /api/me confirms
global_role='global_admin'.
- frontend/src/client/i18n.ts: nav.admin.bereich + admin.title /
.heading / .subtitle / .section.{available,planned} / .coming_soon
plus per-card title+desc, DE+EN.
- frontend/src/styles/global.css: .admin-section-planned spacing,
.admin-card-soon dimming, .admin-soon-badge pill.
- frontend/build.ts: register the renderAdmin entrypoint and admin.ts
client bundle.
This commit is contained in:
@@ -29,6 +29,7 @@ import { renderAgenda } from "./src/agenda";
|
||||
import { renderOnboarding } from "./src/onboarding";
|
||||
import { renderChangelog } from "./src/changelog";
|
||||
import { renderTeam } from "./src/team";
|
||||
import { renderAdmin } from "./src/admin";
|
||||
import { renderAdminTeam } from "./src/admin-team";
|
||||
import { renderNotFound } from "./src/notfound";
|
||||
|
||||
@@ -75,6 +76,7 @@ async function build() {
|
||||
join(import.meta.dir, "src/client/onboarding.ts"),
|
||||
join(import.meta.dir, "src/client/changelog.ts"),
|
||||
join(import.meta.dir, "src/client/team.ts"),
|
||||
join(import.meta.dir, "src/client/admin.ts"),
|
||||
join(import.meta.dir, "src/client/admin-team.ts"),
|
||||
join(import.meta.dir, "src/client/notfound.ts"),
|
||||
],
|
||||
@@ -153,6 +155,7 @@ async function build() {
|
||||
await Bun.write(join(DIST, "onboarding.html"), renderOnboarding());
|
||||
await Bun.write(join(DIST, "changelog.html"), renderChangelog());
|
||||
await Bun.write(join(DIST, "team.html"), renderTeam());
|
||||
await Bun.write(join(DIST, "admin.html"), renderAdmin());
|
||||
await Bun.write(join(DIST, "admin-team.html"), renderAdminTeam());
|
||||
await Bun.write(join(DIST, "notfound.html"), renderNotFound());
|
||||
|
||||
|
||||
108
frontend/src/admin.tsx
Normal file
108
frontend/src/admin.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { h } from "./jsx";
|
||||
import { Sidebar } from "./components/Sidebar";
|
||||
import { BottomNav } from "./components/BottomNav";
|
||||
import { Footer } from "./components/Footer";
|
||||
import { PWAHead } from "./components/PWAHead";
|
||||
|
||||
const ICON_USERS = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>';
|
||||
const ICON_BUILDING = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21h18"/><path d="M5 21V5a2 2 0 0 1 2-2h7a2 2 0 0 1 2 2v16"/><path d="M16 9h3a2 2 0 0 1 2 2v10"/><path d="M9 7h2"/><path d="M9 11h2"/><path d="M9 15h2"/></svg>';
|
||||
const ICON_LOG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="9" y1="13" x2="15" y2="13"/><line x1="9" y1="17" x2="15" y2="17"/></svg>';
|
||||
const ICON_MAIL = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><polyline points="3 7 12 13 21 7"/></svg>';
|
||||
const ICON_FLAG = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><line x1="4" y1="22" x2="4" y2="15"/></svg>';
|
||||
|
||||
interface PlannedCard {
|
||||
icon: string;
|
||||
i18nTitle: string;
|
||||
i18nDesc: string;
|
||||
fallbackTitle: string;
|
||||
fallbackDesc: string;
|
||||
}
|
||||
|
||||
const PLANNED: PlannedCard[] = [
|
||||
{
|
||||
icon: ICON_BUILDING,
|
||||
i18nTitle: "admin.card.departments.title",
|
||||
i18nDesc: "admin.card.departments.desc",
|
||||
fallbackTitle: "Departments / Dezernate",
|
||||
fallbackDesc: "Dezernate anlegen und Mitglieder verwalten.",
|
||||
},
|
||||
{
|
||||
icon: ICON_LOG,
|
||||
i18nTitle: "admin.card.audit.title",
|
||||
i18nDesc: "admin.card.audit.desc",
|
||||
fallbackTitle: "Audit-Log",
|
||||
fallbackDesc: "Wer hat wann was geändert? Nachvollziehbarkeit für sicherheitsrelevante Aktionen.",
|
||||
},
|
||||
{
|
||||
icon: ICON_MAIL,
|
||||
i18nTitle: "admin.card.email_templates.title",
|
||||
i18nDesc: "admin.card.email_templates.desc",
|
||||
fallbackTitle: "Email-Templates",
|
||||
fallbackDesc: "Vorlagen für Einladungen, Erinnerungen und Benachrichtigungen anpassen.",
|
||||
},
|
||||
{
|
||||
icon: ICON_FLAG,
|
||||
i18nTitle: "admin.card.feature_flags.title",
|
||||
i18nDesc: "admin.card.feature_flags.desc",
|
||||
fallbackTitle: "Feature-Flags",
|
||||
fallbackDesc: "Funktionen pro Standort, Dezernat oder Rolle aktivieren.",
|
||||
},
|
||||
];
|
||||
|
||||
export function renderAdmin(): string {
|
||||
return "<!DOCTYPE html>" + (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<meta name="theme-color" content="#65a30d" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||||
<PWAHead />
|
||||
<title data-i18n="admin.title">Admin-Bereich — Paliad</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body className="has-sidebar">
|
||||
<Sidebar currentPath="/admin" />
|
||||
<BottomNav currentPath="/admin" />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
<div className="container">
|
||||
<div className="tool-header">
|
||||
<h1 data-i18n="admin.heading">Admin-Bereich</h1>
|
||||
<p className="tool-subtitle" data-i18n="admin.subtitle">
|
||||
Werkzeuge zur Verwaltung von Paliad. Nur für Administrator:innen sichtbar.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h3 className="section-heading" data-i18n="admin.section.available">Verfügbar</h3>
|
||||
<div className="grid grid-2">
|
||||
<a href="/admin/team" className="card card-link">
|
||||
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_USERS }} />
|
||||
<h2 data-i18n="admin.card.team.title">Team-Verwaltung</h2>
|
||||
<p data-i18n="admin.card.team.desc">Benutzer:innen anlegen, bearbeiten, löschen.</p>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h3 className="section-heading admin-section-planned" data-i18n="admin.section.planned">Geplant</h3>
|
||||
<div className="grid grid-2">
|
||||
{PLANNED.map((c) => (
|
||||
<div className="card admin-card-soon" title="Kommt bald" data-i18n-title="admin.coming_soon">
|
||||
<div className="card-icon" dangerouslySetInnerHTML={{ __html: c.icon }} />
|
||||
<h2 data-i18n={c.i18nTitle}>{c.fallbackTitle}</h2>
|
||||
<p data-i18n={c.i18nDesc}>{c.fallbackDesc}</p>
|
||||
<span className="admin-soon-badge" data-i18n="admin.coming_soon">Kommt bald</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<script src="/assets/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
7
frontend/src/client/admin.ts
Normal file
7
frontend/src/client/admin.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { initI18n } from "./i18n";
|
||||
import { initSidebar } from "./sidebar";
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n();
|
||||
initSidebar();
|
||||
});
|
||||
@@ -1180,7 +1180,24 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
|
||||
// Admin team management (t-paliad-050)
|
||||
"nav.group.admin": "Admin",
|
||||
"nav.admin.bereich": "Admin-Bereich",
|
||||
"nav.admin.team": "Team-Verwaltung",
|
||||
"admin.title": "Admin-Bereich — Paliad",
|
||||
"admin.heading": "Admin-Bereich",
|
||||
"admin.subtitle": "Werkzeuge zur Verwaltung von Paliad. Nur für Administrator:innen sichtbar.",
|
||||
"admin.section.available": "Verfügbar",
|
||||
"admin.section.planned": "Geplant",
|
||||
"admin.coming_soon": "Kommt bald",
|
||||
"admin.card.team.title": "Team-Verwaltung",
|
||||
"admin.card.team.desc": "Benutzer:innen anlegen, bearbeiten, löschen.",
|
||||
"admin.card.departments.title": "Departments / Dezernate",
|
||||
"admin.card.departments.desc": "Dezernate anlegen und Mitglieder verwalten.",
|
||||
"admin.card.audit.title": "Audit-Log",
|
||||
"admin.card.audit.desc": "Wer hat wann was geändert? Nachvollziehbarkeit für sicherheitsrelevante Aktionen.",
|
||||
"admin.card.email_templates.title": "Email-Templates",
|
||||
"admin.card.email_templates.desc": "Vorlagen für Einladungen, Erinnerungen und Benachrichtigungen anpassen.",
|
||||
"admin.card.feature_flags.title": "Feature-Flags",
|
||||
"admin.card.feature_flags.desc": "Funktionen pro Standort, Dezernat oder Rolle aktivieren.",
|
||||
"admin.team.title": "Team-Verwaltung — Paliad",
|
||||
"admin.team.heading": "Team-Verwaltung",
|
||||
"admin.team.subtitle": "Alle Paliad-Konten anzeigen, bearbeiten oder hinzufügen.",
|
||||
@@ -2400,7 +2417,24 @@ const translations: Record<Lang, Record<string, string>> = {
|
||||
|
||||
// Admin team management (t-paliad-050)
|
||||
"nav.group.admin": "Admin",
|
||||
"nav.admin.bereich": "Admin Area",
|
||||
"nav.admin.team": "Team Management",
|
||||
"admin.title": "Admin Area — Paliad",
|
||||
"admin.heading": "Admin Area",
|
||||
"admin.subtitle": "Tools for managing Paliad. Visible only to administrators.",
|
||||
"admin.section.available": "Available",
|
||||
"admin.section.planned": "Planned",
|
||||
"admin.coming_soon": "Coming soon",
|
||||
"admin.card.team.title": "Team Management",
|
||||
"admin.card.team.desc": "Create, edit and delete user accounts.",
|
||||
"admin.card.departments.title": "Departments / Dezernate",
|
||||
"admin.card.departments.desc": "Create departments and manage their members.",
|
||||
"admin.card.audit.title": "Audit Log",
|
||||
"admin.card.audit.desc": "Who changed what, and when. Traceability for security-relevant actions.",
|
||||
"admin.card.email_templates.title": "Email Templates",
|
||||
"admin.card.email_templates.desc": "Customise templates for invitations, reminders and notifications.",
|
||||
"admin.card.feature_flags.title": "Feature Flags",
|
||||
"admin.card.feature_flags.desc": "Enable features per office, department or role.",
|
||||
"admin.team.title": "Team Management — Paliad",
|
||||
"admin.team.heading": "Team Management",
|
||||
"admin.team.subtitle": "View, edit and add Paliad accounts.",
|
||||
|
||||
@@ -139,7 +139,8 @@ export function Sidebar({ currentPath, authenticated = true }: SidebarProps): st
|
||||
role is known — only the first visit waits for /api/me. */}
|
||||
<div className="sidebar-group sidebar-admin-group" id="sidebar-admin-group" style="display:none">
|
||||
<div className="sidebar-group-label" data-i18n="nav.group.admin">Admin</div>
|
||||
{navItem("/admin/team", ICON_SHIELD, "nav.admin.team", "Team-Verwaltung", currentPath)}
|
||||
{navItem("/admin", ICON_SHIELD, "nav.admin.bereich", "Admin-Bereich", currentPath)}
|
||||
{navItem("/admin/team", ICON_USERS, "nav.admin.team", "Team-Verwaltung", currentPath)}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -7073,3 +7073,38 @@ dialog.quick-add-sheet::backdrop {
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* --- Admin landing page (t-paliad-054) ------------------------------
|
||||
/admin index reuses the standard .grid + .card pattern from the
|
||||
public landing page. The "Geplant" section uses .admin-card-soon
|
||||
to dim placeholder cards and add a "Kommt bald" badge so they
|
||||
read as a roadmap rather than broken links. */
|
||||
.admin-section-planned {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
|
||||
.admin-card-soon {
|
||||
position: relative;
|
||||
opacity: 0.6;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.admin-card-soon:hover {
|
||||
border-color: var(--color-border);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.admin-soon-badge {
|
||||
position: absolute;
|
||||
top: 0.85rem;
|
||||
right: 0.85rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
background: var(--color-bg-muted, #f3f4f6);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user