Files
kycnotme/web/src/pages/admin/comments.astro
2025-05-23 18:23:14 +00:00

270 lines
9.0 KiB
Plaintext

---
import { z } from 'astro/zod'
import { Icon } from 'astro-icon/components'
import BadgeSmall from '../../components/BadgeSmall.astro'
import CommentModeration from '../../components/CommentModeration.astro'
import MyPicture from '../../components/MyPicture.astro'
import TimeFormatted from '../../components/TimeFormatted.astro'
import UserBadge from '../../components/UserBadge.astro'
import {
commentStatusFilters,
commentStatusFiltersZodEnum,
getCommentStatusFilterInfo,
getCommentStatusFilterValue,
} from '../../constants/commentStatusFilters'
import BaseLayout from '../../layouts/BaseLayout.astro'
import { cn } from '../../lib/cn'
import { zodParseQueryParamsStoringErrors } from '../../lib/parseUrlFilters'
import { prisma } from '../../lib/prisma'
import { urlWithParams } from '../../lib/urls'
const user = Astro.locals.user
if (!user || (!user.admin && !user.moderator)) {
return Astro.rewrite('/404')
}
const { data: params } = zodParseQueryParamsStoringErrors(
{
status: commentStatusFiltersZodEnum.default('all'),
page: z.number().int().positive().default(1),
},
Astro
)
const PAGE_SIZE = 20
const statusFilter = getCommentStatusFilterInfo(params.status)
const [comments = [], totalComments = 0] = await Astro.locals.banners.try(
'Error fetching comments',
async () =>
prisma.comment.findManyAndCount({
where: statusFilter.whereClause,
include: {
author: {
select: {
name: true,
displayName: true,
picture: true,
},
},
service: {
select: {
name: true,
slug: true,
imageUrl: true,
},
},
parent: {
include: {
author: true,
},
},
votes: true,
},
orderBy: [{ createdAt: 'desc' }, { id: 'asc' }],
skip: (params.page - 1) * PAGE_SIZE,
take: PAGE_SIZE,
}),
[]
)
const totalPages = Math.ceil(totalComments / PAGE_SIZE)
---
<BaseLayout pageTitle="Comment Moderation" widthClassName="max-w-screen-xl">
<div class="mb-8">
<h1 class="font-title mb-4 text-2xl text-green-500">&gt; comments.moderate</h1>
<!-- Status Filters -->
<div class="flex flex-wrap gap-2">
{
commentStatusFilters.map((filter) => (
<a
href={urlWithParams(Astro.url, { status: filter.value })}
class={cn([
'font-title flex items-center gap-2 rounded-md border px-3 py-1 text-sm',
params.status === filter.value
? filter.classNames.filter
: 'border-zinc-700 transition-colors hover:border-green-500/50',
])}
>
<Icon name={filter.icon} class="size-4 shrink-0" />
{filter.label}
</a>
))
}
</div>
</div>
<!-- Comments List -->
<div class="space-y-6">
{
comments
.map((comment) => ({
...comment,
statusFilterInfo: getCommentStatusFilterInfo(getCommentStatusFilterValue(comment)),
}))
.map((comment) => (
<div
id={`comment-${comment.id.toString()}`}
class="rounded-lg border border-zinc-800 bg-black/40 p-6 backdrop-blur-xs"
>
<div class="mb-4 flex flex-wrap items-center gap-2">
{/* Author Info */}
<UserBadge user={comment.author} size="md" />
{/* Service Link */}
<span class="text-xs text-zinc-500">•</span>
<a
href={`/service/${comment.service.slug}#comment-${comment.id.toString()}`}
class="text-sm text-blue-400 transition-colors hover:text-blue-300"
>
{!!comment.service.imageUrl && (
<MyPicture
src={comment.service.imageUrl}
height={16}
width={16}
class="inline-block size-4 rounded-full align-[-0.2em]"
alt=""
/>
)}
{comment.service.name}
</a>
{/* Date */}
<span class="text-xs text-zinc-500">•</span>
<TimeFormatted
date={comment.createdAt}
hourPrecision
caseType="sentence"
class="text-sm text-zinc-400"
/>
<span class="text-xs text-zinc-500">•</span>
{/* Status Badges */}
<BadgeSmall
color={comment.statusFilterInfo.color}
text={comment.statusFilterInfo.label}
icon={comment.statusFilterInfo.icon}
inlineIcon
/>
<span class="text-xs text-zinc-500">•</span>
{/* Link to Comment */}
<a
href={`/service/${comment.service.slug}?showPending=true#comment-${comment.id.toString()}`}
class="text-whit/50 flex items-center gap-1 text-sm transition-colors hover:underline"
>
<Icon name="ri:link" class="size-4" />
Open
</a>
</div>
{/* Parent Comment Context */}
{comment.parent && (
<div class="mb-4 border-l-2 border-zinc-700 pl-4">
<div class="mb-1 text-sm opacity-50">
Replying to <UserBadge user={comment.parent.author} size="md" />
</div>
<div class="text-sm text-zinc-400">{comment.parent.content}</div>
</div>
)}
{/* Comment Content */}
<div class="mb-4 text-sm">{comment.content}</div>
{/* Notes */}
{comment.communityNote && (
<div class="mt-2">
<div class="text-sm font-medium text-green-500">Community Note</div>
<p class="text-sm text-gray-300">{comment.communityNote}</p>
</div>
)}
{comment.internalNote && (
<div class="mt-2">
<div class="text-sm font-medium text-yellow-500">Internal Note</div>
<p class="text-sm text-gray-300">{comment.internalNote}</p>
</div>
)}
{comment.privateContext && (
<div class="mt-2">
<div class="text-sm font-medium text-blue-500">Private Context</div>
<p class="text-sm text-gray-300">{comment.privateContext}</p>
</div>
)}
{comment.orderId && (
<div class="mt-2">
<div class="text-sm font-medium text-purple-500">Order ID</div>
<div class="flex items-center gap-2">
<p class="text-sm text-gray-300">{comment.orderId}</p>
{comment.orderIdStatus && (
<span
class={cn(
'rounded-sm px-1.5 py-0.5 text-xs',
comment.orderIdStatus === 'APPROVED' && 'bg-green-500/20 text-green-400',
comment.orderIdStatus === 'REJECTED' && 'bg-red-500/20 text-red-400',
comment.orderIdStatus === 'PENDING' && 'bg-blue-500/20 text-blue-400'
)}
>
{comment.orderIdStatus}
</span>
)}
</div>
</div>
)}
{(comment.kycRequested || comment.fundsBlocked) && (
<div class="mt-2">
<div class="text-sm font-medium text-red-500">Issue Flags</div>
<div class="flex flex-wrap gap-2">
{comment.kycRequested && (
<span class="rounded-sm bg-red-500/20 px-1.5 py-0.5 text-xs text-red-400">
KYC Requested
</span>
)}
{comment.fundsBlocked && (
<span class="rounded-sm bg-red-500/20 px-1.5 py-0.5 text-xs text-red-400">
Funds Blocked
</span>
)}
</div>
</div>
)}
<CommentModeration class="mt-2" comment={comment} />
</div>
))
}
</div>
<!-- Pagination -->
{
totalPages > 1 && (
<div class="mt-8 flex justify-center gap-2">
{params.page > 1 && (
<a
href={urlWithParams(Astro.url, { page: params.page - 1 })}
class="font-title rounded-md border border-zinc-700 px-3 py-1 text-sm transition-colors hover:border-green-500/50"
>
Previous
</a>
)}
<span class="font-title px-3 py-1 text-sm">
Page {params.page} of {totalPages}
</span>
{params.page < totalPages && (
<a
href={urlWithParams(Astro.url, { page: params.page + 1 })}
class="font-title rounded-md border border-zinc-700 px-3 py-1 text-sm transition-colors hover:border-green-500/50"
>
Next
</a>
)}
</div>
)
}
</BaseLayout>