import {useEffect, useRef, useState} from 'react'; import {useTranslation} from 'react-i18next'; import {Loader2, RotateCcw, Save} from 'lucide-react'; import {toast} from 'sonner'; import {Badge} from '@/components/ui/badge'; import {Button} from '@/components/ui/button'; import {Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle} from '@/components/ui/dialog'; import {Input} from '@/components/ui/input'; import {Label} from '@/components/ui/label'; import {CORRECTABLE_MOODS, IGDB_GENRES, resetCorrection, saveCorrection} from '../utils/corrections.js'; import {genreKey} from '../utils/stats.js'; import {MAX_CORRECTION_GENRES, MAX_CORRECTION_LENGTH} from '../utils/limits.js'; const REFUSAL_KEY = {INVALID_CORRECTION: 'CORRECTION.INVALID'}; const ENDLESS_CHOICES = [[null, 'UNKNOWN'], [true, 'YES'], [false, 'NO']]; const initialValues = (game) => ({ name: game.name ?? '', image: game.imageUrl ?? '', speedrun: game.speedrunUrl ?? '', moods: game.moods ?? [], endless: game.endless ?? null, genres: game.genres ?? [], }); // An empty text field means "none": the Steam header, or no speedrun. const toValue = (value) => (typeof value === 'string' ? value.trim() || null : value); const toggled = (list, item) => (list.includes(item) ? list.filter((each) => each !== item) : [...list, item]); /** * AUDITOR or higher: corrects the shared data of one game for every user. Each field saves on its own; * onChanged loads the game again. */ export default function GameCorrectionDialog({game, open, onOpenChange, onChanged}) { const {t} = useTranslation(); const [values, setValues] = useState(() => initialValues(game)); const [busy, setBusy] = useState(null); // The field whose save or reset the next game from GameDetail answers; the other fields keep their edits. const sentField = useRef(null); useEffect(() => { if (open) setValues(initialValues(game)); // Only when the dialog opens; a reload after a save is handled below. // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); useEffect(() => { const field = sentField.current; if (field) { sentField.current = null; setValues((v) => ({...v, [field]: initialValues(game)[field]})); } }, [game]); const corrected = new Set((game.corrections ?? []).map((c) => c.field.toLowerCase())); const set = (field, value) => setValues((v) => ({...v, [field]: value})); const send = async (field, request) => { setBusy(field); try { const response = await request(); if (response.status === 401) return; if (response.ok) { toast.success(t('CORRECTION.SAVED')); sentField.current = field; onChanged?.(); return; } const data = await response.json().catch(() => ({})); // Core's message is English and for developers, so it goes to the console only. if (data.message) console.warn('Correction refused:', data.code, data.message); toast.error(t(REFUSAL_KEY[data.code] ?? (response.status === 403 ? 'CORRECTION.NO_ACCESS' : 'CORRECTION.ERROR'))); } catch (err) { console.error('Correction error:', err); toast.error(t('INFO.ERROR.NETWORK_FAILURE')); } finally { setBusy(null); } }; const row = (field, input) => (