mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 10:11:13 -05:00
Fix Phase 4 API optimization
This commit is contained in:
@@ -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<Photo[]>([]);
|
||||
const [showUpload, setShowUpload] = useState(false);
|
||||
const [showManagement, setShowManagement] = useState(false);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedPhotoIndex, setSelectedPhotoIndex] = useState<number | null>(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) => {
|
||||
|
||||
@@ -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<Record<string, string>>({});
|
||||
const [avatarUrl, setAvatarUrl] = 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
|
||||
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 <div className="min-h-screen bg-background">
|
||||
<Header />
|
||||
@@ -832,7 +623,7 @@ export default function Profile() {
|
||||
<div className="flex justify-center py-12">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : recentActivity.length === 0 ? (
|
||||
) : typedActivity.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<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>
|
||||
@@ -842,7 +633,7 @@ export default function Profile() {
|
||||
</div>
|
||||
) : (
|
||||
<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 className="flex-shrink-0 mt-1">
|
||||
{activity.type === 'review' ? (
|
||||
|
||||
Reference in New Issue
Block a user