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

@ -18,12 +18,12 @@ window.Page_places = (() => {
// Typen-Konfiguration
// ----------------------------------------------------------
const TYPEN = {
restaurant: { icon: '🍽️', label: 'Restaurant & Café', color: '#F97316' },
freilauf: { icon: '🐕', label: 'Freilauffläche', color: '#22C55E' },
shop: { icon: '🛒', label: 'Shop', color: '#3B82F6' },
kotbeutel: { icon: '🧻', label: 'Kotbeutel-Station', color: '#6B7280' },
tierarzt: { icon: '🩺', label: 'Tierarzt', color: '#EF4444' },
hundeschule: { icon: '🎓', label: 'Hundeschule', color: '#8B5CF6' },
restaurant: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#fork-knife"></use></svg>', label: 'Restaurant & Café', color: '#F97316' },
freilauf: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>', label: 'Freilauffläche', color: '#22C55E' },
shop: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#shopping-cart"></use></svg>', label: 'Shop', color: '#3B82F6' },
kotbeutel: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#trash"></use></svg>', label: 'Kotbeutel-Station', color: '#6B7280' },
tierarzt: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#first-aid"></use></svg>', label: 'Tierarzt', color: '#EF4444' },
hundeschule: { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#graduation-cap"></use></svg>', label: 'Hundeschule', color: '#8B5CF6' },
};
function _esc(s) {
@ -54,13 +54,13 @@ window.Page_places = (() => {
<!-- Toolbar -->
<div class="places-toolbar">
<div class="places-filter" id="places-filter">
<button class="places-filter-btn active" data-typ="">🗺 Alle</button>
<button class="places-filter-btn active" data-typ=""><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-trifold"></use></svg> Alle</button>
${Object.entries(TYPEN).map(([k, t]) =>
`<button class="places-filter-btn" data-typ="${k}">${t.icon} ${t.label}</button>`
).join('')}
</div>
<button class="btn btn-primary btn-sm" id="places-add-btn" style="white-space:nowrap">
+ Ort hinzufügen
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#plus"></use></svg> Ort hinzufügen
</button>
</div>
@ -139,7 +139,7 @@ window.Page_places = (() => {
L.Control.Locate = L.Control.extend({
onAdd() {
const btn = L.DomUtil.create('button', 'places-locate-btn');
btn.innerHTML = '📍';
btn.innerHTML = '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>';
btn.title = 'Meinen Standort';
btn.onclick = async () => {
try {
@ -191,7 +191,7 @@ window.Page_places = (() => {
_markers = [];
_filtered().forEach(place => {
const t = TYPEN[place.typ] || { icon: '📍', color: '#6B7280' };
const t = TYPEN[place.typ] || { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>', color: '#6B7280' };
const icon = L.divIcon({
className: '',
html: `<div style="
@ -242,11 +242,11 @@ window.Page_places = (() => {
}
function _cardHTML(p) {
const t = TYPEN[p.typ] || { icon: '📍', label: p.typ, color: '#6B7280' };
const t = TYPEN[p.typ] || { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>', label: p.typ, color: '#6B7280' };
const flags = [
p.hund_rein === true ? '🐕 Hund rein' : null,
p.leine_pflicht === true ? '🔗 Leinenpflicht' : null,
p.wasser_fuer_hunde === true ? '💧 Wasser' : null,
p.hund_rein === true ? `${UI.icon('dog')} Hund rein` : null,
p.leine_pflicht === true ? `${UI.icon('tag')} Leinenpflicht` : null,
p.wasser_fuer_hunde === true ? `${UI.icon('drop')} Wasser` : null,
].filter(Boolean);
return `
@ -260,7 +260,7 @@ window.Page_places = (() => {
</div>
${flags.length ? `<div class="places-card-flags">${flags.map(f => `<span class="places-flag">${f}</span>`).join('')}</div>` : ''}
</div>
<div class="places-card-arrow"></div>
<div class="places-card-arrow">${UI.icon('arrow-right')}</div>
</div>`;
}
@ -268,13 +268,13 @@ window.Page_places = (() => {
// Detail-Modal
// ----------------------------------------------------------
function _openDetail(place) {
const t = TYPEN[place.typ] || { icon: '📍', label: place.typ, color: '#6B7280' };
const t = TYPEN[place.typ] || { icon: '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg>', label: place.typ, color: '#6B7280' };
const isOwn = _appState.user?.id === place.user_id;
const flags = [
place.hund_rein === true ? '🐕 Hund erlaubt' : (place.hund_rein === false ? '🚫 Kein Hund' : null),
place.leine_pflicht === true ? '🔗 Leinenpflicht' : (place.leine_pflicht === false ? '✅ Leine optional' : null),
place.wasser_fuer_hunde === true ? '💧 Wasser vorhanden': null,
place.hund_rein === true ? `${UI.icon('dog')} Hund erlaubt` : (place.hund_rein === false ? `${UI.icon('x')} Kein Hund` : null),
place.leine_pflicht === true ? `${UI.icon('tag')} Leinenpflicht` : (place.leine_pflicht === false ? `${UI.icon('check')} Leine optional` : null),
place.wasser_fuer_hunde === true ? `${UI.icon('drop')} Wasser vorhanden`: null,
].filter(Boolean);
const body = `
@ -285,8 +285,8 @@ window.Page_places = (() => {
<div style="color:${t.color};font-size:0.9rem">${t.label}</div>
</div>
</div>
${place.adresse ? `<p style="color:var(--c-text-secondary);margin-bottom:var(--space-2)">📍 ${_esc(place.adresse)}</p>` : ''}
${place.website ? `<p style="margin-bottom:var(--space-2)"><a href="${_esc(place.website)}" target="_blank" style="color:var(--c-primary)">🌐 ${_esc(place.website)}</a></p>` : ''}
${place.adresse ? `<p style="color:var(--c-text-secondary);margin-bottom:var(--space-2)">${UI.icon('map-pin')} ${_esc(place.adresse)}</p>` : ''}
${place.website ? `<p style="margin-bottom:var(--space-2)"><a href="${_esc(place.website)}" target="_blank" style="color:var(--c-primary)">${UI.icon('arrow-square-out')} ${_esc(place.website)}</a></p>` : ''}
${flags.length ? `<div style="display:flex;flex-wrap:wrap;gap:var(--space-2);margin-top:var(--space-3)">${flags.map(f => `<span class="places-flag places-flag--detail">${f}</span>`).join('')}</div>` : ''}
<p style="color:var(--c-text-muted);font-size:0.8rem;margin-top:var(--space-4)">
Eingetragen von ${_esc(place.user_name || 'Unbekannt')}
@ -301,7 +301,7 @@ window.Page_places = (() => {
<button type="button" class="btn btn-primary flex-1" id="place-detail-close">Schließen</button>
`;
UI.modal.open({ title: `${t.icon} ${place.name}`, body, footer });
UI.modal.open({ title: `${t.icon} ${_esc(place.name)}`, body, footer });
document.getElementById('place-detail-close')?.addEventListener('click', UI.modal.close);
@ -341,7 +341,7 @@ window.Page_places = (() => {
const isEdit = !!place;
const typOpts = Object.entries(TYPEN)
.map(([k, t]) => `<option value="${k}" ${place?.typ === k ? 'selected' : ''}>${t.icon} ${t.label}</option>`)
.map(([k, t]) => `<option value="${k}" ${place?.typ === k ? 'selected' : ''}>${t.label}</option>`)
.join('');
const body = `
@ -367,12 +367,12 @@ window.Page_places = (() => {
<input class="form-control" type="text" id="pf-lon-disp"
placeholder="Länge" readonly style="flex:1"
value="${place ? place.lon.toFixed(6) : ''}">
<button type="button" class="btn btn-secondary" id="pf-gps-btn" title="GPS">📍</button>
<button type="button" class="btn btn-secondary" id="pf-gps-btn" title="GPS"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg></button>
</div>
<input type="hidden" name="lat" id="pf-lat" value="${place?.lat || ''}">
<input type="hidden" name="lon" id="pf-lon" value="${place?.lon || ''}">
<small id="pf-gps-hint" style="color:var(--c-text-secondary)">
${place ? 'Position gespeichert' : 'GPS-Button drücken oder Standort ermitteln'}
${place ? 'Position gespeichert' : 'GPS-Button drücken oder Standort ermitteln'}
</small>
</div>
@ -392,15 +392,15 @@ window.Page_places = (() => {
<label class="form-label">Hundefreundlichkeit</label>
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
<input type="checkbox" name="hund_rein" ${place?.hund_rein ? 'checked' : ''}>
🐕 Hund darf rein
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg> Hund darf rein
</label>
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
<input type="checkbox" name="leine_pflicht" ${place?.leine_pflicht ? 'checked' : ''}>
🔗 Leinenpflicht beachten
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#tag"></use></svg> Leinenpflicht beachten
</label>
<label style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
<input type="checkbox" name="wasser_fuer_hunde" ${place?.wasser_fuer_hunde ? 'checked' : ''}>
💧 Wasser für Hunde vorhanden
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#drop"></use></svg> Wasser für Hunde vorhanden
</label>
</div>
@ -414,7 +414,7 @@ window.Page_places = (() => {
</button>
`;
UI.modal.open({ title: isEdit ? `${place.name} bearbeiten` : '📍 Neuer Ort', body, footer });
UI.modal.open({ title: isEdit ? `${_esc(place.name)} bearbeiten` : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#map-pin"></use></svg> Neuer Ort', body, footer });
document.getElementById('place-form-cancel')?.addEventListener('click', UI.modal.close);
@ -429,7 +429,7 @@ window.Page_places = (() => {
document.getElementById('pf-lon').value = pos.lon;
document.getElementById('pf-lat-disp').value = pos.lat.toFixed(6);
document.getElementById('pf-lon-disp').value = pos.lon.toFixed(6);
document.getElementById('pf-gps-hint').textContent = 'Standort ermittelt';
document.getElementById('pf-gps-hint').textContent = 'Standort ermittelt';
} catch {
UI.toast.error('GPS nicht verfügbar.');
}