Files
nashel-frontend/src/app/dashboard/offers/page.tsx
T

176 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { getMyOffers, Offer, toggleOffer, deleteOffer } from "@/shared/api/catalog";
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { PlusCircle, Loader2, Pencil, Trash2, PauseCircle, PlayCircle, Image as ImageIcon } from "lucide-react";
import { useSessionStore } from "@/entities/session/store";
import { formatPrice } from "@/shared/lib/formatPrice";
export default function MyOffersPage() {
const router = useRouter();
const { user, isAuth } = useSessionStore();
const [offers, setOffers] = useState<Offer[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Проверка прав доступа
if (!isAuth) return;
const isPerformer = user?.roles?.some(r => ["Master", "Company", "Candidate", "Admin"].includes(r));
if (!isPerformer) {
toast.error("Чтобы создавать услуги, вам нужно стать исполнителем");
router.push("/dashboard/become-performer");
return;
}
const fetchOffers = async () => {
try {
const data = await getMyOffers();
setOffers(data || []);
} catch (error) {
console.error(error);
toast.error("Не удалось загрузить ваши услуги");
} finally {
setLoading(false);
}
};
fetchOffers();
}, [isAuth, user, router]);
const handleToggle = async (id: string) => {
try {
const newStatus = await toggleOffer(id);
setOffers(offers.map(o => o.id === id ? { ...o, isActive: newStatus } : o));
toast.success(newStatus ? "Услуга активирована" : "Услуга приостановлена");
} catch {
toast.error("Не удалось изменить статус услуги");
}
};
const handleDelete = async (id: string) => {
if (!confirm("Вы уверены, что хотите удалить эту услугу?")) return;
try {
await deleteOffer(id);
setOffers(offers.filter(o => o.id !== id));
toast.success("Услуга удалена");
} catch {
toast.error("Не удалось удалить услугу");
}
};
if (loading) {
return (
<div className="flex justify-center items-center h-48">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
);
}
if (offers.length === 0) {
return (
<div className="container max-w-5xl py-10">
<div className="flex flex-col items-center justify-center p-12 text-center bg-card rounded-2xl border shadow-sm">
<div className="mb-4 rounded-full bg-primary/10 p-4">
<PlusCircle className="h-12 w-12 text-primary" />
</div>
<h2 className="text-2xl font-bold mb-3">У вас еще нет услуг</h2>
<p className="text-muted-foreground mb-8 max-w-md text-lg">
Создайте свою первую услугу, чтобы клиенты могли вас найти и предложить заказ.
</p>
<Link href="/dashboard/offers/create">
<Button size="lg" className="gap-2">
<PlusCircle className="h-5 w-5" />
Создать первую услугу
</Button>
</Link>
</div>
</div>
);
}
return (
<div className="container max-w-5xl py-10">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 mb-8 bg-card p-6 rounded-2xl shadow-sm border">
<h1 className="text-3xl font-bold">Мои услуги</h1>
<Link href="/dashboard/offers/create">
<Button className="gap-2">
<PlusCircle className="h-4 w-4" />
Создать услугу
</Button>
</Link>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{offers.map((offer) => (
<Card key={offer.id} className={`flex flex-col transition-all hover:shadow-md ${!offer.isActive ? "bg-muted/30 border-dashed" : "border-border"}`}>
<CardHeader className="pb-3 relative">
<CardTitle className={`text-xl font-semibold line-clamp-1 ${!offer.isActive && "text-muted-foreground"}`}>{offer.title}</CardTitle>
<CardDescription>
{offer.isActive ? (
<span className="inline-flex items-center text-green-600 dark:text-green-500 text-xs font-medium bg-green-100 dark:bg-green-900/30 px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-green-600 dark:bg-green-500 mr-1.5"></span>
Активна
</span>
) : (
<span className="inline-flex items-center text-muted-foreground text-xs font-medium bg-muted px-2 py-0.5 rounded-full">
<span className="w-1.5 h-1.5 rounded-full bg-muted-foreground mr-1.5"></span>
Приостановлена
</span>
)}
</CardDescription>
</CardHeader>
<CardContent className={`flex-1 ${!offer.isActive && "opacity-70 grayscale"}`}>
{offer.images && offer.images.length > 0 && (
<div className="w-full h-32 mb-4 rounded-md bg-muted overflow-hidden relative">
<img src={offer.images[0]} className="w-full h-full object-cover" alt="Primary image" />
</div>
)}
<p className="text-sm text-foreground/80 line-clamp-3 mb-4 leading-relaxed">
{offer.description}
</p>
<div className="font-semibold text-lg flex items-center gap-1.5">
{offer.price.type === 2
? <span className="text-muted-foreground text-base">Договорная</span>
: <>
<span>{formatPrice(offer.price.amount)}</span>
<span className="text-sm font-normal text-muted-foreground relative top-0.5">
{offer.price.type === 1 ? "в час" : "фиксированно"}
</span>
</>}
</div>
</CardContent>
<CardFooter className="flex gap-2 justify-end bg-muted/20 pt-4 mt-auto border-t">
<Link href={`/dashboard/offers/${offer.id}/edit`}>
<Button variant="outline" size="sm" className="group flex items-center gap-0 overflow-hidden px-2 hover:px-3 hover:bg-primary/10 border-transparent hover:border-primary/20 text-muted-foreground hover:text-primary transition-all duration-300 ease-out">
<Pencil className="h-4 w-4 shrink-0" />
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 group-hover:max-w-[120px] group-hover:opacity-100 group-hover:ml-2 transition-all duration-300 ease-out">
Редактировать
</span>
</Button>
</Link>
<Button variant="outline" size="sm" className="group flex items-center gap-0 overflow-hidden px-2 hover:px-3 hover:bg-orange-500/10 border-transparent hover:border-orange-500/20 text-muted-foreground hover:text-orange-500 transition-all duration-300 ease-out" onClick={() => handleToggle(offer.id)}>
{offer.isActive ? <PauseCircle className="h-4 w-4 shrink-0" /> : <PlayCircle className="h-4 w-4 shrink-0" />}
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 group-hover:max-w-[120px] group-hover:opacity-100 group-hover:ml-2 transition-all duration-300 ease-out">
{offer.isActive ? "Приостановить" : "Активировать"}
</span>
</Button>
<Button variant="outline" size="sm" className="group flex items-center gap-0 overflow-hidden px-2 hover:px-3 hover:bg-destructive/10 border-transparent hover:border-destructive/20 text-muted-foreground hover:text-destructive transition-all duration-300 ease-out" onClick={() => handleDelete(offer.id)}>
<Trash2 className="h-4 w-4 shrink-0" />
<span className="max-w-0 overflow-hidden whitespace-nowrap opacity-0 group-hover:max-w-[100px] group-hover:opacity-100 group-hover:ml-2 transition-all duration-300 ease-out">
Удалить
</span>
</Button>
</CardFooter>
</Card>
))}
</div>
</div>
);
}