feat: footer, auto-pause on no viewers, and Redsys question submissions
- 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>
This commit is contained in:
145
pregunta.tsx
Normal file
145
pregunta.tsx
Normal file
@@ -0,0 +1,145 @@
|
||||
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 />);
|
||||
Reference in New Issue
Block a user