From 2be8547a52d0adf6bf2d8f340c1353d3510cde25 Mon Sep 17 00:00:00 2001 From: rgrgogu Date: Thu, 6 Aug 2026 08:11:18 +0800 Subject: [PATCH] good morning --- src/contexts/AdminTaskContext.jsx | 12 +- src/contexts/AuthContext.jsx | 4 +- .../admin/pages/task_list/task/CreateTask.jsx | 5 +- .../admin/pages/task_list/task/EditTask.jsx | 5 +- .../task_list/task/RequirementBuilder.jsx | 58 +---- .../admin/pages/task_list/task/task.schema.js | 7 +- src/modules/auth/components/LoginForm.jsx | 22 +- src/modules/auth/components/RegisterForm.jsx | 88 ++++++- .../client/components/blocks/PassQuiz.jsx | 239 ------------------ src/modules/client/pages/ViewTask.jsx | 29 +-- 10 files changed, 114 insertions(+), 355 deletions(-) delete mode 100644 src/modules/client/components/blocks/PassQuiz.jsx diff --git a/src/contexts/AdminTaskContext.jsx b/src/contexts/AdminTaskContext.jsx index 5e699fa..c712954 100644 --- a/src/contexts/AdminTaskContext.jsx +++ b/src/contexts/AdminTaskContext.jsx @@ -489,7 +489,7 @@ export function AdminTaskProvider({ children }) { [request] ); - // Units/lessons/quizzes are standalone entities that may sit under 0..N + // Units/lessons are standalone entities that may sit under 0..N // courses (junction revamp) — each row now carries a `courses[]` binding // array instead of a single course_title/order_index pair, and // RequirementBuilder's ContentPicker builds its own search string from it, @@ -510,14 +510,6 @@ export function AdminTaskProvider({ children }) { [request] ); - const fetchQuizzesFlat = useCallback( - () => request(async () => { - const res = await api.get('/admin/courses/quizzes-flat'); - return res.data?.data ?? []; - }), - [request] - ); - // ══════════════════════════════════════════════════════════════════════════ // TASK COMPLETIONS // ══════════════════════════════════════════════════════════════════════════ @@ -702,7 +694,7 @@ export function AdminTaskProvider({ children }) { bulkArchiveCompletions, bulkRestoreCompletions, // ── Flat lists for RequirementBuilder ──────────────────────────── - fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat, fetchQuizzesFlat, + fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat, // ── Review workflow ─────────────────────────────────────────────── reviewSubmission, diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx index c21cfa9..6d62788 100644 --- a/src/contexts/AuthContext.jsx +++ b/src/contexts/AuthContext.jsx @@ -35,10 +35,10 @@ export function AuthProvider({ children }) { // recently and its trust window is still valid, in which case the backend // returns otpRequired:false along with a full session, same shape as // verifyOTP's response. - const login = useCallback(async ({ email, password }) => { + const login = useCallback(async ({ email, password, group_code }) => { setAuthError(null) try { - const { data } = await api.post('/auth/login', { email, password }) + const { data } = await api.post('/auth/login', { email, password, ...(group_code ? { group_code } : {}) }) if (data.data.otpRequired === false) { applySession(data.data) return { success: true, otpRequired: false, user: data.data.user } diff --git a/src/modules/admin/pages/task_list/task/CreateTask.jsx b/src/modules/admin/pages/task_list/task/CreateTask.jsx index 4960dee..f5d8cea 100644 --- a/src/modules/admin/pages/task_list/task/CreateTask.jsx +++ b/src/modules/admin/pages/task_list/task/CreateTask.jsx @@ -39,7 +39,7 @@ function SummaryRow({ label, value }) { export default function CreateTask() { const navigate = useNavigate(); const { taskListId } = useParams(); - const { createTask, fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat, fetchQuizzesFlat, fetchTasksFlat, loading } = useAdminTask(); + const { createTask, fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat, fetchTasksFlat, loading } = useAdminTask(); const [step, setStep] = useState(0); const [form, setForm] = useState({ @@ -53,14 +53,12 @@ export default function CreateTask() { const [courses, setCourses] = useState([]); const [units, setUnits] = useState([]); const [lessons, setLessons] = useState([]); - const [quizzes, setQuizzes] = useState([]); const [siblingTasks, setSiblingTasks] = useState([]); useEffect(() => { fetchCoursesFlat().then((d) => d && setCourses(d)); fetchUnitsFlat().then((d) => d && setUnits(d)); fetchLessonsFlat().then((d) => d && setLessons(d)); - fetchQuizzesFlat().then((d) => d && setQuizzes(d)); fetchTasksFlat(taskListId).then((d) => d && setSiblingTasks(d)); }, []); @@ -249,7 +247,6 @@ export default function CreateTask() { courses={courses} units={units} lessons={lessons} - quizzes={quizzes} taskListId={taskListId} /> {errors.requirements && ( diff --git a/src/modules/admin/pages/task_list/task/EditTask.jsx b/src/modules/admin/pages/task_list/task/EditTask.jsx index 779b5e5..1990372 100644 --- a/src/modules/admin/pages/task_list/task/EditTask.jsx +++ b/src/modules/admin/pages/task_list/task/EditTask.jsx @@ -33,14 +33,13 @@ const STATUS_OPTIONS = [ export default function EditTask() { const navigate = useNavigate(); const { taskListId, taskId } = useParams(); - const { fetchTask, updateTask, fetchTasksFlat, fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat, fetchQuizzesFlat, loading } = useAdminTask(); + const { fetchTask, updateTask, fetchTasksFlat, fetchCoursesFlat, fetchUnitsFlat, fetchLessonsFlat, loading } = useAdminTask(); const [form, setForm] = useState(null); const [errors, setErrors] = useState({}); const [courses, setCourses] = useState([]); const [units, setUnits] = useState([]); const [lessons, setLessons] = useState([]); - const [quizzes, setQuizzes] = useState([]); const [siblingTasks, setSiblingTasks] = useState([]); const [confirmOpen, setConfirmOpen] = useState(false); const initialRequirementsRef = useRef(null); @@ -62,7 +61,6 @@ export default function EditTask() { fetchCoursesFlat().then((d) => d && setCourses(d)); fetchUnitsFlat().then((d) => d && setUnits(d)); fetchLessonsFlat().then((d) => d && setLessons(d)); - fetchQuizzesFlat().then((d) => d && setQuizzes(d)); fetchTasksFlat(taskListId).then((d) => d && setSiblingTasks(d.filter((t) => t.task_id !== taskId))); }, [taskListId, taskId]); @@ -245,7 +243,6 @@ export default function EditTask() { courses={courses} units={units} lessons={lessons} - quizzes={quizzes} taskListId={taskListId} /> {errors.requirements && ( diff --git a/src/modules/admin/pages/task_list/task/RequirementBuilder.jsx b/src/modules/admin/pages/task_list/task/RequirementBuilder.jsx index 7fb131b..eb785a9 100644 --- a/src/modules/admin/pages/task_list/task/RequirementBuilder.jsx +++ b/src/modules/admin/pages/task_list/task/RequirementBuilder.jsx @@ -1,5 +1,5 @@ import { useState, useEffect, useMemo } from 'react'; -import { Plus, Trash2, GripVertical, Link, Upload, BookOpen, Layers, FileText, ChevronsUpDown, Tag, Lock, Clock, AlertTriangle, PenLine, ClipboardCheck, ShieldCheck, Link2, Unlink, History } from 'lucide-react'; +import { Plus, Trash2, GripVertical, Link, Upload, BookOpen, Layers, FileText, ChevronsUpDown, Tag, Lock, Clock, AlertTriangle, PenLine, ShieldCheck, Link2, Unlink, History } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -22,7 +22,6 @@ const REQUIREMENT_TYPES = [ { value: 'read_course', label: 'Read a Course', icon: BookOpen, category: 'Content' }, { value: 'read_unit', label: 'Read a Unit', icon: Layers, category: 'Content' }, { value: 'read_lesson', label: 'Read a Lesson', icon: FileText, category: 'Content' }, - { value: 'pass_quiz', label: 'Pass a Quiz', icon: ClipboardCheck, category: 'Content' }, ]; // No longer a choosable option (removed from Add Task) — kept here only so @@ -103,7 +102,7 @@ function BindingLine({ courses = [] }) { return ( - {courses.map((c) => c.title).join(', ')} + {courses.map((c) => c.title).join(', ')} ); } @@ -142,7 +141,7 @@ function ContentPicker({ value, options, idKey, searchKey, labelKey, placeholder - + {dialogTitle ?? placeholder} @@ -187,7 +186,7 @@ function createRequirement(type = 'visit_link') { } // ─── RequirementBuilder ─────────────────────────────────────────────────────── -export default function RequirementBuilder({ value = [], onChange, courses = [], units = [], lessons = [], quizzes = [], taskListId }) { +export default function RequirementBuilder({ value = [], onChange, courses = [], units = [], lessons = [], taskListId }) { const [items, setItems] = useState( value.length > 0 ? value.map((r) => ({ _key: crypto.randomUUID(), ...r })) @@ -229,7 +228,7 @@ export default function RequirementBuilder({ value = [], onChange, courses = [], updateItem(key, { allowed_file_types: next }); }; - const CONTENT_TYPE_TO_REQUIREMENT_TYPE = { course: 'read_course', unit: 'read_unit', lesson: 'read_lesson', quiz: 'pass_quiz' }; + const CONTENT_TYPE_TO_REQUIREMENT_TYPE = { course: 'read_course', unit: 'read_unit', lesson: 'read_lesson' }; const handleContentSelect = (key, content, contentType) => { updateItem(key, { @@ -437,7 +436,7 @@ export default function RequirementBuilder({ value = [], onChange, courses = [], renderTrigger={(c) => (
- {c.title} + {c.title} {fmtDuration(c.duration_seconds) && ( {fmtDuration(c.duration_seconds)} @@ -474,7 +473,7 @@ export default function RequirementBuilder({ value = [], onChange, courses = [],
- {u.title} + {u.title}
{fmtDuration(u.duration_seconds) && ( @@ -515,7 +514,7 @@ export default function RequirementBuilder({ value = [], onChange, courses = [],
- {l.title} + {l.title}
{fmtDuration(l.duration_seconds) && ( @@ -550,47 +549,6 @@ export default function RequirementBuilder({ value = [], onChange, courses = [], )}
)} - - {/* ── pass_quiz fields ── */} - {item.type === 'pass_quiz' && ( -
- - handleContentSelect(item._key, q, 'quiz')} - renderTrigger={(q) => ( -
-
- {q.unit_title} - {q.title} -
-
- )} - renderItem={(q) => ( -
-
- {q.title} - Unit: {q.unit_title} -
- -
- )} - /> - - {/* ── inline no-content error ── */} - {item.reference_id && (item.duration_seconds ?? -1) === 0 && ( -

- - This quiz has no questions yet. -

- )} -
- )} ); diff --git a/src/modules/admin/pages/task_list/task/task.schema.js b/src/modules/admin/pages/task_list/task/task.schema.js index 31ca2b9..fbbf78f 100644 --- a/src/modules/admin/pages/task_list/task/task.schema.js +++ b/src/modules/admin/pages/task_list/task/task.schema.js @@ -1,10 +1,8 @@ import { z } from 'zod'; -import { FileText, Link as LinkIcon, Upload, BookOpen, Layers, PenLine, ClipboardCheck } from 'lucide-react'; +import { FileText, Link as LinkIcon, Upload, BookOpen, Layers, PenLine } from 'lucide-react'; // ── Requirement validation ──────────────────────────────────────────────────── -// pass_quiz reuses the same "picked a reference, and it has content" check as -// the read_* types — its duration_seconds slot carries question_count instead. -const READ_TYPES = ['read_course', 'read_unit', 'read_lesson', 'pass_quiz']; +const READ_TYPES = ['read_course', 'read_unit', 'read_lesson']; // Users commonly type bare domains ("google.com") — default the scheme to https // so the link is actually clickable/navigable once the task is saved. @@ -44,7 +42,6 @@ export const REQUIREMENT_TYPE_META = { read_course: { label: 'Read a Course', icon: BookOpen }, read_unit: { label: 'Read a Unit', icon: Layers }, read_lesson: { label: 'Read a Lesson', icon: FileText }, - pass_quiz: { label: 'Pass a Quiz', icon: ClipboardCheck }, }; export function requirementSummaryText(req) { diff --git a/src/modules/auth/components/LoginForm.jsx b/src/modules/auth/components/LoginForm.jsx index d235232..2d7c42d 100644 --- a/src/modules/auth/components/LoginForm.jsx +++ b/src/modules/auth/components/LoginForm.jsx @@ -13,7 +13,7 @@ * May 23, 2026 lash0000 002 Migrated to Zod + zodResolver; removed shadcn Form wrapper ***********************************************************************************************************************************************************************/ import { useState } from 'react' -import { useNavigate, Link } from 'react-router-dom' +import { useNavigate, Link, useSearchParams } from 'react-router-dom' import { useAuth } from '@/contexts/AuthContext' import { useForm } from 'react-hook-form' import { z } from 'zod' @@ -34,7 +34,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog' -import { Eye, EyeOff, LoaderCircle } from 'lucide-react' +import { Eye, EyeOff, LoaderCircle, Users } from 'lucide-react' // ─── Schema ────────────────────────────────────────────────────────────────── const loginSchema = z.object({ @@ -46,6 +46,8 @@ const loginSchema = z.object({ export function LoginForm({ className, ...props }) { const { login } = useAuth() const navigate = useNavigate() + const [searchParams] = useSearchParams() + const groupCode = searchParams.get('group_code') || '' const [passwordVisible, setPasswordVisible] = useState(false) const [errorDialogOpen, setErrorDialogOpen] = useState(false) @@ -66,7 +68,7 @@ export function LoginForm({ className, ...props }) { } const onSubmit = async ({ email, password }) => { - const result = await login({ email, password }) + const result = await login({ email, password, group_code: groupCode || undefined }) if (result.success) { if (result.otpRequired === false) { @@ -95,7 +97,8 @@ export function LoginForm({ className, ...props }) { } const handleGoogle = () => { - window.location.href = `${import.meta.env.VITE_API_URL}/auth/google` + const params = groupCode ? `?group_code=${encodeURIComponent(groupCode)}` : '' + window.location.href = `${import.meta.env.VITE_API_URL}/auth/google${params}` } if (otpEmail) { @@ -127,6 +130,17 @@ export function LoginForm({ className, ...props }) {

+ {/* Group code notice */} + {groupCode && ( +
+ +

+ Signing in will join group{' '} + {groupCode} +

+
+ )} + {/* Google OAuth */} + +
+ + Or continue with + +
+ + + +

+ Already have an account?{' '} + + Sign in + +

+ + )} {/* ── Step 1: Personal info ── */} {step === 0 && ( @@ -472,13 +531,21 @@ export function RegisterForm({ className, ...props }) { )} - +
+ + +

Already have an account?{' '} - + Sign in

@@ -621,7 +688,10 @@ export function RegisterForm({ className, ...props }) {

Already have an account?{' '} - + Sign in

diff --git a/src/modules/client/components/blocks/PassQuiz.jsx b/src/modules/client/components/blocks/PassQuiz.jsx deleted file mode 100644 index 37a6ce7..0000000 --- a/src/modules/client/components/blocks/PassQuiz.jsx +++ /dev/null @@ -1,239 +0,0 @@ -import { useState, useEffect } from "react"; -import { Badge } from "@/components/ui/badge"; -import { ClipboardCheck, CheckCheck, SendHorizonal, Lock, Zap, Info, Tag } from "lucide-react"; -import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; -import ResponsiveModal from "@/components/generic/ResponsiveModal"; -import { Button } from "@/components/ui/button"; -import { useNavigate } from "react-router-dom"; -import api from "@/utils/api.util"; -import { useClientTiers } from "@/contexts/ClientTiersProvider"; -import { resolveTierBadge } from "@/utils/tierBadge.util"; - -function TierBadge({ tier }) { - const { tierMap } = useClientTiers(); - const { rank, label, cls } = resolveTierBadge(tier ?? 'free', tierMap); - return ( - - {rank > 0 ? : } - {label} - - ); -} - -// ─── PassQuiz — task requirement block for pass_quiz type ───────────────────── -// Mirrors ReadUnit.jsx's fetch-detail-and-navigate pattern. A quiz is always -// unit-scoped; navigation goes into the course reader if the unit is attached -// to a course, otherwise the standalone unit reader. -const PassQuiz = ({ title = "Pass Quizzes", quizzes = [], groupId, taskListId, taskId }) => { - const navigate = useNavigate(); - const [details, setDetails] = useState({}); - const [locked, setLocked] = useState({}); - const [lockedInfo, setLockedInfo] = useState({}); - const [unavailable, setUnavailable] = useState({}); - const [fetching, setFetching] = useState({}); - const [selected, setSelected] = useState(null); - - useEffect(() => { - quizzes.forEach(async (q) => { - if (!q.reference_id) return; - setFetching((prev) => ({ ...prev, [q.reference_id]: true })); - try { - const res = await api.get(`/client/courses/quiz/uuid/${q.reference_id}`); - const d = res.data?.data; - if (d) setDetails((prev) => ({ ...prev, [q.reference_id]: d })); - } catch (err) { - if (err?.response?.status === 403) { - setLocked((prev) => ({ ...prev, [q.reference_id]: true })); - const course = err.response?.data?.course; - if (course) setLockedInfo((prev) => ({ ...prev, [q.reference_id]: course })); - } else { - setUnavailable((prev) => ({ ...prev, [q.reference_id]: true })); - } - } finally { - setFetching((prev) => ({ ...prev, [q.reference_id]: false })); - } - }); - }, []); - - const goToQuiz = (info) => { - if (!info) return; - const taskCtx = taskId ? { has_task: true, groupId, taskListId, taskId } : undefined; - if (info.unit?.course?.course_id) { - navigate(`/course/${info.unit.course.course_id}/unit`, { - state: { quizUnitId: info.unit.unit_id, ...(taskCtx ? { taskCtx } : {}) }, - }); - } else if (info.unit?.uuid) { - navigate(`/units/${info.unit.uuid}/read`, { state: { quizId: true } }); - } - }; - - if (!quizzes.length) return null; - - const hasLocked = Object.values(locked).some(Boolean); - - return ( - <> -
-
- -

{title}

- {quizzes.length} -
- - {hasLocked && ( -
- -
-

Subscription Required

-

- To complete this activity, subscribe to one of our available tier plans. -

-
- -
- )} - - -
- {quizzes.map((q) => { - const info = details[q.reference_id]; - const courseInfo = lockedInfo[q.reference_id]; - const isLocked = locked[q.reference_id]; - const isUnavailable = unavailable[q.reference_id]; - const isFetching = fetching[q.reference_id]; - const passed = info?.has_passed || q.completed; - - if (isLocked) { - return ( -
navigate('/plans')} - className="bg-card rounded-2xl border border-dashed p-4 flex flex-col gap-3 hover:bg-muted/60 hover:shadow-sm transition-all cursor-pointer group w-80 shrink-0 opacity-80" - > -
- - Quiz -
-
-

{q.title}

-

- Subscribe to {courseInfo?.title ?? 'this course'} to unlock this quiz. -

-
- -
-
- ); - } - - if (isUnavailable) { - return ( -
-
- - Quiz - - Unavailable - -
-

{q.title}

-

This quiz is no longer available.

-
- ); - } - - return ( -
!isFetching && setSelected(q)} - className={`bg-card rounded-2xl border p-4 flex flex-col gap-3 transition-all w-80 shrink-0 ${ - isFetching ? 'opacity-60 cursor-wait' : 'hover:bg-muted/60 hover:shadow-sm cursor-pointer group' - }`} - > -
- - Quiz -
- {info?.unit?.course?.subscription - ? - : isFetching && Loading… - } -
-
- {info?.unit?.title && ( -

- in {info.unit.title} -

- )} -

{q.title}

-
- {passed ? ( - - Passed - - ) : ( - Not Attempted - )} - {info?.passing_score && ( - {info.passing_score}% to pass - )} -
-
- ); - })} -
- -
-
- - !v && setSelected(null)} - title={selected?.title} - description="Quiz Info" - footer={ - <> - - - - } - > - {selected && (() => { - const info = details[selected.reference_id]; - return ( -
- {(info?.has_passed || selected.completed) && ( -
- Already Passed -
- )} - {info?.unit?.title && ( -

- Unit: - {info.unit.title} -

- )} - {info?.passing_score && ( -

- Passing score: - {info.passing_score}% -

- )} -
- ); - })()} -
- - ); -}; - -export default PassQuiz; diff --git a/src/modules/client/pages/ViewTask.jsx b/src/modules/client/pages/ViewTask.jsx index 42b936f..2295991 100644 --- a/src/modules/client/pages/ViewTask.jsx +++ b/src/modules/client/pages/ViewTask.jsx @@ -11,7 +11,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import { Trophy, Clock, Paperclip, Plus, Link, BookOpen, LayoutList, FileText, House, - Image, Video, Music, PenLine, ClipboardCheck, Hourglass, XCircle, + Image, Video, Music, PenLine, Hourglass, XCircle, } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; @@ -29,7 +29,6 @@ import VisitLink from '../components/blocks/VisitLink'; import ReadCourse from '../components/blocks/ReadCourse'; import ReadUnit from '../components/blocks/ReadUnit'; import ReadLesson from '../components/blocks/ReadLesson'; -import PassQuiz from '../components/blocks/PassQuiz'; import { useTask } from '@/contexts/ClientTaskContext'; import { PageMeta } from '@/contexts/MetadataContext'; @@ -86,16 +85,6 @@ const RequirementsStatusPanel = ({ requirements = [], latestCompletion, isVisite return { done: done ? 1 : 0, total: 1, binary: true }; }, }, - { - key: 'pass_quiz', - label: 'Pass quizzes', - icon: , - getValue: () => { - const reqs = requirements.filter((r) => r.type === 'pass_quiz'); - const done = reqs.filter((r) => isCompleted(r.requirement_id, r.reference_id)).length; - return { done, total: reqs.length, binary: false }; - }, - }, { key: 'visit_link', label: 'Visit links', @@ -402,7 +391,6 @@ const ViewTask = () => { const readCourseReqs = requirements.filter((r) => r.type === 'read_course'); const readUnitReqs = requirements.filter((r) => r.type === 'read_unit'); const readLessonReqs = requirements.filter((r) => r.type === 'read_lesson'); - const passQuizReqs = requirements.filter((r) => r.type === 'pass_quiz'); const breadcrumbItems = [ { label: 'Home', icon: , to: '/dashboard' }, @@ -601,21 +589,6 @@ const ViewTask = () => { /> )} - {/* pass_quiz */} - {passQuizReqs.length > 0 && ( - ({ - id: r.requirement_id, - requirement_id: r.requirement_id, - reference_id: r.reference_id, - title: r.reference_label ?? 'Quiz', - completed: isCompleted(r.requirement_id, r.reference_id), - }))} - groupId={groupId} - taskListId={taskListId} - taskId={taskId} - /> - )} )}