diff --git a/src/api/courses.api.ts b/src/api/courses.api.ts index 3e14ec3..6cc1d35 100644 --- a/src/api/courses.api.ts +++ b/src/api/courses.api.ts @@ -118,14 +118,21 @@ export const deleteSubCourseVideo = (videoId: number) => http.delete(`/course-management/sub-course-videos/${videoId}`) // Practice APIs - for SubCourse practices (New Hierarchy) -// Practices are sourced from question sets by owner_type=SUB_COURSE. +// Practices are question sets: POST /question-sets with set_type: "PRACTICE", owner_type: "SUB_COURSE". export const getPracticesBySubCourse = (subCourseId: number) => http.get("/question-sets/by-owner", { params: { owner_type: "SUB_COURSE", owner_id: subCourseId }, }) export const createPractice = (data: CreatePracticeRequest) => - http.post("/course-management/practices", data) + http.post("/question-sets", { + title: data.title, + set_type: "PRACTICE", + owner_type: "SUB_COURSE", + owner_id: data.sub_course_id, + ...(data.description?.trim() ? { description: data.description.trim() } : {}), + ...(data.persona ? { persona: data.persona } : {}), + }) export const updatePractice = (practiceId: number, data: UpdatePracticeRequest) => http.put(`/course-management/practices/${practiceId}`, data) diff --git a/src/pages/content-management/AddNewPracticePage.tsx b/src/pages/content-management/AddNewPracticePage.tsx index 0ae8876..5dc295e 100644 --- a/src/pages/content-management/AddNewPracticePage.tsx +++ b/src/pages/content-management/AddNewPracticePage.tsx @@ -1,10 +1,12 @@ -import { useState } from "react" +import { useRef, useState, type ChangeEvent } from "react" import { Link, useParams, useNavigate } from "react-router-dom" -import { ArrowLeft, ArrowRight, ChevronDown, Grid3X3, Check, Plus, Trash2, GripVertical, X, Edit, Rocket } from "lucide-react" +import { ArrowLeft, ArrowRight, ChevronDown, Grid3X3, Check, Plus, Trash2, GripVertical, X, Edit, Rocket, Loader2, Upload } from "lucide-react" +import { toast } from "sonner" import { Card } from "../../components/ui/card" import { Button } from "../../components/ui/button" import { Input } from "../../components/ui/input" import { createQuestionSet, createQuestion, addQuestionToSet } from "../../api/courses.api" +import { uploadVideoFile } from "../../api/files.api" import { Select } from "../../components/ui/select" import type { QuestionOption } from "../../types/course.types" @@ -56,6 +58,18 @@ const STEPS = [ { number: 4, label: "Review" }, ] +/** Prefer direct storage URL; for Vimeo pipeline match SubCourseContentPage player URL shape. */ +function introVideoUrlFromUploadResponse(data: { url?: string; embed_url?: string } | undefined): string | null { + if (!data) return null + const pageUrl = data.url?.trim() + const embedUrl = data.embed_url?.trim() + if (embedUrl) { + const hashFromUrl = pageUrl ? pageUrl.split("/").filter(Boolean).at(-1) : undefined + return hashFromUrl ? `${embedUrl}?h=${hashFromUrl}` : embedUrl + } + return pageUrl || null +} + function createEmptyQuestion(id: string): Question { return { id, @@ -89,6 +103,9 @@ export function AddNewPracticePage() { const [selectedCourse] = useState("B2") const [practiceTitle, setPracticeTitle] = useState("") const [practiceDescription, setPracticeDescription] = useState("") + const [introVideoUrl, setIntroVideoUrl] = useState("") + const [uploadingIntroVideo, setUploadingIntroVideo] = useState(false) + const introVideoFileInputRef = useRef(null) const [shuffleQuestions, setShuffleQuestions] = useState(false) const [passingScore, setPassingScore] = useState(50) const [timeLimitMinutes, setTimeLimitMinutes] = useState(60) @@ -120,6 +137,29 @@ export function AddNewPracticePage() { navigate(`/content/category/${categoryId}/courses/${courseId}/sub-courses/${subCourseId}`) } + const handleIntroVideoFileChange = async (event: ChangeEvent) => { + const file = event.target.files?.[0] + event.target.value = "" + if (!file) return + + setUploadingIntroVideo(true) + try { + const uploadRes = await uploadVideoFile(file, { + title: practiceTitle.trim() || file.name.replace(/\.[^.]+$/, "") || "Practice intro", + description: practiceDescription.trim() || undefined, + }) + const finalUrl = introVideoUrlFromUploadResponse(uploadRes.data?.data) + if (!finalUrl) throw new Error("Missing uploaded video url") + setIntroVideoUrl(finalUrl) + toast.success("Intro video uploaded", { description: "The URL has been filled in for you." }) + } catch (error) { + console.error("Failed to upload intro video:", error) + toast.error("Failed to upload intro video") + } finally { + setUploadingIntroVideo(false) + } + } + const addQuestion = () => { setQuestions([...questions, createEmptyQuestion(String(Date.now()))]) } @@ -170,15 +210,16 @@ export function AddNewPracticePage() { const persona = PERSONAS.find(p => p.id === selectedPersona) const setRes = await createQuestionSet({ title: practiceTitle || "Untitled Practice", - description: practiceDescription, set_type: "PRACTICE", owner_type: "SUB_COURSE", owner_id: Number(subCourseId), - persona: persona?.name, + ...(practiceDescription.trim() ? { description: practiceDescription.trim() } : {}), + ...(persona?.name ? { persona: persona.name } : {}), shuffle_questions: shuffleQuestions, status, passing_score: passingScore, time_limit_minutes: timeLimitMinutes, + ...(introVideoUrl.trim() ? { intro_video_url: introVideoUrl.trim() } : {}), }) const questionSetId = setRes.data?.data?.id @@ -250,7 +291,8 @@ export function AddNewPracticePage() { } return ( -
+
+
{currentStep !== 5 && ( <> {/* Back Link */} @@ -263,9 +305,9 @@ export function AddNewPracticePage() { {/* Header */} -
-

Add New Practice

-

+

+

Add New Practice

+

Create a new immersive practice session for students.

@@ -274,12 +316,12 @@ export function AddNewPracticePage() { {/* Step Tracker */} {currentStep !== 5 && ( -
+
{STEPS.map((step, index) => (
step.number @@ -290,7 +332,7 @@ export function AddNewPracticePage() { {currentStep > step.number ? : step.number}
step.number @@ -303,7 +345,7 @@ export function AddNewPracticePage() {
{index < STEPS.length - 1 && (
step.number ? "bg-brand-500" : "bg-grayScale-200" }`} /> @@ -315,111 +357,170 @@ export function AddNewPracticePage() { {/* Step Content */} {currentStep === 1 && ( - -

Step 1: Context Definition

-

- Define the educational level and curriculum module for this practice. -

+ +
+

Step 1: Context

+

+ Define details and rules for this practice. Curriculum context is shown on the right. +

+
-
- {/* Practice Title */} -
- - setPracticeTitle(e.target.value)} - placeholder="Enter practice title" - /> -
+
+
+
+
+ + setPracticeTitle(e.target.value)} + placeholder="Enter practice title" + className="h-11" + /> +
- {/* Practice Description */} -
- -