Public Access
refactor(limits): server limits in one file, own texts for unknown errors
This commit is contained in:
1 parent
f605917874
commit
24a98062ee
18 files changed
+84
-49
No files matched your search
@@ -13,7 +13,7 @@ import GameCover from './GameCover.jsx';
|
|||||||
import GameSearch from './GameSearch.jsx';
|
import GameSearch from './GameSearch.jsx';
|
||||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||||
import useStartImport from '../hooks/useStartImport.js';
|
import useStartImport from '../hooks/useStartImport.js';
|
||||||
import {GAME_STATUSES} from '../utils/backlog.js';
|
import {GAME_STATUSES, MAX_PLATFORM_LENGTH} from '../utils/limits.js';
|
||||||
import {addGame, hoursToMinutes} from '../utils/manualGames.js';
|
import {addGame, hoursToMinutes} from '../utils/manualGames.js';
|
||||||
import {loadFavoritePlatform, preselectPlatform} from '../utils/platforms.js';
|
import {loadFavoritePlatform, preselectPlatform} from '../utils/platforms.js';
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ function AddForm({hit, favorite, onBack, navigate, expireSession}) {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
) : (
|
) : (
|
||||||
<Input id="add-platform" value={platform} maxLength={32} onChange={(e) => setPlatform(e.target.value)}/>
|
<Input id="add-platform" value={platform} maxLength={MAX_PLATFORM_LENGTH} onChange={(e) => setPlatform(e.target.value)}/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -11,12 +11,11 @@ import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@/c
|
|||||||
import {Textarea} from '@/components/ui/textarea';
|
import {Textarea} from '@/components/ui/textarea';
|
||||||
import {cn} from '@/lib/utils';
|
import {cn} from '@/lib/utils';
|
||||||
import {STATUS_META} from './GameStatusBadge.jsx';
|
import {STATUS_META} from './GameStatusBadge.jsx';
|
||||||
import {GAME_PRIORITIES, GAME_STATUSES, NO_PRIORITY, saveBacklogEntry, toBacklogRequest} from '../utils/backlog.js';
|
import {NO_PRIORITY, saveBacklogEntry, toBacklogRequest} from '../utils/backlog.js';
|
||||||
|
import {GAME_PRIORITIES, GAME_STATUSES, MAX_NOTES_LENGTH} from '../utils/limits.js';
|
||||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||||
import {hasSession} from '../utils/auth.js';
|
import {hasSession} from '../utils/auth.js';
|
||||||
|
|
||||||
const NOTES_MAX = 2000;
|
|
||||||
|
|
||||||
const toForm = (entry) => ({
|
const toForm = (entry) => ({
|
||||||
status: entry.status ?? 'BACKLOG',
|
status: entry.status ?? 'BACKLOG',
|
||||||
rating: entry.rating ?? 0,
|
rating: entry.rating ?? 0,
|
||||||
@@ -133,12 +132,12 @@ export default function BacklogCard({game, onSaved}) {
|
|||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex items-baseline justify-between">
|
<div className="flex items-baseline justify-between">
|
||||||
<Label htmlFor="backlog-notes">{t('BACKLOG.NOTES_LABEL')}</Label>
|
<Label htmlFor="backlog-notes">{t('BACKLOG.NOTES_LABEL')}</Label>
|
||||||
<span className="text-xs text-muted-foreground tabular-nums">{form.notes.length} / {NOTES_MAX}</span>
|
<span className="text-xs text-muted-foreground tabular-nums">{form.notes.length} / {MAX_NOTES_LENGTH}</span>
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="backlog-notes"
|
id="backlog-notes"
|
||||||
rows={3}
|
rows={3}
|
||||||
maxLength={NOTES_MAX}
|
maxLength={MAX_NOTES_LENGTH}
|
||||||
placeholder={t('BACKLOG.NOTES_PLACEHOLDER')}
|
placeholder={t('BACKLOG.NOTES_PLACEHOLDER')}
|
||||||
value={form.notes}
|
value={form.notes}
|
||||||
onChange={(e) => set('notes')(e.target.value)}
|
onChange={(e) => set('notes')(e.target.value)}
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import {Label} from '@/components/ui/label';
|
|||||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||||
import {CORRECTABLE_MOODS, IGDB_GENRES, resetCorrection, saveCorrection} from '../utils/corrections.js';
|
import {CORRECTABLE_MOODS, IGDB_GENRES, resetCorrection, saveCorrection} from '../utils/corrections.js';
|
||||||
import {genreKey} from '../utils/stats.js';
|
import {genreKey} from '../utils/stats.js';
|
||||||
|
import {MAX_CORRECTION_GENRES, MAX_CORRECTION_LENGTH} from '../utils/limits.js';
|
||||||
|
|
||||||
const ENDLESS_CHOICES = [[null, 'UNKNOWN'], [true, 'YES'], [false, 'NO']];
|
const ENDLESS_CHOICES = [[null, 'UNKNOWN'], [true, 'YES'], [false, 'NO']];
|
||||||
// Core refuses more.
|
|
||||||
const MAX_GENRES = 10;
|
|
||||||
|
|
||||||
const initialValues = (game) => ({
|
const initialValues = (game) => ({
|
||||||
name: game.name ?? '',
|
name: game.name ?? '',
|
||||||
@@ -129,12 +128,12 @@ export default function GameCorrectionDialog({game, open, onOpenChange, onChange
|
|||||||
<DialogDescription>{t('CORRECTION.TEXT')}</DialogDescription>
|
<DialogDescription>{t('CORRECTION.TEXT')}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{row('name', <Input id="correction-name" value={values.name} maxLength={512}
|
{row('name', <Input id="correction-name" value={values.name} maxLength={MAX_CORRECTION_LENGTH}
|
||||||
onChange={(e) => set('name', e.target.value)}/>)}
|
onChange={(e) => set('name', e.target.value)}/>)}
|
||||||
{row('image', <Input id="correction-image" value={values.image} maxLength={512}
|
{row('image', <Input id="correction-image" value={values.image} maxLength={MAX_CORRECTION_LENGTH}
|
||||||
placeholder={t('CORRECTION.IMAGE_HINT')}
|
placeholder={t('CORRECTION.IMAGE_HINT')}
|
||||||
onChange={(e) => set('image', e.target.value)}/>)}
|
onChange={(e) => set('image', e.target.value)}/>)}
|
||||||
{row('speedrun', <Input id="correction-speedrun" value={values.speedrun} maxLength={512}
|
{row('speedrun', <Input id="correction-speedrun" value={values.speedrun} maxLength={MAX_CORRECTION_LENGTH}
|
||||||
placeholder={t('CORRECTION.SPEEDRUN_HINT')}
|
placeholder={t('CORRECTION.SPEEDRUN_HINT')}
|
||||||
onChange={(e) => set('speedrun', e.target.value)}/>)}
|
onChange={(e) => set('speedrun', e.target.value)}/>)}
|
||||||
{row('moods', choices('moods', CORRECTABLE_MOODS, (mood) => t(`PLAY_NEXT.MOOD.${mood}`)))}
|
{row('moods', choices('moods', CORRECTABLE_MOODS, (mood) => t(`PLAY_NEXT.MOOD.${mood}`)))}
|
||||||
@@ -150,7 +149,7 @@ export default function GameCorrectionDialog({game, open, onOpenChange, onChange
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{row('genres', choices('genres', genreOptions,
|
{row('genres', choices('genres', genreOptions,
|
||||||
(genre) => t(`STATS.GENRE.${genreKey(genre)}`, {defaultValue: genre}), MAX_GENRES))}
|
(genre) => t(`STATS.GENRE.${genreKey(genre)}`, {defaultValue: genre}), MAX_CORRECTION_GENRES))}
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {Input} from '@/components/ui/input';
|
|||||||
import GameCover from './GameCover.jsx';
|
import GameCover from './GameCover.jsx';
|
||||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||||
import {searchGames} from '../utils/manualGames.js';
|
import {searchGames} from '../utils/manualGames.js';
|
||||||
|
import {MAX_GAME_SEARCH_LENGTH, MIN_GAME_SEARCH_LENGTH} from '../utils/limits.js';
|
||||||
|
|
||||||
const SEARCH_DEBOUNCE_MS = 400;
|
const SEARCH_DEBOUNCE_MS = 400;
|
||||||
const MIN_QUERY = 2;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The search field and the hits of Core's game search. The parent keeps the query, so it stays when the user goes
|
* The search field and the hits of Core's game search. The parent keeps the query, so it stays when the user goes
|
||||||
@@ -29,7 +29,7 @@ export default function GameSearch({query, onQueryChange, onPick, hide = () => f
|
|||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
|
<Search className="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"/>
|
||||||
<Input type="search" autoFocus value={query} maxLength={100} className="pl-8"
|
<Input type="search" autoFocus value={query} maxLength={MAX_GAME_SEARCH_LENGTH} className="pl-8"
|
||||||
placeholder={t('MANUAL_GAME.SEARCH_PLACEHOLDER')}
|
placeholder={t('MANUAL_GAME.SEARCH_PLACEHOLDER')}
|
||||||
aria-label={t('MANUAL_GAME.SEARCH_PLACEHOLDER')}
|
aria-label={t('MANUAL_GAME.SEARCH_PLACEHOLDER')}
|
||||||
onChange={(e) => onQueryChange(e.target.value)}/>
|
onChange={(e) => onQueryChange(e.target.value)}/>
|
||||||
@@ -87,7 +87,7 @@ function useGameSearch(query) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const trimmed = query.trim();
|
const trimmed = query.trim();
|
||||||
if (trimmed.length < MIN_QUERY) {
|
if (trimmed.length < MIN_GAME_SEARCH_LENGTH) {
|
||||||
setSearchState('idle');
|
setSearchState('idle');
|
||||||
setResults([]);
|
setResults([]);
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {Input} from '@/components/ui/input';
|
|||||||
import {Label} from '@/components/ui/label';
|
import {Label} from '@/components/ui/label';
|
||||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||||
import {hoursToMinutes, removeGame, toHours, updateManualGame} from '../utils/manualGames.js';
|
import {hoursToMinutes, removeGame, toHours, updateManualGame} from '../utils/manualGames.js';
|
||||||
|
import {MAX_PLATFORM_LENGTH} from '../utils/limits.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Platform and hours of a game added by hand, and removing it. Steam games get both from the sync.
|
* Platform and hours of a game added by hand, and removing it. Steam games get both from the sync.
|
||||||
@@ -70,7 +71,7 @@ export default function ManualGameCard({game, onSaved}) {
|
|||||||
<div className="grid gap-5 sm:grid-cols-2">
|
<div className="grid gap-5 sm:grid-cols-2">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="manual-platform">{t('MANUAL_GAME.PLATFORM')}</Label>
|
<Label htmlFor="manual-platform">{t('MANUAL_GAME.PLATFORM')}</Label>
|
||||||
<Input id="manual-platform" value={platform} maxLength={32}
|
<Input id="manual-platform" value={platform} maxLength={MAX_PLATFORM_LENGTH}
|
||||||
onChange={(e) => setPlatform(e.target.value)}/>
|
onChange={(e) => setPlatform(e.target.value)}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import {Card} from '@/components/ui/card';
|
|||||||
import GameCover from './GameCover.jsx';
|
import GameCover from './GameCover.jsx';
|
||||||
import PileTrend from './stats/PileTrend.jsx';
|
import PileTrend from './stats/PileTrend.jsx';
|
||||||
import {STATUS_META} from './GameStatusBadge.jsx';
|
import {STATUS_META} from './GameStatusBadge.jsx';
|
||||||
import {backlogShare, GAME_STATUSES} from '../utils/backlog.js';
|
import {backlogShare} from '../utils/backlog.js';
|
||||||
|
import {GAME_STATUSES} from '../utils/limits.js';
|
||||||
import {formatPlaytime} from '../utils/games.js';
|
import {formatPlaytime} from '../utils/games.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Gamepad2} from 'lucide-react';
|
|||||||
|
|
||||||
import StatsSection from './StatsSection.jsx';
|
import StatsSection from './StatsSection.jsx';
|
||||||
import StatTile from './StatTile.jsx';
|
import StatTile from './StatTile.jsx';
|
||||||
import {GAME_STATUSES} from '../../utils/backlog.js';
|
import {GAME_STATUSES} from '../../utils/limits.js';
|
||||||
import {formatPlaytime} from '../../utils/games.js';
|
import {formatPlaytime} from '../../utils/games.js';
|
||||||
|
|
||||||
export default function OverviewStats({overview}) {
|
export default function OverviewStats({overview}) {
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ import PageContainer from '../components/PageContainer.jsx';
|
|||||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||||
import {hasSession} from '../utils/auth.js';
|
import {hasSession} from '../utils/auth.js';
|
||||||
import {createHint, endHint, fetchAdminHints} from '../utils/hints.js';
|
import {createHint, endHint, fetchAdminHints} from '../utils/hints.js';
|
||||||
|
import {MAX_HINT_LENGTH} from '../utils/limits.js';
|
||||||
|
|
||||||
const MAX_TEXT = 500;
|
|
||||||
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
const WEEK_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
// The value of a datetime-local input: local time, no seconds.
|
// The value of a datetime-local input: local time, no seconds.
|
||||||
@@ -129,11 +129,11 @@ export default function AdminHintsPage() {
|
|||||||
<form onSubmit={submit} className="space-y-4">
|
<form onSubmit={submit} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="hintTextEn">{t('ADMIN_HINTS.TEXT_EN')}</Label>
|
<Label htmlFor="hintTextEn">{t('ADMIN_HINTS.TEXT_EN')}</Label>
|
||||||
<Textarea id="hintTextEn" required maxLength={MAX_TEXT} value={form.textEn} onChange={change('textEn')}/>
|
<Textarea id="hintTextEn" required maxLength={MAX_HINT_LENGTH} value={form.textEn} onChange={change('textEn')}/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="hintTextDe">{t('ADMIN_HINTS.TEXT_DE')}</Label>
|
<Label htmlFor="hintTextDe">{t('ADMIN_HINTS.TEXT_DE')}</Label>
|
||||||
<Textarea id="hintTextDe" maxLength={MAX_TEXT} value={form.textDe} onChange={change('textDe')}/>
|
<Textarea id="hintTextDe" maxLength={MAX_HINT_LENGTH} value={form.textDe} onChange={change('textDe')}/>
|
||||||
<p className="text-xs text-muted-foreground">{t('ADMIN_HINTS.TEXT_HELP')}</p>
|
<p className="text-xs text-muted-foreground">{t('ADMIN_HINTS.TEXT_HELP')}</p>
|
||||||
</div>
|
</div>
|
||||||
<fieldset className="space-y-2">
|
<fieldset className="space-y-2">
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import SyncStatusBadge from '../components/SyncStatusBadge.jsx';
|
|||||||
import useForceSync from '../hooks/useForceSync.js';
|
import useForceSync from '../hooks/useForceSync.js';
|
||||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||||
import {formatPlaytime} from '../utils/games.js';
|
import {formatPlaytime} from '../utils/games.js';
|
||||||
import {GAME_STATUSES, libraryQuery, parseStatus} from '../utils/backlog.js';
|
import {libraryQuery, parseStatus} from '../utils/backlog.js';
|
||||||
|
import {GAME_STATUSES, MAX_LIBRARY_SEARCH_LENGTH} from '../utils/limits.js';
|
||||||
import {ArrowUpDown, ChevronLeft, ChevronRight, Clock, EyeOff, Gamepad2, Library, Plus, RefreshCw, Search, SearchX, Timer, Trophy, Upload} from 'lucide-react';
|
import {ArrowUpDown, ChevronLeft, ChevronRight, Clock, EyeOff, Gamepad2, Library, Plus, RefreshCw, Search, SearchX, Timer, Trophy, Upload} from 'lucide-react';
|
||||||
|
|
||||||
import {Badge} from '@/components/ui/badge';
|
import {Badge} from '@/components/ui/badge';
|
||||||
@@ -269,7 +270,7 @@ export default function GameLibrary() {
|
|||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder={t('LIBRARY.SEARCH_PLACEHOLDER')}
|
placeholder={t('LIBRARY.SEARCH_PLACEHOLDER')}
|
||||||
aria-label={t('LIBRARY.SEARCH_PLACEHOLDER')}
|
aria-label={t('LIBRARY.SEARCH_PLACEHOLDER')}
|
||||||
maxLength={100}
|
maxLength={MAX_LIBRARY_SEARCH_LENGTH}
|
||||||
className="pl-8"
|
className="pl-8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import PageContainer from '../components/PageContainer.jsx';
|
|||||||
import useSessionExpired from '../hooks/useSessionExpired.js';
|
import useSessionExpired from '../hooks/useSessionExpired.js';
|
||||||
import useStartImport from '../hooks/useStartImport.js';
|
import useStartImport from '../hooks/useStartImport.js';
|
||||||
import {hasSession} from '../utils/auth.js';
|
import {hasSession} from '../utils/auth.js';
|
||||||
import {GAME_STATUSES} from '../utils/backlog.js';
|
|
||||||
import {
|
import {
|
||||||
checkUndo, getImport, listImports, MAX_IMPORT_ROWS, parseCsv, parseTextList, retryImport, saveImport, undoImport,
|
checkUndo, getImport, listImports, parseCsv, parseTextList, retryImport, saveImport, undoImport,
|
||||||
} from '../utils/imports.js';
|
} from '../utils/imports.js';
|
||||||
import {hoursToMinutes, MAX_PLATFORM_LENGTH, toHours} from '../utils/manualGames.js';
|
import {GAME_STATUSES, MAX_IMPORT_ROWS, MAX_PLATFORM_LENGTH} from '../utils/limits.js';
|
||||||
|
import {hoursToMinutes, toHours} from '../utils/manualGames.js';
|
||||||
import {loadFavoritePlatform, preselectPlatform} from '../utils/platforms.js';
|
import {loadFavoritePlatform, preselectPlatform} from '../utils/platforms.js';
|
||||||
|
|
||||||
const POLL_MS = 2000;
|
const POLL_MS = 2000;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {Skeleton} from '@/components/ui/skeleton';
|
|||||||
import {cn} from '@/lib/utils';
|
import {cn} from '@/lib/utils';
|
||||||
import GameCover from '../components/GameCover.jsx';
|
import GameCover from '../components/GameCover.jsx';
|
||||||
import GameStatusBadge, {STATUS_META} from '../components/GameStatusBadge.jsx';
|
import GameStatusBadge, {STATUS_META} from '../components/GameStatusBadge.jsx';
|
||||||
import {GAME_STATUSES} from '../utils/backlog.js';
|
import {GAME_STATUSES} from '../utils/limits.js';
|
||||||
import {formatPlaytime} from '../utils/games.js';
|
import {formatPlaytime} from '../utils/games.js';
|
||||||
import {hasSession} from '../utils/auth.js';
|
import {hasSession} from '../utils/auth.js';
|
||||||
import {normalizeProfileStyle} from '../utils/profileStyle.js';
|
import {normalizeProfileStyle} from '../utils/profileStyle.js';
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import FeedbackAlert from '../components/FeedbackAlert.jsx';
|
|||||||
import FormField from '../components/FormField.jsx';
|
import FormField from '../components/FormField.jsx';
|
||||||
import {registrationSchema} from '../utils/validation.js';
|
import {registrationSchema} from '../utils/validation.js';
|
||||||
import {clientFetch} from '../utils/api.js';
|
import {clientFetch} from '../utils/api.js';
|
||||||
|
import {MAX_DISPLAY_NAME_LENGTH} from '../utils/limits.js';
|
||||||
|
|
||||||
const BACKEND_ERROR_KEY = {
|
const BACKEND_ERROR_KEY = {
|
||||||
EMAIL_ALREADY_IN_USE: 'REGISTRATION.ERROR_EMAIL_IN_USE',
|
EMAIL_ALREADY_IN_USE: 'REGISTRATION.ERROR_EMAIL_IN_USE',
|
||||||
@@ -68,7 +69,7 @@ export default function RegistrationPage() {
|
|||||||
? {minutes: Math.ceil(Number(response.headers.get('Retry-After') ?? 900) / 60)}
|
? {minutes: Math.ceil(Number(response.headers.get('Retry-After') ?? 900) / 60)}
|
||||||
: undefined;
|
: undefined;
|
||||||
setFeedback({
|
setFeedback({
|
||||||
message: mappedKey ? t(mappedKey, options) : data.message || t('REGISTRATION.ERROR_GENERIC'),
|
message: t(mappedKey ?? 'REGISTRATION.ERROR_GENERIC', options),
|
||||||
isError: true,
|
isError: true,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -131,7 +132,7 @@ export default function RegistrationPage() {
|
|||||||
<Input
|
<Input
|
||||||
id="displayName"
|
id="displayName"
|
||||||
type="text"
|
type="text"
|
||||||
maxLength={24}
|
maxLength={MAX_DISPLAY_NAME_LENGTH}
|
||||||
aria-invalid={!!errors.displayName}
|
aria-invalid={!!errors.displayName}
|
||||||
{...register('displayName')}
|
{...register('displayName')}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ vi.mock('react-i18next', () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function errorResponse(status, code, headers = {}) {
|
function errorResponse(status, code, headers = {}, message = undefined) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
status,
|
status,
|
||||||
headers: new Headers(headers),
|
headers: new Headers(headers),
|
||||||
json: async () => ({code}),
|
json: async () => ({code, message}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,4 +60,13 @@ describe('RegistrationPage', () => {
|
|||||||
|
|
||||||
expect(await screen.findByText('REGISTRATION.ERROR_INVALID_INVITE_CODE')).toBeTruthy();
|
expect(await screen.findByText('REGISTRATION.ERROR_INVALID_INVITE_CODE')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('shows its own text, not the English one of the server, for a refused input', async () => {
|
||||||
|
globalThis.fetch = vi.fn(async () => errorResponse(400, 'VALIDATION_FAILED', {}, 'The request is not valid'));
|
||||||
|
|
||||||
|
submitValidForm();
|
||||||
|
|
||||||
|
expect(await screen.findByText('REGISTRATION.ERROR_GENERIC')).toBeTruthy();
|
||||||
|
expect(screen.queryByText('The request is not valid')).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -32,6 +32,7 @@ import {apiFetch, refreshSession} from '../utils/api.js';
|
|||||||
import {hasSession, markSession} from '../utils/auth.js';
|
import {hasSession, markSession} from '../utils/auth.js';
|
||||||
import {FAVORITE_PLATFORMS, saveFavoritePlatform} from '../utils/platforms.js';
|
import {FAVORITE_PLATFORMS, saveFavoritePlatform} from '../utils/platforms.js';
|
||||||
import {LANGUAGES, saveSettings} from '../utils/settings.js';
|
import {LANGUAGES, saveSettings} from '../utils/settings.js';
|
||||||
|
import {MAX_DISPLAY_NAME_LENGTH} from '../utils/limits.js';
|
||||||
|
|
||||||
const BACKEND_ERROR_KEY = {
|
const BACKEND_ERROR_KEY = {
|
||||||
DISPLAY_NAME_ALREADY_IN_USE: 'SETTINGS.ERROR_DISPLAY_NAME_IN_USE',
|
DISPLAY_NAME_ALREADY_IN_USE: 'SETTINGS.ERROR_DISPLAY_NAME_IN_USE',
|
||||||
@@ -158,7 +159,7 @@ export default function SettingsPage() {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const data = await response.json().catch(() => ({}));
|
const data = await response.json().catch(() => ({}));
|
||||||
const mappedKey = BACKEND_ERROR_KEY[data.code];
|
const mappedKey = BACKEND_ERROR_KEY[data.code];
|
||||||
toast.error(mappedKey ? t(mappedKey) : data.message || t('SETTINGS.ERROR_SAVE'));
|
toast.error(t(mappedKey ?? 'SETTINGS.ERROR_SAVE'));
|
||||||
return {ok: false};
|
return {ok: false};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,7 +394,7 @@ export default function SettingsPage() {
|
|||||||
<Input
|
<Input
|
||||||
id="displayName"
|
id="displayName"
|
||||||
type="text"
|
type="text"
|
||||||
maxLength={24}
|
maxLength={MAX_DISPLAY_NAME_LENGTH}
|
||||||
placeholder={t('SETTINGS.DISPLAY_NAME_PLACEHOLDER')}
|
placeholder={t('SETTINGS.DISPLAY_NAME_PLACEHOLDER')}
|
||||||
aria-invalid={!!errors.displayName}
|
aria-invalid={!!errors.displayName}
|
||||||
{...register('displayName')}
|
{...register('displayName')}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import {apiFetch} from './api.js';
|
import {apiFetch} from './api.js';
|
||||||
|
import {GAME_STATUSES} from './limits.js';
|
||||||
export const GAME_STATUSES = ['BACKLOG', 'PLAYING', 'FINISHED', 'COMPLETED', 'DROPPED'];
|
|
||||||
export const GAME_PRIORITIES = ['HIGH', 'MEDIUM', 'LOW'];
|
|
||||||
|
|
||||||
// Radix Select has no empty value, so "no priority" needs its own value in the form.
|
// Radix Select has no empty value, so "no priority" needs its own value in the form.
|
||||||
export const NO_PRIORITY = 'NONE';
|
export const NO_PRIORITY = 'NONE';
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import {apiFetch} from './api.js';
|
import {apiFetch} from './api.js';
|
||||||
import {parseStatus} from './backlog.js';
|
import {parseStatus} from './backlog.js';
|
||||||
import {hoursToMinutes, MAX_PLATFORM_LENGTH} from './manualGames.js';
|
import {MAX_IMPORT_TITLE_LENGTH, MAX_PLATFORM_LENGTH, MAX_PLAYTIME_MINUTES} from './limits.js';
|
||||||
|
import {hoursToMinutes} from './manualGames.js';
|
||||||
|
|
||||||
const IMPORTS_URL = () => `${import.meta.env.VITE_CORE_API_URL}/api/library/imports`;
|
const IMPORTS_URL = () => `${import.meta.env.VITE_CORE_API_URL}/api/library/imports`;
|
||||||
const JSON_HEADERS = {'Content-Type': 'application/json'};
|
const JSON_HEADERS = {'Content-Type': 'application/json'};
|
||||||
|
|
||||||
// Core's limits: lines per import, title length of the game search, playtime.
|
|
||||||
export const MAX_IMPORT_ROWS = 1000;
|
|
||||||
const MAX_TITLE = 100;
|
|
||||||
const MAX_MINUTES = 6_000_000;
|
|
||||||
|
|
||||||
// The calls return the raw response; the caller handles the status.
|
// The calls return the raw response; the caller handles the status.
|
||||||
export const createImport = (source, rows) =>
|
export const createImport = (source, rows) =>
|
||||||
apiFetch(IMPORTS_URL(), {method: 'POST', headers: JSON_HEADERS, body: JSON.stringify({source, rows})});
|
apiFetch(IMPORTS_URL(), {method: 'POST', headers: JSON_HEADERS, body: JSON.stringify({source, rows})});
|
||||||
@@ -31,7 +27,7 @@ export const undoImport = (id) => apiFetch(`${IMPORTS_URL()}/${id}/games`, {meth
|
|||||||
* One title per line; empty lines are left out.
|
* One title per line; empty lines are left out.
|
||||||
*/
|
*/
|
||||||
export const parseTextList = (text) => text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
export const parseTextList = (text) => text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)
|
||||||
.map((line) => ({title: line.slice(0, MAX_TITLE)}));
|
.map((line) => ({title: line.slice(0, MAX_IMPORT_TITLE_LENGTH)}));
|
||||||
|
|
||||||
// Header names, lower case, per field.
|
// Header names, lower case, per field.
|
||||||
const COLUMNS = {
|
const COLUMNS = {
|
||||||
@@ -65,9 +61,9 @@ export const parseCsv = (text) => {
|
|||||||
const minutes = at.minutes >= 0 ? Math.round(Number(value(record, 'minutes'))) || 0
|
const minutes = at.minutes >= 0 ? Math.round(Number(value(record, 'minutes'))) || 0
|
||||||
: hoursToMinutes(value(record, 'hours'));
|
: hoursToMinutes(value(record, 'hours'));
|
||||||
return {
|
return {
|
||||||
title: value(record, 'title').slice(0, MAX_TITLE),
|
title: value(record, 'title').slice(0, MAX_IMPORT_TITLE_LENGTH),
|
||||||
platform: platform && platform.length <= MAX_PLATFORM_LENGTH ? platform : null,
|
platform: platform && platform.length <= MAX_PLATFORM_LENGTH ? platform : null,
|
||||||
playtimeMinutes: minutes > 0 ? Math.min(minutes, MAX_MINUTES) : null,
|
playtimeMinutes: minutes > 0 ? Math.min(minutes, MAX_PLAYTIME_MINUTES) : null,
|
||||||
status: parseStatus(value(record, 'status').toUpperCase()),
|
status: parseStatus(value(record, 'status').toUpperCase()),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Limits and enums that Core or Auth check. Each copy names its server source: change both together.
|
||||||
|
|
||||||
|
// Core GameStatus
|
||||||
|
export const GAME_STATUSES = ['BACKLOG', 'PLAYING', 'FINISHED', 'COMPLETED', 'DROPPED'];
|
||||||
|
// Core GamePriority, in the order the UI shows it
|
||||||
|
export const GAME_PRIORITIES = ['HIGH', 'MEDIUM', 'LOW'];
|
||||||
|
|
||||||
|
// Core ManualGameUpdate.MAX_PLATFORM_LENGTH, Auth FavoritePlatformRequest @Size(max = 32)
|
||||||
|
export const MAX_PLATFORM_LENGTH = 32;
|
||||||
|
// Core ManualGameUpdate.MAX_MINUTES
|
||||||
|
export const MAX_PLAYTIME_MINUTES = 6_000_000;
|
||||||
|
// Core BacklogEntryDto.notes @Size(max = 2000)
|
||||||
|
export const MAX_NOTES_LENGTH = 2000;
|
||||||
|
|
||||||
|
// Core ImportRequest.MAX_ROWS
|
||||||
|
export const MAX_IMPORT_ROWS = 1000;
|
||||||
|
// Core ImportRequest.Row.title @Size(max = 100)
|
||||||
|
export const MAX_IMPORT_TITLE_LENGTH = 100;
|
||||||
|
// Core ManualGameController.search @Size(min = 2, max = 100)
|
||||||
|
export const MIN_GAME_SEARCH_LENGTH = 2;
|
||||||
|
export const MAX_GAME_SEARCH_LENGTH = 100;
|
||||||
|
// Frontend only: Core's library search takes any length.
|
||||||
|
export const MAX_LIBRARY_SEARCH_LENGTH = 100;
|
||||||
|
// Core GameCorrectionService.MAX_TEXT
|
||||||
|
export const MAX_CORRECTION_LENGTH = 512;
|
||||||
|
// Core GameCorrectionService.MAX_GENRES
|
||||||
|
export const MAX_CORRECTION_GENRES = 10;
|
||||||
|
|
||||||
|
// Auth DisplayNameFormat.PATTERN (3 to 24 characters)
|
||||||
|
export const MAX_DISPLAY_NAME_LENGTH = 24;
|
||||||
|
// Auth CreateHintRequest @Size(max = 500)
|
||||||
|
export const MAX_HINT_LENGTH = 500;
|
||||||
@@ -7,9 +7,6 @@ const JSON_HEADERS = {'Content-Type': 'application/json'};
|
|||||||
// positive keys; Auth also takes only those as background or banner.
|
// positive keys; Auth also takes only those as background or banner.
|
||||||
export const isSteamKey = (appid) => Number(appid) > 0;
|
export const isSteamKey = (appid) => Number(appid) > 0;
|
||||||
|
|
||||||
// Core's limit of a game's platform (ManualGameUpdate.MAX_PLATFORM_LENGTH).
|
|
||||||
export const MAX_PLATFORM_LENGTH = 32;
|
|
||||||
|
|
||||||
// Two decimals: then hoursToMinutes gives back the same minute.
|
// Two decimals: then hoursToMinutes gives back the same minute.
|
||||||
export const toHours = (minutes) => (minutes > 0 ? String(Math.round((minutes / 60) * 100) / 100) : '');
|
export const toHours = (minutes) => (minutes > 0 ? String(Math.round((minutes / 60) * 100) / 100) : '');
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user