mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
prettier
This commit is contained in:
1 parent
c42fd733f0
commit
6d05fa779f
28 files changed
+273
-163
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
@@ -31,7 +35,7 @@ const intensityStyles = (intensity: Intensity) => ({ theme }: { theme: any }) =>
|
||||
boxShadow: `inset 0 1px 0 ${t.glass.highlight}`,
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
interface GlassProps {
|
||||
intensity?: Intensity;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
Reference in new issue
Block a user