Tagebuch:
- Day-One-Listenansicht: Wochentag + Tageszahl + Meta-Zeile (Zeit/Ort/Wetter)
- 4 Ansichten: Liste, Medien-Mosaik, Kalender (mit Sprungbuttons), Karte (GPS-Marker)
- Detail-Ansicht inline im Content-Bereich (kein Fullscreen-Overlay mehr)
- Hero-Bild vollständig sichtbar (object-fit:contain), Lightbox mit Safe-Area
- 2-Spalten-Layout Desktop: Text + Leaflet-Karte + POI-Liste
- EXIF-GPS-Extraktion bei Foto-Upload, historisches Wetter via Archive-API
- NoteStation-Import: Fotos in diary_media (80 Einträge migriert, 94 Medien)
- Stats-Endpoints: /diary/stats, /diary/calendar, /diary/locations
Notiz-Feature:
- Generische notes-Tabelle (parent_type + parent_id + meta_json)
- 📝-Button in 8 Bereichen, Notizblock-Seite mit KI-Analyse
- KI-Toggle in Einstellungen, notes_ki_enabled in User-Profil
Icons & Design:
- fill:currentColor Fix für welcome/onboarding/friends.js
- --c-icon Variable, --c-text-muted Dark Mode aufgehellt
- 15+ neue Phosphor-Icons aus lokaler Kopie
- CSS Network-First im SW, Cache-Control-Middleware
Infrastruktur:
- Wiki-Anreicherungs-Scheduler-Jobs entfernt (abgeschlossen)
- auth.py: notes_ki_enabled + is_social_media im User-Response
836 lines
33 KiB
JavaScript
836 lines
33 KiB
JavaScript
/* ============================================================
|
|
BAN YARO — Freunde
|
|
============================================================ */
|
|
|
|
window.Page_friends = (() => {
|
|
|
|
let _container = null;
|
|
let _appState = null;
|
|
let _searchTimer = null;
|
|
|
|
// ----------------------------------------------------------
|
|
async function init(container, appState, params = {}) {
|
|
_container = container;
|
|
_appState = appState;
|
|
_render(params.suche || null);
|
|
}
|
|
|
|
function refresh() { _loadFriends(); }
|
|
function onDogChange() {}
|
|
|
|
// ----------------------------------------------------------
|
|
// HAUPT-RENDER
|
|
// ----------------------------------------------------------
|
|
function _render(prefill = null) {
|
|
if (!_appState.user) {
|
|
_container.innerHTML = UI.emptyState({
|
|
icon: UI.icon('users'),
|
|
title: 'Anmelden erforderlich',
|
|
text: 'Melde dich an, um Freunde zu finden und Anfragen zu verwalten.',
|
|
action: `<button class="btn btn-primary" onclick="App.navigate('settings')">Anmelden</button>`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const myName = _appState?.user?.name || '';
|
|
const myLink = `${location.origin}/#friends?suche=${encodeURIComponent(myName)}`;
|
|
|
|
_container.innerHTML = `
|
|
<div>
|
|
|
|
<!-- Mein Freundes-Link -->
|
|
<div class="card" style="margin-bottom:var(--space-5);padding:var(--space-4)">
|
|
<div style="display:flex;align-items:center;gap:var(--space-3);margin-bottom:var(--space-3)">
|
|
<div style="width:36px;height:36px;border-radius:50%;flex-shrink:0;
|
|
background:var(--c-primary-subtle);
|
|
display:flex;align-items:center;justify-content:center">
|
|
<svg style="fill:currentColor;width:18px;height:18px;color:var(--c-primary)" aria-hidden="true">
|
|
<use href="/icons/phosphor.svg#link"></use>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
|
color:var(--c-text)">Dein Freundes-Link</div>
|
|
<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">
|
|
Teile ihn — der andere tippt drauf und findet dich sofort.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div style="display:flex;gap:var(--space-2)">
|
|
<div style="flex:1;padding:var(--space-2) var(--space-3);
|
|
background:var(--c-surface-2);border-radius:var(--radius-md);
|
|
font-size:var(--text-xs);color:var(--c-text-secondary);
|
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
|
banyaro.app/#friends?suche=${_esc(encodeURIComponent(myName))}
|
|
</div>
|
|
<button class="btn btn-ghost btn-sm" id="fr-copy-btn" title="Link kopieren">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#link"></use></svg>
|
|
</button>
|
|
<button class="btn btn-primary btn-sm" id="fr-share-btn" title="Link teilen">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#paper-plane-tilt"></use></svg>
|
|
Teilen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Suche -->
|
|
<div style="position:relative;margin-bottom:var(--space-2)">
|
|
<svg class="ph-icon" style="position:absolute;left:var(--space-3);top:50%;
|
|
transform:translateY(-50%);color:var(--c-text-muted);pointer-events:none"
|
|
aria-hidden="true">
|
|
<use href="/icons/phosphor.svg#magnifying-glass"></use>
|
|
</svg>
|
|
<input id="fr-search" type="search" autocomplete="off"
|
|
placeholder="Namen eines Hundebesitzers suchen…"
|
|
value="${_esc(prefill || '')}"
|
|
style="width:100%;box-sizing:border-box;
|
|
padding:var(--space-3) var(--space-3) var(--space-3) 2.5rem;
|
|
border:1.5px solid var(--c-border);border-radius:var(--radius-lg);
|
|
font-size:var(--text-sm);font-family:inherit;
|
|
background:var(--c-surface);color:var(--c-text)">
|
|
</div>
|
|
<p style="font-size:var(--text-xs);color:var(--c-text-muted);
|
|
margin:0 0 var(--space-4) var(--space-1)">
|
|
Tipp: Lass dir den Freundes-Link einer anderen Person schicken — dann klappt die Suche automatisch.
|
|
</p>
|
|
<div id="fr-search-results"></div>
|
|
|
|
<!-- Eingehende Anfragen -->
|
|
<div id="fr-incoming"></div>
|
|
|
|
<!-- Ausstehende Anfragen -->
|
|
<div id="fr-outgoing"></div>
|
|
|
|
<!-- Freundesliste -->
|
|
<div id="fr-list"></div>
|
|
|
|
<!-- Aktivitäten-Feed -->
|
|
<div id="fr-activity"></div>
|
|
|
|
</div>
|
|
`;
|
|
|
|
// Copy-Button
|
|
_container.querySelector('#fr-copy-btn')?.addEventListener('click', () => {
|
|
navigator.clipboard.writeText(myLink).then(() => {
|
|
UI.toast.success('Link kopiert!');
|
|
}).catch(() => {
|
|
UI.toast.info('Link: ' + myLink);
|
|
});
|
|
});
|
|
|
|
// Share-Button (Web Share API, Fallback: Copy)
|
|
_container.querySelector('#fr-share-btn')?.addEventListener('click', async () => {
|
|
if (navigator.share) {
|
|
try {
|
|
await navigator.share({
|
|
title: `${myName} auf Ban Yaro`,
|
|
text: `Füge mich auf Ban Yaro als Freund hinzu!`,
|
|
url: myLink,
|
|
});
|
|
} catch { /* abgebrochen */ }
|
|
} else {
|
|
navigator.clipboard.writeText(myLink).then(() => {
|
|
UI.toast.success('Link kopiert!');
|
|
});
|
|
}
|
|
});
|
|
|
|
// Suche
|
|
const searchInput = _container.querySelector('#fr-search');
|
|
searchInput.addEventListener('input', e => {
|
|
clearTimeout(_searchTimer);
|
|
const q = e.target.value.trim();
|
|
if (q.length < 2) {
|
|
_container.querySelector('#fr-search-results').innerHTML = '';
|
|
return;
|
|
}
|
|
_searchTimer = setTimeout(() => _doSearch(q), 380);
|
|
});
|
|
|
|
// Prefill aus URL-Parameter → sofort suchen
|
|
if (prefill && prefill.length >= 2) {
|
|
_doSearch(prefill);
|
|
}
|
|
|
|
_loadFriends();
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// DATEN LADEN
|
|
// ----------------------------------------------------------
|
|
async function _loadFriends() {
|
|
if (!_appState.user) return;
|
|
try {
|
|
const data = await API.friends.list();
|
|
_renderIncoming(data.incoming || []);
|
|
_renderOutgoing(data.outgoing || []);
|
|
_renderFriends(data.friends || []);
|
|
_updateBadge((data.incoming || []).length);
|
|
} catch { /* silent — 401 bei abgemeldeter Session */ }
|
|
_loadActivity();
|
|
}
|
|
|
|
async function _loadActivity() {
|
|
if (!_appState.user) return;
|
|
const el = _container.querySelector('#fr-activity');
|
|
if (!el) return;
|
|
|
|
// Ladeindikator
|
|
el.innerHTML = `
|
|
<div style="margin-top:var(--space-6)">
|
|
<div class="by-section-label">Aktivitäten</div>
|
|
<div style="text-align:center;padding:var(--space-6) 0;color:var(--c-text-muted);
|
|
font-size:var(--text-sm)">
|
|
<svg class="ph-icon" style="width:20px;height:20px;animation:spin 1s linear infinite"
|
|
aria-hidden="true"><use href="/icons/phosphor.svg#spinner"></use></svg>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const items = await API.friends.activity();
|
|
_renderActivity(items || []);
|
|
} catch {
|
|
el.innerHTML = '';
|
|
}
|
|
}
|
|
|
|
let _activityFilter = 'alle';
|
|
let _activityAll = [];
|
|
|
|
function _renderActivity(items) {
|
|
_activityAll = items;
|
|
const el = _container.querySelector('#fr-activity');
|
|
if (!el) return;
|
|
|
|
const FILTERS = [
|
|
{ key: 'alle', label: 'Alle' },
|
|
{ key: 'walk', label: 'Gassi-Treffen' },
|
|
{ key: 'forum', label: 'Forum' },
|
|
{ key: 'health', label: 'Gesundheit' },
|
|
{ key: 'new_dog', label: 'Neuer Hund' },
|
|
];
|
|
|
|
const chips = FILTERS.map(f => `
|
|
<button class="rk-chip${_activityFilter === f.key ? ' active' : ''}"
|
|
data-af="${f.key}">${f.label}</button>
|
|
`).join('');
|
|
|
|
const filtered = _activityFilter === 'alle'
|
|
? items
|
|
: items.filter(i => i.type === _activityFilter);
|
|
|
|
el.innerHTML = `
|
|
<div style="margin-top:var(--space-6)">
|
|
<div class="by-section-label">Aktivitäten</div>
|
|
<div class="rk-filter-group" style="margin-bottom:var(--space-3);flex-wrap:wrap">
|
|
${chips}
|
|
</div>
|
|
${!filtered.length
|
|
? `<div style="text-align:center;padding:var(--space-8) var(--space-4)">
|
|
<p style="font-size:var(--text-sm);color:var(--c-text-secondary);margin:0">
|
|
Keine Einträge in dieser Kategorie.
|
|
</p>
|
|
</div>`
|
|
: `<div class="fr-activity-timeline">
|
|
${filtered.map(item => _activityItem(item)).join('')}
|
|
</div>`
|
|
}
|
|
</div>
|
|
`;
|
|
|
|
el.querySelectorAll('[data-af]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
_activityFilter = btn.dataset.af;
|
|
_renderActivity(_activityAll);
|
|
});
|
|
});
|
|
|
|
el.querySelectorAll('.fr-activity-item[data-nav]').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const page = btn.dataset.nav;
|
|
const entryId = btn.dataset.entryId ? parseInt(btn.dataset.entryId) : null;
|
|
const type = btn.dataset.type;
|
|
if (!page) return;
|
|
if (entryId && type === 'diary') {
|
|
App.callModule('diary', 'openDetail', entryId);
|
|
} else if (entryId && type === 'walk') {
|
|
App.callModule('walks', 'openDetail', entryId);
|
|
} else if (entryId && type === 'forum') {
|
|
App.callModule('forum', 'openThread', entryId);
|
|
} else {
|
|
App.navigate(page);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
const _ACTIVITY_PAGE = {
|
|
health: 'health',
|
|
walk: 'walks',
|
|
forum: 'forum',
|
|
new_dog: null,
|
|
};
|
|
|
|
function _activityItem(item) {
|
|
const ago = _timeAgo(item.created_at);
|
|
const text = item.text || '';
|
|
const page = _ACTIVITY_PAGE[item.type] || '';
|
|
const dogLabel = item.dog_name
|
|
? `<span class="fr-activity-dog">${_esc(item.dog_name)}</span>`
|
|
: '';
|
|
|
|
const avatar = item.dog_foto
|
|
? `<img src="${_esc(item.dog_foto)}" alt="${_esc(item.dog_name || '')}"
|
|
class="fr-activity-avatar">`
|
|
: item.avatar_url
|
|
? `<img src="${_esc(item.avatar_url)}" alt="${_esc(item.user_name)}"
|
|
class="fr-activity-avatar">`
|
|
: `<div class="fr-activity-avatar fr-activity-avatar--initial">
|
|
${_esc((item.user_name || '?')[0].toUpperCase())}
|
|
</div>`;
|
|
|
|
const tag = page ? `button type="button"` : `div`;
|
|
return `
|
|
<${tag} class="fr-activity-item${page ? ' fr-activity-item--link' : ''}"
|
|
${page ? `data-nav="${page}"` : ''}
|
|
${item.entry_id ? `data-entry-id="${item.entry_id}"` : ''}
|
|
data-type="${item.type}">
|
|
<div class="fr-activity-avatar-wrap">
|
|
${avatar}
|
|
<div class="fr-activity-icon-badge">
|
|
<svg class="ph-icon" style="width:10px;height:10px" aria-hidden="true">
|
|
<use href="/icons/phosphor.svg#${_esc(item.icon)}"></use>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
<div class="fr-activity-body">
|
|
<div class="fr-activity-meta">
|
|
<span class="fr-activity-user">${_esc(item.user_name)}</span>
|
|
${dogLabel}
|
|
</div>
|
|
${text ? `<div class="fr-activity-text">${_esc(text)}</div>` : ''}
|
|
<div class="fr-activity-time">${_esc(ago)}</div>
|
|
</div>
|
|
</${page ? 'button' : 'div'}>
|
|
`;
|
|
}
|
|
|
|
function _timeAgo(iso) {
|
|
if (!iso) return '';
|
|
const diff = Math.floor((Date.now() - new Date(iso + (iso.endsWith('Z') ? '' : 'Z')).getTime()) / 1000);
|
|
if (diff < 60) return 'Gerade eben';
|
|
if (diff < 3600) return `vor ${Math.floor(diff / 60)} Min`;
|
|
if (diff < 86400) return `vor ${Math.floor(diff / 3600)} Std`;
|
|
if (diff < 86400 * 7) return `vor ${Math.floor(diff / 86400)} Tagen`;
|
|
return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit' });
|
|
}
|
|
|
|
function _updateBadge(count) {
|
|
const el = document.getElementById('friends-badge');
|
|
if (!el) return;
|
|
el.textContent = count;
|
|
el.style.display = count > 0 ? '' : 'none';
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// EINGEHENDE ANFRAGEN
|
|
// ----------------------------------------------------------
|
|
function _renderIncoming(list) {
|
|
const el = _container.querySelector('#fr-incoming');
|
|
if (!list.length) { el.innerHTML = ''; return; }
|
|
el.innerHTML = `
|
|
<div style="margin-bottom:var(--space-5)">
|
|
<div class="by-section-label">Anfragen · ${list.length}</div>
|
|
${list.map(r => `
|
|
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-3);
|
|
border-left:3px solid var(--c-primary)">
|
|
<div style="display:flex;align-items:center;gap:var(--space-3);flex-wrap:wrap">
|
|
${_userAvatar(r.requester_name, r.dogs?.[0], r.avatar_url)}
|
|
<div style="flex:1;min-width:120px">
|
|
<div style="font-weight:var(--weight-semibold);color:var(--c-text);
|
|
overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
|
${_esc(r.requester_name)}
|
|
</div>
|
|
${_dogPills(r.dogs, 2)}
|
|
</div>
|
|
<div style="display:flex;gap:var(--space-2);flex-shrink:0">
|
|
<button class="btn btn-primary btn-sm"
|
|
onclick="Page_friends._accept(${r.id})" title="Annehmen">
|
|
<svg class="ph-icon"><use href="/icons/phosphor.svg#check"></use></svg>
|
|
</button>
|
|
<button class="btn btn-ghost btn-sm"
|
|
onclick="Page_friends._decline(${r.id})" title="Ablehnen">
|
|
<svg class="ph-icon"><use href="/icons/phosphor.svg#x"></use></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// GESENDETE ANFRAGEN
|
|
// ----------------------------------------------------------
|
|
function _renderOutgoing(list) {
|
|
const el = _container.querySelector('#fr-outgoing');
|
|
if (!list.length) { el.innerHTML = ''; return; }
|
|
el.innerHTML = `
|
|
<div style="margin-bottom:var(--space-5)">
|
|
<div class="by-section-label">Gesendet</div>
|
|
${list.map(r => `
|
|
<div class="card" style="padding:var(--space-3) var(--space-4);
|
|
margin-bottom:var(--space-2)">
|
|
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
|
<div style="width:36px;height:36px;border-radius:50%;
|
|
background:var(--c-surface-2);
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-weight:var(--weight-bold);color:var(--c-text-secondary);
|
|
font-size:var(--text-sm);flex-shrink:0">
|
|
${_esc((r.addressee_name || '?')[0].toUpperCase())}
|
|
</div>
|
|
<div style="flex:1">
|
|
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
|
color:var(--c-text)">${_esc(r.addressee_name)}</div>
|
|
<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Anfrage ausstehend</div>
|
|
</div>
|
|
<button class="btn btn-ghost btn-sm"
|
|
onclick="Page_friends._cancel(${r.id})" title="Zurückziehen">
|
|
<svg class="ph-icon"><use href="/icons/phosphor.svg#x"></use></svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// FREUNDESLISTE
|
|
// ----------------------------------------------------------
|
|
function _renderFriends(list) {
|
|
const el = _container.querySelector('#fr-list');
|
|
|
|
if (!list.length) {
|
|
el.innerHTML = _emptyState(
|
|
'users-three',
|
|
'Noch keine Freunde',
|
|
'Verbinde dich mit anderen Hundebesitzern. Teile Routen, sieh Aktivitäten und schreib Nachrichten.',
|
|
`<button class="btn btn-primary" id="fr-empty-search">Freunde suchen</button>`
|
|
);
|
|
el.querySelector('#fr-empty-search')?.addEventListener('click', () => {
|
|
_container.querySelector('#fr-search')?.focus();
|
|
});
|
|
return;
|
|
}
|
|
|
|
el.innerHTML = `
|
|
<div>
|
|
<div class="by-section-label">Freunde · ${list.length}</div>
|
|
${list.map(f => _friendCard(f)).join('')}
|
|
</div>
|
|
`;
|
|
|
|
// Klick auf Karte → Mini-Profil
|
|
el.querySelectorAll('.fr-card').forEach(card => {
|
|
card.addEventListener('click', e => {
|
|
if (e.target.closest('button')) return; // Buttons nicht überschreiben
|
|
const fid = parseInt(card.dataset.friendId);
|
|
const fname = card.dataset.friendName;
|
|
const fdogs = JSON.parse(card.dataset.dogs || '[]');
|
|
const fprofile = JSON.parse(card.dataset.profile || '{}');
|
|
_showProfile(fid, fname, fdogs, fprofile);
|
|
});
|
|
});
|
|
}
|
|
|
|
function _friendCard(f) {
|
|
const dogs = f.dogs || [];
|
|
const profile = {
|
|
bio: f.bio || null,
|
|
wohnort: f.wohnort || null,
|
|
erfahrung: f.erfahrung || null,
|
|
social_link: f.social_link || null,
|
|
profil_sichtbarkeit: f.profil_sichtbarkeit || null,
|
|
avatar_url: f.avatar_url || null,
|
|
};
|
|
return `
|
|
<div class="card fr-card" style="padding:var(--space-4);margin-bottom:var(--space-3);cursor:pointer;
|
|
transition:box-shadow 0.15s"
|
|
data-friend-id="${f.friend_id}"
|
|
data-friend-name="${_esc(f.friend_name)}"
|
|
data-dogs="${_esc(JSON.stringify(dogs))}"
|
|
data-profile="${_esc(JSON.stringify(profile))}">
|
|
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
|
|
|
<!-- Avatar (User-Avatar, erstes Hunde-Foto oder Initiale) -->
|
|
${_userAvatar(f.friend_name, dogs[0], f.avatar_url)}
|
|
|
|
<!-- Name + Infos + Hunde -->
|
|
<div style="flex:1;min-width:0">
|
|
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:2px;
|
|
margin-bottom:var(--space-1)">
|
|
<span style="font-weight:var(--weight-semibold);color:var(--c-text)">
|
|
${_esc(f.friend_name)}
|
|
</span>
|
|
${_erfahrungSpan(f.erfahrung)}
|
|
</div>
|
|
${_wohnortLine(f.wohnort)}
|
|
${_bioLine(f.bio, f.profil_sichtbarkeit)}
|
|
<div style="margin-top:var(--space-1)">
|
|
${dogs.length
|
|
? `<div style="display:flex;flex-wrap:wrap;gap:var(--space-1);align-items:center">
|
|
${_dogPills(dogs, 3)}
|
|
</div>`
|
|
: `<div style="font-size:var(--text-xs);color:var(--c-text-muted)">Noch kein Hund eingetragen</div>`
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Aktionen -->
|
|
<div style="display:flex;gap:var(--space-1);flex-shrink:0">
|
|
<button class="btn btn-ghost btn-sm"
|
|
onclick="Page_friends._openChat(${f.friend_id})"
|
|
title="Nachricht schreiben">
|
|
<svg class="ph-icon"><use href="/icons/phosphor.svg#chat-circle-dots"></use></svg>
|
|
</button>
|
|
<svg class="ph-icon" style="width:16px;height:16px;color:var(--c-border);
|
|
align-self:center;flex-shrink:0" aria-hidden="true">
|
|
<use href="/icons/phosphor.svg#caret-down"></use>
|
|
</svg>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Hunde-Foto-Zeile wenn mehrere Hunde Fotos haben -->
|
|
${_dogPhotoRow(dogs)}
|
|
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function _dogPhotoRow(dogs) {
|
|
const withPhotos = dogs.filter(d => d.foto_url);
|
|
if (withPhotos.length < 2) return ''; // 1 Foto schon im Avatar, < 2 lohnt sich nicht
|
|
return `
|
|
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-3);
|
|
padding-top:var(--space-3);border-top:1px solid var(--c-border)">
|
|
${withPhotos.slice(0, 4).map(d => `
|
|
<div style="text-align:center">
|
|
<img src="${_esc(d.foto_url)}" alt="${_esc(d.name)}"
|
|
style="width:44px;height:44px;border-radius:50%;object-fit:cover;
|
|
border:2px solid var(--c-surface)">
|
|
<div style="font-size:10px;color:var(--c-text-muted);margin-top:2px;
|
|
max-width:44px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">
|
|
${_esc(d.name)}
|
|
</div>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// MINI-PROFIL MODAL
|
|
// ----------------------------------------------------------
|
|
function _showProfile(friendId, friendName, dogs, profile = {}) {
|
|
const dogsHTML = dogs.length
|
|
? `<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(100px,1fr));
|
|
gap:var(--space-3);margin-top:var(--space-4)">
|
|
${dogs.map(d => `
|
|
<div style="text-align:center">
|
|
${d.foto_url
|
|
? `<img src="${_esc(d.foto_url)}" alt="${_esc(d.name)}"
|
|
style="width:72px;height:72px;border-radius:50%;object-fit:cover;
|
|
border:2px solid var(--c-primary);margin-bottom:var(--space-2)">`
|
|
: `<div style="width:72px;height:72px;border-radius:50%;
|
|
background:var(--c-surface-2);
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-size:1.75rem;margin:0 auto var(--space-2)">🐕</div>`
|
|
}
|
|
<div style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
|
color:var(--c-text)">${_esc(d.name)}</div>
|
|
${d.rasse
|
|
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary)">${_esc(d.rasse)}</div>`
|
|
: ''}
|
|
</div>
|
|
`).join('')}
|
|
</div>`
|
|
: `<p style="font-size:var(--text-sm);color:var(--c-text-muted);margin-top:var(--space-4)">
|
|
Noch kein Hund eingetragen.
|
|
</p>`;
|
|
|
|
const profileInfoHTML = (() => {
|
|
const parts = [];
|
|
if (profile.wohnort) {
|
|
parts.push(`<div style="display:flex;align-items:center;gap:var(--space-2);
|
|
font-size:var(--text-sm);color:var(--c-text-secondary)">
|
|
📍 ${_esc(profile.wohnort)}
|
|
</div>`);
|
|
}
|
|
if (profile.erfahrung && _erfahrungBadge[profile.erfahrung]) {
|
|
parts.push(`<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
|
${_erfahrungBadge[profile.erfahrung]}
|
|
</div>`);
|
|
}
|
|
if (profile.bio && profile.profil_sichtbarkeit !== 'private') {
|
|
parts.push(`<div style="font-size:var(--text-sm);color:var(--c-text);
|
|
line-height:1.5;padding-top:var(--space-2)">
|
|
${_esc(profile.bio)}
|
|
</div>`);
|
|
}
|
|
if (profile.social_link) {
|
|
parts.push(`<div style="font-size:var(--text-xs);word-break:break-all">
|
|
<a href="${_esc(profile.social_link)}" target="_blank" rel="noopener noreferrer"
|
|
style="color:var(--c-primary)">${_esc(profile.social_link)}</a>
|
|
</div>`);
|
|
}
|
|
if (!parts.length) return '';
|
|
return `<div style="display:flex;flex-direction:column;gap:var(--space-2);
|
|
margin-bottom:var(--space-4)">${parts.join('')}</div>`;
|
|
})();
|
|
|
|
UI.modal.open({
|
|
title: _esc(friendName),
|
|
body: `
|
|
<div>
|
|
${profileInfoHTML}
|
|
<div class="by-section-label">${dogs.length === 1 ? 'Hund' : 'Hunde'}</div>
|
|
${dogsHTML}
|
|
</div>
|
|
`,
|
|
footer: `
|
|
<button class="btn btn-primary" id="modal-chat-btn" form="">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#chat-circle-dots"></use></svg>
|
|
Nachricht schreiben
|
|
</button>
|
|
<button class="btn btn-ghost" id="modal-remove-btn" form=""
|
|
style="color:var(--c-danger)">
|
|
<svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor.svg#user-minus"></use></svg>
|
|
Entfernen
|
|
</button>
|
|
`,
|
|
});
|
|
|
|
document.getElementById('modal-chat-btn')?.addEventListener('click', () => {
|
|
UI.modal.close();
|
|
_openChat(friendId);
|
|
});
|
|
document.getElementById('modal-remove-btn')?.addEventListener('click', async () => {
|
|
UI.modal.close();
|
|
await _removeFriend(friendId, friendName);
|
|
});
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// SUCHE
|
|
// ----------------------------------------------------------
|
|
async function _doSearch(q) {
|
|
if (!_appState.user) return;
|
|
const el = _container.querySelector('#fr-search-results');
|
|
try {
|
|
const results = await API.friends.search(q);
|
|
if (!results.length) {
|
|
el.innerHTML = `
|
|
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-4);
|
|
text-align:center;color:var(--c-text-muted);
|
|
font-size:var(--text-sm)">
|
|
Kein Nutzer gefunden.
|
|
</div>`;
|
|
return;
|
|
}
|
|
el.innerHTML = `
|
|
<div class="card" style="margin-bottom:var(--space-4);overflow:hidden">
|
|
${results.map((u, i) => `
|
|
<div style="display:flex;align-items:center;gap:var(--space-3);
|
|
padding:var(--space-3) var(--space-4);
|
|
${i < results.length - 1 ? 'border-bottom:1px solid var(--c-border)' : ''}">
|
|
${_userAvatar(u.name, null, u.avatar_url)}
|
|
<div style="flex:1;min-width:0">
|
|
<div style="display:flex;align-items:center;flex-wrap:wrap;gap:2px;
|
|
margin-bottom:2px">
|
|
<span style="font-size:var(--text-sm);font-weight:var(--weight-semibold);
|
|
color:var(--c-text)">${_esc(u.name)}</span>
|
|
${_erfahrungSpan(u.erfahrung)}
|
|
</div>
|
|
${_wohnortLine(u.wohnort)}
|
|
${_bioLine(u.bio, u.profil_sichtbarkeit)}
|
|
${u.dogs?.length
|
|
? `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
|
margin-top:2px">
|
|
${u.dogs.map(d => _esc(d.name) + (d.rasse ? ` · ${_esc(d.rasse)}` : '')).join(' | ')}
|
|
</div>`
|
|
: ''}
|
|
</div>
|
|
<button class="btn btn-primary btn-sm fr-add-btn" title="Anfrage senden"
|
|
data-user-id="${u.id}" data-user-name="${_esc(u.name)}">
|
|
<svg class="ph-icon"><use href="/icons/phosphor.svg#user-plus"></use></svg>
|
|
</button>
|
|
</div>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
|
|
el.querySelectorAll('.fr-add-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => _sendRequest(parseInt(btn.dataset.userId), btn));
|
|
});
|
|
|
|
} catch { el.innerHTML = ''; }
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// AKTIONEN
|
|
// ----------------------------------------------------------
|
|
async function _sendRequest(userId, btn) {
|
|
if (!_appState.user) { App.navigate('settings'); return; }
|
|
btn.disabled = true;
|
|
btn.innerHTML = `<svg class="ph-icon"><use href="/icons/phosphor.svg#spinner"></use></svg>`;
|
|
try {
|
|
await API.friends.sendRequest(userId);
|
|
UI.toast.success('Freundschaftsanfrage gesendet!');
|
|
_container.querySelector('#fr-search').value = '';
|
|
_container.querySelector('#fr-search-results').innerHTML = '';
|
|
await _loadFriends();
|
|
} catch (e) {
|
|
UI.toast.error(e.message || 'Fehler beim Senden.');
|
|
btn.disabled = false;
|
|
btn.innerHTML = `<svg class="ph-icon"><use href="/icons/phosphor.svg#user-plus"></use></svg> Anfrage`;
|
|
}
|
|
}
|
|
|
|
async function _accept(id) {
|
|
try {
|
|
await API.friends.accept(id);
|
|
UI.toast.success('Freundschaft angenommen!');
|
|
await _loadFriends();
|
|
} catch (e) { UI.toast.error(e.message); }
|
|
}
|
|
|
|
async function _decline(id) {
|
|
try {
|
|
await API.friends.decline(id);
|
|
await _loadFriends();
|
|
} catch (e) { UI.toast.error(e.message); }
|
|
}
|
|
|
|
async function _cancel(id) {
|
|
try {
|
|
await API.friends.decline(id);
|
|
await _loadFriends();
|
|
} catch (e) { UI.toast.error(e.message); }
|
|
}
|
|
|
|
async function _removeFriend(userId, name) {
|
|
const ok = await UI.modal.confirm({
|
|
title: 'Freund entfernen?',
|
|
message: `${name} wird aus deiner Freundesliste entfernt.`,
|
|
confirmText: 'Entfernen',
|
|
});
|
|
if (!ok) return;
|
|
try {
|
|
await API.friends.remove(userId);
|
|
UI.toast.info('Freund entfernt.');
|
|
await _loadFriends();
|
|
} catch (e) { UI.toast.error(e.message); }
|
|
}
|
|
|
|
async function _openChat(userId) {
|
|
if (!_appState.user) { App.navigate('settings'); return; }
|
|
try {
|
|
const { conversation_id } = await API.chat.start(userId);
|
|
App.navigate('chat', true, { conversation_id });
|
|
} catch (e) { UI.toast.error(e.message); }
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
// RENDER-HELPERS
|
|
// ----------------------------------------------------------
|
|
function _userAvatar(name, firstDog, avatarUrl) {
|
|
if (avatarUrl) {
|
|
return `<img src="${_esc(avatarUrl)}" alt="${_esc(name)}"
|
|
style="width:44px;height:44px;border-radius:50%;object-fit:cover;
|
|
border:2px solid var(--c-primary);flex-shrink:0">`;
|
|
}
|
|
if (firstDog?.foto_url) {
|
|
return `<img src="${_esc(firstDog.foto_url)}" alt="${_esc(firstDog.name)}"
|
|
style="width:44px;height:44px;border-radius:50%;object-fit:cover;
|
|
border:2px solid var(--c-primary);flex-shrink:0">`;
|
|
}
|
|
return `
|
|
<div style="width:44px;height:44px;border-radius:50%;flex-shrink:0;
|
|
background:var(--c-primary-subtle);
|
|
border:2px solid var(--c-primary);
|
|
display:flex;align-items:center;justify-content:center;
|
|
font-weight:var(--weight-bold);color:var(--c-primary)">
|
|
${_esc((name || '?')[0].toUpperCase())}
|
|
</div>`;
|
|
}
|
|
|
|
const _erfahrungBadge = {
|
|
einsteiger: '🐾 Einsteiger',
|
|
erfahren: '⭐ Erfahren',
|
|
trainer: '🎓 Trainer',
|
|
zuechter: '🏅 Züchter',
|
|
};
|
|
|
|
function _erfahrungSpan(erfahrung) {
|
|
if (!erfahrung || !_erfahrungBadge[erfahrung]) return '';
|
|
return `<span style="font-size:10px;padding:1px 5px;border-radius:3px;
|
|
background:var(--c-surface-2);color:var(--c-text-secondary);
|
|
margin-left:4px;white-space:nowrap">${_erfahrungBadge[erfahrung]}</span>`;
|
|
}
|
|
|
|
function _wohnortLine(wohnort) {
|
|
if (!wohnort) return '';
|
|
return `<span style="font-size:var(--text-xs);color:var(--c-text-muted)">📍 ${_esc(wohnort)}</span>`;
|
|
}
|
|
|
|
function _bioLine(bio, sichtbarkeit) {
|
|
if (!bio || sichtbarkeit === 'private') return '';
|
|
const text = bio.length > 120 ? bio.substring(0, 120) + '…' : bio;
|
|
return `<div style="font-size:var(--text-xs);color:var(--c-text-secondary);
|
|
margin-top:var(--space-1);line-height:1.4;
|
|
overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;
|
|
-webkit-box-orient:vertical">${_esc(text)}</div>`;
|
|
}
|
|
|
|
function _dogPills(dogs, max) {
|
|
if (!dogs?.length) return '';
|
|
const visible = dogs.slice(0, max);
|
|
const rest = dogs.length - max;
|
|
return `
|
|
<div style="display:flex;flex-wrap:wrap;gap:4px;align-items:center">
|
|
${visible.map(d => `
|
|
<span style="font-size:10px;padding:1px 6px;border-radius:var(--radius-full);
|
|
background:var(--c-surface-2);color:var(--c-text-secondary)">
|
|
🐕 ${_esc(d.name)}${d.rasse ? ` · ${_esc(d.rasse)}` : ''}
|
|
</span>
|
|
`).join('')}
|
|
${rest > 0 ? `<span style="font-size:10px;color:var(--c-text-muted)">+${rest}</span>` : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
function _esc(s) {
|
|
if (!s) return '';
|
|
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
|
}
|
|
|
|
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>`;
|
|
}
|
|
|
|
// ----------------------------------------------------------
|
|
return { init, refresh, onDogChange, _accept, _decline, _cancel, _removeFriend, _openChat };
|
|
|
|
})();
|