feat(web): responsive app shell + tab navigation
This commit is contained in:
parent
071b5bdb6f
commit
08cae39f59
6 changed files with 73 additions and 19 deletions
19
src/app/(app)/AppNav.tsx
Normal file
19
src/app/(app)/AppNav.tsx
Normal file
|
|
@ -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 <TabBar active={activeTabFor(pathname)} />;
|
||||||
|
}
|
||||||
6
src/app/(app)/home/page.tsx
Normal file
6
src/app/(app)/home/page.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { t } from "@/i18n/common";
|
||||||
|
|
||||||
|
// Placeholder — Task 9 replaces this with the real dashboard.
|
||||||
|
export default function HomePage() {
|
||||||
|
return <h1 style={{ fontWeight: 700 }}>{t.tabs.home}</h1>;
|
||||||
|
}
|
||||||
20
src/app/(app)/layout.tsx
Normal file
20
src/app/(app)/layout.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<AppQueryProvider>
|
||||||
|
<div className="md:flex md:min-h-screen">
|
||||||
|
<AppNav />
|
||||||
|
<main className="mx-auto w-full max-w-2xl flex-1 px-4 pb-24 pt-6 md:pb-6 md:pt-8">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</AppQueryProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/(app)/shell.test.tsx
Normal file
16
src/app/(app)/shell.test.tsx
Normal file
|
|
@ -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(<AppNav />);
|
||||||
|
for (const label of Object.values(t.tabs)) {
|
||||||
|
expect(screen.getByText(label)).toBeInTheDocument();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,21 +1,5 @@
|
||||||
import { ActionButton } from "@seed-design/react";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default function Home() {
|
export default function RootPage() {
|
||||||
return (
|
redirect("/home");
|
||||||
<main style={{ padding: 24 }}>
|
|
||||||
<h1 style={{ fontWeight: 700 }}>Mercury</h1>
|
|
||||||
<button
|
|
||||||
style={{
|
|
||||||
background: "var(--mercury-brand-yellow)",
|
|
||||||
color: "var(--mercury-on-brand)",
|
|
||||||
padding: "12px 20px",
|
|
||||||
borderRadius: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Холбох
|
|
||||||
</button>
|
|
||||||
<ActionButton variant="brandSolid">Seed OK</ActionButton>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,16 @@
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
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: {
|
test: {
|
||||||
environment: "jsdom",
|
environment: "jsdom",
|
||||||
setupFiles: ["./vitest.setup.ts"],
|
setupFiles: ["./vitest.setup.ts"],
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue