mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 13:51:14 -05:00
Reverted to commit 4bd89e9c6a
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
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 { Header } from '@/components/layout/Header';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -54,12 +55,11 @@ export default function Profile() {
|
|||||||
coasterCount: 0,
|
coasterCount: 0,
|
||||||
parkCount: 0
|
parkCount: 0
|
||||||
});
|
});
|
||||||
|
const [recentActivity, setRecentActivity] = useState<any[]>([]);
|
||||||
|
const [activityLoading, setActivityLoading] = useState(false);
|
||||||
|
|
||||||
// Profile field access checking
|
// Profile field access checking
|
||||||
const {
|
const { canViewField, loading: fieldAccessLoading } = useProfileFieldAccess(profile?.user_id);
|
||||||
canViewField,
|
|
||||||
loading: fieldAccessLoading
|
|
||||||
} = useProfileFieldAccess(profile?.user_id);
|
|
||||||
|
|
||||||
// Username validation
|
// Username validation
|
||||||
const usernameValidation = useUsernameValidation(editForm.username, profile?.username);
|
const usernameValidation = useUsernameValidation(editForm.username, profile?.username);
|
||||||
@@ -71,23 +71,27 @@ export default function Profile() {
|
|||||||
fetchCurrentUserProfile();
|
fetchCurrentUserProfile();
|
||||||
}
|
}
|
||||||
}, [username]);
|
}, [username]);
|
||||||
|
|
||||||
const fetchCalculatedStats = async (userId: string) => {
|
const fetchCalculatedStats = async (userId: string) => {
|
||||||
try {
|
try {
|
||||||
// Fetch ride credits stats
|
// Fetch ride credits stats
|
||||||
const {
|
const { data: ridesData, error: ridesError } = await supabase
|
||||||
data: ridesData,
|
.from('user_ride_credits')
|
||||||
error: ridesError
|
.select(`
|
||||||
} = await supabase.from('user_ride_credits').select(`
|
|
||||||
ride_count,
|
ride_count,
|
||||||
rides!inner(category, park_id)
|
rides!inner(category, park_id)
|
||||||
`).eq('user_id', userId);
|
`)
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
if (ridesError) throw ridesError;
|
if (ridesError) throw ridesError;
|
||||||
|
|
||||||
// Calculate total rides count (sum of all ride_count values)
|
// Calculate total rides count (sum of all ride_count values)
|
||||||
const totalRides = ridesData?.reduce((sum, credit) => sum + (credit.ride_count || 0), 0) || 0;
|
const totalRides = ridesData?.reduce((sum, credit) => sum + (credit.ride_count || 0), 0) || 0;
|
||||||
|
|
||||||
// Calculate coasters count (distinct rides where category is roller_coaster)
|
// 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 uniqueCoasters = new Set(coasterRides.map(credit => credit.rides));
|
||||||
const coasterCount = uniqueCoasters.size;
|
const coasterCount = uniqueCoasters.size;
|
||||||
|
|
||||||
@@ -95,6 +99,7 @@ export default function Profile() {
|
|||||||
const parkRides = ridesData?.map(credit => credit.rides?.park_id).filter(Boolean) || [];
|
const parkRides = ridesData?.map(credit => credit.rides?.park_id).filter(Boolean) || [];
|
||||||
const uniqueParks = new Set(parkRides);
|
const uniqueParks = new Set(parkRides);
|
||||||
const parkCount = uniqueParks.size;
|
const parkCount = uniqueParks.size;
|
||||||
|
|
||||||
setCalculatedStats({
|
setCalculatedStats({
|
||||||
rideCount: totalRides,
|
rideCount: totalRides,
|
||||||
coasterCount: coasterCount,
|
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 getCurrentUser = async () => {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
@@ -120,11 +164,14 @@ export default function Profile() {
|
|||||||
};
|
};
|
||||||
const fetchProfile = async (profileUsername: string) => {
|
const fetchProfile = async (profileUsername: string) => {
|
||||||
try {
|
try {
|
||||||
const {
|
const { data, error } = await supabase
|
||||||
data,
|
.from('profiles')
|
||||||
error
|
.select(`*, location:locations(*)`)
|
||||||
} = await supabase.from('profiles').select(`*, location:locations(*)`).eq('username', profileUsername).maybeSingle();
|
.eq('username', profileUsername)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setProfile(data as ProfileType);
|
setProfile(data as ProfileType);
|
||||||
setEditForm({
|
setEditForm({
|
||||||
@@ -135,8 +182,9 @@ 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 for this user
|
// Fetch calculated stats and recent activity for this user
|
||||||
await fetchCalculatedStats(data.user_id);
|
await fetchCalculatedStats(data.user_id);
|
||||||
|
await fetchRecentActivity(data.user_id);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching profile:', error);
|
console.error('Error fetching profile:', error);
|
||||||
@@ -151,20 +199,20 @@ export default function Profile() {
|
|||||||
};
|
};
|
||||||
const fetchCurrentUserProfile = async () => {
|
const fetchCurrentUserProfile = async () => {
|
||||||
try {
|
try {
|
||||||
const {
|
const { data: { user } } = await supabase.auth.getUser();
|
||||||
data: {
|
|
||||||
user
|
|
||||||
}
|
|
||||||
} = await supabase.auth.getUser();
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
navigate('/auth');
|
navigate('/auth');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {
|
|
||||||
data,
|
const { data, error } = await supabase
|
||||||
error
|
.from('profiles')
|
||||||
} = await supabase.from('profiles').select(`*, location:locations(*)`).eq('user_id', user.id).maybeSingle();
|
.select(`*, location:locations(*)`)
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setProfile(data as ProfileType);
|
setProfile(data as ProfileType);
|
||||||
setEditForm({
|
setEditForm({
|
||||||
@@ -175,8 +223,9 @@ 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 for the current user
|
// Fetch calculated stats and recent activity for the current user
|
||||||
await fetchCalculatedStats(user.id);
|
await fetchCalculatedStats(user.id);
|
||||||
|
await fetchRecentActivity(user.id);
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching profile:', error);
|
console.error('Error fetching profile:', error);
|
||||||
@@ -348,13 +397,22 @@ export default function Profile() {
|
|||||||
<CardContent className="p-8">
|
<CardContent className="p-8">
|
||||||
<div className="flex flex-col md:flex-row gap-6">
|
<div className="flex flex-col md:flex-row gap-6">
|
||||||
<div className="flex flex-col items-center md:items-start">
|
<div className="flex flex-col items-center md:items-start">
|
||||||
<PhotoUpload variant="avatar" maxFiles={1} maxSizeMB={1} existingPhotos={canViewField('avatar_url') && profile.avatar_url ? [profile.avatar_url] : []} onUploadComplete={handleAvatarUpload} currentImageId={avatarImageId} onError={error => {
|
<PhotoUpload
|
||||||
toast({
|
variant="avatar"
|
||||||
title: "Upload Error",
|
maxFiles={1}
|
||||||
description: error,
|
maxSizeMB={1}
|
||||||
variant: "destructive"
|
existingPhotos={canViewField('avatar_url') && profile.avatar_url ? [profile.avatar_url] : []}
|
||||||
});
|
onUploadComplete={handleAvatarUpload}
|
||||||
}} className="mb-4" />
|
currentImageId={avatarImageId}
|
||||||
|
onError={error => {
|
||||||
|
toast({
|
||||||
|
title: "Upload Error",
|
||||||
|
description: error,
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2 mt-2">
|
<div className="flex flex-col gap-2 mt-2">
|
||||||
{isOwnProfile && !editing && <Button variant="outline" size="sm" onClick={() => setEditing(true)}>
|
{isOwnProfile && !editing && <Button variant="outline" size="sm" onClick={() => setEditing(true)}>
|
||||||
@@ -431,9 +489,11 @@ export default function Profile() {
|
|||||||
{profile.display_name && <Badge variant="secondary" className="cursor-pointer hover:bg-secondary/80" onClick={() => navigate(`/profile/${profile.username}`)}>@{profile.username}</Badge>}
|
{profile.display_name && <Badge variant="secondary" className="cursor-pointer hover:bg-secondary/80" onClick={() => navigate(`/profile/${profile.username}`)}>@{profile.username}</Badge>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{canViewField('bio') && profile.bio && <p className="text-muted-foreground mb-4 max-w-2xl">
|
{canViewField('bio') && profile.bio && (
|
||||||
|
<p className="text-muted-foreground mb-4 max-w-2xl">
|
||||||
{profile.bio}
|
{profile.bio}
|
||||||
</p>}
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
|
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
@@ -445,16 +505,30 @@ export default function Profile() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Show pronouns if enabled and privacy allows */}
|
{/* Show pronouns if enabled and privacy allows */}
|
||||||
{profile.show_pronouns && canViewField('preferred_pronouns') && profile.preferred_pronouns && <div className="flex items-center gap-1">
|
{profile.show_pronouns && canViewField('preferred_pronouns') && profile.preferred_pronouns && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
<User className="w-4 h-4" />
|
<User className="w-4 h-4" />
|
||||||
{profile.preferred_pronouns}
|
{profile.preferred_pronouns}
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Show personal location if available and privacy allows */}
|
{/* Show personal location if available and privacy allows */}
|
||||||
{canViewField('personal_location') && profile.personal_location && <PersonalLocationDisplay personalLocation={profile.personal_location} userId={profile.user_id} isOwnProfile={isOwnProfile} />}
|
{canViewField('personal_location') && profile.personal_location && (
|
||||||
|
<PersonalLocationDisplay
|
||||||
|
personalLocation={profile.personal_location}
|
||||||
|
userId={profile.user_id}
|
||||||
|
isOwnProfile={isOwnProfile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Show location only if privacy allows */}
|
{/* Show location only if privacy allows */}
|
||||||
{canViewField('location_id') && profile.location && <LocationDisplay location={profile.location} userId={profile.user_id} isOwnProfile={isOwnProfile} />}
|
{canViewField('location_id') && profile.location && (
|
||||||
|
<LocationDisplay
|
||||||
|
location={profile.location}
|
||||||
|
userId={profile.user_id}
|
||||||
|
isOwnProfile={isOwnProfile}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +559,7 @@ export default function Profile() {
|
|||||||
</Card>
|
</Card>
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4 text-center">
|
<CardContent className="p-4 text-center">
|
||||||
<div className="text-2xl font-bold">{profile.review_count}</div>
|
<div className="text-2xl font-bold text-accent">{profile.review_count}</div>
|
||||||
<div className="text-sm text-muted-foreground">Reviews</div>
|
<div className="text-sm text-muted-foreground">Reviews</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -505,17 +579,97 @@ export default function Profile() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Recent Activity</CardTitle>
|
<CardTitle>Recent Activity</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Latest reviews, ratings, and achievements
|
Latest submissions, reviews, credits, and rankings
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="text-center py-12">
|
{activityLoading ? (
|
||||||
<Trophy className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
|
<div className="flex justify-center py-12">
|
||||||
<h3 className="text-xl font-semibold mb-2">Activity Feed Coming Soon</h3>
|
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
|
||||||
<p className="text-muted-foreground">
|
</div>
|
||||||
Track reviews, ratings, and achievements
|
) : recentActivity.length === 0 ? (
|
||||||
</p>
|
<div className="text-center py-12">
|
||||||
</div>
|
<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>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Reviews and ride credits will appear here
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{recentActivity.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' ? (
|
||||||
|
<Star className="w-5 h-5 text-accent" />
|
||||||
|
) : (
|
||||||
|
<Trophy className="w-5 h-5 text-accent" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{activity.type === 'review' ? (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<p className="font-medium">
|
||||||
|
{activity.title || 'Left a review'}
|
||||||
|
</p>
|
||||||
|
{activity.moderation_status === 'pending' && (
|
||||||
|
<Badge variant="secondary" className="text-xs">Pending</Badge>
|
||||||
|
)}
|
||||||
|
{activity.moderation_status === 'flagged' && (
|
||||||
|
<Badge variant="destructive" className="text-xs">Flagged</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 mb-2">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star key={i} className={`w-3 h-3 ${i < activity.rating ? 'fill-accent text-accent' : 'text-muted-foreground'}`} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{activity.park_id && activity.parks ? (
|
||||||
|
<Link to={`/parks/${activity.parks.slug}`} className="text-sm text-muted-foreground hover:text-accent transition-colors">
|
||||||
|
{activity.parks.name}
|
||||||
|
</Link>
|
||||||
|
) : activity.ride_id && activity.rides ? (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<Link to={`/parks/${activity.rides.parks?.slug}/rides/${activity.rides.slug}`} className="hover:text-accent transition-colors">
|
||||||
|
{activity.rides.name}
|
||||||
|
</Link>
|
||||||
|
{activity.rides.parks && (
|
||||||
|
<span className="text-muted-foreground/70"> at {activity.rides.parks.name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<p className="font-medium mb-1">Added ride credit</p>
|
||||||
|
{activity.rides && (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
<Link to={`/parks/${activity.rides.parks?.slug}/rides/${activity.rides.slug}`} className="hover:text-accent transition-colors">
|
||||||
|
{activity.rides.name}
|
||||||
|
</Link>
|
||||||
|
{activity.rides.parks && (
|
||||||
|
<span className="text-muted-foreground/70"> at {activity.rides.parks.name}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{activity.ride_count > 1 && (
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Ridden {activity.ride_count} times
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-shrink-0 text-xs text-muted-foreground">
|
||||||
|
{formatDistanceToNow(new Date(activity.created_at), { addSuffix: true })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@@ -561,7 +715,7 @@ export default function Profile() {
|
|||||||
<TabsContent value="credits" className="mt-6">
|
<TabsContent value="credits" className="mt-6">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Submissions reviews, credits, and rankings will appear here</CardTitle>
|
<CardTitle>Ride Credits</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Track all the rides you've experienced
|
Track all the rides you've experienced
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|||||||
Reference in New Issue
Block a user