- 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
parent
d6e3e54fb6
commit
451d2a2908
@ -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; |
@ -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…
Reference in new issue