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>
72 lines
2.8 KiB
TypeScript
72 lines
2.8 KiB
TypeScript
"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))}
|
||
/>
|
||
);
|
||
}
|