PHASE 1 — Sofort-Cleanup ohne Risiko: - Neue Datei utilities.css mit ~25 Klassen für häufige Kombinationen: * text-xs-muted, text-xs-secondary, text-sm-muted, text-sm-secondary * flex-gap-2/3, flex-col-gap-2/3/4, flex-center-gap-1/2/3 * flex-between, flex-1-min, mb-1/3, mt-1/3 * icon-xs/sm/md/lg, label-block, caption - index.html bindet utilities.css ein - mb-3/mt-3 ergänzt (waren in design-system.css unvollständig) PHASE 2 — .by-tab Modifier für Vereinheitlichung: - .by-tabs.grid (mit --tab-cols Variable für Admin/Health/etc.) - .by-tabs.sticky (Desktop vertikale Tabs für Admin) - .by-tabs.wrap (Zuchthunde, flex-wrap statt scroll) - .by-tabs.separated (Sitting, mit eigenem Hintergrund + Border) PHASE 3 — Inline-Style → Klassen-Migration (Python-Script): - 948 Inline-Styles entfernt (5101 → 4153, -18%) - 962 Migrationen über 47 Page-Dateien - Top-Treffer: admin.js (180), health.js (67), dog-profile.js (67), litters.js (62), settings.js (61), zuchthunde.js (51) - Patterns: text-muted, text-secondary, text-danger, text-xs-muted, text-sm-muted, grid-2 (Duplikat-Bug behoben!), flex-col-gap-3, p-3/4, mb-2/3/4, hidden, w-full, flex-1, ... - Bewahrt bestehende class-Attribute (mergt korrekt) Alle 19 Tests grün. Kein visueller Diff erwartet (gleiche Property-Werte).
708 lines
28 KiB
JavaScript
708 lines
28 KiB
JavaScript
/* ============================================================
|
|
BAN YARO — Playdate-Matching
|
|
Spielkameraden in der Nähe finden, Inserate verwalten, Anfragen
|
|
============================================================ */
|
|
|
|
window.Page_playdate = (() => {
|
|
|
|
let _container = null;
|
|
let _appState = null;
|
|
let _activeTab = 'nearby'; // 'nearby' | 'listings' | 'requests'
|
|
let _userPos = null;
|
|
let _radius = 10;
|
|
let _dogs = [];
|
|
|
|
// ------------------------------------------------------------------
|
|
// Helpers
|
|
// ------------------------------------------------------------------
|
|
function _esc(s) {
|
|
return String(s || '').replace(/&/g, '&').replace(/</g, '<')
|
|
.replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
|
|
function _fmtDate(iso) {
|
|
if (!iso) return '';
|
|
const d = new Date(iso.replace(' ', 'T'));
|
|
return d.toLocaleDateString('de-DE');
|
|
}
|
|
|
|
function _dogAvatar(foto_url, name, size = 48) {
|
|
const initials = _esc((name || '?').charAt(0).toUpperCase());
|
|
if (foto_url) {
|
|
return `<img src="${_esc(foto_url)}" alt="${initials}"
|
|
style="width:${size}px;height:${size}px;border-radius:50%;object-fit:cover;display:block;"
|
|
onerror="this.outerHTML='<div style=\'width:${size}px;height:${size}px;border-radius:50%;background:var(--c-primary-subtle);display:flex;align-items:center;justify-content:center;font-size:${Math.round(size*0.45)}px;font-weight:700;color:var(--c-primary);\'>${initials}</div>'">`;
|
|
}
|
|
return `<div style="width:${size}px;height:${size}px;border-radius:50%;
|
|
background:var(--c-primary-subtle);display:flex;align-items:center;
|
|
justify-content:center;font-size:${Math.round(size * 0.45)}px;
|
|
font-weight:700;color:var(--c-primary);">${initials}</div>`;
|
|
}
|
|
|
|
function _statusBadge(status) {
|
|
const map = {
|
|
pending: ['warning', 'Ausstehend'],
|
|
accepted: ['success', 'Angenommen'],
|
|
declined: ['danger', 'Abgelehnt'],
|
|
};
|
|
const [type, label] = map[status] || ['default', status];
|
|
const colors = {
|
|
warning: 'var(--c-warning, #f59e0b)',
|
|
success: 'var(--c-success, #10b981)',
|
|
danger: 'var(--c-danger, #ef4444)',
|
|
default: 'var(--c-text-muted)',
|
|
};
|
|
return `<span style="font-size:var(--text-xs);font-weight:600;
|
|
color:${colors[type]};padding:2px 8px;border-radius:999px;
|
|
background:${colors[type]}18">${label}</span>`;
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// INIT
|
|
// ------------------------------------------------------------------
|
|
async function init(container, appState) {
|
|
_container = container;
|
|
_appState = appState;
|
|
_dogs = appState.dogs?.filter(d => !d.is_guest) || [];
|
|
_render();
|
|
_switchTab(_activeTab);
|
|
}
|
|
|
|
function refresh() {
|
|
_dogs = _appState?.dogs?.filter(d => !d.is_guest) || [];
|
|
_switchTab(_activeTab);
|
|
}
|
|
|
|
function onDogChange() {
|
|
_dogs = _appState?.dogs?.filter(d => !d.is_guest) || [];
|
|
if (_activeTab === 'listings') _loadListings();
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// RENDER — Grundstruktur mit Tabs
|
|
// ------------------------------------------------------------------
|
|
function _render() {
|
|
_container.innerHTML = `
|
|
<div class="playdate-layout">
|
|
|
|
<!-- Tabs -->
|
|
<div class="by-tabs" id="playdate-tabs" class="mb-4">
|
|
<button class="by-tab active" data-tab="nearby">In der Nähe</button>
|
|
<button class="by-tab" data-tab="listings">Meine Inserate</button>
|
|
<button class="by-tab" data-tab="requests">
|
|
Anfragen
|
|
<span id="playdate-req-badge" style="display:none;margin-left:4px;
|
|
background:var(--c-primary);color:#fff;border-radius:999px;
|
|
padding:1px 6px;font-size:var(--text-xs);font-weight:700">0</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab-Inhalt -->
|
|
<div id="playdate-content"></div>
|
|
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('playdate-tabs').addEventListener('click', e => {
|
|
const btn = e.target.closest('.by-tab');
|
|
if (!btn) return;
|
|
_switchTab(btn.dataset.tab);
|
|
});
|
|
}
|
|
|
|
function _switchTab(tab) {
|
|
_activeTab = tab;
|
|
document.querySelectorAll('#playdate-tabs .by-tab').forEach(b => {
|
|
b.classList.toggle('active', b.dataset.tab === tab);
|
|
});
|
|
const content = document.getElementById('playdate-content');
|
|
if (!content) return;
|
|
|
|
if (tab === 'nearby') _renderNearby(content);
|
|
if (tab === 'listings') _renderListings(content);
|
|
if (tab === 'requests') _renderRequests(content);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// TAB: IN DER NÄHE
|
|
// ------------------------------------------------------------------
|
|
async function _renderNearby(el) {
|
|
el.innerHTML = `
|
|
<div>
|
|
<!-- Toolbar: Radius-Auswahl + Standort-Button -->
|
|
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-4);flex-wrap:wrap">
|
|
<div style="display:flex;align-items:center;gap:var(--space-2)">
|
|
${UI.icon('map-pin')}
|
|
<span class="text-sm-secondary" id="nearby-location-label">
|
|
${_userPos ? 'Standort bekannt' : 'Kein Standort'}
|
|
</span>
|
|
</div>
|
|
<select id="nearby-radius" class="form-select" style="width:auto;font-size:var(--text-sm)">
|
|
<option value="5" ${_radius===5 ? 'selected' : ''}>5 km</option>
|
|
<option value="10" ${_radius===10 ? 'selected' : ''}>10 km</option>
|
|
<option value="25" ${_radius===25 ? 'selected' : ''}>25 km</option>
|
|
<option value="50" ${_radius===50 ? 'selected' : ''}>50 km</option>
|
|
</select>
|
|
<button class="btn btn-ghost btn-sm" id="nearby-locate-btn">
|
|
${UI.icon('crosshair')} Standort aktualisieren
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Info-Hinweis -->
|
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);
|
|
margin-bottom:var(--space-4);padding:var(--space-2) var(--space-3);
|
|
background:var(--c-surface-2);border-radius:var(--radius-md)">
|
|
${UI.icon('info')} Nur Hunde von Nutzern die sich ebenfalls mit einem Inserat eingetragen haben.
|
|
Dein genauer Standort bleibt privat — es wird nur der Ortsname und die ungefähre Entfernung angezeigt.
|
|
</div>
|
|
|
|
<!-- Ergebnisse -->
|
|
<div id="nearby-results">
|
|
<p style="text-align:center;color:var(--c-text-secondary);padding:var(--space-6)">
|
|
Standort wird ermittelt…
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('nearby-radius').addEventListener('change', e => {
|
|
_radius = parseInt(e.target.value, 10);
|
|
_loadNearby();
|
|
});
|
|
|
|
document.getElementById('nearby-locate-btn').addEventListener('click', async () => {
|
|
const btn = document.getElementById('nearby-locate-btn');
|
|
UI.setLoading(btn, true);
|
|
try {
|
|
_userPos = await API.getLocation();
|
|
const label = document.getElementById('nearby-location-label');
|
|
if (label) label.textContent = 'Standort aktualisiert';
|
|
await _loadNearby();
|
|
} catch {
|
|
UI.toast.error('Standort konnte nicht ermittelt werden.');
|
|
} finally {
|
|
UI.setLoading(btn, false);
|
|
}
|
|
});
|
|
|
|
if (!_userPos) {
|
|
try {
|
|
_userPos = await API.getLocation();
|
|
const label = document.getElementById('nearby-location-label');
|
|
if (label) label.textContent = 'Standort bekannt';
|
|
} catch {
|
|
document.getElementById('nearby-results').innerHTML = `
|
|
<div style="text-align:center;padding:var(--space-8);color:var(--c-text-secondary)">
|
|
${UI.icon('map-pin')}
|
|
<p style="margin:var(--space-3) 0 var(--space-4)">
|
|
Standort konnte nicht automatisch ermittelt werden.<br>
|
|
Klicke auf "Standort aktualisieren".
|
|
</p>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
}
|
|
await _loadNearby();
|
|
}
|
|
|
|
async function _loadNearby() {
|
|
if (!_userPos) return;
|
|
const resultsEl = document.getElementById('nearby-results');
|
|
if (!resultsEl) return;
|
|
resultsEl.innerHTML = `<p style="text-align:center;color:var(--c-text-secondary);padding:var(--space-6)">${UI.icon('spinner')} Suche…</p>`;
|
|
|
|
try {
|
|
const data = await API.get(`/playdate/nearby?lat=${_userPos.lat}&lon=${_userPos.lon}&radius=${_radius}`);
|
|
|
|
if (!data || data.length === 0) {
|
|
resultsEl.innerHTML = UI.emptyState({
|
|
icon: UI.icon('paw-print'),
|
|
title: 'Niemand in der Nähe',
|
|
text: `Aktuell hat sich noch niemand in ${_radius} km Umkreis eingetragen. Trage deinen Hund unter "Meine Inserate" ein!`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
resultsEl.innerHTML = `
|
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:var(--space-3)">
|
|
${data.map(d => _nearbyCard(d)).join('')}
|
|
</div>
|
|
`;
|
|
|
|
resultsEl.querySelectorAll('.playdate-anfrage-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const toDogId = parseInt(btn.dataset.dogId, 10);
|
|
const dogName = btn.dataset.dogName;
|
|
_showRequestModal(toDogId, dogName);
|
|
});
|
|
});
|
|
|
|
} catch (err) {
|
|
resultsEl.innerHTML = `<p style="text-align:center;color:var(--c-danger);padding:var(--space-6)">${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
function _nearbyCard(d) {
|
|
return `
|
|
<div class="card p-4">
|
|
<div style="display:flex;gap:var(--space-3);align-items:flex-start;margin-bottom:var(--space-3)">
|
|
${_dogAvatar(d.foto_url, d.dog_name, 56)}
|
|
<div class="flex-1-min">
|
|
<div style="font-weight:var(--weight-semibold);font-size:var(--text-base);
|
|
color:var(--c-text)">${_esc(d.dog_name)}</div>
|
|
${d.rasse ? `<div class="text-sm-secondary">${_esc(d.rasse)}</div>` : ''}
|
|
${d.alter ? `<div class="text-xs-muted">${_esc(d.alter)}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:var(--space-3);margin-bottom:var(--space-3);flex-wrap:wrap">
|
|
<span style="display:flex;align-items:center;gap:4px;font-size:var(--text-xs);color:var(--c-text-secondary)">
|
|
${UI.icon('map-pin')}
|
|
${d.ort_name ? _esc(d.ort_name) + ' · ' : ''}${d.entfernung_km} km entfernt
|
|
</span>
|
|
${d.geschlecht ? `<span class="text-xs-muted">${_esc(d.geschlecht)}</span>` : ''}
|
|
</div>
|
|
|
|
${d.beschreibung ? `
|
|
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);
|
|
margin:0 0 var(--space-3);line-height:1.5">
|
|
${_esc(d.beschreibung)}
|
|
</p>` : ''}
|
|
|
|
<button class="btn btn-primary btn-sm playdate-anfrage-btn"
|
|
data-dog-id="${d.dog_id}"
|
|
data-dog-name="${_esc(d.dog_name)}">
|
|
${UI.icon('paw-print')} Spielkamerad anfragen
|
|
</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function _showRequestModal(toDogId, dogName) {
|
|
const formId = 'playdate-req-form';
|
|
UI.modal.open({
|
|
title: `Anfrage an ${dogName}`,
|
|
body: `
|
|
<form id="${formId}">
|
|
<div class="form-group">
|
|
<label class="form-label">Nachricht (optional)</label>
|
|
<textarea id="req-nachricht" class="form-control" rows="3" maxlength="500"
|
|
placeholder="Hallo! Unsere Hunde könnten super zusammenpassen…"></textarea>
|
|
</div>
|
|
</form>
|
|
`,
|
|
footer: `
|
|
<button class="btn btn-secondary" id="req-cancel-btn">Abbrechen</button>
|
|
<button class="btn btn-primary" id="req-send-btn" form="${formId}">
|
|
${UI.icon('paper-plane-tilt')} Anfrage senden
|
|
</button>
|
|
`,
|
|
});
|
|
|
|
document.getElementById('req-cancel-btn').addEventListener('click', () => UI.modal.close());
|
|
document.getElementById('req-send-btn').addEventListener('click', async () => {
|
|
const btn = document.getElementById('req-send-btn');
|
|
const nachricht = document.getElementById('req-nachricht').value.trim();
|
|
await UI.asyncButton(btn, async () => {
|
|
const result = await API.post('/playdate/request', {
|
|
to_dog_id: toDogId,
|
|
nachricht: nachricht || null,
|
|
});
|
|
UI.modal.close();
|
|
UI.toast.success('Anfrage gesendet! Ein Chat wurde geöffnet.');
|
|
// Zum Chat navigieren
|
|
if (result.conversation_id) {
|
|
setTimeout(() => {
|
|
App.navigate('chat', true, { conversation_id: result.conversation_id });
|
|
}, 800);
|
|
}
|
|
}, { errorMsg: null });
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// TAB: MEINE INSERATE
|
|
// ------------------------------------------------------------------
|
|
async function _renderListings(el) {
|
|
el.innerHTML = `<p style="text-align:center;color:var(--c-text-secondary);padding:var(--space-6)">${UI.icon('spinner')} Lädt…</p>`;
|
|
await _loadListings(el);
|
|
}
|
|
|
|
async function _loadListings(el) {
|
|
const target = el || document.getElementById('playdate-content');
|
|
if (!target) return;
|
|
|
|
if (_dogs.length === 0) {
|
|
target.innerHTML = UI.emptyState({
|
|
icon: UI.icon('paw-print'),
|
|
title: 'Noch kein Hund',
|
|
text: 'Lege zuerst einen Hund in deinem Profil an.',
|
|
action: `<button class="btn btn-primary" onclick="App.navigate('dog-profile')">Hund anlegen</button>`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Listings für alle eigenen Hunde laden
|
|
const listings = {};
|
|
await Promise.all(_dogs.map(async dog => {
|
|
try {
|
|
const data = await API.get(`/playdate/my-listing/${dog.id}`);
|
|
listings[dog.id] = data;
|
|
} catch {
|
|
listings[dog.id] = null;
|
|
}
|
|
}));
|
|
|
|
target.innerHTML = `
|
|
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
${_dogs.map(dog => _listingCard(dog, listings[dog.id])).join('')}
|
|
</div>
|
|
`;
|
|
|
|
// Event-Delegation für alle Buttons
|
|
target.addEventListener('click', async e => {
|
|
const btn = e.target.closest('button[data-action]');
|
|
if (!btn) return;
|
|
const action = btn.dataset.action;
|
|
const dogId = parseInt(btn.dataset.dogId, 10);
|
|
const dog = _dogs.find(d => d.id === dogId);
|
|
|
|
if (action === 'edit') {
|
|
_showListingModal(dog, listings[dogId], async () => {
|
|
await _loadListings();
|
|
});
|
|
}
|
|
if (action === 'deactivate') {
|
|
if (!window.confirm(`Inserat für ${dog?.name} deaktivieren?`)) return;
|
|
try {
|
|
await API.del(`/playdate/listing/${dogId}`);
|
|
UI.toast.success('Inserat deaktiviert.');
|
|
await _loadListings();
|
|
} catch (err) {
|
|
UI.toast.error(err.message);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function _listingCard(dog, listing) {
|
|
const isAktiv = listing && listing.aktiv;
|
|
return `
|
|
<div class="card p-4">
|
|
<div style="display:flex;gap:var(--space-3);align-items:center;margin-bottom:var(--space-3)">
|
|
${_dogAvatar(dog.foto_url, dog.name, 44)}
|
|
<div class="flex-1-min">
|
|
<div style="font-weight:var(--weight-semibold);color:var(--c-text)">${_esc(dog.name)}</div>
|
|
${dog.rasse ? `<div class="text-xs-secondary">${_esc(dog.rasse)}</div>` : ''}
|
|
</div>
|
|
<span style="font-size:var(--text-xs);font-weight:600;
|
|
padding:2px 10px;border-radius:999px;
|
|
background:${isAktiv ? 'var(--c-success-subtle,#d1fae5)' : 'var(--c-surface-2)'};
|
|
color:${isAktiv ? 'var(--c-success,#10b981)' : 'var(--c-text-muted)'}">
|
|
${isAktiv ? 'Aktiv' : 'Inaktiv'}
|
|
</span>
|
|
</div>
|
|
|
|
${isAktiv ? `
|
|
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);margin-bottom:var(--space-3)">
|
|
${UI.icon('map-pin')}
|
|
${listing.ort_name ? _esc(listing.ort_name) + ' · ' : ''}
|
|
Radius: ${listing.radius_km} km
|
|
</div>
|
|
${listing.beschreibung ? `
|
|
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);
|
|
margin:0 0 var(--space-3);line-height:1.5">${_esc(listing.beschreibung)}</p>` : ''}
|
|
` : `
|
|
<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin:0 0 var(--space-3)">
|
|
Noch kein Inserat — trage dich ein, damit andere dich finden können.
|
|
</p>
|
|
`}
|
|
|
|
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap">
|
|
<button class="btn btn-primary btn-sm"
|
|
data-action="edit" data-dog-id="${dog.id}">
|
|
${UI.icon('pencil')} ${isAktiv ? 'Bearbeiten' : 'Inserat anlegen'}
|
|
</button>
|
|
${isAktiv ? `
|
|
<button class="btn btn-ghost btn-sm"
|
|
data-action="deactivate" data-dog-id="${dog.id}">
|
|
${UI.icon('x')} Deaktivieren
|
|
</button>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function _showListingModal(dog, existing, onSaved) {
|
|
const formId = 'listing-form';
|
|
UI.modal.open({
|
|
title: `Inserat für ${dog.name}`,
|
|
body: `
|
|
<form id="${formId}">
|
|
<div class="form-group">
|
|
<label class="form-label">Ort / Standort</label>
|
|
<div class="flex-gap-2">
|
|
<input type="text" id="listing-ort" class="form-control"
|
|
placeholder="z.B. München"
|
|
value="${_esc(existing?.ort_name || '')}">
|
|
<button type="button" class="btn btn-ghost btn-sm" id="listing-gps-btn"
|
|
title="GPS-Standort ermitteln">
|
|
${UI.icon('crosshair')}
|
|
</button>
|
|
</div>
|
|
<input type="hidden" id="listing-lat" value="${existing?.lat || ''}">
|
|
<input type="hidden" id="listing-lon" value="${existing?.lon || ''}">
|
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted);margin-top:4px">
|
|
Tipp: Klicke auf ${UI.icon('crosshair')} um deinen Ortsnamen automatisch zu ermitteln.
|
|
Nur der Ortsname wird für andere sichtbar — nicht dein genauer Standort.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Suchradius</label>
|
|
<select id="listing-radius" class="form-control">
|
|
<option value="5" ${(existing?.radius_km||10)===5 ? 'selected' : ''}>5 km</option>
|
|
<option value="10" ${(existing?.radius_km||10)===10 ? 'selected' : ''}>10 km</option>
|
|
<option value="25" ${(existing?.radius_km||10)===25 ? 'selected' : ''}>25 km</option>
|
|
<option value="50" ${(existing?.radius_km||10)===50 ? 'selected' : ''}>50 km</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Beschreibung (optional)</label>
|
|
<textarea id="listing-beschreibung" class="form-control" rows="3" maxlength="400"
|
|
placeholder="Erzähl etwas über deinen Hund und was ihr sucht…">${_esc(existing?.beschreibung || '')}</textarea>
|
|
</div>
|
|
</form>
|
|
`,
|
|
footer: `
|
|
<button class="btn btn-secondary" id="listing-cancel-btn">Abbrechen</button>
|
|
<button class="btn btn-primary" id="listing-save-btn">
|
|
${UI.icon('floppy-disk')} Speichern
|
|
</button>
|
|
`,
|
|
});
|
|
|
|
// GPS-Button
|
|
document.getElementById('listing-gps-btn').addEventListener('click', async () => {
|
|
const gpsBtn = document.getElementById('listing-gps-btn');
|
|
UI.setLoading(gpsBtn, true);
|
|
try {
|
|
const pos = await API.getLocation();
|
|
document.getElementById('listing-lat').value = pos.lat;
|
|
document.getElementById('listing-lon').value = pos.lon;
|
|
|
|
// Reverse-Geocoding für Ortsname
|
|
try {
|
|
const rev = await fetch(
|
|
`https://nominatim.openstreetmap.org/reverse?lat=${pos.lat}&lon=${pos.lon}&format=json&zoom=10&accept-language=de`,
|
|
{ cache: 'no-store' }
|
|
);
|
|
const geoData = await rev.json();
|
|
const a = geoData.address || {};
|
|
const ort = a.city || a.town || a.village || a.municipality || '';
|
|
if (ort) document.getElementById('listing-ort').value = ort;
|
|
} catch {}
|
|
UI.toast.success('Standort ermittelt.');
|
|
} catch {
|
|
UI.toast.error('Standort konnte nicht ermittelt werden.');
|
|
} finally {
|
|
UI.setLoading(gpsBtn, false);
|
|
}
|
|
});
|
|
|
|
document.getElementById('listing-cancel-btn').addEventListener('click', () => UI.modal.close());
|
|
|
|
document.getElementById('listing-save-btn').addEventListener('click', async () => {
|
|
const btn = document.getElementById('listing-save-btn');
|
|
const lat = parseFloat(document.getElementById('listing-lat').value);
|
|
const lon = parseFloat(document.getElementById('listing-lon').value);
|
|
const ort = document.getElementById('listing-ort').value.trim();
|
|
const rad = parseInt(document.getElementById('listing-radius').value, 10);
|
|
const desc = document.getElementById('listing-beschreibung').value.trim();
|
|
|
|
if (!lat || !lon) {
|
|
UI.toast.error('Bitte ermittle zuerst deinen Standort über den GPS-Button.');
|
|
return;
|
|
}
|
|
|
|
await UI.asyncButton(btn, async () => {
|
|
await API.put('/playdate/listing', {
|
|
dog_id: dog.id,
|
|
lat,
|
|
lon,
|
|
ort_name: ort || null,
|
|
radius_km: rad,
|
|
beschreibung: desc || null,
|
|
});
|
|
UI.modal.close();
|
|
UI.toast.success('Inserat gespeichert!');
|
|
onSaved?.();
|
|
}, { errorMsg: null });
|
|
});
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// TAB: ANFRAGEN
|
|
// ------------------------------------------------------------------
|
|
async function _renderRequests(el) {
|
|
el.innerHTML = `<p style="text-align:center;color:var(--c-text-secondary);padding:var(--space-6)">${UI.icon('spinner')} Lädt…</p>`;
|
|
try {
|
|
const data = await API.get('/playdate/requests');
|
|
const incoming = data.incoming || [];
|
|
const outgoing = data.outgoing || [];
|
|
|
|
// Badge aktualisieren
|
|
const pendingCount = incoming.filter(r => r.status === 'pending').length;
|
|
const badge = document.getElementById('playdate-req-badge');
|
|
if (badge) {
|
|
badge.textContent = pendingCount;
|
|
badge.style.display = pendingCount > 0 ? '' : 'none';
|
|
}
|
|
|
|
if (incoming.length === 0 && outgoing.length === 0) {
|
|
el.innerHTML = UI.emptyState({
|
|
icon: UI.icon('paw-print'),
|
|
title: 'Noch keine Anfragen',
|
|
text: 'Wenn du oder jemand anderes eine Playdate-Anfrage schickt, erscheint sie hier.',
|
|
});
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = `
|
|
<div style="display:flex;flex-direction:column;gap:var(--space-5)">
|
|
|
|
${incoming.length > 0 ? `
|
|
<div>
|
|
<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
|
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:0.05em;
|
|
margin:0 0 var(--space-3)">Eingehende Anfragen</h3>
|
|
<div class="flex-col-gap-3">
|
|
${incoming.map(r => _incomingCard(r)).join('')}
|
|
</div>
|
|
</div>` : ''}
|
|
|
|
${outgoing.length > 0 ? `
|
|
<div>
|
|
<h3 style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
|
color:var(--c-text-muted);text-transform:uppercase;letter-spacing:0.05em;
|
|
margin:0 0 var(--space-3)">Ausgehende Anfragen</h3>
|
|
<div class="flex-col-gap-3">
|
|
${outgoing.map(r => _outgoingCard(r)).join('')}
|
|
</div>
|
|
</div>` : ''}
|
|
|
|
</div>
|
|
`;
|
|
|
|
// Button-Events (Accept/Decline)
|
|
el.querySelectorAll('.req-accept-btn, .req-decline-btn').forEach(btn => {
|
|
btn.addEventListener('click', async () => {
|
|
const reqId = parseInt(btn.dataset.reqId, 10);
|
|
const status = btn.dataset.status;
|
|
await UI.asyncButton(btn, async () => {
|
|
const result = await API.patch(`/playdate/requests/${reqId}`, { status });
|
|
if (status === 'accepted' && result.conversation_id) {
|
|
UI.toast.success('Anfrage angenommen! Chat wurde geöffnet.');
|
|
setTimeout(() => {
|
|
App.navigate('chat', true, { conversation_id: result.conversation_id });
|
|
}, 800);
|
|
} else {
|
|
UI.toast.success(status === 'accepted' ? 'Anfrage angenommen.' : 'Anfrage abgelehnt.');
|
|
}
|
|
await _renderRequests(el);
|
|
}, { errorMsg: null });
|
|
});
|
|
});
|
|
|
|
// Chat-Buttons
|
|
el.querySelectorAll('.req-chat-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
App.navigate('chat', true);
|
|
});
|
|
});
|
|
|
|
} catch (err) {
|
|
el.innerHTML = `<p style="text-align:center;color:var(--c-danger);padding:var(--space-6)">${err.message}</p>`;
|
|
}
|
|
}
|
|
|
|
function _incomingCard(r) {
|
|
const isPending = r.status === 'pending';
|
|
return `
|
|
<div class="card p-4">
|
|
<div style="display:flex;gap:var(--space-3);align-items:flex-start;margin-bottom:var(--space-3)">
|
|
${_dogAvatar(r.from_dog_foto, r.from_dog_name, 44)}
|
|
<div class="flex-1-min">
|
|
<div style="font-weight:var(--weight-semibold);color:var(--c-text)">${_esc(r.from_dog_name)}</div>
|
|
<div class="text-xs-secondary">
|
|
${r.from_dog_rasse ? _esc(r.from_dog_rasse) + ' · ' : ''}
|
|
${r.alter ? _esc(r.alter) + ' · ' : ''}
|
|
von ${_esc(r.from_user_name)}
|
|
</div>
|
|
<div class="text-xs-muted">${_fmtDate(r.created_at)}</div>
|
|
</div>
|
|
${_statusBadge(r.status)}
|
|
</div>
|
|
|
|
${r.nachricht ? `
|
|
<div style="font-size:var(--text-sm);color:var(--c-text-secondary);
|
|
background:var(--c-surface-2);border-radius:var(--radius-md);
|
|
padding:var(--space-2) var(--space-3);margin-bottom:var(--space-3);
|
|
line-height:1.5">
|
|
"${_esc(r.nachricht)}"
|
|
</div>` : ''}
|
|
|
|
${isPending ? `
|
|
<div class="flex-gap-2">
|
|
<button class="btn btn-primary btn-sm req-accept-btn"
|
|
data-req-id="${r.id}" data-status="accepted">
|
|
${UI.icon('check')} Annehmen
|
|
</button>
|
|
<button class="btn btn-ghost btn-sm req-decline-btn"
|
|
data-req-id="${r.id}" data-status="declined">
|
|
${UI.icon('x')} Ablehnen
|
|
</button>
|
|
</div>` : `
|
|
${r.status === 'accepted' ? `
|
|
<button class="btn btn-ghost btn-sm req-chat-btn">
|
|
${UI.icon('chat-circle-dots')} Zum Chat
|
|
</button>` : ''}
|
|
`}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function _outgoingCard(r) {
|
|
return `
|
|
<div class="card p-4">
|
|
<div style="display:flex;gap:var(--space-3);align-items:flex-start;margin-bottom:var(--space-3)">
|
|
${_dogAvatar(r.to_dog_foto, r.to_dog_name, 44)}
|
|
<div class="flex-1-min">
|
|
<div style="font-weight:var(--weight-semibold);color:var(--c-text)">${_esc(r.to_dog_name)}</div>
|
|
<div class="text-xs-secondary">
|
|
${r.to_dog_rasse ? _esc(r.to_dog_rasse) + ' · ' : ''}
|
|
von ${_esc(r.to_user_name)}
|
|
</div>
|
|
<div class="text-xs-muted">${_fmtDate(r.created_at)}</div>
|
|
</div>
|
|
${_statusBadge(r.status)}
|
|
</div>
|
|
|
|
${r.nachricht ? `
|
|
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0 0 var(--space-3)">
|
|
"${_esc(r.nachricht)}"
|
|
</p>` : ''}
|
|
|
|
${r.status === 'accepted' ? `
|
|
<button class="btn btn-ghost btn-sm req-chat-btn">
|
|
${UI.icon('chat-circle-dots')} Chat öffnen
|
|
</button>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
return { init, refresh, onDogChange };
|
|
})();
|