Implement comprehensive fix

This commit is contained in:
gpt-engineer-app[bot]
2025-10-06 19:38:39 +00:00
parent d965d2c299
commit fd20c02e5e
2 changed files with 121 additions and 50 deletions

View File

@@ -81,6 +81,16 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const [newItemsCount, setNewItemsCount] = useState(0); const [newItemsCount, setNewItemsCount] = useState(0);
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [profileCache, setProfileCache] = useState<Map<string, any>>(new Map()); const [profileCache, setProfileCache] = useState<Map<string, any>>(new Map());
const [entityCache, setEntityCache] = useState<{
rides: Map<string, any>,
parks: Map<string, any>,
companies: Map<string, any>
}>({
rides: new Map(),
parks: new Map(),
companies: new Map()
});
const [submissionMemo, setSubmissionMemo] = useState<Map<string, ModerationItem>>(new Map());
const { toast } = useToast(); const { toast } = useToast();
const { isAdmin, isSuperuser } = useUserRole(); const { isAdmin, isSuperuser } = useUserRole();
const { user } = useAuth(); const { user } = useAuth();
@@ -294,11 +304,18 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
.then(res => res.data || []) : Promise.resolve([]) .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 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 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])); 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 // Second pass: attach entity data using maps
let submissionsWithEntities = (submissionsData || []).map(submission => { let submissionsWithEntities = (submissionsData || []).map(submission => {
if (submission.submission_type === 'photo' && submission.content && typeof submission.content === 'object') { if (submission.submission_type === 'photo' && submission.content && typeof submission.content === 'object') {
@@ -378,24 +395,23 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
.select('user_id, username, display_name, avatar_url') .select('user_id, username, display_name, avatar_url')
.in('user_id', userIds); .in('user_id', userIds);
// Update profile cache with stable references // Phase 1: Use fresh data for THIS refresh, update cache for NEXT refresh
setProfileCache(prevCache => { const profileMap = new Map(profiles?.map(p => [p.user_id, p]) || []);
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);
}
});
return newCache;
});
const profileMap = profileCache; // Update cache asynchronously for next refresh
setProfileCache(new Map(profileMap));
// Combine and format items // Phase 3 & 5: Normalize and memoize submissions
const formattedItems: ModerationItem[] = [ const formattedItems: ModerationItem[] = [];
...reviews.map(review => { 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 entity_name = '';
let park_name = ''; let park_name = '';
@@ -406,23 +422,67 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
entity_name = (review as any).parks.name; entity_name = (review as any).parks.name;
} }
return { 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
});
// Check memo first
const existing = newMemo.get(memoKey);
if (existing) {
formattedItems.push(existing);
continue;
}
// Create new item
const newItem: ModerationItem = {
id: review.id, id: review.id,
type: 'review' as const, type: 'review' as const,
content: review, content: review,
created_at: review.created_at, created_at: review.created_at,
user_id: review.user_id, user_id: review.user_id,
status: review.moderation_status, status: review.moderation_status,
user_profile: profileMap.get(review.user_id), user_profile: userProfile,
entity_name, entity_name,
park_name, park_name,
reviewed_at: review.moderated_at, reviewed_at: review.moderated_at,
reviewed_by: review.moderated_by, reviewed_by: review.moderated_by,
reviewer_notes: (review as any).reviewer_notes, reviewer_notes: (review as any).reviewer_notes,
reviewer_profile: review.moderated_by ? profileMap.get(review.moderated_by) : undefined, reviewer_profile: reviewerProfile,
}; };
}),
...submissions.map(submission => ({ 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, id: submission.id,
type: 'content_submission' as const, type: 'content_submission' as const,
content: submission.submission_type === 'photo' ? submission.content : submission, content: submission.submission_type === 'photo' ? submission.content : submission,
@@ -430,15 +490,21 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
user_id: submission.user_id, user_id: submission.user_id,
status: submission.status, status: submission.status,
submission_type: submission.submission_type, submission_type: submission.submission_type,
user_profile: profileMap.get(submission.user_id), user_profile: userProfile,
entity_name: (submission as any).entity_name, entity_name: (submission as any).entity_name,
park_name: (submission as any).park_name, park_name: (submission as any).park_name,
reviewed_at: submission.reviewed_at, reviewed_at: submission.reviewed_at,
reviewed_by: submission.reviewer_id, reviewed_by: submission.reviewer_id,
reviewer_notes: submission.reviewer_notes, 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 // Sort by creation date (newest first) with stable secondary sort by ID
formattedItems.sort((a, b) => { formattedItems.sort((a, b) => {

View File

@@ -9,7 +9,12 @@ function hashContent(obj: any): string {
if (obj === null || obj === undefined) return 'null'; if (obj === null || obj === undefined) return 'null';
if (typeof obj !== 'object') return String(obj); 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 sortedKeys = Object.keys(obj).sort();
const parts = sortedKeys.map(key => `${key}:${hashContent(obj[key])}`); const parts = sortedKeys.map(key => `${key}:${hashContent(obj[key])}`);
return parts.join('|'); return parts.join('|');