mirror of
https://github.com/superschnups/Emy.git
synced 2026-06-21 19:03:17 +00:00
2689 lines
141 KiB
HTML
2689 lines
141 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" id="adminSiteTitle">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="uebermich"
|
||
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">person</span>
|
||
<span>Über mich</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 id="viewWebsiteLink" 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" id="adminSiteTitleHeader">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">—</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> <span id="statCatsSublabel">Training, Wettkampf, Gürtel</span>
|
||
</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">
|
||
|
||
<!-- Galerie Seiten-Texte -->
|
||
<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-8 flex items-center gap-2">
|
||
<span class="material-symbols-outlined text-primary">text_fields</span>
|
||
Galerie – Seiten-Texte
|
||
</h3>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Badge (oben)</label>
|
||
<input id="galBadge" 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">Überschrift (normal)</label>
|
||
<input id="galHeadingMain" 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-bold uppercase"/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Überschrift (farbig)</label>
|
||
<input id="galHeadingColored" 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-bold uppercase"/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Beschreibungstext</label>
|
||
<textarea id="galDescription" rows="3" 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 resize-none"></textarea>
|
||
</div>
|
||
</div>
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mb-4">Stats Ribbon (Zähler unter dem Grid)</h4>
|
||
<div class="mb-4">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Überschrift Ribbon</label>
|
||
<input id="galRibbonHeading" 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-bold"/>
|
||
</div>
|
||
<div class="mb-6">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Beschreibung Ribbon</label>
|
||
<textarea id="galRibbonDesc" rows="2" 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 resize-none"></textarea>
|
||
</div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Statistiken (Wert + Bezeichnung)</label>
|
||
<div class="space-y-3 mb-8" id="galStatsFields"></div>
|
||
<button id="galSaveBtn" type="button" 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">
|
||
Galerie-Texte speichern & deployen
|
||
</button>
|
||
</section>
|
||
|
||
<!-- 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>
|
||
|
||
<!-- Hero-Karte -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Hero-Karte</h4>
|
||
<div class="grid grid-cols-1 gap-4 mb-8">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Status (z.B. Status 2026)</label>
|
||
<input id="hpKarteStatus" 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>
|
||
|
||
<!-- Prüfungs-Karte -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Prüfungs-Karte (pinke Box)</h4>
|
||
<div class="grid grid-cols-1 gap-4 mb-8">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel</label>
|
||
<input id="hpPruefungTitel" 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">Beschreibung</label>
|
||
<textarea id="hpPruefungDesc" rows="2" 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 resize-none"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Dojo-Karte -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Dojo-Karte (graue Box)</h4>
|
||
<div class="grid grid-cols-1 gap-4 mb-8">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel</label>
|
||
<input id="hpDojoTitel" 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">Beschreibung</label>
|
||
<textarea id="hpDojoDesc" rows="2" 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 resize-none"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CTA-Sektion -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Galerie-CTA (dunkler Bereich)</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Überschrift (normal)</label>
|
||
<input id="hpCtaMain" 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">Überschrift (farbig/akzent)</label>
|
||
<input id="hpCtaColored" 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 class="md:col-span-2">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Beschreibungstext</label>
|
||
<textarea id="hpCtaDesc" rows="2" 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 resize-none"></textarea>
|
||
</div>
|
||
<div class="md:col-span-2">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Button-Text</label>
|
||
<input id="hpCtaBtn" 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-bold uppercase tracking-widest"/>
|
||
</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: ÜBER MICH ══ -->
|
||
<div id="section-uebermich" class="hidden">
|
||
<section class="bg-surface-container-lowest rounded-xl p-8 shadow-sm mb-8">
|
||
<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">person</span>
|
||
Über mich – Seite bearbeiten
|
||
</h3>
|
||
|
||
<!-- Bild -->
|
||
<div class="mb-8">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Portrait-Bild</label>
|
||
<div class="flex gap-6 items-start flex-wrap">
|
||
<div class="w-36 h-44 rounded-xl overflow-hidden bg-surface-container flex-shrink-0">
|
||
<img id="umPreview" src="" alt="Portrait" class="w-full h-full object-cover hidden"/>
|
||
<div id="umPlaceholder" 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="umFileInput" 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="umFileInput" accept="image/*" class="sr-only"/>
|
||
<button id="umUploadBtn" 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="umUploadStatus" class="text-xs text-on-surface-variant mt-2 text-center"></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hero-Texte -->
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||
<div class="md:col-span-2">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Badge (z.B. "Meine Reise")</label>
|
||
<input id="umBadge" 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">Überschrift Zeile 1</label>
|
||
<input id="umH1" 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-black uppercase"/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Überschrift Zeile 2 (farbig)</label>
|
||
<input id="umH2" 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-black uppercase"/>
|
||
</div>
|
||
<div class="md:col-span-2">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Überschrift Rest</label>
|
||
<input id="umHSuffix" 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-black uppercase"/>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-6">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Beschreibungstext</label>
|
||
<textarea id="umDesc" 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>
|
||
|
||
<!-- Aktiver Gurt -->
|
||
<div class="mb-6 flex gap-4 items-end">
|
||
<div class="flex-1">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Aktiver Gurt</label>
|
||
<select id="umGurt" 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">
|
||
<option>Weiß</option><option>Gelb</option><option>Orange</option><option>Grün</option>
|
||
<option>Blau</option><option>Lila</option><option>Braun</option><option>Schwarz</option>
|
||
</select>
|
||
</div>
|
||
<button type="button" onclick="window.openBeltModal()" class="px-6 py-3 rounded-xl font-bold text-sm border-2 border-primary/30 text-primary hover:bg-pink-50 transition-all flex items-center gap-2 mb-0.5">
|
||
<span class="material-symbols-outlined text-base">list_alt</span> Gurtweg bearbeiten
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Stats -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Statistik-Boxen</h4>
|
||
<div id="umStatsFields" class="space-y-3 mb-6"></div>
|
||
|
||
<!-- Zitat -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Zitat</h4>
|
||
<div class="mb-4">
|
||
<textarea id="umZitat" rows="3" 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 resize-none"></textarea>
|
||
</div>
|
||
<div class="mb-8">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Autor</label>
|
||
<input id="umZitatAutor" 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>
|
||
|
||
<button id="umSaveBtn" 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 & deployen
|
||
</button>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- ══ SECTION: CAREER ══ -->
|
||
<div id="section-career" 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">emoji_events</span>
|
||
Erfolge-Seite bearbeiten
|
||
</h3>
|
||
|
||
<!-- Hero-Bild -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mb-4">Hero-Bild</h4>
|
||
<div class="flex gap-6 items-start flex-wrap mb-8">
|
||
<div class="w-40 h-48 rounded-xl overflow-hidden bg-surface-container flex-shrink-0">
|
||
<img id="efHeroPreview" src="" alt="Erfolge Hero" class="w-full h-full object-cover hidden"/>
|
||
<div id="efHeroPlaceholder" 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="efHeroFileInput" 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="efHeroFileInput" accept="image/*" class="sr-only"/>
|
||
<button id="efHeroUploadBtn" 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="efHeroUploadStatus" class="text-xs text-on-surface-variant mt-2 text-center"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hero-Texte -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mb-4">Hero-Text</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||
<div class="md:col-span-2">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Badge-Text</label>
|
||
<input id="efBadge" 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">Überschrift (normal)</label>
|
||
<input id="efHeadingMain" 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-black uppercase"/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Überschrift (farbig)</label>
|
||
<input id="efHeadingColored" 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-black uppercase"/>
|
||
</div>
|
||
<div class="md:col-span-2">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Beschreibungstext</label>
|
||
<textarea id="efDescription" rows="3" 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>
|
||
</div>
|
||
|
||
<!-- Haupt-Meilenstein -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Haupt-Meilenstein (große Karte)</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Event / Titel</label>
|
||
<input id="efMeilensteinEvent" 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">Platz / Ergebnis</label>
|
||
<input id="efMeilensteinPlatz" 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">Jahr</label>
|
||
<input id="efMeilensteinJahr" 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">Ort</label>
|
||
<input id="efMeilensteinOrt" 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>
|
||
<input id="efMeilensteinKat" 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 class="md:col-span-2">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Beschreibung</label>
|
||
<textarea id="efMeilensteinDesc" rows="3" 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 resize-none"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Weitere Auszeichnungen -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Weitere Auszeichnungen</h4>
|
||
<div id="efWeitereFields" class="space-y-3 mb-3"></div>
|
||
<button id="efWeitereAdd" type="button" class="flex items-center gap-2 text-sm font-bold text-primary border border-primary/30 rounded-xl px-4 py-2 hover:bg-pink-50 transition-all mb-8">
|
||
<span class="material-symbols-outlined text-base">add</span> Auszeichnung hinzufügen
|
||
</button>
|
||
|
||
<!-- Statistiken -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-2">Statistiken (3 Boxen)</h4>
|
||
<p class="text-xs text-on-surface-variant mb-4">Box 1 = lila, Box 2 = grau, Box 3 = weiß mit pinker Linie.</p>
|
||
<div id="efStatsFields" class="space-y-3 mb-8"></div>
|
||
|
||
<!-- Zitat -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-4">Zitat</h4>
|
||
<div class="mb-4">
|
||
<textarea id="efZitatText" rows="3" 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 resize-none"></textarea>
|
||
</div>
|
||
<div class="mb-8">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Autor</label>
|
||
<input id="efZitatAutor" 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>
|
||
|
||
<button id="careerSaveBtn" 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 mb-12">
|
||
Erfolge-Seite speichern & deployen
|
||
</button>
|
||
|
||
<!-- Einzelerfolge (Markdown) -->
|
||
<h3 class="text-xl font-black font-lexend text-on-surface mb-6 flex items-center gap-2 pt-8 border-t border-zinc-200">
|
||
<span class="material-symbols-outlined text-primary">article</span>
|
||
Einzelerfolge (Berichte)
|
||
</h3>
|
||
<p class="text-sm text-on-surface-variant mb-6">Hier kannst du die Berichte bearbeiten, die auf der Startseite unter "Highlights" erscheinen.</p>
|
||
<div id="individualSuccessList" class="space-y-4"></div>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- ══ SECTION: COMMUNITY ══ -->
|
||
<div id="section-community" class="hidden">
|
||
<section class="bg-surface-container-lowest rounded-xl p-8 shadow-sm mb-8">
|
||
<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">forum</span>
|
||
Gästebuch bearbeiten
|
||
</h3>
|
||
|
||
<!-- Hero Bild -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mb-4">Hero-Bild (Hintergrundbild)</h4>
|
||
<div class="flex gap-6 items-start flex-wrap mb-8">
|
||
<div class="w-48 h-24 rounded-xl overflow-hidden bg-surface-container flex-shrink-0">
|
||
<img id="gbHeroPreview" src="" alt="Gästebuch Hero" class="w-full h-full object-cover hidden"/>
|
||
<div id="gbHeroPlaceholder" class="w-full h-full flex flex-col items-center justify-center text-on-surface-variant">
|
||
<span class="material-symbols-outlined text-3xl mb-1">image</span>
|
||
<span class="text-xs font-medium">Kein Bild</span>
|
||
</div>
|
||
</div>
|
||
<div class="flex-1 min-w-48">
|
||
<label for="gbHeroFileInput" class="block border-2 border-dashed border-primary/30 bg-surface-container-low rounded-xl p-5 text-center cursor-pointer hover:border-primary/60 transition-all mb-3">
|
||
<span class="material-symbols-outlined text-primary block mb-1">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 (16:9 empfohlen)</p>
|
||
</label>
|
||
<input type="file" id="gbHeroFileInput" accept="image/*" class="sr-only"/>
|
||
<button id="gbHeroUploadBtn" 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="gbHeroUploadStatus" class="text-xs text-on-surface-variant mt-2 text-center"></p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Hero Texte -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mb-4">Hero-Text</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Badge</label>
|
||
<input id="gbBadge" 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">Überschrift (normal)</label>
|
||
<input id="gbHeading" 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-bold"/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Überschrift (farbig)</label>
|
||
<input id="gbHeadingColored" 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-bold"/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Beschreibungstext</label>
|
||
<textarea id="gbDescription" rows="3" 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 resize-none"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Kontakt -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mb-4">Kontakt-Info (Sidebar)</h4>
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-8">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">E-Mail</label>
|
||
<input id="gbEmail" type="email" 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">Dojo-Name</label>
|
||
<input id="gbDojo" 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>
|
||
|
||
<!-- Einträge -->
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mt-8 mb-2">Gästebuch-Einträge</h4>
|
||
<p class="text-xs text-on-surface-variant mb-4">Erster Eintrag erscheint im "Neuester Eintrag"-Widget. Farbe "Akzent" erzeugt die lila/pink Verlaufskarte.</p>
|
||
<div id="gbEintragFields" class="space-y-3 mb-3"></div>
|
||
<button id="gbEintragAdd" type="button" class="flex items-center gap-2 text-sm font-bold text-primary border border-primary/30 rounded-xl px-4 py-2 hover:bg-pink-50 transition-all mb-8">
|
||
<span class="material-symbols-outlined text-base">add</span> Eintrag hinzufügen
|
||
</button>
|
||
|
||
<button id="gbSaveBtn" 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">
|
||
Gästebuch speichern & deployen
|
||
</button>
|
||
</section>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- ══ SECTION: SETTINGS ══ -->
|
||
<div id="section-settings" class="hidden">
|
||
<!-- Social Links -->
|
||
<section class="bg-surface-container-lowest rounded-xl p-8 shadow-sm mb-6">
|
||
<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">share</span>
|
||
Social Links
|
||
</h3>
|
||
<p class="text-sm text-on-surface-variant mb-6">Diese Links erscheinen im Footer auf allen Seiten der Website.</p>
|
||
<div class="space-y-4 mb-6">
|
||
<div class="flex items-center gap-3">
|
||
<span class="material-symbols-outlined text-on-surface-variant">photo_camera</span>
|
||
<input id="socialInstagram" type="text" placeholder="Instagram URL (oder #)"
|
||
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"/>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<span class="material-symbols-outlined text-on-surface-variant">smart_display</span>
|
||
<input id="socialYoutube" type="text" placeholder="YouTube URL (oder #)"
|
||
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"/>
|
||
</div>
|
||
<div class="flex items-center gap-3">
|
||
<span class="material-symbols-outlined text-on-surface-variant">mail</span>
|
||
<input id="socialEmail" type="email" placeholder="E-Mail-Adresse"
|
||
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"/>
|
||
</div>
|
||
</div>
|
||
<button type="button" id="saveSocialBtn"
|
||
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">save</span> Speichern & deployen
|
||
</button>
|
||
<p id="socialStatus" class="text-xs text-on-surface-variant mt-2"></p>
|
||
</section>
|
||
|
||
<!-- Gürtel-Rang -->
|
||
<section class="bg-surface-container-lowest rounded-xl p-8 shadow-sm mb-6">
|
||
<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">workspace_premium</span>
|
||
Gürtel-Rang (Global)
|
||
</h3>
|
||
<p class="text-sm text-on-surface-variant mb-4">Dieser Rang wird überall auf der Website angezeigt (Startseite, Über mich, Erfolge).</p>
|
||
<div class="flex gap-3">
|
||
<input id="globalBeltRank" type="text" placeholder="z.B. Grüngurt"
|
||
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 font-bold"/>
|
||
<button type="button" id="saveBeltRankBtn"
|
||
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">save</span> Speichern
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Site-Name -->
|
||
<section class="bg-surface-container-lowest rounded-xl p-8 shadow-sm mb-6">
|
||
<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">badge</span>
|
||
Site-Name
|
||
</h3>
|
||
<p class="text-sm text-on-surface-variant mb-4">Dieser Name erscheint in der Navbar, im Footer und überall auf der Website.</p>
|
||
<div class="flex gap-3">
|
||
<input id="siteTitleInput" type="text" placeholder="z.B. MiyaKarate"
|
||
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 font-bold font-lexend uppercase italic tracking-tight"/>
|
||
<button type="button" id="saveSiteTitleBtn"
|
||
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">save</span> Speichern
|
||
</button>
|
||
</div>
|
||
<p id="siteTitleStatus" class="text-xs text-on-surface-variant mt-2"></p>
|
||
</section>
|
||
|
||
<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 font-bold"/>
|
||
</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>
|
||
|
||
<!-- ═══ EDIT SUCCESS MODAL ═══ -->
|
||
<div id="editSuccessModal" 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-2xl shadow-2xl max-h-[90vh] overflow-y-auto">
|
||
<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_note</span> Bericht bearbeiten
|
||
</h3>
|
||
<input type="hidden" id="esFileName"/>
|
||
<div class="space-y-4">
|
||
<div class="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel</label>
|
||
<input id="esTitle" 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-bold"/>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Rang (z.B. Gold)</label>
|
||
<input id="esRang" 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">Ort</label>
|
||
<input id="esOrt" 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">Datum</label>
|
||
<input id="esDatum" 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 placeholder-zinc-400" placeholder="z.B. 2024-05-12"/>
|
||
</div>
|
||
<div class="col-span-2">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kategorie</label>
|
||
<input id="esKat" 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 placeholder-zinc-400" placeholder="z.B. Wettkämpfe"/>
|
||
</div>
|
||
<div class="col-span-2 flex items-center gap-3 mt-2">
|
||
<input id="esIsWeitere" type="checkbox" class="w-5 h-5 text-primary rounded border-outline-variant focus:ring-primary/30 cursor-pointer"/>
|
||
<label for="esIsWeitere" class="text-sm font-bold text-on-surface-variant cursor-pointer">Unter "Weitere Auszeichnungen" anzeigen (statt als ausführlicher Bericht)</label>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kurzfassung (Summary)</label>
|
||
<textarea id="esSummary" rows="2" 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 resize-none"></textarea>
|
||
</div>
|
||
<div>
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Inhalt (Markdown)</label>
|
||
<textarea id="esContent" rows="8" 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-mono"></textarea>
|
||
</div>
|
||
|
||
<!-- Bild Upload für Einzelerfolg -->
|
||
<div class="pt-4 border-t border-zinc-100">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Beitragsbilder (max 5, Klick wählt Hauptbild)</label>
|
||
|
||
<div id="esImagesPreviewContainer" class="flex gap-3 mb-4 flex-wrap">
|
||
<div class="w-full text-xs text-on-surface-variant italic">Keine Bilder vorhanden.</div>
|
||
</div>
|
||
|
||
<div class="flex items-center gap-4">
|
||
<input type="file" id="esFileInput" accept="image/*, video/*" multiple class="hidden"/>
|
||
<button type="button" onclick="document.getElementById('esFileInput').click()" class="text-xs font-bold text-primary bg-pink-50 px-4 py-2 rounded-full hover:bg-pink-100 transition-all">Neue Bilder wählen</button>
|
||
<p id="esFileStatus" class="text-[10px] text-on-surface-variant mt-1">Nichts ausgewählt</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="flex gap-3 mt-8">
|
||
<button type="button" id="saveSuccessBtn" 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" onclick="document.getElementById('editSuccessModal').classList.add('hidden')" 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>
|
||
|
||
<!-- ═══ BELT MODAL ═══ -->
|
||
<div id="beltModal" 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-3xl shadow-2xl max-h-[90vh] overflow-y-auto">
|
||
<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">workspace_premium</span> Gürtelweg bearbeiten
|
||
</h3>
|
||
<div class="mb-4">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel der Sektion</label>
|
||
<input id="beltSectionTitle" 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-bold"/>
|
||
</div>
|
||
<div class="mb-6">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Untertitel</label>
|
||
<input id="beltSectionSubtitle" 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>
|
||
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mb-4">Gürtel-Liste</h4>
|
||
<div id="beltListContainer" class="space-y-4 mb-6"></div>
|
||
|
||
<button id="addBeltBtn" type="button" class="flex items-center gap-2 text-sm font-bold text-primary border border-primary/30 rounded-xl px-4 py-2 hover:bg-pink-50 transition-all mb-8">
|
||
<span class="material-symbols-outlined text-base">add</span> Gürtel hinzufügen
|
||
</button>
|
||
|
||
<div class="flex gap-3 mt-8">
|
||
<button type="button" id="saveBeltsBtn" 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" onclick="document.getElementById('beltModal').classList.add('hidden')" 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 () {
|
||
// Website URL anpassen (Lokal vs. Server)
|
||
const isLocal = window.location.hostname === 'localhost' || window.location.hostname === 'karate' || window.location.hostname.startsWith('192.168.');
|
||
const websiteUrl = isLocal ? 'http://localhost:1313' : 'https://emy.bonzeipunk.de';
|
||
const linkEl = document.getElementById('viewWebsiteLink');
|
||
if (linkEl) {
|
||
linkEl.href = linkEl.href.replace('http://localhost:1313', websiteUrl);
|
||
}
|
||
|
||
// ─── State ───────────────────────────────────────────────
|
||
let selectedKat = '';
|
||
let currentFilter = 'Alle';
|
||
let allPhotos = [];
|
||
let allCategories = [];
|
||
let pendingFiles = [];
|
||
|
||
let esCurrentImages = [];
|
||
let esMainImageIndex = 0;
|
||
let esNewFilesSelected = false;
|
||
|
||
function renderEsImages(items) {
|
||
const container = document.getElementById('esImagesPreviewContainer');
|
||
container.innerHTML = '';
|
||
if (!items || items.length === 0) {
|
||
container.innerHTML = '<div class="w-full text-xs text-on-surface-variant italic">Keine Bilder vorhanden.</div>';
|
||
return;
|
||
}
|
||
|
||
for (let i = 0; i < items.length; i++) {
|
||
const item = items[i];
|
||
const isFile = item instanceof File;
|
||
const isVideo = isFile ? item.type.startsWith('video/') : item.match(/\.(mp4|webm|mov|m4v)$/i);
|
||
const src = isFile ? URL.createObjectURL(item) : '/erfolge-img/' + item + '?t=' + Date.now();
|
||
|
||
const div = document.createElement('div');
|
||
div.className = 'relative w-20 h-20 rounded-lg overflow-hidden border-4 cursor-pointer transition-all ' + (i === esMainImageIndex ? 'border-primary shadow-lg scale-105 z-10' : 'border-transparent hover:border-primary/50');
|
||
div.onclick = () => {
|
||
esMainImageIndex = i;
|
||
renderEsImages(items);
|
||
};
|
||
|
||
if (isVideo) {
|
||
const vid = document.createElement('video');
|
||
vid.src = src;
|
||
vid.className = 'w-full h-full object-cover';
|
||
vid.muted = true;
|
||
div.appendChild(vid);
|
||
|
||
const playIcon = document.createElement('span');
|
||
playIcon.className = 'material-symbols-outlined absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white drop-shadow-md pointer-events-none';
|
||
playIcon.textContent = 'play_circle';
|
||
div.appendChild(playIcon);
|
||
} else {
|
||
const img = document.createElement('img');
|
||
img.src = src;
|
||
img.className = 'w-full h-full object-cover';
|
||
div.appendChild(img);
|
||
}
|
||
|
||
const badge = document.createElement('div');
|
||
badge.className = 'absolute bottom-0 left-0 right-0 bg-primary/90 text-white text-[9px] text-center font-bold py-1 z-20 ' + (i === esMainImageIndex ? 'block' : 'hidden');
|
||
badge.textContent = 'HAUPTBILD';
|
||
|
||
const deleteBtn = document.createElement('button');
|
||
deleteBtn.className = 'absolute top-1 right-1 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-[10px] shadow-md z-30 hover:bg-red-600 transition-colors';
|
||
deleteBtn.innerHTML = '✕';
|
||
deleteBtn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
items.splice(i, 1);
|
||
if (esMainImageIndex >= items.length) esMainImageIndex = Math.max(0, items.length - 1);
|
||
renderEsImages(items);
|
||
document.getElementById('esFileStatus').textContent = items.length + ' Dateien insgesamt';
|
||
};
|
||
|
||
div.appendChild(badge);
|
||
div.appendChild(deleteBtn);
|
||
container.appendChild(div);
|
||
}
|
||
}
|
||
|
||
// ─── Navigation ──────────────────────────────────────────
|
||
const sections = ['overview', 'media', 'homepage', 'uebermich', '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.'],
|
||
uebermich: ['Über mich', 'Seite "Über mich" bearbeiten – Text, Bild, Erfolge, Zitat.'],
|
||
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(); loadGaleriePage(); }
|
||
if (name === 'homepage') loadHomepage();
|
||
if (name === 'uebermich') loadUebermich();
|
||
if (name === 'career') loadCareer();
|
||
if (name === 'community') loadGaestebuch();
|
||
if (name === 'settings') { renderCatManage(); loadSiteTitle(); loadSocialLinks(); }
|
||
}
|
||
|
||
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, '&').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;
|
||
document.getElementById('statCats').textContent = allCategories.length;
|
||
document.getElementById('statCatsSublabel').textContent = allCategories.slice(0, 3).join(', ') + (allCategories.length > 3 ? '...' : '');
|
||
|
||
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 hk = data.hero_karte || {};
|
||
document.getElementById('hpKarteStatus').value = hk.status || '';
|
||
const pk = data.pruefung_karte || {};
|
||
document.getElementById('hpPruefungTitel').value = pk.titel || '';
|
||
document.getElementById('hpPruefungDesc').value = pk.beschreibung || '';
|
||
const dk = data.dojo_karte || {};
|
||
document.getElementById('hpDojoTitel').value = dk.titel || '';
|
||
document.getElementById('hpDojoDesc').value = dk.beschreibung || '';
|
||
const cta = data.cta || {};
|
||
document.getElementById('hpCtaMain').value = cta.heading_main || '';
|
||
document.getElementById('hpCtaColored').value = cta.heading_colored || '';
|
||
document.getElementById('hpCtaDesc').value = cta.description || '';
|
||
document.getElementById('hpCtaBtn').value = cta.button_text || '';
|
||
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="' + escHtml(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="' + escHtml(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,
|
||
hero_karte: {
|
||
status: document.getElementById('hpKarteStatus').value
|
||
},
|
||
pruefung_karte: {
|
||
titel: document.getElementById('hpPruefungTitel').value,
|
||
beschreibung: document.getElementById('hpPruefungDesc').value
|
||
},
|
||
dojo_karte: {
|
||
titel: document.getElementById('hpDojoTitel').value,
|
||
beschreibung: document.getElementById('hpDojoDesc').value
|
||
},
|
||
cta: {
|
||
heading_main: document.getElementById('hpCtaMain').value,
|
||
heading_colored: document.getElementById('hpCtaColored').value,
|
||
description: document.getElementById('hpCtaDesc').value,
|
||
button_text: document.getElementById('hpCtaBtn').value
|
||
}
|
||
})
|
||
});
|
||
if (r.ok) showToast('✓ Startseite gespeichert & Deploy gestartet');
|
||
} catch (err) { console.error('Save error:', err); }
|
||
});
|
||
|
||
// ─── Über mich ────────────────────────────────────────────
|
||
function createWeitereRow(titel, detail, beschreibung, jahr, ort, kategorie) {
|
||
const row = document.createElement('div');
|
||
row.className = 'bg-surface-container-low rounded-xl p-4 flex flex-col gap-2';
|
||
row.innerHTML =
|
||
'<div class="flex gap-3 items-center">' +
|
||
'<input type="text" value="' + (titel||'') + '" placeholder="Titel" data-we="titel"' +
|
||
' class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-bold"/>' +
|
||
'<input type="text" value="' + (detail||'') + '" placeholder="Untertitel (z.B. Silber – Kata 2024)" data-we="detail"' +
|
||
' class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>' +
|
||
'<button type="button" class="we-delete shrink-0 w-8 h-8 flex items-center justify-center rounded-lg text-error hover:bg-error/10 transition-all">' +
|
||
'<span class="material-symbols-outlined text-lg">delete</span></button>' +
|
||
'</div>' +
|
||
'<textarea data-we="beschreibung" rows="2" placeholder="Beschreibung (optional)"' +
|
||
' class="w-full bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm resize-none">' + (beschreibung||'') + '</textarea>' +
|
||
'<div class="flex gap-3">' +
|
||
'<input type="text" value="' + (jahr||'') + '" placeholder="Jahr (z.B. 2024)" data-we="jahr"' +
|
||
' class="w-28 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>' +
|
||
'<input type="text" value="' + (ort||'') + '" placeholder="Ort (z.B. Berlin)" data-we="ort"' +
|
||
' class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>' +
|
||
'<input type="text" value="' + (kategorie||'') + '" placeholder="Kategorie (z.B. U14)" data-we="kategorie"' +
|
||
' class="w-28 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>' +
|
||
'</div>';
|
||
row.querySelector('.we-delete').addEventListener('click', function() { row.remove(); });
|
||
return row;
|
||
}
|
||
|
||
async function loadUebermich() {
|
||
try {
|
||
const r = await fetch('/api/uebermich');
|
||
const d = await r.json();
|
||
document.getElementById('umBadge').value = d.hero.badge || '';
|
||
document.getElementById('umH1').value = d.hero.heading_line1 || '';
|
||
document.getElementById('umH2').value = d.hero.heading_line2 || '';
|
||
document.getElementById('umHSuffix').value = d.hero.heading_suffix || '';
|
||
document.getElementById('umDesc').value = d.hero.description || '';
|
||
document.getElementById('umGurt').value = d.gurtweg.aktiver_gurt || 'Blau';
|
||
document.getElementById('umZitat').value = d.zitat.text || '';
|
||
document.getElementById('umZitatAutor').value = d.zitat.autor || '';
|
||
|
||
// Stats
|
||
const sf = document.getElementById('umStatsFields');
|
||
sf.innerHTML = '';
|
||
(d.stats || []).forEach(function(s) {
|
||
const row = document.createElement('div');
|
||
row.className = 'flex gap-3 items-center';
|
||
row.innerHTML =
|
||
'<input type="text" value="' + (s.wert||'') + '" placeholder="Wert" data-us="wert"' +
|
||
' 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-us="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"/>';
|
||
sf.appendChild(row);
|
||
});
|
||
|
||
if (d.hero.image) {
|
||
document.getElementById('umPreview').src = '/uebermich-img/' + d.hero.image + '?t=' + Date.now();
|
||
document.getElementById('umPreview').classList.remove('hidden');
|
||
document.getElementById('umPlaceholder').classList.add('hidden');
|
||
}
|
||
} catch(err) { console.error('Uebermich load error:', err); }
|
||
}
|
||
|
||
// ─── Gürtelweg (Gurtweg) ──────────────────────────────────
|
||
let currentUebermichDataForBelts = null;
|
||
|
||
window.openBeltModal = async function() {
|
||
try {
|
||
const r = await fetch('/api/uebermich');
|
||
currentUebermichDataForBelts = await r.json();
|
||
const gw = currentUebermichDataForBelts.gurtweg || {};
|
||
document.getElementById('beltSectionTitle').value = gw.title || '';
|
||
document.getElementById('beltSectionSubtitle').value = gw.subtitle || '';
|
||
renderBeltsList(gw.gurte || []);
|
||
document.getElementById('beltModal').classList.remove('hidden');
|
||
} catch(err) { console.error('Load belts error:', err); }
|
||
};
|
||
|
||
function renderBeltsList(gurte) {
|
||
const container = document.getElementById('beltListContainer');
|
||
container.innerHTML = '';
|
||
gurte.forEach((g, i) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'bg-surface-container-low rounded-xl p-4 flex flex-col gap-3 relative border border-zinc-100';
|
||
div.innerHTML = `
|
||
<div class="flex gap-3 items-center">
|
||
<input type="text" value="${escHtml(g.name || '')}" placeholder="Gürtel Name" data-belt="name"
|
||
class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-bold"/>
|
||
<input type="text" value="${escHtml(g.farbe || '')}" placeholder="CSS-Klasse (z.B. bg-yellow-400)" data-belt="farbe"
|
||
class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-mono text-[10px]"/>
|
||
<button type="button" onclick="this.parentElement.parentElement.remove()" class="shrink-0 w-8 h-8 flex items-center justify-center rounded-lg text-error hover:bg-error/10 transition-all">
|
||
<span class="material-symbols-outlined text-lg">delete</span>
|
||
</button>
|
||
</div>
|
||
<textarea data-belt="geschichte" rows="2" placeholder="Die Geschichte dieses Gürtels..."
|
||
class="w-full bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm resize-none">${escHtml(g.geschichte || '')}</textarea>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
}
|
||
|
||
document.getElementById('addBeltBtn').addEventListener('click', () => {
|
||
const container = document.getElementById('beltListContainer');
|
||
const div = document.createElement('div');
|
||
div.className = 'bg-surface-container-low rounded-xl p-4 flex flex-col gap-3 relative border border-zinc-100';
|
||
div.innerHTML = `
|
||
<div class="flex gap-3 items-center">
|
||
<input type="text" value="" placeholder="Gürtel Name" data-belt="name"
|
||
class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-bold"/>
|
||
<input type="text" value="" placeholder="CSS-Klasse (z.B. bg-yellow-400)" data-belt="farbe"
|
||
class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-mono text-[10px]"/>
|
||
<button type="button" onclick="this.parentElement.parentElement.remove()" class="shrink-0 w-8 h-8 flex items-center justify-center rounded-lg text-error hover:bg-error/10 transition-all">
|
||
<span class="material-symbols-outlined text-lg">delete</span>
|
||
</button>
|
||
</div>
|
||
<textarea data-belt="geschichte" rows="2" placeholder="Die Geschichte dieses Gürtels..."
|
||
class="w-full bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm resize-none"></textarea>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
|
||
document.getElementById('saveBeltsBtn').addEventListener('click', async () => {
|
||
if (!currentUebermichDataForBelts) return;
|
||
const rows = document.getElementById('beltListContainer').querySelectorAll(':scope > div');
|
||
const gurte = Array.from(rows).map(row => {
|
||
return {
|
||
name: row.querySelector('[data-belt="name"]').value,
|
||
farbe: row.querySelector('[data-belt="farbe"]').value,
|
||
geschichte: row.querySelector('[data-belt="geschichte"]').value
|
||
};
|
||
});
|
||
|
||
currentUebermichDataForBelts.gurtweg = {
|
||
title: document.getElementById('beltSectionTitle').value,
|
||
subtitle: document.getElementById('beltSectionSubtitle').value,
|
||
aktiver_gurt: currentUebermichDataForBelts.gurtweg.aktiver_gurt,
|
||
gurte: gurte
|
||
};
|
||
|
||
try {
|
||
const res = await fetch('/api/uebermich', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(currentUebermichDataForBelts)
|
||
});
|
||
if (res.ok) {
|
||
showToast('✓ Gürtelweg gespeichert & Deploy gestartet');
|
||
document.getElementById('beltModal').classList.add('hidden');
|
||
}
|
||
} catch(err) { console.error('Save belts error:', err); }
|
||
});
|
||
|
||
document.getElementById('umFileInput').addEventListener('change', function() {
|
||
const file = this.files[0];
|
||
if (!file) return;
|
||
document.getElementById('umPreview').src = URL.createObjectURL(file);
|
||
document.getElementById('umPreview').classList.remove('hidden');
|
||
document.getElementById('umPlaceholder').classList.add('hidden');
|
||
document.getElementById('umUploadBtn').disabled = false;
|
||
document.getElementById('umUploadStatus').textContent = file.name;
|
||
});
|
||
|
||
document.getElementById('umUploadBtn').addEventListener('click', async function() {
|
||
const file = document.getElementById('umFileInput').files[0];
|
||
if (!file) return;
|
||
this.disabled = true;
|
||
document.getElementById('umUploadStatus').textContent = 'Wird hochgeladen…';
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('image', file);
|
||
const r = await fetch('/api/uebermich/image', { method: 'POST', body: fd });
|
||
if (r.ok) {
|
||
showToast('✓ Bild gespeichert');
|
||
document.getElementById('umUploadStatus').textContent = 'Gespeichert!';
|
||
} else {
|
||
document.getElementById('umUploadStatus').textContent = 'Fehler beim Upload';
|
||
}
|
||
} catch(err) {
|
||
document.getElementById('umUploadStatus').textContent = 'Verbindungsfehler';
|
||
}
|
||
this.disabled = false;
|
||
});
|
||
|
||
document.getElementById('umSaveBtn').addEventListener('click', async function() {
|
||
try {
|
||
const r = await fetch('/api/uebermich');
|
||
const data = await r.json();
|
||
|
||
data.hero.badge = document.getElementById('umBadge').value;
|
||
data.hero.heading_line1 = document.getElementById('umH1').value;
|
||
data.hero.heading_line2 = document.getElementById('umH2').value;
|
||
data.hero.heading_suffix = document.getElementById('umHSuffix').value;
|
||
data.hero.description = document.getElementById('umDesc').value;
|
||
data.gurtweg.aktiver_gurt = document.getElementById('umGurt').value;
|
||
data.zitat.text = document.getElementById('umZitat').value;
|
||
data.zitat.autor = document.getElementById('umZitatAutor').value;
|
||
|
||
const stRows = document.getElementById('umStatsFields').querySelectorAll('div.flex');
|
||
data.stats = Array.from(stRows).map(function(row) {
|
||
return {
|
||
wert: row.querySelector('[data-us="wert"]').value,
|
||
label: row.querySelector('[data-us="label"]').value
|
||
};
|
||
});
|
||
|
||
const res = await fetch('/api/uebermich', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
if (res.ok) showToast('✓ Über mich gespeichert & Deploy gestartet');
|
||
} catch(err) { console.error('Save error:', err); }
|
||
});
|
||
|
||
// ─── Erfolge (Career) ─────────────────────────────────────
|
||
function createAuszeichnungRow(titel, detail, beschreibung) {
|
||
const row = document.createElement('div');
|
||
row.className = 'bg-surface-container-low rounded-xl p-4 flex flex-col gap-3';
|
||
row.innerHTML =
|
||
'<div class="flex gap-3 items-center">' +
|
||
'<input type="text" value="' + escHtml(titel||'') + '" placeholder="Titel" data-ea="titel"' +
|
||
' class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-bold"/>' +
|
||
'<input type="text" value="' + escHtml(detail||'') + '" placeholder="Untertitel (z.B. Silber – Kata 2024)" data-ea="detail"' +
|
||
' class="flex-1 bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>' +
|
||
'<button type="button" class="ea-delete shrink-0 w-8 h-8 flex items-center justify-center rounded-lg text-error hover:bg-error/10 transition-all">' +
|
||
'<span class="material-symbols-outlined text-lg">delete</span></button>' +
|
||
'</div>' +
|
||
'<textarea data-ea="beschreibung" rows="2" placeholder="Beschreibung (optional)"' +
|
||
' class="w-full bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm resize-none">' + escHtml(beschreibung||'') + '</textarea>';
|
||
row.querySelector('.ea-delete').addEventListener('click', function() { row.remove(); });
|
||
return row;
|
||
}
|
||
document.getElementById('efWeitereAdd').addEventListener('click', function() {
|
||
document.getElementById('efWeitereFields').appendChild(createAuszeichnungRow('', ''));
|
||
});
|
||
|
||
document.getElementById('efHeroFileInput').addEventListener('change', function() {
|
||
const file = this.files[0];
|
||
if (!file) return;
|
||
document.getElementById('efHeroPreview').src = URL.createObjectURL(file);
|
||
document.getElementById('efHeroPreview').classList.remove('hidden');
|
||
document.getElementById('efHeroPlaceholder').classList.add('hidden');
|
||
document.getElementById('efHeroUploadBtn').disabled = false;
|
||
document.getElementById('efHeroUploadStatus').textContent = file.name;
|
||
});
|
||
|
||
document.getElementById('efHeroUploadBtn').addEventListener('click', async function() {
|
||
const file = document.getElementById('efHeroFileInput').files[0];
|
||
if (!file) return;
|
||
this.disabled = true;
|
||
document.getElementById('efHeroUploadStatus').textContent = 'Wird hochgeladen…';
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('image', file);
|
||
const r = await fetch('/api/erfolge/image', { method: 'POST', body: fd });
|
||
if (r.ok) {
|
||
showToast('✓ Bild gespeichert');
|
||
document.getElementById('efHeroUploadStatus').textContent = 'Gespeichert!';
|
||
} else {
|
||
document.getElementById('efHeroUploadStatus').textContent = 'Fehler beim Upload';
|
||
}
|
||
} catch(err) {
|
||
document.getElementById('efHeroUploadStatus').textContent = 'Verbindungsfehler';
|
||
}
|
||
this.disabled = false;
|
||
});
|
||
|
||
async function loadCareer() {
|
||
try {
|
||
const r = await fetch('/api/erfolge');
|
||
const d = await r.json();
|
||
const hero = d.hero || {};
|
||
document.getElementById('efBadge').value = hero.badge || '';
|
||
document.getElementById('efHeadingMain').value = hero.heading_main || '';
|
||
document.getElementById('efHeadingColored').value = hero.heading_colored || '';
|
||
document.getElementById('efDescription').value = hero.description || '';
|
||
if (hero.image) {
|
||
document.getElementById('efHeroPreview').src = '/erfolge-img/' + hero.image + '?t=' + Date.now();
|
||
document.getElementById('efHeroPreview').classList.remove('hidden');
|
||
document.getElementById('efHeroPlaceholder').classList.add('hidden');
|
||
}
|
||
const m = d.meilenstein || {};
|
||
document.getElementById('efMeilensteinEvent').value = m.event || '';
|
||
document.getElementById('efMeilensteinPlatz').value = m.platz || '';
|
||
document.getElementById('efMeilensteinDesc').value = m.beschreibung || '';
|
||
document.getElementById('efMeilensteinJahr').value = m.jahr || '';
|
||
document.getElementById('efMeilensteinOrt').value = m.ort || '';
|
||
document.getElementById('efMeilensteinKat').value = m.kategorie || '';
|
||
const wf = document.getElementById('efWeitereFields');
|
||
wf.innerHTML = '';
|
||
(d.weitere_auszeichnungen || []).forEach(function(a) {
|
||
wf.appendChild(createAuszeichnungRow(a.titel, a.detail, a.beschreibung));
|
||
});
|
||
const sf = document.getElementById('efStatsFields');
|
||
sf.innerHTML = '';
|
||
(d.stats || []).forEach(function(s) {
|
||
const row = document.createElement('div');
|
||
row.className = 'flex gap-3 items-center';
|
||
row.innerHTML =
|
||
'<input type="text" value="' + escHtml(s.wert||'') + '" placeholder="Wert" data-es="wert"' +
|
||
' 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="' + escHtml(s.label||'') + '" placeholder="Bezeichnung" data-es="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"/>';
|
||
sf.appendChild(row);
|
||
});
|
||
document.getElementById('efZitatText').value = (d.zitat || {}).text || '';
|
||
document.getElementById('efZitatAutor').value = (d.zitat || {}).autor || '';
|
||
loadIndividualSuccesses();
|
||
} catch(err) { console.error('Career load error:', err); }
|
||
}
|
||
|
||
document.getElementById('careerSaveBtn').addEventListener('click', async function() {
|
||
try {
|
||
const r0 = await fetch('/api/erfolge');
|
||
const existing = await r0.json();
|
||
const weRows = document.getElementById('efWeitereFields').querySelectorAll(':scope > div');
|
||
const stRows = document.getElementById('efStatsFields').querySelectorAll('div.flex');
|
||
const data = {
|
||
hero: {
|
||
badge: document.getElementById('efBadge').value,
|
||
heading_main: document.getElementById('efHeadingMain').value,
|
||
heading_colored: document.getElementById('efHeadingColored').value,
|
||
description: document.getElementById('efDescription').value,
|
||
rang_label: (existing.hero || {}).rang_label || 'Aktueller Rang',
|
||
image: (existing.hero || {}).image || ''
|
||
},
|
||
meilenstein: {
|
||
event: document.getElementById('efMeilensteinEvent').value,
|
||
platz: document.getElementById('efMeilensteinPlatz').value,
|
||
beschreibung: document.getElementById('efMeilensteinDesc').value,
|
||
jahr: document.getElementById('efMeilensteinJahr').value,
|
||
ort: document.getElementById('efMeilensteinOrt').value,
|
||
kategorie: document.getElementById('efMeilensteinKat').value
|
||
},
|
||
weitere_auszeichnungen: Array.from(weRows).map(function(row) {
|
||
return {
|
||
titel: row.querySelector('[data-ea="titel"]').value,
|
||
detail: row.querySelector('[data-ea="detail"]').value,
|
||
beschreibung: row.querySelector('[data-ea="beschreibung"]').value
|
||
};
|
||
}),
|
||
stats: Array.from(stRows).map(function(row) {
|
||
return {
|
||
wert: row.querySelector('[data-es="wert"]').value,
|
||
label: row.querySelector('[data-es="label"]').value
|
||
};
|
||
}),
|
||
zitat: {
|
||
text: document.getElementById('efZitatText').value,
|
||
autor: document.getElementById('efZitatAutor').value
|
||
}
|
||
};
|
||
|
||
const res = await fetch('/api/erfolge', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
if (res.ok) showToast('✓ Erfolge gespeichert & Deploy gestartet');
|
||
} catch(err) { console.error('Career save error:', err); }
|
||
});
|
||
|
||
let individualSuccessData = [];
|
||
|
||
async function loadIndividualSuccesses() {
|
||
try {
|
||
const r = await fetch('/api/individual-successes');
|
||
individualSuccessData = await r.json();
|
||
renderIndividualSuccesses();
|
||
} catch(err) { console.error('Load individual successes error:', err); }
|
||
}
|
||
|
||
function renderIndividualSuccesses() {
|
||
const container = document.getElementById('individualSuccessList');
|
||
container.innerHTML = '';
|
||
individualSuccessData.forEach((item, index) => {
|
||
const div = document.createElement('div');
|
||
div.className = 'flex items-center justify-between bg-surface-container-low rounded-xl px-6 py-4';
|
||
div.innerHTML = `
|
||
<div class="flex items-center gap-4">
|
||
<div class="flex flex-col gap-1 mr-2">
|
||
<button type="button" onclick="window.moveSuccessUp(${index})" class="text-on-surface-variant hover:text-primary disabled:opacity-30" ${index === 0 ? 'disabled' : ''}>
|
||
<span class="material-symbols-outlined text-sm">arrow_upward</span>
|
||
</button>
|
||
<button type="button" onclick="window.moveSuccessDown(${index})" class="text-on-surface-variant hover:text-primary disabled:opacity-30" ${index === individualSuccessData.length - 1 ? 'disabled' : ''}>
|
||
<span class="material-symbols-outlined text-sm">arrow_downward</span>
|
||
</button>
|
||
</div>
|
||
<div class="w-10 h-10 rounded-lg bg-white flex items-center justify-center text-primary">
|
||
<span class="material-symbols-outlined">description</span>
|
||
</div>
|
||
<div>
|
||
<p class="font-bold text-sm">${escHtml(item.title)}</p>
|
||
<p class="text-[10px] text-on-surface-variant uppercase font-bold tracking-widest">${escHtml(item.rang || 'Bericht')}</p>
|
||
</div>
|
||
</div>
|
||
<button type="button" onclick="window.editSuccess('${escHtml(item.fileName)}')" class="text-xs font-bold text-primary hover:bg-pink-50 px-4 py-2 rounded-full transition-all flex items-center gap-1">
|
||
<span class="material-symbols-outlined text-sm">edit</span> Bearbeiten<button type="button" onclick="window.manageInteractions('${escHtml(item.fileName)}', '${escHtml(item.title)}')" class="text-xs font-bold text-secondary hover:bg-purple-50 px-4 py-2 rounded-full transition-all flex items-center gap-1"><span class="material-symbols-outlined text-sm">favorite</span> Interaktionen
|
||
</button>
|
||
`;
|
||
container.appendChild(div);
|
||
});
|
||
}
|
||
|
||
window.moveSuccessUp = async function(index) {
|
||
if (index <= 0) return;
|
||
const temp = individualSuccessData[index - 1];
|
||
individualSuccessData[index - 1] = individualSuccessData[index];
|
||
individualSuccessData[index] = temp;
|
||
renderIndividualSuccesses();
|
||
await saveIndividualSuccessOrder();
|
||
};
|
||
|
||
window.moveSuccessDown = async function(index) {
|
||
if (index >= individualSuccessData.length - 1) return;
|
||
const temp = individualSuccessData[index + 1];
|
||
individualSuccessData[index + 1] = individualSuccessData[index];
|
||
individualSuccessData[index] = temp;
|
||
renderIndividualSuccesses();
|
||
await saveIndividualSuccessOrder();
|
||
};
|
||
|
||
async function saveIndividualSuccessOrder() {
|
||
try {
|
||
const order = individualSuccessData.map(item => item.fileName);
|
||
const res = await fetch('/api/individual-successes/reorder', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ order })
|
||
});
|
||
if (!res.ok) throw new Error('Reorder failed');
|
||
} catch(err) {
|
||
console.error('Save order error:', err);
|
||
}
|
||
}
|
||
|
||
window.editSuccess = async function(fileName) {
|
||
try {
|
||
const r = await fetch('/api/individual-success/' + fileName);
|
||
const data = await r.json();
|
||
document.getElementById('esFileName').value = fileName;
|
||
document.getElementById('esTitle').value = data.title || '';
|
||
document.getElementById('esRang').value = data.rang || '';
|
||
document.getElementById('esOrt').value = data.ort || '';
|
||
document.getElementById('esDatum').value = data.datum || '';
|
||
document.getElementById('esKat').value = data.kategorie || '';
|
||
document.getElementById('esIsWeitere').checked = data.is_weitere_auszeichnung === 'true' || data.is_weitere_auszeichnung === true;
|
||
document.getElementById('esSummary').value = data.summary || '';
|
||
document.getElementById('esContent').value = data.content || '';
|
||
|
||
esNewFilesSelected = false;
|
||
document.getElementById('esFileInput').value = '';
|
||
document.getElementById('esFileStatus').textContent = 'Keine neuen Dateien gewählt';
|
||
|
||
let existingImages = [];
|
||
if (data.images) {
|
||
existingImages = data.images.split(',').map(s => s.trim()).filter(Boolean);
|
||
} else if (data.image) {
|
||
existingImages = [data.image];
|
||
}
|
||
|
||
esCurrentImages = existingImages;
|
||
esMainImageIndex = 0;
|
||
if (data.image && existingImages.includes(data.image)) {
|
||
esMainImageIndex = existingImages.indexOf(data.image);
|
||
}
|
||
|
||
renderEsImages(esCurrentImages);
|
||
document.getElementById('editSuccessModal').classList.remove('hidden');
|
||
} catch(err) { console.error('Edit success error:', err); }
|
||
}
|
||
|
||
document.getElementById('esFileInput').addEventListener('change', function() {
|
||
const newFiles = Array.from(this.files);
|
||
if (esCurrentImages.length + newFiles.length > 5) {
|
||
alert(`Bitte maximal 5 Dateien insgesamt auswählen! (Aktuell: ${esCurrentImages.length})`);
|
||
this.value = '';
|
||
return;
|
||
}
|
||
if (newFiles.length > 0) {
|
||
esNewFilesSelected = true;
|
||
esCurrentImages = esCurrentImages.concat(newFiles);
|
||
renderEsImages(esCurrentImages);
|
||
document.getElementById('esFileStatus').textContent = esCurrentImages.length + ' Dateien insgesamt';
|
||
}
|
||
this.value = '';
|
||
});
|
||
|
||
document.getElementById('saveSuccessBtn').addEventListener('click', async function() {
|
||
const fileName = document.getElementById('esFileName').value;
|
||
const title = document.getElementById('esTitle').value;
|
||
const rang = document.getElementById('esRang').value;
|
||
const ort = document.getElementById('esOrt').value;
|
||
const datum = document.getElementById('esDatum').value;
|
||
const kategorie = document.getElementById('esKat').value;
|
||
const is_weitere_auszeichnung = document.getElementById('esIsWeitere').checked;
|
||
const summary = document.getElementById('esSummary').value;
|
||
const content = document.getElementById('esContent').value;
|
||
const fileInput = document.getElementById('esFileInput');
|
||
|
||
try {
|
||
const existingOnly = esCurrentImages.filter(x => typeof x === 'string');
|
||
const newFilesOnly = esCurrentImages.filter(x => x instanceof File);
|
||
const selectedMainImage = (newFilesOnly.length === 0 && existingOnly.length > 0) ? existingOnly[esMainImageIndex] : '';
|
||
const finalImagesStr = existingOnly.join(',');
|
||
|
||
// 1. Metadaten & Inhalt speichern
|
||
const r = await fetch('/api/individual-success/' + fileName, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
title, rang, ort, datum, kategorie, is_weitere_auszeichnung, summary, content,
|
||
image: selectedMainImage,
|
||
images: finalImagesStr
|
||
})
|
||
});
|
||
|
||
// 2. Bilder falls gewählt
|
||
if (newFilesOnly.length > 0) {
|
||
const fd = new FormData();
|
||
for (let i = 0; i < newFilesOnly.length; i++) {
|
||
fd.append('images', newFilesOnly[i]);
|
||
}
|
||
fd.append('existingImages', JSON.stringify(existingOnly));
|
||
fd.append('mainImageIndex', esMainImageIndex);
|
||
await fetch('/api/individual-success/' + fileName + '/images', {
|
||
method: 'POST',
|
||
body: fd
|
||
});
|
||
}
|
||
|
||
if (r.ok) {
|
||
showToast('✓ Bericht gespeichert & Deploy gestartet');
|
||
document.getElementById('editSuccessModal').classList.add('hidden');
|
||
loadIndividualSuccesses();
|
||
}
|
||
} catch(err) { console.error('Save success error:', err); }
|
||
});
|
||
|
||
// ─── Site-Name ────────────────────────────────────────────
|
||
async function loadSiteTitle() {
|
||
try {
|
||
const r = await fetch('/api/homepage');
|
||
const data = await r.json();
|
||
const title = data.siteTitle || '';
|
||
document.getElementById('siteTitleInput').value = title;
|
||
updateAdminTitle(title);
|
||
} catch (err) { console.error('SiteTitle load error:', err); }
|
||
}
|
||
|
||
function updateAdminTitle(title) {
|
||
if (!title) return;
|
||
document.getElementById('adminSiteTitle').textContent = title;
|
||
document.getElementById('adminSiteTitleHeader').textContent = title;
|
||
}
|
||
|
||
document.getElementById('saveSiteTitleBtn').addEventListener('click', async function () {
|
||
const status = document.getElementById('siteTitleStatus');
|
||
const val = document.getElementById('siteTitleInput').value.trim();
|
||
if (!val) { status.textContent = 'Bitte einen Namen eingeben.'; return; }
|
||
try {
|
||
const r = await fetch('/api/homepage', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ siteTitle: val })
|
||
});
|
||
if (r.ok) {
|
||
updateAdminTitle(val);
|
||
showToast('✓ Site-Name gespeichert & Deploy gestartet');
|
||
status.textContent = '';
|
||
} else {
|
||
status.textContent = 'Fehler beim Speichern.';
|
||
}
|
||
} catch (err) { status.textContent = 'Verbindungsfehler'; }
|
||
});
|
||
|
||
// ─── Galerie Seiten-Texte ─────────────────────────────────
|
||
async function loadGaleriePage() {
|
||
try {
|
||
const r = await fetch('/api/galerie');
|
||
const d = await r.json();
|
||
const hero = d.hero || {};
|
||
document.getElementById('galBadge').value = hero.badge || '';
|
||
document.getElementById('galHeadingMain').value = hero.heading_main || '';
|
||
document.getElementById('galHeadingColored').value = hero.heading_colored || '';
|
||
document.getElementById('galDescription').value = hero.description || '';
|
||
const ribbon = d.ribbon || {};
|
||
document.getElementById('galRibbonHeading').value = ribbon.heading || '';
|
||
document.getElementById('galRibbonDesc').value = ribbon.description || '';
|
||
const sf = document.getElementById('galStatsFields');
|
||
sf.innerHTML = '';
|
||
(ribbon.stats || []).forEach(function(s) {
|
||
const row = document.createElement('div');
|
||
row.className = 'flex gap-3 items-center';
|
||
row.innerHTML =
|
||
'<input type="text" value="' + escHtml(s.wert||'') + '" placeholder="Wert" data-gs="wert"' +
|
||
' 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="' + escHtml(s.label||'') + '" placeholder="Bezeichnung" data-gs="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"/>';
|
||
sf.appendChild(row);
|
||
});
|
||
} catch(err) { console.error('Galerie page load error:', err); }
|
||
}
|
||
|
||
document.getElementById('galSaveBtn').addEventListener('click', async function() {
|
||
try {
|
||
const stRows = document.getElementById('galStatsFields').querySelectorAll('div.flex');
|
||
const data = {
|
||
hero: {
|
||
badge: document.getElementById('galBadge').value,
|
||
heading_main: document.getElementById('galHeadingMain').value,
|
||
heading_colored: document.getElementById('galHeadingColored').value,
|
||
description: document.getElementById('galDescription').value
|
||
},
|
||
ribbon: {
|
||
heading: document.getElementById('galRibbonHeading').value,
|
||
description: document.getElementById('galRibbonDesc').value,
|
||
stats: Array.from(stRows).map(function(row) {
|
||
return {
|
||
wert: row.querySelector('[data-gs="wert"]').value,
|
||
label: row.querySelector('[data-gs="label"]').value
|
||
};
|
||
})
|
||
}
|
||
};
|
||
const res = await fetch('/api/galerie', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
if (res.ok) showToast('✓ Galerie-Texte gespeichert & Deploy gestartet');
|
||
} catch(err) { console.error('Galerie save error:', err); }
|
||
});
|
||
|
||
// ─── Gästebuch ────────────────────────────────────────────
|
||
function createEintragRow(name, rolle, text, farbe, breit) {
|
||
const row = document.createElement('div');
|
||
row.className = 'bg-surface-container-low rounded-xl p-4 flex flex-col gap-3';
|
||
row.innerHTML =
|
||
'<div class="flex gap-3 items-start">' +
|
||
'<div class="flex-1 grid grid-cols-2 gap-3">' +
|
||
'<input type="text" value="' + escHtml(name||'') + '" placeholder="Name" data-gb="name"' +
|
||
' class="bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-bold"/>' +
|
||
'<input type="text" value="' + escHtml(rolle||'') + '" placeholder="Rolle (z.B. Trainerin)" data-gb="rolle"' +
|
||
' class="bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>' +
|
||
'</div>' +
|
||
'<button type="button" class="gb-delete shrink-0 w-8 h-8 flex items-center justify-center rounded-lg text-error hover:bg-error/10 transition-all">' +
|
||
'<span class="material-symbols-outlined text-lg">delete</span></button>' +
|
||
'</div>' +
|
||
'<textarea data-gb="text" rows="2" placeholder="Nachricht"' +
|
||
' class="w-full bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm resize-none">' + escHtml(text||'') + '</textarea>' +
|
||
'<div class="flex gap-4 items-center flex-wrap">' +
|
||
'<label class="flex items-center gap-2 text-sm cursor-pointer">' +
|
||
'<input type="checkbox" data-gb="breit" ' + (breit ? 'checked' : '') + ' class="rounded"/>' +
|
||
'<span class="text-on-surface-variant font-medium">Breite Karte</span>' +
|
||
'</label>' +
|
||
'<select data-gb="farbe" class="bg-white rounded-DEFAULT px-3 py-2 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm">' +
|
||
'<option value="default"' + ((!farbe || farbe === 'default') ? ' selected' : '') + '>Standard (weiß)</option>' +
|
||
'<option value="secondary"' + (farbe === 'secondary' ? ' selected' : '') + '>Akzent (lila/pink)</option>' +
|
||
'</select>' +
|
||
'</div>';
|
||
row.querySelector('.gb-delete').addEventListener('click', function() { row.remove(); });
|
||
return row;
|
||
}
|
||
|
||
document.getElementById('gbEintragAdd').addEventListener('click', function() {
|
||
document.getElementById('gbEintragFields').appendChild(createEintragRow('', '', '', 'default', false));
|
||
});
|
||
|
||
document.getElementById('gbHeroFileInput').addEventListener('change', function() {
|
||
const file = this.files[0];
|
||
if (!file) return;
|
||
document.getElementById('gbHeroPreview').src = URL.createObjectURL(file);
|
||
document.getElementById('gbHeroPreview').classList.remove('hidden');
|
||
document.getElementById('gbHeroPlaceholder').classList.add('hidden');
|
||
document.getElementById('gbHeroUploadBtn').disabled = false;
|
||
document.getElementById('gbHeroUploadStatus').textContent = file.name;
|
||
});
|
||
|
||
document.getElementById('gbHeroUploadBtn').addEventListener('click', async function() {
|
||
const file = document.getElementById('gbHeroFileInput').files[0];
|
||
if (!file) return;
|
||
this.disabled = true;
|
||
document.getElementById('gbHeroUploadStatus').textContent = 'Wird hochgeladen…';
|
||
try {
|
||
const fd = new FormData();
|
||
fd.append('image', file);
|
||
const r = await fetch('/api/gaestebuch/image', { method: 'POST', body: fd });
|
||
if (r.ok) {
|
||
showToast('✓ Bild gespeichert');
|
||
document.getElementById('gbHeroUploadStatus').textContent = 'Gespeichert!';
|
||
} else {
|
||
document.getElementById('gbHeroUploadStatus').textContent = 'Fehler beim Upload';
|
||
}
|
||
} catch(err) {
|
||
document.getElementById('gbHeroUploadStatus').textContent = 'Verbindungsfehler';
|
||
}
|
||
this.disabled = false;
|
||
});
|
||
|
||
async function loadGaestebuch() {
|
||
try {
|
||
const r = await fetch('/api/gaestebuch');
|
||
const d = await r.json();
|
||
const hero = d.hero || {};
|
||
document.getElementById('gbBadge').value = hero.badge || '';
|
||
document.getElementById('gbHeading').value = hero.heading || '';
|
||
document.getElementById('gbHeadingColored').value = hero.heading_colored || '';
|
||
document.getElementById('gbDescription').value = hero.description || '';
|
||
const kontakt = d.kontakt || {};
|
||
document.getElementById('gbEmail').value = kontakt.email || '';
|
||
document.getElementById('gbDojo').value = kontakt.dojo || '';
|
||
if (hero.image) {
|
||
document.getElementById('gbHeroPreview').src = '/gaestebuch-img/' + hero.image + '?t=' + Date.now();
|
||
document.getElementById('gbHeroPreview').classList.remove('hidden');
|
||
document.getElementById('gbHeroPlaceholder').classList.add('hidden');
|
||
}
|
||
const ef = document.getElementById('gbEintragFields');
|
||
ef.innerHTML = '';
|
||
(d.eintraege || []).forEach(function(e) {
|
||
ef.appendChild(createEintragRow(e.name, e.rolle, e.text, e.farbe, e.breit));
|
||
});
|
||
} catch(err) { console.error('Gaestebuch load error:', err); }
|
||
}
|
||
|
||
document.getElementById('gbSaveBtn').addEventListener('click', async function() {
|
||
try {
|
||
const r0 = await fetch('/api/gaestebuch');
|
||
const existing = await r0.json();
|
||
const rows = document.getElementById('gbEintragFields').querySelectorAll(':scope > div');
|
||
const eintraege = Array.from(rows).map(function(row, i) {
|
||
return {
|
||
id: String(i + 1),
|
||
name: row.querySelector('[data-gb="name"]').value,
|
||
rolle: row.querySelector('[data-gb="rolle"]').value,
|
||
text: row.querySelector('[data-gb="text"]').value,
|
||
farbe: row.querySelector('[data-gb="farbe"]').value,
|
||
breit: row.querySelector('[data-gb="breit"]').checked
|
||
};
|
||
});
|
||
const data = {
|
||
hero: {
|
||
badge: document.getElementById('gbBadge').value,
|
||
heading: document.getElementById('gbHeading').value,
|
||
heading_colored: document.getElementById('gbHeadingColored').value,
|
||
description: document.getElementById('gbDescription').value,
|
||
image: (existing.hero || {}).image || ''
|
||
},
|
||
kontakt: {
|
||
email: document.getElementById('gbEmail').value,
|
||
dojo: document.getElementById('gbDojo').value
|
||
},
|
||
eintraege: eintraege
|
||
};
|
||
const res = await fetch('/api/gaestebuch', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
if (res.ok) showToast('✓ Gästebuch gespeichert & Deploy gestartet');
|
||
} catch(err) { console.error('Gaestebuch save error:', err); }
|
||
});
|
||
|
||
// ─── Global (Social, Rank) ───────────────────────────────
|
||
async function loadGlobal() {
|
||
try {
|
||
const r = await fetch('/api/global');
|
||
const d = await r.json();
|
||
const social = d.social || {};
|
||
document.getElementById('socialInstagram').value = social.instagram || '';
|
||
document.getElementById('socialYoutube').value = social.youtube || '';
|
||
document.getElementById('socialEmail').value = social.email || '';
|
||
document.getElementById('globalBeltRank').value = d.belt_rank || '';
|
||
} catch(err) { console.error('Global load error:', err); }
|
||
}
|
||
|
||
document.getElementById('saveBeltRankBtn').addEventListener('click', async function() {
|
||
const val = document.getElementById('globalBeltRank').value.trim();
|
||
try {
|
||
const r = await fetch('/api/global', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ belt_rank: val })
|
||
});
|
||
if (r.ok) showToast('✓ Gürtel-Rang gespeichert & Deploy gestartet');
|
||
} catch(err) { console.error('Save error:', err); }
|
||
});
|
||
|
||
document.getElementById('saveSocialBtn').addEventListener('click', async function() {
|
||
const status = document.getElementById('socialStatus');
|
||
try {
|
||
const r = await fetch('/api/global', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
social: {
|
||
instagram: document.getElementById('socialInstagram').value,
|
||
youtube: document.getElementById('socialYoutube').value,
|
||
email: document.getElementById('socialEmail').value
|
||
}
|
||
})
|
||
});
|
||
if (r.ok) {
|
||
showToast('✓ Social Links gespeichert & Deploy gestartet');
|
||
status.textContent = '';
|
||
} else {
|
||
status.textContent = 'Fehler beim Speichern.';
|
||
}
|
||
} catch(err) { status.textContent = 'Verbindungsfehler'; }
|
||
});
|
||
|
||
|
||
let currentIntId = null;
|
||
let allInteractions = {};
|
||
|
||
async function loadAllInteractions() {
|
||
try {
|
||
const res = await fetch('/api/erfolge-interactions');
|
||
allInteractions = await res.json();
|
||
} catch(err) { console.error('Load interactions error:', err); }
|
||
}
|
||
|
||
window.manageInteractions = async function(fileName, title) {
|
||
currentIntId = fileName.replace('.md', '');
|
||
document.getElementById('intTitle').textContent = title;
|
||
await loadAllInteractions();
|
||
|
||
const data = allInteractions[currentIntId] || { likes: 0, comments: [] };
|
||
document.getElementById('intLikes').value = data.likes || 0;
|
||
|
||
const list = document.getElementById('intCommentsList');
|
||
list.innerHTML = '';
|
||
|
||
if (!data.comments || data.comments.length === 0) {
|
||
list.innerHTML = '<p class="text-center py-8 text-on-surface-variant text-sm italic bg-white/50 rounded-2xl">Noch keine Kommentare vorhanden.</p>';
|
||
} else {
|
||
data.comments.forEach(c => {
|
||
const div = document.createElement('div');
|
||
div.className = 'bg-white rounded-2xl p-4 shadow-sm flex flex-col gap-2 relative group border border-transparent hover:border-red-100 transition-all';
|
||
div.innerHTML = `
|
||
<div class="flex justify-between items-start">
|
||
<div class="flex flex-col">
|
||
<span class="text-xs font-black text-primary uppercase tracking-tighter mb-1 font-lexend">${escHtml(c.name)}</span>
|
||
<span class="text-xs text-on-surface-variant opacity-50 font-medium italic">${new Date(c.date).toLocaleString('de-DE')}</span>
|
||
</div>
|
||
<button onclick="window.deleteComment('${currentIntId}', '${c.id}')" class="text-error hover:bg-error/10 p-2 rounded-lg transition-all">
|
||
<span class="material-symbols-outlined text-sm">delete</span>
|
||
</button>
|
||
</div>
|
||
<p class="text-sm text-on-surface leading-relaxed">${escHtml(c.text)}</p>
|
||
`;
|
||
list.appendChild(div);
|
||
});
|
||
}
|
||
|
||
document.getElementById('interactionsModal').classList.remove('hidden');
|
||
};
|
||
|
||
window.closeInteractions = function() {
|
||
document.getElementById('interactionsModal').classList.add('hidden');
|
||
};
|
||
|
||
window.deleteComment = async function(erfolgId, commentId) {
|
||
if (!confirm('Kommentar wirklich löschen?')) return;
|
||
try {
|
||
const res = await fetch(`/api/erfolge-interactions/comment/${erfolgId}/${commentId}`, { method: 'DELETE' });
|
||
if (res.ok) {
|
||
// Refresh local UI
|
||
const data = allInteractions[erfolgId];
|
||
data.comments = data.comments.filter(c => c.id !== commentId);
|
||
window.manageInteractions(erfolgId + '.md', document.getElementById('intTitle').textContent);
|
||
}
|
||
} catch(err) { alert('Fehler beim Löschen'); }
|
||
};
|
||
|
||
window.saveInteractions = async function() {
|
||
const likes = parseInt(document.getElementById('intLikes').value) || 0;
|
||
if (!allInteractions[currentIntId]) allInteractions[currentIntId] = { likes: 0, comments: [] };
|
||
allInteractions[currentIntId].likes = likes;
|
||
|
||
try {
|
||
const res = await fetch('/api/erfolge-interactions', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(allInteractions)
|
||
});
|
||
if (res.ok) {
|
||
closeInteractions();
|
||
alert('Änderungen gespeichert! Seite wird neu gebaut.');
|
||
}
|
||
} catch(err) { alert('Fehler beim Speichern'); }
|
||
};
|
||
|
||
// ─── Init ─────────────────────────────────────────────────
|
||
loadCategories().then(function () { loadStats(); });
|
||
loadSiteTitle();
|
||
loadGlobal();
|
||
})();
|
||
</script>
|
||
|
||
<!-- Modal für Likes & Kommentare -->
|
||
<div id="interactionsModal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm">
|
||
<div class="bg-surface rounded-3xl w-full max-w-2xl max-h-[80vh] overflow-hidden flex flex-col shadow-2xl animate-reveal-up">
|
||
<div class="p-6 border-b border-zinc-200 flex justify-between items-center bg-white">
|
||
<h3 class="text-xl font-black font-lexend text-on-surface flex items-center gap-2">
|
||
<span class="material-symbols-outlined text-primary">favorite</span>
|
||
Interaktionen: <span id="intTitle" class="text-primary">—</span>
|
||
</h3>
|
||
<button onclick="closeInteractions()" class="w-10 h-10 rounded-full flex items-center justify-center hover:bg-zinc-100 transition-all">
|
||
<span class="material-symbols-outlined">close</span>
|
||
</button>
|
||
</div>
|
||
<div class="p-8 overflow-y-auto flex-1 bg-surface-container-low">
|
||
<div class="mb-8">
|
||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Likes (Zahl)</label>
|
||
<div class="flex items-center gap-4">
|
||
<input id="intLikes" type="number" class="w-32 bg-white rounded-xl px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-lg font-black"/>
|
||
<span class="text-sm text-on-surface-variant">Herzen für diesen Erfolg</span>
|
||
</div>
|
||
</div>
|
||
<h4 class="text-sm font-black font-lexend text-on-surface-variant uppercase tracking-widest mb-4 flex items-center gap-2">
|
||
<span class="material-symbols-outlined text-base">comment</span>
|
||
Kommentare verwalten
|
||
</h4>
|
||
<div id="intCommentsList" class="space-y-3"></div>
|
||
</div>
|
||
<div class="p-6 border-t border-zinc-200 bg-white flex justify-end gap-3">
|
||
<button onclick="closeInteractions()" class="px-6 py-3 rounded-xl font-bold text-zinc-500 hover:bg-zinc-100 transition-all">Abbrechen</button>
|
||
<button onclick="saveInteractions()" class="px-8 py-3 rounded-xl font-black bg-primary text-white shadow-lg shadow-primary/20 active:scale-95 transition-all">Speichern & Deploy</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</body>
|
||
</html>
|