270 lines
9.0 KiB
Plaintext
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">> 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>
|