Files
thrilltrack-explorer/src/pages/Profile.tsx
gpt-engineer-app[bot] d40f0f13aa Apply all API enhancements
2025-10-30 23:55:18 +00:00

904 lines
39 KiB
TypeScript

import { useState, useEffect } from 'react';
import { useParams, useNavigate, Link } from 'react-router-dom';
import { formatDistanceToNow } from 'date-fns';
import { User as SupabaseUser } from '@supabase/supabase-js';
import { Header } from '@/components/layout/Header';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { useAuth } from '@/hooks/useAuth';
import { useProfile } from '@/hooks/useProfile';
import { UserReviewsList } from '@/components/profile/UserReviewsList';
import { UserListManager } from '@/components/lists/UserListManager';
import { RideCreditsManager } from '@/components/profile/RideCreditsManager';
import { useUsernameValidation } from '@/hooks/useUsernameValidation';
import { User, MapPin, Calendar, Star, Trophy, Settings, Camera, Edit3, Save, X, ArrowLeft, Check, AlertCircle, Loader2, UserX, FileText, Image } from 'lucide-react';
import { Profile as ProfileType } from '@/types/database';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
import { getErrorMessage } from '@/lib/errorHandler';
import { PhotoUpload } from '@/components/upload/PhotoUpload';
import { profileEditSchema } from '@/lib/validation';
import { LocationDisplay } from '@/components/profile/LocationDisplay';
import { UserBlockButton } from '@/components/profile/UserBlockButton';
import { PersonalLocationDisplay } from '@/components/profile/PersonalLocationDisplay';
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';
import { useQueryInvalidation } from '@/lib/queryInvalidation';
// Activity type definitions
interface SubmissionActivity {
id: string;
type: 'submission';
submission_type: 'park' | 'ride' | 'photo' | 'company' | 'ride_model';
status: string;
created_at: string;
content?: {
action?: 'edit' | 'create';
name?: string;
slug?: string;
entity_slug?: string;
entity_name?: string;
park_slug?: string;
park_name?: string;
company_type?: string;
manufacturer_slug?: string;
description?: string;
};
photo_preview?: string;
photo_count?: number;
entity_type?: 'park' | 'ride' | 'company';
entity_id?: string;
}
interface RankingActivity {
id: string;
type: 'ranking';
title: string;
description?: string;
list_type: string;
created_at: string;
}
interface ReviewActivity {
id: string;
type: 'review';
rating: number;
title?: string;
content?: string;
created_at: string;
moderation_status?: string;
park_id?: string;
ride_id?: string;
parks?: {
name: string;
slug: string;
} | null;
rides?: {
name: string;
slug: string;
parks?: {
name: string;
slug: string;
} | null;
} | null;
}
interface CreditActivity {
id: string;
type: 'credit';
ride_count: number;
first_ride_date?: string;
created_at: string;
rides?: {
name: string;
slug: string;
parks?: {
name: string;
slug: string;
} | null;
} | null;
}
type ActivityEntry = SubmissionActivity | RankingActivity | ReviewActivity | CreditActivity;
// Type guards
const isSubmissionActivity = (act: ActivityEntry): act is SubmissionActivity =>
act.type === 'submission';
const isRankingActivity = (act: ActivityEntry): act is RankingActivity =>
act.type === 'ranking';
const isReviewActivity = (act: ActivityEntry): act is ReviewActivity =>
act.type === 'review';
const isCreditActivity = (act: ActivityEntry): act is CreditActivity =>
act.type === 'credit';
export default function Profile() {
const {
username
} = useParams<{
username?: string;
}>();
const navigate = useNavigate();
const {
toast
} = useToast();
const { user: authUser } = useAuth();
const { refreshProfile } = useProfile(authUser?.id);
const [profile, setProfile] = useState<ProfileType | null>(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
const [currentUser, setCurrentUser] = useState<SupabaseUser | null>(null);
const [editForm, setEditForm] = useState({
username: '',
display_name: '',
bio: ''
});
const [showUsernameDialog, setShowUsernameDialog] = useState(false);
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const [avatarUrl, setAvatarUrl] = useState<string>('');
const [avatarImageId, setAvatarImageId] = useState<string>('');
// Query invalidation for cache updates
const { invalidateProfileActivity, invalidateProfileStats } = useQueryInvalidation();
// User role checking
const { isModerator, loading: rolesLoading } = useUserRole();
// Update document title when profile changes
useDocumentTitle(profile?.username ? `${profile.username}'s Profile` : 'Profile');
useOpenGraph({
title: profile ? `${profile.display_name || profile.username} - ThrillWiki` : 'User Profile',
description: profile?.bio || (profile ? `${profile.display_name || profile.username}'s profile on ThrillWiki` : undefined),
imageUrl: profile?.avatar_url,
imageId: profile?.avatar_image_id,
type: 'profile',
enabled: !!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) {
fetchProfile(username);
} else {
fetchCurrentUserProfile();
}
}, [username]);
const getCurrentUser = async () => {
const {
data: {
user
}
} = await supabase.auth.getUser();
setCurrentUser(user);
};
const fetchProfile = async (profileUsername: string) => {
try {
// Use filtered_profiles view for privacy-respecting queries
// This view enforces field-level privacy based on user settings
const { data, error } = await supabase
.from('filtered_profiles')
.select(`*`)
.eq('username', profileUsername)
.maybeSingle();
if (error) throw error;
if (data) {
// Fetch location separately if location_id is visible
let locationData = null;
if (data.location_id) {
const { data: location } = await supabase
.from('locations')
.select('*')
.eq('id', data.location_id)
.single();
locationData = location;
}
const profileWithLocation = {
...data,
location: locationData
};
setProfile(profileWithLocation as ProfileType);
setEditForm({
username: data.username || '',
display_name: data.display_name || '',
bio: data.bio || ''
});
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
}
} catch (error) {
console.error('Error fetching profile:', error);
toast({
variant: "destructive",
title: "Error loading profile",
description: getErrorMessage(error)
});
} finally {
setLoading(false);
}
};
const fetchCurrentUserProfile = async () => {
try {
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();
if (error) throw error;
if (data) {
setProfile(data as ProfileType);
setEditForm({
username: data.username || '',
display_name: data.display_name || '',
bio: data.bio || ''
});
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
}
} catch (error) {
console.error('Error fetching profile:', error);
toast({
variant: "destructive",
title: "Error loading profile",
description: getErrorMessage(error)
});
} finally {
setLoading(false);
}
};
const validateForm = () => {
const result = profileEditSchema.safeParse(editForm);
if (!result.success) {
const errors: Record<string, string> = {};
result.error.issues.forEach(issue => {
if (issue.path[0]) {
errors[issue.path[0] as string] = issue.message;
}
});
setFormErrors(errors);
return false;
}
if (!usernameValidation.isValid && editForm.username !== profile?.username) {
setFormErrors({
username: usernameValidation.error || 'Invalid username'
});
return false;
}
setFormErrors({});
return true;
};
const handleSaveProfile = async () => {
if (!profile || !currentUser) return;
if (!validateForm()) return;
const usernameChanged = editForm.username !== profile.username;
if (usernameChanged && !showUsernameDialog) {
setShowUsernameDialog(true);
return;
}
try {
const updateData: any = {
display_name: editForm.display_name,
bio: editForm.bio,
avatar_url: avatarUrl,
avatar_image_id: avatarImageId
};
if (usernameChanged) {
updateData.username = editForm.username;
}
const {
error
} = await supabase.from('profiles').update(updateData).eq('user_id', currentUser.id);
if (error) throw error;
// Invalidate profile caches across the app
if (currentUser.id) {
invalidateProfileActivity(currentUser.id);
invalidateProfileStats(currentUser.id);
}
setProfile(prev => prev ? {
...prev,
...updateData
} : null);
setEditing(false);
setShowUsernameDialog(false);
if (usernameChanged) {
toast({
title: "Profile updated",
description: "Your username and profile URL have been updated successfully."
});
// Navigate to new username URL
navigate(`/profile/${editForm.username}`);
} else {
toast({
title: "Profile updated",
description: "Your profile has been updated successfully."
});
}
} catch (error) {
toast({
variant: "destructive",
title: "Error updating profile",
description: getErrorMessage(error)
});
}
};
const confirmUsernameChange = () => {
setShowUsernameDialog(false);
handleSaveProfile();
};
const handleAvatarUpload = async (urls: string[], imageId?: string) => {
if (!currentUser || !urls[0]) return;
const newAvatarUrl = urls[0];
const newImageId = imageId || '';
// Update local state immediately
setAvatarUrl(newAvatarUrl);
setAvatarImageId(newImageId);
try {
// Update database immediately
const {
error
} = await supabase.from('profiles').update({
avatar_url: newAvatarUrl,
avatar_image_id: newImageId
}).eq('user_id', currentUser.id);
if (error) throw error;
// Invalidate profile activity cache (avatar shows in activity)
if (currentUser.id) {
invalidateProfileActivity(currentUser.id);
}
// Update local profile state
setProfile(prev => prev ? {
...prev,
avatar_url: newAvatarUrl,
avatar_image_id: newImageId
} : null);
// Refresh the auth context to update header avatar
if (refreshProfile) {
await refreshProfile();
}
toast({
title: "Avatar updated",
description: "Your profile picture has been updated successfully."
});
} catch (error) {
// Revert local state on error
setAvatarUrl(profile?.avatar_url || '');
setAvatarImageId(profile?.avatar_image_id || '');
toast({
variant: "destructive",
title: "Error updating avatar",
description: getErrorMessage(error)
});
}
};
if (loading) {
return <div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6">
<div className="h-32 bg-muted rounded-lg"></div>
<div className="h-8 bg-muted rounded w-1/3"></div>
<div className="h-4 bg-muted rounded w-1/2"></div>
</div>
</div>
</div>;
}
if (!profile) {
return <div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="text-center py-12">
<User className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h1 className="text-2xl font-bold mb-4">Profile Not Found</h1>
<p className="text-muted-foreground mb-6">
The profile you're looking for doesn't exist.
</p>
<Button onClick={() => navigate('/')}>
<ArrowLeft className="w-4 h-4 mr-2" />
Go Home
</Button>
</div>
</div>
</div>;
}
return <div className="min-h-screen bg-background">
<Header />
<main className="container mx-auto px-4 py-8">
{/* Profile Header */}
<div className="relative mb-8">
<Card>
<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={profile.avatar_url ? [profile.avatar_url] : []}
onUploadComplete={handleAvatarUpload}
currentImageId={avatarImageId}
onError={error => {
toast({
title: "Upload Error",
description: error,
variant: "destructive"
});
}}
className="mb-4"
/>
<div className="flex flex-col gap-2 mt-2">
{isOwnProfile && !editing && <Button variant="outline" size="sm" onClick={() => setEditing(true)}>
<Edit3 className="w-4 h-4 mr-2" />
Edit Profile
</Button>}
{!isOwnProfile && <UserBlockButton targetUserId={profile.user_id} targetUsername={profile.username} />}
</div>
</div>
<div className="flex-1">
{editing && isOwnProfile ? <div className="space-y-4">
<div>
<Label htmlFor="username">Username</Label>
<div className="relative">
<Input id="username" value={editForm.username} onChange={e => setEditForm(prev => ({
...prev,
username: e.target.value
}))} placeholder="your_username" className={`pr-10 ${formErrors.username ? 'border-destructive' : ''}`} />
<div className="absolute right-3 top-1/2 -translate-y-1/2">
{usernameValidation.isChecking ? <Loader2 className="w-4 h-4 text-muted-foreground animate-spin" /> : editForm.username === profile?.username ? <Check className="w-4 h-4 text-muted-foreground" /> : usernameValidation.isValid ? <Check className="w-4 h-4 text-green-500" /> : usernameValidation.error ? <AlertCircle className="w-4 h-4 text-destructive" /> : null}
</div>
</div>
{formErrors.username && <p className="text-sm text-destructive mt-1">{formErrors.username}</p>}
{usernameValidation.error && editForm.username !== profile?.username && <p className="text-sm text-destructive mt-1">{usernameValidation.error}</p>}
{usernameValidation.isValid && editForm.username !== profile?.username && <p className="text-sm text-green-600 mt-1">Username is available!</p>}
<p className="text-xs text-muted-foreground mt-1">
Your profile URL will be /profile/{editForm.username}
</p>
</div>
<div>
<Label htmlFor="display_name">Display Name</Label>
<Input id="display_name" value={editForm.display_name} onChange={e => setEditForm(prev => ({
...prev,
display_name: e.target.value
}))} placeholder="Your display name" className={formErrors.display_name ? 'border-destructive' : ''} />
{formErrors.display_name && <p className="text-sm text-destructive mt-1">{formErrors.display_name}</p>}
</div>
<div>
<Label htmlFor="bio">Bio</Label>
<Textarea id="bio" value={editForm.bio} onChange={e => setEditForm(prev => ({
...prev,
bio: e.target.value
}))} placeholder="Tell us about yourself..." rows={3} className={formErrors.bio ? 'border-destructive' : ''} />
{formErrors.bio && <p className="text-sm text-destructive mt-1">{formErrors.bio}</p>}
</div>
<div className="flex gap-2">
<Button onClick={handleSaveProfile} size="sm" disabled={usernameValidation.isChecking || editForm.username !== profile?.username && !usernameValidation.isValid}>
<Save className="w-4 h-4 mr-2" />
Save Changes
</Button>
<Button variant="outline" onClick={() => {
setEditing(false);
setFormErrors({});
setEditForm({
username: profile?.username || '',
display_name: profile?.display_name || '',
bio: profile?.bio || ''
});
}} size="sm">
<X className="w-4 h-4 mr-2" />
Cancel
</Button>
</div>
</div> : <div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold">
{profile.display_name || profile.username}
</h1>
{profile.display_name && <Badge variant="secondary" className="cursor-pointer hover:bg-secondary/80" onClick={() => navigate(`/profile/${profile.username}`)}>@{profile.username}</Badge>}
</div>
{profile.bio && (
<p className="text-muted-foreground mb-4 max-w-2xl">
{profile.bio}
</p>
)}
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Calendar className="w-4 h-4" />
Joined {new Date(profile.created_at).toLocaleDateString('en-US', {
month: 'long',
year: 'numeric'
})}
</div>
{/* Show pronouns if enabled and present (privacy already enforced by get_filtered_profile) */}
{profile.show_pronouns && profile.preferred_pronouns && (
<div className="flex items-center gap-1">
<User className="w-4 h-4" />
{profile.preferred_pronouns}
</div>
)}
{/* Show personal location (privacy already enforced by get_filtered_profile) */}
<PersonalLocationDisplay
personalLocation={profile.personal_location}
/>
{/* Show location (privacy already enforced by get_filtered_profile) */}
<LocationDisplay
location={profile.location}
/>
</div>
</div>}
</div>
</div>
</CardContent>
</Card>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-primary">{calculatedStats.rideCount}</div>
<div className="text-sm text-muted-foreground">Rides</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-accent">{calculatedStats.coasterCount}</div>
<div className="text-sm text-muted-foreground">Coasters</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-secondary">{calculatedStats.parkCount}</div>
<div className="text-sm text-muted-foreground">Parks</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-4 text-center">
<div className="text-2xl font-bold text-accent">{profile.review_count}</div>
<div className="text-sm text-muted-foreground">Reviews</div>
</CardContent>
</Card>
</div>
{/* Profile Tabs */}
<Tabs defaultValue="activity" className="w-full">
<TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="reviews">Reviews</TabsTrigger>
<TabsTrigger value="lists">Rankings</TabsTrigger>
<TabsTrigger value="credits">Ride Credits</TabsTrigger>
</TabsList>
<TabsContent value="activity" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Latest submissions, reviews, credits, and rankings
</CardDescription>
</CardHeader>
<CardContent>
{activityLoading ? (
<div className="flex justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
</div>
) : 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>
<p className="text-muted-foreground">
Latest submissions, reviews, credits, and rankings will appear here
</p>
</div>
) : (
<div className="space-y-4">
{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' ? (
<Star className="w-5 h-5 text-accent" />
) : activity.type === 'credit' ? (
<Trophy className="w-5 h-5 text-accent" />
) : activity.type === 'submission' ? (
activity.submission_type === 'photo' ? (
<Image className="w-5 h-5 text-accent" />
) : (
<FileText 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' ? (
(() => {
const reviewActivity = activity as ReviewActivity;
return (
<>
<div className="flex items-center gap-2 mb-1">
<p className="font-medium">
{reviewActivity.title || reviewActivity.content || 'Left a review'}
</p>
</div>
<div className="flex items-center gap-1 mb-2">
{[...Array(5)].map((_, i) => (
<Star key={i} className={`w-3 h-3 ${i < (reviewActivity.rating ?? 0) ? 'fill-accent text-accent' : 'text-muted-foreground'}`} />
))}
</div>
{reviewActivity.parks ? (
<Link to={`/parks/${reviewActivity.parks.slug || ''}`} className="text-sm text-muted-foreground hover:text-accent transition-colors">
{reviewActivity.parks.name || 'Unknown Park'}
</Link>
) : reviewActivity.rides ? (
<div className="text-sm text-muted-foreground">
<Link to={`/parks/${reviewActivity.rides.parks?.slug || ''}/rides/${reviewActivity.rides.slug || ''}`} className="hover:text-accent transition-colors">
{reviewActivity.rides.name || 'Unknown Ride'}
</Link>
{reviewActivity.rides.parks && (
<span className="text-muted-foreground/70"> at {reviewActivity.rides.parks.name || 'Unknown Park'}</span>
)}
</div>
) : null}
</>
);
})()
) : isSubmissionActivity(activity) ? (
<>
<div className="flex items-center gap-2 mb-1">
<p className="font-medium">
{activity.content?.action === 'edit' ? 'Edited' : 'Submitted'}{' '}
{activity.submission_type === 'photo' ? 'photos for' : activity.submission_type || 'content'}
{activity.content?.name && ` ${activity.content.name}`}
</p>
{activity.status === 'pending' && (
<Badge variant="secondary" className="text-xs">Pending</Badge>
)}
{activity.status === 'approved' && (
<Badge variant="default" className="text-xs">Approved</Badge>
)}
{activity.status === 'rejected' && (
<Badge variant="destructive" className="text-xs">Rejected</Badge>
)}
{activity.status === 'partially_approved' && (
<Badge variant="outline" className="text-xs">Partially Approved</Badge>
)}
</div>
{/* Photo preview for photo submissions */}
{activity.submission_type === 'photo' && activity.photo_preview && (
<div className="flex gap-2 items-center mb-2">
<img
src={activity.photo_preview}
alt="Photo preview"
className="w-16 h-16 rounded object-cover border"
/>
{activity.photo_count && activity.photo_count > 1 && (
<span className="text-xs text-muted-foreground">
+{activity.photo_count - 1} more
</span>
)}
</div>
)}
{/* Entity link for photo submissions */}
{activity.submission_type === 'photo' && activity.content?.entity_slug && (
<div className="text-sm text-muted-foreground">
{activity.entity_type === 'park' ? (
<Link to={`/parks/${activity.content.entity_slug}`} className="hover:text-accent transition-colors">
{activity.content.entity_name || 'View park'}
</Link>
) : activity.entity_type === 'ride' ? (
<>
<Link to={`/parks/${activity.content.park_slug}/rides/${activity.content.entity_slug}`} className="hover:text-accent transition-colors">
{activity.content.entity_name || 'View ride'}
</Link>
{activity.content.park_name && (
<span className="text-muted-foreground/70"> at {activity.content.park_name}</span>
)}
</>
) : null}
</div>
)}
{/* Links for entity submissions */}
{activity.status === 'approved' && activity.submission_type !== 'photo' && (
<>
{activity.submission_type === 'park' && activity.content?.slug && (
<Link
to={`/parks/${activity.content.slug}`}
className="text-sm text-accent hover:underline"
>
View park
</Link>
)}
{activity.submission_type === 'ride' && activity.content?.slug && activity.content?.park_slug && (
<div className="text-sm">
<Link
to={`/parks/${activity.content.park_slug}/rides/${activity.content.slug}`}
className="text-accent hover:underline"
>
View ride
</Link>
{activity.content.park_name && (
<span className="text-muted-foreground ml-1">
at {activity.content.park_name}
</span>
)}
</div>
)}
{activity.submission_type === 'company' && activity.content?.slug && (
<Link
to={`/${activity.content.company_type === 'operator' ? 'operators' : activity.content.company_type === 'property_owner' ? 'owners' : activity.content.company_type === 'manufacturer' ? 'manufacturers' : 'designers'}/${activity.content.slug}`}
className="text-sm text-accent hover:underline"
>
View {activity.content.company_type || 'company'}
</Link>
)}
{activity.submission_type === 'ride_model' && activity.content?.slug && activity.content?.manufacturer_slug && (
<Link
to={`/manufacturers/${activity.content.manufacturer_slug}/models/${activity.content.slug}`}
className="text-sm text-accent hover:underline"
>
View model
</Link>
)}
</>
)}
{activity.content?.description && activity.submission_type !== 'photo' && (
<p className="text-sm text-muted-foreground line-clamp-2">
{activity.content.description}
</p>
)}
</>
) : isRankingActivity(activity) ? (
<>
<div className="flex items-center gap-2 mb-1">
<Link
to={`/profile/${profile?.username}/lists`}
className="font-medium hover:text-accent transition-colors"
>
Created ranking: {activity.title || 'Untitled'}
</Link>
<Badge variant="outline" className="text-xs capitalize">
{(activity.list_type || '').replace('_', ' ')}
</Badge>
</div>
{activity.description && (
<p className="text-sm text-muted-foreground line-clamp-2">
{activity.description}
</p>
)}
</>
) : isCreditActivity(activity) ? (
<>
<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>
)}
</>
) : null}
</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>
<TabsContent value="reviews" className="mt-6">
<UserReviewsList userId={profile.user_id} reviewCount={profile.review_count} />
</TabsContent>
<TabsContent value="lists" className="mt-6">
<UserListManager />
</TabsContent>
<TabsContent value="credits" className="mt-6">
<RideCreditsManager userId={profile.user_id} />
</TabsContent>
</Tabs>
</main>
{/* Username Change Confirmation Dialog */}
<AlertDialog open={showUsernameDialog} onOpenChange={setShowUsernameDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Change Username?</AlertDialogTitle>
<AlertDialogDescription>
Changing your username will also change your profile URL from{' '}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
/profile/{profile?.username}
</code>{' '}
to{' '}
<code className="bg-muted px-1 py-0.5 rounded text-xs">
/profile/{editForm.username}
</code>
. This means any existing bookmarks or links to your profile will no longer work.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={confirmUsernameChange}>
Change Username
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>;
}