Feature: Welten-Onboarding, Wetter-Motivation, UX-Fixes (SW by-v715)
Welten (worlds.js): - Swipe-Hints beim ersten Öffnen (JETZT ← → WELT animiert, einmalig) - Kein-Hund-Onboarding: Feature-Preview-Grid statt leerer Karte - Hintergrund-Foto-Hint: Kamera-Karte wenn noch kein Tagebuchfoto - worlds-back: navigiert zu Welcome wenn kein User eingeloggt - Nach Logout: worlds-back Button sofort ausgeblendet Wetter (wetter.js): - Standort-Fehlerseite zu Motivations-Seite umgebaut - Feature-Preview: Gassi-Score, 7-Tage, Regenradar, Rekorde - CTA: Standort freigeben + Registrieren (nur für Gäste) Settings (settings.js): - Logo in Auth-Form: display:block + margin:0 auto zentriert - Header bleibt sichtbar (FAB/Zurück-Navigation funktioniert) Jobs (jobs.js): - 2-Spalten-Grid auf Mobile: auto-fit statt festes 1fr 1fr - Kein doppeltes Padding im Wrapper Backend: - weather.py, achievements.py: diary JOIN fix (d.user_id → dogs JOIN) - Neue Wetter-Badges: wetter_tapfer, jahreszeiten, schnee - Ernährungs-, Reise-, Ausgaben-Seite: diverse UX-Verbesserungen - Presse-Seite erweitert - Ban Yaro Foto-Assets (WebP + HIRES JPG)
This commit is contained in:
parent
aa4849d947
commit
55069d246b
28 changed files with 719 additions and 198 deletions
|
|
@ -12,7 +12,6 @@ window.Page_reise = (() => {
|
|||
const TABS = [
|
||||
{ key: 'checkliste', label: 'Checkliste', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#check-square"></use></svg>' },
|
||||
{ key: 'laender', label: 'EU-Länder', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#globe"></use></svg>' },
|
||||
{ key: 'notfall', label: 'Notfälle', icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>' },
|
||||
];
|
||||
|
||||
const CHECKLIST = [
|
||||
|
|
@ -94,7 +93,15 @@ window.Page_reise = (() => {
|
|||
{ icon: 'heartbeat', text: 'Bewusstlosigkeit: Atemwege freihalten, stabile Seitenlage, 112 rufen' },
|
||||
];
|
||||
|
||||
const LS_KEY = 'banyaro_reise_checkliste';
|
||||
const LS_KEY = 'banyaro_reise_checkliste';
|
||||
const LS_CUSTOM_KEY = 'banyaro_reise_custom'; // {catKey: ["custom item",...]}
|
||||
const LS_HIDDEN_KEY = 'banyaro_reise_hidden'; // {itemKey: true} — gelöschte Standard-Items
|
||||
let _editMode = false;
|
||||
|
||||
function _loadCustom() { try { return JSON.parse(localStorage.getItem(LS_CUSTOM_KEY) || '{}'); } catch { return {}; } }
|
||||
function _saveCustom(d) { try { localStorage.setItem(LS_CUSTOM_KEY, JSON.stringify(d)); } catch {} }
|
||||
function _loadHidden() { try { return JSON.parse(localStorage.getItem(LS_HIDDEN_KEY) || '{}'); } catch { return {}; } }
|
||||
function _saveHidden(d) { try { localStorage.setItem(LS_HIDDEN_KEY, JSON.stringify(d)); } catch {} }
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Helpers
|
||||
|
|
@ -177,37 +184,79 @@ window.Page_reise = (() => {
|
|||
// ------------------------------------------------------------------
|
||||
function _renderCheckliste(el) {
|
||||
const checked = _loadChecked();
|
||||
const custom = _loadCustom();
|
||||
const hidden = _loadHidden();
|
||||
|
||||
const totalItems = CHECKLIST.reduce((s, c) => s + c.items.length, 0);
|
||||
const doneItems = Object.values(checked).filter(Boolean).length;
|
||||
const pct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
|
||||
|
||||
const progressBar = `
|
||||
<div style="margin-bottom:var(--space-5)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;
|
||||
margin-bottom:var(--space-2)">
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||
${doneItems} von ${totalItems} erledigt
|
||||
</span>
|
||||
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
||||
color:var(--c-primary)">${pct}%</span>
|
||||
</div>
|
||||
<div style="height:8px;background:var(--c-surface-2);border-radius:var(--radius-full);overflow:hidden">
|
||||
<div style="height:100%;width:${pct}%;background:var(--c-primary);
|
||||
border-radius:var(--radius-full);transition:width .3s ease"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
// Alle sichtbaren Items zählen
|
||||
let totalItems = 0, doneItems = 0;
|
||||
CHECKLIST.forEach(cat => {
|
||||
cat.items.forEach((_, idx) => {
|
||||
if (!hidden[_itemKey(cat.key, idx)]) {
|
||||
totalItems++;
|
||||
if (checked[_itemKey(cat.key, idx)]) doneItems++;
|
||||
}
|
||||
});
|
||||
(custom[cat.key] || []).forEach((_, i) => {
|
||||
totalItems++;
|
||||
if (checked[`${cat.key}__custom__${i}`]) doneItems++;
|
||||
});
|
||||
});
|
||||
const pct = totalItems > 0 ? Math.round((doneItems / totalItems) * 100) : 0;
|
||||
|
||||
const cats = CHECKLIST.map(cat => {
|
||||
const rows = cat.items.map((item, idx) => {
|
||||
const customItems = custom[cat.key] || [];
|
||||
|
||||
const stdRows = cat.items.map((item, idx) => {
|
||||
if (hidden[_itemKey(cat.key, idx)]) return '';
|
||||
const key = _itemKey(cat.key, idx);
|
||||
const done = !!checked[key];
|
||||
return `<label class="reise-check-row${done ? ' done' : ''}" data-key="${_esc(key)}">
|
||||
if (_editMode) {
|
||||
return `<div class="reise-check-row" style="justify-content:space-between">
|
||||
<span style="flex:1;color:var(--c-text)">${_esc(item)}</span>
|
||||
<button class="reise-del-btn" data-hide="${_esc(key)}"
|
||||
style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
return `<label class="reise-check-row${done ? ' done' : ''}">
|
||||
<input type="checkbox" class="reise-cb" data-key="${_esc(key)}" ${done ? 'checked' : ''}>
|
||||
<span>${_esc(item)}</span>
|
||||
</label>`;
|
||||
}).join('');
|
||||
|
||||
const customRows = customItems.map((item, i) => {
|
||||
const key = `${cat.key}__custom__${i}`;
|
||||
const done = !!checked[key];
|
||||
if (_editMode) {
|
||||
return `<div class="reise-check-row" style="justify-content:space-between">
|
||||
<span style="flex:1;color:var(--c-primary)">${_esc(item)}</span>
|
||||
<button class="reise-del-custom-btn" data-cat="${_esc(cat.key)}" data-idx="${i}"
|
||||
style="background:none;border:none;color:#EF4444;cursor:pointer;padding:4px;flex-shrink:0">
|
||||
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#trash"></use></svg>
|
||||
</button>
|
||||
</div>`;
|
||||
}
|
||||
return `<label class="reise-check-row${done ? ' done' : ''}">
|
||||
<input type="checkbox" class="reise-cb" data-key="${_esc(key)}" ${done ? 'checked' : ''}>
|
||||
<span style="color:var(--c-primary)">${_esc(item)}</span>
|
||||
</label>`;
|
||||
}).join('');
|
||||
|
||||
const addRow = _editMode ? `
|
||||
<div style="padding:var(--space-2) 0;border-top:1px dashed var(--c-border);margin-top:4px">
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<input class="reise-add-input" data-cat="${_esc(cat.key)}"
|
||||
style="flex:1;padding:8px 10px;border-radius:8px;border:1px solid var(--c-border);
|
||||
background:var(--c-bg-card);color:var(--c-text);font-size:var(--text-sm)"
|
||||
placeholder="Eigenes Item hinzufügen…">
|
||||
<button class="reise-add-btn btn btn-primary" data-cat="${_esc(cat.key)}"
|
||||
style="padding:8px 12px;flex-shrink:0;font-size:var(--text-sm)">
|
||||
<svg class="ph-icon" style="width:1rem;height:1rem"><use href="/icons/phosphor.svg#plus"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>` : '';
|
||||
|
||||
return `<div class="card" style="margin-bottom:var(--space-4)">
|
||||
<div style="padding:var(--space-3) var(--space-4);border-bottom:1px solid var(--c-border);
|
||||
display:flex;align-items:center;gap:var(--space-2)">
|
||||
|
|
@ -217,20 +266,12 @@ window.Page_reise = (() => {
|
|||
<span style="font-weight:var(--weight-semibold)">${_esc(cat.label)}</span>
|
||||
</div>
|
||||
<div style="padding:var(--space-2) var(--space-4)">
|
||||
${rows}
|
||||
${stdRows}${customRows}${addRow}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
el.innerHTML = `
|
||||
${progressBar}
|
||||
${cats}
|
||||
<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
<button class="btn btn-secondary" id="reise-reset-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#arrow-counter-clockwise"></use></svg>
|
||||
Alle zurücksetzen
|
||||
</button>
|
||||
</div>
|
||||
<style>
|
||||
.reise-check-row {
|
||||
display:flex;align-items:flex-start;gap:var(--space-3);
|
||||
|
|
@ -245,6 +286,32 @@ window.Page_reise = (() => {
|
|||
accent-color:var(--c-primary);cursor:pointer;
|
||||
}
|
||||
</style>
|
||||
<!-- Fortschritt + Buttons -->
|
||||
<div style="margin-bottom:var(--space-5)">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:var(--space-2)">
|
||||
<span style="font-size:var(--text-sm);color:var(--c-text-secondary)">${doneItems} von ${totalItems} erledigt</span>
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<span style="font-size:var(--text-sm);font-weight:700;color:var(--c-primary)">${pct}%</span>
|
||||
<button id="reise-edit-toggle" style="background:${_editMode ? 'var(--c-primary)' : 'var(--c-bg-card)'};
|
||||
color:${_editMode ? '#fff' : 'var(--c-text-secondary)'};border:1.5px solid var(--c-border);
|
||||
border-radius:8px;padding:5px 10px;cursor:pointer;font-size:var(--text-xs);font-weight:600;
|
||||
display:flex;align-items:center;gap:4px">
|
||||
<svg class="ph-icon" style="width:.9rem;height:.9rem"><use href="/icons/phosphor.svg#pencil-simple"></use></svg>
|
||||
${_editMode ? 'Fertig' : 'Bearbeiten'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height:8px;background:var(--c-surface-2);border-radius:var(--radius-full);overflow:hidden">
|
||||
<div style="height:100%;width:${pct}%;background:var(--c-primary);border-radius:var(--radius-full);transition:width .3s"></div>
|
||||
</div>
|
||||
</div>
|
||||
${cats}
|
||||
${!_editMode ? `<div style="text-align:center;margin-bottom:var(--space-6)">
|
||||
<button class="btn btn-secondary" id="reise-reset-btn">
|
||||
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#arrow-counter-clockwise"></use></svg>
|
||||
Abhaken zurücksetzen
|
||||
</button>
|
||||
</div>` : ''}
|
||||
`;
|
||||
|
||||
// Checkbox events
|
||||
|
|
@ -254,7 +321,55 @@ window.Page_reise = (() => {
|
|||
const cur = _loadChecked();
|
||||
cur[key] = cb.checked;
|
||||
_saveChecked(cur);
|
||||
_renderTabContent(); // re-render to update progress
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Edit-Toggle
|
||||
el.querySelector('#reise-edit-toggle')?.addEventListener('click', () => {
|
||||
_editMode = !_editMode;
|
||||
_renderTabContent();
|
||||
});
|
||||
|
||||
// Standard-Item löschen (verstecken)
|
||||
el.querySelectorAll('.reise-del-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const h = _loadHidden();
|
||||
h[btn.dataset.hide] = true;
|
||||
_saveHidden(h);
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Custom-Item löschen
|
||||
el.querySelectorAll('.reise-del-custom-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const c = _loadCustom();
|
||||
c[btn.dataset.cat] = (c[btn.dataset.cat] || []).filter((_, i) => i !== parseInt(btn.dataset.idx));
|
||||
_saveCustom(c);
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Custom-Item hinzufügen
|
||||
el.querySelectorAll('.reise-add-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const cat = btn.dataset.cat;
|
||||
const input = el.querySelector(`.reise-add-input[data-cat="${cat}"]`);
|
||||
const val = (input?.value || '').trim();
|
||||
if (!val) return;
|
||||
const c = _loadCustom();
|
||||
if (!c[cat]) c[cat] = [];
|
||||
c[cat].push(val);
|
||||
_saveCustom(c);
|
||||
_renderTabContent();
|
||||
});
|
||||
});
|
||||
|
||||
// Enter in Add-Input
|
||||
el.querySelectorAll('.reise-add-input').forEach(input => {
|
||||
input.addEventListener('keydown', e => {
|
||||
if (e.key === 'Enter') el.querySelector(`.reise-add-btn[data-cat="${input.dataset.cat}"]`)?.click();
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue