- Footer: add site-footer with one-liner + Cloud Host branding linking to cloudhost.es (mobile only, hidden on desktop) - Auto-pause: game pauses automatically after AUTOPAUSE_DELAY_MS (default 60s) with no viewers connected; auto-resumes when first viewer connects; shows "Esperando espectadores…" in the UI - Redsys: new /pregunta page lets viewers pay 1€ via Redsys to submit a fill-in question; submitted questions are used as prompts in the next round instead of AI generation; new redsys.ts implements HMAC_SHA256_V1 signing (3DES key derivation + HMAC-SHA256); notification endpoint at /api/redsys/notificacion handles server-to-server confirmation - db.ts: questions table with createPendingQuestion / markQuestionPaid / getNextPendingQuestion / markQuestionUsed - .env.sample: added AUTOPAUSE_DELAY_MS, PUBLIC_URL, REDSYS_* vars Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
146 lines
4.5 KiB
TypeScript
146 lines
4.5 KiB
TypeScript
import React, { useState } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import "./pregunta.css";
|
|
|
|
function App() {
|
|
const params = new URLSearchParams(window.location.search);
|
|
const isOk = params.get("ok") === "1";
|
|
const isKo = params.get("ko") === "1";
|
|
|
|
const [text, setText] = useState("");
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
if (isOk) {
|
|
return (
|
|
<div className="pregunta">
|
|
<div className="pregunta__panel">
|
|
<a href="/" className="pregunta__logo">
|
|
<img src="/assets/logo.svg" alt="argument.es" />
|
|
</a>
|
|
<h1>¡Pregunta enviada!</h1>
|
|
<p className="pregunta__sub">
|
|
Tu pregunta se usará en el próximo sorteo entre las IAs. ¡Gracias por participar!
|
|
</p>
|
|
<a href="/" className="pregunta__btn">Volver al juego</a>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isKo) {
|
|
return (
|
|
<div className="pregunta">
|
|
<div className="pregunta__panel">
|
|
<a href="/" className="pregunta__logo">
|
|
<img src="/assets/logo.svg" alt="argument.es" />
|
|
</a>
|
|
<h1>Pago cancelado</h1>
|
|
<p className="pregunta__sub">
|
|
El pago no se completó. Tu pregunta no ha sido guardada.
|
|
</p>
|
|
<a href="/pregunta" className="pregunta__btn">Intentar de nuevo</a>
|
|
<div className="pregunta__links">
|
|
<a href="/">Volver al juego</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault();
|
|
setError(null);
|
|
setSubmitting(true);
|
|
try {
|
|
const res = await fetch("/api/pregunta/iniciar", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ text: text.trim() }),
|
|
});
|
|
if (!res.ok) {
|
|
const msg = await res.text();
|
|
throw new Error(msg || `Error ${res.status}`);
|
|
}
|
|
const data = (await res.json()) as {
|
|
tpvUrl: string;
|
|
merchantParams: string;
|
|
signature: string;
|
|
signatureVersion: string;
|
|
};
|
|
|
|
// Build and auto-submit the Redsys payment form
|
|
const form = document.createElement("form");
|
|
form.method = "POST";
|
|
form.action = data.tpvUrl;
|
|
for (const [name, value] of Object.entries({
|
|
Ds_SignatureVersion: data.signatureVersion,
|
|
Ds_MerchantParameters: data.merchantParams,
|
|
Ds_Signature: data.signature,
|
|
})) {
|
|
const input = document.createElement("input");
|
|
input.type = "hidden";
|
|
input.name = name;
|
|
input.value = value;
|
|
form.appendChild(input);
|
|
}
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : "Error al procesar la solicitud");
|
|
setSubmitting(false);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="pregunta">
|
|
<div className="pregunta__panel">
|
|
<a href="/" className="pregunta__logo">
|
|
<img src="/assets/logo.svg" alt="argument.es" />
|
|
</a>
|
|
<h1>Propón una pregunta</h1>
|
|
<p className="pregunta__sub">
|
|
Paga 1€ y tu pregunta de completar-la-frase se usará en el próximo sorteo entre las IAs.
|
|
</p>
|
|
|
|
<form onSubmit={handleSubmit} className="pregunta__form">
|
|
<label htmlFor="pregunta-text" className="pregunta__label">
|
|
Tu pregunta (frase de completar)
|
|
</label>
|
|
<textarea
|
|
id="pregunta-text"
|
|
className="pregunta__textarea"
|
|
value={text}
|
|
onChange={(e) => setText(e.target.value)}
|
|
placeholder='Ejemplo: "Lo que más vergüenza da hacer en ___"'
|
|
maxLength={200}
|
|
required
|
|
rows={3}
|
|
autoFocus
|
|
/>
|
|
<div className="pregunta__hint">
|
|
{text.length}/200 caracteres · mínimo 10
|
|
</div>
|
|
|
|
{error && <div className="pregunta__error">{error}</div>}
|
|
|
|
<button
|
|
type="submit"
|
|
className="pregunta__submit"
|
|
disabled={submitting || text.trim().length < 10}
|
|
>
|
|
{submitting ? "Redirigiendo a pago…" : "Pagar 1€ y enviar"}
|
|
</button>
|
|
</form>
|
|
|
|
<div className="pregunta__links">
|
|
<a href="/">Volver al juego</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const root = createRoot(document.getElementById("root")!);
|
|
root.render(<App />);
|