mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 06:31:13 -05:00
feat: Implement privacy modernization plan
This commit is contained in:
@@ -6,156 +6,227 @@ 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 { useToast } from '@/hooks/use-toast';
|
||||
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';
|
||||
interface PrivacySettings {
|
||||
activity_visibility: 'public' | 'private';
|
||||
search_visibility: boolean;
|
||||
show_location: boolean;
|
||||
show_age: boolean;
|
||||
show_avatar: boolean;
|
||||
show_bio: boolean;
|
||||
show_activity_stats: boolean;
|
||||
show_home_park: boolean;
|
||||
}
|
||||
interface ProfilePrivacy {
|
||||
privacy_level: 'public' | 'private';
|
||||
show_pronouns: boolean;
|
||||
}
|
||||
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 { toast } = useToast();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [preferences, setPreferences] = useState<PrivacySettings | null>(null);
|
||||
const form = useForm<ProfilePrivacy & PrivacySettings>({
|
||||
|
||||
const form = useForm<PrivacyFormData>({
|
||||
defaultValues: {
|
||||
privacy_level: (profile?.privacy_level === 'friends' ? 'public' : profile?.privacy_level) || 'public',
|
||||
privacy_level: profile?.privacy_level || 'public',
|
||||
show_pronouns: profile?.show_pronouns || false,
|
||||
activity_visibility: 'public',
|
||||
search_visibility: true,
|
||||
show_location: false,
|
||||
show_age: false,
|
||||
show_avatar: true,
|
||||
show_bio: true,
|
||||
show_activity_stats: true,
|
||||
show_home_park: 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();
|
||||
const { data, error } = await supabase
|
||||
.from('user_preferences')
|
||||
.select('privacy_settings')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
if (error && error.code !== 'PGRST116') {
|
||||
console.error('Error fetching preferences:', error);
|
||||
return;
|
||||
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 privacySettings = data.privacy_settings as any;
|
||||
setPreferences(privacySettings);
|
||||
// Validate the data before using it
|
||||
const validatedSettings = privacySettingsSchema.parse(data.privacy_settings);
|
||||
setPreferences(validatedSettings);
|
||||
|
||||
form.reset({
|
||||
privacy_level: profile?.privacy_level === 'friends' ? 'public' : profile?.privacy_level || 'public',
|
||||
privacy_level: profile?.privacy_level || 'public',
|
||||
show_pronouns: profile?.show_pronouns || false,
|
||||
...privacySettings
|
||||
...validatedSettings
|
||||
});
|
||||
} else {
|
||||
// Initialize preferences if they don't exist
|
||||
await initializePreferences();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching preferences:', 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;
|
||||
const defaultSettings: PrivacySettings = {
|
||||
activity_visibility: 'public',
|
||||
search_visibility: true,
|
||||
show_location: false,
|
||||
show_age: false,
|
||||
show_avatar: true,
|
||||
show_bio: true,
|
||||
show_activity_stats: true,
|
||||
show_home_park: false
|
||||
};
|
||||
|
||||
try {
|
||||
const {
|
||||
error
|
||||
} = await supabase.from('user_preferences').insert([{
|
||||
user_id: user.id,
|
||||
privacy_settings: defaultSettings as any
|
||||
}]);
|
||||
if (error) throw error;
|
||||
setPreferences(defaultSettings);
|
||||
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 === 'friends' ? 'public' : profile?.privacy_level) || 'public',
|
||||
privacy_level: profile?.privacy_level || 'public',
|
||||
show_pronouns: profile?.show_pronouns || false,
|
||||
...defaultSettings
|
||||
...DEFAULT_PRIVACY_SETTINGS
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error initializing preferences:', 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: any) => {
|
||||
if (!user) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
// Update profile privacy settings
|
||||
const {
|
||||
error: profileError
|
||||
} = await supabase.from('profiles').update({
|
||||
privacy_level: data.privacy_level,
|
||||
show_pronouns: data.show_pronouns,
|
||||
updated_at: new Date().toISOString()
|
||||
}).eq('user_id', user.id);
|
||||
if (profileError) throw profileError;
|
||||
|
||||
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 privacySettings: PrivacySettings = {
|
||||
activity_visibility: data.activity_visibility,
|
||||
search_visibility: data.search_visibility,
|
||||
show_location: data.show_location,
|
||||
show_age: data.show_age,
|
||||
show_avatar: data.show_avatar,
|
||||
show_bio: data.show_bio,
|
||||
show_activity_stats: data.show_activity_stats,
|
||||
show_home_park: data.show_home_park
|
||||
};
|
||||
const {
|
||||
error: prefsError
|
||||
} = await supabase.from('user_preferences').upsert([{
|
||||
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,
|
||||
privacy_settings: privacySettings as any,
|
||||
updated_at: new Date().toISOString()
|
||||
}]);
|
||||
if (prefsError) throw prefsError;
|
||||
changed_by: user.id,
|
||||
action: 'privacy_settings_updated',
|
||||
changes: {
|
||||
previous: preferences,
|
||||
updated: privacySettings,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
});
|
||||
|
||||
await refreshProfile();
|
||||
setPreferences(privacySettings);
|
||||
toast({
|
||||
title: 'Privacy settings updated',
|
||||
description: 'Your privacy preferences have been successfully saved.'
|
||||
|
||||
logger.info('Privacy settings updated successfully', {
|
||||
userId: user.id,
|
||||
action: 'update_privacy_settings'
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: error.message || 'Failed to update privacy settings',
|
||||
variant: 'destructive'
|
||||
|
||||
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.errors.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-8">
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
{/* Profile Visibility */}
|
||||
<div className="space-y-4">
|
||||
@@ -173,7 +244,10 @@ export function PrivacyTab() {
|
||||
<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)}>
|
||||
<Select
|
||||
value={form.watch('privacy_level')}
|
||||
onValueChange={(value: 'public' | 'private') => form.setValue('privacy_level', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -195,7 +269,10 @@ export function PrivacyTab() {
|
||||
Display your preferred pronouns on your profile
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={form.watch('show_pronouns')} onCheckedChange={checked => form.setValue('show_pronouns', checked)} />
|
||||
<Switch
|
||||
checked={form.watch('show_pronouns')}
|
||||
onCheckedChange={checked => form.setValue('show_pronouns', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -205,7 +282,10 @@ export function PrivacyTab() {
|
||||
Display your location on your profile
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={form.watch('show_location')} onCheckedChange={checked => form.setValue('show_location', checked)} />
|
||||
<Switch
|
||||
checked={form.watch('show_location')}
|
||||
onCheckedChange={checked => form.setValue('show_location', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -215,7 +295,10 @@ export function PrivacyTab() {
|
||||
Display your birth date on your profile
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={form.watch('show_age')} onCheckedChange={checked => form.setValue('show_age', checked)} />
|
||||
<Switch
|
||||
checked={form.watch('show_age')}
|
||||
onCheckedChange={checked => form.setValue('show_age', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -225,7 +308,10 @@ export function PrivacyTab() {
|
||||
Display your profile picture
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={form.watch('show_avatar')} onCheckedChange={checked => form.setValue('show_avatar', checked)} />
|
||||
<Switch
|
||||
checked={form.watch('show_avatar')}
|
||||
onCheckedChange={checked => form.setValue('show_avatar', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -235,7 +321,10 @@ export function PrivacyTab() {
|
||||
Display your profile bio/description
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={form.watch('show_bio')} onCheckedChange={checked => form.setValue('show_bio', checked)} />
|
||||
<Switch
|
||||
checked={form.watch('show_bio')}
|
||||
onCheckedChange={checked => form.setValue('show_bio', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -245,7 +334,10 @@ export function PrivacyTab() {
|
||||
Display your ride counts and park visits
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={form.watch('show_activity_stats')} onCheckedChange={checked => form.setValue('show_activity_stats', checked)} />
|
||||
<Switch
|
||||
checked={form.watch('show_activity_stats')}
|
||||
onCheckedChange={checked => form.setValue('show_activity_stats', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -255,7 +347,10 @@ export function PrivacyTab() {
|
||||
Display your home park preference
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={form.watch('show_home_park')} onCheckedChange={checked => form.setValue('show_home_park', checked)} />
|
||||
<Switch
|
||||
checked={form.watch('show_home_park')}
|
||||
onCheckedChange={checked => form.setValue('show_home_park', checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
@@ -280,7 +375,10 @@ export function PrivacyTab() {
|
||||
<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)}>
|
||||
<Select
|
||||
value={form.watch('activity_visibility')}
|
||||
onValueChange={(value: 'public' | 'private') => form.setValue('activity_visibility', value)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
@@ -324,7 +422,10 @@ export function PrivacyTab() {
|
||||
Allow your profile to appear in search results
|
||||
</p>
|
||||
</div>
|
||||
<Switch checked={form.watch('search_visibility')} onCheckedChange={checked => form.setValue('search_visibility', checked)} />
|
||||
<Switch
|
||||
checked={form.watch('search_visibility')}
|
||||
onCheckedChange={checked => form.setValue('search_visibility', checked)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -358,5 +459,6 @@ export function PrivacyTab() {
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>;
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user