From 08cae39f597b05ea97b1c647d9d1bae5d1f28537 Mon Sep 17 00:00:00 2001 From: Munkherdene Date: Sat, 22 Aug 2026 20:39:22 +0800 Subject: [PATCH] feat(web): responsive app shell + tab navigation --- src/app/(app)/AppNav.tsx | 19 +++++++++++++++++++ src/app/(app)/home/page.tsx | 6 ++++++ src/app/(app)/layout.tsx | 20 ++++++++++++++++++++ src/app/(app)/shell.test.tsx | 16 ++++++++++++++++ src/app/page.tsx | 22 +++------------------- vitest.config.ts | 9 +++++++++ 6 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 src/app/(app)/AppNav.tsx create mode 100644 src/app/(app)/home/page.tsx create mode 100644 src/app/(app)/layout.tsx create mode 100644 src/app/(app)/shell.test.tsx diff --git a/src/app/(app)/AppNav.tsx b/src/app/(app)/AppNav.tsx new file mode 100644 index 0000000..5f6e673 --- /dev/null +++ b/src/app/(app)/AppNav.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { usePathname } from "next/navigation"; +import { TabBar, type TabKey } from "@/ds"; + +const TAB_ORDER: TabKey[] = ["home", "accounting", "planner", "assets", "profile"]; + +function activeTabFor(pathname: string | null): TabKey { + const match = TAB_ORDER.find((key) => pathname?.startsWith(`/${key}`)); + return match ?? "home"; +} + +/** Client-only sliver of the shell: reads the current route so the layout + * (a server component) can stay server-rendered while `TabBar` still knows + * which tab is active. */ +export function AppNav() { + const pathname = usePathname(); + return ; +} diff --git a/src/app/(app)/home/page.tsx b/src/app/(app)/home/page.tsx new file mode 100644 index 0000000..63682dd --- /dev/null +++ b/src/app/(app)/home/page.tsx @@ -0,0 +1,6 @@ +import { t } from "@/i18n/common"; + +// Placeholder — Task 9 replaces this with the real dashboard. +export default function HomePage() { + return

{t.tabs.home}

; +} diff --git a/src/app/(app)/layout.tsx b/src/app/(app)/layout.tsx new file mode 100644 index 0000000..8292f93 --- /dev/null +++ b/src/app/(app)/layout.tsx @@ -0,0 +1,20 @@ +import type { ReactNode } from "react"; +import { AppQueryProvider } from "@/api/queryClient"; +import { AppNav } from "./AppNav"; + +/** Shell for every authed route (guarded by middleware). Bottom tab bar on + * mobile, left sidebar on desktop — `TabBar` handles its own responsive + * switch, this just positions it next to a centered, max-width content + * column and wraps everything in the shared React Query provider. */ +export default function AppLayout({ children }: { children: ReactNode }) { + return ( + +
+ +
+ {children} +
+
+
+ ); +} diff --git a/src/app/(app)/shell.test.tsx b/src/app/(app)/shell.test.tsx new file mode 100644 index 0000000..3d8dcfa --- /dev/null +++ b/src/app/(app)/shell.test.tsx @@ -0,0 +1,16 @@ +import { render, screen } from "@testing-library/react"; +import { it, expect, vi } from "vitest"; +import { t } from "@/i18n/common"; + +vi.mock("next/navigation", () => ({ + usePathname: () => "/home", +})); + +import { AppNav } from "./AppNav"; + +it("renders all five tab labels", () => { + render(); + for (const label of Object.values(t.tabs)) { + expect(screen.getByText(label)).toBeInTheDocument(); + } +}); diff --git a/src/app/page.tsx b/src/app/page.tsx index 24dd4ef..3719d42 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,21 +1,5 @@ -import { ActionButton } from "@seed-design/react"; +import { redirect } from "next/navigation"; -export default function Home() { - return ( -
-

Mercury

- - Seed OK -
- ); +export default function RootPage() { + redirect("/home"); } diff --git a/vitest.config.ts b/vitest.config.ts index 9828ce6..88094b5 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,7 +1,16 @@ +import { fileURLToPath } from "node:url"; import { defineConfig } from "vitest/config"; import react from "@vitejs/plugin-react"; export default defineConfig({ plugins: [react()], + // Mirrors tsconfig's "@/*" -> "./src/*" path mapping (Next resolves this + // natively; Vite/Vitest need it spelled out) so ds/ modules that import + // via "@/..." (e.g. TabBar.tsx -> "@/i18n/common") resolve under Vitest too. + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, test: { environment: "jsdom", setupFiles: ["./vitest.setup.ts"],