436 lines
17 KiB
Plaintext
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>
|