268 lines
12 KiB
TypeScript
268 lines
12 KiB
TypeScript
'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
|