Refactor moderation queues

This commit is contained in:
gpt-engineer-app[bot]
2025-10-03 18:40:34 +00:00
parent e6238c45b3
commit a2d3ed5ea4
8 changed files with 62 additions and 629 deletions

View File

@@ -14,12 +14,11 @@ import { useAuth } from '@/hooks/useAuth';
import { format } from 'date-fns';
import { PhotoModal } from './PhotoModal';
import { SubmissionReviewManager } from './SubmissionReviewManager';
import { useRealtimeSubmissions } from '@/hooks/useRealtimeSubmissions';
import { useIsMobile } from '@/hooks/use-mobile';
import { SubmissionChangesDisplay } from './SubmissionChangesDisplay';
import { SubmissionItemsList } from './SubmissionItemsList';
import { RealtimeConnectionStatus } from './RealtimeConnectionStatus';
import { MeasurementDisplay } from '@/components/ui/measurement-display';
import { useAdminSettings } from '@/hooks/useAdminSettings';
interface ModerationItem {
id: string;
@@ -70,6 +69,11 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
const { isAdmin, isSuperuser } = useUserRole();
const { user } = useAuth();
// Get admin settings for polling configuration
const { getAdminPanelRefreshMode, getAdminPanelPollInterval } = useAdminSettings();
const refreshMode = getAdminPanelRefreshMode();
const pollInterval = getAdminPanelPollInterval();
// Expose refresh method via ref
useImperativeHandle(ref, () => ({
refresh: () => {
@@ -346,116 +350,26 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
}
};
// Set up realtime subscriptions
const { connectionState: submissionsConnectionState, reconnect: reconnectSubmissions } = useRealtimeSubmissions({
onInsert: async (payload) => {
const newSubmission = payload.new;
// Only add if it matches current filters
const matchesStatusFilter =
activeStatusFilter === 'all' ||
(activeStatusFilter === 'pending' && (newSubmission.status === 'pending' || newSubmission.status === 'partially_approved')) ||
activeStatusFilter === newSubmission.status;
const matchesEntityFilter =
activeEntityFilter === 'all' ||
(activeEntityFilter === 'submissions' && newSubmission.submission_type !== 'photo') ||
(activeEntityFilter === 'photos' && newSubmission.submission_type === 'photo');
if (!matchesStatusFilter || !matchesEntityFilter) return;
// Fetch minimal data for the new submission
try {
const { data: profile } = await supabase
.from('profiles')
.select('user_id, username, display_name, avatar_url')
.eq('user_id', newSubmission.user_id)
.single();
// Fetch entity name if photo submission
let entity_name, park_name;
if (newSubmission.submission_type === 'photo' && newSubmission.content) {
const contentObj = newSubmission.content as any;
const contextType = typeof contentObj.context === 'string' ? contentObj.context : null;
const entityId = contentObj.entity_id || contentObj.ride_id || contentObj.park_id || contentObj.company_id;
if (contextType === 'ride' && entityId) {
const { data: rideData } = await supabase
.from('rides')
.select('name, parks:park_id(name)')
.eq('id', entityId)
.single();
if (rideData) {
entity_name = rideData.name;
park_name = rideData.parks?.name;
}
} else if (contextType === 'park' && entityId) {
const { data: parkData } = await supabase
.from('parks')
.select('name')
.eq('id', entityId)
.single();
if (parkData) entity_name = parkData.name;
} else if (['manufacturer', 'operator', 'designer', 'property_owner'].includes(contextType) && entityId) {
const { data: companyData } = await supabase
.from('companies')
.select('name')
.eq('id', entityId)
.single();
if (companyData) entity_name = companyData.name;
}
}
// Create new item and prepend to list
const newItem: ModerationItem = {
id: newSubmission.id,
type: 'content_submission',
content: newSubmission.submission_type === 'photo' ? newSubmission.content : newSubmission,
created_at: newSubmission.created_at,
user_id: newSubmission.user_id,
status: newSubmission.status,
submission_type: newSubmission.submission_type,
user_profile: profile || undefined,
entity_name,
park_name,
};
setItems(prevItems => [newItem, ...prevItems]);
toast({
title: 'New Submission',
description: 'A new content submission has been added',
});
} catch (error) {
console.error('Error adding new submission to queue:', error);
// Fallback to full refresh on error
fetchItems(activeEntityFilter, activeStatusFilter);
}
},
onUpdate: (payload) => {
// Update items state directly for better UX
setItems(prevItems =>
prevItems.map(item =>
item.id === payload.new.id && item.type === 'content_submission'
? { ...item, status: payload.new.status, content: { ...item.content, ...payload.new } }
: item
)
);
},
onDelete: (payload) => {
setItems(prevItems =>
prevItems.filter(item => !(item.id === payload.old.id && item.type === 'content_submission'))
);
},
enabled: !!user,
});
// Initial fetch on mount and filter changes
useEffect(() => {
if (user) {
fetchItems(activeEntityFilter, activeStatusFilter);
}
}, [activeEntityFilter, activeStatusFilter, user]);
// Polling for auto-refresh
useEffect(() => {
if (!user || refreshMode !== 'auto') return;
const interval = setInterval(() => {
fetchItems(activeEntityFilter, activeStatusFilter);
}, pollInterval);
return () => {
clearInterval(interval);
};
}, [user, refreshMode, pollInterval, activeEntityFilter, activeStatusFilter]);
const handleResetToPending = async (item: ModerationItem) => {
setActionLoading(item.id);
try {
@@ -1765,10 +1679,6 @@ export const ModerationQueue = forwardRef<ModerationQueueRef>((props, ref) => {
<div className={`flex flex-col gap-4 bg-muted/50 rounded-lg ${isMobile ? 'p-3' : 'p-4 sm:flex-row'}`}>
<div className="flex items-center justify-between w-full mb-2 pb-2 border-b border-border">
<h3 className="text-sm font-medium text-muted-foreground">Moderation Queue</h3>
<RealtimeConnectionStatus
connectionState={submissionsConnectionState}
onReconnect={reconnectSubmissions}
/>
</div>
<div className={`flex gap-4 flex-1 ${isMobile ? 'flex-col' : 'flex-col sm:flex-row'}`}>
<div className={`space-y-2 ${isMobile ? 'w-full' : 'min-w-[140px]'}`}>