Enhance contact management with vouches, praises and improved UX

- Added comprehensive vouch and praise system to ContactViewPage with side-by-side layout
- Fixed contact image centering issues using background-image approach
- Added vouch/praise indicators to contact list with proper styling
- Implemented My Home and My Collection pages with filter dropdowns
- Added Personhood Credentials component with QR code verification
- Renamed "Privacy & rCards" to "My Cards" and reordered account tabs
- Removed "Default" labels from rCard privacy settings
- Added Gmail import option alongside LinkedIn and Contacts
- Fixed notification dropdown layout issues and text wrapping
- Improved responsive design across contact views

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
main
Oliver Sylvester-Bradley 2 months ago
parent d6e3e54fb6
commit 451d2a2908
  1. 740
      src/components/account/MyCollectionPage.tsx
  2. 640
      src/components/account/MyHomePage.tsx
  3. 345
      src/components/account/PersonhoodCredentials.tsx
  4. 6
      src/components/account/RCardPrivacySettings.tsx
  5. 25
      src/components/layout/DashboardLayout.tsx
  6. 7
      src/components/notifications/NotificationDropdown.tsx
  7. 20
      src/components/notifications/NotificationItem.tsx
  8. 122
      src/pages/AccountPage.tsx
  9. 104
      src/pages/ContactListPage.tsx
  10. 245
      src/pages/ContactViewPage.tsx
  11. 3
      src/pages/GroupPage.tsx
  12. 56
      src/types/collection.ts
  13. 78
      src/types/personhood.ts
  14. 104
      src/types/userContent.ts

