mirror of
https://github.com/rgrgogu/new_starr.git
synced 2026-09-27 00:12:54 +08:00
fix: throttle request issue per video,audio as indicated requirements in units and lessons
Signed-off-by: rgrgogu <obsequio.rus@gmail.com>
This commit is contained in:
@@ -5,12 +5,13 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useEffect } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useLibrary } from "@/contexts/ClientLibraryContext";
|
||||
import { useClientTiers } from "@/contexts/ClientTiersProvider";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import LockedContentPanel from "@/modules/client/components/LockedContentPanel";
|
||||
import LessonBlock from "../components/LessonBlock.jsx";
|
||||
import { TYPE_DEFS } from "@/modules/admin/components/courses/completionRequirementTypes";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -30,6 +31,7 @@ const LessonDetails = () => {
|
||||
|
||||
const {
|
||||
getLesson, lesson, lessonLoading, unitBlocked, unitBlockedInfo, resetLesson,
|
||||
upsertWatchProgress,
|
||||
} = useLibrary();
|
||||
const { tierMap, getTierCategories } = useClientTiers();
|
||||
|
||||
@@ -46,6 +48,10 @@ const LessonDetails = () => {
|
||||
const blockTypes = new Set((lesson?.blocks ?? []).map((b) => b.type));
|
||||
const isVideoLesson = blockTypes.has("video") || blockTypes.has("text-video");
|
||||
|
||||
// Admin-configured completion requirement (null when nothing's set — default behavior).
|
||||
const requirementDef = lesson?.completion?.type ? TYPE_DEFS[lesson.completion.type] : null;
|
||||
const RequirementIcon = requirementDef?.icon;
|
||||
|
||||
useEffect(() => {
|
||||
getTierCategories();
|
||||
getLesson(uuid);
|
||||
@@ -53,6 +59,13 @@ const LessonDetails = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [uuid]);
|
||||
|
||||
// watch_percent / watch_video / listen_audio — safe to always pass through, the
|
||||
// backend no-ops whichever type (if any) isn't configured on this lesson.
|
||||
const handleWatchProgress = useCallback((percent, meta) => {
|
||||
if (!lesson?.uuid) return;
|
||||
upsertWatchProgress(lesson.uuid, unit?.uuid ?? null, percent, meta);
|
||||
}, [lesson, unit, upsertWatchProgress]);
|
||||
|
||||
const items = [
|
||||
{ label: "Home", icon: <House className="size-4" />, to: `/dashboard` },
|
||||
{ label: "Lessons", to: `/lessons` },
|
||||
@@ -95,16 +108,32 @@ const LessonDetails = () => {
|
||||
<h1 className="font-bold xs:text-2xl lg:text-3xl">{lesson.title}</h1>
|
||||
<p className="text-muted-foreground">{lesson.description ?? ""}</p>
|
||||
{!hasCourse && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground [&_svg]:size-4">
|
||||
{formatDuration(lesson.duration_seconds) && (
|
||||
<>
|
||||
<span className="flex items-center gap-1"><Clock /> {formatDuration(lesson.duration_seconds)}</span>
|
||||
<span>·</span>
|
||||
</>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground [&_svg]:size-4">
|
||||
{formatDuration(lesson.duration_seconds) && (
|
||||
<>
|
||||
<span className="flex items-center gap-1"><Clock /> {formatDuration(lesson.duration_seconds)}</span>
|
||||
<span>·</span>
|
||||
</>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<Video /> {isVideoLesson ? "Video lesson" : "Reading lesson"}
|
||||
</span>
|
||||
{requirementDef && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1">
|
||||
{RequirementIcon && <RequirementIcon />} {requirementDef.label}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{hasCompleted && (
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-emerald-600 dark:text-emerald-400 w-fit">
|
||||
<CheckCheck className="size-4" />
|
||||
Success — you've completed this lesson.
|
||||
</div>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<Video /> {isVideoLesson ? "Video lesson" : "Reading lesson"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -139,7 +168,7 @@ const LessonDetails = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<LessonBlock lesson={lesson} loading={lessonLoading} showHeader={false} />
|
||||
<LessonBlock lesson={lesson} loading={lessonLoading} showHeader={false} onWatchProgress={handleWatchProgress} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import AppBreadcrumb from "@/components/generic/Breadcrumb/AppBreadcrumb";
|
||||
import { useParams, useNavigate } from "react-router-dom";
|
||||
import {
|
||||
House, Timer, CheckCircle2, Check, ClipboardList, Hourglass,
|
||||
House, Timer, CheckCircle2, Check, CheckCheck, ClipboardList, Hourglass,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -14,6 +14,7 @@ import { useLibrary } from "@/contexts/ClientLibraryContext";
|
||||
import { useClientTiers } from "@/contexts/ClientTiersProvider";
|
||||
import { PageMeta } from "@/contexts/MetadataContext";
|
||||
import LockedContentPanel from "@/modules/client/components/LockedContentPanel";
|
||||
import { TYPE_DEFS } from "@/modules/admin/components/courses/completionRequirementTypes";
|
||||
|
||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -157,6 +158,10 @@ const UnitDetails = () => {
|
||||
const currentLesson = lessons.find((l) => l.status !== "completed") ?? null;
|
||||
const progressPct = lessons.length > 0 ? Math.round((completedCount / lessons.length) * 100) : 0;
|
||||
|
||||
// Admin-configured completion requirement for the unit itself (null when nothing's set).
|
||||
const requirementDef = unitDetail?.completion?.type ? TYPE_DEFS[unitDetail.completion.type] : null;
|
||||
const RequirementIcon = requirementDef?.icon;
|
||||
|
||||
const handleLessonClick = (lesson) => {
|
||||
navigate(`/lessons/${lesson.uuid}`);
|
||||
};
|
||||
@@ -187,14 +192,28 @@ const UnitDetails = () => {
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground [&_svg]:size-4">
|
||||
<div className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground [&_svg]:size-4">
|
||||
<span>{lessons.length} {lessons.length === 1 ? "lesson" : "lessons"}</span>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1"><Timer /> {formatDuration(unitDetail.duration_seconds) ?? "—"} total</span>
|
||||
<span>·</span>
|
||||
<span className="text-blue-600 dark:text-blue-400 font-medium">{completedCount} of {lessons.length} complete</span>
|
||||
{requirementDef && (
|
||||
<>
|
||||
<span>·</span>
|
||||
<span className="flex items-center gap-1">
|
||||
{RequirementIcon && <RequirementIcon />} {requirementDef.label}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Progress value={progressPct} />
|
||||
{unitDetail.is_completed && (
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium text-emerald-600 dark:text-emerald-400 w-fit">
|
||||
<CheckCheck className="size-4" />
|
||||
Success — you've completed this unit.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
|
||||
@@ -81,7 +81,7 @@ const UnitReader = () => {
|
||||
unitDetail, unitDetailLoading, unitBlocked, getUnitDetail, resetUnitDetail,
|
||||
lesson, lessonLoading, getLesson, resetLesson,
|
||||
quiz, quizLoading, getUnitQuiz, resetQuiz, submitUnitQuiz, saveUnitQuizDraft,
|
||||
upsertLessonProgress,
|
||||
upsertLessonProgress, upsertWatchProgress,
|
||||
} = useLibrary();
|
||||
|
||||
// Tracks which lessons have been marked completed this session to avoid duplicate calls
|
||||
@@ -273,6 +273,13 @@ const UnitReader = () => {
|
||||
saveUnitQuizDraft(uuid, selectedQuizId, answers);
|
||||
}, [uuid, selectedQuizId, saveUnitQuizDraft]);
|
||||
|
||||
// watch_percent / watch_video / listen_audio — safe to always pass through, the
|
||||
// backend no-ops whichever type (if any) isn't configured on this lesson.
|
||||
const handleWatchProgress = useCallback((percent, meta) => {
|
||||
if (!lesson?.uuid) return;
|
||||
upsertWatchProgress(lesson.uuid, uuid, percent, meta);
|
||||
}, [lesson, uuid, upsertWatchProgress]);
|
||||
|
||||
// ── Next content item ──────────────────────────────────────────────────
|
||||
const getNextContent = useCallback(() => {
|
||||
const idx = allContent.findIndex((item) =>
|
||||
@@ -482,7 +489,7 @@ const UnitReader = () => {
|
||||
nextLabel={nextLabel}
|
||||
/>
|
||||
) : (
|
||||
<LessonBlock lesson={lesson} loading={lessonLoading} />
|
||||
<LessonBlock lesson={lesson} loading={lessonLoading} onWatchProgress={handleWatchProgress} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user