Add ride detail pages

This commit is contained in:
gpt-engineer-app[bot]
2025-09-20 00:59:50 +00:00
parent 90bb0216b7
commit 3a4b52ec18
9 changed files with 1247 additions and 29 deletions

440
src/pages/Profile.tsx Normal file
View File

@@ -0,0 +1,440 @@
import { useState, useEffect } from 'react';
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';
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 {
User,
MapPin,
Calendar,
Star,
Trophy,
Settings,
Camera,
Edit3,
Save,
X,
ArrowLeft
} from 'lucide-react';
import { Profile as ProfileType } from '@/types/database';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
export default function Profile() {
const { username } = useParams<{ username?: string }>();
const navigate = useNavigate();
const { toast } = useToast();
const [profile, setProfile] = useState<ProfileType | null>(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
const [currentUser, setCurrentUser] = useState<any>(null);
const [editForm, setEditForm] = useState({
display_name: '',
bio: '',
avatar_url: ''
});
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 {
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({
display_name: data.display_name || '',
bio: data.bio || '',
avatar_url: data.avatar_url || ''
});
}
} catch (error: any) {
console.error('Error fetching profile:', error);
toast({
variant: "destructive",
title: "Error loading profile",
description: error.message,
});
} 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({
display_name: data.display_name || '',
bio: data.bio || '',
avatar_url: data.avatar_url || ''
});
}
} catch (error: any) {
console.error('Error fetching profile:', error);
toast({
variant: "destructive",
title: "Error loading profile",
description: error.message,
});
} finally {
setLoading(false);
}
};
const handleSaveProfile = async () => {
if (!profile || !currentUser) return;
try {
const { error } = await supabase
.from('profiles')
.update({
display_name: editForm.display_name,
bio: editForm.bio,
avatar_url: editForm.avatar_url
})
.eq('user_id', currentUser.id);
if (error) throw error;
setProfile(prev => prev ? {
...prev,
display_name: editForm.display_name,
bio: editForm.bio,
avatar_url: editForm.avatar_url
} : null);
setEditing(false);
toast({
title: "Profile updated",
description: "Your profile has been updated successfully.",
});
} catch (error: any) {
toast({
variant: "destructive",
title: "Error updating profile",
description: error.message,
});
}
};
const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id;
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">
<Avatar className="w-32 h-32 mb-4">
<AvatarImage src={profile.avatar_url || ''} alt={profile.display_name || profile.username} />
<AvatarFallback className="text-2xl">
{(profile.display_name || profile.username).charAt(0).toUpperCase()}
</AvatarFallback>
</Avatar>
{isOwnProfile && !editing && (
<Button
variant="outline"
size="sm"
onClick={() => setEditing(true)}
className="mt-2"
>
<Edit3 className="w-4 h-4 mr-2" />
Edit Profile
</Button>
)}
</div>
<div className="flex-1">
{editing && isOwnProfile ? (
<div className="space-y-4">
<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"
/>
</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}
/>
</div>
<div>
<Label htmlFor="avatar_url">Avatar URL</Label>
<Input
id="avatar_url"
value={editForm.avatar_url}
onChange={(e) => setEditForm(prev => ({ ...prev, avatar_url: e.target.value }))}
placeholder="https://..."
/>
</div>
<div className="flex gap-2">
<Button onClick={handleSaveProfile} size="sm">
<Save className="w-4 h-4 mr-2" />
Save Changes
</Button>
<Button
variant="outline"
onClick={() => setEditing(false)}
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">@{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>
{profile.location && (
<div className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{profile.location.city}, {profile.location.country}
</div>
)}
</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">{profile.ride_count}</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">{profile.coaster_count}</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">{profile.park_count}</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">{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">Top Lists</TabsTrigger>
<TabsTrigger value="credits">Ride Credits</TabsTrigger>
</TabsList>
<TabsContent value="activity" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
<CardDescription>
Latest reviews, ratings, and achievements
</CardDescription>
</CardHeader>
<CardContent>
<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">Activity Feed Coming Soon</h3>
<p className="text-muted-foreground">
Track reviews, ratings, and achievements
</p>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="reviews" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Reviews ({profile.review_count})</CardTitle>
<CardDescription>
Parks and rides reviews
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-12">
<Star className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">Reviews Coming Soon</h3>
<p className="text-muted-foreground">
User reviews and ratings will appear here
</p>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="lists" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Top Lists</CardTitle>
<CardDescription>
Personal rankings and favorite collections
</CardDescription>
</CardHeader>
<CardContent>
<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">Top Lists Coming Soon</h3>
<p className="text-muted-foreground">
Create and share your favorite park and ride rankings
</p>
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="credits" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Ride Credits</CardTitle>
<CardDescription>
Track all the rides you've experienced
</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-12">
<User className="w-16 h-16 text-muted-foreground mx-auto mb-4" />
<h3 className="text-xl font-semibold mb-2">Ride Credits Coming Soon</h3>
<p className="text-muted-foreground">
Log and track your ride experiences
</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</main>
</div>
);
}