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 { useAuth } from '@/hooks/useAuth';
import { useUsernameValidation } from '@/hooks/useUsernameValidation';
import {
User,
MapPin,
Calendar,
Star,
Trophy,
Settings,
Camera,
Edit3,
Save,
X,
ArrowLeft,
Check,
AlertCircle,
Loader2,
UserX
} from 'lucide-react';
import { 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 { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
@@ -37,12 +21,19 @@ import { PhotoUpload } from '@/components/upload/PhotoUpload';
import { profileEditSchema } from '@/lib/validation';
import { LocationDisplay } from '@/components/profile/LocationDisplay';
import { UserBlockButton } from '@/components/profile/UserBlockButton';
export default function Profile() {
const { username } = useParams<{ username?: string }>();
const {
username
} = useParams<{
username?: string;
}>();
const navigate = useNavigate();
const { toast } = useToast();
const { refreshProfile } = useAuth();
const {
toast
} = useToast();
const {
refreshProfile
} = useAuth();
const [profile, setProfile] = useState<ProfileType | null>(null);
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
@@ -50,7 +41,7 @@ export default function Profile() {
const [editForm, setEditForm] = useState({
username: '',
display_name: '',
bio: '',
bio: ''
});
const [showUsernameDialog, setShowUsernameDialog] = useState(false);
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
@@ -59,7 +50,6 @@ export default function Profile() {
// Username validation
const usernameValidation = useUsernameValidation(editForm.username, profile?.username);
useEffect(() => {
getCurrentUser();
if (username) {
@@ -68,28 +58,27 @@ export default function Profile() {
fetchCurrentUserProfile();
}
}, [username]);
const getCurrentUser = async () => {
const { data: { user } } = await supabase.auth.getUser();
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();
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({
username: data.username || '',
display_name: data.display_name || '',
bio: data.bio || '',
bio: data.bio || ''
});
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
@@ -99,36 +88,34 @@ export default function Profile() {
toast({
variant: "destructive",
title: "Error loading profile",
description: error.message,
description: error.message
});
} finally {
setLoading(false);
}
};
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({
username: data.username || '',
display_name: data.display_name || '',
bio: data.bio || '',
bio: data.bio || ''
});
setAvatarUrl(data.avatar_url || '');
setAvatarImageId(data.avatar_image_id || '');
@@ -138,18 +125,17 @@ export default function Profile() {
toast({
variant: "destructive",
title: "Error loading profile",
description: error.message,
description: error.message
});
} finally {
setLoading(false);
}
};
const validateForm = () => {
const result = profileEditSchema.safeParse(editForm);
if (!result.success) {
const errors: Record<string, string> = {};
result.error.issues.forEach((issue) => {
result.error.issues.forEach(issue => {
if (issue.path[0]) {
errors[issue.path[0] as string] = issue.message;
}
@@ -157,28 +143,23 @@ export default function Profile() {
setFormErrors(errors);
return false;
}
if (!usernameValidation.isValid && editForm.username !== profile?.username) {
setFormErrors({ username: usernameValidation.error || 'Invalid 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,
@@ -186,73 +167,60 @@ export default function Profile() {
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);
const {
error
} = await supabase.from('profiles').update(updateData).eq('user_id', currentUser.id);
if (error) throw error;
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.",
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.",
description: "Your profile has been updated successfully."
});
}
} catch (error: any) {
toast({
variant: "destructive",
title: "Error updating profile",
description: error.message,
description: error.message
});
}
};
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({
const {
error
} = await supabase.from('profiles').update({
avatar_url: newAvatarUrl,
avatar_image_id: newImageId
})
.eq('user_id', currentUser.id);
}).eq('user_id', currentUser.id);
if (error) throw error;
// Update local profile state
@@ -266,29 +234,24 @@ export default function Profile() {
if (refreshProfile) {
await refreshProfile();
}
toast({
title: "Avatar updated",
description: "Your profile picture has been updated successfully.",
description: "Your profile picture has been updated successfully."
});
} catch (error: any) {
// Revert local state on error
setAvatarUrl(profile?.avatar_url || '');
setAvatarImageId(profile?.avatar_image_id || '');
toast({
variant: "destructive",
title: "Error updating avatar",
description: error.message,
description: error.message
});
}
};
const isOwnProfile = currentUser && profile && currentUser.id === profile.user_id;
if (loading) {
return (
<div className="min-h-screen bg-background">
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">
@@ -297,13 +260,10 @@ export default function Profile() {
<div className="h-4 bg-muted rounded w-1/2"></div>
</div>
</div>
</div>
);
</div>;
}
if (!profile) {
return (
<div className="min-h-screen bg-background">
return <div className="min-h-screen bg-background">
<Header />
<div className="container mx-auto px-4 py-8">
<div className="text-center py-12">
@@ -318,12 +278,9 @@ export default function Profile() {
</Button>
</div>
</div>
</div>
);
</div>;
}
return (
<div className="min-h-screen bg-background">
return <div className="min-h-screen bg-background">
<Header />
<main className="container mx-auto px-4 py-8">
@@ -333,77 +290,40 @@ 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}
existingPhotos={profile.avatar_url ? [profile.avatar_url] : []}
onUploadComplete={handleAvatarUpload}
currentImageId={avatarImageId}
onError={(error) => {
<PhotoUpload variant="avatar" maxFiles={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"
/>
}} className="mb-4" />
<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)}>
<Edit3 className="w-4 h-4 mr-2" />
Edit Profile
</Button>
)}
</Button>}
{!isOwnProfile && (
<UserBlockButton
targetUserId={profile.user_id}
targetUsername={profile.username}
/>
)}
{!isOwnProfile && <UserBlockButton targetUserId={profile.user_id} targetUsername={profile.username} />}
</div>
</div>
<div className="flex-1">
{editing && isOwnProfile ? (
<div className="space-y-4">
{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' : ''}`}
/>
<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}
{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>
)}
{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>
@@ -411,76 +331,51 @@ export default function Profile() {
<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>
)}
<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>
)}
<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)}
>
<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={() => {
<Button variant="outline" onClick={() => {
setEditing(false);
setFormErrors({});
setEditForm({
username: profile?.username || '',
display_name: profile?.display_name || '',
bio: profile?.bio || '',
bio: profile?.bio || ''
});
}}
size="sm"
>
}} size="sm">
<X className="w-4 h-4 mr-2" />
Cancel
</Button>
</div>
</div>
) : (
<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>
)}
{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 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">
@@ -492,24 +387,15 @@ export default function Profile() {
</div>
{/* Show pronouns if enabled */}
{profile.show_pronouns && profile.preferred_pronouns && (
<div className="flex items-center gap-1">
{profile.show_pronouns && profile.preferred_pronouns && <div className="flex items-center gap-1">
<User className="w-4 h-4" />
{profile.preferred_pronouns}
</div>
)}
</div>}
{/* Show location only if privacy allows */}
{profile.location && (
<LocationDisplay
location={profile.location}
userId={profile.user_id}
isOwnProfile={isOwnProfile}
/>
)}
{profile.location && <LocationDisplay location={profile.location} userId={profile.user_id} isOwnProfile={isOwnProfile} />}
</div>
</div>
)}
</div>}
</div>
</div>
</CardContent>
@@ -549,7 +435,7 @@ export default function Profile() {
<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="lists">Rankings</TabsTrigger>
<TabsTrigger value="credits">Ride Credits</TabsTrigger>
</TabsList>
@@ -596,10 +482,8 @@ export default function Profile() {
<TabsContent value="lists" className="mt-6">
<Card>
<CardHeader>
<CardTitle>Top Lists</CardTitle>
<CardDescription>
Personal rankings and favorite collections
</CardDescription>
<CardTitle>Rankings</CardTitle>
<CardDescription>Personal rankings of rides</CardDescription>
</CardHeader>
<CardContent>
<div className="text-center py-12">
@@ -660,6 +544,5 @@ export default function Profile() {
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
</div>;
}