fix: load documents on client

This commit is contained in:
Rotimi Best
2025-09-15 19:14:22 +01:00
parent def5318c07
commit fb0131de2d
4 changed files with 81 additions and 26 deletions
@@ -61,7 +61,7 @@
});
snackbar.success('snackbar.course_settings.success.saved');
} catch (error) {
if (error.message) {
if (error instanceof Error) {
errors.description = error.message;
} else {
errors.description = $t('course.navItem.certificates.unexpected_error');
@@ -87,13 +87,13 @@
<UpgradeBanner>{$t('upgrade.certificate')}</UpgradeBanner>
<main class="md:-ml-3 md:-mr-3 px-2 mt-2">
<div class="flex-1 flex flex-col lg:flex-row justify-between gap-3 w-full mb-3 h-4/5">
<section class="w-full lg:w-2/5 h-full">
<main class="mt-2 px-2 md:-ml-3 md:-mr-3">
<div class="mb-3 flex h-4/5 w-full flex-1 flex-col justify-between gap-3 lg:flex-row">
<section class="h-full w-full lg:w-2/5">
<strong class="my-2 text-base font-semibold text-black dark:text-gray-100"
>{$t('course.navItem.certificates.certificate_settings')}</strong
>
<p class="text-xs font-normal my-4 dark:text-gray-100">
<p class="my-4 text-xs font-normal dark:text-gray-100">
{$t('course.navItem.certificates.theme')}
</p>
<RadioButtonGroup
@@ -103,23 +103,23 @@
>
<div class="flex flex-wrap justify-between gap-y-5">
{#each themes as theme}
<div class="flex mr-3">
<div class="mr-3 flex">
<RadioButton value={theme} />
<img
src={`/images/certificate_theme_${theme}.png`}
alt="themes"
class="w-[110px] h-[82px]"
class="h-[82px] w-[110px]"
/>
</div>
{/each}
</div>
</RadioButtonGroup>
<div>
<p class="text-xs font-normal text-black my-2 dark:text-gray-100">
<p class="my-2 text-xs font-normal text-black dark:text-gray-100">
{$t('course.navItem.certificates.logo')}
</p>
<div>
<p class="text-base mt-1 dark:text-gray-100">
<p class="mt-1 text-base dark:text-gray-100">
{$t('course.navItem.certificates.to_update')}
<strong class="font-semibold">{$t('course.navItem.certificates.settings')}</strong>
{$t('course.navItem.certificates.and_upload')}
@@ -133,7 +133,7 @@
/>
</div>
<span class="my-4">
<p class="dark:text-gray-100 text-xs font-normal mt-4 mb-2">
<p class="mb-2 mt-4 text-xs font-normal dark:text-gray-100">
{$t('course.navItem.certificates.description')}
</p>
<TextArea
@@ -160,9 +160,9 @@
</div>
</section>
<section
class="bg-gray-100 dark:bg-neutral-800 flex justify-center items-center rounded-md w-full lg:w-3/5"
class="flex w-full items-center justify-center rounded-md bg-gray-100 lg:w-3/5 dark:bg-neutral-800"
>
<div class="certificate-container flex justify-center items-center">
<div class="certificate-container flex items-center justify-center">
{#if $course.certificate_theme === 'professional'}
<Professional studentName={studentNamePlaceholder} />
{:else if $course.certificate_theme === 'plain'}
@@ -8,16 +8,18 @@
import EyeIcon from 'carbon-icons-svelte/lib/View.svelte';
import isEmpty from 'lodash/isEmpty';
import { createEventDispatcher } from 'svelte';
import type { LessonDocument } from '$lib/utils/types';
const dispatch = createEventDispatcher();
export let mode = MODES.view;
export let displayDocuments: any[] = [];
export let displayDocuments: LessonDocument[] = [];
export let downloadingDocuments: Set<string>;
export let formatFileSize: (bytes: number) => string;
export let openDocumentUploadModal: () => void;
export let deleteDocument: (index: number) => void;
export let downloadDocument: (doc: any) => Promise<void>;
export let downloadDocument: (doc: LessonDocument) => Promise<void>;
export let isLoading = false;
function getFileIcon(type: string) {
switch (type) {
@@ -28,7 +30,7 @@
}
}
function handleViewPDF(document: any) {
function handleViewPDF(document: LessonDocument) {
dispatch('viewPDF', document);
}
</script>
@@ -137,15 +139,17 @@
<PrimaryButton
variant={VARIANTS.CONTAINED_DARK}
onClick={() => handleViewPDF(document)}
{isLoading}
>
<EyeIcon size={16} class="mr-1 inline" />
{$t('course.navItem.lessons.materials.tabs.document.view_pdf')}
</PrimaryButton>
{:else}
<a
href={document.link}
target="_blank"
class="text-sm text-blue-600 underline hover:text-blue-800"
href={isLoading ? '#' : document.link}
target={isLoading ? null : '_blank'}
aria-disabled={isLoading}
class="text-sm text-blue-600 underline hover:text-blue-800 disabled:cursor-not-allowed disabled:opacity-50"
>
{$t('course.navItem.lessons.materials.tabs.document.view_document')}
</a>
@@ -3,7 +3,10 @@
lesson,
deleteLessonDocument
} from '$lib/components/Course/components/Lesson/store/lessons';
import { uploadCourseDocumentStore } from '$lib/components/Course/components/Lesson/store/lessons';
import {
uploadCourseDocumentStore,
presignedDocUrls
} from '$lib/components/Course/components/Lesson/store/lessons';
import MODES from '$lib/utils/constants/mode';
import IconButton from '$lib/components/IconButton/index.svelte';
import CloseIcon from 'carbon-icons-svelte/lib/Close.svelte';
@@ -14,11 +17,12 @@
import { onMount } from 'svelte';
import DocumentList from '../Document/DocumentList.svelte';
import { t } from '$lib/utils/functions/translations';
import { apiClient } from '$lib/utils/services/api';
import type { LessonDocument } from '$lib/utils/types';
import { snackbar } from '$lib/components/Snackbar/store';
export let mode = MODES.view;
// Use real documents from store
$: displayDocuments = $lesson?.materials?.documents || [];
let downloadingDocuments = new Set<string>();
let viewingPDF: any = null;
let pdfViewerOpen = false;
@@ -67,12 +71,47 @@
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
async function downloadDocument(doc: any) {
async function getPresignedDocs(docs: LessonDocument[]) {
if (!docs.length) return {};
if (docs.every((d) => $presignedDocUrls[d.key || ''])) return;
isLoading = true;
try {
const { data } = await apiClient.post('/course/presign/download/document', {
keys: docs.map((d) => d.key)
});
if (!data.success) {
throw new Error(data.message);
}
for (const key in data.urls) {
presignedDocUrls.update((urls) => ({
...urls,
[key]: data.urls[key]
}));
}
} catch (error) {
isLoading = false;
console.error('Error getting presigned docs:', error);
snackbar.error('Error getting presigned docs');
} finally {
isLoading = false;
}
}
async function downloadDocument(doc: LessonDocument) {
downloadingDocuments.add(doc.name);
downloadingDocuments = downloadingDocuments;
if (!doc.key) {
snackbar.error('Document not loaded correctly');
return;
}
try {
const response = await fetch(doc.link);
const response = await fetch($presignedDocUrls[doc.key]);
if (!response.ok) {
throw new Error('Failed to fetch document');
}
@@ -104,7 +143,7 @@
}, 150); // 150ms debounce
}
async function viewPDF(document: any) {
async function viewPDF(document: LessonDocument) {
// Wait for PDF.js to be fully loaded
if (!pdfjsLib) {
// Show loading state while waiting for PDF.js
@@ -136,7 +175,12 @@
error = null;
try {
const response = await fetch(document.link);
if (!document.key) {
snackbar.error('Document not loaded correctly');
return;
}
const response = await fetch($presignedDocUrls[document.key]);
if (!response.ok) {
throw new Error('Failed to fetch PDF');
@@ -227,7 +271,7 @@
debouncedRender();
}
function handleViewPDF(event: CustomEvent) {
function handleViewPDF(event: CustomEvent<LessonDocument>) {
viewPDF(event.detail);
}
@@ -289,10 +333,15 @@
function handleDragStart(event: DragEvent) {
event.preventDefault();
}
$: displayDocuments = $lesson?.materials?.documents || [];
$: getPresignedDocs(displayDocuments);
</script>
<div class="mx-auto w-full max-w-lg">
<DocumentList
{isLoading}
{mode}
{displayDocuments}
{downloadingDocuments}
@@ -30,6 +30,8 @@ export const uploadCourseDocumentStore = writable({
isCancelled: false
});
export const presignedDocUrls = writable<Record<string, string>>({});
export const lessons: Writable<Lesson[]> = writable([]);
export const lessonSections: Writable<LessonSection[]> = writable([]);