Auto-backup: 2026-05-01 21:00

This commit is contained in:
bonzei 2026-05-01 21:00:04 +02:00
parent dd5065a98c
commit 875350d7b1
14 changed files with 363 additions and 65 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -140,12 +140,14 @@ app.post('/api/categories', (req, res) => {
cats.push(name); cats.push(name);
writeCategories(cats); writeCategories(cats);
res.json({ ok: true, categories: cats }); res.json({ ok: true, categories: cats });
rebuildAndDeploy();
}); });
app.delete('/api/categories/:name', (req, res) => { app.delete('/api/categories/:name', (req, res) => {
const cats = readCategories().filter(c => c !== req.params.name); const cats = readCategories().filter(c => c !== req.params.name);
writeCategories(cats); writeCategories(cats);
res.json({ ok: true, categories: cats }); res.json({ ok: true, categories: cats });
rebuildAndDeploy();
}); });
// ── Homepage API ────────────────────────────────────────────── // ── Homepage API ──────────────────────────────────────────────
@ -277,6 +279,84 @@ app.post('/api/erfolge/image', upload.single('image'), async (req, res) => {
} }
}); });
// ── Einzelerfolge (Markdown) API ──────────────────────────────
const ERFOLGE_CONTENT_DIR = path.join(__dirname, 'content/erfolge');
function parseFM(content) {
const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---\r?\n([\s\S]*)$/);
if (!match) return { data: {}, content };
const fm = match[1];
const body = match[2];
const data = {};
fm.split('\n').forEach(line => {
const [key, ...rest] = line.split(':');
if (key && rest.length) data[key.trim()] = rest.join(':').trim().replace(/^"|"$/g, '');
});
return { data, content: body };
}
app.get('/api/individual-successes', (req, res) => {
const files = fs.readdirSync(ERFOLGE_CONTENT_DIR).filter(f => f.endsWith('.md') && f !== '_index.md');
const list = files.map(f => {
const c = fs.readFileSync(path.join(ERFOLGE_CONTENT_DIR, f), 'utf8');
const { data } = parseFM(c);
return { fileName: f, title: data.title || f, rang: data.rang, image: data.image };
});
res.json(list);
});
app.get('/api/individual-success/:file', (req, res) => {
const c = fs.readFileSync(path.join(ERFOLGE_CONTENT_DIR, req.params.file), 'utf8');
const { data, content } = parseFM(c);
res.json({ ...data, content });
});
app.put('/api/individual-success/:file', (req, res) => {
const filePath = path.join(ERFOLGE_CONTENT_DIR, req.params.file);
const oldContent = fs.readFileSync(filePath, 'utf8');
const { data: oldData } = parseFM(oldContent);
const { title, rang, summary, content } = req.body;
const newData = { ...oldData, title, rang, summary };
let fmStr = '---\n';
for (const k in newData) {
if (newData[k]) fmStr += `${k}: "${newData[k]}"\n`;
}
fmStr += '---\n';
fs.writeFileSync(filePath, fmStr + '\n' + content);
res.json({ ok: true });
rebuildAndDeploy();
});
app.post('/api/individual-success/:file/image', upload.single('image'), async (req, res) => {
try {
const baseName = req.params.file.replace('.md', '');
const filename = `success-${baseName}-${Date.now()}.webp`;
await sharp(req.file.buffer)
.resize(1000, 1000, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 85 })
.toFile(path.join(ERFOLGE_IMG_DIR, filename));
// In Markdown Datei schreiben
const filePath = path.join(ERFOLGE_CONTENT_DIR, req.params.file);
const c = fs.readFileSync(filePath, 'utf8');
const { data, content } = parseFM(c);
data.image = filename;
let fmStr = '---\n';
for (const k in data) {
if (data[k]) fmStr += `${k}: "${data[k]}"\n`;
}
fmStr += '---\n';
fs.writeFileSync(filePath, fmStr + '\n' + content);
res.json({ ok: true, image: filename });
rebuildAndDeploy();
} catch (e) {
res.status(500).json({ ok: false, error: e.message });
}
});
// ── Galerie Seiten-Texte API ────────────────────────────────── // ── Galerie Seiten-Texte API ──────────────────────────────────
app.get('/api/galerie', (req, res) => { app.get('/api/galerie', (req, res) => {
res.json(JSON.parse(fs.readFileSync(GALERIE_PAGE_FILE, 'utf8'))); res.json(JSON.parse(fs.readFileSync(GALERIE_PAGE_FILE, 'utf8')));

View file

@ -197,9 +197,9 @@ tailwind.config = {
</div> </div>
<div class="p-6 bg-surface-container-lowest rounded-lg border-l-4 border-secondary"> <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> <p class="text-xs font-bold text-zinc-500 uppercase tracking-widest mb-2">Kategorien</p>
<h3 class="text-4xl font-black text-on-surface font-lexend" id="statCats">3</h3> <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"> <div class="mt-2 text-secondary font-bold text-xs flex items-center gap-1">
<span class="material-symbols-outlined text-sm">label</span> Training, Wettkampf, Gürtel <span class="material-symbols-outlined text-sm">label</span> <span id="statCatsSublabel">Training, Wettkampf, Gürtel</span>
</div> </div>
</div> </div>
<div class="p-6 bg-surface-container-lowest rounded-lg border-l-4 border-on-tertiary-fixed-variant"> <div class="p-6 bg-surface-container-lowest rounded-lg border-l-4 border-on-tertiary-fixed-variant">
@ -685,9 +685,17 @@ tailwind.config = {
<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"/> <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> </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"> <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 speichern & deployen Erfolge-Seite speichern & deployen
</button> </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> </section>
</div> </div>
@ -849,33 +857,57 @@ tailwind.config = {
<!-- ═══ EDIT MODAL ═══ --> <!-- ═══ 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 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"> ...
</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"> <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 <span class="material-symbols-outlined text-primary">edit_note</span> Bericht bearbeiten
</h3> </h3>
<input type="hidden" id="editId"/> <input type="hidden" id="esFileName"/>
<div class="space-y-4"> <div class="space-y-4">
<div> <div class="grid grid-cols-2 gap-4">
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel</label> <div>
<input id="editTitle" type="text" <label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel</label>
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"/> <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> </div>
<div> <div>
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kategorie</label> <label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kurzfassung (Summary)</label>
<select id="editKat" <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>
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>
</select> <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">Hintergrundbild (transparentes Foto)</label>
<div class="flex gap-4 items-center">
<div class="w-20 h-20 rounded-lg overflow-hidden bg-surface-container shrink-0">
<img id="esPreview" src="" class="w-full h-full object-cover hidden"/>
<div id="esPlaceholder" class="w-full h-full flex items-center justify-center text-on-surface-variant">
<span class="material-symbols-outlined">image</span>
</div>
</div>
<div class="flex-1">
<input type="file" id="esFileInput" accept="image/*" 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">Bild wählen</button>
<p id="esFileStatus" class="text-[10px] text-on-surface-variant mt-1">Nichts ausgewählt</p>
</div>
</div>
</div> </div>
</div> </div>
<div class="flex gap-3 mt-8"> <div class="flex gap-3 mt-8">
<button type="button" id="saveEditBtn" <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>
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"> <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>
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> </div>
</div> </div>
@ -1360,6 +1392,8 @@ tailwind.config = {
const data = await r.json(); const data = await r.json();
const photos = data.photos || []; const photos = data.photos || [];
document.getElementById('statPhotos').textContent = photos.length; 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 = {}; const counts = {};
let lastDate = null; let lastDate = null;
@ -1703,20 +1737,23 @@ tailwind.config = {
}); });
// ─── Erfolge (Career) ───────────────────────────────────── // ─── Erfolge (Career) ─────────────────────────────────────
function createAuszeichnungRow(titel, detail) { function createAuszeichnungRow(titel, detail, beschreibung) {
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'bg-surface-container-low rounded-xl p-4 flex gap-3 items-center'; row.className = 'bg-surface-container-low rounded-xl p-4 flex flex-col gap-3';
row.innerHTML = row.innerHTML =
'<div class="flex gap-3 items-center">' +
'<input type="text" value="' + escHtml(titel||'') + '" placeholder="Titel" data-ea="titel"' + '<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"/>' + ' 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"' + '<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"/>' + ' 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">' + '<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>'; '<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(); }); row.querySelector('.ea-delete').addEventListener('click', function() { row.remove(); });
return row; return row;
} }
document.getElementById('efWeitereAdd').addEventListener('click', function() { document.getElementById('efWeitereAdd').addEventListener('click', function() {
document.getElementById('efWeitereFields').appendChild(createAuszeichnungRow('', '')); document.getElementById('efWeitereFields').appendChild(createAuszeichnungRow('', ''));
}); });
@ -1777,7 +1814,7 @@ tailwind.config = {
const wf = document.getElementById('efWeitereFields'); const wf = document.getElementById('efWeitereFields');
wf.innerHTML = ''; wf.innerHTML = '';
(d.weitere_auszeichnungen || []).forEach(function(a) { (d.weitere_auszeichnungen || []).forEach(function(a) {
wf.appendChild(createAuszeichnungRow(a.titel, a.detail)); wf.appendChild(createAuszeichnungRow(a.titel, a.detail, a.beschreibung));
}); });
const sf = document.getElementById('efStatsFields'); const sf = document.getElementById('efStatsFields');
sf.innerHTML = ''; sf.innerHTML = '';
@ -1793,6 +1830,7 @@ tailwind.config = {
}); });
document.getElementById('efZitatText').value = (d.zitat || {}).text || ''; document.getElementById('efZitatText').value = (d.zitat || {}).text || '';
document.getElementById('efZitatAutor').value = (d.zitat || {}).autor || ''; document.getElementById('efZitatAutor').value = (d.zitat || {}).autor || '';
loadIndividualSuccesses();
} catch(err) { console.error('Career load error:', err); } } catch(err) { console.error('Career load error:', err); }
} }
@ -1823,7 +1861,8 @@ tailwind.config = {
weitere_auszeichnungen: Array.from(weRows).map(function(row) { weitere_auszeichnungen: Array.from(weRows).map(function(row) {
return { return {
titel: row.querySelector('[data-ea="titel"]').value, titel: row.querySelector('[data-ea="titel"]').value,
detail: row.querySelector('[data-ea="detail"]').value detail: row.querySelector('[data-ea="detail"]').value,
beschreibung: row.querySelector('[data-ea="beschreibung"]').value
}; };
}), }),
stats: Array.from(stRows).map(function(row) { stats: Array.from(stRows).map(function(row) {
@ -1847,6 +1886,104 @@ tailwind.config = {
} catch(err) { console.error('Career save error:', err); } } catch(err) { console.error('Career save error:', err); }
}); });
async function loadIndividualSuccesses() {
try {
const r = await fetch('/api/individual-successes');
const list = await r.json();
const container = document.getElementById('individualSuccessList');
container.innerHTML = '';
list.forEach(item => {
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="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="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>
`;
container.appendChild(div);
});
} catch(err) { console.error('Load individual successes error:', err); }
}
async function editSuccess(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('esSummary').value = data.summary || '';
document.getElementById('esContent').value = data.content || '';
const preview = document.getElementById('esPreview');
const placeholder = document.getElementById('esPlaceholder');
if (data.image) {
preview.src = '/erfolge-img/' + data.image + '?t=' + Date.now();
preview.classList.remove('hidden');
placeholder.classList.add('hidden');
} else {
preview.classList.add('hidden');
placeholder.classList.remove('hidden');
}
document.getElementById('esFileStatus').textContent = 'Keine neue Datei gewählt';
document.getElementById('editSuccessModal').classList.remove('hidden');
} catch(err) { console.error('Edit success error:', err); }
}
document.getElementById('esFileInput').addEventListener('change', function() {
const file = this.files[0];
if (file) {
document.getElementById('esPreview').src = URL.createObjectURL(file);
document.getElementById('esPreview').classList.remove('hidden');
document.getElementById('esPlaceholder').classList.add('hidden');
document.getElementById('esFileStatus').textContent = file.name;
}
});
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 summary = document.getElementById('esSummary').value;
const content = document.getElementById('esContent').value;
const fileInput = document.getElementById('esFileInput');
try {
// 1. Metadaten & Inhalt speichern
const r = await fetch('/api/individual-success/' + fileName, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, rang, summary, content })
});
// 2. Bild falls gewählt
if (fileInput.files.length > 0) {
const fd = new FormData();
fd.append('image', fileInput.files[0]);
await fetch('/api/individual-success/' + fileName + '/image', {
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 ──────────────────────────────────────────── // ─── Site-Name ────────────────────────────────────────────
async function loadSiteTitle() { async function loadSiteTitle() {
try { try {

View file

@ -2,5 +2,6 @@
"Training", "Training",
"Wettkämpfe", "Wettkämpfe",
"Gürtelprüfungen", "Gürtelprüfungen",
"test" "test",
"Haudrauf"
] ]

View file

@ -2,9 +2,9 @@
"hero": { "hero": {
"badge": "Meine Wettkampf-Geschichte", "badge": "Meine Wettkampf-Geschichte",
"heading_main": "JEDER SIEG", "heading_main": "JEDER SIEG",
"heading_colored": "ZÄHLT.", "heading_colored": "ZÄHLT",
"description": "Von der ersten Gürtelprüfung bis zur Landesmeisterschaft hier sammle ich alle Momente, auf die ich besonders stolz bin.", "description": "Von der ersten Gürtelprüfung bis zur Landesmeisterschaft hier sammle ich alle Momente, auf die ich besonders stolz bin.",
"rang_value": "Blaugurt", "rang_value": "Grüngurt",
"rang_label": "Aktueller Rang", "rang_label": "Aktueller Rang",
"image": "hero.webp" "image": "hero.webp"
}, },
@ -19,15 +19,18 @@
"weitere_auszeichnungen": [ "weitere_auszeichnungen": [
{ {
"titel": "Norddeutsche Meisterschaft", "titel": "Norddeutsche Meisterschaft",
"detail": "Silber Kata 2024" "detail": "Silber Kata 2024",
"beschreibung": "War eine harter Fight"
}, },
{ {
"titel": "Dojo Champion", "titel": "Dojo Champion",
"detail": "Kiai Berlin, intern" "detail": "Kiai Berlin, intern",
"beschreibung": ""
}, },
{ {
"titel": "Fairness-Preis", "titel": "Fairness-Preis",
"detail": "Dojo-Auszeichnung 2023" "detail": "Dojo-Auszeichnung 2023",
"beschreibung": ""
} }
], ],
"stats": [ "stats": [

View file

@ -32,8 +32,8 @@
"name": "Lea M.", "name": "Lea M.",
"rolle": "Freundin", "rolle": "Freundin",
"text": "Die Website sieht so professionell aus! Mega cool, alle deine Erfolge an einem Ort zu sehen. Wir müssen bald mal wieder eis essen gehen!", "text": "Die Website sieht so professionell aus! Mega cool, alle deine Erfolge an einem Ort zu sehen. Wir müssen bald mal wieder eis essen gehen!",
"farbe": "default", "farbe": "secondary",
"breit": false "breit": true
}, },
{ {
"id": "4", "id": "4",

View file

@ -2,16 +2,25 @@
"hero": { "hero": {
"badge": "Visuelle Reise", "badge": "Visuelle Reise",
"heading_main": "Eingefangene", "heading_main": "Eingefangene",
"heading_colored": "Momente.", "heading_colored": "Momente",
"description": "Die Kunst der Disziplin durch die Linse. Von intensiven Trainingseinheiten bis zum Triumph bei Gürtelprüfungen." "description": "Die Kunst der Disziplin durch die Linse. Von intensiven Trainingseinheiten bis zum Triumph bei Gürtelprüfungen."
}, },
"ribbon": { "ribbon": {
"heading": "Hinter der Linse", "heading": "Hinter der Linse",
"description": "Unsere Galerie ist nicht nur Fotos sie ist ein Zeugnis der Disziplin, die wir jeden Tag im Dojo leben.", "description": "Unsere Galerie ist nicht nur Fotos sie ist ein Zeugnis der Disziplin, die wir jeden Tag im Dojo leben.",
"stats": [ "stats": [
{ "wert": "24", "label": "Turniere" }, {
{ "wert": "150+", "label": "Schüler" }, "wert": "24",
{ "wert": "85", "label": "Gürtelprüfungen" } "label": "Turniere"
},
{
"wert": "20",
"label": "Schüler"
},
{
"wert": "6",
"label": "Gürtelprüfungen"
}
] ]
} }
} }

View file

@ -1,5 +1,13 @@
{ {
"photos": [ "photos": [
{
"id": "1777571196788-em155",
"filename": "1777571196788-em155.webp",
"thumb": "1777571196788-em155-thumb.webp",
"title": "254E64FC-1F3E-4694-9828-28C620DB7A0D_1_105_c",
"kategorie": "Haudrauf",
"datum": "2026-04-30"
},
{ {
"id": "1775774640975-43j95", "id": "1775774640975-43j95",
"filename": "1775774640975-43j95.webp", "filename": "1775774640975-43j95.webp",
@ -21,7 +29,7 @@
"filename": "1775774187000-q74m0.webp", "filename": "1775774187000-q74m0.webp",
"thumb": "1775774187000-q74m0-thumb.webp", "thumb": "1775774187000-q74m0-thumb.webp",
"title": "Test", "title": "Test",
"kategorie": "Training", "kategorie": "test",
"datum": "2026-04-09" "datum": "2026-04-09"
} }
] ]

View file

@ -6,13 +6,22 @@
"image": "hero.webp" "image": "hero.webp"
}, },
"hero_karte": { "hero_karte": {
"rang": "Blaugurt", "rang": "Grüngurt",
"status": "Status 2024" "status": "Status 2026"
}, },
"stats": [ "stats": [
{ "value": "7+", "label": "Jahre Training" }, {
{ "value": "1", "label": "Gold Medaillen" }, "value": "7+",
{ "value": "11", "label": "Turniere" } "label": "Jahre Training"
},
{
"value": "1",
"label": "Gold Medaillen"
},
{
"value": "11",
"label": "Turniere"
}
], ],
"pruefung_karte": { "pruefung_karte": {
"titel": "Prüfung bestanden", "titel": "Prüfung bestanden",

BIN
layouts/.DS_Store vendored

Binary file not shown.

View file

@ -219,6 +219,9 @@
<div> <div>
<p class="font-bold">{{ $a.titel }}</p> <p class="font-bold">{{ $a.titel }}</p>
<p class="text-sm opacity-80">{{ $a.detail }}</p> <p class="text-sm opacity-80">{{ $a.detail }}</p>
{{ if $a.beschreibung }}
<p class="text-xs opacity-70 mt-1 leading-relaxed">{{ $a.beschreibung }}</p>
{{ end }}
</div> </div>
</div> </div>
{{ end }} {{ end }}

View file

@ -91,9 +91,9 @@
<!-- Filter --> <!-- Filter -->
<div class="flex flex-wrap items-center gap-3 mb-12" id="filterBar"> <div class="flex flex-wrap items-center gap-3 mb-12" id="filterBar">
<button data-filter="Alle" class="filter-btn px-6 py-3 rounded-full bg-secondary text-on-secondary font-bold text-sm tracking-wide shadow-lg shadow-secondary/20 active:scale-95 transition-transform">Alle</button> <button data-filter="Alle" class="filter-btn px-6 py-3 rounded-full bg-secondary text-on-secondary font-bold text-sm tracking-wide shadow-lg shadow-secondary/20 active:scale-95 transition-transform">Alle</button>
<button data-filter="Training" class="filter-btn px-6 py-3 rounded-full bg-surface-container-low text-on-surface-variant font-bold text-sm tracking-wide hover:bg-surface-container-high transition-colors">Training</button> {{ range .Site.Data.categories }}
<button data-filter="Wettkämpfe" class="filter-btn px-6 py-3 rounded-full bg-surface-container-low text-on-surface-variant font-bold text-sm tracking-wide hover:bg-surface-container-high transition-colors">Wettkämpfe</button> <button data-filter="{{ . }}" class="filter-btn px-6 py-3 rounded-full bg-surface-container-low text-on-surface-variant font-bold text-sm tracking-wide hover:bg-surface-container-high transition-colors">{{ . }}</button>
<button data-filter="Gürtelprüfungen" class="filter-btn px-6 py-3 rounded-full bg-surface-container-low text-on-surface-variant font-bold text-sm tracking-wide hover:bg-surface-container-high transition-colors">Gürtelprüfungen</button> {{ end }}
</div> </div>
<!-- Eigene Fotos (dynamisch vom Admin) --> <!-- Eigene Fotos (dynamisch vom Admin) -->
@ -103,6 +103,7 @@
{{ $katColor := "text-primary-fixed" }} {{ $katColor := "text-primary-fixed" }}
{{ if eq .kategorie "Wettkämpfe" }}{{ $katColor = "text-secondary-fixed" }}{{ end }} {{ if eq .kategorie "Wettkämpfe" }}{{ $katColor = "text-secondary-fixed" }}{{ end }}
{{ if eq .kategorie "Gürtelprüfungen" }}{{ $katColor = "text-primary-container" }}{{ end }} {{ if eq .kategorie "Gürtelprüfungen" }}{{ $katColor = "text-primary-container" }}{{ end }}
{{ if and (ne .kategorie "Training") (ne .kategorie "Wettkämpfe") (ne .kategorie "Gürtelprüfungen") }}{{ $katColor = "text-on-surface-variant" }}{{ end }}
<div class="group relative overflow-hidden rounded-xl cursor-pointer aspect-square gallery-item" data-kat="{{ .kategorie }}" onclick="openLightbox(this)"> <div class="group relative overflow-hidden rounded-xl cursor-pointer aspect-square gallery-item" data-kat="{{ .kategorie }}" onclick="openLightbox(this)">
<img class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" <img class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
src="/gallery/images/{{ .thumb }}" data-full="/gallery/images/{{ .filename }}" src="/gallery/images/{{ .thumb }}" data-full="/gallery/images/{{ .filename }}"

View file

@ -100,23 +100,34 @@
{{ .Site.Data.homepage.siteTitle | default .Site.Title }} {{ .Site.Data.homepage.siteTitle | default .Site.Title }}
</div> </div>
<div class="hidden md:flex items-center gap-8"> <div class="hidden md:flex items-center gap-8">
<a class="text-pink-600 dark:text-pink-400 border-b-2 border-pink-500 pb-1 font-headline tracking-tight uppercase font-bold transition-colors duration-300" href="/">Home</a> <a class="text-pink-600 dark:text-pink-400 border-b-2 border-pink-500 pb-1 font-headline tracking-tight uppercase font-bold transition-colors duration-300" href="{{ "/" | relURL }}">Home</a>
<a class="text-zinc-600 dark:text-zinc-400 font-medium font-headline tracking-tight uppercase hover:text-pink-500 dark:hover:text-pink-300 transition-colors duration-300" href="/galerie/">Galerie</a> <a class="text-zinc-600 dark:text-zinc-400 font-medium font-headline tracking-tight uppercase hover:text-pink-500 dark:hover:text-pink-300 transition-colors duration-300" href="{{ "/galerie/" | relURL }}">Galerie</a>
<a class="text-zinc-600 dark:text-zinc-400 font-medium font-headline tracking-tight uppercase hover:text-pink-500 dark:hover:text-pink-300 transition-colors duration-300" href="/uebermich/">Über mich</a> <a class="text-zinc-600 dark:text-zinc-400 font-medium font-headline tracking-tight uppercase hover:text-pink-500 dark:hover:text-pink-300 transition-colors duration-300" href="{{ "/uebermich/" | relURL }}">Über mich</a>
<a class="text-zinc-600 dark:text-zinc-400 font-medium font-headline tracking-tight uppercase hover:text-pink-500 dark:hover:text-pink-300 transition-colors duration-300" href="/erfolge/">Erfolge</a> <a class="text-zinc-600 dark:text-zinc-400 font-medium font-headline tracking-tight uppercase hover:text-pink-500 dark:hover:text-pink-300 transition-colors duration-300" href="{{ "/erfolge/" | relURL }}">Erfolge</a>
<a class="text-zinc-600 dark:text-zinc-400 font-medium font-headline tracking-tight uppercase hover:text-pink-500 dark:hover:text-pink-300 transition-colors duration-300" href="/gaestebuch/">Gästebuch</a> <a class="text-zinc-600 dark:text-zinc-400 font-medium font-headline tracking-tight uppercase hover:text-pink-500 dark:hover:text-pink-300 transition-colors duration-300" href="{{ "/gaestebuch/" | relURL }}">Gästebuch</a>
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<button onclick="document.getElementById('kontaktModal').classList.remove('hidden')" class="bg-gradient-to-br from-primary to-primary-container text-on-primary px-8 py-3 rounded-xl font-bold font-headline tracking-tight uppercase scale-95 active:scale-90 transition-transform"> <button onclick="document.getElementById('kontaktModal').classList.remove('hidden')" class="bg-gradient-to-br from-primary to-primary-container text-on-primary px-8 py-3 rounded-xl font-bold font-headline tracking-tight uppercase scale-95 active:scale-90 transition-transform">
Kontakt Kontakt
</button> </button>
<button class="md:hidden text-primary"> <button onclick="toggleMobileMenu()" class="md:hidden text-primary">
<span class="material-symbols-outlined">menu</span> <span class="material-symbols-outlined">menu</span>
</button> </button>
</div> </div>
</div> </div>
</nav> </nav>
<!-- Mobile Menu Overlay -->
<div id="mobileMenu" class="hidden fixed inset-0 z-40 bg-white dark:bg-zinc-900 p-6 pt-24">
<div class="flex flex-col gap-6 text-xl font-headline font-bold uppercase tracking-tight">
<a href="{{ "/" | relURL }}" onclick="toggleMobileMenu()">Home</a>
<a href="{{ "/galerie/" | relURL }}" onclick="toggleMobileMenu()">Galerie</a>
<a href="{{ "/uebermich/" | relURL }}" onclick="toggleMobileMenu()">Über mich</a>
<a href="{{ "/erfolge/" | relURL }}" onclick="toggleMobileMenu()">Erfolge</a>
<a href="{{ "/gaestebuch/" | relURL }}" onclick="toggleMobileMenu()">Gästebuch</a>
</div>
</div>
<main class="pt-20"> <main class="pt-20">
<!-- Hero Section --> <!-- Hero Section -->
<section class="relative min-h-[921px] flex items-center overflow-hidden bg-surface"> <section class="relative min-h-[921px] flex items-center overflow-hidden bg-surface">
@ -183,18 +194,33 @@
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-12 gap-8"> <div class="grid grid-cols-1 md:grid-cols-12 gap-8">
{{ range (where .Site.RegularPages "Section" "erfolge") }} {{ range (where .Site.RegularPages "Section" "erfolge") }}
<div class="md:col-span-8 bg-surface-container-lowest rounded-xl p-8 shadow-sm group hover:shadow-xl transition-all duration-500 overflow-hidden relative"> <div class="md:col-span-8 bg-surface-container-lowest rounded-xl shadow-sm group hover:shadow-xl transition-all duration-500 overflow-hidden relative success-card">
<div class="relative z-10"> <div class="p-8 relative z-10">
<span class="bg-secondary text-white px-4 py-1 rounded-full text-xs font-bold uppercase tracking-widest">{{ .Params.rang | default "Highlight" }}</span> <span class="bg-secondary text-white px-4 py-1 rounded-full text-xs font-bold uppercase tracking-widest">{{ .Params.rang | default "Highlight" }}</span>
<h3 class="text-3xl font-black font-headline mt-6 mb-4">{{ .Title }}</h3> <h3 class="text-3xl font-black font-headline mt-6 mb-4">{{ .Title }}</h3>
<p class="text-on-surface-variant mb-8 max-w-md">{{ .Summary }}</p> <p class="text-on-surface-variant mb-8 max-w-md">{{ .Summary }}</p>
<a class="flex items-center gap-2 text-primary font-bold hover:gap-4 transition-all" href="{{ .RelPermalink }}"> <button onclick="toggleSuccess(this)" class="flex items-center gap-2 text-primary font-bold hover:gap-4 transition-all">
Bericht lesen <span class="material-symbols-outlined">arrow_forward</span> <span class="btn-label">Bericht lesen</span> <span class="material-symbols-outlined transition-transform duration-300">expand_more</span>
</a> </button>
</div> </div>
<div class="absolute right-0 bottom-0 w-1/2 h-full opacity-10 group-hover:opacity-20 group-hover:scale-110 transition-all duration-700"> <!-- Aufklappbarer Inhalt -->
<div class="success-body overflow-hidden transition-all duration-500 ease-in-out" style="max-height:0">
<div class="px-8 pb-8 border-t border-surface-container-high">
<div class="pt-6 text-on-surface-variant leading-relaxed space-y-4 text-base">
{{ .Content }}
</div>
<a href="{{ .RelPermalink }}" class="inline-flex items-center gap-2 mt-6 text-sm font-bold text-primary hover:underline">
Zum vollständigen Bericht <span class="material-symbols-outlined text-sm">open_in_new</span>
</a>
</div>
</div>
<div class="absolute right-0 top-0 w-1/3 h-full opacity-10 group-hover:opacity-20 group-hover:scale-110 transition-all duration-700 pointer-events-none">
{{ if .Params.image }}
<img alt="{{ .Title }}" class="w-full h-full object-cover grayscale brightness-50" src="{{ printf "/erfolge-img/%s" .Params.image | relURL }}"/>
{{ else }}
<img alt="Highlight Thumbnail" class="w-full h-full object-cover grayscale brightness-50" <img alt="Highlight Thumbnail" class="w-full h-full object-cover grayscale brightness-50"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuCt0zGFYDYcvBDPfNXlVvPqNdkvn4AvVSTlFysp0raGeWEmbAnpQkad18FIakDIrPbq4d93sRhkJnquI7QoXZrLf22SBA8nG_IjRg0JkMadeTHr_KOs0vgEVpV48jXsqKBSI2Rx4J02al6QxsLdWCA6XRBKslA9R0v-u_SrGGB7oNpfxjRV-6L6REjgsuzlGHPwdyY7bLrk_MBOvHUG9hfQ5bJiaiTiKfchBEBdNdtk3MPJow72blhWqMMflZL_bdxzAfX8TCyWKQ"/> src="https://lh3.googleusercontent.com/aida-public/AB6AXuCt0zGFYDYcvBDPfNXlVvPqNdkvn4AvVSTlFysp0raGeWEmbAnpQkad18FIakDIrPbq4d93sRhkJnquI7QoXZrLf22SBA8nG_IjRg0JkMadeTHr_KOs0vgEVpV48jXsqKBSI2Rx4J02al6QxsLdWCA6XRBKslA9R0v-u_SrGGB7oNpfxjRV-6L6REjgsuzlGHPwdyY7bLrk_MBOvHUG9hfQ5bJiaiTiKfchBEBdNdtk3MPJow72blhWqMMflZL_bdxzAfX8TCyWKQ"/>
{{ end }}
</div> </div>
</div> </div>
{{ end }} {{ end }}
@ -361,7 +387,28 @@ async function submitKontakt(e) {
btn.textContent = 'Absenden'; btn.textContent = 'Absenden';
} }
} }
</script>
function toggleSuccess(btn) {
var card = btn.closest('.success-card');
var body = card.querySelector('.success-body');
var icon = btn.querySelector('.material-symbols-outlined');
var label = btn.querySelector('.btn-label');
if (!body.style.maxHeight || body.style.maxHeight === '0px') {
body.style.maxHeight = body.scrollHeight + 'px';
icon.style.transform = 'rotate(180deg)';
label.textContent = 'Weniger anzeigen';
} else {
body.style.maxHeight = '0';
icon.style.transform = '';
label.textContent = 'Bericht lesen';
}
}
function toggleMobileMenu() {
var menu = document.getElementById('mobileMenu');
if (menu) menu.classList.toggle('hidden');
}
</script>
</body> </body>
</html> </html>

BIN
static/.DS_Store vendored

Binary file not shown.