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

@ -47,7 +47,7 @@ window.Page_dog_profile = (() => {
async function _render() {
if (!_appState.user) {
_container.innerHTML = UI.emptyState({
icon : '🐕',
icon : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#dog"></use></svg>',
title : 'Anmelden erforderlich',
text : 'Melde dich an, um ein Hundeprofil anzulegen.',
action: `<button class="btn btn-primary" id="profile-goto-login">Anmelden</button>`,
@ -85,13 +85,13 @@ window.Page_dog_profile = (() => {
: `<div style="width:120px;height:120px;border-radius:50%;
background:var(--c-surface-2);display:flex;
align-items:center;justify-content:center;
font-size:3.5rem;border:3px solid var(--c-border)">🐕</div>`}
font-size:3.5rem;border:3px solid var(--c-border)">${UI.icon('dog')}</div>`}
<label style="position:absolute;bottom:4px;right:4px;
background:var(--c-primary);color:#fff;border-radius:50%;
width:30px;height:30px;display:flex;align-items:center;
justify-content:center;cursor:pointer;font-size:14px"
title="Foto ändern">
📷
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg>
<input type="file" id="dp-photo-input" accept="image/*"
style="display:none">
</label>
@ -110,7 +110,7 @@ window.Page_dog_profile = (() => {
${geburtstag ? `
<div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px">🎂 Geburtstag</div>
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#calendar-dots"></use></svg> Geburtstag</div>
<div style="font-weight:500;font-size:var(--text-sm)">${geburtstag}</div>
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
${_calcAlter(dog.geburtstag)}
@ -120,7 +120,7 @@ window.Page_dog_profile = (() => {
${dog.geschlecht ? `
<div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px">${dog.geschlecht === 'm' ? '♂' : '♀'} Geschlecht</div>
margin-bottom:2px">${dog.geschlecht === 'm' ? '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-male"></use></svg>' : '<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#gender-female"></use></svg>'} Geschlecht</div>
<div style="font-weight:500;font-size:var(--text-sm)">
${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'}
</div>
@ -129,14 +129,14 @@ window.Page_dog_profile = (() => {
${dog.gewicht_kg ? `
<div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px"> Gewicht</div>
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#scales"></use></svg> Gewicht</div>
<div style="font-weight:500;font-size:var(--text-sm)">${dog.gewicht_kg} kg</div>
</div>
` : ''}
${dog.chip_nr ? `
<div class="card" style="padding:var(--space-3)">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:2px">💾 Chip-Nr.</div>
margin-bottom:2px"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#tag"></use></svg> Chip-Nr.</div>
<div style="font-size:var(--text-xs);font-weight:500;
word-break:break-all">${_esc(dog.chip_nr)}</div>
</div>
@ -151,6 +151,34 @@ window.Page_dog_profile = (() => {
</div>
` : ''}
${dog.is_public ? `
<div style="background:var(--c-primary-subtle);border:1px solid var(--c-primary-light);
border-radius:var(--radius-md);padding:var(--space-4);
margin-bottom:var(--space-5);text-align:left">
<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
margin-bottom:var(--space-2);font-weight:var(--weight-medium)">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#tag"></use></svg> NFC-Link
</div>
<div style="display:flex;align-items:center;gap:var(--space-2);
flex-wrap:wrap">
<code id="dp-nfc-link"
style="flex:1;font-size:var(--text-sm);background:var(--c-surface);
border:1px solid var(--c-border);border-radius:var(--radius-sm);
padding:var(--space-2) var(--space-3);color:var(--c-text);
word-break:break-all">banyaro.app/hund/${dog.id}</code>
<button class="btn btn-secondary btn-sm" id="dp-copy-link-btn"
style="flex-shrink:0;padding:var(--space-2) var(--space-3);
font-size:var(--text-sm);min-height:36px">
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#clipboard-text"></use></svg> Kopieren
</button>
</div>
<p style="margin:var(--space-2) 0 0;font-size:var(--text-xs);
color:var(--c-text-muted)">
Dieser Link kann auf ein NFC-Tag gebrannt werden
</p>
</div>
` : ''}
<div style="display:flex;flex-direction:column;gap:var(--space-2);width:100%">
<button class="btn btn-primary w-full" id="dp-edit-btn">
Profil bearbeiten
@ -182,6 +210,26 @@ window.Page_dog_profile = (() => {
UI.toast.error(err.message || 'Fehler beim Hochladen.');
}
});
// NFC-Link kopieren
document.getElementById('dp-copy-link-btn')?.addEventListener('click', async () => {
const url = `https://banyaro.app/hund/${dog.id}`;
try {
await navigator.clipboard.writeText(url);
UI.toast.success('Link kopiert!');
} catch {
// Fallback für ältere Browser
const el = document.getElementById('dp-nfc-link');
const range = document.createRange();
range.selectNodeContents(el);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
document.execCommand('copy');
sel.removeAllRanges();
UI.toast.success('Link kopiert!');
}
});
// Edit- und Add-Klicks laufen über Event-Delegation in init() — keine direkten Listener nötig.
}
@ -192,7 +240,7 @@ window.Page_dog_profile = (() => {
_container.innerHTML = `
<div style="padding:var(--space-4) 0 var(--space-2)">
<div style="text-align:center;margin-bottom:var(--space-5)">
<div style="font-size:3rem;margin-bottom:var(--space-2)">🐕</div>
<div style="font-size:3rem;margin-bottom:var(--space-2)">${UI.icon('dog')}</div>
<h2 style="font-size:var(--text-xl);font-weight:700;margin:0 0 var(--space-2)">
Hund anlegen
</h2>
@ -215,7 +263,7 @@ window.Page_dog_profile = (() => {
body: _formHTML(null, true),
footer: `
<button type="button" class="btn btn-secondary flex-1" id="dp-form-cancel">Abbrechen</button>
<button type="submit" form="dp-form" class="btn btn-primary flex-1">🐕 Hund anlegen</button>
<button type="submit" form="dp-form" class="btn btn-primary flex-1">${UI.icon('dog')} Hund anlegen</button>
`,
});
_bindForm(null, true);
@ -317,7 +365,7 @@ window.Page_dog_profile = (() => {
background:var(--c-surface-2);border:2px solid var(--c-border);
display:${dog?.foto_url ? 'block' : 'none'}">
<label class="btn btn-secondary btn-sm" style="cursor:pointer;margin:0">
📷 Foto auswählen
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#camera"></use></svg> Foto auswählen
<input type="file" name="foto" accept="image/*" style="display:none"
id="dp-form-foto">
</label>
@ -329,7 +377,7 @@ window.Page_dog_profile = (() => {
${dog ? `<button type="button" class="btn btn-secondary flex-1"
id="dp-form-cancel">Abbrechen</button>` : ''}
<button type="submit" class="btn btn-primary flex-1">
${dog ? 'Speichern' : '🐕 Hund anlegen'}
${dog ? 'Speichern' : `${UI.icon('dog')} Hund anlegen`}
</button>
</div>` : ''}