Download src/components/support/SupportForm.tsx from asnannp/DocDoeAI: direct link, hf CLI and curl.
- Browser
- Download file 8.63 kB
-
https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/support/SupportForm.tsx
- Command line
-
hf download hf://spaces/asnannp/DocDoeAI/src/components/support/SupportForm.tsx
-
curl -L -o SupportForm.tsx https://huggingface.co/spaces/asnannp/DocDoeAI/resolve/main/src/components/support/SupportForm.tsx
8.63 kB
| "use client"; | |
| import { useState } from "react"; | |
| import Link from "next/link"; | |
| import { ArrowLeft, Check, LoaderCircle, RotateCcw } from "lucide-react"; | |
| import { submitSupport, type SupportKind } from "@/lib/api/support"; | |
| import { isApiError } from "@/lib/api/client"; | |
| import { cn } from "@/lib/utils"; | |
| type SupportFormProps = { | |
| kind: SupportKind; | |
| eyebrow: string; | |
| title: string; | |
| intro: string; | |
| categories?: string[]; | |
| showRating?: boolean; | |
| showTitleField?: boolean; | |
| messageLabel: string; | |
| messagePlaceholder: string; | |
| }; | |
| type Phase = "idle" | "submitting" | "done" | "error"; | |
| const inputClass = | |
| "w-full rounded-xl border border-white/12 bg-white/[0.03] px-3.5 py-3 text-[15px] text-white placeholder:text-white/30 outline-none transition focus:border-white/40"; | |
| const labelClass = "block text-xs font-bold uppercase tracking-[0.14em] text-white/45"; | |
| export function SupportForm({ | |
| kind, | |
| eyebrow, | |
| title, | |
| intro, | |
| categories, | |
| showRating = false, | |
| showTitleField = false, | |
| messageLabel, | |
| messagePlaceholder, | |
| }: SupportFormProps) { | |
| const [category, setCategory] = useState<string>(categories?.[0] ?? ""); | |
| const [titleValue, setTitleValue] = useState(""); | |
| const [message, setMessage] = useState(""); | |
| const [rating, setRating] = useState<number | null>(null); | |
| const [phase, setPhase] = useState<Phase>("idle"); | |
| const [error, setError] = useState<string | null>(null); | |
| const canSubmit = message.trim().length > 0 && phase !== "submitting"; | |
| async function handleSubmit() { | |
| if (!canSubmit) return; | |
| setPhase("submitting"); | |
| setError(null); | |
| try { | |
| await submitSupport({ | |
| kind, | |
| message: message.trim(), | |
| category: category || null, | |
| title: showTitleField ? titleValue.trim() || null : null, | |
| rating: showRating ? rating : null, | |
| page_url: typeof window !== "undefined" ? window.location.href : null, | |
| }); | |
| setPhase("done"); | |
| } catch (err) { | |
| // Never expose an internal payload — a plain, retryable message only. | |
| const status = isApiError(err) ? err.status : null; | |
| if (status === 429) { | |
| setError("You've sent a few messages in a short time. Please wait a minute and try again — your text is still here."); | |
| } else if (status === 422) { | |
| setError("Please add a bit more detail to your message before sending."); | |
| } else { | |
| setError("We could not send that just now. Check your connection and try again — your text is still here."); | |
| } | |
| setPhase("error"); | |
| } | |
| } | |
| if (phase === "done") { | |
| return ( | |
| <div className="mx-auto w-full max-w-xl rounded-2xl border border-white/12 bg-white/[0.03] p-8 text-center"> | |
| <span className="mx-auto flex size-12 items-center justify-center rounded-full bg-emerald-500/15 text-emerald-300"> | |
| <Check className="size-6" /> | |
| </span> | |
| <h2 className="mt-5 text-xl font-bold text-white">Thank you — we received it</h2> | |
| <p className="mt-2 text-sm leading-6 text-white/55"> | |
| {kind === "bug" | |
| ? "Your report is saved. The team reviews bug reports during the beta and will act on the clear ones first." | |
| : "Your feedback is saved. Real student feedback is what shapes what DocDoe builds next."} | |
| </p> | |
| <div className="mt-6 flex flex-wrap justify-center gap-3"> | |
| <button | |
| type="button" | |
| onClick={() => { | |
| setMessage(""); | |
| setTitleValue(""); | |
| setRating(null); | |
| setPhase("idle"); | |
| }} | |
| className="inline-flex h-11 items-center gap-2 rounded-xl border border-white/15 px-4 text-sm font-semibold text-white/75 transition hover:text-white" | |
| > | |
| Send another | |
| </button> | |
| <Link | |
| href="/home" | |
| className="inline-flex h-11 items-center gap-2 rounded-xl bg-white px-5 text-sm font-bold text-black transition hover:bg-white/90" | |
| > | |
| Back to DocDoe | |
| </Link> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="mx-auto w-full max-w-xl"> | |
| <Link | |
| href="/" | |
| className="inline-flex items-center gap-2 text-sm font-medium text-white/45 transition hover:text-white" | |
| > | |
| <ArrowLeft className="size-4" /> DocDoe | |
| </Link> | |
| <p className="mt-6 text-[11px] font-bold uppercase tracking-[0.2em] text-white/40">{eyebrow}</p> | |
| <h1 className="mt-3 text-3xl font-semibold tracking-tight text-white sm:text-4xl">{title}</h1> | |
| <p className="mt-4 text-[15px] leading-7 text-white/60">{intro}</p> | |
| <form | |
| className="mt-8 space-y-5" | |
| onSubmit={(event) => { | |
| event.preventDefault(); | |
| void handleSubmit(); | |
| }} | |
| > | |
| {categories && categories.length > 0 ? ( | |
| <div> | |
| <span className={labelClass}>Category</span> | |
| <div className="mt-2 flex flex-wrap gap-2"> | |
| {categories.map((item) => ( | |
| <button | |
| key={item} | |
| type="button" | |
| onClick={() => setCategory(item)} | |
| aria-pressed={category === item} | |
| className={cn( | |
| "rounded-lg border px-3 py-2 text-sm font-semibold transition", | |
| category === item | |
| ? "border-white bg-white text-black" | |
| : "border-white/12 text-white/60 hover:border-white/30 hover:text-white", | |
| )} | |
| > | |
| {item} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ) : null} | |
| {showTitleField ? ( | |
| <label htmlFor="support-title" className="block"> | |
| <span className={labelClass}>Title (optional)</span> | |
| <input | |
| id="support-title" | |
| value={titleValue} | |
| onChange={(event) => setTitleValue(event.target.value)} | |
| placeholder="Short summary" | |
| maxLength={200} | |
| className={cn(inputClass, "mt-2")} | |
| /> | |
| </label> | |
| ) : null} | |
| {showRating ? ( | |
| <div> | |
| <span className={labelClass}>Rating (optional)</span> | |
| <div className="mt-2 flex gap-2"> | |
| {[1, 2, 3, 4, 5].map((value) => ( | |
| <button | |
| key={value} | |
| type="button" | |
| onClick={() => setRating(rating === value ? null : value)} | |
| aria-label={`${value} out of 5`} | |
| aria-pressed={rating === value} | |
| className={cn( | |
| "size-10 rounded-lg border text-sm font-bold transition", | |
| rating !== null && value <= rating | |
| ? "border-white bg-white text-black" | |
| : "border-white/12 text-white/50 hover:border-white/30 hover:text-white", | |
| )} | |
| > | |
| {value} | |
| </button> | |
| ))} | |
| </div> | |
| </div> | |
| ) : null} | |
| <label htmlFor="support-message" className="block"> | |
| <span className={labelClass}>{messageLabel}</span> | |
| <textarea | |
| id="support-message" | |
| value={message} | |
| onChange={(event) => setMessage(event.target.value)} | |
| placeholder={messagePlaceholder} | |
| rows={6} | |
| maxLength={4000} | |
| className={cn(inputClass, "mt-2 resize-y")} | |
| /> | |
| </label> | |
| {error ? ( | |
| <p className="rounded-lg border border-rose-400/25 bg-rose-400/[0.06] px-3 py-2 text-sm text-rose-200/90" role="alert"> | |
| {error} | |
| </p> | |
| ) : null} | |
| <button | |
| type="submit" | |
| disabled={!canSubmit} | |
| className="inline-flex h-12 w-full items-center justify-center gap-2 rounded-xl bg-white px-6 text-[15px] font-bold text-black transition hover:bg-white/90 disabled:cursor-not-allowed disabled:opacity-40 sm:w-auto" | |
| > | |
| {phase === "submitting" ? ( | |
| <> | |
| <LoaderCircle className="size-4 animate-spin" /> Sending… | |
| </> | |
| ) : phase === "error" ? ( | |
| <> | |
| <RotateCcw className="size-4" /> Try again | |
| </> | |
| ) : ( | |
| "Submit" | |
| )} | |
| </button> | |
| <p className="text-xs leading-5 text-white/35"> | |
| We store only what you type here plus your browser type, so we can reproduce the issue. | |
| Nothing is shared publicly. | |
| </p> | |
| </form> | |
| </div> | |
| ); | |
| } | |