import React, { useState, useRef, useEffect } from 'react'; import cl from './GameSettingsModal.module.css'; import Icon from './Icon'; /** * Модалка настроек игры (как Game Settings в Roblox Studio). * * Поля: * - Название (обязательное, до 100 символов) * - Описание (до 500 символов) * - Иконка (data URL Base64) — превью + кнопка «Снять кадр сцены» + загрузка PNG/JPG * - Жанр (селект) * - Публикация — toggle is_public * * Props: * open — открыта ли * initial — { title, description, genre, thumbnail, is_public } * onClose — закрыть без сохранения * onSave(data) — сохранить (data — те же поля что initial) * onCaptureScreenshot — функция возвращающая data URL текущей сцены * mode — 'edit' | 'create' — текст заголовка/кнопки чуть отличается */ const GENRES = [ { id: 'platformer', name: 'Платформер' }, { id: 'racing', name: 'Гонки' }, { id: 'shooter', name: 'Шутер' }, { id: 'sandbox', name: 'Песочница' }, { id: 'adventure', name: 'Приключение' }, { id: 'puzzle', name: 'Головоломка' }, { id: 'tycoon', name: 'Тайкун' }, { id: 'rpg', name: 'РПГ' }, { id: 'other', name: 'Другое' }, ]; const MAX_TITLE_LEN = 100; const MAX_DESC_LEN = 500; const MAX_THUMBNAIL_BYTES = 500 * 1024; // 500 КБ — разумный потолок для base64 const GameSettingsModal = ({ open, initial, onClose, onSave, onCaptureScreenshot, mode = 'edit' }) => { const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [genre, setGenre] = useState('other'); const [thumbnail, setThumbnail] = useState(''); const [isPublic, setIsPublic] = useState(false); const [multiplayer, setMultiplayer] = useState(false); const [maxPlayers, setMaxPlayers] = useState(10); const [isTest, setIsTest] = useState(false); // Задача 12: экран загрузки const [loadingLogo, setLoadingLogo] = useState(''); const [loadingAccent, setLoadingAccent] = useState('#ffc020'); const [loadingSpinner, setLoadingSpinner] = useState(true); const [loadingSkip, setLoadingSkip] = useState(false); const [error, setError] = useState(''); const fileInputRef = useRef(null); const logoInputRef = useRef(null); // Заполняем поля ОДИН РАЗ при открытии модала. // Не зависим от `initial` — родитель часто передаёт литерал-объект, // и каждый его новый instance сбрасывал бы поля при ре-рендере. useEffect(() => { if (!open) return; setTitle(initial?.title || ''); setDescription(initial?.description || ''); setGenre(initial?.genre || 'other'); setThumbnail(initial?.thumbnail || ''); setIsPublic(!!initial?.is_public); setMultiplayer(!!initial?.multiplayer); setIsTest(!!initial?.is_test); const ls = initial?.loading_screen || {}; setLoadingLogo(ls.logo || ''); setLoadingAccent(ls.accentColor || '#ffc020'); setLoadingSpinner(ls.defaultSpinner !== false); setLoadingSkip(!!ls.defaultSkipButton); setMaxPlayers( typeof initial?.max_players === 'number' ? Math.max(2, Math.min(50, initial.max_players)) : 10 ); setError(''); // eslint-disable-next-line react-hooks/exhaustive-deps }, [open]); if (!open) return null; const handleScreenshot = () => { if (!onCaptureScreenshot) return; const data = onCaptureScreenshot(); if (data) setThumbnail(data); }; const handleFileSelect = (e) => { const file = e.target.files?.[0]; if (!file) return; if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setError('Только PNG, JPG или WEBP'); return; } if (file.size > MAX_THUMBNAIL_BYTES) { setError('Файл слишком большой (макс. 500 КБ)'); return; } const reader = new FileReader(); reader.onload = (ev) => { setThumbnail(ev.target.result); setError(''); }; reader.readAsDataURL(file); }; const handleLogoSelect = (e) => { const file = e.target.files?.[0]; if (!file) return; if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setError('Логотип: только PNG, JPG или WEBP'); return; } if (file.size > MAX_THUMBNAIL_BYTES) { setError('Логотип слишком большой (макс. 500 КБ)'); return; } const reader = new FileReader(); reader.onload = (ev) => { setLoadingLogo(ev.target.result); setError(''); }; reader.readAsDataURL(file); }; const handleSubmit = (e) => { e.preventDefault(); const trimmedTitle = title.trim(); if (!trimmedTitle) { setError('Название обязательно'); return; } if (trimmedTitle.length > MAX_TITLE_LEN) { setError(`Название не длиннее ${MAX_TITLE_LEN} символов`); return; } if (description.length > MAX_DESC_LEN) { setError(`Описание не длиннее ${MAX_DESC_LEN} символов`); return; } onSave({ title: trimmedTitle, description: description.trim(), genre, thumbnail, is_public: isPublic, multiplayer, max_players: Math.max(2, Math.min(50, Number(maxPlayers) || 10)), is_test: isTest, loading_screen: { logo: loadingLogo || null, accentColor: loadingAccent || '#ffc020', defaultSpinner: loadingSpinner, defaultSkipButton: loadingSkip, }, }); }; return (