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