- Redesign AccountPage with tabbed interface (Profile, Privacy & rCards, Settings) - Add RCardPrivacySettings component with granular privacy controls - Create RCardManagement component for creating and editing rCards - Extend notification types with privacy settings and rCard structures - Implement privacy levels: none, limited, moderate, intimate - Add location sharing controls with auto-deletion settings - Include data sharing sliders for articles, proposals, offers/wants, gratitude - Support re-sharing controls with hop limits - Enable trust settings (key recovery buddy, circles trusted connection) - Add default rCard categories with customized privacy defaults - Support custom rCard creation with icons and colors - Include comprehensive privacy preview and management interface Features: - Granular privacy control per relationship type (rCard) - Visual privacy level sliders matching the provided design - Custom rCard creation with icon and color selection - Default privacy templates optimized for relationship types - Real-time privacy setting updates - Professional UI matching app design patterns 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>main
parent
142920b264
commit
570f6dba11
@ -0,0 +1,314 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import { |
||||||
|
Dialog, |
||||||
|
DialogTitle, |
||||||
|
DialogContent, |
||||||
|
DialogActions, |
||||||
|
Button, |
||||||
|
TextField, |
||||||
|
Box, |
||||||
|
Typography, |
||||||
|
Avatar, |
||||||
|
Grid, |
||||||
|
IconButton, |
||||||
|
Card, |
||||||
|
CardContent, |
||||||
|
Chip, |
||||||
|
} from '@mui/material'; |
||||||
|
import { |
||||||
|
Business, |
||||||
|
PersonOutline, |
||||||
|
Groups, |
||||||
|
FamilyRestroom, |
||||||
|
Favorite, |
||||||
|
Home, |
||||||
|
Work, |
||||||
|
School, |
||||||
|
LocalHospital, |
||||||
|
Sports, |
||||||
|
Close, |
||||||
|
Edit, |
||||||
|
Delete, |
||||||
|
} from '@mui/icons-material'; |
||||||
|
import type { RCardWithPrivacy } from '../../types/notification'; |
||||||
|
import { DEFAULT_PRIVACY_SETTINGS } from '../../types/notification'; |
||||||
|
|
||||||
|
interface RCardManagementProps { |
||||||
|
open: boolean; |
||||||
|
onClose: () => void; |
||||||
|
onSave: (rCard: RCardWithPrivacy) => void; |
||||||
|
onDelete?: (rCardId: string) => void; |
||||||
|
editingRCard?: RCardWithPrivacy; |
||||||
|
} |
||||||
|
|
||||||
|
const AVAILABLE_ICONS = [ |
||||||
|
{ name: 'Business', icon: <Business />, label: 'Business' }, |
||||||
|
{ name: 'PersonOutline', icon: <PersonOutline />, label: 'Person' }, |
||||||
|
{ name: 'Groups', icon: <Groups />, label: 'Groups' }, |
||||||
|
{ name: 'FamilyRestroom', icon: <FamilyRestroom />, label: 'Family' }, |
||||||
|
{ name: 'Favorite', icon: <Favorite />, label: 'Heart' }, |
||||||
|
{ name: 'Home', icon: <Home />, label: 'Home' }, |
||||||
|
{ name: 'Work', icon: <Work />, label: 'Work' }, |
||||||
|
{ name: 'School', icon: <School />, label: 'School' }, |
||||||
|
{ name: 'LocalHospital', icon: <LocalHospital />, label: 'Medical' }, |
||||||
|
{ name: 'Sports', icon: <Sports />, label: 'Sports' }, |
||||||
|
]; |
||||||
|
|
||||||
|
const AVAILABLE_COLORS = [ |
||||||
|
'#2563eb', // Blue
|
||||||
|
'#10b981', // Green
|
||||||
|
'#8b5cf6', // Purple
|
||||||
|
'#f59e0b', // Orange
|
||||||
|
'#ef4444', // Red
|
||||||
|
'#ec4899', // Pink
|
||||||
|
'#06b6d4', // Cyan
|
||||||
|
'#84cc16', // Lime
|
||||||
|
'#f97316', // Orange-red
|
||||||
|
'#6366f1', // Indigo
|
||||||
|
]; |
||||||
|
|
||||||
|
const RCardManagement = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onSave,
|
||||||
|
onDelete,
|
||||||
|
editingRCard
|
||||||
|
}: RCardManagementProps) => { |
||||||
|
const [formData, setFormData] = useState({ |
||||||
|
name: editingRCard?.name || '', |
||||||
|
description: editingRCard?.description || '', |
||||||
|
color: editingRCard?.color || AVAILABLE_COLORS[0], |
||||||
|
icon: editingRCard?.icon || 'PersonOutline', |
||||||
|
}); |
||||||
|
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({}); |
||||||
|
|
||||||
|
const handleSubmit = () => { |
||||||
|
const newErrors: Record<string, string> = {}; |
||||||
|
|
||||||
|
if (!formData.name.trim()) { |
||||||
|
newErrors.name = 'Name is required'; |
||||||
|
} |
||||||
|
|
||||||
|
if (!formData.description.trim()) { |
||||||
|
newErrors.description = 'Description is required'; |
||||||
|
} |
||||||
|
|
||||||
|
if (Object.keys(newErrors).length > 0) { |
||||||
|
setErrors(newErrors); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const rCardData: RCardWithPrivacy = { |
||||||
|
id: editingRCard?.id || `custom-${Date.now()}`, |
||||||
|
name: formData.name.trim(), |
||||||
|
description: formData.description.trim(), |
||||||
|
color: formData.color, |
||||||
|
icon: formData.icon, |
||||||
|
isDefault: editingRCard?.isDefault || false, |
||||||
|
createdAt: editingRCard?.createdAt || new Date(), |
||||||
|
updatedAt: new Date(), |
||||||
|
privacySettings: editingRCard?.privacySettings || { ...DEFAULT_PRIVACY_SETTINGS }, |
||||||
|
}; |
||||||
|
|
||||||
|
onSave(rCardData); |
||||||
|
handleClose(); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleClose = () => { |
||||||
|
setFormData({ |
||||||
|
name: '', |
||||||
|
description: '', |
||||||
|
color: AVAILABLE_COLORS[0], |
||||||
|
icon: 'PersonOutline', |
||||||
|
}); |
||||||
|
setErrors({}); |
||||||
|
onClose(); |
||||||
|
}; |
||||||
|
|
||||||
|
const handleDelete = () => { |
||||||
|
if (editingRCard && onDelete) { |
||||||
|
onDelete(editingRCard.id); |
||||||
|
handleClose(); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const getIconComponent = (iconName: string) => { |
||||||
|
const iconData = AVAILABLE_ICONS.find(icon => icon.name === iconName); |
||||||
|
return iconData?.icon || <PersonOutline />; |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Dialog open={open} onClose={handleClose} maxWidth="md" fullWidth> |
||||||
|
<DialogTitle> |
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> |
||||||
|
<Typography variant="h6"> |
||||||
|
{editingRCard ? 'Edit Relationship Card' : 'Create New Relationship Card'} |
||||||
|
</Typography> |
||||||
|
<IconButton onClick={handleClose} size="small"> |
||||||
|
<Close /> |
||||||
|
</IconButton> |
||||||
|
</Box> |
||||||
|
</DialogTitle> |
||||||
|
|
||||||
|
<DialogContent> |
||||||
|
<Box sx={{ pt: 2 }}> |
||||||
|
{/* Preview */} |
||||||
|
<Card variant="outlined" sx={{ mb: 3, bgcolor: 'grey.50' }}> |
||||||
|
<CardContent sx={{ textAlign: 'center', py: 3 }}> |
||||||
|
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 2 }}> |
||||||
|
Preview |
||||||
|
</Typography> |
||||||
|
<Avatar |
||||||
|
sx={{
|
||||||
|
bgcolor: formData.color,
|
||||||
|
width: 64,
|
||||||
|
height: 64,
|
||||||
|
mx: 'auto',
|
||||||
|
mb: 2
|
||||||
|
}} |
||||||
|
> |
||||||
|
{getIconComponent(formData.icon)} |
||||||
|
</Avatar> |
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600 }}> |
||||||
|
{formData.name || 'rCard Name'} |
||||||
|
</Typography> |
||||||
|
<Typography variant="body2" color="text.secondary"> |
||||||
|
{formData.description || 'rCard description'} |
||||||
|
</Typography> |
||||||
|
{editingRCard?.isDefault && ( |
||||||
|
<Chip label="Default" size="small" variant="outlined" sx={{ mt: 1 }} /> |
||||||
|
)} |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
|
||||||
|
{/* Form Fields */} |
||||||
|
<Grid container spacing={3}> |
||||||
|
<Grid size={{ xs: 12 }}> |
||||||
|
<TextField |
||||||
|
fullWidth |
||||||
|
label="rCard Name" |
||||||
|
value={formData.name} |
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, name: e.target.value }))} |
||||||
|
error={!!errors.name} |
||||||
|
helperText={errors.name} |
||||||
|
placeholder="e.g., Close Friends, Work Colleagues, Gym Buddies" |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
<Grid size={{ xs: 12 }}> |
||||||
|
<TextField |
||||||
|
fullWidth |
||||||
|
multiline |
||||||
|
rows={3} |
||||||
|
label="Description" |
||||||
|
value={formData.description} |
||||||
|
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))} |
||||||
|
error={!!errors.description} |
||||||
|
helperText={errors.description} |
||||||
|
placeholder="Describe the type of relationship and what you'll share with this group" |
||||||
|
/> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
{/* Icon Selection */} |
||||||
|
<Grid size={{ xs: 12 }}> |
||||||
|
<Typography variant="subtitle2" sx={{ mb: 2 }}> |
||||||
|
Choose an Icon |
||||||
|
</Typography> |
||||||
|
<Grid container spacing={1}> |
||||||
|
{AVAILABLE_ICONS.map((iconData) => ( |
||||||
|
<Grid size="auto" key={iconData.name}> |
||||||
|
<Card |
||||||
|
variant="outlined" |
||||||
|
sx={{ |
||||||
|
cursor: 'pointer', |
||||||
|
border: formData.icon === iconData.name ? 2 : 1, |
||||||
|
borderColor: formData.icon === iconData.name ? 'primary.main' : 'divider', |
||||||
|
'&:hover': { borderColor: 'primary.main' }, |
||||||
|
}} |
||||||
|
onClick={() => setFormData(prev => ({ ...prev, icon: iconData.name }))} |
||||||
|
> |
||||||
|
<CardContent sx={{ p: 2, textAlign: 'center', minWidth: 80 }}> |
||||||
|
<Box sx={{ color: formData.color, mb: 1 }}> |
||||||
|
{iconData.icon} |
||||||
|
</Box> |
||||||
|
<Typography variant="caption"> |
||||||
|
{iconData.label} |
||||||
|
</Typography> |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
</Grid> |
||||||
|
))} |
||||||
|
</Grid> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
{/* Color Selection */} |
||||||
|
<Grid size={{ xs: 12 }}> |
||||||
|
<Typography variant="subtitle2" sx={{ mb: 2 }}> |
||||||
|
Choose a Color |
||||||
|
</Typography> |
||||||
|
<Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}> |
||||||
|
{AVAILABLE_COLORS.map((color) => ( |
||||||
|
<Box |
||||||
|
key={color} |
||||||
|
sx={{ |
||||||
|
width: 40, |
||||||
|
height: 40, |
||||||
|
borderRadius: '50%', |
||||||
|
bgcolor: color, |
||||||
|
cursor: 'pointer', |
||||||
|
border: formData.color === color ? 3 : 2, |
||||||
|
borderColor: formData.color === color ? 'grey.800' : 'grey.300', |
||||||
|
'&:hover': { borderColor: 'grey.600' }, |
||||||
|
}} |
||||||
|
onClick={() => setFormData(prev => ({ ...prev, color }))} |
||||||
|
/> |
||||||
|
))} |
||||||
|
</Box> |
||||||
|
</Grid> |
||||||
|
</Grid> |
||||||
|
|
||||||
|
{/* Default rCard Warning */} |
||||||
|
{editingRCard?.isDefault && ( |
||||||
|
<Box sx={{ mt: 3, p: 2, bgcolor: 'warning.50', borderRadius: 1, border: 1, borderColor: 'warning.200' }}> |
||||||
|
<Typography variant="body2" color="warning.dark"> |
||||||
|
<strong>Note:</strong> This is a default rCard. You can edit its name, description, and appearance,
|
||||||
|
but it cannot be deleted. |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</DialogContent> |
||||||
|
|
||||||
|
<DialogActions sx={{ p: 3 }}> |
||||||
|
<Box sx={{ display: 'flex', width: '100%', justifyContent: 'space-between' }}> |
||||||
|
<Box> |
||||||
|
{editingRCard && !editingRCard.isDefault && onDelete && ( |
||||||
|
<Button |
||||||
|
color="error" |
||||||
|
startIcon={<Delete />} |
||||||
|
onClick={handleDelete} |
||||||
|
> |
||||||
|
Delete rCard |
||||||
|
</Button> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
<Box sx={{ display: 'flex', gap: 1 }}> |
||||||
|
<Button onClick={handleClose}> |
||||||
|
Cancel |
||||||
|
</Button> |
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={handleSubmit} |
||||||
|
startIcon={editingRCard ? <Edit /> : undefined} |
||||||
|
> |
||||||
|
{editingRCard ? 'Save Changes' : 'Create rCard'} |
||||||
|
</Button> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
</DialogActions> |
||||||
|
</Dialog> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default RCardManagement; |
@ -0,0 +1,349 @@ |
|||||||
|
import { useState } from 'react'; |
||||||
|
import { |
||||||
|
Card, |
||||||
|
CardContent, |
||||||
|
Typography, |
||||||
|
Box, |
||||||
|
Switch, |
||||||
|
FormControlLabel, |
||||||
|
Select, |
||||||
|
MenuItem, |
||||||
|
FormControl, |
||||||
|
InputLabel, |
||||||
|
Slider, |
||||||
|
Divider, |
||||||
|
Chip, |
||||||
|
alpha, |
||||||
|
} from '@mui/material'; |
||||||
|
import { |
||||||
|
Security, |
||||||
|
LocationOn, |
||||||
|
Share, |
||||||
|
Refresh, |
||||||
|
VpnKey, |
||||||
|
} from '@mui/icons-material'; |
||||||
|
import type { RCardWithPrivacy, PrivacyLevel, LocationSharingLevel } from '../../types/notification'; |
||||||
|
|
||||||
|
interface RCardPrivacySettingsProps { |
||||||
|
rCard: RCardWithPrivacy; |
||||||
|
onUpdate: (updatedRCard: RCardWithPrivacy) => void; |
||||||
|
} |
||||||
|
|
||||||
|
const RCardPrivacySettings = ({ rCard, onUpdate }: RCardPrivacySettingsProps) => { |
||||||
|
const [settings, setSettings] = useState(rCard.privacySettings); |
||||||
|
|
||||||
|
const handleSettingChange = ( |
||||||
|
category: string, |
||||||
|
field: string, |
||||||
|
value: any |
||||||
|
) => { |
||||||
|
const newSettings = { ...settings }; |
||||||
|
|
||||||
|
if (category === 'dataSharing') { |
||||||
|
(newSettings.dataSharing as any)[field] = value; |
||||||
|
} else if (category === 'reSharing') { |
||||||
|
(newSettings.reSharing as any)[field] = value; |
||||||
|
} else { |
||||||
|
(newSettings as any)[field] = value; |
||||||
|
} |
||||||
|
|
||||||
|
setSettings(newSettings); |
||||||
|
|
||||||
|
const updatedRCard = { |
||||||
|
...rCard, |
||||||
|
privacySettings: newSettings, |
||||||
|
updatedAt: new Date(), |
||||||
|
}; |
||||||
|
|
||||||
|
onUpdate(updatedRCard); |
||||||
|
}; |
||||||
|
|
||||||
|
const getPrivacyLevelColor = (level: PrivacyLevel) => { |
||||||
|
switch (level) { |
||||||
|
case 'none': return '#94a3b8'; |
||||||
|
case 'limited': return '#3b82f6'; |
||||||
|
case 'moderate': return '#f59e0b'; |
||||||
|
case 'intimate': return '#ef4444'; |
||||||
|
default: return '#3b82f6'; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const getPrivacyLevelLabel = (level: PrivacyLevel) => { |
||||||
|
switch (level) { |
||||||
|
case 'none': return 'None'; |
||||||
|
case 'limited': return 'Limited'; |
||||||
|
case 'moderate': return 'Moderate'; |
||||||
|
case 'intimate': return 'Intimate'; |
||||||
|
default: return 'Limited'; |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
const PrivacySlider = ({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
description
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: PrivacyLevel;
|
||||||
|
onChange: (value: PrivacyLevel) => void; |
||||||
|
description?: string; |
||||||
|
}) => { |
||||||
|
const levels: PrivacyLevel[] = ['none', 'limited', 'moderate', 'intimate']; |
||||||
|
const currentIndex = levels.indexOf(value); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Box sx={{ mb: 4 }}> |
||||||
|
<Typography variant="subtitle1" sx={{ fontWeight: 600, mb: 1 }}> |
||||||
|
{label} |
||||||
|
</Typography> |
||||||
|
{description && ( |
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}> |
||||||
|
{description} |
||||||
|
</Typography> |
||||||
|
)} |
||||||
|
<Box sx={{ px: 2 }}> |
||||||
|
<Slider |
||||||
|
value={currentIndex} |
||||||
|
onChange={(_, newValue) => onChange(levels[newValue as number])} |
||||||
|
min={0} |
||||||
|
max={3} |
||||||
|
step={1} |
||||||
|
marks={levels.map((level, index) => ({ |
||||||
|
value: index, |
||||||
|
label: getPrivacyLevelLabel(level), |
||||||
|
}))} |
||||||
|
sx={{ |
||||||
|
color: getPrivacyLevelColor(value), |
||||||
|
'& .MuiSlider-markLabel': { |
||||||
|
fontSize: '0.75rem', |
||||||
|
fontWeight: currentIndex === levels.indexOf(value) ? 600 : 400, |
||||||
|
color: currentIndex === levels.indexOf(value) ? getPrivacyLevelColor(value) : 'text.secondary', |
||||||
|
}, |
||||||
|
'& .MuiSlider-track': { |
||||||
|
border: 'none', |
||||||
|
height: 8, |
||||||
|
}, |
||||||
|
'& .MuiSlider-thumb': { |
||||||
|
height: 20, |
||||||
|
width: 20, |
||||||
|
backgroundColor: getPrivacyLevelColor(value), |
||||||
|
'&:focus, &:hover, &.Mui-active, &.Mui-focusVisible': { |
||||||
|
boxShadow: `0px 0px 0px 8px ${alpha(getPrivacyLevelColor(value), 0.16)}`, |
||||||
|
}, |
||||||
|
}, |
||||||
|
}} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<Card> |
||||||
|
<CardContent sx={{ p: 3 }}> |
||||||
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}> |
||||||
|
<Security color="primary" /> |
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600 }}> |
||||||
|
Privacy Settings for {rCard.name} |
||||||
|
</Typography> |
||||||
|
<Chip
|
||||||
|
label={rCard.isDefault ? 'Default' : 'Custom'}
|
||||||
|
size="small"
|
||||||
|
variant="outlined" |
||||||
|
color={rCard.isDefault ? 'default' : 'primary'} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}> |
||||||
|
Configure what information is shared with contacts assigned to this rCard category. |
||||||
|
</Typography> |
||||||
|
|
||||||
|
{/* Key Recovery & Trust Settings */} |
||||||
|
<Box sx={{ mb: 4 }}> |
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}> |
||||||
|
<VpnKey fontSize="small" /> |
||||||
|
Trust & Recovery |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}> |
||||||
|
<FormControlLabel |
||||||
|
control={ |
||||||
|
<Switch |
||||||
|
checked={settings.keyRecoveryBuddy} |
||||||
|
onChange={(e) => handleSettingChange('general', 'keyRecoveryBuddy', e.target.checked)} |
||||||
|
/> |
||||||
|
} |
||||||
|
label={ |
||||||
|
<Box> |
||||||
|
<Typography variant="body2" sx={{ fontWeight: 500 }}> |
||||||
|
Key Recovery Buddy |
||||||
|
</Typography> |
||||||
|
<Typography variant="caption" color="text.secondary"> |
||||||
|
Allow contacts in this category to help recover your account |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
} |
||||||
|
/> |
||||||
|
|
||||||
|
<FormControlLabel |
||||||
|
control={ |
||||||
|
<Switch |
||||||
|
checked={settings.circlesTrustedConnection} |
||||||
|
onChange={(e) => handleSettingChange('general', 'circlesTrustedConnection', e.target.checked)} |
||||||
|
/> |
||||||
|
} |
||||||
|
label={ |
||||||
|
<Box> |
||||||
|
<Typography variant="body2" sx={{ fontWeight: 500 }}> |
||||||
|
Circles Trusted Connection |
||||||
|
</Typography> |
||||||
|
<Typography variant="caption" color="text.secondary"> |
||||||
|
Include these contacts in your trusted circle |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
</Box> |
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} /> |
||||||
|
|
||||||
|
{/* Location Sharing */} |
||||||
|
<Box sx={{ mb: 4 }}> |
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}> |
||||||
|
<LocationOn fontSize="small" /> |
||||||
|
Location Sharing |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<FormControl fullWidth sx={{ mb: 2 }}> |
||||||
|
<InputLabel>Location Sharing Level</InputLabel> |
||||||
|
<Select |
||||||
|
value={settings.locationSharing} |
||||||
|
label="Location Sharing Level" |
||||||
|
onChange={(e) => handleSettingChange('general', 'locationSharing', e.target.value as LocationSharingLevel)} |
||||||
|
> |
||||||
|
<MenuItem value="never">Never</MenuItem> |
||||||
|
<MenuItem value="limited">Limited (On Request)</MenuItem> |
||||||
|
<MenuItem value="always">Always</MenuItem> |
||||||
|
</Select> |
||||||
|
</FormControl> |
||||||
|
|
||||||
|
{settings.locationSharing !== 'never' && ( |
||||||
|
<Box> |
||||||
|
<Typography variant="body2" sx={{ mb: 1 }}> |
||||||
|
Auto-delete location after: {settings.locationDeletionHours} hours |
||||||
|
</Typography> |
||||||
|
<Slider |
||||||
|
value={settings.locationDeletionHours} |
||||||
|
onChange={(_, value) => handleSettingChange('general', 'locationDeletionHours', value)} |
||||||
|
min={1} |
||||||
|
max={48} |
||||||
|
step={1} |
||||||
|
marks={[ |
||||||
|
{ value: 1, label: '1h' }, |
||||||
|
{ value: 8, label: '8h' }, |
||||||
|
{ value: 24, label: '24h' }, |
||||||
|
{ value: 48, label: '48h' }, |
||||||
|
]} |
||||||
|
sx={{ color: 'primary.main', mt: 2 }} |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} /> |
||||||
|
|
||||||
|
{/* Data Sharing */} |
||||||
|
<Box sx={{ mb: 4 }}> |
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3, display: 'flex', alignItems: 'center', gap: 1 }}> |
||||||
|
<Share fontSize="small" /> |
||||||
|
Data Sharing |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<PrivacySlider |
||||||
|
label="Articles" |
||||||
|
value={settings.dataSharing.articles} |
||||||
|
onChange={(value) => handleSettingChange('dataSharing', 'articles', value)} |
||||||
|
description="Control who can see your shared articles and reading preferences" |
||||||
|
/> |
||||||
|
|
||||||
|
<PrivacySlider |
||||||
|
label="Proposals" |
||||||
|
value={settings.dataSharing.proposals} |
||||||
|
onChange={(value) => handleSettingChange('dataSharing', 'proposals', value)} |
||||||
|
description="Manage visibility of your project proposals and business ideas" |
||||||
|
/> |
||||||
|
|
||||||
|
<PrivacySlider |
||||||
|
label="Offers & Wants" |
||||||
|
value={settings.dataSharing.offersAndWants} |
||||||
|
onChange={(value) => handleSettingChange('dataSharing', 'offersAndWants', value)} |
||||||
|
description="Set who can see what you're offering or looking for" |
||||||
|
/> |
||||||
|
|
||||||
|
<PrivacySlider |
||||||
|
label="Gratitude" |
||||||
|
value={settings.dataSharing.gratitude} |
||||||
|
onChange={(value) => handleSettingChange('dataSharing', 'gratitude', value)} |
||||||
|
description="Control sharing of appreciation and gratitude posts" |
||||||
|
/> |
||||||
|
</Box> |
||||||
|
|
||||||
|
<Divider sx={{ my: 3 }} /> |
||||||
|
|
||||||
|
{/* Re-sharing Settings */} |
||||||
|
<Box sx={{ mb: 2 }}> |
||||||
|
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2, display: 'flex', alignItems: 'center', gap: 1 }}> |
||||||
|
<Refresh fontSize="small" /> |
||||||
|
Re-sharing |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}> |
||||||
|
Allow your shared content to be forwarded through your network |
||||||
|
</Typography> |
||||||
|
|
||||||
|
<FormControlLabel |
||||||
|
control={ |
||||||
|
<Switch |
||||||
|
checked={settings.reSharing.enabled} |
||||||
|
onChange={(e) => handleSettingChange('reSharing', 'enabled', e.target.checked)} |
||||||
|
/> |
||||||
|
} |
||||||
|
label="Enable re-sharing of aggregated data" |
||||||
|
sx={{ mb: 3 }} |
||||||
|
/> |
||||||
|
|
||||||
|
{settings.reSharing.enabled && ( |
||||||
|
<Box> |
||||||
|
<Typography variant="body2" sx={{ mb: 2 }}> |
||||||
|
Maximum sharing hops: {settings.reSharing.maxHops} |
||||||
|
</Typography> |
||||||
|
<Slider |
||||||
|
value={settings.reSharing.maxHops} |
||||||
|
onChange={(_, value) => handleSettingChange('reSharing', 'maxHops', value)} |
||||||
|
min={0} |
||||||
|
max={5} |
||||||
|
step={1} |
||||||
|
marks={[ |
||||||
|
{ value: 0, label: '0' }, |
||||||
|
{ value: 1, label: '1' }, |
||||||
|
{ value: 2, label: '2' }, |
||||||
|
{ value: 3, label: '3' }, |
||||||
|
{ value: 4, label: '4' }, |
||||||
|
{ value: 5, label: '5' }, |
||||||
|
]} |
||||||
|
sx={{ color: 'primary.main' }} |
||||||
|
/> |
||||||
|
<Typography variant="caption" color="text.secondary"> |
||||||
|
Your data can be shared up to {settings.reSharing.maxHops} connections away from you |
||||||
|
</Typography> |
||||||
|
</Box> |
||||||
|
)} |
||||||
|
</Box> |
||||||
|
</CardContent> |
||||||
|
</Card> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default RCardPrivacySettings; |
Loading…
Reference in new issue