Sprint 19: Social, UX-Verbesserungen, Nerd2Noob-Hilfe
This commit is contained in:
parent
10d30bf565
commit
89d87030a2
18 changed files with 930 additions and 74 deletions
|
|
@ -39,6 +39,7 @@ window.Page_events = (() => {
|
|||
let _map = null;
|
||||
let _markers = [];
|
||||
let _clusterGroup = null;
|
||||
let _myRsvp = {}; // { [event_id]: 'going'|'maybe'|null }
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// Phosphor-Icon-Helper
|
||||
|
|
@ -47,6 +48,17 @@ window.Page_events = (() => {
|
|||
return `<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#${name}"></use></svg>`;
|
||||
}
|
||||
|
||||
function _emptyState(icon, title, text, cta = '') {
|
||||
return `<div class="empty-state">
|
||||
<svg class="ph-icon empty-state-icon" aria-hidden="true">
|
||||
<use href="/icons/phosphor.svg#${icon}"></use>
|
||||
</svg>
|
||||
<div class="empty-state-title">${title}</div>
|
||||
${text ? `<p class="empty-state-text">${text}</p>` : ''}
|
||||
${cta ? `<div class="empty-state-cta">${cta}</div>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// init
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -133,7 +145,11 @@ window.Page_events = (() => {
|
|||
|
||||
const filtered = _filtered();
|
||||
if (!filtered.length) {
|
||||
listEl.innerHTML = UI.emptyState({ icon: '🎪', title: 'Keine Events', text: 'Noch keine Veranstaltungen geplant.' });
|
||||
listEl.innerHTML = _emptyState(
|
||||
'calendar-blank',
|
||||
'Keine Events in der Nähe',
|
||||
'Hier erscheinen Hundeveranstaltungen, Treffen und Aktivitäten in deiner Umgebung.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -185,6 +201,7 @@ window.Page_events = (() => {
|
|||
${ev.uhrzeit ? `· ${_icon('clock')} ${ev.uhrzeit} Uhr` : ''}
|
||||
${ev.ort_name ? `· ${_icon('map-pin')} ${UI.escHtml(ev.ort_name)}` : ''}
|
||||
</div>
|
||||
${ev.rsvp_count ? `<span class="event-attendees" data-ev-attendees="${ev.id}">${_icon('users')} ${ev.rsvp_count} nehmen teil</span>` : ''}
|
||||
${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
|
||||
|
|
@ -326,12 +343,26 @@ window.Page_events = (() => {
|
|||
let ev;
|
||||
try { ev = await API.events.get(id); } catch { return; }
|
||||
|
||||
const typ = TYPEN.find(t => t.id === ev.typ) || TYPEN[TYPEN.length - 1];
|
||||
const color = TYP_COLOR[ev.typ] || '#6b7280';
|
||||
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 typ = TYPEN.find(t => t.id === ev.typ) || TYPEN[TYPEN.length - 1];
|
||||
const color = TYP_COLOR[ev.typ] || '#6b7280';
|
||||
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 myRsvp = _myRsvp[id] ?? null;
|
||||
|
||||
// RSVP-Bar (nur für eingeloggte User)
|
||||
const rsvpBar = _state.user ? `
|
||||
<div class="event-rsvp-bar" id="ev-rsvp-bar-${id}">
|
||||
<button class="btn event-rsvp-btn ${myRsvp === 'going' ? 'active' : ''}" data-rsvp-id="${id}" data-rsvp-status="going">
|
||||
${_icon('check-circle')} Ich komme
|
||||
</button>
|
||||
<button class="btn event-rsvp-btn ${myRsvp === 'maybe' ? 'active' : ''}" data-rsvp-id="${id}" data-rsvp-status="maybe">
|
||||
${_icon('question')} Vielleicht
|
||||
</button>
|
||||
${ev.rsvp_count ? `<span class="event-attendees" id="ev-attendees-${id}" data-ev-attendees="${id}" style="margin-left:auto">${_icon('users')} ${ev.rsvp_count} nehmen teil</span>` : `<span class="event-attendees" id="ev-attendees-${id}" data-ev-attendees="${id}" style="margin-left:auto;display:none">${_icon('users')} 0 nehmen teil</span>`}
|
||||
</div>
|
||||
` : (ev.rsvp_count ? `<div class="event-rsvp-bar"><span class="event-attendees" data-ev-attendees="${id}">${_icon('users')} ${ev.rsvp_count} nehmen teil</span></div>` : '');
|
||||
|
||||
const body = `
|
||||
<div style="display:flex;align-items:center;gap:var(--space-2);margin-bottom:var(--space-3)">
|
||||
|
|
@ -348,6 +379,8 @@ window.Page_events = (() => {
|
|||
<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>
|
||||
${rsvpBar}
|
||||
<div id="ev-attendees-panel-${id}"></div>
|
||||
`;
|
||||
|
||||
const footer = isOwn ? `
|
||||
|
|
@ -365,6 +398,80 @@ window.Page_events = (() => {
|
|||
UI.modal.close(); setTimeout(() => _openForm(ev), 50);
|
||||
});
|
||||
document.getElementById('ev-detail-del')?.addEventListener('click', () => _deleteEvent(ev));
|
||||
|
||||
// RSVP-Buttons
|
||||
document.querySelectorAll(`[data-rsvp-id="${id}"]`).forEach(btn => {
|
||||
btn.addEventListener('click', () => _handleRsvp(id, btn.dataset.rsvpStatus));
|
||||
});
|
||||
}
|
||||
|
||||
async function _handleRsvp(eventId, status) {
|
||||
const current = _myRsvp[eventId] ?? null;
|
||||
try {
|
||||
if (current === status) {
|
||||
// Toggle off → absagen
|
||||
await API.events.cancelRsvp(eventId);
|
||||
_myRsvp[eventId] = null;
|
||||
} else {
|
||||
const res = await API.events.rsvp(eventId, status);
|
||||
_myRsvp[eventId] = status;
|
||||
// Teilnehmerzähler aktualisieren
|
||||
_updateAttendeeCount(eventId, res.rsvp_count);
|
||||
}
|
||||
// Button-Styles aktualisieren
|
||||
document.querySelectorAll(`[data-rsvp-id="${eventId}"]`).forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.rsvpStatus === (_myRsvp[eventId] ?? ''));
|
||||
});
|
||||
// Bei Absage Zähler neu laden
|
||||
if (current === status) {
|
||||
const attendees = await API.events.listRsvp(eventId);
|
||||
const goingCount = attendees.filter(a => a.status === 'going').length;
|
||||
_updateAttendeeCount(eventId, goingCount);
|
||||
}
|
||||
} catch (e) { UI.toast(e.message, 'error'); }
|
||||
}
|
||||
|
||||
function _updateAttendeeCount(eventId, count) {
|
||||
// Im Modal
|
||||
const span = document.getElementById(`ev-attendees-${eventId}`);
|
||||
if (span) {
|
||||
if (count > 0) {
|
||||
span.innerHTML = `${_icon('users')} ${count} nehmen teil`;
|
||||
span.style.display = '';
|
||||
} else {
|
||||
span.style.display = 'none';
|
||||
}
|
||||
}
|
||||
// In der Listenansicht (Event-Objekt aktualisieren)
|
||||
const ev = _events.find(x => x.id === eventId);
|
||||
if (ev) {
|
||||
ev.rsvp_count = count;
|
||||
// Karte neu rendern falls sichtbar
|
||||
const card = document.querySelector(`[data-ev-id="${eventId}"]`);
|
||||
if (card) card.outerHTML = _cardHTML(ev);
|
||||
}
|
||||
}
|
||||
|
||||
async function _showAttendees(eventId) {
|
||||
const panel = document.getElementById(`ev-attendees-panel-${eventId}`);
|
||||
if (!panel) return;
|
||||
if (panel.dataset.loaded) { panel.innerHTML = ''; delete panel.dataset.loaded; return; }
|
||||
try {
|
||||
const attendees = await API.events.listRsvp(eventId);
|
||||
if (!attendees.length) { panel.innerHTML = '<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-2)">Noch keine Zusagen.</p>'; }
|
||||
else {
|
||||
panel.innerHTML = `
|
||||
<div class="ev-attendees-list">
|
||||
${attendees.map(a => `
|
||||
<span class="ev-attendee-chip">
|
||||
${a.status === 'going' ? _icon('check-circle') : _icon('question')}
|
||||
${UI.escHtml(a.name)}
|
||||
</span>
|
||||
`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
panel.dataset.loaded = '1';
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
async function _deleteEvent(ev) {
|
||||
|
|
@ -548,6 +655,14 @@ window.Page_events = (() => {
|
|||
return;
|
||||
}
|
||||
|
||||
// Teilnehmer-Liste anzeigen (Karten-Ansicht oder Modal)
|
||||
const attendeesBtn = e.target.closest('[data-ev-attendees]');
|
||||
if (attendeesBtn) {
|
||||
e.stopPropagation();
|
||||
_showAttendees(parseInt(attendeesBtn.dataset.evAttendees));
|
||||
return;
|
||||
}
|
||||
|
||||
// Karten-Klick → Detail
|
||||
const card = e.target.closest('[data-ev-id]');
|
||||
if (card) { _showDetail(parseInt(card.dataset.evId)); }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue