Bladeren bron

RHL-019 (feat): app shell & routing scaffold

Code_Uwe 1 maand geleden
bovenliggende
commit
c9a8b205e0

+ 18 - 0
app/(protected)/[branch]/[year]/[month]/[day]/page.jsx

@@ -0,0 +1,18 @@
+import PlaceholderPage from "@/components/placeholders/PlaceholderPage";
+
+/**
+ * /:branch/:year/:month/:day
+ *
+ * Next.js 15+ treats `params` as an async value (Promise) for dynamic routes.
+ */
+export default async function BranchYearMonthDayPage({ params }) {
+	const resolvedParams = await params;
+
+	return (
+		<PlaceholderPage
+			title="Day"
+			description="Day placeholder (future: file list + PDF viewer)."
+			params={resolvedParams}
+		/>
+	);
+}

+ 18 - 0
app/(protected)/[branch]/[year]/[month]/page.jsx

@@ -0,0 +1,18 @@
+import PlaceholderPage from "@/components/placeholders/PlaceholderPage";
+
+/**
+ * /:branch/:year/:month
+ *
+ * Next.js 15+ treats `params` as an async value (Promise) for dynamic routes.
+ */
+export default async function BranchYearMonthPage({ params }) {
+	const resolvedParams = await params;
+
+	return (
+		<PlaceholderPage
+			title="Month"
+			description="Month placeholder (future: days overview / explorer drill-down)."
+			params={resolvedParams}
+		/>
+	);
+}

+ 18 - 0
app/(protected)/[branch]/[year]/page.jsx

@@ -0,0 +1,18 @@
+import PlaceholderPage from "@/components/placeholders/PlaceholderPage";
+
+/**
+ * /:branch/:year
+ *
+ * Next.js 15+ treats `params` as an async value (Promise) for dynamic routes.
+ */
+export default async function BranchYearPage({ params }) {
+	const resolvedParams = await params;
+
+	return (
+		<PlaceholderPage
+			title="Year"
+			description="Year placeholder (future: months overview / explorer drill-down)."
+			params={resolvedParams}
+		/>
+	);
+}

+ 19 - 0
app/(protected)/[branch]/page.jsx

@@ -0,0 +1,19 @@
+import PlaceholderPage from "@/components/placeholders/PlaceholderPage";
+
+/**
+ * /:branch
+ *
+ * Next.js 15+ treats `params` as an async value (Promise) for dynamic routes.
+ * We explicitly await it here to avoid "sync dynamic APIs" runtime errors.
+ */
+export default async function BranchPage({ params }) {
+	const resolvedParams = await params;
+
+	return (
+		<PlaceholderPage
+			title="Branch"
+			description="Branch dashboard placeholder (will become the explorer entry point)."
+			params={resolvedParams}
+		/>
+	);
+}

+ 22 - 0
app/(protected)/[branch]/search/page.jsx

@@ -0,0 +1,22 @@
+import PlaceholderPage from "@/components/placeholders/PlaceholderPage";
+
+/**
+ * /:branch/search
+ *
+ * Important:
+ * - This is a static segment under [branch].
+ * - It must exist explicitly, so "search" is not interpreted as [year].
+ *
+ * Next.js 15+ treats `params` as an async value (Promise) for dynamic routes.
+ */
+export default async function BranchSearchPage({ params }) {
+	const resolvedParams = await params;
+
+	return (
+		<PlaceholderPage
+			title="Search"
+			description="Search placeholder. Real search UI will be implemented in a later ticket."
+			params={resolvedParams}
+		/>
+	);
+}

+ 14 - 0
app/(protected)/layout.jsx

@@ -0,0 +1,14 @@
+import AppShell from "@/components/app-shell/AppShell";
+
+/**
+ * Protected layout:
+ * - Wraps all authenticated pages in a consistent application shell
+ * - No auth guard yet (comes in later tickets)
+ *
+ * RHL-019 goal:
+ * - the layout exists and is stable so later we only add guard logic,
+ *   without restructuring the UI.
+ */
+export default function ProtectedLayout({ children }) {
+	return <AppShell>{children}</AppShell>;
+}

+ 40 - 0
app/(protected)/page.jsx

