Sprint 11: Freunde & Chat + Phosphor-Icon-Vollmigration

- Freundschaften (pending/accepted), Nutzersuche, Anfragen per Push
- Direktnachrichten mit Polling, iMessage-Stil, Deep-Links aus Push
- Alle Seiten (map, places, diary, health, dog-profile, sitting, knigge,
  forum, wiki, walks) vollständig auf Phosphor-Icons migriert
- Wikidata-Rassen-Scraper (~833 neue Rassen, lokal gespiegelte Fotos)
- TheDogAPI lokal gespiegelt (169 Rassen + Fotos)
- Quiz-Result-Cards horizontal (korrekte Bildproportionen)
- SW by-v89
This commit is contained in:
rene 2026-04-15 21:33:53 +02:00
parent 96bd57f0ad
commit 097295c628
44 changed files with 9980 additions and 300 deletions

View file

@ -34,10 +34,18 @@ window.Page_events = (() => {
let _state = null;
let _events = [];
let _filter = 'alle';
let _quellFilter = 'alle'; // 'alle' | 'vdh' | 'nutzer'
let _view = 'liste'; // liste | karte
let _map = null;
let _markers = [];
// ----------------------------------------------------------
// Phosphor-Icon-Helper
// ----------------------------------------------------------
function _icon(name) {
return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
}
// ----------------------------------------------------------
// init
// ----------------------------------------------------------
@ -59,11 +67,11 @@ window.Page_events = (() => {
_container.innerHTML = `
<div class="events-toolbar">
<div class="events-view-toggle">
<button class="events-view-btn active" data-ev-view="liste"> Liste</button>
<button class="events-view-btn" data-ev-view="karte">🗺 Karte</button>
<button class="events-view-btn active" data-ev-view="liste">${UI.icon('list')} Liste</button>
<button class="events-view-btn" data-ev-view="karte">${UI.icon('map-trifold')} Karte</button>
</div>
<div style="flex:1"></div>
${_state.user ? `<button class="btn btn-primary btn-sm" id="ev-new-btn">+ Event</button>` : ''}
${_state.user ? `<button class="btn btn-primary btn-sm" id="ev-new-btn">${UI.icon('plus')} Event</button>` : ''}
</div>
<div class="events-filter-bar" id="ev-filter-bar">
@ -74,6 +82,14 @@ window.Page_events = (() => {
`).join('')}
</div>
<div class="events-source-bar" id="ev-source-bar">
<button class="events-source-btn active" data-ev-quelle="alle">Alle Quellen</button>
<button class="events-source-btn events-source-vdh" data-ev-quelle="vdh">
<span class="ev-vdh-badge">VDH</span> VDH-Events
</button>
<button class="events-source-btn" data-ev-quelle="nutzer">Von Nutzern</button>
</div>
<div class="events-list" id="ev-list"></div>
<div class="events-map" id="ev-map" style="display:none"></div>
`;
@ -96,6 +112,17 @@ window.Page_events = (() => {
}
}
// ----------------------------------------------------------
// Gefilterte Events ermitteln
// ----------------------------------------------------------
function _filtered() {
let evs = _filter === 'alle' ? _events : _events.filter(e => e.typ === _filter);
if (_quellFilter !== 'alle') {
evs = evs.filter(e => (e.quelle || 'nutzer') === _quellFilter);
}
return evs;
}
// ----------------------------------------------------------
// Liste rendern
// ----------------------------------------------------------
@ -103,7 +130,7 @@ window.Page_events = (() => {
const listEl = document.getElementById('ev-list');
if (!listEl) return;
const filtered = _filter === 'alle' ? _events : _events.filter(e => e.typ === _filter);
const filtered = _filtered();
if (!filtered.length) {
listEl.innerHTML = UI.emptyState({ icon: '🎪', title: 'Keine Events', text: 'Noch keine Veranstaltungen geplant.' });
return;
@ -140,6 +167,8 @@ window.Page_events = (() => {
const typ = TYPEN.find(t => t.id === ev.typ) || TYPEN[TYPEN.length - 1];
const color = TYP_COLOR[ev.typ] || '#6b7280';
const isOwn = _state.user?.id === ev.user_id;
const isVdh = ev.quelle === 'vdh';
return `
<div class="events-card" data-ev-id="${ev.id}" style="border-left-color:${color}">
<div class="events-date-badge">
@ -148,14 +177,22 @@ window.Page_events = (() => {
<span class="month">${mon}</span>
</div>
<div class="events-card-body">
<div class="events-card-title">${UI.escHtml(ev.titel)}</div>
<div class="events-card-title">
${UI.escHtml(ev.titel)}
${isVdh ? `<span class="ev-vdh-badge" title="Vom VDH importiert">VDH</span>` : ''}
</div>
<div class="events-card-meta">
<span class="events-badge" style="background:${color}20;color:${color}">${typ.icon} ${typ.label}</span>
${ev.uhrzeit ? `· ${ev.uhrzeit} Uhr` : ''}
${ev.ort_name ? `· 📍 ${UI.escHtml(ev.ort_name)}` : ''}
${ev.uhrzeit ? `· ${_icon('clock')} ${ev.uhrzeit} Uhr` : ''}
${ev.ort_name ? `· ${_icon('map-pin')} ${UI.escHtml(ev.ort_name)}` : ''}
</div>
${ev.link ? `<div class="events-card-actions">
<a class="btn btn-ghost btn-xs ev-ext-link" href="${UI.escHtml(ev.link)}" target="_blank" rel="noopener" onclick="event.stopPropagation()">
${_icon('arrow-square-out')} Details
</a>
</div>` : ''}
</div>
${isOwn ? `<button class="btn-icon" data-ev-edit="${ev.id}" title="Bearbeiten">✏️</button>` : ''}
${isOwn ? `<button class="btn-icon" data-ev-edit="${ev.id}" title="Bearbeiten">${_icon('pencil-simple')}</button>` : ''}
</div>
`;
}
@ -223,22 +260,33 @@ window.Page_events = (() => {
const d = new Date(ev.datum + 'T00:00:00');
const datum = d.toLocaleDateString('de-DE', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
const isOwn = _state.user?.id === ev.user_id;
const isVdh = ev.quelle === 'vdh';
const body = `
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
<span class="events-badge" style="background:${color}20;color:${color};font-size:var(--text-sm)">${typ.icon} ${typ.label}</span>
${isVdh ? `<span class="ev-vdh-badge">VDH</span>` : ''}
</div>
<div class="events-detail-row">📅 ${datum}${ev.uhrzeit ? ' · ' + ev.uhrzeit + ' Uhr' : ''}</div>
${ev.ort_name ? `<div class="events-detail-row">📍 ${UI.escHtml(ev.ort_name)}</div>` : ''}
<div class="events-detail-row">${_icon('calendar-dots')} ${datum}${ev.uhrzeit ? ' · ' + ev.uhrzeit + ' Uhr' : ''}</div>
${ev.ort_name ? `<div class="events-detail-row">${_icon('map-pin')} ${UI.escHtml(ev.ort_name)}</div>` : ''}
${ev.beschreibung ? `<div class="events-detail-desc">${UI.escHtml(ev.beschreibung)}</div>` : ''}
${ev.link ? `<div class="events-detail-row">🔗 <a href="${UI.escHtml(ev.link)}" target="_blank" rel="noopener">Mehr Infos</a></div>` : ''}
<div class="events-detail-row" style="color:var(--c-text-muted);font-size:var(--text-xs)">Veranstalter: ${UI.escHtml(ev.veranstalter_name || '')}</div>
${ev.link ? `<div class="events-detail-row">
${_icon('arrow-square-out')}
<a href="${UI.escHtml(ev.link)}" target="_blank" rel="noopener">Mehr Infos</a>
</div>` : ''}
<div class="events-detail-row" style="color:var(--c-text-muted);font-size:var(--text-xs)">
${_icon('user')} Veranstalter: ${UI.escHtml(ev.veranstalter_name || '')}
</div>
`;
const footer = isOwn ? `
<button class="btn btn-secondary" id="ev-detail-edit"> Bearbeiten</button>
<button class="btn btn-danger" id="ev-detail-del">🗑 Löschen</button>
` : '';
<button class="btn btn-secondary" id="ev-detail-edit">${_icon('pencil-simple')} Bearbeiten</button>
<button class="btn btn-danger" id="ev-detail-del">${_icon('trash')} Löschen</button>
` : (ev.link ? `
<a class="btn btn-primary" href="${UI.escHtml(ev.link)}" target="_blank" rel="noopener">
${_icon('arrow-square-out')} Zur Veranstaltung
</a>
` : '');
UI.modal.open({ title: UI.escHtml(ev.titel), body, footer });
@ -368,7 +416,16 @@ window.Page_events = (() => {
// Click-Handler
// ----------------------------------------------------------
function _onClick(e) {
// Filter
// Quelle-Filter
const sourceBtn = e.target.closest('[data-ev-quelle]');
if (sourceBtn) {
_quellFilter = sourceBtn.dataset.evQuelle;
document.querySelectorAll('[data-ev-quelle]').forEach(b => b.classList.toggle('active', b.dataset.evQuelle === _quellFilter));
_renderList();
return;
}
// Typ-Filter
const filterBtn = e.target.closest('[data-ev-typ]');
if (filterBtn) {
_filter = filterBtn.dataset.evTyp;
@ -387,8 +444,7 @@ window.Page_events = (() => {
if (_view === 'karte') {
listEl.style.display = 'none';
mapEl.style.display = 'block';
const filtered = _filter === 'alle' ? _events : _events.filter(ev => ev.typ === _filter);
_renderMap(filtered);
_renderMap(_filtered());
} else {
listEl.style.display = '';
mapEl.style.display = 'none';
@ -399,6 +455,9 @@ window.Page_events = (() => {
// Neu-Button
if (e.target.closest('#ev-new-btn')) { openNew(); return; }
// Externer Link — nicht als Karten-Klick behandeln
if (e.target.closest('.ev-ext-link')) return;
// Bearbeiten-Icon auf Karte
const editBtn = e.target.closest('[data-ev-edit]');
if (editBtn) {