feat: join course via QR code (#350)

* chore: install dependencies

* feat: create downloadable image component

* feat: update component design and add download qr button

* feat: update file name

* feat: increase border radius

* chore: add logo svg file

* refactor: move all logic to parent component

* feat: setup writeable store variable

* chore: fix ts error

* feat: setup download function in parent comp

* chore: swap out svelte/qr comp for js comp

* feat: download speed qr code improvements

* feat: add loading state to download button

* fix: responsive adjustments

* refactoring

---------

Co-authored-by: tunny17 <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
This commit is contained in:
Iniubong Obonguko
2024-06-08 05:20:12 +01:00
committed by GitHub
co-authored by tunny17 Rotimi Best
parent 5e7726c6ad
commit f3e0f9b7e2
7 changed files with 198 additions and 15 deletions
+2
View File
@@ -45,6 +45,7 @@
"dayjs": "^1.11.10",
"form-data": "^4.0.0",
"hotkeys-js": "^3.11.2",
"html-to-image": "^1.11.11",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"openai-edge": "^1.2.2",
@@ -84,6 +85,7 @@
"jest": "^29.7.0",
"postcss": "^8.4.24",
"postcss-load-config": "^4.0.1",
"qrcode": "^1.5.3",
"sass": "^1.64.2",
"svelte": "^4.1.2",
"svelte-calendar": "^3.1.6",
@@ -1,20 +1,23 @@
<script lang="ts">
import copy from 'copy-to-clipboard';
import { Popover } from 'carbon-components-svelte';
import Link from 'carbon-icons-svelte/lib/Link.svelte';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import QRCode from 'qrcode';
import { toPng } from 'html-to-image';
import Modal from '$lib/components/Modal/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { ROLE } from '$lib/utils/constants/roles';
import { addGroupMember, fetchGroup } from '$lib/utils/services/courses';
import { course, setCourse } from '$lib/components/Course/store';
import { MultiSelect, Loading } from 'carbon-components-svelte';
import { currentOrg, currentOrgDomain, currentOrgPath } from '$lib/utils/store/org';
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
import { getOrgTeam } from '$lib/utils/services/org';
import type { OrgTeamMember } from '$lib/utils/types/org';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { qrInviteNodeStore } from './store';
import { getStudentInviteLink } from '$lib/utils/functions/course';
import ShareQrImage from './ShareQRImage.svelte';
import {
triggerSendEmail,
NOTIFICATION_NAME
@@ -35,6 +38,8 @@
let selectedTutors: Tutor[] = [];
let isLoadingTutors = false;
let copied = false;
let qrImage = '';
let isLoadingQRDownload = false;
function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
@@ -48,7 +53,7 @@
return;
}
let membersStack = [];
let membersStack: { profile_id?: string; group_id?: string; role_id: number }[] = [];
console.log(`$course`, $course);
for (const tutor of selectedTutors) {
@@ -135,14 +140,47 @@
goto($page.url.pathname);
}
function handleQRDownload() {
if (!$qrInviteNodeStore) {
console.error('Node is not defined');
return;
}
isLoadingQRDownload = true;
setTimeout(() => {
toPng($qrInviteNodeStore)
.then((dataUrl) => {
const link = document.createElement('a');
link.download = `${$course.slug}-qr-code.png`;
link.href = dataUrl;
link.click();
})
.catch((error) => {
isLoadingQRDownload = false;
console.error('Oops, something went wrong!', error);
})
.finally(() => {
isLoadingQRDownload = false;
});
}, 300);
}
async function generateQR(text) {
try {
qrImage = await QRCode.toDataURL(text);
} catch (err) {
console.error(err);
}
}
$: {
selectedTutors = formatSelected(selectedIds);
console.log('selectedTutors', selectedTutors);
const query = new URLSearchParams($page.url.search);
addPeopleParm = query.get('add');
}
$: setTutors($currentOrg.id);
$: generateQR(getStudentInviteLink($course, $currentOrg.siteName, $currentOrgDomain));
</script>
<Modal
@@ -178,7 +216,7 @@
{/if}
</div>
<div class="mb-8 w-[90%] flex justify-between items-center">
<div class="mb-8 w-full flex justify-between items-center">
<div class="w-3/5">
<p class="text-base mb-1 font-semibold">
{$t('course.navItem.people.invite_modal.invite_students')}
@@ -187,21 +225,45 @@
</div>
<div class="relative">
<PrimaryButton
disablePadding={true}
className="text-sm py-2 px-3"
variant={VARIANTS.OUTLINED}
onClick={copyLink}
<button
type="button"
on:click={copyLink}
class="underline text-primary-800 font-bold cursor-pointer capitalize"
>
<Link size={16} class="mr-1" />
{$t('course.navItem.people.invite_modal.copy_link')}
</PrimaryButton>
<Popover caret open={copied} align="bottom">
</button>
<Popover caret open={copied} align="left">
<div style="padding: 5px">{$t('course.navItem.people.invite_modal.success')}</div>
</Popover>
</div>
</div>
<div
class="flex gap-5 flex-col-reverse md:flex-row justify-between items-center md:items-stretch p-4 w-full border rounded-md"
>
<div class="flex flex-col gap-3 items-center md:items-start justify-between">
<span class="font-medium text-sm">
{$t('course.navItem.people.invite_modal.via_qr')}
</span>
<PrimaryButton
isLoading={isLoadingQRDownload}
onClick={handleQRDownload}
label={$t('course.navItem.people.invite_modal.download_qr')}
className="font-medium"
/>
</div>
<div class="w-full md:w-28 border-4 p-1 border-[#f7f7f7]">
<img src={qrImage} alt="link qrcode" class="w-full h-full" />
</div>
</div>
<div class="absolute left-[-1000px] w-[40rem]">
<ShareQrImage {qrImage} course={$course} currentOrg={$currentOrg} />
</div>
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton
className="px-6 py-3"
@@ -0,0 +1,35 @@
<script lang="ts">
import type { CurrentOrg } from '$lib/utils/types/org.ts';
import type { Course } from '$lib/utils/types/index.ts';
import { isFreePlan } from '$lib/utils/store/org';
import { qrInviteNodeStore } from './store';
let node: any;
$: qrInviteNodeStore.set(node);
export let qrImage: string;
export let currentOrg: CurrentOrg;
export let course: Course;
</script>
<div
bind:this={node}
id="qr-container"
class="flex flex-col items-center justify-center h-[60rem] w-[40rem] bg-blue-900 rounded-xl p-10 pb-20"
>
<div class="bg-white pb-3 p-6 rounded-3xl text-center">
<div class="font-bold text-xl p-2 bg-gray-100 my-4 rounded-xl">Scan QR</div>
<img src={qrImage} alt="qrcode" />
<div class="pb-4 pt-1">
<p class="mt-2 font-bold text-2xl text-primary-600">{course.title}</p>
<p class="mt-1 font-semibold text-lg text-black">{currentOrg.name}</p>
</div>
</div>
{#if $isFreePlan}
<div class="bg-white flex items-center gap-x-2 px-4 py-2 rounded-md relative top-16">
<img src="/logo.svg" alt="logo" class="w-6" />
<span class="font-bold text-base">ClassroomIO.com</span>
</div>
{/if}
</div>
@@ -3,3 +3,5 @@ import { writable } from 'svelte/store';
export const deleteMemberModal = writable({
open: false
});
export const qrInviteNodeStore = writable(null);
@@ -803,6 +803,9 @@
"go_to": "go to organization settings",
"invite_students": "Invite Students",
"via_link": "You can invite students via an invite link",
"via_qr": "You can invite students via QR",
"download_qr": "Download QR code",
"qr_downloading_snackbar": "QR code generated successfully, please wait while it downloads",
"copy_link": "Copy link",
"success": "Copied Successfully",
"finish": "Finish"
+5
View File
@@ -0,0 +1,5 @@
<svg width="33" height="24" viewBox="0 0 33 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.153 12.5745C29.5145 12.5745 32.2395 9.84943 32.2395 6.48791C32.2395 3.1264 29.5145 0.401367 26.153 0.401367C22.7915 0.401367 20.0664 3.1264 20.0664 6.48791C20.0664 9.84943 22.7915 12.5745 26.153 12.5745Z" fill="#0542CC"/>
<path d="M32.3048 14.9824V23.8112H18.9277V20.6008C18.9277 17.5241 21.4025 14.9824 24.5461 14.9824H32.3048Z" fill="#0542CC"/>
<path d="M16.7213 0.401367V23.8112H11.7049C8.49443 23.8112 5.55148 22.4735 3.41115 20.4001C1.27082 18.2598 0 15.3168 0 12.1063C0 5.61842 5.21705 0.401367 11.7049 0.401367H16.7213Z" fill="#0542CC"/>
</svg>

After

Width:  |  Height:  |  Size: 661 B