This commit is contained in:
pacnpal
2025-10-04 14:34:37 +00:00
97 changed files with 6202 additions and 1347 deletions

View File

@@ -115,6 +115,17 @@ export function useAdminSettings() {
return value === true || value === 'true';
};
const getAdminPanelRefreshMode = () => {
const value = getSettingValue('system.admin_panel_refresh_mode', 'auto');
// Remove quotes if they exist (JSON string stored in DB)
return typeof value === 'string' ? value.replace(/"/g, '') : value;
};
const getAdminPanelPollInterval = () => {
const value = getSettingValue('system.admin_panel_poll_interval', 30);
return parseInt(value?.toString() || '30') * 1000; // Convert to milliseconds
};
return {
settings,
isLoading,
@@ -132,5 +143,7 @@ export function useAdminSettings() {
getReportThreshold,
getAuditRetentionDays,
getAutoCleanupEnabled,
getAdminPanelRefreshMode,
getAdminPanelPollInterval,
};
}

View File

@@ -81,8 +81,6 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
currentEmail !== previousEmail &&
!newEmailPending
) {
console.log('Email change confirmed:', { from: previousEmail, to: currentEmail });
// Defer Novu update and notifications to avoid blocking auth
setTimeout(async () => {
try {

View File

@@ -0,0 +1,113 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { supabase } from '@/integrations/supabase/client';
interface ModerationStats {
pendingSubmissions: number;
openReports: number;
flaggedContent: number;
}
interface UseModerationStatsOptions {
onStatsChange?: (stats: ModerationStats) => void;
enabled?: boolean;
pollingEnabled?: boolean;
pollingInterval?: number;
}
export const useModerationStats = (options: UseModerationStatsOptions = {}) => {
const {
onStatsChange,
enabled = true,
pollingEnabled = true,
pollingInterval = 30000 // Default 30 seconds
} = options;
const [stats, setStats] = useState<ModerationStats>({
pendingSubmissions: 0,
openReports: 0,
flaggedContent: 0,
});
const [isLoading, setIsLoading] = useState(true);
const [isInitialLoad, setIsInitialLoad] = useState(true);
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
const onStatsChangeRef = useRef(onStatsChange);
// Update ref when callback changes
useEffect(() => {
onStatsChangeRef.current = onStatsChange;
}, [onStatsChange]);
const fetchStats = useCallback(async (silent = false) => {
if (!enabled) return;
try {
// Only show loading on initial load
if (!silent) {
setIsLoading(true);
}
const [submissionsResult, reportsResult, reviewsResult] = await Promise.all([
supabase
.from('content_submissions')
.select('id', { count: 'exact', head: true })
.eq('status', 'pending'),
supabase
.from('reports')
.select('id', { count: 'exact', head: true })
.eq('status', 'pending'),
supabase
.from('reviews')
.select('id', { count: 'exact', head: true })
.eq('moderation_status', 'flagged'),
]);
const newStats = {
pendingSubmissions: submissionsResult.count || 0,
openReports: reportsResult.count || 0,
flaggedContent: reviewsResult.count || 0,
};
setStats(newStats);
setLastUpdated(new Date());
onStatsChangeRef.current?.(newStats);
} catch (error) {
console.error('Error fetching moderation stats:', error);
} finally {
// Only clear loading if it was set
if (!silent) {
setIsLoading(false);
}
if (isInitialLoad) {
setIsInitialLoad(false);
}
}
}, [enabled, isInitialLoad]);
// Initial fetch
useEffect(() => {
if (enabled) {
fetchStats(false); // Show loading
}
}, [enabled, fetchStats]);
// Polling
useEffect(() => {
if (!enabled || !pollingEnabled || isInitialLoad) return;
const interval = setInterval(() => {
fetchStats(true); // Silent refresh
}, pollingInterval);
return () => {
clearInterval(interval);
};
}, [enabled, pollingEnabled, pollingInterval, fetchStats, isInitialLoad]);
return {
stats,
refresh: fetchStats,
isLoading,
lastUpdated
};
};

View File

@@ -1,131 +0,0 @@
import { useEffect, useState, useRef, useCallback } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { RealtimeChannel } from '@supabase/supabase-js';
interface ModerationStats {
pendingSubmissions: number;
openReports: number;
flaggedContent: number;
}
interface UseRealtimeModerationStatsOptions {
onStatsChange?: (stats: ModerationStats) => void;
enabled?: boolean;
debounceMs?: number;
}
export const useRealtimeModerationStats = (options: UseRealtimeModerationStatsOptions = {}) => {
const { onStatsChange, enabled = true, debounceMs = 1000 } = options;
const [stats, setStats] = useState<ModerationStats>({
pendingSubmissions: 0,
openReports: 0,
flaggedContent: 0,
});
const [channel, setChannel] = useState<RealtimeChannel | null>(null);
const updateTimerRef = useRef<NodeJS.Timeout | null>(null);
const onStatsChangeRef = useRef(onStatsChange);
// Update ref when callback changes
useEffect(() => {
onStatsChangeRef.current = onStatsChange;
}, [onStatsChange]);
const fetchStats = useCallback(async () => {
try {
const [submissionsResult, reportsResult, reviewsResult] = await Promise.all([
supabase
.from('content_submissions')
.select('id', { count: 'exact', head: true })
.eq('status', 'pending'),
supabase
.from('reports')
.select('id', { count: 'exact', head: true })
.eq('status', 'pending'),
supabase
.from('reviews')
.select('id', { count: 'exact', head: true })
.eq('moderation_status', 'flagged'),
]);
const newStats = {
pendingSubmissions: submissionsResult.count || 0,
openReports: reportsResult.count || 0,
flaggedContent: reviewsResult.count || 0,
};
setStats(newStats);
onStatsChangeRef.current?.(newStats);
} catch (error) {
console.error('Error fetching moderation stats:', error);
}
}, []);
const debouncedFetchStats = useCallback(() => {
if (updateTimerRef.current) {
clearTimeout(updateTimerRef.current);
}
updateTimerRef.current = setTimeout(fetchStats, debounceMs);
}, [fetchStats, debounceMs]);
useEffect(() => {
if (!enabled) return;
// Initial fetch
fetchStats();
// Set up realtime subscriptions
const realtimeChannel = supabase
.channel('moderation-stats-changes')
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'content_submissions',
},
() => {
console.log('Content submissions changed');
debouncedFetchStats();
}
)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'reports',
},
() => {
console.log('Reports changed');
debouncedFetchStats();
}
)
.on(
'postgres_changes',
{
event: '*',
schema: 'public',
table: 'reviews',
},
() => {
console.log('Reviews changed');
debouncedFetchStats();
}
)
.subscribe((status) => {
console.log('Moderation stats realtime status:', status);
});
setChannel(realtimeChannel);
return () => {
console.log('Cleaning up moderation stats realtime subscription');
if (updateTimerRef.current) {
clearTimeout(updateTimerRef.current);
}
supabase.removeChannel(realtimeChannel);
};
}, [enabled, fetchStats, debouncedFetchStats]);
return { stats, refresh: fetchStats };
};

