mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-21 13:31:13 -05:00
Implement comprehensive fix
This commit is contained in:
@@ -81,6 +81,16 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
const [newItemsCount, setNewItemsCount] = useState(0);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
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 { isAdmin, isSuperuser } = useUserRole();
|
||||
const { user } = useAuth();
|
||||
@@ -294,11 +304,18 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((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,24 +395,23 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((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);
|
||||
}
|
||||
});
|
||||
return newCache;
|
||||
});
|
||||
// Phase 1: Use fresh data for THIS refresh, update cache for NEXT refresh
|
||||
const profileMap = new Map(profiles?.map(p => [p.user_id, p]) || []);
|
||||
|
||||
const profileMap = profileCache;
|
||||
// Update cache asynchronously for next refresh
|
||||
setProfileCache(new Map(profileMap));
|
||||
|
||||
// Combine and format items
|
||||
const formattedItems: ModerationItem[] = [
|
||||
...reviews.map(review => {
|
||||
// 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 = '';
|
||||
|
||||
@@ -406,23 +422,67 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
|
||||
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,
|
||||
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),
|
||||
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: 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,
|
||||
type: 'content_submission' as const,
|
||||
content: submission.submission_type === 'photo' ? submission.content : submission,
|
||||
@@ -430,15 +490,21 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((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) => {
|
||||
|
||||
@@ -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('|');
|
||||
|
||||
Reference in New Issue
Block a user