feat: Implement Modern React Patterns

This commit is contained in:
gpt-engineer-app[bot]
2025-10-13 22:40:35 +00:00
parent 2bd0b414e1
commit 787e16753e
5 changed files with 242 additions and 451 deletions

View File

@@ -8,6 +8,7 @@ import {
useModerationFilters,
usePagination,
useRealtimeSubscriptions,
useQueueQuery,
} from "./index";
import { useModerationQueue } from "@/hooks/useModerationQueue";
@@ -122,13 +123,9 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
// Refs for tracking
const recentlyRemovedRef = useRef<Set<string>>(new Set());
const fetchInProgressRef = useRef(false);
const lastFetchTimeRef = useRef<number>(0);
const initialFetchCompleteRef = useRef(false);
const isMountingRef = useRef(true);
const FETCH_COOLDOWN_MS = 1000;
// Store settings, filters, and pagination in refs to stabilize fetchItems
const settingsRef = useRef(settings);
const filtersRef = useRef(filters);
@@ -153,366 +150,70 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
}, [pagination]);
/**
* Fetch queue items from database
* Replace manual fetching with TanStack Query
*/
const fetchItems = useCallback(
async (silent = false) => {
if (!user) return;
const queueQuery = useQueueQuery({
userId: user?.id,
isAdmin,
isSuperuser,
entityFilter: filtersRef.current.debouncedEntityFilter,
statusFilter: filtersRef.current.debouncedStatusFilter,
tab: filtersRef.current.activeTab,
currentPage: paginationRef.current.currentPage,
pageSize: paginationRef.current.pageSize,
sortConfig: sortRef.current,
enabled: !!user,
});
// Get caller info
const callerStack = new Error().stack;
const callerLine = callerStack?.split("\n")[2]?.trim();
// Update items when query data changes
useEffect(() => {
if (queueQuery.items) {
setItems(queueQuery.items);
console.log('✅ Queue items updated from TanStack Query:', queueQuery.items.length);
}
}, [queueQuery.items]);
console.log("🔄 [FETCH ITEMS] Called", {
silent,
documentHidden: document.hidden,
caller: callerLine,
sortField: sortRef.current.field,
sortDirection: sortRef.current.direction,
timestamp: new Date().toISOString(),
});
// Update loading state based on query status
useEffect(() => {
if (queueQuery.isLoading) {
setLoadingState('loading');
} else if (queueQuery.isRefreshing) {
setLoadingState('refreshing');
} else {
setLoadingState('ready');
}
}, [queueQuery.isLoading, queueQuery.isRefreshing]);
// Prevent concurrent calls
if (fetchInProgressRef.current) {
console.log("⚠️ Fetch already in progress, skipping");
return;
}
// Update total count for pagination
useEffect(() => {
paginationRef.current.setTotalCount(queueQuery.totalCount);
}, [queueQuery.totalCount]);
// Cooldown check
const now = Date.now();
const timeSinceLastFetch = now - lastFetchTimeRef.current;
if (timeSinceLastFetch < FETCH_COOLDOWN_MS && lastFetchTimeRef.current > 0) {
console.log(`⏸️ Fetch cooldown active (${timeSinceLastFetch}ms)`);
return;
}
fetchInProgressRef.current = true;
lastFetchTimeRef.current = now;
console.log("🔍 fetchItems called:", {
entityFilter: filtersRef.current.debouncedEntityFilter,
statusFilter: filtersRef.current.debouncedStatusFilter,
sortField: sortRef.current.field,
sortDirection: sortRef.current.direction,
silent,
timestamp: new Date().toISOString(),
});
try {
// Set loading states
if (!silent) {
setLoadingState("loading");
} else {
setLoadingState("refreshing");
}
// Build base query
let submissionsQuery = supabase
.from("content_submissions")
.select(
`
id,
submission_type,
status,
content,
created_at,
user_id,
reviewed_at,
reviewer_id,
reviewer_notes,
escalated,
assigned_to,
locked_until,
submission_items (
id,
item_type,
item_data,
status
)
`,
);
// CRITICAL: Multi-level ordering
console.log('📊 [SORT QUERY] Applying multi-level sort:', {
level1: 'escalated DESC',
level2: `${sortRef.current.field} ${sortRef.current.direction.toUpperCase()}`,
level3: sortRef.current.field !== 'created_at' ? 'created_at ASC' : 'none'
});
// Level 1: Always sort by escalated first (descending)
submissionsQuery = submissionsQuery.order('escalated', { ascending: false });
// Level 2: Apply user-selected sort (use ref)
submissionsQuery = submissionsQuery.order(
sortRef.current.field,
{ ascending: sortRef.current.direction === 'asc' }
);
// Level 3: Tertiary sort by created_at (if not already primary)
if (sortRef.current.field !== 'created_at') {
submissionsQuery = submissionsQuery.order('created_at', { ascending: true });
}
// Apply tab-based status filtering (use refs)
const tab = filtersRef.current.activeTab;
const statusFilter = filtersRef.current.debouncedStatusFilter;
const entityFilter = filtersRef.current.debouncedEntityFilter;
if (tab === "mainQueue") {
if (statusFilter === "all") {
submissionsQuery = submissionsQuery.in("status", ["pending", "flagged", "partially_approved"]);
} else if (statusFilter === "pending") {
submissionsQuery = submissionsQuery.in("status", ["pending", "partially_approved"]);
} else {
submissionsQuery = submissionsQuery.eq("status", statusFilter);
}
} else {
if (statusFilter === "all") {
submissionsQuery = submissionsQuery.in("status", ["approved", "rejected"]);
} else {
submissionsQuery = submissionsQuery.eq("status", statusFilter);
}
}
// Apply entity type filter
if (entityFilter === "photos") {
submissionsQuery = submissionsQuery.eq("submission_type", "photo");
} else if (entityFilter === "submissions") {
submissionsQuery = submissionsQuery.neq("submission_type", "photo");
}
// Apply access control
if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString();
submissionsQuery = submissionsQuery.or(
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}`,
);
}
// Get total count - build separate query with same filters
let countQuery = supabase
.from("content_submissions")
.select("*", { count: "exact", head: true });
// Apply same filters as main query
if (tab === "mainQueue") {
if (statusFilter === "all") {
countQuery = countQuery.in("status", ["pending", "flagged", "partially_approved"]);
} else if (statusFilter === "pending") {
countQuery = countQuery.in("status", ["pending", "partially_approved"]);
} else {
countQuery = countQuery.eq("status", statusFilter);
}
} else {
if (statusFilter === "all") {
countQuery = countQuery.in("status", ["approved", "rejected"]);
} else {
countQuery = countQuery.eq("status", statusFilter);
}
}
if (entityFilter === "photos") {
countQuery = countQuery.eq("submission_type", "photo");
} else if (entityFilter === "submissions") {
countQuery = countQuery.neq("submission_type", "photo");
}
if (!isAdmin && !isSuperuser) {
const now = new Date().toISOString();
countQuery = countQuery.or(
`assigned_to.is.null,locked_until.lt.${now},assigned_to.eq.${user.id}`,
);
}
const { count } = await countQuery;
paginationRef.current.setTotalCount(count || 0);
// Apply pagination (use refs)
const startIndex = paginationRef.current.startIndex;
const endIndex = paginationRef.current.endIndex;
submissionsQuery = submissionsQuery.range(startIndex, endIndex);
const { data: submissions, error: submissionsError } = await submissionsQuery;
if (submissionsError) throw submissionsError;
// Log the actual data returned to verify sort order
if (submissions && submissions.length > 0) {
const sortField = sortRef.current.field;
const preview = submissions.slice(0, 3).map(s => ({
id: s.id.substring(0, 8),
[sortField]: s[sortField],
escalated: s.escalated
}));
console.log(`📋 [SORT RESULT] First 3 items by ${sortField}:`, preview);
}
// Fetch related profiles and entities
const userIds = [
...new Set([
...(submissions?.map((s) => s.user_id) || []),
...(submissions?.map((s) => s.reviewer_id).filter(Boolean) || []),
]),
];
if (userIds.length > 0) {
await profileCache.bulkFetch(userIds);
}
// Collect entity IDs
if (submissions) {
await entityCache.fetchRelatedEntities(submissions);
}
// Map to ModerationItems
const moderationItems: ModerationItem[] =
submissions?.map((submission) => {
const content = submission.content as any;
let entityName = content?.name || "Unknown";
let parkName: string | undefined;
// Resolve entity names from cache
if (submission.submission_type === "ride" && content?.entity_id) {
const ride = entityCache.getCached("rides", content.entity_id);
if (ride) {
entityName = ride.name;
if (ride.park_id) {
const park = entityCache.getCached("parks", ride.park_id);
if (park) parkName = park.name;
}
}
} else if (submission.submission_type === "park" && content?.entity_id) {
const park = entityCache.getCached("parks", content.entity_id);
if (park) entityName = park.name;
} else if (
["manufacturer", "operator", "designer", "property_owner"].includes(submission.submission_type) &&
content?.entity_id
) {
const company = entityCache.getCached("companies", content.entity_id);
if (company) entityName = company.name;
}
const userProfile = profileCache.getCached(submission.user_id);
const reviewerProfile = submission.reviewer_id ? profileCache.getCached(submission.reviewer_id) : undefined;
return {
id: submission.id,
type: "content_submission",
content: submission.content,
created_at: submission.created_at,
user_id: submission.user_id,
status: submission.status,
submission_type: submission.submission_type,
user_profile: userProfile,
entity_name: entityName,
park_name: parkName,
reviewed_at: submission.reviewed_at || undefined,
reviewed_by: submission.reviewer_id || undefined,
reviewer_notes: submission.reviewer_notes || undefined,
reviewer_profile: reviewerProfile,
escalated: submission.escalated,
assigned_to: submission.assigned_to || undefined,
locked_until: submission.locked_until || undefined,
submission_items: submission.submission_items || undefined,
};
}) || [];
// Apply refresh strategy
const currentRefreshStrategy = settingsRef.current.refreshStrategy;
const currentPreserveInteraction = settingsRef.current.preserveInteraction;
if (silent) {
// Background polling: detect new submissions
const currentDisplayedIds = new Set(items.map((item) => item.id));
const newSubmissions = moderationItems.filter((item) => !currentDisplayedIds.has(item.id));
if (newSubmissions.length > 0) {
console.log("🆕 Detected new submissions:", newSubmissions.length);
setPendingNewItems((prev) => {
const existingIds = new Set(prev.map((p) => p.id));
const uniqueNew = newSubmissions.filter((item) => !existingIds.has(item.id));
if (uniqueNew.length > 0) {
setNewItemsCount((prev) => prev + uniqueNew.length);
}
return [...prev, ...uniqueNew];
});
}
// Apply refresh strategy
switch (currentRefreshStrategy) {
case "notify":
console.log("✅ Queue frozen (notify mode)");
break;
case "merge":
if (newSubmissions.length > 0) {
const currentIds = new Set(items.map((item) => item.id));
const trulyNewSubmissions = newSubmissions.filter((item) => !currentIds.has(item.id));
if (trulyNewSubmissions.length > 0) {
setItems((prev) => [...prev, ...trulyNewSubmissions]);
console.log("🔀 Queue merged - added", trulyNewSubmissions.length, "items");
}
}
break;
case "replace":
// Full replace - simple and predictable
setItems(moderationItems);
console.log("🔄 Queue updated (replace mode)");
if (!currentPreserveInteraction) {
setPendingNewItems([]);
setNewItemsCount(0);
}
break;
}
} else {
// Manual refresh - ALWAYS update items for user-initiated changes (sort/filter)
// Don't use smartMerge here because it won't detect order changes
setItems(moderationItems);
console.log("🔄 Queue updated (manual refresh) - items:", moderationItems.length);
setPendingNewItems([]);
setNewItemsCount(0);
}
} catch (error: any) {
console.error("Error fetching moderation items:", error);
toast({
title: "Error",
description: error.message || "Failed to fetch moderation queue",
variant: "destructive",
});
} finally {
fetchInProgressRef.current = false;
setLoadingState("ready");
}
},
[
user,
isAdmin,
isSuperuser,
profileCache,
entityCache,
toast
],
);
// Mark initial fetch as complete
useEffect(() => {
if (!queueQuery.isLoading && !initialFetchCompleteRef.current) {
initialFetchCompleteRef.current = true;
console.log('✅ Initial queue fetch complete');
}
}, [queueQuery.isLoading]);
/**
* Show pending new items by merging them into the queue
* Manual refresh function
*/
const showNewItems = useCallback(() => {
if (pendingNewItems.length > 0) {
setItems((prev) => [...pendingNewItems, ...prev]);
setPendingNewItems([]);
setNewItemsCount(0);
console.log("✅ New items merged into queue:", pendingNewItems.length);
}
}, [pendingNewItems]);
const refresh = useCallback(async () => {
console.log('🔄 Manual refresh triggered');
await queueQuery.refetch();
}, [queueQuery]);
/**
* Show pending new items by invalidating query
*/
const showNewItems = useCallback(async () => {
console.log('✅ Showing new items via query invalidation');
await queueQuery.invalidate();
setPendingNewItems([]);
setNewItemsCount(0);
}, [queueQuery]);
/**
* Mark an item as being interacted with (prevents realtime updates)
@@ -808,51 +509,36 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
[filters.statusFilter, toast],
);
// Initial fetch on mount
// Mark initial fetch as complete when query loads
useEffect(() => {
if (!user) return;
isMountingRef.current = true;
fetchItems(false).then(() => {
if (!queueQuery.isLoading && !initialFetchCompleteRef.current) {
initialFetchCompleteRef.current = true;
requestAnimationFrame(() => {
isMountingRef.current = false;
});
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user?.id]);
isMountingRef.current = false;
console.log('✅ Initial queue fetch complete');
}
}, [queueQuery.isLoading]);
// Filter and sort changes trigger refetch
// Invalidate query when filters or sort changes
useEffect(() => {
if (!user || !initialFetchCompleteRef.current || isMountingRef.current) return;
console.log('🔄 [SORT/FILTER CHANGE] Detected change:', {
entityFilter: filters.debouncedEntityFilter,
statusFilter: filters.debouncedStatusFilter,
sortField: filters.debouncedSortConfig.field,
sortDirection: filters.debouncedSortConfig.direction,
timestamp: new Date().toISOString()
});
if (
!user ||
!initialFetchCompleteRef.current ||
isMountingRef.current
) return;
console.log('🔄 Filters/sort changed, invalidating query');
pagination.reset();
fetchItems(false);
queueQuery.invalidate();
}, [
filters.debouncedEntityFilter,
filters.debouncedStatusFilter,
filters.debouncedEntityFilter,
filters.debouncedStatusFilter,
filters.debouncedSortConfig.field,
filters.debouncedSortConfig.direction,
user,
fetchItems,
queueQuery,
pagination
]);
// Pagination changes trigger refetch
useEffect(() => {
if (!user || !initialFetchCompleteRef.current || pagination.currentPage === 1) return;
fetchItems(true);
}, [pagination.currentPage, pagination.pageSize, user, fetchItems]);
// Polling effect (when realtime disabled)
useEffect(() => {
if (!user || settings.refreshMode !== "auto" || loadingState === "initial" || settings.useRealtimeQueue) {
@@ -862,14 +548,14 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
console.log("⚠️ Polling ENABLED - interval:", settings.pollInterval);
const interval = setInterval(() => {
console.log("🔄 Polling refresh triggered");
fetchItems(true);
queueQuery.refetch();
}, settings.pollInterval);
return () => {
clearInterval(interval);
console.log("🛑 Polling stopped");
};
}, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue, fetchItems]);
}, [user, settings.refreshMode, settings.pollInterval, loadingState, settings.useRealtimeQueue, queueQuery]);
// Initialize realtime subscriptions
useRealtimeSubscriptions({
@@ -927,9 +613,7 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig):
showNewItems,
interactingWith,
markInteracting,
refresh: async () => {
await fetchItems(false);
},
refresh,
performAction,
deleteSubmission,
resetToPending,