POL-124: Migrate frontend from React Native to Next.js web app
- Replace mobile/ (Expo) with web/ (Next.js 16 + Tailwind + shadcn/ui) - Pages: login, register, pending, championships, championship detail, registrations, profile, admin - Logic/view separated: hooks/ for data, components/ for UI, pages compose both - Types in src/types/ (one interface per file) - Auth: Zustand store + localStorage tokens + cookie presence flag for proxy - API layer: axios client with JWT auto-refresh Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
7
web/src/app/(auth)/layout.tsx
Normal file
7
web/src/app/(auth)/layout.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-violet-50 to-purple-100 p-4">
|
||||
<div className="w-full max-w-md">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
web/src/app/(auth)/login/page.tsx
Normal file
49
web/src/app/(auth)/login/page.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useLoginForm } from "@/hooks/useLoginForm";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function LoginPage() {
|
||||
const { email, setEmail, password, setPassword, error, isLoading, submit } = useLoginForm();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-2 text-4xl">🏆</div>
|
||||
<CardTitle className="text-2xl">Welcome back</CardTitle>
|
||||
<CardDescription>Sign in to your account</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<CardContent className="space-y-4">
|
||||
{error && <p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>}
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-col gap-3">
|
||||
<Button type="submit" className="w-full bg-violet-600 hover:bg-violet-700" disabled={isLoading}>
|
||||
{isLoading ? "Signing in…" : "Sign in"}
|
||||
</Button>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
No account?{" "}
|
||||
<Link href="/register" className="font-medium text-violet-600 hover:underline">
|
||||
Register
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
25
web/src/app/(auth)/pending/page.tsx
Normal file
25
web/src/app/(auth)/pending/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function PendingPage() {
|
||||
return (
|
||||
<Card className="text-center">
|
||||
<CardHeader>
|
||||
<div className="mx-auto mb-2 text-5xl">⏳</div>
|
||||
<CardTitle className="text-2xl">Awaiting approval</CardTitle>
|
||||
<CardDescription>
|
||||
Your organizer account has been submitted. An admin will review it shortly.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-6 text-sm text-gray-500">
|
||||
Once approved you can log in and start creating championships.
|
||||
</p>
|
||||
<Button asChild variant="outline" className="w-full">
|
||||
<Link href="/login">Back to login</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
71
web/src/app/(auth)/register/page.tsx
Normal file
71
web/src/app/(auth)/register/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRegisterForm } from "@/hooks/useRegisterForm";
|
||||
import { MemberFields } from "@/components/auth/MemberFields";
|
||||
import { OrganizerFields } from "@/components/auth/OrganizerFields";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const { role, setRole, form, update, error, isLoading, submit } = useRegisterForm();
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto mb-2 text-4xl">🏅</div>
|
||||
<CardTitle className="text-2xl">Create account</CardTitle>
|
||||
<CardDescription>Join the pole dance community</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<form onSubmit={submit}>
|
||||
<CardContent className="space-y-4">
|
||||
{error && <p className="rounded-md bg-red-50 px-3 py-2 text-sm text-red-600">{error}</p>}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{(["member", "organizer"] as const).map((r) => (
|
||||
<button
|
||||
key={r}
|
||||
type="button"
|
||||
onClick={() => setRole(r)}
|
||||
className={`rounded-lg border-2 p-3 text-sm font-medium transition-colors ${
|
||||
role === r ? "border-violet-600 bg-violet-50 text-violet-700" : "border-gray-200 text-gray-600 hover:border-gray-300"
|
||||
}`}
|
||||
>
|
||||
{r === "member" ? "🏅 Athlete" : "🏆 Organizer"}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<MemberFields
|
||||
full_name={form.full_name}
|
||||
email={form.email}
|
||||
password={form.password}
|
||||
phone={form.phone}
|
||||
onChange={update}
|
||||
/>
|
||||
|
||||
{role === "organizer" && (
|
||||
<OrganizerFields
|
||||
organization_name={form.organization_name}
|
||||
instagram_handle={form.instagram_handle}
|
||||
onChange={update}
|
||||
/>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<CardFooter className="flex flex-col gap-3">
|
||||
<Button type="submit" className="w-full bg-violet-600 hover:bg-violet-700" disabled={isLoading}>
|
||||
{isLoading ? "Creating…" : role === "member" ? "Create account" : "Submit for approval"}
|
||||
</Button>
|
||||
<p className="text-center text-sm text-gray-500">
|
||||
Have an account?{" "}
|
||||
<Link href="/login" className="font-medium text-violet-600 hover:underline">
|
||||
Sign in
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user