Visual edit in Lovable

This commit is contained in:
gpt-engineer-app[bot]
2025-09-28 20:50:36 +00:00
parent ce99aeceed
commit 0513b69710

View File

@@ -13,23 +13,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { useUsernameValidation } from '@/hooks/useUsernameValidation'; import { useUsernameValidation } from '@/hooks/useUsernameValidation';
import { import { User, MapPin, Calendar, Star, Trophy, Settings, Camera, Edit3, Save, X, ArrowLeft, Check, AlertCircle, Loader2, UserX } from 'lucide-react';
User,
MapPin,
Calendar,
Star,
Trophy,
Settings,
Camera,
Edit3,
Save,
X,
ArrowLeft,
Check,
AlertCircle,
Loader2,
UserX
} from 'lucide-react';
import { Profile as ProfileType } from '@/types/database'; import { Profile as ProfileType } from '@/types/database';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
@@ -37,12 +21,19 @@ import { PhotoUpload } from '@/components/upload/PhotoUpload';
import { profileEditSchema } from '@/lib/validation'; import { profileEditSchema } from '@/lib/validation';
import { LocationDisplay } from '@/components/profile/LocationDisplay'; import { LocationDisplay } from '@/components/profile/LocationDisplay';
import { UserBlockButton } from '@/components/profile/UserBlockButton'; import { UserBlockButton } from '@/components/profile/UserBlockButton';
export default function Profile() { export default function Profile() {
const { username } = useParams<{ username?: string }>(); const {
username
} = useParams<{
username?: string;
}>();
const navigate = useNavigate(); const navigate = useNavigate();
const { toast } = useToast(); const {
const { refreshProfile } = useAuth(); toast
} = useToast();
const {
refreshProfile
} = useAuth();
const [profile, setProfile] = useState<ProfileType | null>(null); const [profile, setProfile] = useState<ProfileType | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
@@ -50,16 +41,15 @@ export default function Profile() {
const [editForm, setEditForm] = useState({ const [editForm, setEditForm] = useState({
username: '', username: '',
display_name: '', display_name: '',
bio: '', bio: ''
}); });
const [showUsernameDialog, setShowUsernameDialog] = useState(false); const [showUsernameDialog, setShowUsernameDialog] = useState(false);
const [formErrors, setFormErrors] = useState<Record<string, string>>({}); const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const [avatarUrl, setAvatarUrl] = useState<string>(''); const [avatarUrl, setAvatarUrl] = useState<string>('');
const [avatarImageId, setAvatarImageId] = useState<string>(''); const [avatarImageId, setAvatarImageId] = useState<string>('');
// Username validation // Username validation
const usernameValidation = useUsernameValidation(editForm.username, profile?.username); const usernameValidation = useUsernameValidation(editForm.username, profile?.username);
useEffect(() => { useEffect(() => {
getCurrentUser(); getCurrentUser();
if (username) { if (username) {
@@ -68,28 +58,27 @@ export default function Profile() {
fetchCurrentUserProfile(); fetchCurrentUserProfile();
} }
}, [username]); }, [username]);
const getCurrentUser = async () => { const getCurrentUser = async () => {
const { data: { user } } = await supabase.auth.getUser(); const {
data: {
user
}
} = await supabase.auth.getUser();
setCurrentUser(user); setCurrentUser(user);
}; };
const fetchProfile = async (profileUsername: string) => { const fetchProfile = async (profileUsername: string) => {
try { try {
const { data, error } = await supabase const {
.from('profiles') data,
.select(`*, location:locations(*)`) error
.eq('username', profileUsername) } = await supabase.from('profiles').select(`*, location:locations(*)`).eq('username', profileUsername).maybeSingle();
.maybeSingle();
if (error) throw error; if (error) throw error;
if (data) { if (data) {
setProfile(data as ProfileType); setProfile(data as ProfileType);
setEditForm({ setEditForm({
username: data.username || '', username: data.username || '',
display_name: data.display_name || '', display_name: data.display_name || '',
bio: data.bio || '', bio: data.bio || ''
}); });
setAvatarUrl(data.avatar_url || ''); setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || ''); setAvatarImageId(data.avatar_image_id || '');
@@ -99,36 +88,34 @@ export default function Profile() {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error loading profile", title: "Error loading profile",
description: error.message, description: error.message
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const fetchCurrentUserProfile = async () => { const fetchCurrentUserProfile = async () => {
try { try {
const { data: { user } } = await supabase.auth.getUser(); const {
data: {
user
}
} = await supabase.auth.getUser();
if (!user) { if (!user) {
navigate('/auth'); navigate('/auth');
return; return;
} }
const {
const { data, error } = await supabase data,
.from('profiles') error
.select(`*, location:locations(*)`) } = await supabase.from('profiles').select(`*, location:locations(*)`).eq('user_id', user.id).maybeSingle();
.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({
username: data.username || '', username: data.username || '',
display_name: data.display_name || '', display_name: data.display_name || '',
bio: data.bio || '', bio: data.bio || ''
}); });
setAvatarUrl(data.avatar_url || ''); setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || ''); setAvatarImageId(data.avatar_image_id || '');
@@ -138,18 +125,17 @@ export default function Profile() {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error loading profile", title: "Error loading profile",
description: error.message, description: error.message
}); });
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const validateForm = () => { const validateForm = () => {
const result = profileEditSchema.safeParse(editForm); const result = profileEditSchema.safeParse(editForm);
if (!result.success) { if (!result.success) {
const errors: Record<string, string> = {}; const errors: Record<string, string> = {};
result.error.issues.forEach((issue) => { result.error.issues.forEach(issue => {
if (issue.path[0]) { if (issue.path[0]) {
errors[issue.path[0] as string] = issue.message; errors[issue.path[0] as string] = issue.message;
} }
@@ -157,28 +143,23 @@ export default function Profile() {
setFormErrors(errors); setFormErrors(errors);
return false; return false;
} }
if (!usernameValidation.isValid && editForm.username !== profile?.username) { if (!usernameValidation.isValid && editForm.username !== profile?.username) {
setFormErrors({ username: usernameValidation.error || 'Invalid username' }); setFormErrors({
username: usernameValidation.error || 'Invalid username'
});
return false; return false;
} }
setFormErrors({}); setFormErrors({});
return true; return true;
}; };
const handleSaveProfile = async () => { const handleSaveProfile = async () => {
if (!profile || !currentUser) return; if (!profile || !currentUser) return;
if (!validateForm()) return; if (!validateForm()) return;
const usernameChanged = editForm.username !== profile.username; const usernameChanged = editForm.username !== profile.username;
if (usernameChanged && !showUsernameDialog) { if (usernameChanged && !showUsernameDialog) {
setShowUsernameDialog(true); setShowUsernameDialog(true);
return; return;
} }
try { try {
const updateData: any = { const updateData: any = {
display_name: editForm.display_name, display_name: editForm.display_name,
@@ -186,73 +167,60 @@ export default function Profile() {
avatar_url: avatarUrl, avatar_url: avatarUrl,
avatar_image_id: avatarImageId avatar_image_id: avatarImageId
}; };
if (usernameChanged) { if (usernameChanged) {
updateData.username = editForm.username; updateData.username = editForm.username;
} }
const {
const { error } = await supabase error
.from('profiles') } = await supabase.from('profiles').update(updateData).eq('user_id', currentUser.id);
.update(updateData)
.eq('user_id', currentUser.id);
if (error) throw error; if (error) throw error;
setProfile(prev => prev ? { setProfile(prev => prev ? {
...prev, ...prev,
...updateData ...updateData
} : null); } : null);
setEditing(false); setEditing(false);
setShowUsernameDialog(false); setShowUsernameDialog(false);
if (usernameChanged) { if (usernameChanged) {
toast({ toast({
title: "Profile updated", title: "Profile updated",
description: "Your username and profile URL have been updated successfully.", description: "Your username and profile URL have been updated successfully."
}); });
// Navigate to new username URL // Navigate to new username URL
navigate(`/profile/${editForm.username}`); navigate(`/profile/${editForm.username}`);
} else { } else {
toast({ toast({
title: "Profile updated", title: "Profile updated",
description: "Your profile has been updated successfully.", description: "Your profile has been updated successfully."
}); });
} }
} catch (error: any) { } catch (error: any) {
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error updating profile", title: "Error updating profile",
description: error.message, description: error.message
}); });
} }
}; };
const confirmUsernameChange = () => { const confirmUsernameChange = () => {
setShowUsernameDialog(false); setShowUsernameDialog(false);
handleSaveProfile(); handleSaveProfile();
}; };
const handleAvatarUpload = async (urls: string[], imageId?: string) => { const handleAvatarUpload = async (urls: string[], imageId?: string) => {
if (!currentUser || !urls[0]) return; if (!currentUser || !urls[0]) return;
const newAvatarUrl = urls[0]; const newAvatarUrl = urls[0];
const newImageId = imageId || ''; const newImageId = imageId || '';
// Update local state immediately // Update local state immediately
setAvatarUrl(newAvatarUrl); setAvatarUrl(newAvatarUrl);
setAvatarImageId(newImageId); setAvatarImageId(newImageId);
try { try {
// Update database immediately // Update database immediately
const { error } = await supabase const {
.from('profiles') error
.update({ } = await supabase.from('profiles').update({
avatar_url: newAvatarUrl, avatar_url: newAvatarUrl,
avatar_image_id: newImageId avatar_image_id: newImageId
}) }).eq('user_id', currentUser.id);
.eq('user_id', currentUser.id);
if (error) throw error; if (error) throw error;
// Update local profile state // Update local profile state
@@ -266,29 +234,24 @@ export default function Profile() {
if (refreshProfile) { if (refreshProfile) {
await refreshProfile(); await refreshProfile();
} }
toast({ toast({
title: "Avatar updated", title: "Avatar updated",
description: "Your profile picture has been updated successfully.", description: "Your profile picture has been updated successfully."
}); });
} catch (error: any) { } catch (error: any) {
// Revert local state on error // Revert local state on error
setAvatarUrl(profile?.avatar_url || ''); setAvatarUrl(profile?.avatar_url || '');
setAvatarImageId(profile?.avatar_image_id || ''); setAvatarImageId(profile?.avatar_image_id || '');
toast({ toast({
variant: "destructive", variant: "destructive",
title: "Error updating avatar", title: "Error updating avatar",
description: error.message, description: error.message
}); });
} }
}; };
const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id; const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id;
if (loading) { if (loading) {
return ( return <div className="min-h-screen bg-background">
<div className="min-h-screen bg-background">
<Header /> <Header />
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<div className="animate-pulse space-y-6"> <div className="animate-pulse space-y-6">
@@ -297,13 +260,10 @@ export default function Profile() {
<div className="h-4 bg-muted rounded w-1/2"></div> <div className="h-4 bg-muted rounded w-1/2"></div>
</div> </div>
</div> </div>
</div> </div>;
);
} }
if (!profile) { if (!profile) {
return ( return <div className="min-h-screen bg-background">
<div className="min-h-screen bg-background">
<Header /> <Header />
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
<div className="text-center py-12"> <div className="text-center py-12">
@@ -318,12 +278,9 @@ export default function Profile() {
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>;
);
} }
return <div className="min-h-screen bg-background">
return (
<div className="min-h-screen bg-background">
<Header /> <Header />
<main className="container mx-auto px-4 py-8"> <main className="container mx-auto px-4 py-8">
@@ -333,77 +290,40 @@ 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 <PhotoUpload variant="avatar" maxFiles={1} existingPhotos={profile.avatar_url ? [profile.avatar_url] : []} onUploadComplete={handleAvatarUpload} currentImageId={avatarImageId} onError={error => {
variant="avatar" toast({
maxFiles={1} title: "Upload Error",
existingPhotos={profile.avatar_url ? [profile.avatar_url] : []} description: error,
onUploadComplete={handleAvatarUpload} variant: "destructive"
currentImageId={avatarImageId} });
onError={(error) => { }} className="mb-4" />
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 && ( {isOwnProfile && !editing && <Button variant="outline" size="sm" onClick={() => setEditing(true)}>
<Button
variant="outline"
size="sm"
onClick={() => setEditing(true)}
>
<Edit3 className="w-4 h-4 mr-2" /> <Edit3 className="w-4 h-4 mr-2" />
Edit Profile Edit Profile
</Button> </Button>}
)}
{!isOwnProfile && ( {!isOwnProfile && <UserBlockButton targetUserId={profile.user_id} targetUsername={profile.username} />}
<UserBlockButton
targetUserId={profile.user_id}
targetUsername={profile.username}
/>
)}
</div> </div>
</div> </div>
<div className="flex-1"> <div className="flex-1">
{editing && isOwnProfile ? ( {editing && isOwnProfile ? <div className="space-y-4">
<div className="space-y-4">
<div> <div>
<Label htmlFor="username">Username</Label> <Label htmlFor="username">Username</Label>
<div className="relative"> <div className="relative">
<Input <Input id="username" value={editForm.username} onChange={e => setEditForm(prev => ({
id="username" ...prev,
value={editForm.username} username: e.target.value
onChange={(e) => setEditForm(prev => ({ ...prev, username: e.target.value }))} }))} placeholder="your_username" className={`pr-10 ${formErrors.username ? 'border-destructive' : ''}`} />
placeholder="your_username"
className={`pr-10 ${formErrors.username ? 'border-destructive' : ''}`}
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2"> <div className="absolute right-3 top-1/2 -translate-y-1/2">
{usernameValidation.isChecking ? ( {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}
<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>
</div> </div>
{formErrors.username && ( {formErrors.username && <p className="text-sm text-destructive mt-1">{formErrors.username}</p>}
<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>}
{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"> <p className="text-xs text-muted-foreground mt-1">
Your profile URL will be /profile/{editForm.username} Your profile URL will be /profile/{editForm.username}
</p> </p>
@@ -411,105 +331,71 @@ export default function Profile() {
<div> <div>
<Label htmlFor="display_name">Display Name</Label> <Label htmlFor="display_name">Display Name</Label>
<Input <Input id="display_name" value={editForm.display_name} onChange={e => setEditForm(prev => ({
id="display_name" ...prev,
value={editForm.display_name} display_name: e.target.value
onChange={(e) => setEditForm(prev => ({ ...prev, display_name: e.target.value }))} }))} placeholder="Your display name" className={formErrors.display_name ? 'border-destructive' : ''} />
placeholder="Your display name" {formErrors.display_name && <p className="text-sm text-destructive mt-1">{formErrors.display_name}</p>}
className={formErrors.display_name ? 'border-destructive' : ''}
/>
{formErrors.display_name && (
<p className="text-sm text-destructive mt-1">{formErrors.display_name}</p>
)}
</div> </div>
<div> <div>
<Label htmlFor="bio">Bio</Label> <Label htmlFor="bio">Bio</Label>
<Textarea <Textarea id="bio" value={editForm.bio} onChange={e => setEditForm(prev => ({
id="bio" ...prev,
value={editForm.bio} bio: e.target.value
onChange={(e) => setEditForm(prev => ({ ...prev, bio: e.target.value }))} }))} placeholder="Tell us about yourself..." rows={3} className={formErrors.bio ? 'border-destructive' : ''} />
placeholder="Tell us about yourself..." {formErrors.bio && <p className="text-sm text-destructive mt-1">{formErrors.bio}</p>}
rows={3}
className={formErrors.bio ? 'border-destructive' : ''}
/>
{formErrors.bio && (
<p className="text-sm text-destructive mt-1">{formErrors.bio}</p>
)}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button onClick={handleSaveProfile} size="sm" disabled={usernameValidation.isChecking || editForm.username !== profile?.username && !usernameValidation.isValid}>
onClick={handleSaveProfile}
size="sm"
disabled={usernameValidation.isChecking || (editForm.username !== profile?.username && !usernameValidation.isValid)}
>
<Save className="w-4 h-4 mr-2" /> <Save className="w-4 h-4 mr-2" />
Save Changes Save Changes
</Button> </Button>
<Button <Button variant="outline" onClick={() => {
variant="outline" setEditing(false);
onClick={() => { setFormErrors({});
setEditing(false); setEditForm({
setFormErrors({}); username: profile?.username || '',
setEditForm({ display_name: profile?.display_name || '',
username: profile?.username || '', bio: profile?.bio || ''
display_name: profile?.display_name || '', });
bio: profile?.bio || '', }} size="sm">
});
}}
size="sm"
>
<X className="w-4 h-4 mr-2" /> <X className="w-4 h-4 mr-2" />
Cancel Cancel
</Button> </Button>
</div> </div>
</div> </div> : <div>
) : (
<div>
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h1 className="text-3xl font-bold"> <h1 className="text-3xl font-bold">
{profile.display_name || profile.username} {profile.display_name || profile.username}
</h1> </h1>
{profile.display_name && ( {profile.display_name && <Badge variant="secondary">@{profile.username}</Badge>}
<Badge variant="secondary">@{profile.username}</Badge>
)}
</div> </div>
{profile.bio && ( {profile.bio && <p className="text-muted-foreground mb-4 max-w-2xl">
<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">
<Calendar className="w-4 h-4" /> <Calendar className="w-4 h-4" />
Joined {new Date(profile.created_at).toLocaleDateString('en-US', { Joined {new Date(profile.created_at).toLocaleDateString('en-US', {
month: 'long', month: 'long',
year: 'numeric' year: 'numeric'
})} })}
</div> </div>
{/* Show pronouns if enabled */} {/* Show pronouns if enabled */}
{profile.show_pronouns && profile.preferred_pronouns && ( {profile.show_pronouns && profile.preferred_pronouns && <div className="flex items-center gap-1">
<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 location only if privacy allows */} {/* Show location only if privacy allows */}
{profile.location && ( {profile.location && <LocationDisplay location={profile.location} userId={profile.user_id} isOwnProfile={isOwnProfile} />}
<LocationDisplay
location={profile.location}
userId={profile.user_id}
isOwnProfile={isOwnProfile}
/>
)}
</div> </div>
</div> </div>}
)}
</div> </div>
</div> </div>
</CardContent> </CardContent>
@@ -549,7 +435,7 @@ export default function Profile() {
<TabsList className="grid w-full grid-cols-4"> <TabsList className="grid w-full grid-cols-4">
<TabsTrigger value="activity">Activity</TabsTrigger> <TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="reviews">Reviews</TabsTrigger> <TabsTrigger value="reviews">Reviews</TabsTrigger>
<TabsTrigger value="lists">Top Lists</TabsTrigger> <TabsTrigger value="lists">Rankings</TabsTrigger>
<TabsTrigger value="credits">Ride Credits</TabsTrigger> <TabsTrigger value="credits">Ride Credits</TabsTrigger>
</TabsList> </TabsList>
@@ -596,10 +482,8 @@ export default function Profile() {
<TabsContent value="lists" className="mt-6"> <TabsContent value="lists" className="mt-6">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Top Lists</CardTitle> <CardTitle>Rankings</CardTitle>
<CardDescription> <CardDescription>Personal rankings of rides</CardDescription>
Personal rankings and favorite collections
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-center py-12"> <div className="text-center py-12">
@@ -660,6 +544,5 @@ export default function Profile() {
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div> </div>;
);
} }