Fix: Implement settings cleanup plan

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 20:22:27 +00:00
parent b8f2889b1d
commit 439a7f4abf
3 changed files with 125 additions and 281 deletions

View File

@@ -5,15 +5,13 @@ import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label'; 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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
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 { useProfile } from '@/hooks/useProfile'; import { useProfile } from '@/hooks/useProfile';
import { supabase } from '@/integrations/supabase/client'; import { supabase } from '@/integrations/supabase/client';
import { User, Upload, Trash2, Mail, AlertCircle, X, Check, Loader2 } from 'lucide-react'; import { Trash2, Mail, AlertCircle, X, Check, Loader2 } from 'lucide-react';
import { PhotoUpload } from '@/components/upload/PhotoUpload'; import { PhotoUpload } from '@/components/upload/PhotoUpload';
import { notificationService } from '@/lib/notificationService'; import { notificationService } from '@/lib/notificationService';
import { EmailChangeDialog } from './EmailChangeDialog'; import { EmailChangeDialog } from './EmailChangeDialog';
@@ -22,7 +20,7 @@ import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AccountDeletionDialog } from './AccountDeletionDialog'; import { AccountDeletionDialog } from './AccountDeletionDialog';
import { DeletionStatusBanner } from './DeletionStatusBanner'; import { DeletionStatusBanner } from './DeletionStatusBanner';
import { EmailChangeStatus } from './EmailChangeStatus'; import { EmailChangeStatus } from './EmailChangeStatus';
import { usernameSchema, displayNameSchema, bioSchema, personalLocationSchema, preferredPronounsSchema } from '@/lib/validation'; import { usernameSchema, displayNameSchema, bioSchema } from '@/lib/validation';
import { z } from 'zod'; import { z } from 'zod';
import { AccountDeletionRequest } from '@/types/database'; import { AccountDeletionRequest } from '@/types/database';
import { handleError, handleSuccess, AppError } from '@/lib/errorHandler'; import { handleError, handleSuccess, AppError } from '@/lib/errorHandler';
@@ -35,11 +33,7 @@ import { cn } from '@/lib/utils';
const profileSchema = z.object({ const profileSchema = z.object({
username: usernameSchema, username: usernameSchema,
display_name: displayNameSchema, display_name: displayNameSchema,
bio: bioSchema, bio: bioSchema
preferred_pronouns: preferredPronounsSchema,
show_pronouns: z.boolean(),
preferred_language: z.string(),
personal_location: personalLocationSchema
}); });
type ProfileFormData = z.infer<typeof profileSchema>; type ProfileFormData = z.infer<typeof profileSchema>;
@@ -67,11 +61,7 @@ export function AccountProfileTab() {
defaultValues: { defaultValues: {
username: profile?.username || '', username: profile?.username || '',
display_name: profile?.display_name || '', display_name: profile?.display_name || '',
bio: profile?.bio || '', bio: profile?.bio || ''
preferred_pronouns: profile?.preferred_pronouns || '',
show_pronouns: profile?.show_pronouns || false,
preferred_language: profile?.preferred_language || 'en',
personal_location: profile?.personal_location || ''
} }
}); });
@@ -122,9 +112,7 @@ export function AccountProfileTab() {
const { data: result, error } = await supabase.rpc('update_profile', { const { data: result, error } = await supabase.rpc('update_profile', {
p_username: data.username, p_username: data.username,
p_display_name: data.display_name || null, p_display_name: data.display_name || null,
p_bio: data.bio || null, p_bio: data.bio || null
p_preferred_pronouns: data.preferred_pronouns || null,
p_personal_location: data.personal_location || null
}); });
if (error) { if (error) {
@@ -340,8 +328,6 @@ export function AccountProfileTab() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <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="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="username"> <Label htmlFor="username">
@@ -411,55 +397,6 @@ export function AccountProfileTab() {
)} )}
</div> </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"
/>
{form.formState.errors.preferred_pronouns && (
<p className="text-sm text-destructive">
{form.formState.errors.preferred_pronouns.message}
</p>
)}
</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>
<div className="space-y-2">
<Label htmlFor="personal_location">Personal Location</Label>
<Input
id="personal_location"
{...form.register('personal_location')}
placeholder="e.g., Los Angeles, CA"
/>
{form.formState.errors.personal_location && (
<p className="text-sm text-destructive">
{form.formState.errors.personal_location.message}
</p>
)}
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Button <Button
type="submit" type="submit"

View File

