feat: add Exhibition and Fight applications pages with authentication and approval functionality

This commit is contained in:
2025-08-08 01:39:49 +05:00
parent f3772220ff
commit a97a4ad758
3 changed files with 1102 additions and 0 deletions
+10
View File
@@ -4,6 +4,16 @@ const nextConfig: NextConfig = {
/* config options here */ /* config options here */
reactStrictMode: true, reactStrictMode: true,
output: "standalone", output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "base.mossport.info",
port: "",
pathname: "/api/files/**",
},
],
},
}; };
export default nextConfig; export default nextConfig;
+523
View File
@@ -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<Application[]>([]);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [processingId, setProcessingId] = useState<string | null>(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 (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100">
<div className="max-w-md w-full mx-4">
<div className="bg-white rounded-xl shadow-xl p-8 space-y-8">
<div className="text-center">
<h2
className={`${fluxgore.className} text-3xl text-gray-900 mb-2`}
>
Панель администратора
</h2>
<p className={`${gothampro.className} text-gray-600`}>
Войдите для доступа к заявкам на выставку
</p>
</div>
<form className="space-y-6" onSubmit={handleLogin}>
<div className="space-y-4">
<div>
<label
htmlFor="email"
className={`${gothampro.className} block text-sm font-medium text-gray-700 mb-2`}
>
Email администратора
</label>
<input
id="email"
name="email"
type="email"
required
className={`${gothampro.className} w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200`}
placeholder="admin@example.com"
value={credentials.email}
onChange={(e) =>
setCredentials((prev) => ({
...prev,
email: e.target.value,
}))
}
/>
</div>
<div>
<label
htmlFor="password"
className={`${gothampro.className} block text-sm font-medium text-gray-700 mb-2`}
>
Пароль
</label>
<input
id="password"
name="password"
type="password"
required
className={`${gothampro.className} w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200`}
placeholder="••••••••"
value={credentials.password}
onChange={(e) =>
setCredentials((prev) => ({
...prev,
password: e.target.value,
}))
}
/>
</div>
</div>
{authError && (
<div
className={`${gothampro.className} text-red-600 text-sm text-center bg-red-50 p-3 rounded-lg`}
>
{authError}
</div>
)}
<button
type="submit"
className={`${fluxgore.className} w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
>
Войти
</button>
</form>
</div>
</div>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center">
<div className="w-12 h-12 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<div className={`${gothampro.className} text-xl text-gray-600`}>
Загрузка...
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<Head>
<title>Заявки на выставку автомобилей - Модерация</title>
</Head>
{/* Header */}
<div className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-6 py-6">
<div className="flex justify-between items-center">
<h1
className={`${fluxgore.className} text-2xl md:text-4xl text-[#060606] uppercase`}
>
Заявки на выставку автомобилей
</h1>
<button
onClick={handleLogout}
className={`${fluxgore.className} bg-gray-600 hover:bg-gray-700 text-white px-6 py-3 text-sm font-medium uppercase tracking-wide transition-colors rounded-lg`}
>
Выйти
</button>
</div>
</div>
</div>
{/* Content */}
<div className="max-w-7xl mx-auto px-6 py-8">
{applications.length === 0 ? (
<div className="text-center py-16">
<div className="w-16 h-16 bg-gray-200 rounded-full mx-auto mb-4 flex items-center justify-center">
<svg
className="w-8 h-8 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<h3 className={`${fluxgore.className} text-xl text-gray-900 mb-2`}>
Нет заявок
</h3>
<p className={`${gothampro.className} text-gray-500`}>
Пока нет заявок на выставку для модерации
</p>
</div>
) : (
<div className="space-y-6">
{applications.map((application) => (
<div
key={application.id}
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-200"
>
<div className="p-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Личная информация */}
<div className="space-y-4">
<h3
className={`${fluxgore.className} text-xl text-[#1068B0] border-b border-gray-200 pb-2`}
>
Информация об участнике
</h3>
<div className={`${gothampro.className} space-y-3`}>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-32">
Имя:
</span>
<span className="text-gray-900">
{application.data.name}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-32">
Телефон:
</span>
<span className="text-gray-900">
{application.data.phone}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-32">
Email:
</span>
<span className="text-gray-900">
{application.data.email}
</span>
</div>
</div>
</div>
{/* Информация об автомобиле для выставки */}
<div className="space-y-4">
<h3
className={`${fluxgore.className} text-xl text-[#1068B0] border-b border-gray-200 pb-2`}
>
Автомобиль для выставки
</h3>
<div className={`${gothampro.className} space-y-3`}>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-24">
Марка:
</span>
<span className="text-gray-900">
{application.data.carBrand}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-24">
Модель:
</span>
<span className="text-gray-900">
{application.data.carModel}
</span>
</div>
{application.data.description && (
<div className="flex flex-col">
<span className="font-semibold text-gray-700 mb-1">
Описание:
</span>
<span className="text-gray-900 bg-gray-50 p-3 rounded-lg">
{application.data.description}
</span>
</div>
)}
</div>
</div>
</div>
{/* Фотографии */}
{application.images.length > 0 && (
<div className="mt-8 pt-6 border-t border-gray-200">
<h3
className={`${fluxgore.className} text-xl text-[#1068B0] mb-6`}
>
Фотографии для выставки ({application.images.length})
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{application.images.map((image, index) => {
const imageUrl = getImageUrl(application, image);
return (
<div
key={index}
className="relative cursor-pointer group aspect-square overflow-hidden rounded-lg bg-gray-100"
onClick={() => setSelectedImage(imageUrl)}
>
<Image
src={imageUrl}
alt={`Фото для выставки ${index + 1}`}
fill
className="object-cover group-hover:scale-105 transition-transform duration-200"
/>
<div className="absolute inset-0 bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-200 flex items-center justify-center">
<div className="bg-white bg-opacity-90 text-gray-800 px-3 py-1 rounded-full text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200">
Увеличить
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Дата подачи заявки */}
<div className="mt-6 pt-4 border-t border-gray-200">
<div
className={`${gothampro.className} text-sm text-gray-500`}
>
Заявка подана:{" "}
{new Date(application.created).toLocaleString("ru-RU")}
</div>
</div>
</div>
{/* Кнопки действий */}
<div className="bg-gray-50 px-8 py-4 flex gap-4 justify-end">
<button
onClick={() => handleReject(application.id)}
disabled={processingId === application.id}
className={`${fluxgore.className} bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white px-6 py-2 text-sm font-medium uppercase tracking-wide rounded-lg transition-colors duration-200`}
>
{processingId === application.id
? "Обработка..."
: "Отклонить"}
</button>
<button
onClick={() => handleApprove(application.id)}
disabled={processingId === application.id}
className={`${fluxgore.className} bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white px-6 py-2 text-sm font-medium uppercase tracking-wide rounded-lg transition-colors duration-200`}
>
{processingId === application.id
? "Обработка..."
: "Принять"}
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Модальное окно для увеличенного изображения */}
{selectedImage && (
<div
className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4 backdrop-blur-sm"
onClick={() => setSelectedImage(null)}
>
<div
className="relative max-w-5xl max-h-full w-full"
onClick={(e) => e.stopPropagation()}
>
<div className="relative bg-white rounded-lg overflow-hidden shadow-2xl">
<Image
src={selectedImage}
alt="Увеличенное фото"
width={1200}
height={900}
className="w-full h-auto max-h-[80vh] object-contain"
onError={() => {
console.error("Modal image failed to load:", selectedImage);
setSelectedImage(null);
}}
/>
<button
onClick={() => setSelectedImage(null)}
className="absolute top-4 right-4 bg-black bg-opacity-70 hover:bg-opacity-90 text-white rounded-full w-10 h-10 flex items-center justify-center transition-all duration-200 z-10"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="text-center mt-4">
<p
className={`${gothampro.className} text-white text-sm opacity-75`}
>
Нажмите вне изображения или на × чтобы закрыть
</p>
</div>
</div>
</div>
)}
</div>
);
}
export default ExhibitionApplicationsPage;
+569
View File
@@ -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<Application[]>([]);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [processingId, setProcessingId] = useState<string | null>(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 (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-gray-50 to-gray-100">
<div className="max-w-md w-full mx-4">
<div className="bg-white rounded-xl shadow-xl p-8 space-y-8">
<div className="text-center">
<h2
className={`${fluxgore.className} text-3xl text-gray-900 mb-2`}
>
Панель администратора
</h2>
<p className={`${gothampro.className} text-gray-600`}>
Войдите для доступа к заявкам
</p>
</div>
<form className="space-y-6" onSubmit={handleLogin}>
<div className="space-y-4">
<div>
<label
htmlFor="email"
className={`${gothampro.className} block text-sm font-medium text-gray-700 mb-2`}
>
Email администратора
</label>
<input
id="email"
name="email"
type="email"
required
className={`${gothampro.className} w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200`}
placeholder="admin@example.com"
value={credentials.email}
onChange={(e) =>
setCredentials((prev) => ({
...prev,
email: e.target.value,
}))
}
/>
</div>
<div>
<label
htmlFor="password"
className={`${gothampro.className} block text-sm font-medium text-gray-700 mb-2`}
>
Пароль
</label>
<input
id="password"
name="password"
type="password"
required
className={`${gothampro.className} w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200`}
placeholder="••••••••"
value={credentials.password}
onChange={(e) =>
setCredentials((prev) => ({
...prev,
password: e.target.value,
}))
}
/>
</div>
</div>
{authError && (
<div
className={`${gothampro.className} text-red-600 text-sm text-center bg-red-50 p-3 rounded-lg`}
>
{authError}
</div>
)}
<button
type="submit"
className={`${fluxgore.className} w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-lg transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2`}
>
Войти
</button>
</form>
</div>
</div>
</div>
);
}
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen bg-gray-50">
<div className="text-center">
<div className="w-12 h-12 border-4 border-blue-600 border-t-transparent rounded-full animate-spin mx-auto mb-4"></div>
<div className={`${gothampro.className} text-xl text-gray-600`}>
Загрузка...
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<Head>
<title>Заявки на Битву за Москву - Модерация</title>
</Head>
{/* Header */}
<div className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-6 py-6">
<div className="flex justify-between items-center">
<h1
className={`${fluxgore.className} text-2xl md:text-4xl text-[#060606] uppercase`}
>
Заявки на Битву за Москву
</h1>
<button
onClick={handleLogout}
className={`${fluxgore.className} bg-gray-600 hover:bg-gray-700 text-white px-6 py-3 text-sm font-medium uppercase tracking-wide transition-colors rounded-lg`}
>
Выйти
</button>
</div>
</div>
</div>
{/* Content */}
<div className="max-w-7xl mx-auto px-6 py-8">
{applications.length === 0 ? (
<div className="text-center py-16">
<div className="w-16 h-16 bg-gray-200 rounded-full mx-auto mb-4 flex items-center justify-center">
<svg
className="w-8 h-8 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
</div>
<h3 className={`${fluxgore.className} text-xl text-gray-900 mb-2`}>
Нет заявок
</h3>
<p className={`${gothampro.className} text-gray-500`}>
Пока нет заявок для модерации
</p>
</div>
) : (
<div className="space-y-6">
{applications.map((application) => (
<div
key={application.id}
className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow duration-200"
>
<div className="p-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Личная информация */}
<div className="space-y-4">
<h3
className={`${fluxgore.className} text-xl text-[#1068B0] border-b border-gray-200 pb-2`}
>
Личная информация
</h3>
<div className={`${gothampro.className} space-y-3`}>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-32">
ФИО:
</span>
<span className="text-gray-900">
{application.data.lastName}{" "}
{application.data.firstName}{" "}
{application.data.middleName}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-32">
Дата рождения:
</span>
<span className="text-gray-900">
{application.data.birthDate}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-32">
Гражданство:
</span>
<span className="text-gray-900">
{application.data.citizenship}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-32">
Телефон:
</span>
<span className="text-gray-900">
{application.data.phone}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-32">
Email:
</span>
<span className="text-gray-900">
{application.data.email}
</span>
</div>
</div>
</div>
{/* Информация об автомобиле */}
<div className="space-y-4">
<h3
className={`${fluxgore.className} text-xl text-[#1068B0] border-b border-gray-200 pb-2`}
>
Автомобиль
</h3>
<div className={`${gothampro.className} space-y-3`}>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-24">
Марка:
</span>
<span className="text-gray-900">
{application.data.carBrand}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-24">
Модель:
</span>
<span className="text-gray-900">
{application.data.carModel}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-24">
Двигатель:
</span>
<span className="text-gray-900">
{application.data.engine}
</span>
</div>
<div className="flex flex-col sm:flex-row sm:items-center">
<span className="font-semibold text-gray-700 w-24">
Мощность:
</span>
<span className="text-gray-900">
{application.data.power}
</span>
</div>
{application.data.additionalInfo && (
<div className="flex flex-col">
<span className="font-semibold text-gray-700 mb-1">
Доп. информация:
</span>
<span className="text-gray-900 bg-gray-50 p-3 rounded-lg">
{application.data.additionalInfo}
</span>
</div>
)}
</div>
</div>
</div>
{/* Фотографии */}
{application.images.length > 0 && (
<div className="mt-8 pt-6 border-t border-gray-200">
<h3
className={`${fluxgore.className} text-xl text-[#1068B0] mb-6`}
>
Фотографии автомобиля ({application.images.length})
</h3>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
{application.images.map((image, index) => {
const imageUrl = getImageUrl(application, image);
return (
<div
key={index}
className="relative cursor-pointer group aspect-square overflow-hidden rounded-lg bg-gray-100"
onClick={() => setSelectedImage(imageUrl)}
>
<Image
src={imageUrl}
alt={`Фото автомобиля ${index + 1}`}
fill
className="object-cover group-hover:scale-105 transition-transform duration-200"
/>
<div className="absolute inset-0 bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-200 flex items-center justify-center">
<div className="bg-white bg-opacity-90 text-gray-800 px-3 py-1 rounded-full text-sm font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-200">
Увеличить
</div>
</div>
</div>
);
})}
</div>
</div>
)}
{/* Дата подачи заявки */}
<div className="mt-6 pt-4 border-t border-gray-200">
<div
className={`${gothampro.className} text-sm text-gray-500`}
>
Заявка подана:{" "}
{new Date(application.created).toLocaleString("ru-RU")}
</div>
</div>
</div>
{/* Кнопки действий */}
<div className="bg-gray-50 px-8 py-4 flex gap-4 justify-end">
<button
onClick={() => handleReject(application.id)}
disabled={processingId === application.id}
className={`${fluxgore.className} bg-red-600 hover:bg-red-700 disabled:bg-red-400 text-white px-6 py-2 text-sm font-medium uppercase tracking-wide rounded-lg transition-colors duration-200`}
>
{processingId === application.id
? "Обработка..."
: "Отклонить"}
</button>
<button
onClick={() => handleApprove(application.id)}
disabled={processingId === application.id}
className={`${fluxgore.className} bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white px-6 py-2 text-sm font-medium uppercase tracking-wide rounded-lg transition-colors duration-200`}
>
{processingId === application.id
? "Обработка..."
: "Принять"}
</button>
</div>
</div>
))}
</div>
)}
</div>
{/* Улучшенное модальное окно для увеличенного изображения */}
{selectedImage && (
<div
className="fixed inset-0 bg-black bg-opacity-90 flex items-center justify-center z-50 p-4 backdrop-blur-sm"
onClick={() => setSelectedImage(null)}
>
<div
className="relative max-w-5xl max-h-full w-full"
onClick={(e) => e.stopPropagation()}
>
<div className="relative bg-white rounded-lg overflow-hidden shadow-2xl">
<Image
src={selectedImage}
alt="Увеличенное фото"
width={1200}
height={900}
className="w-full h-auto max-h-[80vh] object-contain"
onError={() => {
console.error("Modal image failed to load:", selectedImage);
setSelectedImage(null);
}}
/>
<button
onClick={() => setSelectedImage(null)}
className="absolute top-4 right-4 bg-black bg-opacity-70 hover:bg-opacity-90 text-white rounded-full w-10 h-10 flex items-center justify-center transition-all duration-200 z-10"
>
<svg
className="w-6 h-6"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<div className="text-center mt-4">
<p
className={`${gothampro.className} text-white text-sm opacity-75`}
>
Нажмите вне изображения или на × чтобы закрыть
</p>
</div>
</div>
</div>
)}
</div>
);
}
export default FightApplicationsPage;