Auto-backup: 2026-05-12 02:17

This commit is contained in:
bonzei 2026-05-12 02:17:31 +02:00
parent e58692de19
commit b9c65ff32b
21 changed files with 707 additions and 155 deletions

BIN
.DS_Store vendored

Binary file not shown.

View file

@ -33,10 +33,10 @@ app.use('/images', express.static(IMAGES_DIR));
// Multer: temporärer Speicher, dann sharp übernimmt // Multer: temporärer Speicher, dann sharp übernimmt
const upload = multer({ const upload = multer({
storage: multer.memoryStorage(), storage: multer.memoryStorage(),
limits: { fileSize: 20 * 1024 * 1024 }, limits: { fileSize: 100 * 1024 * 1024 }, // 100MB limit für Videos
fileFilter: (req, file, cb) => { fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) cb(null, true); if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) cb(null, true);
else cb(new Error('Nur Bilder erlaubt!')); else cb(new Error('Nur Bilder und Videos erlaubt!'));
} }
}); });
@ -300,11 +300,43 @@ app.get('/api/individual-successes', (req, res) => {
const list = files.map(f => { const list = files.map(f => {
const c = fs.readFileSync(path.join(ERFOLGE_CONTENT_DIR, f), 'utf8'); const c = fs.readFileSync(path.join(ERFOLGE_CONTENT_DIR, f), 'utf8');
const { data } = parseFM(c); const { data } = parseFM(c);
return { fileName: f, title: data.title || f, rang: data.rang, image: data.image }; return { fileName: f, title: data.title || f, rang: data.rang, image: data.image, weight: parseInt(data.weight) || 999 };
}); });
list.sort((a, b) => a.weight - b.weight);
res.json(list); res.json(list);
}); });
app.put('/api/individual-successes/reorder', (req, res) => {
const { order } = req.body;
if (!Array.isArray(order)) return res.status(400).json({ ok: false });
order.forEach((fileName, index) => {
const filePath = path.join(ERFOLGE_CONTENT_DIR, fileName);
if (!fs.existsSync(filePath)) return;
const content = fs.readFileSync(filePath, 'utf8');
const { data, content: body } = parseFM(content);
data.weight = index + 1;
let fmStr = '---\n';
for (const k in data) {
if (data[k] !== undefined && data[k] !== null) {
if (k === 'weight') {
fmStr += `${k}: ${data[k]}\n`;
} else {
fmStr += `${k}: "${data[k]}"\n`;
}
}
}
fmStr += '---\n';
fs.writeFileSync(filePath, fmStr + '\n' + body);
});
res.json({ ok: true });
rebuildAndDeploy();
});
app.get('/api/individual-success/:file', (req, res) => { app.get('/api/individual-success/:file', (req, res) => {
const c = fs.readFileSync(path.join(ERFOLGE_CONTENT_DIR, req.params.file), 'utf8'); const c = fs.readFileSync(path.join(ERFOLGE_CONTENT_DIR, req.params.file), 'utf8');
const { data, content } = parseFM(c); const { data, content } = parseFM(c);
@ -315,12 +347,20 @@ app.put('/api/individual-success/:file', (req, res) => {
const filePath = path.join(ERFOLGE_CONTENT_DIR, req.params.file); const filePath = path.join(ERFOLGE_CONTENT_DIR, req.params.file);
const oldContent = fs.readFileSync(filePath, 'utf8'); const oldContent = fs.readFileSync(filePath, 'utf8');
const { data: oldData } = parseFM(oldContent); const { data: oldData } = parseFM(oldContent);
const { title, rang, summary, content } = req.body; const { title, rang, summary, content, ort, datum, kategorie, is_weitere_auszeichnung, image, images } = req.body;
const newData = { ...oldData, title, rang, summary }; const newData = { ...oldData, title, rang, summary, ort, datum, kategorie, is_weitere_auszeichnung };
if (image !== undefined) newData.image = image;
if (images !== undefined) newData.images = images;
let fmStr = '---\n'; let fmStr = '---\n';
for (const k in newData) { for (const k in newData) {
if (newData[k]) fmStr += `${k}: "${newData[k]}"\n`; if (newData[k]) {
if (k === 'weight') {
fmStr += `${k}: ${newData[k]}\n`;
} else {
fmStr += `${k}: "${newData[k]}"\n`;
}
}
} }
fmStr += '---\n'; fmStr += '---\n';
fs.writeFileSync(filePath, fmStr + '\n' + content); fs.writeFileSync(filePath, fmStr + '\n' + content);
@ -328,20 +368,47 @@ app.put('/api/individual-success/:file', (req, res) => {
rebuildAndDeploy(); rebuildAndDeploy();
}); });
app.post('/api/individual-success/:file/image', upload.single('image'), async (req, res) => { app.post('/api/individual-success/:file/images', upload.array('images', 5), async (req, res) => {
try { try {
const baseName = req.params.file.replace('.md', ''); const baseName = req.params.file.replace('.md', '');
const filename = `success-${baseName}-${Date.now()}.webp`; const filenames = [];
await sharp(req.file.buffer)
.resize(1000, 1000, { fit: 'inside', withoutEnlargement: true }) for (const file of req.files) {
.webp({ quality: 85 }) const isVideo = file.mimetype.startsWith('video/');
.toFile(path.join(ERFOLGE_IMG_DIR, filename)); const origExt = path.extname(file.originalname).toLowerCase();
const ext = isVideo ? origExt : '.webp';
const filename = `success-${baseName}-${Date.now()}-${Math.random().toString(36).slice(2,7)}${ext}`;
if (isVideo) {
fs.writeFileSync(path.join(ERFOLGE_IMG_DIR, filename), file.buffer);
} else {
await sharp(file.buffer)
.resize(1000, 1000, { fit: 'inside', withoutEnlargement: true })
.webp({ quality: 85 })
.toFile(path.join(ERFOLGE_IMG_DIR, filename));
}
filenames.push(filename);
}
// In Markdown Datei schreiben // In Markdown Datei schreiben
const filePath = path.join(ERFOLGE_CONTENT_DIR, req.params.file); const filePath = path.join(ERFOLGE_CONTENT_DIR, req.params.file);
const c = fs.readFileSync(filePath, 'utf8'); const c = fs.readFileSync(filePath, 'utf8');
const { data, content } = parseFM(c); const { data, content } = parseFM(c);
data.image = filename;
let existingImages = [];
try { existingImages = JSON.parse(req.body.existingImages || '[]'); } catch(e){}
const allImages = [...existingImages, ...filenames];
let mainIdx = parseInt(req.body.mainImageIndex) || 0;
if (mainIdx < 0 || mainIdx >= allImages.length) mainIdx = 0;
if (allImages.length > 0) {
data.image = allImages[mainIdx]; // Vom User gewähltes Hauptbild
data.images = allImages.join(',');
} else {
delete data.image;
delete data.images;
}
let fmStr = '---\n'; let fmStr = '---\n';
for (const k in data) { for (const k in data) {
@ -350,7 +417,7 @@ app.post('/api/individual-success/:file/image', upload.single('image'), async (r
fmStr += '---\n'; fmStr += '---\n';
fs.writeFileSync(filePath, fmStr + '\n' + content); fs.writeFileSync(filePath, fmStr + '\n' + content);
res.json({ ok: true, image: filename }); res.json({ ok: true, images: filenames });
rebuildAndDeploy(); rebuildAndDeploy();
} catch (e) { } catch (e) {
res.status(500).json({ ok: false, error: e.message }); res.status(500).json({ ok: false, error: e.message });

View file

@ -857,7 +857,26 @@ 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">
<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> </div>
<!-- ═══ EDIT SUCCESS MODAL ═══ --> <!-- ═══ EDIT SUCCESS MODAL ═══ -->
@ -877,6 +896,22 @@ tailwind.config = {
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Rang (z.B. Gold)</label> <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"/> <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">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>
<div> <div>
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kurzfassung (Summary)</label> <label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kurzfassung (Summary)</label>
@ -889,19 +924,16 @@ tailwind.config = {
<!-- Bild Upload für Einzelerfolg --> <!-- Bild Upload für Einzelerfolg -->
<div class="pt-4 border-t border-zinc-100"> <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> <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 class="flex gap-4 items-center">
<div class="w-20 h-20 rounded-lg overflow-hidden bg-surface-container shrink-0"> <div id="esImagesPreviewContainer" class="flex gap-3 mb-4 flex-wrap">
<img id="esPreview" src="" class="w-full h-full object-cover hidden"/> <div class="w-full text-xs text-on-surface-variant italic">Keine Bilder vorhanden.</div>
<div id="esPlaceholder" class="w-full h-full flex items-center justify-center text-on-surface-variant"> </div>
<span class="material-symbols-outlined">image</span>
</div> <div class="flex items-center gap-4">
</div> <input type="file" id="esFileInput" accept="image/*, video/*" multiple class="hidden"/>
<div class="flex-1"> <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>
<input type="file" id="esFileInput" accept="image/*" class="hidden"/> <p id="esFileStatus" class="text-[10px] text-on-surface-variant mt-1">Nichts ausgewählt</p>
<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>
@ -925,6 +957,70 @@ tailwind.config = {
let allPhotos = []; let allPhotos = [];
let allCategories = []; let allCategories = [];
let pendingFiles = []; 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 ────────────────────────────────────────── // ─── Navigation ──────────────────────────────────────────
const sections = ['overview', 'media', 'homepage', 'uebermich', 'career', 'community', 'settings']; const sections = ['overview', 'media', 'homepage', 'uebermich', 'career', 'community', 'settings'];
@ -1886,91 +1982,170 @@ tailwind.config = {
} catch(err) { console.error('Career save error:', err); } } catch(err) { console.error('Career save error:', err); }
}); });
let individualSuccessData = [];
async function loadIndividualSuccesses() { async function loadIndividualSuccesses() {
try { try {
const r = await fetch('/api/individual-successes'); const r = await fetch('/api/individual-successes');
const list = await r.json(); individualSuccessData = await r.json();
const container = document.getElementById('individualSuccessList'); renderIndividualSuccesses();
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); } } catch(err) { console.error('Load individual successes error:', err); }
} }
async function editSuccess(fileName) { 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>
`;
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 { try {
const r = await fetch('/api/individual-success/' + fileName); const r = await fetch('/api/individual-success/' + fileName);
const data = await r.json(); const data = await r.json();
document.getElementById('esFileName').value = fileName; document.getElementById('esFileName').value = fileName;
document.getElementById('esTitle').value = data.title || ''; document.getElementById('esTitle').value = data.title || '';
document.getElementById('esRang').value = data.rang || ''; 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('esSummary').value = data.summary || '';
document.getElementById('esContent').value = data.content || ''; document.getElementById('esContent').value = data.content || '';
const preview = document.getElementById('esPreview'); esNewFilesSelected = false;
const placeholder = document.getElementById('esPlaceholder'); document.getElementById('esFileInput').value = '';
if (data.image) { document.getElementById('esFileStatus').textContent = 'Keine neuen Dateien gewählt';
preview.src = '/erfolge-img/' + data.image + '?t=' + Date.now();
preview.classList.remove('hidden'); let existingImages = [];
placeholder.classList.add('hidden'); if (data.images) {
} else { existingImages = data.images.split(',').map(s => s.trim()).filter(Boolean);
preview.classList.add('hidden'); } else if (data.image) {
placeholder.classList.remove('hidden'); existingImages = [data.image];
} }
document.getElementById('esFileStatus').textContent = 'Keine neue Datei gewählt'; esCurrentImages = existingImages;
esMainImageIndex = 0;
if (data.image && existingImages.includes(data.image)) {
esMainImageIndex = existingImages.indexOf(data.image);
}
renderEsImages(esCurrentImages);
document.getElementById('editSuccessModal').classList.remove('hidden'); document.getElementById('editSuccessModal').classList.remove('hidden');
} catch(err) { console.error('Edit success error:', err); } } catch(err) { console.error('Edit success error:', err); }
} }
document.getElementById('esFileInput').addEventListener('change', function() { document.getElementById('esFileInput').addEventListener('change', function() {
const file = this.files[0]; const newFiles = Array.from(this.files);
if (file) { if (esCurrentImages.length + newFiles.length > 5) {
document.getElementById('esPreview').src = URL.createObjectURL(file); alert(`Bitte maximal 5 Dateien insgesamt auswählen! (Aktuell: ${esCurrentImages.length})`);
document.getElementById('esPreview').classList.remove('hidden'); this.value = '';
document.getElementById('esPlaceholder').classList.add('hidden'); return;
document.getElementById('esFileStatus').textContent = file.name;
} }
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() { document.getElementById('saveSuccessBtn').addEventListener('click', async function() {
const fileName = document.getElementById('esFileName').value; const fileName = document.getElementById('esFileName').value;
const title = document.getElementById('esTitle').value; const title = document.getElementById('esTitle').value;
const rang = document.getElementById('esRang').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 summary = document.getElementById('esSummary').value;
const content = document.getElementById('esContent').value; const content = document.getElementById('esContent').value;
const fileInput = document.getElementById('esFileInput'); const fileInput = document.getElementById('esFileInput');
try { 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 // 1. Metadaten & Inhalt speichern
const r = await fetch('/api/individual-success/' + fileName, { const r = await fetch('/api/individual-success/' + fileName, {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, rang, summary, content }) body: JSON.stringify({
title, rang, ort, datum, kategorie, is_weitere_auszeichnung, summary, content,
image: selectedMainImage,
images: finalImagesStr
})
}); });
// 2. Bild falls gewählt // 2. Bilder falls gewählt
if (fileInput.files.length > 0) { if (newFilesOnly.length > 0) {
const fd = new FormData(); const fd = new FormData();
fd.append('image', fileInput.files[0]); for (let i = 0; i < newFilesOnly.length; i++) {
await fetch('/api/individual-success/' + fileName + '/image', { fd.append('images', newFilesOnly[i]);
}
fd.append('existingImages', JSON.stringify(existingOnly));
fd.append('mainImageIndex', esMainImageIndex);
await fetch('/api/individual-success/' + fileName + '/images', {
method: 'POST', method: 'POST',
body: fd body: fd
}); });

BIN
content/.DS_Store vendored

Binary file not shown.

View file

@ -0,0 +1,34 @@
---
title: "Dojo Champion"
rang: "Kiai Berlin"
date: "2024-01-15"
summary: "Interner Titel als Dojo Champion beim Kiai Berlin."
weight: "4"
ort: "Entenhausen"
datum: "2025-6"
kategorie: "Fight"
images: "success-dojo-champion-1778543253478-vs81l.mp4,success-dojo-champion-1778544116200-5pv3h.mp4,success-dojo-champion-1778544116201-cnz9d.webp,success-dojo-champion-1778544116302-845ro.webp,success-dojo-champion-1778544116365-61zfd.webp"
image: "success-dojo-champion-1778543253478-vs81l.mp4"
---
Ein toller Start ins Jahr! Beim internen Vereinsturnier des Kiai Berlin konnte ich mir den Titel als Dojo Champion sichern. Solche internen Wettkämpfe sind immer ein tolles Erlebnis und stärken den Zusammenhalt.

View file

@ -0,0 +1,26 @@
---
title: "Fairness-Preis"
rang: "Gold"
date: "2023-12-01"
summary: "Auszeichnung mit dem Fairness-Preis 2023 unseres Dojos."
weight: 1
image: "success-fairness-preis-1778528590374.webp"
ort: "Esslingen"
datum: "2024-08"
kategorie: "Wettkampf"
is_weitere_auszeichnung: "true"
---
Zum Jahresabschluss 2023 wurde mir eine ganz besondere Ehre zuteil: Ich durfte den Fairness-Preis unseres Dojos entgegennehmen. Respekt und Fairness sind die Grundpfeiler des Karate, daher bedeutet mir diese Auszeichnung sehr viel.

View file

@ -1,8 +1,24 @@
--- ---
title: "Landesmeisterschaft Berlin 2024" title: "Landesmeisterschaft Berlin 2024"
rang: "Gold" rang: "Gold"
date: 2024-11-15 date: "2024-11-15"
summary: "Erster Platz in der Kategorie Kata U14 bei der Berliner Landesmeisterschaft. Ein unvergesslicher Moment!" summary: "Erster Platz in der Kategorie Kata U14 bei der Berliner Landesmeisterschaft. Ein unvergesslicher Moment!"
image: "success-landesmeisterschaft-2024-1778536783289-lv750.webp"
weight: "2"
is_weitere_auszeichnung: "true"
images: "success-landesmeisterschaft-2024-1778536783289-lv750.webp,success-landesmeisterschaft-2024-1778536783364-ykhep.webp,success-landesmeisterschaft-2024-1778536783524-58ch3.webp,success-landesmeisterschaft-2024-1778536783576-qd5nb.webp"
--- ---
Es war ein langer Weg bis zur Landesmeisterschaft. Monatelang haben wir jeden Tag trainiert, die Kata immer wieder verfeinert. Und dann dieser Moment auf der Matte... Es war ein langer Weg bis zur Landesmeisterschaft. Monatelang haben wir jeden Tag trainiert, die Kata immer wieder verfeinert. Und dann dieser Moment auf der Matte...

View file

@ -0,0 +1,16 @@
---
title: "Landesmeisterschaft (3. Platz)"
rang: "Bronze"
date: "2024-05-09"
summary: "Ein harter Wettkampf, der mit dem 3. Platz belohnt wurde. Im Sommer geht es um die Meisterschaft!"
weight: 3
---
Unglaublich, dass ich das geschafft habe. Nach vielen intensiven Runden konnte ich mir den 3. Platz bei den Landesmeisterschaften sichern! Dieser Erfolg gibt mir wahnsinnig viel Motivation, denn im Sommer kämpfe ich dann um die Meisterschaft. Das Training hat sich gelohnt!

View file

@ -1,8 +1,16 @@
--- ---
title: "Norddeutsche Meisterschaft 2024" title: "Norddeutsche Meisterschaft 2024"
rang: "Silber" rang: "Silber"
date: 2024-08-20 date: "2024-08-20"
summary: "Silbermedaille beim Norddeutschen Turnier das bisher größte Turnier, an dem ich teilgenommen habe." summary: "Silbermedaille beim Norddeutschen Turnier das bisher größte Turnier, an dem ich teilgenommen habe."
weight: 5
--- ---
Über 200 Teilnehmer, drei Kampftage, und am Ende eine silberne Medaille um den Hals. Ein Riesenschritt für mich! Über 200 Teilnehmer, drei Kampftage, und am Ende eine silberne Medaille um den Hals. Ein Riesenschritt für mich!

View file

@ -16,23 +16,7 @@
"ort": "Berlin", "ort": "Berlin",
"kategorie": "U14" "kategorie": "U14"
}, },
"weitere_auszeichnungen": [ "weitere_auszeichnungen": [],
{
"titel": "Norddeutsche Meisterschaft",
"detail": "Silber Kata 2024",
"beschreibung": "War eine harter Fight"
},
{
"titel": "Dojo Champion",
"detail": "Kiai Berlin, intern",
"beschreibung": ""
},
{
"titel": "Fairness-Preis",
"detail": "Dojo-Auszeichnung 2023",
"beschreibung": ""
}
],
"stats": [ "stats": [
{ {
"wert": "5+", "wert": "5+",

View file

@ -6,7 +6,7 @@
"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 ha",
"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": [
{ {

View file

@ -1,5 +1,13 @@
{ {
"photos": [ "photos": [
{
"id": "1778361895205-amw83",
"filename": "1778361895205-amw83.webp",
"thumb": "1778361895205-amw83-thumb.webp",
"title": "EF75AE9A-1670-46F8-AB04-798D22334E66",
"kategorie": "Haudrauf",
"datum": "2026-05-09"
},
{ {
"id": "1777571196788-em155", "id": "1777571196788-em155",
"filename": "1777571196788-em155.webp", "filename": "1777571196788-em155.webp",

BIN
layouts/.DS_Store vendored

Binary file not shown.

View file

@ -124,35 +124,87 @@
<!-- Alle Erfolge aus Content-Dateien --> <!-- Alle Erfolge aus Content-Dateien -->
<section class="bg-surface-container-low py-24 rounded-t-[5rem]"> <section class="bg-surface-container-low py-24 rounded-t-[5rem]">
<div class="max-w-7xl mx-auto px-6"> <div class="max-w-7xl mx-auto px-6">
<h2 class="text-4xl font-headline font-bold text-on-surface mb-16">Alle Erfolge</h2> <h2 class="text-5xl font-headline font-black mb-16 tracking-tight uppercase">MEILENSTEINE DES <span class="text-primary italic">ERFOLGS</span></h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-start"> <div class="space-y-16">
{{ range (where .Site.RegularPages "Section" "erfolge") }} {{ range (where (where .Site.RegularPages "Section" "erfolge") "Params.is_weitere_auszeichnung" "!=" "true").ByWeight }}
<div class="erfolg-card bg-surface-container-lowest rounded-xl editorial-shadow relative overflow-hidden group transition-all duration-500 hover:shadow-xl"> <div class="erfolg-card flex flex-col md:flex-row bg-surface-container-lowest rounded-3xl editorial-shadow relative overflow-hidden group transition-all duration-500 hover:shadow-2xl">
<!-- Immer sichtbar -->
<div class="p-8 relative z-10"> {{ if .Params.image }}
<div class="flex items-center gap-4 mb-5"> <!-- Bild-Bereich -->
<div class="md:w-2/5 relative overflow-hidden bg-surface-container-high shrink-0">
<img src="/erfolge-img/{{ .Params.image }}" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105" alt="{{ .Title }}" />
<!-- Glassmorphism Overlay für den Rang -->
<div class="absolute top-6 left-6 bg-white/30 backdrop-blur-md border border-white/40 px-4 py-2 rounded-full flex items-center gap-2 shadow-lg">
<span class="material-symbols-outlined text-primary text-sm" style="font-variation-settings: 'FILL' 1;">military_tech</span>
<span class="text-pink-900 font-bold uppercase tracking-widest text-xs">{{ .Params.rang | default "Highlight" }}</span>
</div>
</div>
<!-- Text-Bereich -->
<div class="md:w-3/5 p-10 md:p-14 flex flex-col justify-center relative z-10">
{{ else }}
<!-- Text-Bereich (Kein Bild) -->
<div class="w-full p-10 md:p-14 flex flex-col justify-center relative z-10">
<div class="flex items-center gap-4 mb-6">
<div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0"> <div class="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center flex-shrink-0">
<span class="material-symbols-outlined text-2xl text-primary" style="font-variation-settings: 'FILL' 1;">military_tech</span> <span class="material-symbols-outlined text-2xl text-primary" style="font-variation-settings: 'FILL' 1;">military_tech</span>
</div> </div>
<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>
</div> </div>
<h3 class="text-2xl font-headline font-bold text-on-surface mb-3">{{ .Title }}</h3> {{ end }}
<p class="text-on-surface-variant leading-relaxed mb-6">{{ .Summary }}</p>
<h3 class="text-3xl md:text-4xl font-headline font-extrabold text-on-surface mb-4 leading-tight">{{ .Title }}</h3>
<p class="text-lg text-on-surface-variant leading-relaxed mb-8">{{ .Summary }}</p>
<button onclick="toggleErfolg(this)" <button onclick="toggleErfolg(this)"
class="flex items-center gap-2 text-primary font-bold text-sm font-headline uppercase tracking-wider hover:gap-3 transition-all"> class="inline-flex self-start items-center gap-3 text-white bg-gradient-to-r from-primary to-primary-container px-6 py-3 rounded-xl font-bold text-sm font-headline uppercase tracking-wider hover:scale-105 active:scale-95 transition-all shadow-lg shadow-primary/20">
<span class="btn-label">Mehr lesen</span> <span class="btn-label">Ganze Geschichte lesen</span>
<span class="material-symbols-outlined transition-transform duration-300 text-base">expand_more</span> <span class="material-symbols-outlined transition-transform duration-300 text-base">expand_more</span>
</button> </button>
</div>
<!-- Aufklappbarer Inhalt --> <!-- Aufklappbarer Inhalt -->
<div class="erfolg-body overflow-hidden transition-all duration-500 ease-in-out" style="max-height:0"> <div class="erfolg-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-8 mt-8 border-t border-surface-container text-on-surface-variant leading-relaxed space-y-4 text-lg">
<div class="pt-6 text-on-surface-variant leading-relaxed space-y-4 text-base">
{{ .Content }} {{ .Content }}
{{ if .Params.images }}
<div class="mt-8 flex gap-4 overflow-x-auto pb-4 snap-x">
{{ range $img := split .Params.images "," }}
<div class="shrink-0 snap-start relative">
{{ if (or (strings.HasSuffix (lower $img) ".mp4") (strings.HasSuffix (lower $img) ".mov") (strings.HasSuffix (lower $img) ".webm") (strings.HasSuffix (lower $img) ".m4v")) }}
<video src="{{ printf "/erfolge-img/%s" $img | relURL }}" class="h-24 w-24 md:h-32 md:w-32 object-cover rounded-xl cursor-pointer hover:opacity-80 transition-opacity editorial-shadow" onclick="openLightbox(this.src, true)"></video>
<span class="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 text-3xl">play_circle</span>
{{ else }}
<img src="{{ printf "/erfolge-img/%s" $img | relURL }}" class="h-24 w-24 md:h-32 md:w-32 object-cover rounded-xl cursor-pointer hover:opacity-80 transition-opacity editorial-shadow" onclick="openLightbox(this.src, false)"/>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
{{ if or .Params.datum .Params.ort .Params.kategorie }}
<div class="mt-8 pt-6 border-t border-outline-variant/20 flex flex-wrap items-center justify-between gap-6">
{{ if or .Params.datum .Params.ort }}
<div>
{{ if .Params.datum }}<div class="text-xl font-headline text-on-surface">{{ .Params.datum }}</div>{{ end }}
{{ if .Params.ort }}<div class="text-xs uppercase tracking-widest font-bold text-primary">{{ .Params.ort }}</div>{{ end }}
</div>
{{ end }}
{{ if .Params.kategorie }}
<div class="text-right">
<div class="text-xl font-headline text-on-surface">{{ .Params.kategorie }}</div>
<div class="text-xs uppercase tracking-widest font-bold text-primary">Kategorie</div>
</div>
{{ end }}
</div>
{{ end }}
</div> </div>
</div> </div>
</div> </div>
<span class="material-symbols-outlined absolute -bottom-10 -right-10 text-[12rem] text-surface-container-high opacity-10 group-hover:opacity-20 transition-opacity pointer-events-none">sports_martial_arts</span>
<!-- Deko Icon im Hintergrund wenn kein Bild -->
{{ if not .Params.image }}
<span class="material-symbols-outlined absolute -bottom-10 -right-10 text-[20rem] text-surface-container opacity-30 group-hover:opacity-40 transition-opacity pointer-events-none">workspace_premium</span>
{{ end }}
</div> </div>
{{ end }} {{ end }}
</div> </div>
@ -179,32 +231,38 @@
</script> </script>
<!-- Meilensteine Bento Grid --> <!-- Meilensteine Bento Grid -->
<section class="max-w-7xl mx-auto px-6 mt-24"> <section class="max-w-7xl mx-auto px-6 mt-48">
<h2 class="text-5xl font-headline font-black mb-16 tracking-tight">MEILENSTEINE DES <span class="text-primary italic">ERFOLGS</span></h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<!-- Große Karte: Haupt-Meilenstein --> <!-- Große Karte: Haupt-Meilenstein -->
<div class="md:col-span-2 bg-surface-container-lowest rounded-xl p-10 editorial-shadow flex flex-col justify-between relative overflow-hidden group"> <div class="md:col-span-2 relative">
<div class="relative z-10"> <!-- Herausfahrendes Bild -->
<div class="absolute top-0 left-8 right-8 flex justify-center z-0 h-48 overflow-visible">
<img id="he-image" src="" class="h-48 w-64 md:w-80 object-cover rounded-t-2xl shadow-2xl opacity-0 translate-y-full" style="will-change: transform, opacity;" />
</div>
<div class="bg-surface-container-lowest rounded-xl p-10 editorial-shadow flex flex-col justify-between relative overflow-hidden group h-full z-10">
<div class="relative z-10">
<div class="flex items-center gap-4 mb-6"> <div class="flex items-center gap-4 mb-6">
<span class="material-symbols-outlined text-4xl text-primary" style="font-variation-settings: 'FILL' 1;">military_tech</span> <span class="material-symbols-outlined text-4xl text-primary" style="font-variation-settings: 'FILL' 1;">military_tech</span>
<span class="font-headline font-extrabold text-2xl uppercase italic">{{ $e.meilenstein.event }}</span> <span id="he-titel" class="font-headline font-extrabold text-2xl uppercase italic">{{ $e.meilenstein.event }}</span>
</div> </div>
<h3 class="text-4xl font-headline font-bold text-on-surface mb-4">{{ $e.meilenstein.platz }}</h3> <h3 id="he-platz" class="text-4xl font-headline font-bold text-on-surface mb-4">{{ $e.meilenstein.platz }}</h3>
<p class="text-on-surface-variant max-w-md text-lg">{{ $e.meilenstein.beschreibung }}</p> <p id="he-desc" class="text-on-surface-variant max-w-md text-lg">{{ $e.meilenstein.beschreibung }}</p>
</div> </div>
<div class="mt-12 flex items-center gap-6 relative z-10"> <div class="mt-12 flex items-center gap-6 relative z-10">
<div> <div>
<div class="text-3xl font-black text-on-surface">{{ $e.meilenstein.jahr }}</div> <div id="he-jahr" class="text-3xl font-black text-on-surface">{{ $e.meilenstein.jahr }}</div>
<div class="text-xs uppercase tracking-widest font-bold text-primary">{{ $e.meilenstein.ort }}</div> <div id="he-ort" class="text-xs uppercase tracking-widest font-bold text-primary">{{ $e.meilenstein.ort }}</div>
</div> </div>
<div class="h-10 w-[1px] bg-outline-variant/30"></div> <div class="h-10 w-[1px] bg-outline-variant/30"></div>
<div> <div>
<div class="text-3xl font-black text-on-surface">{{ $e.meilenstein.kategorie }}</div> <div id="he-kat" class="text-3xl font-black text-on-surface">{{ $e.meilenstein.kategorie }}</div>
<div class="text-xs uppercase tracking-widest font-bold text-primary">Kategorie</div> <div class="text-xs uppercase tracking-widest font-bold text-primary">Kategorie</div>
</div> </div>
</div> </div>
<span class="material-symbols-outlined absolute -bottom-10 -right-10 text-[15rem] text-surface-container opacity-20 group-hover:opacity-30 transition-opacity">sports_martial_arts</span> <span class="material-symbols-outlined absolute -bottom-10 -right-10 text-[15rem] text-surface-container opacity-20 group-hover:opacity-30 transition-opacity">sports_martial_arts</span>
</div>
</div> </div>
<!-- Weitere Auszeichnungen --> <!-- Weitere Auszeichnungen -->
@ -212,7 +270,13 @@
<h4 class="font-headline font-bold text-xl border-b border-on-primary/20 pb-4">Weitere Auszeichnungen</h4> <h4 class="font-headline font-bold text-xl border-b border-on-primary/20 pb-4">Weitere Auszeichnungen</h4>
{{ $icons := slice "star" "emoji_events" "rewarded_ads" "workspace_premium" "military_tech" "sports_martial_arts" }} {{ $icons := slice "star" "emoji_events" "rewarded_ads" "workspace_premium" "military_tech" "sports_martial_arts" }}
{{ range $i, $a := $e.weitere_auszeichnungen }} {{ range $i, $a := $e.weitere_auszeichnungen }}
<div class="flex gap-4"> <div class="weitere-erfolg-item flex gap-4 cursor-pointer rounded-lg p-2 -m-2 hover:bg-white/10 transition-all active:scale-95"
data-titel="{{ $a.titel }}"
data-detail="{{ $a.detail }}"
data-beschreibung="{{ $a.beschreibung }}"
data-jahr=""
data-ort=""
data-kategorie="">
<div class="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center shrink-0"> <div class="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center shrink-0">
<span class="material-symbols-outlined text-xl">{{ index $icons (mod $i (len $icons)) }}</span> <span class="material-symbols-outlined text-xl">{{ index $icons (mod $i (len $icons)) }}</span>
</div> </div>
@ -225,6 +289,29 @@
</div> </div>
</div> </div>
{{ end }} {{ end }}
{{ $weitereMarkdown := where (where .Site.RegularPages "Section" "erfolge") "Params.is_weitere_auszeichnung" "true" }}
{{ range $i, $page := $weitereMarkdown }}
<div class="weitere-erfolg-item flex gap-4 cursor-pointer rounded-lg p-2 -m-2 hover:bg-white/10 transition-all active:scale-95"
data-titel="{{ .Title }}"
data-detail="{{ .Params.rang }}"
data-beschreibung="{{ .Summary }}"
data-jahr="{{ .Params.datum }}"
data-ort="{{ .Params.ort }}"
data-kategorie="{{ .Params.kategorie }}"
data-image="{{ if .Params.image }}{{ printf "/erfolge-img/%s" .Params.image | relURL }}{{ end }}">
<div class="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center shrink-0">
<span class="material-symbols-outlined text-xl">{{ index $icons (mod $i (len $icons)) }}</span>
</div>
<div>
<p class="font-bold">{{ .Title }}</p>
<p class="text-sm opacity-80">{{ .Params.rang }}</p>
{{ if .Summary }}
<p class="text-xs opacity-70 mt-1 leading-relaxed">{{ .Summary }}</p>
{{ end }}
</div>
</div>
{{ end }}
</div> </div>
<!-- Stats --> <!-- Stats -->
@ -274,5 +361,85 @@
</div> </div>
</footer> </footer>
<!-- Lightbox Modal -->
<div id="imageLightbox" class="hidden fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/90 backdrop-blur-sm transition-opacity" onclick="closeLightbox(event)">
<img id="lightboxImage" src="" class="hidden max-w-[90vw] max-h-[90vh] object-contain rounded-xl shadow-2xl" />
<video id="lightboxVideo" src="" class="hidden max-w-[90vw] max-h-[90vh] object-contain rounded-xl shadow-2xl" controls autoplay></video>
<button class="absolute top-8 right-8 text-white bg-white/10 hover:bg-white/20 rounded-full p-2" onclick="closeLightbox(event, true)">
<span class="material-symbols-outlined text-3xl">close</span>
</button>
</div>
<script>
function openLightbox(src, isVideo) {
const img = document.getElementById('lightboxImage');
const vid = document.getElementById('lightboxVideo');
if (isVideo) {
img.classList.add('hidden');
vid.classList.remove('hidden');
vid.src = src;
} else {
vid.classList.add('hidden');
vid.pause();
img.classList.remove('hidden');
img.src = src;
}
document.getElementById('imageLightbox').classList.remove('hidden');
}
function closeLightbox(e, force) {
if (force || e.target.id === 'imageLightbox') {
document.getElementById('imageLightbox').classList.add('hidden');
document.getElementById('lightboxVideo').pause();
}
}
let animTimeout;
document.querySelectorAll('.weitere-erfolg-item').forEach(function(item) {
item.addEventListener('click', function() {
const titel = this.dataset.titel;
const detail = this.dataset.detail;
const beschreibung = this.dataset.beschreibung || '';
const jahr = this.dataset.jahr || '';
const ort = this.dataset.ort || '';
const kategorie = this.dataset.kategorie || '';
const imageUrl = this.dataset.image || '';
// Bild zurücksetzen
const imgEl = document.getElementById('he-image');
if (imgEl) {
imgEl.style.transition = 'none';
imgEl.style.transform = 'translateY(100%)';
imgEl.style.opacity = '0';
clearTimeout(animTimeout);
}
// Hauptkarte tauschen
document.getElementById('he-titel').textContent = titel;
document.getElementById('he-platz').textContent = detail;
document.getElementById('he-desc').textContent = beschreibung;
document.getElementById('he-jahr').textContent = jahr;
document.getElementById('he-ort').textContent = ort;
document.getElementById('he-kat').textContent = kategorie;
// Aktives Item hervorheben
document.querySelectorAll('.weitere-erfolg-item').forEach(function(el) {
el.classList.remove('bg-white/20', 'ring-2', 'ring-white/40');
});
this.classList.add('bg-white/20', 'ring-2', 'ring-white/40');
// Bild animieren
if (imgEl && imageUrl) {
imgEl.src = imageUrl;
animTimeout = setTimeout(() => {
imgEl.style.transition = 'all 2000ms cubic-bezier(0.2, 0.8, 0.2, 1)';
imgEl.style.transform = 'translateY(-98%)';
imgEl.style.opacity = '1';
}, 2000); // nach 2 Sekunden
}
});
});
</script>
</body> </body>
</html> </html>

View file

@ -142,12 +142,12 @@
{{ .Site.Data.homepage.hero.description }} {{ .Site.Data.homepage.hero.description }}
</p> </p>
<div class="flex flex-wrap gap-4"> <div class="flex flex-wrap gap-4">
<button class="bg-gradient-to-br from-primary to-primary-container text-on-primary px-10 py-5 rounded-xl font-bold font-headline text-lg shadow-xl shadow-primary/20 hover:scale-105 active:scale-95 transition-all"> <a href="{{ "/uebermich/" | relURL }}" class="bg-gradient-to-br from-primary to-primary-container text-on-primary px-10 py-5 rounded-xl font-bold font-headline text-lg shadow-xl shadow-primary/20 hover:scale-105 active:scale-95 transition-all text-center">
Erfahre mehr Erfahre mehr
</button> </a>
<button class="px-10 py-5 rounded-xl font-bold font-headline text-lg text-primary border-2 border-primary/20 hover:bg-primary/5 transition-all"> <a href="{{ "/galerie/" | relURL }}" class="px-10 py-5 rounded-xl font-bold font-headline text-lg text-primary border-2 border-primary/20 hover:bg-primary/5 transition-all text-center">
Galerie ansehen Galerie ansehen
</button> </a>
</div> </div>
</div> </div>
<div class="lg:col-span-6 order-1 lg:order-2 relative"> <div class="lg:col-span-6 order-1 lg:order-2 relative">
@ -193,8 +193,8 @@
<h2 class="text-5xl font-black font-headline mt-4">Meine Highlights</h2> <h2 class="text-5xl font-black font-headline mt-4">Meine Highlights</h2>
</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 first 1 (where (where .Site.RegularPages "Section" "erfolge") "Params.is_weitere_auszeichnung" "!=" "true").ByWeight }}
<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="md:col-span-12 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"> <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>
@ -208,9 +208,41 @@
<div class="px-8 pb-8 border-t border-surface-container-high"> <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"> <div class="pt-6 text-on-surface-variant leading-relaxed space-y-4 text-base">
{{ .Content }} {{ .Content }}
{{ if .Params.images }}
<div class="mt-8 flex gap-4 overflow-x-auto pb-4 snap-x">
{{ range $img := split .Params.images "," }}
<div class="shrink-0 snap-start relative">
{{ if (or (strings.HasSuffix (lower $img) ".mp4") (strings.HasSuffix (lower $img) ".mov") (strings.HasSuffix (lower $img) ".webm") (strings.HasSuffix (lower $img) ".m4v")) }}
<video src="{{ printf "/erfolge-img/%s" $img | relURL }}" class="h-24 w-24 md:h-32 md:w-32 object-cover rounded-xl cursor-pointer hover:opacity-80 transition-opacity editorial-shadow" onclick="openLightbox(this.src, true)"></video>
<span class="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 text-3xl">play_circle</span>
{{ else }}
<img src="{{ printf "/erfolge-img/%s" $img | relURL }}" class="h-24 w-24 md:h-32 md:w-32 object-cover rounded-xl cursor-pointer hover:opacity-80 transition-opacity editorial-shadow" onclick="openLightbox(this.src, false)"/>
{{ end }}
</div>
{{ end }}
</div>
{{ end }}
{{ if or .Params.datum .Params.ort .Params.kategorie }}
<div class="mt-8 pt-6 border-t border-outline-variant/20 flex flex-wrap items-center justify-between gap-6">
{{ if or .Params.datum .Params.ort }}
<div>
{{ if .Params.datum }}<div class="text-xl font-headline text-on-surface">{{ .Params.datum }}</div>{{ end }}
{{ if .Params.ort }}<div class="text-xs uppercase tracking-widest font-bold text-primary">{{ .Params.ort }}</div>{{ end }}
</div>
{{ end }}
{{ if .Params.kategorie }}
<div class="text-right">
<div class="text-xl font-headline text-on-surface">{{ .Params.kategorie }}</div>
<div class="text-xs uppercase tracking-widest font-bold text-primary">Kategorie</div>
</div>
{{ end }}
</div>
{{ end }}
</div> </div>
<a href="{{ .RelPermalink }}" class="inline-flex items-center gap-2 mt-6 text-sm font-bold text-primary hover:underline"> <a href="/erfolge/" 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> Alle Erfolge im Detail ansehen <span class="material-symbols-outlined text-sm">arrow_forward</span>
</a> </a>
</div> </div>
</div> </div>
@ -225,35 +257,22 @@
</div> </div>
{{ end }} {{ end }}
<!-- Karte: Prüfung bestanden --> </div>
<div class="md:col-span-4 bg-primary text-on-primary rounded-xl p-8 flex flex-col justify-between hover:scale-[1.02] transition-transform shadow-xl shadow-primary/20">
<span class="material-symbols-outlined text-4xl" style="font-variation-settings: 'FILL' 1;">military_tech</span> <!-- Bild Link zu Erfolge -->
<div> <div class="mt-12">
<h3 class="text-2xl font-black font-headline mb-2">{{ .Site.Data.homepage.pruefung_karte.titel }}</h3> <a href="/erfolge/" class="block h-64 md:h-80 rounded-xl overflow-hidden relative group editorial-shadow">
<p class="text-on-primary/80">{{ .Site.Data.homepage.pruefung_karte.beschreibung }}</p> <img alt="Alle Erfolge ansehen" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-1000 grayscale group-hover:grayscale-0"
</div>
</div>
<!-- Karte: Dojo Champion -->
<div class="md:col-span-4 bg-surface-container-low rounded-xl p-8 hover:bg-surface-container-high transition-colors">
<div class="w-12 h-12 bg-white rounded-lg flex items-center justify-center mb-6 shadow-sm">
<span class="material-symbols-outlined text-secondary">fitness_center</span>
</div>
<h3 class="text-xl font-black font-headline mb-2">{{ .Site.Data.homepage.dojo_karte.titel }}</h3>
<p class="text-sm text-on-surface-variant italic">{{ .Site.Data.homepage.dojo_karte.beschreibung }}</p>
</div>
<!-- Bild: Team Spirit -->
<div class="md:col-span-8 h-64 rounded-xl overflow-hidden relative group">
<img alt="Teamtraining" class="w-full h-full object-cover group-hover:scale-110 transition-transform duration-1000"
src="https://lh3.googleusercontent.com/aida-public/AB6AXuAxl_8pFN3p4UiOW5vHu3eUKrCj1VrD6wYTDuajtl3iHvjkNJEOLbqbWt2QL5xcC4jC8H4unZhLKoapi4aH8paj81HeIEcOZwkfBBDZyLz6vQpXOODIg4KyEBDvfGzCyJWGf_qTxRmpzgjNfLAOeKA1dCFuRq5MJGSi3gsuobQBv7cR1kGAkgMKXEMTjsKrR_ZvwVQYyb7tWPzAWfTqEt97ViRO1y05wIOMbBiqTx2qXcbzVfsdVvtyAjaMh9Wh7npl8UTJIlvMXA"/> src="https://lh3.googleusercontent.com/aida-public/AB6AXuAxl_8pFN3p4UiOW5vHu3eUKrCj1VrD6wYTDuajtl3iHvjkNJEOLbqbWt2QL5xcC4jC8H4unZhLKoapi4aH8paj81HeIEcOZwkfBBDZyLz6vQpXOODIg4KyEBDvfGzCyJWGf_qTxRmpzgjNfLAOeKA1dCFuRq5MJGSi3gsuobQBv7cR1kGAkgMKXEMTjsKrR_ZvwVQYyb7tWPzAWfTqEt97ViRO1y05wIOMbBiqTx2qXcbzVfsdVvtyAjaMh9Wh7npl8UTJIlvMXA"/>
<div class="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent flex items-end p-8"> <div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent flex items-end p-8 md:p-12">
<div> <div>
<h3 class="text-2xl font-bold font-headline text-white">Team Spirit</h3> <h3 class="text-3xl md:text-5xl font-black font-headline text-white mb-4">Alle Erfolge entdecken</h3>
<p class="text-white/70">Gemeinsam trainieren macht am meisten Spaß.</p> <div class="inline-flex items-center gap-2 bg-primary text-on-primary px-6 py-3 rounded-xl font-bold text-sm uppercase tracking-widest group-hover:bg-primary-dim transition-colors shadow-lg">
Zur Übersicht <span class="material-symbols-outlined text-sm">arrow_forward</span>
</div>
</div> </div>
</div> </div>
</div> </a>
</div> </div>
</section> </section>
@ -263,7 +282,7 @@
<div class="max-w-7xl mx-auto px-6 text-center text-white"> <div class="max-w-7xl mx-auto px-6 text-center text-white">
<h2 class="text-5xl md:text-7xl font-black font-headline italic mb-8">{{ .Site.Data.homepage.cta.heading_main }} <span class="text-primary-fixed">{{ .Site.Data.homepage.cta.heading_colored }}</span></h2> <h2 class="text-5xl md:text-7xl font-black font-headline italic mb-8">{{ .Site.Data.homepage.cta.heading_main }} <span class="text-primary-fixed">{{ .Site.Data.homepage.cta.heading_colored }}</span></h2>
<p class="text-xl text-zinc-400 max-w-2xl mx-auto mb-12">{{ .Site.Data.homepage.cta.description }}</p> <p class="text-xl text-zinc-400 max-w-2xl mx-auto mb-12">{{ .Site.Data.homepage.cta.description }}</p>
<a class="inline-flex items-center gap-4 bg-primary text-white px-12 py-6 rounded-full font-black font-headline text-xl hover:bg-primary-dim transition-colors group" href="/galerie/"> <a class="inline-flex items-center gap-4 bg-primary text-white px-12 py-6 rounded-full font-black font-headline text-xl hover:bg-primary-dim transition-colors group" href="{{ "/galerie/" | relURL }}">
{{ .Site.Data.homepage.cta.button_text }} {{ .Site.Data.homepage.cta.button_text }}
<span class="material-symbols-outlined group-hover:translate-x-2 transition-transform">auto_awesome_motion</span> <span class="material-symbols-outlined group-hover:translate-x-2 transition-transform">auto_awesome_motion</span>
</a> </a>
@ -339,7 +358,39 @@
</div> </div>
</div> </div>
<div id="imageLightbox" class="hidden fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/90 backdrop-blur-sm transition-opacity" onclick="closeLightbox(event)">
<img id="lightboxImage" src="" class="hidden max-w-[90vw] max-h-[90vh] object-contain rounded-xl shadow-2xl" />
<video id="lightboxVideo" src="" class="hidden max-w-[90vw] max-h-[90vh] object-contain rounded-xl shadow-2xl" controls autoplay></video>
<button class="absolute top-8 right-8 text-white bg-white/10 hover:bg-white/20 rounded-full p-2" onclick="closeLightbox(event, true)">
<span class="material-symbols-outlined text-3xl">close</span>
</button>
</div>
<script> <script>
function openLightbox(src, isVideo) {
const img = document.getElementById('lightboxImage');
const vid = document.getElementById('lightboxVideo');
if (isVideo) {
img.classList.add('hidden');
vid.classList.remove('hidden');
vid.src = src;
} else {
vid.classList.add('hidden');
vid.pause();
img.classList.remove('hidden');
img.src = src;
}
document.getElementById('imageLightbox').classList.remove('hidden');
}
function closeLightbox(e, force) {
if (force || e.target.id === 'imageLightbox') {
document.getElementById('imageLightbox').classList.add('hidden');
document.getElementById('lightboxVideo').pause();
}
}
function closeKontakt() { function closeKontakt() {
document.getElementById('kontaktModal').classList.add('hidden'); document.getElementById('kontaktModal').classList.add('hidden');
document.getElementById('kontaktForm').reset(); document.getElementById('kontaktForm').reset();

BIN
static/.DS_Store vendored

Binary file not shown.

0
test
View file

0
test1
View file

View file