From e7b2a70fc98d34f3049cffe75e271ad0123c016a Mon Sep 17 00:00:00 2001 From: Schalli Date: Thu, 18 Jun 2026 13:34:43 +0200 Subject: [PATCH] feat(02-02): login page UI, header/sidebar auth wiring, i18n keys - Create split-screen login page with branding left (#ffed00) and form right (D-01) - Login form has username, password, and remember-me checkbox (D-02) - Wire header user avatar with auth store: shows user initial, dropdown with role badge and logout - Wire sidebar footer with auth store: shows user name, role, and initial - Add auth, header role, and admin i18n keys to both de.json and en.json - All new UI strings use t() function (no hardcoded text) Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/app/(auth)/login/page.tsx | 193 ++++++++++++++++++ apps/web/src/components/layout/header.tsx | 128 ++++++++++-- .../src/components/layout/sidebar-footer.tsx | 43 ++-- apps/web/src/messages/de.json | 69 ++++++- apps/web/src/messages/en.json | 69 ++++++- 5 files changed, 459 insertions(+), 43 deletions(-) create mode 100644 apps/web/src/app/(auth)/login/page.tsx diff --git a/apps/web/src/app/(auth)/login/page.tsx b/apps/web/src/app/(auth)/login/page.tsx new file mode 100644 index 0000000..7a39455 --- /dev/null +++ b/apps/web/src/app/(auth)/login/page.tsx @@ -0,0 +1,193 @@ +'use client'; + +import { useState, useTransition } from 'react'; +import { useTranslations } from 'next-intl'; +import { useRouter } from 'next/navigation'; +import { login } from '@/lib/auth-actions'; + +/** + * Split-screen login page (D-01). + * Left: Tessera branding with primary yellow (#ffed00) background. + * Right: Login form on neutral background. + * No sidebar or header (D-04, handled by (auth) layout). + */ +export default function LoginPage() { + const t = useTranslations('auth'); + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + + const formData = new FormData(e.currentTarget); + + startTransition(async () => { + const result = await login(formData); + if (result.success) { + router.push('/'); + router.refresh(); + } else { + setError(result.error ?? 'invalidCredentials'); + } + }); + } + + return ( +
+ {/* Left: Branding panel (hidden on mobile, visible on md+) */} +
+
+ {/* Tessera logo / text */} +

+ Tessera +

+

+ {t('branding.tagline')} +

+ {/* Abstract grid icon representing modules */} +
+ + + + + + +
+
+
+ + {/* Right: Login form */} +
+
+ {/* Mobile-only branding */} +
+

+ Tessera +

+

+ {t('branding.tagline')} +

+
+ + {/* Login heading */} +
+

+ {t('login')} +

+
+ + {/* Error message */} + {error && ( +
+ {t(`error.${error}`)} +
+ )} + + {/* Login form */} +
+ {/* Username */} +
+ + +
+ + {/* Password */} +
+ + +
+ + {/* Remember me (D-02) */} +
+ + +
+ + {/* Submit button */} + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/layout/header.tsx b/apps/web/src/components/layout/header.tsx index 2996137..1267158 100644 --- a/apps/web/src/components/layout/header.tsx +++ b/apps/web/src/components/layout/header.tsx @@ -1,12 +1,58 @@ 'use client'; +import { useEffect, useRef, useState } from 'react'; import { useTranslations } from 'next-intl'; import { useSidebarStore } from '@/lib/stores/sidebar-store'; +import { useAuthStore } from '@/lib/stores/auth-store'; +import { fetchCurrentUser, logout } from '@/lib/auth-actions'; import { ThemeToggle } from '@/components/theme-toggle'; export function Header() { const t = useTranslations(); + const tHeader = useTranslations('header'); const setMobileOpen = useSidebarStore((s) => s.setMobileOpen); + const { user, setUser } = useAuthStore(); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); + + // Fetch current user on mount to populate auth store + useEffect(() => { + if (!user) { + fetchCurrentUser().then((u) => { + if (u) { + setUser({ + id: u.id, + username: u.username, + displayName: u.displayName, + role: u.role, + tenantId: u.tenantId, + }); + } + }); + } + }, [user, setUser]); + + // Close dropdown on outside click + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setDropdownOpen(false); + } + } + if (dropdownOpen) { + document.addEventListener('mousedown', handleClickOutside); + } + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [dropdownOpen]); + + const userInitial = user + ? (user.displayName ?? user.username).charAt(0).toUpperCase() + : '?'; + + const roleLabel = user ? tHeader(`role.${user.role}`) : ''; return (
@@ -41,32 +87,76 @@ export function Header() { {/* Center: Breadcrumb / page title */}
- {t('header.breadcrumb.home')} + {tHeader('breadcrumb.home')}
{/* Right: Actions */}
- {/* User avatar placeholder - wired to auth in Phase 2 */} -
- + + {userInitial} + + + {/* Dropdown menu */} + {dropdownOpen && ( +
+ {/* User info */} +
+

+ {user?.displayName ?? user?.username ?? '...'} +

+ {user && ( + + {roleLabel} + + )} +
+ + {/* Logout button */} +
+ +
+
+ )}
diff --git a/apps/web/src/components/layout/sidebar-footer.tsx b/apps/web/src/components/layout/sidebar-footer.tsx index 23225f4..c1c4dd6 100644 --- a/apps/web/src/components/layout/sidebar-footer.tsx +++ b/apps/web/src/components/layout/sidebar-footer.tsx @@ -2,11 +2,21 @@ import { useTranslations } from 'next-intl'; import { useSidebarStore } from '@/lib/stores/sidebar-store'; +import { useAuthStore } from '@/lib/stores/auth-store'; import { LocaleSwitcher } from '@/components/locale-switcher'; export function SidebarFooter() { const t = useTranslations('sidebar'); + const tHeader = useTranslations('header'); const isCollapsed = useSidebarStore((s) => s.isCollapsed); + const user = useAuthStore((s) => s.user); + + const userInitial = user + ? (user.displayName ?? user.username).charAt(0).toUpperCase() + : '?'; + + const userName = user?.displayName ?? user?.username ?? t('userPlaceholder'); + const roleLabel = user ? tHeader(`role.${user.role}`) : ''; return (
@@ -39,29 +49,22 @@ export function SidebarFooter() { )} - {/* User info placeholder */} + {/* User info - wired to auth store */}
-
- - - - +
+ {userInitial}
{!isCollapsed && ( - - {t('userPlaceholder')} - +
+ + {userName} + + {user && ( + + {roleLabel} + + )} +
)}
diff --git a/apps/web/src/messages/de.json b/apps/web/src/messages/de.json index 27c70f2..7013da2 100644 --- a/apps/web/src/messages/de.json +++ b/apps/web/src/messages/de.json @@ -4,13 +4,44 @@ "loading": "Laden...", "save": "Speichern", "cancel": "Abbrechen", - "menu": "Menu" + "menu": "Menu", + "confirm": "Bestaetigen", + "delete": "Loeschen", + "edit": "Bearbeiten", + "create": "Erstellen", + "close": "Schliessen", + "yes": "Ja", + "no": "Nein", + "active": "Aktiv", + "inactive": "Inaktiv", + "actions": "Aktionen", + "accessDenied": "Zugriff verweigert" + }, + "auth": { + "login": "Anmelden", + "username": "Benutzername", + "password": "Passwort", + "rememberMe": "Angemeldet bleiben", + "submit": "Anmelden", + "error": { + "invalidCredentials": "Benutzername oder Passwort ungueltig", + "networkError": "Verbindungsfehler. Bitte versuchen Sie es erneut." + }, + "branding": { + "tagline": "Modulare Workflow-Plattform fuer Ihr Unternehmen" + } }, "header": { "search": "Suchen...", "userMenu": "Benutzermenu", "breadcrumb": { "home": "Startseite" + }, + "logout": "Abmelden", + "role": { + "SUPER_ADMIN": "Super-Admin", + "ADMIN": "Admin", + "USER": "Benutzer" } }, "sidebar": { @@ -23,13 +54,47 @@ "categories": { "label": "Kategorien", "empty": "Keine Module" - } + }, + "admin": "Verwaltung", + "users": "Benutzer", + "tenants": "Mandanten" }, "dashboard": { "title": "Dashboard", "empty": "Keine Widgets aktiv", "addWidget": "Widget hinzufuegen" }, + "admin": { + "users": { + "title": "Benutzerverwaltung", + "create": "Benutzer erstellen", + "edit": "Benutzer bearbeiten", + "delete": "Benutzer loeschen", + "deleteConfirm": "Moechten Sie diesen Benutzer wirklich loeschen?", + "name": "Name", + "username": "Benutzername", + "email": "E-Mail", + "displayName": "Anzeigename", + "role": "Rolle", + "status": "Status", + "actions": "Aktionen", + "password": "Passwort", + "noUsers": "Keine Benutzer gefunden" + }, + "tenants": { + "title": "Mandantenverwaltung", + "create": "Mandant erstellen", + "edit": "Mandant bearbeiten", + "name": "Name", + "slug": "Slug", + "status": "Status", + "userCount": "Benutzer", + "actions": "Aktionen", + "noTenants": "Keine Mandanten gefunden", + "deleteConfirm": "Moechten Sie diesen Mandanten wirklich loeschen?", + "cannotDeleteActive": "Mandanten mit aktiven Benutzern koennen nicht geloescht werden" + } + }, "theme": { "light": "Hell", "dark": "Dunkel", diff --git a/apps/web/src/messages/en.json b/apps/web/src/messages/en.json index 1de1900..454ef31 100644 --- a/apps/web/src/messages/en.json +++ b/apps/web/src/messages/en.json @@ -4,13 +4,44 @@ "loading": "Loading...", "save": "Save", "cancel": "Cancel", - "menu": "Menu" + "menu": "Menu", + "confirm": "Confirm", + "delete": "Delete", + "edit": "Edit", + "create": "Create", + "close": "Close", + "yes": "Yes", + "no": "No", + "active": "Active", + "inactive": "Inactive", + "actions": "Actions", + "accessDenied": "Access denied" + }, + "auth": { + "login": "Sign In", + "username": "Username", + "password": "Password", + "rememberMe": "Remember me", + "submit": "Sign In", + "error": { + "invalidCredentials": "Invalid username or password", + "networkError": "Connection error. Please try again." + }, + "branding": { + "tagline": "Modular workflow platform for your organization" + } }, "header": { "search": "Search...", "userMenu": "User menu", "breadcrumb": { "home": "Home" + }, + "logout": "Sign Out", + "role": { + "SUPER_ADMIN": "Super Admin", + "ADMIN": "Admin", + "USER": "User" } }, "sidebar": { @@ -23,13 +54,47 @@ "categories": { "label": "Categories", "empty": "No modules" - } + }, + "admin": "Administration", + "users": "Users", + "tenants": "Tenants" }, "dashboard": { "title": "Dashboard", "empty": "No active widgets", "addWidget": "Add widget" }, + "admin": { + "users": { + "title": "User Management", + "create": "Create User", + "edit": "Edit User", + "delete": "Delete User", + "deleteConfirm": "Are you sure you want to delete this user?", + "name": "Name", + "username": "Username", + "email": "Email", + "displayName": "Display Name", + "role": "Role", + "status": "Status", + "actions": "Actions", + "password": "Password", + "noUsers": "No users found" + }, + "tenants": { + "title": "Tenant Management", + "create": "Create Tenant", + "edit": "Edit Tenant", + "name": "Name", + "slug": "Slug", + "status": "Status", + "userCount": "Users", + "actions": "Actions", + "noTenants": "No tenants found", + "deleteConfirm": "Are you sure you want to delete this tenant?", + "cannotDeleteActive": "Cannot delete tenants with active users" + } + }, "theme": { "light": "Light", "dark": "Dark",