feat: add 4 certificate templates (#295)
* add cloud translation api package * feat: Script that auto generates translations for other languages * add comments * fix: broken translation * improve translation * update locck file * clean up * all round trasnlations check * checkpoint * finish up * move to dev dependencies * fix vercel build error * translations fix * - add documentation - translation bug fix * quick adjust * PR Fixes * add new line * update docs * checkpoint * feat: redesign & addition of certificates * text color update * patterned width fix * "student-name" class remove * checkpoint * PR Fix * quick fix * PR Fixes * update certificate ready page for students * PR Fixes * quick cleanup * move fetchCourses into .ts file * feat: give students access to certificates only after completing course * checkpoint * height fix * Merge branch 'main' of https://github.com/rotimi-best/classroomio into classroomio-issue-272 * Fix; merge PR Fix * fix: width of plain & professional certs * PR Fix * feat: add translation * handle error when trying to download a certificate * add fetchExploreCourses * fix: requested reviews * review fixes checkpoint * replace the certificate background * height fix for patterned certififcates * fix more bugs * rename schema --------- Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
co-authored by
Rotimi Best
parent
f0907dde89
commit
72b58ed2c7
@@ -1,41 +1,79 @@
|
||||
<script>
|
||||
import { RadioButtonGroup, RadioButton, Toggle } from 'carbon-components-svelte';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import Professional from './certificates/Professional.svelte';
|
||||
import Plain from './certificates/Plain.svelte';
|
||||
import Professional from './templates/Professional.svelte';
|
||||
import Plain from './templates/Plain.svelte';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||
import PurpleProfessionalBadge from './templates/PurpleProfessionalBadge.svelte';
|
||||
import BlueProfessionalBadge from './templates/BlueProfessionalBadge.svelte';
|
||||
import PurpleBadgePattern from './templates/PurpleBadgePattern.svelte';
|
||||
import BlueBadgePattern from './templates/BlueBadgePattern.svelte';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { saveCertificateValidation } from '$lib/utils/functions/validator';
|
||||
|
||||
const studentNamePlaceholder = 'Name of student';
|
||||
const themes = ['professional', 'plain'];
|
||||
const themes = [
|
||||
'professional',
|
||||
'plain',
|
||||
'purpleProfessionalBadge',
|
||||
'blueProfessionalBadge',
|
||||
'purpleBadgePattern',
|
||||
'blueBadgePattern'
|
||||
];
|
||||
|
||||
let isSaving = false;
|
||||
let errors = {
|
||||
description: ''
|
||||
};
|
||||
let helperText = '';
|
||||
|
||||
const saveCertificate = async () => {
|
||||
isSaving = true;
|
||||
if (!$course.description) {
|
||||
errors.description = 'Description cannot be empty';
|
||||
|
||||
try {
|
||||
const result = saveCertificateValidation({
|
||||
description: $course.description || '',
|
||||
is_certificate_downloadable: $course.is_certificate_downloadable || false,
|
||||
certificate_theme: $course.certificate_theme || ''
|
||||
});
|
||||
|
||||
if (result && Object.keys(result).length > 0) {
|
||||
errors.description =
|
||||
$t(result.description) || $t('course.navItem.certificates.description_error');
|
||||
throw new Error(errors.description);
|
||||
}
|
||||
|
||||
errors.description = '';
|
||||
|
||||
await updateCourse($course.id, undefined, {
|
||||
description: $course.description || '',
|
||||
is_certificate_downloadable: $course.is_certificate_downloadable || false,
|
||||
certificate_theme: $course.certificate_theme || ''
|
||||
});
|
||||
snackbar.success('snackbar.course_settings.success.saved');
|
||||
} catch (error) {
|
||||
if (error.message) {
|
||||
errors.description = error.message;
|
||||
} else {
|
||||
errors.description = $t('course.navItem.certificates.unexpected_error');
|
||||
}
|
||||
} finally {
|
||||
isSaving = false;
|
||||
return;
|
||||
}
|
||||
await updateCourse($course.id, undefined, {
|
||||
description: $course.description || '',
|
||||
is_certificate_downloadable: $course.is_certificate_downloadable || false,
|
||||
certificate_theme: $course.certificate_theme || ''
|
||||
});
|
||||
isSaving = false;
|
||||
};
|
||||
|
||||
$: helperText = `${$course.description?.length || 0}/200 ${$t(
|
||||
'course.navItem.certificates.characters'
|
||||
)}`;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -63,16 +101,18 @@
|
||||
class="mb-10"
|
||||
disabled={$isFreePlan}
|
||||
>
|
||||
{#each themes as theme}
|
||||
<div class="flex mr-3">
|
||||
<RadioButton value={theme} />
|
||||
<img
|
||||
src={`/images/certificate_theme_${theme}.png`}
|
||||
alt="themes"
|
||||
class="w-[110px] h-[82px]"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex flex-wrap justify-between gap-y-5">
|
||||
{#each themes as theme}
|
||||
<div class="flex mr-3">
|
||||
<RadioButton value={theme} />
|
||||
<img
|
||||
src={`/images/certificate_theme_${theme}.png`}
|
||||
alt="themes"
|
||||
class="w-[110px] h-[82px]"
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</RadioButtonGroup>
|
||||
<div>
|
||||
<p class="text-xs font-normal text-black my-2 dark:text-gray-100">
|
||||
@@ -102,6 +142,7 @@
|
||||
bind:value={$course.description}
|
||||
errorMessage={errors.description}
|
||||
disabled={$isFreePlan}
|
||||
helperMessage={helperText}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
@@ -121,11 +162,19 @@
|
||||
<section
|
||||
class="bg-gray-100 dark:bg-neutral-800 flex justify-center items-center rounded-md w-full lg:w-3/5"
|
||||
>
|
||||
<div class="certificate-container flex justify-center items-center h-5/6">
|
||||
<div class="certificate-container flex justify-center items-center">
|
||||
{#if $course.certificate_theme === 'professional'}
|
||||
<Professional studentName={studentNamePlaceholder} />
|
||||
{:else}
|
||||
{:else if $course.certificate_theme === 'plain'}
|
||||
<Plain studentName={studentNamePlaceholder} />
|
||||
{:else if $course.certificate_theme === 'purpleProfessionalBadge'}
|
||||
<PurpleProfessionalBadge studentName={studentNamePlaceholder} />
|
||||
{:else if $course.certificate_theme === 'blueProfessionalBadge'}
|
||||
<BlueProfessionalBadge studentName={studentNamePlaceholder} />
|
||||
{:else if $course.certificate_theme === 'purpleBadgePattern'}
|
||||
<PurpleBadgePattern studentName={studentNamePlaceholder} />
|
||||
{:else if $course.certificate_theme === 'blueBadgePattern'}
|
||||
<BlueBadgePattern studentName={studentNamePlaceholder} />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
|
||||
<Box>
|
||||
<div class="flex flex-col items-center justify-center w-max h-full gap-2">
|
||||
<img src="/professional.png" alt="Certificate" />
|
||||
<img src="/images/student-certificate-preview.png" alt="Certificate" class="max-w-[218px]" />
|
||||
<p class="text-xl font-normal text-center">
|
||||
{$t('course.navItem.certificates.locked_certificate')}
|
||||
</p>
|
||||
|
||||
+81
-33
@@ -1,59 +1,107 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import Download from 'carbon-icons-svelte/lib/Download.svelte';
|
||||
|
||||
import { PUBLIC_SERVER_URL } from '$env/static/public';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { fetchProfileCourseProgress } from '$lib/utils/services/courses';
|
||||
import type { ProfileCourseProgress } from '$lib/utils/types';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
|
||||
let isLoading = false;
|
||||
let isCourseComplete = false;
|
||||
let progress: ProfileCourseProgress | undefined;
|
||||
|
||||
const downLoadCertificate = async () => {
|
||||
isLoading = true;
|
||||
const response = await fetch(PUBLIC_SERVER_URL + '/downloadCertificate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
theme: `${$course.certificate_theme}`,
|
||||
studentName: `${$profile.fullname}`,
|
||||
courseName: `${$course.title}`,
|
||||
courseDescription: `${$course.description}`,
|
||||
orgLogoUrl: `${$currentOrg.avatar_url}`,
|
||||
orgName: `${$currentOrg.name}`
|
||||
})
|
||||
});
|
||||
const data = await response.blob();
|
||||
console.log(data);
|
||||
const file = new Blob([data], { type: 'application/pdf' });
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
if (!isCourseComplete) return;
|
||||
|
||||
let a = document.createElement('a');
|
||||
document.body.append(a);
|
||||
a.download = 'Certificate of Completion - ' + $currentOrg.name;
|
||||
a.href = fileURL;
|
||||
a.click();
|
||||
a.remove();
|
||||
isLoading = true;
|
||||
try {
|
||||
const response = await fetch(PUBLIC_SERVER_URL + '/downloadCertificate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
theme: `${$course.certificate_theme}`,
|
||||
studentName: `${$profile.fullname}`,
|
||||
courseName: `${$course.title}`,
|
||||
courseDescription: `${$course.description}`,
|
||||
orgLogoUrl: `${$currentOrg.avatar_url}`,
|
||||
orgName: `${$currentOrg.name}`
|
||||
})
|
||||
});
|
||||
const data = await response.blob();
|
||||
console.log(data);
|
||||
const file = new Blob([data], { type: 'application/pdf' });
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
|
||||
let a = document.createElement('a');
|
||||
document.body.append(a);
|
||||
a.download = 'Certificate of Completion - ' + $currentOrg.name;
|
||||
a.href = fileURL;
|
||||
a.click();
|
||||
a.remove();
|
||||
} catch (error) {
|
||||
console.error('Error downloading', error);
|
||||
snackbar.error($t('course.navItem.certificates.unexpected_error'));
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
};
|
||||
|
||||
const hasUserCompletedCourse = async () => {
|
||||
isLoading = true;
|
||||
|
||||
const { data } = await fetchProfileCourseProgress($course.id, $profile.id);
|
||||
progress = data?.[0] || undefined;
|
||||
|
||||
if (progress) {
|
||||
isCourseComplete =
|
||||
progress.lessons_count === progress.lessons_completed &&
|
||||
progress.exercises_count === progress.exercises_completed;
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
hasUserCompletedCourse();
|
||||
});
|
||||
|
||||
$: title = isCourseComplete
|
||||
? 'course.navItem.certificates.unlocked_certificate'
|
||||
: 'course.navItem.certificates.complete_to_download_title';
|
||||
$: subtitle = isCourseComplete
|
||||
? 'course.navItem.certificates.unlocked_certificate_subtitle'
|
||||
: 'course.navItem.certificates.complete_to_download_subtitle';
|
||||
</script>
|
||||
|
||||
<Box>
|
||||
<div class="flex flex-col items-center justify-center w-max h-full gap-5">
|
||||
<img src="/professional.png" alt="Certificate" />
|
||||
<img src="/images/student-certificate-preview.png" alt="Certificate" class="max-w-[218px]" />
|
||||
<p class="text-xl font-normal text-center">
|
||||
{$t('course.navItem.certificates.unlocked_certificate')}
|
||||
{$t(title)}
|
||||
</p>
|
||||
<p class="text-sm font-normal text-center max-w-md">
|
||||
{$t(subtitle)}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.certificates.download_certificate')}
|
||||
className="rounded-md"
|
||||
className="flex items-center gap-2"
|
||||
onClick={downLoadCertificate}
|
||||
isDisabled={!PUBLIC_SERVER_URL}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
isDisabled={!PUBLIC_SERVER_URL || !isCourseComplete}
|
||||
{isLoading}
|
||||
/>
|
||||
>
|
||||
<Download size={16} />
|
||||
{$t('course.navItem.certificates.download_certificate')}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
|
||||
export let studentName = '';
|
||||
const logo = '/logo-512.png';
|
||||
|
||||
const dateIssued = new Date().toLocaleDateString();
|
||||
</script>
|
||||
|
||||
<div class="w-full relative dark:text-black">
|
||||
<img
|
||||
src="/images/blue-badge-pattern-background.svg"
|
||||
alt=""
|
||||
class="w-full h-full absolute z-0 object-cover"
|
||||
/>
|
||||
<div class="relative md:h-[17rem] h-full bg-white flex justify-between z-20 m-2">
|
||||
<div class="w-[70%] pl-3">
|
||||
<header class="absolute w-full flex justify-center items-center gap-1 text-sm font-bold">
|
||||
<img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
|
||||
<h1 class="text-sm">{$currentOrg.name}</h1>
|
||||
</header>
|
||||
|
||||
<div class="font-bold mt-16">
|
||||
<h2 class="text-[7px] m-0">This certificate is awarded to</h2>
|
||||
<h1 class="text-lg m-0 mt-0.5 leading-3 text-[#2D499F]">{studentName}</h1>
|
||||
</div>
|
||||
|
||||
<div class="font-bold mt-3 w-[90%]">
|
||||
<h1 class="text-[7px]">has successfully completed training on</h1>
|
||||
<h2 class="text-lg m-0">
|
||||
{$course.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center my-8 text-center">
|
||||
<div>
|
||||
<h1 class="h-2/4 m-0 text-[7px]">DATE ISSUED:</h1>
|
||||
<p class="border-b border-black font-bold text-[7px]">{dateIssued}</p>
|
||||
</div>
|
||||
<div class="w-[20%] text-[7px]">
|
||||
<div></div>
|
||||
<p class="border-t border-black font-bold text-[7px]">Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-[8px] font-bold w-[90%] desc">
|
||||
{$course.description}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden flex justify-center items-center">
|
||||
<img
|
||||
src="/images/blue-badge-pattern-badge.svg"
|
||||
alt="A badge"
|
||||
class="max-w-none w-[8rem] md:w-[10rem]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
|
||||
export let studentName = '';
|
||||
const logo = '/logo-512.png';
|
||||
|
||||
const dateIssued = new Date().toLocaleDateString();
|
||||
</script>
|
||||
|
||||
<div class="p-3 w-full dark:text-black bg-[#2D499F]">
|
||||
<div class="w-full bg-white pb-5 flex">
|
||||
<div class="w-[80%] pl-3">
|
||||
<header class="flex justify-start items-center gap-1 text-sm font-bold">
|
||||
<img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
|
||||
<h1 class="text-sm">{$currentOrg.name}</h1>
|
||||
</header>
|
||||
|
||||
<div class="font-bold mt-7">
|
||||
<h2 class="text-[7px] m-0">This certificate is awarded to</h2>
|
||||
<h1 class="text-lg m-0 mt-0.5 leading-3 text-[#2D499F]">{studentName}</h1>
|
||||
</div>
|
||||
|
||||
<div class="font-bold mt-3 w-[90%]">
|
||||
<h1 class="text-[7px]">has succesfully completed training on</h1>
|
||||
<h2 class="text-lg m-0">
|
||||
{$course.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-8 mb-5 text-center">
|
||||
<div class="">
|
||||
<h1 class="h-2/4 m-0 text-[7px]">DATE ISSUED:</h1>
|
||||
<p class="border-b border-black font-bold text-[7px]">{dateIssued}</p>
|
||||
</div>
|
||||
<div class="w-[20%] text-[7px]">
|
||||
<div class=""></div>
|
||||
<p class="border-t border-black font-bold text-[7px]">Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-[8px] font-bold w-[90%] desc">
|
||||
{$course.description}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="w-[20%]">
|
||||
<img src="/images/blue-professional-badge.svg" alt="A badge" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+4
-4
@@ -3,13 +3,13 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
|
||||
export let studentName = '';
|
||||
let borderwidth = 'border-4';
|
||||
let bordercolor = 'border-pink-700';
|
||||
let logo = '/logo-512.png';
|
||||
const borderwidth = 'border-4';
|
||||
const bordercolor = 'border-pink-700';
|
||||
const logo = '/logo-512.png';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="certificate-bg flex flex-col items-center justify-center {borderwidth} {bordercolor} py-3"
|
||||
class="certificate-bg w-full flex flex-col items-center justify-center {borderwidth} {bordercolor} py-3"
|
||||
>
|
||||
<div class="flex items-center gap-1 my-2">
|
||||
<img
|
||||
+5
-5
@@ -3,15 +3,15 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
|
||||
export let studentName = '';
|
||||
let borderwidth = 'border-l-8';
|
||||
let bordercolor = 'border-primary-800';
|
||||
let logo = '/logo-512.png';
|
||||
const borderwidth = 'border-l-8';
|
||||
const bordercolor = 'border-primary-800';
|
||||
const logo = '/logo-512.png';
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="certificate-bg flex items-center justify-center border border-dashed border-primary-400"
|
||||
class="certificate-bg w-full flex items-center justify-center border border-dashed border-primary-400"
|
||||
>
|
||||
<div class="{borderwidth} {bordercolor}">
|
||||
<div class="{borderwidth} {bordercolor} w-full">
|
||||
<div class="w-full px-5 py-3">
|
||||
<p class="text-sm font-normal my-2 uppercase text-gray-500">
|
||||
Certificate<br /> of completion
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<script>
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
|
||||
export let studentName = '';
|
||||
const logo = '/logo-512.png';
|
||||
|
||||
const dateIssued = new Date().toLocaleDateString();
|
||||
</script>
|
||||
|
||||
<div class="w-full relative dark:text-black">
|
||||
<img
|
||||
src="/images/purple-badge-pattern-background.svg"
|
||||
alt=""
|
||||
class="w-full h-full absolute z-0 object-cover"
|
||||
/>
|
||||
<div class="relative md:h-[17rem] h-full bg-white flex justify-between z-20 m-2">
|
||||
<div class="w-[70%] pl-3">
|
||||
<header class="absolute w-full flex justify-center items-center gap-1 text-sm font-bold">
|
||||
<img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
|
||||
<h1 class="text-sm">{$currentOrg.name}</h1>
|
||||
</header>
|
||||
|
||||
<div class="font-bold mt-16">
|
||||
<h2 class="text-[7px] m-0">This certificate is awarded to</h2>
|
||||
<h1 class="text-lg m-0 mt-0.5 leading-3 text-[#914397]">{studentName}</h1>
|
||||
</div>
|
||||
|
||||
<div class="font-bold mt-3 w-[90%]">
|
||||
<h1 class="text-[7px]">has successfully completed training on</h1>
|
||||
<h2 class="text-lg m-0">
|
||||
{$course.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center my-8 text-center">
|
||||
<div>
|
||||
<h1 class="h-2/4 m-0 text-[7px]">DATE ISSUED:</h1>
|
||||
<p class="border-b border-black font-bold text-[7px]">{dateIssued}</p>
|
||||
</div>
|
||||
<div class="w-[20%] text-[7px]">
|
||||
<div></div>
|
||||
<p class="border-t border-black font-bold text-[7px]">Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-[8px] font-bold w-[90%] desc">
|
||||
{$course.description}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden flex justify-center items-center">
|
||||
<img
|
||||
src="/images/purple-badge-pattern-badge.svg"
|
||||
alt="A badge"
|
||||
class="max-w-none w-[8rem] md:w-[10rem]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
<script>
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
|
||||
export let studentName = '';
|
||||
const logo = '/logo-512.png';
|
||||
|
||||
const dateIssued = new Date().toLocaleDateString();
|
||||
</script>
|
||||
|
||||
<div class="p-3 w-full dark:text-black bg-[#95449A]">
|
||||
<div class="w-full bg-white pb-5 flex">
|
||||
<div class="w-[80%] pl-3">
|
||||
<header class="flex justify-start items-center gap-1 text-sm font-bold">
|
||||
<img src={$currentOrg.avatar_url ? $currentOrg.avatar_url : logo} alt="logo" class="w-5" />
|
||||
<h1 class="text-sm">{$currentOrg.name}</h1>
|
||||
</header>
|
||||
|
||||
<div class="font-bold mt-7">
|
||||
<h2 class="text-[7px] m-0">This certificate is awarded to</h2>
|
||||
<h1 class="text-lg m-0 mt-0.5 leading-3 text-[#95449A]">{studentName}</h1>
|
||||
</div>
|
||||
|
||||
<div class="font-bold mt-3 w-[90%]">
|
||||
<h1 class="text-[7px]">has succesfully completed training on</h1>
|
||||
<h2 class="text-lg m-0">
|
||||
{$course.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center mt-8 mb-5 text-center">
|
||||
<div class="">
|
||||
<h1 class="h-2/4 m-0 text-[7px]">DATE ISSUED:</h1>
|
||||
<p class="border-b border-black font-bold text-[7px]">{dateIssued}</p>
|
||||
</div>
|
||||
<div class="w-[20%] text-[7px]">
|
||||
<div class=""></div>
|
||||
<p class="border-t border-black font-bold text-[7px]">Signature</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="text-[8px] font-bold w-[90%] desc">
|
||||
{$course.description}
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="w-[20%]">
|
||||
<img src="/images/purple-professional-badge.svg" alt="A badge" class="w-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,47 +0,0 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { get } from 'svelte/store';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
|
||||
export async function fetchCourses(profileId, orgId) {
|
||||
if (!orgId || !profileId) return;
|
||||
|
||||
const match = {};
|
||||
// Filter by profile_id if role isn't admin within organization
|
||||
if (!get(isOrgAdmin)) {
|
||||
match.member_profile_id = profileId;
|
||||
}
|
||||
|
||||
// Gets courses for a particular organisation where the current logged in user is a groupmember
|
||||
const { data: allCourses } = await supabase
|
||||
.rpc('get_courses', {
|
||||
org_id_arg: orgId,
|
||||
profile_id_arg: profileId
|
||||
})
|
||||
.match(match);
|
||||
|
||||
console.log(`allCourses`, allCourses);
|
||||
if (!Array.isArray(allCourses)) {
|
||||
return {
|
||||
allCourses: []
|
||||
};
|
||||
}
|
||||
|
||||
return { allCourses };
|
||||
}
|
||||
|
||||
export async function fetchExploreCourses(profileId, orgId) {
|
||||
if (!orgId || !profileId) return;
|
||||
|
||||
const { data: allCourses } = await supabase.rpc('get_explore_courses', {
|
||||
org_id_arg: orgId,
|
||||
profile_id_arg: profileId
|
||||
});
|
||||
|
||||
if (!Array.isArray(allCourses)) {
|
||||
return {
|
||||
allCourses: []
|
||||
};
|
||||
}
|
||||
|
||||
return { allCourses };
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
import { currentOrgPath, currentOrg } from '$lib/utils/store/org';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { fetchCourses } from '$lib/components/Courses/api';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { Search, Dropdown } from 'carbon-components-svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
@@ -3,23 +3,31 @@
|
||||
import { group } from '../Course/store';
|
||||
import type { GroupPerson } from '$lib/utils/types';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
|
||||
export let allowedRoles: number[] = [];
|
||||
export let onDenied = () => {};
|
||||
export let onlyStudent = false;
|
||||
|
||||
let userRole: number = 0;
|
||||
|
||||
function isAllowed(userRole) {
|
||||
return allowedRoles.includes(userRole);
|
||||
}
|
||||
|
||||
$: {
|
||||
const user: GroupPerson = $group.people.find((person) => person.profile_id === $profile.id)!;
|
||||
|
||||
userRole = user ? user.role_id : userRole;
|
||||
|
||||
if (!$isOrgAdmin && $group.people.length && !allowedRoles.includes(userRole)) {
|
||||
if (!$isOrgAdmin && $group.people.length && !isAllowed(userRole)) {
|
||||
onDenied();
|
||||
}
|
||||
}
|
||||
|
||||
$: show = onlyStudent ? isAllowed(ROLE.STUDENT) : isAllowed(userRole) || $isOrgAdmin;
|
||||
</script>
|
||||
|
||||
{#if allowedRoles.includes(userRole) || $isOrgAdmin}
|
||||
{#if show}
|
||||
<slot />
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user