View File

@@ -1,54 +0,0 @@
import { useEffect, useState, useRef } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { RealtimeChannel } from '@supabase/supabase-js';
interface UseRealtimeSubmissionItemsOptions {
submissionId?: string;
onUpdate?: (payload: any) => void;
enabled?: boolean;
}
export const useRealtimeSubmissionItems = (options: UseRealtimeSubmissionItemsOptions = {}) => {
const { submissionId, onUpdate, enabled = true } = options;
const [channel, setChannel] = useState<RealtimeChannel | null>(null);
// Use ref to store latest callback without triggering re-subscriptions
const onUpdateRef = useRef(onUpdate);
// Update ref when callback changes
useEffect(() => {
onUpdateRef.current = onUpdate;
}, [onUpdate]);
useEffect(() => {
if (!enabled || !submissionId) return;
const realtimeChannel = supabase
.channel(`submission-items-${submissionId}`)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'submission_items',
filter: `submission_id=eq.${submissionId}`,
},
(payload) => {
console.log('Submission item updated:', payload);
onUpdateRef.current?.(payload);
}
)
.subscribe((status) => {
console.log('Submission items realtime status:', status);
});
setChannel(realtimeChannel);
return () => {
console.log('Cleaning up submission items realtime subscription');
supabase.removeChannel(realtimeChannel);
};
}, [submissionId, enabled]);
return { channel };
};

View File

@@ -1,82 +0,0 @@
import { useEffect, useState, useRef } from 'react';
import { supabase } from '@/integrations/supabase/client';
import { RealtimeChannel } from '@supabase/supabase-js';
interface UseRealtimeSubmissionsOptions {
onInsert?: (payload: any) => void;
onUpdate?: (payload: any) => void;
onDelete?: (payload: any) => void;
enabled?: boolean;
}
export const useRealtimeSubmissions = (options: UseRealtimeSubmissionsOptions = {}) => {
const { onInsert, onUpdate, onDelete, enabled = true } = options;
const [channel, setChannel] = useState<RealtimeChannel | null>(null);
// Use refs to store latest callbacks without triggering re-subscriptions
const onInsertRef = useRef(onInsert);
const onUpdateRef = useRef(onUpdate);
const onDeleteRef = useRef(onDelete);
// Update refs when callbacks change
useEffect(() => {
onInsertRef.current = onInsert;
onUpdateRef.current = onUpdate;
onDeleteRef.current = onDelete;
}, [onInsert, onUpdate, onDelete]);
useEffect(() => {
if (!enabled) return;
const realtimeChannel = supabase
.channel('content-submissions-changes')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'content_submissions',
},
(payload) => {
console.log('Submission inserted:', payload);
onInsertRef.current?.(payload);
}
)
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'content_submissions',
},
(payload) => {
console.log('Submission updated:', payload);
onUpdateRef.current?.(payload);
}
)
.on(
'postgres_changes',
{
event: 'DELETE',
schema: 'public',
table: 'content_submissions',
},
(payload) => {
console.log('Submission deleted:', payload);
onDeleteRef.current?.(payload);
}
)
.subscribe((status) => {
console.log('Submissions realtime status:', status);
});
setChannel(realtimeChannel);
return () => {
console.log('Cleaning up submissions realtime subscription');
supabase.removeChannel(realtimeChannel);
};
}, [enabled]);
return { channel };
};

View File

@@ -109,6 +109,7 @@ export function useSearch(options: UseSearchOptions = {}) {
subtitle: `at ${ride.park?.name || 'Unknown Park'}`,
image: ride.image_url,
rating: ride.average_rating,
slug: ride.slug,
data: ride
});
});
@@ -129,6 +130,7 @@ export function useSearch(options: UseSearchOptions = {}) {
title: company.name,
subtitle: company.company_type?.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()) || 'Company',
image: company.logo_url,
slug: company.slug,
data: company
});
});