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 |
Generated
+75
-1
@@ -237,6 +237,9 @@ importers:
|
||||
hotkeys-js:
|
||||
specifier: ^3.11.2
|
||||
version: 3.11.2
|
||||
html-to-image:
|
||||
specifier: ^1.11.11
|
||||
version: 1.11.11
|
||||
js-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
@@ -349,6 +352,9 @@ importers:
|
||||
postcss-load-config:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1([email protected])([email protected])
|
||||
qrcode:
|
||||
specifier: ^1.5.3
|
||||
version: 1.5.3
|
||||
sass:
|
||||
specifier: ^1.64.2
|
||||
version: 1.64.2
|
||||
@@ -7680,6 +7686,14 @@ packages:
|
||||
execa: 0.8.0
|
||||
dev: false
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
||||
dependencies:
|
||||
@@ -8870,6 +8884,10 @@ packages:
|
||||
engines: {node: '>=0.3.1'}
|
||||
dev: false
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -9002,6 +9020,10 @@ packages:
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==}
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -14017,6 +14039,11 @@ packages:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
dev: true
|
||||
|
||||
/[email protected]([email protected]):
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -14475,6 +14502,17 @@ packages:
|
||||
engines: {node: '>=0.6.0', teleport: '>=0.2.0'}
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
dijkstrajs: 1.0.3
|
||||
encode-utf8: 1.0.3
|
||||
pngjs: 5.0.0
|
||||
yargs: 15.4.1
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-OQiU+C+Ds5qiH91qh/mg0w+8nwQuLjM4F4M/PbmhDOoYehPh+Fb0bDjtR1sOvy7YKxvj28Y/M0PhP5uVX0kB+g==}
|
||||
engines: {node: '>=0.6'}
|
||||
@@ -14867,6 +14905,10 @@ packages:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
|
||||
dev: true
|
||||
@@ -15126,7 +15168,6 @@ packages:
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
dev: false
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
|
||||
@@ -17219,6 +17260,10 @@ packages:
|
||||
is-weakset: 2.0.2
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -17329,6 +17374,10 @@ packages:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -17357,6 +17406,14 @@ packages:
|
||||
resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -17366,6 +17423,23 @@ packages:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
cliui: 6.0.0
|
||||
decamelize: 1.2.0
|
||||
find-up: 4.1.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
require-main-filename: 2.0.0
|
||||
set-blocking: 2.0.0
|
||||
string-width: 4.2.3
|
||||
which-module: 2.0.1
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
dev: true
|
||||
|
||||
/[email protected]:
|
||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
Reference in New Issue
Block a user