mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 13:51:14 -05:00
Visual edit in Lovable
This commit is contained in:
@@ -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>;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user