diff --git a/src/components/generic/Dialogs/Client/InfoDialog.jsx b/src/components/generic/Dialogs/Client/InfoDialog.jsx new file mode 100644 index 0000000..dc2dafe --- /dev/null +++ b/src/components/generic/Dialogs/Client/InfoDialog.jsx @@ -0,0 +1,50 @@ +// ─── components/generic/Dialogs/Client/InfoDialog.jsx ──────────────────────── +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Info } from "lucide-react"; + +/** + * Generic one-time informational dialog for client-side validation notices — + * e.g. "this course has no assessment yet". Purely informational: a single + * acknowledgement button, no destructive action. + * + * Usage: + * + */ +export function InfoDialog({ + open, + onOpenChange, + title, + description, + icon: Icon = Info, + actionLabel = "Continue", +}) { + return ( + + + + + + {title} + + {description} + + + + + + + ); +} diff --git a/src/modules/client/pages/AccountSettings.jsx b/src/modules/client/pages/AccountSettings.jsx index 2b2a063..dff543f 100644 --- a/src/modules/client/pages/AccountSettings.jsx +++ b/src/modules/client/pages/AccountSettings.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { KeyRound, CreditCard, Mail, Megaphone, ChevronRight, Eye, EyeOff, ShieldAlert, Trash2 } from "lucide-react"; +import { KeyRound, CreditCard, Mail, Megaphone, Info, ChevronRight, Eye, EyeOff, ShieldAlert, Trash2 } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; @@ -283,6 +283,57 @@ function NewsletterSection() { ); } +// ─── Course Notices ─────────────────────────────────────────────────────────── +// One-time informational dialogs shown while studying (e.g. InfoDialog in +// UnitList.jsx) — this list grows as more generic client notices are added. + +const NOTICE_OPTIONS = [ + { + key: "show_course_notices", + label: "Course notices", + description: "Informational pop-ups about course readiness, such as when an assessment hasn't been built yet.", + }, +]; + +function CourseNoticesSection() { + const { profile, getProfile, updateProfile, profileLoading } = useProfile(); + + useEffect(() => { + getProfile(); + }, []); + + const handleToggle = async (key, value) => { + const result = await updateProfile({ [key]: value }); + if (result?.success) { + toast.success("Preference saved."); + } + }; + + return ( +
+ {NOTICE_OPTIONS.map((opt, i) => ( +
+
+
+

{opt.label}

+

{opt.description}

+
+ {profileLoading ? ( + + ) : ( + handleToggle(opt.key, v)} + /> + )} +
+ {i < NOTICE_OPTIONS.length - 1 && } +
+ ))} +
+ ); +} + // ─── Advertisements ─────────────────────────────────────────────────────────── const AD_OPTIONS = [ @@ -492,6 +543,10 @@ export default function AccountSettings() { +
+ +
+
diff --git a/src/modules/client/pages/UnitList.jsx b/src/modules/client/pages/UnitList.jsx index 9db6c1c..710c1d7 100644 --- a/src/modules/client/pages/UnitList.jsx +++ b/src/modules/client/pages/UnitList.jsx @@ -12,12 +12,14 @@ import { import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"; import ResponsiveModal from "@/components/generic/ResponsiveModal"; +import { InfoDialog } from "@/components/generic/Dialogs/Client/InfoDialog"; import LessonBlock from "../components/LessonBlock.jsx"; import QuizBlock from "../components/blocks/QuizBlock.jsx"; import CourseCompleteBlock from "../components/blocks/CourseCompleteBlock.jsx"; import { useClientCourses } from "@/contexts/ClientCoursesContext"; import { PageMeta } from "@/contexts/MetadataContext"; import { useCourseReadingProgress } from "@/contexts/ClientCourseReadingProgressContext"; +import { useProfile } from "@/contexts/ProfileProvider"; import { Skeleton } from "@/components/ui/skeleton"; import { toast } from "sonner"; import api from "@/utils/api.util"; @@ -312,6 +314,8 @@ const UnitList = () => { resetProgress, } = useCourseReadingProgress(); + const { profile, getProfile } = useProfile(); + // Tracks which lessons have been marked completed this session to avoid duplicate calls const completedSessionRef = useRef(new Set()); @@ -329,6 +333,29 @@ const UnitList = () => { const resetCompletion = () => setSelectedCompletion(false); + // ── No-assessment notice — shown once per course visit, informational only ── + // Gated behind the "Course notices" toggle in Account Settings. + const [noAssessmentDialogOpen, setNoAssessmentDialogOpen] = useState(false); + const noAssessmentNoticeShown = useRef(false); + + useEffect(() => { + getProfile(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + useEffect(() => { + noAssessmentNoticeShown.current = false; + }, [courseId]); + + useEffect(() => { + const shouldShow = + course && !courseBlocked && course.duration_seconds && + !course.assessment && !noAssessmentNoticeShown.current && + profile && (profile.personal_info?.show_course_notices ?? true); + if (!shouldShow) return; + noAssessmentNoticeShown.current = true; + setNoAssessmentDialogOpen(true); + }, [course, courseBlocked, profile]); + // ── Session guard — block navigation while a quiz/assessment is in progress ─ const quizActiveRef = useRef(false); // sync check inside handlers const [quizSessionActive, setQuizSessionActive] = useState(false); @@ -780,6 +807,14 @@ const UnitList = () => { + {/* ── No-assessment notice — informational, shown once per course visit ── */} + + {/* ── Task-mode banner ─────────────────────────────────────────── */} {taskCtx?.has_task && (