307 lines
10 KiB
TypeScript
307 lines
10 KiB
TypeScript
import { useState } from "react";
|
|
import PageShell from "../components/PageShell.jsx";
|
|
|
|
/* ═══════════════════════════════════════════════════════════════
|
|
Feedback page.
|
|
|
|
Section ids match the nav hashes. Right now there's one:
|
|
#website. Add more entries to SECTIONS (bottom of file) as the
|
|
page grows — program feedback, retreat surveys, etc.
|
|
|
|
Nothing is persisted. handleSubmit just flips to the thank-you
|
|
panel; the POST goes where the comment marks it.
|
|
═══════════════════════════════════════════════════════════════ */
|
|
|
|
const ACCENT = "#138ba0";
|
|
const MUTED = "#4a6b72";
|
|
const MAX_CHARS = 1500;
|
|
|
|
// Selectable feedback types. Add or reword freely — the grid reflows.
|
|
const FEEDBACK_TYPES = [
|
|
{
|
|
id: "broken",
|
|
label: "Something's broken",
|
|
hint: "A link, image, or button that doesn't work",
|
|
},
|
|
{
|
|
id: "confusing",
|
|
label: "Hard to use",
|
|
hint: "Something you couldn't find or follow",
|
|
},
|
|
{
|
|
id: "outdated",
|
|
label: "Wrong or missing info",
|
|
hint: "Old dates, typos, an event that isn't listed",
|
|
},
|
|
{
|
|
id: "request",
|
|
label: "Feature request",
|
|
hint: "Something you'd like the site to do",
|
|
},
|
|
{
|
|
id: "praise",
|
|
label: "Kind words",
|
|
hint: "Tell us what's working well",
|
|
},
|
|
{
|
|
id: "other",
|
|
label: "Something else",
|
|
hint: "Anything that doesn't fit the boxes above",
|
|
},
|
|
];
|
|
|
|
/* ── Shared field chrome ─────────────────────────────────────── */
|
|
|
|
const fieldClass =
|
|
"w-full rounded-xl border border-[#4a6b72]/25 bg-white px-4 py-3 text-[#26454c] " +
|
|
"placeholder:text-[#4a6b72]/50 outline-none transition-colors " +
|
|
"focus:border-[#138ba0] focus:ring-2 focus:ring-[#138ba0]/25";
|
|
|
|
function OptionalTag() {
|
|
return (
|
|
<span className="ml-2 rounded-full bg-[#eef9fb] px-2 py-0.5 text-xs font-medium text-[#138ba0]">
|
|
Optional
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/* ── Type picker ─────────────────────────────────────────────── */
|
|
|
|
function TypePicker({ value, onChange }) {
|
|
return (
|
|
<fieldset>
|
|
<legend className="text-base font-semibold text-[#26454c]">
|
|
What kind of feedback is this?
|
|
</legend>
|
|
<p className="mt-1 text-sm" style={{ color: MUTED }}>
|
|
Pick the closest fit. It helps us route it to the right person.
|
|
</p>
|
|
|
|
<div className="mt-4 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
{FEEDBACK_TYPES.map((type) => {
|
|
const selected = value === type.id;
|
|
return (
|
|
<label key={type.id} className="cursor-pointer">
|
|
<input
|
|
type="radio"
|
|
name="feedbackType"
|
|
value={type.id}
|
|
checked={selected}
|
|
onChange={() => onChange(type.id)}
|
|
className="peer sr-only"
|
|
/>
|
|
<span
|
|
className={
|
|
"flex h-full flex-col gap-1 rounded-xl border p-4 transition-colors " +
|
|
"peer-focus-visible:ring-2 peer-focus-visible:ring-[#138ba0]/40 " +
|
|
(selected
|
|
? "border-[#138ba0] bg-[#eef9fb]"
|
|
: "border-[#4a6b72]/20 bg-white hover:border-[#138ba0]/50")
|
|
}
|
|
>
|
|
<span className="flex items-start justify-between gap-2">
|
|
<span className="font-semibold text-[#26454c]">
|
|
{type.label}
|
|
</span>
|
|
<span
|
|
aria-hidden="true"
|
|
className={
|
|
"mt-0.5 h-4 w-4 shrink-0 rounded-full border-2 transition-colors " +
|
|
(selected
|
|
? "border-[#138ba0] bg-[#138ba0] ring-2 ring-inset ring-white"
|
|
: "border-[#4a6b72]/30")
|
|
}
|
|
/>
|
|
</span>
|
|
<span className="text-sm leading-snug" style={{ color: MUTED }}>
|
|
{type.hint}
|
|
</span>
|
|
</span>
|
|
</label>
|
|
);
|
|
})}
|
|
</div>
|
|
</fieldset>
|
|
);
|
|
}
|
|
|
|
/* ── The form ────────────────────────────────────────────────── */
|
|
|
|
function WebsiteFeedbackForm() {
|
|
const [type, setType] = useState(null);
|
|
const [message, setMessage] = useState("");
|
|
const [name, setName] = useState("");
|
|
const [email, setEmail] = useState("");
|
|
const [sent, setSent] = useState(false);
|
|
|
|
const ready = Boolean(type) && message.trim().length > 0;
|
|
|
|
function handleSubmit(event) {
|
|
event.preventDefault();
|
|
if (!ready) return;
|
|
// TODO: POST { type, message, name, email } somewhere.
|
|
setSent(true);
|
|
}
|
|
|
|
function reset() {
|
|
setType(null);
|
|
setMessage("");
|
|
setName("");
|
|
setEmail("");
|
|
setSent(false);
|
|
}
|
|
|
|
if (sent) {
|
|
return (
|
|
<div
|
|
aria-live="polite"
|
|
className="rounded-2xl border border-[#138ba0]/20 bg-white p-8 sm:p-10"
|
|
>
|
|
<h3 className="text-2xl font-bold" style={{ color: ACCENT }}>
|
|
Thanks — we've got it
|
|
</h3>
|
|
<p className="mt-3 max-w-prose" style={{ color: MUTED }}>
|
|
{email
|
|
? `We'll follow up at ${email} if we have questions.`
|
|
: "You sent this anonymously, so we won't be able to reply — but we read everything that comes in."}
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={reset}
|
|
className="mt-6 rounded-full border border-[#138ba0] px-5 py-2.5 font-semibold text-[#138ba0] transition-colors hover:bg-[#eef9fb] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#138ba0]/40"
|
|
>
|
|
Send more feedback
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<form
|
|
onSubmit={handleSubmit}
|
|
noValidate
|
|
className="rounded-2xl border border-[#138ba0]/20 bg-white p-6 sm:p-8"
|
|
>
|
|
<TypePicker value={type} onChange={setType} />
|
|
|
|
{/* Message */}
|
|
<div className="mt-10">
|
|
<label
|
|
htmlFor="feedback-message"
|
|
className="text-base font-semibold text-[#26454c]"
|
|
>
|
|
Tell us more
|
|
</label>
|
|
<p className="mt-1 text-sm" style={{ color: MUTED }}>
|
|
The page you were on and what you expected to happen are the two most
|
|
useful things you can give us.
|
|
</p>
|
|
<textarea
|
|
id="feedback-message"
|
|
rows={7}
|
|
maxLength={MAX_CHARS}
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
placeholder="I was looking for the summer retreat dates on the Events page and…"
|
|
className={`mt-4 resize-y ${fieldClass}`}
|
|
/>
|
|
<div className="mt-2 text-right text-xs tabular-nums" style={{ color: MUTED }}>
|
|
{message.length} / {MAX_CHARS}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Optional contact details */}
|
|
<div className="mt-8 rounded-xl border border-dashed border-[#4a6b72]/30 bg-[#eef9fb]/60 p-5 sm:p-6">
|
|
<div className="flex flex-wrap items-center">
|
|
<h3 className="text-base font-semibold text-[#26454c]">
|
|
Your details
|
|
</h3>
|
|
<OptionalTag />
|
|
</div>
|
|
<p className="mt-1 max-w-prose text-sm" style={{ color: MUTED }}>
|
|
Leave these blank and your feedback comes through anonymously. Fill
|
|
them in only if you'd like a reply — we won't add you to any list.
|
|
</p>
|
|
|
|
<div className="mt-5 grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<label
|
|
htmlFor="feedback-name"
|
|
className="block text-sm font-medium text-[#26454c]"
|
|
>
|
|
Name <span className="font-normal text-[#4a6b72]">(optional)</span>
|
|
</label>
|
|
<input
|
|
id="feedback-name"
|
|
type="text"
|
|
autoComplete="name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
className={`mt-2 ${fieldClass}`}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label
|
|
htmlFor="feedback-email"
|
|
className="block text-sm font-medium text-[#26454c]"
|
|
>
|
|
Email <span className="font-normal text-[#4a6b72]">(optional)</span>
|
|
</label>
|
|
<input
|
|
id="feedback-email"
|
|
type="email"
|
|
autoComplete="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
className={`mt-2 ${fieldClass}`}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Submit */}
|
|
<div className="mt-8 flex flex-wrap items-center gap-4">
|
|
<button
|
|
type="submit"
|
|
disabled={!ready}
|
|
className="rounded-full bg-[#138ba0] px-7 py-3 font-semibold text-white transition-colors hover:bg-[#0f7183] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#138ba0]/40 disabled:cursor-not-allowed disabled:bg-[#4a6b72]/25 disabled:text-white/80"
|
|
>
|
|
Send feedback
|
|
</button>
|
|
{!ready && (
|
|
<p className="text-sm" style={{ color: MUTED }}>
|
|
Choose a type and write a note to send.
|
|
</p>
|
|
)}
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|
|
|
|
/* ── Page ────────────────────────────────────────────────────── */
|
|
|
|
const SECTIONS = [
|
|
{
|
|
id: "website",
|
|
title: "Website Feedback and Requests",
|
|
blurb:
|
|
"Found a broken link, spotted something out of date, or want the site to do something it doesn't? This form goes straight to the people who maintain it.",
|
|
accent: ACCENT,
|
|
background: "#eef9fb",
|
|
content: (
|
|
<div className="mx-auto max-w-3xl px-6">
|
|
<WebsiteFeedbackForm />
|
|
</div>
|
|
),
|
|
},
|
|
];
|
|
|
|
export default function FeedbackPage() {
|
|
return (
|
|
<PageShell
|
|
title="Feedback"
|
|
intro="Tell us what's working, what isn't, and what you'd like to see next."
|
|
sections={SECTIONS}
|
|
/>
|
|
);
|
|
}
|