mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: create improved player for hosted videos
This commit is contained in:
parent
e4d5c44aae
commit
7139448195
6 changed files with 435 additions and 90 deletions
|
|
@ -0,0 +1,205 @@
|
||||||
|
import React, { useEffect, useRef } from 'react'
|
||||||
|
import Plyr from 'plyr'
|
||||||
|
import 'plyr/dist/plyr.css'
|
||||||
|
|
||||||
|
interface VideoDetails {
|
||||||
|
startTime?: number
|
||||||
|
endTime?: number | null
|
||||||
|
autoplay?: boolean
|
||||||
|
muted?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LearnHousePlayerProps {
|
||||||
|
src: string
|
||||||
|
details?: VideoDetails
|
||||||
|
onReady?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LearnHousePlayer: React.FC<LearnHousePlayerProps> = ({ src, details, onReady }) => {
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
const playerRef = useRef<Plyr | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (videoRef.current) {
|
||||||
|
// Initialize Plyr
|
||||||
|
playerRef.current = new Plyr(videoRef.current, {
|
||||||
|
controls: [
|
||||||
|
'play-large',
|
||||||
|
'play',
|
||||||
|
'progress',
|
||||||
|
'current-time',
|
||||||
|
'mute',
|
||||||
|
'volume',
|
||||||
|
'settings',
|
||||||
|
'pip',
|
||||||
|
'fullscreen'
|
||||||
|
],
|
||||||
|
settings: ['quality', 'speed', 'loop'],
|
||||||
|
speed: { selected: 1, options: [0.5, 0.75, 1, 1.25, 1.5, 2] },
|
||||||
|
tooltips: { controls: true, seek: true },
|
||||||
|
keyboard: { focused: true, global: true },
|
||||||
|
seekTime: 10,
|
||||||
|
volume: 1,
|
||||||
|
muted: details?.muted ?? false,
|
||||||
|
autoplay: details?.autoplay ?? false,
|
||||||
|
disableContextMenu: true,
|
||||||
|
hideControls: true,
|
||||||
|
resetOnEnd: false,
|
||||||
|
invertTime: false,
|
||||||
|
ratio: '16:9',
|
||||||
|
fullscreen: { enabled: true, iosNative: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Set initial time if specified
|
||||||
|
if (details?.startTime) {
|
||||||
|
playerRef.current.currentTime = details.startTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle end time
|
||||||
|
if (details?.endTime) {
|
||||||
|
playerRef.current.on('timeupdate', () => {
|
||||||
|
if (playerRef.current && playerRef.current.currentTime >= details.endTime!) {
|
||||||
|
playerRef.current.pause()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call onReady if provided
|
||||||
|
if (onReady) {
|
||||||
|
playerRef.current.on('ready', onReady)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
if (playerRef.current) {
|
||||||
|
playerRef.current.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [details, onReady])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full aspect-video rounded-lg overflow-hidden">
|
||||||
|
<style jsx global>{`
|
||||||
|
.plyr--video {
|
||||||
|
--plyr-color-main: #ffffff;
|
||||||
|
--plyr-video-background: #000000;
|
||||||
|
--plyr-menu-background: #ffffff;
|
||||||
|
--plyr-menu-color: #000000;
|
||||||
|
--plyr-tooltip-background: #ffffff;
|
||||||
|
--plyr-tooltip-color: #000000;
|
||||||
|
--plyr-range-track-height: 4px;
|
||||||
|
--plyr-range-thumb-height: 12px;
|
||||||
|
--plyr-range-thumb-background: #ffffff;
|
||||||
|
--plyr-range-fill-background: #ffffff;
|
||||||
|
--plyr-control-icon-size: 18px;
|
||||||
|
--plyr-control-spacing: 10px;
|
||||||
|
--plyr-control-radius: 4px;
|
||||||
|
--plyr-video-controls-background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.5));
|
||||||
|
}
|
||||||
|
.plyr--full-ui input[type=range] {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.plyr__control--overlaid {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 2px solid #000;
|
||||||
|
}
|
||||||
|
.plyr__control--overlaid svg {
|
||||||
|
fill: #000 !important;
|
||||||
|
}
|
||||||
|
.plyr__control--overlaid:hover {
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
}
|
||||||
|
.plyr__control.plyr__tab-focus,
|
||||||
|
.plyr__control:hover,
|
||||||
|
.plyr__control[aria-expanded=true] {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.plyr__menu__container {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e5e5;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.plyr__menu__container > div {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
.plyr__menu__container,
|
||||||
|
.plyr__menu__container *,
|
||||||
|
.plyr__menu__container button,
|
||||||
|
.plyr__menu__container button:focus,
|
||||||
|
.plyr__menu__container button:active,
|
||||||
|
.plyr__menu__container button[aria-selected="true"] {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
.plyr__menu__container button:hover {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
.plyr__control svg {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
.plyr__control:hover svg {
|
||||||
|
fill: #ffffff;
|
||||||
|
}
|
||||||
|
/* Settings (gear) icon: white by default, black on hover/open */
|
||||||
|
.plyr__controls .plyr__control[data-plyr="settings"] svg {
|
||||||
|
fill: #fff;
|
||||||
|
}
|
||||||
|
.plyr__controls .plyr__control[data-plyr="settings"]:hover svg,
|
||||||
|
.plyr__controls .plyr__control[data-plyr="settings"][aria-expanded="true"] svg {
|
||||||
|
fill: #000;
|
||||||
|
}
|
||||||
|
.plyr__time {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.plyr__progress__buffer {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
.plyr__volume--display {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
.plyr__control[aria-expanded=true] svg {
|
||||||
|
fill: #000000;
|
||||||
|
}
|
||||||
|
.plyr__control[aria-expanded=true] .plyr__tooltip {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.plyr__tooltip {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
.plyr__tooltip::before {
|
||||||
|
border-top-color: #ffffff;
|
||||||
|
}
|
||||||
|
/* Menu and settings icons */
|
||||||
|
.plyr__menu__container .plyr__control svg,
|
||||||
|
.plyr__menu__container button svg {
|
||||||
|
fill: #000000;
|
||||||
|
}
|
||||||
|
.plyr__menu__container .plyr__control:hover svg,
|
||||||
|
.plyr__menu__container button:hover svg {
|
||||||
|
fill: #000000;
|
||||||
|
}
|
||||||
|
/* Settings button when menu is open */
|
||||||
|
.plyr__control[aria-expanded=true] svg {
|
||||||
|
fill: #000000;
|
||||||
|
}
|
||||||
|
/* Settings button hover */
|
||||||
|
.plyr__control[aria-expanded=true]:hover svg {
|
||||||
|
fill: #000000;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
className="plyr-react plyr"
|
||||||
|
playsInline
|
||||||
|
controls
|
||||||
|
>
|
||||||
|
<source src={src} type="video/mp4" />
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LearnHousePlayer
|
||||||
|
|
@ -2,6 +2,7 @@ import React from 'react'
|
||||||
import YouTube from 'react-youtube'
|
import YouTube from 'react-youtube'
|
||||||
import { getActivityMediaDirectory } from '@services/media/media'
|
import { getActivityMediaDirectory } from '@services/media/media'
|
||||||
import { useOrg } from '@components/Contexts/OrgContext'
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
import LearnHousePlayer from './LearnHousePlayer'
|
||||||
|
|
||||||
interface VideoDetails {
|
interface VideoDetails {
|
||||||
startTime?: number
|
startTime?: number
|
||||||
|
|
@ -28,7 +29,6 @@ interface VideoActivityProps {
|
||||||
function VideoActivity({ activity, course }: VideoActivityProps) {
|
function VideoActivity({ activity, course }: VideoActivityProps) {
|
||||||
const org = useOrg() as any
|
const org = useOrg() as any
|
||||||
const [videoId, setVideoId] = React.useState('')
|
const [videoId, setVideoId] = React.useState('')
|
||||||
const videoRef = React.useRef<HTMLVideoElement>(null)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (activity?.content?.uri) {
|
if (activity?.content?.uri) {
|
||||||
|
|
@ -48,41 +48,24 @@ function VideoActivity({ activity, course }: VideoActivityProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle native video time update
|
|
||||||
const handleTimeUpdate = () => {
|
|
||||||
const video = videoRef.current
|
|
||||||
if (video && activity.details?.endTime) {
|
|
||||||
if (video.currentTime >= activity.details.endTime) {
|
|
||||||
video.pause()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle native video load
|
|
||||||
const handleVideoLoad = () => {
|
|
||||||
const video = videoRef.current
|
|
||||||
if (video && activity.details) {
|
|
||||||
video.currentTime = activity.details.startTime || 0
|
|
||||||
video.autoplay = activity.details.autoplay || false
|
|
||||||
video.muted = activity.details.muted || false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-full px-2 sm:px-4">
|
<div className="w-full max-w-full px-2 sm:px-4">
|
||||||
{activity && (
|
{activity && (
|
||||||
<>
|
<>
|
||||||
|
{console.log('Activity type:', activity.activity_sub_type)}
|
||||||
|
{console.log('Video source:', getVideoSrc())}
|
||||||
{activity.activity_sub_type === 'SUBTYPE_VIDEO_HOSTED' && (
|
{activity.activity_sub_type === 'SUBTYPE_VIDEO_HOSTED' && (
|
||||||
<div className="my-3 md:my-5 w-full">
|
<div className="my-3 md:my-5 w-full">
|
||||||
<div className="relative w-full aspect-video rounded-lg overflow-hidden ring-1 ring-gray-300/30 dark:ring-gray-600/30 sm:ring-gray-200/10 sm:dark:ring-gray-700/20 shadow-xs sm:shadow-none">
|
<div className="relative w-full aspect-video rounded-lg overflow-hidden ring-1 ring-gray-300/30 dark:ring-gray-600/30 sm:ring-gray-200/10 sm:dark:ring-gray-700/20 shadow-xs sm:shadow-none">
|
||||||
<video
|
{(() => {
|
||||||
ref={videoRef}
|
const src = getVideoSrc()
|
||||||
className="w-full h-full object-cover"
|
return src ? (
|
||||||
controls
|
<LearnHousePlayer
|
||||||
src={getVideoSrc()}
|
src={src}
|
||||||
onLoadedMetadata={handleVideoLoad}
|
details={activity.details}
|
||||||
onTimeUpdate={handleTimeUpdate}
|
/>
|
||||||
></video>
|
) : null
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -98,10 +81,18 @@ function VideoActivity({ activity, course }: VideoActivityProps) {
|
||||||
autoplay: activity.details?.autoplay ? 1 : 0,
|
autoplay: activity.details?.autoplay ? 1 : 0,
|
||||||
mute: activity.details?.muted ? 1 : 0,
|
mute: activity.details?.muted ? 1 : 0,
|
||||||
start: activity.details?.startTime || 0,
|
start: activity.details?.startTime || 0,
|
||||||
end: activity.details?.endTime || undefined
|
end: activity.details?.endTime || undefined,
|
||||||
|
controls: 1,
|
||||||
|
modestbranding: 1,
|
||||||
|
rel: 0
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
videoId={videoId}
|
videoId={videoId}
|
||||||
|
onReady={(event) => {
|
||||||
|
if (activity.details?.startTime) {
|
||||||
|
event.target.seekTo(activity.details.startTime, true)
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -95,70 +95,147 @@ function VideoModal({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const VideoSettingsForm = () => (
|
const VideoSettingsForm = () => {
|
||||||
<div className="space-y-4 mt-4 p-4 bg-gray-50 rounded-lg">
|
const convertToSeconds = (minutes: number, seconds: number) => {
|
||||||
<h3 className="font-medium text-gray-900 mb-3">Video Settings</h3>
|
return minutes * 60 + seconds;
|
||||||
<div className="grid grid-cols-2 gap-4">
|
};
|
||||||
<div>
|
|
||||||
<Label htmlFor="start-time">Start Time (seconds)</Label>
|
const convertFromSeconds = (totalSeconds: number) => {
|
||||||
<Input
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
id="start-time"
|
const seconds = totalSeconds % 60;
|
||||||
type="number"
|
return { minutes, seconds };
|
||||||
min="0"
|
};
|
||||||
value={videoDetails.startTime}
|
|
||||||
onChange={(e) => setVideoDetails({
|
const startTimeParts = convertFromSeconds(videoDetails.startTime);
|
||||||
...videoDetails,
|
const endTimeParts = videoDetails.endTime ? convertFromSeconds(videoDetails.endTime) : { minutes: 0, seconds: 0 };
|
||||||
startTime: Math.max(0, parseInt(e.target.value) || 0)
|
|
||||||
})}
|
return (
|
||||||
placeholder="0"
|
<div className="space-y-4 mt-4 p-4 bg-gray-50 rounded-lg">
|
||||||
/>
|
<h3 className="font-medium text-gray-900 mb-3">Video Settings</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<Label>Start Time</Label>
|
||||||
|
<div className="flex gap-2 mt-1">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={startTimeParts.minutes}
|
||||||
|
onChange={(e) => {
|
||||||
|
const minutes = Math.max(0, parseInt(e.target.value) || 0);
|
||||||
|
const seconds = startTimeParts.seconds;
|
||||||
|
setVideoDetails({
|
||||||
|
...videoDetails,
|
||||||
|
startTime: convertToSeconds(minutes, seconds)
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="0"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500 mt-1 block">Minutes</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
value={startTimeParts.seconds}
|
||||||
|
onChange={(e) => {
|
||||||
|
const minutes = startTimeParts.minutes;
|
||||||
|
const seconds = Math.max(0, Math.min(59, parseInt(e.target.value) || 0));
|
||||||
|
setVideoDetails({
|
||||||
|
...videoDetails,
|
||||||
|
startTime: convertToSeconds(minutes, seconds)
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
placeholder="0"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500 mt-1 block">Seconds</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label>End Time (optional)</Label>
|
||||||
|
<div className="flex gap-2 mt-1">
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={endTimeParts.minutes}
|
||||||
|
onChange={(e) => {
|
||||||
|
const minutes = Math.max(0, parseInt(e.target.value) || 0);
|
||||||
|
const seconds = endTimeParts.seconds;
|
||||||
|
const totalSeconds = convertToSeconds(minutes, seconds);
|
||||||
|
if (totalSeconds > videoDetails.startTime) {
|
||||||
|
setVideoDetails({
|
||||||
|
...videoDetails,
|
||||||
|
endTime: totalSeconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="0"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500 mt-1 block">Minutes</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="59"
|
||||||
|
value={endTimeParts.seconds}
|
||||||
|
onChange={(e) => {
|
||||||
|
const minutes = endTimeParts.minutes;
|
||||||
|
const seconds = Math.max(0, Math.min(59, parseInt(e.target.value) || 0));
|
||||||
|
const totalSeconds = convertToSeconds(minutes, seconds);
|
||||||
|
if (totalSeconds > videoDetails.startTime) {
|
||||||
|
setVideoDetails({
|
||||||
|
...videoDetails,
|
||||||
|
endTime: totalSeconds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
placeholder="0"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-gray-500 mt-1 block">Seconds</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div className="flex items-center space-x-6 mt-4">
|
||||||
<Label htmlFor="end-time">End Time (seconds, optional)</Label>
|
<label className="flex items-center space-x-2">
|
||||||
<Input
|
<input
|
||||||
id="end-time"
|
type="checkbox"
|
||||||
type="number"
|
checked={videoDetails.autoplay}
|
||||||
min={videoDetails.startTime + 1}
|
onChange={(e) => setVideoDetails({
|
||||||
value={videoDetails.endTime || ''}
|
...videoDetails,
|
||||||
onChange={(e) => setVideoDetails({
|
autoplay: e.target.checked
|
||||||
...videoDetails,
|
})}
|
||||||
endTime: e.target.value ? parseInt(e.target.value) : null
|
className="rounded border-gray-300 text-black focus:ring-black"
|
||||||
})}
|
/>
|
||||||
placeholder="Leave empty for full duration"
|
<span className="text-sm text-gray-700">Autoplay video</span>
|
||||||
/>
|
</label>
|
||||||
|
|
||||||
|
<label className="flex items-center space-x-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={videoDetails.muted}
|
||||||
|
onChange={(e) => setVideoDetails({
|
||||||
|
...videoDetails,
|
||||||
|
muted: e.target.checked
|
||||||
|
})}
|
||||||
|
className="rounded border-gray-300 text-black focus:ring-black"
|
||||||
|
/>
|
||||||
|
<span className="text-sm text-gray-700">Start muted</span>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
<div className="flex items-center space-x-6 mt-4">
|
};
|
||||||
<label className="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={videoDetails.autoplay}
|
|
||||||
onChange={(e) => setVideoDetails({
|
|
||||||
...videoDetails,
|
|
||||||
autoplay: e.target.checked
|
|
||||||
})}
|
|
||||||
className="rounded border-gray-300 text-black focus:ring-black"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">Autoplay video</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className="flex items-center space-x-2">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={videoDetails.muted}
|
|
||||||
onChange={(e) => setVideoDetails({
|
|
||||||
...videoDetails,
|
|
||||||
muted: e.target.checked
|
|
||||||
})}
|
|
||||||
className="rounded border-gray-300 text-black focus:ring-black"
|
|
||||||
/>
|
|
||||||
<span className="text-sm text-gray-700">Start muted</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form.Root onSubmit={handleSubmit}>
|
<Form.Root onSubmit={handleSubmit}>
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"nextjs-toploader": "^1.6.12",
|
"nextjs-toploader": "^1.6.12",
|
||||||
|
"plyr": "^3.7.8",
|
||||||
"prosemirror-state": "^1.4.3",
|
"prosemirror-state": "^1.4.3",
|
||||||
"randomcolor": "^0.6.2",
|
"randomcolor": "^0.6.2",
|
||||||
"re-resizable": "^6.11.2",
|
"re-resizable": "^6.11.2",
|
||||||
|
|
@ -75,6 +76,7 @@
|
||||||
"react-dom": "19.0.0",
|
"react-dom": "19.0.0",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-katex": "^3.0.1",
|
"react-katex": "^3.0.1",
|
||||||
|
"react-plyr": "^2.2.0",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
"react-youtube": "^10.1.0",
|
"react-youtube": "^10.1.0",
|
||||||
"require-in-the-middle": "^7.5.2",
|
"require-in-the-middle": "^7.5.2",
|
||||||
|
|
|
||||||
53
apps/web/pnpm-lock.yaml
generated
53
apps/web/pnpm-lock.yaml
generated
|
|
@ -180,6 +180,9 @@ importers:
|
||||||
nextjs-toploader:
|
nextjs-toploader:
|
||||||
specifier: ^1.6.12
|
specifier: ^1.6.12
|
||||||
version: 1.6.12(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 1.6.12(next@15.3.2(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
plyr:
|
||||||
|
specifier: ^3.7.8
|
||||||
|
version: 3.7.8
|
||||||
prosemirror-state:
|
prosemirror-state:
|
||||||
specifier: ^1.4.3
|
specifier: ^1.4.3
|
||||||
version: 1.4.3
|
version: 1.4.3
|
||||||
|
|
@ -204,6 +207,9 @@ importers:
|
||||||
react-katex:
|
react-katex:
|
||||||
specifier: ^3.0.1
|
specifier: ^3.0.1
|
||||||
version: 3.0.1(prop-types@15.8.1)(react@19.0.0)
|
version: 3.0.1(prop-types@15.8.1)(react@19.0.0)
|
||||||
|
react-plyr:
|
||||||
|
specifier: ^2.2.0
|
||||||
|
version: 2.2.0(react@19.0.0)
|
||||||
react-spinners:
|
react-spinners:
|
||||||
specifier: ^0.13.8
|
specifier: ^0.13.8
|
||||||
version: 0.13.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 0.13.8(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
|
@ -2063,6 +2069,9 @@ packages:
|
||||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
core-js@3.42.0:
|
||||||
|
resolution: {integrity: sha512-Sz4PP4ZA+Rq4II21qkNqOEDTDrCvcANId3xpIgB34NDkWc3UduWj2dqEtN9yZIq8Dk3HyPI33x9sqqU5C8sr0g==}
|
||||||
|
|
||||||
crelt@1.0.6:
|
crelt@1.0.6:
|
||||||
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
|
||||||
|
|
||||||
|
|
@ -2090,6 +2099,9 @@ packages:
|
||||||
currency-codes@2.2.0:
|
currency-codes@2.2.0:
|
||||||
resolution: {integrity: sha512-vpbQc5sEYHGdTVAYUhHnKv0DWiYLRvzl/KKyqeHzBh7HD/j3UlWoScpZ9tN/jG6w2feddWoObsBbaNVu5yDapg==}
|
resolution: {integrity: sha512-vpbQc5sEYHGdTVAYUhHnKv0DWiYLRvzl/KKyqeHzBh7HD/j3UlWoScpZ9tN/jG6w2feddWoObsBbaNVu5yDapg==}
|
||||||
|
|
||||||
|
custom-event-polyfill@1.0.7:
|
||||||
|
resolution: {integrity: sha512-TDDkd5DkaZxZFM8p+1I3yAlvM3rSr1wbrOliG4yJiwinMZN8z/iGL7BTlDkrJcYTmgUSb4ywVCc3ZaUtOtC76w==}
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8:
|
damerau-levenshtein@1.0.8:
|
||||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||||
|
|
||||||
|
|
@ -2783,6 +2795,9 @@ packages:
|
||||||
load-script@1.0.0:
|
load-script@1.0.0:
|
||||||
resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
|
resolution: {integrity: sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==}
|
||||||
|
|
||||||
|
loadjs@4.3.0:
|
||||||
|
resolution: {integrity: sha512-vNX4ZZLJBeDEOBvdr2v/F+0aN5oMuPu7JTqrMwp+DtgK+AryOlpy6Xtm2/HpNr+azEa828oQjOtWsB6iDtSfSQ==}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -3006,6 +3021,9 @@ packages:
|
||||||
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
plyr@3.7.8:
|
||||||
|
resolution: {integrity: sha512-yG/EHDobwbB/uP+4Bm6eUpJ93f8xxHjjk2dYcD1Oqpe1EcuQl5tzzw9Oq+uVAzd2lkM11qZfydSiyIpiB8pgdA==}
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0:
|
possible-typed-array-names@1.1.0:
|
||||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -3121,6 +3139,9 @@ packages:
|
||||||
randomcolor@0.6.2:
|
randomcolor@0.6.2:
|
||||||
resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==}
|
resolution: {integrity: sha512-Mn6TbyYpFgwFuQ8KJKqf3bqqY9O1y37/0jgSK/61PUxV4QfIMv0+K2ioq8DfOjkBslcjwSzRfIDEXfzA9aCx7A==}
|
||||||
|
|
||||||
|
rangetouch@2.0.1:
|
||||||
|
resolution: {integrity: sha512-sln+pNSc8NGaHoLzwNBssFSf/rSYkqeBXzX1AtJlkJiUaVSJSbRAWJk+4omsXkN+EJalzkZhWQ3th1m0FpR5xA==}
|
||||||
|
|
||||||
re-resizable@6.11.2:
|
re-resizable@6.11.2:
|
||||||
resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==}
|
resolution: {integrity: sha512-2xI2P3OHs5qw7K0Ud1aLILK6MQxW50TcO+DetD9eIV58j84TqYeHoZcL9H4GXFXXIh7afhH8mv5iUCXII7OW7A==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -3164,6 +3185,11 @@ packages:
|
||||||
prop-types: ^15.8.1
|
prop-types: ^15.8.1
|
||||||
react: '>=15.3.2 <=18'
|
react: '>=15.3.2 <=18'
|
||||||
|
|
||||||
|
react-plyr@2.2.0:
|
||||||
|
resolution: {integrity: sha512-05oBKvCC9F2VMP7BNhh3x7xeK6PNUMYBPPWIip5sDRuey4HmpghfLQyOfsbNKTssR6kkxRSi2X9neYmfr91NuA==}
|
||||||
|
peerDependencies:
|
||||||
|
react: 16.x
|
||||||
|
|
||||||
react-redux@9.2.0:
|
react-redux@9.2.0:
|
||||||
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
resolution: {integrity: sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|
@ -3552,6 +3578,9 @@ packages:
|
||||||
uri-js@4.4.1:
|
uri-js@4.4.1:
|
||||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||||
|
|
||||||
|
url-polyfill@1.1.13:
|
||||||
|
resolution: {integrity: sha512-tXzkojrv2SujumYthZ/WjF7jaSfNhSXlYMpE5AYdL2I3D7DCeo+mch8KtW2rUuKjDg+3VXODXHVgipt8yGY/eQ==}
|
||||||
|
|
||||||
use-callback-ref@1.3.3:
|
use-callback-ref@1.3.3:
|
||||||
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
@ -5388,6 +5417,8 @@ snapshots:
|
||||||
|
|
||||||
cookie@0.7.2: {}
|
cookie@0.7.2: {}
|
||||||
|
|
||||||
|
core-js@3.42.0: {}
|
||||||
|
|
||||||
crelt@1.0.6: {}
|
crelt@1.0.6: {}
|
||||||
|
|
||||||
cross-spawn@7.0.6:
|
cross-spawn@7.0.6:
|
||||||
|
|
@ -5417,6 +5448,8 @@ snapshots:
|
||||||
first-match: 0.0.1
|
first-match: 0.0.1
|
||||||
nub: 0.0.0
|
nub: 0.0.0
|
||||||
|
|
||||||
|
custom-event-polyfill@1.0.7: {}
|
||||||
|
|
||||||
damerau-levenshtein@1.0.8: {}
|
damerau-levenshtein@1.0.8: {}
|
||||||
|
|
||||||
data-view-buffer@1.0.2:
|
data-view-buffer@1.0.2:
|
||||||
|
|
@ -6238,6 +6271,8 @@ snapshots:
|
||||||
|
|
||||||
load-script@1.0.0: {}
|
load-script@1.0.0: {}
|
||||||
|
|
||||||
|
loadjs@4.3.0: {}
|
||||||
|
|
||||||
locate-path@6.0.0:
|
locate-path@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
p-locate: 5.0.0
|
p-locate: 5.0.0
|
||||||
|
|
@ -6463,6 +6498,14 @@ snapshots:
|
||||||
|
|
||||||
picomatch@4.0.2: {}
|
picomatch@4.0.2: {}
|
||||||
|
|
||||||
|
plyr@3.7.8:
|
||||||
|
dependencies:
|
||||||
|
core-js: 3.42.0
|
||||||
|
custom-event-polyfill: 1.0.7
|
||||||
|
loadjs: 4.3.0
|
||||||
|
rangetouch: 2.0.1
|
||||||
|
url-polyfill: 1.1.13
|
||||||
|
|
||||||
possible-typed-array-names@1.1.0: {}
|
possible-typed-array-names@1.1.0: {}
|
||||||
|
|
||||||
postcss-value-parser@4.2.0: {}
|
postcss-value-parser@4.2.0: {}
|
||||||
|
|
@ -6617,6 +6660,8 @@ snapshots:
|
||||||
|
|
||||||
randomcolor@0.6.2: {}
|
randomcolor@0.6.2: {}
|
||||||
|
|
||||||
|
rangetouch@2.0.1: {}
|
||||||
|
|
||||||
re-resizable@6.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
re-resizable@6.11.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
|
@ -6656,6 +6701,12 @@ snapshots:
|
||||||
prop-types: 15.8.1
|
prop-types: 15.8.1
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
|
||||||
|
react-plyr@2.2.0(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
plyr: 3.7.8
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 19.0.0
|
||||||
|
|
||||||
react-redux@9.2.0(@types/react@19.0.10)(react@19.0.0)(redux@5.0.1):
|
react-redux@9.2.0(@types/react@19.0.10)(react@19.0.0)(redux@5.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/use-sync-external-store': 0.0.6
|
'@types/use-sync-external-store': 0.0.6
|
||||||
|
|
@ -7150,6 +7201,8 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode: 2.3.1
|
punycode: 2.3.1
|
||||||
|
|
||||||
|
url-polyfill@1.1.13: {}
|
||||||
|
|
||||||
use-callback-ref@1.3.3(@types/react@19.0.10)(react@19.0.0):
|
use-callback-ref@1.3.3(@types/react@19.0.10)(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
|
|
||||||
17
apps/web/types/react-plyr.d.ts
vendored
Normal file
17
apps/web/types/react-plyr.d.ts
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
declare module 'react-plyr' {
|
||||||
|
import { Component } from 'react'
|
||||||
|
|
||||||
|
interface PlyrProps {
|
||||||
|
source: {
|
||||||
|
type: string
|
||||||
|
sources: Array<{
|
||||||
|
src: string
|
||||||
|
type: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
options?: any
|
||||||
|
onReady?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Plyr extends Component<PlyrProps> {}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue