Files
pgames-manager/frontend/src/pages/NewGame.tsx
T
mrleo1nid c8d83b3fe7 feat: add mod editing, revision merges and secure game storage
Add immutable revisions, PGVN editor, asset replacement, merge and rebase workflows, authenticated media delivery and reachability GC. Harden the script interpreter, pin unrpyc and remove tracked build metadata. Include current compatibility checks, Naninovel import and regression tests.
2026-10-25 04:42:48 +03:00

104 lines
4.0 KiB
TypeScript

import { useState, type DragEvent, type FormEvent } from "react";
import { useNavigate } from "react-router-dom";
import { formatSize } from "../api";
import { UploadProgress, useUploads } from "../uploads";
import { ErrorNote, PageHeader } from "../ui/Bits";
import { F95Fetch } from "../ui/F95Fetch";
import { Icon } from "../ui/Icon";
const ARCHIVE = /\.(zip|7z|rar|tar|gz|tgz|bz2|xz)$/i;
export function NewGame() {
const navigate = useNavigate();
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [tags, setTags] = useState("");
const [file, setFile] = useState<File | null>(null);
const [over, setOver] = useState(false);
const { start } = useUploads();
const [error, setError] = useState("");
const pick = (f: File | null | undefined) => {
if (!f) return;
if (!ARCHIVE.test(f.name)) {
setError("Нужен архив: zip, 7z, rar или tar");
return;
}
setError("");
setFile(f);
};
const onDrop = (e: DragEvent) => {
e.preventDefault();
setOver(false);
pick(e.dataTransfer.files[0]);
};
const submit = (e: FormEvent) => {
e.preventDefault();
if (!file) return;
setError("");
start({ file, title, description, tags: tags.split(",").map((t) => t.trim()).filter(Boolean) });
setFile(null);
setTitle("");
setDescription("");
setTags("");
};
return (
<div className="page medium">
<PageHeader title="Новая игра"
subtitle="Загрузите архив игры. Сервер определит движок и проверит сценарии. Неподдерживаемые игры будут отклонены с объяснением причины." />
<UploadProgress />
<form className="card form" onSubmit={submit}>
<label className={`dropzone ${over ? "over" : ""} ${file ? "has-file" : ""}`}
onDragOver={(e) => { e.preventDefault(); setOver(true); }} onDragLeave={() => setOver(false)} onDrop={onDrop}>
<input type="file" accept=".zip,.7z,.rar,.tar,.gz,.tgz,.bz2,.xz"
key={file ? "selected" : "empty"}
onChange={(e) => pick(e.target.files?.[0])} />
<Icon name={file ? "file" : "upload"} size={34} className="dz-icon" />
{file ? (
<>
<b>{file.name}</b>
<span className="muted">{formatSize(file.size)} · нажмите, чтобы выбрать другой</span>
</>
) : (
<>
<b>Перетащите архив сюда или нажмите, чтобы выбрать</b>
<span className="muted small">zip, 7z, rar, tar</span>
</>
)}
</label>
<F95Fetch onLoaded={(m) => {
setTitle(m.title);
setDescription(m.description);
setTags(m.tags.join(", "));
}} />
<div className="form-row">
<label className="field">
<span>Название</span>
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="По имени архива" />
</label>
<label className="field">
<span>Теги</span>
<input value={tags} onChange={(e) => setTags(e.target.value)} placeholder="романтика, драма" />
<span className="hint">Через запятую</span>
</label>
</div>
<label className="field">
<span>Описание</span>
<textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={4} />
</label>
{error && <ErrorNote>{error}</ErrorNote>}
<div className="actions">
<button className="btn btn-primary btn-lg" disabled={!file}>
<Icon name="upload" size={20} />Загрузить
</button>
<button type="button" className="btn btn-ghost" onClick={() => navigate(-1)}>Отмена</button>
</div>
</form>
</div>
);
}