Add user catch submission and moderation workflow

This commit is contained in:
ik
2026-09-02 20:33:58 +07:00
parent d3a45248ef
commit 6d536d0ade
13 changed files with 227 additions and 8 deletions
+10
View File
@@ -0,0 +1,10 @@
import type { APIRoute } from "astro";
const base = import.meta.env.API_INTERNAL_URL || "http://localhost:8000";
export const POST: APIRoute = async ({ request, redirect }) => {
const form = await request.formData();
const text = (name: string) => String(form.get(name) || "").trim() || null;
const number = (name: string) => text(name) ? Number(text(name)) : null;
const payload = { fish_slug: String(form.get("fish_slug") || ""), waterbody_slug: String(form.get("waterbody_slug") || ""), x: Number(form.get("x")), y: Number(form.get("y")), weight_g: Number(form.get("weight_g")), bait_name: text("bait_name"), fishing_method: text("fishing_method"), retrieve_method: text("retrieve_method"), retrieve_speed: number("retrieve_speed"), player_name: text("player_name"), comment: text("comment"), website: String(form.get("website") || "") };
try { const response = await fetch(`${base}/api/v1/catch-reports`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload) }); return redirect(response.ok ? "/report?state=sent" : "/report?state=error", 303); }
catch { return redirect("/report?state=error", 303); }
};
+16
View File
@@ -0,0 +1,16 @@
---
import Layout from "../layouts/Layout.astro";
import { api, type DictionaryItem } from "../lib/api";
let fishes: DictionaryItem[] = [], waterbodies: DictionaryItem[] = [], unavailable = false;
try { [fishes, waterbodies] = await Promise.all([api<DictionaryItem[]>("/api/v1/fishes"), api<DictionaryItem[]>("/api/v1/waterbodies")]); } catch { unavailable = true; }
const state = Astro.url.searchParams.get("state");
---
<Layout title="Добавить улов — RF4 Spotter">
<section class="form-hero"><span class="eyebrow">Помогите другим игрокам</span><h1>Добавить улов</h1><p>До проверки модератором запись не влияет на публичный индекс клёва.</p></section>
{state === "sent" && <div class="notice success">Улов отправлен на модерацию. Спасибо!</div>}
{state === "error" && <div class="notice error">Не удалось отправить улов. Проверьте поля и повторите.</div>}
{unavailable ? <div class="state"><h2>Форма временно недоступна</h2></div> : <form class="report-form" method="post" action="/api/report">
<div class="form-grid"><label>Рыба *<select name="fish_slug" required>{fishes.map(x => <option value={x.slug}>{x.name_ru}</option>)}</select></label><label>Водоём *<select name="waterbody_slug" required>{waterbodies.map(x => <option value={x.slug}>{x.name_ru}</option>)}</select></label><label>Координата X *<input name="x" type="number" min="-10000" max="10000" required /></label><label>Координата Y *<input name="y" type="number" min="-10000" max="10000" required /></label><label>Вес, граммы *<input name="weight_g" type="number" min="1" max="3000000" required /></label><label>Приманка<input name="bait_name" maxlength="200" /></label><label>Способ ловли<select name="fishing_method"><option value="">Не указан</option><option value="spinning">Спиннинг</option><option value="bottom">Донная</option><option value="float">Поплавочная</option></select></label><label>Проводка<input name="retrieve_method" maxlength="100" /></label><label>Скорость проводки<input name="retrieve_speed" type="number" min="0" max="100" /></label><label>Ник игрока<input name="player_name" maxlength="100" /></label></div>
<label class="wide">Комментарий<textarea name="comment" maxlength="1000" rows="4"></textarea></label><label class="honeypot" aria-hidden="true">Сайт<input name="website" tabindex="-1" autocomplete="off" /></label><p class="privacy">Ник необязателен. Не отправляйте личные данные. Скриншоты будут добавлены после подключения объектного хранилища.</p><button type="submit">Отправить на проверку</button>
</form>}
</Layout>