Files
kycnotme/web/src/components/CommentSummary.astro
2025-05-26 18:04:45 +00:00

126 lines
3.8 KiB
Plaintext

---
import { Icon } from 'astro-icon/components'
import { Schema } from 'astro-seo-schema'
import { clamp, round, sum, sumBy } from 'lodash-es'
import { cn } from '../lib/cn'
import { prisma } from '../lib/prisma'
import type { HTMLAttributes } from 'astro/types'
type Props = HTMLAttributes<'div'> & {
serviceId: number
itemReviewedId: string
averageUserRating?: number | null
}
const {
serviceId,
itemReviewedId,
averageUserRating: averageUserRatingFromProps,
class: className,
...htmlProps
} = Astro.props
const ratingsFromDb = await prisma.comment.groupBy({
by: ['rating'],
where: {
serviceId,
ratingActive: true,
status: {
in: ['APPROVED', 'VERIFIED'],
},
parentId: null,
suspicious: false,
},
_count: true,
})
const ratings = ([5, 4, 3, 2, 1] as const).map((rating) => ({
rating,
count: ratingsFromDb.find((stat) => stat.rating === rating)?._count ?? 0,
}))
const totalComments = sumBy(ratings, 'count')
const averageUserRatingFromQuery =
totalComments > 0 ? sum(ratings.map((stat) => stat.rating * stat.count)) / totalComments : null
if (averageUserRatingFromProps !== undefined) {
const a = averageUserRatingFromQuery === null ? null : round(averageUserRatingFromQuery, 2)
const b = averageUserRatingFromProps === null ? null : round(averageUserRatingFromProps, 2)
if (a !== b) {
console.error(
`The averageUserRating of the comments shown is different from the averageUserRating from the database. Service ID: ${serviceId} ratingUi: ${averageUserRatingFromQuery} ratingDb: ${averageUserRatingFromProps}`
)
}
}
const averageUserRating =
averageUserRatingFromProps === undefined ? averageUserRatingFromQuery : averageUserRatingFromProps
---
<div {...htmlProps} class={cn('flex flex-wrap items-center justify-center gap-4', className)}>
{
averageUserRating !== null && (
<Schema
item={{
'@context': 'https://schema.org',
'@type': 'AggregateRating',
itemReviewed: { '@id': itemReviewedId },
ratingValue: round(averageUserRating, 1),
bestRating: 5,
worstRating: 1,
ratingCount: totalComments,
}}
/>
)
}
<div class="flex flex-col items-center">
<div class="mb-1 text-5xl">
{averageUserRating !== null ? round(averageUserRating, 1).toLocaleString() : '-'}
</div>
<div class="flex items-center space-x-1">
{
([1, 2, 3, 4, 5] as const).map((rating) => (
<div
class="relative size-5"
style={`--percent: ${clamp((averageUserRating ?? 0) - (rating - 1), 0, 1) * 100}%`}
>
<Icon name="ri:star-line" class="absolute inset-0 size-full text-zinc-500" />
<Icon
name="ri:star-fill"
class="absolute inset-0 size-full text-yellow-400 [clip-path:inset(0_calc(100%_-_var(--percent))_0_0)]"
/>
</div>
))
}
</div>
<div class="mt-1 text-sm text-zinc-400">
{totalComments.toLocaleString()} ratings
</div>
</div>
<div class="grid min-w-32 flex-1 grid-cols-[auto_1fr_auto] items-center gap-1">
{
ratings.map(({ rating, count }) => {
const percent = totalComments > 0 ? (count / totalComments) * 100 : null
return (
<>
<div class="text-center text-xs text-zinc-400" aria-label={`${rating} stars`}>
{rating.toLocaleString()}
</div>
<div class="h-2 flex-1 overflow-hidden rounded-full bg-zinc-700">
<div class="h-full w-(--percent) bg-yellow-400" style={`--percent: ${percent ?? 0}%`} />
</div>
<div class="text-right text-xs text-zinc-400">
{[<span>{round(percent ?? 0).toLocaleString()}</span>, <span class="text-zinc-500">%</span>]}
</div>
</>
)
})
}
</div>
</div>