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() {
|
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