feat(driver-pwa): shared UI + router with auth guard

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
2026-06-18 19:21:10 +10:00
co-authored by claude-flow
parent a7032692f1
commit f74f3688ae
11 changed files with 95 additions and 1 deletions
+25
View File
@@ -0,0 +1,25 @@
import { describe, it, expect, beforeEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import { useAuth } from "@/store/auth";
function renderAt(path: string) {
const qc = new QueryClient();
return render(
<QueryClientProvider client={qc}>
<MemoryRouter initialEntries={[path]}>
<App />
</MemoryRouter>
</QueryClientProvider>
);
}
describe("routing guard", () => {
beforeEach(() => useAuth.getState().clear());
it("redirects to login when no token", () => {
renderAt("/");
expect(screen.getByText(/Вход в приложение/i)).toBeInTheDocument();
});
});
+28 -1
View File
@@ -1,3 +1,30 @@
import type { ReactNode } from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { useAuth } from "@/store/auth";
import { LoginPage } from "@/pages/LoginPage";
import { OnboardingPage } from "@/pages/OnboardingPage";
import { BalancePage } from "@/pages/BalancePage";
import { TopupPage } from "@/pages/TopupPage";
import { PaymentPage } from "@/pages/PaymentPage";
import { HistoryPage } from "@/pages/HistoryPage";
function RequireAuth({ children }: { children: ReactNode }) {
const token = useAuth((s) => s.token);
return token ? <>{children}</> : <Navigate to="/login" replace />;
}
export default function App() {
return <div className="p-4">Премиум Водитель</div>;
return (
<div className="mx-auto max-w-md min-h-full px-4 pt-3 pb-8">
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route path="/onboarding" element={<OnboardingPage />} />
<Route path="/" element={<RequireAuth><BalancePage /></RequireAuth>} />
<Route path="/topup" element={<RequireAuth><TopupPage /></RequireAuth>} />
<Route path="/pay/:orderId" element={<RequireAuth><PaymentPage /></RequireAuth>} />
<Route path="/history" element={<RequireAuth><HistoryPage /></RequireAuth>} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</div>
);
}
@@ -0,0 +1,12 @@
export function AppHeader({ initials }: { initials?: string }) {
return (
<header className="flex items-center justify-between mb-4">
<span className="text-sm font-bold">Премиум Водитель</span>
{initials && (
<span className="w-7 h-7 rounded-full bg-ink text-cream text-[10px] font-bold flex items-center justify-center">
{initials}
</span>
)}
</header>
);
}
@@ -0,0 +1,19 @@
interface Props { value: string; options: { value: string; label: string }[]; onChange: (v: string) => void; }
export function Segment({ value, options, onChange }: Props) {
return (
<div className="flex gap-1.5">
{options.map((o) => (
<button
key={o.value}
type="button"
onClick={() => onChange(o.value)}
className={`flex-1 py-2.5 rounded-xl2 text-xs border ${
value === o.value ? "bg-ink text-cream border-ink font-bold" : "bg-surface border-line text-muted"
}`}
>
{o.label}
</button>
))}
</div>
);
}
@@ -0,0 +1,3 @@
export function Spinner() {
return <span className="inline-block w-3.5 h-3.5 border-2 border-line border-t-ink rounded-full animate-spin" />;
}
@@ -0,0 +1 @@
export function BalancePage() { return null; }
@@ -0,0 +1 @@
export function HistoryPage() { return null; }
@@ -0,0 +1,3 @@
export function LoginPage() {
return <div>Вход в приложение</div>;
}
@@ -0,0 +1 @@
export function OnboardingPage() { return null; }
@@ -0,0 +1 @@
export function PaymentPage() { return null; }
@@ -0,0 +1 @@
export function TopupPage() { return null; }