Files
kycnotme/web/src/components/CommentModeration.astro
2025-06-06 10:09:59 +00:00

436 lines
17 KiB
Plaintext

---
import { Icon } from 'astro-icon/components'
import { cn } from '../lib/cn'
import type { Prisma } from '@prisma/client'
import type { HTMLAttributes } from 'astro/types'
type Props = HTMLAttributes<'div'> & {
comment: Prisma.CommentGetPayload<{
select: {
id: true
status: true
suspicious: true
requiresAdminReview: true
kycRequested: true
fundsBlocked: true
communityNote: true
internalNote: true
privateContext: true
orderId: true
orderIdStatus: true
rating: true
ratingActive: true
}
}>
}
const { comment, class: className, ...divProps } = Astro.props
const user = Astro.locals.user
// Only render for admin/moderator users
if (!user || !user.admin || !user.moderator) return null
---
<div {...divProps} class={cn('text-xs', className)}>
<input type="checkbox" id={`mod-toggle-${String(comment.id)}`} class="peer sr-only" />
<label
for={`mod-toggle-${String(comment.id)}`}
class="text-day-500 hover:text-day-300 peer-focus-visible:ring-offset-night-700 inline-flex cursor-pointer items-center gap-1 rounded-sm peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2"
>
<Icon name="ri:shield-keyhole-line" class="h-3.5 w-3.5" />
<span class="text-xs">Moderation</span>
<Icon name="ri:arrow-down-s-line" class="h-3.5 w-3.5 transition-transform peer-checked:rotate-180" />
</label>
<div
class="bg-night-600 border-night-500 mt-2 hidden overflow-hidden rounded-md border peer-checked:block peer-checked:p-2"
>
<div class="border-night-500 flex flex-wrap items-center gap-1 border-b pb-2">
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.status === 'REJECTED'
? 'border border-red-500/30 bg-red-500/20 text-red-400'
: 'bg-night-700 hover:bg-red-500/20 hover:text-red-400'
)}
data-action="status"
data-value={comment.status === 'REJECTED' ? 'PENDING' : 'REJECTED'}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:close-circle-line" class="h-3.5 w-3.5" />
<span>{comment.status === 'REJECTED' ? 'Unreject' : 'Reject'}</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.status === 'VERIFIED'
? 'border border-green-500/30 bg-green-500/20 text-green-400'
: 'bg-night-700 hover:bg-green-500/20 hover:text-green-400'
)}
data-action="status"
data-value={comment.status === 'VERIFIED' ? 'APPROVED' : 'VERIFIED'}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:verified-badge-line" class="h-3.5 w-3.5" />
<span>{comment.status === 'VERIFIED' ? 'Unverify' : 'Verify'}</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING'
? 'border border-blue-500/30 bg-blue-500/20 text-blue-400'
: 'bg-night-700 hover:bg-blue-500/20 hover:text-blue-400'
)}
data-action="status"
data-value={comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING'
? 'APPROVED'
: 'PENDING'}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:checkbox-circle-line" class="h-3.5 w-3.5" />
<span>
{comment.status === 'PENDING' || comment.status === 'HUMAN_PENDING' ? 'Approve' : 'Pending'}
</span>
</button>
<div class="bg-night-500 h-5 w-px"></div>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.suspicious
? 'border border-yellow-500/30 bg-yellow-500/20 text-yellow-400'
: 'bg-night-700 hover:bg-yellow-500/20 hover:text-yellow-400'
)}
data-action="suspicious"
data-value={!comment.suspicious}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:spam-2-line" class="h-3.5 w-3.5" />
<span>{comment.suspicious ? 'Not Spam' : 'Spam'}</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.requiresAdminReview
? 'border border-purple-500/30 bg-purple-500/20 text-purple-400'
: 'bg-night-700 hover:bg-purple-500/20 hover:text-purple-400'
)}
data-action="requires-admin-review"
data-value={!comment.requiresAdminReview}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:shield-user-line" class="h-3.5 w-3.5" />
<span>{comment.requiresAdminReview ? 'No Admin Review' : 'Needs Admin Review'}</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.kycRequested
? 'border border-red-500/30 bg-red-500/20 text-red-400'
: 'bg-night-700 hover:bg-red-500/20 hover:text-red-400'
)}
data-action="kyc-requested"
data-value={!comment.kycRequested}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:bank-card-line" class="h-3.5 w-3.5" />
<span>{comment.kycRequested ? 'No KYC Issue' : 'KYC Issue'}</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.fundsBlocked
? 'border border-red-500/30 bg-red-500/20 text-red-400'
: 'bg-night-700 hover:bg-red-500/20 hover:text-red-400'
)}
data-action="funds-blocked"
data-value={!comment.fundsBlocked}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:lock-line" class="h-3.5 w-3.5" />
<span>{comment.fundsBlocked ? 'No Funds Issue' : 'Funds Issue'}</span>
</button>
<div class="bg-night-500 h-5 w-px"></div>
{
comment.rating && (
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.ratingActive
? 'border border-blue-500/30 bg-blue-500/20 text-blue-400'
: 'bg-night-700 hover:bg-blue-500/20 hover:text-blue-400'
)}
data-action="toggle-rating-active"
data-value={!comment.ratingActive}
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:star-line" class="h-3.5 w-3.5" />
<span>{comment.ratingActive ? 'Disable Rating' : 'Enable Rating'}</span>
</button>
)
}
</div>
<div class="mt-2 space-y-1.5">
<div class="flex items-center gap-1.5">
<span class="text-day-500 text-[10px]">Community:</span>
<input
type="text"
placeholder="Public note..."
value={comment.communityNote}
class="bg-night-700 border-night-500 flex-1 rounded-sm border px-1.5 py-0.5 text-xs outline-hidden focus:ring-1 focus:ring-blue-500"
data-action="community-note"
data-comment-id={comment.id}
data-user-id={user.id}
/>
</div>
<div class="flex items-center gap-1.5">
<span class="text-day-500 text-[10px]">Internal:</span>
<input
type="text"
placeholder="Mod note..."
value={comment.internalNote}
class="bg-night-700 border-night-500 flex-1 rounded-sm border px-1.5 py-0.5 text-xs outline-hidden focus:ring-1 focus:ring-blue-500"
data-action="internal-note"
data-comment-id={comment.id}
data-user-id={user.id}
/>
</div>
<div class="flex items-center gap-1.5">
<span class="text-day-500 text-[10px]">Private:</span>
<input
type="text"
placeholder="Context..."
value={comment.privateContext}
class="bg-night-700 border-night-500 flex-1 rounded-sm border px-1.5 py-0.5 text-xs outline-hidden focus:ring-1 focus:ring-blue-500"
data-action="private-context"
data-comment-id={comment.id}
data-user-id={user.id}
/>
</div>
{
comment.orderId && (
<div class="border-night-500 mt-3 space-y-1.5 border-t pt-2">
<div class="flex items-center gap-1.5">
<span class="text-day-500 text-[10px]">Order ID:</span>
<div class="bg-night-700 flex-1 rounded-sm px-1.5 py-0.5 text-xs">{comment.orderId}</div>
</div>
<div class="flex items-center gap-1.5">
<span class="text-day-500 text-[10px]">Status:</span>
<div class="flex gap-1">
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.orderIdStatus === 'APPROVED'
? 'border border-green-500/30 bg-green-500/20 text-green-400'
: 'bg-night-700 hover:bg-green-500/20 hover:text-green-400'
)}
data-action="order-id-status"
data-value="APPROVED"
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:check-line" class="h-3.5 w-3.5" />
<span>Approve</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.orderIdStatus === 'REJECTED'
? 'border border-red-500/30 bg-red-500/20 text-red-400'
: 'bg-night-700 hover:bg-red-500/20 hover:text-red-400'
)}
data-action="order-id-status"
data-value="REJECTED"
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:close-line" class="h-3.5 w-3.5" />
<span>Reject</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.orderIdStatus === 'PENDING'
? 'border border-blue-500/30 bg-blue-500/20 text-blue-400'
: 'bg-night-700 hover:bg-blue-500/20 hover:text-blue-400'
)}
data-action="order-id-status"
data-value="PENDING"
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:time-line" class="h-3.5 w-3.5" />
<span>Pending</span>
</button>
<button
class={cn(
'inline-flex items-center gap-1 rounded-sm px-1.5 py-0.5 text-xs transition-colors',
comment.orderIdStatus === 'WITHDRAWN'
? 'border-night-400 bg-night-500/50 text-night-300 border'
: 'bg-night-700 hover:bg-night-500/50 hover:text-night-300'
)}
data-action="order-id-status"
data-value="WITHDRAWN"
data-comment-id={comment.id}
data-user-id={user.id}
>
<Icon name="ri:arrow-go-back-line" class="h-3.5 w-3.5" />
<span>Withdrawn</span>
</button>
</div>
</div>
</div>
)
}
</div>
</div>
</div>
<script>
import { actions } from 'astro:actions'
document.addEventListener('astro:page-load', () => {
// Handle button clicks
document.querySelectorAll('button[data-action]').forEach((btn) => {
btn.addEventListener('click', async () => {
const action = btn.getAttribute('data-action')
const value = btn.getAttribute('data-value')
const commentId = parseInt(btn.getAttribute('data-comment-id') || '0')
const userId = parseInt(btn.getAttribute('data-user-id') || '0')
if (!value || !commentId || !userId) return
try {
const { error } = await actions.comment.moderate({
commentId,
userId,
action: action as any,
value:
action === 'suspicious' ||
action === 'requires-admin-review' ||
action === 'kyc-requested' ||
action === 'funds-blocked' ||
action === 'toggle-rating-active'
? value === 'true'
: value,
})
if (!error) {
// Update button state based on new value
if (action === 'status') {
window.location.reload()
} else if (action === 'suspicious') {
const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'Not Spam' : 'Spam'
btn.classList.toggle('bg-yellow-500/20')
btn.classList.toggle('text-yellow-400')
btn.classList.toggle('border-yellow-500/30')
btn.classList.toggle('border')
btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
} else if (action === 'requires-admin-review') {
const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'No Admin Review' : 'Needs Admin Review'
btn.classList.toggle('bg-purple-500/20')
btn.classList.toggle('text-purple-400')
btn.classList.toggle('border-purple-500/30')
btn.classList.toggle('border')
btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
} else if (action === 'order-id-status') {
// Refresh to show updated order ID status
window.location.reload()
} else if (action === 'kyc-requested') {
const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'No KYC Issue' : 'KYC Issue'
btn.classList.toggle('bg-red-500/20')
btn.classList.toggle('text-red-400')
btn.classList.toggle('border-red-500/30')
btn.classList.toggle('border')
btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
} else if (action === 'funds-blocked') {
const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'No Funds Issue' : 'Funds Issue'
btn.classList.toggle('bg-red-500/20')
btn.classList.toggle('text-red-400')
btn.classList.toggle('border-red-500/30')
btn.classList.toggle('border')
btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
} else if (action === 'toggle-rating-active') {
const span = btn.querySelector('span')
if (span) span.textContent = value === 'true' ? 'Disable Rating' : 'Enable Rating'
btn.classList.toggle('bg-blue-500/20')
btn.classList.toggle('text-blue-400')
btn.classList.toggle('border-blue-500/30')
btn.classList.toggle('border')
btn.classList.toggle('bg-night-700')
btn.setAttribute('data-value', value === 'true' ? 'false' : 'true')
}
} else {
console.error('Error moderating comment:', error)
}
} catch (error) {
console.error('Error moderating comment:', error)
}
})
})
// Handle text input changes
document.querySelectorAll('input[data-action]').forEach((input) => {
const action = input.getAttribute('data-action')
const commentId = parseInt(input.getAttribute('data-comment-id') || '0')
const userId = parseInt(input.getAttribute('data-user-id') || '0')
if (!action || !commentId || !userId) return
let timeout: NodeJS.Timeout
input.addEventListener('input', () => {
clearTimeout(timeout)
timeout = setTimeout(async () => {
try {
const { error } = await actions.comment.moderate({
commentId,
userId,
action: action as any,
value: (input as HTMLInputElement).value,
})
if (error) {
console.error('Error updating note:', error)
}
} catch (error) {
console.error('Error updating note:', error)
}
}, 500) // Debounce for 500ms
})
})
})
</script>