feat: add categorized settings tabs (#30)
Six tabs (General, Meals, Budget, Shopping, Calendar, Account) replace the flat single-page layout. Active tab persists via sessionStorage. Calendar tab auto-activates on OAuth redirect. Tab bar is sticky. All labels translated in de/en/es/it/sv.
This commit is contained in:
+11
-19
@@ -30,7 +30,6 @@
|
||||
"confirm": "Bestätigen",
|
||||
"undo": "Rückgängig"
|
||||
},
|
||||
|
||||
"nav": {
|
||||
"dashboard": "Übersicht",
|
||||
"tasks": "Aufgaben",
|
||||
@@ -45,7 +44,6 @@
|
||||
"navigation": "Navigation",
|
||||
"quickActions": "Schnellaktionen"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"title": "Übersicht",
|
||||
"greetingMorning": "Guten Morgen, {{name}}",
|
||||
@@ -81,7 +79,6 @@
|
||||
"allDay": "Ganztägig",
|
||||
"shoppingMore": "+{{count}} weitere"
|
||||
},
|
||||
|
||||
"tasks": {
|
||||
"title": "Aufgaben",
|
||||
"newTask": "Neue Aufgabe",
|
||||
@@ -149,7 +146,6 @@
|
||||
"listView": "Listenansicht",
|
||||
"kanbanView": "Kanban-Ansicht"
|
||||
},
|
||||
|
||||
"shopping": {
|
||||
"title": "Einkauf",
|
||||
"noLists": "Keine Listen",
|
||||
@@ -190,7 +186,6 @@
|
||||
"catDrugstore": "Drogerie",
|
||||
"catMisc": "Sonstiges"
|
||||
},
|
||||
|
||||
"meals": {
|
||||
"title": "Essensplan",
|
||||
"noMealPlanned": "Kein Essen geplant",
|
||||
@@ -239,7 +234,6 @@
|
||||
"recipeUrlPlaceholder": "https://…",
|
||||
"openRecipe": "Rezept öffnen"
|
||||
},
|
||||
|
||||
"calendar": {
|
||||
"title": "Kalender",
|
||||
"newEvent": "Neuer Termin",
|
||||
@@ -272,7 +266,7 @@
|
||||
"locationPlaceholder": "Optional",
|
||||
"assignedLabel": "Zugewiesen an",
|
||||
"assignedNobody": "- Niemand -",
|
||||
"colorLabel": "Farbe",
|
||||
"colorLabel": "Farbe {{color}}",
|
||||
"descriptionLabel": "Beschreibung",
|
||||
"descriptionPlaceholder": "Optional…",
|
||||
"popupEdit": "Bearbeiten",
|
||||
@@ -310,10 +304,8 @@
|
||||
"dayLongThursday": "Donnerstag",
|
||||
"dayLongFriday": "Freitag",
|
||||
"dayLongSaturday": "Samstag",
|
||||
"timeSuffix": "Uhr",
|
||||
"colorLabel": "Farbe {{color}}"
|
||||
"timeSuffix": "Uhr"
|
||||
},
|
||||
|
||||
"notes": {
|
||||
"title": "Pinnwand",
|
||||
"newNote": "Neue Notiz",
|
||||
@@ -352,7 +344,6 @@
|
||||
"formatQuote": "Zitat",
|
||||
"formatDivider": "Trennlinie"
|
||||
},
|
||||
|
||||
"contacts": {
|
||||
"title": "Kontakte",
|
||||
"newContact": "Neuer Kontakt",
|
||||
@@ -406,7 +397,6 @@
|
||||
"categoryEmergency": "Notfall",
|
||||
"categoryOther": "Sonstiges"
|
||||
},
|
||||
|
||||
"budget": {
|
||||
"title": "Budget",
|
||||
"newEntry": "Neuer Eintrag",
|
||||
@@ -454,9 +444,15 @@
|
||||
"catMisc": "Sonstiges",
|
||||
"loadingIndicator": "Lade…"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"tabGeneral": "Allgemein",
|
||||
"tabMeals": "Mahlzeiten",
|
||||
"tabBudget": "Budget",
|
||||
"tabShopping": "Einkauf",
|
||||
"tabCalendar": "Kalender",
|
||||
"tabAccount": "Konto",
|
||||
"tabsAriaLabel": "Einstellungsbereiche",
|
||||
"sectionDesign": "Design",
|
||||
"sectionShopping": "Einkauf",
|
||||
"shoppingCategoriesLabel": "Einkaufskategorien",
|
||||
@@ -547,7 +543,6 @@
|
||||
"currencyHint": "Legt die Währung für den gesamten Budget-Bereich fest.",
|
||||
"currencySaved": "Währung gespeichert."
|
||||
},
|
||||
|
||||
"login": {
|
||||
"tagline": "Familienplanung. Sicher. Datenschutzfreundlich. Open Source.",
|
||||
"usernameLabel": "Benutzername",
|
||||
@@ -559,20 +554,17 @@
|
||||
"tooManyAttempts": "Zu viele Versuche. Bitte warte kurz.",
|
||||
"invalidCredentials": "Ungültige Anmeldedaten."
|
||||
},
|
||||
|
||||
"install": {
|
||||
"title": "Oikos installieren",
|
||||
"subtitle": "Zur App hinzufügen",
|
||||
"iosTip1": "Tippe auf ",
|
||||
"iosTip2": " \u2192 \"Zum Home-Bildschirm\"",
|
||||
"iosTip2": " → \"Zum Home-Bildschirm\"",
|
||||
"installButton": "Installieren",
|
||||
"dismissLabel": "Schließen"
|
||||
},
|
||||
|
||||
"modal": {
|
||||
"closeLabel": "Schließen"
|
||||
},
|
||||
|
||||
"rrule": {
|
||||
"freqNone": "Keine Wiederholung",
|
||||
"freqDaily": "Täglich",
|
||||
@@ -596,4 +588,4 @@
|
||||
"unitMonth": "Monat",
|
||||
"unitMonths": "Monate"
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-18
@@ -30,7 +30,6 @@
|
||||
"confirm": "Confirm",
|
||||
"undo": "Undo"
|
||||
},
|
||||
|
||||
"nav": {
|
||||
"dashboard": "Overview",
|
||||
"tasks": "Tasks",
|
||||
@@ -45,7 +44,6 @@
|
||||
"navigation": "Navigation",
|
||||
"quickActions": "Quick actions"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"title": "Overview",
|
||||
"greetingMorning": "Good morning, {{name}}",
|
||||
@@ -81,7 +79,6 @@
|
||||
"allDay": "All day",
|
||||
"shoppingMore": "+{{count}} more"
|
||||
},
|
||||
|
||||
"tasks": {
|
||||
"title": "Tasks",
|
||||
"newTask": "New Task",
|
||||
@@ -149,7 +146,6 @@
|
||||
"listView": "List view",
|
||||
"kanbanView": "Kanban view"
|
||||
},
|
||||
|
||||
"shopping": {
|
||||
"title": "Shopping",
|
||||
"noLists": "No lists",
|
||||
@@ -190,7 +186,6 @@
|
||||
"catDrugstore": "Drugstore",
|
||||
"catMisc": "Miscellaneous"
|
||||
},
|
||||
|
||||
"meals": {
|
||||
"title": "Meal Plan",
|
||||
"noMealPlanned": "No meal planned",
|
||||
@@ -239,7 +234,6 @@
|
||||
"recipeUrlPlaceholder": "https://…",
|
||||
"openRecipe": "Open recipe"
|
||||
},
|
||||
|
||||
"calendar": {
|
||||
"title": "Calendar",
|
||||
"newEvent": "New Event",
|
||||
@@ -272,7 +266,7 @@
|
||||
"locationPlaceholder": "Optional",
|
||||
"assignedLabel": "Assigned to",
|
||||
"assignedNobody": "- Nobody -",
|
||||
"colorLabel": "Color",
|
||||
"colorLabel": "Color {{color}}",
|
||||
"descriptionLabel": "Description",
|
||||
"descriptionPlaceholder": "Optional…",
|
||||
"popupEdit": "Edit",
|
||||
@@ -310,10 +304,8 @@
|
||||
"dayLongThursday": "Thursday",
|
||||
"dayLongFriday": "Friday",
|
||||
"dayLongSaturday": "Saturday",
|
||||
"timeSuffix": "",
|
||||
"colorLabel": "Color {{color}}"
|
||||
"timeSuffix": ""
|
||||
},
|
||||
|
||||
"notes": {
|
||||
"title": "Board",
|
||||
"newNote": "New Note",
|
||||
@@ -352,7 +344,6 @@
|
||||
"formatQuote": "Quote",
|
||||
"formatDivider": "Divider"
|
||||
},
|
||||
|
||||
"contacts": {
|
||||
"title": "Contacts",
|
||||
"newContact": "New Contact",
|
||||
@@ -406,7 +397,6 @@
|
||||
"categoryEmergency": "Emergency",
|
||||
"categoryOther": "Other"
|
||||
},
|
||||
|
||||
"budget": {
|
||||
"title": "Budget",
|
||||
"newEntry": "New Entry",
|
||||
@@ -454,9 +444,15 @@
|
||||
"catMisc": "Miscellaneous",
|
||||
"loadingIndicator": "Loading…"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"tabGeneral": "General",
|
||||
"tabMeals": "Meals",
|
||||
"tabBudget": "Budget",
|
||||
"tabShopping": "Shopping",
|
||||
"tabCalendar": "Calendar",
|
||||
"tabAccount": "Account",
|
||||
"tabsAriaLabel": "Settings sections",
|
||||
"sectionDesign": "Appearance",
|
||||
"sectionShopping": "Shopping",
|
||||
"shoppingCategoriesLabel": "Shopping Categories",
|
||||
@@ -547,7 +543,6 @@
|
||||
"currencyHint": "Sets the currency used throughout the budget section.",
|
||||
"currencySaved": "Currency saved."
|
||||
},
|
||||
|
||||
"login": {
|
||||
"tagline": "Family planning. Secure. Privacy-friendly. Open source.",
|
||||
"usernameLabel": "Username",
|
||||
@@ -559,7 +554,6 @@
|
||||
"tooManyAttempts": "Too many attempts. Please wait a moment.",
|
||||
"invalidCredentials": "Invalid credentials."
|
||||
},
|
||||
|
||||
"install": {
|
||||
"title": "Install Oikos",
|
||||
"subtitle": "Add to home screen",
|
||||
@@ -568,11 +562,9 @@
|
||||
"installButton": "Install",
|
||||
"dismissLabel": "Close"
|
||||
},
|
||||
|
||||
"modal": {
|
||||
"closeLabel": "Close"
|
||||
},
|
||||
|
||||
"rrule": {
|
||||
"freqNone": "No recurrence",
|
||||
"freqDaily": "Daily",
|
||||
@@ -596,4 +588,4 @@
|
||||
"unitMonth": "month",
|
||||
"unitMonths": "months"
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-18
@@ -30,7 +30,6 @@
|
||||
"confirm": "Confirmar",
|
||||
"undo": "Deshacer"
|
||||
},
|
||||
|
||||
"nav": {
|
||||
"dashboard": "Inicio",
|
||||
"tasks": "Tareas",
|
||||
@@ -45,7 +44,6 @@
|
||||
"navigation": "Navegación",
|
||||
"quickActions": "Acciones rápidas"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"title": "Inicio",
|
||||
"greetingMorning": "Buenos días, {{name}}",
|
||||
@@ -81,7 +79,6 @@
|
||||
"allDay": "Todo el día",
|
||||
"shoppingMore": "+{{count}} más"
|
||||
},
|
||||
|
||||
"tasks": {
|
||||
"title": "Tareas",
|
||||
"newTask": "Nueva tarea",
|
||||
@@ -149,7 +146,6 @@
|
||||
"listView": "Vista de lista",
|
||||
"kanbanView": "Vista Kanban"
|
||||
},
|
||||
|
||||
"shopping": {
|
||||
"title": "Compras",
|
||||
"noLists": "Sin listas",
|
||||
@@ -190,7 +186,6 @@
|
||||
"catDrugstore": "Droguería",
|
||||
"catMisc": "Otros"
|
||||
},
|
||||
|
||||
"meals": {
|
||||
"title": "Plan de comidas",
|
||||
"noMealPlanned": "Sin comida planificada",
|
||||
@@ -239,7 +234,6 @@
|
||||
"recipeUrlPlaceholder": "https://…",
|
||||
"openRecipe": "Abrir receta"
|
||||
},
|
||||
|
||||
"calendar": {
|
||||
"title": "Calendario",
|
||||
"newEvent": "Nuevo evento",
|
||||
@@ -272,7 +266,7 @@
|
||||
"locationPlaceholder": "Opcional",
|
||||
"assignedLabel": "Asignado a",
|
||||
"assignedNobody": "- Nadie -",
|
||||
"colorLabel": "Color",
|
||||
"colorLabel": "Color {{color}}",
|
||||
"descriptionLabel": "Descripción",
|
||||
"descriptionPlaceholder": "Opcional…",
|
||||
"popupEdit": "Editar",
|
||||
@@ -310,10 +304,8 @@
|
||||
"dayLongThursday": "Jueves",
|
||||
"dayLongFriday": "Viernes",
|
||||
"dayLongSaturday": "Sábado",
|
||||
"timeSuffix": "",
|
||||
"colorLabel": "Color {{color}}"
|
||||
"timeSuffix": ""
|
||||
},
|
||||
|
||||
"notes": {
|
||||
"title": "Notas",
|
||||
"newNote": "Nueva nota",
|
||||
@@ -352,7 +344,6 @@
|
||||
"formatQuote": "Cita",
|
||||
"formatDivider": "Separador"
|
||||
},
|
||||
|
||||
"contacts": {
|
||||
"title": "Contactos",
|
||||
"newContact": "Nuevo contacto",
|
||||
@@ -406,7 +397,6 @@
|
||||
"categoryEmergency": "Emergencia",
|
||||
"categoryOther": "Otros"
|
||||
},
|
||||
|
||||
"budget": {
|
||||
"title": "Presupuesto",
|
||||
"newEntry": "Nueva entrada",
|
||||
@@ -454,9 +444,15 @@
|
||||
"catMisc": "Otros",
|
||||
"loadingIndicator": "Cargando…"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Ajustes",
|
||||
"tabGeneral": "General",
|
||||
"tabMeals": "Comidas",
|
||||
"tabBudget": "Presupuesto",
|
||||
"tabShopping": "Compras",
|
||||
"tabCalendar": "Calendario",
|
||||
"tabAccount": "Cuenta",
|
||||
"tabsAriaLabel": "Secciones de configuración",
|
||||
"sectionDesign": "Diseño",
|
||||
"sectionShopping": "Compras",
|
||||
"shoppingCategoriesLabel": "Categorías de compra",
|
||||
@@ -547,7 +543,6 @@
|
||||
"currencyHint": "Establece la moneda para toda la sección de presupuesto.",
|
||||
"currencySaved": "Moneda guardada."
|
||||
},
|
||||
|
||||
"login": {
|
||||
"tagline": "Planificación familiar. Segura. Privada. Código abierto.",
|
||||
"usernameLabel": "Nombre de usuario",
|
||||
@@ -559,7 +554,6 @@
|
||||
"tooManyAttempts": "Demasiados intentos. Por favor, espera un momento.",
|
||||
"invalidCredentials": "Credenciales incorrectas."
|
||||
},
|
||||
|
||||
"install": {
|
||||
"title": "Instalar Oikos",
|
||||
"subtitle": "Añadir a la pantalla de inicio",
|
||||
@@ -568,11 +562,9 @@
|
||||
"installButton": "Instalar",
|
||||
"dismissLabel": "Cerrar"
|
||||
},
|
||||
|
||||
"modal": {
|
||||
"closeLabel": "Cerrar"
|
||||
},
|
||||
|
||||
"rrule": {
|
||||
"freqNone": "Sin repetición",
|
||||
"freqDaily": "Diariamente",
|
||||
@@ -596,4 +588,4 @@
|
||||
"unitMonth": "mes",
|
||||
"unitMonths": "meses"
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-17
@@ -30,7 +30,6 @@
|
||||
"confirm": "Conferma",
|
||||
"undo": "Annulla"
|
||||
},
|
||||
|
||||
"nav": {
|
||||
"dashboard": "Panoramica",
|
||||
"tasks": "Compiti",
|
||||
@@ -45,7 +44,6 @@
|
||||
"navigation": "Navigazione",
|
||||
"quickActions": "Azioni rapide"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"title": "Panoramica",
|
||||
"greetingMorning": "Buongiorno, {{name}}",
|
||||
@@ -81,7 +79,6 @@
|
||||
"allDay": "Tutto il giorno",
|
||||
"shoppingMore": "+{{count}} altri"
|
||||
},
|
||||
|
||||
"tasks": {
|
||||
"title": "Compiti",
|
||||
"newTask": "Nuovo compito",
|
||||
@@ -149,7 +146,6 @@
|
||||
"listView": "Vista elenco",
|
||||
"kanbanView": "Vista Kanban"
|
||||
},
|
||||
|
||||
"shopping": {
|
||||
"title": "Spesa",
|
||||
"noLists": "Nessuna lista",
|
||||
@@ -190,7 +186,6 @@
|
||||
"catDrugstore": "Drogheria",
|
||||
"catMisc": "Varie"
|
||||
},
|
||||
|
||||
"meals": {
|
||||
"title": "Piano pasti",
|
||||
"noMealPlanned": "Nessun pasto pianificato",
|
||||
@@ -239,7 +234,6 @@
|
||||
"recipeUrlPlaceholder": "https://…",
|
||||
"openRecipe": "Apri ricetta"
|
||||
},
|
||||
|
||||
"calendar": {
|
||||
"title": "Calendario",
|
||||
"newEvent": "Nuovo evento",
|
||||
@@ -272,7 +266,7 @@
|
||||
"locationPlaceholder": "Opzionale",
|
||||
"assignedLabel": "Assegnato a",
|
||||
"assignedNobody": "- Nessuno -",
|
||||
"colorLabel": "Colore",
|
||||
"colorLabel": "Colore {{color}}",
|
||||
"descriptionLabel": "Descrizione",
|
||||
"descriptionPlaceholder": "Opzionale…",
|
||||
"popupEdit": "Modifica",
|
||||
@@ -310,10 +304,8 @@
|
||||
"dayLongThursday": "Giovedì",
|
||||
"dayLongFriday": "Venerdì",
|
||||
"dayLongSaturday": "Sabato",
|
||||
"timeSuffix": "",
|
||||
"colorLabel": "Colore {{color}}"
|
||||
"timeSuffix": ""
|
||||
},
|
||||
|
||||
"notes": {
|
||||
"title": "Bacheca",
|
||||
"newNote": "Nuova nota",
|
||||
@@ -352,7 +344,6 @@
|
||||
"formatQuote": "Citazione",
|
||||
"formatDivider": "Divisore"
|
||||
},
|
||||
|
||||
"contacts": {
|
||||
"title": "Contatti",
|
||||
"newContact": "Nuovo contatto",
|
||||
@@ -406,7 +397,6 @@
|
||||
"categoryEmergency": "Emergenza",
|
||||
"categoryOther": "Altro"
|
||||
},
|
||||
|
||||
"budget": {
|
||||
"title": "Bilancio",
|
||||
"newEntry": "Nuova voce",
|
||||
@@ -454,9 +444,15 @@
|
||||
"catMisc": "Varie",
|
||||
"loadingIndicator": "Caricamento…"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Impostazioni",
|
||||
"tabGeneral": "Generale",
|
||||
"tabMeals": "Pasti",
|
||||
"tabBudget": "Budget",
|
||||
"tabShopping": "Spesa",
|
||||
"tabCalendar": "Calendario",
|
||||
"tabAccount": "Account",
|
||||
"tabsAriaLabel": "Sezioni impostazioni",
|
||||
"sectionDesign": "Aspetto",
|
||||
"sectionShopping": "Spesa",
|
||||
"shoppingCategoriesLabel": "Categorie spesa",
|
||||
@@ -547,7 +543,6 @@
|
||||
"currencyHint": "Imposta la valuta utilizzata in tutta la sezione budget.",
|
||||
"currencySaved": "Valuta salvata."
|
||||
},
|
||||
|
||||
"login": {
|
||||
"tagline": "Pianificazione familiare. Sicura. Rispettosa della privacy. Open source.",
|
||||
"usernameLabel": "Nome utente",
|
||||
@@ -559,7 +554,6 @@
|
||||
"tooManyAttempts": "Troppi tentativi. Attendi un momento.",
|
||||
"invalidCredentials": "Credenziali non valide."
|
||||
},
|
||||
|
||||
"install": {
|
||||
"title": "Installa Oikos",
|
||||
"subtitle": "Aggiungi alla schermata home",
|
||||
@@ -568,8 +562,7 @@
|
||||
"installButton": "Installa",
|
||||
"dismissLabel": "Chiudi"
|
||||
},
|
||||
|
||||
"modal": {
|
||||
"closeLabel": "Chiudi"
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-18
@@ -30,7 +30,6 @@
|
||||
"confirm": "Bekräfta",
|
||||
"undo": "Ångra"
|
||||
},
|
||||
|
||||
"nav": {
|
||||
"dashboard": "Översikt",
|
||||
"tasks": "Uppgifter",
|
||||
@@ -45,7 +44,6 @@
|
||||
"navigation": "Navigering",
|
||||
"quickActions": "Snabba åtgärder"
|
||||
},
|
||||
|
||||
"dashboard": {
|
||||
"title": "Översikt",
|
||||
"greetingMorning": "God morgon, {{name}}",
|
||||
@@ -81,7 +79,6 @@
|
||||
"allDay": "Hela dagen",
|
||||
"shoppingMore": "+{{count}} till"
|
||||
},
|
||||
|
||||
"tasks": {
|
||||
"title": "Uppgifter",
|
||||
"newTask": "Ny uppgift",
|
||||
@@ -149,7 +146,6 @@
|
||||
"listView": "Listvy",
|
||||
"kanbanView": "Kanban-vy"
|
||||
},
|
||||
|
||||
"shopping": {
|
||||
"title": "Shopping",
|
||||
"noLists": "Inga listor",
|
||||
@@ -190,7 +186,6 @@
|
||||
"catDrugstore": "Apotek",
|
||||
"catMisc": "Diverse"
|
||||
},
|
||||
|
||||
"meals": {
|
||||
"title": "Måltidsplan",
|
||||
"noMealPlanned": "Ingen måltid planerad",
|
||||
@@ -239,7 +234,6 @@
|
||||
"recipeUrlPlaceholder": "https://…",
|
||||
"openRecipe": "Öppna recept"
|
||||
},
|
||||
|
||||
"calendar": {
|
||||
"title": "Kalender",
|
||||
"newEvent": "Ny händelse",
|
||||
@@ -272,7 +266,7 @@
|
||||
"locationPlaceholder": "Frivillig",
|
||||
"assignedLabel": "Tilldelad till",
|
||||
"assignedNobody": "- Ingen -",
|
||||
"colorLabel": "Färg",
|
||||
"colorLabel": "Färg {{color}}",
|
||||
"descriptionLabel": "Beskrivning",
|
||||
"descriptionPlaceholder": "Frivillig…",
|
||||
"popupEdit": "Redigera",
|
||||
@@ -310,10 +304,8 @@
|
||||
"dayLongThursday": "Torsdag",
|
||||
"dayLongFriday": "Fredag",
|
||||
"dayLongSaturday": "Lördag",
|
||||
"timeSuffix": "",
|
||||
"colorLabel": "Färg {{color}}"
|
||||
"timeSuffix": ""
|
||||
},
|
||||
|
||||
"notes": {
|
||||
"title": "Anteckningar",
|
||||
"newNote": "Ny anteckning",
|
||||
@@ -352,7 +344,6 @@
|
||||
"formatQuote": "Citationstecken",
|
||||
"formatDivider": "Delare"
|
||||
},
|
||||
|
||||
"contacts": {
|
||||
"title": "Kontakter",
|
||||
"newContact": "Ny kontakt",
|
||||
@@ -406,7 +397,6 @@
|
||||
"categoryEmergency": "Nödsituation",
|
||||
"categoryOther": "Andra"
|
||||
},
|
||||
|
||||
"budget": {
|
||||
"title": "Budget",
|
||||
"newEntry": "Nytt inlägg",
|
||||
@@ -454,9 +444,15 @@
|
||||
"catMisc": "Diverse",
|
||||
"loadingIndicator": "Laddar…"
|
||||
},
|
||||
|
||||
"settings": {
|
||||
"title": "Inställningar",
|
||||
"tabGeneral": "Allmänt",
|
||||
"tabMeals": "Måltider",
|
||||
"tabBudget": "Budget",
|
||||
"tabShopping": "Inköp",
|
||||
"tabCalendar": "Kalender",
|
||||
"tabAccount": "Konto",
|
||||
"tabsAriaLabel": "Inställningsavsnitt",
|
||||
"sectionDesign": "Utseende",
|
||||
"sectionShopping": "Inköp",
|
||||
"shoppingCategoriesLabel": "Inköpskategorier",
|
||||
@@ -547,7 +543,6 @@
|
||||
"currencyHint": "Ställer in valutan som används i hela budgetavsnittet.",
|
||||
"currencySaved": "Valuta sparad."
|
||||
},
|
||||
|
||||
"login": {
|
||||
"tagline": "Familjeplanering. Säker. Sekretessvänlig. Öppen källkod.",
|
||||
"usernameLabel": "Användarnamn",
|
||||
@@ -559,7 +554,6 @@
|
||||
"tooManyAttempts": "För många försök. Vänta ett ögonblick.",
|
||||
"invalidCredentials": "Ogiltiga användaruppgifter."
|
||||
},
|
||||
|
||||
"install": {
|
||||
"title": "Installera Oikos",
|
||||
"subtitle": "Lägg till på startskärmen",
|
||||
@@ -568,11 +562,9 @@
|
||||
"installButton": "Installera",
|
||||
"dismissLabel": "Stäng"
|
||||
},
|
||||
|
||||
"modal": {
|
||||
"closeLabel": "Stäng"
|
||||
},
|
||||
|
||||
"rrule": {
|
||||
"freqNone": "Ingen upprepning",
|
||||
"freqDaily": "Dagligen",
|
||||
@@ -596,4 +588,4 @@
|
||||
"unitMonth": "månad",
|
||||
"unitMonths": "månader"
|
||||
}
|
||||
}
|
||||
}
|
||||
+293
-237
@@ -11,6 +11,7 @@ import { esc } from '/utils/html.js';
|
||||
import '/components/oikos-locale-picker.js';
|
||||
|
||||
const SUPPORTED_CURRENCIES = ['AUD', 'CAD', 'CHF', 'CZK', 'DKK', 'EUR', 'GBP', 'HUF', 'JPY', 'NOK', 'PLN', 'SEK', 'USD'];
|
||||
const SETTINGS_TAB_KEY = 'oikos:settings:tab';
|
||||
|
||||
function buildCurrencyOptions(selected) {
|
||||
const display = typeof Intl.DisplayNames !== 'undefined'
|
||||
@@ -67,6 +68,14 @@ export async function render(container, { user }) {
|
||||
? (appleStatus.lastSync ? t('settings.configuredLastSync', { date: formatDateTime(appleStatus.lastSync) }) : t('settings.configured'))
|
||||
: t('settings.notConnected');
|
||||
|
||||
const activeTab = (syncOk || syncErr)
|
||||
? 'calendar'
|
||||
: (sessionStorage.getItem(SETTINGS_TAB_KEY) ?? 'general');
|
||||
|
||||
const panelHidden = (id) => id === activeTab ? '' : ' hidden';
|
||||
const btnClass = (id) => `settings-tab-btn${id === activeTab ? ' settings-tab-btn--active' : ''}`;
|
||||
const btnAria = (id) => id === activeTab ? 'true' : 'false';
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="page settings-page">
|
||||
<div class="page__header">
|
||||
@@ -76,257 +85,275 @@ export async function render(container, { user }) {
|
||||
${syncOk ? `<div class="settings-banner settings-banner--success">${syncOk === 'google' ? t('settings.syncSuccessGoogle') : t('settings.syncSuccessApple')}</div>` : ''}
|
||||
${syncErr ? `<div class="settings-banner settings-banner--error">${syncErr === 'google' ? t('settings.syncErrorGoogle') : t('settings.syncErrorApple')}</div>` : ''}
|
||||
|
||||
<!-- Design -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionDesign')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.cardAppearance')}</h3>
|
||||
<div class="theme-toggle" id="theme-toggle">
|
||||
<button class="theme-toggle__btn ${currentTheme() === 'system' ? 'theme-toggle__btn--active' : ''}" data-theme-value="system" aria-label="${t('settings.themeSysLabel')}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
${t('settings.themeSystem')}
|
||||
</button>
|
||||
<button class="theme-toggle__btn ${currentTheme() === 'light' ? 'theme-toggle__btn--active' : ''}" data-theme-value="light" aria-label="${t('settings.themeLightLabel')}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
${t('settings.themeLight')}
|
||||
</button>
|
||||
<button class="theme-toggle__btn ${currentTheme() === 'dark' ? 'theme-toggle__btn--active' : ''}" data-theme-value="dark" aria-label="${t('settings.themeDarkLabel')}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
${t('settings.themeDark')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<nav class="settings-tabs" role="tablist" aria-label="${t('settings.tabsAriaLabel')}">
|
||||
<button class="${btnClass('general')}" role="tab" data-tab="general" aria-selected="${btnAria('general')}">${t('settings.tabGeneral')}</button>
|
||||
<button class="${btnClass('meals')}" role="tab" data-tab="meals" aria-selected="${btnAria('meals')}">${t('settings.tabMeals')}</button>
|
||||
<button class="${btnClass('budget')}" role="tab" data-tab="budget" aria-selected="${btnAria('budget')}">${t('settings.tabBudget')}</button>
|
||||
<button class="${btnClass('shopping')}" role="tab" data-tab="shopping" aria-selected="${btnAria('shopping')}">${t('settings.tabShopping')}</button>
|
||||
<button class="${btnClass('calendar')}" role="tab" data-tab="calendar" aria-selected="${btnAria('calendar')}">${t('settings.tabCalendar')}</button>
|
||||
<button class="${btnClass('account')}" role="tab" data-tab="account" aria-selected="${btnAria('account')}">${t('settings.tabAccount')}</button>
|
||||
</nav>
|
||||
|
||||
<!-- Sprache -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.languageTitle')}</h2>
|
||||
<div class="settings-card">
|
||||
<oikos-locale-picker></oikos-locale-picker>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Essensplan -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionMeals')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.mealTypesLabel')}</h3>
|
||||
<p class="form-hint" style="margin-bottom:var(--space-3)">${t('settings.mealTypesHint')}</p>
|
||||
<div class="meal-type-toggles" id="meal-type-toggles">
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="breakfast" checked>
|
||||
<span>${t('meals.typeBreakfast')}</span>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="lunch" checked>
|
||||
<span>${t('meals.typeLunch')}</span>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="dinner" checked>
|
||||
<span>${t('meals.typeDinner')}</span>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="snack" checked>
|
||||
<span>${t('meals.typeSnack')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Budget -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionBudget')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.currencyLabel')}</h3>
|
||||
<p class="form-hint" style="margin-bottom:var(--space-3)">${t('settings.currencyHint')}</p>
|
||||
<select class="form-input" id="currency-select">
|
||||
${buildCurrencyOptions(prefs.currency)}
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Einkauf: Kategorien -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionShopping')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.shoppingCategoriesLabel')}</h3>
|
||||
<p class="form-hint" style="margin-bottom:var(--space-3)">${t('settings.shoppingCategoriesHint')}</p>
|
||||
<ul class="cat-list" id="cat-list">
|
||||
${categories.map((c, i) => categoryRowHtml(c, i === 0, i === categories.length - 1)).join('')}
|
||||
</ul>
|
||||
<form class="cat-add-form" id="cat-add-form" novalidate autocomplete="off">
|
||||
<input class="form-input" type="text" id="cat-add-input"
|
||||
placeholder="${t('settings.shoppingCategoryPlaceholder')}"
|
||||
maxlength="60" />
|
||||
<button type="submit" class="btn btn--primary">${t('common.add')}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mein Konto -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionAccount')}</h2>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-user-info">
|
||||
<div class="settings-avatar" style="background:${esc(user?.avatar_color) || '#007AFF'}">
|
||||
${esc(initials(user?.display_name))}
|
||||
</div>
|
||||
<div>
|
||||
<div class="settings-user-info__name">${esc(user?.display_name)}</div>
|
||||
<div class="settings-user-info__username">@${esc(user?.username)}</div>
|
||||
<!-- Panel: Allgemein (Design + Sprache) -->
|
||||
<div class="settings-tab-panel" data-panel="general" role="tabpanel"${panelHidden('general')}>
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionDesign')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.cardAppearance')}</h3>
|
||||
<div class="theme-toggle" id="theme-toggle">
|
||||
<button class="theme-toggle__btn ${currentTheme() === 'system' ? 'theme-toggle__btn--active' : ''}" data-theme-value="system" aria-label="${t('settings.themeSysLabel')}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||||
${t('settings.themeSystem')}
|
||||
</button>
|
||||
<button class="theme-toggle__btn ${currentTheme() === 'light' ? 'theme-toggle__btn--active' : ''}" data-theme-value="light" aria-label="${t('settings.themeLightLabel')}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
||||
${t('settings.themeLight')}
|
||||
</button>
|
||||
<button class="theme-toggle__btn ${currentTheme() === 'dark' ? 'theme-toggle__btn--active' : ''}" data-theme-value="dark" aria-label="${t('settings.themeDarkLabel')}">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
||||
${t('settings.themeDark')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.changePassword')}</h3>
|
||||
<form id="password-form" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="current-password">${t('settings.currentPasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="current-password" autocomplete="current-password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-password">${t('settings.newPasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="new-password" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="confirm-password">${t('settings.confirmPasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="confirm-password" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div id="password-error" class="form-error" hidden></div>
|
||||
<button type="submit" class="btn btn--primary">${t('settings.savePassword')}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.languageTitle')}</h2>
|
||||
<div class="settings-card">
|
||||
<oikos-locale-picker></oikos-locale-picker>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Kalender-Synchronisation -->
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionCalendarSync')}</h2>
|
||||
|
||||
<!-- Google Calendar -->
|
||||
<div class="settings-card">
|
||||
<div class="settings-sync-header">
|
||||
<div class="settings-sync-logo settings-sync-logo--google">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05"/>
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="settings-sync-info">
|
||||
<div class="settings-sync-info__name">${t('settings.googleCalendar')}</div>
|
||||
<div class="settings-sync-info__status ${googleStatus.connected ? 'settings-sync-info__status--connected' : ''}">
|
||||
${googleStatusText}
|
||||
</div>
|
||||
<!-- Panel: Mahlzeiten -->
|
||||
<div class="settings-tab-panel" data-panel="meals" role="tabpanel"${panelHidden('meals')}>
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionMeals')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.mealTypesLabel')}</h3>
|
||||
<p class="form-hint" style="margin-bottom:var(--space-3)">${t('settings.mealTypesHint')}</p>
|
||||
<div class="meal-type-toggles" id="meal-type-toggles">
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="breakfast" checked>
|
||||
<span>${t('meals.typeBreakfast')}</span>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="lunch" checked>
|
||||
<span>${t('meals.typeLunch')}</span>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="dinner" checked>
|
||||
<span>${t('meals.typeDinner')}</span>
|
||||
</label>
|
||||
<label class="toggle-row">
|
||||
<input type="checkbox" value="snack" checked>
|
||||
<span>${t('meals.typeSnack')}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
${googleStatus.configured ? `
|
||||
<div class="settings-sync-actions">
|
||||
${googleStatus.connected ? `
|
||||
<button class="btn btn--secondary" id="google-sync-btn">${t('settings.syncNow')}</button>
|
||||
${user?.role === 'admin' ? `<button class="btn btn--danger-outline" id="google-disconnect-btn">${t('settings.disconnect')}</button>` : ''}
|
||||
` : `
|
||||
${user?.role === 'admin' ? `<a href="/api/v1/calendar/google/auth" class="btn btn--primary">${t('settings.connectGoogle')}</a>` : `<span class="form-hint">${t('settings.googleOnlyAdmin')}</span>`}
|
||||
`}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Apple Calendar -->
|
||||
<div class="settings-card">
|
||||
<div class="settings-sync-header">
|
||||
<div class="settings-sync-logo settings-sync-logo--apple">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="settings-sync-info">
|
||||
<div class="settings-sync-info__name">${t('settings.appleCalendar')}</div>
|
||||
<div class="settings-sync-info__status ${appleStatus.configured ? 'settings-sync-info__status--connected' : ''}">
|
||||
${appleStatusText}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Panel: Budget -->
|
||||
<div class="settings-tab-panel" data-panel="budget" role="tabpanel"${panelHidden('budget')}>
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionBudget')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.currencyLabel')}</h3>
|
||||
<p class="form-hint" style="margin-bottom:var(--space-3)">${t('settings.currencyHint')}</p>
|
||||
<select class="form-input" id="currency-select">
|
||||
${buildCurrencyOptions(prefs.currency)}
|
||||
</select>
|
||||
</div>
|
||||
${appleStatus.configured ? `
|
||||
<div class="settings-sync-actions">
|
||||
<button class="btn btn--secondary" id="apple-sync-btn">${t('settings.syncNow')}</button>
|
||||
${appleStatus.connected && user?.role === 'admin' ? `<button class="btn btn--danger-outline" id="apple-disconnect-btn">${t('settings.disconnect')}</button>` : ''}
|
||||
</div>
|
||||
` : user?.role === 'admin' ? `
|
||||
<form id="apple-connect-form" class="settings-form settings-form--compact">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apple-caldav-url">${t('settings.caldavUrlLabel')}</label>
|
||||
<input class="form-input" type="url" id="apple-caldav-url" placeholder="${t('settings.caldavUrlPlaceholder')}" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apple-username">${t('settings.appleIdLabel')}</label>
|
||||
<input class="form-input" type="email" id="apple-username" autocomplete="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apple-password">${t('settings.applePasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="apple-password" autocomplete="current-password" required />
|
||||
<span class="form-hint">${t('settings.applePasswordHint')}</span>
|
||||
</div>
|
||||
<div id="apple-connect-error" class="form-error" hidden></div>
|
||||
<button type="submit" class="btn btn--primary" id="apple-connect-btn">${t('settings.appleConnectBtn')}</button>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Panel: Einkauf -->
|
||||
<div class="settings-tab-panel" data-panel="shopping" role="tabpanel"${panelHidden('shopping')}>
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionShopping')}</h2>
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.shoppingCategoriesLabel')}</h3>
|
||||
<p class="form-hint" style="margin-bottom:var(--space-3)">${t('settings.shoppingCategoriesHint')}</p>
|
||||
<ul class="cat-list" id="cat-list">
|
||||
${categories.map((c, i) => categoryRowHtml(c, i === 0, i === categories.length - 1)).join('')}
|
||||
</ul>
|
||||
<form class="cat-add-form" id="cat-add-form" novalidate autocomplete="off">
|
||||
<input class="form-input" type="text" id="cat-add-input"
|
||||
placeholder="${t('settings.shoppingCategoryPlaceholder')}"
|
||||
maxlength="60" />
|
||||
<button type="submit" class="btn btn--primary">${t('common.add')}</button>
|
||||
</form>
|
||||
` : `<span class="form-hint">${t('settings.appleOnlyAdmin')}</span>`}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Familienmitglieder (nur Admin) -->
|
||||
${user?.role === 'admin' ? `
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionFamily')}</h2>
|
||||
<div class="settings-card" id="members-card">
|
||||
<ul class="settings-members" id="members-list">
|
||||
${users.map(memberHtml).join('')}
|
||||
</ul>
|
||||
<button class="btn btn--primary settings-add-btn" id="add-member-btn">${t('settings.addMember')}</button>
|
||||
</div>
|
||||
<!-- Panel: Kalender -->
|
||||
<div class="settings-tab-panel" data-panel="calendar" role="tabpanel"${panelHidden('calendar')}>
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionCalendarSync')}</h2>
|
||||
|
||||
<div class="settings-card settings-card--hidden" id="add-member-form-card">
|
||||
<h3 class="settings-card__title">${t('settings.newMemberTitle')}</h3>
|
||||
<form id="add-member-form" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-username">${t('settings.usernameLabel')}</label>
|
||||
<input class="form-input" type="text" id="new-username" required autocomplete="off" />
|
||||
<!-- Google Calendar -->
|
||||
<div class="settings-card">
|
||||
<div class="settings-sync-header">
|
||||
<div class="settings-sync-logo settings-sync-logo--google">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
|
||||
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
|
||||
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05"/>
|
||||
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="settings-sync-info">
|
||||
<div class="settings-sync-info__name">${t('settings.googleCalendar')}</div>
|
||||
<div class="settings-sync-info__status ${googleStatus.connected ? 'settings-sync-info__status--connected' : ''}">
|
||||
${googleStatusText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-display-name">${t('settings.displayNameLabel')}</label>
|
||||
<input class="form-input" type="text" id="new-display-name" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-member-password">${t('settings.memberPasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="new-member-password" minlength="8" required autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-avatar-color">${t('settings.colorLabel')}</label>
|
||||
<input class="form-input form-input--color" type="color" id="new-avatar-color" value="#007AFF" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-role">${t('settings.roleLabel')}</label>
|
||||
<select class="form-input" id="new-role">
|
||||
<option value="member">${t('settings.roleMember')}</option>
|
||||
<option value="admin">${t('settings.roleAdmin')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="member-error" class="form-error" hidden></div>
|
||||
<div class="settings-form-actions">
|
||||
<button type="submit" class="btn btn--primary">${t('settings.createMember')}</button>
|
||||
<button type="button" class="btn btn--secondary" id="cancel-add-member">${t('settings.cancelAddMember')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
` : ''}
|
||||
${googleStatus.configured ? `
|
||||
<div class="settings-sync-actions">
|
||||
${googleStatus.connected ? `
|
||||
<button class="btn btn--secondary" id="google-sync-btn">${t('settings.syncNow')}</button>
|
||||
${user?.role === 'admin' ? `<button class="btn btn--danger-outline" id="google-disconnect-btn">${t('settings.disconnect')}</button>` : ''}
|
||||
` : `
|
||||
${user?.role === 'admin' ? `<a href="/api/v1/calendar/google/auth" class="btn btn--primary">${t('settings.connectGoogle')}</a>` : `<span class="form-hint">${t('settings.googleOnlyAdmin')}</span>`}
|
||||
`}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<!-- Abmelden -->
|
||||
<section class="settings-section">
|
||||
<button class="btn btn--danger-outline settings-logout-btn" id="logout-btn">${t('settings.logout')}</button>
|
||||
</section>
|
||||
<!-- Apple Calendar -->
|
||||
<div class="settings-card">
|
||||
<div class="settings-sync-header">
|
||||
<div class="settings-sync-logo settings-sync-logo--apple">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.8-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="settings-sync-info">
|
||||
<div class="settings-sync-info__name">${t('settings.appleCalendar')}</div>
|
||||
<div class="settings-sync-info__status ${appleStatus.configured ? 'settings-sync-info__status--connected' : ''}">
|
||||
${appleStatusText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${appleStatus.configured ? `
|
||||
<div class="settings-sync-actions">
|
||||
<button class="btn btn--secondary" id="apple-sync-btn">${t('settings.syncNow')}</button>
|
||||
${appleStatus.connected && user?.role === 'admin' ? `<button class="btn btn--danger-outline" id="apple-disconnect-btn">${t('settings.disconnect')}</button>` : ''}
|
||||
</div>
|
||||
` : user?.role === 'admin' ? `
|
||||
<form id="apple-connect-form" class="settings-form settings-form--compact">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apple-caldav-url">${t('settings.caldavUrlLabel')}</label>
|
||||
<input class="form-input" type="url" id="apple-caldav-url" placeholder="${t('settings.caldavUrlPlaceholder')}" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apple-username">${t('settings.appleIdLabel')}</label>
|
||||
<input class="form-input" type="email" id="apple-username" autocomplete="username" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="apple-password">${t('settings.applePasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="apple-password" autocomplete="current-password" required />
|
||||
<span class="form-hint">${t('settings.applePasswordHint')}</span>
|
||||
</div>
|
||||
<div id="apple-connect-error" class="form-error" hidden></div>
|
||||
<button type="submit" class="btn btn--primary" id="apple-connect-btn">${t('settings.appleConnectBtn')}</button>
|
||||
</form>
|
||||
` : `<span class="form-hint">${t('settings.appleOnlyAdmin')}</span>`}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Panel: Konto -->
|
||||
<div class="settings-tab-panel" data-panel="account" role="tabpanel"${panelHidden('account')}>
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionAccount')}</h2>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="settings-user-info">
|
||||
<div class="settings-avatar" style="background:${esc(user?.avatar_color) || '#007AFF'}">
|
||||
${esc(initials(user?.display_name))}
|
||||
</div>
|
||||
<div>
|
||||
<div class="settings-user-info__name">${esc(user?.display_name)}</div>
|
||||
<div class="settings-user-info__username">@${esc(user?.username)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<h3 class="settings-card__title">${t('settings.changePassword')}</h3>
|
||||
<form id="password-form" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="current-password">${t('settings.currentPasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="current-password" autocomplete="current-password" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-password">${t('settings.newPasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="new-password" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="confirm-password">${t('settings.confirmPasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="confirm-password" autocomplete="new-password" minlength="8" required />
|
||||
</div>
|
||||
<div id="password-error" class="form-error" hidden></div>
|
||||
<button type="submit" class="btn btn--primary">${t('settings.savePassword')}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
${user?.role === 'admin' ? `
|
||||
<section class="settings-section">
|
||||
<h2 class="settings-section__title">${t('settings.sectionFamily')}</h2>
|
||||
<div class="settings-card" id="members-card">
|
||||
<ul class="settings-members" id="members-list">
|
||||
${users.map(memberHtml).join('')}
|
||||
</ul>
|
||||
<button class="btn btn--primary settings-add-btn" id="add-member-btn">${t('settings.addMember')}</button>
|
||||
</div>
|
||||
|
||||
<div class="settings-card settings-card--hidden" id="add-member-form-card">
|
||||
<h3 class="settings-card__title">${t('settings.newMemberTitle')}</h3>
|
||||
<form id="add-member-form" class="settings-form">
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-username">${t('settings.usernameLabel')}</label>
|
||||
<input class="form-input" type="text" id="new-username" required autocomplete="off" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-display-name">${t('settings.displayNameLabel')}</label>
|
||||
<input class="form-input" type="text" id="new-display-name" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-member-password">${t('settings.memberPasswordLabel')}</label>
|
||||
<input class="form-input" type="password" id="new-member-password" minlength="8" required autocomplete="new-password" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-avatar-color">${t('settings.colorLabel')}</label>
|
||||
<input class="form-input form-input--color" type="color" id="new-avatar-color" value="#007AFF" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="new-role">${t('settings.roleLabel')}</label>
|
||||
<select class="form-input" id="new-role">
|
||||
<option value="member">${t('settings.roleMember')}</option>
|
||||
<option value="admin">${t('settings.roleAdmin')}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="member-error" class="form-error" hidden></div>
|
||||
<div class="settings-form-actions">
|
||||
<button type="submit" class="btn btn--primary">${t('settings.createMember')}</button>
|
||||
<button type="button" class="btn btn--secondary" id="cancel-add-member">${t('settings.cancelAddMember')}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
` : ''}
|
||||
|
||||
<section class="settings-section">
|
||||
<button class="btn btn--danger-outline settings-logout-btn" id="logout-btn">${t('settings.logout')}</button>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -346,6 +373,7 @@ export async function render(container, { user }) {
|
||||
// --------------------------------------------------------
|
||||
|
||||
function bindEvents(container, user, categories) {
|
||||
bindTabEvents(container);
|
||||
bindCategoryEvents(container);
|
||||
// Theme-Toggle
|
||||
const themeToggle = container.querySelector('#theme-toggle');
|
||||
@@ -586,6 +614,34 @@ function bindEvents(container, user, categories) {
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------
|
||||
// Tab-Navigation
|
||||
// --------------------------------------------------------
|
||||
|
||||
function bindTabEvents(container) {
|
||||
const tabList = container.querySelector('.settings-tabs');
|
||||
if (!tabList) return;
|
||||
|
||||
tabList.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-tab]');
|
||||
if (!btn) return;
|
||||
const tab = btn.dataset.tab;
|
||||
|
||||
tabList.querySelectorAll('[data-tab]').forEach((b) => {
|
||||
const active = b.dataset.tab === tab;
|
||||
b.classList.toggle('settings-tab-btn--active', active);
|
||||
b.setAttribute('aria-selected', String(active));
|
||||
});
|
||||
|
||||
container.querySelectorAll('[data-panel]').forEach((panel) => {
|
||||
panel.hidden = panel.dataset.panel !== tab;
|
||||
});
|
||||
|
||||
try { sessionStorage.setItem(SETTINGS_TAB_KEY, tab); } catch (_) {}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function bindDeleteButtons(container, user) {
|
||||
container.querySelectorAll('[data-delete-user]').forEach((btn) => {
|
||||
btn.replaceWith(btn.cloneNode(true)); // Doppelte Listener vermeiden
|
||||
|
||||
@@ -42,6 +42,54 @@
|
||||
border: 1px solid var(--color-danger);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Tab-Navigation
|
||||
-------------------------------------------------------- */
|
||||
|
||||
.settings-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
margin-bottom: var(--space-6);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--color-background);
|
||||
z-index: 10;
|
||||
padding-top: var(--space-1);
|
||||
}
|
||||
|
||||
.settings-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.settings-tab-btn {
|
||||
flex-shrink: 0;
|
||||
padding: var(--space-3) var(--space-4);
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast), border-color var(--transition-fast);
|
||||
min-height: 44px;
|
||||
white-space: nowrap;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.settings-tab-btn:hover {
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
|
||||
.settings-tab-btn--active {
|
||||
color: var(--color-accent);
|
||||
border-bottom-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------
|
||||
Sections
|
||||
-------------------------------------------------------- */
|
||||
|
||||
Reference in New Issue
Block a user