From 4ac8c07215b715d6f082b919a21e3339f2a72e0a Mon Sep 17 00:00:00 2001 From: ditiqi Date: Wed, 8 Jan 2025 20:29:07 +0800 Subject: [PATCH] add --- apps/web/package.json | 1 - apps/web/src/app/main/course/page.tsx | 34 --- .../src/app/main/courses/instructor/page.tsx | 2 +- .../course/detail/CourseDetailContent.tsx | 84 ------- .../course/detail/CourseDetailContext.tsx | 6 +- .../CourseDetailDescription.tsx | 29 +++ .../CourseDetailNavBar.tsx | 47 ++++ .../Description/Overview.tsx | 67 ++++++ .../Description/index.ts | 0 ...eoPage.tsx => CourseDetailDisplayArea.tsx} | 33 +-- .../CourseDetailHeader/CourseDetailHeader.tsx | 57 +++++ .../course/detail/CourseDetailLayout.tsx | 52 +++-- .../models/course/detail/CourseSyllabus.tsx | 214 ------------------ .../detail/CourseSyllabus/CollapsedButton.tsx | 19 ++ .../detail/CourseSyllabus/CourseSyllabus.tsx | 105 +++++++++ .../detail/CourseSyllabus/LectureItem.tsx | 36 +++ .../detail/CourseSyllabus/SectionItem.tsx | 68 ++++++ .../detail/CourseSyllabus/SyllabusHeader.tsx | 18 ++ .../course/detail/CourseSyllabus/index.ts | 1 + .../models/course/manage/CourseBasicForm.tsx | 28 --- .../models/course/manage/CourseEditor.tsx | 17 +- .../course/manage/CourseEditorLayout.tsx | 87 +++---- .../manage/CourseForms/CourseBasicForm.tsx | 50 ++++ .../CourseForms/CourseTargetForm copy.tsx | 45 ++++ .../models/course/manage/navItems.tsx | 54 ++--- .../src/components/presentation/NavBar.tsx | 58 +++++ .../presentation/form/FormArrayField.tsx | 210 ++++++++++------- .../presentation/form/FormDynamicInputs.tsx | 154 +++++++++++++ .../presentation/form/FormInput.tsx | 167 ++++++++------ .../ControlButtons/Brightness.tsx | 32 +++ .../ControlButtons/FullScreen.tsx | 29 +++ .../video-player/ControlButtons/Play.tsx | 25 ++ .../video-player/ControlButtons/Setting.tsx | 62 +++++ .../video-player/ControlButtons/Speed.tsx | 59 +++++ .../video-player/ControlButtons/TimeLine.tsx | 64 ++++++ .../video-player/ControlButtons/Volume.tsx | 43 ++++ .../video-player/ControlButtons/index.ts | 4 + .../video-player/LoadingOverlay.tsx | 2 +- .../video-player/VideoControls.tsx | 201 +++------------- .../{VideoScreen.tsx => VideoDisplay.tsx} | 73 +++--- .../presentation/video-player/VideoPlayer.tsx | 22 +- .../video-player/VideoPlayerLayout.tsx | 7 +- .../presentation/video-player/interface.ts | 10 + .../presentation/video-player/utlis.ts | 2 +- apps/web/src/index.css | 34 --- apps/web/src/routes/index.tsx | 15 ++ pnpm-lock.yaml | 69 ------ 47 files changed, 1539 insertions(+), 957 deletions(-) delete mode 100644 apps/web/src/app/main/course/page.tsx delete mode 100644 apps/web/src/components/models/course/detail/CourseDetailContent.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseDetailDescription/CourseDetailDescription.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseDetailDescription/CourseDetailNavBar.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseDetailDescription/Description/Overview.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseDetailDescription/Description/index.ts rename apps/web/src/components/models/course/detail/{CourseVideoPage.tsx => CourseDetailDisplayArea.tsx} (56%) create mode 100644 apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx delete mode 100644 apps/web/src/components/models/course/detail/CourseSyllabus.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseSyllabus/CollapsedButton.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseSyllabus/SyllabusHeader.tsx create mode 100644 apps/web/src/components/models/course/detail/CourseSyllabus/index.ts delete mode 100644 apps/web/src/components/models/course/manage/CourseBasicForm.tsx create mode 100644 apps/web/src/components/models/course/manage/CourseForms/CourseBasicForm.tsx create mode 100644 apps/web/src/components/models/course/manage/CourseForms/CourseTargetForm copy.tsx create mode 100644 apps/web/src/components/presentation/NavBar.tsx create mode 100644 apps/web/src/components/presentation/form/FormDynamicInputs.tsx create mode 100644 apps/web/src/components/presentation/video-player/ControlButtons/Brightness.tsx create mode 100644 apps/web/src/components/presentation/video-player/ControlButtons/FullScreen.tsx create mode 100644 apps/web/src/components/presentation/video-player/ControlButtons/Play.tsx create mode 100644 apps/web/src/components/presentation/video-player/ControlButtons/Setting.tsx create mode 100644 apps/web/src/components/presentation/video-player/ControlButtons/Speed.tsx create mode 100644 apps/web/src/components/presentation/video-player/ControlButtons/TimeLine.tsx create mode 100644 apps/web/src/components/presentation/video-player/ControlButtons/Volume.tsx create mode 100644 apps/web/src/components/presentation/video-player/ControlButtons/index.ts rename apps/web/src/components/presentation/video-player/{VideoScreen.tsx => VideoDisplay.tsx} (87%) create mode 100644 apps/web/src/components/presentation/video-player/interface.ts diff --git a/apps/web/package.json b/apps/web/package.json index 9f4b82a..2e58707 100755 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -49,7 +49,6 @@ "hls.js": "^1.5.18", "idb-keyval": "^6.2.1", "mitt": "^3.0.1", - "plyr-react": "^5.3.0", "react": "18.2.0", "react-beautiful-dnd": "^13.1.1", "react-dom": "18.2.0", diff --git a/apps/web/src/app/main/course/page.tsx b/apps/web/src/app/main/course/page.tsx deleted file mode 100644 index 9f5dd6d..0000000 --- a/apps/web/src/app/main/course/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { CourseCard } from "@web/src/components/models/course/card/CourseCard"; -import { CourseDetail } from "@web/src/components/models/course/detail/CourseDetailContent"; -import { CourseSyllabus } from "@web/src/components/models/course/detail/CourseSyllabus"; - -export const CoursePage = () => { - // 假设这些数据从API获取 - const course: any = { - /* course data */ - }; - const sections: any = [ - /* sections data */ - ]; - - return ( -
-
- {/* 左侧课程详情 */} -
- -
- {/* 右侧课程大纲 */} -
- - { - console.log("Clicked lecture:", lectureId); - }} - /> -
-
-
- ); -}; diff --git a/apps/web/src/app/main/courses/instructor/page.tsx b/apps/web/src/app/main/courses/instructor/page.tsx index bd27993..55339af 100644 --- a/apps/web/src/app/main/courses/instructor/page.tsx +++ b/apps/web/src/app/main/courses/instructor/page.tsx @@ -52,7 +52,7 @@ export default function InstructorCoursesPage() { renderItem={(course) => ( { - navigate(`/course/${course.id}/detail`, { + navigate(`/course/${course.id}/manage`, { replace: true, }); }} diff --git a/apps/web/src/components/models/course/detail/CourseDetailContent.tsx b/apps/web/src/components/models/course/detail/CourseDetailContent.tsx deleted file mode 100644 index f533b3f..0000000 --- a/apps/web/src/components/models/course/detail/CourseDetailContent.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { CheckCircleIcon } from "@heroicons/react/24/outline"; -import { Course } from "@nice/common"; -import { motion } from "framer-motion"; -import React from "react"; -import CourseDetailSkeleton from "./CourseDetailSkeleton"; -interface CourseDetailProps { - course: Course; - isLoading: boolean; -} - -export const CourseDetailContent: React.FC = ({ - course, - isLoading, -}) => { - if (isLoading || !course) { - return ; - } - return ( -
- {/* 课程标题区域 */} -
-

{course.title}

- {course.subTitle && ( -

{course.subTitle}

- )} -
- - {/* 课程描述 */} -
-

{course.description}

-
- - {/* 学习目标 */} -
-

学习目标

-
- {course.objectives.map((objective, index) => ( -
- - {objective} -
- ))} -
-
- - {/* 适合人群 */} -
-

适合人群

-
- {course.audiences.map((audience, index) => ( -
- - {audience} -
- ))} -
-
- - {/* 课程要求 */} -
-

课程要求

-
    - {course.requirements.map((requirement, index) => ( -
  • {requirement}
  • - ))} -
-
- - {/* 可获得技能 */} -
-

可获得技能

-
- {course.skills.map((skill, index) => ( - - {skill} - - ))} -
-
-
- ); -}; diff --git a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx index ffa25ff..72507b5 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailContext.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailContext.tsx @@ -9,6 +9,8 @@ interface CourseDetailContextType { selectedLectureId?: string | undefined; setSelectedLectureId?: React.Dispatch>; isLoading?: boolean; + isHeaderVisible: boolean; // 新增 + setIsHeaderVisible: (visible: boolean) => void; // 新增 } interface CourseFormProviderProps { children: ReactNode; @@ -34,7 +36,7 @@ export function CourseDetailProvider({ const [selectedLectureId, setSelectedLectureId] = useState< string | undefined >(undefined); - + const [isHeaderVisible, setIsHeaderVisible] = useState(true); // 新增 return ( {children} diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription/CourseDetailDescription.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription/CourseDetailDescription.tsx new file mode 100644 index 0000000..a3c8114 --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseDetailDescription/CourseDetailDescription.tsx @@ -0,0 +1,29 @@ +import { CheckCircleIcon } from "@heroicons/react/24/outline"; +import { Course } from "@nice/common"; +import { motion } from "framer-motion"; +import React from "react"; +import CourseDetailSkeleton from "../CourseDetailSkeleton"; +import CourseDetailNavBar from "./CourseDetailNavBar"; +interface CourseDetailProps { + course: Course; + isLoading: boolean; +} + +export const CourseDetailDescription: React.FC = ({ + course, + isLoading, +}) => { + return ( + <> + + +
+ {isLoading || !course ? ( + + ) : ( + + )} +
+ + ); +}; diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription/CourseDetailNavBar.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription/CourseDetailNavBar.tsx new file mode 100644 index 0000000..a5b862d --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseDetailDescription/CourseDetailNavBar.tsx @@ -0,0 +1,47 @@ +import { NavBar } from "@web/src/components/presentation/NavBar"; +import { HomeIcon, BellIcon } from "@heroicons/react/24/outline"; +import { + DocumentTextIcon, + MagnifyingGlassIcon, + StarIcon, +} from "@heroicons/react/24/solid"; + +export default function CourseDetailNavBar() { + const navItems = [ + { + id: "search", + icon: , + label: "搜索", + }, + { + id: "overview", + icon: , + label: "概述", + }, + { + id: "notes", + icon: , + label: "备注", + }, + { + id: "announcements", + icon: , + label: "公告", + }, + { + id: "reviews", + icon: , + label: "评价", + }, + ]; + + return ( +
+ console.log("Selected:", id)} + /> +
+ ); +} diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/Overview.tsx b/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/Overview.tsx new file mode 100644 index 0000000..334c36b --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/Overview.tsx @@ -0,0 +1,67 @@ +import { useContext } from "react"; +import { CourseDetailContext } from "../../CourseDetailContext"; +import { CheckCircleIcon } from "@heroicons/react/24/solid"; + +export function Overview() { + const { course } = useContext(CourseDetailContext); + return ( + <> +
+ {/* 课程描述 */} +
+

{course?.description}

+
+ + {/* 学习目标 */} +
+

学习目标

+
+ {course?.objectives.map((objective, index) => ( +
+ + {objective} +
+ ))} +
+
+ + {/* 适合人群 */} +
+

适合人群

+
+ {course?.audiences.map((audience, index) => ( +
+ + {audience} +
+ ))} +
+
+ + {/* 课程要求 */} +
+

课程要求

+
    + {course?.requirements.map((requirement, index) => ( +
  • {requirement}
  • + ))} +
+
+ + {/* 可获得技能 */} +
+

可获得技能

+
+ {course?.skills.map((skill, index) => ( + + {skill} + + ))} +
+
+
+ + ); +} diff --git a/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/index.ts b/apps/web/src/components/models/course/detail/CourseDetailDescription/Description/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/apps/web/src/components/models/course/detail/CourseVideoPage.tsx b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx similarity index 56% rename from apps/web/src/components/models/course/detail/CourseVideoPage.tsx rename to apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx index a083182..2db3ffa 100644 --- a/apps/web/src/components/models/course/detail/CourseVideoPage.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailDisplayArea.tsx @@ -1,23 +1,20 @@ -// components/CourseVideoPage.tsx +// components/CourseDetailDisplayArea.tsx import { motion, useScroll, useTransform } from "framer-motion"; import React from "react"; import { VideoPlayer } from "@web/src/components/presentation/video-player/VideoPlayer"; -import { CourseDetailContent } from "./CourseDetailContent"; +import { CourseDetailDescription } from "./CourseDetailDescription/CourseDetailDescription"; import { Course } from "@nice/common"; -interface CourseVideoPageProps { +interface CourseDetailDisplayAreaProps { course: Course; videoSrc?: string; videoPoster?: string; isLoading?: boolean; } -export const CourseVideoPage: React.FC = ({ - course, - videoSrc, - videoPoster, - isLoading = false, -}) => { +export const CourseDetailDisplayArea: React.FC< + CourseDetailDisplayAreaProps +> = ({ course, videoSrc, videoPoster, isLoading = false }) => { // 创建滚动动画效果 const { scrollY } = useScroll(); const videoScale = useTransform(scrollY, [0, 200], [1, 0.8]); @@ -26,13 +23,16 @@ export const CourseVideoPage: React.FC = ({ return (
{/* 固定的视频区域 */} + {/* 移除 sticky 定位,让视频区域随页面滚动 */} -
+ className="w-full bg-black"> +
@@ -42,10 +42,13 @@ export const CourseVideoPage: React.FC = ({ initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.5, delay: 0.2 }} - className="max-w-6xl mx-auto px-4 py-8"> - + className="w-full"> +
); }; -export default CourseVideoPage; +export default CourseDetailDisplayArea; diff --git a/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx new file mode 100644 index 0000000..4ef5e54 --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseDetailHeader/CourseDetailHeader.tsx @@ -0,0 +1,57 @@ +// components/Header.tsx +import { motion, useScroll, useTransform } from "framer-motion"; +import { useContext, useEffect, useState } from "react"; +import { CourseDetailContext } from "../CourseDetailContext"; + +export const CourseDetailHeader = () => { + const { scrollY } = useScroll(); + + const [lastScrollY, setLastScrollY] = useState(0); + const { course, isHeaderVisible, setIsHeaderVisible } = + useContext(CourseDetailContext); + useEffect(() => { + const updateHeader = () => { + const current = scrollY.get(); + const direction = current > lastScrollY ? "down" : "up"; + + if (direction === "down" && current > 100) { + setIsHeaderVisible(false); + } else if (direction === "up") { + setIsHeaderVisible(true); + } + + setLastScrollY(current); + }; + + // 使用 requestAnimationFrame 来优化性能 + const unsubscribe = scrollY.on("change", () => { + requestAnimationFrame(updateHeader); + }); + + return () => { + unsubscribe(); + }; + }, [lastScrollY, scrollY, setIsHeaderVisible]); + + return ( + +
+
+