@@ -0,0 +1,40 @@
+import PlaceholderPage from "@/components/placeholders/PlaceholderPage";
+
+/**
+ * /
+ *
+ * RHL-019:
+ * - Placeholder entry page
+ *
+ * Later:
+ * - If unauthenticated: redirect to /login
+ * - If authenticated: redirect to a branch route (e.g. /NL01)
+ */
+export default function ProtectedEntryPage() {
+	return (
+		<PlaceholderPage
+			title="Entry"
+			description='This is the protected entry route ("/"). Later it will redirect based on the session.'
+		>
+			<div className="space-y-2 text-sm text-muted-foreground">
+				<p>Try these URLs manually:</p>
+				<ul className="list-disc pl-5">
+					<li>
+						<code className="rounded bg-muted px-1 py-0.5">/login</code>
+					</li>
+					<li>
+						<code className="rounded bg-muted px-1 py-0.5">/NL01</code>
+					</li>
+					<li>
+						<code className="rounded bg-muted px-1 py-0.5">
+							/NL01/2025/12/31
+						</code>
+					</li>
+					<li>
+						<code className="rounded bg-muted px-1 py-0.5">/NL01/search</code>
+					</li>
+				</ul>
+			</div>
+		</PlaceholderPage>
+	);
+}

+ 18 - 0
app/(public)/layout.jsx

@@ -0,0 +1,18 @@
+/**
+ * Public layout for routes that should not display the authenticated app shell.
+ *
+ * Examples:
+ * - /login
+ *
+ * Keep it minimal and centered.
+ * The root layout (app/layout.js) already provides theme + global styling.
+ */
+export default function PublicLayout({ children }) {
+	return (
+		<div className="min-h-screen">
+			<div className="mx-auto flex min-h-screen w-full max-w-md items-center justify-center p-4">
+				{children}
+			</div>
+		</div>
+	);
+}

+ 56 - 0
app/(public)/login/page.jsx

@@ -0,0 +1,56 @@
+import { Button } from "@/components/ui/button";
+
+/**
+ * /login
+ *
+ * RHL-019 scope:
+ * - UI placeholder only
+ * - No form logic, no apiClient calls, no redirects yet
+ *
+ * Future tickets (RHL-020/RHL-021) will implement:
+ * - session checks (getMe)
+ * - actual login flow
+ * - redirects into the protected app
+ */
+export default function LoginPage() {
+	return (
+		<div className="w-full space-y-6">
+			<div className="space-y-2 text-center">
+				<h1 className="text-2xl font-semibold tracking-tight">
+					RHL Lieferscheine
+				</h1>
+				<p className="text-sm text-muted-foreground">
+					Login placeholder (RHL-019 scaffold)
+				</p>
+			</div>
+
+			<div className="rounded-lg border bg-card p-6 text-card-foreground shadow-sm">
+				<div className="space-y-3">
+					<p className="text-sm text-muted-foreground">
+						This is a placeholder page. The real login form and session guard
+						will be implemented in a later ticket.
+					</p>
+
+					<div className="flex gap-2">
+						<Button disabled aria-disabled="true" title="Not implemented yet">
+							Sign in (TODO)
+						</Button>
+						<Button
+							variant="outline"
+							disabled
+							aria-disabled="true"
+							title="Not implemented yet"
+						>
+							Forgot password (TODO)
+						</Button>
+					</div>
+				</div>
+			</div>
+
+			<p className="text-center text-xs text-muted-foreground">
+				Tip: You can already test routing by opening /, /NL01, /NL01/2025/12/31
+				etc.
+			</p>
+		</div>
+	);
+}

+ 12 - 5
app/layout.js → app/layout.jsx

