This commit is contained in:
nvrl committed 2026-04-24 12:54:48 +02:00
1 parent c42fd733f0
commit 6d05fa779f
28 files changed
+272 -162

No files matched your search

+1 -6
View File
@@ -512,12 +512,7 @@ const App: React.FC = () => {
</Box>
) : (
<Box sx={{ display: isMobile ? 'none' : 'block' }}>
<Button
variant="contained"
component={Link}
to="/login"
sx={{ borderRadius: 2 }}
>
<Button variant="contained" component={Link} to="/login" sx={{ borderRadius: 2 }}>
Login
</Button>
</Box>
@@ -1,10 +1,7 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { useContext } from 'react';
import { act, render } from '@testing-library/react';
import {
BannerPerfContext,
BannerPerformanceWrapper,
} from './BannerPerformanceWrapper';
import { BannerPerfContext, BannerPerformanceWrapper } from './BannerPerformanceWrapper';
type IOEntryLike = { isIntersecting: boolean };
type IOCallback = (entries: IOEntryLike[]) => void;
@@ -45,9 +42,7 @@ const mockMatchMedia = (reduced: boolean) => {
const Probe = () => {
const { isActive, targetFps } = useContext(BannerPerfContext);
return (
<div data-testid="probe" data-active={String(isActive)} data-fps={String(targetFps)} />
);
return <div data-testid="probe" data-active={String(isActive)} data-fps={String(targetFps)} />;
};
describe('BannerPerformanceWrapper', () => {
@@ -37,10 +37,10 @@ export const BannerPerformanceWrapper = ({
useEffect(() => {
const el = ref.current;
if (!el || typeof IntersectionObserver === 'undefined') return;
const io = new IntersectionObserver(
([entry]) => setVisible(entry.isIntersecting),
{ threshold: 0.01, rootMargin },
);
const io = new IntersectionObserver(([entry]) => setVisible(entry.isIntersecting), {
threshold: 0.01,
rootMargin,
});
io.observe(el);
return () => io.disconnect();
}, [rootMargin]);
@@ -55,10 +55,7 @@ export const BannerPerformanceWrapper = ({
return (
<BannerPerfContext.Provider value={value}>
<Box
ref={ref}
sx={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}
>
<Box ref={ref} sx={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
{children}
</Box>
</BannerPerfContext.Provider>
+2 -7
View File
@@ -28,9 +28,7 @@ const MatrixBanner: React.FC<DynamicBannerProps & { settings: Banner['settings']
username,
settings,
}) => {
const [durations] = useState(() =>
Array.from({ length: 20 }, () => 2 + Math.random() * 3),
);
const [durations] = useState(() => Array.from({ length: 20 }, () => 2 + Math.random() * 3));
return (
<div
style={{
@@ -73,10 +71,7 @@ const MatrixBanner: React.FC<DynamicBannerProps & { settings: Banner['settings']
);
};
const pickBanner = (
banner: Banner | null,
props: DynamicBannerProps,
): React.ReactElement => {
const pickBanner = (banner: Banner | null, props: DynamicBannerProps): React.ReactElement => {
if (!banner || banner.slug === 'generative-web') {
return <GenerativeBanner {...props} settings={banner?.settings} />;
}
@@ -232,7 +232,17 @@ const GenerativeFlowBanner: React.FC<GenerativeFlowProps> = ({ username, palette
}
cancelAnimationFrame(animationFrameId);
};
}, [seed, brightColors, blobCount, flowSpeed, vibrancy, blurAmount, distortionFactor, theme, gate]);
}, [
seed,
brightColors,
blobCount,
flowSpeed,
vibrancy,
blurAmount,
distortionFactor,
theme,
gate,
]);
return (
<canvas
@@ -36,14 +36,7 @@ describe('EmptyState', () => {
});
it('renders icon slot', () => {
render(
wrap(
<EmptyState
title="Nothing"
icon={<span data-testid="icon">📭</span>}
/>,
),
);
render(wrap(<EmptyState title="Nothing" icon={<span data-testid="icon">📭</span>} />));
expect(screen.getByTestId('icon')).toBeInTheDocument();
});
});
@@ -45,10 +45,7 @@ describe('FlavorCard', () => {
render(
<MemoryRouter>
<ThemeProvider theme={getTheme('mocha')}>
<FlavorCard
flavor={{ id: 1, name: 'X', category_name: 'Fruit' }}
showCategory={false}
/>
<FlavorCard flavor={{ id: 1, name: 'X', category_name: 'Fruit' }} showCategory={false} />
</ThemeProvider>
</MemoryRouter>,
);
@@ -66,10 +63,7 @@ describe('FlavorCard', () => {
render(
<MemoryRouter>
<ThemeProvider theme={getTheme('mocha')}>
<FlavorCard
flavor={{ id: 2, name: 'Cap', average_rating: 5 }}
caption="12 reviews"
/>
<FlavorCard flavor={{ id: 2, name: 'Cap', average_rating: 5 }} caption="12 reviews" />
</ThemeProvider>
</MemoryRouter>,
);
+14 -7
View File
@@ -50,11 +50,7 @@ export const FlavorCard = ({
}}
>
<Box sx={{ position: 'absolute', top: 10, right: 10, zIndex: 1 }}>
<StatusBadge
isLegacy={flavor.is_legacy}
isAvailable={flavor.is_available}
size="small"
/>
<StatusBadge isLegacy={flavor.is_legacy} isAvailable={flavor.is_available} size="small" />
</Box>
{flavor.image_url && (
@@ -88,7 +84,11 @@ export const FlavorCard = ({
)}
<CardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 0.75 }}>
<Typography variant="h6" sx={{ fontSize: compact ? '0.95rem' : '1rem', lineHeight: 1.25 }} noWrap>
<Typography
variant="h6"
sx={{ fontSize: compact ? '0.95rem' : '1rem', lineHeight: 1.25 }}
noWrap
>
{flavor.name}
</Typography>
@@ -98,7 +98,14 @@ export const FlavorCard = ({
</Typography>
)}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mt: 'auto' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mt: 'auto',
}}
>
<RatingBadge score={flavor.average_rating || 0} size="small" />
{caption && (
<Typography variant="caption" color="text.secondary" noWrap>
+7 -1
View File
@@ -45,7 +45,13 @@ describe('FormCard', () => {
});
it('applies danger styling when danger prop set', () => {
render(wrap(<FormCard title="Delete account" danger>content</FormCard>));
render(
wrap(
<FormCard title="Delete account" danger>
content
</FormCard>,
),
);
const title = screen.getByText('Delete account');
const color = window.getComputedStyle(title).color;
// MUI uses the palette.error.main — we just assert the title DOM exists and has a color distinct from default
+10 -7
View File
@@ -9,10 +9,7 @@ const collectStyles = () =>
.map((s) => s.textContent ?? '')
.join('\n');
const renderWithTheme = (
ui: React.ReactElement,
mode: CatppuccinTheme = 'mocha',
) => {
const renderWithTheme = (ui: React.ReactElement, mode: CatppuccinTheme = 'mocha') => {
const theme = getTheme(mode);
const result = render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);
return { theme, ...result };
@@ -36,7 +33,9 @@ describe('Glass primitives', () => {
it('GlassCard intensity="strong" emits blurStrong + tintStrong into emitted CSS', () => {
const { theme } = renderWithTheme(
<GlassCard data-testid="g" intensity="strong">x</GlassCard>,
<GlassCard data-testid="g" intensity="strong">
x
</GlassCard>,
);
const css = collectStyles();
expect(css).toContain(theme.tokens.glass.blurStrong);
@@ -45,7 +44,9 @@ describe('Glass primitives', () => {
it('GlassCard intensity="subtle" emits tintSubtle into emitted CSS', () => {
const { theme } = renderWithTheme(
<GlassCard data-testid="g" intensity="subtle">x</GlassCard>,
<GlassCard data-testid="g" intensity="subtle">
x
</GlassCard>,
);
const css = collectStyles();
expect(css).toContain(theme.tokens.glass.tintSubtle);
@@ -53,7 +54,9 @@ describe('Glass primitives', () => {
it('does not forward `intensity` prop to the DOM', () => {
const { getByTestId } = renderWithTheme(
<GlassCard data-testid="g" intensity="strong">x</GlassCard>,
<GlassCard data-testid="g" intensity="strong">
x
</GlassCard>,
);
expect(getByTestId('g').getAttribute('intensity')).toBeNull();
});
+7 -3
View File
@@ -5,11 +5,15 @@ import Box from '@mui/material/Box';
type Intensity = 'subtle' | 'default' | 'strong';
const intensityStyles = (intensity: Intensity) => ({ theme }: { theme: any }) => {
const intensityStyles =
(intensity: Intensity) =>
({ theme }: { theme: any }) => {
const t = theme.tokens;
const tint =
intensity === 'strong' ? t.glass.tintStrong
: intensity === 'subtle' ? t.glass.tintSubtle
intensity === 'strong'
? t.glass.tintStrong
: intensity === 'subtle'
? t.glass.tintSubtle
: t.glass.tint;
const blur = intensity === 'strong' ? t.glass.blurStrong : t.glass.blur;
return {
+5 -5
View File
@@ -22,13 +22,13 @@ const Root = styled(Box, {
`conic-gradient(from 210deg at 50% -10%, ${alpha(p, 0.35)}, ${alpha(s, 0.25)}, ${alpha(p, 0.1)}, ${alpha(p, 0.35)})`,
];
const minimalLayers = [
`linear-gradient(180deg, ${alpha(p, 0.12)} 0%, transparent 60%)`,
];
const minimalLayers = [`linear-gradient(180deg, ${alpha(p, 0.12)} 0%, transparent 60%)`];
const backgroundImage =
variant === 'aurora' ? auroraLayers.join(', ')
: variant === 'minimal' ? minimalLayers.join(', ')
variant === 'aurora'
? auroraLayers.join(', ')
: variant === 'minimal'
? minimalLayers.join(', ')
: meshLayers.join(', ');
return {
+1 -4
View File
@@ -46,10 +46,7 @@ export const ReviewRow = ({ review, showFlavor = true, compact }: ReviewRowProps
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, minWidth: 0 }}>
<Avatar
src={review.user_avatar || undefined}
sx={{ width: 28, height: 28 }}
>
<Avatar src={review.user_avatar || undefined} sx={{ width: 28, height: 28 }}>
{!review.user_avatar && review.user.charAt(0).toUpperCase()}
</Avatar>
<Box sx={{ minWidth: 0, flex: 1 }}>
+1 -5
View File
@@ -37,11 +37,7 @@ export const SectionHeader = ({ title, subtitle, action, compact }: SectionHeade
{title}
</Typography>
{subtitle && (
<Typography
variant="body2"
color="text.secondary"
sx={{ mt: 0.5, maxWidth: '60ch' }}
>
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5, maxWidth: '60ch' }}>
{subtitle}
</Typography>
)}
+1 -2
View File
@@ -26,8 +26,7 @@ export const ConfirmProvider = ({ children }: { children: ReactNode }) => {
const resolverRef = useRef<((value: boolean) => void) | null>(null);
const confirm = useCallback((input: ConfirmOptions | string) => {
const normalized: ConfirmOptions =
typeof input === 'string' ? { message: input } : input;
const normalized: ConfirmOptions = typeof input === 'string' ? { message: input } : input;
setOpts(normalized);
setOpen(true);
return new Promise<boolean>((resolve) => {
+16 -3
View File
@@ -22,7 +22,10 @@ const About: React.FC = () => {
{t('common.backToArchive')}
</Button>
<GlassCard intensity="subtle" sx={{ p: { xs: 2.5, sm: 4, md: 6 }, maxWidth: 900, width: '100%' }}>
<GlassCard
intensity="subtle"
sx={{ p: { xs: 2.5, sm: 4, md: 6 }, maxWidth: 900, width: '100%' }}
>
<SectionHeader title={t('about.title')} compact />
<Typography
variant="body1"
@@ -96,10 +99,20 @@ const About: React.FC = () => {
<Box sx={{ mt: 4 }}>
<SectionHeader title={t('about.disclaimerTitle')} compact />
<Typography variant="caption" color="text.secondary" component="p" sx={{ mb: 2, lineHeight: 1.6 }}>
<Typography
variant="caption"
color="text.secondary"
component="p"
sx={{ mb: 2, lineHeight: 1.6 }}
>
<strong>{t('about.liabilityContentTitle')}</strong> {t('about.liabilityContentDesc')}
</Typography>
<Typography variant="caption" color="text.secondary" component="p" sx={{ lineHeight: 1.6 }}>
<Typography
variant="caption"
color="text.secondary"
component="p"
sx={{ lineHeight: 1.6 }}
>
<strong>{t('about.copyrightTitle')}</strong> {t('about.copyrightDesc')}
</Typography>
</Box>
+8 -2
View File
@@ -285,7 +285,10 @@ const AdminDashboard: React.FC = () => {
<Box>
<Grid container spacing={isMobile ? 2 : 3} sx={{ mb: 6 }}>
<Grid size={{ xs: 6, md: 3 }}>
<GlassCard intensity="subtle" sx={{ bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1) }}>
<GlassCard
intensity="subtle"
sx={{ bgcolor: (theme) => alpha(theme.palette.primary.main, 0.1) }}
>
<CardContent sx={{ p: isMobile ? 2 : 3 }}>
<Typography variant="overline" sx={{ fontSize: '0.6rem' }}>
{t('admin.totalUsers')}
@@ -297,7 +300,10 @@ const AdminDashboard: React.FC = () => {
</GlassCard>
</Grid>
<Grid size={{ xs: 6, md: 3 }}>
<GlassCard intensity="subtle" sx={{ bgcolor: (theme) => alpha(theme.palette.error.main, 0.1) }}>
<GlassCard
intensity="subtle"
sx={{ bgcolor: (theme) => alpha(theme.palette.error.main, 0.1) }}
>
<CardContent sx={{ p: isMobile ? 2 : 3 }}>
<Typography variant="overline" sx={{ fontSize: '0.6rem' }}>
{t('admin.openTickets')}
+53 -22
View File
@@ -33,13 +33,7 @@ import { formatDate } from '../utils/date';
import { useTranslation } from 'react-i18next';
import MentionTextField from '../components/MentionTextField';
import RatingBadge from '../components/RatingBadge';
import {
PageShell,
HeroBackdrop,
SectionHeader,
GlassCard,
EmptyState,
} from '../components/ui';
import { PageShell, HeroBackdrop, SectionHeader, GlassCard, EmptyState } from '../components/ui';
import { useToast } from '../hooks/useToast';
interface Reply {
@@ -101,11 +95,11 @@ interface CommunityFeedProps {
adminMode?: boolean;
}
const SidebarCard: React.FC<{ icon: React.ReactNode; title: React.ReactNode; children: React.ReactNode }> = ({
icon,
title,
children,
}) => (
const SidebarCard: React.FC<{
icon: React.ReactNode;
title: React.ReactNode;
children: React.ReactNode;
}> = ({ icon, title, children }) => (
<GlassCard intensity="subtle">
<Box
sx={{
@@ -174,7 +168,9 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
setTopFollowed(Array.isArray(topFollowedRes.data) ? topFollowedRes.data.slice(0, 5) : []);
const notifData = Array.isArray(notifRes.data) ? notifRes.data : notifRes.data.results || [];
const notifData = Array.isArray(notifRes.data)
? notifRes.data
: notifRes.data.results || [];
setNotifications(notifData.slice(0, 5));
} catch (err) {
const status = (err as { response?: { status?: number } }).response?.status;
@@ -278,11 +274,18 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
to={`/profile/${rating.user}`}
style={{ textDecoration: 'none', color: 'inherit' }}
>
<Typography variant="subtitle1" sx={{ fontWeight: 'bold', lineHeight: 1.2 }}>
<Typography
variant="subtitle1"
sx={{ fontWeight: 'bold', lineHeight: 1.2 }}
>
{rating.user}
</Typography>
</Link>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block' }}>
<Typography
variant="caption"
color="text.secondary"
sx={{ display: 'block' }}
>
{formatDate(rating.created_at)}
</Typography>
</Box>
@@ -309,7 +312,11 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
<MuiLink
component={Link}
to={`/flavor/${rating.flavor}`}
sx={{ color: 'primary.main', fontWeight: 'bold', textDecoration: 'none' }}
sx={{
color: 'primary.main',
fontWeight: 'bold',
textDecoration: 'none',
}}
>
{rating.flavor_name}
</MuiLink>
@@ -322,7 +329,11 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
<RichText text={rating.comment} />
</Typography>
) : (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontStyle: 'italic' }}
>
No comment provided.
</Typography>
)}
@@ -367,14 +378,23 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
{rating.replies.map((reply) => (
<Box key={reply.id} sx={{ mb: 1.5 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="subtitle2" sx={{ fontWeight: 'bold', fontSize: '0.8rem' }}>
<Typography
variant="subtitle2"
sx={{ fontWeight: 'bold', fontSize: '0.8rem' }}
>
{reply.user}
</Typography>
<Typography variant="caption" color="text.secondary">
{formatDate(reply.created_at)}
</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<Typography variant="body2">
<RichText text={reply.text} />
</Typography>
@@ -394,7 +414,9 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
<MentionTextField
placeholder={t('community.writeReply')}
value={replyInputs[rating.id] || ''}
onChange={(val) => setReplyInputs((prev) => ({ ...prev, [rating.id]: val }))}
onChange={(val) =>
setReplyInputs((prev) => ({ ...prev, [rating.id]: val }))
}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
handleReplySubmit(rating.id);
@@ -416,7 +438,12 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
))}
{totalPages > 1 && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
<Pagination count={totalPages} page={page} onChange={handlePageChange} color="primary" />
<Pagination
count={totalPages}
page={page}
onChange={handlePageChange}
color="primary"
/>
</Box>
)}
</Box>
@@ -549,7 +576,11 @@ const CommunityFeed: React.FC<CommunityFeedProps> = ({ adminMode }) => {
</Box>
<ListItemText
primary={flavor.name}
primaryTypographyProps={{ variant: 'body2', fontWeight: 'bold', noWrap: true }}
primaryTypographyProps={{
variant: 'body2',
fontWeight: 'bold',
noWrap: true,
}}
/>
<RatingBadge score={flavor.average_rating || 0} size="small" />
</ListItemButton>
+54 -12
View File
@@ -160,7 +160,9 @@ const Dashboard: React.FC = () => {
max === min ? 0 : l > 0.5 ? (max - min) / (2 - max - min) : (max - min) / (max + min);
return { rgb: `rgb(${c[0]}, ${c[1]}, ${c[2]})`, l, s };
});
const vibrant = processed.filter((c) => c.l > 0.2 && c.l < 0.85).sort((a, b) => b.s - a.s);
const vibrant = processed
.filter((c) => c.l > 0.2 && c.l < 0.85)
.sort((a, b) => b.s - a.s);
if (vibrant.length >= 2) setPalette([vibrant[0].rgb, vibrant[1].rgb]);
else if (vibrant.length === 1) setPalette([vibrant[0].rgb, vibrant[0].rgb]);
else setPalette([]);
@@ -219,7 +221,9 @@ const Dashboard: React.FC = () => {
const exploreCategories = useMemo(() => {
if (!data) return ['All'];
const cats = new Set(
data.missing_flavors.filter((f) => f.category_name !== 'Packs and other').map((f) => f.category_name),
data.missing_flavors
.filter((f) => f.category_name !== 'Packs and other')
.map((f) => f.category_name),
);
return ['All', ...Array.from(cats).sort()];
}, [data]);
@@ -401,7 +405,11 @@ const Dashboard: React.FC = () => {
</Typography>
</Box>
{i < 1 && (
<Divider orientation="vertical" flexItem sx={{ borderStyle: 'solid', opacity: 0.1 }} />
<Divider
orientation="vertical"
flexItem
sx={{ borderStyle: 'solid', opacity: 0.1 }}
/>
)}
</React.Fragment>
))}
@@ -510,7 +518,11 @@ const Dashboard: React.FC = () => {
<EmptyState
title={t('dashboard.noRatings')}
action={
<Button onClick={() => setActiveTab(1)} variant="contained" sx={{ borderRadius: 2 }}>
<Button
onClick={() => setActiveTab(1)}
variant="contained"
sx={{ borderRadius: 2 }}
>
{t('dashboard.exploreFlavors')}
</Button>
}
@@ -539,7 +551,11 @@ const Dashboard: React.FC = () => {
onChange={setEditComment}
/>
<Stack direction="row" spacing={1}>
<Button variant="contained" size="small" onClick={() => handleUpdateRating(rating.id)}>
<Button
variant="contained"
size="small"
onClick={() => handleUpdateRating(rating.id)}
>
{t('common.save')}
</Button>
<Button
@@ -590,7 +606,12 @@ const Dashboard: React.FC = () => {
{rating.flavor_name}
</Link>
</Typography>
<Stack direction="row" spacing={1} alignItems="center" sx={{ mt: 0.5 }}>
<Stack
direction="row"
spacing={1}
alignItems="center"
sx={{ mt: 0.5 }}
>
<Typography variant="caption" color="text.secondary">
{t(`categories.${rating.category_slug}`, {
defaultValue: rating.category_name,
@@ -608,11 +629,22 @@ const Dashboard: React.FC = () => {
<RatingBadge score={rating.score} size={isMobile ? 'medium' : 'large'} />
</Box>
<Box sx={{ mb: 2, p: 2, bgcolor: alpha(theme.palette.text.primary, 0.03), borderRadius: 2 }}>
<Box
sx={{
mb: 2,
p: 2,
bgcolor: alpha(theme.palette.text.primary, 0.03),
borderRadius: 2,
}}
>
{rating.comment ? (
<RichText text={rating.comment} />
) : (
<Typography variant="body2" color="text.secondary" sx={{ fontStyle: 'italic' }}>
<Typography
variant="body2"
color="text.secondary"
sx={{ fontStyle: 'italic' }}
>
{t('dashboard.noComment')}
</Typography>
)}
@@ -623,9 +655,16 @@ const Dashboard: React.FC = () => {
size="small"
startIcon={<CommentIcon fontSize="small" />}
onClick={() =>
setExpandedReplies((prev) => ({ ...prev, [rating.id]: !prev[rating.id] }))
setExpandedReplies((prev) => ({
...prev,
[rating.id]: !prev[rating.id],
}))
}
sx={{ textTransform: 'none', fontWeight: 'bold', color: 'text.secondary' }}
sx={{
textTransform: 'none',
fontWeight: 'bold',
color: 'text.secondary',
}}
>
{rating.replies.length} {t('common.replies')}
</Button>
@@ -652,7 +691,9 @@ const Dashboard: React.FC = () => {
</Stack>
<Collapse in={expandedReplies[rating.id]}>
<Box sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}>
<Box
sx={{ mt: 2, pt: 2, borderTop: '1px solid', borderColor: 'divider' }}
>
{rating.replies.map((reply) => (
<Box
key={reply.id}
@@ -886,7 +927,8 @@ const Dashboard: React.FC = () => {
size="small"
sx={{
height: 24,
bgcolor: exploreSort === 'circle' ? 'secondary.main' : 'primary.main',
bgcolor:
exploreSort === 'circle' ? 'secondary.main' : 'primary.main',
}}
/>
</Box>
+18 -4
View File
@@ -157,7 +157,9 @@ const FlavorList: React.FC = () => {
/>
)}
<CardContent sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
<CardContent
sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 1 }}
>
<Typography variant="h6">{flavor.name}</Typography>
<Typography variant="body2" color="text.secondary">
{flavor.description}
@@ -194,9 +196,17 @@ const FlavorList: React.FC = () => {
.map((r) => (
<Box
key={r.id}
sx={{ mb: 1, borderBottom: '1px solid', borderColor: 'divider', pb: 0.5 }}
sx={{
mb: 1,
borderBottom: '1px solid',
borderColor: 'divider',
pb: 0.5,
}}
>
<Typography
variant="caption"
sx={{ fontWeight: 'bold', display: 'block' }}
>
<Typography variant="caption" sx={{ fontWeight: 'bold', display: 'block' }}>
{r.user} ({r.score}/10):
</Typography>
<Typography variant="body2" sx={{ fontSize: '0.8rem' }}>
@@ -248,7 +258,11 @@ const FlavorList: React.FC = () => {
})
}
/>
<Button variant="contained" size="small" onClick={() => handleRate(flavor.id)}>
<Button
variant="contained"
size="small"
onClick={() => handleRate(flavor.id)}
>
Submit
</Button>
</Box>
+1 -10
View File
@@ -1,14 +1,5 @@
import React, { useState } from 'react';
import {
Typography,
TextField,
Button,
Tab,
Tabs,
Alert,
Stack,
Box,
} from '@mui/material';
import { Typography, TextField, Button, Tab, Tabs, Alert, Stack, Box } from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { Link } from 'react-router-dom';
import api, { setTokens } from '../lib/api';
+1 -5
View File
@@ -526,11 +526,7 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
}}
>
<RatingBadge score={currentTop.average_rating || 0} size="large" />
<Typography
variant="body2"
color="text.secondary"
sx={{ fontWeight: 'bold' }}
>
<Typography variant="body2" color="text.secondary" sx={{ fontWeight: 'bold' }}>
{currentTop.ratings.length} {t('common.reviews')}
</Typography>
</Box>
+5 -8
View File
@@ -1,11 +1,5 @@
import React from 'react';
import {
Typography,
Box,
Button,
ToggleButton,
ToggleButtonGroup,
} from '@mui/material';
import { Typography, Box, Button, ToggleButton, ToggleButtonGroup } from '@mui/material';
import { Link } from 'react-router-dom';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useTitle } from '../hooks/useTitle';
@@ -50,7 +44,10 @@ const Privacy: React.FC = () => {
</ToggleButtonGroup>
</Box>
<GlassCard intensity="subtle" sx={{ p: { xs: 2.5, sm: 4, md: 6 }, maxWidth: 900, width: '100%' }}>
<GlassCard
intensity="subtle"
sx={{ p: { xs: 2.5, sm: 4, md: 6 }, maxWidth: 900, width: '100%' }}
>
<SectionHeader title={t('privacy.title')} />
<Typography variant="body1" paragraph sx={{ lineHeight: 1.7 }}>
{t('privacy.intro')}
+19 -5
View File
@@ -138,7 +138,9 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
max === min ? 0 : l > 0.5 ? (max - min) / (2 - max - min) : (max - min) / (max + min);
return { rgb: `rgb(${c[0]}, ${c[1]}, ${c[2]})`, l, s };
});
const vibrant = processed.filter((c) => c.l > 0.2 && c.l < 0.85).sort((a, b) => b.s - a.s);
const vibrant = processed
.filter((c) => c.l > 0.2 && c.l < 0.85)
.sort((a, b) => b.s - a.s);
if (vibrant.length >= 2) setPalette([vibrant[0].rgb, vibrant[1].rgb]);
else if (vibrant.length === 1) setPalette([vibrant[0].rgb, vibrant[0].rgb]);
else setPalette([]);
@@ -356,7 +358,9 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
/>
</Box>
<CardContent sx={{ pt: 0, px: { xs: 2, md: 5 }, pb: { xs: 2, md: 4 }, position: 'relative' }}>
<CardContent
sx={{ pt: 0, px: { xs: 2, md: 5 }, pb: { xs: 2, md: 4 }, position: 'relative' }}
>
<Box
sx={{
display: 'flex',
@@ -766,7 +770,10 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
}}
>
<Stack direction="row" spacing={2.5} alignItems="center">
<Link to={`/profile/${comment.author_username}`} style={{ textDecoration: 'none' }}>
<Link
to={`/profile/${comment.author_username}`}
style={{ textDecoration: 'none' }}
>
<Avatar
src={comment.author_avatar || undefined}
sx={{
@@ -784,7 +791,10 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
to={`/profile/${comment.author_username}`}
style={{ textDecoration: 'none', color: 'inherit' }}
>
<Typography variant="subtitle1" sx={{ fontWeight: '900', lineHeight: 1.2 }}>
<Typography
variant="subtitle1"
sx={{ fontWeight: '900', lineHeight: 1.2 }}
>
{comment.author_username}
</Typography>
</Link>
@@ -800,7 +810,11 @@ const PublicProfile: React.FC<PublicProfileProps> = ({ adminMode }) => {
{(currentUser?.username === comment.author_username ||
currentUser?.username === data.username ||
currentUser?.is_superuser) && (
<IconButton size="small" color="error" onClick={() => handleDeleteComment(comment.id)}>
<IconButton
size="small"
color="error"
onClick={() => handleDeleteComment(comment.id)}
>
<DeleteIcon fontSize="small" />
</IconButton>
)}
+12 -2
View File
@@ -323,7 +323,12 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
{t('settings.avatarButton')}
<input type="file" hidden accept="image/*" onChange={handleAvatarUpload} />
</Button>
<Typography variant="caption" display="block" sx={{ mt: 1 }} color="text.secondary">
<Typography
variant="caption"
display="block"
sx={{ mt: 1 }}
color="text.secondary"
>
{t('settings.avatarHint')}
</Typography>
</Box>
@@ -367,7 +372,12 @@ const Settings: React.FC<SettingsProps> = ({ themeName, onThemeChange }) => {
subtitle={t('settings.confirmEmailHint', { email })}
onSubmit={handleConfirmEmail}
actions={
<Button variant="contained" color="secondary" type="submit" sx={{ borderRadius: 2 }}>
<Button
variant="contained"
color="secondary"
type="submit"
sx={{ borderRadius: 2 }}
>
{t('settings.confirmButton')}
</Button>
}
+7 -2
View File
@@ -405,7 +405,9 @@ const Support: React.FC = () => {
<Collapse in={expandedId === ticket.id}>
<Divider />
<Box sx={{ p: 2, bgcolor: (theme) => alpha(theme.palette.background.default, 0.5) }}>
<Box
sx={{ p: 2, bgcolor: (theme) => alpha(theme.palette.background.default, 0.5) }}
>
{currentUser?.is_superuser && (
<Stack direction="row" spacing={1} sx={{ mb: 2, overflowX: 'auto', pb: 1 }}>
{(['open', 'in_progress', 'resolved', 'closed'] as const).map((s) => (
@@ -458,7 +460,10 @@ const Support: React.FC = () => {
boxShadow: msg.is_admin ? 2 : 0,
}}
>
<Typography variant="caption" sx={{ fontWeight: 'bold', display: 'block' }}>
<Typography
variant="caption"
sx={{ fontWeight: 'bold', display: 'block' }}
>
{msg.username} {msg.is_admin && '(Support)'}
</Typography>
<Typography variant="body2" sx={{ my: 0.5 }}>
@@ -5,7 +5,13 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import api from '../../lib/api';
import { useTitle } from '../../hooks/useTitle';
import { useTranslation } from 'react-i18next';
import { PageShell, HeroBackdrop, SectionHeader, FlavorCard, EmptyState } from '../../components/ui';
import {
PageShell,
HeroBackdrop,
SectionHeader,
FlavorCard,
EmptyState,
} from '../../components/ui';
interface Rating {
id: number;
+1 -7
View File
@@ -34,13 +34,7 @@ import MentionTextField from '../../components/MentionTextField';
import RichText from '../../components/RichText';
import RatingBadge from '../../components/RatingBadge';
import StatusBadge from '../../components/StatusBadge';
import {
PageShell,
HeroBackdrop,
GlassCard,
SectionHeader,
EmptyState,
} from '../../components/ui';
import { PageShell, HeroBackdrop, GlassCard, SectionHeader, EmptyState } from '../../components/ui';
import { useToast } from '../../hooks/useToast';
import { useConfirm } from '../../hooks/useConfirm';