mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 18:11:12 -05:00
feat: Implement Novu subscriber update
This commit is contained in:
@@ -12,6 +12,7 @@ export interface ModerationActionsConfig {
|
||||
user: User | null;
|
||||
onActionStart: (itemId: string) => void;
|
||||
onActionComplete: () => void;
|
||||
currentLockSubmissionId?: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,36 +46,70 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio
|
||||
try {
|
||||
// Handle photo submissions
|
||||
if (action === 'approved' && item.submission_type === 'photo') {
|
||||
const { data: photoSubmission } = await supabase
|
||||
const { data: photoSubmission, error: fetchError } = await supabase
|
||||
.from('photo_submissions')
|
||||
.select(`*, items:photo_submission_items(*), submission:content_submissions!inner(user_id)`)
|
||||
.select(`
|
||||
*,
|
||||
items:photo_submission_items(*),
|
||||
submission:content_submissions!inner(user_id)
|
||||
`)
|
||||
.eq('submission_id', item.id)
|
||||
.single();
|
||||
|
||||
if (photoSubmission && photoSubmission.items) {
|
||||
const { data: existingPhotos } = await supabase
|
||||
.from('photos')
|
||||
.select('id')
|
||||
.eq('submission_id', item.id);
|
||||
// Add explicit error handling
|
||||
if (fetchError) {
|
||||
throw new Error(`Failed to fetch photo submission: ${fetchError.message}`);
|
||||
}
|
||||
|
||||
if (!existingPhotos || existingPhotos.length === 0) {
|
||||
const photoRecords = photoSubmission.items.map((photoItem: any) => ({
|
||||
entity_id: photoSubmission.entity_id,
|
||||
entity_type: photoSubmission.entity_type,
|
||||
cloudflare_image_id: photoItem.cloudflare_image_id,
|
||||
cloudflare_image_url: photoItem.cloudflare_image_url,
|
||||
title: photoItem.title || null,
|
||||
caption: photoItem.caption || null,
|
||||
date_taken: photoItem.date_taken || null,
|
||||
order_index: photoItem.order_index,
|
||||
submission_id: photoSubmission.submission_id,
|
||||
submitted_by: photoSubmission.submission?.user_id,
|
||||
approved_by: user?.id,
|
||||
approved_at: new Date().toISOString(),
|
||||
}));
|
||||
if (!photoSubmission) {
|
||||
throw new Error('Photo submission not found');
|
||||
}
|
||||
|
||||
await supabase.from('photos').insert(photoRecords);
|
||||
}
|
||||
// Type assertion with validation
|
||||
const typedPhotoSubmission = photoSubmission as {
|
||||
id: string;
|
||||
entity_id: string;
|
||||
entity_type: string;
|
||||
items: Array<{
|
||||
id: string;
|
||||
cloudflare_image_id: string;
|
||||
cloudflare_image_url: string;
|
||||
caption?: string;
|
||||
title?: string;
|
||||
date_taken?: string;
|
||||
date_taken_precision?: string;
|
||||
order_index: number;
|
||||
}>;
|
||||
submission: { user_id: string };
|
||||
};
|
||||
|
||||
// Validate required fields
|
||||
if (!typedPhotoSubmission.items || typedPhotoSubmission.items.length === 0) {
|
||||
throw new Error('No photo items found in submission');
|
||||
}
|
||||
|
||||
const { data: existingPhotos } = await supabase
|
||||
.from('photos')
|
||||
.select('id')
|
||||
.eq('submission_id', item.id);
|
||||
|
||||
if (!existingPhotos || existingPhotos.length === 0) {
|
||||
const photoRecords = typedPhotoSubmission.items.map((photoItem) => ({
|
||||
entity_id: typedPhotoSubmission.entity_id,
|
||||
entity_type: typedPhotoSubmission.entity_type,
|
||||
cloudflare_image_id: photoItem.cloudflare_image_id,
|
||||
cloudflare_image_url: photoItem.cloudflare_image_url,
|
||||
title: photoItem.title || null,
|
||||
caption: photoItem.caption || null,
|
||||
date_taken: photoItem.date_taken || null,
|
||||
order_index: photoItem.order_index,
|
||||
submission_id: item.id,
|
||||
submitted_by: typedPhotoSubmission.submission?.user_id,
|
||||
approved_by: user?.id,
|
||||
approved_at: new Date().toISOString(),
|
||||
}));
|
||||
|
||||
await supabase.from('photos').insert(photoRecords);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,8 +87,10 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
||||
|
||||
// Start countdown timer for lock expiry
|
||||
const startLockTimer = useCallback((expiresAt: Date) => {
|
||||
// Clear any existing timer first to prevent leaks
|
||||
if (lockTimerRef.current) {
|
||||
clearInterval(lockTimerRef.current);
|
||||
lockTimerRef.current = null;
|
||||
}
|
||||
|
||||
lockTimerRef.current = setInterval(() => {
|
||||
@@ -96,93 +98,39 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
||||
const timeLeft = expiresAt.getTime() - now.getTime();
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
// Lock expired
|
||||
setCurrentLock(null);
|
||||
// Clear timer before showing toast to prevent double-firing
|
||||
if (lockTimerRef.current) {
|
||||
clearInterval(lockTimerRef.current);
|
||||
lockTimerRef.current = null;
|
||||
}
|
||||
|
||||
setCurrentLock(null);
|
||||
|
||||
toast({
|
||||
title: 'Lock Expired',
|
||||
description: 'Your submission lock has expired',
|
||||
description: 'Your review lock has expired. Claim another submission to continue.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
|
||||
if (onLockStateChange) {
|
||||
onLockStateChange();
|
||||
}
|
||||
}
|
||||
}, 1000); // Check every second
|
||||
}, [toast]);
|
||||
}, 1000);
|
||||
}, [toast, onLockStateChange]); // Add dependencies to avoid stale closures
|
||||
|
||||
// Clean up timer on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Comprehensive cleanup on unmount
|
||||
if (lockTimerRef.current) {
|
||||
clearInterval(lockTimerRef.current);
|
||||
lockTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Claim next submission from queue
|
||||
const claimNext = useCallback(async (): Promise<string | null> => {
|
||||
if (!user?.id) {
|
||||
toast({
|
||||
title: 'Authentication Required',
|
||||
description: 'You must be logged in to claim submissions',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const { data, error } = await supabase.rpc('claim_next_submission', {
|
||||
moderator_id: user.id,
|
||||
lock_duration: '15 minutes',
|
||||
});
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
toast({
|
||||
title: 'Queue Empty',
|
||||
description: 'No submissions available to review',
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const claimed = data[0] as QueuedSubmission;
|
||||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
|
||||
|
||||
setCurrentLock({
|
||||
submissionId: claimed.submission_id,
|
||||
expiresAt,
|
||||
});
|
||||
|
||||
startLockTimer(expiresAt);
|
||||
fetchStats();
|
||||
|
||||
toast({
|
||||
title: 'Submission Claimed',
|
||||
description: `${claimed.submission_type} submission (waiting ${formatInterval(claimed.waiting_time)})`,
|
||||
});
|
||||
|
||||
// Trigger refresh callback
|
||||
if (onLockStateChange) {
|
||||
onLockStateChange();
|
||||
}
|
||||
|
||||
return claimed.submission_id;
|
||||
} catch (error: any) {
|
||||
console.error('Error claiming submission:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message || 'Failed to claim submission',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [user, toast, startLockTimer, fetchStats]);
|
||||
|
||||
// Extend current lock
|
||||
// Claim a specific submission (CRM-style claim any)
|
||||
const extendLock = useCallback(async (submissionId: string): Promise<boolean> => {
|
||||
if (!user?.id) return false;
|
||||
|
||||
@@ -248,6 +196,7 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
||||
|
||||
if (lockTimerRef.current) {
|
||||
clearInterval(lockTimerRef.current);
|
||||
lockTimerRef.current = null; // Explicitly null it out
|
||||
}
|
||||
|
||||
fetchStats();
|
||||
@@ -341,6 +290,13 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
// Get submission details FIRST for better toast message
|
||||
const { data: submission } = await supabase
|
||||
.from('content_submissions')
|
||||
.select('submission_type')
|
||||
.eq('id', submissionId)
|
||||
.single();
|
||||
|
||||
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
|
||||
|
||||
const { error } = await supabase
|
||||
@@ -361,14 +317,17 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
||||
});
|
||||
|
||||
startLockTimer(expiresAt);
|
||||
fetchStats();
|
||||
|
||||
// Enhanced toast with submission type
|
||||
const submissionType = submission?.submission_type || 'submission';
|
||||
toast({
|
||||
title: 'Submission Claimed',
|
||||
description: 'You now have 15 minutes to review this submission',
|
||||
title: '✅ Submission Claimed',
|
||||
description: `${submissionType.charAt(0).toUpperCase() + submissionType.slice(1)} locked for 15 minutes. Start reviewing now.`,
|
||||
duration: 4000,
|
||||
});
|
||||
|
||||
// Trigger refresh callback
|
||||
// Force UI refresh to update queue
|
||||
fetchStats();
|
||||
if (onLockStateChange) {
|
||||
onLockStateChange();
|
||||
}
|
||||
@@ -377,15 +336,15 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
||||
} catch (error: any) {
|
||||
console.error('Error claiming submission:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message || 'Failed to claim submission',
|
||||
title: 'Failed to Claim Submission',
|
||||
description: error.message || 'Could not claim this submission. Try again.',
|
||||
variant: 'destructive',
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
setIsLoading(false); // Always clear loading state
|
||||
}
|
||||
}, [user, toast, startLockTimer, fetchStats]);
|
||||
}, [user, toast, startLockTimer, fetchStats, onLockStateChange]);
|
||||
|
||||
// Reassign submission
|
||||
const reassignSubmission = useCallback(async (submissionId: string, newModeratorId: string): Promise<boolean> => {
|
||||
@@ -474,7 +433,6 @@ export const useModerationQueue = (config?: UseModerationQueueConfig) => {
|
||||
currentLock, // Exposed for reactive UI updates
|
||||
queueStats,
|
||||
isLoading,
|
||||
claimNext,
|
||||
claimSubmission,
|
||||
extendLock,
|
||||
releaseLock,
|
||||
|
||||
Reference in New Issue
Block a user