192 lines
5.7 KiB
TypeScript
192 lines
5.7 KiB
TypeScript
import React, { useMemo, useState } from "react";
|
|
import { createRoot } from "react-dom/client";
|
|
import "./support.css";
|
|
|
|
type FormState = {
|
|
name: string;
|
|
email: string;
|
|
message: string;
|
|
};
|
|
|
|
type Status = "idle" | "sending" | "success" | "error";
|
|
|
|
const tips = [
|
|
"Session code or lobby name",
|
|
"What you were trying to do",
|
|
"Anything that looked broken or confusing",
|
|
];
|
|
|
|
const contactNotes = [
|
|
"Messages are routed to the Negopoly support desk.",
|
|
"Add an email if you want a reply.",
|
|
"No account needed. Just send and go.",
|
|
];
|
|
|
|
const MAX_NAME_LENGTH = 80;
|
|
const MAX_EMAIL_LENGTH = 120;
|
|
const MAX_MESSAGE_LENGTH = 2000;
|
|
|
|
function Support() {
|
|
const [form, setForm] = useState<FormState>({
|
|
name: "",
|
|
email: "",
|
|
message: "",
|
|
});
|
|
const [status, setStatus] = useState<Status>("idle");
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const canSubmit = useMemo(() => {
|
|
return (
|
|
status !== "sending" &&
|
|
form.name.trim().length > 0 &&
|
|
form.email.trim().length > 0 &&
|
|
form.message.trim().length > 0
|
|
);
|
|
}, [form, status]);
|
|
|
|
const statusLabel = useMemo(() => {
|
|
if (status === "success") return "Message sent. We'll follow up if you left contact info.";
|
|
if (status === "error") return error ?? "Something went wrong. Try again in a moment.";
|
|
if (status === "sending") return "Sending your message...";
|
|
return "We only use your contact info to reply to this request.";
|
|
}, [status, error]);
|
|
|
|
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
|
event.preventDefault();
|
|
if (!canSubmit) return;
|
|
setStatus("sending");
|
|
setError(null);
|
|
|
|
try {
|
|
const res = await fetch("/api/support", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
name: form.name,
|
|
email: form.email,
|
|
message: form.message,
|
|
}),
|
|
});
|
|
|
|
const payload = await res.json().catch(() => ({}));
|
|
if (!res.ok) {
|
|
throw new Error(payload?.message ?? "Unable to send message.");
|
|
}
|
|
|
|
setStatus("success");
|
|
setForm({ name: "", email: "", message: "" });
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : "Unable to send message.";
|
|
setStatus("error");
|
|
setError(message);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="support">
|
|
<header className="support__hero">
|
|
<div className="support__badge">NegoCity support desk</div>
|
|
<div>
|
|
<a className="support__back" href="/">
|
|
Back to NegoCity
|
|
</a>
|
|
<h1>Support, reports, and lost deals.</h1>
|
|
<p>
|
|
Tell us what went wrong, what you need, or where the Banker needs a hand. We will read
|
|
every message that lands in the support vault.
|
|
</p>
|
|
</div>
|
|
</header>
|
|
|
|
<section className="support__grid">
|
|
<div className="support__info">
|
|
<div className="info-card">
|
|
<h2>What to include</h2>
|
|
<ul>
|
|
{tips.map((tip) => (
|
|
<li key={tip}>{tip}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<div className="info-card accent">
|
|
<h2>How this works</h2>
|
|
<ul>
|
|
{contactNotes.map((note) => (
|
|
<li key={note}>{note}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<div className="info-card link-card">
|
|
<h2>Need the rules instead?</h2>
|
|
<p>Head to the full guide to get back into the game quickly.</p>
|
|
<a className="info-link" href="/rules">
|
|
View the rulebook
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<form className="support__form" onSubmit={handleSubmit}>
|
|
<div className="form-head">
|
|
<h2>Send a message</h2>
|
|
<span className={`status ${status}`} role="status" aria-live="polite">
|
|
{statusLabel}
|
|
</span>
|
|
</div>
|
|
|
|
<label className="field">
|
|
<span>Name</span>
|
|
<input
|
|
type="text"
|
|
name="name"
|
|
autoComplete="name"
|
|
placeholder="Your name"
|
|
maxLength={MAX_NAME_LENGTH}
|
|
value={form.name}
|
|
onChange={(event) => setForm({ ...form, name: event.target.value })}
|
|
required
|
|
/>
|
|
</label>
|
|
|
|
<label className="field">
|
|
<span>Email</span>
|
|
<input
|
|
type="email"
|
|
name="email"
|
|
autoComplete="email"
|
|
placeholder="you@email.com"
|
|
maxLength={MAX_EMAIL_LENGTH}
|
|
value={form.email}
|
|
onChange={(event) => setForm({ ...form, email: event.target.value })}
|
|
required
|
|
/>
|
|
</label>
|
|
|
|
<label className="field">
|
|
<span>Message</span>
|
|
<textarea
|
|
name="message"
|
|
placeholder="Describe what you need, the session code, and anything else we should know."
|
|
rows={6}
|
|
maxLength={MAX_MESSAGE_LENGTH}
|
|
value={form.message}
|
|
onChange={(event) => setForm({ ...form, message: event.target.value })}
|
|
required
|
|
/>
|
|
</label>
|
|
|
|
<div className="form-actions">
|
|
<button type="submit" className="send" disabled={!canSubmit}>
|
|
{status === "sending" ? "Sending..." : "Send message"}
|
|
</button>
|
|
<span className="char-count">
|
|
{form.message.length}/{MAX_MESSAGE_LENGTH}
|
|
</span>
|
|
</div>
|
|
</form>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const root = createRoot(document.getElementById("root")!);
|
|
root.render(<Support />);
|