"use client"; import { useEffect, useMemo, useState } from "react"; import { Helmet } from "@/lib/helmet"; import { useNavigate, useParams } from "@/lib/router"; import { api } from "@/lib/api"; import type * as Types from "@/lib/types"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import GalleryLightbox, { type GalleryLightboxItem } from "@/components/GalleryLightbox"; import ProgressiveImage from "@/components/ProgressiveImage"; import { useToast } from "@/hooks/use-toast"; import Markdown from "@/components/Markdown"; import CouponDialogFa from "@/components/CouponDialogFa"; import { formatJalali, formatNumberPersian, formatToman, getEventCardImageUrl, getEventHeroImageUrl, getEventSeoImageUrl, getGalleryImageBlurUrl, getGalleryImageFullUrl, getGalleryImagePreviewUrl, resolveErrorMessage, toPersianDigits, } from "@/lib/utils"; import { useAuth } from "@/contexts/AuthContext"; import { siteUrl } from "@/lib/site"; const typeLabel: Record = { online: "آنلاین", on_site: "حضوری", hybrid: "آنلاین و حضوری", }; type EventDetailProps = { initialEvent?: Types.EventDetailSchema | null; }; function buildPaymentSnapshot( event: Types.EventDetailSchema, eventThumb: string | null, payload: { baseAmount: number; discountAmount: number; amount: number; }, ) { return JSON.stringify({ event_id: event.id, slug: event.slug, title: event.title, thumb: eventThumb, base_amount: payload.baseAmount, discount_amount: payload.discountAmount, amount: payload.amount, started_at: new Date().toISOString(), success_markdown: event.registration_success_markdown, }); } export default function EventDetail({ initialEvent = null }: EventDetailProps) { const { slug } = useParams<{ slug: string }>(); const { isAuthenticated } = useAuth(); const navigate = useNavigate(); const { toast } = useToast(); const [event, setEvent] = useState(initialEvent); const [eventThumb, setEventThumb] = useState( initialEvent ? getEventCardImageUrl(initialEvent) : null, ); const [lightboxIndex, setLightboxIndex] = useState(null); const [loading, setLoading] = useState(!initialEvent); const [open, setOpen] = useState(false); const [submitting, setSubmitting] = useState(false); const [alreadyRegistered, setAlreadyRegistered] = useState(false); const [nowTs, setNowTs] = useState(() => Date.now()); const basePrice = Number(event?.price ?? 0); const isFree = useMemo(() => basePrice <= 0, [basePrice]); const siteName = "انجمن علمی کامپیوتر شرق گیلان"; const defaultDescription = "جزئیات کامل رویدادهای انجمن علمی کامپیوتر شرق گیلان شامل زمان، مکان و شرایط ثبت‌نام."; const toAbsoluteUrl = (value?: string | null) => { if (!value) return undefined; if (value.startsWith("http")) return value; const normalizedSite = siteUrl.endsWith("/") ? siteUrl.slice(0, -1) : siteUrl; const normalizedPath = value.startsWith("/") ? value.slice(1) : value; return `${normalizedSite}/${normalizedPath}`; }; const sanitizeDescription = (value?: string | null) => { if (!value) return defaultDescription; const stripped = value.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); if (!stripped) return defaultDescription; if (stripped.length <= 160) return stripped; return `${stripped.slice(0, 157)}...`; }; const canonicalUrl = event ? `${siteUrl}/events/${event.slug}` : `${siteUrl}/events`; const primaryImage = event ? toAbsoluteUrl(getEventSeoImageUrl(event)) ?? `${siteUrl}/favicon.ico` : `${siteUrl}/favicon.ico`; const pageTitle = event ? `${event.title} | ${siteName}` : `جزئیات رویداد | ${siteName}`; const pageDescription = sanitizeDescription(event?.description); const pageRobots = event?.status === "draft" ? "noindex, nofollow" : "index, follow"; useEffect(() => { let cancelled = false; async function checkRegistration() { if (!isAuthenticated || !event?.id) { return; } try { const res = await api.getRegistrationStatus(event.id); if (!cancelled) { setAlreadyRegistered(res.is_registered); } } catch { // Ignore registration status failures on the detail page. } } checkRegistration(); return () => { cancelled = true; }; }, [event?.id, isAuthenticated]); const goSuccess = (registrationId?: string) => { if (!event) return; const query = registrationId ? `?registration_id=${registrationId}` : ""; setAlreadyRegistered(true); toast({ title: "ثبت‌نام با موفقیت انجام شد!", variant: "success" }); navigate(`/events/${event.slug}/success${query}`); }; const handleMainCTA = async () => { if (!event) return; if (!isAuthenticated) { toast({ title: "ابتدا وارد شوید", description: "برای ثبت‌نام در رویداد باید وارد حساب کاربری خود شوید.", variant: "destructive", }); navigate("/auth"); return; } if (!isFree) { setOpen(true); return; } try { setSubmitting(true); const res = await api.registerForEvent(event.id); goSuccess(res.ticket_id); } catch (error: unknown) { const msg = resolveErrorMessage(error, ""); if (msg.includes("already registered")) { setAlreadyRegistered(true); toast({ title: "شما قبلاً ثبت‌نام کرده‌اید", variant: "destructive" }); return; } toast({ title: "خطا در ثبت‌نام", description: msg || "لطفاً دوباره تلاش کنید.", variant: "destructive", }); } finally { setSubmitting(false); } }; const handleContinueFromModal = async (coupon?: string, finalAmount?: number) => { if (!event) return; if (!isAuthenticated) { toast({ title: "ابتدا وارد شوید", description: "برای ثبت‌نام در رویداد باید وارد حساب کاربری خود شوید.", variant: "destructive", }); navigate("/auth"); return; } try { setSubmitting(true); const reg = await api.registerForEvent(event.id, coupon); if (finalAmount === 0) { window.sessionStorage.setItem( "payment:last", buildPaymentSnapshot(event, eventThumb, { baseAmount: Number(event.price ?? 0), discountAmount: Number(event.price ?? 0), amount: 0, }), ); await api.ChangeRegistrationStatus(reg.id, "confirmed"); goSuccess(reg.ticket_id); return; } const result = await api.createPayment({ event_id: event.id, description: `پرداخت رویداد: ${event.title}`, discount_code: (coupon ?? "").trim() || null, }); if (!result?.start_pay_url || Number(result.amount) === 0) { window.sessionStorage.setItem( "payment:last", buildPaymentSnapshot(event, eventThumb, { baseAmount: result.base_amount, discountAmount: result.discount_amount ?? result.base_amount, amount: 0, }), ); goSuccess(reg.ticket_id); return; } window.sessionStorage.setItem( "payment:last", buildPaymentSnapshot(event, eventThumb, { baseAmount: result.base_amount, discountAmount: result.discount_amount, amount: result.amount, }), ); window.location.href = result.start_pay_url; } catch (error: unknown) { const msg = resolveErrorMessage(error, ""); if (msg.includes("already registered")) { setAlreadyRegistered(true); toast({ title: "شما قبلاً ثبت‌نام کرده‌اید", variant: "destructive" }); return; } toast({ title: "خطا در پردازش پرداخت", description: msg || "لطفاً دوباره تلاش کنید.", variant: "destructive", }); } finally { setSubmitting(false); setOpen(false); } }; useEffect(() => { let cancelled = false; async function loadEvent() { try { if (!slug) return; if (initialEvent && initialEvent.slug === slug) { setEvent(initialEvent); setEventThumb(getEventCardImageUrl(initialEvent)); return; } const data = await api.getEventBySlug(slug); if (cancelled) return; setEvent(data); setEventThumb(getEventCardImageUrl(data)); } catch (error: unknown) { if (cancelled) return; toast({ title: "خطا در بارگذاری رویداد", description: resolveErrorMessage(error, "لطفاً دوباره تلاش کنید."), variant: "destructive", }); } finally { if (!cancelled) { setLoading(false); } } } loadEvent(); return () => { cancelled = true; }; }, [initialEvent, slug, toast]); useEffect(() => { const timer = window.setInterval(() => setNowTs(Date.now()), 1000); return () => window.clearInterval(timer); }, []); const rsTs = useMemo( () => (event?.registration_start_date ? new Date(event.registration_start_date).getTime() : null), [event?.registration_start_date], ); const deadlineTs = useMemo( () => (event?.registration_end_date ? new Date(event.registration_end_date).getTime() : null), [event?.registration_end_date], ); const remainingMs = useMemo( () => (deadlineTs != null ? Math.max(0, deadlineTs - nowTs) : null), [deadlineTs, nowTs], ); const formatCountdownTwoDigit = (value: number) => toPersianDigits(value.toString().padStart(2, "0")); const formatCountdownNumber = (value: number) => formatNumberPersian(value); const formatRemainingWords = (ms: number) => { const total = Math.max(0, Math.floor(ms / 1000)); const days = Math.floor(total / 86400); const hours = Math.floor((total % 86400) / 3600); const minutes = Math.floor((total % 3600) / 60); const seconds = total % 60; if (days === 0) { return `${formatCountdownTwoDigit(hours)} ساعت و ${formatCountdownTwoDigit(minutes)} دقیقه و ${formatCountdownTwoDigit(seconds)} ثانیه`; } return `${formatCountdownNumber(days)} روز و ${formatCountdownTwoDigit(hours)} ساعت و ${formatCountdownTwoDigit(minutes)} دقیقه و ${formatCountdownTwoDigit(seconds)} ثانیه`; }; const meta = useMemo(() => { if (!event) return null; const registrationOpen = (rsTs == null || nowTs >= rsTs) && (deadlineTs == null || nowTs <= deadlineTs); const unlimited = event.capacity == null; const remaining = unlimited ? Number.POSITIVE_INFINITY : Math.max(0, (event.capacity || 0) - (event.registration_count || 0)); const full = !unlimited && remaining <= 0; return { registrationOpen, remaining, full }; }, [deadlineTs, event, nowTs, rsTs]); const galleryItems = useMemo( () => (event?.gallery_images ?? []).map((image) => ({ id: image.id, alt: image.title || event?.title || "تصویر رویداد", title: image.title, previewSrc: getGalleryImagePreviewUrl(image), blurSrc: getGalleryImageBlurUrl(image), fullSrc: getGalleryImageFullUrl(image), })), [event], ); const eventStructuredData = useMemo(() => { if (!event) return null; const attendanceModeMap: Record = { online: "https://schema.org/OnlineEventAttendanceMode", on_site: "https://schema.org/OfflineEventAttendanceMode", hybrid: "https://schema.org/MixedEventAttendanceMode", }; const statusMap: Record = { published: "https://schema.org/EventScheduled", completed: "https://schema.org/EventCompleted", cancelled: "https://schema.org/EventCancelled", draft: "https://schema.org/EventPostponed", }; const data: Record = { "@context": "https://schema.org", "@type": "Event", name: event.title, description: pageDescription, startDate: event.start_time, url: canonicalUrl, eventAttendanceMode: attendanceModeMap[event.event_type] ?? attendanceModeMap.hybrid, eventStatus: statusMap[event.status] ?? statusMap.published, organizer: { "@type": "Organization", name: siteName, url: siteUrl, }, }; if (event.end_time) { data.endDate = event.end_time; } if (primaryImage) { data.image = [primaryImage]; } if (event.event_type === "online") { data.location = { "@type": "VirtualLocation", url: event.online_link || canonicalUrl, }; } else { const location: Record = { "@type": "Place", name: event.location || event.address || siteName, }; if (event.address) { location.address = event.address; } if (event.location) { location.description = event.location; } data.location = location; } const offers: Record = { "@type": "Offer", url: canonicalUrl, priceCurrency: "IRR", price: String(event.price ?? 0), availability: meta?.full ? "https://schema.org/SoldOut" : "https://schema.org/InStock", }; if (event.registration_start_date) { offers.validFrom = event.registration_start_date; } if (event.registration_end_date) { offers.validThrough = event.registration_end_date; } data.offers = offers; return data; }, [canonicalUrl, event, meta?.full, pageDescription, primaryImage, siteName]); const helmet = ( {pageTitle} {event?.start_time && } {event?.end_time && } {event?.updated_at && } {eventStructuredData && ( )} ); const withHelmet = (node: JSX.Element) => ( <> {helmet} {node} ); if (loading) { return withHelmet(
در حال بارگذاری رویداد...
, ); } if (!event) { return withHelmet(
رویداد مورد نظر یافت نشد.
, ); } const beforeStart = rsTs != null && nowTs < rsTs; const ended = deadlineTs !== null && remainingMs === 0; const showCountdown = !beforeStart && deadlineTs !== null && (remainingMs ?? 0) > 0; return withHelmet(
{beforeStart && (
ثبت‌نام از {formatJalali(event.registration_start_date!)} آغاز می‌شود.
)} {showCountdown && remainingMs != null && (
زمان باقی‌مانده تا پایان ثبت‌نام: {formatRemainingWords(remainingMs)}
)} {ended && (
مهلت ثبت‌نام به پایان رسیده است.
)}
{event.title} {formatJalali(event.start_time)} {event.end_time ? ` تا ${formatJalali(event.end_time)}` : null}
{typeLabel[event.event_type] || event.event_type}
{galleryItems.length ? (

گالری تصاویر

{galleryItems.map((image, index) => ( ))}
) : null}
اطلاعات ثبت‌نام جزئیات دسترسی به رویداد {event.address &&
آدرس: {event.address}
}
ظرفیت کل: {event.capacity == null ? "نامحدود" : formatNumberPersian(event.capacity)}
{meta && event.capacity != null && (
ظرفیت باقی‌مانده:{" "} {meta.remaining === Number.POSITIVE_INFINITY ? "نامحدود" : formatNumberPersian(meta.remaining)}
)}
هزینه حضور: {event.price ? formatToman(event.price) : "رایگان"}
{!isFree && ( api.checkDiscountCode(event.id, code)} onContinue={handleContinueFromModal} /> )}
{ if (!isOpen) { setLightboxIndex(null); } }} />
); }