diff --git a/frontend/frontend/src/components/questionbank/QuestionBankView.jsx b/frontend/frontend/src/components/questionbank/QuestionBankView.jsx
deleted file mode 100644
index a16603b..0000000
--- a/frontend/frontend/src/components/questionbank/QuestionBankView.jsx
+++ /dev/null
@@ -1,1132 +0,0 @@
-import { useState, useEffect } from "react";
-import { Link } from "react-router-dom";
-import {
- questionsAPI,
- categoriesAPI,
- downloadJobsAPI,
-} from "../../services/api";
-import AdminNavbar from "../common/AdminNavbar";
-import ShareDialog from "./ShareDialog";
-import { useAuth } from "../../contexts/AuthContext";
-
-export default function QuestionBankView() {
- const { dbUser } = useAuth();
- const [questions, setQuestions] = useState([]);
- const [categories, setCategories] = useState([]);
- const [showForm, setShowForm] = useState(false);
- const [editingId, setEditingId] = useState(null);
- const [formData, setFormData] = useState({
- type: "text",
- question_content: "",
- answer: "",
- category: "",
- image: null,
- youtube_url: "",
- start_time: 0,
- end_time: 0,
- });
- const [imagePreview, setImagePreview] = useState(null);
- const [selectedQuestions, setSelectedQuestions] = useState([]);
- const [showBulkCategory, setShowBulkCategory] = useState(false);
- const [bulkCategory, setBulkCategory] = useState("");
- const [downloadJob, setDownloadJob] = useState(null);
- const [downloadProgress, setDownloadProgress] = useState(0);
- const [shareTarget, setShareTarget] = useState(null); // question or array of ids for sharing
-
- // Filter and sort state
- const [searchTerm, setSearchTerm] = useState("");
- const [filterCategory, setFilterCategory] = useState("");
- const [filterType, setFilterType] = useState("");
- const [filterOwner, setFilterOwner] = useState("");
- const [sortBy, setSortBy] = useState("created_at");
- const [sortOrder, setSortOrder] = useState("desc");
-
- useEffect(() => {
- loadQuestions();
- loadCategories();
- }, [searchTerm, filterCategory, filterType, filterOwner, sortBy, sortOrder]);
-
- const loadQuestions = async () => {
- try {
- const params = {
- sort_by: sortBy,
- sort_order: sortOrder,
- };
- if (searchTerm) params.search = searchTerm;
- if (filterCategory) params.category = filterCategory;
- if (filterType) params.type = filterType;
- if (filterOwner) params.owner = filterOwner;
-
- const response = await questionsAPI.getAll(params);
- setQuestions(response.data);
- } catch (error) {
- console.error("Error loading questions:", error);
- }
- };
-
- const loadCategories = async () => {
- try {
- const response = await categoriesAPI.getAll();
- setCategories(response.data);
- } catch (error) {
- console.error("Error loading categories:", error);
- }
- };
-
- const handleSubmit = async (e) => {
- e.preventDefault();
- try {
- if (editingId) {
- // Update existing question
- if (formData.type === "image" && formData.image) {
- const formDataToSend = new FormData();
- formDataToSend.append("type", "image");
- formDataToSend.append("question_content", formData.question_content);
- formDataToSend.append("answer", formData.answer);
- formDataToSend.append("category", formData.category);
- formDataToSend.append("image", formData.image);
- await questionsAPI.updateWithImage(editingId, formDataToSend);
- } else {
- await questionsAPI.update(editingId, {
- type: formData.type,
- question_content: formData.question_content,
- answer: formData.answer,
- category: formData.category,
- });
- }
- } else {
- // Create new question
- if (formData.type === "image" && formData.image) {
- const formDataToSend = new FormData();
- formDataToSend.append("type", "image");
- formDataToSend.append("question_content", formData.question_content);
- formDataToSend.append("answer", formData.answer);
- formDataToSend.append("category", formData.category);
- formDataToSend.append("image", formData.image);
- await questionsAPI.createWithImage(formDataToSend);
- } else if (formData.type === "youtube_audio") {
- // Create YouTube audio question
- const response = await questionsAPI.create({
- type: "youtube_audio",
- question_content: formData.question_content,
- answer: formData.answer,
- category: formData.category,
- youtube_url: formData.youtube_url,
- start_time: formData.start_time,
- end_time: formData.end_time,
- });
-
- // Start polling for download progress if job was created
- if (response.data.job) {
- setDownloadJob(response.data.job);
- pollDownloadStatus(response.data.job.id);
- return; // Don't close form yet, wait for download
- }
- } else {
- await questionsAPI.create({
- type: formData.type,
- question_content: formData.question_content,
- answer: formData.answer,
- category: formData.category,
- });
- }
- }
- setShowForm(false);
- setEditingId(null);
- setFormData({
- type: "text",
- question_content: "",
- answer: "",
- category: "",
- image: null,
- youtube_url: "",
- start_time: 0,
- end_time: 0,
- });
- setImagePreview(null);
- loadQuestions();
- } catch (error) {
- console.error("Error saving question:", error);
- alert("Error saving question");
- }
- };
-
- const handleEdit = (question) => {
- setEditingId(question.id);
- setFormData({
- type: question.type,
- question_content: question.question_content,
- answer: question.answer,
- category: question.category || "",
- image: null,
- });
- setShowForm(true);
- };
-
- const handleCancelEdit = () => {
- setShowForm(false);
- setEditingId(null);
- setFormData({
- type: "text",
- question_content: "",
- answer: "",
- category: "",
- image: null,
- youtube_url: "",
- start_time: 0,
- end_time: 0,
- });
- setImagePreview(null);
- setDownloadJob(null);
- setDownloadProgress(0);
- };
-
- const pollDownloadStatus = async (jobId) => {
- const pollInterval = setInterval(async () => {
- try {
- const response = await downloadJobsAPI.getStatus(jobId);
- const job = response.data;
-
- setDownloadProgress(job.progress);
-
- if (job.status === "completed") {
- clearInterval(pollInterval);
- setDownloadJob(null);
- setDownloadProgress(0);
- setShowForm(false);
- setFormData({
- type: "text",
- question_content: "",
- answer: "",
- category: "",
- image: null,
- youtube_url: "",
- start_time: 0,
- end_time: 0,
- });
- loadQuestions();
- alert("Audio downloaded successfully!");
- } else if (job.status === "failed") {
- clearInterval(pollInterval);
- setDownloadJob(null);
- setDownloadProgress(0);
- alert(`Download failed: ${job.error_message || "Unknown error"}`);
- }
- } catch (error) {
- clearInterval(pollInterval);
- console.error("Error polling download status:", error);
- setDownloadJob(null);
- setDownloadProgress(0);
- }
- }, 2000); // Poll every 2 seconds
- };
-
- const handleImageChange = (file) => {
- if (file && file.type.startsWith("image/")) {
- setFormData({ ...formData, image: file });
- const reader = new FileReader();
- reader.onloadend = () => {
- setImagePreview(reader.result);
- };
- reader.readAsDataURL(file);
- }
- };
-
- const handlePaste = (e) => {
- const items = e.clipboardData?.items;
- if (!items) return;
-
- for (let i = 0; i < items.length; i++) {
- if (items[i].type.startsWith("image/")) {
- e.preventDefault();
- const file = items[i].getAsFile();
- if (file) {
- handleImageChange(file);
- }
- break;
- }
- }
- };
-
- const handleDelete = async (id) => {
- if (!confirm("Are you sure you want to delete this question?")) return;
- try {
- await questionsAPI.delete(id);
- loadQuestions();
- } catch (error) {
- console.error("Error deleting question:", error);
- }
- };
-
- const toggleQuestionSelection = (id) => {
- setSelectedQuestions((prev) =>
- prev.includes(id) ? prev.filter((qId) => qId !== id) : [...prev, id],
- );
- };
-
- const toggleSelectAll = () => {
- if (selectedQuestions.length === questions.length) {
- setSelectedQuestions([]);
- } else {
- setSelectedQuestions(questions.map((q) => q.id));
- }
- };
-
- const handleBulkDelete = async () => {
- if (selectedQuestions.length === 0) return;
- if (
- !confirm(
- `Are you sure you want to delete ${selectedQuestions.length} question(s)?`,
- )
- )
- return;
-
- try {
- await Promise.all(selectedQuestions.map((id) => questionsAPI.delete(id)));
- setSelectedQuestions([]);
- loadQuestions();
- } catch (error) {
- console.error("Error bulk deleting questions:", error);
- alert("Error deleting questions");
- }
- };
-
- const handleBulkCategoryAssign = async () => {
- if (selectedQuestions.length === 0) return;
-
- try {
- await Promise.all(
- selectedQuestions.map((id) => {
- const question = questions.find((q) => q.id === id);
- return questionsAPI.update(id, {
- question_content: question.question_content,
- answer: question.answer,
- type: question.type,
- category: bulkCategory,
- });
- }),
- );
- setSelectedQuestions([]);
- setShowBulkCategory(false);
- setBulkCategory("");
- loadQuestions();
- } catch (error) {
- console.error("Error bulk updating categories:", error);
- alert("Error updating categories");
- }
- };
-
- const handleSort = (column) => {
- if (sortBy === column) {
- setSortOrder(sortOrder === "asc" ? "desc" : "asc");
- } else {
- setSortBy(column);
- setSortOrder("asc");
- }
- };
-
- const SortIndicator = ({ column }) => {
- if (sortBy !== column) return ↕;
- return {sortOrder === "asc" ? "↑" : "↓"};
- };
-
- const sortableHeaderStyle = {
- padding: "0.75rem",
- textAlign: "left",
- borderBottom: "2px solid #ddd",
- cursor: "pointer",
- userSelect: "none",
- };
-
- return (
- <>
-
-
-
-
Question Bank
-
-
-
-
-
- {showForm && (
-
- )}
-
-
- {/* Filter and Sort Controls */}
-
- {/* Search */}
-
- setSearchTerm(e.target.value)}
- style={{
- width: "100%",
- padding: "0.5rem",
- border: "1px solid #ddd",
- borderRadius: "4px",
- }}
- />
-
-
- {/* Category Filter */}
-
-
-
-
- {/* Type Filter */}
-
-
-
-
- {/* Owner Filter */}
-
-
-
-
- {/* Clear Filters */}
- {(searchTerm || filterCategory || filterType || filterOwner || sortBy !== "created_at" || sortOrder !== "desc") && (
-
- )}
-
-
-
-
Questions ({questions.length})
- {selectedQuestions.length > 0 && (
-
-
- {selectedQuestions.length} selected
-
-
-
-
-
- )}
-
-
- {showBulkCategory && selectedQuestions.length > 0 && (
-
-
-
-
-
-
- )}
-
- {questions.length === 0 ? (
-
No questions yet. Create your first question above!
- ) : (
-
-
-
-
- |
- 0
- }
- onChange={toggleSelectAll}
- style={{ cursor: "pointer" }}
- />
- |
- handleSort("type")}
- style={{
- ...sortableHeaderStyle,
- width: "80px",
- }}
- >
- Type
-
- |
- handleSort("category")}
- style={{
- ...sortableHeaderStyle,
- width: "120px",
- }}
- >
- Category
-
- |
- handleSort("question_content")}
- style={sortableHeaderStyle}
- >
- Question
-
- |
- handleSort("answer")}
- style={sortableHeaderStyle}
- >
- Answer
-
- |
-
- Creator
- |
-
- Actions
- |
-
-
-
- {questions.map((q) => (
-
- |
- toggleQuestionSelection(q.id)}
- style={{ cursor: "pointer" }}
- />
- |
-
-
- {q.type.toUpperCase()}
-
- |
-
- {q.category ? (
-
- {q.category}
-
- ) : (
-
- None
-
- )}
- |
-
- {q.question_content}
- {q.image_path && (
-
- )}
- |
-
- {q.answer}
- |
-
-
- {q.created_by === dbUser?.id ? "You" : (q.creator_name || "Unknown")}
-
- |
-
-
- {q.created_by === dbUser?.id && (
- <>
-
-
-
- >
- )}
-
- |
-
- ))}
-
-
-
- )}
-
-
- {downloadJob && (
-
-
Downloading Audio...
-
-
- {downloadProgress}% complete
-
-
- )}
-
-
- {shareTarget && (
- setShareTarget(null)}
- />
- )}
- >
- );
-}
diff --git a/frontend/frontend/src/components/questionbank/QuestionBankView.tsx b/frontend/frontend/src/components/questionbank/QuestionBankView.tsx
new file mode 100644
index 0000000..aa47825
--- /dev/null
+++ b/frontend/frontend/src/components/questionbank/QuestionBankView.tsx
@@ -0,0 +1,997 @@
+import { useState, useEffect, useRef, useMemo, useCallback } from "react";
+import { Link } from "react-router-dom";
+import { useVirtualizer } from "@tanstack/react-virtual";
+import {
+ questionsAPI,
+ categoriesAPI,
+ downloadJobsAPI,
+} from "../../services/api";
+import AdminNavbar from "../common/AdminNavbar";
+import ShareDialog from "./ShareDialog";
+import { useAuth } from "../../contexts/AuthContext";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Badge } from "@/components/ui/badge";
+import {
+ Sheet,
+ SheetContent,
+ SheetHeader,
+ SheetTitle,
+ SheetDescription,
+ SheetFooter,
+} from "@/components/ui/sheet";
+import { cn } from "@/lib/utils";
+import {
+ Search,
+ Plus,
+ X,
+ FileText,
+ Image,
+ Music,
+ Pencil,
+ Share2,
+ Trash2,
+ ArrowUp,
+ ArrowDown,
+ ArrowUpDown,
+ Upload,
+ MessageSquare,
+} from "lucide-react";
+
+/* ─── Types ──────────────────────────────────────────────────────── */
+
+interface Question {
+ id: number;
+ question_content: string;
+ answer: string;
+ type: string;
+ category?: string;
+ image_path?: string;
+ audio_path?: string;
+ created_by?: number;
+ creator_name?: string;
+ [key: string]: unknown;
+}
+
+interface Category {
+ id: number;
+ name: string;
+}
+
+interface QuestionFormData {
+ type: string;
+ question_content: string;
+ answer: string;
+ category: string;
+ image: File | null;
+ youtube_url: string;
+ start_time: number;
+ end_time: number;
+}
+
+interface DownloadJob {
+ id: string;
+ status: string;
+ progress: number;
+ error_message?: string;
+}
+
+type SortField = "type" | "category" | "question_content" | "answer" | "created_at";
+type SortOrder = "asc" | "desc";
+
+const INITIAL_FORM: QuestionFormData = {
+ type: "text",
+ question_content: "",
+ answer: "",
+ category: "",
+ image: null,
+ youtube_url: "",
+ start_time: 0,
+ end_time: 0,
+};
+
+/* ─── Sort header sub-component ──────────────────────────────────── */
+
+function SortHeader({
+ label,
+ field,
+ sortBy,
+ sortOrder,
+ onSort,
+ className,
+}: {
+ label: string;
+ field: SortField;
+ sortBy: SortField;
+ sortOrder: SortOrder;
+ onSort: (field: SortField) => void;
+ className?: string;
+}) {
+ const active = sortBy === field;
+ return (
+
+ );
+}
+
+/* ─── Type icon helper ───────────────────────────────────────────── */
+
+function TypeIcon({ type }: { type: string }) {
+ const cls = "size-4 shrink-0";
+ switch (type) {
+ case "image":
+ return ;
+ case "youtube_audio":
+ return ;
+ default:
+ return ;
+ }
+}
+
+/* ═══════════════════════════════════════════════════════════════════ */
+
+export default function QuestionBankView() {
+ const { dbUser } = useAuth();
+
+ /* ─── State ──────────────────────────────────────────────── */
+ const [questions, setQuestions] = useState([]);
+ const [categories, setCategories] = useState([]);
+ const [sheetOpen, setSheetOpen] = useState(false);
+ const [editingId, setEditingId] = useState(null);
+ const [formData, setFormData] = useState({ ...INITIAL_FORM });
+ const [imagePreview, setImagePreview] = useState(null);
+ const [selectedQuestions, setSelectedQuestions] = useState([]);
+ const [showBulkCategory, setShowBulkCategory] = useState(false);
+ const [bulkCategory, setBulkCategory] = useState("");
+ const [downloadJob, setDownloadJob] = useState(null);
+ const [downloadProgress, setDownloadProgress] = useState(0);
+ const [shareTarget, setShareTarget] = useState(null);
+
+ // Filter and sort state
+ const [searchTerm, setSearchTerm] = useState("");
+ const [filterCategory, setFilterCategory] = useState("");
+ const [filterType, setFilterType] = useState("");
+ const [filterOwner, setFilterOwner] = useState("");
+ const [sortBy, setSortBy] = useState("created_at");
+ const [sortOrder, setSortOrder] = useState("desc");
+
+ const scrollRef = useRef(null);
+
+ /* ─── Load data ──────────────────────────────────────────── */
+ useEffect(() => {
+ loadQuestions();
+ loadCategories();
+ }, [searchTerm, filterCategory, filterType, filterOwner, sortBy, sortOrder]);
+
+ const loadQuestions = async () => {
+ try {
+ const params: Record = {
+ sort_by: sortBy,
+ sort_order: sortOrder,
+ };
+ if (searchTerm) params.search = searchTerm;
+ if (filterCategory) params.category = filterCategory;
+ if (filterType) params.type = filterType;
+ if (filterOwner) params.owner = filterOwner;
+
+ const response = await questionsAPI.getAll(params);
+ setQuestions(response.data);
+ } catch (error) {
+ console.error("Error loading questions:", error);
+ }
+ };
+
+ const loadCategories = async () => {
+ try {
+ const response = await categoriesAPI.getAll();
+ setCategories(response.data);
+ } catch (error) {
+ console.error("Error loading categories:", error);
+ }
+ };
+
+ /* ─── Form handlers ──────────────────────────────────────── */
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ try {
+ if (editingId) {
+ if (formData.type === "image" && formData.image) {
+ const fd = new FormData();
+ fd.append("type", "image");
+ fd.append("question_content", formData.question_content);
+ fd.append("answer", formData.answer);
+ fd.append("category", formData.category);
+ fd.append("image", formData.image);
+ await questionsAPI.updateWithImage(editingId, fd);
+ } else {
+ await questionsAPI.update(editingId, {
+ type: formData.type,
+ question_content: formData.question_content,
+ answer: formData.answer,
+ category: formData.category,
+ });
+ }
+ } else {
+ if (formData.type === "image" && formData.image) {
+ const fd = new FormData();
+ fd.append("type", "image");
+ fd.append("question_content", formData.question_content);
+ fd.append("answer", formData.answer);
+ fd.append("category", formData.category);
+ fd.append("image", formData.image);
+ await questionsAPI.createWithImage(fd);
+ } else if (formData.type === "youtube_audio") {
+ const response = await questionsAPI.create({
+ type: "youtube_audio",
+ question_content: formData.question_content,
+ answer: formData.answer,
+ category: formData.category,
+ youtube_url: formData.youtube_url,
+ start_time: formData.start_time,
+ end_time: formData.end_time,
+ });
+ if (response.data.job) {
+ setDownloadJob(response.data.job);
+ pollDownloadStatus(response.data.job.id);
+ return;
+ }
+ } else {
+ await questionsAPI.create({
+ type: formData.type,
+ question_content: formData.question_content,
+ answer: formData.answer,
+ category: formData.category,
+ });
+ }
+ }
+ closeSheet();
+ loadQuestions();
+ } catch (error) {
+ console.error("Error saving question:", error);
+ alert("Error saving question");
+ }
+ };
+
+ const closeSheet = () => {
+ setSheetOpen(false);
+ setEditingId(null);
+ setFormData({ ...INITIAL_FORM });
+ setImagePreview(null);
+ setDownloadJob(null);
+ setDownloadProgress(0);
+ };
+
+ const handleEdit = (question: Question) => {
+ setEditingId(question.id);
+ setFormData({
+ type: question.type,
+ question_content: question.question_content,
+ answer: question.answer,
+ category: question.category || "",
+ image: null,
+ youtube_url: "",
+ start_time: 0,
+ end_time: 0,
+ });
+ setSheetOpen(true);
+ };
+
+ const pollDownloadStatus = async (jobId: string) => {
+ const pollInterval = setInterval(async () => {
+ try {
+ const response = await downloadJobsAPI.getStatus(jobId);
+ const job = response.data;
+ setDownloadProgress(job.progress);
+
+ if (job.status === "completed") {
+ clearInterval(pollInterval);
+ setDownloadJob(null);
+ setDownloadProgress(0);
+ closeSheet();
+ loadQuestions();
+ alert("Audio downloaded successfully!");
+ } else if (job.status === "failed") {
+ clearInterval(pollInterval);
+ setDownloadJob(null);
+ setDownloadProgress(0);
+ alert(`Download failed: ${job.error_message || "Unknown error"}`);
+ }
+ } catch (error) {
+ clearInterval(pollInterval);
+ console.error("Error polling download status:", error);
+ setDownloadJob(null);
+ setDownloadProgress(0);
+ }
+ }, 2000);
+ };
+
+ const handleImageChange = (file: File | undefined) => {
+ if (file && file.type.startsWith("image/")) {
+ setFormData({ ...formData, image: file });
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setImagePreview(reader.result as string);
+ };
+ reader.readAsDataURL(file);
+ }
+ };
+
+ const handlePaste = (e: React.ClipboardEvent) => {
+ const items = e.clipboardData?.items;
+ if (!items) return;
+ for (let i = 0; i < items.length; i++) {
+ if (items[i].type.startsWith("image/")) {
+ e.preventDefault();
+ const file = items[i].getAsFile();
+ if (file) handleImageChange(file);
+ break;
+ }
+ }
+ };
+
+ /* ─── Question actions ───────────────────────────────────── */
+
+ const handleDelete = async (id: number) => {
+ if (!confirm("Are you sure you want to delete this question?")) return;
+ try {
+ await questionsAPI.delete(id);
+ loadQuestions();
+ } catch (error) {
+ console.error("Error deleting question:", error);
+ }
+ };
+
+ const toggleQuestionSelection = (id: number) => {
+ setSelectedQuestions((prev) =>
+ prev.includes(id) ? prev.filter((qId) => qId !== id) : [...prev, id],
+ );
+ };
+
+ const toggleSelectAll = () => {
+ if (selectedQuestions.length === questions.length) {
+ setSelectedQuestions([]);
+ } else {
+ setSelectedQuestions(questions.map((q) => q.id));
+ }
+ };
+
+ const handleBulkDelete = async () => {
+ if (selectedQuestions.length === 0) return;
+ if (!confirm(`Are you sure you want to delete ${selectedQuestions.length} question(s)?`)) return;
+ try {
+ await Promise.all(selectedQuestions.map((id) => questionsAPI.delete(id)));
+ setSelectedQuestions([]);
+ loadQuestions();
+ } catch (error) {
+ console.error("Error bulk deleting questions:", error);
+ alert("Error deleting questions");
+ }
+ };
+
+ const handleBulkCategoryAssign = async () => {
+ if (selectedQuestions.length === 0) return;
+ try {
+ await Promise.all(
+ selectedQuestions.map((id) => {
+ const question = questions.find((q) => q.id === id);
+ if (!question) return Promise.resolve();
+ return questionsAPI.update(id, {
+ question_content: question.question_content,
+ answer: question.answer,
+ type: question.type,
+ category: bulkCategory,
+ });
+ }),
+ );
+ setSelectedQuestions([]);
+ setShowBulkCategory(false);
+ setBulkCategory("");
+ loadQuestions();
+ } catch (error) {
+ console.error("Error bulk updating categories:", error);
+ alert("Error updating categories");
+ }
+ };
+
+ const handleSort = (column: SortField) => {
+ if (sortBy === column) {
+ setSortOrder(sortOrder === "asc" ? "desc" : "asc");
+ } else {
+ setSortBy(column);
+ setSortOrder("asc");
+ }
+ };
+
+ /* ─── Derived ────────────────────────────────────────────── */
+
+ const hasActiveFilters =
+ searchTerm || filterCategory || filterType || filterOwner || sortBy !== "created_at" || sortOrder !== "desc";
+
+ const clearFilters = () => {
+ setSearchTerm("");
+ setFilterCategory("");
+ setFilterType("");
+ setFilterOwner("");
+ setSortBy("created_at");
+ setSortOrder("desc");
+ };
+
+ /* ─── Virtualizer ────────────────────────────────────────── */
+
+ const rowVirtualizer = useVirtualizer({
+ count: questions.length,
+ getScrollElement: () => scrollRef.current,
+ estimateSize: useCallback(() => 56, []),
+ overscan: 10,
+ });
+
+ const selectStyle =
+ "h-9 rounded-lg border border-input bg-transparent px-3 text-sm outline-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50";
+
+ /* ═══ Render ══════════════════════════════════════════════════ */
+ return (
+ <>
+
+
+ {/* ── Toolbar ─────────────────────────────────────── */}
+
+ {/* Search */}
+
+
+ setSearchTerm(e.target.value)}
+ placeholder="Search questions or answers..."
+ className="h-9 pl-9"
+ />
+
+
+ {/* Category filter */}
+
+
+ {/* Type filter */}
+
+
+ {/* Owner filter */}
+
+
+ {/* Count */}
+
+ {questions.length} question{questions.length !== 1 && "s"}
+
+
+ {/* Clear filters */}
+ {hasActiveFilters && (
+
+ )}
+
+ {/* Add Question */}
+
+
+
+ {/* ── List header (sort row) ──────────────────────── */}
+
+ {/* Checkbox */}
+
+ 0
+ ? "border-emerald-500 bg-emerald-500 text-white"
+ : "border-muted-foreground/30 hover:border-muted-foreground/60",
+ )}
+ >
+ {selectedQuestions.length === questions.length && questions.length > 0 && "✓"}
+
+
+ {/* Type */}
+
+
+
+ {/* Question */}
+
+
+
+ {/* Answer */}
+
+
+
+ {/* Category */}
+
+
+
+ {/* Creator */}
+
+ Creator
+
+ {/* Actions spacer */}
+
+
+
+ {/* ── Virtualized list ────────────────────────────── */}
+
+ {questions.length === 0 ? (
+
+
+
+
+
+
No questions yet
+
Create your first question to get started.
+
+
+
+ ) : (
+
+ {rowVirtualizer.getVirtualItems().map((virtualRow) => {
+ const q = questions[virtualRow.index];
+ const isSelected = selectedQuestions.includes(q.id);
+ const isOwner = q.created_by === dbUser?.id;
+
+ return (
+
+ {/* Checkbox */}
+
+ toggleQuestionSelection(q.id)}
+ className={cn(
+ "flex size-4 cursor-pointer items-center justify-center rounded border text-[0.6rem]",
+ isSelected
+ ? "border-emerald-500 bg-emerald-500 text-white"
+ : "border-muted-foreground/30 hover:border-muted-foreground/60",
+ )}
+ >
+ {isSelected && "✓"}
+
+
+
+ {/* Type icon */}
+
+
+
+
+ {/* Question text */}
+
+ {q.question_content}
+
+
+ {/* Answer */}
+
+ {q.answer}
+
+
+ {/* Category */}
+
+ {q.category ? (
+
+ {q.category}
+
+ ) : (
+ --
+ )}
+
+
+ {/* Creator */}
+
+ {isOwner ? (
+
+ You
+
+ ) : (
+
+ {q.creator_name || "Unknown"}
+
+ )}
+
+
+ {/* Hover actions */}
+
+ {isOwner && (
+ <>
+
+
+
+ >
+ )}
+
+
+ );
+ })}
+
+ )}
+
+
+ {/* ── Bulk action bar ─────────────────────────────── */}
+ {selectedQuestions.length > 0 && (
+
+
+ {selectedQuestions.length} selected
+
+
+ {/* Assign Category */}
+ {showBulkCategory ? (
+
+
+
+
+
+ ) : (
+
+ )}
+
+ {/* Share Selected */}
+
+
+ {/* Delete Selected */}
+
+
+ )}
+
+
+ {/* ── Sheet (Add / Edit form) ────────────────────────── */}
+ { if (!open) closeSheet(); else setSheetOpen(true); }}>
+
+
+ {editingId ? "Edit Question" : "Add Question"}
+
+ {editingId ? "Update this question's details." : "Create a new trivia question."}
+
+
+
+
+
+
+
+
+
+
+
+ {/* ── Download progress toast (outside sheet) ─────────── */}
+ {downloadJob && !sheetOpen && (
+
+
Downloading Audio...
+
+
{downloadProgress}% complete
+
+ )}
+
+ {/* ── Share dialog ────────────────────────────────────── */}
+ {shareTarget && (
+ setShareTarget(null)}
+ />
+ )}
+ >
+ );
+}