admin/features/events/detail/admin-event-detail/AdminEventDetailsSection.tsx

268 lines
12 KiB
TypeScript
Raw Permalink 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 type { AdminEventDetailData } from './types'
import dynamic from 'next/dynamic'
import DOMPurify from 'isomorphic-dompurify'
import type { EventFaq, EventMedia } from '@/services/eventDetail'
import Button from '@/components/formElements/Button'
import AdminTableViewButton from '@/components/ui/AdminTableViewButton'
import FallbackImage from '@/components/media/FallbackImage'
import { Card, CardBody, CardHeader } from '@/components/heroui/Card'
import { Chip } from '@/components/heroui/Chip'
import { APP_ROUTES } from '@/constants/routes'
import { getBooleanStatus } from '@/constants/status'
import { DEFAULT_EVENT_IMAGE } from '@/constants/images'
import { formatNumber, formatPersonName } from '@/helpers'
import { getPublicMapApiKey } from '@/helpers/mapir'
import { formatIranianMobile, formatPersianDate } from '@/lib/formatters'
import { ageRestrictionLabel, genderRestrictionLabel } from '@/lib/eventAudience'
import { texts } from '@/texts'
import { DetailItem } from './AdminEventDetailUi'
const MapirPreviewMap = dynamic(() => import('@/components/map/MapirPreviewMap'), { ssr: false })
const number = formatNumber
const hostDisplayName = (event: AdminEventDetailData) => formatPersonName(event.organizer?.firstName, event.organizer?.lastName)
const restrictionStatus = (value: string | null | undefined, label: string) => (value === 'open' ? 'بدون محدودیت' : `دارد · ${label}`)
interface AdminEventDetailsSectionProps {
event: AdminEventDetailData
categoryName: string
cityName: string
provinceName: string
media: EventMedia[]
faqs: EventFaq[]
onEditCommission: () => void
}
const AdminEventDetailsSection = ({
event,
categoryName,
cityName,
provinceName,
media,
faqs,
onEditCommission,
}: AdminEventDetailsSectionProps) => {
const autoGroupBadge = getBooleanStatus(event.settings.autoCreateGroup, {
true: 'گروه چت خودکار',
false: 'بدون گروه خودکار',
})
const waitlistOfferBadge = getBooleanStatus(event.settings.waitlistAutoOffer, {
true: 'پیشنهاد لیست انتظار',
false: 'بدون پیشنهاد انتظار',
})
const reviewSmsBadge = getBooleanStatus(event.settings.sendReviewRequestSms ?? false, {
true: 'پیامک درخواست نظر',
false: 'بدون پیامک نظر',
})
return (
<Card className="admin-surface">
<CardHeader className="border-b border-secondary-40 px-4 py-3 text-sm font-bold text-secondary-10">اطلاعات عمومی رویداد</CardHeader>
<CardBody className="flex flex-col gap-3 p-3">
<div className="flex flex-wrap gap-2">
<Chip
size="sm"
variant="flat"
>
ذخیره · {number(event.bookmarkCount ?? 0)}
</Chip>
<Chip
color={autoGroupBadge.chipColor}
size="sm"
variant="flat"
>
{autoGroupBadge.label}
</Chip>
<Chip
color={waitlistOfferBadge.chipColor}
size="sm"
variant="flat"
>
{waitlistOfferBadge.label}
</Chip>
<Chip
color={reviewSmsBadge.chipColor}
size="sm"
variant="flat"
>
{reviewSmsBadge.label}
</Chip>
<Chip
size="sm"
variant="flat"
>
پایان · {formatPersianDate(event.endsAt)}
</Chip>
</div>
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
<DetailItem label="نشانی اینترنتی">
<span dir="ltr">{event.slug}</span>
</DetailItem>
<DetailItem label="دسته‌بندی">{categoryName}</DetailItem>
<DetailItem label="مکان">
{provinceName}، {cityName}
{event.address ? `، ${event.address}` : ''}
</DetailItem>
<DetailItem label="میزبان">
<div className="flex flex-wrap items-center gap-2">
<span>{hostDisplayName(event)}</span>
{event.organizer?.mobile ? (
<span
className="text-xs font-normal text-secondary-30"
dir="ltr"
>
({formatIranianMobile(event.organizer.mobile)})
</span>
) : null}
<AdminTableViewButton
label="مشاهده میزبان"
mode="navigate"
to={APP_ROUTES.USER_DETAIL(event.organizerId)}
/>
</div>
</DetailItem>
<DetailItem label="کمیسیون پلتفرم">
<div className="flex flex-wrap items-center gap-2">
<span>{number(event.effectiveCommissionPercent ?? 0)}٪</span>
<span className="text-sm font-normal text-secondary-30">{event.commissionPercent != null ? '(اختصاصی)' : '(پیش‌فرض)'}</span>
<Button
size="sm"
variant="flat"
onClick={onEditCommission}
>
ویرایش
</Button>
</div>
</DetailItem>
<DetailItem label={texts.events.cancellationFeeMoreThan24HoursLabel}>
{number(event.cancellationFeePercentMoreThan24Hours)}٪
</DetailItem>
<DetailItem label={texts.events.cancellationFee12To24HoursLabel}>{number(event.cancellationFeePercent12To24Hours)}٪</DetailItem>
<DetailItem label={texts.events.cancellationFeeLabel}>{number(event.cancellationFeePercent)}٪</DetailItem>
<DetailItem label="محدودیت جنسیتی">
{restrictionStatus(event.genderRestriction, genderRestrictionLabel(event.genderRestriction))}
</DetailItem>
<DetailItem label="محدودیت سنی">{restrictionStatus(event.ageRestriction, ageRestrictionLabel(event.ageRestriction))}</DetailItem>
<DetailItem label="نمایش آدرس">
{event.settings.addressVisibility === 'attendees_only' ? 'فقط رزروکنندگان' : 'نمایش برای همه'}
</DetailItem>
<DetailItem label="منطقهٔ عمومی">{event.settings.generalArea ?? 'ثبت نشده'}</DetailItem>
<DetailItem label="امتیاز و نظرها">
{event.avgRating == null ? 'بدون امتیاز' : `${number(event.avgRating)} از ۵`} · {number(event.reviewsCount)} نظر
</DetailItem>
<DetailItem label="جایگاه‌های نمایش">
{event.discoveryPlacements?.length
? event.discoveryPlacements.map((item) => `${item.placement} (${number(item.priority)})`).join('، ')
: '—'}
</DetailItem>
<DetailItem label="زمان انتشار">{event.publishedAt ? formatPersianDate(String(event.publishedAt)) : 'منتشر نشده'}</DetailItem>
<DetailItem label="تایید ادمین">
{event.adminApprovedAt ? formatPersianDate(String(event.adminApprovedAt)) : 'تایید نشده'}
</DetailItem>
{event.rejectionReason ? <DetailItem label="آخرین دلیل رد">{event.rejectionReason}</DetailItem> : null}
<DetailItem label="ایجاد">{formatPersianDate(String(event.createdAt))}</DetailItem>
<DetailItem label="به‌روزرسانی">{formatPersianDate(String(event.updatedAt))}</DetailItem>
{event.lat != null && event.lng != null && Number.isFinite(event.lat) && Number.isFinite(event.lng) ? (
<DetailItem label="مختصات">
<span dir="ltr">
{event.lat.toFixed(6)}, {event.lng.toFixed(6)}
</span>
</DetailItem>
) : null}
{event.clonedFromEventId ? (
<DetailItem label="کلون‌شده از">
<AdminTableViewButton
label="مشاهده رویداد اصلی"
mode="navigate"
to={APP_ROUTES.MANAGE_EVENT_DETAIL(event.clonedFromEventId)}
/>
</DetailItem>
) : null}
</div>
{event.lat != null && event.lng != null && Number.isFinite(event.lat) && Number.isFinite(event.lng) && getPublicMapApiKey() ? (
<div>
<p className="mb-2 text-sm text-secondary-30">موقعیت روی نقشه</p>
<div className="relative aspect-[21/9] max-h-40 w-full overflow-hidden rounded-xl border border-default-200">
<MapirPreviewMap
lat={event.lat}
lng={event.lng}
/>
</div>
</div>
) : null}
{event.shortDescription || event.description ? (
<div className="rounded-xl border border-secondary-40 bg-secondary-50/60 p-3">
<span className="text-sm text-secondary-30">توضیحات</span>
{event.shortDescription ? <p className="mt-2 text-sm font-semibold text-secondary-30">{event.shortDescription}</p> : null}
{event.description ? (
<div
dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(event.description) }}
className="mt-2 text-sm leading-7 text-secondary-30 [&_a]:text-primary [&_a]:underline [&_blockquote]:border-s-2 [&_blockquote]:border-default-300 [&_blockquote]:ps-3 [&_blockquote]:italic [&_h1]:mb-3 [&_h1]:mt-5 [&_h1]:text-2xl [&_h1]:font-extrabold [&_h1]:text-secondary-10 [&_h2]:mb-2 [&_h2]:mt-4 [&_h2]:text-lg [&_h2]:font-bold [&_h2]:text-secondary-10 [&_h3]:mb-2 [&_h3]:mt-3 [&_h3]:text-base [&_h3]:font-bold [&_h3]:text-secondary-10 [&_h4]:mt-3 [&_h4]:font-semibold [&_h4]:text-secondary-10 [&_h5]:mt-3 [&_h5]:font-semibold [&_h5]:text-secondary-10 [&_h6]:mt-2 [&_h6]:text-xs [&_h6]:font-semibold [&_h6]:text-secondary-20 [&_hr]:my-4 [&_hr]:border-default-200 [&_img]:my-3 [&_img]:max-w-full [&_img]:rounded-xl [&_li]:my-1 [&_mark]:rounded-sm [&_mark]:bg-yellow-200 [&_mark]:px-1 [&_ol]:list-decimal [&_ol]:ps-5 [&_p+_p]:mt-3 [&_pre]:my-3 [&_pre]:overflow-x-auto [&_pre]:rounded-xl [&_pre]:bg-secondary-10 [&_pre]:p-4 [&_pre]:text-secondary-40 [&_ul]:list-disc [&_ul]:ps-5"
/>
) : null}
</div>
) : null}
<div>
<span className="text-sm text-secondary-30">رسانهها ({number(media.length)})</span>
{media.length ? (
<div className="mt-2 grid grid-cols-5 gap-2 sm:grid-cols-8">
{media.map((item) => (
<FallbackImage
key={item.id}
alt={`رسانه ${event.title}`}
className="aspect-square w-full rounded-lg object-cover"
fallbackSrc={DEFAULT_EVENT_IMAGE}
src={item.thumbnailUrl ?? item.url}
/>
))}
</div>
) : event.posterUrl ? (
<div className="mt-2 max-w-[5.5rem]">
<FallbackImage
alt={`پوستر ${event.title}`}
className="aspect-square w-full rounded-lg object-cover"
fallbackSrc={DEFAULT_EVENT_IMAGE}
src={event.posterUrl}
/>
</div>
) : (
<p className="mt-1 text-sm text-secondary-30">رسانهای ثبت نشده است.</p>
)}
</div>
<div>
<span className="text-sm text-secondary-30">سوالات متداول ({number(faqs.length)})</span>
{faqs.length ? (
<div className="mt-2 space-y-2">
{faqs.map((faq) => (
<div
key={faq.id}
className="rounded-xl border border-secondary-40 bg-secondary-50/60 p-3"
>
<h3 className="text-sm font-bold text-secondary-20">{faq.question}</h3>
<p className="mt-1 whitespace-pre-line text-xs leading-6 text-secondary-30">{faq.answer}</p>
</div>
))}
</div>
) : (
<p className="mt-1 text-sm text-secondary-30">سوالی ثبت نشده است.</p>
)}
</div>
</CardBody>
</Card>
)
}
export default AdminEventDetailsSection