From 570f6dba112a07d1bd18a6fef39bcc3f814e9913 Mon Sep 17 00:00:00 2001 From: Oliver Sylvester-Bradley Date: Fri, 18 Jul 2025 17:17:24 +0100 Subject: [PATCH] Implement comprehensive My Account page with rCard privacy management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/components/account/RCardManagement.tsx | 314 +++++++++++++ .../account/RCardPrivacySettings.tsx | 349 +++++++++++++++ src/pages/AccountPage.tsx | 420 ++++++++++++++++-- src/types/notification.ts | 50 +++ 4 files changed, 1095 insertions(+), 38 deletions(-) create mode 100644 src/components/account/RCardManagement.tsx create mode 100644 src/components/account/RCardPrivacySettings.tsx diff --git a/src/components/account/RCardManagement.tsx b/src/components/account/RCardManagement.tsx new file mode 100644 index 0000000..02e3328 --- /dev/null +++ b/src/components/account/RCardManagement.tsx @@ -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: , label: 'Business' }, + { name: 'PersonOutline', icon: , label: 'Person' }, + { name: 'Groups', icon: , label: 'Groups' }, + { name: 'FamilyRestroom', icon: , label: 'Family' }, + { name: 'Favorite', icon: , label: 'Heart' }, + { name: 'Home', icon: , label: 'Home' }, + { name: 'Work', icon: , label: 'Work' }, + { name: 'School', icon: , label: 'School' }, + { name: 'LocalHospital', icon: , label: 'Medical' }, + { name: 'Sports', icon: , 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>({}); + + const handleSubmit = () => { + const newErrors: Record = {}; + + 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 ( + + + + + {editingRCard ? 'Edit Relationship Card' : 'Create New Relationship Card'} + + + + + + + + + + {/* Preview */} + + + + Preview + + + {getIconComponent(formData.icon)} + + + {formData.name || 'rCard Name'} + + + {formData.description || 'rCard description'} + + {editingRCard?.isDefault && ( + + )} + + + + {/* Form Fields */} + + + setFormData(prev => ({ ...prev, name: e.target.value }))} + error={!!errors.name} + helperText={errors.name} + placeholder="e.g., Close Friends, Work Colleagues, Gym Buddies" + /> + + + + 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" + /> + + + {/* Icon Selection */} + + + Choose an Icon + + + {AVAILABLE_ICONS.map((iconData) => ( + + setFormData(prev => ({ ...prev, icon: iconData.name }))} + > + + + {iconData.icon} + + + {iconData.label} + + + + + ))} + + + + {/* Color Selection */} + + + Choose a Color + + + {AVAILABLE_COLORS.map((color) => ( + setFormData(prev => ({ ...prev, color }))} + /> + ))} + + + + + {/* Default rCard Warning */} + {editingRCard?.isDefault && ( + + + Note: This is a default rCard. You can edit its name, description, and appearance, + but it cannot be deleted. + + + )} + + + + + + + {editingRCard && !editingRCard.isDefault && onDelete && ( + + )} + + + + + + + + + ); +}; + +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 ( + + ); +}; 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 - - - + + 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 + + + + + + + + + + + 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[] = [ {