diff --git a/src/modules/admin/pages/assets/AddAsset.jsx b/src/modules/admin/pages/assets/AddAsset.jsx
index e7cf541..29350e8 100644
--- a/src/modules/admin/pages/assets/AddAsset.jsx
+++ b/src/modules/admin/pages/assets/AddAsset.jsx
@@ -13,6 +13,7 @@ import { useUploadQueue } from "@/contexts/UploadQueueContext";
import { useAuth } from "@/contexts/AuthContext";
import { MAX_ASSET_FILE_SIZE_SINGLE, MAX_ASSET_FILE_SIZE_SINGLE_LABEL } from "@/utils/assetUpload.util";
import { formatFileSize } from "@/utils/format.util";
+import { isQasEnv, QAS_UPLOAD_LIMIT_MB } from "@/utils/environment.util";
import { useUnsavedChangesGuard } from "@/hooks/useUnsavedChangesGuard";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
@@ -239,6 +240,11 @@ export default function AddAsset() {
error={errors._file?.message}
/>
+ {isQasEnv && (
+
+ Self-hosted staging environment — uploads over {QAS_UPLOAD_LIMIT_MB}MB may fail (Cloudflare limit).
+
+ )}
+ {isQasEnv && (
+
+ Self-hosted staging environment — uploads over {QAS_UPLOAD_LIMIT_MB}MB may fail (Cloudflare limit).
+
+ )}
{/* ── File list ── */}
{total > 0 && (
diff --git a/src/modules/client/components/blocks/FileUpload.jsx b/src/modules/client/components/blocks/FileUpload.jsx
index 3d785af..c32ea6e 100644
--- a/src/modules/client/components/blocks/FileUpload.jsx
+++ b/src/modules/client/components/blocks/FileUpload.jsx
@@ -13,6 +13,7 @@ import {
} from "lucide-react";
import { cn } from "@/lib/utils";
import { toast } from "sonner";
+import { isQasEnv, QAS_UPLOAD_LIMIT_MB } from "@/utils/environment.util";
// ── Helpers ───────────────────────────────────────────────────────────────────
function formatBytes(b) {
@@ -271,6 +272,11 @@ const FileUpload = ({
Drop files here or click to browse
Upload your work to submit with this task
{derivedHint}
+ {isQasEnv && (
+
+ Self-hosted staging environment — uploads over {QAS_UPLOAD_LIMIT_MB}MB may fail (Cloudflare limit).
+
+ )}
)}
@@ -303,6 +309,11 @@ const FileUpload = ({
)}
{derivedHint}
+ {isQasEnv && (
+
+ Self-hosted staging environment — uploads over {QAS_UPLOAD_LIMIT_MB}MB may fail (Cloudflare limit).
+
+ )}
)}
diff --git a/src/modules/client/layout/ClientLayout.jsx b/src/modules/client/layout/ClientLayout.jsx
index d62f44f..b921ca6 100644
--- a/src/modules/client/layout/ClientLayout.jsx
+++ b/src/modules/client/layout/ClientLayout.jsx
@@ -38,6 +38,8 @@ import { AVATAR_COLORS } from "@/data/profile.data"
import { Badge } from "@/components/ui/badge"
import ClientNotificationBell from "@/components/generic/ClientNotificationBell"
import StickyAnnouncementBar from "@/components/generic/StickyAnnouncementBar"
+import EnvironmentBanner from "@/components/generic/EnvironmentBanner"
+import { isQasEnv } from "@/utils/environment.util"
import { QRCodeCanvas } from "qrcode.react"
// ─── Refer / Invite dialog ────────────────────────────────────────────────────
@@ -443,6 +445,7 @@ const ClientLayout = () => {
diff --git a/src/modules/client/pages/UnitList.jsx b/src/modules/client/pages/UnitList.jsx
index 044afe4..bb29416 100644
--- a/src/modules/client/pages/UnitList.jsx
+++ b/src/modules/client/pages/UnitList.jsx
@@ -30,7 +30,7 @@ import { TYPE_DEFS } from "@/modules/admin/components/courses/completionRequirem
const QuizPrerequisiteGate = ({ previousQuizzes, units, onQuizClick }) => {
const [open, setOpen] = useState(false);
- const passed = previousQuizzes.filter((q) => q.has_passed).length;
+ const passed = previousQuizzes.filter((q) => q.has_passed).length;
const useModal = previousQuizzes.length > QUIZ_MODAL_THRESHOLD;
return (
@@ -95,11 +95,10 @@ const QuizGateList = ({ requiredQuizzes, units, onQuizClick, onItemClick }) => (
onQuizClick({ unit: units.find((u) => u.unit_id === q.unitId), quiz: { quiz_id: q.quizId } });
onItemClick?.();
}}
- className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${
- q.has_passed
+ className={`flex items-center gap-3 p-3 rounded-lg border transition-colors ${q.has_passed
? "bg-emerald-50 dark:bg-emerald-900/20 border-emerald-200 dark:border-emerald-800"
: "border-border hover:bg-muted cursor-pointer"
- }`}
+ }`}
>
{q.has_passed
?
@@ -207,20 +206,20 @@ const SidebarContent = ({
{(unit.lessons ?? []).map((lesson) => {
const lessonDone = getLessonCompleted?.(lesson);
return (
-
onLessonClick({ unit, lesson })}
- className={`flex items-center gap-2 pl-4 pr-3 py-1.5 text-sm rounded-md hover:bg-muted-foreground/10 hover:text-foreground cursor-pointer transition-colors ${selectedLessonId === lesson.lesson_id
- ? "bg-muted-foreground/10 text-foreground font-medium"
- : "text-muted-foreground"
- }`}
- >
- {lessonDone
- ?
- :
- }
- {lesson.title}
-
+ onLessonClick({ unit, lesson })}
+ className={`flex items-center gap-2 pl-4 pr-3 py-1.5 text-sm rounded-md hover:bg-muted-foreground/10 hover:text-foreground cursor-pointer transition-colors ${selectedLessonId === lesson.lesson_id
+ ? "bg-muted-foreground/10 text-foreground font-medium"
+ : "text-muted-foreground"
+ }`}
+ >
+ {lessonDone
+ ?
+ :
+ }
+ {lesson.title}
+
);
})}
{unit.quiz && (() => {
@@ -228,21 +227,20 @@ const SidebarContent = ({
return (
onQuizClick({ unit, quiz: unit.quiz })}
- className={`flex items-center gap-2 pl-6 pr-3 py-1.5 text-sm rounded-md cursor-pointer transition-colors ${
- selectedQuizId === unit.quiz.quiz_id
+ className={`flex items-center gap-2 pl-6 pr-3 py-1.5 text-sm rounded-md cursor-pointer transition-colors ${selectedQuizId === unit.quiz.quiz_id
? "bg-muted-foreground/10 text-foreground font-medium"
: unit.quiz.has_passed
- ? "text-emerald-600 dark:text-emerald-400 hover:bg-muted-foreground/10"
- : quizLocked
- ? "text-muted-foreground/50 hover:bg-muted-foreground/5"
- : "text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground"
- }`}
+ ? "text-emerald-600 dark:text-emerald-400 hover:bg-muted-foreground/10"
+ : quizLocked
+ ? "text-muted-foreground/50 hover:bg-muted-foreground/5"
+ : "text-muted-foreground hover:bg-muted-foreground/10 hover:text-foreground"
+ }`}
>
{unit.quiz.has_passed
?
: quizLocked
- ?
- :
+ ?
+ :
}
{"Quiz"}
@@ -335,6 +333,7 @@ const UnitList = () => {
const [sidebarOpen, setSidebarOpen] = useState(false);
const [desktopSidebarOpen, setDesktopSidebarOpen] = useState(true);
const [helpOpen, setHelpOpen] = useState(false);
+ const [taskModeOpen, setTaskModeOpen] = useState(false);
const resetCompletion = () => setSelectedCompletion(false);
@@ -373,19 +372,19 @@ const UnitList = () => {
const courseNoticeTitle = courseNoticeMissingQuiz && courseNoticeNoAssessment
? "Quizzes & Assessment Coming Soon"
: courseNoticeMissingQuiz
- ? "Quizzes Coming Soon"
- : "Assessment Coming Soon";
+ ? "Quizzes Coming Soon"
+ : "Assessment Coming Soon";
const courseNoticeDescription = courseNoticeMissingQuiz && courseNoticeNoAssessment
? "Some units in this course don't have a quiz yet, and this course does not have an assessment built yet — we are currently working on both. You may proceed with your studies. Enjoy!"
: courseNoticeMissingQuiz
- ? "Some units in this course don't have a quiz yet — we are currently working on it. You may proceed with your studies. Enjoy!"
- : "This course does not have an assessment built yet — we are currently working on it. You may proceed with your studies. Enjoy!";
+ ? "Some units in this course don't have a quiz yet — we are currently working on it. You may proceed with your studies. Enjoy!"
+ : "This course does not have an assessment built yet — we are currently working on it. You may proceed with your studies. Enjoy!";
// ── Session guard — block navigation while a quiz/assessment is in progress ─
- const quizActiveRef = useRef(false); // sync check inside handlers
+ const quizActiveRef = useRef(false); // sync check inside handlers
const [quizSessionActive, setQuizSessionActive] = useState(false);
- const [pendingNav, setPendingNav] = useState(null); // deferred nav fn
+ const [pendingNav, setPendingNav] = useState(null); // deferred nav fn
const setQuizActive = useCallback((active) => {
quizActiveRef.current = active;
@@ -596,7 +595,7 @@ const UnitList = () => {
.then(({ data }) => {
if (data?.data?.has_task) setTaskCtx(data.data);
})
- .catch(() => {}); // non-critical — silently swallow
+ .catch(() => { }); // non-critical — silently swallow
}, [courseId]);
// ── Toast when a task's read requirements are all done ────────────────
@@ -693,8 +692,8 @@ const UnitList = () => {
label: selectedCompletion
? "Course Complete"
: selectedAssessment
- ? "Course Assessment"
- : currentUnit ? `Unit ${(course?.units?.indexOf(currentUnit) ?? 0) + 1}: ${currentUnit.title}` : "Select a lesson"
+ ? "Course Assessment"
+ : currentUnit ? `Unit ${(course?.units?.indexOf(currentUnit) ?? 0) + 1}: ${currentUnit.title}` : "Select a lesson"
},
];
const breadcrumbItemsMobile = [
@@ -823,9 +822,9 @@ const UnitList = () => {
const units = course?.units ?? [];
const nextContent = getNextContent();
const nextLabel = nextContent
- ? nextContent.type === "lesson" ? nextContent.lesson.title
- : nextContent.type === "quiz" ? (nextContent.quiz.title || "Quiz")
- : (nextContent.assessment?.title || "Course Assessment")
+ ? nextContent.type === "lesson" ? nextContent.lesson.title
+ : nextContent.type === "quiz" ? (nextContent.quiz.title || "Quiz")
+ : (nextContent.assessment?.title || "Course Assessment")
: null;
const showSidebarContent = desktopSidebarOpen && !quizSessionActive;
@@ -973,20 +972,21 @@ const UnitList = () => {
- {/* ── Task-mode banner ─────────────────────────────────────────── */}
- {taskCtx?.has_task && (
-
-
-
- {course?.is_completed
- ? 'Course complete — tracking finished'
- : 'Task mode — reading progress is being tracked automatically'
- }
-
+ {/* ── "Task mode" info dialog ── */}
+
setTaskModeOpen(false)}>Got it}
+ >
+
+
+ This course is part of one of your assigned tasks .
+ Your reading and completion progress here is being tracked automatically — no need to do anything extra.
+
- )}
+
{/* ── Up next after passing a quiz ── */}
{selectedQuizId && quiz?.has_passed && !quizSessionActive && nextContent && (
@@ -999,7 +999,7 @@ const UnitList = () => {
Up next
{nextContent.type === "lesson" ? nextContent.lesson.title
: nextContent.type === "quiz" ? (nextContent.quiz.title || "Quiz")
- : (nextContent.assessment?.title || "Course Assessment")}
+ : (nextContent.assessment?.title || "Course Assessment")}