@ -0,0 +1,740 @@
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Chip,
Avatar,
IconButton,
Menu,
MenuItem,
TextField,
InputAdornment,
Grid,
Button,
Divider,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
FormControl,
InputLabel,
Select,
alpha,
useTheme,
} from '@mui/material';
import {
Search,
MoreVert,
Favorite,
FavoriteBorder,
Edit,
Delete,
Launch,
Article,
Image as ImageIcon,
Link as LinkIcon,
AttachFile,
LocalOffer,
ShoppingCart,
PostAdd,
Visibility,
FolderOpen,
QueryStats,
Psychology,
Send,
AutoFixHigh,
StarOutline,
AutoAwesome,
} from '@mui/icons-material';
import type { BookmarkedItem, Collection, CollectionFilter, CollectionStats } from '../../types/collection';
interface MyCollectionPageProps {
// Props would come from parent component
}
const MyCollectionPage = ({}: MyCollectionPageProps) => {
const theme = useTheme();
const [items, setItems] = useState<BookmarkedItem[]>([]);
const [collections, setCollections] = useState<Collection[]>([]);
const [filteredItems, setFilteredItems] = useState<BookmarkedItem[]>([]);
const [filter] = useState<CollectionFilter>({});
const [searchQuery, setSearchQuery] = useState('');
const [selectedCollection, setSelectedCollection] = useState<string>('all');
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [sortBy, setSortBy] = useState<'recent' | 'title' | 'author'>('recent');
const [menuAnchor, setMenuAnchor] = useState<{ [key: string]: HTMLElement | null }>({});
const [showQueryDialog, setShowQueryDialog] = useState(false);
const [queryText, setQueryText] = useState('');
const [stats, setStats] = useState<CollectionStats>({
totalItems: 0,
unreadItems: 0,
favoriteItems: 0,
byType: {},
byCategory: {},
recentlyAdded: 0,
});
// Mock data
useEffect(() => {
const mockCollections: Collection[] = [
{
id: 'reading-list',
name: 'Reading List',
description: 'Articles to read later',
items: [],
isDefault: true,
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
updatedAt: new Date(),
},
{
id: 'design-inspiration',
name: 'Design Inspiration',
description: 'Design ideas and inspiration',
items: [],
isDefault: false,
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 20),
updatedAt: new Date(),
},
{
id: 'tech-resources',
name: 'Tech Resources',
description: 'Useful development resources',
items: [],
isDefault: false,
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 15),
updatedAt: new Date(),
},
];
const mockItems: BookmarkedItem[] = [
{
id: '1',
originalId: 'article-123',
type: 'article',
title: 'The Future of Web Development',
description: 'An in-depth look at emerging trends in web development including AI integration and new frameworks.',
content: 'Web development is evolving rapidly with new technologies...',
author: {
id: 'author-1',
name: 'Sarah Johnson',
avatar: '/api/placeholder/40/40',
},
source: 'TechBlog',
bookmarkedAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
tags: ['web-development', 'ai', 'trends'],
notes: 'Good insights on AI integration. Need to research the frameworks mentioned.',
category: 'Technology',
isRead: false,
isFavorite: true,
},
{
id: '2',
originalId: 'post-456',
type: 'post',
title: 'Remote Work Best Practices',
description: 'Tips for staying productive while working remotely',
content: 'Working remotely requires discipline and the right tools...',
author: {
id: 'author-2',
name: 'Mike Chen',
avatar: '/api/placeholder/40/40',
},
source: 'LinkedIn',
bookmarkedAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
tags: ['remote-work', 'productivity', 'tips'],
category: 'Work',
isRead: true,
isFavorite: false,
},
{
id: '3',
originalId: 'link-789',
type: 'link',
title: 'Design System Component Library',
url: 'https://designsystem.example.com',
description: 'Comprehensive component library for modern design systems',
author: {
id: 'author-3',
name: 'Design Team',
avatar: '/api/placeholder/40/40',
},
source: 'Design Community',
bookmarkedAt: new Date(Date.now() - 1000 * 60 * 60 * 48),
tags: ['design-system', 'components', 'ui'],
notes: 'Great reference for our upcoming design system project',
category: 'Design',
isRead: false,
isFavorite: true,
},
{
id: '4',
originalId: 'offer-101',
type: 'offer',
title: 'Freelance React Developer Available',
description: 'Experienced React developer offering freelance services',
author: {
id: 'author-4',
name: 'Alex Rodriguez',
avatar: '/api/placeholder/40/40',
},
source: 'Freelance Board',
bookmarkedAt: new Date(Date.now() - 1000 * 60 * 60 * 72),
tags: ['react', 'freelance', 'development'],
category: 'Opportunities',
isRead: true,
isFavorite: false,
},
{
id: '5',
originalId: 'image-202',
type: 'image',
title: 'Modern Office Interior Design',
imageUrl: '/api/placeholder/600/400',
description: 'Beautiful modern office space with natural lighting',
author: {
id: 'author-5',
name: 'Interior Design Studio',
avatar: '/api/placeholder/40/40',
},
source: 'Design Portfolio',
bookmarkedAt: new Date(Date.now() - 1000 * 60 * 60 * 96),
tags: ['office', 'interior', 'modern'],
category: 'Design',
isRead: true,
isFavorite: true,
},
{
id: '6',
originalId: 'file-303',
type: 'file',
title: 'Product Strategy Template',
description: 'Comprehensive template for product strategy documentation',
author: {
id: 'author-6',
name: 'Product Manager',
avatar: '/api/placeholder/40/40',
},
source: 'Product Community',
bookmarkedAt: new Date(Date.now() - 1000 * 60 * 60 * 120),
tags: ['product', 'strategy', 'template'],
notes: 'Use this for Q2 strategy planning',
category: 'Product',
isRead: false,
isFavorite: false,
},
];
setCollections(mockCollections);
setItems(mockItems);
setFilteredItems(mockItems);
// Calculate stats
const categories = [...new Set(mockItems.map(item => item.category).filter(Boolean))];
const types = [...new Set(mockItems.map(item => item.type))];
const recentDate = new Date(Date.now() - 1000 * 60 * 60 * 24 * 7);
const newStats: CollectionStats = {
totalItems: mockItems.length,
unreadItems: mockItems.filter(item => !item.isRead).length,
favoriteItems: mockItems.filter(item => item.isFavorite).length,
byType: types.reduce((acc, type) => ({
...acc,
[type]: mockItems.filter(item => item.type === type).length
}), {}),
byCategory: categories.reduce((acc, category) => ({
...acc,
[category || 'Uncategorized']: mockItems.filter(item => item.category === category).length
}), {}),
recentlyAdded: mockItems.filter(item => item.bookmarkedAt > recentDate).length,
};
setStats(newStats);
}, []);
// Filter and sort items
useEffect(() => {
let filtered = [...items];
// Filter by collection
if (selectedCollection !== 'all') {
// In a real implementation, this would filter by collection membership
filtered = filtered; // For now, show all items
}
// Filter by category
if (selectedCategory !== 'all') {
filtered = filtered.filter(item => item.category === selectedCategory);
}
// Filter by search query
if (searchQuery) {
filtered = filtered.filter(item =>
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.content?.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.author.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.tags.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase())) ||
item.notes?.toLowerCase().includes(searchQuery.toLowerCase())
);
}
// Sort items
filtered.sort((a, b) => {
switch (sortBy) {
case 'title':
return a.title.localeCompare(b.title);
case 'author':
return a.author.name.localeCompare(b.author.name);
case 'recent':
default:
return b.bookmarkedAt.getTime() - a.bookmarkedAt.getTime();
}
});
setFilteredItems(filtered);
}, [items, selectedCollection, selectedCategory, searchQuery, sortBy, filter]);
const handleMenuOpen = (itemId: string, event: React.MouseEvent<HTMLElement>) => {
setMenuAnchor({ ...menuAnchor, [itemId]: event.currentTarget });
};
const handleMenuClose = (itemId: string) => {
setMenuAnchor({ ...menuAnchor, [itemId]: null });
};
const handleToggleFavorite = (itemId: string) => {
setItems(prev => prev.map(item =>
item.id === itemId ? { ...item, isFavorite: !item.isFavorite } : item
));
};
const handleMarkAsRead = (itemId: string) => {
setItems(prev => prev.map(item =>
item.id === itemId ? { ...item, isRead: true, lastViewedAt: new Date() } : item
));
};
const handleRunQuery = () => {
// In a real implementation, this would use AI to query the bookmarked content
console.log('Running query:', queryText);
setShowQueryDialog(false);
setQueryText('');
};
const getContentIcon = (type: string) => {
switch (type) {
case 'post': return <PostAdd />;
case 'offer': return <LocalOffer />;
case 'want': return <ShoppingCart />;
case 'image': return <ImageIcon />;
case 'link': return <LinkIcon />;
case 'file': return <AttachFile />;
case 'article': return <Article />;
default: return <PostAdd />;
}
};
const formatDate = (date: Date) => {
const now = new Date();
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
if (diffInHours < 24) {
return `${Math.floor(diffInHours)}h ago`;
} else if (diffInHours < 168) {
return `${Math.floor(diffInHours / 24)}d ago`;
} else {
return date.toLocaleDateString();
}
};
const categories = [...new Set(items.map(item => item.category).filter(Boolean))];
const renderBookmarkedItem = (item: BookmarkedItem) => (
<Card key={item.id} sx={{ mb: 2 }}>
<CardContent>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main' }}>
{getContentIcon(item.type)}
</Avatar>
<Box>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
{item.title}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
<Chip
label={item.type.charAt(0).toUpperCase() + item.type.slice(1)}
size="small"
variant="outlined"
/>
{item.category && (
<Chip
label={item.category}
size="small"
variant="outlined"
color="secondary"
/>
)}
{!item.isRead && (
<Chip
label="Unread"
size="small"
color="warning"
/>
)}
<Typography variant="caption" color="text.secondary">
{formatDate(item.bookmarkedAt)}
</Typography>
</Box>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<IconButton
size="small"
onClick={() => handleToggleFavorite(item.id)}
color={item.isFavorite ? 'error' : 'default'}
>
{item.isFavorite ? <Favorite /> : <FavoriteBorder />}
</IconButton>
<IconButton
size="small"
onClick={(e) => handleMenuOpen(item.id, e)}
>
<MoreVert />
</IconButton>
</Box>
</Box>
{/* Author */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
<Avatar src={item.author.avatar} sx={{ width: 24, height: 24 }}>
{item.author.name.charAt(0)}
</Avatar>
<Typography variant="body2" color="text.secondary">
by {item.author.name} {item.source}
</Typography>
</Box>
{/* Content */}
{item.description && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{item.description}
</Typography>
)}
{/* Image for image type */}
{item.type === 'image' && item.imageUrl && (
<Box
component="img"
src={item.imageUrl}
sx={{
width: '100%',
maxHeight: 200,
objectFit: 'cover',
borderRadius: 1,
mb: 2,
}}
/>
)}
{/* User Notes */}
{item.notes && (
<Box sx={{
p: 2,
bgcolor: alpha(theme.palette.primary.main, 0.04),
borderRadius: 1,
mb: 2,
borderLeft: 4,
borderColor: 'primary.main'
}}>
<Typography variant="body2" sx={{ fontStyle: 'italic' }}>
"{item.notes}"
</Typography>
</Box>
)}
{/* Tags */}
{item.tags && item.tags.length > 0 && (
<Box sx={{ mb: 2 }}>
{item.tags.map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
variant="outlined"
sx={{ mr: 0.5, mb: 0.5 }}
/>
))}
</Box>
)}
<Divider sx={{ mb: 2 }} />
{/* Actions */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', gap: 1 }}>
{!item.isRead && (
<Button
size="small"
onClick={() => handleMarkAsRead(item.id)}
startIcon={<Visibility />}
>
Mark as Read
</Button>
)}
<Button size="small" startIcon={<Launch />}>
Open
</Button>
</Box>
<Typography variant="caption" color="text.secondary">
Saved {formatDate(item.bookmarkedAt)}
</Typography>
</Box>
</CardContent>
{/* Menu */}
<Menu
anchorEl={menuAnchor[item.id]}
open={Boolean(menuAnchor[item.id])}
onClose={() => handleMenuClose(item.id)}
>
<MenuItem onClick={() => handleMenuClose(item.id)}>
<Edit sx={{ mr: 1 }} /> Edit Notes
</MenuItem>
<MenuItem onClick={() => handleMenuClose(item.id)}>
<FolderOpen sx={{ mr: 1 }} /> Move to Collection
</MenuItem>
<MenuItem onClick={() => handleMenuClose(item.id)}>
<Launch sx={{ mr: 1 }} /> Open Original
</MenuItem>
<MenuItem onClick={() => handleMenuClose(item.id)}>
<Delete sx={{ mr: 1 }} /> Remove
</MenuItem>
</Menu>
</Card>
);
return (
<Box>
{/* Header */}
<Box sx={{ mb: 4 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h5" sx={{ fontWeight: 600 }}>
My Collection
</Typography>
<Button
variant="contained"
startIcon={<AutoAwesome />}
onClick={() => setShowQueryDialog(true)}
>
Query Collection
</Button>
</Box>
</Box>
{/* Search and Filters */}
<Box sx={{ mb: 3 }}>
<TextField
fullWidth
placeholder="Search your bookmarks..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
}}
sx={{ mb: 2 }}
/>
{/* Filters */}
<Grid container spacing={2} sx={{ mb: 2 }}>
<Grid size={{ xs: 12, md: 4 }}>
<FormControl fullWidth size="small">
<InputLabel>Collection</InputLabel>
<Select
value={selectedCollection}
label="Collection"
onChange={(e) => setSelectedCollection(e.target.value)}
>
<MenuItem value="all">All Collections</MenuItem>
{collections.map((collection) => (
<MenuItem key={collection.id} value={collection.id}>
{collection.name}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<FormControl fullWidth size="small">
<InputLabel>Category</InputLabel>
<Select
value={selectedCategory}
label="Category"
onChange={(e) => setSelectedCategory(e.target.value)}
>
<MenuItem value="all">All Categories</MenuItem>
{categories.map((category) => (
<MenuItem key={category} value={category}>
{category}
</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid size={{ xs: 12, md: 4 }}>
<FormControl fullWidth size="small">
<InputLabel>Sort By</InputLabel>
<Select
value={sortBy}
label="Sort By"
onChange={(e) => setSortBy(e.target.value as any)}
>
<MenuItem value="recent">Recently Added</MenuItem>
<MenuItem value="title">Title</MenuItem>
<MenuItem value="author">Author</MenuItem>
</Select>
</FormControl>
</Grid>
</Grid>
</Box>
{/* Content List */}
<Box>
{filteredItems.length === 0 ? (
<Card>
<CardContent sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary" gutterBottom>
No bookmarks found
</Typography>
<Typography variant="body2" color="text.secondary">
{searchQuery
? `No bookmarks match "${searchQuery}"`
: "You haven't bookmarked any content yet"
}
</Typography>
</CardContent>
</Card>
) : (
filteredItems.map(renderBookmarkedItem)
)}
</Box>
{/* Query Dialog */}
<Dialog open={showQueryDialog} onClose={() => setShowQueryDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AutoAwesome />
AI Query Assistant
</Box>
</DialogTitle>
<DialogContent sx={{ p: 0 }}>
{/* Chat-like interface */}
<Box sx={{
height: 400,
display: 'flex',
flexDirection: 'column',
bgcolor: alpha(theme.palette.background.default, 0.5)
}}>
{/* Messages area */}
<Box sx={{
flexGrow: 1,
p: 3,
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: 2
}}>
{/* AI intro message */}
<Box sx={{ display: 'flex', gap: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main', width: 32, height: 32 }}>
<AutoAwesome fontSize="small" />
</Avatar>
<Box sx={{
bgcolor: 'background.paper',
p: 2,
borderRadius: 2,
border: 1,
borderColor: 'divider',
maxWidth: '80%'
}}>
<Typography variant="body2">
Hi! I'm your AI assistant. I can help you search and analyze your bookmarked content.
Ask me anything about your saved articles, posts, and notes.
</Typography>
</Box>
</Box>
</Box>
{/* Input area */}
<Box sx={{
p: 2,
borderTop: 1,
borderColor: 'divider',
bgcolor: 'background.paper'
}}>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
<TextField
fullWidth
multiline
maxRows={4}
placeholder="Ask me about your collection... (e.g., Show me all articles about design systems from the last month)"
value={queryText}
onChange={(e) => setQueryText(e.target.value)}
variant="outlined"
size="small"
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
bgcolor: alpha(theme.palette.background.default, 0.5),
'&:hover': {
bgcolor: alpha(theme.palette.background.default, 0.7),
},
'&.Mui-focused': {
bgcolor: 'background.paper',
}
}
}}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
if (queryText.trim()) {
handleRunQuery();
}
}
}}
/>
<Button
variant="contained"
onClick={handleRunQuery}
disabled={!queryText.trim()}
sx={{
minWidth: 'auto',
px: 2,
borderRadius: 3,
height: 40
}}
>
<Send fontSize="small" />
</Button>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
Press Enter to send, Shift+Enter for new line
</Typography>
</Box>
</Box>
</DialogContent>
<DialogActions sx={{ p: 2 }}>
<Button onClick={() => setShowQueryDialog(false)}>Close</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default MyCollectionPage;

@ -0,0 +1,640 @@
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Card,
CardContent,
Chip,
Avatar,
IconButton,
Menu,
MenuItem,
TextField,
InputAdornment,
Grid,
Button,
Divider,
} from '@mui/material';
import {
Search,
FilterList,
MoreVert,
Visibility,
VisibilityOff,
Edit,
Delete,
Share,
ThumbUp,
Comment,
Download,
Launch,
Article,
Image as ImageIcon,
Link as LinkIcon,
AttachFile,
LocalOffer,
ShoppingCart,
PostAdd,
} from '@mui/icons-material';
import type { UserContent, ContentFilter, ContentStats, ContentType } from '../../types/userContent';
interface MyHomePageProps {
// Props would come from parent component
}
const MyHomePage = ({}: MyHomePageProps) => {
const [content, setContent] = useState<UserContent[]>([]);
const [filteredContent, setFilteredContent] = useState<UserContent[]>([]);
const [filter] = useState<ContentFilter>({});
const [searchQuery, setSearchQuery] = useState('');
const [selectedTab, setSelectedTab] = useState<'all' | ContentType>('all');
const [menuAnchor, setMenuAnchor] = useState<{ [key: string]: HTMLElement | null }>({});
const [filterMenuAnchor, setFilterMenuAnchor] = useState<HTMLElement | null>(null);
const [stats, setStats] = useState<ContentStats>({
totalItems: 0,
byType: {
post: 0,
offer: 0,
want: 0,
image: 0,
link: 0,
file: 0,
article: 0,
},
byVisibility: {
public: 0,
network: 0,
private: 0,
},
totalViews: 0,
totalLikes: 0,
totalComments: 0,
});
// Mock data
useEffect(() => {
const mockContent: UserContent[] = [
{
id: '1',
type: 'post',
title: 'Thoughts on Remote Work Culture',
content: 'After working remotely for 3 years, I\'ve learned that the key to success is creating boundaries and maintaining human connections...',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2), // 2 hours ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 2),
tags: ['remote-work', 'productivity', 'culture'],
visibility: 'public',
viewCount: 245,
likeCount: 18,
commentCount: 7,
rCardIds: ['business', 'colleague'],
attachments: [],
},
{
id: '2',
type: 'offer',
title: 'UI/UX Design Consultation',
description: 'Offering design consultation services for early-stage startups',
content: 'I\'m offering UI/UX design consultation for early-stage startups. 10+ years experience with SaaS products.',
category: 'Design Services',
price: '$150/hour',
availability: 'available',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24), // 1 day ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 24),
tags: ['design', 'consultation', 'startup'],
visibility: 'network',
viewCount: 89,
likeCount: 12,
commentCount: 3,
rCardIds: ['business', 'colleague'],
},
{
id: '3',
type: 'want',
title: 'Looking for React Native Developer',
description: 'Need an experienced React Native developer for mobile app project',
content: 'Looking for an experienced React Native developer to help with a mobile app project. 3-month contract, remote work possible.',
category: 'Development',
budget: '$5000-8000',
urgency: 'high',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 48), // 2 days ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 48),
tags: ['react-native', 'mobile', 'contract'],
visibility: 'public',
viewCount: 156,
likeCount: 8,
commentCount: 15,
rCardIds: ['business'],
},
{
id: '4',
type: 'link',
title: 'Great Article on Design Systems',
url: 'https://designsystems.com/article',
linkTitle: 'Building Scalable Design Systems',
linkDescription: 'A comprehensive guide to creating and maintaining design systems that scale with your organization.',
domain: 'designsystems.com',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 72), // 3 days ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 72),
tags: ['design-systems', 'article', 'resource'],
visibility: 'public',
viewCount: 67,
likeCount: 14,
commentCount: 2,
rCardIds: ['business', 'colleague'],
},
{
id: '5',
type: 'image',
title: 'Office Setup 2024',
imageUrl: '/api/placeholder/600/400',
imageAlt: 'Modern home office setup with dual monitors',
caption: 'Finally got my home office setup just right! Dual 4K monitors and a standing desk make all the difference.',
dimensions: { width: 600, height: 400 },
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 96), // 4 days ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 96),
tags: ['office', 'setup', 'workspace'],
visibility: 'network',
viewCount: 123,
likeCount: 24,
commentCount: 9,
rCardIds: ['colleague', 'friend'],
},
{
id: '6',
type: 'file',
title: 'Product Requirements Template',
fileName: 'PRD_Template_v2.pdf',
fileUrl: '/files/prd-template.pdf',
fileSize: 2048576, // 2MB
fileType: 'application/pdf',
downloadCount: 45,
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 120), // 5 days ago
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 120),
tags: ['template', 'product', 'documentation'],
visibility: 'public',
viewCount: 89,
likeCount: 16,
commentCount: 4,
rCardIds: ['business'],
},
{
id: '7',
type: 'article',
title: 'The Future of Product Management',
content: 'In this comprehensive article, I explore how AI and automation are reshaping the role of product managers...',
excerpt: 'AI and automation are reshaping product management. Here\'s what PMs need to know about the future.',
readTime: 8,
publishedAt: new Date(Date.now() - 1000 * 60 * 60 * 168), // 1 week ago
featuredImage: '/api/placeholder/400/200',
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 168),
updatedAt: new Date(Date.now() - 1000 * 60 * 60 * 168),
tags: ['product-management', 'ai', 'future'],
visibility: 'public',
viewCount: 342,
likeCount: 28,
commentCount: 12,
rCardIds: ['business', 'colleague'],
},
];
setContent(mockContent);
setFilteredContent(mockContent);
// Calculate stats
const newStats: ContentStats = {
totalItems: mockContent.length,
byType: {
post: mockContent.filter(c => c.type === 'post').length,
offer: mockContent.filter(c => c.type === 'offer').length,
want: mockContent.filter(c => c.type === 'want').length,
image: mockContent.filter(c => c.type === 'image').length,
link: mockContent.filter(c => c.type === 'link').length,
file: mockContent.filter(c => c.type === 'file').length,
article: mockContent.filter(c => c.type === 'article').length,
},
byVisibility: {
public: mockContent.filter(c => c.visibility === 'public').length,
network: mockContent.filter(c => c.visibility === 'network').length,
private: mockContent.filter(c => c.visibility === 'private').length,
},
totalViews: mockContent.reduce((sum, c) => sum + c.viewCount, 0),
totalLikes: mockContent.reduce((sum, c) => sum + c.likeCount, 0),
totalComments: mockContent.reduce((sum, c) => sum + c.commentCount, 0),
};
setStats(newStats);
}, []);
// Filter content
useEffect(() => {
let filtered = [...content];
// Filter by type
if (selectedTab !== 'all') {
filtered = filtered.filter(item => item.type === selectedTab);
}
// Filter by search query
if (searchQuery) {
filtered = filtered.filter(item =>
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.tags?.some(tag => tag.toLowerCase().includes(searchQuery.toLowerCase()))
);
}
setFilteredContent(filtered);
}, [content, selectedTab, searchQuery, filter]);
const handleMenuOpen = (contentId: string, event: React.MouseEvent<HTMLElement>) => {
setMenuAnchor({ ...menuAnchor, [contentId]: event.currentTarget });
};
const handleMenuClose = (contentId: string) => {
setMenuAnchor({ ...menuAnchor, [contentId]: null });
};
const handleFilterMenuOpen = (event: React.MouseEvent<HTMLElement>) => {
setFilterMenuAnchor(event.currentTarget);
};
const handleFilterMenuClose = () => {
setFilterMenuAnchor(null);
};
const handleFilterSelect = (filterType: 'all' | ContentType) => {
setSelectedTab(filterType);
handleFilterMenuClose();
};
const getContentIcon = (type: ContentType) => {
switch (type) {
case 'post': return <PostAdd />;
case 'offer': return <LocalOffer />;
case 'want': return <ShoppingCart />;
case 'image': return <ImageIcon />;
case 'link': return <LinkIcon />;
case 'file': return <AttachFile />;
case 'article': return <Article />;
default: return <PostAdd />;
}
};
const getVisibilityIcon = (visibility: string) => {
switch (visibility) {
case 'public': return <Visibility />;
case 'network': return <VisibilityOff />;
case 'private': return <VisibilityOff />;
default: return <Visibility />;
}
};
const formatFileSize = (bytes: number) => {
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
if (bytes === 0) return '0 Bytes';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
};
const formatDate = (date: Date) => {
const now = new Date();
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
if (diffInHours < 24) {
return `${Math.floor(diffInHours)}h ago`;
} else if (diffInHours < 168) {
return `${Math.floor(diffInHours / 24)}d ago`;
} else {
return date.toLocaleDateString();
}
};
const renderContentItem = (item: UserContent) => (
<Card key={item.id} sx={{ mb: 2 }}>
<CardContent>
{/* Header */}
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main' }}>
{getContentIcon(item.type)}
</Avatar>
<Box>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
{item.title}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 0.5 }}>
<Chip
label={item.type.charAt(0).toUpperCase() + item.type.slice(1)}
size="small"
variant="outlined"
/>
<Chip
icon={getVisibilityIcon(item.visibility)}
label={item.visibility.charAt(0).toUpperCase() + item.visibility.slice(1)}
size="small"
variant="outlined"
/>
<Typography variant="caption" color="text.secondary">
{formatDate(item.createdAt)}
</Typography>
</Box>
</Box>
</Box>
<IconButton
size="small"
onClick={(e) => handleMenuOpen(item.id, e)}
>
<MoreVert />
</IconButton>
</Box>
{/* Content based on type */}
{(item.type === 'post' || item.type === 'article') && (
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
{'content' in item ? item.content.substring(0, 200) + (item.content.length > 200 ? '...' : '') : ''}
</Typography>
)}
{item.type === 'offer' && 'price' in item && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{item.content}
</Typography>
<Chip label={item.price} color="success" size="small" />
<Chip label={item.availability} color="primary" size="small" sx={{ ml: 1 }} />
</Box>
)}
{item.type === 'want' && 'budget' in item && (
<Box sx={{ mb: 2 }}>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{item.content}
</Typography>
<Chip label={item.budget} color="info" size="small" />
<Chip label={`${item.urgency} priority`} color="warning" size="small" sx={{ ml: 1 }} />
</Box>
)}
{item.type === 'link' && 'url' in item && (
<Box sx={{ mb: 2, p: 2, bgcolor: 'grey.50', borderRadius: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{item.linkTitle}
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{item.linkDescription}
</Typography>
<Typography variant="caption" color="primary.main">
{item.domain}
</Typography>
</Box>
)}
{item.type === 'image' && 'imageUrl' in item && (
<Box sx={{ mb: 2 }}>
<Box
component="img"
src={item.imageUrl}
alt={item.imageAlt}
sx={{
width: '100%',
maxHeight: 300,
objectFit: 'cover',
borderRadius: 1,
mb: 1,
}}
/>
<Typography variant="body2" color="text.secondary">
{item.caption}
</Typography>
</Box>
)}
{item.type === 'file' && 'fileName' in item && (
<Box sx={{ mb: 2, p: 2, bgcolor: 'grey.50', borderRadius: 1, display: 'flex', alignItems: 'center', gap: 2 }}>
<AttachFile />
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle2">{item.fileName}</Typography>
<Typography variant="caption" color="text.secondary">
{formatFileSize(item.fileSize)} {item.downloadCount} downloads
</Typography>
</Box>
<Button size="small" startIcon={<Download />}>
Download
</Button>
</Box>
)}
{item.type === 'article' && 'readTime' in item && (
<Box sx={{ mb: 2 }}>
{'featuredImage' in item && item.featuredImage && (
<Box
component="img"
src={item.featuredImage}
sx={{
width: '100%',
height: 200,
objectFit: 'cover',
borderRadius: 1,
mb: 2,
}}
/>
)}
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
{item.excerpt}
</Typography>
<Typography variant="caption" color="text.secondary">
{item.readTime} min read
</Typography>
</Box>
)}
{/* Tags */}
{item.tags && item.tags.length > 0 && (
<Box sx={{ mb: 2 }}>
{item.tags.map((tag) => (
<Chip
key={tag}
label={tag}
size="small"
variant="outlined"
sx={{ mr: 0.5, mb: 0.5 }}
/>
))}
</Box>
)}
<Divider sx={{ mb: 2 }} />
{/* Stats */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
<Comment fontSize="small" />
<Typography variant="caption">{item.commentCount}</Typography>
</Box>
</Box>
<Button size="small" startIcon={<Share />}>
Share
</Button>
</Box>
</CardContent>
{/* Menu */}
<Menu
anchorEl={menuAnchor[item.id]}
open={Boolean(menuAnchor[item.id])}
onClose={() => handleMenuClose(item.id)}
>
<MenuItem onClick={() => handleMenuClose(item.id)}>
<Edit sx={{ mr: 1 }} /> Edit
</MenuItem>
<MenuItem onClick={() => handleMenuClose(item.id)}>
<Launch sx={{ mr: 1 }} /> View Details
</MenuItem>
<MenuItem onClick={() => handleMenuClose(item.id)}>
<Delete sx={{ mr: 1 }} /> Delete
</MenuItem>
</Menu>
</Card>
);
return (
<Box>
{/* Header */}
<Box sx={{ mb: 4 }}>
<Typography variant="h5" sx={{ fontWeight: 600, mb: 2 }}>
My Content
</Typography>
</Box>
{/* Search and Filters */}
<Box sx={{ mb: 3 }}>
<TextField
fullWidth
placeholder="Search your content..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search />
</InputAdornment>
),
endAdornment: (
<InputAdornment position="end">
<IconButton size="small" onClick={handleFilterMenuOpen}>
<FilterList />
</IconButton>
</InputAdornment>
),
}}
sx={{ mb: 2 }}
/>
{/* Filter Menu */}
<Menu
anchorEl={filterMenuAnchor}
open={Boolean(filterMenuAnchor)}
onClose={handleFilterMenuClose}
PaperProps={{
sx: { mt: 1 }
}}
>
<MenuItem onClick={() => handleFilterSelect('all')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Typography>All</Typography>
<Chip size="small" label={stats.totalItems} variant="outlined" />
</Box>
</MenuItem>
<MenuItem onClick={() => handleFilterSelect('post')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<PostAdd fontSize="small" />
<Typography>Posts</Typography>
</Box>
<Chip size="small" label={stats.byType.post} variant="outlined" />
</Box>
</MenuItem>
<MenuItem onClick={() => handleFilterSelect('offer')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<LocalOffer fontSize="small" />
<Typography>Offers</Typography>
</Box>
<Chip size="small" label={stats.byType.offer} variant="outlined" />
</Box>
</MenuItem>
<MenuItem onClick={() => handleFilterSelect('want')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ShoppingCart fontSize="small" />
<Typography>Wants</Typography>
</Box>
<Chip size="small" label={stats.byType.want} variant="outlined" />
</Box>
</MenuItem>
<MenuItem onClick={() => handleFilterSelect('image')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<ImageIcon fontSize="small" />
<Typography>Images</Typography>
</Box>
<Chip size="small" label={stats.byType.image} variant="outlined" />
</Box>
</MenuItem>
<MenuItem onClick={() => handleFilterSelect('link')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<LinkIcon fontSize="small" />
<Typography>Links</Typography>
</Box>
<Chip size="small" label={stats.byType.link} variant="outlined" />
</Box>
</MenuItem>
<MenuItem onClick={() => handleFilterSelect('file')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<AttachFile fontSize="small" />
<Typography>Files</Typography>
</Box>
<Chip size="small" label={stats.byType.file} variant="outlined" />
</Box>
</MenuItem>
<MenuItem onClick={() => handleFilterSelect('article')}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Article fontSize="small" />
<Typography>Articles</Typography>
</Box>
<Chip size="small" label={stats.byType.article} variant="outlined" />
</Box>
</MenuItem>
</Menu>
</Box>
{/* Content List */}
<Box>
{filteredContent.length === 0 ? (
<Card>
<CardContent sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h6" color="text.secondary" gutterBottom>
No content found
</Typography>
<Typography variant="body2" color="text.secondary">
{searchQuery
? `No content matches "${searchQuery}"`
: selectedTab === 'all'
? "You haven't shared any content yet"
: `No ${selectedTab} content found`
}
</Typography>
</CardContent>
</Card>
) : (
filteredContent.map(renderContentItem)
)}
</Box>
</Box>
);
};
export default MyHomePage;

@ -0,0 +1,345 @@
import { useState } from 'react';
import {
Card,
CardContent,
Typography,
Box,
Avatar,
Chip,
Button,
Grid,
LinearProgress,
IconButton,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
List,
ListItem,
ListItemAvatar,
ListItemText,
Divider,
Badge,
Tooltip,
useTheme,
alpha,
} from '@mui/material';
import {
VerifiedUser,
QrCode,
Share,
Timeline,
LocationOn,
CalendarToday,
TrendingUp,
Security,
People,
Star,
Refresh,
Info,
Close,
} from '@mui/icons-material';
import { QRCodeSVG } from 'qrcode.react';
import type { PersonhoodCredentials, PersonhoodVerification } from '../../types/personhood';
interface PersonhoodCredentialsProps {
credentials: PersonhoodCredentials;
onGenerateQR: () => void;
onRefreshCredentials: () => void;
}
const PersonhoodCredentials = ({
credentials,
onGenerateQR,
onRefreshCredentials
}: PersonhoodCredentialsProps) => {
const theme = useTheme();
const [showQRDialog, setShowQRDialog] = useState(false);
const [showHistoryDialog, setShowHistoryDialog] = useState(false);
const getCredibilityLevel = (score: number) => {
if (score >= 90) return { label: 'Excellent', color: 'success' as const };
if (score >= 75) return { label: 'High', color: 'info' as const };
if (score >= 60) return { label: 'Good', color: 'warning' as const };
if (score >= 40) return { label: 'Fair', color: 'warning' as const };
return { label: 'Low', color: 'error' as const };
};
const formatDate = (date: Date) => {
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
}).format(date);
};
const formatRelativeTime = (date: Date) => {
const now = new Date();
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffInDays === 0) return 'Today';
if (diffInDays === 1) return 'Yesterday';
if (diffInDays < 7) return `${diffInDays} days ago`;
if (diffInDays < 30) return `${Math.floor(diffInDays / 7)} weeks ago`;
if (diffInDays < 365) return `${Math.floor(diffInDays / 30)} months ago`;
return `${Math.floor(diffInDays / 365)} years ago`;
};
const credibilityLevel = getCredibilityLevel(credentials.credibilityScore);
return (
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<VerifiedUser color="primary" sx={{ fontSize: 32 }} />
<Box>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Personhood Credentials
</Typography>
<Typography variant="body2" color="text.secondary">
Verified human authenticity through real-world connections
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
<Tooltip title="Generate verification QR code">
<Button
variant="outlined"
startIcon={<QrCode />}
onClick={() => setShowQRDialog(true)}
size="small"
>
My QR
</Button>
</Tooltip>
<Tooltip title="Refresh credentials">
<IconButton onClick={onRefreshCredentials} size="small">
<Refresh />
</IconButton>
</Tooltip>
</Box>
</Box>
{/* Total Verifications Card */}
<Card sx={{ mb: 3, background: `linear-gradient(135deg, ${alpha(theme.palette.primary.main, 0.1)}, ${alpha(theme.palette.secondary.main, 0.1)})` }}>
<CardContent>
<Box sx={{ textAlign: 'center' }}>
<Typography variant="h4" color="success.main" sx={{ fontWeight: 700 }}>
{credentials.totalVerifications}
</Typography>
<Typography variant="body2" color="text.secondary">
Total Verifications
</Typography>
</Box>
</CardContent>
</Card>
{/* Recent Verifications */}
<Card sx={{ mb: 3 }}>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
<Typography variant="h6" sx={{ fontWeight: 600 }}>
Recent Verifications
</Typography>
<Button
size="small"
endIcon={<Timeline />}
onClick={() => setShowHistoryDialog(true)}
>
View All
</Button>
</Box>
{credentials.verifications.slice(0, 3).map((verification) => (
<Box key={verification.id} sx={{ display: 'flex', alignItems: 'center', gap: 2, py: 1 }}>
<Avatar src={verification.verifierAvatar} sx={{ width: 40, height: 40 }}>
{verification.verifierName.charAt(0)}
</Avatar>
<Box sx={{ flexGrow: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{verification.verifierName}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{verification.location && (
<Chip
icon={<LocationOn />}
label={`${verification.location.city}, ${verification.location.country}`}
size="small"
variant="outlined"
/>
)}
<Typography variant="caption" color="text.secondary">
{formatRelativeTime(verification.verifiedAt)}
</Typography>
</Box>
</Box>
<Box sx={{ textAlign: 'right' }}>
<Chip
label={`${verification.trustScore}%`}
size="small"
color={verification.trustScore >= 80 ? 'success' : verification.trustScore >= 60 ? 'warning' : 'error'}
/>
{verification.isReciprocal && (
<Tooltip title="Reciprocal verification">
<Star sx={{ color: 'gold', fontSize: 16, ml: 0.5 }} />
</Tooltip>
)}
</Box>
</Box>
))}
{credentials.verifications.length === 0 && (
<Box sx={{ textAlign: 'center', py: 4 }}>
<Typography variant="body2" color="text.secondary">
No verifications yet. Share your QR code with trusted contacts to start building your personhood credentials.
</Typography>
</Box>
)}
</CardContent>
</Card>
{/* Certificates */}
{credentials.certificates.length > 0 && (
<Card>
<CardContent>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 2 }}>
Earned Certificates
</Typography>
<Grid container spacing={2}>
{credentials.certificates.map((certificate) => (
<Grid size={{ xs: 12, sm: 6, md: 4 }} key={certificate.id}>
<Card variant="outlined">
<CardContent sx={{ textAlign: 'center', py: 2 }}>
<Badge color="success" variant="dot" invisible={!certificate.isActive}>
<Security sx={{ fontSize: 40, color: 'primary.main', mb: 1 }} />
</Badge>
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
{certificate.name}
</Typography>
<Typography variant="caption" color="text.secondary">
Issued {formatDate(certificate.issuedAt)}
</Typography>
</CardContent>
</Card>
</Grid>
))}
</Grid>
</CardContent>
</Card>
)}
{/* QR Code Dialog */}
<Dialog open={showQRDialog} onClose={() => setShowQRDialog(false)} maxWidth="sm" fullWidth>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6">My Verification QR Code</Typography>
<IconButton onClick={() => setShowQRDialog(false)} size="small">
<Close />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
<Box sx={{ textAlign: 'center', py: 2 }}>
<Box sx={{ display: 'inline-block', p: 2, bgcolor: 'white', borderRadius: 2, mb: 2 }}>
<QRCodeSVG
value={credentials.qrCode}
size={200}
level="M"
includeMargin={true}
bgColor="#ffffff"
fgColor="#000000"
/>
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 1 }}>
Scan to Verify My Personhood
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
Ask trusted contacts to scan this QR code to verify that you are a real human being. Each verification strengthens your credibility score.
</Typography>
<Chip
icon={<Info />}
label="QR code expires in 24 hours"
size="small"
variant="outlined"
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowQRDialog(false)}>Close</Button>
<Button
variant="contained"
startIcon={<Share />}
onClick={onGenerateQR}
>
Share QR Code
</Button>
</DialogActions>
</Dialog>
{/* Verification History Dialog */}
<Dialog open={showHistoryDialog} onClose={() => setShowHistoryDialog(false)} maxWidth="md" fullWidth>
<DialogTitle>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography variant="h6">Verification History</Typography>
<IconButton onClick={() => setShowHistoryDialog(false)} size="small">
<Close />
</IconButton>
</Box>
</DialogTitle>
<DialogContent>
<List>
{credentials.verifications.map((verification, index) => (
<Box key={verification.id}>
<ListItem>
<ListItemAvatar>
<Avatar src={verification.verifierAvatar}>
{verification.verifierName.charAt(0)}
</Avatar>
</ListItemAvatar>
<ListItemText
primary={verification.verifierName}
secondary={
<Box>
<Typography variant="body2" color="text.secondary">
{formatDate(verification.verifiedAt)} Trust Score: {verification.trustScore}%
</Typography>
{verification.location && (
<Typography variant="caption" color="text.secondary">
{verification.location.city}, {verification.location.country}
</Typography>
)}
{verification.notes && (
<Typography variant="caption" sx={{ fontStyle: 'italic', display: 'block' }}>
"{verification.notes}"
</Typography>
)}
</Box>
}
/>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Chip
label={verification.verificationMethod}
size="small"
variant="outlined"
/>
{verification.isReciprocal && (
<Tooltip title="Reciprocal verification">
<Star sx={{ color: 'gold' }} />
</Tooltip>
)}
</Box>
</ListItem>
{index < credentials.verifications.length - 1 && <Divider />}
</Box>
))}
</List>
</DialogContent>
<DialogActions>
<Button onClick={() => setShowHistoryDialog(false)}>Close</Button>
</DialogActions>
</Dialog>
</Box>
);
};
export default PersonhoodCredentials;

