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:
co-authored by
tunny17
Rotimi Best
parent
5e7726c6ad
commit
f3e0f9b7e2
@@ -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"
|
||||
|
||||
@@ -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 |
Reference in New Issue
Block a user