diff --git a/src/components/account/MyCollectionPage.tsx b/src/components/account/MyCollectionPage.tsx new file mode 100644 index 0000000..0db9647 --- /dev/null +++ b/src/components/account/MyCollectionPage.tsx @@ -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([]); + const [collections, setCollections] = useState([]); + const [filteredItems, setFilteredItems] = useState([]); + const [filter] = useState({}); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCollection, setSelectedCollection] = useState('all'); + const [selectedCategory, setSelectedCategory] = useState('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({ + 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) => { + 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 ; + case 'offer': return ; + case 'want': return ; + case 'image': return ; + case 'link': return ; + case 'file': return ; + case 'article': return
; + default: return ; + } + }; + + 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) => ( + + + {/* Header */} + + + + {getContentIcon(item.type)} + + + + {item.title} + + + + {item.category && ( + + )} + {!item.isRead && ( + + )} + + {formatDate(item.bookmarkedAt)} + + + + + + handleToggleFavorite(item.id)} + color={item.isFavorite ? 'error' : 'default'} + > + {item.isFavorite ? : } + + handleMenuOpen(item.id, e)} + > + + + + + + {/* Author */} + + + {item.author.name.charAt(0)} + + + by {item.author.name} • {item.source} + + + + {/* Content */} + {item.description && ( + + {item.description} + + )} + + {/* Image for image type */} + {item.type === 'image' && item.imageUrl && ( + + )} + + {/* User Notes */} + {item.notes && ( + + + "{item.notes}" + + + )} + + {/* Tags */} + {item.tags && item.tags.length > 0 && ( + + {item.tags.map((tag) => ( + + ))} + + )} + + + + {/* Actions */} + + + {!item.isRead && ( + + )} + + + + Saved {formatDate(item.bookmarkedAt)} + + + + + {/* Menu */} + handleMenuClose(item.id)} + > + handleMenuClose(item.id)}> + Edit Notes + + handleMenuClose(item.id)}> + Move to Collection + + handleMenuClose(item.id)}> + Open Original + + handleMenuClose(item.id)}> + Remove + + + + ); + + return ( + + {/* Header */} + + + + My Collection + + + + + + {/* Search and Filters */} + + setSearchQuery(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + }} + sx={{ mb: 2 }} + /> + + {/* Filters */} + + + + Collection + + + + + + Category + + + + + + Sort By + + + + + + + {/* Content List */} + + {filteredItems.length === 0 ? ( + + + + No bookmarks found + + + {searchQuery + ? `No bookmarks match "${searchQuery}"` + : "You haven't bookmarked any content yet" + } + + + + ) : ( + filteredItems.map(renderBookmarkedItem) + )} + + + {/* Query Dialog */} + setShowQueryDialog(false)} maxWidth="md" fullWidth> + + + + AI Query Assistant + + + + {/* Chat-like interface */} + + {/* Messages area */} + + {/* AI intro message */} + + + + + + + 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. + + + + + + {/* Input area */} + + + 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(); + } + } + }} + /> + + + + Press Enter to send, Shift+Enter for new line + + + + + + + + + + ); +}; + +export default MyCollectionPage; \ No newline at end of file diff --git a/src/components/account/MyHomePage.tsx b/src/components/account/MyHomePage.tsx new file mode 100644 index 0000000..1d5a41d --- /dev/null +++ b/src/components/account/MyHomePage.tsx @@ -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([]); + const [filteredContent, setFilteredContent] = useState([]); + const [filter] = useState({}); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedTab, setSelectedTab] = useState<'all' | ContentType>('all'); + const [menuAnchor, setMenuAnchor] = useState<{ [key: string]: HTMLElement | null }>({}); + const [filterMenuAnchor, setFilterMenuAnchor] = useState(null); + const [stats, setStats] = useState({ + 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) => { + setMenuAnchor({ ...menuAnchor, [contentId]: event.currentTarget }); + }; + + const handleMenuClose = (contentId: string) => { + setMenuAnchor({ ...menuAnchor, [contentId]: null }); + }; + + const handleFilterMenuOpen = (event: React.MouseEvent) => { + setFilterMenuAnchor(event.currentTarget); + }; + + const handleFilterMenuClose = () => { + setFilterMenuAnchor(null); + }; + + const handleFilterSelect = (filterType: 'all' | ContentType) => { + setSelectedTab(filterType); + handleFilterMenuClose(); + }; + + const getContentIcon = (type: ContentType) => { + switch (type) { + case 'post': return ; + case 'offer': return ; + case 'want': return ; + case 'image': return ; + case 'link': return ; + case 'file': return ; + case 'article': return
; + default: return ; + } + }; + + const getVisibilityIcon = (visibility: string) => { + switch (visibility) { + case 'public': return ; + case 'network': return ; + case 'private': return ; + default: return ; + } + }; + + 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) => ( + + + {/* Header */} + + + + {getContentIcon(item.type)} + + + + {item.title} + + + + + + {formatDate(item.createdAt)} + + + + + handleMenuOpen(item.id, e)} + > + + + + + {/* Content based on type */} + {(item.type === 'post' || item.type === 'article') && ( + + {'content' in item ? item.content.substring(0, 200) + (item.content.length > 200 ? '...' : '') : ''} + + )} + + {item.type === 'offer' && 'price' in item && ( + + + {item.content} + + + + + )} + + {item.type === 'want' && 'budget' in item && ( + + + {item.content} + + + + + )} + + {item.type === 'link' && 'url' in item && ( + + + {item.linkTitle} + + + {item.linkDescription} + + + {item.domain} + + + )} + + {item.type === 'image' && 'imageUrl' in item && ( + + + + {item.caption} + + + )} + + {item.type === 'file' && 'fileName' in item && ( + + + + {item.fileName} + + {formatFileSize(item.fileSize)} • {item.downloadCount} downloads + + + + + )} + + {item.type === 'article' && 'readTime' in item && ( + + {'featuredImage' in item && item.featuredImage && ( + + )} + + {item.excerpt} + + + {item.readTime} min read + + + )} + + {/* Tags */} + {item.tags && item.tags.length > 0 && ( + + {item.tags.map((tag) => ( + + ))} + + )} + + + + {/* Stats */} + + + + + {item.commentCount} + + + + + + + {/* Menu */} + handleMenuClose(item.id)} + > + handleMenuClose(item.id)}> + Edit + + handleMenuClose(item.id)}> + View Details + + handleMenuClose(item.id)}> + Delete + + + + ); + + return ( + + {/* Header */} + + + My Content + + + + {/* Search and Filters */} + + setSearchQuery(e.target.value)} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: ( + + + + + + ), + }} + sx={{ mb: 2 }} + /> + + {/* Filter Menu */} + + handleFilterSelect('all')}> + + All + + + + handleFilterSelect('post')}> + + + + Posts + + + + + handleFilterSelect('offer')}> + + + + Offers + + + + + handleFilterSelect('want')}> + + + + Wants + + + + + handleFilterSelect('image')}> + + + + Images + + + + + handleFilterSelect('link')}> + + + + Links + + + + + handleFilterSelect('file')}> + + + + Files + + + + + handleFilterSelect('article')}> + + +
+ Articles + + + + +
+
+ + {/* Content List */} + + {filteredContent.length === 0 ? ( + + + + No content found + + + {searchQuery + ? `No content matches "${searchQuery}"` + : selectedTab === 'all' + ? "You haven't shared any content yet" + : `No ${selectedTab} content found` + } + + + + ) : ( + filteredContent.map(renderContentItem) + )} + +
+ ); +}; + +export default MyHomePage; \ No newline at end of file diff --git a/src/components/account/PersonhoodCredentials.tsx b/src/components/account/PersonhoodCredentials.tsx new file mode 100644 index 0000000..371ec01 --- /dev/null +++ b/src/components/account/PersonhoodCredentials.tsx @@ -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 ( + + + + + + + Personhood Credentials + + + Verified human authenticity through real-world connections + + + + + + + + + + + + + + + + {/* Total Verifications Card */} + + + + + {credentials.totalVerifications} + + + Total Verifications + + + + + + {/* Recent Verifications */} + + + + + Recent Verifications + + + + + {credentials.verifications.slice(0, 3).map((verification) => ( + + + {verification.verifierName.charAt(0)} + + + + {verification.verifierName} + + + {verification.location && ( + } + label={`${verification.location.city}, ${verification.location.country}`} + size="small" + variant="outlined" + /> + )} + + {formatRelativeTime(verification.verifiedAt)} + + + + + = 80 ? 'success' : verification.trustScore >= 60 ? 'warning' : 'error'} + /> + {verification.isReciprocal && ( + + + + )} + + + ))} + + {credentials.verifications.length === 0 && ( + + + No verifications yet. Share your QR code with trusted contacts to start building your personhood credentials. + + + )} + + + + {/* Certificates */} + {credentials.certificates.length > 0 && ( + + + + Earned Certificates + + + {credentials.certificates.map((certificate) => ( + + + + + + + + {certificate.name} + + + Issued {formatDate(certificate.issuedAt)} + + + + + ))} + + + + )} + + {/* QR Code Dialog */} + setShowQRDialog(false)} maxWidth="sm" fullWidth> + + + My Verification QR Code + setShowQRDialog(false)} size="small"> + + + + + + + + + + + Scan to Verify My Personhood + + + Ask trusted contacts to scan this QR code to verify that you are a real human being. Each verification strengthens your credibility score. + + } + label="QR code expires in 24 hours" + size="small" + variant="outlined" + /> + + + + + + + + + {/* Verification History Dialog */} + setShowHistoryDialog(false)} maxWidth="md" fullWidth> + + + Verification History + setShowHistoryDialog(false)} size="small"> + + + + + + + {credentials.verifications.map((verification, index) => ( + + + + + {verification.verifierName.charAt(0)} + + + + + {formatDate(verification.verifiedAt)} • Trust Score: {verification.trustScore}% + + {verification.location && ( + + {verification.location.city}, {verification.location.country} + + )} + {verification.notes && ( + + "{verification.notes}" + + )} + + } + /> + + + {verification.isReciprocal && ( + + + + )} + + + {index < credentials.verifications.length - 1 && } + + ))} + + + + + + + + ); +}; + +export default PersonhoodCredentials; \ No newline at end of file diff --git a/src/components/account/RCardPrivacySettings.tsx b/src/components/account/RCardPrivacySettings.tsx index 7a9e212..04b1cff 100644 --- a/src/components/account/RCardPrivacySettings.tsx +++ b/src/components/account/RCardPrivacySettings.tsx @@ -164,12 +164,6 @@ const RCardPrivacySettings = ({ rCard, onUpdate }: RCardPrivacySettingsProps) => Privacy Settings for {rCard.name} - diff --git a/src/components/layout/DashboardLayout.tsx b/src/components/layout/DashboardLayout.tsx index ee28e39..19cc8c1 100644 --- a/src/components/layout/DashboardLayout.tsx +++ b/src/components/layout/DashboardLayout.tsx @@ -327,31 +327,6 @@ const DashboardLayout = ({ children }: DashboardLayoutProps) => { {navItems.map((item) => renderNavItem(item))} - - - - - handleNavigation('/settings')} - sx={{ - mx: 1, - borderRadius: 2, - minHeight: 48, - }} - > - - - - - - - ); diff --git a/src/components/notifications/NotificationDropdown.tsx b/src/components/notifications/NotificationDropdown.tsx index afa849c..b0af35d 100644 --- a/src/components/notifications/NotificationDropdown.tsx +++ b/src/components/notifications/NotificationDropdown.tsx @@ -100,6 +100,7 @@ const NotificationDropdown = ({ borderRadius: 2, border: 1, borderColor: 'divider', + overflow: 'hidden', '&::before': { content: '""', display: 'block', @@ -140,7 +141,7 @@ const NotificationDropdown = ({ {/* Summary Stats */} - + {/* Filter Chips */} - + {/* Notification List */} - + {filteredNotifications.length === 0 ? ( diff --git a/src/components/notifications/NotificationItem.tsx b/src/components/notifications/NotificationItem.tsx index 5c321b6..759256b 100644 --- a/src/components/notifications/NotificationItem.tsx +++ b/src/components/notifications/NotificationItem.tsx @@ -183,7 +183,7 @@ const NotificationItem = ({ }, }} > - + {/* Avatar and Icon */} - - + + {notification.title} - + {getStatusChip()} @@ -234,25 +234,27 @@ const NotificationItem = ({ WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden', + wordBreak: 'break-word', + overflowWrap: 'break-word', }} > {notification.message} - + {formatTimeAgo(notification.createdAt)} {/* Action Buttons */} {notification.isActionable && notification.status === 'pending' && ( - + @@ -261,7 +263,7 @@ const NotificationItem = ({ variant="contained" startIcon={} onClick={handleAccept} - sx={{ textTransform: 'none', minWidth: 'auto' }} + sx={{ textTransform: 'none', minWidth: 'auto', fontSize: '0.75rem' }} > Accept @@ -274,7 +276,7 @@ const NotificationItem = ({ variant="outlined" startIcon={} onClick={handleAssignClick} - sx={{ textTransform: 'none' }} + sx={{ textTransform: 'none', fontSize: '0.75rem', flexShrink: 0 }} > Assign to rCard diff --git a/src/pages/AccountPage.tsx b/src/pages/AccountPage.tsx index dddac62..a841397 100644 --- a/src/pages/AccountPage.tsx +++ b/src/pages/AccountPage.tsx @@ -28,11 +28,17 @@ import { Add, Edit, Settings, + Dashboard, + BookmarkBorder, } 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'; +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 { children?: React.ReactNode; @@ -55,6 +61,78 @@ const AccountPage = () => { const [selectedRCard, setSelectedRCard] = useState(null); const [showRCardManagement, setShowRCardManagement] = useState(false); const [editingRCard, setEditingRCard] = useState(null); + const [personhoodCredentials, setPersonhoodCredentials] = useState({ + 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(() => { // 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) => { switch (iconName) { case 'Business': @@ -182,7 +270,9 @@ const AccountPage = () => { }} > } label="Profile" /> - } label="Privacy & rCards" /> + } label="My Cards" /> + } label="My Home" /> + } label="My Collection" /> } label="Account Settings" /> @@ -251,10 +341,19 @@ const AccountPage = () => { + + {/* Personhood Credentials Section */} + + + - {/* Privacy & rCards Tab */} + {/* My Cards Tab */} @@ -322,9 +421,6 @@ const AccountPage = () => { - {rCard.isDefault && ( - - )} { @@ -365,8 +461,22 @@ const AccountPage = () => { - {/* Account Settings Tab */} + {/* My Home Tab */} + + + + + + {/* My Collection Tab */} + + + + + + + {/* Account Settings Tab */} + diff --git a/src/pages/ContactListPage.tsx b/src/pages/ContactListPage.tsx index 08357ba..c9a3db9 100644 --- a/src/pages/ContactListPage.tsx +++ b/src/pages/ContactListPage.tsx @@ -28,7 +28,10 @@ import { Email, QrCode, Group, - CloudDownload + CloudDownload, + VerifiedUser, + Favorite, + MailOutline } from '@mui/icons-material'; import { dataService } from '../services/dataService'; import type { Contact } from '../types/contact'; @@ -111,14 +114,46 @@ const ContactListPage = () => { Contacts - + + + - - - - + @@ -399,6 +449,153 @@ const ContactViewPage = () => { + + + + {/* Vouches and Praises Section */} + + + Vouches & Praises + + + + + {/* What I've Sent */} + + + + + + Sent to {contact.name.split(' ')[0]} + + + + + {/* Vouch item */} + + + + + React Development + • 1 week ago + + + "Exceptional React skills and clean code practices." + + + + + {/* Praise items */} + + + + + Leadership + • 3 days ago + + + "Great leadership during project crunch time!" + + + + + + + + + Communication + • 1 week ago + + + "Always clear and helpful in discussions." + + + + + + + + 1 vouch • 2 praises sent + + + + + + {/* What I've Received */} + + + + + + + + Received from {contact.name.split(' ')[0]} + + + + + {/* Vouch items */} + + + + + TypeScript Skills + • 2 days ago + + + "Excellent TypeScript developer with attention to detail." + + + + + + + + + Project Management + • 1 week ago + + + "Great at coordinating teams and delivering on time." + + + + + {/* Praise items */} + + + + + Teamwork + • 3 days ago + + + "Amazing collaborative spirit and helpful attitude." + + + + + + + + 2 vouches • 1 praise received + + + + + + + ); diff --git a/src/pages/GroupPage.tsx b/src/pages/GroupPage.tsx index 02b8e31..78b24ec 100644 --- a/src/pages/GroupPage.tsx +++ b/src/pages/GroupPage.tsx @@ -91,9 +91,6 @@ const GroupPage = () => { Groups - - Explore and manage your {filteredGroups.length} groups -