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:
parent
96bd57f0ad
commit
097295c628
44 changed files with 9980 additions and 300 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue