import { useEffect, useState } from "react" import { Link, useParams, useNavigate } from "react-router-dom" import { ArrowLeft, Plus, FileText, Layers, Edit, Trash2, X, Video, MoreVertical, Play } from "lucide-react" import spinnerSrc from "../../assets/Circular-indeterminate progress indicator.svg" import { Card } from "../../components/ui/card" import alertSrc from "../../assets/Alert.svg" import { Badge } from "../../components/ui/badge" import { Button } from "../../components/ui/button" import { Input } from "../../components/ui/input" import { getSubModulesByCourse, getQuestionSetsByOwner, getVideosBySubModule, updatePractice, deleteQuestionSet, createCourseVideo, updateSubCourseVideo, deleteSubCourseVideo, getVimeoSample, } from "../../api/courses.api" import { uploadVideoFile } from "../../api/files.api" import type { SubCourse, QuestionSet, SubCourseVideo, VimeoSampleVideo, VideoStatus, VideoVisibility, } from "../../types/course.types" import { SpinnerIcon } from "../../components/ui/spinner-icon" type TabType = "lesson" | "practice" type StatusFilter = "all" | "published" | "draft" | "archived" /** Human Language–only sub-module editor: lesson (videos) + practice tabs; not used by general course flows. */ export function HumanLanguageSubModulePage() { const { categoryId, courseId, subModuleId } = useParams<{ categoryId: string courseId: string subModuleId: string }>() const navigate = useNavigate() const [subCourse, setSubCourse] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [activeTab, setActiveTab] = useState("lesson") const [statusFilter] = useState("all") const [practices, setPractices] = useState([]) const [videos, setVideos] = useState([]) const [practicesLoading, setPracticesLoading] = useState(false) const [videosLoading, setVideosLoading] = useState(false) const [showEditPracticeModal, setShowEditPracticeModal] = useState(false) const [practiceToEdit, setPracticeToEdit] = useState(null) const [showDeleteModal, setShowDeleteModal] = useState(false) const [practiceToDelete, setPracticeToDelete] = useState(null) const [deleting, setDeleting] = useState(false) const [title, setTitle] = useState("") const [description, setDescription] = useState("") const [persona, setPersona] = useState("") const [saving, setSaving] = useState(false) const [saveError, setSaveError] = useState(null) const [showAddVideoModal, setShowAddVideoModal] = useState(false) const [showEditVideoModal, setShowEditVideoModal] = useState(false) const [videoToEdit, setVideoToEdit] = useState(null) const [showDeleteVideoModal, setShowDeleteVideoModal] = useState(false) const [videoToDelete, setVideoToDelete] = useState(null) const [deletingVideo, setDeletingVideo] = useState(false) const [openVideoMenuId, setOpenVideoMenuId] = useState(null) const [videoTitle, setVideoTitle] = useState("") const [videoDescription, setVideoDescription] = useState("") const [videoUrl, setVideoUrl] = useState("") const [videoFile, setVideoFile] = useState(null) const [videoFileSize, setVideoFileSize] = useState(0) const [videoDuration, setVideoDuration] = useState(0) const [videoResolution, setVideoResolution] = useState("1080p") const [videoVisibility, setVideoVisibility] = useState("PUBLISHED") const [videoStatus, setVideoStatus] = useState("PUBLISHED") const [videoDisplayOrder, setVideoDisplayOrder] = useState(1) // Vimeo preview state const [showPreviewModal, setShowPreviewModal] = useState(false) const [previewIframe, setPreviewIframe] = useState("") const [previewVideo, setPreviewVideo] = useState(null) const [previewLoading, setPreviewLoading] = useState(false) useEffect(() => { const fetchData = async () => { if (!subModuleId || !courseId) return try { const subCoursesRes = await getSubModulesByCourse(Number(courseId)) const foundSubCourse = subCoursesRes.data.data.sub_courses?.find( (sc) => sc.id === Number(subModuleId) ) setSubCourse(foundSubCourse ?? null) } catch (err) { console.error("Failed to fetch course data:", err) setError("Failed to load course") } finally { setLoading(false) } } fetchData() }, [subModuleId, courseId]) const fetchPractices = async () => { if (!subModuleId) return setPracticesLoading(true) try { const res = await getQuestionSetsByOwner("SUB_MODULE", Number(subModuleId)) const raw = res.data.data const list = Array.isArray(raw) ? raw : raw?.question_sets ?? [] setPractices(list) } catch (err) { console.error("Failed to fetch practices:", err) } finally { setPracticesLoading(false) } } const fetchVideos = async () => { if (!subModuleId) return setVideosLoading(true) try { const res = await getVideosBySubModule(Number(subModuleId)) setVideos(res.data.data.videos ?? []) } catch (err) { console.error("Failed to fetch videos:", err) } finally { setVideosLoading(false) } } useEffect(() => { if (activeTab === "practice") { fetchPractices() } else if (activeTab === "lesson") { fetchVideos() } }, [activeTab, subModuleId]) const handleAddPractice = () => { navigate(`/content/human-language/${categoryId}/${courseId}/sub-module/${subModuleId}/add-practice`) } const handleEditClick = (practice: QuestionSet) => { setPracticeToEdit(practice) setTitle(practice.title) setDescription(practice.description) setPersona(practice.persona || "") setSaveError(null) setShowEditPracticeModal(true) } const handleSaveEditPractice = async () => { if (!practiceToEdit) return setSaving(true) setSaveError(null) try { await updatePractice(practiceToEdit.id, { title, description, persona, }) setShowEditPracticeModal(false) setPracticeToEdit(null) setTitle("") setDescription("") setPersona("") await fetchPractices() } catch (err) { console.error("Failed to update practice:", err) setSaveError("Failed to update practice") } finally { setSaving(false) } } const handleDeleteClick = (practice: QuestionSet) => { setPracticeToDelete(practice) setShowDeleteModal(true) } const handleConfirmDelete = async () => { if (!practiceToDelete) return setDeleting(true) try { await deleteQuestionSet(practiceToDelete.id) setShowDeleteModal(false) setPracticeToDelete(null) await fetchPractices() } catch (err) { console.error("Failed to delete practice:", err) } finally { setDeleting(false) } } const handlePracticeClick = (practiceId: number) => { navigate(`/content/human-language/${categoryId}/${courseId}/sub-module/${subModuleId}/practices/${practiceId}/questions`) } const handleAddVideo = () => { setVideoTitle("") setVideoDescription("") setVideoUrl("") setVideoFile(null) setVideoFileSize(0) setVideoDuration(0) setVideoResolution("1080p") setVideoVisibility("PUBLISHED") setVideoStatus("PUBLISHED") setVideoDisplayOrder(1) setSaveError(null) setShowAddVideoModal(true) } const handleVideoFileSelect = (file: File | null) => { setVideoFile(file) if (!file) { setVideoFileSize(0) setVideoDuration(0) return } setVideoFileSize(file.size) const video = document.createElement("video") const objectUrl = URL.createObjectURL(file) video.preload = "metadata" video.src = objectUrl video.onloadedmetadata = () => { setVideoDuration(Math.max(0, Math.round(video.duration || 0))) URL.revokeObjectURL(objectUrl) } video.onerror = () => { URL.revokeObjectURL(objectUrl) } } const handleSaveNewVideo = async () => { if (!subModuleId || !videoFile) return setSaving(true) setSaveError(null) try { const uploadRes = await uploadVideoFile(videoFile, { title: videoTitle.trim(), description: videoDescription.trim(), }) // Per backend guide, use embed_url as the video_url reference. const embedUrl = uploadRes.data?.data?.embed_url?.trim() const vimeoUrl = uploadRes.data?.data?.url?.trim() if (!embedUrl) throw new Error("Missing uploaded video embed_url") // Backend requires: https://player.vimeo.com/video/?h= // where is the last path segment from `url` (e.g. https://vimeo.com//) const hashFromUrl = vimeoUrl ? vimeoUrl.split("/").filter(Boolean).at(-1) : undefined const finalVideoUrl = hashFromUrl ? `${embedUrl}?h=${hashFromUrl}` : embedUrl const finalTitle = videoTitle.trim() || videoFile.name await createCourseVideo({ sub_module_id: Number(subModuleId), title: finalTitle, description: videoDescription.trim(), video_url: finalVideoUrl, duration: videoDuration, resolution: videoResolution.trim() || undefined, visibility: videoVisibility, display_order: Number.isFinite(videoDisplayOrder) ? videoDisplayOrder : undefined, status: videoStatus, }) setShowAddVideoModal(false) setVideoTitle("") setVideoDescription("") setVideoUrl("") setVideoFile(null) setVideoFileSize(0) setVideoDuration(0) setVideoResolution("1080p") setVideoVisibility("PUBLISHED") setVideoStatus("PUBLISHED") setVideoDisplayOrder(1) await fetchVideos() } catch (err) { console.error("Failed to create video:", err) setSaveError("Failed to create video") } finally { setSaving(false) } } const handleEditVideoClick = (video: SubCourseVideo) => { setVideoToEdit(video) setVideoTitle(video.title) setVideoDescription(video.description || "") setVideoUrl(video.video_url || "") setSaveError(null) setShowEditVideoModal(true) } const handleSaveEditVideo = async () => { if (!videoToEdit) return setSaving(true) setSaveError(null) try { await updateSubCourseVideo(videoToEdit.id, { title: videoTitle, description: videoDescription, video_url: videoUrl, }) setShowEditVideoModal(false) setVideoToEdit(null) setVideoTitle("") setVideoDescription("") setVideoUrl("") await fetchVideos() } catch (err) { console.error("Failed to update video:", err) setSaveError("Failed to update video") } finally { setSaving(false) } } const handleDeleteVideoClick = (video: SubCourseVideo) => { setVideoToDelete(video) setShowDeleteVideoModal(true) } const handleConfirmDeleteVideo = async () => { if (!videoToDelete) return setDeletingVideo(true) try { await deleteSubCourseVideo(videoToDelete.id) setShowDeleteVideoModal(false) setVideoToDelete(null) await fetchVideos() } catch (err) { console.error("Failed to delete video:", err) } finally { setDeletingVideo(false) } } // Preview a video card. // We prefer embedding directly from `video_url` because Vimeo embeds may require the `h=` hash. const handlePreviewVideo = async (video: SubCourseVideo) => { setShowPreviewModal(true) setPreviewLoading(true) setPreviewIframe("") setPreviewVideo(null) try { const directUrl = video.video_url?.trim() if (directUrl) { setPreviewIframe( ``, ) setPreviewVideo(null) return } // Fallback to sample API when a direct URL is unavailable. const idMatch = video.video_url?.match(/(\d{5,})/) const vimeoId = idMatch?.[1] ?? "76979871" // fallback to Big Buck Bunny const res = await getVimeoSample(vimeoId) setPreviewIframe(res.data.data.iframe) setPreviewVideo(res.data.data.video) } catch { setPreviewIframe("") } finally { setPreviewLoading(false) } } const filteredPractices = practices.filter((practice) => { if (statusFilter === "all") return true if (statusFilter === "published") return practice.status === "PUBLISHED" if (statusFilter === "draft") return practice.status === "DRAFT" if (statusFilter === "archived") return practice.status === "ARCHIVED" return true }) if (loading) { return (

Loading course…

) } if (error) { return (

{error}

) } return (
{/* Back Button */} Back to Human Language {/* SubCourse Header */}

{subCourse?.title}

{subCourse?.level && ( {subCourse.level} )}

{subCourse?.description || "No description available"}

Use Lesson for videos/audio and{" "} Practice for question sets tied to this sub-module.

{/* Tabs */}
{/* Content */} {activeTab === "practice" && ( <> {practicesLoading ? (

Loading practices…

) : filteredPractices.length === 0 ? (

No practices yet

Create your first practice to get started

) : (
{filteredPractices.map((practice) => { const statusConfig: Record = { PUBLISHED: { bg: "bg-green-50 text-green-700 ring-1 ring-inset ring-green-200", dot: "bg-green-500", text: "Published" }, DRAFT: { bg: "bg-grayScale-50 text-grayScale-600 ring-1 ring-inset ring-grayScale-200", dot: "bg-grayScale-400", text: "Draft" }, ARCHIVED: { bg: "bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-200", dot: "bg-amber-500", text: "Archived" }, } const status = statusConfig[practice.status] ?? statusConfig.DRAFT return ( handlePracticeClick(practice.id)} >

{practice.title}

{status.text}

{practice.description}

{practice.set_type} {practice.persona && ( {practice.persona} )}
{practice.owner_type.replace("_", " ")}
{practice.shuffle_questions && ( Shuffle ON )}
{new Date(practice.created_at).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", })}
e.stopPropagation()}>
) })}
)} )} {activeTab === "lesson" && ( <> {videosLoading ? (

Loading videos…

) : videos.length === 0 ? (

No videos yet

Upload your first video to get started

) : (
{videos.map((video, index) => { const gradients = [ "bg-gradient-to-br from-blue-100 via-blue-50 to-indigo-100", "bg-gradient-to-br from-amber-100 via-yellow-50 to-orange-100", "bg-gradient-to-br from-purple-100 via-fuchsia-50 to-pink-100", "bg-gradient-to-br from-emerald-100 via-green-50 to-teal-100", ] const formatDuration = (seconds: number) => { const mins = Math.floor(seconds / 60) const secs = seconds % 60 return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` } return ( {/* Thumbnail with duration */}
{video.thumbnail ? ( {video.title} ) : (
)}
{formatDuration(video.duration || 0)}
{/* Content */}
{/* Status and menu */}
{video.is_published ? "PUBLISHED" : "DRAFT"}
{openVideoMenuId === video.id && (
)}
{/* Title */}

{video.title}

{/* Edit / Preview buttons */}
{/* Publish button */}
) })}
)} )} {/* Delete Modal */} {showDeleteModal && practiceToDelete && (

Delete Practice

Are you sure you want to delete{" "} {practiceToDelete.title}? This action cannot be undone.

)} {/* Edit Practice Modal */} {showEditPracticeModal && practiceToEdit && (

Edit Practice

setTitle(e.target.value)} placeholder="Enter practice title" />