diff --git a/src/components/generic/GroupMultiSelect.jsx b/src/components/generic/GroupMultiSelect.jsx
index d35a970..850fd23 100644
--- a/src/components/generic/GroupMultiSelect.jsx
+++ b/src/components/generic/GroupMultiSelect.jsx
@@ -20,7 +20,9 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
-import { Check, ChevronsUpDown, X, Users } from 'lucide-react';
+import { Check, ChevronsUpDown, X, Users, ChevronLeft, ChevronRight } from 'lucide-react';
+
+const PAGE_SIZE = 5;
export default function GroupMultiSelect({
value = [],
@@ -31,6 +33,7 @@ export default function GroupMultiSelect({
}) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
+ const [page, setPage] = useState(1);
const [allGroups, setAllGroups] = useState(groupsProp ?? []);
const [loadingGroups, setLoadingGroups] = useState(!groupsProp);
const [dropdownStyle, setDropdownStyle] = useState({});
@@ -109,6 +112,17 @@ export default function GroupMultiSelect({
const filteredIds = filtered.map((g) => g.group_id);
const allFilteredSelected = filteredIds.length > 0 && filteredIds.every((id) => value.includes(id));
+ // ── Pagination ────────────────────────────────────────────────────────────
+ const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
+ const safePage = Math.min(page, totalPages);
+ const paginated = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
+
+ // Reset to page 1 whenever the search term changes
+ const handleSearchChange = (e) => {
+ setSearch(e.target.value);
+ setPage(1);
+ };
+
const toggle = (groupId) =>
onChange(value.includes(groupId)
? value.filter((id) => id !== groupId)
@@ -141,7 +155,7 @@ export default function GroupMultiSelect({
setSearch(e.target.value)}
+ onChange={handleSearchChange}
placeholder="Search groups…"
className="h-8 text-sm"
/>
@@ -173,7 +187,7 @@ export default function GroupMultiSelect({
)}
{/* Options */}
-
+
{loadingGroups ? (
-
Loading groups…
@@ -183,7 +197,7 @@ export default function GroupMultiSelect({
No groups found.
) : (
- filtered.map((g) => {
+ paginated.map((g) => {
const selected = value.includes(g.group_id);
return (
-
+
+ {/* Pagination */}
+ {!loadingGroups && filtered.length > PAGE_SIZE && (
+
+
+
+ {safePage} / {totalPages}
+
+
+
+ )}
,
document.body
);
@@ -222,7 +265,7 @@ export default function GroupMultiSelect({
ref={triggerRef}
type="button"
disabled={disabled}
- onClick={() => { setOpen((o) => !o); setSearch(''); }}
+ onClick={() => { setOpen((o) => !o); setSearch(''); setPage(1); }}
className={cn(
'w-full min-h-9 px-3 py-1.5 rounded-md border border-input bg-background text-sm',
'flex items-center gap-1.5 text-left',
diff --git a/src/components/generic/TaskMultiSelect.jsx b/src/components/generic/TaskMultiSelect.jsx
index f2bae6a..0ccaf4f 100644
--- a/src/components/generic/TaskMultiSelect.jsx
+++ b/src/components/generic/TaskMultiSelect.jsx
@@ -20,7 +20,9 @@ import { createPortal } from 'react-dom';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { cn } from '@/lib/utils';
-import { Check, ChevronsUpDown, X, ListChecks } from 'lucide-react';
+import { Check, ChevronsUpDown, X, ListChecks, ChevronLeft, ChevronRight } from 'lucide-react';
+
+const PAGE_SIZE = 5;
export default function TaskMultiSelect({
value = [],
@@ -31,6 +33,7 @@ export default function TaskMultiSelect({
}) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState('');
+ const [page, setPage] = useState(1);
const [dropdownStyle, setDropdownStyle] = useState({});
const triggerRef = useRef(null);
@@ -86,6 +89,17 @@ export default function TaskMultiSelect({
const filteredIds = filtered.map((t) => t.task_id);
const allFilteredSelected = filteredIds.length > 0 && filteredIds.every((id) => value.includes(id));
+ // ── Pagination ────────────────────────────────────────────────────────────
+ const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
+ const safePage = Math.min(page, totalPages);
+ const paginated = filtered.slice((safePage - 1) * PAGE_SIZE, safePage * PAGE_SIZE);
+
+ // Reset to page 1 whenever the search term changes
+ const handleSearchChange = (e) => {
+ setSearch(e.target.value);
+ setPage(1);
+ };
+
const toggle = (taskId) =>
onChange(value.includes(taskId)
? value.filter((id) => id !== taskId)
@@ -118,7 +132,7 @@ export default function TaskMultiSelect({
setSearch(e.target.value)}
+ onChange={handleSearchChange}
placeholder="Search tasks…"
className="h-8 text-sm"
/>
@@ -150,13 +164,13 @@ export default function TaskMultiSelect({
)}
{/* Options */}
-
+
{filtered.length === 0 ? (
-
No tasks found.
) : (
- filtered.map((t) => {
+ paginated.map((t) => {
const selected = value.includes(t.task_id);
return (
-
+
+ {/* Pagination */}
+ {filtered.length > PAGE_SIZE && (
+
+
+
+ {safePage} / {totalPages}
+
+
+
+ )}
,
document.body
);
@@ -195,7 +238,7 @@ export default function TaskMultiSelect({
ref={triggerRef}
type="button"
disabled={disabled}
- onClick={() => { setOpen((o) => !o); setSearch(''); }}
+ onClick={() => { setOpen((o) => !o); setSearch(''); setPage(1); }}
className={cn(
'w-full min-h-9 px-3 py-1.5 rounded-md border border-input bg-background text-sm',
'flex items-center gap-1.5 text-left',
diff --git a/src/modules/admin/config/users/rowActions.config.jsx b/src/modules/admin/config/users/rowActions.config.jsx
index 2794893..3134499 100644
--- a/src/modules/admin/config/users/rowActions.config.jsx
+++ b/src/modules/admin/config/users/rowActions.config.jsx
@@ -21,13 +21,6 @@ export function buildRowActions({ navigate, onArchive, onBan, onUnban }) {
icon: ,
onClick: (row) => navigate(`view/${row.user_id}`),
},
- {
- key: "edit",
- label: "Edit Info",
- icon: ,
- onClick: (row) => navigate(`edit/${row.user_id}`),
- disabled: (row) => row.role === "super_admin",
- },
{
key: "ban",
label: "Ban User",
diff --git a/src/modules/admin/pages/task_list/task/CreateTask.jsx b/src/modules/admin/pages/task_list/task/CreateTask.jsx
index ed9c336..c910161 100644
--- a/src/modules/admin/pages/task_list/task/CreateTask.jsx
+++ b/src/modules/admin/pages/task_list/task/CreateTask.jsx
@@ -15,7 +15,6 @@ import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
-import { Switch } from '@/components/ui/switch';
import { ArrowLeft, ChevronRight, ChevronLeft, Check, FileText, ListChecks, ClipboardList, Link as LinkIcon, Clock } from 'lucide-react';
import DeadlinePicker from '@/components/generic/DeadlinePicker';
@@ -47,7 +46,6 @@ export default function CreateTask() {
name: '',
description: '',
deadline: '',
- is_required: true,
requirements: [],
prerequisite_task_ids: [],
});
@@ -109,7 +107,6 @@ export default function CreateTask() {
name: form.name.trim(),
description: form.description.trim() || null,
deadline: form.deadline || null,
- is_required: form.is_required,
// use result.data — it carries the schema's transforms (e.g. link_url scheme defaulting)
// strip duration_seconds — it's only used for local validation
requirements: result.data.requirements.map((r) => {
@@ -214,19 +211,6 @@ export default function CreateTask() {
/>
-
-
-
-
- Later tasks in this list stay locked until this one is complete. Turn off for optional tasks.
-
-
-
setForm({ ...form, is_required: v })}
- />
-
-
diff --git a/src/modules/admin/pages/task_list/task/EditTask.jsx b/src/modules/admin/pages/task_list/task/EditTask.jsx
index d450acf..2326c3a 100644
--- a/src/modules/admin/pages/task_list/task/EditTask.jsx
+++ b/src/modules/admin/pages/task_list/task/EditTask.jsx
@@ -13,7 +13,6 @@ import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
-import { Switch } from '@/components/ui/switch';
import { Skeleton } from '@/components/ui/skeleton';
import DeadlinePicker from '@/components/generic/DeadlinePicker';
import {
@@ -56,7 +55,6 @@ export default function EditTask() {
description: data.description ?? '',
deadline: data.deadline ?? '',
status: data.status ?? 'pending',
- is_required: data.is_required ?? true,
requirements: reqs,
prerequisite_task_ids: (data.prerequisites ?? []).map((p) => p.task_id),
});
@@ -102,7 +100,6 @@ export default function EditTask() {
description: form.description.trim() || null,
deadline: form.deadline || null,
status: form.status,
- is_required: form.is_required,
requirements,
prerequisite_task_ids: form.prerequisite_task_ids ?? [],
});
@@ -215,19 +212,6 @@ export default function EditTask() {
-
-
-
-
- Later tasks in this list stay locked until this one is complete. Turn off for optional tasks.
-
-
-
setForm({ ...form, is_required: v })}
- />
-
-
diff --git a/src/modules/client/components/blocks/QuizBlock.jsx b/src/modules/client/components/blocks/QuizBlock.jsx
index 95b9164..8512428 100644
--- a/src/modules/client/components/blocks/QuizBlock.jsx
+++ b/src/modules/client/components/blocks/QuizBlock.jsx
@@ -75,7 +75,6 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
const [submitting, setSubmitting] = useState(false);
const [starting, setStarting] = useState(false);
const [result, setResult] = useState(null);
- const [reviewAttempted, setReviewAttempted] = useState(false);
const [submitError, setSubmitError] = useState(null);
// ── Timer state ───────────────────────────────────────────────────────────
@@ -107,7 +106,6 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
setResult(null);
setRemainingSeconds(null);
setTimeExpired(false);
- setReviewAttempted(false);
sessionRef.current = { sessionId: null, expiresAt: null };
timerFiredRef.current = false;
const draft = quiz?.active_session?.draft_answers ?? {};
@@ -222,7 +220,6 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
setResult(null);
setRemainingSeconds(null);
setTimeExpired(false);
- setReviewAttempted(false);
sessionRef.current = { sessionId: null, expiresAt: null };
timerFiredRef.current = false;
setStage("intro");
@@ -266,7 +263,6 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
};
const handleGoToReview = () => {
- setReviewAttempted(true);
setStage("review");
};
@@ -910,11 +906,11 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
- {reviewAttempted && unansweredCount > 0 && (
+ {unansweredCount > 0 && (
Answer all {total} questions first — {unansweredCount} remaining.
@@ -968,7 +964,7 @@ const QuizBlock = ({ quiz, loading = false, onStart, onDraft, onRefreshSession,
Previous
-
-
+ 0)}>
{submitting ? "Submitting…" : isLast ? "Review answers" : "Next"}
{!submitting && }
+ {isLast && unansweredCount > 0 && (
+
+ Answer all {total} questions first — {unansweredCount} remaining.
+
+ )}
);