Implement comprehensive My Account page with rCard privacy management

- 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
Oliver Sylvester-Bradley 2 months ago
parent 142920b264
commit 570f6dba11
  1. 314
      src/components/account/RCardManagement.tsx
  2. 349
      src/components/account/RCardPrivacySettings.tsx
  3. 386
      src/pages/AccountPage.tsx
  4. 50
      src/types/notification.ts

@ -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;

@ -1,48 +1,392 @@
import { Box, Typography, Container, Avatar, Paper, Button } from '@mui/material';
import { Edit } from '@mui/icons-material';
import { useState, useEffect } from 'react';
import {
Container,
Typography,
Box,
Paper,
Tabs,
Tab,
Grid,
Card,
CardContent,
Avatar,
Button,
IconButton,
Chip,
useTheme,
alpha,
} from '@mui/material';
import {
Person,
Security,
Business,
PersonOutline,
Groups,
FamilyRestroom,
Favorite,
Home,
Add,
Edit,
Settings,
} from '@mui/icons-material';
import { DEFAULT_RCARDS, DEFAULT_PRIVACY_SETTINGS } from '../types/notification';
import type { RCardWithPrivacy } from '../types/notification';
import RCardPrivacySettings from '../components/account/RCardPrivacySettings';
import RCardManagement from '../components/account/RCardManagement';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
const TabPanel = ({ children, value, index }: TabPanelProps) => {
return (
<div hidden={value !== index}>
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
</div>
);
};
const AccountPage = () => {
const theme = useTheme();
const [tabValue, setTabValue] = useState(0);
const [rCards, setRCards] = useState<RCardWithPrivacy[]>([]);
const [selectedRCard, setSelectedRCard] = useState<RCardWithPrivacy | null>(null);
const [showRCardManagement, setShowRCardManagement] = useState(false);
const [editingRCard, setEditingRCard] = useState<RCardWithPrivacy | null>(null);
useEffect(() => {
// Initialize rCards with default privacy settings
const initialRCards: RCardWithPrivacy[] = DEFAULT_RCARDS.map((rCard, index) => ({
...rCard,
id: `default-${index}`,
isDefault: true,
createdAt: new Date(),
updatedAt: new Date(),
privacySettings: {
...DEFAULT_PRIVACY_SETTINGS,
// Customize privacy levels based on rCard type
keyRecoveryBuddy: rCard.name === 'Close Family',
circlesTrustedConnection: ['Close Family', 'Family', 'Friend'].includes(rCard.name),
locationSharing: rCard.name === 'Close Family' ? 'always' :
rCard.name === 'Family' ? 'limited' : 'never',
dataSharing: {
articles: rCard.name === 'Business' ? 'moderate' : 'limited',
proposals: rCard.name === 'Business' ? 'moderate' : 'limited',
offersAndWants: rCard.name === 'Business' ? 'moderate' : 'limited',
gratitude: ['Close Family', 'Family', 'Friend'].includes(rCard.name) ? 'intimate' : 'limited',
},
},
}));
setRCards(initialRCards);
setSelectedRCard(initialRCards[0]);
}, []);
const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
const handleRCardSelect = (rCard: RCardWithPrivacy) => {
setSelectedRCard(rCard);
};
const handleRCardUpdate = (updatedRCard: RCardWithPrivacy) => {
setRCards(prev =>
prev.map(card => card.id === updatedRCard.id ? updatedRCard : card)
);
setSelectedRCard(updatedRCard);
};
const handleCreateRCard = () => {
setEditingRCard(null);
setShowRCardManagement(true);
};
const handleEditRCard = (rCard: RCardWithPrivacy) => {
setEditingRCard(rCard);
setShowRCardManagement(true);
};
const handleRCardSave = (rCard: RCardWithPrivacy) => {
if (editingRCard) {
// Update existing rCard
setRCards(prev =>
prev.map(card => card.id === rCard.id ? rCard : card)
);
if (selectedRCard?.id === rCard.id) {
setSelectedRCard(rCard);
}
} else {
// Add new rCard
setRCards(prev => [...prev, rCard]);
setSelectedRCard(rCard);
}
};
const handleRCardDelete = (rCardId: string) => {
setRCards(prev => prev.filter(card => card.id !== rCardId));
if (selectedRCard?.id === rCardId) {
setSelectedRCard(rCards[0] || null);
}
};
const getRCardIcon = (iconName: string) => {
switch (iconName) {
case 'Business':
return <Business />;
case 'PersonOutline':
return <PersonOutline />;
case 'Groups':
return <Groups />;
case 'FamilyRestroom':
return <FamilyRestroom />;
case 'Favorite':
return <Favorite />;
case 'Home':
return <Home />;
default:
return <PersonOutline />;
}
};
return (
<Container maxWidth="lg">
<Box sx={{ py: 4 }}>
<Typography variant="h4" sx={{ mb: 3, fontWeight: 600 }}>
<Container maxWidth="lg" sx={{ py: 4 }}>
{/* Header */}
<Box sx={{ mb: 4 }}>
<Typography variant="h3" component="h1" sx={{ fontWeight: 700, mb: 1 }}>
My Account
</Typography>
<Typography variant="body1" color="text.secondary">
Manage your profile, privacy settings, and relationship cards (rCards)
</Typography>
</Box>
{/* Navigation Tabs */}
<Paper sx={{ mb: 3 }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
sx={{
borderBottom: 1,
borderColor: 'divider',
'& .MuiTab-root': {
minHeight: 64,
textTransform: 'none',
fontWeight: 500,
}
}}
>
<Tab icon={<Person />} label="Profile" />
<Tab icon={<Security />} label="Privacy & rCards" />
<Tab icon={<Settings />} label="Account Settings" />
</Tabs>
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', md: 'row' }, gap: 3 }}>
<Box sx={{ flex: { xs: 1, md: '0 0 33%' } }}>
<Paper sx={{ p: 3, textAlign: 'center' }}>
{/* Profile Tab */}
<TabPanel value={tabValue} index={0}>
<Box sx={{ p: 3 }}>
<Grid container spacing={3}>
<Grid size={{ xs: 12, md: 4 }}>
<Card>
<CardContent sx={{ textAlign: 'center', p: 4 }}>
<Avatar
sx={{ width: 120, height: 120, mx: 'auto', mb: 2 }}
sx={{
width: 120,
height: 120,
mx: 'auto',
mb: 2,
bgcolor: 'primary.main',
fontSize: '3rem'
}}
alt="Profile"
src="/static/images/avatar/2.jpg"
>
U
J
</Avatar>
<Typography variant="h6" sx={{ mb: 1 }}>
User Name
<Typography variant="h5" sx={{ fontWeight: 600, mb: 1 }}>
John Doe
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
user@example.com
Product Manager at TechCorp
</Typography>
<Button variant="outlined" startIcon={<Edit />}>
Edit Profile
</Button>
</Paper>
</CardContent>
</Card>
</Grid>
<Grid size={{ xs: 12, md: 8 }}>
<Card>
<CardContent sx={{ p: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
Profile Information
</Typography>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Box>
<Typography variant="subtitle2" color="text.secondary">Email</Typography>
<Typography variant="body1">john.doe@example.com</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Phone</Typography>
<Typography variant="body1">+1 (555) 123-4567</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Location</Typography>
<Typography variant="body1">San Francisco, CA</Typography>
</Box>
<Box>
<Typography variant="subtitle2" color="text.secondary">Bio</Typography>
<Typography variant="body1">
Passionate product manager with 8+ years of experience building user-centered products.
I love connecting with fellow professionals and sharing insights about product strategy.
</Typography>
</Box>
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</Box>
</TabPanel>
<Box sx={{ flex: 1 }}>
<Paper sx={{ p: 3 }}>
<Typography variant="h6" sx={{ mb: 2 }}>
Account Settings
{/* Privacy & rCards Tab */}
<TabPanel value={tabValue} index={1}>
<Box sx={{ p: 3 }}>
<Grid container spacing={3}>
{/* rCard List */}
<Grid size={{ xs: 12, md: 4 }}>
<Card>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Relationship Cards
</Typography>
<Typography variant="body1" color="text.secondary">
Manage your account preferences and settings.
<IconButton size="small" color="primary" onClick={handleCreateRCard}>
<Add />
</IconButton>
</Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Control what information you share with different types of connections
</Typography>
</Paper>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{rCards.map((rCard) => (
<Card
key={rCard.id}
variant="outlined"
sx={{
cursor: 'pointer',
border: selectedRCard?.id === rCard.id ? 2 : 1,
borderColor: selectedRCard?.id === rCard.id ? 'primary.main' : 'divider',
backgroundColor: selectedRCard?.id === rCard.id
? alpha(theme.palette.primary.main, 0.04)
: 'transparent',
'&:hover': {
backgroundColor: alpha(theme.palette.action.hover, 0.5),
},
}}
onClick={() => handleRCardSelect(rCard)}
>
<CardContent sx={{ p: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar
sx={{
bgcolor: rCard.color || 'primary.main',
width: 40,
height: 40
}}
>
{getRCardIcon(rCard.icon || 'PersonOutline')}
</Avatar>
<Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{rCard.name}
</Typography>
<Typography
variant="caption"
color="text.secondary"
sx={{
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
}}
>
{rCard.description}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{rCard.isDefault && (
<Chip label="Default" size="small" variant="outlined" />
)}
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
handleEditRCard(rCard);
}}
>
<Edit fontSize="small" />
</IconButton>
</Box>
</Box>
</CardContent>
</Card>
))}
</Box>
</CardContent>
</Card>
</Grid>
{/* Privacy Settings */}
<Grid size={{ xs: 12, md: 8 }}>
{selectedRCard ? (
<RCardPrivacySettings
rCard={selectedRCard}
onUpdate={handleRCardUpdate}
/>
) : (
<Card>
<CardContent sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary">
Select an rCard to view privacy settings
</Typography>
</CardContent>
</Card>
)}
</Grid>
</Grid>
</Box>
</TabPanel>
{/* Account Settings Tab */}
<TabPanel value={tabValue} index={2}>
<Box sx={{ p: 3 }}>
<Card>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
Account Settings
</Typography>
<Typography variant="body2" color="text.secondary">
Account settings coming soon...
</Typography>
</CardContent>
</Card>
</Box>
</TabPanel>
</Paper>
{/* rCard Management Dialog */}
<RCardManagement
open={showRCardManagement}
onClose={() => setShowRCardManagement(false)}
onSave={handleRCardSave}
onDelete={handleRCardDelete}
editingRCard={editingRCard || undefined}
/>
</Container>
);
};

@ -99,6 +99,56 @@ export interface NotificationSummary {
};
}
export type PrivacyLevel = 'none' | 'limited' | 'moderate' | 'intimate';
export type LocationSharingLevel = 'never' | 'limited' | 'always';
export interface PrivacySettings {
keyRecoveryBuddy: boolean;
circlesTrustedConnection: boolean;
locationSharing: LocationSharingLevel;
locationDeletionHours: number;
dataSharing: {
articles: PrivacyLevel;
proposals: PrivacyLevel;
offersAndWants: PrivacyLevel;
gratitude: PrivacyLevel;
};
reSharing: {
enabled: boolean;
maxHops: number;
};
}
export interface RCardWithPrivacy extends RCard {
privacySettings: PrivacySettings;
}
export interface ContactPrivacyOverride {
contactId: string;
rCardId: string;
overrides: Partial<PrivacySettings>;
createdAt: Date;
updatedAt: Date;
}
// Default privacy settings template
export const DEFAULT_PRIVACY_SETTINGS: PrivacySettings = {
keyRecoveryBuddy: false,
circlesTrustedConnection: false,
locationSharing: 'never',
locationDeletionHours: 8,
dataSharing: {
articles: 'limited',
proposals: 'limited',
offersAndWants: 'limited',
gratitude: 'limited',
},
reSharing: {
enabled: true,
maxHops: 3,
},
};
// Default rCard categories
export const DEFAULT_RCARDS: Omit<RCard, 'id' | 'createdAt' | 'updatedAt'>[] = [
{

Loading…
Cancel
Save