From bee6e9e50b6bc91dce06041e27c4e89cdaf99436 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Thu, 30 Oct 2025 23:28:55 +0000 Subject: [PATCH] Fix Phase 4 API optimization --- src/components/upload/EntityPhotoGallery.tsx | 49 +--- src/pages/Profile.tsx | 243 ++----------------- 2 files changed, 26 insertions(+), 266 deletions(-) diff --git a/src/components/upload/EntityPhotoGallery.tsx b/src/components/upload/EntityPhotoGallery.tsx index f36739db..e9943686 100644 --- a/src/components/upload/EntityPhotoGallery.tsx +++ b/src/components/upload/EntityPhotoGallery.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useState } from 'react'; import { Camera, Upload, LogIn, Settings, ArrowUpDown } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Card, CardContent } from '@/components/ui/card'; @@ -14,11 +14,9 @@ import { useNavigate } from 'react-router-dom'; import { UppyPhotoSubmissionUpload } from '@/components/upload/UppyPhotoSubmissionUpload'; import { PhotoManagementDialog } from '@/components/upload/PhotoManagementDialog'; import { PhotoModal } from '@/components/moderation/PhotoModal'; -import { supabase } from '@/integrations/supabase/client'; import { EntityPhotoGalleryProps } from '@/types/submissions'; import { useUserRole } from '@/hooks/useUserRole'; -import { getErrorMessage } from '@/lib/errorHandler'; -import { logger } from '@/lib/logger'; +import { useEntityPhotos } from '@/hooks/photos/useEntityPhotos'; interface Photo { id: string; @@ -38,47 +36,18 @@ export function EntityPhotoGallery({ const { user } = useAuth(); const navigate = useNavigate(); const { isModerator } = useUserRole(); - const [photos, setPhotos] = useState([]); const [showUpload, setShowUpload] = useState(false); const [showManagement, setShowManagement] = useState(false); - const [loading, setLoading] = useState(true); const [selectedPhotoIndex, setSelectedPhotoIndex] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [sortBy, setSortBy] = useState<'newest' | 'oldest'>('newest'); - useEffect(() => { - fetchPhotos(); - }, [entityId, entityType, sortBy]); - - const fetchPhotos = async () => { - try { - // Fetch photos directly from the photos table - const { data: photoData, error } = await supabase - .from('photos') - .select('id, cloudflare_image_url, title, caption, submitted_by, created_at, order_index') - .eq('entity_type', entityType) - .eq('entity_id', entityId) - .order('created_at', { ascending: sortBy === 'oldest' }); - - if (error) throw error; - - // Map to Photo interface - const mappedPhotos: Photo[] = photoData?.map((photo) => ({ - id: photo.id, - url: photo.cloudflare_image_url, - caption: photo.caption || undefined, - title: photo.title || undefined, - user_id: photo.submitted_by, - created_at: photo.created_at, - })) || []; - - setPhotos(mappedPhotos); - } catch (error: unknown) { - logger.error('Failed to fetch photos', { error: getErrorMessage(error), entityId, entityType }); - } finally { - setLoading(false); - } - }; + // Use optimized photos hook with caching + const { data: photos = [], isLoading: loading, refetch } = useEntityPhotos( + entityType, + entityId, + sortBy + ); const handleUploadClick = () => { if (!user) { @@ -90,7 +59,7 @@ export function EntityPhotoGallery({ const handleSubmissionComplete = () => { setShowUpload(false); - fetchPhotos(); // Refresh photos after submission + refetch(); // Refresh photos after submission }; const handlePhotoClick = (index: number) => { diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 118d7495..9cc76684 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -32,6 +32,8 @@ import { PersonalLocationDisplay } from '@/components/profile/PersonalLocationDi import { useUserRole } from '@/hooks/useUserRole'; import { useDocumentTitle } from '@/hooks/useDocumentTitle'; import { useOpenGraph } from '@/hooks/useOpenGraph'; +import { useProfileActivity } from '@/hooks/profile/useProfileActivity'; +import { useProfileStats } from '@/hooks/profile/useProfileStats'; // Activity type definitions interface SubmissionActivity { @@ -147,13 +149,6 @@ export default function Profile() { const [formErrors, setFormErrors] = useState>({}); const [avatarUrl, setAvatarUrl] = useState(''); const [avatarImageId, setAvatarImageId] = useState(''); - const [calculatedStats, setCalculatedStats] = useState({ - rideCount: 0, - coasterCount: 0, - parkCount: 0 - }); - const [recentActivity, setRecentActivity] = useState([]); - const [activityLoading, setActivityLoading] = useState(false); // User role checking const { isModerator, loading: rolesLoading } = useUserRole(); @@ -172,6 +167,18 @@ export default function Profile() { // Username validation const usernameValidation = useUsernameValidation(editForm.username, profile?.username); + + // Optimized activity and stats hooks + const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id; + const { data: calculatedStats = { rideCount: 0, coasterCount: 0, parkCount: 0 } } = useProfileStats(profile?.user_id); + const { data: recentActivity = [], isLoading: activityLoading } = useProfileActivity( + profile?.user_id, + isOwnProfile || false, + isModerator() + ); + // Cast activity to local types for type safety + const typedActivity = recentActivity as ActivityEntry[]; + useEffect(() => { getCurrentUser(); if (username) { @@ -181,214 +188,6 @@ export default function Profile() { } }, [username]); - const fetchCalculatedStats = async (userId: string) => { - try { - // Fetch ride credits stats - const { data: ridesData, error: ridesError } = await supabase - .from('user_ride_credits') - .select(` - ride_count, - rides!inner(category, park_id) - `) - .eq('user_id', userId); - - if (ridesError) throw ridesError; - - // Calculate total rides count (sum of all ride_count values) - const totalRides = ridesData?.reduce((sum, credit) => sum + (credit.ride_count || 0), 0) || 0; - - // Calculate coasters count (distinct rides where category is roller_coaster) - const coasterRides = ridesData?.filter(credit => - credit.rides?.category === 'roller_coaster' - ) || []; - const uniqueCoasters = new Set(coasterRides.map(credit => credit.rides)); - const coasterCount = uniqueCoasters.size; - - // Calculate parks count (distinct parks where user has ridden at least one ride) - const parkRides = ridesData?.map(credit => credit.rides?.park_id).filter(Boolean) || []; - const uniqueParks = new Set(parkRides); - const parkCount = uniqueParks.size; - - setCalculatedStats({ - rideCount: totalRides, - coasterCount: coasterCount, - parkCount: parkCount - }); - } catch (error) { - console.error('Error fetching calculated stats:', error); - toast({ - variant: 'destructive', - description: getErrorMessage(error), - }); - // Set defaults on error - setCalculatedStats({ - rideCount: 0, - coasterCount: 0, - parkCount: 0 - }); - } - }; - - const fetchRecentActivity = async (userId: string) => { - setActivityLoading(true); - try { - const isOwnProfile = currentUser && currentUser.id === userId; - - // Wait for role loading to complete - if (rolesLoading) { - setActivityLoading(false); - return; - } - - // Check user privacy settings for activity visibility - const { data: preferences } = await supabase - .from('user_preferences') - .select('privacy_settings') - .eq('user_id', userId) - .single(); - - const privacySettings = preferences?.privacy_settings as { activity_visibility?: string } | null; - const activityVisibility = privacySettings?.activity_visibility || 'public'; - - // If activity is not public and viewer is not owner or moderator, show empty - if (activityVisibility !== 'public' && !isOwnProfile && !isModerator()) { - setRecentActivity([]); - setActivityLoading(false); - return; - } - - // Fetch last 10 reviews - let reviewsQuery = supabase - .from('reviews') - .select('id, rating, title, created_at, moderation_status, park_id, ride_id, parks(name, slug), rides(name, slug, parks(name, slug))') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .limit(10); - - // Regular users viewing others: show only approved reviews - if (!isOwnProfile && !isModerator()) { - reviewsQuery = reviewsQuery.eq('moderation_status', 'approved'); - } - - const { data: reviews, error: reviewsError } = await reviewsQuery; - if (reviewsError) throw reviewsError; - - // Fetch last 10 ride credits - const { data: credits, error: creditsError } = await supabase - .from('user_ride_credits') - .select('id, ride_count, first_ride_date, created_at, rides(name, slug, parks(name, slug))') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .limit(10); - - if (creditsError) throw creditsError; - - // Fetch last 10 submissions with enriched data - let submissionsQuery = supabase - .from('content_submissions') - .select('id, submission_type, content, status, created_at') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .limit(10); - - // Regular users viewing others: show only approved submissions - // Moderators/Admins/Superusers see all submissions (they bypass the submission process) - if (!isOwnProfile && !isModerator()) { - submissionsQuery = submissionsQuery.eq('status', 'approved'); - } - - const { data: submissions, error: submissionsError } = await submissionsQuery; - if (submissionsError) throw submissionsError; - - // Enrich submissions with entity data and photos - const enrichedSubmissions = await Promise.all((submissions || []).map(async (sub) => { - const enriched: any = { ...sub }; - - // For photo submissions, get photo count and preview - if (sub.submission_type === 'photo') { - const { data: photoSubs } = await supabase - .from('photo_submissions') - .select('id, entity_type, entity_id') - .eq('submission_id', sub.id) - .maybeSingle(); - - if (photoSubs) { - const { data: photoItems, count } = await supabase - .from('photo_submission_items') - .select('cloudflare_image_url', { count: 'exact' }) - .eq('photo_submission_id', photoSubs.id) - .order('order_index', { ascending: true }) - .limit(1); - - enriched.photo_count = count || 0; - enriched.photo_preview = photoItems?.[0]?.cloudflare_image_url; - enriched.entity_type = photoSubs.entity_type; - enriched.entity_id = photoSubs.entity_id; - - // Get entity name/slug for linking - if (photoSubs.entity_type === 'park') { - const { data: park } = await supabase - .from('parks') - .select('name, slug') - .eq('id', photoSubs.entity_id) - .single(); - enriched.content = { ...enriched.content, entity_name: park?.name, entity_slug: park?.slug }; - } else if (photoSubs.entity_type === 'ride') { - const { data: ride } = await supabase - .from('rides') - .select('name, slug, parks!inner(name, slug)') - .eq('id', photoSubs.entity_id) - .single(); - enriched.content = { - ...enriched.content, - entity_name: ride?.name, - entity_slug: ride?.slug, - park_name: ride?.parks?.name, - park_slug: ride?.parks?.slug - }; - } - } - } - - return enriched; - })); - - // Fetch last 10 rankings (public top lists) - let rankingsQuery = supabase - .from('user_top_lists') - .select('id, title, description, list_type, created_at') - .eq('user_id', userId) - .order('created_at', { ascending: false }) - .limit(10); - - if (!isOwnProfile) { - rankingsQuery = rankingsQuery.eq('is_public', true); - } - - const { data: rankings, error: rankingsError } = await rankingsQuery; - if (rankingsError) throw rankingsError; - - // Combine and sort by date - const combined = [ - ...(reviews?.map(r => ({ ...r, type: 'review' as const })) || []), - ...(credits?.map(c => ({ ...c, type: 'credit' as const })) || []), - ...(enrichedSubmissions?.map(s => ({ ...s, type: 'submission' as const })) || []), - ...(rankings?.map(r => ({ ...r, type: 'ranking' as const })) || []) - ].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) - .slice(0, 15) as ActivityEntry[]; - - setRecentActivity(combined); - } catch (error) { - console.error('Error fetching recent activity:', error); - toast({ - variant: 'destructive', - description: getErrorMessage(error), - }); - setRecentActivity([]); - } finally { - setActivityLoading(false); - } - }; const getCurrentUser = async () => { const { data: { @@ -434,10 +233,6 @@ export default function Profile() { }); setAvatarUrl(data.avatar_url || ''); setAvatarImageId(data.avatar_image_id || ''); - - // Fetch calculated stats and recent activity for this user - await fetchCalculatedStats(data.user_id); - await fetchRecentActivity(data.user_id); } } catch (error) { console.error('Error fetching profile:', error); @@ -475,10 +270,6 @@ export default function Profile() { }); setAvatarUrl(data.avatar_url || ''); setAvatarImageId(data.avatar_image_id || ''); - - // Fetch calculated stats and recent activity for the current user - await fetchCalculatedStats(user.id); - await fetchRecentActivity(user.id); } } catch (error) { console.error('Error fetching profile:', error); @@ -609,7 +400,7 @@ export default function Profile() { }); } }; - const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id; + if (loading) { return
@@ -832,7 +623,7 @@ export default function Profile() {
- ) : recentActivity.length === 0 ? ( + ) : typedActivity.length === 0 ? (

No recent activity yet

@@ -842,7 +633,7 @@ export default function Profile() {
) : (
- {recentActivity.map(activity => ( + {typedActivity.map(activity => (
{activity.type === 'review' ? (