177 lines
6.7 KiB
TypeScript
177 lines
6.7 KiB
TypeScript
"use client";
|
||
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import { Link, useLocation, useNavigate } from "@/lib/router";
|
||
import { api } from "@/lib/api";
|
||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Button } from "@/components/ui/button";
|
||
import ProgressiveImage from "@/components/ProgressiveImage";
|
||
import type * as Types from "@/lib/types";
|
||
import { formatJalali, formatNumberPersian, formatToman, getEventCardImageUrl } from "@/lib/utils";
|
||
import type { EventsContentPage } from "@/lib/content-cms";
|
||
|
||
type EventsProps = {
|
||
content: EventsContentPage;
|
||
initialEvents?: Types.EventListItemSchema[];
|
||
initialSearch?: string;
|
||
};
|
||
|
||
function labelPrice(event: Types.EventListItemSchema) {
|
||
const price = Number(event?.price ?? 0);
|
||
return price <= 0 ? "رایگان" : formatToman(price);
|
||
}
|
||
|
||
function modeFa(eventType: Types.EventListItemSchema["event_type"]) {
|
||
return eventType === "online" ? "آنلاین" : "حضوری";
|
||
}
|
||
|
||
function spotsLeft(event: Types.EventListItemSchema) {
|
||
const cap = Number(event.capacity);
|
||
const used = Number(event.registration_count);
|
||
return cap - used;
|
||
}
|
||
|
||
function isAvailable(event: Types.EventListItemSchema) {
|
||
const now = new Date();
|
||
const end = new Date(event.registration_end_date ?? event.start_time);
|
||
const timeOk = end.getTime() > now.getTime();
|
||
return timeOk && spotsLeft(event) > 0;
|
||
}
|
||
|
||
function notAvailableReasonFa(event: Types.EventListItemSchema) {
|
||
const now = new Date();
|
||
const end = new Date(event.registration_end_date ?? event.start_time);
|
||
if (end.getTime() <= now.getTime()) return "ثبتنام پایانیافته";
|
||
if (spotsLeft(event) <= 0) return "ظرفیت تکمیل";
|
||
return "غیرقابل ثبتنام";
|
||
}
|
||
|
||
export default function Events({
|
||
content,
|
||
initialEvents = [],
|
||
initialSearch = "",
|
||
}: EventsProps) {
|
||
const navigate = useNavigate();
|
||
const location = useLocation();
|
||
const [events, setEvents] = useState<Types.EventListItemSchema[]>(initialEvents);
|
||
const [search, setSearch] = useState(initialSearch);
|
||
const [loading, setLoading] = useState(!initialEvents.length && !initialSearch);
|
||
|
||
useEffect(() => {
|
||
setEvents(initialEvents);
|
||
}, [initialEvents]);
|
||
|
||
useEffect(() => {
|
||
setSearch(initialSearch);
|
||
}, [initialSearch]);
|
||
|
||
const loadEvents = useCallback(async () => {
|
||
try {
|
||
setLoading(true);
|
||
const data = await api.getEvents({
|
||
search: search || undefined,
|
||
statuses: ["published", "completed"],
|
||
limit: 30,
|
||
});
|
||
setEvents(data);
|
||
} catch (error) {
|
||
console.error("Error loading events:", error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [search]);
|
||
|
||
useEffect(() => {
|
||
loadEvents();
|
||
}, [loadEvents]);
|
||
|
||
useEffect(() => {
|
||
const params = new URLSearchParams();
|
||
if (search.trim()) {
|
||
params.set("search", search.trim());
|
||
}
|
||
const basePath = location.pathname || "/events";
|
||
const nextPath = params.size
|
||
? `${basePath}?${params.toString()}`
|
||
: basePath;
|
||
navigate(nextPath, { replace: true });
|
||
}, [location.pathname, navigate, search]);
|
||
|
||
return (
|
||
<main className="min-h-screen bg-background" dir="rtl">
|
||
<div className="container mx-auto px-4 py-8">
|
||
<h1 className="mb-3 text-4xl font-bold">{content.title}</h1>
|
||
{content.intro ? <p className="mb-8 max-w-3xl leading-8 text-muted-foreground">{content.intro}</p> : null}
|
||
|
||
<div className="mb-8">
|
||
<Input
|
||
type="text"
|
||
placeholder="جستجو در رویدادها..."
|
||
aria-label="جستجو در رویدادها"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
className="max-w-md"
|
||
/>
|
||
</div>
|
||
|
||
{loading ? (
|
||
<p className="text-center text-muted-foreground">در حال بارگذاری...</p>
|
||
) : events.length === 0 ? (
|
||
<p className="text-center text-muted-foreground">رویدادی یافت نشد</p>
|
||
) : (
|
||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||
{events.map((event) => (
|
||
<Link key={event.id} to={`/events/${event.slug}`} className="block h-full">
|
||
<Card className="h-full flex flex-col hover:shadow-lg transition-shadow">
|
||
<div className="w-full aspect-video overflow-hidden rounded-lg">
|
||
<ProgressiveImage
|
||
src={getEventCardImageUrl(event)}
|
||
alt={event.title}
|
||
wrapperClassName="h-full w-full"
|
||
className="h-full w-full object-cover"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex-1 flex flex-col justify-between">
|
||
<CardHeader>
|
||
<div className="flex items-start justify-between gap-2">
|
||
<CardTitle className="line-clamp-2">{event.title}</CardTitle>
|
||
<Badge variant="default">{modeFa(event.event_type)}</Badge>
|
||
</div>
|
||
<CardDescription>{formatJalali(event.start_time, false)}</CardDescription>
|
||
</CardHeader>
|
||
|
||
<CardContent>
|
||
<div className="grid gap-1 text-sm" dir="rtl">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-muted-foreground">ظرفیت رویداد</span>
|
||
<span className="font-medium">
|
||
{formatNumberPersian(Number(event?.capacity ?? 0) - Number(event?.registration_count ?? 0))}
|
||
/
|
||
{formatNumberPersian(Number(event?.capacity ?? 0))} نفر
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-muted-foreground">هزینهی ثبتنام</span>
|
||
<span className="font-medium">{labelPrice(event)}</span>
|
||
</div>
|
||
{isAvailable(event) ? (
|
||
<Button>جزئیات رویداد</Button>
|
||
) : (
|
||
<Button variant="secondary">{notAvailableReasonFa(event)}</Button>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</div>
|
||
</Card>
|
||
</Link>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</main>
|
||
);
|
||
}
|