Fix Phase 4 API optimization

This commit is contained in:
gpt-engineer-app[bot]
2025-10-30 23:28:55 +00:00
parent cecb27a302
commit bee6e9e50b
2 changed files with 26 additions and 266 deletions

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react'; import { useState } from 'react';
import { Camera, Upload, LogIn, Settings, ArrowUpDown } from 'lucide-react'; import { Camera, Upload, LogIn, Settings, ArrowUpDown } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
@@ -14,11 +14,9 @@ import { useNavigate } from 'react-router-dom';
import { UppyPhotoSubmissionUpload } from '@/components/upload/UppyPhotoSubmissionUpload'; import { UppyPhotoSubmissionUpload } from '@/components/upload/UppyPhotoSubmissionUpload';
import { PhotoManagementDialog } from '@/components/upload/PhotoManagementDialog'; import { PhotoManagementDialog } from '@/components/upload/PhotoManagementDialog';
import { PhotoModal } from '@/components/moderation/PhotoModal'; import { PhotoModal } from '@/components/moderation/PhotoModal';
import { supabase } from '@/integrations/supabase/client';
import { EntityPhotoGalleryProps } from '@/types/submissions'; import { EntityPhotoGalleryProps } from '@/types/submissions';
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { getErrorMessage } from '@/lib/errorHandler'; import { useEntityPhotos } from '@/hooks/photos/useEntityPhotos';
import { logger } from '@/lib/logger';
interface Photo { interface Photo {
id: string; id: string;
@@ -38,47 +36,18 @@ export function EntityPhotoGallery({
const { user } = useAuth(); const { user } = useAuth();
const navigate = useNavigate(); const navigate = useNavigate();
const { isModerator } = useUserRole(); const { isModerator } = useUserRole();
const [photos, setPhotos] = useState<Photo[]>([]);
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);
const [showManagement, setShowManagement] = useState(false); const [showManagement, setShowManagement] = useState(false);
const [loading, setLoading] = useState(true);
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState<number | null>(null); const [selectedPhotoIndex, setSelectedPhotoIndex] = useState<number | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [sortBy, setSortBy] = useState<'newest' | 'oldest'>('newest'); const [sortBy, setSortBy] = useState<'newest' | 'oldest'>('newest');
useEffect(() => { // Use optimized photos hook with caching
fetchPhotos(); const { data: photos = [], isLoading: loading, refetch } = useEntityPhotos(
}, [entityId, entityType, sortBy]); entityType,
entityId,
const fetchPhotos = async () => { sortBy
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);
}
};
const handleUploadClick = () => { const handleUploadClick = () => {
if (!user) { if (!user) {
@@ -90,7 +59,7 @@ export function EntityPhotoGallery({
const handleSubmissionComplete = () => { const handleSubmissionComplete = () => {
setShowUpload(false); setShowUpload(false);
fetchPhotos(); // Refresh photos after submission refetch(); // Refresh photos after submission
}; };
const handlePhotoClick = (index: number) => { const handlePhotoClick = (index: number) => {

View File

@@ -32,6 +32,8 @@ import { PersonalLocationDisplay } from '@/components/profile/PersonalLocationDi
import { useUserRole } from '@/hooks/useUserRole'; import { useUserRole } from '@/hooks/useUserRole';
import { useDocumentTitle } from '@/hooks/useDocumentTitle'; import { useDocumentTitle } from '@/hooks/useDocumentTitle';
import { useOpenGraph } from '@/hooks/useOpenGraph'; import { useOpenGraph } from '@/hooks/useOpenGraph';
import { useProfileActivity } from '@/hooks/profile/useProfileActivity';
import { useProfileStats } from '@/hooks/profile/useProfileStats';
// Activity type definitions // Activity type definitions
interface SubmissionActivity { interface SubmissionActivity {
@@ -147,13 +149,6 @@ export default function Profile() {
const [formErrors, setFormErrors] = useState<Record<string, string>>({}); const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const [avatarUrl, setAvatarUrl] = useState<string>(''); const [avatarUrl, setAvatarUrl] = useState<string>('');
const [avatarImageId, setAvatarImageId] = useState<string>(''); const [avatarImageId, setAvatarImageId] = useState<string>('');
const [calculatedStats, setCalculatedStats] = useState({
rideCount: 0,
coasterCount: 0,
parkCount: 0
});
const [recentActivity, setRecentActivity] = useState<ActivityEntry[]>([]);
const [activityLoading, setActivityLoading] = useState(false);
// User role checking // User role checking
const { isModerator, loading: rolesLoading } = useUserRole(); const { isModerator, loading: rolesLoading } = useUserRole();
@@ -172,6 +167,18 @@ export default function Profile() {
// Username validation // Username validation
const usernameValidation = useUsernameValidation(editForm.username, profile?.username); 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(() => { useEffect(() => {
getCurrentUser(); getCurrentUser();
if (username) { if (username) {
@@ -181,214 +188,6 @@ export default function Profile() {
} }
}, [username]); }, [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 getCurrentUser = async () => {
const { const {
data: { data: {
@@ -434,10 +233,6 @@ export default function Profile() {
}); });
setAvatarUrl(data.avatar_url || ''); setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || ''); 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) { } catch (error) {
console.error('Error fetching profile:', error); console.error('Error fetching profile:', error);
@@ -475,10 +270,6 @@ export default function Profile() {
}); });
setAvatarUrl(data.avatar_url || ''); setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || ''); 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) { } catch (error) {
console.error('Error fetching profile:', 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) { if (loading) {
return <div className="min-h-screen bg-background"> return <div className="min-h-screen bg-background">
<Header /> <Header />
@@ -832,7 +623,7 @@ export default function Profile() {
<div className="flex justify-center py-12"> <div className="flex justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" /> <Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div> </div>
) : recentActivity.length === 0 ? ( ) : typedActivity.length === 0 ? (
<div className="text-center py-12"> <div className="text-center py-12">
<Trophy className="w-16 h-16 text-muted-foreground mx-auto mb-4" /> <Trophy className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">No recent activity yet</h3> <h3 className="text-xl font-semibold mb-2">No recent activity yet</h3>
@@ -842,7 +633,7 @@ export default function Profile() {
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{recentActivity.map(activity => ( {typedActivity.map(activity => (
<div key={`${activity.type}-${activity.id}`} className="flex gap-4 p-4 rounded-lg border bg-card hover:bg-accent/5 transition-colors"> <div key={`${activity.type}-${activity.id}`} className="flex gap-4 p-4 rounded-lg border bg-card hover:bg-accent/5 transition-colors">
<div className="flex-shrink-0 mt-1"> <div className="flex-shrink-0 mt-1">
{activity.type === 'review' ? ( {activity.type === 'review' ? (