mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:11:11 -05:00
463 lines
16 KiB
TypeScript
463 lines
16 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { useForm } from 'react-hook-form';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Switch } from '@/components/ui/switch';
|
|
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 { handleError, handleSuccess, AppError } from '@/lib/errorHandler';
|
|
import { logger } from '@/lib/logger';
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
import { useProfile } from '@/hooks/useProfile';
|
|
import { supabase } from '@/integrations/supabase/client';
|
|
import { Eye, UserX, Shield, Search } from 'lucide-react';
|
|
import { BlockedUsers } from '@/components/privacy/BlockedUsers';
|
|
import type { PrivacySettings, PrivacyFormData } from '@/types/privacy';
|
|
import { privacyFormSchema, privacySettingsSchema, DEFAULT_PRIVACY_SETTINGS } from '@/lib/privacyValidation';
|
|
import { z } from 'zod';
|
|
|
|
export function PrivacyTab() {
|
|
const { user } = useAuth();
|
|
const { data: profile, refreshProfile } = useProfile(user?.id);
|
|
const [loading, setLoading] = useState(false);
|
|
const [preferences, setPreferences] = useState<PrivacySettings | null>(null);
|
|
|
|
const form = useForm<PrivacyFormData>({
|
|
resolver: zodResolver(privacyFormSchema),
|
|
defaultValues: {
|
|
privacy_level: (profile?.privacy_level === 'public' || profile?.privacy_level === 'private')
|
|
? profile.privacy_level
|
|
: 'public',
|
|
show_pronouns: profile?.show_pronouns || false,
|
|
...DEFAULT_PRIVACY_SETTINGS
|
|
}
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchPreferences();
|
|
}, [user]);
|
|
|
|
const fetchPreferences = async () => {
|
|
if (!user) return;
|
|
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('user_preferences')
|
|
.select('privacy_settings')
|
|
.eq('user_id', user.id)
|
|
.maybeSingle();
|
|
|
|
if (error && error.code !== 'PGRST116') {
|
|
logger.error('Failed to fetch privacy preferences', {
|
|
userId: user.id,
|
|
action: 'fetch_privacy_preferences',
|
|
error: error.message,
|
|
errorCode: error.code
|
|
});
|
|
throw error;
|
|
}
|
|
|
|
if (data?.privacy_settings) {
|
|
const parseResult = privacySettingsSchema.safeParse(data.privacy_settings);
|
|
|
|
if (parseResult.success) {
|
|
setPreferences(parseResult.data);
|
|
form.reset({
|
|
privacy_level: (profile?.privacy_level === 'public' || profile?.privacy_level === 'private')
|
|
? profile.privacy_level
|
|
: 'public',
|
|
show_pronouns: profile?.show_pronouns || false,
|
|
...parseResult.data
|
|
});
|
|
} else {
|
|
console.warn('Invalid privacy settings, reinitializing with defaults', {
|
|
errors: parseResult.error.issues
|
|
});
|
|
await initializePreferences();
|
|
}
|
|
} else {
|
|
await initializePreferences();
|
|
}
|
|
} catch (error) {
|
|
logger.error('Error fetching privacy preferences', {
|
|
userId: user.id,
|
|
action: 'fetch_privacy_preferences',
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
|
|
handleError(error, {
|
|
action: 'Load privacy settings',
|
|
userId: user.id
|
|
});
|
|
}
|
|
};
|
|
|
|
const initializePreferences = async () => {
|
|
if (!user) return;
|
|
|
|
try {
|
|
const { error } = await supabase
|
|
.from('user_preferences')
|
|
.insert([{
|
|
user_id: user.id,
|
|
privacy_settings: DEFAULT_PRIVACY_SETTINGS
|
|
}]);
|
|
|
|
if (error) {
|
|
logger.error('Failed to initialize privacy preferences', {
|
|
userId: user.id,
|
|
action: 'initialize_privacy_preferences',
|
|
error: error.message
|
|
});
|
|
throw error;
|
|
}
|
|
|
|
setPreferences(DEFAULT_PRIVACY_SETTINGS);
|
|
form.reset({
|
|
privacy_level: (profile?.privacy_level === 'public' || profile?.privacy_level === 'private')
|
|
? profile.privacy_level
|
|
: 'public',
|
|
show_pronouns: profile?.show_pronouns || false,
|
|
...DEFAULT_PRIVACY_SETTINGS
|
|
});
|
|
} catch (error) {
|
|
logger.error('Error initializing privacy preferences', {
|
|
userId: user.id,
|
|
action: 'initialize_privacy_preferences',
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
|
|
handleError(error, {
|
|
action: 'Initialize privacy settings',
|
|
userId: user.id
|
|
});
|
|
}
|
|
};
|
|
|
|
const onSubmit = async (data: PrivacyFormData) => {
|
|
if (!user) return;
|
|
|
|
setLoading(true);
|
|
|
|
try {
|
|
// Validate the form data
|
|
const validated = privacyFormSchema.parse(data);
|
|
|
|
// Update profile privacy settings
|
|
const { error: profileError } = await supabase
|
|
.from('profiles')
|
|
.update({
|
|
privacy_level: validated.privacy_level,
|
|
show_pronouns: validated.show_pronouns,
|
|
updated_at: new Date().toISOString()
|
|
})
|
|
.eq('user_id', user.id);
|
|
|
|
if (profileError) {
|
|
logger.error('Failed to update profile privacy', {
|
|
userId: user.id,
|
|
action: 'update_profile_privacy',
|
|
error: profileError.message,
|
|
errorCode: profileError.code
|
|
});
|
|
throw profileError;
|
|
}
|
|
|
|
// Extract privacy settings (exclude profile fields)
|
|
const { privacy_level, show_pronouns, ...privacySettings } = validated;
|
|
|
|
// Update user preferences
|
|
const { error: prefsError } = await supabase
|
|
.from('user_preferences')
|
|
.upsert([{
|
|
user_id: user.id,
|
|
privacy_settings: privacySettings,
|
|
updated_at: new Date().toISOString()
|
|
}]);
|
|
|
|
if (prefsError) {
|
|
logger.error('Failed to update privacy preferences', {
|
|
userId: user.id,
|
|
action: 'update_privacy_preferences',
|
|
error: prefsError.message,
|
|
errorCode: prefsError.code
|
|
});
|
|
throw prefsError;
|
|
}
|
|
|
|
// Log to audit trail
|
|
await supabase.from('profile_audit_log').insert([{
|
|
user_id: user.id,
|
|
changed_by: user.id,
|
|
action: 'privacy_settings_updated',
|
|
changes: {
|
|
previous: preferences,
|
|
updated: privacySettings,
|
|
timestamp: new Date().toISOString()
|
|
} as any
|
|
}]);
|
|
|
|
await refreshProfile();
|
|
setPreferences(privacySettings);
|
|
|
|
logger.info('Privacy settings updated successfully', {
|
|
userId: user.id,
|
|
action: 'update_privacy_settings'
|
|
});
|
|
|
|
handleSuccess(
|
|
'Privacy settings updated',
|
|
'Your privacy preferences have been successfully saved.'
|
|
);
|
|
} catch (error) {
|
|
logger.error('Failed to update privacy settings', {
|
|
userId: user.id,
|
|
action: 'update_privacy_settings',
|
|
error: error instanceof Error ? error.message : String(error)
|
|
});
|
|
|
|
if (error instanceof z.ZodError) {
|
|
handleError(
|
|
new AppError(
|
|
'Invalid privacy settings',
|
|
'VALIDATION_ERROR',
|
|
error.issues.map(e => e.message).join(', ')
|
|
),
|
|
{ action: 'Validate privacy settings', userId: user.id }
|
|
);
|
|
} else {
|
|
handleError(error, {
|
|
action: 'Update privacy settings',
|
|
userId: user.id
|
|
});
|
|
}
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
{/* Profile Visibility + Activity & Content Grid */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Profile Visibility */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<Eye className="w-5 h-5" />
|
|
<CardTitle>Profile Visibility</CardTitle>
|
|
</div>
|
|
<CardDescription>
|
|
Control who can see your profile and personal information.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-3">
|
|
<Label htmlFor="privacy_level">Profile Privacy</Label>
|
|
<Select
|
|
value={form.watch('privacy_level')}
|
|
onValueChange={(value: 'public' | 'private') => form.setValue('privacy_level', value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="public">
|
|
Public - Anyone can view your profile
|
|
</SelectItem>
|
|
<SelectItem value="private">
|
|
Private - Only you can view your profile
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label>Show Pronouns</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Display your preferred pronouns on your profile
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={form.watch('show_pronouns')}
|
|
onCheckedChange={checked => form.setValue('show_pronouns', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label>Show Location</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Display your location on your profile
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={form.watch('show_location')}
|
|
onCheckedChange={checked => form.setValue('show_location', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label>Show Age/Birth Date</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Display your birth date on your profile
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={form.watch('show_age')}
|
|
onCheckedChange={checked => form.setValue('show_age', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label>Show Avatar</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Display your profile picture
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={form.watch('show_avatar')}
|
|
onCheckedChange={checked => form.setValue('show_avatar', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label>Show Bio</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Display your profile bio/description
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={form.watch('show_bio')}
|
|
onCheckedChange={checked => form.setValue('show_bio', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label>Show Activity Statistics</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Display your ride counts and park visits
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={form.watch('show_activity_stats')}
|
|
onCheckedChange={checked => form.setValue('show_activity_stats', checked)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label>Show Home Park</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Display your home park preference
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={form.watch('show_home_park')}
|
|
onCheckedChange={checked => form.setValue('show_home_park', checked)}
|
|
/>
|
|
</div>
|
|
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Activity & Content */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="w-5 h-5" />
|
|
<CardTitle>Activity & Content</CardTitle>
|
|
</div>
|
|
<CardDescription>
|
|
Control the visibility of your activities and content.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="space-y-3">
|
|
<Label htmlFor="activity_visibility">Activity Visibility</Label>
|
|
<Select
|
|
value={form.watch('activity_visibility')}
|
|
onValueChange={(value: 'public' | 'private') => form.setValue('activity_visibility', value)}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="public">
|
|
Public - Anyone can see your reviews and lists
|
|
</SelectItem>
|
|
<SelectItem value="private">
|
|
Private - Only you can see your activities
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<p className="text-sm text-muted-foreground">
|
|
This affects the visibility of your reviews, ride credits, and top lists.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Search & Discovery */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<Search className="w-5 h-5" />
|
|
<CardTitle>Search & Discovery</CardTitle>
|
|
</div>
|
|
<CardDescription>
|
|
Control how others can find and discover your profile.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-1">
|
|
<Label>Search Visibility</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Allow your profile to appear in search results
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
checked={form.watch('search_visibility')}
|
|
onCheckedChange={checked => form.setValue('search_visibility', checked)}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Blocked Users */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center gap-2">
|
|
<UserX className="w-5 h-5" />
|
|
<CardTitle>Blocked Users</CardTitle>
|
|
</div>
|
|
<CardDescription>
|
|
Manage users you have blocked from interacting with you.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<BlockedUsers />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Save Button */}
|
|
<div className="flex justify-end">
|
|
<Button type="submit" disabled={loading}>
|
|
{loading ? 'Saving...' : 'Save Privacy Settings'}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|