Pull title, description and tags from an F95zone thread

Admin/uploader pastes a thread link and presses "Подтянуть" on the new
game and edit forms; the backend reads the public first post (no login).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
mrleo1nidandClaude Sonnet 5.5 committed 2026-10-25 03:57:28 +03:00
1 parent ac164d1782
commit ae8e175376
5 files changed
+227

No files matched your search

+6
View File
@@ -4,6 +4,7 @@ import { api, formatSize, uploadRaw, type GameDetail, type Version } from "../ap
import { useAuth } from "../auth";
import { getRecent, pushRecent } from "../recent";
import { Cover, ErrorNote, Spinner } from "../ui/Bits";
import { F95Fetch } from "../ui/F95Fetch";
import { Icon } from "../ui/Icon";
import { useUi } from "../ui/Ui";
@@ -358,6 +359,11 @@ function GameEditor({ game, onDone }: { game: GameDetail; onDone: (changed: bool
};
return (
<form className="editor" onSubmit={(e) => { e.preventDefault(); void save(); }}>
<F95Fetch onLoaded={(m) => {
setTitle(m.title);
setDescription(m.description);
setTags(m.tags.join(", "));
}} />
<label className="field"><span>Название</span><input value={title} onChange={(e) => setTitle(e.target.value)} /></label>
<label className="field"><span>Описание</span><textarea value={description} onChange={(e) => setDescription(e.target.value)} rows={5} /></label>
<label className="field"><span>Теги</span><input value={tags} onChange={(e) => setTags(e.target.value)} placeholder="через запятую: романтика, драма" /></label>
+6
View File
@@ -2,6 +2,7 @@ import { useState, type DragEvent, type FormEvent } from "react";
import { useNavigate } from "react-router-dom";
import { api, formatSize, uploadRaw, type Game } from "../api";
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;
@@ -80,6 +81,11 @@ export function NewGame() {
)}
</label>
<F95Fetch onLoaded={(m) => {
setTitle(m.title);
setDescription(m.description);
setTags(m.tags.join(", "));
}} />
<div className="form-row">
<label className="field">
<span>Название</span>
+44
View File
@@ -0,0 +1,44 @@
import { useState } from "react";
import { api } from "../api";
export interface F95Meta {
url: string;
title: string;
description: string;
tags: string[];
}
/** Link to an F95zone thread + a button that pulls the title, description and tags from it. */
export function F95Fetch({ onLoaded }: { onLoaded: (m: F95Meta) => void }) {
const [url, setUrl] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState("");
const load = async () => {
setBusy(true);
setError("");
try {
onLoaded(await api.post<F95Meta>("/api/metadata/f95", { url }));
} catch (e) {
setError((e as Error).message);
} finally {
setBusy(false);
}
};
return (
<div className="field">
<span>Ссылка на тему F95zone</span>
<div style={{ display: "flex", gap: "0.5rem" }}>
<input value={url} onChange={(e) => setUrl(e.target.value)} style={{ flex: 1 }}
placeholder="https://f95zone.to/threads/…"
onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); if (url.trim() && !busy) void load(); } }} />
<button type="button" className="btn" disabled={!url.trim() || busy} onClick={() => void load()}>
{busy ? "Загрузка…" : "Подтянуть"}
</button>
</div>
<span className="hint">Заполнит название, описание и теги. Результат можно поправить перед сохранением.</span>
{error && <span className="hint" style={{ color: "var(--danger, #e5484d)" }}>{error}</span>}
</div>
);
}