import { useCallback, useEffect, useState } from "react"; import { AlertCircle, ArrowLeft, BookOpen, Calendar, Clock, Hash, Loader2, RefreshCw, Sparkles, } from "lucide-react"; import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; import { toast } from "sonner"; import { getPracticesByParentLesson } from "../../api/courses.api"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { Card, CardContent } from "../../components/ui/card"; import type { GetPracticesByParentContextResponse, ParentContextPractice, } from "../../types/course.types"; import { resolveThumbnailForPreview } from "../../lib/videoPreview"; import { cn } from "../../lib/utils"; function unwrapPracticesEnvelope( res: { data?: GetPracticesByParentContextResponse & { Data?: GetPracticesByParentContextResponse["data"] } }, ): GetPracticesByParentContextResponse["data"] | null { const b = res.data; if (!b) return null; return b.data ?? b.Data ?? null; } function formatPracticeDate(iso: string): string { const d = new Date(iso); return Number.isNaN(d.getTime()) ? iso : d.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }); } function PracticeCard({ practice, index, total, }: { practice: ParentContextPractice; index: number; total: number; }) { const [imgFailed, setImgFailed] = useState(false); const thumb = resolveThumbnailForPreview(practice.story_image); const showThumb = Boolean(thumb) && !imgFailed; return (
{showThumb ? ( <> setImgFailed(true)} />
) : (
No cover image
)}
Practice {index + 1} of {total} ID {practice.id}

{practice.title}

{practice.story_description?.trim() ? (

Story & instructions

{practice.story_description}

) : null} {practice.quick_tips?.trim() ? (

Quick tips

{practice.quick_tips}

) : null}
Question set {practice.question_set_id} {formatPracticeDate(practice.created_at)}
); } export function LessonPracticesPage() { const navigate = useNavigate(); const { level, courseId, moduleId, lessonId } = useParams<{ level: string; courseId: string; moduleId: string; lessonId: string; }>(); const [searchParams] = useSearchParams(); const lessonTitle = searchParams.get("lessonTitle")?.trim() || ""; const backHref = `/new-content/learn-english/${level}/courses/${courseId}/modules/${moduleId}`; const [practices, setPractices] = useState([]); const [totalCount, setTotalCount] = useState(0); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(null); const lid = lessonId ? Number(lessonId) : NaN; const validLesson = Number.isFinite(lid) && lid > 0; const load = useCallback(async () => { if (!validLesson) { setLoading(false); setLoadError("Invalid lesson."); setPractices([]); return; } setLoading(true); setLoadError(null); try { const res = await getPracticesByParentLesson(lid, { limit: 100, offset: 0 }); const envelope = unwrapPracticesEnvelope(res); const list = Array.isArray(envelope?.practices) ? envelope.practices : []; setPractices(list); setTotalCount( typeof envelope?.total_count === "number" ? envelope.total_count : list.length, ); } catch { setPractices([]); setTotalCount(0); setLoadError("Could not load practices for this lesson."); toast.error("Failed to load practices"); } finally { setLoading(false); } }, [lid, validLesson]); useEffect(() => { void load(); }, [load]); const displayTitle = lessonTitle || (validLesson ? `Lesson #${lid}` : "Lesson practices"); const addPracticeHref = `/new-content/learn-english/${level}/courses/add-practice?backTo=module&courseId=${courseId}&moduleId=${moduleId}&lessonId=${lid}&lessonTitle=${encodeURIComponent(lessonTitle || displayTitle)}`; return (
Back to module

Lesson practices

{displayTitle}

Review speaking practices linked to this lesson. Thumbnails and copy come from your published practice content.

{!loading && !loadError ? (
{practices.length}{" "} {practices.length === 1 ? "practice" : "practices"} {totalCount > practices.length ? ( Showing {practices.length} of {totalCount} ) : null}
) : null}
{loading ? (

Loading practices

Fetching content for this lesson…

) : loadError ? (

Something went wrong

{loadError}

) : practices.length === 0 ? (

No practices yet

This lesson does not have any linked practices. Create one to give learners a structured speaking activity after the video.

) : (
{practices.map((p, i) => ( ))}

Source:{" "} GET /lessons/{lid}/practices

)}
); }