mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
@@ -0,0 +1,592 @@
|
||||
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 { useCourses } from "@/contexts/AdminCoursesContext";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
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 {
|
||||
fetchQuiz, createQuiz, updateQuiz,
|
||||
createQuizQuestion, updateQuizQuestion,
|
||||
course, unit, quiz, loading,
|
||||
} = useCourses();
|
||||
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);
|
||||
|
||||
const breadcrumbItems = [
|
||||
{ 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" },
|
||||
];
|
||||
|
||||
// ── Fetch ──────────────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
await fetchQuiz(courseId, unitId);
|
||||
setInitializing(false);
|
||||
})();
|
||||
}, [courseId, unitId]);
|
||||
|
||||
// ── Seed ──────────────────────────────────────────────────────────────────
|
||||
useEffect(() => {
|
||||
if (!quiz) return;
|
||||
const t = quiz.title ?? "";
|
||||
const ps = quiz.passing_score ?? 70;
|
||||
const ir = quiz.is_required === true || quiz.is_required === 1;
|
||||
const mq = quiz.max_questions ?? "";
|
||||
const sq = quiz.shuffle_questions === true || quiz.shuffle_questions === 1;
|
||||
const qs = (quiz.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 });
|
||||
}, [quiz]);
|
||||
|
||||
// ── 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 = quiz?.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);
|
||||
quizId = res?.data?.data?.data?.quiz_id;
|
||||
if (!quizId) return;
|
||||
} else {
|
||||
await updateQuiz(courseId, unitId, quizId, meta);
|
||||
}
|
||||
|
||||
for (let i = 0; i < questions.length; i++) {
|
||||
const q = { ...questions[i], order_index: i, updatedBy: user?.user_id };
|
||||
if (q.question_id) {
|
||||
await updateQuizQuestion(courseId, unitId, quizId, q.question_id, q);
|
||||
} else {
|
||||
await createQuizQuestion(courseId, unitId, quizId, q);
|
||||
}
|
||||
}
|
||||
|
||||
initialSnapshot.current = snapQuiz({ title, passingScore, isRequired, maxQuestions, shuffleQuestions, questions });
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
// ── 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(-1)}>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user