@ -164,12 +164,6 @@ const RCardPrivacySettings = ({ rCard, onUpdate }: RCardPrivacySettingsProps) =>
<Typography variant="h6" sx={{ fontWeight: 600 }}> <Typography variant="h6" sx={{ fontWeight: 600 }}>
Privacy Settings for {rCard.name} Privacy Settings for {rCard.name}
</Typography> </Typography>
<Chip
label={rCard.isDefault ? 'Default' : 'Custom'}
size="small"
variant="outlined"
color={rCard.isDefault ? 'default' : 'primary'}
/>
</Box> </Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 4 }}>

@ -327,31 +327,6 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => {
{navItems.map((item) => renderNavItem(item))} {navItems.map((item) => renderNavItem(item))}
</List> </List>
<Divider />
<List sx={{ py: 2 }}>
<ListItem disablePadding>
<ListItemButton
onClick={() => handleNavigation('/settings')}
sx={{
mx: 1,
borderRadius: 2,
minHeight: 48,
}}
>
<ListItemIcon sx={{ minWidth: 40 }}>
<Settings />
</ListItemIcon>
<ListItemText
primary="Settings"
primaryTypographyProps={{
fontSize: '0.875rem',
fontWeight: 500,
}}
/>
</ListItemButton>
</ListItem>
</List>
</Box> </Box>
); );

