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 [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,51 +395,94 @@ 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 => {
|
// Update cache asynchronously for next refresh
|
||||||
const existing = newCache.get(p.user_id);
|
setProfileCache(new Map(profileMap));
|
||||||
// Only update if data actually changed
|
|
||||||
if (!existing || JSON.stringify(existing) !== JSON.stringify(p)) {
|
// Phase 3 & 5: Normalize and memoize submissions
|
||||||
newCache.set(p.user_id, p);
|
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;
|
|
||||||
});
|
// Check memo first
|
||||||
|
const existing = newMemo.get(memoKey);
|
||||||
const profileMap = profileCache;
|
if (existing) {
|
||||||
|
formattedItems.push(existing);
|
||||||
// Combine and format items
|
continue;
|
||||||
const formattedItems: ModerationItem[] = [
|
}
|
||||||
...reviews.map(review => {
|
|
||||||
let entity_name = '';
|
// Create new item
|
||||||
let park_name = '';
|
const newItem: ModerationItem = {
|
||||||
|
id: review.id,
|
||||||
if ((review as any).rides) {
|
type: 'review' as const,
|
||||||
entity_name = (review as any).rides.name;
|
content: review,
|
||||||
park_name = (review as any).rides.parks?.name;
|
created_at: review.created_at,
|
||||||
} else if ((review as any).parks) {
|
user_id: review.user_id,
|
||||||
entity_name = (review as any).parks.name;
|
status: review.moderation_status,
|
||||||
}
|
user_profile: userProfile,
|
||||||
|
entity_name,
|
||||||
return {
|
park_name,
|
||||||
id: review.id,
|
reviewed_at: review.moderated_at,
|
||||||
type: 'review' as const,
|
reviewed_by: review.moderated_by,
|
||||||
content: review,
|
reviewer_notes: (review as any).reviewer_notes,
|
||||||
created_at: review.created_at,
|
reviewer_profile: reviewerProfile,
|
||||||
user_id: review.user_id,
|
};
|
||||||
status: review.moderation_status,
|
|
||||||
user_profile: profileMap.get(review.user_id),
|
newMemo.set(memoKey, newItem);
|
||||||
entity_name,
|
formattedItems.push(newItem);
|
||||||
park_name,
|
}
|
||||||
reviewed_at: review.moderated_at,
|
|
||||||
reviewed_by: review.moderated_by,
|
// Process submissions
|
||||||
reviewer_notes: (review as any).reviewer_notes,
|
for (const submission of submissions) {
|
||||||
reviewer_profile: review.moderated_by ? profileMap.get(review.moderated_by) : undefined,
|
const userProfile = profileMap.get(submission.user_id);
|
||||||
};
|
const reviewerProfile = submission.reviewer_id ? profileMap.get(submission.reviewer_id) : undefined;
|
||||||
}),
|
|
||||||
...submissions.map(submission => ({
|
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) => {
|
||||||
|
|||||||
@@ -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('|');
|
||||||
|
|||||||
Reference in New Issue
Block a user