refactor(limits): server limits in one file, own texts for unknown errors
Build, Test & Push Frontend / quality-check (push) Successful in 19s
Build, Test & Push Frontend / docker (push) Successful in 27s

This commit is contained in:
lobo committed 2026-09-29 14:48:28 +02:00
1 parent f605917874
commit 24a98062ee
18 files changed
+84 -49

No files matched your search

+3 -3
View File
@@ -15,8 +15,8 @@ import PageContainer from '../components/PageContainer.jsx';
import useSessionExpired from '../hooks/useSessionExpired.js';
import {hasSession} from '../utils/auth.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;
// 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">
<div className="space-y-2">
<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 className="space-y-2">
<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>
</div>
<fieldset className="space-y-2">
+3 -2
View File
@@ -6,7 +6,8 @@ import SyncStatusBadge from '../components/SyncStatusBadge.jsx';
import useForceSync from '../hooks/useForceSync.js';
import useSessionExpired from '../hooks/useSessionExpired.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 {Badge} from '@/components/ui/badge';
@@ -269,7 +270,7 @@ export default function GameLibrary() {
onChange={(e) => setSearch(e.target.value)}
placeholder={t('LIBRARY.SEARCH_PLACEHOLDER')}
aria-label={t('LIBRARY.SEARCH_PLACEHOLDER')}
maxLength={100}
maxLength={MAX_LIBRARY_SEARCH_LENGTH}
className="pl-8"
/>
</div>
+3 -3
View File
@@ -15,11 +15,11 @@ import PageContainer from '../components/PageContainer.jsx';
import useSessionExpired from '../hooks/useSessionExpired.js';
import useStartImport from '../hooks/useStartImport.js';
import {hasSession} from '../utils/auth.js';
import {GAME_STATUSES} from '../utils/backlog.js';
import {
checkUndo, getImport, listImports, MAX_IMPORT_ROWS, parseCsv, parseTextList, retryImport, saveImport, undoImport,
checkUndo, getImport, listImports, parseCsv, parseTextList, retryImport, saveImport, undoImport,
} 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';
const POLL_MS = 2000;
+1 -1
View File
@@ -9,7 +9,7 @@ import {Skeleton} from '@/components/ui/skeleton';
import {cn} from '@/lib/utils';
import GameCover from '../components/GameCover.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 {hasSession} from '../utils/auth.js';
import {normalizeProfileStyle} from '../utils/profileStyle.js';
+3 -2
View File
@@ -13,6 +13,7 @@ import FeedbackAlert from '../components/FeedbackAlert.jsx';
import FormField from '../components/FormField.jsx';
import {registrationSchema} from '../utils/validation.js';
import {clientFetch} from '../utils/api.js';
import {MAX_DISPLAY_NAME_LENGTH} from '../utils/limits.js';
const BACKEND_ERROR_KEY = {
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)}
: undefined;
setFeedback({
message: mappedKey ? t(mappedKey, options) : data.message || t('REGISTRATION.ERROR_GENERIC'),
message: t(mappedKey ?? 'REGISTRATION.ERROR_GENERIC', options),
isError: true,
});
} catch (err) {
@@ -131,7 +132,7 @@ export default function RegistrationPage() {
<Input
id="displayName"
type="text"
maxLength={24}
maxLength={MAX_DISPLAY_NAME_LENGTH}
aria-invalid={!!errors.displayName}
{...register('displayName')}
/>
+11 -2
View File
@@ -12,12 +12,12 @@ vi.mock('react-i18next', () => ({
}),
}));
function errorResponse(status, code, headers = {}) {
function errorResponse(status, code, headers = {}, message = undefined) {
return {
ok: false,
status,
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();
});
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();
});
});
+3 -2
View File
@@ -32,6 +32,7 @@ import {apiFetch, refreshSession} from '../utils/api.js';
import {hasSession, markSession} from '../utils/auth.js';
import {FAVORITE_PLATFORMS, saveFavoritePlatform} from '../utils/platforms.js';
import {LANGUAGES, saveSettings} from '../utils/settings.js';
import {MAX_DISPLAY_NAME_LENGTH} from '../utils/limits.js';
const BACKEND_ERROR_KEY = {
DISPLAY_NAME_ALREADY_IN_USE: 'SETTINGS.ERROR_DISPLAY_NAME_IN_USE',
@@ -158,7 +159,7 @@ export default function SettingsPage() {
if (!response.ok) {
const data = await response.json().catch(() => ({}));
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};
}
@@ -393,7 +394,7 @@ export default function SettingsPage() {
<Input
id="displayName"
type="text"
maxLength={24}
maxLength={MAX_DISPLAY_NAME_LENGTH}
placeholder={t('SETTINGS.DISPLAY_NAME_PLACEHOLDER')}
aria-invalid={!!errors.displayName}
{...register('displayName')}