Compare commits
No commits in common. "ea670310e26eda9d0dcf4c5d04a6ee030fe9c187" and "cc36ead720887e6322e3be27c3c9484f80251989" have entirely different histories.
ea670310e2
...
cc36ead720
4 changed files with 4 additions and 705 deletions
5
Makefile
5
Makefile
|
|
@ -9,8 +9,7 @@ DS_IP := 10.47.11.10
|
||||||
# Hinweis: NPM braucht 10.47.11.99 als Forward-IP (Macvlan-Shim), nicht .10
|
# Hinweis: NPM braucht 10.47.11.99 als Forward-IP (Macvlan-Shim), nicht .10
|
||||||
DS_SSH_PORT := 22
|
DS_SSH_PORT := 22
|
||||||
DS_PATH := /volume1/docker/ban-yaro
|
DS_PATH := /volume1/docker/ban-yaro
|
||||||
CONTAINER := ban-yaro # container_name (für docker logs/exec)
|
CONTAINER := ban-yaro
|
||||||
SERVICE := banyaro # service-name in docker-compose.yml (für docker compose restart)
|
|
||||||
GIT_REMOTE := origin
|
GIT_REMOTE := origin
|
||||||
DOCKER := sudo /usr/local/bin/docker
|
DOCKER := sudo /usr/local/bin/docker
|
||||||
|
|
||||||
|
|
@ -108,7 +107,7 @@ push:
|
||||||
restart: check-ssh
|
restart: check-ssh
|
||||||
@ssh $(DS_HOST) " \
|
@ssh $(DS_HOST) " \
|
||||||
cd $(DS_PATH) && \
|
cd $(DS_PATH) && \
|
||||||
$(DOCKER) compose restart $(SERVICE)"
|
$(DOCKER) compose restart $(CONTAINER)"
|
||||||
@echo " ✓ Neugestartet."
|
@echo " ✓ Neugestartet."
|
||||||
|
|
||||||
# ----------------------------------------------------------
|
# ----------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -214,7 +214,8 @@ const App = (() => {
|
||||||
|
|
||||||
function _onLoggedOut() {
|
function _onLoggedOut() {
|
||||||
state.user = null;
|
state.user = null;
|
||||||
navigate('settings', false);
|
// Zeige Login wenn nötig
|
||||||
|
// Für MVP: direkte Weiterleitung zu Einstellungen/Login
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _loadDogs() {
|
async function _loadDogs() {
|
||||||
|
|
|
||||||
|
|
@ -1,395 +0,0 @@
|
||||||
/* ============================================================
|
|
||||||
BAN YARO — Hunde-Profil
|
|
||||||
Seiten-Modul: Profil anlegen / anzeigen / bearbeiten.
|
|
||||||
============================================================ */
|
|
||||||
|
|
||||||
window.Page_dog_profile = (() => {
|
|
||||||
|
|
||||||
let _container = null;
|
|
||||||
let _appState = null;
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// INIT / REFRESH / LIFECYCLE
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
async function init(container, appState) {
|
|
||||||
_container = container;
|
|
||||||
_appState = appState;
|
|
||||||
await _render();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refresh() {
|
|
||||||
await _render();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onDogChange(dog) {
|
|
||||||
await _render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// HAUPTRENDER
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
async function _render() {
|
|
||||||
if (!_appState.user) {
|
|
||||||
_container.innerHTML = UI.emptyState({
|
|
||||||
icon : '🐕',
|
|
||||||
title : 'Anmelden erforderlich',
|
|
||||||
text : 'Melde dich an, um ein Hundeprofil anzulegen.',
|
|
||||||
action: `<button class="btn btn-primary" id="profile-goto-login">Anmelden</button>`,
|
|
||||||
});
|
|
||||||
_container.querySelector('#profile-goto-login')
|
|
||||||
?.addEventListener('click', () => App.navigate('settings'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_appState.activeDog) {
|
|
||||||
_renderCreateForm();
|
|
||||||
} else {
|
|
||||||
_renderProfile(_appState.activeDog);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// PROFIL-ANSICHT
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _renderProfile(dog) {
|
|
||||||
const geburtstag = dog.geburtstag
|
|
||||||
? new Date(dog.geburtstag + 'T00:00:00')
|
|
||||||
.toLocaleDateString('de-DE', { day: 'numeric', month: 'long', year: 'numeric' })
|
|
||||||
: null;
|
|
||||||
|
|
||||||
_container.innerHTML = `
|
|
||||||
<div style="text-align:center;padding:var(--space-6) var(--space-2) var(--space-4)">
|
|
||||||
|
|
||||||
<!-- Profilfoto mit Upload-Button -->
|
|
||||||
<div style="position:relative;display:inline-block;margin-bottom:var(--space-4)">
|
|
||||||
${dog.foto_url
|
|
||||||
? `<img src="${dog.foto_url}" alt="${_esc(dog.name)}"
|
|
||||||
style="width:120px;height:120px;border-radius:50%;object-fit:cover;
|
|
||||||
border:3px solid var(--c-primary)">`
|
|
||||||
: `<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>`}
|
|
||||||
<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">
|
|
||||||
📷
|
|
||||||
<input type="file" id="dp-photo-input" accept="image/*"
|
|
||||||
capture="user" style="display:none">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Name + Rasse -->
|
|
||||||
<h2 style="font-size:var(--text-2xl);font-weight:700;
|
|
||||||
color:var(--c-text);margin:0 0 var(--space-1)">${_esc(dog.name)}</h2>
|
|
||||||
${dog.rasse
|
|
||||||
? `<p style="color:var(--c-text-secondary);margin:0 0 var(--space-5)">${_esc(dog.rasse)}</p>`
|
|
||||||
: `<p style="margin:0 0 var(--space-5)"></p>`}
|
|
||||||
|
|
||||||
<!-- Info-Grid -->
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3);
|
|
||||||
margin-bottom:var(--space-5);text-align:left">
|
|
||||||
${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>
|
|
||||||
<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)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${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>
|
|
||||||
<div style="font-weight:500;font-size:var(--text-sm)">
|
|
||||||
${dog.geschlecht === 'm' ? 'Rüde' : 'Hündin'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
${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>
|
|
||||||
<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>
|
|
||||||
<div style="font-size:var(--text-xs);font-weight:500;
|
|
||||||
word-break:break-all">${_esc(dog.chip_nr)}</div>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${dog.bio ? `
|
|
||||||
<div class="card" style="padding:var(--space-4);margin-bottom:var(--space-5);text-align:left">
|
|
||||||
<p style="margin:0;color:var(--c-text-secondary);font-style:italic;line-height:1.6">
|
|
||||||
"${_esc(dog.bio)}"
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
<button class="btn btn-primary w-full" id="dp-edit-btn">
|
|
||||||
Profil bearbeiten
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Foto hochladen
|
|
||||||
document.getElementById('dp-photo-input')?.addEventListener('change', async e => {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
try {
|
|
||||||
const fd = new FormData();
|
|
||||||
fd.append('file', file);
|
|
||||||
const result = await API.dogs.uploadPhoto(dog.id, fd);
|
|
||||||
// State in-place aktualisieren
|
|
||||||
dog.foto_url = result.foto_url;
|
|
||||||
_appState.activeDog = { ..._appState.activeDog, foto_url: result.foto_url };
|
|
||||||
_appState.dogs = _appState.dogs.map(d =>
|
|
||||||
d.id === dog.id ? _appState.activeDog : d
|
|
||||||
);
|
|
||||||
UI.toast.success('Foto gespeichert.');
|
|
||||||
_renderProfile(_appState.activeDog);
|
|
||||||
} catch (err) {
|
|
||||||
UI.toast.error(err.message || 'Fehler beim Hochladen.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bearbeiten öffnen
|
|
||||||
document.getElementById('dp-edit-btn')?.addEventListener('click', () => {
|
|
||||||
_openEditModal(dog);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// NEU ANLEGEN (direkt auf der Seite, kein Modal)
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _renderCreateForm() {
|
|
||||||
_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>
|
|
||||||
<h2 style="font-size:var(--text-xl);font-weight:700;margin:0 0 var(--space-2)">
|
|
||||||
Hund anlegen
|
|
||||||
</h2>
|
|
||||||
<p style="color:var(--c-text-secondary);margin:0">
|
|
||||||
Erstelle das Profil für deinen Hund.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
${_formHTML(null)}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
_bindForm(null, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// BEARBEITEN (Modal)
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _openEditModal(dog) {
|
|
||||||
UI.modal.open({ title: `${dog.name} bearbeiten`, body: _formHTML(dog) });
|
|
||||||
_bindForm(dog, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// FORMULAR HTML
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _formHTML(dog) {
|
|
||||||
const today = new Date().toISOString().slice(0, 10);
|
|
||||||
return `
|
|
||||||
<form id="dp-form" autocomplete="off">
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Name *</label>
|
|
||||||
<input class="form-control" type="text" name="name"
|
|
||||||
value="${_esc(dog?.name || '')}"
|
|
||||||
placeholder="z. B. Ban Yaro" required>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">
|
|
||||||
Rasse
|
|
||||||
<span style="color:var(--c-text-secondary)">(optional)</span>
|
|
||||||
</label>
|
|
||||||
<input class="form-control" type="text" name="rasse"
|
|
||||||
value="${_esc(dog?.rasse || '')}"
|
|
||||||
placeholder="z. B. Mischling, Golden Retriever…">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Geburtstag</label>
|
|
||||||
<input class="form-control" type="date" name="geburtstag"
|
|
||||||
value="${dog?.geburtstag || ''}" max="${today}">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Geschlecht</label>
|
|
||||||
<select class="form-control" name="geschlecht">
|
|
||||||
<option value="" ${!dog?.geschlecht ? 'selected' : ''}>–</option>
|
|
||||||
<option value="m" ${dog?.geschlecht === 'm' ? 'selected' : ''}>Rüde</option>
|
|
||||||
<option value="w" ${dog?.geschlecht === 'w' ? 'selected' : ''}>Hündin</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:var(--space-3)">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Gewicht (kg)</label>
|
|
||||||
<input class="form-control" type="number" name="gewicht_kg"
|
|
||||||
value="${dog?.gewicht_kg || ''}"
|
|
||||||
min="0.1" max="120" step="0.1" placeholder="z. B. 28.5">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Chip-Nummer</label>
|
|
||||||
<input class="form-control" type="text" name="chip_nr"
|
|
||||||
value="${_esc(dog?.chip_nr || '')}" placeholder="15-stellig">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">
|
|
||||||
Bio / Steckbrief
|
|
||||||
<span style="color:var(--c-text-secondary)">(optional)</span>
|
|
||||||
</label>
|
|
||||||
<textarea class="form-control" name="bio" rows="2"
|
|
||||||
placeholder="Kurze Beschreibung…">${_esc(dog?.bio || '')}</textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label"
|
|
||||||
style="display:flex;align-items:center;gap:var(--space-2);cursor:pointer">
|
|
||||||
<input type="checkbox" name="is_public" ${dog?.is_public ? 'checked' : ''}>
|
|
||||||
Öffentliches Profil (für NFC-Tag)
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="display:flex;gap:var(--space-2);margin-top:var(--space-4)">
|
|
||||||
${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'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${dog ? `
|
|
||||||
<div style="margin-top:var(--space-5);padding-top:var(--space-4);
|
|
||||||
border-top:1px solid var(--c-border);text-align:center">
|
|
||||||
<button type="button" class="btn btn-ghost btn-sm" id="dp-delete-btn"
|
|
||||||
style="color:var(--c-danger)">
|
|
||||||
${dog.name} löschen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
` : ''}
|
|
||||||
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// FORMULAR EVENTS
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _bindForm(dog, inModal) {
|
|
||||||
const form = document.getElementById('dp-form');
|
|
||||||
if (!form) return;
|
|
||||||
|
|
||||||
document.getElementById('dp-form-cancel')
|
|
||||||
?.addEventListener('click', UI.modal.close);
|
|
||||||
|
|
||||||
document.getElementById('dp-delete-btn')?.addEventListener('click', async () => {
|
|
||||||
const ok = await UI.modal.confirm({
|
|
||||||
title : `${dog.name} löschen?`,
|
|
||||||
message: 'Tagebuch-Einträge und Gesundheitsdaten werden ebenfalls gelöscht. Nicht rückgängig.',
|
|
||||||
confirmText: 'Löschen',
|
|
||||||
danger : true,
|
|
||||||
});
|
|
||||||
if (!ok) return;
|
|
||||||
try {
|
|
||||||
await API.dogs.delete(dog.id);
|
|
||||||
_appState.dogs = _appState.dogs.filter(d => d.id !== dog.id);
|
|
||||||
_appState.activeDog = _appState.dogs[0] || null;
|
|
||||||
if (inModal) UI.modal.close();
|
|
||||||
UI.toast.success(`${dog.name} wurde gelöscht.`);
|
|
||||||
await _render();
|
|
||||||
} catch (err) {
|
|
||||||
UI.toast.error(err.message || 'Fehler beim Löschen.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
form.addEventListener('submit', async e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const btn = form.querySelector('[type="submit"]');
|
|
||||||
const fd = UI.formData(form);
|
|
||||||
|
|
||||||
if (!fd.name?.trim()) {
|
|
||||||
UI.toast.warning('Bitte einen Namen eingeben.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await UI.asyncButton(btn, async () => {
|
|
||||||
const payload = {
|
|
||||||
name: fd.name.trim(),
|
|
||||||
rasse: fd.rasse || null,
|
|
||||||
geburtstag: fd.geburtstag || null,
|
|
||||||
geschlecht: fd.geschlecht || null,
|
|
||||||
gewicht_kg: fd.gewicht_kg ? parseFloat(fd.gewicht_kg) : null,
|
|
||||||
chip_nr: fd.chip_nr || null,
|
|
||||||
bio: fd.bio || null,
|
|
||||||
is_public: 'is_public' in fd,
|
|
||||||
};
|
|
||||||
|
|
||||||
let saved;
|
|
||||||
if (dog) {
|
|
||||||
saved = await API.dogs.update(dog.id, payload);
|
|
||||||
_appState.dogs = _appState.dogs.map(d => d.id === dog.id ? saved : d);
|
|
||||||
_appState.activeDog = saved;
|
|
||||||
if (inModal) UI.modal.close();
|
|
||||||
UI.toast.success('Profil gespeichert.');
|
|
||||||
} else {
|
|
||||||
saved = await API.dogs.create(payload);
|
|
||||||
_appState.dogs.push(saved);
|
|
||||||
_appState.activeDog = saved;
|
|
||||||
UI.toast.success(`${saved.name} wurde angelegt! 🎉`);
|
|
||||||
}
|
|
||||||
await _render();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// HELPER
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _calcAlter(geburtstag) {
|
|
||||||
const born = new Date(geburtstag + 'T00:00:00');
|
|
||||||
const tage = Math.floor((Date.now() - born) / 86400000);
|
|
||||||
if (tage < 0) return '';
|
|
||||||
if (tage < 30) return `${tage} Tag${tage !== 1 ? 'e' : ''} alt`;
|
|
||||||
if (tage < 365) {
|
|
||||||
const m = Math.floor(tage / 30);
|
|
||||||
return `${m} Monat${m !== 1 ? 'e' : ''} alt`;
|
|
||||||
}
|
|
||||||
const j = Math.floor(tage / 365);
|
|
||||||
const m = Math.floor((tage % 365) / 30);
|
|
||||||
return m > 0
|
|
||||||
? `${j} Jahr${j !== 1 ? 'e' : ''}, ${m} Monat${m !== 1 ? 'e' : ''} alt`
|
|
||||||
: `${j} Jahr${j !== 1 ? 'e' : ''} alt`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _esc(str) {
|
|
||||||
if (!str) return '';
|
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// PUBLIC
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
return { init, refresh, onDogChange };
|
|
||||||
|
|
||||||
})();
|
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
/* ============================================================
|
|
||||||
BAN YARO — Einstellungen / Account
|
|
||||||
Login, Registrierung, Logout, Account-Info.
|
|
||||||
============================================================ */
|
|
||||||
|
|
||||||
window.Page_settings = (() => {
|
|
||||||
|
|
||||||
let _container = null;
|
|
||||||
let _appState = null;
|
|
||||||
let _mode = 'login'; // 'login' | 'register'
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// INIT / REFRESH
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
async function init(container, appState) {
|
|
||||||
_container = container;
|
|
||||||
_appState = appState;
|
|
||||||
_render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function refresh() {
|
|
||||||
_render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// RENDER
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _render() {
|
|
||||||
if (_appState.user) {
|
|
||||||
_renderAccount();
|
|
||||||
} else {
|
|
||||||
_renderAuth(_mode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// EINGELOGGT — Account-Übersicht
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _renderAccount() {
|
|
||||||
const u = _appState.user;
|
|
||||||
_container.innerHTML = `
|
|
||||||
<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 style="display:flex;align-items:center;gap:var(--space-4)">
|
|
||||||
<div style="width:56px;height:56px;border-radius:50%;
|
|
||||||
background:var(--c-primary);color:#fff;
|
|
||||||
display:flex;align-items:center;justify-content:center;
|
|
||||||
font-size:1.5rem;font-weight:700;flex-shrink:0">
|
|
||||||
${_esc(u.name.charAt(0).toUpperCase())}
|
|
||||||
</div>
|
|
||||||
<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>
|
|
||||||
${u.is_premium
|
|
||||||
? `<span class="badge badge-primary" style="margin-top:var(--space-1)">
|
|
||||||
⭐ Ban Yaro Plus
|
|
||||||
</span>`
|
|
||||||
: `<span class="badge" style="margin-top:var(--space-1);
|
|
||||||
color:var(--c-text-secondary)">
|
|
||||||
Kostenlos
|
|
||||||
</span>`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card" style="margin-bottom:var(--space-4)">
|
|
||||||
<div class="card-body" style="padding:0">
|
|
||||||
<div class="sidebar-item" data-page="dog-profile"
|
|
||||||
style="padding:var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border)">
|
|
||||||
<span>🐕</span>
|
|
||||||
<span>Hunde-Profile</span>
|
|
||||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-item" id="settings-push-btn"
|
|
||||||
style="padding:var(--space-4);border-radius:0;border-bottom:1px solid var(--c-border)">
|
|
||||||
<span>🔔</span>
|
|
||||||
<span>Push-Benachrichtigungen</span>
|
|
||||||
<span style="margin-left:auto;color:var(--c-text-secondary)">›</span>
|
|
||||||
</div>
|
|
||||||
<div class="sidebar-item" id="settings-logout-btn"
|
|
||||||
style="padding:var(--space-4);border-radius:0;cursor:pointer;
|
|
||||||
color:var(--c-danger)">
|
|
||||||
<span>🚪</span>
|
|
||||||
<span>Abmelden</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="text-align:center;color:var(--c-text-secondary);
|
|
||||||
font-size:var(--text-xs)">
|
|
||||||
Ban Yaro · banyaro.app<br>
|
|
||||||
Deine Daten liegen auf einem eigenen Server in Deutschland.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('settings-logout-btn')?.addEventListener('click', async () => {
|
|
||||||
const ok = await UI.modal.confirm({
|
|
||||||
title : 'Abmelden?',
|
|
||||||
message: 'Du wirst aus deinem Konto abgemeldet.',
|
|
||||||
confirmText: 'Abmelden',
|
|
||||||
});
|
|
||||||
if (!ok) return;
|
|
||||||
try {
|
|
||||||
await API.auth.logout();
|
|
||||||
} catch { /* cookie wird trotzdem gelöscht */ }
|
|
||||||
_appState.user = null;
|
|
||||||
_appState.dogs = [];
|
|
||||||
_appState.activeDog = null;
|
|
||||||
UI.toast.info('Du wurdest abgemeldet.');
|
|
||||||
_render();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('settings-push-btn')?.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
await API.subscribeToPush();
|
|
||||||
UI.toast.success('Push-Benachrichtigungen aktiviert.');
|
|
||||||
} catch {
|
|
||||||
UI.toast.warning('Push-Benachrichtigungen konnten nicht aktiviert werden.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// NICHT EINGELOGGT — Login / Registrierung
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _renderAuth(mode) {
|
|
||||||
_mode = mode;
|
|
||||||
_container.innerHTML = `
|
|
||||||
<div style="max-width:380px;margin:0 auto;padding:var(--space-6) var(--space-4)">
|
|
||||||
|
|
||||||
<!-- Logo -->
|
|
||||||
<div style="text-align:center;margin-bottom:var(--space-6)">
|
|
||||||
<img src="/icons/icon-180.png" alt="Ban Yaro"
|
|
||||||
style="width:72px;height:72px;border-radius:var(--radius-lg);
|
|
||||||
margin-bottom:var(--space-3)">
|
|
||||||
<h1 style="font-size:var(--text-2xl);font-weight:700;margin:0">Ban Yaro</h1>
|
|
||||||
<p style="color:var(--c-text-secondary);margin:var(--space-1) 0 0">
|
|
||||||
Alles rund um deinen Hund
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tab-Toggle -->
|
|
||||||
<div style="display:flex;background:var(--c-surface-2);
|
|
||||||
border-radius:var(--radius-md);padding:3px;
|
|
||||||
margin-bottom:var(--space-5)">
|
|
||||||
<button id="tab-login"
|
|
||||||
class="btn flex-1 ${mode === 'login' ? 'btn-primary' : 'btn-ghost'}"
|
|
||||||
style="border-radius:calc(var(--radius-md) - 2px)">
|
|
||||||
Anmelden
|
|
||||||
</button>
|
|
||||||
<button id="tab-register"
|
|
||||||
class="btn flex-1 ${mode === 'register' ? 'btn-primary' : 'btn-ghost'}"
|
|
||||||
style="border-radius:calc(var(--radius-md) - 2px)">
|
|
||||||
Registrieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${mode === 'login' ? _loginFormHTML() : _registerFormHTML()}
|
|
||||||
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('tab-login')
|
|
||||||
?.addEventListener('click', () => _renderAuth('login'));
|
|
||||||
document.getElementById('tab-register')
|
|
||||||
?.addEventListener('click', () => _renderAuth('register'));
|
|
||||||
|
|
||||||
if (mode === 'login') {
|
|
||||||
_bindLoginForm();
|
|
||||||
} else {
|
|
||||||
_bindRegisterForm();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function _loginFormHTML() {
|
|
||||||
return `
|
|
||||||
<form id="auth-form" autocomplete="on" novalidate>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">E-Mail</label>
|
|
||||||
<input class="form-control" type="email" name="email"
|
|
||||||
placeholder="deine@email.de" autocomplete="email" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Passwort</label>
|
|
||||||
<input class="form-control" type="password" name="password"
|
|
||||||
placeholder="Passwort" autocomplete="current-password" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-full" style="margin-top:var(--space-2)">
|
|
||||||
Anmelden
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _registerFormHTML() {
|
|
||||||
return `
|
|
||||||
<form id="auth-form" autocomplete="on" novalidate>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Dein Name</label>
|
|
||||||
<input class="form-control" type="text" name="name"
|
|
||||||
placeholder="z. B. Maria" autocomplete="name" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">E-Mail</label>
|
|
||||||
<input class="form-control" type="email" name="email"
|
|
||||||
placeholder="deine@email.de" autocomplete="email" required>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Passwort</label>
|
|
||||||
<input class="form-control" type="password" name="password"
|
|
||||||
placeholder="Mindestens 8 Zeichen" autocomplete="new-password"
|
|
||||||
minlength="8" required>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary w-full" style="margin-top:var(--space-2)">
|
|
||||||
Konto erstellen
|
|
||||||
</button>
|
|
||||||
<p style="text-align:center;font-size:var(--text-xs);
|
|
||||||
color:var(--c-text-secondary);margin-top:var(--space-3)">
|
|
||||||
Mit der Registrierung stimmst du unseren Datenschutzhinweisen zu.<br>
|
|
||||||
Deine Daten werden ausschließlich auf unserem Server gespeichert.
|
|
||||||
</p>
|
|
||||||
</form>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function _bindLoginForm() {
|
|
||||||
document.getElementById('auth-form')?.addEventListener('submit', async e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const btn = e.target.querySelector('[type="submit"]');
|
|
||||||
const fd = UI.formData(e.target);
|
|
||||||
|
|
||||||
await UI.asyncButton(btn, async () => {
|
|
||||||
const result = await API.auth.login(fd.email, fd.password);
|
|
||||||
localStorage.setItem('by_token', result.token);
|
|
||||||
|
|
||||||
// User-Daten laden
|
|
||||||
_appState.user = await API.auth.me();
|
|
||||||
document.getElementById('sidebar-username').textContent = _appState.user.name;
|
|
||||||
|
|
||||||
// Hunde laden
|
|
||||||
try {
|
|
||||||
_appState.dogs = await API.dogs.list();
|
|
||||||
_appState.activeDog = _appState.dogs[0] || null;
|
|
||||||
} catch { /* keine Hunde = okay */ }
|
|
||||||
|
|
||||||
UI.toast.success(`Willkommen zurück, ${_appState.user.name}!`);
|
|
||||||
|
|
||||||
// Nach Login: Tagebuch oder Profil anlegen
|
|
||||||
if (_appState.activeDog) {
|
|
||||||
App.navigate('diary');
|
|
||||||
} else {
|
|
||||||
App.navigate('dog-profile');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function _bindRegisterForm() {
|
|
||||||
document.getElementById('auth-form')?.addEventListener('submit', async e => {
|
|
||||||
e.preventDefault();
|
|
||||||
const btn = e.target.querySelector('[type="submit"]');
|
|
||||||
const fd = UI.formData(e.target);
|
|
||||||
|
|
||||||
if (!fd.name?.trim()) {
|
|
||||||
UI.toast.warning('Bitte einen Namen eingeben.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if ((fd.password || '').length < 8) {
|
|
||||||
UI.toast.warning('Passwort muss mindestens 8 Zeichen lang sein.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await UI.asyncButton(btn, async () => {
|
|
||||||
const result = await API.auth.register(fd.email, fd.password, fd.name.trim());
|
|
||||||
localStorage.setItem('by_token', result.token);
|
|
||||||
|
|
||||||
_appState.user = await API.auth.me();
|
|
||||||
document.getElementById('sidebar-username').textContent = _appState.user.name;
|
|
||||||
_appState.dogs = [];
|
|
||||||
_appState.activeDog = null;
|
|
||||||
|
|
||||||
UI.toast.success(`Willkommen bei Ban Yaro, ${_appState.user.name}! 🐕`);
|
|
||||||
// Direkt zur Profil-Anlage
|
|
||||||
App.navigate('dog-profile');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// HELPER
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
function _esc(str) {
|
|
||||||
if (!str) return '';
|
|
||||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
// PUBLIC
|
|
||||||
// ----------------------------------------------------------
|
|
||||||
return { init, refresh };
|
|
||||||
|
|
||||||
})();
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue