'use client'; import { Star } from 'lucide-react'; import { api } from '@/lib/trpc'; import { Button, EmptyState } from '@/components/ui'; import { cn, formatRelativeTime } from '@/lib/utils'; /** * Five stars, filled to the rating. * * The number goes in the accessible name rather than being inferred from a row * of glyphs — §8, meaning is never carried by shape alone. The stars themselves * are decorative once the label says "4 out of 5". */ function Stars({ rating, className }: { rating: number; className?: string }) { return ( {[1, 2, 3, 4, 5].map((n) => ( ))} ); } /** * What people wrote about this pro. * * A page, never the whole history: `ratingCount` in the header counts every * rating this pro has ever had, and a profile with sixty of them would push the * hire button somewhere nobody scrolls. The heading says which it is showing so * the two numbers cannot be read as a contradiction. * * Only published reviews exist as far as this is concerned — `pro.reviews` * enforces that server-side, because the publication gate is what stops a pro * retaliating against a bad review before it is visible. */ export function ReviewList({ proId, ratingCount }: { proId: string; ratingCount: number }) { const reviews = api.pro.reviews.useInfiniteQuery( { proId }, { getNextPageParam: (last) => last.nextCursor, staleTime: 60_000, retry: false }, ); const rows = reviews.data?.pages.flatMap((page) => page.reviews) ?? []; if (reviews.isLoading) { return (
{/* §6.11 — three is enough to say "loading"; twenty is a lie about what is coming. */} {[0, 1, 2].map((n) => (
))}
); } if (rows.length === 0) { return ( 0 ? 'This pro has been rated, but nobody has left written feedback that is public yet.' : 'Nobody has reviewed this pro yet. Reviews appear once a booking is finished.' } /> ); } return (
    {rows.map((review) => (
  • {review.authorImage ? ( // eslint-disable-next-line @next/next/no-img-element -- remote avatar, no loader configured ) : ( {review.authorName?.[0] ?? '?'} )} {review.authorName ?? 'A customer'} {formatRelativeTime(review.publishedAt)}

    {review.body}

  • ))}
{reviews.hasNextPage && ( )}
); }