{course?.title}

+
+ +
+
+ ); +}; + +export default CourseDetailHeader; diff --git a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx index 8e5b7ec..4f4ecd5 100644 --- a/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx +++ b/apps/web/src/components/models/course/detail/CourseDetailLayout.tsx @@ -1,9 +1,10 @@ import { motion } from "framer-motion"; import { useContext, useState } from "react"; import { CourseDetailContext } from "./CourseDetailContext"; -import { CourseSyllabus } from "./CourseSyllabus"; -import { CourseDetailContent } from "./CourseDetailContent"; -import CourseVideoPage from "./CourseVideoPage"; + +import CourseDetailDisplayArea from "./CourseDetailDisplayArea"; +import { CourseSyllabus } from "./CourseSyllabus/CourseSyllabus"; +import CourseDetailHeader from "./CourseDetailHeader/CourseDetailHeader"; export default function CourseDetailLayout() { const { course, selectedLectureId, isLoading, setSelectedLectureId } = @@ -15,28 +16,33 @@ export default function CourseDetailLayout() { const [isSyllabusOpen, setIsSyllabusOpen] = useState(false); return (
+ {/* 添加 Header 组件 */} {/* 主内容区域 */} - - + {" "} + {/* 添加这个包装 div */} + + + + {/* 课程大纲侧边栏 */} + setIsSyllabusOpen(!isSyllabusOpen)} /> - - - {/* 课程大纲侧边栏 */} - setIsSyllabusOpen(!isSyllabusOpen)} - /> +
); } diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus.tsx deleted file mode 100644 index 51f18cf..0000000 --- a/apps/web/src/components/models/course/detail/CourseSyllabus.tsx +++ /dev/null @@ -1,214 +0,0 @@ -import { XMarkIcon, BookOpenIcon } from "@heroicons/react/24/outline"; -import { - ChevronDownIcon, - ClockIcon, - PlayCircleIcon, -} from "@heroicons/react/24/outline"; -import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; -import React, { useState, useRef } from "react"; -import { motion, AnimatePresence } from "framer-motion"; -import { SectionDto } from "@nice/common"; - -interface CourseSyllabusProps { - sections: SectionDto[]; - onLectureClick?: (lectureId: string) => void; - isOpen: boolean; - onToggle: () => void; -} - -export const CourseSyllabus: React.FC = ({ - sections, - onLectureClick, - isOpen, - onToggle, -}) => { - const [expandedSections, setExpandedSections] = useState([]); - const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); - - const toggleSection = (sectionId: string) => { - setExpandedSections((prev) => - prev.includes(sectionId) - ? prev.filter((id) => id !== sectionId) - : [...prev, sectionId] - ); - - // 平滑滚动到选中的章节 - setTimeout(() => { - sectionRefs.current[sectionId]?.scrollIntoView({ - behavior: "smooth", - block: "start", - }); - }, 100); - }; - - return ( - - {/* 收起时显示的展开按钮 */} - {!isOpen && ( - - - - )} - - {/* 展开的课程大纲 */} - - {isOpen && ( - - {/* 标题栏 */} -
-

课程大纲

- -
- - {/* 课程大纲内容 */} -
-
- {/* 原有的 sections mapping 内容 */} - {sections.map((section) => ( - - (sectionRefs.current[section.id] = - el) - } - initial={{ opacity: 0, y: 20 }} - animate={{ opacity: 1, y: 0 }} - transition={{ duration: 0.3 }} - className="border rounded-lg overflow-hidden bg-white shadow-sm hover:shadow-md transition-shadow"> - - - - {expandedSections.includes( - section.id - ) && ( - - {section.lectures.map( - (lecture) => ( - - onLectureClick?.( - lecture.id - ) - }> - -
-

- { - lecture.title - } -

- {lecture.description && ( -

- { - lecture.description - } -

- )} -
-
- - - { - lecture.duration - } - 分钟 - -
-
- ) - )} -
- )} -
-
- ))} -
-
-
- )} -
-
- ); -}; diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/CollapsedButton.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/CollapsedButton.tsx new file mode 100644 index 0000000..08f04b3 --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/CollapsedButton.tsx @@ -0,0 +1,19 @@ +import { BookOpenIcon } from "@heroicons/react/24/outline"; +import { motion } from "framer-motion"; +import React from "react"; +interface CollapsedButtonProps { + onToggle: () => void; +} + +export const CollapsedButton: React.FC = ({ + onToggle, +}) => ( + + + +); diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx new file mode 100644 index 0000000..58212a1 --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/CourseSyllabus.tsx @@ -0,0 +1,105 @@ +import { XMarkIcon, BookOpenIcon } from "@heroicons/react/24/outline"; +import { + ChevronDownIcon, + ClockIcon, + PlayCircleIcon, +} from "@heroicons/react/24/outline"; +import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; +import React, { useState, useRef, useContext } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { SectionDto } from "@nice/common"; +import { SyllabusHeader } from "./SyllabusHeader"; +import { SectionItem } from "./SectionItem"; +import { CollapsedButton } from "./CollapsedButton"; +import { CourseDetailContext } from "../CourseDetailContext"; + +interface CourseSyllabusProps { + sections: SectionDto[]; + onLectureClick?: (lectureId: string) => void; + isOpen: boolean; + onToggle: () => void; +} + +export const CourseSyllabus: React.FC = ({ + sections, + onLectureClick, + isOpen, + onToggle, +}) => { + const { isHeaderVisible } = useContext(CourseDetailContext); + const [expandedSections, setExpandedSections] = useState([]); + const sectionRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); + + const toggleSection = (sectionId: string) => { + setExpandedSections((prev) => + prev.includes(sectionId) + ? prev.filter((id) => id !== sectionId) + : [...prev, sectionId] + ); + + setTimeout(() => { + sectionRefs.current[sectionId]?.scrollIntoView({ + behavior: "smooth", + block: "start", + }); + }, 100); + }; + + return ( + <> + + {/* 收起时的悬浮按钮 */} + {!isOpen && ( + + + + )} + + + + {isOpen && ( + + + +
+
+ {sections.map((section) => ( + + (sectionRefs.current[ + section.id + ] = el) + } + section={section} + isExpanded={expandedSections.includes( + section.id + )} + onToggle={toggleSection} + onLectureClick={onLectureClick} + /> + ))} +
+
+
+ )} +
+
+ + ); +}; diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx new file mode 100644 index 0000000..9a87d75 --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/LectureItem.tsx @@ -0,0 +1,36 @@ +// components/CourseSyllabus/LectureItem.tsx + +import { Lecture } from "@nice/common"; +import React from "react"; +import { motion } from "framer-motion"; +import { ClockIcon, PlayCircleIcon } from "@heroicons/react/24/outline"; + +interface LectureItemProps { + lecture: Lecture; + onClick: (lectureId: string) => void; +} + +export const LectureItem: React.FC = ({ + lecture, + onClick, +}) => ( + onClick(lecture.id)}> + +
+

