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
|
||||
const upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: { fileSize: 20 * 1024 * 1024 },
|
||||
limits: { fileSize: 100 * 1024 * 1024 }, // 100MB limit für Videos
|
||||
fileFilter: (req, file, cb) => {
|
||||
if (file.mimetype.startsWith('image/')) cb(null, true);
|
||||
else cb(new Error('Nur Bilder erlaubt!'));
|
||||
if (file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) cb(null, true);
|
||||
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 c = fs.readFileSync(path.join(ERFOLGE_CONTENT_DIR, f), 'utf8');
|
||||
const { data } = parseFM(c);
|
||||
return { fileName: f, title: data.title || f, rang: data.rang, image: data.image };
|
||||
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);
|
||||
});
|
||||
|
||||
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) => {
|
||||
const c = fs.readFileSync(path.join(ERFOLGE_CONTENT_DIR, req.params.file), 'utf8');
|
||||
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 oldContent = fs.readFileSync(filePath, 'utf8');
|
||||
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';
|
||||
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';
|
||||
fs.writeFileSync(filePath, fmStr + '\n' + content);
|
||||
|
|
@ -328,20 +368,47 @@ app.put('/api/individual-success/:file', (req, res) => {
|
|||
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 {
|
||||
const baseName = req.params.file.replace('.md', '');
|
||||
const filename = `success-${baseName}-${Date.now()}.webp`;
|
||||
await sharp(req.file.buffer)
|
||||
const filenames = [];
|
||||
|
||||
for (const file of req.files) {
|
||||
const isVideo = file.mimetype.startsWith('video/');
|
||||
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
|
||||
const filePath = path.join(ERFOLGE_CONTENT_DIR, req.params.file);
|
||||
const c = fs.readFileSync(filePath, 'utf8');
|
||||
const { data, content } = parseFM(c);
|
||||
data.image = filename;
|
||||
|
||||
let 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';
|
||||
for (const k in data) {
|
||||
|
|
@ -350,7 +417,7 @@ app.post('/api/individual-success/:file/image', upload.single('image'), async (r
|
|||
fmStr += '---\n';
|
||||
fs.writeFileSync(filePath, fmStr + '\n' + content);
|
||||
|
||||
res.json({ ok: true, image: filename });
|
||||
res.json({ ok: true, images: filenames });
|
||||
rebuildAndDeploy();
|
||||
} catch (e) {
|
||||
res.status(500).json({ ok: false, error: e.message });
|
||||
|
|
|
|||
251
admin.html
251
admin.html
|
|
@ -857,7 +857,26 @@ tailwind.config = {
|
|||
|
||||
<!-- ═══ EDIT MODAL ═══ -->
|
||||
<div id="editModal" class="hidden fixed inset-0 bg-black/40 backdrop-blur-sm z-50 flex items-center justify-center p-4">
|
||||
...
|
||||
<div class="bg-surface-container-lowest rounded-xl p-8 w-full max-w-md shadow-2xl">
|
||||
<h3 class="text-xl font-black font-lexend text-on-surface mb-6 flex items-center gap-2">
|
||||
<span class="material-symbols-outlined text-primary">edit</span> Foto bearbeiten
|
||||
</h3>
|
||||
<input type="hidden" id="editId"/>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Titel</label>
|
||||
<input id="editTitle" type="text" class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm font-bold"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kategorie</label>
|
||||
<select id="editKat" class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-8">
|
||||
<button type="button" id="saveEditBtn" class="flex-1 py-3 rounded-xl font-black bg-gradient-to-r from-primary to-primary-container text-on-primary active:scale-95 transition-all font-lexend">Speichern</button>
|
||||
<button type="button" id="cancelEditBtn" class="px-6 py-3 rounded-xl font-bold bg-surface-container text-on-surface-variant hover:bg-surface-container-high transition-all">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ═══ EDIT SUCCESS MODAL ═══ -->
|
||||
|
|
@ -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>
|
||||
<input id="esRang" type="text" class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Ort</label>
|
||||
<input id="esOrt" type="text" class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Datum</label>
|
||||
<input id="esDatum" type="text" class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm placeholder-zinc-400" placeholder="z.B. 2024-05-12"/>
|
||||
</div>
|
||||
<div class="col-span-2">
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kategorie</label>
|
||||
<input id="esKat" type="text" class="w-full bg-surface-container-low rounded-DEFAULT px-4 py-3 text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm placeholder-zinc-400" placeholder="z.B. Wettkämpfe"/>
|
||||
</div>
|
||||
<div class="col-span-2 flex items-center gap-3 mt-2">
|
||||
<input id="esIsWeitere" type="checkbox" class="w-5 h-5 text-primary rounded border-outline-variant focus:ring-primary/30 cursor-pointer"/>
|
||||
<label for="esIsWeitere" class="text-sm font-bold text-on-surface-variant cursor-pointer">Unter "Weitere Auszeichnungen" anzeigen (statt als ausführlicher Bericht)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-2">Kurzfassung (Summary)</label>
|
||||
|
|
@ -889,22 +924,19 @@ tailwind.config = {
|
|||
|
||||
<!-- Bild Upload für Einzelerfolg -->
|
||||
<div class="pt-4 border-t border-zinc-100">
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Hintergrundbild (transparentes Foto)</label>
|
||||
<div class="flex gap-4 items-center">
|
||||
<div class="w-20 h-20 rounded-lg overflow-hidden bg-surface-container shrink-0">
|
||||
<img id="esPreview" src="" class="w-full h-full object-cover hidden"/>
|
||||
<div id="esPlaceholder" class="w-full h-full flex items-center justify-center text-on-surface-variant">
|
||||
<span class="material-symbols-outlined">image</span>
|
||||
<label class="block text-xs font-bold text-on-surface-variant uppercase tracking-widest mb-3">Beitragsbilder (max 5, Klick wählt Hauptbild)</label>
|
||||
|
||||
<div id="esImagesPreviewContainer" class="flex gap-3 mb-4 flex-wrap">
|
||||
<div class="w-full text-xs text-on-surface-variant italic">Keine Bilder vorhanden.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<input type="file" id="esFileInput" accept="image/*" class="hidden"/>
|
||||
<button type="button" onclick="document.getElementById('esFileInput').click()" class="text-xs font-bold text-primary bg-pink-50 px-4 py-2 rounded-full hover:bg-pink-100 transition-all">Bild wählen</button>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<input type="file" id="esFileInput" accept="image/*, video/*" multiple class="hidden"/>
|
||||
<button type="button" onclick="document.getElementById('esFileInput').click()" class="text-xs font-bold text-primary bg-pink-50 px-4 py-2 rounded-full hover:bg-pink-100 transition-all">Neue Bilder wählen</button>
|
||||
<p id="esFileStatus" class="text-[10px] text-on-surface-variant mt-1">Nichts ausgewählt</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-8">
|
||||
<button type="button" id="saveSuccessBtn" class="flex-1 py-3 rounded-xl font-black bg-gradient-to-r from-primary to-primary-container text-on-primary active:scale-95 transition-all font-lexend">Speichern</button>
|
||||
<button type="button" onclick="document.getElementById('editSuccessModal').classList.add('hidden')" class="px-6 py-3 rounded-xl font-bold bg-surface-container text-on-surface-variant hover:bg-surface-container-high transition-all">Abbrechen</button>
|
||||
|
|
@ -926,6 +958,70 @@ tailwind.config = {
|
|||
let allCategories = [];
|
||||
let pendingFiles = [];
|
||||
|
||||
let esCurrentImages = [];
|
||||
let esMainImageIndex = 0;
|
||||
let esNewFilesSelected = false;
|
||||
|
||||
function renderEsImages(items) {
|
||||
const container = document.getElementById('esImagesPreviewContainer');
|
||||
container.innerHTML = '';
|
||||
if (!items || items.length === 0) {
|
||||
container.innerHTML = '<div class="w-full text-xs text-on-surface-variant italic">Keine Bilder vorhanden.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
const isFile = item instanceof File;
|
||||
const isVideo = isFile ? item.type.startsWith('video/') : item.match(/\.(mp4|webm|mov|m4v)$/i);
|
||||
const src = isFile ? URL.createObjectURL(item) : '/erfolge-img/' + item + '?t=' + Date.now();
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.className = 'relative w-20 h-20 rounded-lg overflow-hidden border-4 cursor-pointer transition-all ' + (i === esMainImageIndex ? 'border-primary shadow-lg scale-105 z-10' : 'border-transparent hover:border-primary/50');
|
||||
div.onclick = () => {
|
||||
esMainImageIndex = i;
|
||||
renderEsImages(items);
|
||||
};
|
||||
|
||||
if (isVideo) {
|
||||
const vid = document.createElement('video');
|
||||
vid.src = src;
|
||||
vid.className = 'w-full h-full object-cover';
|
||||
vid.muted = true;
|
||||
div.appendChild(vid);
|
||||
|
||||
const playIcon = document.createElement('span');
|
||||
playIcon.className = 'material-symbols-outlined absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-white drop-shadow-md pointer-events-none';
|
||||
playIcon.textContent = 'play_circle';
|
||||
div.appendChild(playIcon);
|
||||
} else {
|
||||
const img = document.createElement('img');
|
||||
img.src = src;
|
||||
img.className = 'w-full h-full object-cover';
|
||||
div.appendChild(img);
|
||||
}
|
||||
|
||||
const badge = document.createElement('div');
|
||||
badge.className = 'absolute bottom-0 left-0 right-0 bg-primary/90 text-white text-[9px] text-center font-bold py-1 z-20 ' + (i === esMainImageIndex ? 'block' : 'hidden');
|
||||
badge.textContent = 'HAUPTBILD';
|
||||
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'absolute top-1 right-1 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-[10px] shadow-md z-30 hover:bg-red-600 transition-colors';
|
||||
deleteBtn.innerHTML = '✕';
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
items.splice(i, 1);
|
||||
if (esMainImageIndex >= items.length) esMainImageIndex = Math.max(0, items.length - 1);
|
||||
renderEsImages(items);
|
||||
document.getElementById('esFileStatus').textContent = items.length + ' Dateien insgesamt';
|
||||
};
|
||||
|
||||
div.appendChild(badge);
|
||||
div.appendChild(deleteBtn);
|
||||
container.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Navigation ──────────────────────────────────────────
|
||||
const sections = ['overview', 'media', 'homepage', 'uebermich', 'career', 'community', 'settings'];
|
||||
const pageTitles = {
|
||||
|
|
@ -1886,17 +1982,32 @@ tailwind.config = {
|
|||
} catch(err) { console.error('Career save error:', err); }
|
||||
});
|
||||
|
||||
let individualSuccessData = [];
|
||||
|
||||
async function loadIndividualSuccesses() {
|
||||
try {
|
||||
const r = await fetch('/api/individual-successes');
|
||||
const list = await r.json();
|
||||
individualSuccessData = await r.json();
|
||||
renderIndividualSuccesses();
|
||||
} catch(err) { console.error('Load individual successes error:', err); }
|
||||
}
|
||||
|
||||
function renderIndividualSuccesses() {
|
||||
const container = document.getElementById('individualSuccessList');
|
||||
container.innerHTML = '';
|
||||
list.forEach(item => {
|
||||
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>
|
||||
|
|
@ -1905,72 +2016,136 @@ tailwind.config = {
|
|||
<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">
|
||||
<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);
|
||||
});
|
||||
} catch(err) { console.error('Load individual successes error:', err); }
|
||||
}
|
||||
|
||||
async function editSuccess(fileName) {
|
||||
window.moveSuccessUp = async function(index) {
|
||||
if (index <= 0) return;
|
||||
const temp = individualSuccessData[index - 1];
|
||||
individualSuccessData[index - 1] = individualSuccessData[index];
|
||||
individualSuccessData[index] = temp;
|
||||
renderIndividualSuccesses();
|
||||
await saveIndividualSuccessOrder();
|
||||
};
|
||||
|
||||
window.moveSuccessDown = async function(index) {
|
||||
if (index >= individualSuccessData.length - 1) return;
|
||||
const temp = individualSuccessData[index + 1];
|
||||
individualSuccessData[index + 1] = individualSuccessData[index];
|
||||
individualSuccessData[index] = temp;
|
||||
renderIndividualSuccesses();
|
||||
await saveIndividualSuccessOrder();
|
||||
};
|
||||
|
||||
async function saveIndividualSuccessOrder() {
|
||||
try {
|
||||
const order = individualSuccessData.map(item => item.fileName);
|
||||
const res = await fetch('/api/individual-successes/reorder', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ order })
|
||||
});
|
||||
if (!res.ok) throw new Error('Reorder failed');
|
||||
} catch(err) {
|
||||
console.error('Save order error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
window.editSuccess = async function(fileName) {
|
||||
try {
|
||||
const r = await fetch('/api/individual-success/' + fileName);
|
||||
const data = await r.json();
|
||||
document.getElementById('esFileName').value = fileName;
|
||||
document.getElementById('esTitle').value = data.title || '';
|
||||
document.getElementById('esRang').value = data.rang || '';
|
||||
document.getElementById('esOrt').value = data.ort || '';
|
||||
document.getElementById('esDatum').value = data.datum || '';
|
||||
document.getElementById('esKat').value = data.kategorie || '';
|
||||
document.getElementById('esIsWeitere').checked = data.is_weitere_auszeichnung === 'true' || data.is_weitere_auszeichnung === true;
|
||||
document.getElementById('esSummary').value = data.summary || '';
|
||||
document.getElementById('esContent').value = data.content || '';
|
||||
|
||||
const preview = document.getElementById('esPreview');
|
||||
const placeholder = document.getElementById('esPlaceholder');
|
||||
if (data.image) {
|
||||
preview.src = '/erfolge-img/' + data.image + '?t=' + Date.now();
|
||||
preview.classList.remove('hidden');
|
||||
placeholder.classList.add('hidden');
|
||||
} else {
|
||||
preview.classList.add('hidden');
|
||||
placeholder.classList.remove('hidden');
|
||||
esNewFilesSelected = false;
|
||||
document.getElementById('esFileInput').value = '';
|
||||
document.getElementById('esFileStatus').textContent = 'Keine neuen Dateien gewählt';
|
||||
|
||||
let existingImages = [];
|
||||
if (data.images) {
|
||||
existingImages = data.images.split(',').map(s => s.trim()).filter(Boolean);
|
||||
} else if (data.image) {
|
||||
existingImages = [data.image];
|
||||
}
|
||||
|
||||
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');
|
||||
} catch(err) { console.error('Edit success error:', err); }
|
||||
}
|
||||
|
||||
document.getElementById('esFileInput').addEventListener('change', function() {
|
||||
const file = this.files[0];
|
||||
if (file) {
|
||||
document.getElementById('esPreview').src = URL.createObjectURL(file);
|
||||
document.getElementById('esPreview').classList.remove('hidden');
|
||||
document.getElementById('esPlaceholder').classList.add('hidden');
|
||||
document.getElementById('esFileStatus').textContent = file.name;
|
||||
const newFiles = Array.from(this.files);
|
||||
if (esCurrentImages.length + newFiles.length > 5) {
|
||||
alert(`Bitte maximal 5 Dateien insgesamt auswählen! (Aktuell: ${esCurrentImages.length})`);
|
||||
this.value = '';
|
||||
return;
|
||||
}
|
||||
if (newFiles.length > 0) {
|
||||
esNewFilesSelected = true;
|
||||
esCurrentImages = esCurrentImages.concat(newFiles);
|
||||
renderEsImages(esCurrentImages);
|
||||
document.getElementById('esFileStatus').textContent = esCurrentImages.length + ' Dateien insgesamt';
|
||||
}
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
document.getElementById('saveSuccessBtn').addEventListener('click', async function() {
|
||||
const fileName = document.getElementById('esFileName').value;
|
||||
const title = document.getElementById('esTitle').value;
|
||||
const rang = document.getElementById('esRang').value;
|
||||
const ort = document.getElementById('esOrt').value;
|
||||
const datum = document.getElementById('esDatum').value;
|
||||
const kategorie = document.getElementById('esKat').value;
|
||||
const is_weitere_auszeichnung = document.getElementById('esIsWeitere').checked;
|
||||
const summary = document.getElementById('esSummary').value;
|
||||
const content = document.getElementById('esContent').value;
|
||||
const fileInput = document.getElementById('esFileInput');
|
||||
|
||||
try {
|
||||
const existingOnly = esCurrentImages.filter(x => typeof x === 'string');
|
||||
const newFilesOnly = esCurrentImages.filter(x => x instanceof File);
|
||||
const selectedMainImage = (newFilesOnly.length === 0 && existingOnly.length > 0) ? existingOnly[esMainImageIndex] : '';
|
||||
const finalImagesStr = existingOnly.join(',');
|
||||
|
||||
// 1. Metadaten & Inhalt speichern
|
||||
const r = await fetch('/api/individual-success/' + fileName, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title, rang, summary, content })
|
||||
body: JSON.stringify({
|
||||
title, rang, ort, datum, kategorie, is_weitere_auszeichnung, summary, content,
|
||||
image: selectedMainImage,
|
||||
images: finalImagesStr
|
||||
})
|
||||
});
|
||||
|
||||
// 2. Bild falls gewählt
|
||||
if (fileInput.files.length > 0) {
|
||||
// 2. Bilder falls gewählt
|
||||
if (newFilesOnly.length > 0) {
|
||||
const fd = new FormData();
|
||||
fd.append('image', fileInput.files[0]);
|
||||
await fetch('/api/individual-success/' + fileName + '/image', {
|
||||
for (let i = 0; i < newFilesOnly.length; i++) {
|
||||
fd.append('images', newFilesOnly[i]);
|
||||
}
|
||||
fd.append('existingImages', JSON.stringify(existingOnly));
|
||||
fd.append('mainImageIndex', esMainImageIndex);
|
||||
await fetch('/api/individual-success/' + fileName + '/images', {
|
||||
method: 'POST',
|
||||
body: fd
|
||||
});
|
||||
|
|
|
|||
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"
|
||||
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!"
|
||||
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...
|
||||
|
|
|
|||
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"
|
||||
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."
|
||||
weight: 5
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Ü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",
|
||||
"kategorie": "U14"
|
||||
},
|
||||
"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": ""
|
||||
}
|
||||
],
|
||||
"weitere_auszeichnungen": [],
|
||||
"stats": [
|
||||
{
|
||||
"wert": "5+",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"description": "Die Kunst der Disziplin durch die Linse. Von intensiven Trainingseinheiten bis zum Triumph bei Gürtelprüfungen."
|
||||
},
|
||||
"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.",
|
||||
"stats": [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
{
|
||||
"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",
|
||||
"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 -->
|
||||
<section class="bg-surface-container-low py-24 rounded-t-[5rem]">
|
||||
<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>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
|
||||
{{ range (where .Site.RegularPages "Section" "erfolge") }}
|
||||
<div class="erfolg-card bg-surface-container-lowest rounded-xl editorial-shadow relative overflow-hidden group transition-all duration-500 hover:shadow-xl">
|
||||
<!-- Immer sichtbar -->
|
||||
<div class="p-8 relative z-10">
|
||||
<div class="flex items-center gap-4 mb-5">
|
||||
<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="space-y-16">
|
||||
{{ range (where (where .Site.RegularPages "Section" "erfolge") "Params.is_weitere_auszeichnung" "!=" "true").ByWeight }}
|
||||
<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">
|
||||
|
||||
{{ if .Params.image }}
|
||||
<!-- 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">
|
||||
<span class="material-symbols-outlined text-2xl text-primary" style="font-variation-settings: 'FILL' 1;">military_tech</span>
|
||||
</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>
|
||||
</div>
|
||||
<h3 class="text-2xl font-headline font-bold text-on-surface mb-3">{{ .Title }}</h3>
|
||||
<p class="text-on-surface-variant leading-relaxed mb-6">{{ .Summary }}</p>
|
||||
{{ end }}
|
||||
|
||||
<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)"
|
||||
class="flex items-center gap-2 text-primary font-bold text-sm font-headline uppercase tracking-wider hover:gap-3 transition-all">
|
||||
<span class="btn-label">Mehr lesen</span>
|
||||
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">Ganze Geschichte lesen</span>
|
||||
<span class="material-symbols-outlined transition-transform duration-300 text-base">expand_more</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Aufklappbarer Inhalt -->
|
||||
<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-6 text-on-surface-variant leading-relaxed space-y-4 text-base">
|
||||
<div class="pt-8 mt-8 border-t border-surface-container text-on-surface-variant leading-relaxed space-y-4 text-lg">
|
||||
{{ .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>
|
||||
<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>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
|
@ -179,40 +231,52 @@
|
|||
</script>
|
||||
|
||||
<!-- Meilensteine Bento Grid -->
|
||||
<section class="max-w-7xl mx-auto px-6 mt-24">
|
||||
<h2 class="text-5xl font-headline font-black mb-16 tracking-tight">MEILENSTEINE DES <span class="text-primary italic">ERFOLGS</span></h2>
|
||||
<section class="max-w-7xl mx-auto px-6 mt-48">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
|
||||
<!-- 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">
|
||||
<!-- 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">
|
||||
<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>
|
||||
<h3 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>
|
||||
<h3 id="he-platz" class="text-4xl font-headline font-bold text-on-surface mb-4">{{ $e.meilenstein.platz }}</h3>
|
||||
<p id="he-desc" class="text-on-surface-variant max-w-md text-lg">{{ $e.meilenstein.beschreibung }}</p>
|
||||
</div>
|
||||
<div class="mt-12 flex items-center gap-6 relative z-10">
|
||||
<div>
|
||||
<div 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-jahr" class="text-3xl font-black text-on-surface">{{ $e.meilenstein.jahr }}</div>
|
||||
<div id="he-ort" class="text-xs uppercase tracking-widest font-bold text-primary">{{ $e.meilenstein.ort }}</div>
|
||||
</div>
|
||||
<div class="h-10 w-[1px] bg-outline-variant/30"></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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weitere Auszeichnungen -->
|
||||
<div class="bg-primary rounded-xl p-8 text-on-primary flex flex-col gap-8 shadow-2xl shadow-primary/20">
|
||||
<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" }}
|
||||
{{ 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">
|
||||
<span class="material-symbols-outlined text-xl">{{ index $icons (mod $i (len $icons)) }}</span>
|
||||
</div>
|
||||
|
|
@ -225,6 +289,29 @@
|
|||
</div>
|
||||
</div>
|
||||
{{ 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>
|
||||
|
||||
<!-- Stats -->
|
||||
|
|
@ -274,5 +361,85 @@
|
|||
</div>
|
||||
</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>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -142,12 +142,12 @@
|
|||
{{ .Site.Data.homepage.hero.description }}
|
||||
</p>
|
||||
<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
|
||||
</button>
|
||||
<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>
|
||||
<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
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-8">
|
||||
{{ range (where .Site.RegularPages "Section" "erfolge") }}
|
||||
<div class="md:col-span-8 bg-surface-container-lowest rounded-xl shadow-sm group hover:shadow-xl transition-all duration-500 overflow-hidden relative success-card">
|
||||
{{ range first 1 (where (where .Site.RegularPages "Section" "erfolge") "Params.is_weitere_auszeichnung" "!=" "true").ByWeight }}
|
||||
<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">
|
||||
<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>
|
||||
|
|
@ -208,9 +208,41 @@
|
|||
<div class="px-8 pb-8 border-t border-surface-container-high">
|
||||
<div class="pt-6 text-on-surface-variant leading-relaxed space-y-4 text-base">
|
||||
{{ .Content }}
|
||||
|
||||
{{ 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>
|
||||
<a href="{{ .RelPermalink }}" class="inline-flex items-center gap-2 mt-6 text-sm font-bold text-primary hover:underline">
|
||||
Zum vollständigen Bericht <span class="material-symbols-outlined text-sm">open_in_new</span>
|
||||
{{ 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>
|
||||
<a href="/erfolge/" class="inline-flex items-center gap-2 mt-6 text-sm font-bold text-primary hover:underline">
|
||||
Alle Erfolge im Detail ansehen <span class="material-symbols-outlined text-sm">arrow_forward</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -225,35 +257,22 @@
|
|||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Karte: Prüfung bestanden -->
|
||||
<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 -->
|
||||
<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"
|
||||
<!-- Bild Link zu Erfolge -->
|
||||
<div class="mt-12">
|
||||
<a href="/erfolge/" class="block h-64 md:h-80 rounded-xl overflow-hidden relative group editorial-shadow">
|
||||
<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"
|
||||
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>
|
||||
<h3 class="text-2xl font-bold font-headline text-white">Team Spirit</h3>
|
||||
<p class="text-white/70">Gemeinsam trainieren macht am meisten Spaß.</p>
|
||||
<h3 class="text-3xl md:text-5xl font-black font-headline text-white mb-4">Alle Erfolge entdecken</h3>
|
||||
<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>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -263,7 +282,7 @@
|
|||
<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>
|
||||
<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 }}
|
||||
<span class="material-symbols-outlined group-hover:translate-x-2 transition-transform">auto_awesome_motion</span>
|
||||
</a>
|
||||
|
|
@ -339,7 +358,39 @@
|
|||
</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>
|
||||
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() {
|
||||
document.getElementById('kontaktModal').classList.add('hidden');
|
||||
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