mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 09:31:12 -05:00
feat: Optimize moderation queue desktop layout and fix release lock
This commit is contained in:
@@ -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,21 +454,26 @@ export const QueueItem = memo(({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className={isMobile ? 'space-y-4 mt-4' : 'grid grid-cols-1 lg:grid-cols-[1fr,auto] gap-6 items-start mt-4'}>
|
||||||
<Label htmlFor={`notes-${item.id}`}>Moderation Notes (optional)</Label>
|
{/* Left: Notes textarea */}
|
||||||
<Textarea
|
<div className="space-y-2">
|
||||||
id={`notes-${item.id}`}
|
<Label htmlFor={`notes-${item.id}`}>Moderation Notes (optional)</Label>
|
||||||
placeholder="Add notes about your moderation decision..."
|
<Textarea
|
||||||
value={notes[item.id] || ''}
|
id={`notes-${item.id}`}
|
||||||
onChange={(e) => onNoteChange(item.id, e.target.value)}
|
placeholder="Add notes about your moderation decision..."
|
||||||
onFocus={() => onInteractionFocus(item.id)}
|
value={notes[item.id] || ''}
|
||||||
onBlur={() => onInteractionBlur(item.id)}
|
onChange={(e) => onNoteChange(item.id, e.target.value)}
|
||||||
rows={2}
|
onFocus={() => onInteractionFocus(item.id)}
|
||||||
disabled={isLockedByOther || currentLockSubmissionId !== item.id}
|
onBlur={() => onInteractionBlur(item.id)}
|
||||||
/>
|
rows={isMobile ? 2 : 4}
|
||||||
</div>
|
className={!isMobile ? 'min-h-[120px]' : ''}
|
||||||
|
disabled={isLockedByOther || currentLockSubmissionId !== item.id}
|
||||||
<div className={`flex gap-2 pt-2 ${isMobile ? 'flex-col' : 'flex-col sm:flex-row'}`}>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 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' && (
|
||||||
<>
|
<>
|
||||||
@@ -469,6 +529,7 @@ export const QueueItem = memo(({
|
|||||||
<XCircle className={isMobile ? "w-5 h-5 mr-2" : "w-4 h-4 mr-2"} />
|
<XCircle className={isMobile ? "w-5 h-5 mr-2" : "w-4 h-4 mr-2"} />
|
||||||
Reject
|
Reject
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
Reference in New Issue
Block a user