{lecture.title}

+ {lecture.description && ( +

+ {lecture.description} +

+ )} +
+
+ + {lecture.duration}分钟 +
+
+); diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx new file mode 100644 index 0000000..100fb6e --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/SectionItem.tsx @@ -0,0 +1,68 @@ +import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { SectionDto } from "@nice/common"; +import { AnimatePresence, motion } from "framer-motion"; +import React from "react"; +import { LectureItem } from "./LectureItem"; + +// components/CourseSyllabus/SectionItem.tsx +interface SectionItemProps { + section: SectionDto; + isExpanded: boolean; + onToggle: (sectionId: string) => void; + onLectureClick: (lectureId: string) => void; + ref: React.RefObject; +} + +export const SectionItem = React.forwardRef( + ({ section, isExpanded, onToggle, onLectureClick }, ref) => ( + + + + + {isExpanded && ( + + {section.lectures.map((lecture) => ( + + ))} + + )} + + + ) +); diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/SyllabusHeader.tsx b/apps/web/src/components/models/course/detail/CourseSyllabus/SyllabusHeader.tsx new file mode 100644 index 0000000..1b4d6bf --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/SyllabusHeader.tsx @@ -0,0 +1,18 @@ +// components/CourseSyllabus/SyllabusHeader.tsx +import React from "react"; + +import { XMarkIcon } from "@heroicons/react/24/outline"; +interface SyllabusHeaderProps { + onToggle: () => void; +} + +export const SyllabusHeader: React.FC = ({ onToggle }) => ( +
+

课程大纲

