admin/components/reviews/ReviewCard.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

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