feat: add documents to Lesson materials (#572)

* feat: Initial logic for the pdf upload

* feat: uploa logic, translations and ui flow complete

* chore:console log cleanup

* feat: fixed upload issue and added cancel upload button

* feat: fixing the download document ish

* feat: All done and ready for use

* feat: updated the document viewing logic

* feat: some minor refactor

* feat: added migration file

* fix: dark mode, translations, restyling,

---------

Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Nwosu Emmanuel
2025-09-10 11:39:06 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 7a77bd6c79
commit 937f1d8155
32 changed files with 1682 additions and 50 deletions
+12
View File
@@ -5,4 +5,16 @@ export const ALLOWED_CONTENT_TYPES = [
'video/x-matroska'
] as const;
export const ALLOWED_DOCUMENT_TYPES = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/msword' // .doc
] as const;
export const MAX_FILE_SIZE = 500 * 1024 * 1024; // 500MB
export const MAX_DOCUMENT_SIZE = 5 * 1024 * 1024; // 5MB
export const BUCKET_NAME = {
VIDEOS: 'videos',
DOCUMENTS: 'documents'
} as const;
+99 -3
View File
@@ -1,6 +1,11 @@
import { GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { ZCourseDownloadPresignedUrl, ZCoursePresignUrlUpload } from '$src/types/course';
import {
ZCourseDocumentPresignUrlUpload,
ZCourseDownloadPresignedUrl,
ZCoursePresignUrlUpload
} from '$src/types/course';
import { BUCKET_NAME } from '$src/constants/upload';
import { CLOUDFLARE } from '$src/constants';
import type { GetSignedUrlParameters } from '$src/utils/s3';
import { Hono } from 'hono';
@@ -20,7 +25,7 @@ presignRouter.post('/upload', authMiddleware, async (c) => {
try {
const command = new PutObjectCommand({
Bucket: CLOUDFLARE.R2.BUCKET,
Bucket: BUCKET_NAME.VIDEOS,
Key: fileKey,
ContentType: fileType
}) as GetSignedUrlParameters[1];
@@ -60,6 +65,55 @@ presignRouter.post('/upload', authMiddleware, async (c) => {
}
});
presignRouter.post('/document/upload', authMiddleware, async (c) => {
const body = await c.req.json();
const result = ZCourseDocumentPresignUrlUpload.parse(body);
const { fileName, fileType } = result;
const fileKey = generateFileKey(fileName);
try {
const command = new PutObjectCommand({
Bucket: BUCKET_NAME.DOCUMENTS,
Key: fileKey,
ContentType: fileType
}) as GetSignedUrlParameters[1];
const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, {
expiresIn: CLOUDFLARE.R2.PRESIGN_EXPIRATION_TIME
});
return c.json({
success: true,
url: presignedUrl,
fileKey,
message: 'Document pre-signed URL generated successfully'
});
} catch (error) {
console.error('Error generating document pre-signed URL:', error);
if (error instanceof Error && error.name === 'SignatureDoesNotMatch') {
return c.json(
{
success: false,
type: 'SIGNATURE_ERROR',
message: 'Invalid signature for the request'
},
403
);
}
return c.json(
{
success: false,
type: 'INTERNAL_ERROR',
message: 'Error generating document pre-signed URL'
},
500
);
}
});
presignRouter.post('/download', authMiddleware, async (c) => {
const body = await c.req.json();
const result = ZCourseDownloadPresignedUrl.parse(body);
@@ -72,7 +126,7 @@ presignRouter.post('/download', authMiddleware, async (c) => {
await Promise.all(
keys.map(async (key) => {
const command = new GetObjectCommand({
Bucket: CLOUDFLARE.R2.BUCKET,
Bucket: BUCKET_NAME.VIDEOS,
Key: key
}) as GetSignedUrlParameters[1];
@@ -101,3 +155,45 @@ presignRouter.post('/download', authMiddleware, async (c) => {
);
}
});
presignRouter.post('/download/document', authMiddleware, async (c) => {
const body = await c.req.json();
const result = ZCourseDownloadPresignedUrl.parse(body);
const { keys } = result;
try {
const signedUrls: Record<string, string> = {};
await Promise.all(
keys.map(async (key) => {
const command = new GetObjectCommand({
Bucket: BUCKET_NAME.DOCUMENTS,
Key: key
}) as GetSignedUrlParameters[1];
const presignedUrl = await getSignedUrl(s3Client as GetSignedUrlParameters[0], command, {
expiresIn: CLOUDFLARE.R2.DOWNLOAD_EXPIRATION_TIME
});
signedUrls[key] = presignedUrl;
})
);
return c.json({
success: true,
urls: signedUrls,
message: 'Document URLs retrieved successfully'
});
} catch (error) {
console.error('Error Retrieving Document:', error);
return c.json(
{
success: false,
type: 'INTERNAL_ERROR',
message: 'Error Retrieving Document'
},
500
);
}
});
+16 -1
View File
@@ -1,4 +1,8 @@
import { ALLOWED_CONTENT_TYPES, MAX_FILE_SIZE } from '$src/constants/upload';
import {
ALLOWED_CONTENT_TYPES,
ALLOWED_DOCUMENT_TYPES,
MAX_FILE_SIZE
} from '$src/constants/upload';
import { z } from 'zod';
@@ -32,6 +36,17 @@ export const ZCoursePresignUrlUpload = z.object({
export type TCoursePresignUrlUpload = z.infer<typeof ZCoursePresignUrlUpload>;
export const ZCourseDocumentPresignUrlUpload = z.object({
fileName: z.string().min(1, 'File name is required'),
fileType: z.enum(ALLOWED_DOCUMENT_TYPES, {
errorMap: () => ({
message: `Invalid content type. Allowed types: ${ALLOWED_DOCUMENT_TYPES.join(', ')}`
})
})
});
export type TCourseDocumentPresignUrlUpload = z.infer<typeof ZCourseDocumentPresignUrlUpload>;
export const ZCourseDownloadPresignedUrl = z.object({
keys: z.array(z.string()).min(1, 'File names array is required')
});