mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 08:11:13 -05:00
feat: Implement username change functionality
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user