From 50cd8b26c6d1208ffa9060aebab3041f70f59337 Mon Sep 17 00:00:00 2001 From: Enea Gjoka Date: Sat, 30 Aug 2025 17:56:28 -0400 Subject: [PATCH 01/17] Initial chunked uploads --- apps/web/components/community/index.tsx | 35 ++++++- apps/web/lib/chunked-upload.ts | 126 ++++++++++++++++++++++++ apps/web/services/medialit.ts | 121 +++++++++++++++++++++-- 3 files changed, 274 insertions(+), 8 deletions(-) create mode 100644 apps/web/lib/chunked-upload.ts diff --git a/apps/web/components/community/index.tsx b/apps/web/components/community/index.tsx index 9eeb8e600..bddba89bf 100644 --- a/apps/web/components/community/index.tsx +++ b/apps/web/components/community/index.tsx @@ -579,13 +579,44 @@ export function CommunityForum({ const uploadFile = async (file: File) => { try { const presignedUrl = await getPresignedUrl(); - const media = await uploadToServer(presignedUrl, file); - return media; + + // Use chunked upload for files larger than 10MB + const useChunkedUpload = file.size > 10 * 1024 * 1024; + + if (useChunkedUpload) { + const media = await uploadFileInChunks(presignedUrl, file); + return media; + } else { + const media = await uploadToServer(presignedUrl, file); + return media; + } } catch (err) { throw new Error(`Media upload: ${err.message}`); } }; + const uploadFileInChunks = async ( + presignedUrl: string, + file: File, + ): Promise => { + const { uploadFileInChunks: uploadChunked } = await import("../../lib/chunked-upload"); + + return uploadChunked({ + file, + presignedUrl, + access: "public", + caption: file.name, + group: community?.name, + onProgress: (progress) => { + // You can add progress tracking here if needed + console.log(`Upload progress: ${progress.percentage}%`); + }, + onError: (error) => { + console.error("Chunked upload error:", error); + }, + }); + }; + const uploadToServer = async ( presignedUrl: string, file: File, diff --git a/apps/web/lib/chunked-upload.ts b/apps/web/lib/chunked-upload.ts new file mode 100644 index 000000000..54e1a1c32 --- /dev/null +++ b/apps/web/lib/chunked-upload.ts @@ -0,0 +1,126 @@ +import { Media } from "@courselit/common-models"; +import { + initializeChunkedUpload, + uploadChunk, + completeChunkedUpload, + abortChunkedUpload, + ChunkedUploadInit, +} from "../services/medialit"; + +const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks + +export interface ChunkedUploadProgress { + uploadedChunks: number; + totalChunks: number; + percentage: number; + uploadedBytes: number; + totalBytes: number; +} + +export interface ChunkedUploadOptions { + file: File; + presignedUrl: string; + access?: string; + caption?: string; + group?: string; + onProgress?: (progress: ChunkedUploadProgress) => void; + onError?: (error: Error) => void; + chunkSize?: number; +} + +export class ChunkedUploader { + private file: File; + private presignedUrl: string; + private options: ChunkedUploadOptions; + private uploadId: string | null = null; + private aborted = false; + private chunkSize: number; + + constructor(options: ChunkedUploadOptions) { + this.file = options.file; + this.presignedUrl = options.presignedUrl; + this.options = options; + this.chunkSize = options.chunkSize || CHUNK_SIZE; + } + + async upload(): Promise { + try { + const totalChunks = Math.ceil(this.file.size / this.chunkSize); + + // Initialize chunked upload + const initParams: ChunkedUploadInit = { + fileName: this.file.name, + fileSize: this.file.size, + mimeType: this.file.type, + totalChunks, + access: this.options.access, + caption: this.options.caption, + group: this.options.group, + }; + + const initResponse = await initializeChunkedUpload(initParams, this.presignedUrl); + this.uploadId = initResponse.uploadId; + + // Upload chunks + let uploadedBytes = 0; + for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { + if (this.aborted) { + throw new Error("Upload aborted"); + } + + const start = chunkNumber * this.chunkSize; + const end = Math.min(start + this.chunkSize, this.file.size); + const chunk = this.file.slice(start, end); + + await uploadChunk(this.uploadId, chunkNumber, chunk, this.presignedUrl); + + uploadedBytes += chunk.size; + + // Report progress + if (this.options.onProgress) { + const progress: ChunkedUploadProgress = { + uploadedChunks: chunkNumber + 1, + totalChunks, + percentage: Math.round((uploadedBytes / this.file.size) * 100), + uploadedBytes, + totalBytes: this.file.size, + }; + this.options.onProgress(progress); + } + } + + // Complete upload + const media = await completeChunkedUpload(this.uploadId, this.presignedUrl); + return media; + } catch (error) { + if (this.uploadId && !this.aborted) { + try { + await abortChunkedUpload(this.uploadId, this.presignedUrl); + } catch (abortError) { + console.error("Failed to abort chunked upload:", abortError); + } + } + + if (this.options.onError) { + this.options.onError(error as Error); + } + throw error; + } + } + + async abort(): Promise { + this.aborted = true; + if (this.uploadId) { + await abortChunkedUpload(this.uploadId, this.presignedUrl); + } + } +} + +export async function uploadFileInChunks(options: ChunkedUploadOptions): Promise { + const uploader = new ChunkedUploader(options); + return uploader.upload(); +} + +export function shouldUseChunkedUpload(file: File, threshold: number = 10 * 1024 * 1024): boolean { + return file.size > threshold; // Default 10MB threshold +} \ No newline at end of file diff --git a/apps/web/services/medialit.ts b/apps/web/services/medialit.ts index 73b534742..0e2a01451 100644 --- a/apps/web/services/medialit.ts +++ b/apps/web/services/medialit.ts @@ -1,5 +1,5 @@ import { Media } from "@courselit/common-models"; -import { responses } from "../config/strings.ts"; +import { responses } from "../config/strings"; const medialitServer = process.env.MEDIALIT_SERVER || "https://medialit.cloud"; @@ -108,11 +108,7 @@ export async function deleteMedia(mediaId: string): Promise { throw new Error(response.error); } - if (response.message === "success") { - return true; - } else { - throw new Error(response.message); - } + return response.message === "success"; } function checkMediaLitAPIKeyOrThrow() { @@ -120,3 +116,116 @@ function checkMediaLitAPIKeyOrThrow() { throw new Error(responses.medialit_apikey_notfound); } } + +// Chunked upload functions +export interface ChunkedUploadInit { + fileName: string; + fileSize: number; + mimeType: string; + totalChunks: number; + access?: string; + caption?: string; + group?: string; +} + +export interface ChunkedUploadResponse { + uploadId: string; + message: string; +} + +export interface ChunkUploadResponse { + message: string; + uploadedChunks: number; + totalChunks: number; +} + +export async function initializeChunkedUpload( + params: ChunkedUploadInit, + presignedUrl: string, +): Promise { + const response = await fetch(`${presignedUrl.replace('/create', '/chunked/init')}`, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to initialize chunked upload"); + } + + return response.json(); +} + +export async function uploadChunk( + uploadId: string, + chunkNumber: number, + chunk: Blob, + presignedUrl: string, +): Promise { + const formData = new FormData(); + formData.append("chunk", chunk); + formData.append("chunkNumber", chunkNumber.toString()); + + const baseUrl = presignedUrl.split('?')[0].replace('/create', ''); + const queryParams = presignedUrl.split('?')[1] || ''; + const uploadUrl = `${baseUrl}/chunked/upload/${uploadId}?${queryParams}`; + + const response = await fetch(uploadUrl, { + method: "POST", + body: formData, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to upload chunk"); + } + + return response.json(); +} + +export async function completeChunkedUpload( + uploadId: string, + presignedUrl: string, +): Promise { + const baseUrl = presignedUrl.split('?')[0].replace('/create', ''); + const queryParams = presignedUrl.split('?')[1] || ''; + const completeUrl = `${baseUrl}/chunked/complete/${uploadId}?${queryParams}`; + + const response = await fetch(completeUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to complete chunked upload"); + } + + return response.json(); +} + +export async function abortChunkedUpload( + uploadId: string, + presignedUrl: string, +): Promise { + const baseUrl = presignedUrl.split('?')[0].replace('/create', ''); + const queryParams = presignedUrl.split('?')[1] || ''; + const abortUrl = `${baseUrl}/chunked/abort/${uploadId}?${queryParams}`; + + const response = await fetch(abortUrl, { + method: "DELETE", + headers: { + "content-type": "application/json", + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to abort chunked upload"); + } +} From 2692fd267a6a45ee75ca4d67e0818b22a0ce4b86 Mon Sep 17 00:00:00 2001 From: Enea Gjoka Date: Sat, 30 Aug 2025 20:14:00 -0400 Subject: [PATCH 02/17] Fix URL construction for chunked upload --- apps/web/services/medialit.ts | 40 ++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/apps/web/services/medialit.ts b/apps/web/services/medialit.ts index 0e2a01451..f1b62587f 100644 --- a/apps/web/services/medialit.ts +++ b/apps/web/services/medialit.ts @@ -143,7 +143,15 @@ export async function initializeChunkedUpload( params: ChunkedUploadInit, presignedUrl: string, ): Promise { - const response = await fetch(`${presignedUrl.replace('/create', '/chunked/init')}`, { + // Extract base URL and query params from presigned URL + const url = new URL(presignedUrl); + const baseUrl = `${url.protocol}//${url.host}`; + const queryParams = url.search; + + // Construct chunked init URL + const initUrl = `${baseUrl}/media/chunked/init${queryParams}`; + + const response = await fetch(initUrl, { method: "POST", headers: { "content-type": "application/json", @@ -169,9 +177,13 @@ export async function uploadChunk( formData.append("chunk", chunk); formData.append("chunkNumber", chunkNumber.toString()); - const baseUrl = presignedUrl.split('?')[0].replace('/create', ''); - const queryParams = presignedUrl.split('?')[1] || ''; - const uploadUrl = `${baseUrl}/chunked/upload/${uploadId}?${queryParams}`; + // Extract base URL and query params from presigned URL + const url = new URL(presignedUrl); + const baseUrl = `${url.protocol}//${url.host}`; + const queryParams = url.search; + + // Construct chunk upload URL + const uploadUrl = `${baseUrl}/media/chunked/upload/${uploadId}${queryParams}`; const response = await fetch(uploadUrl, { method: "POST", @@ -190,9 +202,13 @@ export async function completeChunkedUpload( uploadId: string, presignedUrl: string, ): Promise { - const baseUrl = presignedUrl.split('?')[0].replace('/create', ''); - const queryParams = presignedUrl.split('?')[1] || ''; - const completeUrl = `${baseUrl}/chunked/complete/${uploadId}?${queryParams}`; + // Extract base URL and query params from presigned URL + const url = new URL(presignedUrl); + const baseUrl = `${url.protocol}//${url.host}`; + const queryParams = url.search; + + // Construct complete upload URL + const completeUrl = `${baseUrl}/media/chunked/complete/${uploadId}${queryParams}`; const response = await fetch(completeUrl, { method: "POST", @@ -213,9 +229,13 @@ export async function abortChunkedUpload( uploadId: string, presignedUrl: string, ): Promise { - const baseUrl = presignedUrl.split('?')[0].replace('/create', ''); - const queryParams = presignedUrl.split('?')[1] || ''; - const abortUrl = `${baseUrl}/chunked/abort/${uploadId}?${queryParams}`; + // Extract base URL and query params from presigned URL + const url = new URL(presignedUrl); + const baseUrl = `${url.protocol}//${url.host}`; + const queryParams = url.search; + + // Construct abort upload URL + const abortUrl = `${baseUrl}/media/chunked/abort/${uploadId}${queryParams}`; const response = await fetch(abortUrl, { method: "DELETE", From 0a57f8a7a1ce2567f3ae025f288dc7da247b93b9 Mon Sep 17 00:00:00 2001 From: Enea Gjoka Date: Sat, 30 Aug 2025 21:13:46 -0400 Subject: [PATCH 03/17] Fix URL construction for chunked upload of videos --- .../components-library/src/chunked-upload.ts | 249 ++++++++++++++++++ packages/components-library/src/index.ts | 10 + .../src/media-selector/index.tsx | 67 +++-- 3 files changed, 308 insertions(+), 18 deletions(-) create mode 100644 packages/components-library/src/chunked-upload.ts diff --git a/packages/components-library/src/chunked-upload.ts b/packages/components-library/src/chunked-upload.ts new file mode 100644 index 000000000..4d0e64044 --- /dev/null +++ b/packages/components-library/src/chunked-upload.ts @@ -0,0 +1,249 @@ +export interface ChunkedUploadInit { + fileName: string; + fileSize: number; + mimeType: string; + totalChunks: number; + access?: string; + caption?: string; + group?: string; +} + +export interface ChunkedUploadResponse { + uploadId: string; + message: string; +} + +export interface ChunkUploadResponse { + message: string; + uploadedChunks: number; + totalChunks: number; +} + +export interface ChunkedUploadProgress { + uploadedChunks: number; + totalChunks: number; + percentage: number; + uploadedBytes: number; + totalBytes: number; +} + +export interface ChunkedUploadOptions { + file: File; + presignedUrl: string; + access?: string; + caption?: string; + group?: string; + onProgress?: (progress: ChunkedUploadProgress) => void; + onError?: (error: Error) => void; + chunkSize?: number; +} + +const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks + +async function initializeChunkedUpload( + params: ChunkedUploadInit, + presignedUrl: string, +): Promise { + // Extract base URL and query params from presigned URL + const url = new URL(presignedUrl); + const baseUrl = `${url.protocol}//${url.host}`; + const queryParams = url.search; + + // Construct chunked init URL + const initUrl = `${baseUrl}/media/chunked/init${queryParams}`; + + const response = await fetch(initUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + body: JSON.stringify(params), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to initialize chunked upload"); + } + + return response.json(); +} + +async function uploadChunk( + uploadId: string, + chunkNumber: number, + chunk: Blob, + presignedUrl: string, +): Promise { + const formData = new FormData(); + formData.append("chunk", chunk); + formData.append("chunkNumber", chunkNumber.toString()); + + // Extract base URL and query params from presigned URL + const url = new URL(presignedUrl); + const baseUrl = `${url.protocol}//${url.host}`; + const queryParams = url.search; + + // Construct chunk upload URL + const uploadUrl = `${baseUrl}/media/chunked/upload/${uploadId}${queryParams}`; + + const response = await fetch(uploadUrl, { + method: "POST", + body: formData, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to upload chunk"); + } + + return response.json(); +} + +async function completeChunkedUpload( + uploadId: string, + presignedUrl: string, +): Promise { + // Extract base URL and query params from presigned URL + const url = new URL(presignedUrl); + const baseUrl = `${url.protocol}//${url.host}`; + const queryParams = url.search; + + // Construct complete upload URL + const completeUrl = `${baseUrl}/media/chunked/complete/${uploadId}${queryParams}`; + + const response = await fetch(completeUrl, { + method: "POST", + headers: { + "content-type": "application/json", + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to complete chunked upload"); + } + + return response.json(); +} + +async function abortChunkedUpload( + uploadId: string, + presignedUrl: string, +): Promise { + // Extract base URL and query params from presigned URL + const url = new URL(presignedUrl); + const baseUrl = `${url.protocol}//${url.host}`; + const queryParams = url.search; + + // Construct abort upload URL + const abortUrl = `${baseUrl}/media/chunked/abort/${uploadId}${queryParams}`; + + const response = await fetch(abortUrl, { + method: "DELETE", + headers: { + "content-type": "application/json", + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || "Failed to abort chunked upload"); + } +} + +export class ChunkedUploader { + private file: File; + private presignedUrl: string; + private options: ChunkedUploadOptions; + private uploadId: string | null = null; + private aborted = false; + private chunkSize: number; + + constructor(options: ChunkedUploadOptions) { + this.file = options.file; + this.presignedUrl = options.presignedUrl; + this.options = options; + this.chunkSize = options.chunkSize || CHUNK_SIZE; + } + + async upload(): Promise { + try { + const totalChunks = Math.ceil(this.file.size / this.chunkSize); + + // Initialize chunked upload + const initParams: ChunkedUploadInit = { + fileName: this.file.name, + fileSize: this.file.size, + mimeType: this.file.type, + totalChunks, + access: this.options.access, + caption: this.options.caption, + group: this.options.group, + }; + + const initResponse = await initializeChunkedUpload(initParams, this.presignedUrl); + this.uploadId = initResponse.uploadId; + + // Upload chunks + let uploadedBytes = 0; + for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { + if (this.aborted) { + throw new Error("Upload aborted"); + } + + const start = chunkNumber * this.chunkSize; + const end = Math.min(start + this.chunkSize, this.file.size); + const chunk = this.file.slice(start, end); + + await uploadChunk(this.uploadId, chunkNumber, chunk, this.presignedUrl); + + uploadedBytes += chunk.size; + + // Report progress + if (this.options.onProgress) { + const progress: ChunkedUploadProgress = { + uploadedChunks: chunkNumber + 1, + totalChunks, + percentage: Math.round((uploadedBytes / this.file.size) * 100), + uploadedBytes, + totalBytes: this.file.size, + }; + this.options.onProgress(progress); + } + } + + // Complete upload + const media = await completeChunkedUpload(this.uploadId, this.presignedUrl); + return media; + } catch (error) { + if (this.uploadId && !this.aborted) { + try { + await abortChunkedUpload(this.uploadId, this.presignedUrl); + } catch (abortError) { + console.error("Failed to abort chunked upload:", abortError); + } + } + + if (this.options.onError) { + this.options.onError(error as Error); + } + throw error; + } + } + + async abort(): Promise { + this.aborted = true; + if (this.uploadId) { + await abortChunkedUpload(this.uploadId, this.presignedUrl); + } + } +} + +export async function uploadFileInChunks(options: ChunkedUploadOptions): Promise { + const uploader = new ChunkedUploader(options); + return uploader.upload(); +} + +export function shouldUseChunkedUpload(file: File, threshold: number = 10 * 1024 * 1024): boolean { + return file.size > threshold; // Default 10MB threshold +} \ No newline at end of file diff --git a/packages/components-library/src/index.ts b/packages/components-library/src/index.ts index 2c37c6571..f5ab54437 100644 --- a/packages/components-library/src/index.ts +++ b/packages/components-library/src/index.ts @@ -38,6 +38,13 @@ import CssIdField from "./css-id-field"; import Select from "./select"; import Tooltip from "./tooltip"; import DragAndDrop from "./drag-and-drop"; +import { + uploadFileInChunks, + shouldUseChunkedUpload, + ChunkedUploader, + type ChunkedUploadOptions, + type ChunkedUploadProgress +} from "./chunked-upload"; export { Button as Button2 } from "./components/ui/button"; export * from "./components/ui/avatar"; @@ -105,4 +112,7 @@ export { CssIdField, DragAndDrop, getSymbolFromCurrency, + uploadFileInChunks, + shouldUseChunkedUpload, + ChunkedUploader, }; diff --git a/packages/components-library/src/media-selector/index.tsx b/packages/components-library/src/media-selector/index.tsx index 91b1ec69e..1f5aac8d2 100644 --- a/packages/components-library/src/media-selector/index.tsx +++ b/packages/components-library/src/media-selector/index.tsx @@ -71,7 +71,7 @@ const MediaSelector = (props: MediaSelectorProps) => { }; const [uploadData, setUploadData] = useState(defaultUploadData); const fileInput: React.RefObject = React.createRef(); - const [selectedFile, setSelectedFile] = useState(); + const [selectedFile, setSelectedFile] = useState(); const [caption, setCaption] = useState(""); const { toast } = useToast(); const { @@ -112,29 +112,60 @@ const MediaSelector = (props: MediaSelectorProps) => { }, [dialogOpened]); const uploadToServer = async (presignedUrl: string): Promise => { - const fD = new FormData(); - fD.append("caption", (uploadData.caption = caption)); - fD.append("access", uploadData.public ? "public" : "private"); - fD.append("file", selectedFile); - + const file = selectedFile; + if (!file) { + throw new Error("No file selected"); + } + + const access = uploadData.public ? "public" : "private"; + setUploadData( Object.assign({}, uploadData, { uploading: true, }), ); - const res = await fetch(presignedUrl, { - method: "POST", - body: fD, - }); - if (res.status === 200) { - const media = await res.json(); - if (media) { - delete media.group; - } - return media; + + // Use chunked upload for files larger than 10MB + const useChunkedUpload = file.size > 10 * 1024 * 1024; + + if (useChunkedUpload) { + // Use local chunked upload utility + const { uploadFileInChunks } = await import("../chunked-upload"); + + return uploadFileInChunks({ + file, + presignedUrl, + access, + caption: uploadData.caption || caption, + onProgress: (progress) => { + // You can add progress tracking here if needed + console.log(`Upload progress: ${progress.percentage}%`); + }, + onError: (error) => { + console.error("Chunked upload error:", error); + }, + }); } else { - const resp = await res.json(); - throw new Error(resp.error); + // Regular upload for smaller files + const fD = new FormData(); + fD.append("caption", (uploadData.caption = caption)); + fD.append("access", access); + fD.append("file", file); + + const res = await fetch(presignedUrl, { + method: "POST", + body: fD, + }); + if (res.status === 200) { + const media = await res.json(); + if (media) { + delete media.group; + } + return media; + } else { + const resp = await res.json(); + throw new Error(resp.error); + } } }; From 51c94e780811473b12b954cd612c79d103c3e017 Mon Sep 17 00:00:00 2001 From: Enea Gjoka Date: Sat, 30 Aug 2025 23:09:13 -0400 Subject: [PATCH 04/17] Always chunk uploads --- .../src/media-selector/index.tsx | 56 +++++-------------- 1 file changed, 15 insertions(+), 41 deletions(-) diff --git a/packages/components-library/src/media-selector/index.tsx b/packages/components-library/src/media-selector/index.tsx index 1f5aac8d2..c01f9b40d 100644 --- a/packages/components-library/src/media-selector/index.tsx +++ b/packages/components-library/src/media-selector/index.tsx @@ -125,48 +125,22 @@ const MediaSelector = (props: MediaSelectorProps) => { }), ); - // Use chunked upload for files larger than 10MB - const useChunkedUpload = file.size > 10 * 1024 * 1024; - - if (useChunkedUpload) { - // Use local chunked upload utility - const { uploadFileInChunks } = await import("../chunked-upload"); + // Use local chunked upload utility + const { uploadFileInChunks } = await import("../chunked-upload"); - return uploadFileInChunks({ - file, - presignedUrl, - access, - caption: uploadData.caption || caption, - onProgress: (progress) => { - // You can add progress tracking here if needed - console.log(`Upload progress: ${progress.percentage}%`); - }, - onError: (error) => { - console.error("Chunked upload error:", error); - }, - }); - } else { - // Regular upload for smaller files - const fD = new FormData(); - fD.append("caption", (uploadData.caption = caption)); - fD.append("access", access); - fD.append("file", file); - - const res = await fetch(presignedUrl, { - method: "POST", - body: fD, - }); - if (res.status === 200) { - const media = await res.json(); - if (media) { - delete media.group; - } - return media; - } else { - const resp = await res.json(); - throw new Error(resp.error); - } - } + return uploadFileInChunks({ + file, + presignedUrl, + access, + caption: uploadData.caption || caption, + onProgress: (progress) => { + // You can add progress tracking here if needed + console.log(`Upload progress: ${progress.percentage}%`); + }, + onError: (error) => { + console.error("Chunked upload error:", error); + }, + }); }; const uploadFile = async (e: React.FormEvent) => { From f6abb74070faff755f4797ac3f15665b7e2bd201 Mon Sep 17 00:00:00 2001 From: Enea Gjoka Date: Sat, 30 Aug 2025 23:10:50 -0400 Subject: [PATCH 05/17] Always chunk uploads --- packages/components-library/src/media-selector/index.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/components-library/src/media-selector/index.tsx b/packages/components-library/src/media-selector/index.tsx index c01f9b40d..e8ceb6c71 100644 --- a/packages/components-library/src/media-selector/index.tsx +++ b/packages/components-library/src/media-selector/index.tsx @@ -9,7 +9,7 @@ import { FetchBuilder } from "@courselit/utils"; import Form from "../form"; import FormField from "../form-field"; import React from "react"; -import { Button2, PageBuilderPropertyHeader, Tooltip, useToast } from ".."; +import { Button2, PageBuilderPropertyHeader, Tooltip, uploadFileInChunks, useToast } from ".."; import { X } from "lucide-react"; interface Strings { @@ -124,9 +124,6 @@ const MediaSelector = (props: MediaSelectorProps) => { uploading: true, }), ); - - // Use local chunked upload utility - const { uploadFileInChunks } = await import("../chunked-upload"); return uploadFileInChunks({ file, From 99d2cf87541947bdc57b451e6fe16b90aebbb3ff Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 31 Aug 2025 10:56:08 -0400 Subject: [PATCH 06/17] Fix docker images push --- .github/workflows/publish-docker-images.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-docker-images.yaml b/.github/workflows/publish-docker-images.yaml index e48572cf2..1751ac97a 100644 --- a/.github/workflows/publish-docker-images.yaml +++ b/.github/workflows/publish-docker-images.yaml @@ -30,13 +30,13 @@ jobs: id: metaapp uses: docker/metadata-action@v3 with: - images: codelit/courselit-app + images: egjoka/courselit-app - name: Docker meta for the queue id: metaqueue uses: docker/metadata-action@v3 with: - images: codelit/courselit-queue + images: egjoka/courselit-queue - name: Build and push app id: docker_build_app From 71844849c88f0ec7a29b3b522843a0e38aaa149e Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 31 Aug 2025 11:07:41 -0400 Subject: [PATCH 07/17] Fix hanging fonts issue --- .github/workflows/publish-docker-images.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/publish-docker-images.yaml b/.github/workflows/publish-docker-images.yaml index e48572cf2..b67016852 100644 --- a/.github/workflows/publish-docker-images.yaml +++ b/.github/workflows/publish-docker-images.yaml @@ -8,6 +8,8 @@ on: jobs: publish-docker-images: runs-on: ubuntu-latest + env: + NODE_OPTIONS: --no-network-family-autoselection --network-family-autoselection-attempt-timeout=5000 steps: - name: checkout uses: actions/checkout@v1 From 693bd8b9d141902832c69065db76c31ca058785a Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 31 Aug 2025 12:50:30 -0400 Subject: [PATCH 08/17] Fixing chunked uploads --- .github/workflows/publish-docker-images.yaml | 2 +- services/app/Dockerfile | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-docker-images.yaml b/.github/workflows/publish-docker-images.yaml index b7639e24c..dd7dc3aae 100644 --- a/.github/workflows/publish-docker-images.yaml +++ b/.github/workflows/publish-docker-images.yaml @@ -9,7 +9,7 @@ jobs: publish-docker-images: runs-on: ubuntu-latest env: - NODE_OPTIONS: --no-network-family-autoselection --network-family-autoselection-attempt-timeout=5000 + NODE_OPTIONS: --dns-result-order=ipv4first --no-network-family-autoselection --network-family-autoselection-attempt-timeout=5000 steps: - name: checkout uses: actions/checkout@v1 diff --git a/services/app/Dockerfile b/services/app/Dockerfile index f2d4ca5c5..c1e6bf14e 100644 --- a/services/app/Dockerfile +++ b/services/app/Dockerfile @@ -38,6 +38,9 @@ WORKDIR /app COPY --from=deps /app/ ./ +# Add ca-certificates to the builder image +RUN apk add --no-cache ca-certificates + # Build all workspaces RUN pnpm run build From 6415fdeaf186bd83fc24d55d87b7323c54033849 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 31 Aug 2025 13:50:16 -0400 Subject: [PATCH 09/17] Progress Indicator for Media Uploads --- .../src/media-selector/index.tsx | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/components-library/src/media-selector/index.tsx b/packages/components-library/src/media-selector/index.tsx index e8ceb6c71..6ddf4c068 100644 --- a/packages/components-library/src/media-selector/index.tsx +++ b/packages/components-library/src/media-selector/index.tsx @@ -73,6 +73,7 @@ const MediaSelector = (props: MediaSelectorProps) => { const fileInput: React.RefObject = React.createRef(); const [selectedFile, setSelectedFile] = useState(); const [caption, setCaption] = useState(""); + const [uploadProgress, setUploadProgress] = useState(0); const { toast } = useToast(); const { strings, @@ -131,8 +132,7 @@ const MediaSelector = (props: MediaSelectorProps) => { access, caption: uploadData.caption || caption, onProgress: (progress) => { - // You can add progress tracking here if needed - console.log(`Upload progress: ${progress.percentage}%`); + setUploadProgress(progress.percentage); }, onError: (error) => { console.error("Chunked upload error:", error); @@ -160,6 +160,7 @@ const MediaSelector = (props: MediaSelectorProps) => { setUploading(false); setSelectedFile(undefined); setCaption(""); + setUploadProgress(0); setDialogOpened(false); } }; @@ -271,6 +272,20 @@ const MediaSelector = (props: MediaSelectorProps) => { className="mt-2" required /> + {uploading && ( +
+
+ Upload Progress + {uploadProgress}% +
+
+
+
+
+ )} Date: Sun, 31 Aug 2025 14:05:29 -0400 Subject: [PATCH 10/17] Chunked Signatures for URL --- .github/workflows/publish-docker-images.yaml | 2 +- apps/web/components/community/index.tsx | 12 ++++---- .../src/media-selector/index.tsx | 30 +++++++++++++------ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish-docker-images.yaml b/.github/workflows/publish-docker-images.yaml index dd7dc3aae..308967965 100644 --- a/.github/workflows/publish-docker-images.yaml +++ b/.github/workflows/publish-docker-images.yaml @@ -1,4 +1,4 @@ -name: Publish NPM Packages +name: Publish Docker Images on: push: diff --git a/apps/web/components/community/index.tsx b/apps/web/components/community/index.tsx index bddba89bf..fba026fe6 100644 --- a/apps/web/components/community/index.tsx +++ b/apps/web/components/community/index.tsx @@ -579,10 +579,10 @@ export function CommunityForum({ const uploadFile = async (file: File) => { try { const presignedUrl = await getPresignedUrl(); - + // Use chunked upload for files larger than 10MB const useChunkedUpload = file.size > 10 * 1024 * 1024; - + if (useChunkedUpload) { const media = await uploadFileInChunks(presignedUrl, file); return media; @@ -599,8 +599,10 @@ export function CommunityForum({ presignedUrl: string, file: File, ): Promise => { - const { uploadFileInChunks: uploadChunked } = await import("../../lib/chunked-upload"); - + const { uploadFileInChunks: uploadChunked } = await import( + "../../lib/chunked-upload" + ); + return uploadChunked({ file, presignedUrl, @@ -609,7 +611,7 @@ export function CommunityForum({ group: community?.name, onProgress: (progress) => { // You can add progress tracking here if needed - console.log(`Upload progress: ${progress.percentage}%`); + console.error(`Upload progress: ${progress.percentage}%`); }, onError: (error) => { console.error("Chunked upload error:", error); diff --git a/packages/components-library/src/media-selector/index.tsx b/packages/components-library/src/media-selector/index.tsx index 6ddf4c068..3b84eb931 100644 --- a/packages/components-library/src/media-selector/index.tsx +++ b/packages/components-library/src/media-selector/index.tsx @@ -9,7 +9,13 @@ import { FetchBuilder } from "@courselit/utils"; import Form from "../form"; import FormField from "../form-field"; import React from "react"; -import { Button2, PageBuilderPropertyHeader, Tooltip, uploadFileInChunks, useToast } from ".."; +import { + Button2, + PageBuilderPropertyHeader, + Tooltip, + uploadFileInChunks, + useToast, +} from ".."; import { X } from "lucide-react"; interface Strings { @@ -96,10 +102,11 @@ const MediaSelector = (props: MediaSelectorProps) => { props.onSelection(media); }; - const getPresignedUrl = async () => { + const getPresignedUrl = async (chunked = false) => { const fetch = new FetchBuilder() .setUrl(`${address.backend}/api/media/presigned`) .setIsGraphQLEndpoint(false) + .setPayload({ chunked }) .build(); const response = await fetch.exec(); return response.url; @@ -117,18 +124,21 @@ const MediaSelector = (props: MediaSelectorProps) => { if (!file) { throw new Error("No file selected"); } - + const access = uploadData.public ? "public" : "private"; - + setUploadData( Object.assign({}, uploadData, { uploading: true, }), ); - + + // Get a chunked presigned URL with longer validity + const chunkedPresignedUrl = await getPresignedUrl(true); + return uploadFileInChunks({ file, - presignedUrl, + presignedUrl: chunkedPresignedUrl, access, caption: uploadData.caption || caption, onProgress: (progress) => { @@ -279,9 +289,11 @@ const MediaSelector = (props: MediaSelectorProps) => { {uploadProgress}%
-
From efb8704fa204bf4aa888ad3264c85be66fd46944 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 31 Aug 2025 14:06:09 -0400 Subject: [PATCH 11/17] Chunked Signatures for URL --- apps/docs/src/pages/en/website/blocks.md | 10 +- apps/docs/src/pages/en/website/themes.md | 36 ++-- apps/web/lib/chunked-upload.ts | 47 +++- apps/web/package.json | 202 +++++++++--------- apps/web/services/medialit.ts | 10 +- .../components-library/src/chunked-upload.ts | 57 +++-- packages/components-library/src/index.ts | 6 +- 7 files changed, 208 insertions(+), 160 deletions(-) diff --git a/apps/docs/src/pages/en/website/blocks.md b/apps/docs/src/pages/en/website/blocks.md index a937bd2c6..39357ab89 100644 --- a/apps/docs/src/pages/en/website/blocks.md +++ b/apps/docs/src/pages/en/website/blocks.md @@ -36,7 +36,7 @@ You will also see the newly added link on the header itself. 3. Click on the pencil icon against the newly added link to edit it as shown above. 4. Change the label (displayed as text on the header block) and the URL (where the user should be taken upon clicking the label on the header) and click `Done` to save. ![Header edit link](/assets/pages/header-edit-link.png) - + ### [Rich Text](#rich-text) @@ -61,7 +61,7 @@ You can also use the floating controls to do the same as shown below. 2. Click on the floating `link` button to reveal a popup text input. 3. In the popup text input, enter the URL as shown below. ![Create a hyperlink in rich text block](/assets/pages/rich-text-create-hyperlink.gif) - + ### [Hero](#hero) @@ -87,7 +87,7 @@ Following is how it looks on a page. 4. In the button action, enter the URL the user should be taken to upon clicking. a. If the URL is from your own school, use its relative form, i.e., `/courses`. b. If the URL is from some external website, use the absolute (complete) URL, i.e., `https://website.com/courses`. - + ### [Grid](#grid) @@ -132,7 +132,7 @@ A grid block comes in handy when you want to show some sort of list, for example 4. In the button action, enter the URL the user should be taken to upon clicking. a. If the URL is from your own school, use its relative form, i.e., `/courses`. b. If the URL is from some external website, use the absolute (complete) URL, i.e., `https://website.com/courses`. - + ### [Featured](#featured) @@ -268,7 +268,7 @@ In the `Design` panel, you can customize: - Maximum width - Vertical padding - Social media links (Facebook, Twitter, Instagram, LinkedIn, YouTube, Discord, GitHub) - + ## [Shared blocks](#shared-blocks) diff --git a/apps/docs/src/pages/en/website/themes.md b/apps/docs/src/pages/en/website/themes.md index 0700d2e29..5c704e4f2 100644 --- a/apps/docs/src/pages/en/website/themes.md +++ b/apps/docs/src/pages/en/website/themes.md @@ -192,14 +192,14 @@ The typography editor lets you customize text styles across your website. These - Header 3: Smaller titles for subsections - Header 4: Small titles for minor sections - Preheader: Introductory text that appears above headers - +
Subheaders - Subheader 1: Primary subheaders for section introductions - Subheader 2: Secondary subheaders for supporting text -
+
Body Text @@ -207,7 +207,7 @@ The typography editor lets you customize text styles across your website. These - Text 1: Main body text for content - Text 2: Secondary body text for supporting content - Caption: Small text for image captions and footnotes -
+
Interactive Elements @@ -215,7 +215,7 @@ The typography editor lets you customize text styles across your website. These - Link: Text for clickable links - Button: Text for buttons and calls-to-action - Input: Text for form fields and search boxes -
+ For each text style, you can customize: @@ -243,7 +243,7 @@ CourseLit provides a carefully curated selection of professional fonts, organize - **Mulish**: A geometric sans-serif with a modern feel - **Nunito**: A well-balanced font with rounded terminals - **Work Sans**: A clean, modern font with a geometric feel - +
Serif Fonts @@ -253,7 +253,7 @@ CourseLit provides a carefully curated selection of professional fonts, organize - **Playfair Display**: An elegant serif font for headings - **Roboto Slab**: A serif variant of Roboto - **Source Serif 4**: A serif font designed for digital reading -
+
Display Fonts @@ -264,7 +264,7 @@ CourseLit provides a carefully curated selection of professional fonts, organize - **Rubik**: A sans-serif with a geometric feel - **Oswald**: A reworking of the classic style - **Bebas Neue**: A display font with a strong personality -
+
Modern Fonts @@ -272,7 +272,7 @@ CourseLit provides a carefully curated selection of professional fonts, organize - **Lato**: A sans-serif font with a warm feel - **PT Sans**: A font designed for public use - **Quicksand**: A display sans-serif with rounded terminals -
+ Each font is optimized for web use and includes multiple weights for flexibility in design. All fonts support Latin characters and are carefully selected for their readability and professional appearance. @@ -290,7 +290,7 @@ The interactives editor allows you to customize the appearance of interactive el - Shadow effects: From None to 2X Large - Custom styles: Add your own custom styles using [supported Tailwind classes](#supported-tailwind-classes) - Disabled state: How the button looks when it can't be clicked - +
Link @@ -300,7 +300,7 @@ The interactives editor allows you to customize the appearance of interactive el - Text shadow: Add depth to your links - Custom styles: Add your own custom styles using [supported Tailwind classes](#supported-tailwind-classes) - Disabled state: How the link looks when it can't be clicked -
+
Card @@ -309,7 +309,7 @@ The interactives editor allows you to customize the appearance of interactive el - Border style: Choose from various border styles - Shadow effects: Add depth to your cards - Custom styles: Add your own custom styles using [supported Tailwind classes](#supported-tailwind-classes) -
+
Input @@ -320,7 +320,7 @@ The interactives editor allows you to customize the appearance of interactive el - Shadow effects: Add depth to your input fields - Custom styles: Add your own custom styles using [supported Tailwind classes](#supported-tailwind-classes) - Disabled state: How the input looks when it can't be used -
+ ### 4. Structure @@ -332,14 +332,14 @@ The structure editor lets you customize the layout of your pages, like section p Page - Maximum width options: - 2XL (42rem): Compact layout - 3XL (48rem): Standard layout - 4XL (56rem): Wide layout - 5XL (64rem): Extra wide layout - 6XL (72rem): Full width layout - +
Section - Horizontal padding: Space on the left and right sides (None to 9X Large) - Vertical padding: Space on the top and bottom (None to 9X Large) -
+ ## Publishing Changes @@ -387,7 +387,7 @@ When adding custom styles to interactive elements, you can use the following Tai - `text-6xl`: 6X large text - `text-7xl`: 7X large text - `text-8xl`: 8X large text - +
Padding @@ -399,7 +399,7 @@ When adding custom styles to interactive elements, you can use the following Tai #### Horizontal Padding - `px-4` to `px-20`: Horizontal padding from 1rem to 5rem -
+
Colors @@ -454,7 +454,7 @@ Variants available: `hover`, `disabled`, `dark` - `ease-out`: Ease out - `ease-in-out`: Ease in and out - `ease-linear`: Linear -
+
Transforms @@ -481,7 +481,7 @@ Variants available: `hover`, `disabled`, `dark` - `scale-110`: 110% scale - `scale-125`: 125% scale - `scale-150`: 150% scale -
+
Shadows diff --git a/apps/web/lib/chunked-upload.ts b/apps/web/lib/chunked-upload.ts index 54e1a1c32..7c5654149 100644 --- a/apps/web/lib/chunked-upload.ts +++ b/apps/web/lib/chunked-upload.ts @@ -46,7 +46,7 @@ export class ChunkedUploader { async upload(): Promise { try { const totalChunks = Math.ceil(this.file.size / this.chunkSize); - + // Initialize chunked upload const initParams: ChunkedUploadInit = { fileName: this.file.name, @@ -58,12 +58,19 @@ export class ChunkedUploader { group: this.options.group, }; - const initResponse = await initializeChunkedUpload(initParams, this.presignedUrl); + const initResponse = await initializeChunkedUpload( + initParams, + this.presignedUrl, + ); this.uploadId = initResponse.uploadId; // Upload chunks let uploadedBytes = 0; - for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { + for ( + let chunkNumber = 0; + chunkNumber < totalChunks; + chunkNumber++ + ) { if (this.aborted) { throw new Error("Upload aborted"); } @@ -72,7 +79,12 @@ export class ChunkedUploader { const end = Math.min(start + this.chunkSize, this.file.size); const chunk = this.file.slice(start, end); - await uploadChunk(this.uploadId, chunkNumber, chunk, this.presignedUrl); + await uploadChunk( + this.uploadId, + chunkNumber, + chunk, + this.presignedUrl, + ); uploadedBytes += chunk.size; @@ -81,7 +93,9 @@ export class ChunkedUploader { const progress: ChunkedUploadProgress = { uploadedChunks: chunkNumber + 1, totalChunks, - percentage: Math.round((uploadedBytes / this.file.size) * 100), + percentage: Math.round( + (uploadedBytes / this.file.size) * 100, + ), uploadedBytes, totalBytes: this.file.size, }; @@ -90,17 +104,23 @@ export class ChunkedUploader { } // Complete upload - const media = await completeChunkedUpload(this.uploadId, this.presignedUrl); + const media = await completeChunkedUpload( + this.uploadId, + this.presignedUrl, + ); return media; } catch (error) { if (this.uploadId && !this.aborted) { try { await abortChunkedUpload(this.uploadId, this.presignedUrl); } catch (abortError) { - console.error("Failed to abort chunked upload:", abortError); + console.error( + "Failed to abort chunked upload:", + abortError, + ); } } - + if (this.options.onError) { this.options.onError(error as Error); } @@ -116,11 +136,16 @@ export class ChunkedUploader { } } -export async function uploadFileInChunks(options: ChunkedUploadOptions): Promise { +export async function uploadFileInChunks( + options: ChunkedUploadOptions, +): Promise { const uploader = new ChunkedUploader(options); return uploader.upload(); } -export function shouldUseChunkedUpload(file: File, threshold: number = 10 * 1024 * 1024): boolean { +export function shouldUseChunkedUpload( + file: File, + threshold: number = 10 * 1024 * 1024, +): boolean { return file.size > threshold; // Default 10MB threshold -} \ No newline at end of file +} diff --git a/apps/web/package.json b/apps/web/package.json index 80c2bfe61..62abda36c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,103 +1,103 @@ { - "name": "@courselit/web", - "version": "0.61.12", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "prettier": "prettier --write **/*.ts" - }, - "dependencies": { - "@courselit/common-logic": "workspace:^", - "@courselit/common-models": "workspace:^", - "@courselit/components-library": "workspace:^", - "@courselit/icons": "workspace:^", - "@courselit/page-blocks": "workspace:^", - "@courselit/page-models": "workspace:^", - "@courselit/page-primitives": "workspace:^", - "@courselit/state-management": "workspace:^", - "@courselit/utils": "workspace:^", - "@courselit/email-editor": "workspace:^", - "@hookform/resolvers": "^3.9.1", - "@radix-ui/react-alert-dialog": "^1.1.2", - "@radix-ui/react-avatar": "^1.1.3", - "@radix-ui/react-checkbox": "^1.1.4", - "@radix-ui/react-collapsible": "^1.1.3", - "@radix-ui/react-compose-refs": "^1.1.1", - "@radix-ui/react-dialog": "^1.1.6", - "@radix-ui/react-dropdown-menu": "^2.1.6", - "@radix-ui/react-label": "^2.1.2", - "@radix-ui/react-popover": "^1.1.6", - "@radix-ui/react-radio-group": "^1.2.3", - "@radix-ui/react-scroll-area": "^1.2.3", - "@radix-ui/react-select": "^2.1.6", - "@radix-ui/react-separator": "^1.1.2", - "@radix-ui/react-slot": "^1.1.2", - "@radix-ui/react-switch": "^1.1.3", - "@radix-ui/react-tabs": "^1.1.3", - "@radix-ui/react-toast": "^1.2.6", - "@radix-ui/react-toggle": "^1.1.6", - "@radix-ui/react-toggle-group": "^1.1.7", - "@radix-ui/react-tooltip": "^1.1.8", - "@radix-ui/react-visually-hidden": "^1.1.0", - "@stripe/stripe-js": "^5.4.0", - "@types/base-64": "^1.0.0", - "archiver": "^5.3.1", - "aws4": "^1.13.2", - "base-64": "^1.0.0", - "chart.js": "^4.4.7", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.1", - "color-convert": "^3.1.0", - "cookie": "^0.4.2", - "date-fns": "^4.1.0", - "graphql": "^16.10.0", - "graphql-type-json": "^0.3.2", - "lodash.debounce": "^4.0.8", - "lucide-react": "^0.475.0", - "mongodb": "^6.15.0", - "mongoose": "^8.13.1", - "next": "^14.2.4", - "next-auth": "5.0.0-beta.19", - "next-themes": "^0.4.6", - "nodemailer": "^6.7.2", - "pug": "^3.0.2", - "razorpay": "^2.9.4", - "react": "^18.2.0", - "react-chartjs-2": "^5.3.0", - "react-csv": "^2.2.2", - "react-dom": "^18.2.0", - "react-hook-form": "^7.54.1", - "react-redux": "^8.1.2", - "recharts": "^2.15.1", - "remirror": "^3.0.1", - "sharp": "^0.33.2", - "slugify": "^1.6.5", - "stripe": "^17.5.0", - "tailwind-merge": "^2.5.4", - "tailwindcss-animate": "^1.0.7", - "zod": "^3.24.1" - }, - "devDependencies": { - "@types/bcryptjs": "^2.4.2", - "@types/cookie": "^0.4.1", - "@types/mongodb": "^4.0.7", - "@types/node": "17.0.21", - "@types/nodemailer": "^6.4.4", - "@types/pug": "^2.0.6", - "@types/react": "18.2.31", - "@types/react-redux": "^7.1.23", - "@types/redux-thunk": "^2.1.0", - "eslint": "8.48.0", - "eslint-config-next": "^14.0.4", - "eslint-config-prettier": "^9.0.0", - "postcss": "^8.4.27", - "prettier": "^3.0.2", - "tailwind-config": "workspace:^", - "tailwindcss": "^3.4.1", - "tsconfig": "workspace:^", - "typescript": "^5.6.2" - } + "name": "@courselit/web", + "version": "0.61.12", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "prettier": "prettier --write **/*.ts" + }, + "dependencies": { + "@courselit/common-logic": "workspace:^", + "@courselit/common-models": "workspace:^", + "@courselit/components-library": "workspace:^", + "@courselit/icons": "workspace:^", + "@courselit/page-blocks": "workspace:^", + "@courselit/page-models": "workspace:^", + "@courselit/page-primitives": "workspace:^", + "@courselit/state-management": "workspace:^", + "@courselit/utils": "workspace:^", + "@courselit/email-editor": "workspace:^", + "@hookform/resolvers": "^3.9.1", + "@radix-ui/react-alert-dialog": "^1.1.2", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-popover": "^1.1.6", + "@radix-ui/react-radio-group": "^1.2.3", + "@radix-ui/react-scroll-area": "^1.2.3", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.3", + "@radix-ui/react-toast": "^1.2.6", + "@radix-ui/react-toggle": "^1.1.6", + "@radix-ui/react-toggle-group": "^1.1.7", + "@radix-ui/react-tooltip": "^1.1.8", + "@radix-ui/react-visually-hidden": "^1.1.0", + "@stripe/stripe-js": "^5.4.0", + "@types/base-64": "^1.0.0", + "archiver": "^5.3.1", + "aws4": "^1.13.2", + "base-64": "^1.0.0", + "chart.js": "^4.4.7", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "color-convert": "^3.1.0", + "cookie": "^0.4.2", + "date-fns": "^4.1.0", + "graphql": "^16.10.0", + "graphql-type-json": "^0.3.2", + "lodash.debounce": "^4.0.8", + "lucide-react": "^0.475.0", + "mongodb": "^6.15.0", + "mongoose": "^8.13.1", + "next": "^14.2.4", + "next-auth": "5.0.0-beta.19", + "next-themes": "^0.4.6", + "nodemailer": "^6.7.2", + "pug": "^3.0.2", + "razorpay": "^2.9.4", + "react": "^18.2.0", + "react-chartjs-2": "^5.3.0", + "react-csv": "^2.2.2", + "react-dom": "^18.2.0", + "react-hook-form": "^7.54.1", + "react-redux": "^8.1.2", + "recharts": "^2.15.1", + "remirror": "^3.0.1", + "sharp": "^0.33.2", + "slugify": "^1.6.5", + "stripe": "^17.5.0", + "tailwind-merge": "^2.5.4", + "tailwindcss-animate": "^1.0.7", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.2", + "@types/cookie": "^0.4.1", + "@types/mongodb": "^4.0.7", + "@types/node": "17.0.21", + "@types/nodemailer": "^6.4.4", + "@types/pug": "^2.0.6", + "@types/react": "18.2.31", + "@types/react-redux": "^7.1.23", + "@types/redux-thunk": "^2.1.0", + "eslint": "8.48.0", + "eslint-config-next": "^14.0.4", + "eslint-config-prettier": "^9.0.0", + "postcss": "^8.4.27", + "prettier": "^3.0.2", + "tailwind-config": "workspace:^", + "tailwindcss": "^3.4.1", + "tsconfig": "workspace:^", + "typescript": "^5.6.2" + } } diff --git a/apps/web/services/medialit.ts b/apps/web/services/medialit.ts index f1b62587f..18115ff28 100644 --- a/apps/web/services/medialit.ts +++ b/apps/web/services/medialit.ts @@ -147,10 +147,10 @@ export async function initializeChunkedUpload( const url = new URL(presignedUrl); const baseUrl = `${url.protocol}//${url.host}`; const queryParams = url.search; - + // Construct chunked init URL const initUrl = `${baseUrl}/media/chunked/init${queryParams}`; - + const response = await fetch(initUrl, { method: "POST", headers: { @@ -181,7 +181,7 @@ export async function uploadChunk( const url = new URL(presignedUrl); const baseUrl = `${url.protocol}//${url.host}`; const queryParams = url.search; - + // Construct chunk upload URL const uploadUrl = `${baseUrl}/media/chunked/upload/${uploadId}${queryParams}`; @@ -206,7 +206,7 @@ export async function completeChunkedUpload( const url = new URL(presignedUrl); const baseUrl = `${url.protocol}//${url.host}`; const queryParams = url.search; - + // Construct complete upload URL const completeUrl = `${baseUrl}/media/chunked/complete/${uploadId}${queryParams}`; @@ -233,7 +233,7 @@ export async function abortChunkedUpload( const url = new URL(presignedUrl); const baseUrl = `${url.protocol}//${url.host}`; const queryParams = url.search; - + // Construct abort upload URL const abortUrl = `${baseUrl}/media/chunked/abort/${uploadId}${queryParams}`; diff --git a/packages/components-library/src/chunked-upload.ts b/packages/components-library/src/chunked-upload.ts index 4d0e64044..efecd8e92 100644 --- a/packages/components-library/src/chunked-upload.ts +++ b/packages/components-library/src/chunked-upload.ts @@ -48,10 +48,10 @@ async function initializeChunkedUpload( const url = new URL(presignedUrl); const baseUrl = `${url.protocol}//${url.host}`; const queryParams = url.search; - + // Construct chunked init URL const initUrl = `${baseUrl}/media/chunked/init${queryParams}`; - + const response = await fetch(initUrl, { method: "POST", headers: { @@ -82,7 +82,7 @@ async function uploadChunk( const url = new URL(presignedUrl); const baseUrl = `${url.protocol}//${url.host}`; const queryParams = url.search; - + // Construct chunk upload URL const uploadUrl = `${baseUrl}/media/chunked/upload/${uploadId}${queryParams}`; @@ -107,7 +107,7 @@ async function completeChunkedUpload( const url = new URL(presignedUrl); const baseUrl = `${url.protocol}//${url.host}`; const queryParams = url.search; - + // Construct complete upload URL const completeUrl = `${baseUrl}/media/chunked/complete/${uploadId}${queryParams}`; @@ -134,7 +134,7 @@ async function abortChunkedUpload( const url = new URL(presignedUrl); const baseUrl = `${url.protocol}//${url.host}`; const queryParams = url.search; - + // Construct abort upload URL const abortUrl = `${baseUrl}/media/chunked/abort/${uploadId}${queryParams}`; @@ -169,7 +169,7 @@ export class ChunkedUploader { async upload(): Promise { try { const totalChunks = Math.ceil(this.file.size / this.chunkSize); - + // Initialize chunked upload const initParams: ChunkedUploadInit = { fileName: this.file.name, @@ -181,12 +181,19 @@ export class ChunkedUploader { group: this.options.group, }; - const initResponse = await initializeChunkedUpload(initParams, this.presignedUrl); + const initResponse = await initializeChunkedUpload( + initParams, + this.presignedUrl, + ); this.uploadId = initResponse.uploadId; // Upload chunks let uploadedBytes = 0; - for (let chunkNumber = 0; chunkNumber < totalChunks; chunkNumber++) { + for ( + let chunkNumber = 0; + chunkNumber < totalChunks; + chunkNumber++ + ) { if (this.aborted) { throw new Error("Upload aborted"); } @@ -195,7 +202,12 @@ export class ChunkedUploader { const end = Math.min(start + this.chunkSize, this.file.size); const chunk = this.file.slice(start, end); - await uploadChunk(this.uploadId, chunkNumber, chunk, this.presignedUrl); + await uploadChunk( + this.uploadId, + chunkNumber, + chunk, + this.presignedUrl, + ); uploadedBytes += chunk.size; @@ -204,7 +216,9 @@ export class ChunkedUploader { const progress: ChunkedUploadProgress = { uploadedChunks: chunkNumber + 1, totalChunks, - percentage: Math.round((uploadedBytes / this.file.size) * 100), + percentage: Math.round( + (uploadedBytes / this.file.size) * 100, + ), uploadedBytes, totalBytes: this.file.size, }; @@ -213,17 +227,23 @@ export class ChunkedUploader { } // Complete upload - const media = await completeChunkedUpload(this.uploadId, this.presignedUrl); + const media = await completeChunkedUpload( + this.uploadId, + this.presignedUrl, + ); return media; } catch (error) { if (this.uploadId && !this.aborted) { try { await abortChunkedUpload(this.uploadId, this.presignedUrl); } catch (abortError) { - console.error("Failed to abort chunked upload:", abortError); + console.error( + "Failed to abort chunked upload:", + abortError, + ); } } - + if (this.options.onError) { this.options.onError(error as Error); } @@ -239,11 +259,16 @@ export class ChunkedUploader { } } -export async function uploadFileInChunks(options: ChunkedUploadOptions): Promise { +export async function uploadFileInChunks( + options: ChunkedUploadOptions, +): Promise { const uploader = new ChunkedUploader(options); return uploader.upload(); } -export function shouldUseChunkedUpload(file: File, threshold: number = 10 * 1024 * 1024): boolean { +export function shouldUseChunkedUpload( + file: File, + threshold: number = 10 * 1024 * 1024, +): boolean { return file.size > threshold; // Default 10MB threshold -} \ No newline at end of file +} diff --git a/packages/components-library/src/index.ts b/packages/components-library/src/index.ts index f5ab54437..33e20b94c 100644 --- a/packages/components-library/src/index.ts +++ b/packages/components-library/src/index.ts @@ -38,12 +38,10 @@ import CssIdField from "./css-id-field"; import Select from "./select"; import Tooltip from "./tooltip"; import DragAndDrop from "./drag-and-drop"; -import { - uploadFileInChunks, +import { + uploadFileInChunks, shouldUseChunkedUpload, ChunkedUploader, - type ChunkedUploadOptions, - type ChunkedUploadProgress } from "./chunked-upload"; export { Button as Button2 } from "./components/ui/button"; From 9b0c3bb606d5b673bcb43843046fb32d6221a8c5 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 31 Aug 2025 14:36:46 -0400 Subject: [PATCH 12/17] Properly send the JSON variables --- packages/components-library/src/media-selector/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/components-library/src/media-selector/index.tsx b/packages/components-library/src/media-selector/index.tsx index 3b84eb931..bacc02127 100644 --- a/packages/components-library/src/media-selector/index.tsx +++ b/packages/components-library/src/media-selector/index.tsx @@ -106,7 +106,8 @@ const MediaSelector = (props: MediaSelectorProps) => { const fetch = new FetchBuilder() .setUrl(`${address.backend}/api/media/presigned`) .setIsGraphQLEndpoint(false) - .setPayload({ chunked }) + .setPayload(JSON.stringify({ chunked })) + .setHeaders({ "Content-Type": "application/json" }) .build(); const response = await fetch.exec(); return response.url; From 6eafc502b8e627dbf54e630a171264aef99cd912 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 31 Aug 2025 14:36:58 -0400 Subject: [PATCH 13/17] Properly send the JSON variables --- apps/docs/src/pages/en/website/blocks.md | 14 ++++----- apps/docs/src/pages/en/website/themes.md | 36 ++++++++++++------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/apps/docs/src/pages/en/website/blocks.md b/apps/docs/src/pages/en/website/blocks.md index 39357ab89..24e6b014e 100644 --- a/apps/docs/src/pages/en/website/blocks.md +++ b/apps/docs/src/pages/en/website/blocks.md @@ -35,7 +35,7 @@ You will also see the newly added link on the header itself. 3. Click on the pencil icon against the newly added link to edit it as shown above. 4. Change the label (displayed as text on the header block) and the URL (where the user should be taken upon clicking the label on the header) and click `Done` to save. -![Header edit link](/assets/pages/header-edit-link.png) + ![Header edit link](/assets/pages/header-edit-link.png)
### [Rich Text](#rich-text) @@ -60,7 +60,7 @@ You can also use the floating controls to do the same as shown below. > Double-clicking the text to select won't work due to a bug. We are working on it. 2. Click on the floating `link` button to reveal a popup text input. 3. In the popup text input, enter the URL as shown below. -![Create a hyperlink in rich text block](/assets/pages/rich-text-create-hyperlink.gif) + ![Create a hyperlink in rich text block](/assets/pages/rich-text-create-hyperlink.gif) ### [Hero](#hero) @@ -85,8 +85,8 @@ Following is how it looks on a page. 3. In the button text field, add the text that will be visible on the button. 4. In the button action, enter the URL the user should be taken to upon clicking. -a. If the URL is from your own school, use its relative form, i.e., `/courses`. -b. If the URL is from some external website, use the absolute (complete) URL, i.e., `https://website.com/courses`. + a. If the URL is from your own school, use its relative form, i.e., `/courses`. + b. If the URL is from some external website, use the absolute (complete) URL, i.e., `https://website.com/courses`. ### [Grid](#grid) @@ -130,8 +130,8 @@ A grid block comes in handy when you want to show some sort of list, for example 3. In the button text field, add the text that will be visible on the button. 4. In the button action, enter the URL the user should be taken to upon clicking. -a. If the URL is from your own school, use its relative form, i.e., `/courses`. -b. If the URL is from some external website, use the absolute (complete) URL, i.e., `https://website.com/courses`. + a. If the URL is from your own school, use its relative form, i.e., `/courses`. + b. If the URL is from some external website, use the absolute (complete) URL, i.e., `https://website.com/courses`. ### [Featured](#featured) @@ -268,7 +268,7 @@ In the `Design` panel, you can customize: - Maximum width - Vertical padding - Social media links (Facebook, Twitter, Instagram, LinkedIn, YouTube, Discord, GitHub) - + ## [Shared blocks](#shared-blocks) diff --git a/apps/docs/src/pages/en/website/themes.md b/apps/docs/src/pages/en/website/themes.md index 5c704e4f2..2d7bed189 100644 --- a/apps/docs/src/pages/en/website/themes.md +++ b/apps/docs/src/pages/en/website/themes.md @@ -192,14 +192,14 @@ The typography editor lets you customize text styles across your website. These - Header 3: Smaller titles for subsections - Header 4: Small titles for minor sections - Preheader: Introductory text that appears above headers - +
Subheaders - Subheader 1: Primary subheaders for section introductions - Subheader 2: Secondary subheaders for supporting text -
+
Body Text @@ -207,7 +207,7 @@ The typography editor lets you customize text styles across your website. These - Text 1: Main body text for content - Text 2: Secondary body text for supporting content - Caption: Small text for image captions and footnotes -
+
Interactive Elements @@ -215,7 +215,7 @@ The typography editor lets you customize text styles across your website. These - Link: Text for clickable links - Button: Text for buttons and calls-to-action - Input: Text for form fields and search boxes -
+ For each text style, you can customize: @@ -243,7 +243,7 @@ CourseLit provides a carefully curated selection of professional fonts, organize - **Mulish**: A geometric sans-serif with a modern feel - **Nunito**: A well-balanced font with rounded terminals - **Work Sans**: A clean, modern font with a geometric feel - +
Serif Fonts @@ -253,7 +253,7 @@ CourseLit provides a carefully curated selection of professional fonts, organize - **Playfair Display**: An elegant serif font for headings - **Roboto Slab**: A serif variant of Roboto - **Source Serif 4**: A serif font designed for digital reading -
+
Display Fonts @@ -264,7 +264,7 @@ CourseLit provides a carefully curated selection of professional fonts, organize - **Rubik**: A sans-serif with a geometric feel - **Oswald**: A reworking of the classic style - **Bebas Neue**: A display font with a strong personality -
+
Modern Fonts @@ -272,7 +272,7 @@ CourseLit provides a carefully curated selection of professional fonts, organize - **Lato**: A sans-serif font with a warm feel - **PT Sans**: A font designed for public use - **Quicksand**: A display sans-serif with rounded terminals -
+ Each font is optimized for web use and includes multiple weights for flexibility in design. All fonts support Latin characters and are carefully selected for their readability and professional appearance. @@ -290,7 +290,7 @@ The interactives editor allows you to customize the appearance of interactive el - Shadow effects: From None to 2X Large - Custom styles: Add your own custom styles using [supported Tailwind classes](#supported-tailwind-classes) - Disabled state: How the button looks when it can't be clicked - +
Link @@ -300,7 +300,7 @@ The interactives editor allows you to customize the appearance of interactive el - Text shadow: Add depth to your links - Custom styles: Add your own custom styles using [supported Tailwind classes](#supported-tailwind-classes) - Disabled state: How the link looks when it can't be clicked -
+
Card @@ -309,7 +309,7 @@ The interactives editor allows you to customize the appearance of interactive el - Border style: Choose from various border styles - Shadow effects: Add depth to your cards - Custom styles: Add your own custom styles using [supported Tailwind classes](#supported-tailwind-classes) -
+
Input @@ -320,7 +320,7 @@ The interactives editor allows you to customize the appearance of interactive el - Shadow effects: Add depth to your input fields - Custom styles: Add your own custom styles using [supported Tailwind classes](#supported-tailwind-classes) - Disabled state: How the input looks when it can't be used -
+ ### 4. Structure @@ -332,14 +332,14 @@ The structure editor lets you customize the layout of your pages, like section p Page - Maximum width options: - 2XL (42rem): Compact layout - 3XL (48rem): Standard layout - 4XL (56rem): Wide layout - 5XL (64rem): Extra wide layout - 6XL (72rem): Full width layout - +
Section - Horizontal padding: Space on the left and right sides (None to 9X Large) - Vertical padding: Space on the top and bottom (None to 9X Large) -
+ ## Publishing Changes @@ -387,7 +387,7 @@ When adding custom styles to interactive elements, you can use the following Tai - `text-6xl`: 6X large text - `text-7xl`: 7X large text - `text-8xl`: 8X large text - +
Padding @@ -399,7 +399,7 @@ When adding custom styles to interactive elements, you can use the following Tai #### Horizontal Padding - `px-4` to `px-20`: Horizontal padding from 1rem to 5rem -
+
Colors @@ -454,7 +454,7 @@ Variants available: `hover`, `disabled`, `dark` - `ease-out`: Ease out - `ease-in-out`: Ease in and out - `ease-linear`: Linear -
+
Transforms @@ -481,7 +481,7 @@ Variants available: `hover`, `disabled`, `dark` - `scale-110`: 110% scale - `scale-125`: 125% scale - `scale-150`: 150% scale -
+
Shadows From fb78c832b87f64e84c6f1be1b41123f42b006e7e Mon Sep 17 00:00:00 2001 From: Enea Gjoka Date: Sun, 31 Aug 2025 19:42:59 -0400 Subject: [PATCH 14/17] Autoplay videos and increase video width --- apps/web/components/public/lesson-viewer/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/web/components/public/lesson-viewer/index.tsx b/apps/web/components/public/lesson-viewer/index.tsx index 8a906eb60..0d31a58d9 100644 --- a/apps/web/components/public/lesson-viewer/index.tsx +++ b/apps/web/components/public/lesson-viewer/index.tsx @@ -187,7 +187,7 @@ const LessonViewer = ({ return (
-
+
{!lesson && !error && (
@@ -224,6 +224,7 @@ const LessonViewer = ({
)} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 656361cbc..35d217232 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -617,6 +617,9 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-player: + specifier: ^3.3.2 + version: 3.3.2(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.2.0 version: 2.6.0 @@ -2646,6 +2649,31 @@ packages: '@types/react': optional: true + '@mux/mux-data-google-ima@0.2.8': + resolution: {integrity: sha512-0ZEkHdcZ6bS8QtcjFcoJeZxJTpX7qRIledf4q1trMWPznugvtajCjCM2kieK/pzkZj1JM6liDRFs1PJSfVUs2A==} + + '@mux/mux-player-react@3.5.3': + resolution: {integrity: sha512-f0McZbIXYDkzecFwhhkf0JgEInPnsOClgBqBhkdhRlLRdrAzMATib+D3Di3rPkRHNH7rc/WWORvSxgJz6m6zkA==} + peerDependencies: + '@types/react': ^18.0.0 + '@types/react-dom': '*' + react: ^18.3.1 + react-dom: ^18.3.1 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@mux/mux-player@3.5.3': + resolution: {integrity: sha512-uXKFXbdtioAi+clSVfD60Rw4r7OvA62u2jV6aar9loW9qMsmKv8LU+8uaIaWQjyAORp6E0S37GOVjo72T6O2eQ==} + + '@mux/mux-video@0.26.1': + resolution: {integrity: sha512-gkMdBAgNlB4+krANZHkQFzYWjWeNsJz69y1/hnPtmNQnpvW+O7oc71OffcZrbblyibSxWMQ6MQpYmBVjXlp6sA==} + + '@mux/playback-core@0.30.1': + resolution: {integrity: sha512-rnO1NE9xHDyzbAkmE6ygJYcD7cyyMt7xXqWTykxlceaoSXLjUqgp42HDio7Lcidto4x/O4FIa7ztjV2aCBCXgQ==} + '@napi-rs/wasm-runtime@0.2.9': resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==} @@ -4435,6 +4463,9 @@ packages: '@svgmoji/twemoji@3.2.0': resolution: {integrity: sha512-6xqZgh9viFDKf5wvrxw56ImCR3Ni84IqwK45lxojOe1Gc1Mni1GpPfr4gb7WHDKjumfx+K7BHSvX0KXt3Nr3CQ==} + '@svta/common-media-library@0.12.4': + resolution: {integrity: sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -4970,6 +5001,12 @@ packages: cpu: [x64] os: [win32] + '@vercel/edge@1.2.2': + resolution: {integrity: sha512-1+y+f6rk0Yc9ss9bRDgz/gdpLimwoRteKHhrcgHvEpjbP1nyT3ByqEMWm2BTcpIO5UtDmIFXc8zdq4LR190PDA==} + + '@vimeo/player@2.29.0': + resolution: {integrity: sha512-9JjvjeqUndb9otCCFd0/+2ESsLk7VkDE6sxOBy9iy2ukezuQbplVRi+g9g59yAurKofbmTi/KcKxBGO/22zWRw==} + '@vscode/emmet-helper@2.11.0': resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} @@ -5268,6 +5305,15 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + bcp-47-match@2.0.3: + resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} + + bcp-47-normalize@2.3.0: + resolution: {integrity: sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==} + + bcp-47@2.1.0: + resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} + better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -5401,9 +5447,17 @@ packages: resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} engines: {node: '>=12.13'} + castable-video@1.1.10: + resolution: {integrity: sha512-/T1I0A4VG769wTEZ8gWuy1Crn9saAfRTd1UYTb8xbOPlN78+zOi/1nU2dD5koNkfE5VWvgabkIqrGKmyNXOjSQ==} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + ce-la-react@0.3.1: + resolution: {integrity: sha512-g0YwpZDPIwTwFumGTzNHcgJA6VhFfFCJkSNdUdC04br2UfU+56JDrJrJva3FZ7MToB4NDHAFBiPE/PZdNl1mQA==} + peerDependencies: + react: ^18.3.1 + chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} engines: {node: '>=8'} @@ -5496,6 +5550,9 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cloudflare-video-element@1.3.4: + resolution: {integrity: sha512-F9g+tXzGEXI6v6L48qXxr8vnR8+L6yy7IhpJxK++lpzuVekMHTixxH7/dzLuq6OacVGziU4RB5pzZYJ7/LYtJg==} + clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -5508,6 +5565,9 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + codem-isoboxer@0.3.10: + resolution: {integrity: sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==} + codemirror@6.0.1: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} @@ -5695,6 +5755,9 @@ packages: currency-symbol-map@5.1.0: resolution: {integrity: sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==} + custom-media-element@1.4.5: + resolution: {integrity: sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==} + d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} @@ -5745,6 +5808,12 @@ packages: dash-get@1.0.2: resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} + dash-video-element@0.1.6: + resolution: {integrity: sha512-4gHShaQjcFv6diX5EzB6qAdUGKlIUGGZY8J8yp2pQkWqR0jX4c6plYy0cFraN7mr0DZINe8ujDN1fssDYxJjcg==} + + dashjs@5.0.3: + resolution: {integrity: sha512-TXndNnCUjFjF2nYBxDVba+hWRpVkadkQ8flLp7kHkem+5+wZTfRShJCnVkPUosmjS0YPE9fVNLbYPJxHBeQZvA==} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -6831,6 +6900,12 @@ packages: hastscript@7.2.0: resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} + hls-video-element@1.5.7: + resolution: {integrity: sha512-R+uYimNZQndT2iqBgW7Gm0KiHT6pmlt5tnT63rYIcqOEcKD59M6pmdwqtX2vKPfHo+1ACM14Fy9JF1YMwlrLdQ==} + + hls.js@1.6.11: + resolution: {integrity: sha512-tdDwOAgPGXohSiNE4oxGr3CI9Hx9lsGLFe6TULUvRk2TfHS+w1tSAJntrvxsHaxvjtr6BXsDZM7NOqJFhU4mmg==} + hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -6923,6 +6998,9 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + immediate@3.0.6: + resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -6935,6 +7013,9 @@ packages: import-meta-resolve@2.2.2: resolution: {integrity: sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==} + imsc@1.1.5: + resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -7528,6 +7609,9 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + lie@3.1.1: + resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} + lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -7561,6 +7645,9 @@ packages: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} + localforage@1.10.0: + resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} + locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -7774,6 +7861,12 @@ packages: mdast-util-to-string@3.2.0: resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} + media-chrome@4.11.1: + resolution: {integrity: sha512-+2niDc4qOwlpFAjwxg1OaizK/zKV6y7QqGm4nBFEVlSaG0ZBgOmfc4IXAPiirZqAlZGaFFUaMqCl1SpGU0/naA==} + + media-tracks@0.3.3: + resolution: {integrity: sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -8050,6 +8143,13 @@ packages: muri@1.3.0: resolution: {integrity: sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==} + deprecated: This package is no longer supported. Please use https://www.npmjs.com/package/mongodb-connection-string-url + + mux-embed@5.12.0: + resolution: {integrity: sha512-Qj1MAqgAP4eNI5CmlvGCd0d0ZfcLU2wb4QzN5QqIa+IIa8NInQBZAiAfdsGWklY9mz+xYGkduy31BLafpuzlLw==} + + mux-embed@5.9.0: + resolution: {integrity: sha512-wmunL3uoPhma/tWy8PrDPZkvJpXvSFBwbD3KkC4PG8Ztjfb1X3hRJwGUAQyRz7z99b/ovLm2UTTitrkvStjH4w==} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -8073,6 +8173,9 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true + native-promise-only@0.8.1: + resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} + natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -8449,6 +8552,9 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} + player.style@0.1.10: + resolution: {integrity: sha512-Jxv7tlaQ3SFCddsN35jzoGnCHB3/xMTbJOgn4zcsmF0lcZvRPq5UkRRAD5tZm8CvzKndUvtoDlG6GSPL/N/SrA==} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -8781,6 +8887,13 @@ packages: react-is@19.1.0: resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} + react-player@3.3.2: + resolution: {integrity: sha512-MBSCxTA1FPyMR19Wy+2LtVjguhrLl9p2l5nODY4fbumgsoaCEuhMLpZvxh8RWjzzvqL8V3jYcPfw/XhqrbTzFw==} + peerDependencies: + '@types/react': ^18.0.0 + react: ^18.3.1 + react-dom: ^18.3.1 + react-promise-suspense@0.3.4: resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} @@ -9125,6 +9238,9 @@ packages: sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} + sax@1.2.1: + resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} + saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -9285,6 +9401,7 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} + deprecated: The work that was done in this beta branch won't be included in future versions space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} @@ -9302,6 +9419,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + spotify-audio-element@1.0.3: + resolution: {integrity: sha512-I1/qD8cg/UnTlCIMiKSdZUJTyYfYhaqFK7LIVElc48eOqUUbVCaw1bqL8I6mJzdMJTh3eoNyF/ewvB7NoS/g9A==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -9455,6 +9575,9 @@ packages: suf-log@2.5.3: resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} + super-media-element@1.4.2: + resolution: {integrity: sha512-9pP/CVNp4NF2MNlRzLwQkjiTgKKe9WYXrLh9+8QokWmMxz+zt2mf1utkWLco26IuA3AfVcTb//qtlTIjY3VHxA==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -9539,6 +9662,9 @@ packages: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} + tiktok-video-element@0.1.1: + resolution: {integrity: sha512-BaiVzvNz2UXDKTdSrXzrNf4q6Ecc+/utYUh7zdEu2jzYcJVDoqYbVfUl0bCfMoOeeAqg28vD/yN63Y3E9jOrlA==} + tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -9698,6 +9824,9 @@ packages: tw-animate-css@1.2.8: resolution: {integrity: sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g==} + twitch-video-element@0.1.4: + resolution: {integrity: sha512-SDpZ4f7sZmwHF6XG5PF0KWuP18pH/kNG04MhTcpqJby7Lk/D3TS/lCYd+RSg0rIAAVi1LDgSIo1yJs9kmHlhgw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -9765,6 +9894,10 @@ packages: engines: {node: '>=4.2.0'} hasBin: true + ua-parser-js@1.0.41: + resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -9935,6 +10068,9 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vimeo-video-element@1.5.5: + resolution: {integrity: sha512-9QVvKPPnubMNeNYHY5KZqAYerVMuVG+7PSK+6IrEUD7a/wnCGtzb8Sfxl9qNxDAL6Q8i+p+5SDoVKobCd866vw==} + vite@3.2.11: resolution: {integrity: sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -10031,6 +10167,10 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + weakmap-polyfill@2.0.4: + resolution: {integrity: sha512-ZzxBf288iALJseijWelmECm/1x7ZwQn3sMYIkDr2VvZp7r6SEKuT8D0O9Wiq6L9Nl5mazrOMcmiZE/2NCenaxw==} + engines: {node: '>=8.10.0'} + web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -10105,6 +10245,9 @@ packages: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} + wistia-video-element@1.3.4: + resolution: {integrity: sha512-2l22oaQe4jUfi3yvsh2m2oCEgvbqTzaSYx6aJnZAvV5hlMUJlyZheFUnaj0JU2wGlHdVGV7xNY+5KpKu+ruLYA==} + with@7.0.2: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} @@ -10195,6 +10338,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + youtube-video-element@1.6.2: + resolution: {integrity: sha512-YHDIOAqgRpfl1Ois9HcB8UFtWOxK8KJrV5TXpImj4BKYP1rWT04f/fMM9tQ9SYZlBKukT7NR+9wcI3UpB5BMDQ==} + zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} @@ -12446,6 +12592,43 @@ snapshots: optionalDependencies: '@types/react': 18.3.20 + '@mux/mux-data-google-ima@0.2.8': + dependencies: + mux-embed: 5.9.0 + + '@mux/mux-player-react@3.5.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@mux/mux-player': 3.5.3(react@18.3.1) + '@mux/playback-core': 0.30.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.20 + '@types/react-dom': 18.3.6(@types/react@18.3.20) + + '@mux/mux-player@3.5.3(react@18.3.1)': + dependencies: + '@mux/mux-video': 0.26.1 + '@mux/playback-core': 0.30.1 + media-chrome: 4.11.1(react@18.3.1) + player.style: 0.1.10(react@18.3.1) + transitivePeerDependencies: + - react + + '@mux/mux-video@0.26.1': + dependencies: + '@mux/mux-data-google-ima': 0.2.8 + '@mux/playback-core': 0.30.1 + castable-video: 1.1.10 + custom-media-element: 1.4.5 + media-tracks: 0.3.3 + + '@mux/playback-core@0.30.1': + dependencies: + hls.js: 1.6.11 + mux-embed: 5.12.0 + '@napi-rs/wasm-runtime@0.2.9': dependencies: '@emnapi/core': 1.4.3 @@ -15717,6 +15900,8 @@ snapshots: '@babel/runtime': 7.27.0 '@svgmoji/core': 3.2.0 + '@svta/common-media-library@0.12.4': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.5': @@ -16371,6 +16556,13 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.7.0': optional: true + '@vercel/edge@1.2.2': {} + + '@vimeo/player@2.29.0': + dependencies: + native-promise-only: 0.8.1 + weakmap-polyfill: 2.0.4 + '@vscode/emmet-helper@2.11.0': dependencies: emmet: 2.4.11 @@ -16823,6 +17015,19 @@ snapshots: base64-js@1.5.1: {} + bcp-47-match@2.0.3: {} + + bcp-47-normalize@2.3.0: + dependencies: + bcp-47: 2.1.0 + bcp-47-match: 2.0.3 + + bcp-47@2.1.0: + dependencies: + is-alphabetical: 2.0.1 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -16987,8 +17192,16 @@ snapshots: case-anything@2.1.13: {} + castable-video@1.1.10: + dependencies: + custom-media-element: 1.4.5 + ccount@2.0.1: {} + ce-la-react@0.3.1(react@18.3.1): + dependencies: + react: 18.3.1 + chalk@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -17074,12 +17287,16 @@ snapshots: clone@1.0.4: {} + cloudflare-video-element@1.3.4: {} + clsx@2.1.1: {} cluster-key-slot@1.1.2: {} co@4.6.0: {} + codem-isoboxer@0.3.10: {} + codemirror@6.0.1: dependencies: '@codemirror/autocomplete': 6.18.6 @@ -17252,6 +17469,8 @@ snapshots: currency-symbol-map@5.1.0: {} + custom-media-element@1.4.5: {} + d3-array@3.2.4: dependencies: internmap: 2.0.3 @@ -17294,6 +17513,24 @@ snapshots: dash-get@1.0.2: {} + dash-video-element@0.1.6: + dependencies: + custom-media-element: 1.4.5 + dashjs: 5.0.3 + + dashjs@5.0.3: + dependencies: + '@svta/common-media-library': 0.12.4 + bcp-47-match: 2.0.3 + bcp-47-normalize: 2.3.0 + codem-isoboxer: 0.3.10 + fast-deep-equal: 3.1.3 + html-entities: 2.6.0 + imsc: 1.1.5 + localforage: 1.10.0 + path-browserify: 1.0.1 + ua-parser-js: 1.0.41 + data-uri-to-buffer@4.0.1: {} data-urls@3.0.2: @@ -18671,6 +18908,14 @@ snapshots: property-information: 6.5.0 space-separated-tokens: 2.0.2 + hls-video-element@1.5.7: + dependencies: + custom-media-element: 1.4.5 + hls.js: 1.6.11 + media-tracks: 0.3.3 + + hls.js@1.6.11: {} + hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -18771,6 +19016,8 @@ snapshots: ignore@5.3.2: {} + immediate@3.0.6: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -18783,6 +19030,10 @@ snapshots: import-meta-resolve@2.2.2: {} + imsc@1.1.5: + dependencies: + sax: 1.2.1 + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -19597,6 +19848,10 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 + lie@3.1.1: + dependencies: + immediate: 3.0.6 + lilconfig@2.1.0: {} lilconfig@3.1.3: {} @@ -19640,6 +19895,10 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 + localforage@1.10.0: + dependencies: + lie: 3.1.1 + locate-path@3.0.0: dependencies: p-locate: 3.0.0 @@ -19904,6 +20163,15 @@ snapshots: dependencies: '@types/mdast': 3.0.15 + media-chrome@4.11.1(react@18.3.1): + dependencies: + '@vercel/edge': 1.2.2 + ce-la-react: 0.3.1(react@18.3.1) + transitivePeerDependencies: + - react + + media-tracks@0.3.3: {} + media-typer@0.3.0: {} memory-pager@1.5.0: {} @@ -20332,6 +20600,10 @@ snapshots: muri@1.3.0: {} + mux-embed@5.12.0: {} + + mux-embed@5.9.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -20346,6 +20618,8 @@ snapshots: napi-postinstall@0.1.6: {} + native-promise-only@0.8.1: {} + natural-compare-lite@1.4.0: {} natural-compare@1.4.0: {} @@ -20721,6 +20995,12 @@ snapshots: dependencies: find-up: 3.0.0 + player.style@0.1.10(react@18.3.1): + dependencies: + media-chrome: 4.11.1(react@18.3.1) + transitivePeerDependencies: + - react + possible-typed-array-names@1.1.0: {} postcss-import@15.1.0(postcss@8.5.3): @@ -21151,6 +21431,24 @@ snapshots: react-is@19.1.0: {} + react-player@3.3.2(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@mux/mux-player-react': 3.5.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@types/react': 18.3.20 + cloudflare-video-element: 1.3.4 + dash-video-element: 0.1.6 + hls-video-element: 1.5.7 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + spotify-audio-element: 1.0.3 + tiktok-video-element: 0.1.1 + twitch-video-element: 0.1.4 + vimeo-video-element: 1.5.5 + wistia-video-element: 1.3.4 + youtube-video-element: 1.6.2 + transitivePeerDependencies: + - '@types/react-dom' + react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 @@ -21726,6 +22024,8 @@ snapshots: dependencies: suf-log: 2.5.3 + sax@1.2.1: {} + saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -21949,6 +22249,8 @@ snapshots: split2@4.2.0: {} + spotify-audio-element@1.0.3: {} + sprintf-js@1.0.3: {} sprintf-js@1.1.3: {} @@ -22122,6 +22424,8 @@ snapshots: dependencies: s.color: 0.0.15 + super-media-element@1.4.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -22268,6 +22572,8 @@ snapshots: throttle-debounce@3.0.1: {} + tiktok-video-element@0.1.1: {} + tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -22475,6 +22781,8 @@ snapshots: tw-animate-css@1.2.8: {} + twitch-video-element@0.1.4: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -22544,6 +22852,8 @@ snapshots: typescript@4.9.5: {} + ua-parser-js@1.0.41: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -22765,6 +23075,10 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vimeo-video-element@1.5.5: + dependencies: + '@vimeo/player': 2.29.0 + vite@3.2.11(@types/node@18.19.87): dependencies: esbuild: 0.15.18 @@ -22843,6 +23157,8 @@ snapshots: dependencies: defaults: 1.0.4 + weakmap-polyfill@2.0.4: {} + web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} @@ -22935,6 +23251,10 @@ snapshots: dependencies: string-width: 5.1.2 + wistia-video-element@1.3.4: + dependencies: + super-media-element: 1.4.2 + with@7.0.2: dependencies: '@babel/parser': 7.27.0 @@ -23008,6 +23328,8 @@ snapshots: yocto-queue@0.1.0: {} + youtube-video-element@1.6.2: {} + zip-stream@4.1.1: dependencies: archiver-utils: 3.0.4 From 5ce87244bb40caaa3c3c74fdd34173754d822fdb Mon Sep 17 00:00:00 2001 From: Enea Date: Mon, 8 Sep 2025 20:30:46 -0400 Subject: [PATCH 16/17] Revert "ReactPlayer" This reverts commit 2dbee4101eb7326d548a2ae151bb0be7097a0ccb. --- .../content/section/[section]/lesson/page.tsx | 13 +- .../public/lesson-viewer/embed-viewer.tsx | 15 +- .../components/public/lesson-viewer/index.tsx | 21 +- packages/components-library/package.json | 1 - .../src/video-with-preview.tsx | 83 ++++- pnpm-lock.yaml | 322 ------------------ 6 files changed, 91 insertions(+), 364 deletions(-) diff --git a/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/page.tsx b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/page.tsx index 409d30d24..80db5a594 100644 --- a/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/page.tsx +++ b/apps/web/app/(with-contexts)/dashboard/(sidebar)/product/[id]/content/section/[section]/lesson/page.tsx @@ -1,7 +1,6 @@ "use client"; import { useState, useCallback, useEffect, useContext } from "react"; -import ReactPlayer from "react-player"; import { useRouter, useParams, useSearchParams } from "next/navigation"; import Link from "next/link"; import { @@ -290,12 +289,12 @@ export default function LessonPage() { UIConstants.YOUTUBE_REGEX, ) && (
-
)} diff --git a/apps/web/components/public/lesson-viewer/embed-viewer.tsx b/apps/web/components/public/lesson-viewer/embed-viewer.tsx index 2af702fa2..5a9ccc5fc 100644 --- a/apps/web/components/public/lesson-viewer/embed-viewer.tsx +++ b/apps/web/components/public/lesson-viewer/embed-viewer.tsx @@ -1,16 +1,17 @@ import React from "react"; -import ReactPlayer from "react-player"; import { UIConstants } from "@courselit/common-models"; const YouTubeEmbed = ({ content }: { content: string }) => { + const match = content.match(UIConstants.YOUTUBE_REGEX); + return (
-
); diff --git a/apps/web/components/public/lesson-viewer/index.tsx b/apps/web/components/public/lesson-viewer/index.tsx index e003f79de..0d31a58d9 100644 --- a/apps/web/components/public/lesson-viewer/index.tsx +++ b/apps/web/components/public/lesson-viewer/index.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import ReactPlayer from "react-player"; import { FetchBuilder } from "@courselit/utils"; import { LESSON_TYPE_VIDEO, @@ -223,20 +222,22 @@ const LessonViewer = ({ LESSON_TYPE_VIDEO, ) === lesson.type && (
-
- + -
+ Your browser does not support the video tag. + { + const videoId = getVideoId(); + // Add autoplay parameter for in-place videos + const shouldAutoplay = isPlaying; + + if (videoType === "youtube") { + if (videoId) { + return `https://www.youtube.com/embed/${videoId}${shouldAutoplay ? "?autoplay=1" : ""}`; + } + + // If we couldn't extract the ID but it's a YouTube URL, try to use it directly + const hasParams = videoUrl.includes("?"); + return ( + videoUrl + + (shouldAutoplay && !hasParams + ? "?autoplay=1" + : shouldAutoplay && hasParams + ? "&autoplay=1" + : "") + ); + } + + if (videoType === "vimeo" && videoId) { + return `https://player.vimeo.com/video/${videoId}${shouldAutoplay ? "?autoplay=1" : ""}`; + } + + // For self-hosted videos, return the URL as is + return videoUrl; + }; // Calculate aspect ratio style const aspectRatioStyle = () => { @@ -197,14 +226,23 @@ export function VideoWithPreview({ {!modal && isPlaying ? ( // In-place video player
- + {videoType !== "self-hosted" ? ( + + ) : ( + + )}
) : ( // Thumbnail with play button @@ -250,14 +288,25 @@ export function VideoWithPreview({ className="w-full max-w-xl sm:max-w-2xl md:max-w-3xl" style={aspectRatioStyle()} > - + {videoType !== "self-hosted" ? ( + // YouTube or Vimeo video + + ) : ( + // Self-hosted video + + )}
)} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 35d217232..656361cbc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -617,9 +617,6 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) - react-player: - specifier: ^3.3.2 - version: 3.3.2(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) tailwind-merge: specifier: ^2.2.0 version: 2.6.0 @@ -2649,31 +2646,6 @@ packages: '@types/react': optional: true - '@mux/mux-data-google-ima@0.2.8': - resolution: {integrity: sha512-0ZEkHdcZ6bS8QtcjFcoJeZxJTpX7qRIledf4q1trMWPznugvtajCjCM2kieK/pzkZj1JM6liDRFs1PJSfVUs2A==} - - '@mux/mux-player-react@3.5.3': - resolution: {integrity: sha512-f0McZbIXYDkzecFwhhkf0JgEInPnsOClgBqBhkdhRlLRdrAzMATib+D3Di3rPkRHNH7rc/WWORvSxgJz6m6zkA==} - peerDependencies: - '@types/react': ^18.0.0 - '@types/react-dom': '*' - react: ^18.3.1 - react-dom: ^18.3.1 - peerDependenciesMeta: - '@types/react': - optional: true - '@types/react-dom': - optional: true - - '@mux/mux-player@3.5.3': - resolution: {integrity: sha512-uXKFXbdtioAi+clSVfD60Rw4r7OvA62u2jV6aar9loW9qMsmKv8LU+8uaIaWQjyAORp6E0S37GOVjo72T6O2eQ==} - - '@mux/mux-video@0.26.1': - resolution: {integrity: sha512-gkMdBAgNlB4+krANZHkQFzYWjWeNsJz69y1/hnPtmNQnpvW+O7oc71OffcZrbblyibSxWMQ6MQpYmBVjXlp6sA==} - - '@mux/playback-core@0.30.1': - resolution: {integrity: sha512-rnO1NE9xHDyzbAkmE6ygJYcD7cyyMt7xXqWTykxlceaoSXLjUqgp42HDio7Lcidto4x/O4FIa7ztjV2aCBCXgQ==} - '@napi-rs/wasm-runtime@0.2.9': resolution: {integrity: sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg==} @@ -4463,9 +4435,6 @@ packages: '@svgmoji/twemoji@3.2.0': resolution: {integrity: sha512-6xqZgh9viFDKf5wvrxw56ImCR3Ni84IqwK45lxojOe1Gc1Mni1GpPfr4gb7WHDKjumfx+K7BHSvX0KXt3Nr3CQ==} - '@svta/common-media-library@0.12.4': - resolution: {integrity: sha512-9EuOoaNmz7JrfGwjsrD9SxF9otU5TNMnbLu1yU4BeLK0W5cDxVXXR58Z89q9u2AnHjIctscjMTYdlqQ1gojTuw==} - '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -5001,12 +4970,6 @@ packages: cpu: [x64] os: [win32] - '@vercel/edge@1.2.2': - resolution: {integrity: sha512-1+y+f6rk0Yc9ss9bRDgz/gdpLimwoRteKHhrcgHvEpjbP1nyT3ByqEMWm2BTcpIO5UtDmIFXc8zdq4LR190PDA==} - - '@vimeo/player@2.29.0': - resolution: {integrity: sha512-9JjvjeqUndb9otCCFd0/+2ESsLk7VkDE6sxOBy9iy2ukezuQbplVRi+g9g59yAurKofbmTi/KcKxBGO/22zWRw==} - '@vscode/emmet-helper@2.11.0': resolution: {integrity: sha512-QLxjQR3imPZPQltfbWRnHU6JecWTF1QSWhx3GAKQpslx7y3Dp6sIIXhKjiUJ/BR9FX8PVthjr9PD6pNwOJfAzw==} @@ -5305,15 +5268,6 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - bcp-47-match@2.0.3: - resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} - - bcp-47-normalize@2.3.0: - resolution: {integrity: sha512-8I/wfzqQvttUFz7HVJgIZ7+dj3vUaIyIxYXaTRP1YWoSDfzt6TUmxaKZeuXR62qBmYr+nvuWINFRl6pZ5DlN4Q==} - - bcp-47@2.1.0: - resolution: {integrity: sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==} - better-path-resolve@1.0.0: resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} engines: {node: '>=4'} @@ -5447,17 +5401,9 @@ packages: resolution: {integrity: sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng==} engines: {node: '>=12.13'} - castable-video@1.1.10: - resolution: {integrity: sha512-/T1I0A4VG769wTEZ8gWuy1Crn9saAfRTd1UYTb8xbOPlN78+zOi/1nU2dD5koNkfE5VWvgabkIqrGKmyNXOjSQ==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - ce-la-react@0.3.1: - resolution: {integrity: sha512-g0YwpZDPIwTwFumGTzNHcgJA6VhFfFCJkSNdUdC04br2UfU+56JDrJrJva3FZ7MToB4NDHAFBiPE/PZdNl1mQA==} - peerDependencies: - react: ^18.3.1 - chalk@3.0.0: resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} engines: {node: '>=8'} @@ -5550,9 +5496,6 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} - cloudflare-video-element@1.3.4: - resolution: {integrity: sha512-F9g+tXzGEXI6v6L48qXxr8vnR8+L6yy7IhpJxK++lpzuVekMHTixxH7/dzLuq6OacVGziU4RB5pzZYJ7/LYtJg==} - clsx@2.1.1: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} @@ -5565,9 +5508,6 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} - codem-isoboxer@0.3.10: - resolution: {integrity: sha512-eNk3TRV+xQMJ1PEj0FQGY8KD4m0GPxT487XJ+Iftm7mVa9WpPFDMWqPt+46buiP5j5Wzqe5oMIhqBcAeKfygSA==} - codemirror@6.0.1: resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==} @@ -5755,9 +5695,6 @@ packages: currency-symbol-map@5.1.0: resolution: {integrity: sha512-LO/lzYRw134LMDVnLyAf1dHE5tyO6axEFkR3TXjQIOmMkAM9YL6QsiUwuXzZAmFnuDJcs4hayOgyIYtViXFrLw==} - custom-media-element@1.4.5: - resolution: {integrity: sha512-cjrsQufETwxjvwZbYbKBCJNvmQ2++G9AvT45zDi7NXL9k2PdVcs2h0jQz96J6G4TMKRCcEsoJ+QTgQD00Igtjw==} - d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} @@ -5808,12 +5745,6 @@ packages: dash-get@1.0.2: resolution: {integrity: sha512-4FbVrHDwfOASx7uQVxeiCTo7ggSdYZbqs8lH+WU6ViypPlDbe9y6IP5VVUDQBv9DcnyaiPT5XT0UWHgJ64zLeQ==} - dash-video-element@0.1.6: - resolution: {integrity: sha512-4gHShaQjcFv6diX5EzB6qAdUGKlIUGGZY8J8yp2pQkWqR0jX4c6plYy0cFraN7mr0DZINe8ujDN1fssDYxJjcg==} - - dashjs@5.0.3: - resolution: {integrity: sha512-TXndNnCUjFjF2nYBxDVba+hWRpVkadkQ8flLp7kHkem+5+wZTfRShJCnVkPUosmjS0YPE9fVNLbYPJxHBeQZvA==} - data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -6900,12 +6831,6 @@ packages: hastscript@7.2.0: resolution: {integrity: sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw==} - hls-video-element@1.5.7: - resolution: {integrity: sha512-R+uYimNZQndT2iqBgW7Gm0KiHT6pmlt5tnT63rYIcqOEcKD59M6pmdwqtX2vKPfHo+1ACM14Fy9JF1YMwlrLdQ==} - - hls.js@1.6.11: - resolution: {integrity: sha512-tdDwOAgPGXohSiNE4oxGr3CI9Hx9lsGLFe6TULUvRk2TfHS+w1tSAJntrvxsHaxvjtr6BXsDZM7NOqJFhU4mmg==} - hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} @@ -6998,9 +6923,6 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - immediate@3.0.6: - resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==} - import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -7013,9 +6935,6 @@ packages: import-meta-resolve@2.2.2: resolution: {integrity: sha512-f8KcQ1D80V7RnqVm+/lirO9zkOxjGxhaTC1IPrBGd3MEfNgmNG67tSUO9gTi2F3Blr2Az6g1vocaxzkVnWl9MA==} - imsc@1.1.5: - resolution: {integrity: sha512-V8je+CGkcvGhgl2C1GlhqFFiUOIEdwXbXLiu1Fcubvvbo+g9inauqT3l0pNYXGoLPBj3jxtZz9t+wCopMkwadQ==} - imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -7609,9 +7528,6 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lie@3.1.1: - resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==} - lilconfig@2.1.0: resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} engines: {node: '>=10'} @@ -7645,9 +7561,6 @@ packages: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} - localforage@1.10.0: - resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==} - locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -7861,12 +7774,6 @@ packages: mdast-util-to-string@3.2.0: resolution: {integrity: sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==} - media-chrome@4.11.1: - resolution: {integrity: sha512-+2niDc4qOwlpFAjwxg1OaizK/zKV6y7QqGm4nBFEVlSaG0ZBgOmfc4IXAPiirZqAlZGaFFUaMqCl1SpGU0/naA==} - - media-tracks@0.3.3: - resolution: {integrity: sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w==} - media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -8143,13 +8050,6 @@ packages: muri@1.3.0: resolution: {integrity: sha512-FiaFwKl864onHFFUV/a2szAl7X0fxVlSKNdhTf+BM8i8goEgYut8u5P9MqQqIYwvaMxjzVESsoEm/2kfkFH1rg==} - deprecated: This package is no longer supported. Please use https://www.npmjs.com/package/mongodb-connection-string-url - - mux-embed@5.12.0: - resolution: {integrity: sha512-Qj1MAqgAP4eNI5CmlvGCd0d0ZfcLU2wb4QzN5QqIa+IIa8NInQBZAiAfdsGWklY9mz+xYGkduy31BLafpuzlLw==} - - mux-embed@5.9.0: - resolution: {integrity: sha512-wmunL3uoPhma/tWy8PrDPZkvJpXvSFBwbD3KkC4PG8Ztjfb1X3hRJwGUAQyRz7z99b/ovLm2UTTitrkvStjH4w==} mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -8173,9 +8073,6 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true - native-promise-only@0.8.1: - resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} - natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -8552,9 +8449,6 @@ packages: resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} engines: {node: '>=8'} - player.style@0.1.10: - resolution: {integrity: sha512-Jxv7tlaQ3SFCddsN35jzoGnCHB3/xMTbJOgn4zcsmF0lcZvRPq5UkRRAD5tZm8CvzKndUvtoDlG6GSPL/N/SrA==} - possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -8887,13 +8781,6 @@ packages: react-is@19.1.0: resolution: {integrity: sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==} - react-player@3.3.2: - resolution: {integrity: sha512-MBSCxTA1FPyMR19Wy+2LtVjguhrLl9p2l5nODY4fbumgsoaCEuhMLpZvxh8RWjzzvqL8V3jYcPfw/XhqrbTzFw==} - peerDependencies: - '@types/react': ^18.0.0 - react: ^18.3.1 - react-dom: ^18.3.1 - react-promise-suspense@0.3.4: resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} @@ -9238,9 +9125,6 @@ packages: sass-formatter@0.7.9: resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} - sax@1.2.1: - resolution: {integrity: sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==} - saxes@6.0.0: resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} engines: {node: '>=v12.22.7'} @@ -9401,7 +9285,6 @@ packages: source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} - deprecated: The work that was done in this beta branch won't be included in future versions space-separated-tokens@1.1.5: resolution: {integrity: sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==} @@ -9419,9 +9302,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - spotify-audio-element@1.0.3: - resolution: {integrity: sha512-I1/qD8cg/UnTlCIMiKSdZUJTyYfYhaqFK7LIVElc48eOqUUbVCaw1bqL8I6mJzdMJTh3eoNyF/ewvB7NoS/g9A==} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -9575,9 +9455,6 @@ packages: suf-log@2.5.3: resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} - super-media-element@1.4.2: - resolution: {integrity: sha512-9pP/CVNp4NF2MNlRzLwQkjiTgKKe9WYXrLh9+8QokWmMxz+zt2mf1utkWLco26IuA3AfVcTb//qtlTIjY3VHxA==} - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -9662,9 +9539,6 @@ packages: resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} engines: {node: '>=10'} - tiktok-video-element@0.1.1: - resolution: {integrity: sha512-BaiVzvNz2UXDKTdSrXzrNf4q6Ecc+/utYUh7zdEu2jzYcJVDoqYbVfUl0bCfMoOeeAqg28vD/yN63Y3E9jOrlA==} - tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} @@ -9824,9 +9698,6 @@ packages: tw-animate-css@1.2.8: resolution: {integrity: sha512-AxSnYRvyFnAiZCUndS3zQZhNfV/B77ZhJ+O7d3K6wfg/jKJY+yv6ahuyXwnyaYA9UdLqnpCwhTRv9pPTBnPR2g==} - twitch-video-element@0.1.4: - resolution: {integrity: sha512-SDpZ4f7sZmwHF6XG5PF0KWuP18pH/kNG04MhTcpqJby7Lk/D3TS/lCYd+RSg0rIAAVi1LDgSIo1yJs9kmHlhgw==} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -9894,10 +9765,6 @@ packages: engines: {node: '>=4.2.0'} hasBin: true - ua-parser-js@1.0.41: - resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} - hasBin: true - unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -10068,9 +9935,6 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} - vimeo-video-element@1.5.5: - resolution: {integrity: sha512-9QVvKPPnubMNeNYHY5KZqAYerVMuVG+7PSK+6IrEUD7a/wnCGtzb8Sfxl9qNxDAL6Q8i+p+5SDoVKobCd866vw==} - vite@3.2.11: resolution: {integrity: sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -10167,10 +10031,6 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} - weakmap-polyfill@2.0.4: - resolution: {integrity: sha512-ZzxBf288iALJseijWelmECm/1x7ZwQn3sMYIkDr2VvZp7r6SEKuT8D0O9Wiq6L9Nl5mazrOMcmiZE/2NCenaxw==} - engines: {node: '>=8.10.0'} - web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} @@ -10245,9 +10105,6 @@ packages: resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==} engines: {node: '>=12'} - wistia-video-element@1.3.4: - resolution: {integrity: sha512-2l22oaQe4jUfi3yvsh2m2oCEgvbqTzaSYx6aJnZAvV5hlMUJlyZheFUnaj0JU2wGlHdVGV7xNY+5KpKu+ruLYA==} - with@7.0.2: resolution: {integrity: sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==} engines: {node: '>= 10.0.0'} @@ -10338,9 +10195,6 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - youtube-video-element@1.6.2: - resolution: {integrity: sha512-YHDIOAqgRpfl1Ois9HcB8UFtWOxK8KJrV5TXpImj4BKYP1rWT04f/fMM9tQ9SYZlBKukT7NR+9wcI3UpB5BMDQ==} - zip-stream@4.1.1: resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} engines: {node: '>= 10'} @@ -12592,43 +12446,6 @@ snapshots: optionalDependencies: '@types/react': 18.3.20 - '@mux/mux-data-google-ima@0.2.8': - dependencies: - mux-embed: 5.9.0 - - '@mux/mux-player-react@3.5.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': - dependencies: - '@mux/mux-player': 3.5.3(react@18.3.1) - '@mux/playback-core': 0.30.1 - prop-types: 15.8.1 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - optionalDependencies: - '@types/react': 18.3.20 - '@types/react-dom': 18.3.6(@types/react@18.3.20) - - '@mux/mux-player@3.5.3(react@18.3.1)': - dependencies: - '@mux/mux-video': 0.26.1 - '@mux/playback-core': 0.30.1 - media-chrome: 4.11.1(react@18.3.1) - player.style: 0.1.10(react@18.3.1) - transitivePeerDependencies: - - react - - '@mux/mux-video@0.26.1': - dependencies: - '@mux/mux-data-google-ima': 0.2.8 - '@mux/playback-core': 0.30.1 - castable-video: 1.1.10 - custom-media-element: 1.4.5 - media-tracks: 0.3.3 - - '@mux/playback-core@0.30.1': - dependencies: - hls.js: 1.6.11 - mux-embed: 5.12.0 - '@napi-rs/wasm-runtime@0.2.9': dependencies: '@emnapi/core': 1.4.3 @@ -15900,8 +15717,6 @@ snapshots: '@babel/runtime': 7.27.0 '@svgmoji/core': 3.2.0 - '@svta/common-media-library@0.12.4': {} - '@swc/counter@0.1.3': {} '@swc/helpers@0.5.5': @@ -16556,13 +16371,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.7.0': optional: true - '@vercel/edge@1.2.2': {} - - '@vimeo/player@2.29.0': - dependencies: - native-promise-only: 0.8.1 - weakmap-polyfill: 2.0.4 - '@vscode/emmet-helper@2.11.0': dependencies: emmet: 2.4.11 @@ -17015,19 +16823,6 @@ snapshots: base64-js@1.5.1: {} - bcp-47-match@2.0.3: {} - - bcp-47-normalize@2.3.0: - dependencies: - bcp-47: 2.1.0 - bcp-47-match: 2.0.3 - - bcp-47@2.1.0: - dependencies: - is-alphabetical: 2.0.1 - is-alphanumerical: 2.0.1 - is-decimal: 2.0.1 - better-path-resolve@1.0.0: dependencies: is-windows: 1.0.2 @@ -17192,16 +16987,8 @@ snapshots: case-anything@2.1.13: {} - castable-video@1.1.10: - dependencies: - custom-media-element: 1.4.5 - ccount@2.0.1: {} - ce-la-react@0.3.1(react@18.3.1): - dependencies: - react: 18.3.1 - chalk@3.0.0: dependencies: ansi-styles: 4.3.0 @@ -17287,16 +17074,12 @@ snapshots: clone@1.0.4: {} - cloudflare-video-element@1.3.4: {} - clsx@2.1.1: {} cluster-key-slot@1.1.2: {} co@4.6.0: {} - codem-isoboxer@0.3.10: {} - codemirror@6.0.1: dependencies: '@codemirror/autocomplete': 6.18.6 @@ -17469,8 +17252,6 @@ snapshots: currency-symbol-map@5.1.0: {} - custom-media-element@1.4.5: {} - d3-array@3.2.4: dependencies: internmap: 2.0.3 @@ -17513,24 +17294,6 @@ snapshots: dash-get@1.0.2: {} - dash-video-element@0.1.6: - dependencies: - custom-media-element: 1.4.5 - dashjs: 5.0.3 - - dashjs@5.0.3: - dependencies: - '@svta/common-media-library': 0.12.4 - bcp-47-match: 2.0.3 - bcp-47-normalize: 2.3.0 - codem-isoboxer: 0.3.10 - fast-deep-equal: 3.1.3 - html-entities: 2.6.0 - imsc: 1.1.5 - localforage: 1.10.0 - path-browserify: 1.0.1 - ua-parser-js: 1.0.41 - data-uri-to-buffer@4.0.1: {} data-urls@3.0.2: @@ -18908,14 +18671,6 @@ snapshots: property-information: 6.5.0 space-separated-tokens: 2.0.2 - hls-video-element@1.5.7: - dependencies: - custom-media-element: 1.4.5 - hls.js: 1.6.11 - media-tracks: 0.3.3 - - hls.js@1.6.11: {} - hoist-non-react-statics@3.3.2: dependencies: react-is: 16.13.1 @@ -19016,8 +18771,6 @@ snapshots: ignore@5.3.2: {} - immediate@3.0.6: {} - import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -19030,10 +18783,6 @@ snapshots: import-meta-resolve@2.2.2: {} - imsc@1.1.5: - dependencies: - sax: 1.2.1 - imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -19848,10 +19597,6 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lie@3.1.1: - dependencies: - immediate: 3.0.6 - lilconfig@2.1.0: {} lilconfig@3.1.3: {} @@ -19895,10 +19640,6 @@ snapshots: pify: 4.0.1 strip-bom: 3.0.0 - localforage@1.10.0: - dependencies: - lie: 3.1.1 - locate-path@3.0.0: dependencies: p-locate: 3.0.0 @@ -20163,15 +19904,6 @@ snapshots: dependencies: '@types/mdast': 3.0.15 - media-chrome@4.11.1(react@18.3.1): - dependencies: - '@vercel/edge': 1.2.2 - ce-la-react: 0.3.1(react@18.3.1) - transitivePeerDependencies: - - react - - media-tracks@0.3.3: {} - media-typer@0.3.0: {} memory-pager@1.5.0: {} @@ -20600,10 +20332,6 @@ snapshots: muri@1.3.0: {} - mux-embed@5.12.0: {} - - mux-embed@5.9.0: {} - mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -20618,8 +20346,6 @@ snapshots: napi-postinstall@0.1.6: {} - native-promise-only@0.8.1: {} - natural-compare-lite@1.4.0: {} natural-compare@1.4.0: {} @@ -20995,12 +20721,6 @@ snapshots: dependencies: find-up: 3.0.0 - player.style@0.1.10(react@18.3.1): - dependencies: - media-chrome: 4.11.1(react@18.3.1) - transitivePeerDependencies: - - react - possible-typed-array-names@1.1.0: {} postcss-import@15.1.0(postcss@8.5.3): @@ -21431,24 +21151,6 @@ snapshots: react-is@19.1.0: {} - react-player@3.3.2(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - '@mux/mux-player-react': 3.5.3(@types/react-dom@18.3.6(@types/react@18.3.20))(@types/react@18.3.20)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) - '@types/react': 18.3.20 - cloudflare-video-element: 1.3.4 - dash-video-element: 0.1.6 - hls-video-element: 1.5.7 - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - spotify-audio-element: 1.0.3 - tiktok-video-element: 0.1.1 - twitch-video-element: 0.1.4 - vimeo-video-element: 1.5.5 - wistia-video-element: 1.3.4 - youtube-video-element: 1.6.2 - transitivePeerDependencies: - - '@types/react-dom' - react-promise-suspense@0.3.4: dependencies: fast-deep-equal: 2.0.1 @@ -22024,8 +21726,6 @@ snapshots: dependencies: suf-log: 2.5.3 - sax@1.2.1: {} - saxes@6.0.0: dependencies: xmlchars: 2.2.0 @@ -22249,8 +21949,6 @@ snapshots: split2@4.2.0: {} - spotify-audio-element@1.0.3: {} - sprintf-js@1.0.3: {} sprintf-js@1.1.3: {} @@ -22424,8 +22122,6 @@ snapshots: dependencies: s.color: 0.0.15 - super-media-element@1.4.2: {} - supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -22572,8 +22268,6 @@ snapshots: throttle-debounce@3.0.1: {} - tiktok-video-element@0.1.1: {} - tiny-invariant@1.3.3: {} tiny-warning@1.0.3: {} @@ -22781,8 +22475,6 @@ snapshots: tw-animate-css@1.2.8: {} - twitch-video-element@0.1.4: {} - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -22852,8 +22544,6 @@ snapshots: typescript@4.9.5: {} - ua-parser-js@1.0.41: {} - unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -23075,10 +22765,6 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 - vimeo-video-element@1.5.5: - dependencies: - '@vimeo/player': 2.29.0 - vite@3.2.11(@types/node@18.19.87): dependencies: esbuild: 0.15.18 @@ -23157,8 +22843,6 @@ snapshots: dependencies: defaults: 1.0.4 - weakmap-polyfill@2.0.4: {} - web-namespaces@2.0.1: {} web-streams-polyfill@3.3.3: {} @@ -23251,10 +22935,6 @@ snapshots: dependencies: string-width: 5.1.2 - wistia-video-element@1.3.4: - dependencies: - super-media-element: 1.4.2 - with@7.0.2: dependencies: '@babel/parser': 7.27.0 @@ -23328,8 +23008,6 @@ snapshots: yocto-queue@0.1.0: {} - youtube-video-element@1.6.2: {} - zip-stream@4.1.1: dependencies: archiver-utils: 3.0.4 From 0206dec11b36722c775d9e633076f5cbd70f7f88 Mon Sep 17 00:00:00 2001 From: Enea Date: Sun, 14 Sep 2025 18:53:34 -0400 Subject: [PATCH 17/17] Taller PDF --- apps/web/components/public/lesson-viewer/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/web/components/public/lesson-viewer/index.tsx b/apps/web/components/public/lesson-viewer/index.tsx index 0d31a58d9..d3c350f2d 100644 --- a/apps/web/components/public/lesson-viewer/index.tsx +++ b/apps/web/components/public/lesson-viewer/index.tsx @@ -275,11 +275,11 @@ const LessonViewer = ({ )} {String.prototype.toUpperCase.call(LESSON_TYPE_PDF) === lesson.type && ( -
+