From a97a4ad75861e365fd4c3a570d34da211dbb1d7d Mon Sep 17 00:00:00 2001 From: Zhakhangir Anuarbek Date: Fri, 8 Aug 2025 01:39:49 +0500 Subject: [PATCH] feat: add Exhibition and Fight applications pages with authentication and approval functionality --- next.config.ts | 10 + src/pages/approve/exhibition.tsx | 523 ++++++++++++++++++++++++++++ src/pages/approve/fight.tsx | 569 +++++++++++++++++++++++++++++++ 3 files changed, 1102 insertions(+) create mode 100644 src/pages/approve/exhibition.tsx create mode 100644 src/pages/approve/fight.tsx diff --git a/next.config.ts b/next.config.ts index e0fb35a..cb9dce9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -4,6 +4,16 @@ const nextConfig: NextConfig = { /* config options here */ reactStrictMode: true, output: "standalone", + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "base.mossport.info", + port: "", + pathname: "/api/files/**", + }, + ], + }, }; export default nextConfig; diff --git a/src/pages/approve/exhibition.tsx b/src/pages/approve/exhibition.tsx new file mode 100644 index 0000000..e90761b --- /dev/null +++ b/src/pages/approve/exhibition.tsx @@ -0,0 +1,523 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState, useEffect } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import PocketBase from "pocketbase"; +import { fluxgore, gothampro } from "@/utils/fonts"; + +const pb = new PocketBase("https://base.mossport.info"); + +interface FormData { + name: string; + phone: string; + email: string; + carBrand: string; + carModel: string; + description: string; +} + +interface Application { + id: string; + data: FormData; + images: string[]; + type: string; + status?: string; + approved?: boolean; + created: string; +} + +function ExhibitionApplicationsPage() { + const [applications, setApplications] = useState([]); + const [selectedImage, setSelectedImage] = useState(null); + const [loading, setLoading] = useState(true); + const [processingId, setProcessingId] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [credentials, setCredentials] = useState({ email: "", password: "" }); + const [authError, setAuthError] = useState(""); + + useEffect(() => { + // Проверяем, авторизован ли пользователь + if (pb.authStore.isValid) { + setIsAuthenticated(true); + fetchApplications(); + } else { + setLoading(false); + } + }, []); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setAuthError(""); + + try { + await pb.admins.authWithPassword(credentials.email, credentials.password); + setIsAuthenticated(true); + setLoading(true); + await fetchApplications(); + } catch (error) { + console.error("Auth error:", error); + setAuthError("Неверный email или пароль"); + } + }; + + const handleLogout = () => { + pb.authStore.clear(); + setIsAuthenticated(false); + setApplications([]); + }; + + const fetchApplications = async () => { + try { + const records = await pb.collection("forms").getFullList({ + filter: 'type = "exhibition" && approved != true', + sort: "-created", + }); + + const formattedApplications = records.map((record: any) => ({ + id: record.id, + data: + typeof record.data === "string" + ? JSON.parse(record.data) + : record.data, + images: record.images || [], + type: record.type, + status: record.status || "pending", + approved: record.approved || false, + created: record.created, + })); + + setApplications(formattedApplications); + } catch (error) { + console.error("Error fetching applications:", error); + + // Detailed error logging + if ( + typeof error === "object" && + error !== null && + "response" in error && + typeof (error as any).response === "object" + ) { + console.error("Response status:", (error as any).response.status); + console.error("Response data:", (error as any).response.data); + } + + if (typeof error === "object" && error !== null && "data" in error) { + console.error("Error data:", (error as any).data); + } + + if (error instanceof Error && error.message.includes("403")) { + setAuthError("Недостаточно прав доступа"); + handleLogout(); + } + } finally { + setLoading(false); + } + }; + + const handleApprove = async (id: string) => { + setProcessingId(id); + try { + await pb.collection("forms").update(id, { + status: "approved", + approved: true + }); + setApplications((prev) => prev.filter((app) => app.id !== id)); + alert("Заявка одобрена!"); + } catch (error) { + console.error("Error approving application:", error); + alert("Ошибка при одобрении заявки"); + } finally { + setProcessingId(null); + } + }; + + const handleReject = async (id: string) => { + setProcessingId(id); + try { + await pb.collection("forms").update(id, { + status: "rejected", + approved: false + }); + setApplications((prev) => prev.filter((app) => app.id !== id)); + alert("Заявка отклонена!"); + } catch (error) { + console.error("Error rejecting application:", error); + alert("Ошибка при отклонении заявки"); + } finally { + setProcessingId(null); + } + }; + + const getImageUrl = (record: Application, filename: string) => { + const url = `${pb.baseUrl}/api/files/forms/${record.id}/${filename}`; + + if (pb.authStore.token) { + const separator = url.includes("?") ? "&" : "?"; + return `${url}${separator}token=${pb.authStore.token}`; + } + + return url; + }; + + // Форма авторизации + if (!isAuthenticated) { + return ( +
+
+
+
+

+ Панель администратора +

+

+ Войдите для доступа к заявкам на выставку +

+
+
+
+
+ + + setCredentials((prev) => ({ + ...prev, + email: e.target.value, + })) + } + /> +
+
+ + + setCredentials((prev) => ({ + ...prev, + password: e.target.value, + })) + } + /> +
+
+ + {authError && ( +
+ {authError} +
+ )} + + +
+
+
+
+ ); + } + + if (loading) { + return ( +
+
+
+
+ Загрузка... +
+
+
+ ); + } + + return ( +
+ + Заявки на выставку автомобилей - Модерация + + + {/* Header */} +
+
+
+

+ Заявки на выставку автомобилей +

+ +
+
+
+ + {/* Content */} +
+ {applications.length === 0 ? ( +
+
+ + + +
+

+ Нет заявок +

+

+ Пока нет заявок на выставку для модерации +

+
+ ) : ( +
+ {applications.map((application) => ( +
+
+
+ {/* Личная информация */} +
+

+ Информация об участнике +

+
+
+ + Имя: + + + {application.data.name} + +
+
+ + Телефон: + + + {application.data.phone} + +
+
+ + Email: + + + {application.data.email} + +
+
+
+ + {/* Информация об автомобиле для выставки */} +
+

+ Автомобиль для выставки +

+
+
+ + Марка: + + + {application.data.carBrand} + +
+
+ + Модель: + + + {application.data.carModel} + +
+ {application.data.description && ( +
+ + Описание: + + + {application.data.description} + +
+ )} +
+
+
+ + {/* Фотографии */} + {application.images.length > 0 && ( +
+

+ Фотографии для выставки ({application.images.length}) +

+
+ {application.images.map((image, index) => { + const imageUrl = getImageUrl(application, image); + return ( +
setSelectedImage(imageUrl)} + > + {`Фото +
+
+ Увеличить +
+
+
+ ); + })} +
+
+ )} + + {/* Дата подачи заявки */} +
+
+ Заявка подана:{" "} + {new Date(application.created).toLocaleString("ru-RU")} +
+
+
+ + {/* Кнопки действий */} +
+ + +
+
+ ))} +
+ )} +
+ + {/* Модальное окно для увеличенного изображения */} + {selectedImage && ( +
setSelectedImage(null)} + > +
e.stopPropagation()} + > +
+ Увеличенное фото { + console.error("Modal image failed to load:", selectedImage); + setSelectedImage(null); + }} + /> + +
+
+

+ Нажмите вне изображения или на × чтобы закрыть +

+
+
+
+ )} +
+ ); +} + +export default ExhibitionApplicationsPage; diff --git a/src/pages/approve/fight.tsx b/src/pages/approve/fight.tsx new file mode 100644 index 0000000..511627c --- /dev/null +++ b/src/pages/approve/fight.tsx @@ -0,0 +1,569 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { useState, useEffect } from "react"; +import Head from "next/head"; +import Image from "next/image"; +import PocketBase from "pocketbase"; +import { fluxgore, gothampro } from "@/utils/fonts"; + +const pb = new PocketBase("https://base.mossport.info"); + +interface FormData { + lastName: string; + firstName: string; + middleName: string; + birthDate: string; + citizenship: string; + phone: string; + email: string; + carBrand: string; + carModel: string; + engine: string; + power: string; + additionalInfo: string; +} + +interface Application { + id: string; + data: FormData; + images: string[]; + type: string; + status?: string; + approved?: boolean; // Add this new field + created: string; +} + +function FightApplicationsPage() { + const [applications, setApplications] = useState([]); + const [selectedImage, setSelectedImage] = useState(null); + const [loading, setLoading] = useState(true); + const [processingId, setProcessingId] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [credentials, setCredentials] = useState({ email: "", password: "" }); + const [authError, setAuthError] = useState(""); + + useEffect(() => { + // Проверяем, авторизован ли пользователь + if (pb.authStore.isValid) { + setIsAuthenticated(true); + fetchApplications(); + } else { + setLoading(false); + } + }, []); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setAuthError(""); + + try { + await pb.admins.authWithPassword(credentials.email, credentials.password); + setIsAuthenticated(true); + setLoading(true); + await fetchApplications(); + } catch (error) { + console.error("Auth error:", error); + setAuthError("Неверный email или пароль"); + } + }; + + const handleLogout = () => { + pb.authStore.clear(); + setIsAuthenticated(false); + setApplications([]); + }; + + const fetchApplications = async () => { + try { + const records = await pb.collection("forms").getFullList({ + filter: 'type = "fight" && approved != true', // Updated filter to use boolean field + sort: "-created", + }); + + const formattedApplications = records.map((record: any) => ({ + id: record.id, + data: + typeof record.data === "string" + ? JSON.parse(record.data) + : record.data, + images: record.images || [], + type: record.type, + status: record.status || "pending", + approved: record.approved || false, // Include the approved field + created: record.created, + })); + + setApplications(formattedApplications); + } catch (error) { + console.error("Error fetching applications:", error); + + // Detailed error logging + if ( + typeof error === "object" && + error !== null && + "response" in error && + typeof (error as any).response === "object" + ) { + console.error("Response status:", (error as any).response.status); + console.error("Response data:", (error as any).response.data); + } + + if (typeof error === "object" && error !== null && "data" in error) { + console.error("Error data:", (error as any).data); + } + + // Add more detailed error logging + if (typeof error === "object" && error !== null && "data" in error) { + console.error("Error details:", (error as any).data); + } + if (error instanceof Error && error.message.includes("403")) { + setAuthError("Недостаточно прав доступа"); + handleLogout(); + } + } finally { + setLoading(false); + } + }; + + const handleApprove = async (id: string) => { + setProcessingId(id); + try { + await pb.collection("forms").update(id, { + status: "approved", + approved: true // Set the boolean field + }); + setApplications((prev) => prev.filter((app) => app.id !== id)); + alert("Заявка одобрена!"); + } catch (error) { + console.error("Error approving application:", error); + alert("Ошибка при одобрении заявки"); + } finally { + setProcessingId(null); + } + }; + + const handleReject = async (id: string) => { + setProcessingId(id); + try { + await pb.collection("forms").update(id, { + status: "rejected", + approved: false // Explicitly set to false + }); + setApplications((prev) => prev.filter((app) => app.id !== id)); + alert("Заявка отклонена!"); + } catch (error) { + console.error("Error rejecting application:", error); + alert("Ошибка при отклонении заявки"); + } finally { + setProcessingId(null); + } + }; + + const getImageUrl = (record: Application, filename: string) => { + // Manual URL construction with proper token handling + const url = `${pb.baseUrl}/api/files/forms/${record.id}/${filename}`; + + // Add auth token if available + if (pb.authStore.token) { + const separator = url.includes("?") ? "&" : "?"; + return `${url}${separator}token=${pb.authStore.token}`; + } + + return url; + }; + + // Форма авторизации + if (!isAuthenticated) { + return ( +
+
+
+
+

+ Панель администратора +

+

+ Войдите для доступа к заявкам +

+
+
+
+
+ + + setCredentials((prev) => ({ + ...prev, + email: e.target.value, + })) + } + /> +
+
+ + + setCredentials((prev) => ({ + ...prev, + password: e.target.value, + })) + } + /> +
+
+ + {authError && ( +
+ {authError} +
+ )} + + +
+
+
+
+ ); + } + + if (loading) { + return ( +
+
+
+
+ Загрузка... +
+
+
+ ); + } + + return ( +
+ + Заявки на Битву за Москву - Модерация + + + {/* Header */} +
+
+
+

+ Заявки на Битву за Москву +

+ +
+
+
+ + {/* Content */} +
+ {applications.length === 0 ? ( +
+
+ + + +
+

+ Нет заявок +

+

+ Пока нет заявок для модерации +

+
+ ) : ( +
+ {applications.map((application) => ( +
+
+
+ {/* Личная информация */} +
+

+ Личная информация +

+
+
+ + ФИО: + + + {application.data.lastName}{" "} + {application.data.firstName}{" "} + {application.data.middleName} + +
+
+ + Дата рождения: + + + {application.data.birthDate} + +
+
+ + Гражданство: + + + {application.data.citizenship} + +
+
+ + Телефон: + + + {application.data.phone} + +
+
+ + Email: + + + {application.data.email} + +
+
+
+ + {/* Информация об автомобиле */} +
+

+ Автомобиль +

+
+
+ + Марка: + + + {application.data.carBrand} + +
+
+ + Модель: + + + {application.data.carModel} + +
+
+ + Двигатель: + + + {application.data.engine} + +
+
+ + Мощность: + + + {application.data.power} + +
+ {application.data.additionalInfo && ( +
+ + Доп. информация: + + + {application.data.additionalInfo} + +
+ )} +
+
+
+ + {/* Фотографии */} + {application.images.length > 0 && ( +
+

+ Фотографии автомобиля ({application.images.length}) +

+
+ {application.images.map((image, index) => { + const imageUrl = getImageUrl(application, image); + return ( +
setSelectedImage(imageUrl)} + > + {`Фото +
+
+ Увеличить +
+
+
+ ); + })} +
+
+ )} + + {/* Дата подачи заявки */} +
+
+ Заявка подана:{" "} + {new Date(application.created).toLocaleString("ru-RU")} +
+
+
+ + {/* Кнопки действий */} +
+ + +
+
+ ))} +
+ )} +
+ + {/* Улучшенное модальное окно для увеличенного изображения */} + {selectedImage && ( +
setSelectedImage(null)} + > +
e.stopPropagation()} + > +
+ Увеличенное фото { + console.error("Modal image failed to load:", selectedImage); + setSelectedImage(null); + }} + /> + +
+
+

+ Нажмите вне изображения или на × чтобы закрыть +

+
+
+
+ )} +
+ ); +} + +export default FightApplicationsPage;