mirror of
https://github.com/superschnups/Emy.git
synced 2026-06-22 03:13:10 +00:00
Auto-backup: 2026-05-12 02:17
This commit is contained in:
parent
e58692de19
commit
b9c65ff32b
21 changed files with 707 additions and 155 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
|
|
@ -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 });
|
||||||
|
|
|
||||||
291
admin.html
291
admin.html
|
|
@ -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>
|
||||||
|
|
@ -926,6 +958,70 @@ tailwind.config = {
|
||||||
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'];
|
||||||
const pageTitles = {
|
const pageTitles = {
|
||||||
|
|
@ -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
BIN
content/.DS_Store
vendored
Binary file not shown.
34
content/erfolge/dojo-champion.md
Normal file
34
content/erfolge/dojo-champion.md
Normal 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.
|
||||||
26
content/erfolge/fairness-preis.md
Normal file
26
content/erfolge/fairness-preis.md
Normal 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.
|
||||||
|
|
@ -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...
|
||||||
|
|
|
||||||
16
content/erfolge/landesmeisterschaft-platz3.md
Normal file
16
content/erfolge/landesmeisterschaft-platz3.md
Normal 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!
|
||||||
|
|
@ -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!
|
||||||
|
|
|
||||||
|
|
@ -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+",
|
||||||
|
|
|
||||||
|
|
@ -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": [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
BIN
layouts/.DS_Store
vendored
Binary file not shown.
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
<div>
|
|
||||||
<h3 class="text-2xl font-black font-headline mb-2">{{ .Site.Data.homepage.pruefung_karte.titel }}</h3>
|
|
||||||
<p class="text-on-primary/80">{{ .Site.Data.homepage.pruefung_karte.beschreibung }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Karte: Dojo Champion -->
|
<!-- Bild Link zu Erfolge -->
|
||||||
<div class="md:col-span-4 bg-surface-container-low rounded-xl p-8 hover:bg-surface-container-high transition-colors">
|
<div class="mt-12">
|
||||||
<div class="w-12 h-12 bg-white rounded-lg flex items-center justify-center mb-6 shadow-sm">
|
<a href="/erfolge/" class="block h-64 md:h-80 rounded-xl overflow-hidden relative group editorial-shadow">
|
||||||
<span class="material-symbols-outlined text-secondary">fitness_center</span>
|
<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>
|
|
||||||
<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
BIN
static/.DS_Store
vendored
Binary file not shown.
BIN
static/erfolge-img/success-dojo-champion-1778543253478-vs81l.mp4
Normal file
BIN
static/erfolge-img/success-dojo-champion-1778543253478-vs81l.mp4
Normal file
Binary file not shown.
BIN
static/erfolge-img/success-dojo-champion-1778544116200-5pv3h.mp4
Normal file
BIN
static/erfolge-img/success-dojo-champion-1778544116200-5pv3h.mp4
Normal file
Binary file not shown.
0
test
0
test
0
test1
0
test1
0
test1222
0
test1222
Loading…
Reference in a new issue