Feature: Hunde-Persönlichkeitstest + Kilometer-Lebenswerk-Badge (SW by-v698)
- personality.js: 10-Fragen-Quiz mit 4 Typen (Abenteurer/Entdecker/Kuschler/Denker), Ergebnis-Speicherung in localStorage, Share-Funktion - achievements.py: neue Badge-Kategorie km_lebenswerk (Bronze 100 km bis Platin 5000 km) - settings.js: Lifetime-km-Balken mit Meilenstein-Markierungen bei 100/500/1000/5000 km - app.js + index.html: personality-Seite registriert
This commit is contained in:
parent
a4e97348ed
commit
0fdc32eaf4
5 changed files with 601 additions and 27 deletions
|
|
@ -229,6 +229,7 @@ window.Page_settings = (() => {
|
|||
</div>
|
||||
<div id="settings-streak" style="display:flex;align-items:center;gap:8px;
|
||||
padding:0 var(--space-4) var(--space-3);flex-wrap:wrap"></div>
|
||||
<div id="settings-lifetime-km" style="border-top:1px solid var(--c-border)"></div>
|
||||
</div>
|
||||
|
||||
<!-- Züchter-Profil Slot -->
|
||||
|
|
@ -442,10 +443,88 @@ window.Page_settings = (() => {
|
|||
: `<span style="color:var(--c-text-muted);font-size:var(--text-sm)">🔥 Noch kein Streak — heute aktiv werden!</span>`;
|
||||
}
|
||||
|
||||
// Lifetime-km Balken mit Meilenstein-Markierungen
|
||||
const lifetimeEl = document.getElementById('settings-lifetime-km');
|
||||
if (lifetimeEl) {
|
||||
const km = s.total_km ?? 0;
|
||||
const MILESTONES = [
|
||||
{ km: 100, label: '100', badge: '100-km-Club', color: '#cd7f32' },
|
||||
{ km: 500, label: '500', badge: '500-km-Wanderer', color: '#94a3b8' },
|
||||
{ km: 1000, label: '1k', badge: 'Tausend-km-Held', color: '#f59e0b' },
|
||||
{ km: 5000, label: '5k', badge: 'Ultraläufer', color: '#cbd5e1' },
|
||||
];
|
||||
const maxKm = 5000;
|
||||
const pct = Math.min(km / maxKm * 100, 100);
|
||||
const nextM = MILESTONES.find(m => km < m.km);
|
||||
const reachedM = MILESTONES.filter(m => km >= m.km);
|
||||
const lastBadge = reachedM.length ? reachedM[reachedM.length - 1] : null;
|
||||
|
||||
const markers = MILESTONES.map(m => {
|
||||
const pos = (m.km / maxKm * 100).toFixed(1);
|
||||
const reached = km >= m.km;
|
||||
return `<div title="${m.badge}" style="position:absolute;left:${pos}%;top:-4px;transform:translateX(-50%);
|
||||
width:12px;height:12px;border-radius:50%;border:2px solid ${m.color};
|
||||
background:${reached ? m.color : 'var(--c-bg)'};z-index:2">
|
||||
</div>
|
||||
<div style="position:absolute;left:${pos}%;top:12px;transform:translateX(-50%);
|
||||
font-size:9px;color:${reached ? m.color : 'var(--c-text-muted)'};font-weight:600;
|
||||
white-space:nowrap">${m.label}</div>`;
|
||||
}).join('');
|
||||
|
||||
lifetimeEl.innerHTML = `
|
||||
<div style="padding:var(--space-3) var(--space-4) 0;
|
||||
display:flex;justify-content:space-between;align-items:center">
|
||||
<span style="font-size:var(--text-xs);font-weight:600;color:var(--c-text-secondary);
|
||||
text-transform:uppercase;letter-spacing:.05em">🐾 Lebenswerk-km</span>
|
||||
<span style="font-size:var(--text-sm);font-weight:700;color:var(--c-primary)">${km} km</span>
|
||||
</div>
|
||||
<div style="padding:var(--space-3) var(--space-4) var(--space-4)">
|
||||
<div style="position:relative;height:8px;background:var(--c-border);border-radius:4px;
|
||||
overflow:visible;margin-bottom:22px">
|
||||
<div style="position:absolute;left:0;top:0;height:100%;width:${pct}%;
|
||||
background:linear-gradient(90deg,#10b981,#0ea5e9);
|
||||
border-radius:4px;z-index:1;transition:width .6s"></div>
|
||||
${markers}
|
||||
</div>
|
||||
${nextM
|
||||
? `<div style="font-size:11px;color:var(--c-text-muted)">
|
||||
Noch <strong>${(nextM.km - km).toLocaleString('de-DE')} km</strong>
|
||||
bis <span style="color:${nextM.color};font-weight:600">${nextM.badge}</span>
|
||||
</div>`
|
||||
: `<div style="font-size:11px;color:var(--c-primary);font-weight:600">
|
||||
Ultraläufer-Legende erreicht! 🏆
|
||||
</div>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (badgesEl && a.categories) {
|
||||
// SVG-Schild für jede Kategorie
|
||||
const shield = (color, dark, emoji, opacity = 1) => `
|
||||
<svg viewBox="0 0 60 72" xmlns="http://www.w3.org/2000/svg"
|
||||
// Foto-Hintergründe für bestimmte Badge-Kategorien
|
||||
const _BADGE_PHOTOS = {
|
||||
'schnee_held': '/img/banyaro/winter_schnee.webp',
|
||||
'jahreszeiten': '/img/banyaro/herbst_bach.webp',
|
||||
'wetter_tapfer': '/img/banyaro/fruehling_playdate.webp',
|
||||
};
|
||||
|
||||
// SVG-Schild für jede Kategorie (mit optionalem Foto-Hintergrund)
|
||||
const shield = (color, dark, emoji, opacity = 1, catId = '') => {
|
||||
const photo = _BADGE_PHOTOS[catId];
|
||||
const clipId = `clip_${catId || Math.random().toString(36).slice(2)}`;
|
||||
const path = 'M30 3 L57 15 L57 38 Q57 60 30 70 Q3 60 3 38 L3 15 Z';
|
||||
if (photo && opacity === 1) {
|
||||
return `<svg viewBox="0 0 60 72" xmlns="http://www.w3.org/2000/svg"
|
||||
style="width:56px;height:56px;filter:drop-shadow(0 2px 8px rgba(0,0,0,.4))">
|
||||
<defs>
|
||||
<clipPath id="${clipId}"><path d="${path}"/></clipPath>
|
||||
</defs>
|
||||
<image href="${photo}" x="0" y="0" width="60" height="72"
|
||||
clip-path="url(#${clipId})" preserveAspectRatio="xMidYMid slice"/>
|
||||
<path d="${path}" fill="rgba(0,0,0,0.28)"/>
|
||||
<path d="${path}" fill="none" stroke="rgba(255,255,255,.4)" stroke-width="1.5"/>
|
||||
<text x="30" y="43" text-anchor="middle" dominant-baseline="middle"
|
||||
font-size="22" style="user-select:none">${emoji}</text>
|
||||
</svg>`;
|
||||
}
|
||||
return `<svg viewBox="0 0 60 72" xmlns="http://www.w3.org/2000/svg"
|
||||
style="width:56px;height:56px;filter:drop-shadow(0 2px 6px ${color}66)">
|
||||
<defs>
|
||||
<linearGradient id="g_${color.replace('#','')}" x1="0" y1="0" x2="1" y2="1">
|
||||
|
|
@ -453,13 +532,12 @@ window.Page_settings = (() => {
|
|||
<stop offset="100%" stop-color="${dark}"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M30 3 L57 15 L57 38 Q57 60 30 70 Q3 60 3 38 L3 15 Z"
|
||||
fill="url(#g_${color.replace('#','')})" opacity="${opacity}"/>
|
||||
<path d="M30 3 L57 15 L57 38 Q57 60 30 70 Q3 60 3 38 L3 15 Z"
|
||||
fill="none" stroke="rgba(255,255,255,.25)" stroke-width="1.5"/>
|
||||
<path d="${path}" fill="url(#g_${color.replace('#','')})" opacity="${opacity}"/>
|
||||
<path d="${path}" fill="none" stroke="rgba(255,255,255,.25)" stroke-width="1.5"/>
|
||||
<text x="30" y="43" text-anchor="middle" dominant-baseline="middle"
|
||||
font-size="22" style="user-select:none">${emoji}</text>
|
||||
</svg>`;
|
||||
};
|
||||
|
||||
badgesEl.innerHTML = (a.categories || []).map(cat => {
|
||||
const cur = cat.current_tier;
|
||||
|
|
@ -474,8 +552,8 @@ window.Page_settings = (() => {
|
|||
|
||||
// Aktuelles Schild
|
||||
const shieldSvg = cur
|
||||
? shield(cur.color, cur.dark, cat.emoji)
|
||||
: shield('#9ca3af', '#6b7280', cat.emoji, 0.5);
|
||||
? shield(cur.color, cur.dark, cat.emoji, 1, cat.id)
|
||||
: shield('#9ca3af', '#6b7280', cat.emoji, 0.5, cat.id);
|
||||
|
||||
// Fortschrittsbalken
|
||||
const progressBar = nxt ? `
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue