From d19660b50c1af8721382e144761b2a80e82e84b7 Mon Sep 17 00:00:00 2001 From: openit Date: Fri, 6 Mar 2026 15:41:29 +0000 Subject: [PATCH] Update project 10 FRONT TICKET QR 2 --- frontend-client/package-lock.json | 36 ----- frontend-client/src/api/client.ts | 23 +++ frontend-client/src/app/login/page.tsx | 194 +++++++++++++++++++++++++ frontend-client/src/app/page.tsx | 76 ++++++---- frontend-client/src/store/authStore.ts | 3 + 5 files changed, 266 insertions(+), 66 deletions(-) create mode 100644 frontend-client/src/app/login/page.tsx diff --git a/frontend-client/package-lock.json b/frontend-client/package-lock.json index 3c111f8..1cb58d4 100644 --- a/frontend-client/package-lock.json +++ b/frontend-client/package-lock.json @@ -329,9 +329,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -348,9 +345,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -367,9 +361,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -386,9 +377,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -986,9 +974,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1003,9 +988,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1020,9 +1002,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1037,9 +1016,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1054,9 +1030,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1071,9 +1044,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1088,9 +1058,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1105,9 +1072,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/frontend-client/src/api/client.ts b/frontend-client/src/api/client.ts index 7259a51..2877487 100644 --- a/frontend-client/src/api/client.ts +++ b/frontend-client/src/api/client.ts @@ -14,6 +14,29 @@ apiClient.interceptors.request.use((config) => { return config; }); +// ─── Auth ───────────────────────────────────────────────────────────────────── + +interface LoginResponse { + access_token: string; + token_type: string; +} + +/** + * POST /auth/login + * Returns a Bearer access token on success. + * Throws AxiosError 401 for invalid credentials, 422 for validation errors. + */ +export async function loginApi( + email: string, + password: string +): Promise<{ access_token: string }> { + const response = await apiClient.post("/auth/login", { + email, + password, + }); + return { access_token: response.data.access_token }; +} + // ─── Domain types (mirror backend schemas) ──────────────────────────────────── export interface TournamentInfo { diff --git a/frontend-client/src/app/login/page.tsx b/frontend-client/src/app/login/page.tsx new file mode 100644 index 0000000..90d9e52 --- /dev/null +++ b/frontend-client/src/app/login/page.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useState, type FormEvent } from "react"; +import { useRouter } from "next/navigation"; +import Link from "next/link"; +import { Mail, Lock, Loader2, Check } from "lucide-react"; +import type { AxiosError } from "axios"; +import { loginApi } from "@/api/client"; +import { useAuthStore } from "@/store/authStore"; + +// ─── Toast helper ───────────────────────────────────────────────────────────── + +type ToastState = { message: string; variant: "success" | "error" } | null; + +function Toast({ toast }: { toast: ToastState }) { + if (!toast) return null; + const isSuccess = toast.variant === "success"; + return ( +
+ {isSuccess && } + {toast.message} +
+ ); +} + +// ─── Input field ────────────────────────────────────────────────────────────── + +function Field({ + id, + label, + type, + value, + onChange, + placeholder, + icon: Icon, + autoComplete, +}: { + id: string; + label: string; + type: string; + value: string; + onChange: (v: string) => void; + placeholder: string; + icon: React.ElementType; + autoComplete?: string; +}) { + return ( +
+ +
+ + onChange(e.target.value)} + placeholder={placeholder} + autoComplete={autoComplete} + required + className="flex-1 bg-transparent text-[15px] text-white placeholder-[#4B5563] outline-none" + /> +
+
+ ); +} + +// ─── Page ───────────────────────────────────────────────────────────────────── + +export default function LoginPage() { + const router = useRouter(); + const setToken = useAuthStore((s) => s.setToken); + + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [loading, setLoading] = useState(false); + const [toast, setToast] = useState(null); + + function showToast(message: string, variant: "success" | "error") { + setToast({ message, variant }); + setTimeout(() => setToast(null), 3000); + } + + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + if (loading) return; + + setLoading(true); + try { + const { access_token } = await loginApi(email, password); + setToken(access_token); + showToast("Успешно", "success"); + // Short delay so the user sees the success toast before navigation + setTimeout(() => router.push("/tickets"), 900); + } catch (err) { + const axiosErr = err as AxiosError; + const status = axiosErr.response?.status; + if (status === 401 || status === 422) { + showToast("Ошибка авторизации", "error"); + } else { + showToast("Ошибка сети. Попробуйте снова.", "error"); + } + } finally { + setLoading(false); + } + } + + return ( +
+
+ + + + {/* ── Brand ── */} +
+
+ F +
+

+ Вход в аккаунт +

+

+ Войдите, чтобы управлять билетами +

+
+ + {/* ── Form ── */} +
+ + + + + + + {/* ── Footer links ── */} +
+

+ Нет аккаунта?{" "} + + Создать + +

+ + ← Вернуться к событиям + +
+ +
+
+ ); +} diff --git a/frontend-client/src/app/page.tsx b/frontend-client/src/app/page.tsx index 8df1176..c86b59c 100644 --- a/frontend-client/src/app/page.tsx +++ b/frontend-client/src/app/page.tsx @@ -1,8 +1,10 @@ "use client"; import { useState } from "react"; -import { Search, CalendarDays, Ticket, User } from "lucide-react"; +import Link from "next/link"; +import { Search, CalendarDays, Ticket, User, LogIn } from "lucide-react"; import EventCard, { type EventItem } from "@/components/EventCard"; +import { useAuthStore } from "@/store/authStore"; const MOCK_EVENTS: EventItem[] = [ { @@ -43,31 +45,36 @@ const MOCK_EVENTS: EventItem[] = [ const FILTERS = ["Все", "Ближайшие", "Популярные", "Скоро"] as const; type Filter = (typeof FILTERS)[number]; -const NAV_ITEMS = [ - { label: "События", icon: CalendarDays }, - { label: "Мои билеты", icon: Ticket }, - { label: "Профиль", icon: User }, -] as const; - export default function HomePage() { + const token = useAuthStore((s) => s.token); const [activeFilter, setActiveFilter] = useState("Все"); - const [activeNav, setActiveNav] = useState(0); return (
- {/* Phone-width wrapper */}
+ {/* ── Header ── */}

События

- +
+ {!token && ( + + + Войти + + )} + +
{/* ── Filter tabs ── */} @@ -97,23 +104,32 @@ export default function HomePage() { {/* ── Bottom navigation ── */} +
); diff --git a/frontend-client/src/store/authStore.ts b/frontend-client/src/store/authStore.ts index 37d0375..475f4fa 100644 --- a/frontend-client/src/store/authStore.ts +++ b/frontend-client/src/store/authStore.ts @@ -9,6 +9,8 @@ interface AuthUser { interface AuthState { token: string | null; user: AuthUser | null; + /** Persist token only (user data not available from login response). */ + setToken: (token: string) => void; setAuth: (token: string, user: AuthUser) => void; clearAuth: () => void; } @@ -18,6 +20,7 @@ export const useAuthStore = create()( (set) => ({ token: null, user: null, + setToken: (token) => set({ token }), setAuth: (token, user) => set({ token, user }), clearAuth: () => set({ token: null, user: null }), }),