654 lines
23 KiB
TypeScript
654 lines
23 KiB
TypeScript
"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<string, string> = {
|
||
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<Types.EventDetailSchema | null>(initialEvent);
|
||
const [eventThumb, setEventThumb] = useState<string | null>(
|
||
initialEvent ? getEventCardImageUrl(initialEvent) : null,
|
||
);
|
||
const [lightboxIndex, setLightboxIndex] = useState<number | null>(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<number | null>(
|
||
() => (event?.registration_start_date ? new Date(event.registration_start_date).getTime() : null),
|
||
[event?.registration_start_date],
|
||
);
|
||
|
||
const deadlineTs = useMemo<number | null>(
|
||
() => (event?.registration_end_date ? new Date(event.registration_end_date).getTime() : null),
|
||
[event?.registration_end_date],
|
||
);
|
||
|
||
const remainingMs = useMemo<number | null>(
|
||
() => (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<GalleryLightboxItem[]>(
|
||
() =>
|
||
(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<string, string> = {
|
||
online: "https://schema.org/OnlineEventAttendanceMode",
|
||
on_site: "https://schema.org/OfflineEventAttendanceMode",
|
||
hybrid: "https://schema.org/MixedEventAttendanceMode",
|
||
};
|
||
|
||
const statusMap: Record<string, string> = {
|
||
published: "https://schema.org/EventScheduled",
|
||
completed: "https://schema.org/EventCompleted",
|
||
cancelled: "https://schema.org/EventCancelled",
|
||
draft: "https://schema.org/EventPostponed",
|
||
};
|
||
|
||
const data: Record<string, unknown> = {
|
||
"@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<string, unknown> = {
|
||
"@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<string, unknown> = {
|
||
"@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 = (
|
||
<Helmet>
|
||
<title>{pageTitle}</title>
|
||
<meta name="description" content={pageDescription} />
|
||
<meta name="robots" content={pageRobots} />
|
||
<link rel="canonical" href={canonicalUrl} />
|
||
<meta property="og:title" content={pageTitle} />
|
||
<meta property="og:description" content={pageDescription} />
|
||
<meta property="og:type" content="event" />
|
||
<meta property="og:url" content={canonicalUrl} />
|
||
<meta property="og:site_name" content={siteName} />
|
||
<meta property="og:image" content={primaryImage} />
|
||
<meta property="og:locale" content="fa_IR" />
|
||
{event?.start_time && <meta property="event:start_time" content={event.start_time} />}
|
||
{event?.end_time && <meta property="event:end_time" content={event.end_time} />}
|
||
{event?.updated_at && <meta property="og:updated_time" content={event.updated_at} />}
|
||
<meta name="twitter:card" content="summary_large_image" />
|
||
<meta name="twitter:title" content={pageTitle} />
|
||
<meta name="twitter:description" content={pageDescription} />
|
||
<meta name="twitter:image" content={primaryImage} />
|
||
{eventStructuredData && (
|
||
<script type="application/ld+json">{JSON.stringify(eventStructuredData)}</script>
|
||
)}
|
||
</Helmet>
|
||
);
|
||
|
||
const withHelmet = (node: JSX.Element) => (
|
||
<>
|
||
{helmet}
|
||
{node}
|
||
</>
|
||
);
|
||
|
||
if (loading) {
|
||
return withHelmet(
|
||
<div className="min-h-[60vh] flex items-center justify-center text-muted-foreground">
|
||
در حال بارگذاری رویداد...
|
||
</div>,
|
||
);
|
||
}
|
||
|
||
if (!event) {
|
||
return withHelmet(
|
||
<div className="min-h-[60vh] flex items-center justify-center">
|
||
رویداد مورد نظر یافت نشد.
|
||
</div>,
|
||
);
|
||
}
|
||
|
||
const beforeStart = rsTs != null && nowTs < rsTs;
|
||
const ended = deadlineTs !== null && remainingMs === 0;
|
||
const showCountdown = !beforeStart && deadlineTs !== null && (remainingMs ?? 0) > 0;
|
||
|
||
return withHelmet(
|
||
<div className="container mx-auto px-4 py-8" dir="rtl">
|
||
{beforeStart && (
|
||
<div className="mb-6">
|
||
<div className="rounded-xl border p-4 text-center bg-sky-50 text-sky-900 border-sky-200 dark:bg-sky-900/30 dark:text-sky-100 dark:border-sky-800">
|
||
ثبتنام از <strong className="font-semibold">{formatJalali(event.registration_start_date!)}</strong> آغاز میشود.
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{showCountdown && remainingMs != null && (
|
||
<div className="mb-6">
|
||
<div className="rounded-xl border p-4 text-center bg-emerald-50 text-emerald-900 border-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-100 dark:border-emerald-800">
|
||
<div className="flex flex-col items-center gap-1 sm:flex-row sm:justify-center">
|
||
<span>زمان باقیمانده تا پایان ثبتنام:</span>
|
||
<strong className="font-extrabold tracking-wider sm:ms-1">
|
||
{formatRemainingWords(remainingMs)}
|
||
</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{ended && (
|
||
<div className="mb-6">
|
||
<div className="rounded-xl border p-4 text-center bg-rose-50 text-rose-900 border-rose-200 dark:bg-rose-900/30 dark:text-rose-100 dark:border-rose-800">
|
||
مهلت ثبتنام به پایان رسیده است.
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||
<div className="lg:col-span-2">
|
||
<Card className="overflow-hidden">
|
||
<div className="w-full aspect-video overflow-hidden rounded-lg">
|
||
<ProgressiveImage
|
||
src={getEventHeroImageUrl(event)}
|
||
blurSrc={event.absolute_featured_image_thumbnail_url}
|
||
alt={event.title}
|
||
wrapperClassName="h-full w-full"
|
||
className="h-full w-full object-cover"
|
||
/>
|
||
</div>
|
||
|
||
<CardHeader>
|
||
<div className="flex items-start justify-between gap-3">
|
||
<div>
|
||
<CardTitle className="text-2xl">{event.title}</CardTitle>
|
||
<CardDescription className="mt-1">
|
||
{formatJalali(event.start_time)}
|
||
{event.end_time ? ` تا ${formatJalali(event.end_time)}` : null}
|
||
</CardDescription>
|
||
</div>
|
||
<div className="flex flex-col items-end gap-2">
|
||
<Badge variant="default">{typeLabel[event.event_type] || event.event_type}</Badge>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<Markdown content={event.description} justify size="base" />
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{galleryItems.length ? (
|
||
<div className="mt-6">
|
||
<h3 className="text-lg font-semibold mb-3">گالری تصاویر</h3>
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
|
||
{galleryItems.map((image, index) => (
|
||
<button
|
||
key={image.id}
|
||
type="button"
|
||
className="overflow-hidden rounded-md text-right transition-transform hover:scale-[1.01] focus:outline-none focus:ring-2 focus:ring-primary/40"
|
||
onClick={() => setLightboxIndex(index)}
|
||
>
|
||
<ProgressiveImage
|
||
src={image.previewSrc}
|
||
blurSrc={image.blurSrc}
|
||
alt={image.alt}
|
||
wrapperClassName="h-36 w-full"
|
||
className="h-36 w-full object-cover"
|
||
/>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
<div className="lg:col-span-1">
|
||
<div className="lg:sticky lg:top-24">
|
||
<Card>
|
||
<CardHeader>
|
||
<CardTitle className="text-base">اطلاعات ثبتنام</CardTitle>
|
||
<CardDescription>جزئیات دسترسی به رویداد</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3 text-sm">
|
||
{event.address && <div>آدرس: {event.address}</div>}
|
||
<div>
|
||
ظرفیت کل: {event.capacity == null ? "نامحدود" : formatNumberPersian(event.capacity)}
|
||
</div>
|
||
{meta && event.capacity != null && (
|
||
<div>
|
||
ظرفیت باقیمانده:{" "}
|
||
{meta.remaining === Number.POSITIVE_INFINITY
|
||
? "نامحدود"
|
||
: formatNumberPersian(meta.remaining)}
|
||
</div>
|
||
)}
|
||
<div>هزینه حضور: {event.price ? formatToman(event.price) : "رایگان"}</div>
|
||
|
||
<Button
|
||
onClick={handleMainCTA}
|
||
className="w-full mt-2"
|
||
disabled={
|
||
submitting ||
|
||
alreadyRegistered ||
|
||
event.status !== "published" ||
|
||
meta?.full === true ||
|
||
!meta?.registrationOpen
|
||
}
|
||
>
|
||
{event.status !== "published"
|
||
? "ثبتنام این رویداد فعال نیست"
|
||
: alreadyRegistered
|
||
? "شما قبلاً ثبتنام کردهاید"
|
||
: !meta?.registrationOpen
|
||
? "ثبتنام هنوز آغاز نشده است"
|
||
: meta?.full
|
||
? "ظرفیت ثبتنام تکمیل شده است"
|
||
: submitting
|
||
? "در حال ثبتنام..."
|
||
: event.price === 0
|
||
? "ثبتنام (رایگان)"
|
||
: "ثبتنام و ادامه پرداخت"}
|
||
</Button>
|
||
|
||
{!isFree && (
|
||
<CouponDialogFa
|
||
open={open}
|
||
onOpenChange={setOpen}
|
||
basePrice={basePrice}
|
||
onVerifyCouponRaw={(code) => api.checkDiscountCode(event.id, code)}
|
||
onContinue={handleContinueFromModal}
|
||
/>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<GalleryLightbox
|
||
items={galleryItems}
|
||
open={lightboxIndex !== null}
|
||
initialIndex={lightboxIndex ?? 0}
|
||
onOpenChange={(isOpen) => {
|
||
if (!isOpen) {
|
||
setLightboxIndex(null);
|
||
}
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|