Sprint 14: Profil-Editor in Settings — Avatar-Upload, Bio, Wohnort, Erfahrung, Social-Link, Sichtbarkeit
This commit is contained in:
parent
41c4ba3dd6
commit
a60db21782
1 changed files with 197 additions and 3 deletions
|
|
@ -38,17 +38,51 @@ window.Page_settings = (() => {
|
||||||
// ----------------------------------------------------------
|
// ----------------------------------------------------------
|
||||||
function _renderAccount() {
|
function _renderAccount() {
|
||||||
const u = _appState.user;
|
const u = _appState.user;
|
||||||
|
|
||||||
|
// Avatar: Bild oder Buchstabe
|
||||||
|
const avatarInner = u.avatar_url
|
||||||
|
? `<img src="${_esc(u.avatar_url)}" alt="Avatar"
|
||||||
|
style="width:56px;height:56px;border-radius:50%;object-fit:cover;display:block">`
|
||||||
|
: _esc(u.name.charAt(0).toUpperCase());
|
||||||
|
|
||||||
|
// Mitglied seit
|
||||||
|
const memberSince = (() => {
|
||||||
|
if (!u.created_at) return '';
|
||||||
|
const d = new Date(u.created_at);
|
||||||
|
return d.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' });
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Erfahrungs-Labels
|
||||||
|
const erfahrungLabel = {
|
||||||
|
einsteiger: 'Einsteiger (erster Hund)',
|
||||||
|
erfahren: 'Erfahrener Hundehalter',
|
||||||
|
trainer: 'Trainer / Ausbilder',
|
||||||
|
zuechter: 'Züchter',
|
||||||
|
};
|
||||||
|
|
||||||
_container.innerHTML = `
|
_container.innerHTML = `
|
||||||
<div style="max-width:400px;margin:0 auto;padding:var(--space-4) 0">
|
<div style="max-width:400px;margin:0 auto;padding:var(--space-4) 0">
|
||||||
|
|
||||||
<div class="card" style="padding:var(--space-5);margin-bottom:var(--space-4)">
|
<div class="card" style="padding:var(--space-5);margin-bottom:var(--space-4)">
|
||||||
<div style="display:flex;align-items:center;gap:var(--space-4)">
|
<div style="display:flex;align-items:center;gap:var(--space-4)">
|
||||||
<div style="width:56px;height:56px;border-radius:50%;
|
<div id="settings-avatar-btn"
|
||||||
|
style="width:56px;height:56px;border-radius:50%;
|
||||||
background:var(--c-primary);color:#fff;
|
background:var(--c-primary);color:#fff;
|
||||||
display:flex;align-items:center;justify-content:center;
|
display:flex;align-items:center;justify-content:center;
|
||||||
font-size:1.5rem;font-weight:700;flex-shrink:0">
|
font-size:1.5rem;font-weight:700;flex-shrink:0;
|
||||||
${_esc(u.name.charAt(0).toUpperCase())}
|
cursor:pointer;overflow:hidden;position:relative">
|
||||||
|
${avatarInner}
|
||||||
|
<div style="position:absolute;inset:0;background:rgba(0,0,0,0.25);
|
||||||
|
display:flex;align-items:center;justify-content:center;
|
||||||
|
opacity:0;transition:opacity .15s"
|
||||||
|
class="avatar-overlay">
|
||||||
|
<svg style="width:20px;height:20px;color:#fff" fill="currentColor" viewBox="0 0 256 256">
|
||||||
|
<use href="/icons/phosphor.svg#camera"></use>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="file" id="settings-avatar-input" accept="image/*"
|
||||||
|
style="display:none">
|
||||||
<div>
|
<div>
|
||||||
<div style="font-weight:700;font-size:var(--text-lg)">${_esc(u.name)}</div>
|
<div style="font-weight:700;font-size:var(--text-lg)">${_esc(u.name)}</div>
|
||||||
<div style="color:var(--c-text-secondary);font-size:var(--text-sm)">${_esc(u.email)}</div>
|
<div style="color:var(--c-text-secondary);font-size:var(--text-sm)">${_esc(u.email)}</div>
|
||||||
|
|
@ -64,6 +98,53 @@ window.Page_settings = (() => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Mein Profil -->
|
||||||
|
<div class="card" style="margin-bottom:var(--space-4)">
|
||||||
|
<div style="padding:var(--space-3) var(--space-4);
|
||||||
|
font-size:var(--text-xs);font-weight:600;
|
||||||
|
color:var(--c-text-secondary);text-transform:uppercase;
|
||||||
|
letter-spacing:0.05em;border-bottom:1px solid var(--c-border);
|
||||||
|
display:flex;align-items:center;justify-content:space-between">
|
||||||
|
<span>Mein Profil</span>
|
||||||
|
<button id="settings-profile-edit-btn"
|
||||||
|
class="btn btn-ghost"
|
||||||
|
style="font-size:var(--text-xs);padding:var(--space-1) var(--space-2)">
|
||||||
|
Profil bearbeiten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div style="padding:var(--space-4);display:flex;flex-direction:column;gap:var(--space-3)">
|
||||||
|
${memberSince
|
||||||
|
? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||||
|
Mitglied seit ${_esc(memberSince)}
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
|
${u.bio
|
||||||
|
? `<div style="font-size:var(--text-sm)">${_esc(u.bio)}</div>`
|
||||||
|
: ''}
|
||||||
|
${u.wohnort
|
||||||
|
? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||||
|
📍 ${_esc(u.wohnort)}
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
|
${u.erfahrung && erfahrungLabel[u.erfahrung]
|
||||||
|
? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||||
|
${_esc(erfahrungLabel[u.erfahrung])}
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
|
${u.social_link
|
||||||
|
? `<div style="font-size:var(--text-sm)">
|
||||||
|
<a href="${_esc(u.social_link)}" target="_blank" rel="noopener"
|
||||||
|
style="color:var(--c-primary)">${_esc(u.social_link)}</a>
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
|
${!u.bio && !u.wohnort && !u.erfahrung && !u.social_link
|
||||||
|
? `<div style="font-size:var(--text-sm);color:var(--c-text-secondary)">
|
||||||
|
Noch kein Profil ausgefüllt.
|
||||||
|
</div>`
|
||||||
|
: ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card" style="margin-bottom:var(--space-4)">
|
<div class="card" style="margin-bottom:var(--space-4)">
|
||||||
<div class="card-body" style="padding:0">
|
<div class="card-body" style="padding:0">
|
||||||
<div class="sidebar-item" data-page="dog-profile"
|
<div class="sidebar-item" data-page="dog-profile"
|
||||||
|
|
@ -147,6 +228,119 @@ window.Page_settings = (() => {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Avatar-Hover-Overlay
|
||||||
|
const avatarBtn = document.getElementById('settings-avatar-btn');
|
||||||
|
const avatarOverlay = avatarBtn?.querySelector('.avatar-overlay');
|
||||||
|
if (avatarBtn && avatarOverlay) {
|
||||||
|
avatarBtn.addEventListener('mouseenter', () => { avatarOverlay.style.opacity = '1'; });
|
||||||
|
avatarBtn.addEventListener('mouseleave', () => { avatarOverlay.style.opacity = '0'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avatar-Upload
|
||||||
|
avatarBtn?.addEventListener('click', () => {
|
||||||
|
document.getElementById('settings-avatar-input')?.click();
|
||||||
|
});
|
||||||
|
document.getElementById('settings-avatar-input')?.addEventListener('change', async e => {
|
||||||
|
const file = e.target.files?.[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append('file', file);
|
||||||
|
const res = await API.post('/api/profile/avatar', fd);
|
||||||
|
_appState.user.avatar_url = res.avatar_url;
|
||||||
|
UI.toast.success('Avatar aktualisiert.');
|
||||||
|
_render();
|
||||||
|
} catch {
|
||||||
|
UI.toast.error('Avatar-Upload fehlgeschlagen.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Profil bearbeiten
|
||||||
|
document.getElementById('settings-profile-edit-btn')?.addEventListener('click', () => {
|
||||||
|
const u = _appState.user;
|
||||||
|
const inputStyle = `width:100%;box-sizing:border-box;padding:var(--space-2) var(--space-3);
|
||||||
|
border:1.5px solid var(--c-border);border-radius:var(--radius-md);
|
||||||
|
font-size:var(--text-sm);font-family:inherit;
|
||||||
|
background:var(--c-surface);color:var(--c-text)`;
|
||||||
|
|
||||||
|
const erfahrungOpts = [
|
||||||
|
['', 'Bitte wählen...'],
|
||||||
|
['einsteiger', 'Einsteiger (erster Hund)'],
|
||||||
|
['erfahren', 'Erfahrener Hundehalter'],
|
||||||
|
['trainer', 'Trainer / Ausbilder'],
|
||||||
|
['zuechter', 'Züchter'],
|
||||||
|
].map(([val, label]) =>
|
||||||
|
`<option value="${_esc(val)}" ${u.erfahrung === val ? 'selected' : ''}>${_esc(label)}</option>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
const sichtbarkeitOpts = [
|
||||||
|
['public', 'Öffentlich'],
|
||||||
|
['friends', 'Nur Freunde'],
|
||||||
|
['private', 'Privat'],
|
||||||
|
].map(([val, label]) =>
|
||||||
|
`<option value="${_esc(val)}" ${(u.profil_sichtbarkeit || 'public') === val ? 'selected' : ''}>${_esc(label)}</option>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
UI.modal.open({
|
||||||
|
title: 'Profil bearbeiten',
|
||||||
|
body: `
|
||||||
|
<form id="profile-form" style="display:flex;flex-direction:column;gap:var(--space-4)">
|
||||||
|
<div>
|
||||||
|
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Bio</label>
|
||||||
|
<textarea name="bio" maxlength="300" rows="4"
|
||||||
|
placeholder="Kurze Vorstellung (max. 300 Zeichen)"
|
||||||
|
style="${inputStyle};resize:vertical">${_esc(u.bio || '')}</textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Wohnort</label>
|
||||||
|
<input name="wohnort" type="text" maxlength="60"
|
||||||
|
placeholder="z.B. München"
|
||||||
|
value="${_esc(u.wohnort || '')}"
|
||||||
|
style="${inputStyle}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Erfahrung</label>
|
||||||
|
<select name="erfahrung" style="${inputStyle}">${erfahrungOpts}</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Social-Link</label>
|
||||||
|
<input name="social_link" type="url" maxlength="120"
|
||||||
|
placeholder="https://instagram.com/dein-hundeaccount"
|
||||||
|
value="${_esc(u.social_link || '')}"
|
||||||
|
style="${inputStyle}">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="display:block;font-size:var(--text-sm);font-weight:600;margin-bottom:var(--space-1)">Profil-Sichtbarkeit</label>
|
||||||
|
<select name="profil_sichtbarkeit" style="${inputStyle}">${sichtbarkeitOpts}</select>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
`,
|
||||||
|
footer: `
|
||||||
|
<button type="submit" form="profile-form" class="btn btn-primary">Speichern</button>
|
||||||
|
<button type="button" class="btn btn-ghost" data-modal-close>Abbrechen</button>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('profile-form')?.addEventListener('submit', async e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const btn = document.querySelector('[form="profile-form"]');
|
||||||
|
const fd = UI.formData(e.target);
|
||||||
|
await UI.asyncButton(btn, async () => {
|
||||||
|
const updated = await API.patch('/api/profile', {
|
||||||
|
bio: fd.bio || '',
|
||||||
|
wohnort: fd.wohnort || '',
|
||||||
|
erfahrung: fd.erfahrung || '',
|
||||||
|
social_link: fd.social_link || '',
|
||||||
|
profil_sichtbarkeit: fd.profil_sichtbarkeit || 'public',
|
||||||
|
});
|
||||||
|
Object.assign(_appState.user, updated);
|
||||||
|
UI.modal.close?.();
|
||||||
|
UI.toast.success('Profil gespeichert.');
|
||||||
|
_render();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('settings-logout-btn')?.addEventListener('click', async () => {
|
document.getElementById('settings-logout-btn')?.addEventListener('click', async () => {
|
||||||
const ok = await UI.modal.confirm({
|
const ok = await UI.modal.confirm({
|
||||||
title : 'Abmelden?',
|
title : 'Abmelden?',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue