mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-13 18:17:03 +01:00
redesign community page
This commit is contained in:
1 parent
0e9ea23d98
commit
0bed331c4e
2 files changed
+354
-165
No files matched your search
@@ -0,0 +1,43 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTheme, alpha } from '@mui/material';
|
||||
|
||||
interface RichTextProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
const RichText: React.FC<RichTextProps> = ({ text }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
if (!text) return null;
|
||||
|
||||
const parts = text.split(/(@\w+)/g);
|
||||
return (
|
||||
<span>
|
||||
{parts.map((part, i) => {
|
||||
if (part.startsWith('@')) {
|
||||
const username = part.substring(1);
|
||||
return (
|
||||
<Link
|
||||
key={i}
|
||||
to={`/profile/${username}`}
|
||||
style={{
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'none',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: alpha(theme.palette.primary.main, 0.1),
|
||||
padding: '0 4px',
|
||||
borderRadius: '4px'
|
||||
}}
|
||||
>
|
||||
{part}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return part;
|
||||
})}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichText;
|
||||
@@ -19,12 +19,27 @@ import {
|
||||
Pagination,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
alpha
|
||||
alpha,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Collapse
|
||||
} from '@mui/material';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import CommentIcon from '@mui/icons-material/Comment';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import WhatshotIcon from '@mui/icons-material/Whatshot';
|
||||
import VerifiedIcon from '@mui/icons-material/Verified';
|
||||
import { Link } from 'react-router-dom';
|
||||
import api from '../api';
|
||||
import { useTitle } from '../hooks/useTitle';
|
||||
import RichText from '../components/RichText';
|
||||
|
||||
interface Reply {
|
||||
id: number;
|
||||
user: string;
|
||||
text: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface FeedRating {
|
||||
id: number;
|
||||
@@ -39,6 +54,7 @@ interface FeedRating {
|
||||
score: number;
|
||||
comment: string;
|
||||
created_at: string;
|
||||
replies: Reply[];
|
||||
}
|
||||
|
||||
interface FollowedUser {
|
||||
@@ -55,6 +71,13 @@ const CommunityFeed: React.FC = () => {
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [followingSearch, setFollowingSearch] = useState('');
|
||||
|
||||
// Trending Flavors State (for sidebar)
|
||||
const [trending, setTrending] = useState<any[]>([]);
|
||||
|
||||
// Reply State
|
||||
const [replyInputs, setReplyInputs] = useState<{[key: number]: string}>({});
|
||||
const [expandedReplies, setExpandedReplies] = useState<{[key: number]: boolean}>({});
|
||||
|
||||
const filteredFollowing = following.filter(user =>
|
||||
user.username.toLowerCase().includes(followingSearch.toLowerCase())
|
||||
@@ -63,19 +86,23 @@ const CommunityFeed: React.FC = () => {
|
||||
const fetchFeedData = async (pageNum: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const [feedRes, followingRes] = await Promise.all([
|
||||
const [feedRes, followingRes, trendingRes] = await Promise.all([
|
||||
api.get(`ratings/feed/?page=${pageNum}`),
|
||||
api.get('users/following_list/')
|
||||
api.get('users/following_list/'),
|
||||
api.get('flavors/top/') // Using top as trending for now
|
||||
]);
|
||||
|
||||
const feedData = feedRes.data.results || [];
|
||||
const feedData = feedRes.data.results || (Array.isArray(feedRes.data) ? feedRes.data : []);
|
||||
setRatings(feedData);
|
||||
|
||||
// Calculate total pages (DRF returns total count)
|
||||
const count = feedRes.data.count || 0;
|
||||
setTotalPages(Math.ceil(count / 10)); // 10 is our page_size
|
||||
setTotalPages(Math.ceil(count / 10));
|
||||
|
||||
setFollowing(followingRes.data);
|
||||
const followData = Array.isArray(followingRes.data) ? followingRes.data : (followingRes.data.results || []);
|
||||
setFollowing(followData);
|
||||
|
||||
setTrending(Array.isArray(trendingRes.data) ? trendingRes.data.slice(0, 5) : []);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch feed data', err);
|
||||
} finally {
|
||||
@@ -92,195 +119,314 @@ const CommunityFeed: React.FC = () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
const handleReplyToggle = (ratingId: number) => {
|
||||
setExpandedReplies(prev => ({ ...prev, [ratingId]: !prev[ratingId] }));
|
||||
};
|
||||
|
||||
const handleReplySubmit = async (ratingId: number) => {
|
||||
const text = replyInputs[ratingId];
|
||||
if (!text) return;
|
||||
try {
|
||||
const res = await api.post(`ratings/${ratingId}/reply/`, { text });
|
||||
// Optimistically update the UI
|
||||
setRatings(prevRatings => prevRatings.map(r => {
|
||||
if (r.id === ratingId) {
|
||||
return { ...r, replies: [...r.replies, res.data] };
|
||||
}
|
||||
return r;
|
||||
}));
|
||||
setReplyInputs(prev => ({ ...prev, [ratingId]: '' }));
|
||||
} catch (err) {
|
||||
alert('Failed to send reply');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading && ratings.length === 0) return <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" sx={{ py: 4 }}>
|
||||
<Box sx={{ mb: 6 }}>
|
||||
<Typography variant="h3" sx={{ mb: 1, fontWeight: 'bold' }}>Community Activity</Typography>
|
||||
<Typography variant="h6" color="text.secondary" sx={{ maxWidth: 700, lineHeight: 1.6 }}>
|
||||
Discover which Holy Energy flavors your circle is currently testing.
|
||||
See their latest ratings, read their reviews, and find your next favorite together.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={4}>
|
||||
{/* Main Feed */}
|
||||
{/* Main Feed Column */}
|
||||
<Grid size={{ xs: 12, md: 8 }}>
|
||||
<Box sx={{ mb: 4 }}>
|
||||
<Typography variant="h4" sx={{ fontWeight: '800', mb: 1 }}>Community Feed</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary">
|
||||
See what your friends are drinking and rating.
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{ratings.length === 0 ? (
|
||||
<Box sx={{ p: 4, textAlign: 'center', borderRadius: 4, bgcolor: 'background.paper', border: '1px solid', borderColor: 'divider' }}>
|
||||
<Typography variant="h6" gutterBottom>Your feed is quiet...</Typography>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Follow other Holy Energy fans to see their latest flavor ratings and reviews here!
|
||||
<Box sx={{
|
||||
p: 6,
|
||||
textAlign: 'center',
|
||||
borderRadius: 4,
|
||||
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.6),
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: '1px dashed',
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<Typography variant="h6" gutterBottom color="text.secondary">It's quiet in here...</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
Follow more users to populate your feed with flavor reviews!
|
||||
</Typography>
|
||||
<Button variant="contained" component={Link} to="/" sx={{ borderRadius: 2 }}>
|
||||
Find flavors and reviewers
|
||||
<Button variant="outlined" component={Link} to="/" sx={{ borderRadius: 2 }}>
|
||||
Discover Users
|
||||
</Button>
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
{loading ? (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}><CircularProgress /></Box>
|
||||
) : (
|
||||
<>
|
||||
{ratings.map(rating => (
|
||||
<Card key={rating.id} sx={{ borderRadius: 3, transition: 'transform 0.2s', '&:hover': { transform: 'translateY(-2px)' } }}>
|
||||
<CardContent sx={{ p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none', color: 'inherit', display: 'flex', alignItems: 'center' }}>
|
||||
<Avatar src={rating.user_avatar || undefined} sx={{ width: 40, height: 40, mr: 2 }}>
|
||||
{!rating.user_avatar && rating.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{ratings.map(rating => (
|
||||
<Card key={rating.id} elevation={0} sx={{
|
||||
borderRadius: 3,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.8),
|
||||
backdropFilter: 'blur(12px)',
|
||||
transition: 'transform 0.2s, box-shadow 0.2s',
|
||||
'&:hover': {
|
||||
boxShadow: (theme) => `0 4px 20px ${alpha(theme.palette.primary.main, 0.08)}`,
|
||||
borderColor: (theme) => alpha(theme.palette.primary.main, 0.3)
|
||||
}
|
||||
}}>
|
||||
<CardContent sx={{ p: 2.5, '&:last-child': { pb: 2.5 } }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none' }}>
|
||||
<Avatar src={rating.user_avatar || undefined} sx={{ width: 44, height: 44, border: '2px solid', borderColor: 'divider' }}>
|
||||
{!rating.user_avatar && rating.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</Link>
|
||||
<Box>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', '&:hover': { color: 'primary.main' } }}>
|
||||
{rating.user}
|
||||
</Typography>
|
||||
<Link to={`/profile/${rating.user}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '1rem', lineHeight: 1.2 }}>
|
||||
{rating.user}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
rated <Link to={`/flavor/${rating.flavor}`} style={{ color: 'inherit', fontWeight: 'bold', textDecoration: 'none' }}>{rating.flavor_name}</Link>
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
|
||||
{new Date(rating.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
{/* Rating Badge */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1),
|
||||
color: 'primary.main',
|
||||
px: 1.5, py: 0.5,
|
||||
borderRadius: 2,
|
||||
minWidth: 50
|
||||
}}>
|
||||
<Typography variant="h6" sx={{ fontWeight: '900', lineHeight: 1 }}>{rating.score}</Typography>
|
||||
<Typography variant="caption" sx={{ fontSize: '0.65rem', fontWeight: 'bold', opacity: 0.8 }}>/ 10</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 3, flexDirection: { xs: 'column', sm: 'row' } }}>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none' }}>
|
||||
<Box sx={{ width: { xs: '100%', sm: 100 }, aspectRatio: '1/1', borderRadius: 2, overflow: 'hidden', border: '1px solid', borderColor: 'divider' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src={rating.flavor_image || undefined}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
/>
|
||||
</Box>
|
||||
</Link>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Typography variant="h6" sx={{ fontSize: '1.1rem', mb: 0.5, '&:hover': { color: 'primary.main' } }}>
|
||||
{rating.flavor_name}
|
||||
</Typography>
|
||||
</Link>
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1.5 }}>
|
||||
{rating.category_name}
|
||||
{/* Content Body */}
|
||||
<Box sx={{ display: 'flex', gap: 2, mb: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
{rating.comment ? (
|
||||
<Typography variant="body1" sx={{ lineHeight: 1.6, fontSize: '0.95rem', mb: 1 }}>
|
||||
<RichText text={rating.comment} />
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1.5 }}>
|
||||
{/* Compact rating for mobile, stars for desktop */}
|
||||
<Box sx={{ display: { xs: 'flex', sm: 'none' }, alignItems: 'center', gap: 0.5 }}>
|
||||
<Typography variant="body2" sx={{ fontWeight: 'bold' }}>⭐ {rating.score}/10</Typography>
|
||||
</Box>
|
||||
<Box sx={{ display: { xs: 'none', sm: 'flex' }, alignItems: 'center' }}>
|
||||
<MuiRating value={rating.score} readOnly max={10} size="small" />
|
||||
<Typography variant="body2" sx={{ ml: 1, fontWeight: 'bold' }}>{rating.score}/10</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic', mb: 1 }}>
|
||||
No review text provided.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
{/* Flavor Thumbnail */}
|
||||
<Link to={`/flavor/${rating.flavor}`} style={{ flexShrink: 0 }}>
|
||||
<Box sx={{
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
bgcolor: 'action.hover',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<Box
|
||||
component="img"
|
||||
src={rating.flavor_image || undefined}
|
||||
alt={rating.flavor_name}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'contain', p: 0.5 }}
|
||||
/>
|
||||
</Box>
|
||||
</Link>
|
||||
</Box>
|
||||
|
||||
{rating.comment && (
|
||||
<Typography variant="body2" sx={{
|
||||
fontStyle: 'italic',
|
||||
color: 'text.secondary',
|
||||
borderLeft: '3px solid',
|
||||
borderColor: 'divider',
|
||||
pl: 2,
|
||||
py: 0.5
|
||||
}}>
|
||||
"{rating.comment}"
|
||||
<Divider sx={{ my: 1.5, borderStyle: 'dashed' }} />
|
||||
|
||||
{/* Actions / Footer */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
size="small"
|
||||
startIcon={<CommentIcon fontSize="small" />}
|
||||
onClick={() => handleReplyToggle(rating.id)}
|
||||
sx={{ textTransform: 'none', color: 'text.secondary', '&:hover': { color: 'primary.main', bgcolor: 'transparent' } }}
|
||||
>
|
||||
{rating.replies.length > 0 ? `${rating.replies.length} Replies` : 'Reply'}
|
||||
</Button>
|
||||
|
||||
{/* Small timestamp or generic action */}
|
||||
<Box />
|
||||
</Box>
|
||||
|
||||
{/* Replies Section (Collapsible) */}
|
||||
<Collapse in={expandedReplies[rating.id]}>
|
||||
<Box sx={{ mt: 2, pl: 2, borderLeft: '2px solid', borderColor: 'divider' }}>
|
||||
{/* Existing Replies */}
|
||||
{rating.replies.map((reply) => (
|
||||
<Box key={reply.id} sx={{ mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 0.5 }}>
|
||||
<Avatar src={undefined} sx={{ width: 20, height: 20, fontSize: '0.7rem' }}>
|
||||
{reply.user.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.85rem' }}>
|
||||
{reply.user}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{new Date(reply.created_at).toLocaleDateString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ pl: 3.5, fontSize: '0.9rem' }}>
|
||||
<RichText text={reply.text} />
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* Reply Input */}
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<TextField
|
||||
placeholder="Write a reply..."
|
||||
size="small"
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
value={replyInputs[rating.id] || ''}
|
||||
onChange={(e) => setReplyInputs(prev => ({ ...prev, [rating.id]: e.target.value }))}
|
||||
sx={{ '& .MuiOutlinedInput-root': { borderRadius: 4, bgcolor: 'background.paper' } }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleReplySubmit(rating.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
color="primary"
|
||||
disabled={!replyInputs[rating.id]}
|
||||
onClick={() => handleReplySubmit(rating.id)}
|
||||
sx={{ bgcolor: 'primary.main', color: 'white', '&:hover': { bgcolor: 'primary.dark' }, width: 40, height: 40 }}
|
||||
>
|
||||
<SendIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={page}
|
||||
onChange={handlePageChange}
|
||||
color="primary"
|
||||
size="large"
|
||||
sx={{
|
||||
'& .MuiPaginationItem-root': { borderRadius: 2, fontWeight: 'bold' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Collapse>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{totalPages > 1 && (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 4 }}>
|
||||
<Pagination
|
||||
count={totalPages}
|
||||
page={page}
|
||||
onChange={handlePageChange}
|
||||
color="primary"
|
||||
shape="rounded"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Grid>
|
||||
|
||||
{/* Sidebar - Desktop Only */}
|
||||
<Grid size={{ xs: 12, md: 4 }} sx={{ display: { xs: 'none', md: 'block' } }}>
|
||||
<Card sx={{ borderRadius: 3, position: { md: 'sticky' }, top: { md: 100 } }}>
|
||||
<CardContent sx={{ p: 0 }}>
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.4),
|
||||
backdropFilter: 'blur(8px)',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider'
|
||||
}}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold', mb: 1 }}>Following ({following.length})</Typography>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Search friends..."
|
||||
value={followingSearch}
|
||||
onChange={(e) => setFollowingSearch(e.target.value)}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon sx={{ fontSize: '1.1rem' }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 1,
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'& fieldset': { border: 'none' },
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.05)'
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ position: 'sticky', top: 100, display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
|
||||
{/* Following Widget */}
|
||||
<Card elevation={0} sx={{ borderRadius: 3, border: '1px solid', borderColor: 'divider', overflow: 'hidden' }}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider', bgcolor: 'action.hover' }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>Following</Typography>
|
||||
</Box>
|
||||
{following.length === 0 ? (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">You aren't following anyone yet.</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<List sx={{ py: 0, maxHeight: '60vh', overflowY: 'auto' }}>
|
||||
{filteredFollowing.map((user) => (
|
||||
<React.Fragment key={user.id}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton component={Link} to={`/profile/${user.username}`}>
|
||||
<ListItemAvatar>
|
||||
<Avatar src={user.avatar || undefined} sx={{ width: 32, height: 32 }}>
|
||||
{!user.avatar && user.username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={user.username}
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 'bold' }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
<Divider component="li" />
|
||||
</React.Fragment>
|
||||
))}
|
||||
{filteredFollowing.length === 0 && followingSearch && (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">No matching friends found.</Typography>
|
||||
<Box sx={{ maxHeight: 300, overflowY: 'auto' }}>
|
||||
{following.length === 0 ? (
|
||||
<Box sx={{ p: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">You aren't following anyone yet.</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<List disablePadding>
|
||||
{filteredFollowing.map((user) => (
|
||||
<ListItemButton key={user.id} component={Link} to={`/profile/${user.username}`}>
|
||||
<ListItemAvatar>
|
||||
<Avatar src={user.avatar || undefined} sx={{ width: 32, height: 32 }}>
|
||||
{!user.avatar && user.username.charAt(0).toUpperCase()}
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={user.username}
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 'bold' }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Box>
|
||||
<Box sx={{ p: 1.5, borderTop: '1px solid', borderColor: 'divider' }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Filter..."
|
||||
value={followingSearch}
|
||||
onChange={(e) => setFollowingSearch(e.target.value)}
|
||||
InputProps={{
|
||||
startAdornment: <InputAdornment position="start"><SearchIcon fontSize="small" /></InputAdornment>,
|
||||
sx: { borderRadius: 2, fontSize: '0.85rem' }
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Card>
|
||||
|
||||
{/* Trending / Top Rated Widget */}
|
||||
<Card elevation={0} sx={{ borderRadius: 3, border: '1px solid', borderColor: 'divider' }}>
|
||||
<Box sx={{ p: 2, borderBottom: '1px solid', borderColor: 'divider', bgcolor: 'action.hover', display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<WhatshotIcon color="error" fontSize="small" />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>Top Rated Flavors</Typography>
|
||||
</Box>
|
||||
<List disablePadding>
|
||||
{trending.map((flavor, index) => (
|
||||
<ListItemButton key={flavor.id} component={Link} to={`/flavor/${flavor.id}`} sx={{ py: 1.5 }}>
|
||||
<Box sx={{ mr: 2, fontWeight: 'bold', color: 'text.secondary', width: 20, textAlign: 'center' }}>
|
||||
{index + 1}
|
||||
</Box>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Avatar
|
||||
src={flavor.image_url}
|
||||
variant="rounded"
|
||||
sx={{ width: 32, height: 32, mr: 2, bgcolor: 'transparent' }}
|
||||
imgProps={{ sx: { objectFit: 'contain' } }}
|
||||
>
|
||||
F
|
||||
</Avatar>
|
||||
<ListItemText
|
||||
primary={flavor.name}
|
||||
secondary={`${flavor.average_rating ? flavor.average_rating.toFixed(1) : '-'} / 10`}
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 'bold', noWrap: true }}
|
||||
secondaryTypographyProps={{ variant: 'caption', color: 'primary.main', fontWeight: 'bold' }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</List>
|
||||
</Card>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
|
||||
Reference in new issue
Block a user