mirror of
https://github.com/superschnups/Emy.git
synced 2026-06-22 03:13:10 +00:00
1054 lines
47 KiB
HTML
1054 lines
47 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">
|
|
<h3 class="text-lg font-extrabold font-lexend mb-4">Fotos nach Kategorie</h3>
|
|
<div class="space-y-4" id="catOverviewList">
|
|
<p class="text-sm text-on-surface-variant">Wird geladen…</p>
|
|
</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');
|
|
}
|
|
});
|
|
|
|
// ─── 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, '&').replace(/</g, '<')
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
// ─── 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 = { Training: 0, 'Wettkämpfe': 0, 'Gürtelprüfungen': 0 };
|
|
let lastDate = null;
|
|
photos.forEach(p => {
|
|
if (counts[p.kategorie] !== undefined) counts[p.kategorie]++;
|
|
const ts = parseInt(p.id);
|
|
if (!isNaN(ts) && (!lastDate || ts > lastDate)) lastDate = ts;
|
|
});
|
|
|
|
document.getElementById('catTraining').textContent = counts['Training'] + ' Fotos';
|
|
document.getElementById('catWettkampf').textContent = counts['Wettkämpfe'] + ' Fotos';
|
|
document.getElementById('catGuertel').textContent = counts['Gürtelprüfungen'] + ' Fotos';
|
|
|
|
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 ─────────────────────────────────────────────────
|
|
loadStats();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|