feat(module): Add practice button on each lesson card
- VideoCard: optional onAddPractice for lesson-scoped CTA - ModuleDetailPage: navigate to add-practice with lessonId and lessonTitle - AddPracticeFlow: show context banner when opened from a lesson Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
77b71abfd8
commit
bb6680acfb
|
|
@ -23,6 +23,17 @@ export function AddPracticeFlow() {
|
||||||
const backTo = searchParams.get("backTo");
|
const backTo = searchParams.get("backTo");
|
||||||
const courseId = searchParams.get("courseId");
|
const courseId = searchParams.get("courseId");
|
||||||
const moduleId = searchParams.get("moduleId");
|
const moduleId = searchParams.get("moduleId");
|
||||||
|
const lessonId = searchParams.get("lessonId");
|
||||||
|
const lessonTitleRaw = searchParams.get("lessonTitle");
|
||||||
|
const lessonTitleDisplay = (() => {
|
||||||
|
const raw = lessonTitleRaw?.trim();
|
||||||
|
if (!raw) return null;
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(raw);
|
||||||
|
} catch {
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
const isModuleContext = backTo === "module";
|
const isModuleContext = backTo === "module";
|
||||||
const isCourseContext = backTo === "modules";
|
const isCourseContext = backTo === "modules";
|
||||||
|
|
@ -251,6 +262,23 @@ export function AddPracticeFlow() {
|
||||||
<p className="text-grayScale-400 text-base">
|
<p className="text-grayScale-400 text-base">
|
||||||
Create a new immersive practice session for students.
|
Create a new immersive practice session for students.
|
||||||
</p>
|
</p>
|
||||||
|
{lessonId ? (
|
||||||
|
<div className="mt-4 rounded-xl border border-violet-200 bg-violet-50/80 px-4 py-3 text-sm text-violet-950">
|
||||||
|
<p className="font-semibold text-violet-900">Practice for this lesson</p>
|
||||||
|
<p className="mt-1 text-violet-800/90">
|
||||||
|
This session will be associated with lesson{" "}
|
||||||
|
<span className="font-mono font-bold text-violet-950">#{lessonId}</span>
|
||||||
|
{lessonTitleDisplay ? (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
— <span className="font-medium">{lessonTitleDisplay}</span>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
. The module-level flow still uses the same steps; use this context when naming and
|
||||||
|
configuring the practice.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx-auto w-[70%] mb-12">
|
<div className="mx-auto w-[70%] mb-12">
|
||||||
|
|
|
||||||
|
|
@ -398,6 +398,11 @@ export function ModuleDetailPage() {
|
||||||
thumbnailGradient={LESSON_THUMB_GRADIENTS[i % LESSON_THUMB_GRADIENTS.length]}
|
thumbnailGradient={LESSON_THUMB_GRADIENTS[i % LESSON_THUMB_GRADIENTS.length]}
|
||||||
onEdit={() => openEditLesson(lesson)}
|
onEdit={() => openEditLesson(lesson)}
|
||||||
onDelete={() => setDeletingLesson(lesson)}
|
onDelete={() => setDeletingLesson(lesson)}
|
||||||
|
onAddPractice={() =>
|
||||||
|
navigate(
|
||||||
|
`/new-content/learn-english/${level}/courses/add-practice?backTo=module&courseId=${courseId}&moduleId=${moduleId}&lessonId=${lesson.id}&lessonTitle=${encodeURIComponent(lesson.title)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { MoreVertical, Edit2, Play, Pencil, Trash2 } from "lucide-react";
|
import { MoreVertical, Edit2, Play, Pencil, Trash2, Calendar } from "lucide-react";
|
||||||
import { Button } from "../../../components/ui/button";
|
import { Button } from "../../../components/ui/button";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -38,6 +38,8 @@ interface VideoCardProps {
|
||||||
hoverModuleActions?: boolean;
|
hoverModuleActions?: boolean;
|
||||||
onEdit?: () => void;
|
onEdit?: () => void;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
|
/** When set (e.g. on module lesson cards), shows an "Add practice" control scoped to this lesson. */
|
||||||
|
onAddPractice?: () => void;
|
||||||
onPublish?: () => void;
|
onPublish?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,6 +53,7 @@ export function VideoCard({
|
||||||
onEdit,
|
onEdit,
|
||||||
onDelete,
|
onDelete,
|
||||||
onPublish,
|
onPublish,
|
||||||
|
onAddPractice,
|
||||||
hoverModuleActions = false,
|
hoverModuleActions = false,
|
||||||
}: VideoCardProps) {
|
}: VideoCardProps) {
|
||||||
const [thumbFailed, setThumbFailed] = useState(false);
|
const [thumbFailed, setThumbFailed] = useState(false);
|
||||||
|
|
@ -342,6 +345,21 @@ export function VideoCard({
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
|
{hoverModuleActions && onAddPractice ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="h-9 w-full shrink-0 rounded-lg border-brand-200 text-[12px] font-bold text-brand-600 hover:bg-brand-50"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onAddPractice();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Calendar className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
|
Add practice
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Actions (footer) — not used for API lesson cards with hover tools */}
|
{/* Actions (footer) — not used for API lesson cards with hover tools */}
|
||||||
{!hoverModuleActions ? (
|
{!hoverModuleActions ? (
|
||||||
<div className="pt-2 space-y-3 mt-auto">
|
<div className="pt-2 space-y-3 mt-auto">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user