ClassLensPortal / chatkit /frontend /src /components /ReportIssueForm.tsx
taboola
add student metrics dashboard, save quiz answer records, save original uploaded files
25cb9ab
Raw History Blame Contribute Delete
6.41 kB
import { useState } from "react";
import { apiPost } from "../lib/api";
const CATEGORIES: { value: string; label: string }[] = [
{ value: "bug", label: "Bug" },
{ value: "feature", label: "Feature request" },
{ value: "other", label: "Other" },
];
function FlagIcon() {
return (
<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M5 21V4a1 1 0 0 1 1-1h11.5a.5.5 0 0 1 .4.8l-3 4a.5.5 0 0 0 0 .6l3 4a.5.5 0 0 1-.4.8H6" />
</svg>
);
}
export function ReportIssueButton({ className }: { className?: string }) {
const [open, setOpen] = useState(false);
return (
<>
<button
onClick={() => setOpen(true)}
className={className || "flex items-center gap-1.5 text-sm text-white/60 hover:text-white transition-colors"}
title="Report an issue"
>
<FlagIcon />
Report an issue
</button>
{open && <ReportIssueModal onClose={() => setOpen(false)} />}
</>
);
}
function ReportIssueModal({ onClose }: { onClose: () => void }) {
const [category, setCategory] = useState("bug");
const [subject, setSubject] = useState("");
const [description, setDescription] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");
const [done, setDone] = useState(false);
const handleSubmit = async () => {
if (!subject.trim() || !description.trim()) {
setError("Please fill in both a subject and a description.");
return;
}
setSubmitting(true);
setError("");
try {
await apiPost("/api/issues", {
category,
subject: subject.trim(),
description: description.trim(),
page_context: window.location.hash || window.location.pathname,
});
setDone(true);
} catch (e) {
setError(e instanceof Error ? e.message : "Failed to submit — please try again.");
} finally {
setSubmitting(false);
}
};
return (
<div
className="fixed inset-0 z-[100] flex items-center justify-center bg-black/40 p-4"
onClick={onClose}
>
<div
className="w-full max-w-md rounded-2xl bg-[var(--color-surface)] border border-[var(--color-border)] shadow-xl p-6 space-y-4"
onClick={(e) => e.stopPropagation()}
>
{done ? (
<div className="text-center py-4 space-y-2">
<div className="mx-auto flex items-center justify-center w-10 h-10 rounded-full bg-emerald-500/10 text-emerald-500">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 12.5 9.5 18 20 6" /></svg>
</div>
<p className="text-sm font-medium text-[var(--color-text)]">Thanks — your report was sent.</p>
<button
onClick={onClose}
className="mt-2 text-sm px-4 py-1.5 rounded-lg bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-light)] transition-colors"
>
Close
</button>
</div>
) : (
<>
<div className="flex items-center justify-between">
<h3 className="text-sm font-bold uppercase tracking-widest text-[var(--color-text)]">Report an Issue</h3>
<button onClick={onClose} className="text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition-colors" title="Close">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 5l14 14M19 5 5 19" /></svg>
</button>
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Type</label>
<div className="flex gap-2">
{CATEGORIES.map((c) => (
<button
key={c.value}
onClick={() => setCategory(c.value)}
className={`text-xs px-3 py-1.5 rounded-full border transition-colors ${
category === c.value
? "bg-[var(--color-primary)] text-white border-[var(--color-primary)]"
: "text-[var(--color-text-muted)] border-[var(--color-border)] hover:border-[var(--color-primary)]/50"
}`}
>
{c.label}
</button>
))}
</div>
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Subject</label>
<input
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Short summary of the issue"
className="input text-sm"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-medium text-[var(--color-text-muted)]">Description</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="What happened, what did you expect, and any steps to reproduce it?"
rows={4}
className="input text-sm resize-none"
/>
</div>
{error && <p className="text-xs text-red-500">{error}</p>}
<div className="flex justify-end gap-2 pt-1">
<button
onClick={onClose}
className="text-sm px-4 py-1.5 rounded-lg border border-[var(--color-border)] text-[var(--color-text-muted)] hover:text-[var(--color-text)] transition-colors"
>
Cancel
</button>
<button
onClick={() => { void handleSubmit(); }}
disabled={submitting}
className="text-sm px-4 py-1.5 rounded-lg bg-[var(--color-primary)] text-white hover:bg-[var(--color-primary-light)] disabled:opacity-40 transition-colors"
>
{submitting ? "Sending…" : "Send report"}
</button>
</div>
</>
)}
</div>
</div>
);
}