mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
616 lines
24 KiB
React
616 lines
24 KiB
React
import { useEffect, useRef, useState } from "react";
|
||
import { useNavigate, useParams } from "react-router-dom";
|
||
import { ArrowLeft, House, Plus, Save, HelpCircle, ChevronUp, ChevronDown } from "lucide-react";
|
||
import { toast } from "sonner";
|
||
|
||
import { useCourses } from "@/contexts/AdminCoursesContext";
|
||
import { useAuth } from "@/contexts/AuthContext";
|
||
import api from "@/utils/api.util";
|
||
import { PageMeta } from "@/contexts/MetadataContext";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Label } from "@/components/ui/label";
|
||
import { Checkbox } from "@/components/ui/checkbox";
|
||
import { Spinner } from "@/components/ui/spinner";
|
||
import { cn } from "@/lib/utils";
|
||
import { QuestionCard, makeQuestion } from "../../../components/courses/QuestionEditor";
|
||
|
||
// ── Validation ────────────────────────────────────────────────────────────────
|
||
|
||
function validate(questions) {
|
||
const errors = {};
|
||
questions.forEach((q, i) => {
|
||
const qErr = {};
|
||
if (!q.question?.trim()) qErr.question = "Question text is required.";
|
||
const correctCount = q.options.filter((o) => o.is_correct).length;
|
||
if (correctCount === 0) qErr.options = "At least one correct answer is required.";
|
||
if (q.options.some((o) => !o.text?.trim())) qErr.options = "All option texts are required.";
|
||
if (Object.keys(qErr).length) errors[i] = qErr;
|
||
});
|
||
return errors;
|
||
}
|
||
|
||
// ── Dirty-check snapshot (stable fields only, strips internal _tempId) ────────
|
||
|
||
function snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions }) {
|
||
return JSON.stringify({
|
||
title, passingScore, isRequired, maxQuestions, shuffleQuestions,
|
||
questions: questions.map((q) => ({
|
||
question_id: q.question_id ?? null,
|
||
question: q.question,
|
||
type: q.type,
|
||
points: q.points ?? 1,
|
||
options: (q.options ?? []).map((o) => ({
|
||
option_id: o.option_id ?? null,
|
||
text: o.text,
|
||
is_correct: o.is_correct,
|
||
})),
|
||
})),
|
||
});
|
||
}
|
||
|
||
// ── Jump to input ─────────────────────────────────────────────────────────────
|
||
|
||
function JumpToInput({ max, onJump }) {
|
||
const [val, setVal] = useState("");
|
||
|
||
const handleJump = () => {
|
||
const n = parseInt(val, 10);
|
||
if (!isNaN(n) && n >= 1 && n <= max) {
|
||
onJump(n - 1);
|
||
setVal("");
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="flex gap-1">
|
||
<Input
|
||
type="number"
|
||
min={1}
|
||
max={max}
|
||
value={val}
|
||
onChange={(e) => setVal(e.target.value)}
|
||
onKeyDown={(e) => e.key === "Enter" && handleJump()}
|
||
placeholder={`1–${max}`}
|
||
className="h-7 text-xs"
|
||
/>
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
variant="outline"
|
||
className="h-7 px-2 text-xs shrink-0"
|
||
onClick={handleJump}
|
||
>
|
||
Go
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Question Navigator ────────────────────────────────────────────────────────
|
||
|
||
const TYPE_LABEL = {
|
||
multiple_choice: "MC",
|
||
multi_select: "MS",
|
||
true_false: "TF",
|
||
};
|
||
|
||
function QuestionNavigator({ questions, activeIndex, onJump, onMove, navItemRefs, navContainerRef, errors }) {
|
||
return (
|
||
<div className="flex flex-col h-full">
|
||
|
||
<div className="px-3 py-3 border-b shrink-0">
|
||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
||
Questions
|
||
</p>
|
||
<p className="text-xs text-muted-foreground mt-0.5">
|
||
{questions.length} total
|
||
</p>
|
||
</div>
|
||
|
||
<div ref={navContainerRef} className="flex-1 overflow-y-auto py-2">
|
||
{questions.length === 0 ? (
|
||
<p className="text-xs text-muted-foreground text-center py-8 px-3">
|
||
No questions yet.
|
||
</p>
|
||
) : (
|
||
<div className="space-y-0.5 px-2">
|
||
{questions.map((q, i) => {
|
||
const isActive = i === activeIndex;
|
||
const hasError = !!errors?.[i];
|
||
const typeLabel = TYPE_LABEL[q.type] ?? "Q";
|
||
|
||
return (
|
||
<div
|
||
key={q._tempId ?? q.question_id ?? i}
|
||
ref={(el) => (navItemRefs.current[i] = el)}
|
||
onClick={() => onJump(i)}
|
||
className={cn(
|
||
"group flex items-center gap-1.5 rounded-md px-2 py-1.5 cursor-pointer transition-colors",
|
||
isActive
|
||
? "bg-primary/10 text-primary"
|
||
: hasError
|
||
? "bg-destructive/10 text-destructive hover:bg-destructive/20"
|
||
: "hover:bg-muted text-foreground"
|
||
)}
|
||
>
|
||
<span className={cn(
|
||
"flex h-5 w-5 shrink-0 items-center justify-center rounded text-[10px] font-semibold",
|
||
isActive
|
||
? "bg-primary text-primary-foreground"
|
||
: hasError
|
||
? "bg-destructive text-destructive-foreground"
|
||
: "bg-muted-foreground/20 text-muted-foreground"
|
||
)}>
|
||
{i + 1}
|
||
</span>
|
||
|
||
<span className="text-[10px] font-medium text-muted-foreground shrink-0 w-5">
|
||
{typeLabel}
|
||
</span>
|
||
|
||
<span className="flex-1 text-xs truncate min-w-0">
|
||
{q.question?.trim()
|
||
? q.question.trim()
|
||
: <span className="italic text-muted-foreground">Untitled</span>
|
||
}
|
||
</span>
|
||
|
||
<div className="hidden group-hover:flex items-center gap-0.5 shrink-0">
|
||
<button
|
||
type="button"
|
||
onClick={(e) => { e.stopPropagation(); onMove(i, "up"); }}
|
||
disabled={i === 0}
|
||
className="h-4 w-4 flex items-center justify-center rounded hover:bg-muted-foreground/20 disabled:opacity-30 transition-colors"
|
||
>
|
||
<ChevronUp className="h-3 w-3" />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={(e) => { e.stopPropagation(); onMove(i, "down"); }}
|
||
disabled={i === questions.length - 1}
|
||
className="h-4 w-4 flex items-center justify-center rounded hover:bg-muted-foreground/20 disabled:opacity-30 transition-colors"
|
||
>
|
||
<ChevronDown className="h-3 w-3" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{questions.length > 5 && (
|
||
<div className="px-3 py-3 border-t shrink-0 space-y-1.5">
|
||
<p className="text-xs text-muted-foreground">Jump to question</p>
|
||
<JumpToInput max={questions.length} onJump={onJump} />
|
||
</div>
|
||
)}
|
||
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// ── Main Page ─────────────────────────────────────────────────────────────────
|
||
|
||
export default function ModifyQuiz() {
|
||
const navigate = useNavigate();
|
||
const { courseId, unitId } = useParams();
|
||
const {
|
||
createQuiz, updateQuiz,
|
||
bulkSyncQuizQuestions,
|
||
course, unit, loading,
|
||
} = useCourses();
|
||
|
||
const [localQuiz, setLocalQuiz] = useState(null);
|
||
const { user } = useAuth();
|
||
|
||
const [initializing, setInitializing] = useState(true);
|
||
const [questions, setQuestions] = useState([]);
|
||
const [errors, setErrors] = useState({});
|
||
const [activeIndex, setActiveIndex] = useState(0);
|
||
|
||
const [title, setTitle] = useState("");
|
||
const [passingScore, setPassingScore] = useState(70);
|
||
const [isRequired, setIsRequired] = useState(false);
|
||
const [maxQuestions, setMaxQuestions] = useState("");
|
||
const [shuffleQuestions, setShuffleQuestions] = useState(false);
|
||
|
||
const questionRefs = useRef([]);
|
||
const navItemRefs = useRef([]);
|
||
const navContainerRef = useRef(null);
|
||
const headerRef = useRef(null);
|
||
const initialSnapshot = useRef(null);
|
||
|
||
// Junction revamp — this builder runs course-scoped AND from the standalone
|
||
// Unit Library (/admin/units/:unitId/quiz/edit, no :courseId param).
|
||
const scopeBase = courseId
|
||
? `/admin/courses/${courseId}/units/${unitId}`
|
||
: `/admin/units/${unitId}`;
|
||
|
||
const breadcrumbItems = courseId
|
||
? [
|
||
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
|
||
{ label: "Courses", to: "/admin/courses" },
|
||
{ label: course?.title ?? "…", to: `/admin/courses/${courseId}` },
|
||
{ label: unit?.title ?? "…", to: `/admin/courses/${courseId}/units/${unitId}` },
|
||
{ label: "Quiz" },
|
||
]
|
||
: [
|
||
{ label: "Home", icon: <House className="size-4" />, to: "/admin" },
|
||
{ label: "Units Library", to: "/admin/units" },
|
||
{ label: unit?.title ?? "…", to: `/admin/units/${unitId}/view` },
|
||
{ label: "Quiz" },
|
||
];
|
||
|
||
// ── Fetch — silently treat 404 as "no quiz yet" (create mode) ─────────────
|
||
useEffect(() => {
|
||
(async () => {
|
||
try {
|
||
const { data } = await api.get(`${scopeBase}/quiz`);
|
||
const result = data?.data?.data ?? null;
|
||
setLocalQuiz(result);
|
||
} catch (err) {
|
||
if (err?.response?.status !== 404) {
|
||
toast(err?.response?.data?.message ?? "Could not load quiz.");
|
||
}
|
||
// 404 → no quiz yet, stay in create mode with localQuiz = null
|
||
} finally {
|
||
setInitializing(false);
|
||
}
|
||
})();
|
||
}, [courseId, unitId]);
|
||
|
||
// ── Seed form from fetched quiz ────────────────────────────────────────────
|
||
useEffect(() => {
|
||
if (!localQuiz) return;
|
||
const t = localQuiz.title ?? "";
|
||
const ps = localQuiz.passing_score ?? 70;
|
||
const ir = localQuiz.is_required === true || localQuiz.is_required === 1;
|
||
const mq = localQuiz.max_questions ?? "";
|
||
const sq = localQuiz.shuffle_questions === true || localQuiz.shuffle_questions === 1;
|
||
const qs = (localQuiz.questions ?? []).map((q) => ({ ...q, _tempId: q.question_id, options: q.options ?? [] }));
|
||
setTitle(t); setPassingScore(ps); setIsRequired(ir); setMaxQuestions(mq); setShuffleQuestions(sq); setQuestions(qs);
|
||
initialSnapshot.current = snapQuiz({ title: t, passingScore: ps, isRequired: ir, maxQuestions: mq, shuffleQuestions: sq, questions: qs });
|
||
}, [localQuiz]);
|
||
|
||
// ── Measure sticky header → --quiz-h ──────────────────────────────────────
|
||
useEffect(() => {
|
||
if (!headerRef.current) return;
|
||
const update = () => {
|
||
document.documentElement.style.setProperty(
|
||
"--quiz-h",
|
||
`${headerRef.current.offsetHeight}px`
|
||
);
|
||
};
|
||
update();
|
||
window.addEventListener("resize", update);
|
||
return () => window.removeEventListener("resize", update);
|
||
}, []);
|
||
|
||
// ── Scroll active nav item into view ──────────────────────────────────────
|
||
useEffect(() => {
|
||
const item = navItemRefs.current[activeIndex];
|
||
const container = navContainerRef.current;
|
||
if (!item || !container) return;
|
||
|
||
const itemTop = item.offsetTop;
|
||
const itemBottom = itemTop + item.offsetHeight;
|
||
const viewTop = container.scrollTop;
|
||
const viewBottom = viewTop + container.clientHeight;
|
||
|
||
if (itemTop < viewTop) {
|
||
container.scrollTop = itemTop;
|
||
} else if (itemBottom > viewBottom) {
|
||
container.scrollTop = itemBottom - container.clientHeight;
|
||
}
|
||
}, [activeIndex]);
|
||
|
||
// ── IntersectionObserver — highlight nav as user scrolls ──────────────────
|
||
useEffect(() => {
|
||
if (!questions.length) return;
|
||
const observers = [];
|
||
|
||
questionRefs.current.forEach((el, i) => {
|
||
if (!el) return;
|
||
const observer = new IntersectionObserver(
|
||
([entry]) => { if (entry.isIntersecting) setActiveIndex(i); },
|
||
{ rootMargin: "-20% 0px -70% 0px", threshold: 0 }
|
||
);
|
||
observer.observe(el);
|
||
observers.push(observer);
|
||
});
|
||
|
||
return () => observers.forEach((o) => o.disconnect());
|
||
}, [questions.length]);
|
||
|
||
// ── Scroll helper with sticky offset ──────────────────────────────────────
|
||
const scrollToQuestion = (index) => {
|
||
const el = questionRefs.current[index];
|
||
if (!el) return;
|
||
const navbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--navbar-h") || "0", 10);
|
||
const quizH = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--quiz-h") || "0", 10);
|
||
const offset = navbarH + quizH + 16;
|
||
const top = el.getBoundingClientRect().top + window.scrollY - offset;
|
||
window.scrollTo({ top, behavior: "smooth" });
|
||
};
|
||
|
||
// ── Question actions ───────────────────────────────────────────────────────
|
||
const addQuestion = (type = "multiple_choice") => {
|
||
setQuestions((prev) => {
|
||
const next = [...prev, { ...makeQuestion(type), order_index: prev.length }];
|
||
setTimeout(() => {
|
||
const idx = next.length - 1;
|
||
setActiveIndex(idx);
|
||
scrollToQuestion(idx);
|
||
}, 50);
|
||
return next;
|
||
});
|
||
};
|
||
|
||
const updateQuestion = (index, updated) => {
|
||
setQuestions((prev) => prev.map((q, i) => i === index ? updated : q));
|
||
setErrors((prev) => { const e = { ...prev }; delete e[index]; return e; });
|
||
};
|
||
|
||
const removeQuestion = (index) => {
|
||
setQuestions((prev) => prev.filter((_, i) => i !== index));
|
||
setActiveIndex((prev) => Math.max(0, prev >= index ? prev - 1 : prev));
|
||
};
|
||
|
||
const moveQuestion = (index, direction) => {
|
||
setQuestions((prev) => {
|
||
const next = [...prev];
|
||
const swapIndex = direction === "up" ? index - 1 : index + 1;
|
||
if (swapIndex < 0 || swapIndex >= next.length) return prev;
|
||
[next[index], next[swapIndex]] = [next[swapIndex], next[index]];
|
||
return next;
|
||
});
|
||
const newIndex = direction === "up" ? index - 1 : index + 1;
|
||
setActiveIndex(newIndex);
|
||
setTimeout(() => scrollToQuestion(newIndex), 50);
|
||
};
|
||
|
||
const jumpTo = (index) => {
|
||
setActiveIndex(index);
|
||
scrollToQuestion(index);
|
||
};
|
||
|
||
const totalPoints = questions.reduce((sum, q) => sum + (q.points ?? 1), 0);
|
||
|
||
// ── Dirty tracking ─────────────────────────────────────────────────────────
|
||
const isDirty = initialSnapshot.current === null
|
||
? questions.length > 0 // new quiz — enable once they've added a question
|
||
: snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions }) !== initialSnapshot.current;
|
||
|
||
// ── Save ───────────────────────────────────────────────────────────────────
|
||
const handleSave = async () => {
|
||
const errs = validate(questions);
|
||
if (Object.keys(errs).length) {
|
||
setErrors(errs);
|
||
jumpTo(parseInt(Object.keys(errs)[0], 10));
|
||
return;
|
||
}
|
||
|
||
let quizId = localQuiz?.quiz_id;
|
||
const meta = {
|
||
title: title || "Unit Quiz",
|
||
passing_score: passingScore,
|
||
is_required: isRequired,
|
||
max_questions: maxQuestions ? parseInt(maxQuestions) : null,
|
||
shuffle_questions: shuffleQuestions,
|
||
updatedBy: user?.user_id,
|
||
createdBy: user?.user_id,
|
||
};
|
||
|
||
if (!quizId) {
|
||
const res = await createQuiz(courseId, unitId, meta);
|
||
// res is the raw response body: { status, message, data: { data: quiz } }
|
||
// (createQuiz() itself unwraps the same object as data?.data?.data to get
|
||
// the quiz row — this needs the same two levels, not three).
|
||
quizId = res?.data?.data?.quiz_id;
|
||
if (!quizId) return;
|
||
setLocalQuiz((prev) => ({ ...prev, quiz_id: quizId }));
|
||
} else {
|
||
await updateQuiz(courseId, unitId, quizId, meta);
|
||
}
|
||
|
||
await bulkSyncQuizQuestions(courseId, unitId, quizId, questions, user?.user_id);
|
||
|
||
initialSnapshot.current = snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions });
|
||
navigate(`${scopeBase}/view`);
|
||
};
|
||
|
||
// ── Render ─────────────────────────────────────────────────────────────────
|
||
return (
|
||
<div className="flex flex-col min-h-screen bg-muted/60">
|
||
<PageMeta title={unit ? `${unit.title} – Quiz - STARR` : undefined} />
|
||
|
||
{/* ── Sticky header ── */}
|
||
<div
|
||
ref={headerRef}
|
||
className="sticky z-20 shrink-0 border-b bg-white/70 dark:bg-zinc-900/70 backdrop-blur-md"
|
||
style={{ top: "var(--navbar-h)" }}
|
||
>
|
||
<div className="lg:container lg:mx-auto lg:px-6 px-4">
|
||
<div className="flex items-center gap-3 py-3">
|
||
<Button type="button" variant="ghost" size="icon" onClick={() => navigate(`${scopeBase}/view`)}>
|
||
<ArrowLeft className="h-4 w-4" />
|
||
</Button>
|
||
<div className="flex-1 min-w-0">
|
||
<h1 className="text-xl font-semibold leading-tight flex items-center gap-2">
|
||
<HelpCircle className="h-5 w-5 text-muted-foreground" />
|
||
Unit Quiz
|
||
</h1>
|
||
<p className="text-sm text-muted-foreground">
|
||
{questions.length} question{questions.length !== 1 ? "s" : ""} · {totalPoints} total point{totalPoints !== 1 ? "s" : ""}
|
||
</p>
|
||
</div>
|
||
<Button onClick={handleSave} disabled={loading || !isDirty}>
|
||
{loading ? <Spinner className="h-4 w-4 mr-2" /> : <Save className="h-4 w-4 mr-2" />}
|
||
Save Quiz
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* ── Split layout ── */}
|
||
<div className="flex flex-1 lg:container lg:mx-auto lg:px-6 px-0 w-full items-start">
|
||
|
||
{/* LEFT — Navigator */}
|
||
<div
|
||
className="hidden lg:flex flex-col w-60 shrink-0 border-r bg-background"
|
||
style={{
|
||
position: "sticky",
|
||
top: `calc(var(--navbar-h) + var(--quiz-h, 0px))`,
|
||
height: `calc(100vh - var(--navbar-h) - var(--quiz-h, 0px))`,
|
||
}}
|
||
>
|
||
<QuestionNavigator
|
||
questions={questions}
|
||
activeIndex={activeIndex}
|
||
onJump={jumpTo}
|
||
onMove={moveQuestion}
|
||
navItemRefs={navItemRefs}
|
||
navContainerRef={navContainerRef}
|
||
errors={errors}
|
||
/>
|
||
</div>
|
||
|
||
{/* RIGHT — Content */}
|
||
<div className="flex-1 min-w-0 px-4 lg:px-8 py-6">
|
||
{initializing ? (
|
||
<div className="flex items-center justify-center py-20">
|
||
<Spinner className="h-6 w-6" />
|
||
</div>
|
||
) : (
|
||
<div className="max-w-2xl space-y-6 pb-16">
|
||
|
||
{/* Settings */}
|
||
<div className="rounded-lg border bg-card p-6 space-y-5">
|
||
<p className="text-sm font-medium">Settings</p>
|
||
|
||
<div className="space-y-1.5">
|
||
<Label>Title</Label>
|
||
<Input
|
||
value={title}
|
||
onChange={(e) => setTitle(e.target.value)}
|
||
placeholder="Unit Quiz"
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1.5 max-w-[200px]">
|
||
<Label>Passing Score (%)</Label>
|
||
<Input
|
||
type="number"
|
||
min={0}
|
||
max={100}
|
||
value={passingScore}
|
||
onChange={(e) => setPassingScore(parseInt(e.target.value) || 0)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="space-y-1.5">
|
||
<Label>
|
||
Max Questions{" "}
|
||
<span className="text-muted-foreground font-normal text-xs">(blank = show all)</span>
|
||
</Label>
|
||
<Input
|
||
type="number" min={1} max={questions.length || undefined}
|
||
value={maxQuestions}
|
||
onChange={(e) => setMaxQuestions(e.target.value)}
|
||
placeholder={`All (${questions.length})`}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<Checkbox
|
||
id="quiz_required"
|
||
checked={isRequired === true}
|
||
onCheckedChange={(val) => setIsRequired(val)}
|
||
/>
|
||
<Label htmlFor="quiz_required" className="cursor-pointer">
|
||
Required to proceed to next unit
|
||
</Label>
|
||
</div>
|
||
|
||
<div className="flex items-center gap-3">
|
||
<Checkbox
|
||
id="quiz_shuffle"
|
||
checked={shuffleQuestions === true}
|
||
onCheckedChange={(val) => setShuffleQuestions(val)}
|
||
/>
|
||
<Label htmlFor="quiz_shuffle" className="cursor-pointer">
|
||
Shuffle question order for each attempt
|
||
</Label>
|
||
</div>
|
||
</div>
|
||
|
||
{maxQuestions && parseInt(maxQuestions) < questions.length && (
|
||
<p className="text-xs text-muted-foreground bg-muted rounded-md px-3 py-2">
|
||
Takers will see <strong>{maxQuestions}</strong> randomly selected questions
|
||
out of <strong>{questions.length}</strong> in the pool.
|
||
</p>
|
||
)}
|
||
|
||
{/* Questions */}
|
||
<div className="space-y-4">
|
||
{questions.length === 0 ? (
|
||
<div className="rounded-lg border border-dashed bg-card p-12 flex flex-col items-center gap-3 text-center">
|
||
<HelpCircle className="h-8 w-8 text-muted-foreground/40" />
|
||
<p className="text-sm text-muted-foreground">
|
||
No questions yet. Add one below.
|
||
</p>
|
||
</div>
|
||
) : (
|
||
questions.map((q, i) => (
|
||
<div
|
||
key={q._tempId ?? q.question_id ?? i}
|
||
ref={(el) => (questionRefs.current[i] = el)}
|
||
onClick={() => setActiveIndex(i)}
|
||
>
|
||
<QuestionCard
|
||
question={q}
|
||
index={i}
|
||
onChange={(updated) => updateQuestion(i, updated)}
|
||
onRemove={() => removeQuestion(i)}
|
||
error={errors[i]}
|
||
/>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
{/* Add question */}
|
||
<div className="rounded-lg border bg-card p-4">
|
||
<p className="text-xs text-muted-foreground uppercase tracking-wide font-medium mb-3">
|
||
Add Question
|
||
</p>
|
||
<div className="flex flex-wrap gap-2">
|
||
{[
|
||
{ type: "multiple_choice", label: "Multiple Choice" },
|
||
{ type: "multi_select", label: "Multi Select" },
|
||
{ type: "true_false", label: "True / False" },
|
||
].map(({ type, label }) => (
|
||
<Button
|
||
key={type}
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => addQuestion(type)}
|
||
>
|
||
<Plus className="h-3.5 w-3.5 mr-1" />
|
||
{label}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
} |