v1.2 - added react router
This commit is contained in:
parent
c83ff25a36
commit
b0fba52c0e
28 changed files with 4318 additions and 993 deletions
307
src/pages/Feedback.tsx
Normal file
307
src/pages/Feedback.tsx
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue