Files
guilan-ace-frontend/src/views/Events.tsx

177 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { 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>
);
}