feat: Optimize moderation queue desktop layout and fix release lock

This commit is contained in:
gpt-engineer-app[bot]
2025-10-15 15:37:28 +00:00
parent 92759f917d
commit adad353e4b
3 changed files with 97 additions and 22 deletions

View File

@@ -233,8 +233,10 @@ export const QueueItem = memo(({
)} )}
</CardHeader> </CardHeader>
<CardContent className={`space-y-4 ${isMobile ? 'p-4 pt-0' : ''}`}> <CardContent className={`${isMobile ? 'p-4 pt-0 space-y-4' : 'p-6 pt-0'}`}>
<div className={`bg-muted/50 rounded-lg ${isMobile ? 'p-3' : 'p-4'}`}> <div className={`bg-muted/50 rounded-lg ${isMobile ? 'p-3 space-y-3' : 'p-4'} ${
!isMobile ? 'lg:grid lg:grid-cols-[1fr,320px] lg:gap-6' : ''
}`}>
{item.type === 'review' ? ( {item.type === 'review' ? (
<div> <div>
{item.content.title && ( {item.content.title && (
@@ -361,6 +363,59 @@ export const QueueItem = memo(({
/> />
</div> </div>
)} )}
{/* Right sidebar on desktop: metadata & context */}
{!isMobile && (item.entity_name || item.park_name || item.user_profile) && (
<div className="space-y-3">
{(item.entity_name || item.park_name) && (
<div className="bg-card rounded-md border p-3 space-y-2">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Context
</div>
{item.entity_name && (
<div className="text-sm">
<span className="text-xs text-muted-foreground block mb-0.5">
{item.park_name ? 'Ride' : 'Entity'}
</span>
<span className="font-medium">{item.entity_name}</span>
</div>
)}
{item.park_name && (
<div className="text-sm">
<span className="text-xs text-muted-foreground block mb-0.5">Park</span>
<span className="font-medium">{item.park_name}</span>
</div>
)}
</div>
)}
{item.user_profile && (
<div className="bg-card rounded-md border p-3 space-y-2">
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide">
Submitter
</div>
<div className="flex items-center gap-2">
<Avatar className="h-8 w-8">
<AvatarImage src={item.user_profile.avatar_url} />
<AvatarFallback className="text-xs">
{(item.user_profile.display_name || item.user_profile.username)?.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="text-sm">
<div className="font-medium">
{item.user_profile.display_name || item.user_profile.username}
</div>
{item.user_profile.display_name && (
<div className="text-xs text-muted-foreground">
@{item.user_profile.username}
</div>
)}
</div>
</div>
</div>
)}
</div>
)}
</div> </div>
{/* Action buttons based on status */} {/* Action buttons based on status */}
@@ -399,6 +454,8 @@ export const QueueItem = memo(({
</div> </div>
)} )}
<div className={isMobile ? 'space-y-4 mt-4' : 'grid grid-cols-1 lg:grid-cols-[1fr,auto] gap-6 items-start mt-4'}>
{/* Left: Notes textarea */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor={`notes-${item.id}`}>Moderation Notes (optional)</Label> <Label htmlFor={`notes-${item.id}`}>Moderation Notes (optional)</Label>
<Textarea <Textarea
@@ -408,12 +465,15 @@ export const QueueItem = memo(({
onChange={(e) => onNoteChange(item.id, e.target.value)} onChange={(e) => onNoteChange(item.id, e.target.value)}
onFocus={() => onInteractionFocus(item.id)} onFocus={() => onInteractionFocus(item.id)}
onBlur={() => onInteractionBlur(item.id)} onBlur={() => onInteractionBlur(item.id)}
rows={2} rows={isMobile ? 2 : 4}
className={!isMobile ? 'min-h-[120px]' : ''}
disabled={isLockedByOther || currentLockSubmissionId !== item.id} disabled={isLockedByOther || currentLockSubmissionId !== item.id}
/> />
</div> </div>
<div className={`flex gap-2 pt-2 ${isMobile ? 'flex-col' : 'flex-col sm:flex-row'}`}> {/* Right: Action buttons */}
<div className={isMobile ? 'flex flex-col gap-2' : 'grid grid-cols-2 gap-2 min-w-[400px]'}>
{/* Show Review Items button for content submissions */} {/* Show Review Items button for content submissions */}
{item.type === 'content_submission' && ( {item.type === 'content_submission' && (
<> <>
@@ -470,6 +530,7 @@ export const QueueItem = memo(({
Reject Reject
</Button> </Button>
</div> </div>
</div>
</> </>
)} )}

View File

@@ -126,7 +126,7 @@ export function SubmissionChangesDisplay({
</div> </div>
{(changes.action === 'edit' || changes.action === 'create') && changes.totalChanges > 0 && ( {(changes.action === 'edit' || changes.action === 'create') && changes.totalChanges > 0 && (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1 lg:grid lg:grid-cols-3 xl:grid-cols-4">
{changes.fieldChanges.slice(0, 5).map((change, idx) => ( {changes.fieldChanges.slice(0, 5).map((change, idx) => (
<FieldDiff key={idx} change={change} compact /> <FieldDiff key={idx} change={change} compact />
))} ))}
@@ -221,7 +221,7 @@ export function SubmissionChangesDisplay({
{changes.fieldChanges.length > 0 && ( {changes.fieldChanges.length > 0 && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Creation Data (with moderator edits highlighted)</h4> <h4 className="text-sm font-medium">Creation Data (with moderator edits highlighted)</h4>
<div className="grid gap-2"> <div className="grid gap-2 lg:grid-cols-2">
{changes.fieldChanges.map((change, idx) => { {changes.fieldChanges.map((change, idx) => {
// Highlight fields that were added OR modified by moderator // Highlight fields that were added OR modified by moderator
const wasEditedByModerator = item.original_data && const wasEditedByModerator = item.original_data &&
@@ -254,7 +254,7 @@ export function SubmissionChangesDisplay({
{changes.fieldChanges.length > 0 && ( {changes.fieldChanges.length > 0 && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Creation Data</h4> <h4 className="text-sm font-medium">Creation Data</h4>
<div className="grid gap-2"> <div className="grid gap-2 lg:grid-cols-2">
{changes.fieldChanges.map((change, idx) => ( {changes.fieldChanges.map((change, idx) => (
<div key={idx}> <div key={idx}>
<FieldDiff change={change} /> <FieldDiff change={change} />
@@ -300,7 +300,7 @@ export function SubmissionChangesDisplay({
{changes.fieldChanges.length > 0 && ( {changes.fieldChanges.length > 0 && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h4 className="text-sm font-medium">Field Changes ({changes.fieldChanges.length})</h4> <h4 className="text-sm font-medium">Field Changes ({changes.fieldChanges.length})</h4>
<div className="grid gap-2"> <div className="grid gap-2 lg:grid-cols-2">
{changes.fieldChanges.map((change, idx) => ( {changes.fieldChanges.map((change, idx) => (
<FieldDiff key={idx} change={change} /> <FieldDiff key={idx} change={change} />
))} ))}

View File

@@ -231,6 +231,8 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
const releaseLock = useCallback(async (submissionId: string): Promise<boolean> => { const releaseLock = useCallback(async (submissionId: string): Promise<boolean> => {
if (!user?.id) return false; if (!user?.id) return false;
setIsLoading(true);
try { try {
const { data, error } = await supabase.rpc('release_submission_lock', { const { data, error } = await supabase.rpc('release_submission_lock', {
submission_id: submissionId, submission_id: submissionId,
@@ -250,6 +252,11 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
fetchStats(); fetchStats();
toast({
title: 'Lock Released',
description: 'You can now claim another submission',
});
// Trigger refresh callback // Trigger refresh callback
if (onLockStateChange) { if (onLockStateChange) {
onLockStateChange(); onLockStateChange();
@@ -261,9 +268,16 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
return false; return false;
} catch (error: any) { } catch (error: any) {
console.error('Error releasing lock:', error); console.error('Error releasing lock:', error);
toast({
title: 'Failed to Release Lock',
description: error.message || 'An error occurred',
variant: 'destructive',
});
return false; return false;
} finally {
setIsLoading(false);
} }
}, [user, fetchStats]); }, [user, fetchStats, toast, onLockStateChange]);
// Get time remaining on current lock // Get time remaining on current lock
const getTimeRemaining = useCallback((): number | null => { const getTimeRemaining = useCallback((): number | null => {