Refactor user settings implementation

This commit is contained in:
gpt-engineer-app[bot]
2025-09-28 19:54:33 +00:00
parent 7fc30413ad
commit 01837bc999
12 changed files with 2456 additions and 0 deletions

View File

@@ -0,0 +1,386 @@
import { useState, useEffect } 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 { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Card, CardContent, CardDescription, CardHeader } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { Switch } from '@/components/ui/switch';
import { useToast } from '@/hooks/use-toast';
import { useAuth } from '@/hooks/useAuth';
import { supabase } from '@/integrations/supabase/client';
import { MapPin, Calendar, Globe, Accessibility } from 'lucide-react';
const locationSchema = z.object({
date_of_birth: z.string().optional(),
preferred_pronouns: z.string().max(20).optional(),
timezone: z.string(),
preferred_language: z.string(),
location_id: z.string().optional()
});
type LocationFormData = z.infer<typeof locationSchema>;
interface AccessibilityOptions {
font_size: 'small' | 'medium' | 'large';
high_contrast: boolean;
reduced_motion: boolean;
}
export function LocationTab() {
const { user, profile, refreshProfile } = useAuth();
const { toast } = useToast();
const [loading, setLoading] = useState(false);
const [locations, setLocations] = useState<any[]>([]);
const [accessibility, setAccessibility] = useState<AccessibilityOptions>({
font_size: 'medium',
high_contrast: false,
reduced_motion: false
});
const form = useForm<LocationFormData>({
resolver: zodResolver(locationSchema),
defaultValues: {
date_of_birth: profile?.date_of_birth || '',
preferred_pronouns: profile?.preferred_pronouns || '',
timezone: profile?.timezone || 'UTC',
preferred_language: profile?.preferred_language || 'en',
location_id: profile?.location_id || ''
}
});
useEffect(() => {
fetchLocations();
fetchAccessibilityPreferences();
}, [user]);
const fetchLocations = async () => {
try {
const { data, error } = await supabase
.from('locations')
.select('id, name, city, state_province, country')
.order('name');
if (error) throw error;
setLocations(data || []);
} catch (error) {
console.error('Error fetching locations:', error);
}
};
const fetchAccessibilityPreferences = async () => {
if (!user) return;
try {
const { data, error } = await supabase
.from('user_preferences')
.select('accessibility_options')
.eq('user_id', user.id)
.maybeSingle();
if (error && error.code !== 'PGRST116') {
console.error('Error fetching accessibility preferences:', error);
return;
}
if (data?.accessibility_options) {
setAccessibility(data.accessibility_options as any);
}
} catch (error) {
console.error('Error fetching accessibility preferences:', error);
}
};
const onSubmit = async (data: LocationFormData) => {
if (!user) return;
setLoading(true);
try {
const { error } = await supabase
.from('profiles')
.update({
date_of_birth: data.date_of_birth || null,
preferred_pronouns: data.preferred_pronouns || null,
timezone: data.timezone,
preferred_language: data.preferred_language,
location_id: data.location_id || null,
updated_at: new Date().toISOString()
})
.eq('user_id', user.id);
if (error) throw error;
await refreshProfile();
toast({
title: 'Information updated',
description: 'Your location and personal information has been successfully updated.'
});
} catch (error: any) {
toast({
title: 'Error',
description: error.message || 'Failed to update information',
variant: 'destructive'
});
} finally {
setLoading(false);
}
};
const saveAccessibilityPreferences = async () => {
if (!user) return;
try {
const { error } = await supabase
.from('user_preferences')
.upsert([{
user_id: user.id,
accessibility_options: accessibility as any,
updated_at: new Date().toISOString()
}]);
if (error) throw error;
toast({
title: 'Accessibility preferences saved',
description: 'Your accessibility settings have been updated.'
});
} catch (error: any) {
toast({
title: 'Error',
description: error.message || 'Failed to save accessibility preferences',
variant: 'destructive'
});
}
};
const updateAccessibility = (key: keyof AccessibilityOptions, value: any) => {
setAccessibility(prev => ({ ...prev, [key]: value }));
};
const timezones = [
'UTC',
'America/New_York',
'America/Chicago',
'America/Denver',
'America/Los_Angeles',
'America/Toronto',
'Europe/London',
'Europe/Berlin',
'Europe/Paris',
'Asia/Tokyo',
'Asia/Shanghai',
'Australia/Sydney'
];
return (
<div className="space-y-8">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Location Settings */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<MapPin className="w-5 h-5" />
<h3 className="text-lg font-medium">Location Settings</h3>
</div>
<Card>
<CardHeader>
<CardDescription>
Set your location for better personalized content and timezone display.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="location_id">Home Location</Label>
<Select
value={form.watch('location_id')}
onValueChange={(value) => form.setValue('location_id', value)}
>
<SelectTrigger>
<SelectValue placeholder="Select your location" />
</SelectTrigger>
<SelectContent>
<SelectItem value="">No location selected</SelectItem>
{locations.map((location) => (
<SelectItem key={location.id} value={location.id}>
{location.name}
{location.city && `, ${location.city}`}
{location.state_province && `, ${location.state_province}`}
{location.country && `, ${location.country}`}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Your location helps us show relevant parks and events near you.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="timezone">Timezone</Label>
<Select
value={form.watch('timezone')}
onValueChange={(value) => form.setValue('timezone', value)}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{timezones.map((tz) => (
<SelectItem key={tz} value={tz}>
{tz}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Used to display dates and times in your local timezone.
</p>
</div>
</CardContent>
</Card>
</div>
<Separator />
{/* Personal Information */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5" />
<h3 className="text-lg font-medium">Personal Information</h3>
</div>
<Card>
<CardHeader>
<CardDescription>
Optional personal information that can be displayed on your profile.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="date_of_birth">Date of Birth</Label>
<Input
id="date_of_birth"
type="date"
{...form.register('date_of_birth')}
/>
<p className="text-sm text-muted-foreground">
Used to calculate your age if you choose to display it.
</p>
</div>
<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"
/>
<p className="text-sm text-muted-foreground">
How you'd like others to refer to you.
</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>
</CardContent>
</Card>
</div>
<div className="flex justify-end">
<Button type="submit" disabled={loading}>
{loading ? 'Saving...' : 'Save Information'}
</Button>
</div>
</form>
<Separator />
{/* Accessibility Options */}
<div className="space-y-4">
<div className="flex items-center gap-2">
<Accessibility className="w-5 h-5" />
<h3 className="text-lg font-medium">Accessibility Options</h3>
</div>
<Card>
<CardHeader>
<CardDescription>
Customize the interface to meet your accessibility needs.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-3">
<Label>Font Size</Label>
<Select
value={accessibility.font_size}
onValueChange={(value: 'small' | 'medium' | 'large') =>
updateAccessibility('font_size', value)
}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="small">Small</SelectItem>
<SelectItem value="medium">Medium (Default)</SelectItem>
<SelectItem value="large">Large</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label>High Contrast</Label>
<p className="text-sm text-muted-foreground">
Increase contrast for better visibility
</p>
</div>
<Switch
checked={accessibility.high_contrast}
onCheckedChange={(checked) => updateAccessibility('high_contrast', checked)}
/>
</div>
<div className="flex items-center justify-between">
<div className="space-y-1">
<Label>Reduced Motion</Label>
<p className="text-sm text-muted-foreground">
Minimize animations and transitions
</p>
</div>
<Switch
checked={accessibility.reduced_motion}
onCheckedChange={(checked) => updateAccessibility('reduced_motion', checked)}
/>
</div>
<div className="flex justify-end">
<Button onClick={saveAccessibilityPreferences}>
Save Accessibility Settings
</Button>
</div>
</CardContent>
</Card>
</div>
</div>
);
}