import { useCourse } from '@components/Contexts/CourseContext' import { useOrg } from '@components/Contexts/OrgContext' import { getAPIUrl } from '@services/config/config' import { updateCourseThumbnail } from '@services/courses/courses' import { getCourseThumbnailMediaDirectory } from '@services/media/media' import { ArrowBigUpDash, UploadCloud, Image as ImageIcon } from 'lucide-react' import { useLHSession } from '@components/Contexts/LHSessionContext' import React, { useState, useEffect, useRef } from 'react' import { mutate } from 'swr' import UnsplashImagePicker from './UnsplashImagePicker' const MAX_FILE_SIZE = 8_000_000; // 8MB const VALID_MIME_TYPES = ['image/jpeg', 'image/jpg', 'image/png'] as const; type ValidMimeType = typeof VALID_MIME_TYPES[number]; function ThumbnailUpdate() { const fileInputRef = useRef(null); const course = useCourse() as any const session = useLHSession() as any; const org = useOrg() as any const [localThumbnail, setLocalThumbnail] = useState<{ file: File; url: string } | null>(null) const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState('') const [showError, setShowError] = useState(false) const [showUnsplashPicker, setShowUnsplashPicker] = useState(false) const withUnpublishedActivities = course ? course.withUnpublishedActivities : false // Cleanup blob URLs when component unmounts or when thumbnail changes useEffect(() => { return () => { if (localThumbnail?.url) { URL.revokeObjectURL(localThumbnail.url); } }; }, [localThumbnail]); const validateFile = (file: File): boolean => { if (!VALID_MIME_TYPES.includes(file.type as ValidMimeType)) { setError(`Invalid file type: ${file.type}. Please upload only PNG or JPG/JPEG images`); setShowError(true); return false; } if (file.size > MAX_FILE_SIZE) { setError(`File size (${(file.size / 1024 / 1024).toFixed(2)}MB) exceeds the 8MB limit`); setShowError(true); return false; } setShowError(false); return true; } const handleFileChange = async (event: React.ChangeEvent) => { setError(''); setShowError(false); const file = event.target.files?.[0]; if (!file) { setError('Please select a file'); setShowError(true); return; } if (!validateFile(file)) { event.target.value = ''; return; } const blobUrl = URL.createObjectURL(file); setLocalThumbnail({ file, url: blobUrl }); await updateThumbnail(file); } const handleUnsplashSelect = async (imageUrl: string) => { try { setIsLoading(true); const response = await fetch(imageUrl); const blob = await response.blob(); if (!VALID_MIME_TYPES.includes(blob.type as ValidMimeType)) { throw new Error('Invalid image format from Unsplash'); } const file = new File([blob], `unsplash_${Date.now()}.jpg`, { type: blob.type }); if (!validateFile(file)) { return; } const blobUrl = URL.createObjectURL(file); setLocalThumbnail({ file, url: blobUrl }); await updateThumbnail(file); } catch (err) { setError('Failed to process Unsplash image'); setIsLoading(false); } } const updateThumbnail = async (file: File) => { setIsLoading(true); try { const res = await updateCourseThumbnail( course.courseStructure.course_uuid, file, session.data?.tokens?.access_token ); await mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`); await new Promise((r) => setTimeout(r, 1500)); if (res.success === false) { setError(res.HTTPmessage); setShowError(true); } else { setError(''); setShowError(false); } } catch (err) { setError('Failed to update thumbnail'); setShowError(true); } finally { setIsLoading(false); } } return (
{showError && error && (
{error}
)}
{localThumbnail ? ( Course thumbnail ) : ( Course thumbnail )} {!isLoading && (
)}
{isLoading && (
Uploading...
)}

Supported formats: PNG, JPG/JPEG

{showUnsplashPicker && ( setShowUnsplashPicker(false)} /> )}
) } export default ThumbnailUpdate