Emy/admin.html
2026-04-12 00:36:00 +02:00

1130 lines
51 KiB
HTML

<!DOCTYPE html>
<html class="light" lang="de">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>MiyaKarate Admin</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700;800;900&family=Be+Vietnam+Pro:wght@300;400;500;600;700&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"outline-variant": "#abadae",
"on-error-container": "#510017",
"on-tertiary": "#f5f2f1",
"on-surface": "#2c2f30",
"on-secondary": "#ffedff",
"on-primary-fixed-variant": "#5c0031",
"secondary": "#8930b0",
"primary-fixed": "#ff6ea9",
"secondary-dim": "#7c20a3",
"on-secondary-fixed-variant": "#7d21a4",
"error-container": "#f74b6d",
"on-tertiary-fixed": "#515050",
"surface-container-lowest": "#ffffff",
"background": "#f5f6f7",
"inverse-on-surface": "#9b9d9e",
"surface-container": "#e6e8ea",
"tertiary": "#5c5b5b",
"error": "#b41340",
"surface-dim": "#d1d5d7",
"surface": "#f5f6f7",
"on-secondary-fixed": "#580079",
"surface-tint": "#b30065",
"surface-container-low": "#eff1f2",
"on-tertiary-fixed-variant": "#6e6d6d",
"inverse-surface": "#0c0f10",
"tertiary-dim": "#504f4f",
"on-primary-fixed": "#000000",
"surface-variant": "#dadddf",
"surface-container-high": "#e0e3e4",
"on-primary": "#ffeff2",
"tertiary-fixed-dim": "#f3f0ef",
"tertiary-container": "#ffffff",
"on-background": "#2c2f30",
"on-surface-variant": "#595c5d",
"on-tertiary-container": "#636262",
"on-error": "#ffefef",
"secondary-fixed": "#f0c1ff",
"primary": "#b30065",
"tertiary-fixed": "#ffffff",
"primary-container": "#ff6ea9",
"on-primary-container": "#4b0027",
"primary-dim": "#9d0058",
"on-secondary-container": "#72129a",
"error-dim": "#a70138",
"outline": "#757778",
"primary-fixed-dim": "#ff4e9e",
"secondary-fixed-dim": "#eaaeff",
"inverse-primary": "#ff479c",
"secondary-container": "#f0c1ff",
"surface-container-highest": "#dadddf",
"surface-bright": "#f5f6f7"
},
borderRadius: {
DEFAULT: "1rem",
lg: "2rem",
xl: "3rem",
full: "9999px"
},
fontFamily: {
headline: ["Lexend"],
body: ["Be Vietnam Pro"],
label: ["Be Vietnam Pro"]
}
}
}
}
</script>
<style>
body { font-family: 'Be Vietnam Pro', sans-serif; }
h1, h2, h3, .font-lexend { font-family: 'Lexend', sans-serif; }
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.drop-active {
background: rgba(179, 0, 101, 0.04) !important;
border-color: #b30065 !important;
transform: scale(1.01);
}
</style>
</head>
<body class="bg-surface text-on-surface min-h-screen">
<!-- ═══ SIDEBAR ═══ -->
<aside class="h-screen w-64 fixed left-0 top-0 bg-zinc-50 flex flex-col py-6 gap-2 z-40">
<div class="px-6 mb-8">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full bg-gradient-to-br from-primary to-primary-container flex items-center justify-center text-white shadow-lg shadow-primary/20">
<span class="material-symbols-outlined" style="font-variation-settings:'FILL' 1;">bolt</span>
</div>
<div>
<h1 class="text-xl font-black text-primary font-lexend">MiyaKarate</h1>
<p class="text-[10px] uppercase tracking-widest text-zinc-500 font-bold">Admin Panel</p>
</div>
</div>
</div>
<nav class="flex-1 space-y-1" id="sideNav">
<a data-section="overview"
class="nav-link active-nav bg-pink-50 text-primary rounded-xl mx-2 flex items-center gap-3 px-4 py-3 font-lexend font-medium hover:translate-x-1 duration-300 transition-all cursor-pointer">
<span class="material-symbols-outlined">dashboard</span>
<span>Übersicht</span>
</a>
<a data-section="media"
class="nav-link text-zinc-600 hover:bg-zinc-100 rounded-xl mx-2 flex items-center gap-3 px-4 py-3 font-lexend font-medium hover:translate-x-1 duration-300 transition-all cursor-pointer">
<span class="material-symbols-outlined">collections</span>
<span>Galerie</span>
</a>
<a data-section="career"
class="nav-link text-zinc-600 hover:bg-zinc-100 rounded-xl mx-2 flex items-center gap-3 px-4 py-3 font-lexend font-medium hover:translate-x-1 duration-300 transition-all cursor-pointer">
<span class="material-symbols-outlined">emoji_events</span>
<span>Erfolge</span>
</a>
<a data-section="community"
class="nav-link text-zinc-600 hover:bg-zinc-100 rounded-xl mx-2 flex items-center gap-3 px-4 py-3 font-lexend font-medium hover:translate-x-1 duration-300 transition-all cursor-pointer">
<span class="material-symbols-outlined">forum</span>
<span>Gästebuch</span>
</a>
<a data-section="homepage"
class="nav-link text-zinc-600 hover:bg-zinc-100 rounded-xl mx-2 flex items-center gap-3 px-4 py-3 font-lexend font-medium hover:translate-x-1 duration-300 transition-all cursor-pointer">
<span class="material-symbols-outlined">home</span>
<span>Startseite</span>
</a>
<a data-section="settings"
class="nav-link text-zinc-600 hover:bg-zinc-100 rounded-xl mx-2 flex items-center gap-3 px-4 py-3 font-lexend font-medium hover:translate-x-1 duration-300 transition-all cursor-pointer">
<span class="material-symbols-outlined">tune</span>
<span>Einstellungen</span>
</a>
</nav>
<div class="px-4 mt-4">
<button id="btnAddContent"
class="w-full py-4 bg-gradient-to-r from-primary to-primary-container text-on-primary rounded-xl font-lexend font-bold text-sm shadow-xl shadow-primary/20 active:scale-95 transition-all">
Inhalt hinzufügen
</button>
</div>
<div class="mt-auto border-t border-zinc-200 pt-4">
<a href="http://localhost:1313/galerie/" target="_blank"
class="text-zinc-600 hover:bg-zinc-100 rounded-xl mx-2 flex items-center gap-3 px-4 py-3 font-lexend font-medium transition-all">
<span class="material-symbols-outlined">open_in_new</span>
<span>Website ansehen</span>
</a>
</div>
</aside>
<!-- ═══ MAIN ═══ -->
<main class="ml-64 p-8 min-h-screen">
<!-- Top Header -->
<header class="flex justify-between items-center mb-10">
<div>
<h2 class="text-3xl font-extrabold text-on-surface font-lexend tracking-tight" id="pageTitle">Übersicht</h2>
<p class="text-on-surface-variant font-medium" id="pageSubtitle">Willkommen zurück. Hier ist dein aktueller Stand.</p>
</div>
<div class="flex items-center gap-4">
<button class="w-12 h-12 rounded-full bg-surface-container-low flex items-center justify-center text-on-surface-variant hover:bg-surface-container transition-all">
<span class="material-symbols-outlined">notifications</span>
</button>
<div class="flex items-center gap-3 bg-surface-container-low px-4 py-2 rounded-full">
<div class="w-8 h-8 rounded-full bg-gradient-to-br from-primary to-primary-container flex items-center justify-center text-white text-xs font-black">M</div>
<span class="font-lexend font-bold text-sm">MiyaKarate</span>
</div>
</div>
</header>
<!-- ══ SECTION: OVERVIEW ══ -->
<div id="section-overview">
<!-- Stats Bento -->
<div class="grid grid-cols-12 gap-6 mb-8">
<div class="col-span-12 lg:col-span-8 grid grid-cols-3 gap-4 bg-surface-container-low p-6 rounded-xl">
<div class="p-6 bg-surface-container-lowest rounded-lg border-l-4 border-primary">
<p class="text-xs font-bold text-zinc-500 uppercase tracking-widest mb-2">Fotos gesamt</p>
<h3 class="text-4xl font-black text-on-surface font-lexend" id="statPhotos"></h3>
<div class="mt-2 text-primary font-bold text-xs flex items-center gap-1">
<span class="material-symbols-outlined text-sm">photo_library</span> In der Galerie
</div>
</div>
<div class="p-6 bg-surface-container-lowest rounded-lg border-l-4 border-secondary">
<p class="text-xs font-bold text-zinc-500 uppercase tracking-widest mb-2">Kategorien</p>
<h3 class="text-4xl font-black text-on-surface font-lexend" id="statCats">3</h3>
<div class="mt-2 text-secondary font-bold text-xs flex items-center gap-1">
<span class="material-symbols-outlined text-sm">label</span> Training, Wettkampf, Gürtel
</div>
</div>
<div class="p-6 bg-surface-container-lowest rounded-lg border-l-4 border-on-tertiary-fixed-variant">
<p class="text-xs font-bold text-zinc-500 uppercase tracking-widest mb-2">Letzter Upload</p>
<h3 class="text-xl font-black text-on-surface font-lexend leading-tight" id="statLast"></h3>
<div class="mt-2 text-zinc-500 font-bold text-xs flex items-center gap-1">
<span class="material-symbols-outlined text-sm">schedule</span> Zuletzt hochgeladen
</div>
</div>
</div>
<!-- Fast Actions -->
<div class="col-span-12 lg:col-span-4 bg-gradient-to-br from-secondary to-secondary-dim p-8 rounded-xl relative overflow-hidden flex flex-col justify-between">
<div class="relative z-10">
<h4 class="text-2xl font-black text-white font-lexend mb-2">Schnellzugriff</h4>
<p class="text-white/80 text-sm font-medium">Direkt loslegen.</p>
</div>
<div class="grid grid-cols-2 gap-3 relative z-10 mt-6">
<button data-section="media"
class="nav-trigger bg-white/20 backdrop-blur-md border border-white/30 text-white rounded-lg py-3 px-2 flex flex-col items-center gap-2 hover:bg-white/30 transition-all active:scale-95">
<span class="material-symbols-outlined">upload_file</span>
<span class="text-[10px] font-bold uppercase tracking-tighter">Fotos hochladen</span>
</button>
<button data-section="media"
class="nav-trigger bg-white/20 backdrop-blur-md border border-white/30 text-white rounded-lg py-3 px-2 flex flex-col items-center gap-2 hover:bg-white/30 transition-all active:scale-95">
<span class="material-symbols-outlined">grid_view</span>
<span class="text-[10px] font-bold uppercase tracking-tighter">Galerie öffnen</span>
</button>
</div>
<div class="absolute -right-10 -bottom-10 opacity-20 transform -rotate-12">
<span class="material-symbols-outlined text-[160px] text-white">sports_martial_arts</span>
</div>
</div>
</div>
<!-- Kategorie-Übersicht -->
<div class="bg-surface-container-low rounded-xl p-6 mb-8">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-extrabold font-lexend">Fotos nach Kategorie</h3>
<button type="button" id="overviewAddCatBtn"
class="flex items-center gap-1 text-xs font-bold text-primary hover:bg-pink-50 px-3 py-1.5 rounded-full transition-all">
<span class="material-symbols-outlined text-sm">add</span> Neue Kategorie
</button>
</div>
<div class="space-y-4" id="catOverviewList">
<p class="text-sm text-on-surface-variant">Wird geladen…</p>
</div>
<div id="overviewAddCatForm" class="hidden mt-4 flex gap-2">
<input id="overviewNewCatInput" type="text" placeholder="Kategoriename…"
class="flex-1 bg-surface-container rounded-DEFAULT px-3 py-2 text-on-surface placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>
<button type="button" id="overviewSaveCatBtn"
class="px-4 py-2 rounded-xl font-bold text-sm bg-primary text-on-primary active:scale-95 transition-all">
Hinzufügen
</button>
<button type="button" id="overviewCancelCatBtn"
class="px-3 py-2 rounded-xl font-bold text-sm bg-surface-container text-on-surface-variant">
</button>
</div>
</div>
</div>
<!-- ══ SECTION: MEDIA LIBRARY ══ -->
<div id="section-media" class="hidden">
<!-- Upload -->
<section class="bg-surface-container-lowest rounded-xl p-8 mb-8 shadow-sm">
<h3 class="text-xl font-black font-lexend text-on-surface mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">upload_file</span>
Fotos hochladen
</h3>
<label id="dropZone" for="fileInput"
class="block border-2 border-dashed border-primary/30 bg-surface-container-low rounded-xl p-12 text-center cursor-pointer mb-6 transition-all">
<span class="material-symbols-outlined text-primary block mb-4" style="font-size:3rem;">add_photo_alternate</span>
<p class="text-lg font-bold text-primary mb-1 pointer-events-none font-lexend">Fotos hier reinziehen</p>
<p class="text-on-surface-variant text-sm pointer-events-none">oder klicken zum Auswählen</p>
<p class="text-zinc-400 text-xs mt-2 pointer-events-none">JPG, PNG, HEIC · mehrere auf einmal möglich</p>
</label>
<input type="file" id="fileInput" multiple accept="image/*" class="sr-only"/>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-6">
<div>
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel (optional)</label>
<input id="uploadTitle" type="text" placeholder="z.B. Landesmeisterschaft 2024"
class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>
</div>
<div>
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kategorie</label>
<div class="flex gap-2 flex-wrap pt-1" id="katButtons"></div>
</div>
</div>
<div id="previewList" class="grid grid-cols-4 sm:grid-cols-6 gap-3 mb-6"></div>
<div id="progressWrap" class="hidden mb-4">
<div class="h-2 bg-surface-variant rounded-full overflow-hidden">
<div id="progressBar" class="h-full bg-gradient-to-r from-primary to-primary-container rounded-full transition-all duration-300" style="width:0%"></div>
</div>
<p id="progressText" class="text-xs text-on-surface-variant mt-2 text-center font-medium">Wird hochgeladen…</p>
</div>
<button id="uploadBtn" type="button" disabled
class="w-full py-4 rounded-xl font-black text-lg bg-gradient-to-r from-primary to-primary-container text-on-primary shadow-lg shadow-primary/20 disabled:opacity-40 disabled:cursor-not-allowed active:scale-95 transition-all font-lexend">
Hochladen
</button>
<p id="uploadStatus" class="text-center text-xs mt-3 text-on-surface-variant font-medium"></p>
</section>
<!-- Galerie -->
<section class="bg-surface-container-lowest rounded-xl p-8 shadow-sm">
<div class="flex items-center justify-between mb-6 flex-wrap gap-3">
<h3 class="text-xl font-black font-lexend text-on-surface flex items-center gap-2">
<span class="material-symbols-outlined text-primary">photo_library</span>
Galerie
<span id="photoCount" class="text-xs font-bold text-primary bg-pink-50 px-3 py-1 rounded-full">0 Fotos</span>
</h3>
<div class="flex gap-2 flex-wrap" id="filterButtons">
<button type="button" data-filter="Alle"
class="filter-btn text-xs font-bold px-3 py-1.5 rounded-full bg-primary text-on-primary">Alle</button>
</div>
</div>
<div id="photoGrid"></div>
</section>
</div>
<!-- ══ SECTION: HOMEPAGE ══ -->
<div id="section-homepage" class="hidden">
<section class="bg-surface-container-lowest rounded-xl p-8 shadow-sm">
<h3 class="text-xl font-black font-lexend text-on-surface mb-8 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">home</span>
Startseite bearbeiten
</h3>
<!-- Hero-Bild -->
<div class="mb-8">
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Hero-Bild</label>
<div class="flex gap-6 items-start flex-wrap">
<div class="w-48 h-48 rounded-xl overflow-hidden bg-surface-container flex-shrink-0">
<img id="heroPreview" src="" alt="Hero-Vorschau" class="w-full h-full object-cover hidden"/>
<div id="heroPlaceholder" class="w-full h-full flex flex-col items-center justify-center text-on-surface-variant">
<span class="material-symbols-outlined text-4xl mb-2">image</span>
<span class="text-xs font-medium">Kein Bild</span>
</div>
</div>
<div class="flex-1 min-w-48">
<label for="heroFileInput"
class="block border-2 border-dashed border-primary/30 bg-surface-container-low rounded-xl p-6 text-center cursor-pointer hover:border-primary/60 transition-all mb-3">
<span class="material-symbols-outlined text-primary block mb-2">upload_file</span>
<p class="text-sm font-bold text-primary font-lexend">Bild auswählen</p>
<p class="text-xs text-on-surface-variant mt-1">JPG, PNG, HEIC</p>
</label>
<input type="file" id="heroFileInput" accept="image/*" class="sr-only"/>
<button id="heroUploadBtn" disabled
class="w-full py-3 rounded-xl font-bold text-sm bg-gradient-to-r from-primary to-primary-container text-on-primary shadow-md shadow-primary/20 disabled:opacity-40 disabled:cursor-not-allowed active:scale-95 transition-all font-lexend">
Bild hochladen
</button>
<p id="heroUploadStatus" class="text-xs text-on-surface-variant mt-2 text-center"></p>
</div>
</div>
</div>
<!-- Badge-Text -->
<div class="mb-6">
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Badge-Text (über der Überschrift)</label>
<input id="heroBadge" type="text"
class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-medium tracking-widest uppercase"/>
</div>
<!-- Beschreibungstext -->
<div class="mb-8">
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Beschreibungstext</label>
<textarea id="heroDescription" rows="4"
class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm leading-relaxed resize-none"></textarea>
</div>
<!-- Stats -->
<div class="mb-8">
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Statistiken</label>
<div class="space-y-3" id="statsFields"></div>
</div>
<button id="heroSaveBtn"
class="w-full py-4 rounded-xl font-black text-lg bg-gradient-to-r from-primary to-primary-container text-on-primary shadow-lg shadow-primary/20 active:scale-95 transition-all font-lexend">
Änderungen speichern
</button>
</section>
</div>
<!-- ══ SECTION: CAREER ══ -->
<div id="section-career" class="hidden">
<div class="bg-surface-container-low rounded-xl p-12 text-center">
<span class="material-symbols-outlined text-6xl text-surface-variant mb-4 block">emoji_events</span>
<h3 class="text-xl font-black font-lexend text-on-surface-variant">Erfolge — demnächst verfügbar</h3>
</div>
</div>
<!-- ══ SECTION: COMMUNITY ══ -->
<div id="section-community" class="hidden">
<div class="bg-surface-container-low rounded-xl p-12 text-center">
<span class="material-symbols-outlined text-6xl text-surface-variant mb-4 block">forum</span>
<h3 class="text-xl font-black font-lexend text-on-surface-variant">Gästebuch — demnächst verfügbar</h3>
</div>
</div>
<!-- ══ SECTION: SETTINGS ══ -->
<div id="section-settings" class="hidden">
<section class="bg-surface-container-lowest rounded-xl p-8 shadow-sm">
<h3 class="text-xl font-black font-lexend text-on-surface mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">label</span>
Kategorien verwalten
</h3>
<p class="text-sm text-on-surface-variant mb-6">Kategorien werden für Galerie und Upload verwendet. Neue Kategorien erscheinen sofort überall.</p>
<!-- Liste -->
<div class="space-y-2 mb-6" id="catManageList">
<p class="text-sm text-on-surface-variant">Wird geladen…</p>
</div>
<!-- Neue Kategorie hinzufügen -->
<div class="flex gap-3">
<input id="newCatInput" type="text" placeholder="Neue Kategorie…"
class="flex-1 bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface placeholder-zinc-400 focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>
<button type="button" id="addCatBtn"
class="px-6 py-3 rounded-xl font-black text-sm bg-primary text-on-primary active:scale-95 transition-all font-lexend flex items-center gap-2">
<span class="material-symbols-outlined text-sm">add</span> Hinzufügen
</button>
</div>
<p id="newCatStatus" class="text-xs text-on-surface-variant mt-2"></p>
</section>
</div>
</main>
<!-- ═══ EDIT MODAL ═══ -->
<div id="editModal" class="hidden fixed inset-0 bg-black/40 backdrop-blur-sm z-50 flex items-center justify-center p-4">
<div class="bg-surface-container-lowest rounded-xl p-8 w-full max-w-md shadow-2xl">
<h3 class="text-xl font-black font-lexend text-on-surface mb-6 flex items-center gap-2">
<span class="material-symbols-outlined text-primary">edit</span> Foto bearbeiten
</h3>
<input type="hidden" id="editId"/>
<div class="space-y-4">
<div>
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel</label>
<input id="editTitle" type="text"
class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>
</div>
<div>
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kategorie</label>
<select id="editKat"
class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm">
</select>
</div>
</div>
<div class="flex gap-3 mt-8">
<button type="button" id="saveEditBtn"
class="flex-1 py-3 rounded-xl font-black bg-gradient-to-r from-primary to-primary-container text-on-primary active:scale-95 transition-all font-lexend">
Speichern
</button>
<button type="button" id="cancelEditBtn"
class="px-6 py-3 rounded-xl font-bold bg-surface-container text-on-surface-variant hover:bg-surface-container-high transition-all">
Abbrechen
</button>
</div>
</div>
</div>
<!-- ═══ TOAST ═══ -->
<div id="toast"
class="hidden fixed bottom-6 left-1/2 -translate-x-1/2 bg-on-surface text-surface-container-lowest px-6 py-3 rounded-full font-bold text-sm shadow-xl z-50 whitespace-nowrap font-lexend">
</div>
<script>
(function () {
// ─── State ───────────────────────────────────────────────
let selectedKat = '';
let currentFilter = 'Alle';
let allPhotos = [];
let allCategories = [];
let pendingFiles = [];
// ─── Navigation ──────────────────────────────────────────
const sections = ['overview', 'media', 'homepage', 'career', 'community', 'settings'];
const pageTitles = {
overview: ['Übersicht', 'Willkommen zurück. Hier ist dein aktueller Stand.'],
media: ['Galerie', 'Fotos hochladen und verwalten.'],
homepage: ['Startseite', 'Hero-Bild und Text der Startseite bearbeiten.'],
career: ['Erfolge', 'Meilensteine und Gürtelprüfungen.'],
community: ['Gästebuch', 'Kommentare und Einträge.'],
settings: ['Einstellungen', 'Konfiguration des Admin-Bereichs.']
};
function switchSection(name) {
sections.forEach(s => {
document.getElementById('section-' + s).classList.toggle('hidden', s !== name);
});
document.querySelectorAll('.nav-link').forEach(el => {
const active = el.dataset.section === name;
el.classList.toggle('active-nav', active);
el.classList.toggle('bg-pink-50', active);
el.classList.toggle('text-primary', active);
el.classList.toggle('text-zinc-600', !active);
el.classList.toggle('hover:bg-zinc-100', !active);
});
const [title, sub] = pageTitles[name] || ['Admin', ''];
document.getElementById('pageTitle').textContent = title;
document.getElementById('pageSubtitle').textContent = sub;
if (name === 'media') loadPhotos();
if (name === 'homepage') loadHomepage();
if (name === 'settings') renderCatManage();
}
document.getElementById('sideNav').addEventListener('click', function (e) {
const link = e.target.closest('.nav-link');
if (link) switchSection(link.dataset.section);
});
document.querySelectorAll('.nav-trigger').forEach(btn => {
btn.addEventListener('click', function () {
switchSection(btn.dataset.section);
});
});
document.getElementById('btnAddContent').addEventListener('click', function () {
switchSection('media');
});
// ─── Kategorien laden & UI aufbauen ──────────────────────
async function loadCategories() {
try {
const r = await fetch('/api/categories');
allCategories = await r.json();
} catch (e) {
allCategories = ['Training', 'Wettkämpfe', 'Gürtelprüfungen'];
}
if (!selectedKat || !allCategories.includes(selectedKat)) {
selectedKat = allCategories[0] || '';
}
renderKatButtons();
renderFilterButtons();
renderEditKatOptions();
renderCatManage();
}
function renderKatButtons() {
const container = document.getElementById('katButtons');
container.innerHTML = '';
allCategories.forEach(function (kat) {
const btn = document.createElement('button');
btn.type = 'button';
btn.dataset.kat = kat;
btn.textContent = kat;
const isActive = kat === selectedKat;
btn.className = 'kat-btn px-4 py-2 rounded-full text-xs font-bold transition-all ' +
(isActive ? 'bg-primary text-on-primary' : 'bg-surface-container text-on-surface-variant');
container.appendChild(btn);
});
}
function renderFilterButtons() {
const container = document.getElementById('filterButtons');
// "Alle"-Button behalten, Rest neu
const alleBtn = container.querySelector('[data-filter="Alle"]');
container.innerHTML = '';
if (alleBtn) container.appendChild(alleBtn);
allCategories.forEach(function (kat) {
const btn = document.createElement('button');
btn.type = 'button';
btn.dataset.filter = kat;
btn.textContent = kat;
btn.className = 'filter-btn text-xs font-bold px-3 py-1.5 rounded-full ' +
(currentFilter === kat ? 'bg-primary text-on-primary' : 'bg-surface-container text-on-surface-variant');
container.appendChild(btn);
});
}
function renderEditKatOptions() {
const sel = document.getElementById('editKat');
const current = sel.value;
sel.innerHTML = '';
allCategories.forEach(function (kat) {
const opt = document.createElement('option');
opt.value = kat;
opt.textContent = kat;
sel.appendChild(opt);
});
if (current && allCategories.includes(current)) sel.value = current;
}
function renderCatManage() {
const list = document.getElementById('catManageList');
if (!list) return;
if (!allCategories.length) {
list.innerHTML = '<p class="text-sm text-on-surface-variant">Noch keine Kategorien.</p>';
return;
}
list.innerHTML = '';
allCategories.forEach(function (kat) {
const row = document.createElement('div');
row.className = 'flex items-center justify-between bg-surface-container-low rounded-xl px-4 py-3';
row.innerHTML =
'<div class="flex items-center gap-3">' +
'<span class="material-symbols-outlined text-primary text-sm">label</span>' +
'<span class="font-bold text-sm">' + escHtml(kat) + '</span>' +
'</div>' +
'<button type="button" data-del="' + escHtml(kat) + '" class="del-cat-btn text-xs text-error font-bold px-3 py-1 rounded-full hover:bg-pink-50 transition-all flex items-center gap-1">' +
'<span class="material-symbols-outlined text-sm">delete</span> Löschen' +
'</button>';
list.appendChild(row);
});
}
// ─── Kategorie-Buttons (Upload) ───────────────────────────
document.getElementById('katButtons').addEventListener('click', function (e) {
const btn = e.target.closest('.kat-btn');
if (!btn) return;
selectedKat = btn.dataset.kat;
document.querySelectorAll('.kat-btn').forEach(b => {
const isActive = b === btn;
b.classList.toggle('bg-primary', isActive);
b.classList.toggle('text-on-primary', isActive);
b.classList.toggle('bg-surface-container', !isActive);
b.classList.toggle('text-on-surface-variant', !isActive);
});
});
// ─── Kategorie hinzufügen ─────────────────────────────────
document.getElementById('addCatBtn').addEventListener('click', async function () {
const input = document.getElementById('newCatInput');
const status = document.getElementById('newCatStatus');
const name = input.value.trim();
if (!name) return;
try {
const r = await fetch('/api/categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
const data = await r.json();
if (r.ok) {
allCategories = data.categories;
input.value = '';
status.textContent = '';
renderKatButtons();
renderFilterButtons();
renderEditKatOptions();
renderCatManage();
showToast('✓ Kategorie "' + name + '" hinzugefügt');
} else {
status.textContent = data.error || 'Fehler';
}
} catch (e) {
status.textContent = 'Verbindungsfehler';
}
});
document.getElementById('newCatInput').addEventListener('keydown', function (e) {
if (e.key === 'Enter') document.getElementById('addCatBtn').click();
});
// ─── Kategorie löschen ────────────────────────────────────
document.getElementById('catManageList').addEventListener('click', async function (e) {
const btn = e.target.closest('.del-cat-btn');
if (!btn) return;
const name = btn.dataset.del;
if (!confirm('Kategorie "' + name + '" wirklich löschen?')) return;
try {
const r = await fetch('/api/categories/' + encodeURIComponent(name), { method: 'DELETE' });
const data = await r.json();
if (r.ok) {
allCategories = data.categories;
if (selectedKat === name) selectedKat = allCategories[0] || '';
renderKatButtons();
renderFilterButtons();
renderEditKatOptions();
renderCatManage();
showToast('Kategorie "' + name + '" gelöscht');
}
} catch (e) {
showToast('Verbindungsfehler');
}
});
// ─── Schnell-Kategorie aus Übersicht ─────────────────────
document.getElementById('overviewAddCatBtn').addEventListener('click', function () {
document.getElementById('overviewAddCatForm').classList.remove('hidden');
document.getElementById('overviewNewCatInput').focus();
});
document.getElementById('overviewCancelCatBtn').addEventListener('click', function () {
document.getElementById('overviewAddCatForm').classList.add('hidden');
document.getElementById('overviewNewCatInput').value = '';
});
async function overviewSaveCat() {
const input = document.getElementById('overviewNewCatInput');
const name = input.value.trim();
if (!name) return;
try {
const r = await fetch('/api/categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name })
});
const data = await r.json();
if (r.ok) {
allCategories = data.categories;
input.value = '';
document.getElementById('overviewAddCatForm').classList.add('hidden');
renderKatButtons();
renderFilterButtons();
renderEditKatOptions();
renderCatManage();
loadStats();
showToast('✓ Kategorie "' + name + '" hinzugefügt');
} else {
showToast(data.error || 'Fehler');
}
} catch (e) {
showToast('Verbindungsfehler');
}
}
document.getElementById('overviewSaveCatBtn').addEventListener('click', overviewSaveCat);
document.getElementById('overviewNewCatInput').addEventListener('keydown', function (e) {
if (e.key === 'Enter') overviewSaveCat();
if (e.key === 'Escape') document.getElementById('overviewCancelCatBtn').click();
});
// ─── Drag & Drop ──────────────────────────────────────────
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
dropZone.addEventListener('dragover', function (e) {
e.preventDefault();
dropZone.classList.add('drop-active');
});
dropZone.addEventListener('dragleave', function () {
dropZone.classList.remove('drop-active');
});
dropZone.addEventListener('drop', function (e) {
e.preventDefault();
dropZone.classList.remove('drop-active');
setFiles(Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/')));
});
fileInput.addEventListener('change', function () {
setFiles(Array.from(fileInput.files));
});
function setFiles(files) {
pendingFiles = files;
const previewList = document.getElementById('previewList');
previewList.innerHTML = '';
const uploadBtn = document.getElementById('uploadBtn');
const uploadStatus = document.getElementById('uploadStatus');
if (!files.length) { uploadBtn.disabled = true; return; }
uploadBtn.disabled = false;
uploadStatus.textContent = files.length + ' Foto(s) ausgewählt';
files.forEach(function (f) {
const url = URL.createObjectURL(f);
const div = document.createElement('div');
div.className = 'aspect-square rounded-DEFAULT overflow-hidden bg-surface-container';
const img = document.createElement('img');
img.src = url;
img.className = 'w-full h-full object-cover';
div.appendChild(img);
previewList.appendChild(div);
});
}
// ─── Upload ───────────────────────────────────────────────
document.getElementById('uploadBtn').addEventListener('click', async function () {
if (!pendingFiles.length) return;
const uploadBtn = document.getElementById('uploadBtn');
const progressWrap = document.getElementById('progressWrap');
const progressBar = document.getElementById('progressBar');
const progressText = document.getElementById('progressText');
const uploadStatus = document.getElementById('uploadStatus');
uploadBtn.disabled = true;
progressWrap.classList.remove('hidden');
progressBar.style.width = '0%';
uploadStatus.textContent = 'Wird hochgeladen…';
let prog = 0;
const ticker = setInterval(function () {
prog = Math.min(prog + 4, 85);
progressBar.style.width = prog + '%';
}, 150);
try {
const fd = new FormData();
pendingFiles.forEach(f => fd.append('photos', f));
fd.append('title', document.getElementById('uploadTitle').value.trim());
fd.append('kategorie', selectedKat);
const r = await fetch('/api/upload', { method: 'POST', body: fd });
clearInterval(ticker);
if (r.ok) {
progressBar.style.width = '100%';
progressText.textContent = 'Fertig!';
const data = await r.json();
showToast('✓ ' + data.added.length + ' Foto(s) hochgeladen');
fileInput.value = '';
pendingFiles = [];
document.getElementById('previewList').innerHTML = '';
document.getElementById('uploadTitle').value = '';
uploadStatus.textContent = '';
setTimeout(function () {
progressWrap.classList.add('hidden');
progressBar.style.width = '0%';
progressText.textContent = 'Wird hochgeladen…';
uploadBtn.disabled = true;
}, 1500);
loadPhotos();
loadStats();
} else {
const err = await r.text();
uploadStatus.textContent = 'Fehler: ' + err;
uploadBtn.disabled = false;
progressWrap.classList.add('hidden');
}
} catch (err) {
clearInterval(ticker);
uploadStatus.textContent = 'Verbindungsfehler: ' + err.message;
uploadBtn.disabled = false;
progressWrap.classList.add('hidden');
}
});
// ─── Filter ───────────────────────────────────────────────
document.getElementById('filterButtons').addEventListener('click', function (e) {
const btn = e.target.closest('.filter-btn');
if (!btn) return;
currentFilter = btn.dataset.filter;
document.querySelectorAll('.filter-btn').forEach(b => {
const active = b === btn;
b.classList.toggle('bg-primary', active);
b.classList.toggle('text-on-primary', active);
b.classList.toggle('bg-surface-container', !active);
b.classList.toggle('text-on-surface-variant', !active);
});
renderPhotos();
});
// ─── Fotos laden & rendern ────────────────────────────────
async function loadPhotos() {
try {
const r = await fetch('/api/photos');
const data = await r.json();
allPhotos = data.photos || [];
renderPhotos();
} catch (err) {
console.error('Load error:', err);
}
}
function renderPhotos() {
const photoGrid = document.getElementById('photoGrid');
const photoCount = document.getElementById('photoCount');
const filtered = currentFilter === 'Alle'
? allPhotos
: allPhotos.filter(p => p.kategorie === currentFilter);
photoCount.textContent = allPhotos.length + ' Fotos';
if (!filtered.length) {
photoGrid.innerHTML =
'<div class="text-center py-16 text-on-surface-variant">' +
'<span class="material-symbols-outlined text-5xl text-surface-variant mb-3 block">' +
(currentFilter === 'Alle' ? 'photo_library' : 'search_off') + '</span>' +
'<p class="font-bold font-lexend">' +
(currentFilter === 'Alle' ? 'Noch keine Fotos' : 'Keine Fotos in dieser Kategorie') + '</p>' +
'</div>';
return;
}
const katColors = {
'Training': 'bg-pink-100 text-primary',
'Wettkämpfe': 'bg-purple-100 text-secondary',
'Gürtelprüfungen': 'bg-amber-100 text-amber-700'
};
photoGrid.innerHTML = '';
const grid = document.createElement('div');
grid.className = 'grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4';
filtered.forEach(function (p) {
const card = document.createElement('div');
card.className = 'group relative bg-surface-container-lowest rounded-DEFAULT overflow-hidden shadow-sm hover:-translate-y-1 transition-transform duration-200';
const imgWrap = document.createElement('div');
imgWrap.className = 'aspect-square overflow-hidden';
const img = document.createElement('img');
img.src = '/images/' + p.thumb;
img.alt = p.title;
img.className = 'w-full h-full object-cover group-hover:scale-105 transition-transform duration-300';
imgWrap.appendChild(img);
const info = document.createElement('div');
info.className = 'p-3';
info.innerHTML =
'<p class="font-bold text-on-surface text-xs truncate mb-1">' + escHtml(p.title) + '</p>' +
'<span class="text-[10px] font-bold px-2 py-0.5 rounded-full ' +
(katColors[p.kategorie] || 'bg-surface-container text-on-surface-variant') + '">' +
escHtml(p.kategorie) + '</span>';
const actions = document.createElement('div');
actions.className = 'absolute top-2 right-2 flex gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity';
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.className = 'w-8 h-8 bg-surface-container-lowest rounded-full shadow-md flex items-center justify-center hover:scale-110 transition-transform';
editBtn.innerHTML = '<span class="material-symbols-outlined text-sm text-on-surface">edit</span>';
editBtn.addEventListener('click', function () { openEdit(p.id, p.title, p.kategorie); });
const delBtn = document.createElement('button');
delBtn.type = 'button';
delBtn.className = 'w-8 h-8 bg-error rounded-full shadow-md flex items-center justify-center hover:scale-110 transition-transform';
delBtn.innerHTML = '<span class="material-symbols-outlined text-sm text-on-error">delete</span>';
delBtn.addEventListener('click', function () { deletePhoto(p.id); });
actions.appendChild(editBtn);
actions.appendChild(delBtn);
card.appendChild(imgWrap);
card.appendChild(info);
card.appendChild(actions);
grid.appendChild(card);
});
photoGrid.appendChild(grid);
}
function escHtml(s) {
return String(s)
.replace(/&/g, '&amp;').replace(/</g, '&lt;')
.replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ─── Stats für Übersicht ──────────────────────────────────
async function loadStats() {
try {
const r = await fetch('/api/photos');
const data = await r.json();
const photos = data.photos || [];
document.getElementById('statPhotos').textContent = photos.length;
const counts = {};
let lastDate = null;
photos.forEach(p => {
counts[p.kategorie] = (counts[p.kategorie] || 0) + 1;
const ts = parseInt(p.id);
if (!isNaN(ts) && (!lastDate || ts > lastDate)) lastDate = ts;
});
const icons = ['fitness_center', 'emoji_events', 'workspace_premium', 'sports_martial_arts', 'star', 'label'];
const colors = ['bg-pink-100 text-primary', 'bg-purple-100 text-secondary', 'bg-amber-100 text-amber-600', 'bg-blue-100 text-blue-600', 'bg-green-100 text-green-600', 'bg-surface-container text-on-surface-variant'];
const list = document.getElementById('catOverviewList');
list.innerHTML = '';
allCategories.forEach(function (kat, i) {
const colorClass = colors[i % colors.length];
const icon = icons[i % icons.length];
const row = document.createElement('div');
row.className = 'flex items-center justify-between';
row.innerHTML =
'<div class="flex items-center gap-3">' +
'<div class="w-8 h-8 ' + colorClass + ' rounded flex items-center justify-center">' +
'<span class="material-symbols-outlined text-sm">' + icon + '</span></div>' +
'<span class="font-bold text-sm">' + escHtml(kat) + '</span>' +
'</div>' +
'<span class="text-sm font-black">' + (counts[kat] || 0) + ' Fotos</span>';
list.appendChild(row);
});
if (lastDate) {
const d = new Date(lastDate);
document.getElementById('statLast').textContent =
d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', year: 'numeric' });
}
} catch (err) {
console.error('Stats error:', err);
}
}
// ─── Edit Modal ───────────────────────────────────────────
function openEdit(id, title, kat) {
document.getElementById('editId').value = id;
document.getElementById('editTitle').value = title;
document.getElementById('editKat').value = kat;
document.getElementById('editModal').classList.remove('hidden');
document.getElementById('editTitle').focus();
}
document.getElementById('cancelEditBtn').addEventListener('click', function () {
document.getElementById('editModal').classList.add('hidden');
});
document.getElementById('saveEditBtn').addEventListener('click', async function () {
const id = document.getElementById('editId').value;
try {
const r = await fetch('/api/photo/' + id, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: document.getElementById('editTitle').value,
kategorie: document.getElementById('editKat').value
})
});
if (r.ok) {
document.getElementById('editModal').classList.add('hidden');
showToast('✓ Gespeichert');
loadPhotos();
loadStats();
}
} catch (err) {
console.error('Save error:', err);
}
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape') document.getElementById('editModal').classList.add('hidden');
});
// ─── Löschen ──────────────────────────────────────────────
async function deletePhoto(id) {
if (!confirm('Foto wirklich löschen? Das kann nicht rückgängig gemacht werden.')) return;
try {
const r = await fetch('/api/photo/' + id, { method: 'DELETE' });
if (r.ok) {
showToast('Foto gelöscht');
loadPhotos();
loadStats();
}
} catch (err) {
console.error('Delete error:', err);
}
}
// ─── Toast ────────────────────────────────────────────────
function showToast(msg) {
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.remove('hidden');
setTimeout(function () { t.classList.add('hidden'); }, 3000);
}
// ─── Homepage ─────────────────────────────────────────────
async function loadHomepage() {
try {
const r = await fetch('/api/homepage');
const data = await r.json();
const hero = data.hero || {};
document.getElementById('heroBadge').value = hero.badge || '';
document.getElementById('heroDescription').value = hero.description || '';
const statsFields = document.getElementById('statsFields');
statsFields.innerHTML = '';
(data.stats || []).forEach(function(s) {
const row = document.createElement('div');
row.className = 'flex gap-3 items-center';
row.innerHTML =
'<input type="text" value="' + s.value + '" placeholder="Wert" data-stat="value"' +
' class="w-24 bg-surface-container-low rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-black text-center"/>' +
'<input type="text" value="' + s.label + '" placeholder="Bezeichnung" data-stat="label"' +
' class="flex-1 bg-surface-container-low rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>';
statsFields.appendChild(row);
});
if (hero.image) {
document.getElementById('heroPreview').src = '/hero/' + hero.image + '?t=' + Date.now();
document.getElementById('heroPreview').classList.remove('hidden');
document.getElementById('heroPlaceholder').classList.add('hidden');
}
} catch (err) { console.error('Homepage load error:', err); }
}
// Hero-Bild Vorschau
document.getElementById('heroFileInput').addEventListener('change', function () {
const file = this.files[0];
if (!file) return;
document.getElementById('heroPreview').src = URL.createObjectURL(file);
document.getElementById('heroPreview').classList.remove('hidden');
document.getElementById('heroPlaceholder').classList.add('hidden');
document.getElementById('heroUploadBtn').disabled = false;
document.getElementById('heroUploadStatus').textContent = file.name;
});
// Hero-Bild Upload
document.getElementById('heroUploadBtn').addEventListener('click', async function () {
const file = document.getElementById('heroFileInput').files[0];
if (!file) return;
this.disabled = true;
document.getElementById('heroUploadStatus').textContent = 'Wird hochgeladen…';
try {
const fd = new FormData();
fd.append('image', file);
const r = await fetch('/api/homepage/image', { method: 'POST', body: fd });
if (r.ok) {
showToast('✓ Bild gespeichert');
document.getElementById('heroUploadStatus').textContent = 'Gespeichert!';
} else {
document.getElementById('heroUploadStatus').textContent = 'Fehler beim Upload';
}
} catch (err) {
document.getElementById('heroUploadStatus').textContent = 'Verbindungsfehler';
}
this.disabled = false;
});
// Text speichern
document.getElementById('heroSaveBtn').addEventListener('click', async function () {
try {
const rows = document.getElementById('statsFields').querySelectorAll('div');
const stats = Array.from(rows).map(function(row) {
return {
value: row.querySelector('[data-stat="value"]').value,
label: row.querySelector('[data-stat="label"]').value
};
});
const r = await fetch('/api/homepage', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
badge: document.getElementById('heroBadge').value,
description: document.getElementById('heroDescription').value,
stats: stats
})
});
if (r.ok) showToast('✓ Startseite gespeichert');
} catch (err) { console.error('Save error:', err); }
});
// ─── Init ─────────────────────────────────────────────────
loadCategories().then(function () { loadStats(); });
})();
</script>
</body>
</html>