mirror of
https://github.com/superschnups/Emy.git
synced 2026-06-22 03:13:10 +00:00
Auto-backup: 2026-05-01 21:00
This commit is contained in:
parent
dd5065a98c
commit
875350d7b1
14 changed files with 363 additions and 65 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
|
@ -140,12 +140,14 @@ app.post('/api/categories', (req, res) => {
|
|||
cats.push(name);
|
||||
writeCategories(cats);
|
||||
res.json({ ok: true, categories: cats });
|
||||
rebuildAndDeploy();
|
||||
});
|
||||
|
||||
app.delete('/api/categories/:name', (req, res) => {
|
||||
const cats = readCategories().filter(c => c !== req.params.name);
|
||||
writeCategories(cats);
|
||||
res.json({ ok: true, categories: cats });
|
||||
rebuildAndDeploy();
|
||||
});
|
||||
|
||||
// ── 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 ──────────────────────────────────
|
||||
app.get('/api/galerie', (req, res) => {
|
||||
res.json(JSON.parse(fs.readFileSync(GALERIE_PAGE_FILE, 'utf8')));
|
||||
|
|
|
|||
195
admin.html
195
admin.html
|
|
@ -197,9 +197,9 @@ tailwind.config = {
|
|||
</div>
|
||||
<div class="p-6 bg-surface-container-lowest rounded-lg border-l-4 border-secondary">
|
||||
<p class="text-xs font-bold text-zinc-500 uppercase tracking-widest mb-2">Kategorien</p>
|
||||
<h3 class="text-4xl font-black text-on-surface font-lexend" id="statCats">3</h3>
|
||||
<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> Training, Wettkampf, Gürtel
|
||||
<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">
|
||||
|
|
@ -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"/>
|
||||
</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">
|
||||
Erfolge speichern & deployen
|
||||
<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>
|
||||
|
||||
|
|
@ -849,33 +857,57 @@ tailwind.config = {
|
|||
|
||||
<!-- ═══ 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">
|
||||
...
|
||||
</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</span> Foto bearbeiten
|
||||
<span class="material-symbols-outlined text-primary">edit_note</span> Bericht bearbeiten
|
||||
</h3>
|
||||
<input type="hidden" id="editId"/>
|
||||
<input type="hidden" id="esFileName"/>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel</label>
|
||||
<input id="editTitle" type="text"
|
||||
class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>
|
||||
<div 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>
|
||||
<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>
|
||||
<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">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 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>
|
||||
<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>
|
||||
|
|
@ -1360,6 +1392,8 @@ tailwind.config = {
|
|||
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;
|
||||
|
|
@ -1703,20 +1737,23 @@ tailwind.config = {
|
|||
});
|
||||
|
||||
// ─── Erfolge (Career) ─────────────────────────────────────
|
||||
function createAuszeichnungRow(titel, detail) {
|
||||
function createAuszeichnungRow(titel, detail, beschreibung) {
|
||||
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 =
|
||||
'<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>';
|
||||
'<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('', ''));
|
||||
});
|
||||
|
|
@ -1777,7 +1814,7 @@ tailwind.config = {
|
|||
const wf = document.getElementById('efWeitereFields');
|
||||
wf.innerHTML = '';
|
||||
(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');
|
||||
sf.innerHTML = '';
|
||||
|
|
@ -1793,6 +1830,7 @@ tailwind.config = {
|
|||
});
|
||||
document.getElementById('efZitatText').value = (d.zitat || {}).text || '';
|
||||
document.getElementById('efZitatAutor').value = (d.zitat || {}).autor || '';
|
||||
loadIndividualSuccesses();
|
||||
} catch(err) { console.error('Career load error:', err); }
|
||||
}
|
||||
|
||||
|
|
@ -1823,7 +1861,8 @@ tailwind.config = {
|
|||
weitere_auszeichnungen: Array.from(weRows).map(function(row) {
|
||||
return {
|
||||
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) {
|
||||
|
|
@ -1847,6 +1886,104 @@ tailwind.config = {
|
|||
} 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 ────────────────────────────────────────────
|
||||
async function loadSiteTitle() {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@
|
|||
"Training",
|
||||
"Wettkämpfe",
|
||||
"Gürtelprüfungen",
|
||||
"test"
|
||||
"test",
|
||||
"Haudrauf"
|
||||
]
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
"hero": {
|
||||
"badge": "Meine Wettkampf-Geschichte",
|
||||
"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.",
|
||||
"rang_value": "Blaugurt",
|
||||
"rang_value": "Grüngurt",
|
||||
"rang_label": "Aktueller Rang",
|
||||
"image": "hero.webp"
|
||||
},
|
||||
|
|
@ -19,15 +19,18 @@
|
|||
"weitere_auszeichnungen": [
|
||||
{
|
||||
"titel": "Norddeutsche Meisterschaft",
|
||||
"detail": "Silber – Kata 2024"
|
||||
"detail": "Silber – Kata 2024",
|
||||
"beschreibung": "War eine harter Fight"
|
||||
},
|
||||
{
|
||||
"titel": "Dojo Champion",
|
||||
"detail": "Kiai Berlin, intern"
|
||||
"detail": "Kiai Berlin, intern",
|
||||
"beschreibung": ""
|
||||
},
|
||||
{
|
||||
"titel": "Fairness-Preis",
|
||||
"detail": "Dojo-Auszeichnung 2023"
|
||||
"detail": "Dojo-Auszeichnung 2023",
|
||||
"beschreibung": ""
|
||||
}
|
||||
],
|
||||
"stats": [
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@
|
|||
"name": "Lea M.",
|
||||
"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!",
|
||||
"farbe": "default",
|
||||
"breit": false
|
||||
"farbe": "secondary",
|
||||
"breit": true
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
|
|
|
|||
|
|
@ -2,16 +2,25 @@
|
|||
"hero": {
|
||||
"badge": "Visuelle Reise",
|
||||
"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."
|
||||
},
|
||||
"ribbon": {
|
||||
"heading": "Hinter der Linse",
|
||||
"description": "Unsere Galerie ist nicht nur Fotos – sie ist ein Zeugnis der Disziplin, die wir jeden Tag im Dojo leben.",
|
||||
"stats": [
|
||||
{ "wert": "24", "label": "Turniere" },
|
||||
{ "wert": "150+", "label": "Schüler" },
|
||||
{ "wert": "85", "label": "Gürtelprüfungen" }
|
||||
{
|
||||
"wert": "24",
|
||||
"label": "Turniere"
|
||||
},
|
||||
{
|
||||
"wert": "20",
|
||||
"label": "Schüler"
|
||||
},
|
||||
{
|
||||
"wert": "6",
|
||||
"label": "Gürtelprüfungen"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"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",
|
||||
"filename": "1775774640975-43j95.webp",
|
||||
|
|
@ -21,7 +29,7 @@
|
|||
"filename": "1775774187000-q74m0.webp",
|
||||
"thumb": "1775774187000-q74m0-thumb.webp",
|
||||
"title": "Test",
|
||||
"kategorie": "Training",
|
||||
"kategorie": "test",
|
||||
"datum": "2026-04-09"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -6,13 +6,22 @@
|
|||
"image": "hero.webp"
|
||||
},
|
||||
"hero_karte": {
|
||||
"rang": "Blaugurt",
|
||||
"status": "Status 2024"
|
||||
"rang": "Grüngurt",
|
||||
"status": "Status 2026"
|
||||
},
|
||||
"stats": [
|
||||
{ "value": "7+", "label": "Jahre Training" },
|
||||
{ "value": "1", "label": "Gold Medaillen" },
|
||||
{ "value": "11", "label": "Turniere" }
|
||||
{
|
||||
"value": "7+",
|
||||
"label": "Jahre Training"
|
||||
},
|
||||
{
|
||||
"value": "1",
|
||||
"label": "Gold Medaillen"
|
||||
},
|
||||
{
|
||||
"value": "11",
|
||||
"label": "Turniere"
|
||||
}
|
||||
],
|
||||
"pruefung_karte": {
|
||||
"titel": "Prüfung bestanden",
|
||||
|
|
|
|||
BIN
layouts/.DS_Store
vendored
BIN
layouts/.DS_Store
vendored
Binary file not shown.
|
|
@ -219,6 +219,9 @@
|
|||
<div>
|
||||
<p class="font-bold">{{ $a.titel }}</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>
|
||||
{{ end }}
|
||||
|
|
|
|||
|
|
@ -91,9 +91,9 @@
|
|||
<!-- Filter -->
|
||||
<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="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>
|
||||
<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="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>
|
||||
{{ range .Site.Data.categories }}
|
||||
<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>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Eigene Fotos (dynamisch vom Admin) -->
|
||||
|
|
@ -103,6 +103,7 @@
|
|||
{{ $katColor := "text-primary-fixed" }}
|
||||
{{ if eq .kategorie "Wettkämpfe" }}{{ $katColor = "text-secondary-fixed" }}{{ 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)">
|
||||
<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 }}"
|
||||
|
|
|
|||
|
|
@ -100,23 +100,34 @@
|
|||
{{ .Site.Data.homepage.siteTitle | default .Site.Title }}
|
||||
</div>
|
||||
<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-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="/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="/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="/gaestebuch/">Gästebuch</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/" | 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/" | 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/" | 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/" | relURL }}">Gästebuch</a>
|
||||
</div>
|
||||
<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">
|
||||
Kontakt
|
||||
</button>
|
||||
<button class="md:hidden text-primary">
|
||||
<button onclick="toggleMobileMenu()" class="md:hidden text-primary">
|
||||
<span class="material-symbols-outlined">menu</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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">
|
||||
<!-- Hero Section -->
|
||||
<section class="relative min-h-[921px] flex items-center overflow-hidden bg-surface">
|
||||
|
|
@ -183,18 +194,33 @@
|
|||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-8">
|
||||
{{ 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="relative z-10">
|
||||
<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="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>
|
||||
<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>
|
||||
<a class="flex items-center gap-2 text-primary font-bold hover:gap-4 transition-all" href="{{ .RelPermalink }}">
|
||||
Bericht lesen <span class="material-symbols-outlined">arrow_forward</span>
|
||||
</a>
|
||||
<button onclick="toggleSuccess(this)" class="flex items-center gap-2 text-primary font-bold hover:gap-4 transition-all">
|
||||
<span class="btn-label">Bericht lesen</span> <span class="material-symbols-outlined transition-transform duration-300">expand_more</span>
|
||||
</button>
|
||||
</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"
|
||||
src="https://lh3.googleusercontent.com/aida-public/AB6AXuCt0zGFYDYcvBDPfNXlVvPqNdkvn4AvVSTlFysp0raGeWEmbAnpQkad18FIakDIrPbq4d93sRhkJnquI7QoXZrLf22SBA8nG_IjRg0JkMadeTHr_KOs0vgEVpV48jXsqKBSI2Rx4J02al6QxsLdWCA6XRBKslA9R0v-u_SrGGB7oNpfxjRV-6L6REjgsuzlGHPwdyY7bLrk_MBOvHUG9hfQ5bJiaiTiKfchBEBdNdtk3MPJow72blhWqMMflZL_bdxzAfX8TCyWKQ"/>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
|
@ -361,7 +387,28 @@ async function submitKontakt(e) {
|
|||
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>
|
||||
</html>
|
||||
|
|
|
|||
BIN
static/.DS_Store
vendored
BIN
static/.DS_Store
vendored
Binary file not shown.
Loading…
Reference in a new issue