From fd20c02e5ed2c9becb4225b9c6480d41a6fec925 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 19:38:39 +0000 Subject: [PATCH] Implement comprehensive fix --- src/components/moderation/ModerationQueue.tsx | 164 ++++++++++++------ src/lib/smartStateUpdate.ts | 7 +- 2 files changed, 121 insertions(+), 50 deletions(-) diff --git a/src/components/moderation/ModerationQueue.tsx b/src/components/moderation/ModerationQueue.tsx index 74b04a5d..43b6fbd8 100644 --- a/src/components/moderation/ModerationQueue.tsx +++ b/src/components/moderation/ModerationQueue.tsx @@ -81,6 +81,16 @@ export const ModerationQueue = forwardRef((props, ref) => { const [newItemsCount, setNewItemsCount] = useState(0); const [isRefreshing, setIsRefreshing] = useState(false); const [profileCache, setProfileCache] = useState>(new Map()); + const [entityCache, setEntityCache] = useState<{ + rides: Map, + parks: Map, + companies: Map + }>({ + rides: new Map(), + parks: new Map(), + companies: new Map() + }); + const [submissionMemo, setSubmissionMemo] = useState>(new Map()); const { toast } = useToast(); const { isAdmin, isSuperuser } = useUserRole(); const { user } = useAuth(); @@ -294,11 +304,18 @@ export const ModerationQueue = forwardRef((props, ref) => { .then(res => res.data || []) : Promise.resolve([]) ]); - // Create lookup maps for O(1) access + // Phase 2: Create lookup maps for O(1) access and update cache const ridesMap = new Map(ridesData.map((r: any) => [r.id, r] as [string, any])); const parksMap = new Map(parksData.map((p: any) => [p.id, p] as [string, any])); const companiesMap = new Map(companiesData.map((c: any) => [c.id, c] as [string, any])); + // Update entity cache asynchronously for next refresh + setEntityCache({ + rides: ridesMap, + parks: parksMap, + companies: companiesMap + }); + // Second pass: attach entity data using maps let submissionsWithEntities = (submissionsData || []).map(submission => { if (submission.submission_type === 'photo' && submission.content && typeof submission.content === 'object') { @@ -378,51 +395,94 @@ export const ModerationQueue = forwardRef((props, ref) => { .select('user_id, username, display_name, avatar_url') .in('user_id', userIds); - // Update profile cache with stable references - setProfileCache(prevCache => { - const newCache = new Map(prevCache); - profiles?.forEach(p => { - const existing = newCache.get(p.user_id); - // Only update if data actually changed - if (!existing || JSON.stringify(existing) !== JSON.stringify(p)) { - newCache.set(p.user_id, p); - } + // Phase 1: Use fresh data for THIS refresh, update cache for NEXT refresh + const profileMap = new Map(profiles?.map(p => [p.user_id, p]) || []); + + // Update cache asynchronously for next refresh + setProfileCache(new Map(profileMap)); + + // Phase 3 & 5: Normalize and memoize submissions + const formattedItems: ModerationItem[] = []; + const newMemo = new Map(submissionMemo); + + // Helper to create stable memoization key + const createMemoKey = (item: any): string => { + return `${item.id}-${item.status}-${item.reviewed_at || 'null'}-${JSON.stringify(item.entity_name || '')}-${JSON.stringify(item.park_name || '')}`; + }; + + // Process reviews + for (const review of reviews) { + let entity_name = ''; + let park_name = ''; + + if ((review as any).rides) { + entity_name = (review as any).rides.name; + park_name = (review as any).rides.parks?.name; + } else if ((review as any).parks) { + entity_name = (review as any).parks.name; + } + + const userProfile = profileMap.get(review.user_id); + const reviewerProfile = review.moderated_by ? profileMap.get(review.moderated_by) : undefined; + + const memoKey = createMemoKey({ + id: review.id, + status: review.moderation_status, + reviewed_at: review.moderated_at, + entity_name, + park_name }); - return newCache; - }); - - const profileMap = profileCache; - - // Combine and format items - const formattedItems: ModerationItem[] = [ - ...reviews.map(review => { - let entity_name = ''; - let park_name = ''; - - if ((review as any).rides) { - entity_name = (review as any).rides.name; - park_name = (review as any).rides.parks?.name; - } else if ((review as any).parks) { - entity_name = (review as any).parks.name; - } - - return { - id: review.id, - type: 'review' as const, - content: review, - created_at: review.created_at, - user_id: review.user_id, - status: review.moderation_status, - user_profile: profileMap.get(review.user_id), - entity_name, - park_name, - reviewed_at: review.moderated_at, - reviewed_by: review.moderated_by, - reviewer_notes: (review as any).reviewer_notes, - reviewer_profile: review.moderated_by ? profileMap.get(review.moderated_by) : undefined, - }; - }), - ...submissions.map(submission => ({ + + // Check memo first + const existing = newMemo.get(memoKey); + if (existing) { + formattedItems.push(existing); + continue; + } + + // Create new item + const newItem: ModerationItem = { + id: review.id, + type: 'review' as const, + content: review, + created_at: review.created_at, + user_id: review.user_id, + status: review.moderation_status, + user_profile: userProfile, + entity_name, + park_name, + reviewed_at: review.moderated_at, + reviewed_by: review.moderated_by, + reviewer_notes: (review as any).reviewer_notes, + reviewer_profile: reviewerProfile, + }; + + newMemo.set(memoKey, newItem); + formattedItems.push(newItem); + } + + // Process submissions + for (const submission of submissions) { + const userProfile = profileMap.get(submission.user_id); + const reviewerProfile = submission.reviewer_id ? profileMap.get(submission.reviewer_id) : undefined; + + const memoKey = createMemoKey({ + id: submission.id, + status: submission.status, + reviewed_at: submission.reviewed_at, + entity_name: (submission as any).entity_name, + park_name: (submission as any).park_name + }); + + // Check memo first + const existing = newMemo.get(memoKey); + if (existing) { + formattedItems.push(existing); + continue; + } + + // Create new item + const newItem: ModerationItem = { id: submission.id, type: 'content_submission' as const, content: submission.submission_type === 'photo' ? submission.content : submission, @@ -430,15 +490,21 @@ export const ModerationQueue = forwardRef((props, ref) => { user_id: submission.user_id, status: submission.status, submission_type: submission.submission_type, - user_profile: profileMap.get(submission.user_id), + user_profile: userProfile, entity_name: (submission as any).entity_name, park_name: (submission as any).park_name, reviewed_at: submission.reviewed_at, reviewed_by: submission.reviewer_id, reviewer_notes: submission.reviewer_notes, - reviewer_profile: submission.reviewer_id ? profileMap.get(submission.reviewer_id) : undefined, - })), - ]; + reviewer_profile: reviewerProfile, + }; + + newMemo.set(memoKey, newItem); + formattedItems.push(newItem); + } + + // Update memo cache + setSubmissionMemo(newMemo); // Sort by creation date (newest first) with stable secondary sort by ID formattedItems.sort((a, b) => { diff --git a/src/lib/smartStateUpdate.ts b/src/lib/smartStateUpdate.ts index a0e6846a..3a1e60d4 100644 --- a/src/lib/smartStateUpdate.ts +++ b/src/lib/smartStateUpdate.ts @@ -9,7 +9,12 @@ function hashContent(obj: any): string { if (obj === null || obj === undefined) return 'null'; if (typeof obj !== 'object') return String(obj); - // Sort keys for stable hashing + // Handle arrays + if (Array.isArray(obj)) { + return `[${obj.map(hashContent).join(',')}]`; + } + + // Sort keys for stable hashing (CRITICAL for nested objects!) const sortedKeys = Object.keys(obj).sort(); const parts = sortedKeys.map(key => `${key}:${hashContent(obj[key])}`); return parts.join('|');