feat: Implement username change functionality

This commit is contained in:
gpt-engineer-app[bot]
2025-09-28 17:53:37 +00:00
parent 3e1ea57c11
commit 6b329f6887
6 changed files with 282 additions and 22 deletions

View File

@@ -10,7 +10,9 @@ 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 { useUsernameValidation } from '@/hooks/useUsernameValidation';
import {
User,
MapPin,
@@ -22,12 +24,16 @@ import {
Edit3,
Save,
X,
ArrowLeft
ArrowLeft,
Check,
AlertCircle,
Loader2
} from 'lucide-react';
import { Profile as ProfileType } from '@/types/database';
import { supabase } from '@/integrations/supabase/client';
import { useToast } from '@/hooks/use-toast';
import { PhotoUpload } from '@/components/upload/PhotoUpload';
import { profileEditSchema } from '@/lib/validation';
export default function Profile() {
const { username } = useParams<{ username?: string }>();
@@ -39,11 +45,17 @@ export default function Profile() {
const [editing, setEditing] = useState(false);
const [currentUser, setCurrentUser] = useState<any>(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>('');
// Username validation
const usernameValidation = useUsernameValidation(editForm.username, profile?.username);
useEffect(() => {
getCurrentUser();
@@ -72,6 +84,7 @@ export default function Profile() {
if (data) {
setProfile(data as ProfileType);
setEditForm({
username: data.username || '',
display_name: data.display_name || '',
bio: data.bio || '',
});
@@ -110,6 +123,7 @@ export default function Profile() {
if (data) {
setProfile(data as ProfileType);
setEditForm({
username: data.username || '',
display_name: data.display_name || '',
bio: data.bio || '',
});
@@ -128,35 +142,80 @@ export default function Profile() {
}
};
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({
display_name: editForm.display_name,
bio: editForm.bio,
avatar_url: avatarUrl,
avatar_image_id: avatarImageId
})
.update(updateData)
.eq('user_id', currentUser.id);
if (error) throw error;
setProfile(prev => prev ? {
...prev,
display_name: editForm.display_name,
bio: editForm.bio,
avatar_url: avatarUrl,
avatar_image_id: avatarImageId
...updateData
} : null);
setEditing(false);
toast({
title: "Profile updated",
description: "Your profile has been updated successfully.",
});
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: any) {
toast({
variant: "destructive",
@@ -166,6 +225,11 @@ export default function Profile() {
}
};
const confirmUsernameChange = () => {
setShowUsernameDialog(false);
handleSaveProfile();
};
const handleAvatarUpload = async (urls: string[], imageId?: string) => {
if (!currentUser || !urls[0]) return;
@@ -298,6 +362,42 @@ export default function Profile() {
<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
@@ -305,7 +405,11 @@ export default function Profile() {
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>
@@ -316,17 +420,33 @@ export default function Profile() {
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">
<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)}
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" />
@@ -493,6 +613,32 @@ export default function Profile() {
</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>
);
}