Files
hope-events/frontend/src/components/shared/FormFieldExtras.tsx
T
joshuaandClaude Opus 5.5 afcc14cbb3 Add post-event feedback forms
A Feedback step in the event wizard builds a survey that is sent at a
chosen time to everyone who checked in (or everyone registered), with
personal links and one reminder after 3 days. Answers can be anonymous.
Adds rating (1-5 stars) and multiple-choice field types, also usable on
attendee forms, and a Feedback summary report with response rate,
average ratings, choice breakdowns, comments and event comparison.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:28:29 +02:00

72 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import React from "react";
import { Star } from "lucide-react";
/** Field types added for feedback (also usable on attendee forms). */
export const EXTRA_FIELD_TYPES = ["rating", "multiple_choice"] as const;
export const isExtraFieldType = (t: string) => (EXTRA_FIELD_TYPES as readonly string[]).includes(t);
/** The choices of a multiple-choice field (stored as a JSON array of strings). */
export const fieldChoices = (field: { options?: unknown }): string[] =>
Array.isArray(field.options) ? field.options.map(String).filter(Boolean) : [];
/**
* Input for a rating (1–5 stars) or multiple-choice field. Values are strings, like every
* other form answer: "1".."5" for a rating, the chosen option for multiple choice.
*/
export function ExtraFieldInput({ field, value, onChange, idPrefix = "f" }: {
field: { id?: string; type: string; label: string; options?: unknown };
value: string;
onChange: (v: string) => void;
idPrefix?: string;
}) {
if (field.type === "rating") {
const n = Number(value) || 0;
return (
<div className="flex items-center gap-1" role="radiogroup" aria-label={field.label}>
{[1, 2, 3, 4, 5].map(star => (
<button
key={star}
type="button"
role="radio"
aria-checked={n === star}
aria-label={`${star} star${star === 1 ? "" : "s"}`}
onClick={() => onChange(n === star ? "" : String(star))}
className="p-0.5 rounded focus:outline-none focus:ring-2 focus:ring-amber-300"
>
<Star className={`w-7 h-7 ${star <= n ? "fill-amber-400 text-amber-400" : "text-gray-300"}`} />
</button>
))}
{n > 0 && <span className="text-xs text-gray-500 ml-1">{n} of 5</span>}
</div>
);
}
const choices = fieldChoices(field);
return (
<div className="space-y-1" role="radiogroup" aria-label={field.label}>
{choices.map(c => (
<label key={c} className="flex items-center gap-2 text-sm cursor-pointer">
<input type="radio" name={`${idPrefix}-${field.id || field.label}`} checked={value === c} onChange={() => onChange(c)} />
{c}
</label>
))}
</div>
);
}
/** Builder controls for a multiple-choice field's options (one per line). */
export function ChoicesEditor({ options, onChange }: { options: unknown; onChange: (opts: string[]) => void }) {
const text = Array.isArray(options) ? options.join("\n") : "";
return (
<textarea
className="border rounded px-2 py-1 text-xs w-full mt-1"
rows={3}
placeholder={"One choice per line, e.g.\nWorship\nTalks\nFood"}
value={text}
onChange={e => onChange(e.target.value.split("\n"))}
onBlur={e => onChange(e.target.value.split("\n").map(s => s.trim()).filter(Boolean))}
/>
);
}