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