feat(driver-pwa): shared UI + router with auth guard
Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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; }
|
||||
Reference in New Issue
Block a user