mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
Refactor user settings implementation
This commit is contained in:
316
src/components/settings/AccountProfileTab.tsx
Normal file
316
src/components/settings/AccountProfileTab.tsx
Normal file
@@ -0,0 +1,316 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger } from '@/components/ui/alert-dialog';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
import { User, Upload, Trash2 } from 'lucide-react';
|
||||
import { SimplePhotoUpload } from './SimplePhotoUpload';
|
||||
|
||||
const profileSchema = z.object({
|
||||
username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_-]+$/),
|
||||
display_name: z.string().max(50).optional(),
|
||||
bio: z.string().max(500).optional(),
|
||||
preferred_pronouns: z.string().max(20).optional(),
|
||||
show_pronouns: z.boolean(),
|
||||
preferred_language: z.string()
|
||||
});
|
||||
|
||||
type ProfileFormData = z.infer<typeof profileSchema>;
|
||||
|
||||
export function AccountProfileTab() {
|
||||
const { user, profile, refreshProfile } = useAuth();
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [avatarLoading, setAvatarLoading] = useState(false);
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||
|
||||
const form = useForm<ProfileFormData>({
|
||||
resolver: zodResolver(profileSchema),
|
||||
defaultValues: {
|
||||
username: profile?.username || '',
|
||||
display_name: profile?.display_name || '',
|
||||
bio: profile?.bio || '',
|
||||
preferred_pronouns: profile?.preferred_pronouns || '',
|
||||
show_pronouns: profile?.show_pronouns || false,
|
||||
preferred_language: profile?.preferred_language || 'en'
|
||||
}
|
||||
});
|
||||
|
||||
const onSubmit = async (data: ProfileFormData) => {
|
||||
if (!user) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
username: data.username,
|
||||
display_name: data.display_name || null,
|
||||
bio: data.bio || null,
|
||||
preferred_pronouns: data.preferred_pronouns || null,
|
||||
show_pronouns: data.show_pronouns,
|
||||
preferred_language: data.preferred_language,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
await refreshProfile();
|
||||
toast({
|
||||
title: 'Profile updated',
|
||||
description: 'Your profile has been successfully updated.'
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message || 'Failed to update profile',
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (imageId: string, imageUrl: string) => {
|
||||
if (!user) return;
|
||||
|
||||
setAvatarLoading(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('profiles')
|
||||
.update({
|
||||
avatar_image_id: imageId,
|
||||
avatar_url: imageUrl,
|
||||
updated_at: new Date().toISOString()
|
||||
})
|
||||
.eq('user_id', user.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
await refreshProfile();
|
||||
toast({
|
||||
title: 'Avatar updated',
|
||||
description: 'Your avatar has been successfully updated.'
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message || 'Failed to update avatar',
|
||||
variant: 'destructive'
|
||||
});
|
||||
} finally {
|
||||
setAvatarLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAccount = async () => {
|
||||
if (!user) return;
|
||||
|
||||
try {
|
||||
// This would typically involve multiple steps:
|
||||
// 1. Anonymize or delete user data
|
||||
// 2. Delete the auth user
|
||||
// For now, we'll just show a message
|
||||
toast({
|
||||
title: 'Account deletion requested',
|
||||
description: 'Please contact support to complete account deletion.',
|
||||
variant: 'destructive'
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message || 'Failed to delete account',
|
||||
variant: 'destructive'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Profile Picture */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Profile Picture</h3>
|
||||
<div className="flex items-center gap-6">
|
||||
<Avatar className="w-20 h-20">
|
||||
<AvatarImage src={profile?.avatar_url || undefined} />
|
||||
<AvatarFallback className="text-lg">
|
||||
{profile?.display_name?.[0] || profile?.username?.[0] || <User className="w-8 h-8" />}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="space-y-2">
|
||||
<SimplePhotoUpload
|
||||
onUpload={handleAvatarUpload}
|
||||
disabled={avatarLoading}
|
||||
>
|
||||
<Button variant="outline" disabled={avatarLoading}>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
{avatarLoading ? 'Uploading...' : 'Change Avatar'}
|
||||
</Button>
|
||||
</SimplePhotoUpload>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
JPG, PNG or GIF. Max size 5MB.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Profile Information */}
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<h3 className="text-lg font-medium">Profile Information</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="username">Username *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
{...form.register('username')}
|
||||
placeholder="Enter your username"
|
||||
/>
|
||||
{form.formState.errors.username && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.username.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="display_name">Display Name</Label>
|
||||
<Input
|
||||
id="display_name"
|
||||
{...form.register('display_name')}
|
||||
placeholder="Enter your display name"
|
||||
/>
|
||||
{form.formState.errors.display_name && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.display_name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="bio">Bio</Label>
|
||||
<Textarea
|
||||
id="bio"
|
||||
{...form.register('bio')}
|
||||
placeholder="Tell us about yourself..."
|
||||
rows={4}
|
||||
/>
|
||||
{form.formState.errors.bio && (
|
||||
<p className="text-sm text-destructive">
|
||||
{form.formState.errors.bio.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="preferred_pronouns">Preferred Pronouns</Label>
|
||||
<Input
|
||||
id="preferred_pronouns"
|
||||
{...form.register('preferred_pronouns')}
|
||||
placeholder="e.g., they/them, she/her, he/him"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="preferred_language">Preferred Language</Label>
|
||||
<Select
|
||||
value={form.watch('preferred_language')}
|
||||
onValueChange={(value) => form.setValue('preferred_language', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="en">English</SelectItem>
|
||||
<SelectItem value="es">Español</SelectItem>
|
||||
<SelectItem value="fr">Français</SelectItem>
|
||||
<SelectItem value="de">Deutsch</SelectItem>
|
||||
<SelectItem value="it">Italiano</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Saving...' : 'Save Changes'}
|
||||
</Button>
|
||||
</form>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Account Information */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-medium">Account Information</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4 bg-muted/50 rounded-lg">
|
||||
<div>
|
||||
<p className="text-sm font-medium">Email</p>
|
||||
<p className="text-sm text-muted-foreground">{user?.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-medium">Account Created</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{profile?.created_at ? new Date(profile.created_at).toLocaleDateString() : 'N/A'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Danger Zone */}
|
||||
<Card className="border-destructive">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-destructive">Danger Zone</CardTitle>
|
||||
<CardDescription>
|
||||
These actions cannot be undone. Please be careful.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<AlertDialog open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" className="w-fit">
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Delete Account
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your account
|
||||
and remove all your data from our servers, including your reviews,
|
||||
ride credits, and profile information.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={handleDeleteAccount}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Yes, delete my account
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user