@ -100,6 +100,7 @@ const NotificationDropdown = ({
borderRadius: 2, borderRadius: 2,
border: 1, border: 1,
borderColor: 'divider', borderColor: 'divider',
overflow: 'hidden',
'&::before': { '&::before': {
content: '""', content: '""',
display: 'block', display: 'block',
@ -140,7 +141,7 @@ const NotificationDropdown = ({
</Box> </Box>
{/* Summary Stats */} {/* Summary Stats */}
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}> <Box sx={{ display: 'flex', gap: 1, mb: 2, flexWrap: 'wrap' }}>
<Chip <Chip
size="small" size="small"
label={`${summary.total} Total`} label={`${summary.total} Total`}
@ -165,7 +166,7 @@ const NotificationDropdown = ({
</Box> </Box>
{/* Filter Chips */} {/* Filter Chips */}
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Chip <Chip
size="small" size="small"
label="All" label="All"
@ -194,7 +195,7 @@ const NotificationDropdown = ({
</Box> </Box>
{/* Notification List */} {/* Notification List */}
<Box sx={{ maxHeight: 400, overflow: 'auto' }}> <Box sx={{ maxHeight: 400, overflow: 'auto', overflowX: 'hidden' }}>
{filteredNotifications.length === 0 ? ( {filteredNotifications.length === 0 ? (
<Box sx={{ p: 4, textAlign: 'center' }}> <Box sx={{ p: 4, textAlign: 'center' }}>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">

@ -183,7 +183,7 @@ const NotificationItem = ({
}, },
}} }}
> >
<Box sx={{ display: 'flex', width: '100%', gap: 2 }}> <Box sx={{ display: 'flex', width: '100%', gap: 2, minWidth: 0 }}>
{/* Avatar and Icon */} {/* Avatar and Icon */}
<Box sx={{ position: 'relative' }}> <Box sx={{ position: 'relative' }}>
<Avatar <Avatar
@ -213,11 +213,11 @@ const NotificationItem = ({
{/* Content */} {/* Content */}
<Box sx={{ flexGrow: 1, minWidth: 0 }}> <Box sx={{ flexGrow: 1, minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', mb: 1, gap: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 600, lineHeight: 1.2 }}> <Typography variant="subtitle2" sx={{ fontWeight: 600, lineHeight: 1.2, flexGrow: 1, minWidth: 0 }}>
{notification.title} {notification.title}
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexShrink: 0 }}>
{getStatusChip()} {getStatusChip()}
<IconButton size="small" onClick={handleMenuClick}> <IconButton size="small" onClick={handleMenuClick}>
<MoreVert /> <MoreVert />
@ -234,25 +234,27 @@ const NotificationItem = ({
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
overflow: 'hidden', overflow: 'hidden',
wordBreak: 'break-word',
overflowWrap: 'break-word',
}} }}
> >
{notification.message} {notification.message}
</Typography> </Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}> <Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', flexWrap: 'wrap', gap: 1 }}>
<Typography variant="caption" color="text.secondary"> <Typography variant="caption" color="text.secondary">
{formatTimeAgo(notification.createdAt)} {formatTimeAgo(notification.createdAt)}
</Typography> </Typography>
{/* Action Buttons */} {/* Action Buttons */}
{notification.isActionable && notification.status === 'pending' && ( {notification.isActionable && notification.status === 'pending' && (
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1, flexShrink: 0 }}>
<Button <Button
size="small" size="small"
variant="outlined" variant="outlined"
startIcon={<Cancel />} startIcon={<Cancel />}
onClick={handleReject} onClick={handleReject}
sx={{ textTransform: 'none', minWidth: 'auto' }} sx={{ textTransform: 'none', minWidth: 'auto', fontSize: '0.75rem' }}
> >
Decline Decline
</Button> </Button>
@ -261,7 +263,7 @@ const NotificationItem = ({
variant="contained" variant="contained"
startIcon={<CheckCircle />} startIcon={<CheckCircle />}
onClick={handleAccept} onClick={handleAccept}
sx={{ textTransform: 'none', minWidth: 'auto' }} sx={{ textTransform: 'none', minWidth: 'auto', fontSize: '0.75rem' }}
> >
Accept Accept
</Button> </Button>
@ -274,7 +276,7 @@ const NotificationItem = ({
variant="outlined" variant="outlined"
startIcon={<Assignment />} startIcon={<Assignment />}
onClick={handleAssignClick} onClick={handleAssignClick}
sx={{ textTransform: 'none' }} sx={{ textTransform: 'none', fontSize: '0.75rem', flexShrink: 0 }}
> >
Assign to rCard Assign to rCard
</Button> </Button>

@ -28,11 +28,17 @@ import {
Add, Add,
Edit, Edit,
Settings, Settings,
Dashboard,
BookmarkBorder,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { DEFAULT_RCARDS, DEFAULT_PRIVACY_SETTINGS } from '../types/notification'; import { DEFAULT_RCARDS, DEFAULT_PRIVACY_SETTINGS } from '../types/notification';
import type { RCardWithPrivacy } from '../types/notification'; import type { RCardWithPrivacy } from '../types/notification';
import RCardPrivacySettings from '../components/account/RCardPrivacySettings'; import RCardPrivacySettings from '../components/account/RCardPrivacySettings';
import RCardManagement from '../components/account/RCardManagement'; import RCardManagement from '../components/account/RCardManagement';
import MyHomePage from '../components/account/MyHomePage';
import MyCollectionPage from '../components/account/MyCollectionPage';
import PersonhoodCredentials from '../components/account/PersonhoodCredentials';
import type { PersonhoodCredentials as PersonhoodCredentialsType } from '../types/personhood';
interface TabPanelProps { interface TabPanelProps {
children?: React.ReactNode; children?: React.ReactNode;
@ -55,6 +61,78 @@ const AccountPage = () => {
const [selectedRCard, setSelectedRCard] = useState<RCardWithPrivacy | null>(null); const [selectedRCard, setSelectedRCard] = useState<RCardWithPrivacy | null>(null);
const [showRCardManagement, setShowRCardManagement] = useState(false); const [showRCardManagement, setShowRCardManagement] = useState(false);
const [editingRCard, setEditingRCard] = useState<RCardWithPrivacy | null>(null); const [editingRCard, setEditingRCard] = useState<RCardWithPrivacy | null>(null);
const [personhoodCredentials, setPersonhoodCredentials] = useState<PersonhoodCredentialsType>({
userId: 'user-123',
totalVerifications: 12,
uniqueVerifiers: 8,
reciprocalVerifications: 5,
averageTrustScore: 87.5,
credibilityScore: 92,
verificationStreak: 7,
lastVerificationAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
firstVerificationAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 365),
qrCode: 'https://nao.network/verify/user-123?code=abc123xyz',
verifications: [
{
id: 'ver-1',
verifierId: 'user-456',
verifierName: 'Sarah Johnson',
verifierAvatar: '/api/placeholder/40/40',
verifiedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 2),
location: { city: 'San Francisco', country: 'USA' },
verificationMethod: 'qr_scan',
trustScore: 95,
isReciprocal: true,
notes: 'Met at tech conference, verified in person',
isActive: true,
},
{
id: 'ver-2',
verifierId: 'user-789',
verifierName: 'Mike Chen',
verifierAvatar: '/api/placeholder/40/40',
verifiedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5),
location: { city: 'New York', country: 'USA' },
verificationMethod: 'qr_scan',
trustScore: 88,
isReciprocal: false,
isActive: true,
},
{
id: 'ver-3',
verifierId: 'user-321',
verifierName: 'Emma Davis',
verifierAvatar: '/api/placeholder/40/40',
verifiedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 10),
location: { city: 'London', country: 'UK' },
verificationMethod: 'qr_scan',
trustScore: 92,
isReciprocal: true,
notes: 'Colleague verification',
isActive: true,
},
],
certificates: [
{
id: 'cert-1',
type: 'basic',
name: 'Human Verified',
description: 'Basic human verification certificate',
requiredVerifications: 5,
issuedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 30),
isActive: true,
},
{
id: 'cert-2',
type: 'community',
name: 'Community Trusted',
description: 'Trusted by the community',
requiredVerifications: 10,
issuedAt: new Date(Date.now() - 1000 * 60 * 60 * 24 * 7),
isActive: true,
},
],
});
useEffect(() => { useEffect(() => {
// Initialize rCards with default privacy settings // Initialize rCards with default privacy settings
@ -135,6 +213,16 @@ const AccountPage = () => {
}); });
}; };
const handleGenerateQR = () => {
// In a real implementation, this would generate a new QR code
console.log('Generating new QR code...');
};
const handleRefreshCredentials = () => {
// In a real implementation, this would refresh credentials from server
console.log('Refreshing personhood credentials...');
};
const getRCardIcon = (iconName: string) => { const getRCardIcon = (iconName: string) => {
switch (iconName) { switch (iconName) {
case 'Business': case 'Business':
@ -182,7 +270,9 @@ const AccountPage = () => {
}} }}
> >
<Tab icon={<Person />} label="Profile" /> <Tab icon={<Person />} label="Profile" />
<Tab icon={<Security />} label="Privacy & rCards" /> <Tab icon={<Security />} label="My Cards" />
<Tab icon={<Dashboard />} label="My Home" />
<Tab icon={<BookmarkBorder />} label="My Collection" />
<Tab icon={<Settings />} label="Account Settings" /> <Tab icon={<Settings />} label="Account Settings" />
</Tabs> </Tabs>
@ -251,10 +341,19 @@ const AccountPage = () => {
</Card> </Card>
</Grid> </Grid>
</Grid> </Grid>
{/* Personhood Credentials Section */}
<Box sx={{ mt: 4 }}>
<PersonhoodCredentials
credentials={personhoodCredentials}
onGenerateQR={handleGenerateQR}
onRefreshCredentials={handleRefreshCredentials}
/>
</Box>
</Box> </Box>
</TabPanel> </TabPanel>
{/* Privacy & rCards Tab */} {/* My Cards Tab */}
<TabPanel value={tabValue} index={1}> <TabPanel value={tabValue} index={1}>
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Grid container spacing={3}> <Grid container spacing={3}>
@ -322,9 +421,6 @@ const AccountPage = () => {
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{rCard.isDefault && (
<Chip label="Default" size="small" variant="outlined" />
)}
<IconButton <IconButton
size="small" size="small"
onClick={(e) => { onClick={(e) => {
@ -365,8 +461,22 @@ const AccountPage = () => {
</Box> </Box>
</TabPanel> </TabPanel>
{/* Account Settings Tab */} {/* My Home Tab */}
<TabPanel value={tabValue} index={2}> <TabPanel value={tabValue} index={2}>
<Box sx={{ p: 3 }}>
<MyHomePage />
</Box>
</TabPanel>
{/* My Collection Tab */}
<TabPanel value={tabValue} index={3}>
<Box sx={{ p: 3 }}>
<MyCollectionPage />
</Box>
</TabPanel>
{/* Account Settings Tab */}
<TabPanel value={tabValue} index={4}>
<Box sx={{ p: 3 }}> <Box sx={{ p: 3 }}>
<Card> <Card>
<CardContent> <CardContent>

@ -28,7 +28,10 @@ import {
Email, Email,
QrCode, QrCode,
Group, Group,
CloudDownload CloudDownload,
VerifiedUser,
Favorite,
MailOutline
} from '@mui/icons-material'; } from '@mui/icons-material';
import { dataService } from '../services/dataService'; import { dataService } from '../services/dataService';
import type { Contact } from '../types/contact'; import type { Contact } from '../types/contact';
@ -111,14 +114,46 @@ const ContactListPage = () => {
Contacts Contacts
</Typography> </Typography>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1, flexWrap: 'wrap' }}>
<Button
variant="outlined"
startIcon={<MailOutline />}
onClick={() => navigate('/import/gmail')}
sx={{
borderRadius: 2,
color: '#EA4335',
borderColor: '#EA4335',
'&:hover': {
borderColor: '#EA4335',
backgroundColor: alpha('#EA4335', 0.04)
}
}}
>
Import Gmail
</Button>
<Button
variant="outlined"
startIcon={<LinkedIn />}
onClick={() => navigate('/import/linkedin')}
sx={{
borderRadius: 2,
color: '#0077b5',
borderColor: '#0077b5',
'&:hover': {
borderColor: '#0077b5',
backgroundColor: alpha('#0077b5', 0.04)
}
}}
>
Import LinkedIn
</Button>
<Button <Button
variant="outlined" variant="outlined"
startIcon={<CloudDownload />} startIcon={<CloudDownload />}
onClick={() => navigate('/import')} onClick={() => navigate('/import/contacts')}
sx={{ borderRadius: 2 }} sx={{ borderRadius: 2 }}
> >
Import Import Contacts
</Button> </Button>
<Button <Button
variant="outlined" variant="outlined"
@ -221,13 +256,27 @@ const ContactListPage = () => {
> >
<CardContent sx={{ p: 3 }}> <CardContent sx={{ p: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar <Box
src={contact.profileImage} sx={{
alt={contact.name} width: 64,
sx={{ width: 64, height: 64 }} height: 64,
borderRadius: '50%',
backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none',
backgroundSize: 'cover',
backgroundPosition: 'center center',
backgroundRepeat: 'no-repeat',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: contact.profileImage ? 'transparent' : 'primary.main',
color: 'white',
fontSize: '1.5rem',
fontWeight: 'bold',
flexShrink: 0
}}
> >
{contact.name.charAt(0)} {!contact.profileImage && contact.name.charAt(0)}
</Avatar> </Box>
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}> <Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
@ -254,6 +303,41 @@ const ContactListPage = () => {
}} }}
/> />
)} )}
{/* Vouch and Praise Indicators */}
<Chip
icon={<VerifiedUser sx={{ fontSize: 14 }} />}
label="2"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
height: 20,
borderRadius: 1,
backgroundColor: alpha(theme.palette.primary.main, 0.04),
borderColor: alpha(theme.palette.primary.main, 0.12),
color: 'primary.main',
'& .MuiChip-icon': {
fontSize: 14,
},
}}
/>
<Chip
icon={<Favorite sx={{ fontSize: 14 }} />}
label="3"
size="small"
variant="outlined"
sx={{
fontSize: '0.75rem',
height: 20,
borderRadius: 1,
backgroundColor: alpha('#f8bbd9', 0.3),
borderColor: alpha('#d81b60', 0.3),
color: '#d81b60',
'& .MuiChip-icon': {
fontSize: 14,
},
}}
/>
</Box> </Box>
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}> <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>

@ -13,7 +13,9 @@ import {
Card, Card,
CardContent, CardContent,
Alert, Alert,
Skeleton Skeleton,
alpha,
useTheme
} from '@mui/material'; } from '@mui/material';
import { import {
ArrowBack, ArrowBack,
@ -27,7 +29,13 @@ import {
Person, Person,
Schedule, Schedule,
Source, Source,
Group Group,
Add,
ThumbUp,
Star,
Send,
VerifiedUser,
Favorite
} from '@mui/icons-material'; } from '@mui/icons-material';
import { dataService } from '../services/dataService'; import { dataService } from '../services/dataService';
import type { Contact } from '../types/contact'; import type { Contact } from '../types/contact';
@ -40,6 +48,7 @@ const ContactViewPage = () => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme();
useEffect(() => { useEffect(() => {
const loadContact = async () => { const loadContact = async () => {
@ -118,7 +127,7 @@ const ContactViewPage = () => {
if (isLoading) { if (isLoading) {
return ( return (
<Box sx={{ height: '100%' }}> <Box sx={{ height: '100%', p: { xs: 2, md: 3 } }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}> <Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Skeleton variant="circular" width={40} height={40} /> <Skeleton variant="circular" width={40} height={40} />
<Skeleton variant="text" width={200} height={40} sx={{ ml: 2 }} /> <Skeleton variant="text" width={200} height={40} sx={{ ml: 2 }} />
@ -145,7 +154,7 @@ const ContactViewPage = () => {
if (error || !contact) { if (error || !contact) {
return ( return (
<Box sx={{ height: '100%' }}> <Box sx={{ height: '100%', p: { xs: 2, md: 3 } }}>
<Button <Button
startIcon={<ArrowBack />} startIcon={<ArrowBack />}
onClick={handleBack} onClick={handleBack}
@ -163,7 +172,7 @@ const ContactViewPage = () => {
const sourceDetails = getSourceDetails(contact.source); const sourceDetails = getSourceDetails(contact.source);
return ( return (
<Box sx={{ height: '100%' }}> <Box sx={{ height: '100%', p: { xs: 2, md: 3 } }}>
<Button <Button
startIcon={<ArrowBack />} startIcon={<ArrowBack />}
onClick={handleBack} onClick={handleBack}
@ -178,23 +187,32 @@ const ContactViewPage = () => {
alignItems: 'flex-start', alignItems: 'flex-start',
mb: 3, mb: 3,
flexDirection: { xs: 'column', sm: 'row' }, flexDirection: { xs: 'column', sm: 'row' },
textAlign: { xs: 'center', sm: 'left' } textAlign: { xs: 'center', sm: 'left' },
gap: { xs: 3, sm: '20px' }
}}> }}>
<Avatar <Box
src={contact.profileImage}
alt={contact.name}
sx={{ sx={{
width: { xs: 100, sm: 120 }, width: { xs: 100, sm: 120 },
height: { xs: 100, sm: 120 }, height: { xs: 100, sm: 120 },
mr: { xs: 0, sm: 3 }, borderRadius: '50%',
mb: { xs: 2, sm: 0 }, backgroundImage: contact.profileImage ? `url(${contact.profileImage})` : 'none',
mx: { xs: 'auto', sm: 0 } backgroundSize: 'cover',
backgroundPosition: 'center center',
backgroundRepeat: 'no-repeat',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: contact.profileImage ? 'transparent' : 'primary.main',
color: 'white',
fontSize: { xs: '2rem', sm: '3rem' },
fontWeight: 'bold',
flexShrink: 0
}} }}
> >
{contact.name.charAt(0)} {!contact.profileImage && contact.name.charAt(0)}
</Avatar> </Box>
<Box sx={{ flex: 1 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
<Typography variant="h4" component="h1" gutterBottom> <Typography variant="h4" component="h1" gutterBottom>
{contact.name} {contact.name}
</Typography> </Typography>
@ -219,24 +237,56 @@ const ContactViewPage = () => {
{contact.tags?.map((tag) => ( {contact.tags?.map((tag) => (
<Chip key={tag} label={tag} size="small" /> <Chip key={tag} label={tag} size="small" />
))} ))}
<Chip
variant="outlined"
icon={<Add fontSize="small" />}
label="Add tag"
size="small"
clickable
sx={{
borderStyle: 'dashed',
color: 'text.secondary',
borderColor: 'text.secondary',
'&:hover': {
borderColor: 'primary.main',
color: 'primary.main',
}
}}
/>
</Box> </Box>
{/* Vouch and Praise Actions */}
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
gap: 1, gap: 1,
flexWrap: 'wrap', flexWrap: 'wrap',
justifyContent: { xs: 'center', sm: 'flex-start' } justifyContent: { xs: 'center', sm: 'flex-start' },
mt: 2
}}> }}>
<Button variant="outlined" startIcon={<Edit />} size="small"> <Button
Edit variant="contained"
startIcon={<VerifiedUser />}
size="small"
color="primary"
>
Send Vouch
</Button> </Button>
<Button variant="outlined" startIcon={<Share />} size="small"> <Button
Share variant="contained"
startIcon={<Favorite />}
size="small"
sx={{
backgroundColor: '#f8bbd9',
color: '#d81b60',
'&:hover': {
backgroundColor: '#f48fb1'
}
}}
>
Send Praise
</Button> </Button>
<IconButton color="error" size="small">
<Delete />
</IconButton>
</Box> </Box>
</Box> </Box>
</Box> </Box>
@ -399,6 +449,153 @@ const ContactViewPage = () => {
</Card> </Card>
</Grid> </Grid>
</Grid> </Grid>
<Divider sx={{ my: 3 }} />
{/* Vouches and Praises Section */}
<Box sx={{ mb: 3 }}>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
Vouches & Praises
</Typography>
<Card variant="outlined">
<Grid container sx={{ minHeight: 300 }}>
{/* What I've Sent */}
<Grid size={{ xs: 12, md: 6 }} sx={{ borderRight: { md: 1 }, borderColor: { md: 'divider' } }}>
<CardContent sx={{ p: 3, height: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Send sx={{ mr: 1, color: 'success.main', fontSize: 20 }} />
<Typography variant="h6" sx={{ fontWeight: 600, color: 'success.main' }}>
Sent to {contact.name.split(' ')[0]}
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Vouch item */}
<Box sx={{ display: 'flex', gap: 2, p: 2, bgcolor: alpha(theme.palette.primary.main, 0.04), borderRadius: 2 }}>
<VerifiedUser sx={{ color: 'primary.main', fontSize: 20, mt: 0.5, flexShrink: 0 }} />
<Box sx={{ minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>React Development</Typography>
<Typography variant="caption" color="text.secondary"> 1 week ago</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
"Exceptional React skills and clean code practices."
</Typography>
</Box>
</Box>
{/* Praise items */}
<Box sx={{ display: 'flex', gap: 2, p: 2, bgcolor: alpha('#f8bbd9', 0.15), borderRadius: 2 }}>
<Favorite sx={{ color: '#d81b60', fontSize: 20, mt: 0.5, flexShrink: 0 }} />
<Box sx={{ minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Leadership</Typography>
<Typography variant="caption" color="text.secondary"> 3 days ago</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
"Great leadership during project crunch time!"
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 2, p: 2, bgcolor: alpha('#f8bbd9', 0.15), borderRadius: 2 }}>
<Favorite sx={{ color: '#d81b60', fontSize: 20, mt: 0.5, flexShrink: 0 }} />
<Box sx={{ minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Communication</Typography>
<Typography variant="caption" color="text.secondary"> 1 week ago</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
"Always clear and helpful in discussions."
</Typography>
</Box>
</Box>
</Box>
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: 'divider', textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary">
1 vouch 2 praises sent
</Typography>
</Box>
</CardContent>
</Grid>
{/* What I've Received */}
<Grid size={{ xs: 12, md: 6 }}>
<CardContent sx={{ p: 3, height: '100%' }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<Box sx={{
width: 20,
height: 20,
borderRadius: '50%',
border: 2,
borderColor: 'info.main',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
mr: 1
}}>
<Box sx={{ width: 6, height: 6, borderRadius: '50%', bgcolor: 'info.main' }} />
</Box>
<Typography variant="h6" sx={{ fontWeight: 600, color: 'info.main' }}>
Received from {contact.name.split(' ')[0]}
</Typography>
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Vouch items */}
<Box sx={{ display: 'flex', gap: 2, p: 2, bgcolor: alpha(theme.palette.primary.main, 0.04), borderRadius: 2 }}>
<VerifiedUser sx={{ color: 'primary.main', fontSize: 20, mt: 0.5, flexShrink: 0 }} />
<Box sx={{ minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>TypeScript Skills</Typography>
<Typography variant="caption" color="text.secondary"> 2 days ago</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
"Excellent TypeScript developer with attention to detail."
</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', gap: 2, p: 2, bgcolor: alpha(theme.palette.primary.main, 0.04), borderRadius: 2 }}>
<VerifiedUser sx={{ color: 'primary.main', fontSize: 20, mt: 0.5, flexShrink: 0 }} />
<Box sx={{ minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Project Management</Typography>
<Typography variant="caption" color="text.secondary"> 1 week ago</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
"Great at coordinating teams and delivering on time."
</Typography>
</Box>
</Box>
{/* Praise items */}
<Box sx={{ display: 'flex', gap: 2, p: 2, bgcolor: alpha('#f8bbd9', 0.15), borderRadius: 2 }}>
<Favorite sx={{ color: '#d81b60', fontSize: 20, mt: 0.5, flexShrink: 0 }} />
<Box sx={{ minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>Teamwork</Typography>
<Typography variant="caption" color="text.secondary"> 3 days ago</Typography>
</Box>
<Typography variant="body2" color="text.secondary">
"Amazing collaborative spirit and helpful attitude."
</Typography>
</Box>
</Box>
</Box>
<Box sx={{ mt: 3, pt: 2, borderTop: 1, borderColor: 'divider', textAlign: 'center' }}>
<Typography variant="caption" color="text.secondary">
2 vouches 1 praise received
</Typography>
</Box>
</CardContent>
</Grid>
</Grid>
</Card>
</Box>
</Paper> </Paper>
</Box> </Box>
); );

@ -91,9 +91,6 @@ const GroupPage = () => {
<Typography variant="h4" component="h1" sx={{ fontWeight: 700, mb: 1 }}> <Typography variant="h4" component="h1" sx={{ fontWeight: 700, mb: 1 }}>
Groups Groups
</Typography> </Typography>
<Typography variant="body1" color="text.secondary">
Explore and manage your {filteredGroups.length} groups
</Typography>
</Box> </Box>
<Box sx={{ display: 'flex', gap: 1 }}> <Box sx={{ display: 'flex', gap: 1 }}>
<Button <Button

@ -0,0 +1,56 @@
export interface BookmarkedItem {
id: string;
originalId: string; // ID of the original content
type: 'post' | 'article' | 'link' | 'image' | 'file' | 'offer' | 'want';
title: string;
description?: string;
content?: string;
url?: string;
imageUrl?: string;
author: {
id: string;
name: string;
avatar?: string;
};
source: string; // Where it was bookmarked from
bookmarkedAt: Date;
tags: string[];
notes?: string; // User's personal notes
category?: string; // User-defined category
isRead: boolean;
isFavorite: boolean;
lastViewedAt?: Date;
}
export interface Collection {
id: string;
name: string;
description?: string;
items: BookmarkedItem[];
isDefault: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface CollectionFilter {
type?: string;
category?: string;
author?: string;
isRead?: boolean;
isFavorite?: boolean;
dateRange?: {
start: Date;
end: Date;
};
searchQuery?: string;
tags?: string[];
}
export interface CollectionStats {
totalItems: number;
unreadItems: number;
favoriteItems: number;
byType: Record<string, number>;
byCategory: Record<string, number>;
recentlyAdded: number; // Added in last 7 days
}

@ -0,0 +1,78 @@
export interface PersonhoodVerification {
id: string;
verifierId: string;
verifierName: string;
verifierAvatar?: string;
verifiedAt: Date;
location?: {
city: string;
country: string;
coordinates?: {
lat: number;
lng: number;
};
};
verificationMethod: 'qr_scan' | 'nfc_tap' | 'biometric' | 'manual';
trustScore: number; // 0-100
isReciprocal: boolean; // If the verifier also got verified by this person
notes?: string;
expiresAt?: Date;
isActive: boolean;
}
export interface PersonhoodCredentials {
userId: string;
totalVerifications: number;
uniqueVerifiers: number;
reciprocalVerifications: number;
averageTrustScore: number;
credibilityScore: number; // Calculated score based on various factors
verificationStreak: number; // Days since last verification
lastVerificationAt?: Date;
firstVerificationAt?: Date;
verifications: PersonhoodVerification[];
certificates: PersonhoodCertificate[];
qrCode: string; // QR code data for verification
}
export interface PersonhoodCertificate {
id: string;
type: 'basic' | 'advanced' | 'premium' | 'community';
name: string;
description: string;
requiredVerifications: number;
issuedAt: Date;
expiresAt?: Date;
isActive: boolean;
badgeUrl?: string;
}
export interface PersonhoodStats {
verificationTrend: {
period: string;
count: number;
}[];
topLocations: {
location: string;
count: number;
}[];
verificationMethods: {
method: string;
count: number;
percentage: number;
}[];
trustScoreDistribution: {
range: string;
count: number;
}[];
}
export interface QRCodeSession {
id: string;
qrCode: string;
createdAt: Date;
expiresAt: Date;
isActive: boolean;
scansCount: number;
successfulVerifications: number;
}

@ -0,0 +1,104 @@
export type ContentType = 'post' | 'offer' | 'want' | 'image' | 'link' | 'file' | 'article';
export interface BaseContent {
id: string;
type: ContentType;
title: string;
description?: string;
createdAt: Date;
updatedAt: Date;
tags?: string[];
visibility: 'public' | 'network' | 'private';
viewCount: number;
likeCount: number;
commentCount: number;
rCardIds: string[]; // Which rCards can see this content
}
export interface Post extends BaseContent {
type: 'post';
content: string;
attachments?: string[];
}
export interface Offer extends BaseContent {
type: 'offer';
content: string;
category: string;
price?: string;
availability: 'available' | 'pending' | 'completed';
location?: string;
}
export interface Want extends BaseContent {
type: 'want';
content: string;
category: string;
budget?: string;
urgency: 'low' | 'medium' | 'high';
location?: string;
}
export interface Image extends BaseContent {
type: 'image';
imageUrl: string;
imageAlt: string;
caption?: string;
dimensions?: {
width: number;
height: number;
};
}
export interface Link extends BaseContent {
type: 'link';
url: string;
linkTitle: string;
linkDescription?: string;
linkImage?: string;
domain: string;
}
export interface File extends BaseContent {
type: 'file';
fileUrl: string;
fileName: string;
fileSize: number;
fileType: string;
downloadCount: number;
}
export interface Article extends BaseContent {
type: 'article';
content: string;
excerpt: string;
readTime: number; // in minutes
publishedAt?: Date;
featuredImage?: string;
}
export type UserContent = Post | Offer | Want | Image | Link | File | Article;
export interface ContentFilter {
type?: ContentType;
visibility?: 'public' | 'network' | 'private';
dateRange?: {
start: Date;
end: Date;
};
tags?: string[];
searchQuery?: string;
}
export interface ContentStats {
totalItems: number;
byType: Record<ContentType, number>;
byVisibility: {
public: number;
network: number;
private: number;
};
totalViews: number;
totalLikes: number;
totalComments: number;
}
Loading…
Cancel
Save