Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import type { ReactNode } from 'react'
|
|
|
|
import UserIdentity from '@/components/consumer/UserIdentity'
|
|
import HeatIconFill from '@/components/icons/HeatIconFill'
|
|
import { formatPersianDate, formatRelativeTimePersian } from '@/lib/formatters'
|
|
import { texts } from '@/texts'
|
|
|
|
/** Styling-token variant: which surface the card is rendered on. */
|
|
export type ReviewCardTone = 'consumer' | 'organizer'
|
|
|
|
interface ReviewCardProps {
|
|
rating: number
|
|
createdAt: string
|
|
body?: string | null
|
|
hostReplyBody?: string | null
|
|
tone: ReviewCardTone
|
|
authorName?: string
|
|
authorAvatarUrl?: string | null
|
|
/** When set, avatar + name link to the author's public `/u/:id` profile. */
|
|
authorUserId?: string | null
|
|
/** Optional content rendered between the header row and the body, e.g. an event link. */
|
|
headerExtra?: ReactNode
|
|
footer?: ReactNode
|
|
className?: string
|
|
}
|
|
|
|
const TONE_STYLES: Record<
|
|
ReviewCardTone,
|
|
{
|
|
date: string
|
|
body: string
|
|
replyContainer: string
|
|
replyTitle: string
|
|
replyBody: string
|
|
useRelativeTime: boolean
|
|
}
|
|
> = {
|
|
consumer: {
|
|
date: 'text-[10px] font-medium text-secondary-30',
|
|
body: 'mt-2 text-sm font-medium leading-7 text-secondary-30',
|
|
replyContainer: 'mt-3 rounded-xl bg-tertiary-900/5 p-3',
|
|
replyTitle: 'text-xs font-medium text-tertiary-900',
|
|
replyBody: 'mt-1 text-sm font-medium leading-6 text-secondary-30',
|
|
useRelativeTime: true,
|
|
},
|
|
organizer: {
|
|
date: 'text-sm text-tertiary-400',
|
|
body: 'mt-2 text-sm leading-7 text-tertiary-500',
|
|
replyContainer: 'mt-3 rounded-xl bg-primary-25 p-3',
|
|
replyTitle: 'text-xs font-bold text-primary',
|
|
replyBody: 'mt-1 text-sm leading-6',
|
|
useRelativeTime: false,
|
|
},
|
|
}
|
|
|
|
/** Shared review list item: author + heart rating + optional body + host reply. */
|
|
const ReviewCard = ({
|
|
rating,
|
|
createdAt,
|
|
body,
|
|
hostReplyBody,
|
|
tone,
|
|
authorName,
|
|
authorAvatarUrl,
|
|
authorUserId,
|
|
headerExtra,
|
|
footer,
|
|
className,
|
|
}: ReviewCardProps) => {
|
|
const styles = TONE_STYLES[tone]
|
|
const dateLabel = styles.useRelativeTime ? formatRelativeTimePersian(createdAt) : formatPersianDate(createdAt)
|
|
|
|
return (
|
|
<li className={className}>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<UserIdentity
|
|
name={authorName}
|
|
nameFallback={texts.events.guest}
|
|
size={36}
|
|
src={authorAvatarUrl}
|
|
subtitle={<p className={styles.date}>{dateLabel}</p>}
|
|
userId={authorUserId}
|
|
variant="circle"
|
|
/>
|
|
<div className="flex shrink-0 items-center gap-1 text-sm font-medium text-tertiary-900">
|
|
<HeatIconFill className="size-3.5 text-primary" />
|
|
<span>{Math.round(rating).toLocaleString('fa-IR')}</span>
|
|
</div>
|
|
</div>
|
|
{headerExtra}
|
|
{body ? <p className={styles.body}>{body}</p> : null}
|
|
{hostReplyBody ? (
|
|
<div className={styles.replyContainer}>
|
|
<p className={styles.replyTitle}>{texts.reviews.hostReply}</p>
|
|
<p className={styles.replyBody}>{hostReplyBody}</p>
|
|
</div>
|
|
) : null}
|
|
{footer}
|
|
</li>
|
|
)
|
|
}
|
|
|
|
export default ReviewCard
|