feat(05-01): settings shell, header link, and i18n keys for dashboard phase
- Create settings/layout.tsx with SettingsSidebar and back-to-dashboard link
- Create settings/page.tsx with redirect to /settings/dashboard
- Create settings-sidebar.tsx with Widgets and Calendar nav items, aria-current
- Add Settings link in header user dropdown (gear icon, before logout)
- Add settings namespace (DE+EN) with all category and action keys
- Add widgets namespace (DE+EN) with all widget names, descriptions, error states
- Add header.settings key ("Einstellungen"/"Settings")
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,47 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import { SettingsSidebar } from '@/components/settings/settings-sidebar';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings nested layout (D-19, D-20).
|
||||||
|
* Renders within the portal AppShell, adding its own sub-sidebar on the left.
|
||||||
|
* Back-to-dashboard link at top of content area per UI-SPEC.
|
||||||
|
*/
|
||||||
|
export default function SettingsLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const t = useTranslations('settings');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full">
|
||||||
|
<SettingsSidebar />
|
||||||
|
<div className="flex-1 overflow-y-auto p-6">
|
||||||
|
{/* Back to Dashboard link */}
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="15 18 9 12 15 6" />
|
||||||
|
</svg>
|
||||||
|
{t('backToDashboard')}
|
||||||
|
</Link>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings index page — redirects to /settings/dashboard (the default category).
|
||||||
|
*/
|
||||||
|
export default function SettingsPage() {
|
||||||
|
redirect('/settings/dashboard');
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { useSidebarStore } from '@/lib/stores/sidebar-store';
|
import { useSidebarStore } from '@/lib/stores/sidebar-store';
|
||||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||||
@@ -128,6 +129,31 @@ export function Header() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Settings link */}
|
||||||
|
<div className="px-2 py-1">
|
||||||
|
<Link
|
||||||
|
href="/settings"
|
||||||
|
onClick={() => setDropdownOpen(false)}
|
||||||
|
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground hover:bg-muted transition-colors"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
|
</svg>
|
||||||
|
{tHeader('settings')}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Logout button */}
|
{/* Logout button */}
|
||||||
<div className="px-2 py-1">
|
<div className="px-2 py-1">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings sub-sidebar (D-19, D-20).
|
||||||
|
* Phase 05 categories: Dashboard > Widgets, Calendar.
|
||||||
|
* Active item uses sidebar-accent pattern with aria-current="page".
|
||||||
|
*/
|
||||||
|
export function SettingsSidebar() {
|
||||||
|
const t = useTranslations('settings');
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
label: t('categoryWidgets'),
|
||||||
|
href: '/settings/dashboard',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('categoryCalendar'),
|
||||||
|
href: '/settings/dashboard/calendar',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const isActive = (href: string) => {
|
||||||
|
if (href === '/settings/dashboard') {
|
||||||
|
// Active when exactly on /settings/dashboard (not on sub-routes like /calendar)
|
||||||
|
return pathname === '/settings/dashboard';
|
||||||
|
}
|
||||||
|
return pathname.startsWith(href);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside
|
||||||
|
className="flex w-[220px] shrink-0 flex-col border-r border-sidebar-border bg-sidebar"
|
||||||
|
role="navigation"
|
||||||
|
aria-label={t('navLabel')}
|
||||||
|
>
|
||||||
|
<div className="p-4">
|
||||||
|
<h2 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||||
|
{t('categoryDashboard')}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<nav className="flex flex-col gap-1 px-3">
|
||||||
|
{items.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className={`flex items-center rounded-md px-2 py-1.5 text-sm transition-colors ${
|
||||||
|
isActive(item.href)
|
||||||
|
? 'bg-sidebar-accent text-sidebar-accent-foreground font-medium'
|
||||||
|
: 'text-sidebar-foreground hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
aria-current={isActive(item.href) ? 'page' : undefined}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
"breadcrumb": {
|
"breadcrumb": {
|
||||||
"home": "Startseite"
|
"home": "Startseite"
|
||||||
},
|
},
|
||||||
|
"settings": "Einstellungen",
|
||||||
"logout": "Abmelden",
|
"logout": "Abmelden",
|
||||||
"role": {
|
"role": {
|
||||||
"SUPER_ADMIN": "Super-Admin",
|
"SUPER_ADMIN": "Super-Admin",
|
||||||
@@ -98,6 +99,48 @@
|
|||||||
"empty": "Keine Widgets aktiv",
|
"empty": "Keine Widgets aktiv",
|
||||||
"addWidget": "Widget hinzufuegen"
|
"addWidget": "Widget hinzufuegen"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"link": "Einstellungen",
|
||||||
|
"navLabel": "Einstellungen",
|
||||||
|
"backToDashboard": "Zurueck zum Dashboard",
|
||||||
|
"categoryDashboard": "Dashboard",
|
||||||
|
"categoryWidgets": "Widgets",
|
||||||
|
"categoryCalendar": "Kalender",
|
||||||
|
"sourceDeleteConfirm": "Moechten Sie diese Kalenderquelle wirklich loeschen?",
|
||||||
|
"sourceDeleteCta": "Quelle loeschen",
|
||||||
|
"providerDeleteConfirm": "Moechten Sie diesen Suchanbieter wirklich loeschen?",
|
||||||
|
"providerDeleteCta": "Anbieter loeschen",
|
||||||
|
"cancel": "Abbrechen"
|
||||||
|
},
|
||||||
|
"widgets": {
|
||||||
|
"emptyHeading": "Keine Widgets aktiv",
|
||||||
|
"emptyBody": "Klicken Sie auf Bearbeiten, um Widgets hinzuzufuegen.",
|
||||||
|
"addWidget": "Widget hinzufuegen",
|
||||||
|
"catalogTitle": "Widget hinzufuegen",
|
||||||
|
"deleteTooltip": "Widget entfernen",
|
||||||
|
"editMode": "Dashboard bearbeiten",
|
||||||
|
"saveChanges": "Aenderungen speichern",
|
||||||
|
"layoutLoadError": "Dashboard konnte nicht geladen werden. Bitte laden Sie die Seite neu.",
|
||||||
|
"widgetSaveError": "Aenderungen konnten nicht gespeichert werden. Bitte versuchen Sie es erneut.",
|
||||||
|
"clock.name": "Uhr",
|
||||||
|
"clock.description": "Zeigt die aktuelle Uhrzeit an",
|
||||||
|
"clock.dateHint": "Datum anzeigen",
|
||||||
|
"search.name": "Suchleiste",
|
||||||
|
"search.description": "Schnellsuche im Web",
|
||||||
|
"search.placeholder": "Suchen...",
|
||||||
|
"calendar.name": "Kalender",
|
||||||
|
"calendar.description": "Kommende Termine",
|
||||||
|
"calendar.emptyNoSources": "Keine Kalenderquellen konfiguriert",
|
||||||
|
"calendar.emptyNoEvents": "Keine anstehenden Termine",
|
||||||
|
"calendar.connectionSuccess": "Verbindung erfolgreich",
|
||||||
|
"calendar.connectionError": "Verbindung fehlgeschlagen. Bitte ueberpruefen Sie die URL und Zugangsdaten.",
|
||||||
|
"calendar.addSource": "Quelle hinzufuegen",
|
||||||
|
"calendar.sourceEmpty": "Keine Kalenderquellen eingerichtet. Fuegen Sie eine Quelle hinzu, um Termine anzuzeigen.",
|
||||||
|
"note.name": "Notizen",
|
||||||
|
"note.description": "Freitext-Notizen mit Markdown",
|
||||||
|
"note.defaultTitle": "Notiz",
|
||||||
|
"note.autosaveError": "Speichern fehlgeschlagen"
|
||||||
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"users": {
|
"users": {
|
||||||
"title": "Benutzerverwaltung",
|
"title": "Benutzerverwaltung",
|
||||||
|
|||||||
@@ -67,6 +67,7 @@
|
|||||||
"breadcrumb": {
|
"breadcrumb": {
|
||||||
"home": "Home"
|
"home": "Home"
|
||||||
},
|
},
|
||||||
|
"settings": "Settings",
|
||||||
"logout": "Sign Out",
|
"logout": "Sign Out",
|
||||||
"role": {
|
"role": {
|
||||||
"SUPER_ADMIN": "Super Admin",
|
"SUPER_ADMIN": "Super Admin",
|
||||||
@@ -98,6 +99,48 @@
|
|||||||
"empty": "No active widgets",
|
"empty": "No active widgets",
|
||||||
"addWidget": "Add widget"
|
"addWidget": "Add widget"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"link": "Settings",
|
||||||
|
"navLabel": "Settings",
|
||||||
|
"backToDashboard": "Back to Dashboard",
|
||||||
|
"categoryDashboard": "Dashboard",
|
||||||
|
"categoryWidgets": "Widgets",
|
||||||
|
"categoryCalendar": "Calendar",
|
||||||
|
"sourceDeleteConfirm": "Are you sure you want to delete this calendar source?",
|
||||||
|
"sourceDeleteCta": "Delete source",
|
||||||
|
"providerDeleteConfirm": "Are you sure you want to delete this search provider?",
|
||||||
|
"providerDeleteCta": "Delete provider",
|
||||||
|
"cancel": "Cancel"
|
||||||
|
},
|
||||||
|
"widgets": {
|
||||||
|
"emptyHeading": "No active widgets",
|
||||||
|
"emptyBody": "Click edit to add widgets to your dashboard.",
|
||||||
|
"addWidget": "Add widget",
|
||||||
|
"catalogTitle": "Add widget",
|
||||||
|
"deleteTooltip": "Remove widget",
|
||||||
|
"editMode": "Edit dashboard",
|
||||||
|
"saveChanges": "Save changes",
|
||||||
|
"layoutLoadError": "Could not load dashboard. Please reload the page.",
|
||||||
|
"widgetSaveError": "Could not save changes. Please try again.",
|
||||||
|
"clock.name": "Clock",
|
||||||
|
"clock.description": "Shows the current time",
|
||||||
|
"clock.dateHint": "Show date",
|
||||||
|
"search.name": "Search",
|
||||||
|
"search.description": "Quick web search",
|
||||||
|
"search.placeholder": "Search...",
|
||||||
|
"calendar.name": "Calendar",
|
||||||
|
"calendar.description": "Upcoming events",
|
||||||
|
"calendar.emptyNoSources": "No calendar sources configured",
|
||||||
|
"calendar.emptyNoEvents": "No upcoming events",
|
||||||
|
"calendar.connectionSuccess": "Connection successful",
|
||||||
|
"calendar.connectionError": "Connection failed. Please check the URL and credentials.",
|
||||||
|
"calendar.addSource": "Add source",
|
||||||
|
"calendar.sourceEmpty": "No calendar sources configured. Add a source to display events.",
|
||||||
|
"note.name": "Notes",
|
||||||
|
"note.description": "Free-text notes with Markdown",
|
||||||
|
"note.defaultTitle": "Note",
|
||||||
|
"note.autosaveError": "Save failed"
|
||||||
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
"users": {
|
"users": {
|
||||||
"title": "User Management",
|
"title": "User Management",
|
||||||
|
|||||||
Reference in New Issue
Block a user