feat: Implement Novu subscriber update

This commit is contained in:
gpt-engineer-app[bot]
2025-10-15 16:49:58 +00:00
parent fca235269f
commit 4b697fe45a
6 changed files with 128 additions and 118 deletions

View File

@@ -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);
}
}

View File

@@ -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,