Visual edit in Lovable

This commit is contained in:
gpt-engineer-app[bot]
2025-09-29 19:18:47 +00:00
parent 4bd89e9c6a
commit abea5d605a

View File

@@ -1,6 +1,5 @@
import { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns';
import { useParams, useNavigate } from 'react-router-dom';
import { Header } from '@/components/layout/Header';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -55,11 +54,12 @@ export default function Profile() {
coasterCount: 0,
parkCount: 0
});
const [recentActivity, setRecentActivity] = useState<any[]>([]);
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,27 +71,23 @@ 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;
@@ -99,7 +95,6 @@ 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,
@@ -115,45 +110,6 @@ 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: {
@@ -164,14 +120,11 @@ 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({
@@ -182,9 +135,8 @@ export default function Profile() {
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
// Fetch calculated stats and recent activity for this user
// Fetch calculated stats for this user
await fetchCalculatedStats(data.user_id);
await fetchRecentActivity(data.user_id);
}
} catch (error: any) {
console.error('Error fetching profile:', error);
@@ -199,20 +151,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({
@@ -223,9 +175,8 @@ export default function Profile() {
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
// Fetch calculated stats and recent activity for the current user
// Fetch calculated stats for the current user
await fetchCalculatedStats(user.id);
await fetchRecentActivity(user.id);
}
} catch (error: any) {
console.error('Error fetching profile:', error);
@@ -397,22 +348,13 @@ export default function Profile() {
<CardContent className="p-8">
<div className="flex flex-col md:flex-row gap-6">
<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 variant="avatar" maxFiles={1} maxSizeMB={1} existingPhotos={canViewField('avatar_url') && profile.avatar_url ? [profile.avatar_url] : []} onUploadComplete={handleAvatarUpload} currentImageId={avatarImageId} onError={error => {
toast({
title: "Upload Error",
description: error,
variant: "destructive"
});
}}
className="mb-4"
/>
}} className="mb-4" />
<div className="flex flex-col gap-2 mt-2">
{isOwnProfile && !editing && <Button variant="outline" size="sm" onClick={() => setEditing(true)}>
@@ -489,11 +431,9 @@ 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>}
</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}
</p>
)}
</p>}
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
@@ -505,30 +445,16 @@ export default function Profile() {
</div>
{/* 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" />
{profile.preferred_pronouns}
</div>
)}
</div>}
{/* 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 */}
{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>
@@ -559,7 +485,7 @@ export default function Profile() {
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-accent">{profile.review_count}</div>
<div className="text-2xl font-bold">{profile.review_count}</div>
<div className="text-sm text-muted-foreground">Reviews</div>
</CardContent>
</Card>
@@ -579,97 +505,17 @@ export default function Profile() {
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Latest submissions, reviews, credits, and rankings
Latest reviews, ratings, and achievements
</CardDescription>
</CardHeader>
<CardContent>
{activityLoading ? (
<div className="flex justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
) : recentActivity.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>
<h3 className="text-xl font-semibold mb-2">Activity Feed Coming Soon</h3>
<p className="text-muted-foreground">
Reviews and ride credits will appear here
Track reviews, ratings, and achievements
</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>
</Card>
</TabsContent>
@@ -715,7 +561,7 @@ export default function Profile() {
<TabsContent value="credits" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Ride Credits</CardTitle>
<CardTitle>Submissions reviews, credits, and rankings will appear here</CardTitle>
<CardDescription>
Track all the rides you've experienced
</CardDescription>