Merge branch 'main' into narl.io

This commit is contained in:
nvrl committed 2026-02-17 19:54:31 +01:00
commit e9132cd074
11 files changed
+161 -100

No files matched your search

+17 -13
View File
@@ -317,11 +317,13 @@ const App: React.FC = () => {
</ListItemButton>
</ListItem>
<ListItem disablePadding>
<ListItemButton component={Link} to="/community" onClick={() => setDrawerOpen(false)}>
<ListItemText primary={t('nav.community')} />
</ListItemButton>
</ListItem>
{user && (
<ListItem disablePadding>
<ListItemButton component={Link} to="/community" onClick={() => setDrawerOpen(false)}>
<ListItemText primary={t('nav.community')} />
</ListItemButton>
</ListItem>
)}
<ListItemButton onClick={() => setCatOpen(!catOpen)}>
<ListItemText primary={t('nav.categories')} />
@@ -444,14 +446,16 @@ const App: React.FC = () => {
</Typography>
<Box sx={{ display: { xs: 'none', md: 'flex' }, ml: 4 }}>
<Button
color="inherit"
component={Link}
to="/community"
sx={{ fontWeight: 'bold', textTransform: 'none', fontSize: '1rem', mr: 2 }}
>
{t('nav.community')}
</Button>
{user && (
<Button
color="inherit"
component={Link}
to="/community"
sx={{ fontWeight: 'bold', textTransform: 'none', fontSize: '1rem', mr: 2 }}
>
{t('nav.community')}
</Button>
)}
<Button
color="inherit"
endIcon={<ArrowDropDownIcon />}
+52
View File
@@ -0,0 +1,52 @@
import React from 'react';
import { Box, Typography, alpha, useTheme } from '@mui/material';
interface RatingBadgeProps {
score: number;
size?: 'small' | 'medium' | 'large';
sx?: any;
}
const RatingBadge: React.FC<RatingBadgeProps> = ({ score, size = 'medium', sx }) => {
const theme = useTheme();
const isLarge = size === 'large';
const isSmall = size === 'small';
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
bgcolor: alpha(theme.palette.primary.main, 0.1),
color: 'primary.main',
px: isLarge ? 2.5 : (isSmall ? 1 : 1.5),
py: isLarge ? 0.8 : (isSmall ? 0.2 : 0.5),
borderRadius: isLarge ? 2.5 : (isSmall ? 1 : 2),
minWidth: isLarge ? 70 : (isSmall ? 40 : 50),
border: '1px solid',
borderColor: alpha(theme.palette.primary.main, 0.2),
lineHeight: 1,
...sx
}}>
<Typography
variant={isLarge ? "h5" : (isSmall ? "caption" : "subtitle1")}
sx={{ fontWeight: '900', lineHeight: 1 }}
>
{typeof score === 'number' ? score.toFixed(isLarge ? 1 : 0) : score}
</Typography>
<Typography
variant="caption"
sx={{
fontSize: isLarge ? '0.7rem' : (isSmall ? '0.5rem' : '0.6rem'),
fontWeight: 'bold',
opacity: 0.8,
mt: 0.2
}}
>
/ 10
</Typography>
</Box>
);
};
export default RatingBadge;
+4 -2
View File
@@ -88,9 +88,10 @@ const resources = {
},
"common": {
"search": "Search...",
"back": "Back",
"backTo": "Back to {{page}}",
"backToArchive": "Back to Archive",
"backToHome": "Back to Home",
"backTo": "Back to",
"buyNow": "Buy Now",
"reviews": "reviews",
"rank": "Rank",
@@ -218,9 +219,10 @@ const resources = {
},
"common": {
"search": "Suchen...",
"back": "Zurück",
"backTo": "Zurück zu {{page}}",
"backToArchive": "Zurück zum Archiv",
"backToHome": "Zurück zur Startseite",
"backTo": "Zurück zu",
"buyNow": "Jetzt kaufen",
"reviews": "Bewertungen",
"rank": "Rang",
+34 -14
View File
@@ -27,6 +27,7 @@ import SendIcon from '@mui/icons-material/Send';
import WhatshotIcon from '@mui/icons-material/Whatshot';
import NotificationsIcon from '@mui/icons-material/Notifications';
import PeopleIcon from '@mui/icons-material/People';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { Link, useNavigate } from 'react-router-dom';
import api from '../api';
import { useTitle } from '../hooks/useTitle';
@@ -34,6 +35,7 @@ import RichText from '../components/RichText';
import { formatDate } from '../utils/date';
import { useTranslation } from 'react-i18next';
import MentionTextField from '../components/MentionTextField';
import RatingBadge from '../components/RatingBadge';
interface Reply {
id: number;
@@ -100,6 +102,14 @@ const CommunityFeed: React.FC = () => {
user.username.toLowerCase().includes(followingSearch.toLowerCase())
);
const handleGoBack = () => {
if (window.history.length > 1) {
navigate(-1);
} else {
navigate('/');
}
};
const fetchFeedData = async (pageNum: number) => {
setLoading(true);
try {
@@ -177,6 +187,28 @@ const CommunityFeed: React.FC = () => {
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<Box sx={{ mb: 4 }}>
<Button
variant="outlined"
onClick={handleGoBack}
startIcon={<ArrowBackIcon />}
sx={{
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>
<Box sx={{ mb: 6 }}>
<Typography variant="h3" sx={{ fontWeight: '800', mb: 1 }}>{t('community.title')}</Typography>
<Typography variant="h6" color="text.secondary">
@@ -239,18 +271,7 @@ const CommunityFeed: React.FC = () => {
</Typography>
</Box>
</Box>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1),
color: 'primary.main',
px: 2, py: 0.5,
borderRadius: 2
}}>
<Typography variant="h5" sx={{ fontWeight: '900', lineHeight: 1 }}>{rating.score}</Typography>
<Typography variant="caption" sx={{ fontSize: '0.6rem', fontWeight: 'bold' }}>/ 10</Typography>
</Box>
<RatingBadge score={rating.score} />
</Box>
{/* Flavor and Comment */}
@@ -397,10 +418,9 @@ const CommunityFeed: React.FC = () => {
<Avatar src={flavor.image_url} variant="rounded" sx={{ width: 32, height: 32, mr: 2, bgcolor: 'transparent' }} imgProps={{ sx: { objectFit: 'contain' } }} />
<ListItemText
primary={flavor.name}
secondary={`${(flavor.average_rating || 0).toFixed(1)} / 10`}
primaryTypographyProps={{ variant: 'body2', fontWeight: 'bold', noWrap: true }}
secondaryTypographyProps={{ variant: 'caption', color: 'primary.main', fontWeight: 'bold' }}
/>
<RatingBadge score={flavor.average_rating || 0} size="small" />
</ListItemButton>
))}
</List>
+1 -1
View File
@@ -107,7 +107,7 @@ const Dashboard: React.FC = () => {
}
}}
>
{t('common.backToHome').replace('Home', 'Back')}
{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 }}>
+5 -5
View File
@@ -4,15 +4,16 @@ import {
Card,
CardContent,
Box,
Rating as MuiRating,
TextField,
Button,
Accordion,
AccordionSummary,
AccordionDetails
AccordionDetails,
Rating as MuiRating
} from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import api from '../api';
import RatingBadge from '../components/RatingBadge';
interface Rating {
id: number;
@@ -143,8 +144,7 @@ const FlavorList: React.FC = () => {
)}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<Typography variant="body2" sx={{ mr: 1 }}>Avg:</Typography>
<MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} max={10} size="small" />
<Typography variant="body2" sx={{ ml: 1 }}>({(flavor.average_rating || 0).toFixed(1)})</Typography>
<RatingBadge score={flavor.average_rating || 0} size="small" />
</Box>
{flavor.ratings && flavor.ratings.length > 0 && (
@@ -175,7 +175,7 @@ const FlavorList: React.FC = () => {
<MuiRating
max={10}
value={ratingInput[flavor.id]?.score || 0}
onChange={(_, val) => setRatingInput({...ratingInput, [flavor.id]: {...(ratingInput[flavor.id] || {comment: ''}), score: val || 0}})}
onChange={(_: any, val: number | null) => setRatingInput({...ratingInput, [flavor.id]: {...(ratingInput[flavor.id] || {comment: ''}), score: val || 0}})}
/>
<TextField
label="Comment (Optional)"
+9 -19
View File
@@ -7,22 +7,21 @@ import {
CircularProgress,
Button,
Avatar,
Rating as MuiRating,
Paper,
Chip,
Container,
IconButton,
useMediaQuery,
alpha
} from '@mui/material';
import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import StarIcon from '@mui/icons-material/Star';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import EmojiEventsIcon from '@mui/icons-material/EmojiEvents';
import api from '../api';
import { useTitle } from '../hooks/useTitle';
import { formatDate } from '../utils/date';
import RatingBadge from '../components/RatingBadge';
interface Flavor {
id: number;
@@ -50,7 +49,6 @@ interface Review {
const MainPage: React.FC = () => {
const { t } = useTranslation();
const isMobileSize = useMediaQuery('(max-width:600px)');
const [topFlavors, setTopFlavors] = useState<Flavor[]>([]);
const [newestFlavors, setNewestFlavors] = useState<Flavor[]>([]);
const [recentReviews, setRecentReviews] = useState<Review[]>([]);
@@ -202,8 +200,7 @@ const MainPage: React.FC = () => {
<Typography variant="h6" sx={{ fontSize: '1rem' }}>{flavor.name}</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mb: 1 }}>{flavor.category_name}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} max={10} size="small" />
<Typography variant="caption" sx={{ ml: 1 }}>({(flavor.average_rating || 0).toFixed(1)})</Typography>
<RatingBadge score={flavor.average_rating || 0} size="small" />
</Box>
</CardContent>
</Box>
@@ -362,7 +359,7 @@ const MainPage: React.FC = () => {
<Box sx={{ flex: { xs: '1 1 100%', xl: '1 1 65%' }, minWidth: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 4, justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<StarIcon color="primary" sx={{ mr: 1, fontSize: '2rem' }} />
<EmojiEventsIcon color="primary" sx={{ mr: 1, fontSize: '2rem' }} />
<Typography variant="h4" sx={{ fontWeight: 'bold' }}>{t('home.hallOfFame')}</Typography>
</Box>
<Box sx={{ display: 'flex', gap: 1 }}>
@@ -409,10 +406,10 @@ const MainPage: React.FC = () => {
{currentTop.name}
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', mb: { xs: 2, md: 3 }, flexWrap: 'wrap' }}>
<MuiRating value={currentTop.average_rating || 0} readOnly precision={0.5} max={10} size={isMobileSize ? "medium" : "large"} />
<Typography variant="h5" sx={{ ml: { xs: 0, sm: 2 }, mt: { xs: 1, sm: 0 }, fontWeight: 'bold', fontSize: { xs: '1.25rem', md: '1.5rem' } }}>
{(currentTop.average_rating || 0).toFixed(1)} / 10
<Box sx={{ display: 'flex', alignItems: 'center', mb: { xs: 2, md: 3 }, flexWrap: 'wrap', gap: 2 }}>
<RatingBadge score={currentTop.average_rating || 0} size="large" />
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 'bold' }}>
{currentTop.ratings.length} {t('common.reviews')}
</Typography>
</Box>
@@ -497,14 +494,7 @@ const MainPage: React.FC = () => {
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.8rem', '&:hover': { color: 'primary.main' } }}>{review.user}</Typography>
</Link>
<Box sx={{ flexGrow: 1 }} />
{/* Compact rating for sidebar and tablets */}
<Box sx={{ display: { xs: 'flex', xl: 'none' }, alignItems: 'center', gap: 0.5 }}>
<StarIcon sx={{ fontSize: '1rem', color: 'primary.main' }} />
<Typography variant="caption" sx={{ fontWeight: 'bold' }}>{review.score}/10</Typography>
</Box>
<Box sx={{ display: { xs: 'none', xl: 'flex' }, alignItems: 'center' }}>
<MuiRating value={review.score} readOnly size="small" max={10} />
</Box>
<RatingBadge score={review.score} size="small" />
</Box>
<Typography variant="caption" color="text.secondary" display="block" sx={{ mb: 1 }}>
{formatDate(review.created_at)} • on <Link to={`/flavor/${review.flavor}`} style={{ color: 'inherit', fontWeight: 'bold' }}>{review.flavor_name}</Link>
+3 -1
View File
@@ -19,6 +19,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { Link } from 'react-router-dom';
import api from '../api';
import { useTitle } from '../hooks/useTitle';
import { useTranslation } from 'react-i18next';
interface Rating {
id: number;
@@ -43,6 +44,7 @@ interface ProfileData {
}
const PublicProfile: React.FC = () => {
const { t } = useTranslation();
const { username } = useParams<{ username: string }>();
const navigate = useNavigate();
const [data, setData] = useState<ProfileData | null>(null);
@@ -144,7 +146,7 @@ const PublicProfile: React.FC = () => {
}
}}
>
Back
{window.history.length > 1 ? t('common.back') : t('common.backToHome')}
</Button>
{/* Profile Header */}
+25 -5
View File
@@ -17,7 +17,7 @@ import {
Grid
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { Link } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import api from '../api';
import { useTranslation } from 'react-i18next';
import { useTitle } from '../hooks/useTitle';
@@ -30,6 +30,7 @@ interface SettingsProps {
const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
const { t, i18n } = useTranslation();
const navigate = useNavigate();
useTitle(t('nav.settings'));
const [username, setUsername] = useState('');
const [email, setEmail] = useState('');
@@ -44,6 +45,14 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
const [isDeleting, setIsDeleting] = useState(false);
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null);
const handleGoBack = () => {
if (window.history.length > 1) {
navigate(-1);
} else {
navigate('/');
}
};
useEffect(() => {
const fetchUser = async () => {
try {
@@ -162,12 +171,23 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
<Container maxWidth="lg" sx={{ py: 4 }}>
<Button
variant="outlined"
component={Link}
to="/"
onClick={handleGoBack}
startIcon={<ArrowBackIcon />}
sx={{ mb: 4, textTransform: 'none', borderRadius: 2 }}
sx={{
mb: 4,
borderRadius: 2,
textTransform: 'none',
fontWeight: 'bold',
color: 'text.secondary',
borderColor: 'divider',
'&:hover': {
borderColor: 'primary.main',
color: 'primary.main',
bgcolor: 'transparent'
}
}}
>
{t('common.backToHome')}
{window.history.length > 1 ? t('common.back') : t('common.backToHome')}
</Button>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 3, mb: 6 }}>
@@ -5,7 +5,6 @@ import {
Box,
Card,
CardContent,
Rating as MuiRating,
CircularProgress,
Container,
Button
@@ -13,6 +12,8 @@ import {
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import api from '../../api';
import { useTitle } from '../../hooks/useTitle';
import RatingBadge from '../../components/RatingBadge';
import { useTranslation } from 'react-i18next';
interface Rating {
id: number;
@@ -37,6 +38,7 @@ interface Flavor {
}
const CategoryFlavors: React.FC = () => {
const { t } = useTranslation();
const { slug } = useParams<{ slug: string }>();
const navigate = useNavigate();
const [flavors, setFlavors] = useState<Flavor[]>([]);
@@ -95,7 +97,7 @@ const CategoryFlavors: React.FC = () => {
}
}}
>
Back
{window.history.length > 1 ? t('common.back') : t('common.backToHome')}
</Button>
</Box>
@@ -152,8 +154,7 @@ const CategoryFlavors: React.FC = () => {
<Typography variant="h6" sx={{ fontSize: '1.1rem', mb: 1 }}>{flavor.name}</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', mb: 1 }}>
<MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} max={10} size="small" />
<Typography variant="caption" sx={{ ml: 1 }}>({(flavor.average_rating || 0).toFixed(1)})</Typography>
<RatingBadge score={flavor.average_rating || 0} size="small" />
</Box>
<Typography variant="caption" color="text.secondary">{flavor.ratings.length} reviews</Typography>
</CardContent>
+6 -36
View File
@@ -27,6 +27,7 @@ import CommentIcon from '@mui/icons-material/Comment';
import { formatDate } from '../../utils/date';
import MentionTextField from '../../components/MentionTextField';
import RichText from '../../components/RichText';
import RatingBadge from '../../components/RatingBadge';
interface Reply {
id: number;
@@ -193,34 +194,6 @@ const FlavorDetail: React.FC = () => {
}
};
const RatingBadge = ({ score, size = 'medium' }: { score: number, size?: 'small' | 'medium' | 'large' }) => {
const isLarge = size === 'large';
const isSmall = size === 'small';
return (
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1),
color: 'primary.main',
px: isLarge ? 3 : (isSmall ? 1 : 2),
py: isLarge ? 1 : 0.5,
borderRadius: isLarge ? 3 : 2,
minWidth: isLarge ? 80 : 50,
border: '1px solid',
borderColor: (theme) => alpha(theme.palette.primary.main, 0.2)
}}>
<Typography variant={isLarge ? "h4" : (isSmall ? "subtitle2" : "h6")} sx={{ fontWeight: '900', lineHeight: 1 }}>
{score}
</Typography>
<Typography variant="caption" sx={{ fontSize: isLarge ? '0.75rem' : '0.6rem', fontWeight: 'bold', opacity: 0.8 }}>
/ 10
</Typography>
</Box>
);
};
if (loading) return <Box sx={{ display: 'flex', justifyContent: 'center', mt: 8 }}><CircularProgress /></Box>;
if (!flavor) return <Typography>Flavor not found</Typography>;
@@ -245,7 +218,7 @@ const FlavorDetail: React.FC = () => {
}
}}
>
{t('common.backToHome').replace('Home', 'Back')}
{window.history.length > 1 ? t('common.back') : t('common.backToHome')}
</Button>
</Box>
@@ -311,13 +284,10 @@ const FlavorDetail: React.FC = () => {
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 3 }}>
<RatingBadge score={Number((flavor.average_rating || 0).toFixed(1))} size="large" />
<Box>
<MuiRating value={flavor.average_rating || 0} readOnly precision={0.5} size="small" />
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 'bold' }}>
{flavor.ratings.length} {t('common.reviews')}
</Typography>
</Box>
<RatingBadge score={flavor.average_rating || 0} size="large" />
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 'bold' }}>
{flavor.ratings.length} {t('common.reviews')}
</Typography>
</Box>
<Typography variant="body2" sx={{ lineHeight: 1.7, color: 'text.secondary', mb: 4 }}>