diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 25b8e350..e03a7220 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate, Link } from 'react-router-dom'; +import { formatDistanceToNow } from 'date-fns'; import { Header } from '@/components/layout/Header'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -54,12 +55,11 @@ export default function Profile() { coasterCount: 0, parkCount: 0 }); + const [recentActivity, setRecentActivity] = useState([]); + const [activityLoading, setActivityLoading] = useState(false); // Profile field access checking - const { - canViewField, - loading: fieldAccessLoading - } = useProfileFieldAccess(profile?.user_id); + const { canViewField, loading: fieldAccessLoading } = useProfileFieldAccess(profile?.user_id); // Username validation const usernameValidation = useUsernameValidation(editForm.username, profile?.username); @@ -71,23 +71,27 @@ export default function Profile() { fetchCurrentUserProfile(); } }, [username]); + const fetchCalculatedStats = async (userId: string) => { try { // Fetch ride credits stats - const { - data: ridesData, - error: ridesError - } = await supabase.from('user_ride_credits').select(` + const { data: ridesData, error: ridesError } = await supabase + .from('user_ride_credits') + .select(` ride_count, rides!inner(category, park_id) - `).eq('user_id', userId); + `) + .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 coasterRides = ridesData?.filter(credit => + credit.rides?.category === 'roller_coaster' + ) || []; const uniqueCoasters = new Set(coasterRides.map(credit => credit.rides)); const coasterCount = uniqueCoasters.size; @@ -95,6 +99,7 @@ export default function Profile() { 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, @@ -110,6 +115,45 @@ export default function Profile() { }); } }; + + const fetchRecentActivity = async (userId: string) => { + setActivityLoading(true); + try { + // Fetch last 10 reviews + const { data: reviews, error: reviewsError } = await 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); + + 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; + + // Combine and sort by date + const combined = [ + ...(reviews?.map(r => ({ ...r, type: 'review' })) || []), + ...(credits?.map(c => ({ ...c, type: 'credit' })) || []) + ].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .slice(0, 15); + + setRecentActivity(combined); + } catch (error: any) { + console.error('Error fetching recent activity:', error); + setRecentActivity([]); + } finally { + setActivityLoading(false); + } + }; const getCurrentUser = async () => { const { data: { @@ -120,11 +164,14 @@ export default function Profile() { }; const fetchProfile = async (profileUsername: string) => { try { - const { - data, - error - } = await supabase.from('profiles').select(`*, location:locations(*)`).eq('username', profileUsername).maybeSingle(); + const { data, error } = await supabase + .from('profiles') + .select(`*, location:locations(*)`) + .eq('username', profileUsername) + .maybeSingle(); + if (error) throw error; + if (data) { setProfile(data as ProfileType); setEditForm({ @@ -134,9 +181,10 @@ export default function Profile() { }); setAvatarUrl(data.avatar_url || ''); setAvatarImageId(data.avatar_image_id || ''); - - // Fetch calculated stats for this user + + // Fetch calculated stats and recent activity for this user await fetchCalculatedStats(data.user_id); + await fetchRecentActivity(data.user_id); } } catch (error: any) { console.error('Error fetching profile:', error); @@ -151,20 +199,20 @@ export default function Profile() { }; const fetchCurrentUserProfile = async () => { try { - const { - data: { - user - } - } = await supabase.auth.getUser(); + const { data: { user } } = await supabase.auth.getUser(); if (!user) { navigate('/auth'); return; } - const { - data, - error - } = await supabase.from('profiles').select(`*, location:locations(*)`).eq('user_id', user.id).maybeSingle(); + + const { data, error } = await supabase + .from('profiles') + .select(`*, location:locations(*)`) + .eq('user_id', user.id) + .maybeSingle(); + if (error) throw error; + if (data) { setProfile(data as ProfileType); setEditForm({ @@ -174,9 +222,10 @@ export default function Profile() { }); setAvatarUrl(data.avatar_url || ''); setAvatarImageId(data.avatar_image_id || ''); - - // Fetch calculated stats for the current user + + // Fetch calculated stats and recent activity for the current user await fetchCalculatedStats(user.id); + await fetchRecentActivity(user.id); } } catch (error: any) { console.error('Error fetching profile:', error); @@ -348,13 +397,22 @@ export default function Profile() {
- { - toast({ - title: "Upload Error", - description: error, - variant: "destructive" - }); - }} className="mb-4" /> + { + toast({ + title: "Upload Error", + description: error, + variant: "destructive" + }); + }} + className="mb-4" + />
{isOwnProfile && !editing &&
- {canViewField('bio') && profile.bio &&

+ {canViewField('bio') && profile.bio && ( +

{profile.bio} -

} +

+ )}
@@ -445,16 +505,30 @@ export default function Profile() {
{/* Show pronouns if enabled and privacy allows */} - {profile.show_pronouns && canViewField('preferred_pronouns') && profile.preferred_pronouns &&
+ {profile.show_pronouns && canViewField('preferred_pronouns') && profile.preferred_pronouns && ( +
{profile.preferred_pronouns} -
} +
+ )} {/* Show personal location if available and privacy allows */} - {canViewField('personal_location') && profile.personal_location && } + {canViewField('personal_location') && profile.personal_location && ( + + )} {/* Show location only if privacy allows */} - {canViewField('location_id') && profile.location && } + {canViewField('location_id') && profile.location && ( + + )}
}
@@ -485,7 +559,7 @@ export default function Profile() { -
{profile.review_count}
+
{profile.review_count}
Reviews
@@ -505,17 +579,97 @@ export default function Profile() { Recent Activity - Latest reviews, ratings, and achievements + Latest submissions, reviews, credits, and rankings -
- -

Activity Feed Coming Soon

-

- Track reviews, ratings, and achievements -

-
+ {activityLoading ? ( +
+ +
+ ) : recentActivity.length === 0 ? ( +
+ +

No recent activity yet

+

+ Reviews and ride credits will appear here +

+
+ ) : ( +
+ {recentActivity.map(activity => ( +
+
+ {activity.type === 'review' ? ( + + ) : ( + + )} +
+ +
+ {activity.type === 'review' ? ( + <> +
+

+ {activity.title || 'Left a review'} +

+ {activity.moderation_status === 'pending' && ( + Pending + )} + {activity.moderation_status === 'flagged' && ( + Flagged + )} +
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+ {activity.park_id && activity.parks ? ( + + {activity.parks.name} + + ) : activity.ride_id && activity.rides ? ( +
+ + {activity.rides.name} + + {activity.rides.parks && ( + at {activity.rides.parks.name} + )} +
+ ) : null} + + ) : ( + <> +

Added ride credit

+ {activity.rides && ( +
+ + {activity.rides.name} + + {activity.rides.parks && ( + at {activity.rides.parks.name} + )} +
+ )} + {activity.ride_count > 1 && ( +

+ Ridden {activity.ride_count} times +

+ )} + + )} +
+ +
+ {formatDistanceToNow(new Date(activity.created_at), { addSuffix: true })} +
+
+ ))} +
+ )}
@@ -561,7 +715,7 @@ export default function Profile() { - Submissions reviews, credits, and rankings will appear here + Ride Credits Track all the rides you've experienced