+ +
+); diff --git a/apps/web/src/components/models/course/detail/CourseSyllabus/index.ts b/apps/web/src/components/models/course/detail/CourseSyllabus/index.ts new file mode 100644 index 0000000..a294db3 --- /dev/null +++ b/apps/web/src/components/models/course/detail/CourseSyllabus/index.ts @@ -0,0 +1 @@ +export * from "./CourseSyllabus"; diff --git a/apps/web/src/components/models/course/manage/CourseBasicForm.tsx b/apps/web/src/components/models/course/manage/CourseBasicForm.tsx deleted file mode 100644 index 2fa06ef..0000000 --- a/apps/web/src/components/models/course/manage/CourseBasicForm.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { SubmitHandler, useFormContext } from 'react-hook-form'; - -import { CourseFormData, useCourseEditor } from './CourseEditorContext'; -import { CourseLevel, CourseLevelLabel } from '@nice/common'; -import { FormInput } from '@web/src/components/presentation/form/FormInput'; -import { FormSelect } from '@web/src/components/presentation/form/FormSelect'; -import { FormArrayField } from '@web/src/components/presentation/form/FormArrayField'; -import { convertToOptions } from '@nice/client'; - -export function CourseBasicForm() { - const { register, formState: { errors }, watch, handleSubmit } = useFormContext(); - - return ( -
- - - - - {/* */} - - ); -} - diff --git a/apps/web/src/components/models/course/manage/CourseEditor.tsx b/apps/web/src/components/models/course/manage/CourseEditor.tsx index bc2dfe8..bbb5152 100644 --- a/apps/web/src/components/models/course/manage/CourseEditor.tsx +++ b/apps/web/src/components/models/course/manage/CourseEditor.tsx @@ -1,12 +1,13 @@ - -import { CourseBasicForm } from "./CourseBasicForm"; +import { CourseBasicForm } from "./CourseForms/CourseBasicForm"; import { CourseFormProvider } from "./CourseEditorContext"; import CourseEditorLayout from "./CourseEditorLayout"; export default function CourseEditor({ id }: { id?: string }) { - return - - - - -} \ No newline at end of file + return ( + + + + + + ); +} diff --git a/apps/web/src/components/models/course/manage/CourseEditorLayout.tsx b/apps/web/src/components/models/course/manage/CourseEditorLayout.tsx index 221a435..08ae53e 100644 --- a/apps/web/src/components/models/course/manage/CourseEditorLayout.tsx +++ b/apps/web/src/components/models/course/manage/CourseEditorLayout.tsx @@ -3,48 +3,53 @@ import { useNavigate } from "react-router-dom"; import { DEFAULT_NAV_ITEMS } from "./navItems"; import CourseEditorHeader from "./CourseEditorHeader"; import { motion } from "framer-motion"; -import { NavItem } from "@nice/client" +import { NavItem } from "@nice/client"; import CourseEditorSidebar from "./CourseEditorSidebar"; interface CourseEditorLayoutProps { - children: ReactNode; + children: ReactNode; } - -export default function CourseEditorLayout({ children }: CourseEditorLayoutProps) { - const [isHovered, setIsHovered] = useState(false); - const [selectedSection, setSelectedSection] = useState(0); - const [navItems, setNavItems] = useState(DEFAULT_NAV_ITEMS); - const navigate = useNavigate(); - const handleNavigation = (item: NavItem, index: number) => { - setSelectedSection(index); - navigate(item.path); - }; - return ( -
- -
- - -
-
-

- {navItems[selectedSection]?.label} -

-
-
{children}
-
-
-
-
- ); -} \ No newline at end of file +export default function CourseEditorLayout({ + children, +}: CourseEditorLayoutProps) { + const [isHovered, setIsHovered] = useState(false); + const [selectedSection, setSelectedSection] = useState(0); + const [navItems, setNavItems] = useState(DEFAULT_NAV_ITEMS); + const navigate = useNavigate(); + const handleNavigation = (item: NavItem, index: number) => { + setSelectedSection(index); + navigate(item.path); + }; + return ( +
+ +
+ + +
+
+

+ {navItems[selectedSection]?.label} +

+
+
{children}
+
+
+
+
+ ); +} diff --git a/apps/web/src/components/models/course/manage/CourseForms/CourseBasicForm.tsx b/apps/web/src/components/models/course/manage/CourseForms/CourseBasicForm.tsx new file mode 100644 index 0000000..f9042ac --- /dev/null +++ b/apps/web/src/components/models/course/manage/CourseForms/CourseBasicForm.tsx @@ -0,0 +1,50 @@ +import { SubmitHandler, useFormContext } from "react-hook-form"; + +import { CourseFormData, useCourseEditor } from "../CourseEditorContext"; +import { CourseLevel, CourseLevelLabel } from "@nice/common"; +import { FormInput } from "@web/src/components/presentation/form/FormInput"; +import { FormSelect } from "@web/src/components/presentation/form/FormSelect"; +import { FormArrayField } from "@web/src/components/presentation/form/FormArrayField"; +import { convertToOptions } from "@nice/client"; +import { FormDynamicInputs } from "@web/src/components/presentation/form/FormDynamicInputs"; + +export function CourseBasicForm() { + const { + register, + formState: { errors }, + watch, + handleSubmit, + } = useFormContext(); + + return ( +
+ + + + + + + {/* */} + + ); +} diff --git a/apps/web/src/components/models/course/manage/CourseForms/CourseTargetForm copy.tsx b/apps/web/src/components/models/course/manage/CourseForms/CourseTargetForm copy.tsx new file mode 100644 index 0000000..557aa55 --- /dev/null +++ b/apps/web/src/components/models/course/manage/CourseForms/CourseTargetForm copy.tsx @@ -0,0 +1,45 @@ +import { SubmitHandler, useFormContext } from "react-hook-form"; + +import { CourseFormData, useCourseEditor } from "../CourseEditorContext"; +import { CourseLevel, CourseLevelLabel } from "@nice/common"; +import { FormInput } from "@web/src/components/presentation/form/FormInput"; +import { FormSelect } from "@web/src/components/presentation/form/FormSelect"; +import { FormArrayField } from "@web/src/components/presentation/form/FormArrayField"; +import { convertToOptions } from "@nice/client"; + +export function CourseBasicForm() { + const { + register, + formState: { errors }, + watch, + handleSubmit, + } = useFormContext(); + + return ( +
+ + + + + {/* */} + + ); +} diff --git a/apps/web/src/components/models/course/manage/navItems.tsx b/apps/web/src/components/models/course/manage/navItems.tsx index 57ac873..b2342ae 100644 --- a/apps/web/src/components/models/course/manage/navItems.tsx +++ b/apps/web/src/components/models/course/manage/navItems.tsx @@ -1,28 +1,30 @@ -import { AcademicCapIcon, BookOpenIcon, Cog6ToothIcon, VideoCameraIcon } from '@heroicons/react/24/outline'; -import { NavItem } from '@nice/client'; +import { + AcademicCapIcon, + BookOpenIcon, + Cog6ToothIcon, + VideoCameraIcon, +} from "@heroicons/react/24/outline"; +import { NavItem } from "@nice/client"; export const DEFAULT_NAV_ITEMS: (NavItem & { isCompleted?: boolean })[] = [ - { - - label: "课程概述", - icon: , - path: "/manage/overview" - }, - { - - label: "目标学员", - icon: , - path: "/manage/overview" - }, - { - - label: "课程内容", - icon: , - path: "/manage/content" - }, - { - label: "课程设置", - icon: , - path: "/manage/settings" - }, -]; \ No newline at end of file + { + label: "课程概述", + icon: , + path: "/manage/overview", + }, + { + label: "目标学员", + icon: , + path: "/manage/target", + }, + { + label: "课程内容", + icon: , + path: "/manage/content", + }, + { + label: "课程设置", + icon: , + path: "/manage/settings", + }, +]; diff --git a/apps/web/src/components/presentation/NavBar.tsx b/apps/web/src/components/presentation/NavBar.tsx new file mode 100644 index 0000000..c707bc0 --- /dev/null +++ b/apps/web/src/components/presentation/NavBar.tsx @@ -0,0 +1,58 @@ +// components/NavBar.tsx +import { motion } from "framer-motion"; +import React, { useState } from "react"; + +interface NavItem { + id: string; + icon?: React.ReactNode; + label: string; +} + +interface NavBarProps { + items: NavItem[]; + defaultSelected?: string; + onSelect?: (id: string) => void; +} + +export const NavBar = ({ items, defaultSelected, onSelect }: NavBarProps) => { + const [selected, setSelected] = useState(defaultSelected || items[0]?.id); + + const handleSelect = (id: string) => { + setSelected(id); + onSelect?.(id); + }; + + return ( + + ); +}; diff --git a/apps/web/src/components/presentation/form/FormArrayField.tsx b/apps/web/src/components/presentation/form/FormArrayField.tsx index 93c5b27..a0f931c 100644 --- a/apps/web/src/components/presentation/form/FormArrayField.tsx +++ b/apps/web/src/components/presentation/form/FormArrayField.tsx @@ -1,89 +1,131 @@ -import { useFormContext } from 'react-hook-form'; -import { PlusIcon, XMarkIcon, Bars3Icon } from '@heroicons/react/24/outline'; -import { Reorder } from 'framer-motion'; -import { useState } from 'react'; -import FormError from './FormError'; -import { UUIDGenerator } from '@nice/common'; +import { useFormContext } from "react-hook-form"; +import { PlusIcon, XMarkIcon, Bars3Icon } from "@heroicons/react/24/outline"; +import { Reorder } from "framer-motion"; +import React, { useState } from "react"; +import FormError from "./FormError"; +import { UUIDGenerator } from "@nice/common"; interface ArrayFieldProps { - name: string; - label: string; - placeholder?: string; - addButtonText?: string; - inputProps?: React.InputHTMLAttributes; + name: string; + label: string; + placeholder?: string; + addButtonText?: string; + inputProps?: React.InputHTMLAttributes; } type ItemType = { id: string; value: string }; -const inputStyles = "w-full rounded-md border border-gray-300 bg-white p-2 outline-none transition-all duration-300 ease-out focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:ring-opacity-50 placeholder:text-gray-400 shadow-sm"; -const buttonStyles = "rounded-md inline-flex items-center gap-1 px-4 py-2 text-sm font-medium transition-colors "; -export function FormArrayField({ name, - label, - placeholder, - addButtonText = "添加项目", - inputProps = {} }: ArrayFieldProps) { - const { register, watch, setValue, formState: { errors }, trigger } = useFormContext(); - const [items, setItems] = useState(() => - (watch(name) as string[])?.map(value => ({ id: UUIDGenerator.generate(), value })) || [] - ); - const error = errors[name]?.message as string; +const inputStyles = + "w-full rounded-md border border-gray-300 bg-white p-2 outline-none transition-all duration-300 ease-out focus:border-blue-500 focus:ring-2 focus:ring-blue-200 focus:ring-opacity-50 placeholder:text-gray-400 shadow-sm"; +const buttonStyles = + "rounded-md inline-flex items-center gap-1 px-4 py-2 text-sm font-medium transition-colors "; +export function FormArrayField({ + name, + label, + placeholder, + addButtonText = "添加项目", + inputProps = {}, +}: ArrayFieldProps) { + const { + register, + watch, + setValue, + formState: { errors }, + trigger, + } = useFormContext(); + const [items, setItems] = useState( + () => + (watch(name) as string[])?.map((value) => ({ + id: UUIDGenerator.generate(), + value, + })) || [] + ); + const error = errors[name]?.message as string; - const updateItems = (newItems: ItemType[]) => { - setItems(newItems); - setValue(name, newItems.map(item => item.value)); - }; - return ( -
- -
- - {items.map((item, index) => ( - -
-
- updateItems( - items.map(i => i.id === item.id ? { ...i, value: e.target.value } : i) - )} - onBlur={() => trigger(name)} - placeholder={placeholder} - className={inputStyles} - /> - {inputProps.maxLength && ( - - {inputProps.maxLength - (item.value?.length || 0)} - - )} -
- -
-
- ))} -
+ const updateItems = (newItems: ItemType[]) => { + setItems(newItems); + setValue( + name, + newItems.map((item) => item.value) + ); + }; + return ( +
+ +
+ + {items.map((item, index) => ( + +
+
+ + updateItems( + items.map((i) => + i.id === item.id + ? { + ...i, + value: e.target + .value, + } + : i + ) + ) + } + onBlur={() => trigger(name)} + placeholder={placeholder} + className={inputStyles} + /> + {inputProps.maxLength && ( + + {inputProps.maxLength - + (item.value?.length || 0)} + + )} +
+ +
+
+ ))} +
- -
- -
- ); -} \ No newline at end of file + +
+ +
+ ); +} diff --git a/apps/web/src/components/presentation/form/FormDynamicInputs.tsx b/apps/web/src/components/presentation/form/FormDynamicInputs.tsx new file mode 100644 index 0000000..ec64367 --- /dev/null +++ b/apps/web/src/components/presentation/form/FormDynamicInputs.tsx @@ -0,0 +1,154 @@ +// components/FormDynamicInputs.tsx +import { useFieldArray, useFormContext } from "react-hook-form"; +import { AnimatePresence, motion } from "framer-motion"; +import React, { useState } from "react"; +import { CheckIcon, XMarkIcon, PlusIcon } from "@heroicons/react/24/outline"; +import FormError from "./FormError"; + +export interface DynamicFormInputProps + extends Omit< + React.InputHTMLAttributes, + "type" + > { + name: string; + label: string; + type?: + | "text" + | "textarea" + | "password" + | "email" + | "number" + | "tel" + | "url" + | "search" + | "date" + | "time" + | "datetime-local"; + rows?: number; +} + +export function FormDynamicInputs({ + name, + label, + type = "text", + rows = 4, + className, + ...restProps +}: DynamicFormInputProps) { + const [focusedIndexes, setFocusedIndexes] = useState([]); + const { + register, + formState: { errors }, + watch, + setValue, + trigger, + control, + } = useFormContext(); + + const { fields, append, remove } = useFieldArray({ + control, + name, + }); + + const handleBlur = async (index: number) => { + setFocusedIndexes(focusedIndexes.filter((i) => i !== index)); + await trigger(`${name}.${index}`); + }; + + const values = watch(name) || []; + const fieldErrors = errors[name] as any; + + const inputClasses = ` + w-full rounded-md border bg-white p-2 pr-8 outline-none shadow-sm + transition-all duration-300 ease-out placeholder:text-gray-400 + border-gray-300 focus:border-blue-500 focus:ring-blue-200 + `; + + const InputElement = type === "textarea" ? "textarea" : "input"; + + return ( +
+ + + + {fields.map((field, index) => ( + +
+ + setFocusedIndexes([ + ...focusedIndexes, + index, + ]) + } + onBlur={() => handleBlur(index)} + className={inputClasses} + /> + +
+ {values[index] && + focusedIndexes.includes(index) && ( + + )} + {values[index] && !fieldErrors?.[index] && ( + + )} +
+ + {index > 0 && ( + remove(index)} + className="absolute -right-2 -top-2 p-1 bg-red-500 rounded-full + text-white shadow-sm opacity-0 group-hover:opacity-100 + transition-opacity duration-200"> + + + )} +
+ {fieldErrors?.[index]?.message && ( + + )} +
+ ))} +
+ + append("")} + className="flex items-center gap-1 text-blue-500 hover:text-blue-600 + transition-colors px-4 py-2 rounded-lg hover:bg-blue-50"> + + 添加新{label} + +
+ ); +} diff --git a/apps/web/src/components/presentation/form/FormInput.tsx b/apps/web/src/components/presentation/form/FormInput.tsx index 85d273f..cbefe8e 100644 --- a/apps/web/src/components/presentation/form/FormInput.tsx +++ b/apps/web/src/components/presentation/form/FormInput.tsx @@ -1,88 +1,105 @@ -import { useFormContext } from 'react-hook-form'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useState } from 'react'; -import { CheckIcon, XMarkIcon } from '@heroicons/react/24/outline'; -import FormError from './FormError'; +import { useFormContext } from "react-hook-form"; +import { AnimatePresence, motion } from "framer-motion"; +import React, { useState } from "react"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import FormError from "./FormError"; -export interface FormInputProps extends Omit, 'type'> { - name: string; - label: string; - type?: 'text' | 'textarea' | 'password' | 'email' | 'number' | 'tel' | 'url' | 'search' | 'date' | 'time' | 'datetime-local'; - rows?: number; +export interface FormInputProps + extends Omit< + React.InputHTMLAttributes, + "type" + > { + name: string; + label: string; + type?: + | "text" + | "textarea" + | "password" + | "email" + | "number" + | "tel" + | "url" + | "search" + | "date" + | "time" + | "datetime-local"; + rows?: number; } export function FormInput({ - name, - label, - type = 'text', - rows = 4, - className, - ...restProps + name, + label, + type = "text", + rows = 4, + className, + ...restProps }: FormInputProps) { - const [isFocused, setIsFocused] = useState(false); - const { - register, - formState: { errors }, - watch, - setValue, - trigger, // Add trigger from useFormContext - } = useFormContext(); - const handleBlur = async () => { - setIsFocused(false); - await trigger(name); // Trigger validation for this field - }; - const value = watch(name); - const error = errors[name]?.message as string; - const isValid = value && !error; + const [isFocused, setIsFocused] = useState(false); + const { + register, + formState: { errors }, + watch, + setValue, + trigger, // Add trigger from useFormContext + } = useFormContext(); + const handleBlur = async () => { + setIsFocused(false); + await trigger(name); // Trigger validation for this field + }; + const value = watch(name); + const error = errors[name]?.message as string; + const isValid = value && !error; - const inputClasses = ` + const inputClasses = ` w-full rounded-md border bg-white p-2 pr-8 outline-none shadow-sm transition-all duration-300 ease-out placeholder:text-gray-400 - ${error ? 'border-red-500 focus:border-red-500 focus:ring-red-200' : 'border-gray-300 focus:border-blue-500 focus:ring-blue-200'} - ${isFocused ? 'ring-2 ring-opacity-50' : ''} - ${className || ''} + ${error ? "border-red-500 focus:border-red-500 focus:ring-red-200" : "border-gray-300 focus:border-blue-500 focus:ring-blue-200"} + ${isFocused ? "ring-2 ring-opacity-50" : ""} + ${className || ""} `; - const InputElement = type === 'textarea' ? 'textarea' : 'input'; + const InputElement = type === "textarea" ? "textarea" : "input"; - return ( -
-
- - {restProps.maxLength && ( - - {value?.length || 0}/{restProps.maxLength} - - )} -
+ return ( +
+
+ + {restProps.maxLength && ( + + {value?.length || 0}/{restProps.maxLength} + + )} +
-
- + setIsFocused(true)} + onBlur={handleBlur} + className={inputClasses} + /> - onFocus={() => setIsFocused(true)} - onBlur={handleBlur} - className={inputClasses} - /> - -
- {value && isFocused && ( - - )} - {isValid && } -
- -
-
- ); -} \ No newline at end of file +
+ {value && isFocused && ( + + )} + {isValid && ( + + )} +
+ +
+ + ); +} diff --git a/apps/web/src/components/presentation/video-player/ControlButtons/Brightness.tsx b/apps/web/src/components/presentation/video-player/ControlButtons/Brightness.tsx new file mode 100644 index 0000000..2dcaf4d --- /dev/null +++ b/apps/web/src/components/presentation/video-player/ControlButtons/Brightness.tsx @@ -0,0 +1,32 @@ +import { SunIcon } from "@heroicons/react/24/solid"; +import { useContext } from "react"; +import { VideoPlayerContext } from "../VideoPlayer"; + +export default function Brightness() { + const { brightness, setBrightness } = useContext(VideoPlayerContext); + return ( + <> + {/* 亮度控制 */} +
+ +
+
+ + setBrightness(parseFloat(e.target.value)) + } + className="h-24 w-2 accent-primary-500 [-webkit-appearance:slider-vertical]" + /> +
+
+
+ + ); +} diff --git a/apps/web/src/components/presentation/video-player/ControlButtons/FullScreen.tsx b/apps/web/src/components/presentation/video-player/ControlButtons/FullScreen.tsx new file mode 100644 index 0000000..b48b3e5 --- /dev/null +++ b/apps/web/src/components/presentation/video-player/ControlButtons/FullScreen.tsx @@ -0,0 +1,29 @@ +import { useContext } from "react"; +import { VideoPlayerContext } from "../VideoPlayer"; +import { + ArrowsPointingInIcon, + ArrowsPointingOutIcon, +} from "@heroicons/react/24/solid"; + +export default function FullScreen() { + const { videoRef } = useContext(VideoPlayerContext); + return ( + <> + + + ); +} diff --git a/apps/web/src/components/presentation/video-player/ControlButtons/Play.tsx b/apps/web/src/components/presentation/video-player/ControlButtons/Play.tsx new file mode 100644 index 0000000..fdff076 --- /dev/null +++ b/apps/web/src/components/presentation/video-player/ControlButtons/Play.tsx @@ -0,0 +1,25 @@ +import { useContext } from "react"; +import { VideoPlayerContext } from "../VideoPlayer"; +import { PauseIcon, PlayIcon } from "@heroicons/react/24/solid"; + +export default function Play() { + const { isPlaying, videoRef } = useContext(VideoPlayerContext); + + return ( + <> + + + ); +} diff --git a/apps/web/src/components/presentation/video-player/ControlButtons/Setting.tsx b/apps/web/src/components/presentation/video-player/ControlButtons/Setting.tsx new file mode 100644 index 0000000..2ece400 --- /dev/null +++ b/apps/web/src/components/presentation/video-player/ControlButtons/Setting.tsx @@ -0,0 +1,62 @@ +import { useContext } from "react"; +import { VideoPlayerContext } from "../VideoPlayer"; +import { AnimatePresence, motion } from "framer-motion"; +import { Cog6ToothIcon } from "@heroicons/react/24/solid"; + +export default function Setting() { + const { + isSettingsOpen, + setIsSettingsOpen, + resolution, + setResolution, + resolutions, + } = useContext(VideoPlayerContext); + + return ( + <> +
+ + + + {isSettingsOpen && ( + + {/* 清晰度选择器 */} +
+
+ 清晰度 +
+ {resolutions.map((res) => ( + + ))} +
+
+ )} +
+
+ + ); +} diff --git a/apps/web/src/components/presentation/video-player/ControlButtons/Speed.tsx b/apps/web/src/components/presentation/video-player/ControlButtons/Speed.tsx new file mode 100644 index 0000000..8489a50 --- /dev/null +++ b/apps/web/src/components/presentation/video-player/ControlButtons/Speed.tsx @@ -0,0 +1,59 @@ +import { useContext } from "react"; +import { VideoPlayerContext } from "../VideoPlayer"; +import { ChevronUpDownIcon } from "@heroicons/react/24/solid"; +import { PlaybackSpeed } from "../type"; + +export default function Speed() { + const { + setIsSpeedOpen, + isSpeedOpen, + playbackSpeed, + setPlaybackSpeed, + videoRef, + } = useContext(VideoPlayerContext); + return ( + <> +
+ + {isSpeedOpen && ( +
+
+
+ {[0.5, 0.75, 1.0, 1.25, 1.5, 2.0].map( + (speed) => ( + + ) + )} +
+
+
+ )} +
+ + ); +} diff --git a/apps/web/src/components/presentation/video-player/ControlButtons/TimeLine.tsx b/apps/web/src/components/presentation/video-player/ControlButtons/TimeLine.tsx new file mode 100644 index 0000000..f07c31a --- /dev/null +++ b/apps/web/src/components/presentation/video-player/ControlButtons/TimeLine.tsx @@ -0,0 +1,64 @@ +import React, { useContext } from "react"; +import { VideoPlayerContext } from "../VideoPlayer"; +import { motion } from "framer-motion"; + +export default function TimeLine() { + const { + currentTime, + duration, + progressRef, + setIsDragging, + videoRef, + isDragging, + isHovering, + } = useContext(VideoPlayerContext); + const handleProgressClick = (e: React.MouseEvent) => { + if (!videoRef.current || !progressRef.current) return; + + const rect = progressRef.current.getBoundingClientRect(); + const percent = (e.clientX - rect.left) / rect.width; + videoRef.current.currentTime = percent * videoRef.current.duration; + }; + return ( + <> +
{ + setIsDragging(true); + handleProgressClick(e); + }}> + {/* 背景条 */} +
+ {/* 播放进度 */} + + {/* 进度球 */} + + {/* 预览进度 */} + +
+ + ); +} diff --git a/apps/web/src/components/presentation/video-player/ControlButtons/Volume.tsx b/apps/web/src/components/presentation/video-player/ControlButtons/Volume.tsx new file mode 100644 index 0000000..bb25b68 --- /dev/null +++ b/apps/web/src/components/presentation/video-player/ControlButtons/Volume.tsx @@ -0,0 +1,43 @@ +import { useContext } from "react"; +import { VideoPlayerContext } from "../VideoPlayer"; +import { SpeakerWaveIcon, SpeakerXMarkIcon } from "@heroicons/react/24/solid"; + +export default function Volume() { + const { isMuted, setIsMuted, volume, setVolume, videoRef } = + useContext(VideoPlayerContext); + return ( + <> + {/* 音量控制 */} +
+ +
+
+ { + const newVolume = parseFloat(e.target.value); + setVolume(newVolume); + if (videoRef.current) { + videoRef.current.volume = newVolume; + } + }} + className="h-24 w-2 accent-primary-500 [-webkit-appearance:slider-vertical]" + /> +
+
+
+ + ); +} diff --git a/apps/web/src/components/presentation/video-player/ControlButtons/index.ts b/apps/web/src/components/presentation/video-player/ControlButtons/index.ts new file mode 100644 index 0000000..35cbbf0 --- /dev/null +++ b/apps/web/src/components/presentation/video-player/ControlButtons/index.ts @@ -0,0 +1,4 @@ +export * from "./Brightness"; +export * from "./Volume"; +export * from "./Speed"; +export * from "./Play"; diff --git a/apps/web/src/components/presentation/video-player/LoadingOverlay.tsx b/apps/web/src/components/presentation/video-player/LoadingOverlay.tsx index 4facd0c..e3f051f 100644 --- a/apps/web/src/components/presentation/video-player/LoadingOverlay.tsx +++ b/apps/web/src/components/presentation/video-player/LoadingOverlay.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from "react"; import Hls from "hls.js"; import { motion, AnimatePresence } from "framer-motion"; -import "plyr/dist/plyr.css"; + import { VideoPlayerContext } from "./VideoPlayer"; export const LoadingOverlay = () => { const { loadingProgress } = useContext(VideoPlayerContext); diff --git a/apps/web/src/components/presentation/video-player/VideoControls.tsx b/apps/web/src/components/presentation/video-player/VideoControls.tsx index 692f4bb..73d4547 100644 --- a/apps/web/src/components/presentation/video-player/VideoControls.tsx +++ b/apps/web/src/components/presentation/video-player/VideoControls.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useRef, useContext, useState } from "react"; -import Hls from "hls.js"; import { motion, AnimatePresence } from "framer-motion"; import { PlayIcon, @@ -9,11 +8,20 @@ import { Cog6ToothIcon, ArrowsPointingOutIcon, ArrowsPointingInIcon, + ChevronUpDownIcon, + SunIcon, } from "@heroicons/react/24/solid"; -import "plyr/dist/plyr.css"; + import { VideoPlayerContext } from "./VideoPlayer"; import { formatTime } from "./utlis"; import { PlaybackSpeed } from "./type"; +import Volume from "./ControlButtons/Volume"; +import Brightness from "./ControlButtons/Brightness"; +import Speed from "./ControlButtons/Speed"; +import Play from "./ControlButtons/Play"; +import Setting from "./ControlButtons/Setting"; +import FullScreen from "./ControlButtons/FullScreen"; +import TimeLine from "./ControlButtons/TimeLine"; export const Controls = () => { const { @@ -27,6 +35,8 @@ export const Controls = () => { isReady, setIsReady, isPlaying, + setIsSpeedOpen, + isSpeedOpen, setIsPlaying, bufferingState, @@ -46,16 +56,12 @@ export const Controls = () => { isDragging, setIsDragging, isHovering, + isBrightnessOpen, + setIsBrightnessOpen, setIsHovering, progressRef, } = useContext(VideoPlayerContext); - const handleProgressClick = (e: React.MouseEvent) => { - if (!videoRef.current || !progressRef.current) return; - const rect = progressRef.current.getBoundingClientRect(); - const percent = (e.clientX - rect.left) / rect.width; - videoRef.current.currentTime = percent * videoRef.current.duration; - }; // 控制栏显示逻辑 useEffect(() => { let timer: number; @@ -84,190 +90,35 @@ export const Controls = () => { }} className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4"> {/* 进度条 */} -
{ - setIsDragging(true); - handleProgressClick(e); - }}> - {/* 背景条 */} -
- {/* 播放进度 */} - - {/* 进度球 */} - - {/* 预览进度 */} - -
+ {/* 控制按钮区域 */}
{/* 播放/暂停按钮 */} - - - {/* 音量控制 */} -
- -
-
- { - const newVolume = parseFloat( - e.target.value - ); - setVolume(newVolume); - if (videoRef.current) { - videoRef.current.volume = newVolume; - } - }} - className="h-24 w-2 accent-primary-500 [-webkit-appearance:slider-vertical]" - /> -
-
-
+ {/* 时间显示 */} {duration && ( - + {formatTime(currentTime)} / {formatTime(duration)} )}
{/* 右侧控制按钮 */}
- {/* 设置按钮 */} -
- - {/* 设置菜单 */} - - {isSettingsOpen && ( - - {/* 倍速选择 */} -
-

- 播放速度 -

- {[0.5, 0.75, 1.0, 1.25, 1.5, 2.0].map( - (speed) => ( - - ) - )} -
+ {/* 音量 */} + + {/* 亮度 */} + - {/* 亮度调节 */} -
-

- 亮度 -

- - setBrightness( - parseFloat(e.target.value) - ) - } - className="w-full accent-primary-500" - /> -
-
- )} -
-
+ {/* 倍速控制 */} + + {/* 设置按钮 */} + {/* 全屏按钮 */} - +
diff --git a/apps/web/src/components/presentation/video-player/VideoScreen.tsx b/apps/web/src/components/presentation/video-player/VideoDisplay.tsx similarity index 87% rename from apps/web/src/components/presentation/video-player/VideoScreen.tsx rename to apps/web/src/components/presentation/video-player/VideoDisplay.tsx index 3c8f7c4..8b89885 100644 --- a/apps/web/src/components/presentation/video-player/VideoScreen.tsx +++ b/apps/web/src/components/presentation/video-player/VideoDisplay.tsx @@ -1,61 +1,38 @@ -// VideoPlayer.tsx import React, { useContext, useEffect, useRef, useState } from "react"; import Hls from "hls.js"; -import { motion, AnimatePresence } from "framer-motion"; -import "plyr/dist/plyr.css"; + import { VideoPlayerContext } from "./VideoPlayer"; -interface VideoScreenProps { +interface VideoDisplayProps { autoPlay?: boolean; - // className?: string; - // qualities?: { label: string; value: string }[]; - // onQualityChange?: (quality: string) => void; } -export const VideoScreen: React.FC = ({ +export const VideoDisplay: React.FC = ({ autoPlay = false, }) => { const { src, poster, onError, - showControls, - setShowControls, - isSettingsOpen, - setIsSettingsOpen, - playbackSpeed, - setPlaybackSpeed, videoRef, - isReady, setIsReady, - isPlaying, setIsPlaying, - error, setError, - bufferingState, setBufferingState, - volume, - setVolume, isMuted, - setIsMuted, - loadingProgress, setLoadingProgress, - currentTime, setCurrentTime, - duration, setDuration, brightness, - setBrightness, isDragging, setIsDragging, - isHovering, - setIsHovering, progressRef, + resolution, + setResolutions, } = useContext(VideoPlayerContext); - // 处理进度条拖拽 + // 处理进度条拖拽 const handleProgressDrag = (e: MouseEvent) => { if (!isDragging || !videoRef.current || !progressRef.current) return; - const rect = progressRef.current.getBoundingClientRect(); const percent = Math.max( 0, @@ -68,23 +45,19 @@ export const VideoScreen: React.FC = ({ useEffect(() => { const handleMouseUp = () => setIsDragging(false); const handleMouseMove = (e: MouseEvent) => handleProgressDrag(e); - if (isDragging) { document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); } - return () => { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("mouseup", handleMouseUp); }; }, [isDragging]); - // 添加控制栏组件 - + // 初始化 HLS 和事件监听 useEffect(() => { let hls: Hls; - const initializeHls = async () => { if (!videoRef.current) return; @@ -98,6 +71,10 @@ export const VideoScreen: React.FC = ({ if (videoRef.current.canPlayType("application/vnd.apple.mpegurl")) { videoRef.current.src = src; setIsReady(true); + + // 设置视频时长 + setDuration(videoRef.current.duration); + if (autoPlay) { try { await videoRef.current.play(); @@ -128,6 +105,10 @@ export const VideoScreen: React.FC = ({ hls.on(Hls.Events.MANIFEST_PARSED, async () => { setIsReady(true); + + // 设置视频时长 + setDuration(videoRef.current?.duration || 0); + if (autoPlay && videoRef.current) { try { await videoRef.current.play(); @@ -141,7 +122,6 @@ export const VideoScreen: React.FC = ({ hls.on(Hls.Events.BUFFER_APPENDING, () => { setBufferingState(true); }); - hls.on(Hls.Events.FRAG_BUFFERED, (_, data) => { setBufferingState(false); if (data.stats) { @@ -150,8 +130,8 @@ export const VideoScreen: React.FC = ({ setLoadingProgress(Math.round(progress)); } }); - let networkError; let fatalError; + let networkError; hls.on(Hls.Events.ERROR, (_, data) => { if (data.fatal) { switch (data.type) { @@ -178,20 +158,34 @@ export const VideoScreen: React.FC = ({ } }); }; - // Event handlers + + // 事件处理 const handlePlay = () => setIsPlaying(true); const handlePause = () => setIsPlaying(false); const handleEnded = () => setIsPlaying(false); const handleWaiting = () => setBufferingState(true); const handlePlaying = () => setBufferingState(false); + const handleLoadedMetadata = () => { + if (videoRef.current) { + // 设置视频时长 + setDuration(videoRef.current.duration); + } + }; + if (videoRef.current) { videoRef.current.addEventListener("play", handlePlay); videoRef.current.addEventListener("pause", handlePause); videoRef.current.addEventListener("ended", handleEnded); videoRef.current.addEventListener("waiting", handleWaiting); videoRef.current.addEventListener("playing", handlePlaying); + videoRef.current.addEventListener( + "loadedmetadata", + handleLoadedMetadata + ); } + initializeHls(); + return () => { if (videoRef.current) { videoRef.current.removeEventListener("play", handlePlay); @@ -199,6 +193,10 @@ export const VideoScreen: React.FC = ({ videoRef.current.removeEventListener("ended", handleEnded); videoRef.current.removeEventListener("waiting", handleWaiting); videoRef.current.removeEventListener("playing", handlePlaying); + videoRef.current.removeEventListener( + "loadedmetadata", + handleLoadedMetadata + ); } if (hls) { hls.destroy(); @@ -218,7 +216,6 @@ export const VideoScreen: React.FC = ({ onTimeUpdate={() => { if (videoRef.current) { setCurrentTime(videoRef.current.currentTime); - setDuration(videoRef.current.duration); } }} /> diff --git a/apps/web/src/components/presentation/video-player/VideoPlayer.tsx b/apps/web/src/components/presentation/video-player/VideoPlayer.tsx index 90fc0bf..4a86f4b 100644 --- a/apps/web/src/components/presentation/video-player/VideoPlayer.tsx +++ b/apps/web/src/components/presentation/video-player/VideoPlayer.tsx @@ -1,6 +1,7 @@ import React, { createContext, ReactNode, useRef, useState } from "react"; import { PlaybackSpeed } from "./type"; import VideoPlayerLayout from "./VideoPlayerLayout"; +import { Resolution } from "./interface"; interface VideoPlayerContextType { src: string; @@ -38,6 +39,14 @@ interface VideoPlayerContextType { isHovering: boolean; setIsHovering: React.Dispatch>; progressRef: React.RefObject; + isSpeedOpen: boolean; + setIsSpeedOpen: React.Dispatch>; + isBrightnessOpen: boolean; + setIsBrightnessOpen: React.Dispatch>; + resolution: number; + setResolution: React.Dispatch>; + resolutions: Resolution[]; + setResolutions: React.Dispatch>; } export const VideoPlayerContext = createContext( null @@ -66,10 +75,13 @@ export function VideoPlayer({ const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [brightness, setBrightness] = useState(1); + const [resolution, setResolution] = useState(-1); + const [resolutions, setResolutions] = useState([]); const [isDragging, setIsDragging] = useState(false); const [isHovering, setIsHovering] = useState(false); const progressRef = useRef(null); - + const [isSpeedOpen, setIsSpeedOpen] = useState(false); + const [isBrightnessOpen, setIsBrightnessOpen] = useState(false); return ( diff --git a/apps/web/src/components/presentation/video-player/VideoPlayerLayout.tsx b/apps/web/src/components/presentation/video-player/VideoPlayerLayout.tsx index 0da46a8..5bcbb51 100644 --- a/apps/web/src/components/presentation/video-player/VideoPlayerLayout.tsx +++ b/apps/web/src/components/presentation/video-player/VideoPlayerLayout.tsx @@ -2,7 +2,7 @@ import { useContext } from "react"; import { VideoPlayerContext } from "./VideoPlayer"; import Controls from "./VideoControls"; import { AnimatePresence } from "framer-motion"; -import { VideoScreen } from "./VideoScreen"; +import { VideoDisplay } from "./VideoDisplay"; import LoadingOverlay from "./LoadingOverlay"; export default function VideoPlayerLayout() { @@ -17,14 +17,15 @@ export default function VideoPlayerLayout() { return ( <>
{ setIsHovering(true); setShowControls(true); }}> {!isReady &&
123
} {!isReady && } - + {(showControls || isDragging) && } diff --git a/apps/web/src/components/presentation/video-player/interface.ts b/apps/web/src/components/presentation/video-player/interface.ts new file mode 100644 index 0000000..5579328 --- /dev/null +++ b/apps/web/src/components/presentation/video-player/interface.ts @@ -0,0 +1,10 @@ +// 定义清晰度选项的类型 +export interface Resolution { + id: number; + height: number; + width: number; + bitrate: number; + label: string; + url?: string; // 可选:清晰度对应的视频URL + active?: boolean; // 可选:是否是当前激活的清晰度 +} diff --git a/apps/web/src/components/presentation/video-player/utlis.ts b/apps/web/src/components/presentation/video-player/utlis.ts index edd8a74..3bee59a 100644 --- a/apps/web/src/components/presentation/video-player/utlis.ts +++ b/apps/web/src/components/presentation/video-player/utlis.ts @@ -1,4 +1,4 @@ -export const formatTime = (seconds: number): string => { +export const formatTime = (seconds: number = 0): string => { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 102492c..6e90447 100755 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -107,37 +107,3 @@ .custom-table .ant-table-tbody > tr:last-child > td { border-bottom: none; /* 去除最后一行的底部边框 */ } -/* 自定义 Plyr 样式 */ -.plyr--full-ui input[type="range"] { - color: #ff0000; /* YouTube 红色 */ -} - -.plyr__control--overlaid { - background: rgba(255, 0, 0, 0.8); -} - -.plyr--video .plyr__control:hover { - background: #ff0000; -} - -.plyr--full-ui input[type="range"]::-webkit-slider-thumb { - background: #ff0000; -} - -.plyr--full-ui input[type="range"]::-moz-range-thumb { - background: #ff0000; -} - -.plyr--full-ui input[type="range"]::-ms-thumb { - background: #ff0000; -} - -/* 缓冲条样式 */ -.plyr__progress__buffer { - color: rgba(255, 255, 255, 0.25); -} - -/* 控制栏背景 */ -.plyr--video .plyr__controls { - background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.7)); -} diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx index e55f26a..a2ef037 100755 --- a/apps/web/src/routes/index.tsx +++ b/apps/web/src/routes/index.tsx @@ -19,6 +19,7 @@ import StudentCoursesPage from "../app/main/courses/student/page"; import InstructorCoursesPage from "../app/main/courses/instructor/page"; import HomePage from "../app/main/home/page"; import { CourseDetailPage } from "../app/main/course/detail/page"; + interface CustomIndexRouteObject extends IndexRouteObject { name?: string; breadcrumb?: string; @@ -85,6 +86,20 @@ export const routes: CustomRouteObject[] = [ { path: ":id?/manage", // 使用 ? 表示 id 参数是可选的 element: , + children: [ + { + index: true, // This will make :id?/manage the default route + element: , + }, + { + path: "overview", + element: , // You might want to create a specific overview component + }, + { + path: "target", + element: , // Create a specific target page component + }, + ], }, { path: ":id?/detail", // 使用 ? 表示 id 参数是可选的 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d183884..5abfd1f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -341,9 +341,6 @@ importers: mitt: specifier: ^3.0.1 version: 3.0.1 - plyr-react: - specifier: ^5.3.0 - version: 5.3.0(plyr@3.7.8)(react@18.2.0) react: specifier: 18.2.0 version: 18.2.0 @@ -3601,9 +3598,6 @@ packages: copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} - core-js@3.39.0: - resolution: {integrity: sha512-raM0ew0/jJUqkJ0E6e8UDtl+y/7ktFivgWvqw8dNSQeNWoSDLvQ1H/RN3aPXB9tBd4/FhyR4RDPGhsNIMsAn7g==} - core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -3666,9 +3660,6 @@ packages: custom-error-instance@2.1.1: resolution: {integrity: sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg==} - custom-event-polyfill@1.0.7: - resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==} - date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -4863,9 +4854,6 @@ packages: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} - loadjs@4.3.0: - resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==} - locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -5379,19 +5367,6 @@ packages: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} - plyr-react@5.3.0: - resolution: {integrity: sha512-m36/HrpHwg1N2rq3E31E8/kpAH55vk6qHUg17MG4uu9jbWYxnkN39lLmZQwxW7/qpDPfW5aGUJ6R3u23V0R3zA==} - engines: {node: '>=16'} - peerDependencies: - plyr: ^3.7.7 - react: '>=16.8' - peerDependenciesMeta: - react: - optional: true - - plyr@3.7.8: - resolution: {integrity: sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==} - possible-typed-array-names@1.0.0: resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} engines: {node: '>= 0.4'} @@ -5535,9 +5510,6 @@ packages: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} - rangetouch@2.0.1: - resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==} - raw-body@2.5.2: resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} engines: {node: '>= 0.8'} @@ -5770,15 +5742,6 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' - react-aptor@2.0.0: - resolution: {integrity: sha512-YnCayokuhAwmBBP4Oc0bbT2l6ApfsjbY3DEEVUddIKZEBlGl1npzjHHzWnSqWuboSbMZvRqUM01Io9yiIp1wcg==} - engines: {node: '>=12.7.0'} - peerDependencies: - react: '>=16.8' - peerDependenciesMeta: - react: - optional: true - react-beautiful-dnd@13.1.1: resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} deprecated: 'react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672' @@ -6587,9 +6550,6 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} - url-polyfill@1.1.12: - resolution: {integrity: sha512-mYFmBHCapZjtcNHW0MDq9967t+z4Dmg5CJ0KqysK3+ZbyoNOWQHksGCTWwDhxGXllkWlOc10Xfko6v4a3ucM6A==} - use-memo-one@1.1.3: resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} peerDependencies: @@ -10523,8 +10483,6 @@ snapshots: dependencies: toggle-selection: 1.0.6 - core-js@3.39.0: {} - core-util-is@1.0.3: {} cors@2.8.5: @@ -10605,8 +10563,6 @@ snapshots: custom-error-instance@2.1.1: {} - custom-event-polyfill@1.0.7: {} - date-fns@2.30.0: dependencies: '@babel/runtime': 7.25.0 @@ -12098,8 +12054,6 @@ snapshots: loader-runner@4.3.0: {} - loadjs@4.3.0: {} - locate-path@3.0.0: dependencies: p-locate: 3.0.0 @@ -12546,21 +12500,6 @@ snapshots: pluralize@8.0.0: {} - plyr-react@5.3.0(plyr@3.7.8)(react@18.2.0): - dependencies: - plyr: 3.7.8 - react-aptor: 2.0.0(react@18.2.0) - optionalDependencies: - react: 18.2.0 - - plyr@3.7.8: - dependencies: - core-js: 3.39.0 - custom-event-polyfill: 1.0.7 - loadjs: 4.3.0 - rangetouch: 2.0.1 - url-polyfill: 1.1.12 - possible-typed-array-names@1.0.0: {} postcss-import@15.1.0(postcss@8.4.49): @@ -12686,8 +12625,6 @@ snapshots: range-parser@1.2.1: {} - rangetouch@2.0.1: {} - raw-body@2.5.2: dependencies: bytes: 3.1.2 @@ -13016,10 +12953,6 @@ snapshots: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - react-aptor@2.0.0(react@18.2.0): - optionalDependencies: - react: 18.2.0 - react-beautiful-dnd@13.1.1(react-dom@18.2.0(react@18.2.0))(react@18.2.0): dependencies: '@babel/runtime': 7.25.0 @@ -13944,8 +13877,6 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 - url-polyfill@1.1.12: {} - use-memo-one@1.1.3(react@18.2.0): dependencies: react: 18.2.0