Sprint 15: Suche, Ausweis, Teilen, Widget
- Volltext-Suche im Tagebuch (LIKE über Titel/Text/Tags, Debounce 350ms)
- Digitaler Heimtierausweis als druckbare HTML-Seite (/ausweis/{dog_id})
Enthält Impfungen, Medikamente, Allergien, Tierärzte, Chip-Nr.
- Hund teilen: Einladungslink-System (dog_shares-Tabelle, /teilen/{token})
Geteilte Hunde erscheinen in der Hundeliste, Tagebuch/Gesundheit lesbar
- Widget-Seite /#widget: zufälliges Tagebuchbild + nächste Erinnerung
Als PWA-Shortcut im Manifest verankert
- SW-Cache by-v144, APP_VER 117
This commit is contained in:
parent
d5f09cd16b
commit
34f29f9d0a
16 changed files with 917 additions and 35 deletions
|
|
@ -3,7 +3,7 @@
|
|||
Router, State-Management, Navigation, Initialisierung.
|
||||
============================================================ */
|
||||
|
||||
const APP_VER = '116'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
const APP_VER = '117'; // ← bei jedem Deploy mit Frontend-Änderungen erhöhen
|
||||
|
||||
const App = (() => {
|
||||
|
||||
|
|
@ -56,6 +56,7 @@ const App = (() => {
|
|||
admin: { title: 'Admin', module: null, requiresAuth: true },
|
||||
impressum: { title: 'Impressum', module: null },
|
||||
datenschutz: { title: 'Datenschutz', module: null },
|
||||
widget: { title: 'Widget', module: null, requiresAuth: true },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
@ -573,9 +574,16 @@ const App = (() => {
|
|||
_bindNavigation();
|
||||
await _checkAuth();
|
||||
|
||||
// Einladungslink /teilen/{token} → direkt annehmen
|
||||
const inviteMatch = location.pathname.match(/^\/teilen\/([A-Za-z0-9_-]+)$/);
|
||||
if (inviteMatch) {
|
||||
const token = inviteMatch[1];
|
||||
navigate('diary', false);
|
||||
_handleInvite(token);
|
||||
return;
|
||||
}
|
||||
|
||||
// Erste Seite laden: Hash aus URL oder Standard 'diary'.
|
||||
// Bewusst NACH _checkAuth(), damit _loadPage() nur einmal aufgerufen wird
|
||||
// (vorher war Hash-Navigation auch in _bindNavigation() → doppelter Aufruf).
|
||||
const rawHash = location.hash.replace('#', '');
|
||||
const [hashPage, hashQuery] = rawHash.split('?');
|
||||
const hashParams = {};
|
||||
|
|
@ -588,6 +596,38 @@ const App = (() => {
|
|||
navigate(startPage, false, hashParams);
|
||||
}
|
||||
|
||||
async function _handleInvite(token) {
|
||||
try {
|
||||
const info = await API.sharing.info(token);
|
||||
if (info.accepted_at) {
|
||||
UI.toast.success(`Du hast bereits Zugriff auf ${info.dog_name}.`);
|
||||
history.replaceState(null, '', '/');
|
||||
return;
|
||||
}
|
||||
const ok = await UI.modal.confirm(
|
||||
`<strong>${UI.escape(info.owner_name)}</strong> möchte das Profil von
|
||||
<strong>${UI.escape(info.dog_name)}</strong> mit dir teilen
|
||||
(${info.role === 'editor' ? 'Lesen & Schreiben' : 'Nur lesen'}).
|
||||
Möchtest du die Einladung annehmen?`
|
||||
);
|
||||
if (!ok) { history.replaceState(null, '', '/'); return; }
|
||||
await API.sharing.accept(token);
|
||||
// Hundeliste neu laden
|
||||
state.dogs = await API.dogs.list();
|
||||
const newDog = state.dogs.find(d => d.name === info.dog_name);
|
||||
if (newDog) {
|
||||
state.activeDog = newDog;
|
||||
localStorage.setItem('by_active_dog', String(newDog.id));
|
||||
_renderDogSwitcher();
|
||||
}
|
||||
history.replaceState(null, '', '/');
|
||||
UI.toast.success(`${UI.escape(info.dog_name)} wurde deiner Liste hinzugefügt!`);
|
||||
} catch (e) {
|
||||
UI.toast.error(e.message || 'Einladungslink ungültig.');
|
||||
history.replaceState(null, '', '/');
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------
|
||||
// AUTH-GATE HELPER — einheitlicher "Bitte anmelden"-Block
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue