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';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useSidebarStore } from '@/lib/stores/sidebar-store';
|
||||
import { useAuthStore } from '@/lib/stores/auth-store';
|
||||
@@ -128,6 +129,31 @@ export function Header() {
|
||||
)}
|
||||
</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 */}
|
||||
<div className="px-2 py-1">
|
||||
<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": {
|
||||
"home": "Startseite"
|
||||
},
|
||||
"settings": "Einstellungen",
|
||||
"logout": "Abmelden",
|
||||
"role": {
|
||||
"SUPER_ADMIN": "Super-Admin",
|
||||
@@ -98,6 +99,48 @@
|
||||
"empty": "Keine Widgets aktiv",
|
||||
"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": {
|
||||
"users": {
|
||||
"title": "Benutzerverwaltung",
|
||||
|
||||
@@ -67,6 +67,7 @@
|
||||
"breadcrumb": {
|
||||
"home": "Home"
|
||||
},
|
||||
"settings": "Settings",
|
||||
"logout": "Sign Out",
|
||||
"role": {
|
||||
"SUPER_ADMIN": "Super Admin",
|
||||
@@ -98,6 +99,48 @@
|
||||
"empty": "No active widgets",
|
||||
"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": {
|
||||
"users": {
|
||||
"title": "User Management",
|
||||
|
||||
Reference in New Issue
Block a user