= {};
+
+ 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 || ;
+ };
+
+ return (
+
+ );
+};
+
+export default RCardManagement;
\ No newline at end of file
diff --git a/src/components/account/RCardPrivacySettings.tsx b/src/components/account/RCardPrivacySettings.tsx
new file mode 100644
index 0000000..9489b3f
--- /dev/null
+++ b/src/components/account/RCardPrivacySettings.tsx
@@ -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 (
+
+
+ {label}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ 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)}`,
+ },
+ },
+ }}
+ />
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ Privacy Settings for {rCard.name}
+
+
+
+
+
+ Configure what information is shared with contacts assigned to this rCard category.
+
+
+ {/* Key Recovery & Trust Settings */}
+
+
+
+ Trust & Recovery
+
+
+
+ handleSettingChange('general', 'keyRecoveryBuddy', e.target.checked)}
+ />
+ }
+ label={
+
+
+ Key Recovery Buddy
+
+
+ Allow contacts in this category to help recover your account
+
+
+ }
+ />
+
+ handleSettingChange('general', 'circlesTrustedConnection', e.target.checked)}
+ />
+ }
+ label={
+
+
+ Circles Trusted Connection
+
+
+ Include these contacts in your trusted circle
+
+
+ }
+ />
+
+
+
+
+
+ {/* Location Sharing */}
+
+
+
+ Location Sharing
+
+
+
+ Location Sharing Level
+
+
+
+ {settings.locationSharing !== 'never' && (
+
+
+ Auto-delete location after: {settings.locationDeletionHours} hours
+
+ 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 }}
+ />
+
+ )}
+
+
+
+
+ {/* Data Sharing */}
+
+
+
+ Data Sharing
+
+
+ handleSettingChange('dataSharing', 'articles', value)}
+ description="Control who can see your shared articles and reading preferences"
+ />
+
+ handleSettingChange('dataSharing', 'proposals', value)}
+ description="Manage visibility of your project proposals and business ideas"
+ />
+
+ handleSettingChange('dataSharing', 'offersAndWants', value)}
+ description="Set who can see what you're offering or looking for"
+ />
+
+ handleSettingChange('dataSharing', 'gratitude', value)}
+ description="Control sharing of appreciation and gratitude posts"
+ />
+
+
+
+
+ {/* Re-sharing Settings */}
+
+
+
+ Re-sharing
+
+
+
+ Allow your shared content to be forwarded through your network
+
+
+ handleSettingChange('reSharing', 'enabled', e.target.checked)}
+ />
+ }
+ label="Enable re-sharing of aggregated data"
+ sx={{ mb: 3 }}
+ />
+
+ {settings.reSharing.enabled && (
+
+
+ Maximum sharing hops: {settings.reSharing.maxHops}
+
+ 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' }}
+ />
+
+ Your data can be shared up to {settings.reSharing.maxHops} connections away from you
+
+
+ )}
+
+
+
+ );
+};
+
+export default RCardPrivacySettings;
\ No newline at end of file
diff --git a/src/pages/AccountPage.tsx b/src/pages/AccountPage.tsx
index 62e6052..0bb9ef3 100644
--- a/src/pages/AccountPage.tsx
+++ b/src/pages/AccountPage.tsx
@@ -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 (
+
+ {value === index && {children}}
+
+ );
+};
const AccountPage = () => {
+ const theme = useTheme();
+ const [tabValue, setTabValue] = useState(0);
+ const [rCards, setRCards] = useState([]);
+ const [selectedRCard, setSelectedRCard] = useState(null);
+ const [showRCardManagement, setShowRCardManagement] = useState(false);
+ const [editingRCard, setEditingRCard] = useState(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 ;
+ case 'PersonOutline':
+ return ;
+ case 'Groups':
+ return ;
+ case 'FamilyRestroom':
+ return ;
+ case 'Favorite':
+ return ;
+ case 'Home':
+ return ;
+ default:
+ return ;
+ }
+ };
+
return (
-
-
-
+
+ {/* Header */}
+
+
My Account
-
-
-
-
-
- U
-
-
- User Name
-
-
- user@example.com
-
- }>
- Edit Profile
-
-
+
+ Manage your profile, privacy settings, and relationship cards (rCards)
+
+
+
+ {/* Navigation Tabs */}
+
+
+ } label="Profile" />
+ } label="Privacy & rCards" />
+ } label="Account Settings" />
+
+
+ {/* Profile Tab */}
+
+
+
+
+
+
+
+ J
+
+
+ John Doe
+
+
+ Product Manager at TechCorp
+
+ }>
+ Edit Profile
+
+
+
+
+
+
+
+
+
+ Profile Information
+
+
+
+ Email
+ john.doe@example.com
+
+
+ Phone
+ +1 (555) 123-4567
+
+
+ Location
+ San Francisco, CA
+
+
+ Bio
+
+ Passionate product manager with 8+ years of experience building user-centered products.
+ I love connecting with fellow professionals and sharing insights about product strategy.
+
+
+
+
+
+
+
-
-
-
-
- Account Settings
-
-
- Manage your account preferences and settings.
-
-
+
+
+ {/* Privacy & rCards Tab */}
+
+
+
+ {/* rCard List */}
+
+
+
+
+
+ Relationship Cards
+
+
+
+
+
+
+
+ Control what information you share with different types of connections
+
+
+
+ {rCards.map((rCard) => (
+ handleRCardSelect(rCard)}
+ >
+
+
+
+ {getRCardIcon(rCard.icon || 'PersonOutline')}
+
+
+
+ {rCard.name}
+
+
+ {rCard.description}
+
+
+
+ {rCard.isDefault && (
+
+ )}
+ {
+ e.stopPropagation();
+ handleEditRCard(rCard);
+ }}
+ >
+
+
+
+
+
+
+ ))}
+
+
+
+
+
+ {/* Privacy Settings */}
+
+ {selectedRCard ? (
+
+ ) : (
+
+
+
+ Select an rCard to view privacy settings
+
+
+
+ )}
+
+
-
-
+
+
+ {/* Account Settings Tab */}
+
+
+
+
+
+ Account Settings
+
+
+ Account settings coming soon...
+
+
+
+
+
+
+
+ {/* rCard Management Dialog */}
+ setShowRCardManagement(false)}
+ onSave={handleRCardSave}
+ onDelete={handleRCardDelete}
+ editingRCard={editingRCard || undefined}
+ />
);
};
diff --git a/src/types/notification.ts b/src/types/notification.ts
index 606f4d3..c772323 100644
--- a/src/types/notification.ts
+++ b/src/types/notification.ts
@@ -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;
+ 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[] = [
{