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 }) => (
+
+)
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 ? (