redesigned dashboard

This commit is contained in:
nvrl committed 2026-02-17 20:10:56 +01:00
1 parent ff8f5fc182
commit f3dc60ced8
4 files changed
+375 -234

No files matched your search

+2 -1
View File
@@ -108,13 +108,14 @@ class FlavorSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True)
category_slug = serializers.CharField(source='category.slug', read_only=True)
average_rating = serializers.FloatField(read_only=True)
followed_average_rating = serializers.FloatField(read_only=True)
user_rating = serializers.SerializerMethodField()
ratings = RatingSerializer(many=True, read_only=True)
image_url = serializers.SerializerMethodField()
class Meta:
model = Flavor
fields = ['id', 'name', 'category', 'category_name', 'category_slug', 'description', 'average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'is_legacy', 'shop_url']
fields = ['id', 'name', 'category', 'category_name', 'category_slug', 'description', 'average_rating', 'followed_average_rating', 'user_rating', 'ratings', 'image_url', 'is_available', 'is_legacy', 'shop_url']
def get_image_url(self, obj):
if obj.image:
+9 -3
View File
@@ -508,13 +508,19 @@ class UserViewSet(viewsets.ModelViewSet):
def dashboard(self, request):
user = request.user
rated_ids = user.ratings.values_list('flavor_id', flat=True)
followed_users = user.following.all()
# Annotate with community average rating so it's available for sorting/display
# Missing flavors with community avg and followed circle avg
missing_flavors = Flavor.objects.exclude(id__in=rated_ids) \
.select_related('category') \
.annotate(average_rating=Avg('ratings__score'))
.annotate(
average_rating=Avg('ratings__score'),
followed_average_rating=Avg('ratings__score', filter=models.Q(ratings__user__in=followed_users))
).order_by('category__name', 'name')
rated_flavors = user.ratings.select_related('flavor', 'flavor__category')
rated_flavors = user.ratings.select_related('flavor', 'flavor__category') \
.prefetch_related('replies', 'replies__user') \
.order_by('-created_at')
return Response({
'user': UserSerializer(user).data,
+14 -2
View File
@@ -32,7 +32,13 @@ const resources = {
"missing": "Missing",
"noRatings": "You haven't rated any flavors yet.",
"exploreFlavors": "Go explore flavors",
"allRated": "Amazing! You've rated everything! 🏆"
"allRated": "Amazing! You've rated everything! 🏆",
"myReviews": "My Reviews",
"exploreNew": "Explore New",
"editingReview": "Editing review for {{flavor}}",
"newScore": "New Score:",
"searchMissing": "Search flavors you haven't rated...",
"circleAvg": "Average rating from people you follow"
},
"settings": {
"subtitle": "Manage your account and preferences",
@@ -163,7 +169,13 @@ const resources = {
"missing": "Fehlend",
"noRatings": "Du hast noch keine Sorten bewertet.",
"exploreFlavors": "Sorten entdecken",
"allRated": "Wahnsinn! Du hast alles bewertet! 🏆"
"allRated": "Wahnsinn! Du hast alles bewertet! 🏆",
"myReviews": "Meine Rezensionen",
"exploreNew": "Neu entdecken",
"editingReview": "Rezension bearbeiten für {{flavor}}",
"newScore": "Neue Bewertung:",
"searchMissing": "Suche nach Sorten, die du noch nicht bewertet hast...",
"circleAvg": "Durchschnittsbewertung der Personen, denen du folgst"
},
"settings": {
"subtitle": "Verwalte dein Konto und deine Einstellungen",
+350 -228
View File
@@ -1,25 +1,39 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useMemo } from 'react';
import {
Typography,
Card,
CardContent,
Box,
Button,
TextField,
Container,
CircularProgress,
Avatar,
Tabs,
Tab,
IconButton,
Grid,
alpha,
useTheme,
Divider,
Stack,
Chip,
Collapse,
TextField,
InputAdornment,
Tooltip
} from '@mui/material';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { Link, useNavigate } from 'react-router-dom';
import ShareIcon from '@mui/icons-material/Share';
import CommentIcon from '@mui/icons-material/Comment';
import VerifiedIcon from '@mui/icons-material/Verified';
import SearchIcon from '@mui/icons-material/Search';
import { useNavigate, Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import api from '../api';
import { useTitle } from '../hooks/useTitle';
import RatingBadge from '../components/RatingBadge';
import MentionTextField from '../components/MentionTextField';
import RichText from '../components/RichText';
import { formatDate } from '../utils/date';
interface DashboardData {
user: { username: string, avatar: string | null };
@@ -32,258 +46,366 @@ interface DashboardData {
const Dashboard: React.FC = () => {
const { t } = useTranslation();
const navigate = useNavigate();
const theme = useTheme();
useTitle(t('nav.dashboard'));
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState(0);
// Filtering states
const [exploreCategory, setExploreCategory] = useState<string>('All');
const [exploreSearch, setExploreSearch] = useState('');
// Inline Review Edit state
const [editingRatingId, setEditingRatingId] = useState<number | null>(null);
const [editScore, setEditScore] = useState(0);
const [editComment, setEditComment] = useState('');
// Inline Reply state
const [replyInputs, setReplyInputs] = useState<{[key: number]: string}>({});
const [expandedReplies, setExpandedReplies] = useState<{[key: number]: boolean}>({});
const shareUrl = data ? `${window.location.origin}/profile/${data.user.username}` : '';
const copyToClipboard = () => {
const handleCopyLink = () => {
navigator.clipboard.writeText(shareUrl);
alert(t('dashboard.copySuccess'));
};
const handleGoBack = () => {
if (window.history.length > 1) {
navigate(-1);
} else {
navigate('/');
}
if (window.history.length > 1) navigate(-1);
else navigate('/');
};
const fetchData = async () => {
try {
const res = await api.get('users/dashboard/');
setData(res.data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
useEffect(() => {
const fetchData = async () => {
try {
const res = await api.get('users/dashboard/');
setData(res.data);
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
fetchData();
}, []);
// Update Review Logic
const handleUpdateRating = async (ratingId: number) => {
try {
await api.patch(`ratings/${ratingId}/`, { score: editScore, comment: editComment });
setEditingRatingId(null);
fetchData();
} catch (err) { alert('Failed to update review'); }
};
const handleDeleteRating = async (ratingId: number) => {
if (!confirm('Delete this review?')) return;
try {
await api.delete(`ratings/${ratingId}/`);
fetchData();
} catch (err) { alert('Failed to delete review'); }
};
// Reply Logic
const handleReplySubmit = async (ratingId: number) => {
const text = replyInputs[ratingId];
if (!text) return;
try {
await api.post(`ratings/${ratingId}/reply/`, { text });
setReplyInputs({ ...replyInputs, [ratingId]: '' });
setExpandedReplies({ ...expandedReplies, [ratingId]: true });
fetchData();
} catch (err) { alert('Failed to send reply'); }
};
const handleDeleteReply = async (replyId: number) => {
if (!confirm('Delete this reply?')) return;
try {
await api.delete(`replies/${replyId}/`);
fetchData();
} catch (err) { alert('Failed to delete reply'); }
};
// Performance Optimization: Memoize grouped missing flavors
const categories = useMemo(() => {
if (!data) return ['All'];
const cats = new Set(data.missing_flavors.map(f => f.category_name));
return ['All', ...Array.from(cats).sort()];
}, [data]);
const filteredMissing = useMemo(() => {
if (!data) return [];
return data.missing_flavors.filter(f => {
const matchCat = exploreCategory === 'All' || f.category_name === exploreCategory;
const matchSearch = f.name.toLowerCase().includes(exploreSearch.toLowerCase());
return matchCat && matchSearch;
});
}, [data, exploreCategory, exploreSearch]);
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
if (!data) return <Typography>Please login to view dashboard.</Typography>;
const groupAndSort = (array: any[], sortKey: string) => {
const grouped = array.reduce((result, currentValue) => {
const groupKey = currentValue.category_name || currentValue.flavor?.category_name || 'Other';
(result[groupKey] = result[groupKey] || []).push(currentValue);
return result;
}, {});
// Sort items within each group
Object.keys(grouped).forEach(key => {
grouped[key].sort((a: any, b: any) => (b[sortKey] || 0) - (a[sortKey] || 0));
});
return grouped;
};
const ratedGrouped = groupAndSort(data.my_ratings, 'score');
const missingGrouped = groupAndSort(data.missing_flavors, 'average_rating');
return (
<Container maxWidth={false} sx={{ px: { xs: 2, sm: 4, md: 6 }, py: 4 }}>
<Button
variant="outlined"
onClick={handleGoBack}
startIcon={<ArrowBackIcon />}
sx={{
mb: 4,
borderRadius: 2,
textTransform: 'none',
fontWeight: 'bold',
color: 'text.secondary',
borderColor: 'divider',
'&:hover': {
borderColor: 'primary.main',
color: 'primary.main',
bgcolor: 'transparent'
}
}}
>
{window.history.length > 1 ? t('common.back') : t('common.backToHome')}
</Button>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 6, flexWrap: 'wrap', gap: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar src={data.user.avatar || undefined} sx={{ width: 64, height: 64, border: '3px solid', borderColor: 'primary.main' }}>
{!data.user.avatar && data.user.username.charAt(0).toUpperCase()}
</Avatar>
<Box>
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>{t('nav.dashboard')}</Typography>
<Typography variant="body2" color="text.secondary">{t('dashboard.welcome', { username: data.user.username })}</Typography>
</Box>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, bgcolor: 'background.paper', p: 1, borderRadius: 2, border: '1px solid', borderColor: 'divider' }}>
<Typography variant="caption" sx={{ ml: 1, mr: 2, display: { xs: 'none', md: 'block' } }}>{t('dashboard.shareProfile')}</Typography>
<TextField
size="small"
variant="standard"
value={shareUrl}
slotProps={{ input: { readOnly: true, disableUnderline: true } }}
sx={{ width: { xs: 150, sm: 250 }, px: 1 }}
/>
<IconButton size="small" onClick={copyToClipboard} color="primary">
<ContentCopyIcon fontSize="small" />
</IconButton>
</Box>
<Container maxWidth="lg" sx={{ py: 4 }}>
{/* Action Bar */}
<Box sx={{ mb: 4, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button
variant="outlined"
onClick={handleGoBack}
startIcon={<ArrowBackIcon />}
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 'bold', color: 'text.secondary', borderColor: 'divider' }}
>
{window.history.length > 1 ? t('common.back') : t('common.backToHome')}
</Button>
<Button
variant="contained"
onClick={handleCopyLink}
startIcon={<ShareIcon />}
sx={{ borderRadius: 2, textTransform: 'none', fontWeight: 'bold' }}
>
{t('dashboard.shareProfile')}
</Button>
</Box>
{/* Header Profile Info */}
<Card elevation={0} sx={{
borderRadius: 4,
mb: 4,
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.6),
backdropFilter: 'blur(12px)',
border: '1px solid',
borderColor: 'divider'
}}>
<CardContent sx={{ p: 4 }}>
<Grid container spacing={3} alignItems="center">
<Grid size={{ xs: 12, sm: 'auto' }}>
<Avatar src={data.user.avatar || undefined} sx={{ width: 100, height: 100, border: '4px solid', borderColor: 'primary.main' }}>
{!data.user.avatar && data.user.username.charAt(0).toUpperCase()}
</Avatar>
</Grid>
<Grid size={{ xs: 12, sm: 8, md: 9 }}>
<Typography variant="h3" sx={{ fontWeight: '900', mb: 0.5 }}>{data.user.username}</Typography>
<Typography variant="subtitle1" color="text.secondary" gutterBottom>
{t('dashboard.welcome', { username: data.user.username })}
</Typography>
<Stack direction="row" spacing={3} sx={{ mt: 2 }}>
<Box>
<Typography variant="h5" sx={{ fontWeight: '900', color: 'primary.main' }}>{data.rated_count}</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 }}>{t('dashboard.myRatings')}</Typography>
</Box>
<Divider orientation="vertical" flexItem />
<Box>
<Typography variant="h5" sx={{ fontWeight: '900' }}>{data.missing_count}</Typography>
<Typography variant="caption" sx={{ fontWeight: 'bold', textTransform: 'uppercase', letterSpacing: 1 }}>{t('dashboard.missing')}</Typography>
</Box>
</Stack>
</Grid>
</Grid>
</CardContent>
</Card>
{/* Tabs Section */}
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 4 }}>
<Tabs value={activeTab} onChange={(_, v) => setActiveTab(v)} textColor="primary" indicatorColor="primary">
<Tab label={`${t('dashboard.myRatings')} (${data.rated_count})`} sx={{ fontWeight: 'bold' }} />
<Tab label={`${t('dashboard.missing')} (${data.missing_count})`} sx={{ fontWeight: 'bold' }} />
<Tab label={t('dashboard.myReviews')} sx={{ fontWeight: '900', textTransform: 'none', fontSize: '1.1rem' }} />
<Tab label={t('dashboard.exploreNew')} sx={{ fontWeight: '900', textTransform: 'none', fontSize: '1.1rem' }} />
</Tabs>
</Box>
{activeTab === 0 ? (
<Box>
{Object.keys(ratedGrouped).length === 0 ? (
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography color="text.secondary">{t('dashboard.noRatings')}</Typography>
<Button component={Link} to="/" sx={{ mt: 2 }}>{t('dashboard.exploreFlavors')}</Button>
</Box>
) : (
Object.entries(ratedGrouped).map(([category, items]: [string, any]) => (
<Box key={category} sx={{ mb: 6 }}>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 'bold' }}>{category}</Typography>
<Box sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(2, 1fr)',
sm: 'repeat(3, 1fr)',
md: 'repeat(4, 1fr)',
lg: 'repeat(6, 1fr)',
xl: 'repeat(8, 1fr)'
},
gap: 2
}}>
{items.map((rating: any) => (
<Tooltip key={rating.id} title={`${rating.flavor_name} - ${rating.score}/10`} arrow>
<Card sx={{
position: 'relative',
transition: 'all 0.3s ease',
'&:hover': { transform: 'translateY(-4px)', boxShadow: 4 }
}}>
<Link to={`/flavor/${rating.flavor}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<Box sx={{ position: 'relative', aspectRatio: '1/1', overflow: 'hidden' }}>
<Box
component="img"
src={rating.flavor_image || undefined}
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
loading="lazy"
/>
<Box sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bgcolor: 'rgba(0,0,0,0.6)',
color: 'white',
p: 0.5,
textAlign: 'center',
backdropFilter: 'blur(4px)'
}}>
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>{rating.score}/10</Typography>
</Box>
</Box>
<CardContent sx={{ p: 1, '&:last-child': { pb: 1 } }}>
<Typography variant="caption" sx={{
fontWeight: 'bold',
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
fontSize: '0.75rem'
}}>
{rating.flavor_name}
</Typography>
</CardContent>
</Link>
</Card>
</Tooltip>
))}
</Box>
</Box>
))
)}
</Box>
) : (
<Box>
{Object.keys(missingGrouped).length === 0 ? (
<Box sx={{ textAlign: 'center', py: 8 }}>
<Typography variant="h5" color="primary">{t('dashboard.allRated')}</Typography>
</Box>
) : (
Object.entries(missingGrouped).map(([category, flavors]: [string, any]) => (
<Box key={category} sx={{ mb: 6 }}>
<Typography variant="h5" sx={{ mb: 3, fontWeight: 'bold' }}>{category}</Typography>
<Box sx={{
display: 'grid',
gridTemplateColumns: {
xs: 'repeat(2, 1fr)',
sm: 'repeat(3, 1fr)',
md: 'repeat(4, 1fr)',
lg: 'repeat(6, 1fr)',
xl: 'repeat(8, 1fr)'
},
gap: 2
}}>
{flavors.map((flavor: any) => (
<Tooltip key={flavor.id} title={`${flavor.name} - Community Avg: ${(flavor.average_rating || 0).toFixed(1)}/10`} arrow>
<Card key={flavor.id} sx={{
transition: 'all 0.3s ease',
'&:hover': { transform: 'translateY(-4px)', boxShadow: 4 }
}}>
<Link to={`/flavor/${flavor.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<Box sx={{ position: 'relative', aspectRatio: '1/1', overflow: 'hidden', borderBottom: '1px solid', borderColor: 'divider' }}>
<Box
component="img"
src={flavor.image_url || undefined}
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
loading="lazy"
/>
<Box sx={{
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
bgcolor: 'rgba(0,0,0,0.5)',
color: 'white',
p: 0.5,
textAlign: 'center',
backdropFilter: 'blur(2px)'
}}>
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>{(flavor.average_rating || 0).toFixed(1)}/10</Typography>
</Box>
</Box>
<CardContent sx={{ p: 1, '&:last-child': { pb: 1 } }}>
<Typography variant="caption" sx={{
fontWeight: 'bold',
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
fontSize: '0.75rem'
}}>
{flavor.name}
</Typography>
</CardContent>
</Link>
</Card>
</Tooltip>
))}
</Box>
</Box>
))
)}
</Box>
{/* TAB 0: MY REVIEWS */}
{activeTab === 0 && (
<Stack spacing={3}>
{data.my_ratings.length === 0 ? (
<Box sx={{ py: 10, textAlign: 'center', bgcolor: 'action.hover', borderRadius: 4, border: '1px dashed', borderColor: 'divider' }}>
<Typography variant="h6" color="text.secondary">{t('dashboard.noRatings')}</Typography>
<Button onClick={() => setActiveTab(1)} sx={{ mt: 2 }}>{t('dashboard.exploreFlavors')}</Button>
</Box>
) : (
data.my_ratings.map(rating => (
<Card key={rating.id} elevation={0} sx={{
borderRadius: 4,
border: '1px solid',
borderColor: 'divider',
bgcolor: (theme) => alpha(theme.palette.background.paper, 0.8),
transition: 'border-color 0.2s',
'&:hover': { borderColor: 'primary.main' }
}}>
<CardContent sx={{ p: 3 }}>
{editingRatingId === rating.id ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold' }}>{t('dashboard.editingReview', { flavor: rating.flavor_name })}</Typography>
<Stack direction="row" spacing={2} alignItems="center">
<Typography variant="body2">{t('dashboard.newScore')}</Typography>
<TextField
type="number" size="small"
slotProps={{ input: { inputProps: { min: 1, max: 10 } } }}
value={editScore} onChange={(e) => setEditScore(Number(e.target.value))}
sx={{ width: 80 }}
/>
</Stack>
<MentionTextField
multiline rows={3} value={editComment}
onChange={setEditComment}
/>
<Stack direction="row" spacing={1}>
<Button variant="contained" size="small" onClick={() => handleUpdateRating(rating.id)}>{t('common.save')}</Button>
<Button variant="outlined" size="small" onClick={() => setEditingRatingId(null)}>{t('common.cancel')}</Button>
</Stack>
</Box>
) : (
<>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', mb: 2 }}>
<Box sx={{ display: 'flex', gap: 2 }}>
<Avatar src={rating.flavor_image || undefined} variant="rounded" sx={{ width: 60, height: 60, bgcolor: 'action.hover', border: '1px solid', borderColor: 'divider', p: 0.5 }}>
F
</Avatar>
<Box>
<Typography variant="h6" sx={{ fontWeight: 'bold', lineHeight: 1.2 }}>
<Link to={`/flavor/${rating.flavor}`} style={{ color: 'inherit', textDecoration: 'none' }}>{rating.flavor_name}</Link>
</Typography>
<Typography variant="caption" color="text.secondary" display="block">
{t(`categories.${rating.category_slug}`, { defaultValue: rating.category_name })} • {formatDate(rating.created_at)}
</Typography>
</Box>
</Box>
<RatingBadge score={rating.score} size="large" />
</Box>
<Box sx={{ mb: 2, p: 2, bgcolor: alpha('#000', 0.02), borderRadius: 2 }}>
{rating.comment ? (
<RichText text={rating.comment} />
) : (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>{t('dashboard.noRatings')}</Typography>
)}
</Box>
<Stack direction="row" spacing={2} alignItems="center">
<Button
size="small"
startIcon={<CommentIcon fontSize="small" />}
onClick={() => setExpandedReplies(prev => ({ ...prev, [rating.id]: !prev[rating.id] }))}
sx={{ textTransform: 'none', fontWeight: 'bold', color: 'text.secondary' }}
>
{rating.replies.length} {t('common.replies')}
</Button>
<Box sx={{ flexGrow: 1 }} />
<Button size="small" sx={{ fontWeight: 'bold' }} onClick={() => { setEditingRatingId(rating.id); setEditScore(rating.score); setEditComment(rating.comment || ''); }}>{t('common.edit')}</Button>
<Button size="small" color="error" sx={{ fontWeight: 'bold' }} onClick={() => handleDeleteRating(rating.id)}>{t('common.delete')}</Button>
</Stack>
{/* Replies Inline */}
<Collapse in={expandedReplies[rating.id]}>
<Box sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
{rating.replies.map((reply: any) => (
<Box key={reply.id} sx={{ mb: 2, pl: 2, borderLeft: '3px solid', borderColor: 'primary.main' }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.85rem' }}>
{reply.user}
{reply.user === data.user.username && <VerifiedIcon sx={{ fontSize: '0.8rem', color: 'primary.main', ml: 0.5 }} />}
</Typography>
<Stack direction="row" spacing={1} alignItems="center">
<Typography variant="caption" color="text.secondary">{formatDate(reply.created_at)}</Typography>
{reply.user === data.user.username && (
<Button size="small" color="error" sx={{ minWidth: 0, p: 0, fontSize: '0.7rem' }} onClick={() => handleDeleteReply(reply.id)}>{t('common.delete')}</Button>
)}
</Stack>
</Box>
<Typography variant="body2"><RichText text={reply.text} /></Typography>
</Box>
))}
<Stack direction="row" spacing={1} sx={{ mt: 2 }}>
<MentionTextField
placeholder={t('community.writeReply')}
value={replyInputs[rating.id] || ''}
onChange={val => setReplyInputs({ ...replyInputs, [rating.id]: val })}
onKeyDown={e => e.key === 'Enter' && !e.shiftKey && handleReplySubmit(rating.id)}
/>
<Button variant="contained" size="small" onClick={() => handleReplySubmit(rating.id)} disabled={!replyInputs[rating.id]} sx={{ height: 40, px: 3 }}>{t('common.reply')}</Button>
</Stack>
</Box>
</Collapse>
</>
)}
</CardContent>
</Card>
))
)}
</Stack>
)}
{/* TAB 1: EXPLORE NEW */}
{activeTab === 1 && (
<Box>
{/* Filter Controls */}
<Stack direction={{ xs: 'column', md: 'row' }} spacing={2} sx={{ mb: 4 }}>
<Box sx={{ flexGrow: 1 }}>
<TextField
fullWidth placeholder={t('dashboard.searchMissing')}
value={exploreSearch} onChange={e => setExploreSearch(e.target.value)}
slotProps={{ input: { startAdornment: <InputAdornment position="start"><SearchIcon /></InputAdornment> } }}
sx={{ bgcolor: 'background.paper', borderRadius: 2 }}
/>
</Box>
<Stack direction="row" spacing={1} sx={{ overflowX: 'auto', pb: 1, '&::-webkit-scrollbar': { display: 'none' } }}>
{categories.map(cat => (
<Chip
key={cat} label={cat === 'All' ? t('nav.home').replace('Home', 'All') : t(`categories.${data.missing_flavors.find(f => f.category_name === cat)?.category_slug}`, { defaultValue: cat })}
onClick={() => setExploreCategory(cat)}
color={exploreCategory === cat ? 'primary' : 'default'}
variant={exploreCategory === cat ? 'filled' : 'outlined'}
sx={{ fontWeight: 'bold', height: 40, px: 1 }}
/>
))}
</Stack>
</Stack>
{filteredMissing.length === 0 ? (
<Box sx={{ py: 10, textAlign: 'center' }}>
<Typography variant="h5" color="primary" sx={{ fontWeight: '900' }}>{t('dashboard.allRated')}</Typography>
</Box>
) : (
<Grid container spacing={2}>
{filteredMissing.map(flavor => (
<Grid key={flavor.id} size={{ xs: 12, sm: 6, md: 4 }}>
<Card elevation={0} sx={{
height: '100%', borderRadius: 3, border: '1px solid', borderColor: 'divider',
transition: 'all 0.2s ease', '&:hover': { transform: 'translateY(-4px)', borderColor: 'primary.main', boxShadow: 4 }
}}>
<Link to={`/flavor/${flavor.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
<Box sx={{ position: 'relative', aspectRatio: '16/10', bgcolor: 'action.hover', display: 'flex', alignItems: 'center', justifyContent: 'center', p: 2 }}>
<Box component="img" src={flavor.image_url || undefined} sx={{ height: '100%', maxWidth: '100%', objectFit: 'contain', filter: 'drop-shadow(0 4px 8px rgba(0,0,0,0.1))' }} />
<Box sx={{ position: 'absolute', top: 10, left: 10 }}>
<RatingBadge score={flavor.average_rating || 0} size="small" />
</Box>
{flavor.followed_average_rating && (
<Box sx={{ position: 'absolute', top: 10, right: 10 }}>
<Tooltip title={t('dashboard.circleAvg')}>
<Box sx={{ bgcolor: alpha(theme.palette.secondary.main, 0.9), color: 'white', px: 1, py: 0.2, borderRadius: 1, display: 'flex', alignItems: 'center', gap: 0.5 }}>
<VerifiedIcon sx={{ fontSize: '0.8rem' }} />
<Typography variant="caption" sx={{ fontWeight: '900' }}>{flavor.followed_average_rating.toFixed(1)}</Typography>
</Box>
</Tooltip>
</Box>
)}
</Box>
<CardContent sx={{ p: 2 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', mb: 0.5, display: '-webkit-box', WebkitLineClamp: 1, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>
{flavor.name}
</Typography>
<Typography variant="caption" color="text.secondary" display="block">{t(`categories.${flavor.category_slug}`, { defaultValue: flavor.category_name })}</Typography>
</CardContent>
</Link>
</Card>
</Grid>
))}
</Grid>
)}
</Box>
)}
</Container>
);