@@ -340,97 +340,12 @@ export function DataExportTab() {
<CardHeader> <CardHeader>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Activity className="w-5 h-5" /> <Activity className="w-5 h-5" />
<CardTitle>Account Activity</CardTitle> <CardTitle>Recent Activity</CardTitle>
</div> </div>
<CardDescription> <CardDescription>
Recent account activity and changes Recent account activity and changes
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-6">
{rateLimited && nextAvailableAt && (
<div className="flex items-start gap-3 p-4 border border-yellow-500/20 bg-yellow-500/10 rounded-lg">
<Clock className="w-5 h-5 text-yellow-500 mt-0.5" />
<div className="space-y-1">
<p className="text-sm font-medium">Rate Limited</p>
<p className="text-sm text-muted-foreground">
You can export your data once per hour. Next export available at{' '}
{formatDate(nextAvailableAt)}.
</p>
</div>
</div>
)}
<div className="space-y-4">
<p className="text-sm text-muted-foreground">
Choose what to include in your export:
</p>
<div className="space-y-3">
<div className="flex items-center justify-between">
<Label htmlFor="include_reviews">Include Reviews</Label>
<Switch
id="include_reviews"
checked={exportOptions.include_reviews}
onCheckedChange={(checked) =>
setExportOptions({ ...exportOptions, include_reviews: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="include_lists">Include Lists</Label>
<Switch
id="include_lists"
checked={exportOptions.include_lists}
onCheckedChange={(checked) =>
setExportOptions({ ...exportOptions, include_lists: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="include_activity_log">Include Activity Log</Label>
<Switch
id="include_activity_log"
checked={exportOptions.include_activity_log}
onCheckedChange={(checked) =>
setExportOptions({ ...exportOptions, include_activity_log: checked })
}
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="include_preferences">Include Preferences</Label>
<Switch
id="include_preferences"
checked={exportOptions.include_preferences}
onCheckedChange={(checked) =>
setExportOptions({ ...exportOptions, include_preferences: checked })
}
/>
</div>
</div>
</div>
<div className="flex items-start gap-3 p-4 border rounded-lg">
<AlertCircle className="w-5 h-5 text-blue-500 mt-0.5" />
<div className="space-y-1">
<p className="text-sm font-medium">GDPR Compliance</p>
<p className="text-sm text-muted-foreground">
This export includes all personal data we store about you. You can use this for backup purposes or to migrate to another service.
</p>
</div>
</div>
<Button
onClick={handleDataExport}
disabled={exporting || rateLimited}
className="w-full"
>
<Download className="w-4 h-4 mr-2" />
{exporting ? 'Exporting Data...' : 'Export My Data'}
</Button>
</CardContent>
<CardContent> <CardContent>
{recentActivity.length === 0 ? ( {recentActivity.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4"> <p className="text-sm text-muted-foreground text-center py-4">

View File

@@ -237,6 +237,7 @@ export function SecurityTab() {
icon: <DiscordIcon className="w-5 h-5" /> icon: <DiscordIcon className="w-5 h-5" />
} }
]; ];
return ( return (
<> <>
<PasswordUpdateDialog <PasswordUpdateDialog
@@ -247,16 +248,16 @@ export function SecurityTab() {
}} }}
/> />
<div className="space-y-8"> <div className="space-y-6">
{/* Password Section - Conditional based on auth method */} {/* Password + Connected Accounts Grid */}
<div className="space-y-4"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="flex items-center gap-2"> {/* Password Section */}
<Key className="w-5 h-5" />
<h3 className="text-lg font-medium">{hasPassword ? 'Change Password' : 'Add Password'}</h3>
</div>
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center gap-2">
<Key className="w-5 h-5" />
<CardTitle>{hasPassword ? 'Change Password' : 'Add Password'}</CardTitle>
</div>
<CardDescription> <CardDescription>
{hasPassword ? ( {hasPassword ? (
<>Update your password to keep your account secure.</> <>Update your password to keep your account secure.</>
@@ -265,7 +266,7 @@ export function SecurityTab() {
)} )}
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent>
{hasPassword ? ( {hasPassword ? (
<Button onClick={() => setPasswordDialogOpen(true)}> <Button onClick={() => setPasswordDialogOpen(true)}>
Change Password Change Password
@@ -284,22 +285,16 @@ export function SecurityTab() {
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div>
<Separator />
{/* Social Login Connections */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Globe className="w-5 h-5" />
<h3 className="text-lg font-medium">Connected Accounts</h3>
</div>
{/* Connected Accounts */}
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center gap-2">
<Globe className="w-5 h-5" />
<CardTitle>Connected Accounts</CardTitle>
</div>
<CardDescription> <CardDescription>
Manage your social login connections for easier access to your account. Manage your social login connections for easier access to your account.
To enable social providers, configure them in your Supabase Auth settings.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
@@ -364,31 +359,29 @@ export function SecurityTab() {
</Card> </Card>
</div> </div>
<Separator /> {/* Two-Factor Authentication - Full Width */}
{/* Two-Factor Authentication */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Smartphone className="w-5 h-5" />
<h3 className="text-lg font-medium">Two-Factor Authentication</h3>
</div>
<TOTPSetup />
</div>
<Separator />
{/* Active Sessions & Login History */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Shield className="w-5 h-5" />
<h3 className="text-lg font-medium">
Active Sessions {sessions.length > 0 && `(${sessions.length})`}
</h3>
</div>
<Card> <Card>
<CardHeader> <CardHeader>
<div className="flex items-center gap-2">
<Smartphone className="w-5 h-5" />
<CardTitle>Two-Factor Authentication</CardTitle>
</div>
<CardDescription>
Add an extra layer of security to your account with TOTP authentication
</CardDescription>
</CardHeader>
<CardContent>
<TOTPSetup />
</CardContent>
</Card>
{/* Active Sessions - Full Width */}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<Shield className="w-5 h-5" />
<CardTitle>Active Sessions {sessions.length > 0 && `(${sessions.length})`}</CardTitle>
</div>
<CardDescription> <CardDescription>
Review and manage your active login sessions across all devices Review and manage your active login sessions across all devices
</CardDescription> </CardDescription>
@@ -453,7 +446,6 @@ export function SecurityTab() {
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div>
<SessionRevokeConfirmDialog <SessionRevokeConfirmDialog
open={!!sessionToRevoke} open={!!sessionToRevoke}