diff --git a/partials/landing/account/dashboard.php b/partials/landing/account/dashboard.php index 0d5395c..289b06d 100755 --- a/partials/landing/account/dashboard.php +++ b/partials/landing/account/dashboard.php @@ -325,6 +325,15 @@ $sectionLinks = [ +
$meta): ?>
diff --git a/public/assets/css/app.css b/public/assets/css/app.css index a433802..7edbb10 100755 --- a/public/assets/css/app.css +++ b/public/assets/css/app.css @@ -252,6 +252,10 @@ body { .avatar-component-nav { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; } .avatar-component-nav__button { border: 1px solid var(--color-border); background: #fff; color: var(--color-text); border-radius: 999px; padding: 8px 12px; font: inherit; font-size: 13px; font-weight: 700; cursor: pointer; } .avatar-component-nav__button.is-active { background: var(--color-primary); color: var(--color-primary-contrast); border-color: var(--color-primary); } +.avatar-loader { width: 100%; display: grid; gap: 8px; padding: 12px 14px; background: rgba(255,255,255,0.72); border: 1px solid var(--color-border); border-radius: 14px; } +.avatar-loader__bar { width: 100%; height: 10px; overflow: hidden; border-radius: 999px; background: #ece2d4; } +.avatar-loader__fill { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, #37485a, #68819a); transition: width 180ms ease; } +.avatar-loader__meta { display: flex; justify-content: space-between; gap: 10px; align-items: center; } .avatar-component-panels { width: 100%; display: grid; } .avatar-component-panel { display: none; width: 100%; gap: 10px; } .avatar-component-panel.is-active { display: grid; } diff --git a/public/assets/js/app.js b/public/assets/js/app.js index ff394b0..ce9813e 100755 --- a/public/assets/js/app.js +++ b/public/assets/js/app.js @@ -793,19 +793,81 @@ document.addEventListener('DOMContentLoaded', () => { return activeTab?.getAttribute('data-avatar-component-tab') || 'avatar_lorelei_eyes_variant'; }; + const setAvatarLoaderState = (form, active, loaded = 0, total = 0, label = 'Varianten werden geladen') => { + const loader = form.querySelector('[data-avatar-loader]'); + const fill = form.querySelector('[data-avatar-loader-fill]'); + const progress = form.querySelector('[data-avatar-loader-progress]'); + const labelNode = form.querySelector('[data-avatar-loader-label]'); + if (!loader || !fill || !progress || !labelNode) return; + + if (!active) { + loader.hidden = true; + fill.style.width = '0%'; + progress.textContent = '0%'; + labelNode.textContent = label; + return; + } + + const percent = total > 0 ? Math.max(0, Math.min(100, Math.round((loaded / total) * 100))) : 0; + loader.hidden = false; + fill.style.width = `${percent}%`; + progress.textContent = `${percent}%`; + labelNode.textContent = label; + }; + const updateAvatarOptionPreviews = (form, activeField = null) => { const selector = activeField ? `[data-avatar-component-panel="${activeField}"] [data-avatar-option-thumb]` : '[data-avatar-option-thumb]'; - form.querySelectorAll(selector).forEach((image) => { + const images = Array.from(form.querySelectorAll(selector)); + if (!images.length) { + setAvatarLoaderState(form, false); + return; + } + + let loaded = 0; + const label = activeField + ? `Varianten fuer ${form.querySelector(`[data-avatar-component-tab="${activeField}"]`)?.textContent?.trim() || 'diesen Bereich'}` + : 'Varianten werden geladen'; + setAvatarLoaderState(form, true, 0, images.length, label); + + images.forEach((image) => { const field = image.getAttribute('data-avatar-option-field') || ''; const value = image.getAttribute('data-avatar-option-value') || ''; const params = buildAvatarParams(form, { [field]: value }); - if (!params) return; + if (!params) { + loaded += 1; + setAvatarLoaderState(form, true, loaded, images.length, label); + return; + } - image.dataset.avatarLoaded = 'true'; - image.src = `${avatarEndpoint}?${params.toString()}`; + const src = `${avatarEndpoint}?${params.toString()}`; + if (image.dataset.avatarLoadedSrc === src) { + loaded += 1; + setAvatarLoaderState(form, true, loaded, images.length, label); + if (loaded >= images.length) { + window.setTimeout(() => setAvatarLoaderState(form, false), 180); + } + return; + } + + const preload = new Image(); + const finalize = () => { + loaded += 1; + setAvatarLoaderState(form, true, loaded, images.length, label); + if (loaded >= images.length) { + window.setTimeout(() => setAvatarLoaderState(form, false), 180); + } + }; + + preload.onload = () => { + image.src = src; + image.dataset.avatarLoadedSrc = src; + finalize(); + }; + preload.onerror = finalize; + preload.src = src; }); }; @@ -880,7 +942,10 @@ document.addEventListener('DOMContentLoaded', () => { form.querySelectorAll('[data-avatar-component-tab]').forEach((button) => { button.addEventListener('click', () => { const field = button.getAttribute('data-avatar-component-tab'); - if (field) setActiveComponentPanel(field); + if (field) { + ensureAvatarBuilderLoaded(form); + setActiveComponentPanel(field); + } }); }); @@ -939,6 +1004,14 @@ document.addEventListener('DOMContentLoaded', () => { ensureAvatarBuilderLoaded(form); }); + document.querySelectorAll('[data-modal-open="modalAvatar"]').forEach((button) => { + button.addEventListener('click', () => { + const form = document.querySelector('#modalAvatar [data-avatar-builder]'); + if (!form) return; + window.setTimeout(() => ensureAvatarBuilderLoaded(form), 20); + }); + }); + document.querySelectorAll('[data-nav-events]').forEach(link => { link.addEventListener('click', async () => { if (window.location.pathname === '/' && window.location.hash === '#events') {