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:
+242
-156
@@ -27,13 +27,19 @@ interface Application {
|
||||
}
|
||||
|
||||
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 [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("");
|
||||
const [activeTab, setActiveTab] = useState<"pending" | "approved">("pending");
|
||||
|
||||
useEffect(() => {
|
||||
// Проверяем, авторизован ли пользователь
|
||||
@@ -63,17 +69,27 @@ function ExhibitionApplicationsPage() {
|
||||
const handleLogout = () => {
|
||||
pb.authStore.clear();
|
||||
setIsAuthenticated(false);
|
||||
setApplications([]);
|
||||
setPendingApplications([]);
|
||||
setApprovedApplications([]);
|
||||
};
|
||||
|
||||
const fetchApplications = async () => {
|
||||
try {
|
||||
const records = await pb.collection("forms").getFullList({
|
||||
filter: 'type = "exhibition" && approved != true',
|
||||
// Fetch pending applications - exclude both approved and rejected
|
||||
const pendingRecords = await pb.collection("forms").getFullList({
|
||||
filter:
|
||||
'type = "exhibition" && approved != true && status != "rejected"',
|
||||
sort: "-created",
|
||||
});
|
||||
|
||||
const formattedApplications = records.map((record: any) => ({
|
||||
// Fetch approved applications
|
||||
const approvedRecords = await pb.collection("forms").getFullList({
|
||||
filter: 'type = "exhibition" && approved = true',
|
||||
sort: "-created",
|
||||
});
|
||||
|
||||
const formatApplications = (records: any[]) =>
|
||||
records.map((record: any) => ({
|
||||
id: record.id,
|
||||
data:
|
||||
typeof record.data === "string"
|
||||
@@ -86,7 +102,8 @@ function ExhibitionApplicationsPage() {
|
||||
created: record.created,
|
||||
}));
|
||||
|
||||
setApplications(formattedApplications);
|
||||
setPendingApplications(formatApplications(pendingRecords));
|
||||
setApprovedApplications(formatApplications(approvedRecords));
|
||||
} catch (error) {
|
||||
console.error("Error fetching applications:", error);
|
||||
|
||||
@@ -119,9 +136,18 @@ function ExhibitionApplicationsPage() {
|
||||
try {
|
||||
await pb.collection("forms").update(id, {
|
||||
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("Заявка одобрена!");
|
||||
} catch (error) {
|
||||
console.error("Error approving application:", error);
|
||||
@@ -136,9 +162,9 @@ function ExhibitionApplicationsPage() {
|
||||
try {
|
||||
await pb.collection("forms").update(id, {
|
||||
status: "rejected",
|
||||
approved: false
|
||||
approved: false,
|
||||
});
|
||||
setApplications((prev) => prev.filter((app) => app.id !== id));
|
||||
setPendingApplications((prev) => prev.filter((app) => app.id !== id));
|
||||
alert("Заявка отклонена!");
|
||||
} catch (error) {
|
||||
console.error("Error rejecting application:", error);
|
||||
@@ -159,6 +185,148 @@ function ExhibitionApplicationsPage() {
|
||||
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) {
|
||||
return (
|
||||
@@ -284,9 +452,40 @@ function ExhibitionApplicationsPage() {
|
||||
</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 */}
|
||||
<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
|
||||
@@ -303,164 +502,51 @@ function ExhibitionApplicationsPage() {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className={`${fluxgore.className} text-xl text-gray-900 mb-2`}>
|
||||
Нет заявок
|
||||
<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>
|
||||
{pendingApplications.map((application) =>
|
||||
renderApplicationCard(application, true)
|
||||
)}
|
||||
</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`}
|
||||
)
|
||||
) : approvedApplications.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full mx-auto mb-4 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-8 h-8 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
Фотографии для выставки ({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"
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</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">
|
||||
{approvedApplications.map((application) =>
|
||||
renderApplicationCard(application, false)
|
||||
)}
|
||||
|
||||
{/* Дата подачи заявки */}
|
||||
<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>
|
||||
|
||||
+274
-192
@@ -28,18 +28,24 @@ interface Application {
|
||||
images: string[];
|
||||
type: string;
|
||||
status?: string;
|
||||
approved?: boolean; // Add this new field
|
||||
approved?: boolean;
|
||||
created: string;
|
||||
}
|
||||
|
||||
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 [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("");
|
||||
const [activeTab, setActiveTab] = useState<"pending" | "approved">("pending");
|
||||
|
||||
useEffect(() => {
|
||||
// Проверяем, авторизован ли пользователь
|
||||
@@ -69,17 +75,26 @@ function FightApplicationsPage() {
|
||||
const handleLogout = () => {
|
||||
pb.authStore.clear();
|
||||
setIsAuthenticated(false);
|
||||
setApplications([]);
|
||||
setPendingApplications([]);
|
||||
setApprovedApplications([]);
|
||||
};
|
||||
|
||||
const fetchApplications = async () => {
|
||||
try {
|
||||
const records = await pb.collection("forms").getFullList({
|
||||
filter: 'type = "fight" && approved != true', // Updated filter to use boolean field
|
||||
// Fetch pending applications - exclude both approved and rejected
|
||||
const pendingRecords = await pb.collection("forms").getFullList({
|
||||
filter: 'type = "fight" && approved != true && status != "rejected"',
|
||||
sort: "-created",
|
||||
});
|
||||
|
||||
const formattedApplications = records.map((record: any) => ({
|
||||
// Fetch approved applications
|
||||
const approvedRecords = await pb.collection("forms").getFullList({
|
||||
filter: 'type = "fight" && approved = true',
|
||||
sort: "-created",
|
||||
});
|
||||
|
||||
const formatApplications = (records: any[]) =>
|
||||
records.map((record: any) => ({
|
||||
id: record.id,
|
||||
data:
|
||||
typeof record.data === "string"
|
||||
@@ -88,11 +103,12 @@ function FightApplicationsPage() {
|
||||
images: record.images || [],
|
||||
type: record.type,
|
||||
status: record.status || "pending",
|
||||
approved: record.approved || false, // Include the approved field
|
||||
approved: record.approved || false,
|
||||
created: record.created,
|
||||
}));
|
||||
|
||||
setApplications(formattedApplications);
|
||||
setPendingApplications(formatApplications(pendingRecords));
|
||||
setApprovedApplications(formatApplications(approvedRecords));
|
||||
} catch (error) {
|
||||
console.error("Error fetching applications:", error);
|
||||
|
||||
@@ -129,9 +145,18 @@ function FightApplicationsPage() {
|
||||
try {
|
||||
await pb.collection("forms").update(id, {
|
||||
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("Заявка одобрена!");
|
||||
} catch (error) {
|
||||
console.error("Error approving application:", error);
|
||||
@@ -146,9 +171,9 @@ function FightApplicationsPage() {
|
||||
try {
|
||||
await pb.collection("forms").update(id, {
|
||||
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("Заявка отклонена!");
|
||||
} catch (error) {
|
||||
console.error("Error rejecting application:", error);
|
||||
@@ -171,6 +196,179 @@ function FightApplicationsPage() {
|
||||
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) {
|
||||
return (
|
||||
@@ -296,9 +494,40 @@ function FightApplicationsPage() {
|
||||
</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 */}
|
||||
<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
|
||||
@@ -315,198 +544,51 @@ function FightApplicationsPage() {
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className={`${fluxgore.className} text-xl text-gray-900 mb-2`}>
|
||||
Нет заявок
|
||||
<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>
|
||||
{pendingApplications.map((application) =>
|
||||
renderApplicationCard(application, true)
|
||||
)}
|
||||
</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`}
|
||||
)
|
||||
) : approvedApplications.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-16 h-16 bg-green-100 rounded-full mx-auto mb-4 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-8 h-8 text-green-600"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
Фотографии автомобиля ({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"
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
</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">
|
||||
{approvedApplications.map((application) =>
|
||||
renderApplicationCard(application, false)
|
||||
)}
|
||||
|
||||
{/* Дата подачи заявки */}
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user