diff --git a/apps/value-based-design/src/app/(content)/workshops/[module]/[lesson]/(view)/shared-page.tsx b/apps/value-based-design/src/app/(content)/workshops/[module]/[lesson]/(view)/shared-page.tsx index 7fcaa273b..41c863750 100644 --- a/apps/value-based-design/src/app/(content)/workshops/[module]/[lesson]/(view)/shared-page.tsx +++ b/apps/value-based-design/src/app/(content)/workshops/[module]/[lesson]/(view)/shared-page.tsx @@ -10,7 +10,9 @@ import { Transcript } from '@/app/(content)/_components/video-transcript-rendere import { WorkshopPricing } from '@/app/(content)/workshops/_components/workshop-pricing-server' import { WorkshopResourceList } from '@/app/(content)/workshops/_components/workshop-resource-list' import Exercise from '@/app/(content)/workshops/[module]/[lesson]/(view)/exercise/_components/exercise' +import { CloudinaryDownloadButton } from '@/components/image-uploader/cloudinary-download-button' import { PlayerContainerSkeleton } from '@/components/player-skeleton' +import { getAllImageResourcesForResource } from '@/lib/image-resource-query' import type { Lesson } from '@/lib/lessons' import { getLessonMuxPlaybackId, @@ -109,7 +111,7 @@ export async function LessonPage({
- + )} + {downloadableAssets.length > 0 && ( + + )}
diff --git a/apps/value-based-design/src/app/(content)/workshops/[module]/[lesson]/edit/_components/edit-lesson-form.tsx b/apps/value-based-design/src/app/(content)/workshops/[module]/[lesson]/edit/_components/edit-lesson-form.tsx index cc30ec7a9..0acdea2ee 100644 --- a/apps/value-based-design/src/app/(content)/workshops/[module]/[lesson]/edit/_components/edit-lesson-form.tsx +++ b/apps/value-based-design/src/app/(content)/workshops/[module]/[lesson]/edit/_components/edit-lesson-form.tsx @@ -4,12 +4,14 @@ import * as React from 'react' import { useParams } from 'next/navigation' import { LessonMetadataFormFields } from '@/app/(content)/tutorials/[module]/[lesson]/edit/_components/edit-lesson-form-metadata' import { onLessonSave } from '@/app/(content)/tutorials/[module]/[lesson]/edit/actions' +import { DownloadableImageResourceUploader } from '@/components/image-uploader/downloadable-image-resource-uploader' import { env } from '@/env.mjs' import { useIsMobile } from '@/hooks/use-is-mobile' import { sendResourceChatMessage } from '@/lib/ai-chat-query' import { Lesson, LessonSchema } from '@/lib/lessons' import { updateLesson } from '@/lib/lessons-query' import { zodResolver } from '@hookform/resolvers/zod' +import { DownloadIcon } from 'lucide-react' import { useSession } from 'next-auth/react' import { useTheme } from 'next-themes' import { useForm, type UseFormReturn } from 'react-hook-form' @@ -76,6 +78,22 @@ export function EditLessonForm({ lesson }: Omit) { user={session?.data?.user} onSave={onLessonSaveWithModule} theme={theme} + tools={[ + { + id: 'downloadable-media', + label: 'Downloadable Media', + icon: () => ( + + ), + toolComponent: ( + + ), + }, + ]} > +}> = ({ resourceId, lessonTitle, abilityLoader }) => { + const ability = abilityLoader ? use(abilityLoader) : null + const canView = ability?.canView + + const { mutateAsync: download, status } = + api.imageResources.download.useMutation({ + onSuccess: (response) => { + if (response instanceof TRPCError) { + throw new Error('Sorry, I could not find that file.') + } + + const byteCharacters = atob(response.data) + const byteNumbers = new Array(byteCharacters.length) + for (let i = 0; i < byteCharacters.length; i++) { + byteNumbers[i] = byteCharacters.charCodeAt(i) + } + const byteArray = new Uint8Array(byteNumbers) + const blob = new Blob([byteArray], { type: 'application/zip' }) + + // Create download link and trigger download + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = response.fileName + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + }, + }) + + const downloadButton = canView ? ( + <> + Download + + ) : ( + <> + Download + + ) + + return ( + + ) +} + +const Spinner: React.FunctionComponent<{ + className?: string +}> = ({ className = 'w-8 h-8', ...rest }) => ( + + Loading + + + +) diff --git a/apps/value-based-design/src/components/image-uploader/cloudinary-upload-button.tsx b/apps/value-based-design/src/components/image-uploader/cloudinary-upload-button.tsx index 6b92838c0..dace662c3 100644 --- a/apps/value-based-design/src/components/image-uploader/cloudinary-upload-button.tsx +++ b/apps/value-based-design/src/components/image-uploader/cloudinary-upload-button.tsx @@ -8,10 +8,12 @@ import { useSession } from 'next-auth/react' import { Button } from '@coursebuilder/ui' -export const CloudinaryUploadButton: React.FC<{ dir: string; id: string }> = ({ - dir, - id, -}) => { +export const CloudinaryUploadButton: React.FC<{ + dir: string + id: string + is_downloadable?: boolean + resourceOfId?: string +}> = ({ dir, id, is_downloadable = false, resourceOfId }) => { const session = useSession() const cloudinaryRef = React.useRef() const widgetRef = React.useRef() @@ -20,6 +22,8 @@ export const CloudinaryUploadButton: React.FC<{ dir: string; id: string }> = ({ cloudinaryRef.current = (window as any).cloudinary }, []) + const folder = is_downloadable ? `${dir}/${id}/downloadables` : `${dir}/${id}` + return session?.data?.user ? (