@@ -14,7 +14,7 @@ const geistMono = Geist_Mono({
 
 export const metadata = {
 	title: "RHL Lieferscheine",
-	description: "Interne Lieferscheine-App",
+	description: "Internal delivery note browser",
 };
 
 export default function RootLayout({ children }) {
@@ -24,10 +24,17 @@ export default function RootLayout({ children }) {
 				className={`${geistSans.variable} ${geistMono.variable} min-h-screen bg-background text-foreground antialiased`}
 			>
 				<ThemeProvider
-					attribute="class" // <html class="dark"> ...
-					defaultTheme="system" // System-Theme als Default
-					enableSystem // System-Theme berücksichtigen
-					disableTransitionOnChange // keine hässlichen Transition-Jumps
+					/*
+						Theme setup (shadcn/ui + next-themes):
+						- attribute="class" means themes are controlled via <html class="dark">
+						- defaultTheme="system" respects the OS preference
+						- enableSystem keeps system sync
+						- disableTransitionOnChange avoids flicker/jumpy transitions
+					*/
+					attribute="class"
+					defaultTheme="system"
+					enableSystem
+					disableTransitionOnChange
 				>
 					{children}
 				</ThemeProvider>

+ 0 - 15
app/page.js

@@ -1,15 +0,0 @@
-import { ModeToggle } from "@/components/ui/mode-toggle";
-
-export default function Home() {
-	return (
-		<>
-			<main className="flex min-h-screen flex-col items-center justify-center gap-4">
-				<h1 className="text-3xl font-bold">RHL Lieferscheine</h1>
-				<p className="text-muted-foreground">
-					Darkmode-Test mit shadcn + next-themes
-				</p>
-				<ModeToggle />
-			</main>
-		</>
-	);
-}

+ 41 - 0
components/app-shell/AppShell.jsx

@@ -0,0 +1,41 @@
+import React from "react";
+import TopNav from "@/components/app-shell/TopNav";
+import SidebarPlaceholder from "@/components/app-shell/SidebarPlaceholder";
+
+/**
+ * AppShell
+ *
+ * Purpose:
+ * - Provide a stable frame for all protected pages:
+ *   - Top navigation (always visible)
+ *   - Optional sidebar area (desktop)
+ *   - Main content area (route pages render here)
+ *
+ * Layout notes:
+ * - The outer wrapper must be `flex flex-col` so the content region can use `flex-1`
+ *   and fill the remaining viewport height below the TopNav.
+ *
+ * Test/runtime note:
+ * - Our Vitest/Vite setup currently uses the "classic" JSX runtime in unit tests,
+ *   which requires React to be in scope. Importing React here ensures tests work
+ *   without introducing additional build tooling configuration.
+ */
+export default function AppShell({ children }) {
+	return (
+		<div className="min-h-screen flex flex-col">
+			<TopNav />
+
+			{/* Content area below the top navigation */}
+			<div className="mx-auto flex w-full max-w-7xl flex-1 gap-4 px-4 py-4">
+				{/* Sidebar is reserved space for navigation/filter UI.
+            We hide it on small screens for now (responsive baseline). */}
+				<aside className="hidden w-64 shrink-0 md:block">
+					<SidebarPlaceholder />
+				</aside>
+
+				{/* Main content: all route pages render here */}
+				<main className="min-w-0 flex-1">{children}</main>
+			</div>
+		</div>
+	);
+}

+ 39 - 0
components/app-shell/SidebarPlaceholder.jsx

@@ -0,0 +1,39 @@
+import React from "react";
+
+/**
+ * SidebarPlaceholder
+ *
+ * Why we reserve a sidebar area even for branch users:
+ * - Admin/dev will later need a branch selector.
+ * - Branch users can still benefit from navigation and filters:
+ *   - quick jump (year/month/day)
+ *   - search filters (archive toggle, date range)
+ *   - recent/favorites
+ *
+ * RHL-019:
+ * - Placeholder only (no logic yet).
+ *
+ * Test/runtime note:
+ * - See AppShell.jsx for details why we import React explicitly.
+ */
+export default function SidebarPlaceholder() {
+	return (
+		<div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
+			<div className="space-y-3">
+				<div>
+					<p className="text-sm font-medium">Sidebar</p>
+					<p className="text-xs text-muted-foreground">
+						Navigation & filters will live here.
+					</p>
+				</div>
+
+				<ul className="space-y-1 text-xs text-muted-foreground">
+					<li>• Branch context (label or selector)</li>
+					<li>• Explorer navigation (year/month/day)</li>
+					<li>• Search filters (archive, date range, ...)</li>
+					<li>• Shortcuts (recent, favorites)</li>
+				</ul>
+			</div>
+		</div>
+	);
+}

+ 57 - 0
components/app-shell/TopNav.jsx

@@ -0,0 +1,57 @@
+import React from "react";
+import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import UserStatus from "@/components/app-shell/UserStatus";
+
+/**
+ * TopNav
+ *
+ * RHL-019:
+ * - App branding
+ * - User status placeholder (later: getMe + role/branch info)
+ * - Logout button placeholder (later: wired to apiClient.logout)
+ * - Theme toggle placeholder (optional; can be replaced by the real ModeToggle)
+ *
+ * Test/runtime note:
+ * - See AppShell.jsx for details why we import React explicitly.
+ */
+export default function TopNav() {
+	return (
+		<header className="sticky top-0 z-50 w-full border-b bg-background/80 backdrop-blur">
+			<div className="mx-auto flex h-14 max-w-7xl items-center justify-between px-4">
+				<div className="flex items-center gap-3">
+					<Link href="/" className="font-semibold tracking-tight">
+						RHL Lieferscheine
+					</Link>
+					<span className="text-xs text-muted-foreground">
+						App shell scaffold
+					</span>
+				</div>
+
+				<div className="flex items-center gap-2">
+					<UserStatus />
+
+					<Button
+						variant="outline"
+						size="sm"
+						disabled
+						aria-disabled="true"
+						title="Theme toggle will be added later"
+					>
+						Theme
+					</Button>
+
+					<Button
+						variant="outline"
+						size="sm"
+						disabled
+						aria-disabled="true"
+						title="Logout wiring will be added later"
+					>
+						Logout
+					</Button>
+				</div>
+			</div>
+		</header>
+	);
+}

+ 25 - 0
components/app-shell/UserStatus.jsx

@@ -0,0 +1,25 @@
+import React from "react";
+
+/**
+ * UserStatus
+ *
+ * RHL-019:
+ * - Placeholder only.
+ *
+ * Later:
+ * - This component will read session state (via apiClient.getMe()) and display:
+ *   - username or userId
+ *   - role
+ *   - branchId (for branch users)
+ *
+ * Test/runtime note:
+ * - See AppShell.jsx for details why we import React explicitly.
+ */
+export default function UserStatus() {
+	return (
+		<div className="hidden items-center gap-2 md:flex">
+			<span className="text-xs text-muted-foreground">User:</span>
+			<span className="text-xs">Not loaded</span>
+		</div>
+	);
+}

+ 44 - 0
components/placeholders/PlaceholderPage.jsx

@@ -0,0 +1,44 @@
+/**
+ * PlaceholderPage
+ *
+ * A small, reusable component to render consistent placeholder pages.
+ *
+ * Goals:
+ * - Avoid duplicating layout code across many route pages.
+ * - Make it easy to replace placeholders with real UI later.
+ */
+export default function PlaceholderPage({
+	title,
+	description,
+	params,
+	children,
+}) {
+	return (
+		<div className="space-y-4">
+			<div className="space-y-1">
+				<h1 className="text-2xl font-semibold tracking-tight">{title}</h1>
+				{description ? (
+					<p className="text-sm text-muted-foreground">{description}</p>
+				) : null}
+			</div>
+
+			<div className="rounded-lg border bg-card p-4 text-card-foreground shadow-sm">
+				<div className="space-y-3">
+					<p className="text-sm font-medium">Route params</p>
+
+					{params ? (
+						<pre className="overflow-auto rounded-md bg-muted p-3 text-xs">
+							{JSON.stringify(params, null, 2)}
+						</pre>
+					) : (
+						<p className="text-xs text-muted-foreground">
+							No params for this route.
+						</p>
+					)}
+
+					{children ? <div className="pt-2">{children}</div> : null}
+				</div>
+			</div>
+		</div>
+	);
+}

+ 10 - 4
components/ui/mode-toggle.js → components/ui/mode-toggle.jsx

@@ -1,7 +1,15 @@
-// components/mode-toggle.js
 "use client";
 
-import React from "react";
+/**
+ * ModeToggle
+ *
+ * Small theme toggle dropdown using next-themes.
+ *
+ * Note:
+ * - This is not wired into the AppShell TopNav yet (RHL-019 uses a placeholder button).
+ * - We keep it in the project because it is useful later and already proven working.
+ */
+
 import { Moon, Sun } from "lucide-react";
 import { useTheme } from "next-themes";
 
@@ -20,9 +28,7 @@ export function ModeToggle() {
 		<DropdownMenu>
 			<DropdownMenuTrigger asChild>
 				<Button variant="outline" size="icon">
-					{/* Sun (Light) */}
 					<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
-					{/* Moon (Dark) */}
 					<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
 					<span className="sr-only">Theme umschalten</span>
 				</Button>

+ 0 - 9
components/ui/theme-provider.js

@@ -1,9 +0,0 @@
-// components/theme-provider.js
-"use client";
-
-import React from "react";
-import { ThemeProvider as NextThemesProvider } from "next-themes";
-
-export function ThemeProvider({ children, ...props }) {
-	return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
-}

+ 17 - 0
components/ui/theme-provider.jsx

@@ -0,0 +1,17 @@
+"use client";
+
+/**
+ * ThemeProvider
+ *
+ * Thin wrapper around next-themes ThemeProvider.
+ *
+ * Why we keep it:
+ * - Centralizes the ThemeProvider import and keeps app/layout clean.
+ * - Allows future extensions (e.g. default theme policy, logging) in one place.
+ */
+
+import { ThemeProvider as NextThemesProvider } from "next-themes";
+
+export function ThemeProvider({ children, ...props }) {
+	return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
+}