Refactor exhibition and fight application pages to separate pending and approved applications

- Split applications into pending and approved states for better management.
- Implemented tab navigation for switching between pending and approved applications.
- Updated fetching logic to retrieve pending applications excluding rejected ones.
- Enhanced application rendering with a dedicated render function for application cards.
- Adjusted approval and rejection logic to update the respective application lists.
- Improved user feedback with appropriate messages for empty states in both tabs.
This commit is contained in:
2025-08-17 19:09:49 +05:00
parent 7ad664490b
commit b115404b1f
2 changed files with 559 additions and 391 deletions
+262 -176
View File
@@ -27,13 +27,19 @@ interface Application {
} }
function ExhibitionApplicationsPage() { function ExhibitionApplicationsPage() {
const [applications, setApplications] = useState<Application[]>([]); const [pendingApplications, setPendingApplications] = useState<Application[]>(
[]
);
const [approvedApplications, setApprovedApplications] = useState<
Application[]
>([]);
const [selectedImage, setSelectedImage] = useState<string | null>(null); const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [processingId, setProcessingId] = useState<string | null>(null); const [processingId, setProcessingId] = useState<string | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
const [credentials, setCredentials] = useState({ email: "", password: "" }); const [credentials, setCredentials] = useState({ email: "", password: "" });
const [authError, setAuthError] = useState(""); const [authError, setAuthError] = useState("");
const [activeTab, setActiveTab] = useState<"pending" | "approved">("pending");
useEffect(() => { useEffect(() => {
// Проверяем, авторизован ли пользователь // Проверяем, авторизован ли пользователь
@@ -63,30 +69,41 @@ function ExhibitionApplicationsPage() {
const handleLogout = () => { const handleLogout = () => {
pb.authStore.clear(); pb.authStore.clear();
setIsAuthenticated(false); setIsAuthenticated(false);
setApplications([]); setPendingApplications([]);
setApprovedApplications([]);
}; };
const fetchApplications = async () => { const fetchApplications = async () => {
try { try {
const records = await pb.collection("forms").getFullList({ // Fetch pending applications - exclude both approved and rejected
filter: 'type = "exhibition" && approved != true', const pendingRecords = await pb.collection("forms").getFullList({
filter:
'type = "exhibition" && approved != true && status != "rejected"',
sort: "-created", sort: "-created",
}); });
const formattedApplications = records.map((record: any) => ({ // Fetch approved applications
id: record.id, const approvedRecords = await pb.collection("forms").getFullList({
data: filter: 'type = "exhibition" && approved = true',
typeof record.data === "string" sort: "-created",
? 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); const formatApplications = (records: any[]) =>
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,
}));
setPendingApplications(formatApplications(pendingRecords));
setApprovedApplications(formatApplications(approvedRecords));
} catch (error) { } catch (error) {
console.error("Error fetching applications:", error); console.error("Error fetching applications:", error);
@@ -119,9 +136,18 @@ function ExhibitionApplicationsPage() {
try { try {
await pb.collection("forms").update(id, { await pb.collection("forms").update(id, {
status: "approved", status: "approved",
approved: true approved: true,
}); });
setApplications((prev) => prev.filter((app) => app.id !== id));
// Move from pending to approved
const approvedApp = pendingApplications.find((app) => app.id === id);
if (approvedApp) {
approvedApp.status = "approved";
approvedApp.approved = true;
setApprovedApplications((prev) => [approvedApp, ...prev]);
setPendingApplications((prev) => prev.filter((app) => app.id !== id));
}
alert("Заявка одобрена!"); alert("Заявка одобрена!");
} catch (error) { } catch (error) {
console.error("Error approving application:", error); console.error("Error approving application:", error);
@@ -136,9 +162,9 @@ function ExhibitionApplicationsPage() {
try { try {
await pb.collection("forms").update(id, { await pb.collection("forms").update(id, {
status: "rejected", status: "rejected",
approved: false approved: false,
}); });
setApplications((prev) => prev.filter((app) => app.id !== id)); setPendingApplications((prev) => prev.filter((app) => app.id !== id));
alert("Заявка отклонена!"); alert("Заявка отклонена!");
} catch (error) { } catch (error) {
console.error("Error rejecting application:", error); console.error("Error rejecting application:", error);
@@ -159,6 +185,148 @@ function ExhibitionApplicationsPage() {
return url; return url;
}; };
const renderApplicationCard = (
application: Application,
showActions = true
) => (
<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 flex justify-between items-center">
<div className={`${gothampro.className} text-sm text-gray-500`}>
Заявка подана:{" "}
{new Date(application.created).toLocaleString("ru-RU")}
</div>
{application.approved && (
<div className="flex items-center">
<span className="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
Одобрено
</span>
</div>
)}
</div>
</div>
{/* Кнопки действий */}
{showActions && (
<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>
);
// Форма авторизации // Форма авторизации
if (!isAuthenticated) { if (!isAuthenticated) {
return ( return (
@@ -284,13 +452,77 @@ function ExhibitionApplicationsPage() {
</div> </div>
</div> </div>
{/* Tabs */}
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex border-b border-gray-200">
<button
onClick={() => setActiveTab("pending")}
className={`${
fluxgore.className
} px-6 py-3 font-medium text-sm transition-colors duration-200 border-b-2 ${
activeTab === "pending"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Ожидающие ({pendingApplications.length})
</button>
<button
onClick={() => setActiveTab("approved")}
className={`${
fluxgore.className
} px-6 py-3 font-medium text-sm transition-colors duration-200 border-b-2 ${
activeTab === "approved"
? "border-green-500 text-green-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Одобренные ({approvedApplications.length})
</button>
</div>
</div>
{/* Content */} {/* Content */}
<div className="max-w-7xl mx-auto px-6 py-8"> <div className="max-w-7xl mx-auto px-6 py-8">
{applications.length === 0 ? ( {activeTab === "pending" ? (
pendingApplications.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">
{pendingApplications.map((application) =>
renderApplicationCard(application, true)
)}
</div>
)
) : approvedApplications.length === 0 ? (
<div className="text-center py-16"> <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"> <div className="w-16 h-16 bg-green-100 rounded-full mx-auto mb-4 flex items-center justify-center">
<svg <svg
className="w-8 h-8 text-gray-400" className="w-8 h-8 text-green-600"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -299,168 +531,22 @@ function ExhibitionApplicationsPage() {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={2} 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" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
</div> </div>
<h3 className={`${fluxgore.className} text-xl text-gray-900 mb-2`}> <h3 className={`${fluxgore.className} text-xl text-gray-900 mb-2`}>
Нет заявок Нет одобренных заявок
</h3> </h3>
<p className={`${gothampro.className} text-gray-500`}> <p className={`${gothampro.className} text-gray-500`}>
Пока нет заявок на выставку для модерации Пока нет одобренных заявок
</p> </p>
</div> </div>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{applications.map((application) => ( {approvedApplications.map((application) =>
<div renderApplicationCard(application, false)
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>
)} )}
</div> </div>
+293 -211
View File
@@ -28,18 +28,24 @@ interface Application {
images: string[]; images: string[];
type: string; type: string;
status?: string; status?: string;
approved?: boolean; // Add this new field approved?: boolean;
created: string; created: string;
} }
function FightApplicationsPage() { function FightApplicationsPage() {
const [applications, setApplications] = useState<Application[]>([]); const [pendingApplications, setPendingApplications] = useState<Application[]>(
[]
);
const [approvedApplications, setApprovedApplications] = useState<
Application[]
>([]);
const [selectedImage, setSelectedImage] = useState<string | null>(null); const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [processingId, setProcessingId] = useState<string | null>(null); const [processingId, setProcessingId] = useState<string | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false);
const [credentials, setCredentials] = useState({ email: "", password: "" }); const [credentials, setCredentials] = useState({ email: "", password: "" });
const [authError, setAuthError] = useState(""); const [authError, setAuthError] = useState("");
const [activeTab, setActiveTab] = useState<"pending" | "approved">("pending");
useEffect(() => { useEffect(() => {
// Проверяем, авторизован ли пользователь // Проверяем, авторизован ли пользователь
@@ -69,30 +75,40 @@ function FightApplicationsPage() {
const handleLogout = () => { const handleLogout = () => {
pb.authStore.clear(); pb.authStore.clear();
setIsAuthenticated(false); setIsAuthenticated(false);
setApplications([]); setPendingApplications([]);
setApprovedApplications([]);
}; };
const fetchApplications = async () => { const fetchApplications = async () => {
try { try {
const records = await pb.collection("forms").getFullList({ // Fetch pending applications - exclude both approved and rejected
filter: 'type = "fight" && approved != true', // Updated filter to use boolean field const pendingRecords = await pb.collection("forms").getFullList({
filter: 'type = "fight" && approved != true && status != "rejected"',
sort: "-created", sort: "-created",
}); });
const formattedApplications = records.map((record: any) => ({ // Fetch approved applications
id: record.id, const approvedRecords = await pb.collection("forms").getFullList({
data: filter: 'type = "fight" && approved = true',
typeof record.data === "string" sort: "-created",
? 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); const formatApplications = (records: any[]) =>
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,
}));
setPendingApplications(formatApplications(pendingRecords));
setApprovedApplications(formatApplications(approvedRecords));
} catch (error) { } catch (error) {
console.error("Error fetching applications:", error); console.error("Error fetching applications:", error);
@@ -129,9 +145,18 @@ function FightApplicationsPage() {
try { try {
await pb.collection("forms").update(id, { await pb.collection("forms").update(id, {
status: "approved", status: "approved",
approved: true // Set the boolean field approved: true,
}); });
setApplications((prev) => prev.filter((app) => app.id !== id));
// Move from pending to approved
const approvedApp = pendingApplications.find((app) => app.id === id);
if (approvedApp) {
approvedApp.status = "approved";
approvedApp.approved = true;
setApprovedApplications((prev) => [approvedApp, ...prev]);
setPendingApplications((prev) => prev.filter((app) => app.id !== id));
}
alert("Заявка одобрена!"); alert("Заявка одобрена!");
} catch (error) { } catch (error) {
console.error("Error approving application:", error); console.error("Error approving application:", error);
@@ -146,9 +171,9 @@ function FightApplicationsPage() {
try { try {
await pb.collection("forms").update(id, { await pb.collection("forms").update(id, {
status: "rejected", status: "rejected",
approved: false // Explicitly set to false approved: false,
}); });
setApplications((prev) => prev.filter((app) => app.id !== id)); setPendingApplications((prev) => prev.filter((app) => app.id !== id));
alert("Заявка отклонена!"); alert("Заявка отклонена!");
} catch (error) { } catch (error) {
console.error("Error rejecting application:", error); console.error("Error rejecting application:", error);
@@ -171,6 +196,179 @@ function FightApplicationsPage() {
return url; return url;
}; };
const renderApplicationCard = (
application: Application,
showActions = true
) => (
<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 flex justify-between items-center">
<div className={`${gothampro.className} text-sm text-gray-500`}>
Заявка подана:{" "}
{new Date(application.created).toLocaleString("ru-RU")}
</div>
{application.approved && (
<div className="flex items-center">
<span className="bg-green-100 text-green-800 text-xs font-medium px-2.5 py-0.5 rounded-full">
Одобрено
</span>
</div>
)}
</div>
</div>
{/* Кнопки действий */}
{showActions && (
<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>
);
// Форма авторизации // Форма авторизации
if (!isAuthenticated) { if (!isAuthenticated) {
return ( return (
@@ -296,13 +494,77 @@ function FightApplicationsPage() {
</div> </div>
</div> </div>
{/* Tabs */}
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex border-b border-gray-200">
<button
onClick={() => setActiveTab("pending")}
className={`${
fluxgore.className
} px-6 py-3 font-medium text-sm transition-colors duration-200 border-b-2 ${
activeTab === "pending"
? "border-blue-500 text-blue-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Ожидающие ({pendingApplications.length})
</button>
<button
onClick={() => setActiveTab("approved")}
className={`${
fluxgore.className
} px-6 py-3 font-medium text-sm transition-colors duration-200 border-b-2 ${
activeTab === "approved"
? "border-green-500 text-green-600"
: "border-transparent text-gray-500 hover:text-gray-700"
}`}
>
Одобренные ({approvedApplications.length})
</button>
</div>
</div>
{/* Content */} {/* Content */}
<div className="max-w-7xl mx-auto px-6 py-8"> <div className="max-w-7xl mx-auto px-6 py-8">
{applications.length === 0 ? ( {activeTab === "pending" ? (
pendingApplications.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">
{pendingApplications.map((application) =>
renderApplicationCard(application, true)
)}
</div>
)
) : approvedApplications.length === 0 ? (
<div className="text-center py-16"> <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"> <div className="w-16 h-16 bg-green-100 rounded-full mx-auto mb-4 flex items-center justify-center">
<svg <svg
className="w-8 h-8 text-gray-400" className="w-8 h-8 text-green-600"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -311,202 +573,22 @@ function FightApplicationsPage() {
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
strokeWidth={2} 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" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
/> />
</svg> </svg>
</div> </div>
<h3 className={`${fluxgore.className} text-xl text-gray-900 mb-2`}> <h3 className={`${fluxgore.className} text-xl text-gray-900 mb-2`}>
Нет заявок Нет одобренных заявок
</h3> </h3>
<p className={`${gothampro.className} text-gray-500`}> <p className={`${gothampro.className} text-gray-500`}>
Пока нет заявок для модерации Пока нет одобренных заявок
</p> </p>
</div> </div>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{applications.map((application) => ( {approvedApplications.map((application) =>
<div renderApplicationCard(application, false)
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>
)} )}
</div> </div>