Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio
This commit is contained in:
@@ -14,6 +14,11 @@ html {
|
|||||||
#calendar .grid {
|
#calendar .grid {
|
||||||
--sc-theme-calendar-colors-background-highlight: var(--main-primary-color);
|
--sc-theme-calendar-colors-background-highlight: var(--main-primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html .dark {
|
||||||
|
--scrollbarBG: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
|
scrollbar-color: var(--thumbBG) var(--scrollbarBG);
|
||||||
|
|||||||
@@ -6,12 +6,15 @@
|
|||||||
import SendAlt from 'carbon-icons-svelte/lib/SendAlt.svelte';
|
import SendAlt from 'carbon-icons-svelte/lib/SendAlt.svelte';
|
||||||
import ChartPie from 'carbon-icons-svelte/lib/ChartPie.svelte';
|
import ChartPie from 'carbon-icons-svelte/lib/ChartPie.svelte';
|
||||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||||
|
import Settings from 'carbon-icons-svelte/lib/Settings.svelte';
|
||||||
|
import { apps } from './store';
|
||||||
|
|
||||||
import QandA from './components/QandA/index.svelte';
|
import QandA from './components/QandA/index.svelte';
|
||||||
import LiveChat from './components/LiveChat/index.svelte';
|
import LiveChat from './components/LiveChat/index.svelte';
|
||||||
import Notes from './components/Notes/index.svelte';
|
import Notes from './components/Notes/index.svelte';
|
||||||
import Poll from './components/Poll/index.svelte';
|
import Poll from './components/Poll/index.svelte';
|
||||||
import APPS_CONSTANTS from './constants';
|
import APPS_CONSTANTS from './constants';
|
||||||
|
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||||
|
|
||||||
let selectedApp;
|
let selectedApp;
|
||||||
|
|
||||||
@@ -48,8 +51,21 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="root">
|
<div class={`root ${$apps.open ? 'open dark:bg-slate-800' : 'close dark:bg-slate-800'}`}>
|
||||||
<div class="apps">
|
<div class={`apps`}>
|
||||||
|
<div class="lg:hidden">
|
||||||
|
<IconButton
|
||||||
|
buttonClassName="lg:hidden"
|
||||||
|
toolTipProps={{ title: 'Settings', hotkeys: ['A', '0'] }}
|
||||||
|
onClick={() => {
|
||||||
|
handleAppClick;
|
||||||
|
$apps.dropdown = !$apps.dropdown;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Settings size={24} class="carbon-icon dark:text-white lg:hidden" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
toolTipProps={{ title: 'Live Chat', hotkeys: ['A', '1'] }}
|
toolTipProps={{ title: 'Live Chat', hotkeys: ['A', '1'] }}
|
||||||
value={APPS_CONSTANTS.APPS.LIVE_CHAT}
|
value={APPS_CONSTANTS.APPS.LIVE_CHAT}
|
||||||
@@ -104,7 +120,7 @@
|
|||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.root {
|
.root {
|
||||||
height: 93vh;
|
height: calc(100vh - 48px);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-left: 1px solid var(--border-color);
|
border-left: 1px solid var(--border-color);
|
||||||
@@ -129,4 +145,25 @@
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1024px) {
|
||||||
|
.root {
|
||||||
|
position: absolute;
|
||||||
|
height: calc(100vh - 109px);
|
||||||
|
top: 109px;
|
||||||
|
right: 0;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
.open {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(0);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateX(100%);
|
||||||
|
display: none;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
export const apps = writable({
|
||||||
|
open: false,
|
||||||
|
dropdown: false
|
||||||
|
});
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import GoogleIconColored from '../Icons/GoogleIconColored.svelte';
|
import GoogleIconColored from '../Icons/GoogleIconColored.svelte';
|
||||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||||
|
|
||||||
export let supabase;
|
export let supabase: SupabaseClient;
|
||||||
export let handleSubmit;
|
export let handleSubmit = () => {};
|
||||||
export let isLogin = true;
|
export let isLogin = true;
|
||||||
export let showOnlyContent = false;
|
export let showOnlyContent = false;
|
||||||
export let isLoading = false;
|
export let isLoading = false;
|
||||||
export let showLogo = false;
|
export let showLogo = false;
|
||||||
export let formRef;
|
export let formRef;
|
||||||
|
export let hideGoogleAuth = false;
|
||||||
|
|
||||||
async function signInWithGoogle() {
|
async function signInWithGoogle() {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
@@ -19,12 +22,11 @@
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
console.log('signInWithGoogle');
|
console.log('signInWithGoogle');
|
||||||
const { user, session, error } = await supabase.auth.signInWithOAuth({
|
const { error, data } = await supabase.auth.signInWithOAuth({
|
||||||
provider: 'google'
|
provider: 'google'
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('user', user);
|
console.log('data', data);
|
||||||
console.log('session', session);
|
|
||||||
console.log('error', error);
|
console.log('error', error);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('catch error', error);
|
console.log('catch error', error);
|
||||||
@@ -57,7 +59,7 @@
|
|||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</form>
|
</form>
|
||||||
{#if !showOnlyContent}
|
{#if !showOnlyContent && !hideGoogleAuth}
|
||||||
<div class="w-10/12 mb-3">
|
<div class="w-10/12 mb-3">
|
||||||
<p class="dark:text-white text-sm mb-5">or sign up with:</p>
|
<p class="dark:text-white text-sm mb-5">or sign up with:</p>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
@@ -75,10 +77,14 @@
|
|||||||
{#if !showOnlyContent}
|
{#if !showOnlyContent}
|
||||||
<div class="w-full p-6 border-t border-grey text-center">
|
<div class="w-full p-6 border-t border-grey text-center">
|
||||||
{#if isLogin}
|
{#if isLogin}
|
||||||
Not registered yet? <a class="hover:underline text-primary-700" href="/signup">Sign up</a>
|
Not registered yet? <a
|
||||||
|
class="hover:underline text-primary-700"
|
||||||
|
href="/signup{$page.url.search}">Sign up</a
|
||||||
|
>
|
||||||
{:else}
|
{:else}
|
||||||
Already have an account? <a class="hover:underline text-primary-700" href="/login"
|
Already have an account? <a
|
||||||
>Log In</a
|
class="hover:underline text-primary-700"
|
||||||
|
href="/login{$page.url.search}">Log In</a
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
|
|
||||||
export let className = '';
|
export let className = '';
|
||||||
</script>
|
</script>
|
||||||
@@ -12,7 +13,9 @@
|
|||||||
class="{className} ml-2 hover:no-underline"
|
class="{className} ml-2 hover:no-underline"
|
||||||
>
|
>
|
||||||
<PrimaryButton>
|
<PrimaryButton>
|
||||||
View site
|
{#if !$isMobile}
|
||||||
|
View site
|
||||||
|
{/if}
|
||||||
<ArrowUpRightIcon />
|
<ArrowUpRightIcon />
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -45,8 +45,8 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="md:-ml-4 md:-mr-4 px-2">
|
<main class="md:-ml-4 md:-mr-4 px-2">
|
||||||
<div class="flex-1 flex flex-col md:flex-row justify-between gap-3 w-full mb-3 h-4/5">
|
<div class="flex-1 flex flex-col lg:flex-row justify-between gap-3 w-full mb-3 h-4/5">
|
||||||
<section class="w-full md:w-2/5 h-full">
|
<section class="w-full lg:w-2/5 h-full">
|
||||||
<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"
|
||||||
>Certificate settings</strong
|
>Certificate settings</strong
|
||||||
>
|
>
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section
|
<section
|
||||||
class="bg-gray-100 dark:bg-gray-700 flex justify-center items-center rounded-md w-full md:w-3/5"
|
class="bg-gray-100 dark:bg-gray-700 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 h-5/6">
|
||||||
{#if $course.certificate_theme === 'professional'}
|
{#if $course.certificate_theme === 'professional'}
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
<Modal
|
<Modal
|
||||||
onClose={() => (openModal = false)}
|
onClose={() => (openModal = false)}
|
||||||
bind:open={openModal}
|
bind:open={openModal}
|
||||||
width="w-[60%]"
|
width="w-[80%] md:w-[60%]"
|
||||||
containerClass="overflow-hidden"
|
containerClass="overflow-hidden"
|
||||||
modalHeading="Add New Lesson"
|
modalHeading="Add New Lesson"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -165,8 +165,8 @@
|
|||||||
<p class="dark:text-white mt-4 text-sm">Created Jul 3, 2021</p>
|
<p class="dark:text-white mt-4 text-sm">Created Jul 3, 2021</p>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<Box className="mt-3">
|
<Box className="mt-3 text-center">
|
||||||
<div class="flex justify-between flex-col items-center w-96">
|
<div class="flex justify-between flex-col items-center w-[80%] md:w-96">
|
||||||
<img src="/images/empty-exercise-icon.svg" alt="Exercise" class="my-2.5 mx-auto" />
|
<img src="/images/empty-exercise-icon.svg" alt="Exercise" class="my-2.5 mx-auto" />
|
||||||
<h2 class="text-xl my-1.5 font-normal">No exercises added for this lesson</h2>
|
<h2 class="text-xl my-1.5 font-normal">No exercises added for this lesson</h2>
|
||||||
<p class="text-sm text-center text-slate-500">
|
<p class="text-sm text-center text-slate-500">
|
||||||
|
|||||||
@@ -11,13 +11,15 @@
|
|||||||
const onChange = (tab) => () => (currentTab = tab);
|
const onChange = (tab) => () => (currentTab = tab);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="w-full h-full flex flex-row items-start gap-3">
|
<section class="w-full h-full flex flex-col md:flex-row items-start gap-3">
|
||||||
<div class="flex-1 flex-col">
|
<div class="flex flex-row md:flex-col gap-2 items-center">
|
||||||
<p class="text-sm font-normal text-start text-[#4F4B4B] dark:text-[#b0a9a9] mb-3">Add By</p>
|
<p class="text-sm font-normal text-start text-[#4F4B4B] dark:text-[#b0a9a9] mb-3 w-full">
|
||||||
|
Add By
|
||||||
|
</p>
|
||||||
{#each tabs as item (item.value)}
|
{#each tabs as item (item.value)}
|
||||||
<button
|
<button
|
||||||
on:click={onChange(item.value)}
|
on:click={onChange(item.value)}
|
||||||
class={`w-full px-4 py-3 my-5 border ${
|
class={`w-full px-4 py-3 my-1 border ${
|
||||||
currentTab === item.value
|
currentTab === item.value
|
||||||
? 'border border-[#0233BD] bg-[#F5F8FE] dark:text-black'
|
? 'border border-[#0233BD] bg-[#F5F8FE] dark:text-black'
|
||||||
: 'border border-gray-200'
|
: 'border border-gray-200'
|
||||||
|
|||||||
@@ -197,13 +197,13 @@
|
|||||||
<Modal
|
<Modal
|
||||||
{onClose}
|
{onClose}
|
||||||
bind:open={$uploadCourseVideoStore.isModalOpen}
|
bind:open={$uploadCourseVideoStore.isModalOpen}
|
||||||
width="w-4/5 h-[566px]"
|
width="w-4/5 w-[90%] h-[80%] md:h-[566px]"
|
||||||
modalHeading="Add a Video"
|
modalHeading="Add a Video"
|
||||||
>
|
>
|
||||||
<VideoUploader {lessonId} />
|
<VideoUploader {lessonId} />
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<h1 class="text-4xl mt-0">
|
<h1 class="text-4xl mt-0 capitalize">
|
||||||
{lessonTitle}
|
{lessonTitle}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@@ -386,7 +386,7 @@
|
|||||||
</article>
|
</article>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<Box>
|
<Box className="text-center">
|
||||||
<img src="/no-video.svg" alt="Video not found" />
|
<img src="/no-video.svg" alt="Video not found" />
|
||||||
<h3 class="text-xl font-normal dark:text-white py-2">
|
<h3 class="text-xl font-normal dark:text-white py-2">
|
||||||
No note, video or slide added for this lesson yet
|
No note, video or slide added for this lesson yet
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
import { updateCourse } from '$lib/utils/services/courses';
|
import { updateCourse } from '$lib/utils/services/courses';
|
||||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
|
import { menu } from '$lib/components/Org/store';
|
||||||
|
|
||||||
// export let lessonId;
|
// export let lessonId;
|
||||||
export let path;
|
export let path;
|
||||||
@@ -32,12 +33,14 @@
|
|||||||
let show = null;
|
let show = null;
|
||||||
let isLessonActive = false;
|
let isLessonActive = false;
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
$menu.hidden = !$menu.hidden;
|
||||||
|
};
|
||||||
|
|
||||||
function handleMainGroupClick(href) {
|
function handleMainGroupClick(href) {
|
||||||
return () => {
|
return () => {
|
||||||
if ($isMobile) {
|
|
||||||
setTimeout(() => (show = false), 500);
|
|
||||||
}
|
|
||||||
goto(href);
|
goto(href);
|
||||||
|
toggleSidebar();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,145 +149,66 @@
|
|||||||
|
|
||||||
<Settings />
|
<Settings />
|
||||||
|
|
||||||
<div class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}">
|
<aside
|
||||||
{#if show}
|
class={`${
|
||||||
<div class="relative h-full dark:bg-gray-800">
|
$menu.hidden
|
||||||
<!-- <PageNav title="Back to courses" paddingClass="pl-2" titleSize="text-md">
|
? '-translate-x-[100%] absolute md:translate-x-0 md:relative z-[40]'
|
||||||
<slot:fragment slot="image">
|
: 'translate-x-0 absolute md:relative z-[40]'
|
||||||
<Avatar src={$course.logo} />
|
} transition w-[350px] min-w-[350px] md:max-w-[350px] bg-gray-100 dark:bg-gray-800 h-[calc(100vh-48px)] overflow-y-auto border border-l-0 border-t-0 border-b-0 border-r-1`}
|
||||||
<IconButton
|
>
|
||||||
value="toggle"
|
<div class="h-full flex flex-col">
|
||||||
onClick={() => goto(`${$currentOrgPath}/courses`)}
|
<ul class="my-5">
|
||||||
size="small"
|
{#each navItems as navItem}
|
||||||
contained={true}
|
{#if !navItem.show || (typeof navItem.show === 'function' && navItem.show())}
|
||||||
|
<NavExpandable
|
||||||
|
label={navItem.label}
|
||||||
|
handleClick={handleMainGroupClick(navItem.to)}
|
||||||
|
isGroupActive={(path || $page.url.pathname) === navItem.to}
|
||||||
|
isExpanded={isLessonActive}
|
||||||
|
total={navItem.isLecture ? ($lessons || []).length : 0}
|
||||||
>
|
>
|
||||||
<ArrowLeftIcon size={20} class="carbon-icon dark:text-white" />
|
{#if navItem.isLecture}
|
||||||
</IconButton>
|
{#each $lessons as item, index}
|
||||||
</slot:fragment>
|
<a
|
||||||
<slot:fragment slot="widget">
|
class="pl-7 w-[95%] text-[0.80rem] mb-1 text-black dark:text-white {isStudent &&
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
!item.is_unlocked
|
||||||
<IconButton onClick={() => ($settingsDialog.open = !$settingsDialog.open)}>
|
? 'cursor-not-allowed'
|
||||||
<SettingsIcon size={20} class="carbon-icon dark:text-white" />
|
: ''}"
|
||||||
</IconButton>
|
href={isStudent && !item.is_unlocked
|
||||||
</RoleBasedSecurity>
|
? $page.url.pathname
|
||||||
</slot:fragment>
|
: getLessonsRoute($course.id, item.id)}
|
||||||
</PageNav> -->
|
on:click={toggleSidebar}
|
||||||
<div class="h-full py-3 overflow-y-auto">
|
aria-disabled={!item.is_unlocked}
|
||||||
{#each navItems as navItem}
|
>
|
||||||
{#if !navItem.show || (typeof navItem.show === 'function' && navItem.show())}
|
<div
|
||||||
<NavExpandable
|
class="flex items-center py-3 px-4 {NavClasses.item} {(
|
||||||
label={navItem.label}
|
path || $page.url.pathname
|
||||||
handleClick={handleMainGroupClick(navItem.to)}
|
).includes(item.id) && NavClasses.active}"
|
||||||
isGroupActive={(path || $page.url.pathname) === navItem.to}
|
|
||||||
isExpanded={isLessonActive}
|
|
||||||
total={navItem.isLecture ? ($lessons || []).length : 0}
|
|
||||||
>
|
|
||||||
{#if navItem.isLecture}
|
|
||||||
{#each $lessons as item, index}
|
|
||||||
<a
|
|
||||||
class="pl-7 w-[95%] text-[0.80rem] mb-1 text-black dark:text-white {isStudent &&
|
|
||||||
!item.is_unlocked
|
|
||||||
? 'cursor-not-allowed'
|
|
||||||
: ''}"
|
|
||||||
href={isStudent && !item.is_unlocked
|
|
||||||
? $page.url.pathname
|
|
||||||
: getLessonsRoute($course.id, item.id)}
|
|
||||||
on:click={() => {
|
|
||||||
if ($isMobile) {
|
|
||||||
setTimeout(() => (show = false), 500);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
aria-disabled={!item.is_unlocked}
|
|
||||||
>
|
>
|
||||||
<div
|
<TextChip
|
||||||
class="flex items-center py-3 px-4 {NavClasses.item} {(
|
value={getLectureNo(index + 1)}
|
||||||
path || $page.url.pathname
|
className="bg-primary-200 text-primary-600 text-xs mr-2"
|
||||||
).includes(item.id) && NavClasses.active}"
|
size="sm"
|
||||||
>
|
shape="rounded-full"
|
||||||
<TextChip
|
/>
|
||||||
value={getLectureNo(index + 1)}
|
<span>{item.title}</span>
|
||||||
className="bg-primary-200 text-primary-600 text-xs mr-2"
|
<span class="grow" />
|
||||||
size="sm"
|
{#if !item.is_unlocked}
|
||||||
shape="rounded-full"
|
<span class="text-md ml-2" title="This lesson is locked.">
|
||||||
/>
|
<LockedIcon class="carbon-icon dark:text-white" />
|
||||||
<span>{item.title}</span>
|
</span>
|
||||||
<span class="grow" />
|
{:else if item.is_complete}
|
||||||
{#if !item.is_unlocked}
|
<span class="ml-2" title="You have completed this lesson">
|
||||||
<span class="text-md ml-2" title="This lesson is locked.">
|
<CheckmarkFilled class="carbon-icon dark:text-white" />
|
||||||
<LockedIcon class="carbon-icon dark:text-white" />
|
</span>
|
||||||
</span>
|
{/if}
|
||||||
{:else if item.is_complete}
|
</div>
|
||||||
<span class="ml-2" title="You have completed this lesson">
|
</a>
|
||||||
<CheckmarkFilled class="carbon-icon dark:text-white" />
|
{/each}
|
||||||
</span>
|
{/if}
|
||||||
{/if}
|
</NavExpandable>
|
||||||
</div>
|
{/if}
|
||||||
</a>
|
{/each}
|
||||||
{/each}
|
</ul>
|
||||||
{/if}
|
|
||||||
</NavExpandable>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="toggler rounded-full shadow-lg absolute bottom-0">
|
|
||||||
<IconButton
|
|
||||||
value="toggle"
|
|
||||||
onClick={() => (show = !show)}
|
|
||||||
size={$isMobile ? 'large' : 'small'}
|
|
||||||
color="text-black"
|
|
||||||
toolTipProps={$isMobile
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
title: 'Toggle sidebar',
|
|
||||||
hotkeys: ['B'],
|
|
||||||
direction: 'right'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if show}
|
|
||||||
<ChevronLeftIcon size={16} />
|
|
||||||
{:else}
|
|
||||||
<ChevronRightIcon size={16} />
|
|
||||||
{/if}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.root {
|
|
||||||
height: calc(100vh - 48px);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-width: 360px;
|
|
||||||
max-width: 360px;
|
|
||||||
top: 0;
|
|
||||||
border-right: 1px solid var(--border-color);
|
|
||||||
background-color: rgb(250, 251, 252);
|
|
||||||
|
|
||||||
&.hide {
|
|
||||||
min-width: 15px;
|
|
||||||
max-width: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.footer {
|
|
||||||
border-top: 1px solid var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggler {
|
|
||||||
right: -15px;
|
|
||||||
z-index: 10;
|
|
||||||
border: 1px solid var(--border-color);
|
|
||||||
top: 50px;
|
|
||||||
height: fit-content;
|
|
||||||
background: var(--border-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
|
||||||
.root {
|
|
||||||
width: 98%;
|
|
||||||
min-width: unset;
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@@ -1,27 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { Popover } from 'carbon-components-svelte';
|
||||||
|
import Link from 'carbon-icons-svelte/lib/Link.svelte';
|
||||||
import Modal from '$lib/components/Modal/index.svelte';
|
import Modal from '$lib/components/Modal/index.svelte';
|
||||||
|
import copy from 'copy-to-clipboard';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
|
||||||
import { ROLE } from '$lib/utils/constants/roles';
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
import { addGroupMember, fetchGroup } from '$lib/utils/services/courses';
|
import { addGroupMember, fetchGroup } from '$lib/utils/services/courses';
|
||||||
import { invitationModal, resetForm } from './store';
|
import { invitationModal, resetForm } from './store';
|
||||||
import { validateForm } from '$lib/components/Courses/functions';
|
|
||||||
import { course, setCourse } from '$lib/components/Course/store';
|
import { course, setCourse } from '$lib/components/Course/store';
|
||||||
import { MultiSelect, Loading } from 'carbon-components-svelte';
|
import { MultiSelect, Loading } from 'carbon-components-svelte';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import { getOrgTeam } from '$lib/utils/services/org';
|
import { getOrgTeam } from '$lib/utils/services/org';
|
||||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||||
import { sendTeacherCourseWelcome } from './utils';
|
import { sendTeacherCourseWelcome } from './utils';
|
||||||
|
import type { Course } from '$lib/utils/types';
|
||||||
let errors: {
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
students: string;
|
import { getStudentInviteLink } from '$lib/utils/functions/course';
|
||||||
tutors: string;
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
} = {
|
|
||||||
students: '',
|
|
||||||
tutors: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Tutor {
|
interface Tutor {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -33,6 +27,8 @@
|
|||||||
let selectedIds: Array<number> = [];
|
let selectedIds: Array<number> = [];
|
||||||
let selectedTutors: Tutor[] = [];
|
let selectedTutors: Tutor[] = [];
|
||||||
let isLoadingTutors = false;
|
let isLoadingTutors = false;
|
||||||
|
let link = '';
|
||||||
|
let copied = false;
|
||||||
|
|
||||||
function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
|
function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
|
||||||
return value !== null && value !== undefined;
|
return value !== null && value !== undefined;
|
||||||
@@ -41,28 +37,14 @@
|
|||||||
i.length === 0 ? [] : i.map((id) => tutors.find((tutor) => tutor.id === id)).filter(notEmpty);
|
i.length === 0 ? [] : i.map((id) => tutors.find((tutor) => tutor.id === id)).filter(notEmpty);
|
||||||
|
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
const { hasError, fieldErrors, students } = validateForm({
|
if (!selectedTutors.length) {
|
||||||
...$invitationModal,
|
resetForm();
|
||||||
title: 'sfdsd',
|
return;
|
||||||
description: 'dsfdsf',
|
}
|
||||||
tutors: 'sgsg'
|
|
||||||
});
|
|
||||||
|
|
||||||
errors = fieldErrors;
|
|
||||||
if (hasError) return;
|
|
||||||
|
|
||||||
let membersStack = [];
|
let membersStack = [];
|
||||||
console.log(`$course`, $course);
|
console.log(`$course`, $course);
|
||||||
|
|
||||||
// Create groupmemebers from group_id and add teachers and students
|
|
||||||
for (const studentEmail of students) {
|
|
||||||
membersStack.push({
|
|
||||||
email: studentEmail,
|
|
||||||
group_id: $course.group?.id,
|
|
||||||
role_id: ROLE.STUDENT
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const tutor of selectedTutors) {
|
for (const tutor of selectedTutors) {
|
||||||
membersStack.push({
|
membersStack.push({
|
||||||
profile_id: tutor.profileId,
|
profile_id: tutor.profileId,
|
||||||
@@ -128,24 +110,34 @@
|
|||||||
isLoadingTutors = false;
|
isLoadingTutors = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyLink() {
|
||||||
|
copy(link);
|
||||||
|
copied = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
copied = false;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
selectedTutors = formatSelected(selectedIds);
|
selectedTutors = formatSelected(selectedIds);
|
||||||
console.log('selectedTutors', selectedTutors);
|
console.log('selectedTutors', selectedTutors);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: setTutors($currentOrg.id, $invitationModal.open);
|
$: setTutors($currentOrg.id, $invitationModal.open);
|
||||||
|
|
||||||
|
$: link = getStudentInviteLink($course, $currentOrg.siteName);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
onClose={resetForm}
|
onClose={resetForm}
|
||||||
bind:open={$invitationModal.open}
|
bind:open={$invitationModal.open}
|
||||||
width="w-2/5"
|
width="w-4/5 md:w-2/5"
|
||||||
maxWidth="max-w-lg"
|
maxWidth="max-w-lg"
|
||||||
modalHeading="Invite people"
|
modalHeading="Invite people"
|
||||||
>
|
>
|
||||||
<form on:submit|preventDefault={onSubmit}>
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
<div class="mb-4">
|
<div class="mb-8">
|
||||||
<p class="text-base mb-1">Invite Tutors</p>
|
<p class="text-base mb-1 font-semibold">Invite Tutors</p>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
disabled={isLoadingTutors}
|
disabled={isLoadingTutors}
|
||||||
label="Select tutors in organization..."
|
label="Select tutors in organization..."
|
||||||
@@ -169,19 +161,29 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextArea
|
<div class="mb-8 w-[90%] flex justify-between items-center">
|
||||||
label="Invite Students"
|
<div class="w-3/5">
|
||||||
labelClassName="text-base"
|
<p class="text-base mb-1 font-semibold">Invite Students</p>
|
||||||
bind:value={$invitationModal.students}
|
<p class=" text-sm">You can invite students via an invite link</p>
|
||||||
rows="2"
|
</div>
|
||||||
maxRows={3}
|
|
||||||
placeholder="student@gmail.com, john@gmail.com"
|
|
||||||
helperMessage="To invite people, add their emails separated by a comma"
|
|
||||||
errorMessage={errors.students}
|
|
||||||
className="mb-4"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="mt-5 flex items-center">
|
<div class="relative">
|
||||||
|
<PrimaryButton
|
||||||
|
disablePadding={true}
|
||||||
|
className="text-sm py-2 px-3"
|
||||||
|
variant={VARIANTS.OUTLINED}
|
||||||
|
onClick={copyLink}
|
||||||
|
>
|
||||||
|
<Link size={16} class="mr-1" />
|
||||||
|
Copy link
|
||||||
|
</PrimaryButton>
|
||||||
|
<Popover caret open={copied} align="bottom">
|
||||||
|
<div style="padding: 5px">Copied Successfully</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-5 flex items-center flex-row-reverse">
|
||||||
<PrimaryButton className="px-6 py-3" label="Finish" type="submit" />
|
<PrimaryButton className="px-6 py-3" label="Finish" type="submit" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ export const deleteMemberModal = writable({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const invitationModal = writable({
|
export const invitationModal = writable({
|
||||||
open: false,
|
open: false
|
||||||
students: ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resetForm = () =>
|
export const resetForm = () =>
|
||||||
invitationModal.update(() => ({
|
invitationModal.update(() => ({
|
||||||
open: false,
|
open: false
|
||||||
students: ''
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export async function sendTeacherCourseWelcome(params: {
|
|||||||
const accessToken = data.session?.access_token || '';
|
const accessToken = data.session?.access_token || '';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/email/course/welcome', {
|
const response = await fetch('/api/email/course/teacher_welcome', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export const course: Writable<Course> = writable({
|
|||||||
description: '',
|
description: '',
|
||||||
cost: 0,
|
cost: 0,
|
||||||
currency: '',
|
currency: '',
|
||||||
|
status: 'ACTIVE',
|
||||||
is_certificate_downloadable: false,
|
is_certificate_downloadable: false,
|
||||||
certificate_theme: 'professional',
|
certificate_theme: 'professional',
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -20,6 +21,7 @@ export const course: Writable<Course> = writable({
|
|||||||
videoUrl: '',
|
videoUrl: '',
|
||||||
showDiscount: false,
|
showDiscount: false,
|
||||||
discount: 0,
|
discount: 0,
|
||||||
|
paymentLink: '',
|
||||||
reward: {
|
reward: {
|
||||||
show: false,
|
show: false,
|
||||||
description: ''
|
description: ''
|
||||||
|
|||||||
@@ -16,9 +16,7 @@
|
|||||||
export let isFetching = false;
|
export let isFetching = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
const user = $group.people.find(
|
const user = $group.people.find((person) => person.profile_id === $profile.id);
|
||||||
(person) => person.profile_id === $profile.id
|
|
||||||
);
|
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
isStudent = user.role_id === 3;
|
isStudent = user.role_id === 3;
|
||||||
@@ -58,6 +56,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.rightBar.isMobile {
|
.rightBar.isMobile {
|
||||||
margin-left: 10px;
|
/* margin-left: 10px; */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -7,36 +7,12 @@
|
|||||||
export let show = true;
|
export let show = true;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section
|
<section class="root z-10">
|
||||||
class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}"
|
|
||||||
>
|
|
||||||
{#if show}
|
{#if show}
|
||||||
<div class="relative h-full bg-white dark:bg-gray-800">
|
<div class="relative h-full bg-white dark:bg-gray-800">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="toggler rounded-full shadow-lg absolute bottom-0">
|
|
||||||
<IconButton
|
|
||||||
value="toggle"
|
|
||||||
onClick={() => (show = !show)}
|
|
||||||
size={$isMobile ? 'large' : 'small'}
|
|
||||||
color="text-black"
|
|
||||||
toolTipProps={$isMobile
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
title: 'Toggle editor',
|
|
||||||
hotkeys: ['B'],
|
|
||||||
direction: 'right',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if show}
|
|
||||||
<ChevronLeftIcon size={16} />
|
|
||||||
{:else}
|
|
||||||
<ChevronRightIcon size={16} />
|
|
||||||
{/if}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@@ -50,11 +26,6 @@
|
|||||||
left: 0;
|
left: 0;
|
||||||
border-right: 1px solid var(--border-color);
|
border-right: 1px solid var(--border-color);
|
||||||
background-color: rgb(250, 251, 252);
|
background-color: rgb(250, 251, 252);
|
||||||
|
|
||||||
&.hide {
|
|
||||||
min-width: 15px;
|
|
||||||
max-width: 15px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggler {
|
.toggler {
|
||||||
@@ -67,10 +38,5 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.root {
|
|
||||||
width: 98%;
|
|
||||||
min-width: unset;
|
|
||||||
max-width: unset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
import isEmpty from 'lodash/isEmpty';
|
import isEmpty from 'lodash/isEmpty';
|
||||||
import { Toggle } from 'carbon-components-svelte';
|
import { Toggle, Select, SelectItem } from 'carbon-components-svelte';
|
||||||
|
|
||||||
import TextField from '$lib/components/Form/TextField.svelte';
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||||
|
import { isCourseFree } from '$lib/utils/functions/course';
|
||||||
|
import type { Course } from '$lib/utils/types';
|
||||||
|
|
||||||
export let course = {};
|
export let course: Course;
|
||||||
|
|
||||||
let discount = 0;
|
let discount = 0;
|
||||||
let showDiscount;
|
let paymentLink = '';
|
||||||
let giftToggled;
|
let showDiscount = false;
|
||||||
|
let giftToggled = false;
|
||||||
|
|
||||||
let hasBeenSet = false;
|
let hasBeenSet = false;
|
||||||
|
|
||||||
function setDefaults(course) {
|
function setDefaults(course: Course) {
|
||||||
console.log('\n\nsetDefaults course', course);
|
console.log('\n\nsetDefaults course', course);
|
||||||
if (isEmpty(course) || hasBeenSet) return;
|
if (isEmpty(course) || hasBeenSet) return;
|
||||||
hasBeenSet = true;
|
hasBeenSet = true;
|
||||||
|
|
||||||
|
paymentLink = get(course, 'metadata.paymentLink', '');
|
||||||
discount = get(course, 'metadata.discount', 0);
|
discount = get(course, 'metadata.discount', 0);
|
||||||
showDiscount = get(course, 'metadata.showDiscount', false);
|
showDiscount = get(course, 'metadata.showDiscount', false);
|
||||||
giftToggled = get(course, 'metadata.reward.show', false);
|
giftToggled = get(course, 'metadata.reward.show', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setter(value, setterKey) {
|
function setter(value: string | number | boolean, setterKey: string) {
|
||||||
console.log('\n\nsetter course', value);
|
|
||||||
if (typeof value !== 'boolean' && !value) return;
|
if (typeof value !== 'boolean' && !value) return;
|
||||||
|
|
||||||
const _course = cloneDeep(course);
|
const _course = cloneDeep(course);
|
||||||
@@ -34,66 +38,76 @@
|
|||||||
course = _course;
|
course = _course;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChange(html) {
|
function handleChange(html: string) {
|
||||||
const _course = cloneDeep(course);
|
const _course = cloneDeep(course);
|
||||||
set(_course, 'metadata.reward.description', html);
|
set(_course, 'metadata.reward.description', html);
|
||||||
course = _course;
|
course = _course;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: setter(showDiscount, 'metadata.showDiscount');
|
$: setter(showDiscount, 'metadata.showDiscount');
|
||||||
|
$: setter(paymentLink, 'metadata.paymentLink');
|
||||||
$: setter(discount, 'metadata.discount');
|
$: setter(discount, 'metadata.discount');
|
||||||
$: setter(giftToggled, 'metadata.reward.show');
|
$: setter(giftToggled, 'metadata.reward.show');
|
||||||
|
|
||||||
$: setDefaults(course);
|
$: setDefaults(course);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TextField
|
{#if typeof course !== 'undefined'}
|
||||||
className="mt-5"
|
<Select labelText="Currency" bind:selected={course.currency}>
|
||||||
labelClassName="font-bold"
|
<SelectItem value="NGN" />
|
||||||
label="Currency"
|
<SelectItem value="USD" />
|
||||||
value={course.currency}
|
</Select>
|
||||||
isDisabled={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextField
|
|
||||||
className="mt-5"
|
|
||||||
labelClassName="font-bold"
|
|
||||||
label="Cost"
|
|
||||||
type="number"
|
|
||||||
bind:value={course.cost}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="mt-5">
|
|
||||||
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
|
|
||||||
<Toggle labelText="Discount" bind:toggled={showDiscount}>
|
|
||||||
<span slot="labelA">No</span>
|
|
||||||
<span slot="labelB">Yes</span>
|
|
||||||
</Toggle>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if showDiscount}
|
|
||||||
<TextField
|
<TextField
|
||||||
className="mt-5"
|
className="mt-5"
|
||||||
labelClassName="font-bold"
|
labelClassName="font-bold"
|
||||||
label="Discout Percent"
|
label="Cost"
|
||||||
type="number"
|
type="number"
|
||||||
bind:value={discount}
|
bind:value={course.cost}
|
||||||
helperMessage="In Percentage %"
|
|
||||||
/>
|
/>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="mt-5">
|
{#if !isCourseFree(course.cost || 0)}
|
||||||
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
|
<TextField
|
||||||
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}>
|
className="mt-5"
|
||||||
<span slot="labelA">No</span>
|
labelClassName="font-bold"
|
||||||
<span slot="labelB">Yes</span>
|
label="Payment Link"
|
||||||
</Toggle>
|
helperMessage="Stripe, Lemon Squeezy or any payment link"
|
||||||
</div>
|
bind:value={paymentLink}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if giftToggled}
|
<div class="mt-5">
|
||||||
<p class="dark:text-white font-bold mt-5">About Gift</p>
|
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
|
||||||
|
<Toggle labelText="Discount" bind:toggled={showDiscount}>
|
||||||
<div class="h-2/5">
|
<span slot="labelA">No</span>
|
||||||
<TextEditor value={get(course, 'metadata.reward.description')} onChange={handleChange} />
|
<span slot="labelB">Yes</span>
|
||||||
|
</Toggle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if showDiscount}
|
||||||
|
<TextField
|
||||||
|
className="mt-5"
|
||||||
|
labelClassName="font-bold"
|
||||||
|
label="Discout Percent"
|
||||||
|
type="number"
|
||||||
|
bind:value={discount}
|
||||||
|
helperMessage="In Percentage %"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="mt-5">
|
||||||
|
<!-- <label for="text-field" class="m-0 font-bold mb-2">Discount</label> -->
|
||||||
|
<Toggle labelText="Gift on Completion" bind:toggled={giftToggled}>
|
||||||
|
<span slot="labelA">No</span>
|
||||||
|
<span slot="labelB">Yes</span>
|
||||||
|
</Toggle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if giftToggled}
|
||||||
|
<p class="dark:text-white font-bold mt-5">About Gift</p>
|
||||||
|
|
||||||
|
<div class="h-2/5">
|
||||||
|
<TextEditor value={get(course, 'metadata.reward.description', '')} onChange={handleChange} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -18,13 +18,16 @@
|
|||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import { updateCourse } from '$lib/utils/services/courses';
|
import { updateCourse } from '$lib/utils/services/courses';
|
||||||
import generateSlug from '$lib/utils/functions/generateSlug';
|
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||||
|
import ChevronLeftIcon from 'carbon-icons-svelte/lib/ChevronLeft.svelte';
|
||||||
|
|
||||||
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
|
|
||||||
export let course;
|
export let course;
|
||||||
export let courseId;
|
export let courseId;
|
||||||
|
|
||||||
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
|
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
let show = false;
|
||||||
const sections = [
|
const sections = [
|
||||||
{
|
{
|
||||||
key: 1,
|
key: 1,
|
||||||
@@ -115,69 +118,112 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Container show={true}>
|
<aside
|
||||||
<!-- Publish Header Section -->
|
class={`${
|
||||||
{#if !selectedSection}
|
show ? '-translate-x-[100%] fixed z-[9999]' : 'translate-x-0 fixed z-[9999]'
|
||||||
<div class="flex justify-between items-center px-2 w-full">
|
} left-0 transition w-[280px] min-w-[280px] bg-gray-100 dark:bg-gray-800 h-full border border-l-0 border-t-0 border-b-0 border-r-1`}
|
||||||
<CloseButton onClick={handleClose} />
|
>
|
||||||
<div class="flex items-center">
|
<div class="toggler rounded-full shadow-lg absolute">
|
||||||
<PrimaryButton
|
<IconButton
|
||||||
label="Save"
|
value="toggle"
|
||||||
type="button"
|
onClick={() => (show = !show)}
|
||||||
className="mr-1"
|
size={$isMobile ? 'large' : 'small'}
|
||||||
variant={VARIANTS.OUTLINED}
|
color="text-black"
|
||||||
onClick={handleSave}
|
toolTipProps={$isMobile
|
||||||
isLoading={loading}
|
? {}
|
||||||
/>
|
: {
|
||||||
<PrimaryButton label="Publish" type="button" onClick={handlePublish} isDisabled={loading} />
|
title: 'Toggle editor',
|
||||||
<IconButton onClick={handlePreview} disabled={loading || !course.slug}>
|
direction: 'right'
|
||||||
<ArrowUpRightIcon size={24} class="carbon-icon dark:text-white" title="Preview" />
|
}}
|
||||||
</IconButton>
|
>
|
||||||
</div>
|
{#if show}
|
||||||
</div>
|
<ChevronRightIcon size={16} />
|
||||||
<div class="flex justify-between items-center px-2 w-full mb-2">
|
{:else}
|
||||||
<h3 class="dark:text-white">Page Builder</h3>
|
<ChevronLeftIcon size={16} />
|
||||||
</div>
|
|
||||||
{#each sections as section}
|
|
||||||
<button
|
|
||||||
class="w-full flex items-center justify-between px-2 py-3 border border-b-0 border-gray-300"
|
|
||||||
on:click={handleSectionSelect(section.key)}
|
|
||||||
>
|
|
||||||
<p class="dark:text-white mr-2">{section.title} section</p>
|
|
||||||
<ChevronRightIcon size={24} class="carbon-class" />
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
{:else}
|
|
||||||
<!-- Title -->
|
|
||||||
<div class="flex items-center {borderBottomGrey} w-full">
|
|
||||||
<IconButton onClick={handleClose}>
|
|
||||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" title="Go back" />
|
|
||||||
</IconButton>
|
|
||||||
<h3 class="dark:text-white">{selectedSection.title}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="title-content p-2 overflow-y-auto">
|
|
||||||
{#if selectedSection.key === 1}
|
|
||||||
<HeaderForm bind:course />
|
|
||||||
{:else if selectedSection.key === 2}
|
|
||||||
<RequirementForm bind:course />
|
|
||||||
{:else if selectedSection.key === 3}
|
|
||||||
<DescriptionForm bind:course />
|
|
||||||
{:else if selectedSection.key === 4}
|
|
||||||
<GoalsForm bind:course />
|
|
||||||
{:else if selectedSection.key === 5}
|
|
||||||
<ReviewsForm bind:course />
|
|
||||||
{:else if selectedSection.key === 6}
|
|
||||||
<InstructorForm bind:course />
|
|
||||||
{:else if selectedSection.key === 7}
|
|
||||||
<PricingForm bind:course />
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</IconButton>
|
||||||
{/if}
|
</div>
|
||||||
</Container>
|
<div class="h-full flex flex-col">
|
||||||
|
{#if !selectedSection}
|
||||||
|
<div class="flex justify-between items-center px-2 w-full">
|
||||||
|
<CloseButton onClick={handleClose} />
|
||||||
|
<div class="flex items-center">
|
||||||
|
<PrimaryButton
|
||||||
|
label="Save"
|
||||||
|
type="button"
|
||||||
|
className="mr-1"
|
||||||
|
variant={VARIANTS.OUTLINED}
|
||||||
|
onClick={handleSave}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
<PrimaryButton
|
||||||
|
label="Publish"
|
||||||
|
type="button"
|
||||||
|
onClick={handlePublish}
|
||||||
|
isDisabled={loading}
|
||||||
|
/>
|
||||||
|
<IconButton onClick={handlePreview} disabled={loading || !course.slug}>
|
||||||
|
<ArrowUpRightIcon size={24} class="carbon-icon dark:text-white" title="Preview" />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between items-center px-2 w-full mb-2">
|
||||||
|
<h3 class="dark:text-white">Page Builder</h3>
|
||||||
|
</div>
|
||||||
|
{#each sections as section}
|
||||||
|
<button
|
||||||
|
class="w-full flex items-center justify-between px-2 py-3 border border-b-0 border-gray-300"
|
||||||
|
on:click={handleSectionSelect(section.key)}
|
||||||
|
>
|
||||||
|
<p class="dark:text-white mr-2">{section.title} section</p>
|
||||||
|
<ChevronRightIcon size={24} class="carbon-class" />
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
{:else}
|
||||||
|
<!-- Title -->
|
||||||
|
<div class="flex items-center {borderBottomGrey} w-full">
|
||||||
|
<IconButton onClick={handleClose}>
|
||||||
|
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" title="Go back" />
|
||||||
|
</IconButton>
|
||||||
|
<h3 class="dark:text-white">{selectedSection.title}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="title-content p-2 overflow-y-auto">
|
||||||
|
{#if selectedSection.key === 1}
|
||||||
|
<HeaderForm bind:course />
|
||||||
|
{:else if selectedSection.key === 2}
|
||||||
|
<RequirementForm bind:course />
|
||||||
|
{:else if selectedSection.key === 3}
|
||||||
|
<DescriptionForm bind:course />
|
||||||
|
{:else if selectedSection.key === 4}
|
||||||
|
<GoalsForm bind:course />
|
||||||
|
{:else if selectedSection.key === 5}
|
||||||
|
<ReviewsForm bind:course />
|
||||||
|
{:else if selectedSection.key === 6}
|
||||||
|
<InstructorForm bind:course />
|
||||||
|
{:else if selectedSection.key === 7}
|
||||||
|
<PricingForm bind:course />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.title-content {
|
.title-content {
|
||||||
height: 90%;
|
height: 90%;
|
||||||
}
|
}
|
||||||
|
.toggler {
|
||||||
|
right: -15px;
|
||||||
|
z-index: 10;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
top: 50px;
|
||||||
|
height: fit-content;
|
||||||
|
background: var(--border-color);
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 767px) {
|
||||||
|
.toggler {
|
||||||
|
right: -25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
|
import Modal from '$lib/components/Modal/index.svelte';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { coursePaymentValidation } from '$lib/utils/functions/validator';
|
||||||
|
import { Link } from 'carbon-components-svelte';
|
||||||
|
import { sendTeacherStudentJoinRequest, sendStudentRequestReceipt } from './utils';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
|
||||||
|
export let open = false;
|
||||||
|
export let paymentLink = '';
|
||||||
|
export let teacherEmail = '';
|
||||||
|
export let courseName = '';
|
||||||
|
|
||||||
|
let fields = {
|
||||||
|
fullname: '',
|
||||||
|
email: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
let errors = {
|
||||||
|
fullname: '',
|
||||||
|
email: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const STEPS = {
|
||||||
|
STEP_1: 'ENTER_DETAILS',
|
||||||
|
STEP_2: 'GO_TO_PAYMENT'
|
||||||
|
};
|
||||||
|
|
||||||
|
let step = STEPS.STEP_1;
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
errors = coursePaymentValidation(fields);
|
||||||
|
console.log('coursePayment errors', errors);
|
||||||
|
|
||||||
|
if (Object.keys(errors).length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentLink) {
|
||||||
|
step = STEPS.STEP_2;
|
||||||
|
|
||||||
|
// Send email to first added teacher of student wanting to join
|
||||||
|
sendTeacherStudentJoinRequest({
|
||||||
|
to: teacherEmail,
|
||||||
|
courseName,
|
||||||
|
studentEmail: fields.email,
|
||||||
|
studentFullname: fields.fullname
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClickPaymentLink() {
|
||||||
|
// Send email to student to send proof of payment to teacher
|
||||||
|
sendStudentRequestReceipt({
|
||||||
|
to: fields.email,
|
||||||
|
courseName,
|
||||||
|
teacherEmail,
|
||||||
|
studentFullname: fields.fullname,
|
||||||
|
orgName: $currentOrg.name
|
||||||
|
});
|
||||||
|
|
||||||
|
fields.fullname = '';
|
||||||
|
fields.email = '';
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal onClose={() => (open = false)} bind:open width="w-96" modalHeading="Process course payment">
|
||||||
|
{#if step === STEPS.STEP_1}
|
||||||
|
<form on:submit|preventDefault={onSubmit}>
|
||||||
|
<TextField
|
||||||
|
label="Your Fullname"
|
||||||
|
bind:value={fields.fullname}
|
||||||
|
autoFocus={true}
|
||||||
|
placeholder="John Doe"
|
||||||
|
className="mb-4"
|
||||||
|
isRequired={true}
|
||||||
|
autoComplete={false}
|
||||||
|
errorMessage={errors.fullname}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
label="Your Fullname"
|
||||||
|
bind:value={fields.email}
|
||||||
|
placeholder="johndoe@email.com"
|
||||||
|
className="mb-4"
|
||||||
|
isRequired={true}
|
||||||
|
autoComplete={false}
|
||||||
|
errorMessage={errors.email}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-5 flex items-center flex-row-reverse">
|
||||||
|
<PrimaryButton
|
||||||
|
className="px-6 py-3"
|
||||||
|
label={paymentLink ? 'Next' : 'Finish'}
|
||||||
|
type="submit"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{:else if step === STEPS.STEP_2}
|
||||||
|
<p>
|
||||||
|
You will now be taken to a payment page, once you've paid send a proof of payment to the
|
||||||
|
course admin
|
||||||
|
</p>
|
||||||
|
<div class="mt-5 flex items-center flex-row-reverse">
|
||||||
|
<Link href={paymentLink} target="_blank" on:click={onClickPaymentLink}>Go to payment</Link>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Modal>
|
||||||
@@ -1,20 +1,24 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
|
||||||
import type { Course } from '$lib/utils/types';
|
|
||||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
|
||||||
import get from 'lodash/get';
|
import get from 'lodash/get';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||||
|
import { isCourseFree } from '$lib/utils/functions/course';
|
||||||
|
import { getStudentInviteLink } from '$lib/utils/functions/course';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import PaymentModal from './PaymentModal.svelte';
|
||||||
|
import type { Course } from '$lib/utils/types';
|
||||||
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
|
|
||||||
export let className = '';
|
export let className = '';
|
||||||
export let courseData: Course = {
|
export let editMode = false;
|
||||||
id: '',
|
export let courseData: Course;
|
||||||
title: '',
|
export let startCoursePayment = false;
|
||||||
description: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
let calculatedCost = 0;
|
let calculatedCost = 0;
|
||||||
let discount = 0;
|
let discount = 0;
|
||||||
let formatter: Intl.NumberFormat | undefined;
|
let formatter: Intl.NumberFormat | undefined;
|
||||||
|
let isFree = false;
|
||||||
|
|
||||||
function calcDisc(percent: number, cost: number) {
|
function calcDisc(percent: number, cost: number) {
|
||||||
if (!percent) return cost;
|
if (!percent) return cost;
|
||||||
@@ -23,27 +27,51 @@
|
|||||||
return Math.round(discountedPrice);
|
return Math.round(discountedPrice);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToCart() {}
|
function handleJoinCourse() {
|
||||||
|
if (editMode) return;
|
||||||
|
|
||||||
function buyNow() {}
|
if (isFree) {
|
||||||
|
const link = getStudentInviteLink(courseData, $currentOrg.siteName);
|
||||||
|
goto(link);
|
||||||
|
} else {
|
||||||
|
startCoursePayment = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setFormatter(currency: string | undefined) {
|
function setFormatter(currency: string | undefined) {
|
||||||
if (!currency) return;
|
if (!currency) return;
|
||||||
formatter = getCurrencyFormatter(currency);
|
formatter = getCurrencyFormatter(currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTeacherEmail(group: Course['group']) {
|
||||||
|
const firstTutor = group?.members?.find((m) => m.role_id === ROLE.TUTOR);
|
||||||
|
|
||||||
|
return firstTutor?.profile?.email || '';
|
||||||
|
}
|
||||||
|
|
||||||
$: setFormatter(courseData.currency);
|
$: setFormatter(courseData.currency);
|
||||||
$: discount = get(courseData, 'metadata.discount', 0);
|
$: discount = get(courseData, 'metadata.discount', 0);
|
||||||
$: calculatedCost = calcDisc(discount, courseData.cost || 0);
|
$: calculatedCost = calcDisc(discount, courseData.cost || 0);
|
||||||
|
$: isFree = isCourseFree(calculatedCost);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<PaymentModal
|
||||||
|
bind:open={startCoursePayment}
|
||||||
|
paymentLink={get(courseData, 'metadata.paymentLink', '')}
|
||||||
|
courseName={courseData.title}
|
||||||
|
teacherEmail={getTeacherEmail(courseData.group)}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Pricing Details -->
|
<!-- Pricing Details -->
|
||||||
<aside class="{className} price-container md:sticky md:top-0 md:shadow-2xl md:rounded-lg m-h-fit">
|
<aside class="{className} price-container lg:sticky lg:top-10 lg:shadow-2xl lg:rounded-lg m-h-fit">
|
||||||
<div class="p-10">
|
<div class="p-10">
|
||||||
<!-- Pricing -->
|
<!-- Pricing -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<p class="dark:text-white font-medium text-lg">
|
<p class="dark:text-white font-medium text-lg">
|
||||||
{formatter?.format(calculatedCost) || calculatedCost}
|
{formatter?.format(calculatedCost) || calculatedCost}
|
||||||
|
{#if isFree}
|
||||||
|
<span class="text-sm">(Free)</span>
|
||||||
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
{#if courseData?.metadata?.showDiscount}
|
{#if courseData?.metadata?.showDiscount}
|
||||||
<p class="dark:text-white font-light text-sm text-gray-500">
|
<p class="dark:text-white font-light text-sm text-gray-500">
|
||||||
@@ -58,15 +86,9 @@
|
|||||||
<!-- Call To Action Buttons -->
|
<!-- Call To Action Buttons -->
|
||||||
<div class="flex flex-col w-full items-center">
|
<div class="flex flex-col w-full items-center">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Add to Cart"
|
label={isFree ? 'Join Course' : 'Buy Now'}
|
||||||
className="w-full sm:w-full py-3 mb-3"
|
className="w-full sm:w-full py-3 mb-3"
|
||||||
onClick={addToCart}
|
onClick={handleJoinCourse}
|
||||||
/>
|
|
||||||
<PrimaryButton
|
|
||||||
label="Buy Now"
|
|
||||||
className="w-full sm:w-full py-3 mb-3"
|
|
||||||
variant={VARIANTS.OUTLINED}
|
|
||||||
onClick={addToCart}
|
|
||||||
/>
|
/>
|
||||||
<p class="dark:text-white font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p>
|
<p class="dark:text-white font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,9 +142,9 @@
|
|||||||
height: fit-content;
|
height: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media screen and (min-width: 768px) and (max-width: 1024px) {
|
||||||
.price-container {
|
.price-container {
|
||||||
width: 100%;
|
width: 405px;
|
||||||
min-width: unset;
|
min-width: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
export async function sendTeacherStudentJoinRequest(params: {
|
||||||
|
to: string;
|
||||||
|
courseName: string;
|
||||||
|
studentEmail: string;
|
||||||
|
studentFullname: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await supabase.auth.getSession();
|
||||||
|
const accessToken = data.session?.access_token || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/email/course/teacher_student_buycourse', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: accessToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
console.log('Sent invite email', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending invite email', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendStudentRequestReceipt(params: {
|
||||||
|
to: string;
|
||||||
|
courseName: string;
|
||||||
|
orgName: string;
|
||||||
|
studentFullname: string;
|
||||||
|
teacherEmail: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await supabase.auth.getSession();
|
||||||
|
const accessToken = data.session?.access_token || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/email/course/student_prove_payment', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: accessToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
console.log('Sent invite email', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending invite email', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,11 +25,8 @@
|
|||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
export let courseData: Course = {
|
export let editMode = false;
|
||||||
id: '',
|
export let courseData: Course;
|
||||||
title: '',
|
|
||||||
description: ''
|
|
||||||
};
|
|
||||||
const ratingsImg = [
|
const ratingsImg = [
|
||||||
'/images/rating-1.svg',
|
'/images/rating-1.svg',
|
||||||
'/images/rating-2.svg',
|
'/images/rating-2.svg',
|
||||||
@@ -43,6 +40,7 @@
|
|||||||
let player: any;
|
let player: any;
|
||||||
let averageRating = 0;
|
let averageRating = 0;
|
||||||
let totalRatings = 0;
|
let totalRatings = 0;
|
||||||
|
let startCoursePayment = false;
|
||||||
|
|
||||||
// initialize the expandDescription array with 'false' values for each review.
|
// initialize the expandDescription array with 'false' values for each review.
|
||||||
let expandDescription = Array(reviews.length).fill(false);
|
let expandDescription = Array(reviews.length).fill(false);
|
||||||
@@ -61,16 +59,6 @@
|
|||||||
activeNav = window.location.hash;
|
activeNav = window.location.hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToCart() {}
|
|
||||||
|
|
||||||
function buyNow() {
|
|
||||||
// if (courseData.cost) {
|
|
||||||
// paystackApi.tra;
|
|
||||||
// } else {
|
|
||||||
// // Course is free
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
function initPlyr(_player: any, _video: string | undefined) {
|
function initPlyr(_player: any, _video: string | undefined) {
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
|
|
||||||
@@ -119,7 +107,13 @@
|
|||||||
<p class="dark:text-white author my-3 text-sm">
|
<p class="dark:text-white author my-3 text-sm">
|
||||||
{get(courseData, 'metadata.instructor.name', '')}
|
{get(courseData, 'metadata.instructor.name', '')}
|
||||||
</p>
|
</p>
|
||||||
<PrimaryButton label="Start Course" className="px-6 py-5 mt-6 sm:w-fit" />
|
<PrimaryButton
|
||||||
|
label="Start Course"
|
||||||
|
className="px-6 py-5 mt-6 sm:w-fit"
|
||||||
|
onClick={() => {
|
||||||
|
startCoursePayment = true;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Banner Image getEmbedId(videoUrl) -->
|
<!-- Banner Image getEmbedId(videoUrl) -->
|
||||||
@@ -164,14 +158,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<PricingSection className="md:hidden" {courseData} />
|
<PricingSection className="md:hidden" {courseData} {editMode} bind:startCoursePayment />
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<div class="bg-white dark:bg-gray-800 w-full">
|
<div class="bg-white dark:bg-gray-800 w-full">
|
||||||
<div class="py-8 lg:w-10/12 w-full m-2 lg:m-auto flex justify-between">
|
<div
|
||||||
|
class="py-8 lg:w-11/12 w-full m-2 lg:m-auto flex flex-col-reverse lg:flex-row items-center lg:items-start justify-between max-w-[1200px]"
|
||||||
|
>
|
||||||
<!-- Course Details -->
|
<!-- Course Details -->
|
||||||
<div class="course-content w-full p-3 md:w-9/12 md:mr-14">
|
<div class="course-content w-full p-3 lg:w-10/12 lg:mr-14">
|
||||||
<!-- Navigation -->
|
<!-- Navigation -->
|
||||||
<nav class="flex items-center border-b border-gray-300 pb-3">
|
<nav class="flex items-center border-b border-gray-300 py-3 sticky top-11 bg-white">
|
||||||
{#each NAV_ITEMS as navItem}
|
{#each NAV_ITEMS as navItem}
|
||||||
<a
|
<a
|
||||||
href="{$page.url.pathname}{navItem.key}"
|
href="{$page.url.pathname}{navItem.key}"
|
||||||
@@ -416,7 +412,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pricing Details -->
|
<!-- Pricing Details -->
|
||||||
<PricingSection className="hidden md:block" {courseData} />
|
<PricingSection {courseData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -464,7 +460,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.course-content {
|
.course-content {
|
||||||
max-width: 608px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||||
import PasswordEye from '../Icons/PasswordEye.svelte';
|
import PasswordEye from '../Icons/PasswordEye.svelte';
|
||||||
|
|
||||||
export let label = '';
|
export let label = '';
|
||||||
export let placeholder = '';
|
export let placeholder = '';
|
||||||
export let value = '';
|
export let value: string | number;
|
||||||
export let name = '';
|
export let name = '';
|
||||||
export let onKeyDown = () => {};
|
export let onKeyDown = () => {};
|
||||||
export let className = '';
|
export let className = '';
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
export let logo;
|
export let logo;
|
||||||
export let orgName;
|
export let orgName;
|
||||||
export let isOrgSite = false;
|
export let isOrgSite = false;
|
||||||
export let backgroundColor = 'bg-blue';
|
export let backgroundColor = 'bg-white';
|
||||||
|
|
||||||
let navClass = '';
|
let navClass = '';
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||||
|
|
||||||
let selected = 0;
|
let selected = 0;
|
||||||
let query = new URLSearchParams($page.url.search);
|
let query = new URLSearchParams($page.url.search);
|
||||||
@@ -18,19 +19,22 @@
|
|||||||
key: 0,
|
key: 0,
|
||||||
label: 'Profile',
|
label: 'Profile',
|
||||||
tabKey: '',
|
tabKey: '',
|
||||||
href: $page.url.pathname
|
href: $page.url.pathname,
|
||||||
|
disabled: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 1,
|
key: 1,
|
||||||
label: 'Organization',
|
label: 'Organization',
|
||||||
tabKey: 'org',
|
tabKey: 'org',
|
||||||
href: `${$page.url.pathname}?tab=org`
|
href: `${$page.url.pathname}?tab=org`,
|
||||||
|
disabled: !$isOrgAdmin
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 2,
|
key: 2,
|
||||||
label: 'LandingPage',
|
label: 'LandingPage',
|
||||||
tabKey: 'landingpage',
|
tabKey: 'landingpage',
|
||||||
href: `${$page.url.pathname}?tab=landingpage`
|
href: `${$page.url.pathname}?tab=landingpage`,
|
||||||
|
disabled: !$isOrgAdmin
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -59,7 +63,7 @@
|
|||||||
|
|
||||||
<Tabs autoWidth bind:selected>
|
<Tabs autoWidth bind:selected>
|
||||||
{#each tabs as tab}
|
{#each tabs as tab}
|
||||||
<Tab label={tab.label} href={tab.href} />
|
<Tab label={tab.label} href={tab.href} disabled={tab.disabled} />
|
||||||
{/each}
|
{/each}
|
||||||
<!-- <Tab label="Account" /> -->
|
<!-- <Tab label="Account" /> -->
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
|
|||||||
@@ -82,7 +82,8 @@ export let landingPageSettings: Writable<OrgLandingPageJson> = writable({
|
|||||||
title: 'Our Story',
|
title: 'Our Story',
|
||||||
content:
|
content:
|
||||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.',
|
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Repudiandae, quaerat cum. Ullam similique quae dicta ipsum atque quam fugit iusto eligendi magni voluptatum aut, exercitationem deserunt vitae iste rem sunt!\nLorem ipsum, dolor sit amet consectetur adipisicing elit. Maiores obcaecati veritatis tenetur in, nihil repellat quasi cumque sed molestias id reiciendis harum doloremque minus fugit quia cum dolores quas pariatur.',
|
||||||
imageUrl: '',
|
imageUrl:
|
||||||
|
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/landingpage/riverside.jpeg',
|
||||||
show: true
|
show: true
|
||||||
},
|
},
|
||||||
courses: {
|
courses: {
|
||||||
|
|||||||
@@ -19,12 +19,12 @@
|
|||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
label: 'Home'
|
label: 'Dashboard'
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/quiz',
|
|
||||||
label: 'Quizzes'
|
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// path: '/quiz',
|
||||||
|
// label: 'Quizzes'
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
path: '/courses',
|
path: '/courses',
|
||||||
label: 'Courses'
|
label: 'Courses'
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export const VARIANTS = {
|
|||||||
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
|
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
|
||||||
OUTLINED: 'OUTLINED',
|
OUTLINED: 'OUTLINED',
|
||||||
NONE: 'NONE',
|
NONE: 'NONE',
|
||||||
|
LINK: 'LINK',
|
||||||
TEXT: 'TEXT',
|
TEXT: 'TEXT',
|
||||||
TEXT_DANGER: 'TEXT_DANGER'
|
TEXT_DANGER: 'TEXT_DANGER'
|
||||||
};
|
};
|
||||||
@@ -25,6 +26,7 @@ export const VARIANTS_CLASS = {
|
|||||||
[VARIANTS.NONE]:
|
[VARIANTS.NONE]:
|
||||||
'border-none hover:border-black-300 hover:bg-gray-200 dark:hover:bg-gray-500 text-black',
|
'border-none hover:border-black-300 hover:bg-gray-200 dark:hover:bg-gray-500 text-black',
|
||||||
[VARIANTS.TEXT]: 'text-black hover:underline',
|
[VARIANTS.TEXT]: 'text-black hover:underline',
|
||||||
|
[VARIANTS.LINK]: 'text-primary-500 dark:text-primary-500 hover:underline',
|
||||||
[VARIANTS.TEXT_DANGER]:
|
[VARIANTS.TEXT_DANGER]:
|
||||||
'text-red-500 hover:border border-t-0 border-l-0 border-r-0 border-red-300'
|
'text-red-500 hover:border border-t-0 border-l-0 border-r-0 border-red-300'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,12 +13,12 @@
|
|||||||
export let isDisabled = false;
|
export let isDisabled = false;
|
||||||
export let isLoading = false;
|
export let isLoading = false;
|
||||||
|
|
||||||
$: isDisabled = isLoading ? true : false;
|
$: isDisabled = isLoading ? true : isDisabled;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="{isDisabled
|
class="{isDisabled
|
||||||
? 'bg-opacity-25 hover:bg-opacity-25 cursor-not-allowed'
|
? 'opacity-25 cursor-not-allowed'
|
||||||
: 'cursor-pointer'} flex items-center h-auto {VARIANTS_CLASS[
|
: 'cursor-pointer'} flex items-center h-auto {VARIANTS_CLASS[
|
||||||
isLoading ? VARIANTS.OUTLINED : variant
|
isLoading ? VARIANTS.OUTLINED : variant
|
||||||
]} {!disablePadding &&
|
]} {!disablePadding &&
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-full flex flex-col">
|
<div class="w-full flex flex-col">
|
||||||
<div class="flex items-center border-b-2 w-full">
|
<div class="flex items-center border-b-2 w-full overflow-x-auto">
|
||||||
{#each tabs as tab}
|
{#each tabs as tab}
|
||||||
{#if !tab.icon && !tab.badgeValue}
|
{#if !tab.icon && !tab.badgeValue}
|
||||||
<button
|
<button
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
: 'dark:bg-gray-500 dark:text-white'} dark:bg-transparent font-semibold focus:outline-none w-24 text-left py-3 mr-10"
|
: 'dark:bg-gray-500 dark:text-white'} dark:bg-transparent font-semibold focus:outline-none w-24 text-left py-3 mr-10"
|
||||||
on:click={onChange(tab.value)}
|
on:click={onChange(tab.value)}
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-4 gap-3 items-center">
|
<div class="grid grid-cols-5 md:grid-cols-4 gap-5 md:gap-3 items-center">
|
||||||
{#if tab.icon}
|
{#if tab.icon}
|
||||||
<svelte:component this={tab.icon} />
|
<svelte:component this={tab.icon} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
export let id = '';
|
export let id = '';
|
||||||
export let value: string;
|
export let value: string;
|
||||||
export let onChange = (html = '') => {};
|
export let onChange = (html: string) => {};
|
||||||
export let height = 300;
|
export let height = 300;
|
||||||
export let placeholder = '';
|
export let placeholder = '';
|
||||||
export let editorWindowRef;
|
export let editorWindowRef = null;
|
||||||
|
|
||||||
const apiKey = PUBLIC_TINYMCE_API_KEY;
|
const apiKey = PUBLIC_TINYMCE_API_KEY;
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
<input
|
<input
|
||||||
style="display:none"
|
style="display:none"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".jpg, .jpeg, .png"
|
accept=".jpg, .jpeg, .png, .webp"
|
||||||
on:change={(e) => onFileSelected(e)}
|
on:change={(e) => onFileSelected(e)}
|
||||||
bind:this={fileinput}
|
bind:this={fileinput}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export const ROUTE = {
|
|||||||
COURSE: '/course',
|
COURSE: '/course',
|
||||||
LOGIN: '/login',
|
LOGIN: '/login',
|
||||||
SIGN_UP: '/signup',
|
SIGN_UP: '/signup',
|
||||||
INVITE_SIGNUP: '/invite',
|
INVITE_TEACHER: '/invite/t',
|
||||||
|
INVITE_STUDENT: '/invite/s',
|
||||||
PROFILE: '/profile',
|
PROFILE: '/profile',
|
||||||
PEOPLE: '/people',
|
PEOPLE: '/people',
|
||||||
DISCUSSIONS: '/discussions',
|
DISCUSSIONS: '/discussions',
|
||||||
@@ -20,7 +21,7 @@ export const PUBLIC_ROUTES = [
|
|||||||
`^${ROUTE.HOME}$`,
|
`^${ROUTE.HOME}$`,
|
||||||
ROUTE.LOGIN,
|
ROUTE.LOGIN,
|
||||||
ROUTE.SIGN_UP,
|
ROUTE.SIGN_UP,
|
||||||
`^${ROUTE.INVITE_SIGNUP}/.*`,
|
`^${ROUTE.INVITE_TEACHER}/.*`,
|
||||||
ROUTE.FORGOT,
|
ROUTE.FORGOT,
|
||||||
ROUTE.RESET,
|
ROUTE.RESET,
|
||||||
`^${ROUTE.PAGES}/.*`,
|
`^${ROUTE.PAGES}/.*`,
|
||||||
@@ -31,7 +32,8 @@ export const PUBLIC_ROUTES = [
|
|||||||
export const ROUTES_TO_HIDE_NAV = [
|
export const ROUTES_TO_HIDE_NAV = [
|
||||||
ROUTE.LOGIN,
|
ROUTE.LOGIN,
|
||||||
ROUTE.SIGN_UP,
|
ROUTE.SIGN_UP,
|
||||||
`^${ROUTE.INVITE_SIGNUP}/.*`,
|
`^${ROUTE.INVITE_TEACHER}/.*`,
|
||||||
|
`^${ROUTE.INVITE_STUDENT}/.*`,
|
||||||
ROUTE.FORGOT,
|
ROUTE.FORGOT,
|
||||||
ROUTE.RESET,
|
ROUTE.RESET,
|
||||||
ROUTE.ONBOARDING
|
ROUTE.ONBOARDING
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { Course } from '../types';
|
||||||
|
|
||||||
|
export const isCourseFree = (cost: number) => !(Number(cost) > 0);
|
||||||
|
|
||||||
|
export const getStudentInviteLink = (_course: Course, orgSiteName: string) => {
|
||||||
|
const hash = encodeURIComponent(
|
||||||
|
btoa(
|
||||||
|
JSON.stringify({
|
||||||
|
groupId: _course.group?.id,
|
||||||
|
name: _course.title,
|
||||||
|
description: _course.description,
|
||||||
|
orgSiteName
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return `https://${orgSiteName}.classroomio.com/invite/s/${hash}`;
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
export default (currency: string | undefined) => {
|
export default (currency: string | undefined) => {
|
||||||
return new Intl.NumberFormat('en-US', {
|
const locale = currency == 'NGN' ? 'en-NG' : 'en-US';
|
||||||
|
return new Intl.NumberFormat(locale, {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
currency: currency,
|
currency: currency,
|
||||||
minimumFractionDigits: 2,
|
minimumFractionDigits: 2
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
|
import type { Profile } from '$lib/components/Course/components/People/types';
|
||||||
|
|
||||||
|
export async function getProfile(email: string): Promise<Profile | null> {
|
||||||
|
const { data } = await supabase.from('profile').select('*').eq('email', email).single();
|
||||||
|
console.log('getProfile data', data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -134,6 +134,22 @@ export const authValidation = (fields = {}) => {
|
|||||||
return processErrors(error);
|
return processErrors(error);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const coursePaymentValidation = (fields = {}) => {
|
||||||
|
const schema = z.object({
|
||||||
|
fullname: z.string().min(6, {
|
||||||
|
message: 'Must be 6 or more characters long',
|
||||||
|
invalid_type_error: 'Must not be empty'
|
||||||
|
}),
|
||||||
|
email: z.string().email({
|
||||||
|
message: 'Invalid email address',
|
||||||
|
invalid_type_error: 'Must not be empty'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const { error } = schema.safeParse(fields);
|
||||||
|
|
||||||
|
return processErrors(error);
|
||||||
|
};
|
||||||
|
|
||||||
export const resetValidation = (fields = {}) => {
|
export const resetValidation = (fields = {}) => {
|
||||||
const { error } = resetValidationSchema.safeParse(fields);
|
const { error } = resetValidationSchema.safeParse(fields);
|
||||||
|
|
||||||
|
|||||||
@@ -125,14 +125,16 @@ export async function getOrgAudience(orgId) {
|
|||||||
email,
|
email,
|
||||||
avatar_url,
|
avatar_url,
|
||||||
created_at,
|
created_at,
|
||||||
organizationmember!inner(
|
groupmember!inner(
|
||||||
role_id,
|
role_id,
|
||||||
organization_id
|
group_id:group!inner(
|
||||||
|
organization_id
|
||||||
|
)
|
||||||
)
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.eq('organizationmember.organization_id', orgId)
|
.eq('groupmember.group.organization_id', orgId)
|
||||||
.eq('organizationmember.role_id', 3); // is a student, tutor is 2 and admin is 1
|
.eq('groupmember.role_id', ROLE.STUDENT);
|
||||||
|
|
||||||
console.log('data', data);
|
console.log('data', data);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { MailService } from '@sendgrid/mail';
|
|||||||
import { SENDGRID_API_KEY } from '$env/static/private';
|
import { SENDGRID_API_KEY } from '$env/static/private';
|
||||||
|
|
||||||
export const SENDGRID_FROM = 'Rotimi Best <best@classroomio.com>';
|
export const SENDGRID_FROM = 'Rotimi Best <best@classroomio.com>';
|
||||||
export const SENDGRID_FROM_NOTIFY = 'ClassroomIO <notify@classroomio.com>';
|
export const SENDGRID_FROM_NOREPLY = 'ClassroomIO <noreply@classroomio.com>';
|
||||||
|
|
||||||
export let sendgrid: MailService;
|
export let sendgrid: MailService;
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { writable, derived } from 'svelte/store';
|
|||||||
import { STEPS } from '../constants/quiz';
|
import { STEPS } from '../constants/quiz';
|
||||||
import type { Writable } from 'svelte/store';
|
import type { Writable } from 'svelte/store';
|
||||||
import type { CurrentOrg, OrgTeamMember } from '../types/org';
|
import type { CurrentOrg, OrgTeamMember } from '../types/org';
|
||||||
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
|
|
||||||
export const orgs = writable<CurrentOrg[]>([]);
|
export const orgs = writable<CurrentOrg[]>([]);
|
||||||
export const currentOrg: Writable<CurrentOrg> = writable({
|
export const currentOrg: Writable<CurrentOrg> = writable({
|
||||||
@@ -17,6 +18,10 @@ export const currentOrg: Writable<CurrentOrg> = writable({
|
|||||||
});
|
});
|
||||||
export const orgAudience = writable([]);
|
export const orgAudience = writable([]);
|
||||||
export const orgTeam = writable<OrgTeamMember[]>([]);
|
export const orgTeam = writable<OrgTeamMember[]>([]);
|
||||||
|
export const isOrgAdmin = derived(
|
||||||
|
currentOrg,
|
||||||
|
($currentOrg) => parseInt($currentOrg.role_id) === ROLE.ADMIN
|
||||||
|
);
|
||||||
export const currentOrgPath = derived(currentOrg, ($currentOrg) =>
|
export const currentOrgPath = derived(currentOrg, ($currentOrg) =>
|
||||||
$currentOrg.siteName ? `/org/${$currentOrg.siteName}` : ''
|
$currentOrg.siteName ? `/org/${$currentOrg.siteName}` : ''
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface CourseMetadata {
|
|||||||
videoUrl?: '';
|
videoUrl?: '';
|
||||||
showDiscount: false;
|
showDiscount: false;
|
||||||
discount: 0;
|
discount: 0;
|
||||||
|
paymentLink?: string;
|
||||||
reward?: {
|
reward?: {
|
||||||
show: boolean;
|
show: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -64,6 +65,8 @@ export interface Group {
|
|||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
organization_id?: string /* foreign key to organization.id */;
|
organization_id?: string /* foreign key to organization.id */;
|
||||||
organization?: Organization;
|
organization?: Organization;
|
||||||
|
tutors: Profile[];
|
||||||
|
members: Groupmember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Role {
|
export interface Role {
|
||||||
@@ -93,7 +96,7 @@ export interface Course {
|
|||||||
logo?: string;
|
logo?: string;
|
||||||
slug?: any; // type unknown;
|
slug?: any; // type unknown;
|
||||||
metadata?: CourseMetadata;
|
metadata?: CourseMetadata;
|
||||||
cost?: number;
|
cost: number;
|
||||||
currency?: string;
|
currency?: string;
|
||||||
group?: Group;
|
group?: Group;
|
||||||
organization?: Organization;
|
organization?: Organization;
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
console.log('Get user profile', authUser);
|
console.log('Get user profile', authUser);
|
||||||
|
|
||||||
if (!authUser) {
|
if (!authUser) {
|
||||||
goto('/login');
|
goto('/login?redirect=/' + path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has profile
|
// Check if user has profile
|
||||||
@@ -315,14 +315,14 @@
|
|||||||
{#if !isQuizPage($page.url.pathname)}
|
{#if !isQuizPage($page.url.pathname)}
|
||||||
<OrgSideBar />
|
<OrgSideBar />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="org-slot w-full bg-white dark:bg-gray-800">
|
<div class="org-slot bg-white dark:bg-gray-800 w-full">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else if isLMSPage($page.url.pathname)}
|
{:else if isLMSPage($page.url.pathname)}
|
||||||
<div class="org-root w-full flex items-center justify-between">
|
<div class="org-root w-full flex items-center justify-between">
|
||||||
<LMSSideBar />
|
<LMSSideBar />
|
||||||
<div class="org-slot w-full bg-white dark:bg-gray-800">
|
<div class="org-slot bg-white dark:bg-gray-800 w-full">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { render } from 'svelte-email';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import JoinRequestTemplate from './template.svelte';
|
||||||
|
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
|
||||||
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
const sendgrid = getSendgrid();
|
||||||
|
const supabase = getSupabase();
|
||||||
|
|
||||||
|
export async function POST({ request }) {
|
||||||
|
const { to, courseName, teacherEmail, studentFullname, orgName } = await request.json();
|
||||||
|
const accessToken = request.headers.get('Authorization') || '';
|
||||||
|
console.log(
|
||||||
|
'/POST api/email/course/student_prove_payment',
|
||||||
|
to,
|
||||||
|
courseName,
|
||||||
|
teacherEmail,
|
||||||
|
studentFullname,
|
||||||
|
orgName
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!to || !courseName || !teacherEmail || !studentFullname || !orgName) {
|
||||||
|
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
const { data } = await supabase.auth.getUser(accessToken);
|
||||||
|
user = data.user;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
from: SENDGRID_FROM_NOREPLY,
|
||||||
|
to,
|
||||||
|
replyTo: teacherEmail,
|
||||||
|
subject: `[${courseName}] One more step left`,
|
||||||
|
html: render({
|
||||||
|
template: JoinRequestTemplate,
|
||||||
|
props: {
|
||||||
|
courseName,
|
||||||
|
studentFullname,
|
||||||
|
teacherEmail,
|
||||||
|
orgName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
sendgrid.send(options);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
message: 'Email sent'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Text } from 'svelte-email';
|
||||||
|
|
||||||
|
export let courseName = '';
|
||||||
|
export let studentFullname = '';
|
||||||
|
export let teacherEmail = '';
|
||||||
|
export let orgName = '';
|
||||||
|
|
||||||
|
const fontFamily =
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
|
||||||
|
|
||||||
|
const paragraph = {
|
||||||
|
fontFamily,
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: '26px'
|
||||||
|
};
|
||||||
|
const paragraphContent = {
|
||||||
|
...paragraph,
|
||||||
|
margin: '20px 0'
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerParagraph = {
|
||||||
|
...paragraph,
|
||||||
|
margin: '0px'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Text style={paragraph}>Hi {studentFullname},</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
You are one step closer to joining: {courseName}
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
Please send your proof of payment to: {teacherEmail}, in order to join the course.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={footerParagraph}>Talk to you soon and see you in class.</Text>
|
||||||
|
<Text style={footerParagraph}>{orgName}</Text>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
|
||||||
|
import ProvePayment from './prove_payment.svelte';
|
||||||
|
|
||||||
|
export let courseName = '';
|
||||||
|
export let studentFullname = '';
|
||||||
|
export let orgName = '';
|
||||||
|
export let teacherEmail = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<EmailTemplate
|
||||||
|
children={ProvePayment}
|
||||||
|
props={{
|
||||||
|
courseName,
|
||||||
|
studentFullname,
|
||||||
|
orgName,
|
||||||
|
teacherEmail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { render } from 'svelte-email';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import WelcomeTemplate from './template.svelte';
|
||||||
|
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
|
||||||
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
const sendgrid = getSendgrid();
|
||||||
|
const supabase = getSupabase();
|
||||||
|
|
||||||
|
// Sends email to student when they successfully join a course
|
||||||
|
export async function POST({ request }) {
|
||||||
|
const { to, orgName, courseName } = await request.json();
|
||||||
|
const accessToken = request.headers.get('Authorization') || '';
|
||||||
|
console.log('/POST api/email/course/student_welcome', to, orgName);
|
||||||
|
|
||||||
|
if (!to || !orgName || !courseName) {
|
||||||
|
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
const { data } = await supabase.auth.getUser(accessToken);
|
||||||
|
user = data.user;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
from: SENDGRID_FROM_NOREPLY,
|
||||||
|
to,
|
||||||
|
subject: `[${orgName}] Welcome to Class`,
|
||||||
|
html: render({
|
||||||
|
template: WelcomeTemplate,
|
||||||
|
props: {
|
||||||
|
orgName,
|
||||||
|
courseName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
sendgrid.send(options);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
message: 'Email sent'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
|
||||||
|
import Welcome from './welcome.svelte';
|
||||||
|
|
||||||
|
export let orgName = '';
|
||||||
|
export let courseName = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<EmailTemplate
|
||||||
|
children={Welcome}
|
||||||
|
props={{
|
||||||
|
orgName,
|
||||||
|
courseName
|
||||||
|
}}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Text } from 'svelte-email';
|
||||||
|
|
||||||
|
export let orgName = '';
|
||||||
|
export let courseName = '';
|
||||||
|
|
||||||
|
const fontFamily =
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
|
||||||
|
|
||||||
|
const paragraph = {
|
||||||
|
fontFamily,
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: '26px'
|
||||||
|
};
|
||||||
|
const paragraphContent = {
|
||||||
|
...paragraph,
|
||||||
|
margin: '20px 0'
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerParagraph = {
|
||||||
|
...paragraph,
|
||||||
|
margin: '0px'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Text style={paragraph}>Hey there,</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
Congratulations, you’ve successfully joined “{courseName}"
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
Everything has been set up for you to have an amazing learning experience.
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
If you run into any issues, don’t fail to reach out to your instructor(s).
|
||||||
|
</Text>
|
||||||
|
<Text style={footerParagraph}>Cheers,</Text>
|
||||||
|
<Text style={footerParagraph}>{orgName}</Text>
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { render } from 'svelte-email';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import JoinRequestTemplate from './template.svelte';
|
||||||
|
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
|
||||||
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
const sendgrid = getSendgrid();
|
||||||
|
const supabase = getSupabase();
|
||||||
|
|
||||||
|
export async function POST({ request }) {
|
||||||
|
const { to, courseName, studentEmail, studentFullname } = await request.json();
|
||||||
|
const accessToken = request.headers.get('Authorization') || '';
|
||||||
|
console.log(
|
||||||
|
'/POST api/email/course/teacher_student_buycourse',
|
||||||
|
to,
|
||||||
|
courseName,
|
||||||
|
studentEmail,
|
||||||
|
studentFullname
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!to || !courseName || !studentEmail || !studentFullname) {
|
||||||
|
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
const { data } = await supabase.auth.getUser(accessToken);
|
||||||
|
user = data.user;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
from: SENDGRID_FROM_NOREPLY,
|
||||||
|
to,
|
||||||
|
subject: `[${courseName}] Request to Join Course!`,
|
||||||
|
html: render({
|
||||||
|
template: JoinRequestTemplate,
|
||||||
|
props: {
|
||||||
|
courseName,
|
||||||
|
studentFullname,
|
||||||
|
studentEmail
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
sendgrid.send(options);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
message: 'Email sent'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Text, Link } from 'svelte-email';
|
||||||
|
|
||||||
|
export let courseName = '';
|
||||||
|
export let studentFullname = '';
|
||||||
|
export let studentEmail = '';
|
||||||
|
|
||||||
|
const fontFamily =
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
|
||||||
|
|
||||||
|
const paragraph = {
|
||||||
|
fontFamily,
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: '26px'
|
||||||
|
};
|
||||||
|
const paragraphContent = {
|
||||||
|
...paragraph,
|
||||||
|
margin: '20px 0'
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerParagraph = {
|
||||||
|
...paragraph,
|
||||||
|
margin: '0px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkStyle = {
|
||||||
|
color: '#1d4ee2'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Text style={paragraph}>Hi amazing tutor,</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
A new student has requested to join a course you are teaching: “{courseName}"
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraphContent}>Student details</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
Name: {studentFullname}<br />
|
||||||
|
Email: {studentEmail}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={footerParagraph}>Cheers</Text>
|
||||||
|
<Text style={footerParagraph}>
|
||||||
|
<Link style={linkStyle} href="https://classroomio.com">ClassroomIO</Link>
|
||||||
|
</Text>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
|
||||||
|
import JoinRequest from './join_request.svelte';
|
||||||
|
|
||||||
|
export let courseName = '';
|
||||||
|
export let studentFullname = '';
|
||||||
|
export let studentEmail = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<EmailTemplate
|
||||||
|
children={JoinRequest}
|
||||||
|
props={{
|
||||||
|
courseName,
|
||||||
|
studentFullname,
|
||||||
|
studentEmail
|
||||||
|
}}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { render } from 'svelte-email';
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import WelcomeTemplate from './template.svelte';
|
||||||
|
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
|
||||||
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
const sendgrid = getSendgrid();
|
||||||
|
const supabase = getSupabase();
|
||||||
|
|
||||||
|
export async function POST({ request }) {
|
||||||
|
const { to, courseName } = await request.json();
|
||||||
|
const accessToken = request.headers.get('Authorization') || '';
|
||||||
|
console.log('/POST api/email/course/teacher_student_joined', to, courseName);
|
||||||
|
|
||||||
|
if (!to || !courseName) {
|
||||||
|
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
const { data } = await supabase.auth.getUser(accessToken);
|
||||||
|
user = data.user;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return json({ success: false, message: 'Unauthenticated user' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
from: SENDGRID_FROM_NOREPLY,
|
||||||
|
to,
|
||||||
|
subject: `[${courseName}] You've got a new student!`,
|
||||||
|
html: render({
|
||||||
|
template: WelcomeTemplate,
|
||||||
|
props: {
|
||||||
|
courseName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
sendgrid.send(options);
|
||||||
|
|
||||||
|
return json({
|
||||||
|
success: true,
|
||||||
|
message: 'Email sent'
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import EmailTemplate from '$lib/components/Email/EmailTemplate.svelte';
|
||||||
|
import Welcome from './welcome.svelte';
|
||||||
|
|
||||||
|
export let courseName = '';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<EmailTemplate
|
||||||
|
children={Welcome}
|
||||||
|
props={{
|
||||||
|
courseName
|
||||||
|
}}
|
||||||
|
/>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { Text, Link } from 'svelte-email';
|
||||||
|
|
||||||
|
export let courseName = '';
|
||||||
|
|
||||||
|
const fontFamily =
|
||||||
|
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif';
|
||||||
|
|
||||||
|
const paragraph = {
|
||||||
|
fontFamily,
|
||||||
|
fontSize: '16px',
|
||||||
|
lineHeight: '26px'
|
||||||
|
};
|
||||||
|
const paragraphContent = {
|
||||||
|
...paragraph,
|
||||||
|
margin: '20px 0'
|
||||||
|
};
|
||||||
|
|
||||||
|
const footerParagraph = {
|
||||||
|
...paragraph,
|
||||||
|
margin: '0px'
|
||||||
|
};
|
||||||
|
|
||||||
|
const linkStyle = {
|
||||||
|
color: '#1d4ee2'
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Text style={paragraph}>Hi amazing tutor,</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
Congratulations, a new student has joined a course you are teaching: “{courseName}"
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
We hope they have a great experience learning from the best (YOU).
|
||||||
|
</Text>
|
||||||
|
<Text style={paragraphContent}>
|
||||||
|
If you run into any issues, please don’t fail to reach out to us, we’d love to make your teaching
|
||||||
|
experience as easy as possible.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text style={footerParagraph}>Cheers</Text>
|
||||||
|
<Text style={footerParagraph}>
|
||||||
|
<Link style={linkStyle} href="https://classroomio.com">ClassroomIO</Link>
|
||||||
|
</Text>
|
||||||
+3
-3
@@ -1,7 +1,7 @@
|
|||||||
import { render } from 'svelte-email';
|
import { render } from 'svelte-email';
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import WelcomeTemplate from './template.svelte';
|
import WelcomeTemplate from './template.svelte';
|
||||||
import { getSendgrid, SENDGRID_FROM_NOTIFY } from '$lib/utils/services/sendgrid';
|
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
|
||||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
const sendgrid = getSendgrid();
|
const sendgrid = getSendgrid();
|
||||||
@@ -10,7 +10,7 @@ const supabase = getSupabase();
|
|||||||
export async function POST({ request }) {
|
export async function POST({ request }) {
|
||||||
const { to, name, orgName, courseName, orgSiteName } = await request.json();
|
const { to, name, orgName, courseName, orgSiteName } = await request.json();
|
||||||
const accessToken = request.headers.get('Authorization') || '';
|
const accessToken = request.headers.get('Authorization') || '';
|
||||||
console.log('/POST api/email/course/welcome', to, name, orgName);
|
console.log('/POST api/email/course/teacher_welcome', to, name, orgName);
|
||||||
|
|
||||||
if (!to || !name || !orgName || !courseName || !orgSiteName) {
|
if (!to || !name || !orgName || !courseName || !orgSiteName) {
|
||||||
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
|
return json({ success: false, message: 'Missing required fields' }, { status: 400 });
|
||||||
@@ -32,7 +32,7 @@ export async function POST({ request }) {
|
|||||||
const inviteLink = `${origin}/org/${orgSiteName}/courses`;
|
const inviteLink = `${origin}/org/${orgSiteName}/courses`;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
from: SENDGRID_FROM_NOTIFY,
|
from: SENDGRID_FROM_NOREPLY,
|
||||||
to,
|
to,
|
||||||
subject: `You have been invited to a course in ${orgName}!`,
|
subject: `You have been invited to a course in ${orgName}!`,
|
||||||
html: render({
|
html: render({
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { render } from 'svelte-email';
|
import { render } from 'svelte-email';
|
||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import TeacherInviteTemplate from './template.svelte';
|
import TeacherInviteTemplate from './template.svelte';
|
||||||
import { getSendgrid, SENDGRID_FROM_NOTIFY } from '$lib/utils/services/sendgrid';
|
import { getSendgrid, SENDGRID_FROM_NOREPLY } from '$lib/utils/services/sendgrid';
|
||||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
const sendgrid = getSendgrid();
|
const sendgrid = getSendgrid();
|
||||||
@@ -15,7 +15,7 @@ export async function POST({ request }) {
|
|||||||
const accessToken = request.headers.get('Authorization');
|
const accessToken = request.headers.get('Authorization');
|
||||||
console.log('/POST api/email/invite', body);
|
console.log('/POST api/email/invite', body);
|
||||||
|
|
||||||
if (!org || !Object.keys(org) || !email) {
|
if (!org || !Object.keys(org) || !email || !accessToken) {
|
||||||
return json(
|
return json(
|
||||||
{ success: false, message: 'Org data and Teacher name are required' },
|
{ success: false, message: 'Org data and Teacher name are required' },
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
@@ -43,11 +43,11 @@ export async function POST({ request }) {
|
|||||||
orgId: id,
|
orgId: id,
|
||||||
orgSiteName: siteName
|
orgSiteName: siteName
|
||||||
});
|
});
|
||||||
const inviteLink = `${origin}/invite/${encodeURIComponent(btoa(inviteData))}`;
|
const inviteLink = `${origin}/invite/t/${encodeURIComponent(btoa(inviteData))}`;
|
||||||
console.log('inviteData', inviteData);
|
console.log('inviteData', inviteData);
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
from: SENDGRID_FROM_NOTIFY,
|
from: SENDGRID_FROM_NOREPLY,
|
||||||
to: email,
|
to: email,
|
||||||
subject: `Join ${name} on ClassroomIO`,
|
subject: `Join ${name} on ClassroomIO`,
|
||||||
html: render({
|
html: render({
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
export const load = ({ params = { slug: '' } }) => {
|
import { fetchCourse } from '$lib/utils/services/courses';
|
||||||
|
import { supabase, getSupabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
if (!supabase) {
|
||||||
|
getSupabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const load = async ({ params = { slug: '' } }) => {
|
||||||
|
const { data } = await fetchCourse(undefined, params.slug);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slug: params.slug,
|
slug: params.slug,
|
||||||
|
course: data
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,39 +1,21 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import Backdrop from '$lib/components/Backdrop/index.svelte';
|
|
||||||
import { Moon } from 'svelte-loading-spinners';
|
|
||||||
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
|
import CourseLandingPage from '$lib/components/CourseLandingPage/index.svelte';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import type { Course } from '$lib/utils/types';
|
||||||
import { setCourse, course } from '$lib/components/Course/store';
|
|
||||||
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
|
|
||||||
import { fetchCourse } from '$lib/utils/services/courses';
|
|
||||||
import { browser } from '$app/environment';
|
|
||||||
|
|
||||||
export let data;
|
interface Data {
|
||||||
|
course: Course;
|
||||||
|
}
|
||||||
|
export let data: Data;
|
||||||
|
|
||||||
let courseData = {};
|
let courseData: Course;
|
||||||
let loading = false;
|
|
||||||
|
|
||||||
async function triggerSetCourse(_profile) {
|
function setCourseData(course: Course | null) {
|
||||||
if (!browser) return;
|
if (!course) return;
|
||||||
loading = true;
|
|
||||||
const { data: _data } = await fetchCourse(undefined, data.slug);
|
courseData = { ...course };
|
||||||
setCourse(_data);
|
|
||||||
loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCourseData(course, lessons) {
|
$: setCourseData(data.course);
|
||||||
courseData = { ...course, lessons };
|
|
||||||
console.log('courseData', courseData);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: triggerSetCourse($profile);
|
|
||||||
$: setCourseData($course, $lessons);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if loading}
|
<CourseLandingPage {courseData} />
|
||||||
<Backdrop>
|
|
||||||
<Moon size="60" color="#1d4ed8" unit="px" duration="1s" />
|
|
||||||
</Backdrop>
|
|
||||||
{:else}
|
|
||||||
<CourseLandingPage {courseData} />
|
|
||||||
{/if}
|
|
||||||
|
|||||||
@@ -138,77 +138,80 @@
|
|||||||
|
|
||||||
<CourseContainer bind:isStudent>
|
<CourseContainer bind:isStudent>
|
||||||
<PageNav title="Attendance" />
|
<PageNav title="Attendance" />
|
||||||
|
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||||
<section class="mx-9 my-5">
|
<section class="flex items-center mx-2 lg:mx-9 my-5">
|
||||||
<div class="flex flex-row items-center justify-between">
|
<div
|
||||||
<div class="flex">
|
class="flex flex-col lg:flex-row items-start gap-2 lg:items-center justify-between w-full"
|
||||||
<p class="flex items-center mr-5">
|
>
|
||||||
<Checkbox checked disabled /> Present
|
<div class="flex">
|
||||||
</p>
|
<p class="flex items-center mr-5">
|
||||||
<p class="flex items-center">
|
<Checkbox checked disabled /> Present
|
||||||
<Checkbox disabled /> Absent
|
</p>
|
||||||
</p>
|
<p class="flex items-center">
|
||||||
|
<Checkbox disabled /> Absent
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Search
|
||||||
|
class="dark:text-slate-950"
|
||||||
|
placeholder="Search students"
|
||||||
|
bind:value={searchValue}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</section>
|
||||||
<Search
|
|
||||||
class="dark:text-slate-950"
|
|
||||||
placeholder="Search students"
|
|
||||||
bind:value={searchValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="my-5 mx-9">
|
<section class="my-5 mx-2 lg:mx-9">
|
||||||
<StructuredList class="m-0">
|
<StructuredList class="m-0 relative">
|
||||||
<!-- Moved the lesson headers outside the students loop -->
|
<!-- Moved the lesson headers outside the students loop -->
|
||||||
<StructuredListHead class="bg-primary-100">
|
<StructuredListHead class="bg-primary-100">
|
||||||
<StructuredListRow head class="mx-7">
|
<StructuredListRow head class="mx-7">
|
||||||
<StructuredListCell head class="text-primary-600 py-3">Student</StructuredListCell>
|
<StructuredListCell head class="text-primary-600 py-3">Student</StructuredListCell>
|
||||||
{#each $lessons as lesson, index}
|
{#each $lessons as lesson, index}
|
||||||
<StructuredListCell head class="text-primary-600 py-3"
|
<StructuredListCell head class="text-primary-600 py-3"
|
||||||
>Lesson 0{getLectureNo(index + 1)}</StructuredListCell
|
>Lesson 0{getLectureNo(index + 1)}</StructuredListCell
|
||||||
>
|
>
|
||||||
{/each}
|
|
||||||
</StructuredListRow>
|
|
||||||
</StructuredListHead>
|
|
||||||
|
|
||||||
{#each searchStudents(searchValue) as student}
|
|
||||||
<StructuredListBody>
|
|
||||||
<StructuredListRow>
|
|
||||||
<StructuredListCell>
|
|
||||||
<div class="w-1/4 flex items-center">
|
|
||||||
<p class="dark:text-white font-semibold">
|
|
||||||
{student.profile.fullname}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</StructuredListCell>
|
|
||||||
{#each $lessons as lesson}
|
|
||||||
<StructuredListCell class="">
|
|
||||||
<Checkbox
|
|
||||||
class={isStudent && 'cursor-not-allowed'}
|
|
||||||
disabled={isStudent}
|
|
||||||
checked={$attendance[student.id]
|
|
||||||
? $attendance[student.id][lesson.id]
|
|
||||||
? $attendance[student.id][lesson.id].is_present
|
|
||||||
: false
|
|
||||||
: false}
|
|
||||||
on:change={(e) => handleAttendanceChange(e, student, lesson)}
|
|
||||||
/>
|
|
||||||
</StructuredListCell>
|
|
||||||
{/each}
|
{/each}
|
||||||
</StructuredListRow>
|
</StructuredListRow>
|
||||||
</StructuredListBody>
|
</StructuredListHead>
|
||||||
{/each}
|
|
||||||
</StructuredList>
|
{#each searchStudents(searchValue) as student}
|
||||||
{#if students.length === 0}
|
<StructuredListBody>
|
||||||
<div>
|
<StructuredListRow>
|
||||||
<Box>
|
<StructuredListCell>
|
||||||
|
<div class="w-1/4 flex items-center">
|
||||||
|
<p class="dark:text-white font-semibold">
|
||||||
|
{student.profile.fullname}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</StructuredListCell>
|
||||||
|
{#each $lessons as lesson}
|
||||||
|
<StructuredListCell class="">
|
||||||
|
<Checkbox
|
||||||
|
class={isStudent && 'cursor-not-allowed'}
|
||||||
|
disabled={isStudent}
|
||||||
|
checked={$attendance[student.id]
|
||||||
|
? $attendance[student.id][lesson.id]
|
||||||
|
? $attendance[student.id][lesson.id].is_present
|
||||||
|
: false
|
||||||
|
: false}
|
||||||
|
on:change={(e) => handleAttendanceChange(e, student, lesson)}
|
||||||
|
/>
|
||||||
|
</StructuredListCell>
|
||||||
|
{/each}
|
||||||
|
</StructuredListRow>
|
||||||
|
</StructuredListBody>
|
||||||
|
{/each}
|
||||||
|
</StructuredList>
|
||||||
|
{#if students.length === 0}
|
||||||
|
<Box className="h-[300px] w-full">
|
||||||
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
|
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
|
||||||
<h3 class="text-3xl text-gray-500 dark:text-white">No Student Added</h3>
|
<h3 class="text-3xl text-gray-500 dark:text-white text-center">No Student Added</h3>
|
||||||
</Box>
|
</Box>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
{#if students.length !== 0}
|
||||||
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
|
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
|
||||||
</section>
|
{/if}
|
||||||
|
</section>
|
||||||
|
</PageBody>
|
||||||
</CourseContainer>
|
</CourseContainer>
|
||||||
|
|||||||
@@ -30,26 +30,21 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="root absolute inset-0 flex justify-center items-center z-50 bg-white"
|
class="absolute inset-0 z-50 bg-white"
|
||||||
in:fly={{ y: 500, duration: 500 }}
|
in:fly={{ y: 500, duration: 500 }}
|
||||||
out:fly={{ y: 500, duration: 500 }}
|
out:fly={{ y: 500, duration: 500 }}
|
||||||
>
|
>
|
||||||
<Editor {courseId} bind:course={courseData} />
|
<Editor {courseId} bind:course={courseData} />
|
||||||
<div class="rightBar" class:isMobile={$isMobile}>
|
<div class="rightBar">
|
||||||
<CourseLandingPage bind:courseData />
|
<CourseLandingPage bind:courseData editMode={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.rightBar {
|
.rightBar {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: calc(100% - 360px);
|
width: 100%;
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
}
|
|
||||||
|
|
||||||
.rightBar.isMobile {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -321,12 +321,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<Box>
|
<Box>
|
||||||
<div class="flex justify-between flex-col items-center w-96">
|
<div class="flex justify-between flex-col items-center w-[90%] md:w-96">
|
||||||
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
<img src="/images/empty-lesson-icon.svg" alt="Lesson" class="my-2.5 mx-auto" />
|
||||||
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
|
<h2 class="text-xl my-1.5 font-normal">No lessons yet</h2>
|
||||||
<p class="text-sm text-center text-slate-500">
|
<p class="text-sm text-center text-slate-500">
|
||||||
Share your knowledge with the world by creating engaging lessons. Start by clicking on
|
Share your knowledge with the world by creating engaging lessons. Start by clicking on
|
||||||
the New Lesson button.
|
the Add button.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -4,17 +4,19 @@
|
|||||||
import { fetchCourse, fetchLesson } from '$lib/utils/services/courses';
|
import { fetchCourse, fetchLesson } from '$lib/utils/services/courses';
|
||||||
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import { Loading } from 'carbon-components-svelte';
|
import { Loading } from 'carbon-components-svelte';
|
||||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||||
import PageBody from '$lib/components/PageBody/index.svelte';
|
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||||
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
|
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
|
||||||
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
|
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
|
||||||
import MODES from '$lib/utils/constants/mode.js';
|
import MODES from '$lib/utils/constants/mode';
|
||||||
import { setCourse, course } from '$lib/components/Course/store';
|
import { setCourse, course } from '$lib/components/Course/store';
|
||||||
import Download from 'carbon-icons-svelte/lib/Download.svelte';
|
import Download from 'carbon-icons-svelte/lib/Download.svelte';
|
||||||
|
import OverflowMenuVertical from 'carbon-icons-svelte/lib/OverflowMenuVertical.svelte';
|
||||||
|
import { apps } from '$lib/components/Apps/store';
|
||||||
|
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||||
import {
|
import {
|
||||||
lesson,
|
lesson,
|
||||||
lessons,
|
lessons,
|
||||||
@@ -144,6 +146,10 @@
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleApps = () => {
|
||||||
|
$apps.open = !$apps.open;
|
||||||
|
};
|
||||||
|
|
||||||
function toggleMode() {
|
function toggleMode() {
|
||||||
prevMode = mode;
|
prevMode = mode;
|
||||||
mode = mode === MODES.edit ? MODES.view : MODES.edit;
|
mode = mode === MODES.edit ? MODES.view : MODES.edit;
|
||||||
@@ -176,9 +182,20 @@
|
|||||||
<svelte:fragment slot="widget">
|
<svelte:fragment slot="widget">
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
{#if data.isMaterialsTabActive}
|
{#if data.isMaterialsTabActive}
|
||||||
<div class="flex items-center">
|
<div class="lg:hidden">
|
||||||
|
<IconButton onClick={toggleApps} buttonClassName="">
|
||||||
|
<OverflowMenuVertical size={24} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class={`flex-row ${
|
||||||
|
$apps.dropdown && $apps.open
|
||||||
|
? 'absolute lg:relative top-[85px] lg:top-0 right-14 lg:right-0 z-40 dark:bg-gray-800 p-3 lg:p-0'
|
||||||
|
: 'hidden'
|
||||||
|
} lg:flex items-center`}
|
||||||
|
>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
className="mr-2"
|
className="mb-2 lg:mb-0 mr-2"
|
||||||
variant={VARIANTS.OUTLINED}
|
variant={VARIANTS.OUTLINED}
|
||||||
onClick={downloadLesson}
|
onClick={downloadLesson}
|
||||||
{isLoading}
|
{isLoading}
|
||||||
@@ -207,7 +224,7 @@
|
|||||||
<PageBody>
|
<PageBody>
|
||||||
<Materials lessonId={data.lessonId} {mode} {prevMode} {toggleMode} bind:isSaving />
|
<Materials lessonId={data.lessonId} {mode} {prevMode} {toggleMode} bind:isSaving />
|
||||||
|
|
||||||
<div class="w-full flex flex-row-reverse">
|
<div class="w-full flex flex-row-reverse mt-10">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
onClick={markLessonComplete}
|
onClick={markLessonComplete}
|
||||||
isLoading={isMarkingComplete}
|
isLoading={isMarkingComplete}
|
||||||
|
|||||||
@@ -8,10 +8,7 @@
|
|||||||
import { ROLE } from '$lib/utils/constants/roles';
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
import { setCourse, course, group } from '$lib/components/Course/store';
|
import { setCourse, course, group } from '$lib/components/Course/store';
|
||||||
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
|
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||||
import {
|
import { fetchCourse, fetchExercisesByMarks } from '$lib/utils/services/courses';
|
||||||
fetchCourse,
|
|
||||||
fetchExercisesByMarks,
|
|
||||||
} from '$lib/utils/services/courses';
|
|
||||||
import { getLectureNo } from '$lib/components/Course/function';
|
import { getLectureNo } from '$lib/components/Course/function';
|
||||||
import { fetchMarks } from '$lib/utils/services/marks';
|
import { fetchMarks } from '$lib/utils/services/marks';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
@@ -19,10 +16,8 @@
|
|||||||
export let data;
|
export let data;
|
||||||
const { courseId } = data;
|
const { courseId } = data;
|
||||||
|
|
||||||
let borderBottomGrey =
|
let borderBottomGrey = 'border-r-0 border-t-0 border-b border-l-0 border-gray-300';
|
||||||
'border-r-0 border-t-0 border-b border-l-0 border-gray-300';
|
let borderleftGrey = 'border-r-0 border-t-0 border-b-0 border-l border-gray-300';
|
||||||
let borderleftGrey =
|
|
||||||
'border-r-0 border-t-0 border-b-0 border-l border-gray-300';
|
|
||||||
let students = [];
|
let students = [];
|
||||||
let isStudent = false;
|
let isStudent = false;
|
||||||
|
|
||||||
@@ -52,11 +47,11 @@
|
|||||||
if (studentMarksByExerciseId[groupmember_id]) {
|
if (studentMarksByExerciseId[groupmember_id]) {
|
||||||
studentMarksByExerciseId[groupmember_id] = {
|
studentMarksByExerciseId[groupmember_id] = {
|
||||||
...studentMarksByExerciseId[groupmember_id],
|
...studentMarksByExerciseId[groupmember_id],
|
||||||
[exercise_id]: total_points_gotten,
|
[exercise_id]: total_points_gotten
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
studentMarksByExerciseId[groupmember_id] = {
|
studentMarksByExerciseId[groupmember_id] = {
|
||||||
[exercise_id]: total_points_gotten,
|
[exercise_id]: total_points_gotten
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -70,11 +65,11 @@
|
|||||||
if (lessonMapping[lesson_id]) {
|
if (lessonMapping[lesson_id]) {
|
||||||
lessonMapping[lesson_id] = {
|
lessonMapping[lesson_id] = {
|
||||||
...lessonMapping[lesson_id],
|
...lessonMapping[lesson_id],
|
||||||
[exercise_id]: { title: exercise_title, points },
|
[exercise_id]: { title: exercise_title, points }
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
lessonMapping[lesson_id] = {
|
lessonMapping[lesson_id] = {
|
||||||
[exercise_id]: { title: exercise_title, points },
|
[exercise_id]: { title: exercise_title, points }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -82,20 +77,16 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
students = isStudent
|
students = isStudent
|
||||||
? $group.people.filter(
|
? $group.people.filter((person) => !!person.profile && person.profile.id === $profile.id)
|
||||||
(person) => !!person.profile && person.profile.id === $profile.id
|
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT);
|
||||||
)
|
|
||||||
: $group.people.filter(
|
|
||||||
(person) => !!person.profile && person.role_id === ROLE.STUDENT
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<CourseContainer bind:isStudent>
|
<CourseContainer bind:isStudent>
|
||||||
<PageNav title="Marks" />
|
<PageNav title="Marks" />
|
||||||
|
|
||||||
<PageBody width="w-11/12">
|
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||||
<div class="table rounded-md border border-gray-300 w-auto">
|
<div class="table rounded-md border border-gray-300 w-full">
|
||||||
<div class="flex items-center {borderBottomGrey}">
|
<div class="flex items-center {borderBottomGrey}">
|
||||||
<div class="box flex items-center p-3">
|
<div class="box flex items-center p-3">
|
||||||
<p class="dark:text-white w-40">Student</p>
|
<p class="dark:text-white w-40">Student</p>
|
||||||
@@ -111,8 +102,7 @@
|
|||||||
>
|
>
|
||||||
{#each Object.keys(lessonMapping[lesson.id]) as exerciseId, index}
|
{#each Object.keys(lessonMapping[lesson.id]) as exerciseId, index}
|
||||||
<p
|
<p
|
||||||
class="col text-sm dark:text-white {index &&
|
class="col text-sm dark:text-white {index && borderleftGrey}"
|
||||||
borderleftGrey}"
|
|
||||||
title={lessonMapping[lesson.id][exerciseId].title}
|
title={lessonMapping[lesson.id][exerciseId].title}
|
||||||
>
|
>
|
||||||
{lessonMapping[lesson.id][exerciseId].title}
|
{lessonMapping[lesson.id][exerciseId].title}
|
||||||
@@ -129,9 +119,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#each students as student}
|
{#each students as student}
|
||||||
<div
|
<div class="flex relative items-center p-3 cursor-pointer {borderBottomGrey}">
|
||||||
class="flex relative items-center p-3 cursor-pointer {borderBottomGrey}"
|
|
||||||
>
|
|
||||||
<div class="w-40 flex items-center">
|
<div class="w-40 flex items-center">
|
||||||
<img
|
<img
|
||||||
alt="Student avatar"
|
alt="Student avatar"
|
||||||
@@ -143,11 +131,7 @@
|
|||||||
{student.profile.fullname}
|
{student.profile.fullname}
|
||||||
</p>
|
</p>
|
||||||
<p class="dark:text-white">
|
<p class="dark:text-white">
|
||||||
{`${
|
{`${student.assigned_student_id ? '#' + student.assigned_student_id : '-'}`}
|
||||||
student.assigned_student_id
|
|
||||||
? '#' + student.assigned_student_id
|
|
||||||
: '-'
|
|
||||||
}`}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -176,9 +160,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<Box>
|
<Box>
|
||||||
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
|
<AudioConsoleIcon size={32} class="carbon-icon w-80" />
|
||||||
<h3 class="text-3xl text-gray-500 dark:text-white">
|
<h3 class="text-3xl text-gray-500 dark:text-white">No Student Added</h3>
|
||||||
No Student Added
|
|
||||||
</h3>
|
|
||||||
</Box>
|
</Box>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||||
|
import Filter from 'carbon-icons-svelte/lib/Filter.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import {
|
import {
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
import { fetchCourse, deleteGroupMember, updatedGroupMember } from '$lib/utils/services/courses';
|
import { fetchCourse, deleteGroupMember, updatedGroupMember } from '$lib/utils/services/courses';
|
||||||
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
||||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||||
|
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import Select from '$lib/components/Form/Select.svelte';
|
import Select from '$lib/components/Form/Select.svelte';
|
||||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||||
@@ -135,138 +137,148 @@
|
|||||||
</RoleBasedSecurity>
|
</RoleBasedSecurity>
|
||||||
</slot:fragment>
|
</slot:fragment>
|
||||||
</PageNav>
|
</PageNav>
|
||||||
<section class="my-5 mx-9">
|
<PageBody width="w-full max-w-6xl md:w-11/12">
|
||||||
<div class="flex flex-row flex-end justify-end items-center mb-7">
|
<section class="my-5 mx-2 md:mx-9">
|
||||||
<div class="mr-5 w-80">
|
<div
|
||||||
<Search
|
class="flex flex-col md:flex-row flex-end gap-2 justify-end items-start md:items-center mb-7"
|
||||||
class="dark:text-slate-950 border-0 bg-zinc-100 w-full"
|
|
||||||
placeholder="Search people"
|
|
||||||
bind:value={searchValue}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<Select bind:value={filterBy} options={ROLES} className="dark:text-black mt-3 w-20" />
|
|
||||||
<!-- <select bind:value={filterBy} class="mt-3">
|
|
||||||
{#each ROLES as option}
|
|
||||||
<option value={option.value}>{option.label}</option>
|
|
||||||
{/each}
|
|
||||||
</select> -->
|
|
||||||
</div>
|
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
|
||||||
<p class="dark:text-white hidden lg:block text-lg w-20" />
|
|
||||||
</RoleBasedSecurity>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<StructuredList class="m-0">
|
|
||||||
<StructuredListHead
|
|
||||||
class="bg-slate-100 dark:bg-neutral-800 dark:border-2 dark:border-neutral-800"
|
|
||||||
>
|
>
|
||||||
<StructuredListRow head class="mx-7">
|
<div class="max-w-[320px]">
|
||||||
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
<Search
|
||||||
>Name</StructuredListCell
|
class="dark:text-slate-950 border-0 bg-zinc-100 w-full"
|
||||||
>
|
placeholder="Search people"
|
||||||
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
bind:value={searchValue}
|
||||||
>Role</StructuredListCell
|
/>
|
||||||
>
|
</div>
|
||||||
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
<div class="mb-3">
|
||||||
>Action</StructuredListCell
|
<Select
|
||||||
>
|
bind:value={filterBy}
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
options={ROLES}
|
||||||
<p class="dark:text-white hidden lg:block text-lg w-20" />
|
className="dark:text-black mt-3 max-w-[80px]"
|
||||||
</RoleBasedSecurity>
|
/>
|
||||||
</StructuredListRow>
|
<!-- <select bind:value={filterBy} class="mt-3">
|
||||||
</StructuredListHead>
|
{#each ROLES as option}
|
||||||
|
<option value={option.value}>{option.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select> -->
|
||||||
|
</div>
|
||||||
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
|
<p class="dark:text-white hidden lg:block text-lg w-20" />
|
||||||
|
</RoleBasedSecurity>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#each filterPeople(searchValue, people) as person}
|
<StructuredList class="m-0">
|
||||||
<StructuredListBody>
|
<StructuredListHead
|
||||||
<StructuredListRow class="relative">
|
class="bg-slate-100 dark:bg-neutral-800 dark:border-2 dark:border-neutral-800"
|
||||||
<!-- first column -->
|
>
|
||||||
<StructuredListCell class="w-3/6">
|
<StructuredListRow head class="mx-7">
|
||||||
{#if person.profile}
|
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
||||||
<div class="flex items-center absolute bottom-3">
|
>Name</StructuredListCell
|
||||||
<Avatar
|
>
|
||||||
src={person.profile.avatar_url}
|
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
||||||
name={person.profile.fullname}
|
>Role</StructuredListCell
|
||||||
width="w-8"
|
>
|
||||||
height="h-8"
|
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
|
||||||
className="mr-3"
|
>Action</StructuredListCell
|
||||||
/>
|
>
|
||||||
<div class="flex items-center">
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
<div class="mr-2">
|
<p class="dark:text-white hidden lg:block text-lg w-20" />
|
||||||
<p class="dark:text-white text-base font-normal">
|
</RoleBasedSecurity>
|
||||||
{person.profile.fullname}
|
</StructuredListRow>
|
||||||
</p>
|
</StructuredListHead>
|
||||||
<p class="text-xs text-primary-600">
|
|
||||||
{getEmail(person)}
|
{#each filterPeople(searchValue, people) as person}
|
||||||
</p>
|
<StructuredListBody>
|
||||||
</div>
|
<StructuredListRow class="relative">
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
<!-- first column -->
|
||||||
<div class="hidden lg:flex lg:justify-between lg:items-center mr-2">
|
<StructuredListCell class="w-4/6 md:w-3/6">
|
||||||
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
|
{#if person.profile}
|
||||||
|
<div class="flex items-start lg:items-center">
|
||||||
|
<Avatar
|
||||||
|
src={person.profile.avatar_url}
|
||||||
|
name={person.profile.fullname}
|
||||||
|
width="w-8"
|
||||||
|
height="h-8"
|
||||||
|
className="mr-3"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col lg:flex-row items-start lg:items-center">
|
||||||
|
<div class="mr-2">
|
||||||
|
<p class="dark:text-white text-base font-normal">
|
||||||
|
{person.profile.fullname}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-primary-600 line-clamp-1">
|
||||||
|
{getEmail(person)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</RoleBasedSecurity>
|
<div class="flex items-center">
|
||||||
{#if person.profile_id == $profile.id}
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
<ComingSoon label="You" />
|
<CopyButton
|
||||||
|
text={getEmail(person)}
|
||||||
|
feedback="Copied Email to clipboard"
|
||||||
|
/>
|
||||||
|
</RoleBasedSecurity>
|
||||||
|
{#if person.profile_id == $profile.id}
|
||||||
|
<ComingSoon label="You" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex items-start lg:items-center w-2/4">
|
||||||
|
<TextChip
|
||||||
|
value={person.email.substring(0, 2).toUpperCase()}
|
||||||
|
className="bg-primary-200 text-black font-semibold text-xs mr-3"
|
||||||
|
shape="rounded-full"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href="mailto:{person.email}"
|
||||||
|
class="text-md text-primary-600 mr-2 dark:text-white"
|
||||||
|
>
|
||||||
|
{person.email}
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
|
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
|
||||||
|
</RoleBasedSecurity>
|
||||||
|
|
||||||
|
<TextChip
|
||||||
|
value="Pending"
|
||||||
|
className="text-xs bg-yellow-200 text-yellow-700 h-fit"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</StructuredListCell>
|
||||||
|
|
||||||
|
<!-- second column -->
|
||||||
|
<StructuredListCell class="w-1/4 px-3">
|
||||||
|
<p class=" dark:text-white font-normal text-center text-base w-1/4">
|
||||||
|
{ROLE_LABEL[person.role_id]}
|
||||||
|
</p>
|
||||||
|
</StructuredListCell>
|
||||||
|
|
||||||
|
<!-- third column -->
|
||||||
|
<StructuredListCell class="p-0 w-1/4">
|
||||||
|
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||||
|
<div class="hidden lg:flex lg:justify-between lg:items-center w-20">
|
||||||
|
{#if person.profile_id !== $profile.id}
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
member = person;
|
||||||
|
$deleteMemberModal.open = true;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||||
|
</IconButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RoleBasedSecurity>
|
||||||
{:else}
|
</StructuredListCell>
|
||||||
<div class="flex items-center w-2/4 absolute bottom-3">
|
</StructuredListRow>
|
||||||
<TextChip
|
</StructuredListBody>
|
||||||
value={person.email.substring(0, 2).toUpperCase()}
|
{/each}
|
||||||
className="bg-primary-200 text-black font-semibold text-xs mr-3"
|
</StructuredList>
|
||||||
shape="rounded-full"
|
<!-- <Pagination totalItems={10} pageSizes={[10, 15, 20]} /> -->
|
||||||
/>
|
</section>
|
||||||
<a
|
</PageBody>
|
||||||
href="mailto:{person.email}"
|
|
||||||
class="text-md text-primary-600 mr-2 dark:text-white"
|
|
||||||
>
|
|
||||||
{person.email}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
|
||||||
<div class="hidden lg:flex lg:justify-between lg:items-center w-20">
|
|
||||||
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
|
|
||||||
</div>
|
|
||||||
</RoleBasedSecurity>
|
|
||||||
|
|
||||||
<TextChip
|
|
||||||
value="Pending"
|
|
||||||
className="text-xs bg-yellow-200 text-yellow-700 h-fit"
|
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</StructuredListCell>
|
|
||||||
|
|
||||||
<!-- second column -->
|
|
||||||
<StructuredListCell class="w-1/4 py-4 px-3">
|
|
||||||
<p class="dark:text-white font-normal text-base w-1/4 my-auto">
|
|
||||||
{ROLE_LABEL[person.role_id]}
|
|
||||||
</p>
|
|
||||||
</StructuredListCell>
|
|
||||||
|
|
||||||
<!-- third column -->
|
|
||||||
<StructuredListCell class="p-0 w-1/4">
|
|
||||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
|
||||||
<div class="hidden lg:flex lg:justify-between lg:items-center w-20">
|
|
||||||
{#if person.profile_id !== $profile.id}
|
|
||||||
<IconButton
|
|
||||||
onClick={() => {
|
|
||||||
member = person;
|
|
||||||
$deleteMemberModal.open = true;
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
|
||||||
</IconButton>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</RoleBasedSecurity>
|
|
||||||
</StructuredListCell>
|
|
||||||
</StructuredListRow>
|
|
||||||
</StructuredListBody>
|
|
||||||
{/each}
|
|
||||||
</StructuredList>
|
|
||||||
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
|
|
||||||
</section>
|
|
||||||
</CourseContainer>
|
</CourseContainer>
|
||||||
|
|||||||
@@ -244,8 +244,8 @@
|
|||||||
<CourseContainer>
|
<CourseContainer>
|
||||||
<PageNav title="Submitted Exercises" />
|
<PageNav title="Submitted Exercises" />
|
||||||
|
|
||||||
<PageBody width="w-11/12 overflow-x-auto overflow-y-hidden">
|
<PageBody width="w-full max-w-6xl md:w-11/12 overflow-x-auto">
|
||||||
<div class="flex items-center w-full">
|
<div class="flex items-center w-full overflow-x-scroll">
|
||||||
{#each sections as { id, title, items }, idx (id)}
|
{#each sections as { id, title, items }, idx (id)}
|
||||||
<div
|
<div
|
||||||
class="section rounded-md bg-gray-100 dark:bg-gray-800 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
|
class="section rounded-md bg-gray-100 dark:bg-gray-800 border border-gray-50 p-3 h-80 mr-3 overflow-hidden"
|
||||||
@@ -301,7 +301,7 @@
|
|||||||
{`${item.tutor ? 'created by' + item.tutor.name : ''}`}
|
{`${item.tutor ? 'created by' + item.tutor.name : ''}`}
|
||||||
</p>
|
</p>
|
||||||
</a>
|
</a>
|
||||||
<p class="dark:text-white text-gray-500 dark:text-white text-xs">
|
<p class="dark:text-white text-gray-500 text-xs">
|
||||||
{item.submittedAt}
|
{item.submittedAt}
|
||||||
</p>
|
</p>
|
||||||
<!-- <div class="badge rounded-md px-2 bg-green-500 text-white">
|
<!-- <div class="badge rounded-md px-2 bg-green-500 text-white">
|
||||||
@@ -324,10 +324,15 @@
|
|||||||
.section {
|
.section {
|
||||||
max-width: 355px;
|
max-width: 355px;
|
||||||
min-width: 355px;
|
min-width: 355px;
|
||||||
height: 80vh;
|
height: 75vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
height: 95%;
|
height: 95%;
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.section {
|
||||||
|
min-width: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { redirect } from '@sveltejs/kit';
|
||||||
|
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
|
||||||
|
import { getCurrentOrg } from '$lib/utils/services/org';
|
||||||
|
|
||||||
|
if (!supabase) {
|
||||||
|
getSupabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const load = async ({ params = { hash: '' } }) => {
|
||||||
|
try {
|
||||||
|
const courseHashData = atob(decodeURIComponent(params.hash));
|
||||||
|
console.log('courseHashData', courseHashData);
|
||||||
|
|
||||||
|
const { groupId, name, description, orgSiteName } = JSON.parse(courseHashData);
|
||||||
|
|
||||||
|
if (!groupId || !name || !description || !orgSiteName) {
|
||||||
|
throw 'Validation failed';
|
||||||
|
}
|
||||||
|
const currentOrg = await getCurrentOrg(orgSiteName, true);
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
currentOrg
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error decoding course invite params.hash', error);
|
||||||
|
throw redirect(301, '/404');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
|
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
import { setTheme } from '$lib/utils/functions/theme';
|
||||||
|
import { addGroupMember } from '$lib/utils/services/courses';
|
||||||
|
import type { CurrentOrg } from '$lib/utils/types/org.js';
|
||||||
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
|
import { profile } from '$lib/utils/store/user.js';
|
||||||
|
import { sendStudentCourseWelcome, sendTeacherStudentJoinedCourse } from './utils';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
let supabase = getSupabase();
|
||||||
|
let loading = false;
|
||||||
|
let refresh = false;
|
||||||
|
|
||||||
|
let disableSubmit = false;
|
||||||
|
let formRef: HTMLFormElement;
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
loading = true;
|
||||||
|
|
||||||
|
if (refresh) {
|
||||||
|
window.location.reload();
|
||||||
|
refresh = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$profile.id || !$profile.email) {
|
||||||
|
console.log('Profile not found', $profile);
|
||||||
|
refresh = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const member = {
|
||||||
|
profile_id: $profile.id,
|
||||||
|
group_id: data.groupId,
|
||||||
|
role_id: ROLE.STUDENT
|
||||||
|
};
|
||||||
|
|
||||||
|
const teacherMembers = await supabase
|
||||||
|
.from('groupmember')
|
||||||
|
.select('id, profile(email)')
|
||||||
|
.eq('group_id', data.groupId)
|
||||||
|
.eq('role_id', ROLE.TUTOR);
|
||||||
|
|
||||||
|
const teachers: Array<string> =
|
||||||
|
teacherMembers.data?.map((teacher) => {
|
||||||
|
return teacher.profile?.email || '';
|
||||||
|
}) || [];
|
||||||
|
|
||||||
|
addGroupMember(member).then((addedMember) => {
|
||||||
|
loading = false;
|
||||||
|
if (addedMember.error) {
|
||||||
|
console.error('Error adding student to group', data.groupId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send email welcoming student to the course
|
||||||
|
sendStudentCourseWelcome({
|
||||||
|
to: $profile.email,
|
||||||
|
orgName: data.currentOrg?.name,
|
||||||
|
courseName: data.name
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send notification to teacher(s) that a student has joined the course.
|
||||||
|
sendTeacherStudentJoinedCourse({
|
||||||
|
to: teachers,
|
||||||
|
courseName: data.name
|
||||||
|
});
|
||||||
|
|
||||||
|
// go to lms
|
||||||
|
return goto('/lms');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCurOrg(cOrg: CurrentOrg) {
|
||||||
|
if (!cOrg) return;
|
||||||
|
currentOrg.set(cOrg);
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
setTheme(data.currentOrg?.theme || '');
|
||||||
|
});
|
||||||
|
|
||||||
|
$: setCurOrg(data.currentOrg as CurrentOrg);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Join {data.name} on ClassroomIO</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<AuthUI
|
||||||
|
{supabase}
|
||||||
|
isLogin={false}
|
||||||
|
{handleSubmit}
|
||||||
|
isLoading={loading}
|
||||||
|
showOnlyContent={true}
|
||||||
|
showLogo={true}
|
||||||
|
bind:formRef
|
||||||
|
>
|
||||||
|
<div class="mt-0 w-full">
|
||||||
|
{#if refresh}
|
||||||
|
<p class="dark:text-white text-sm font-light text-center">
|
||||||
|
Something went wrong, please try again.
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<h3 class="dark:text-white text-lg font-medium mt-0 mb-4 text-center">{data.name}</h3>
|
||||||
|
<p class="dark:text-white text-sm font-light text-center">{data.description}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="my-4 w-full flex justify-center items-center">
|
||||||
|
<PrimaryButton
|
||||||
|
label={refresh ? 'Try again' : 'Join Course'}
|
||||||
|
type="submit"
|
||||||
|
isDisabled={disableSubmit || loading}
|
||||||
|
isLoading={loading}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</AuthUI>
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
|
|
||||||
|
export async function sendStudentCourseWelcome(params: {
|
||||||
|
to: string;
|
||||||
|
orgName: string;
|
||||||
|
courseName: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await supabase.auth.getSession();
|
||||||
|
const accessToken = data.session?.access_token || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/email/course/student_welcome', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: accessToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
console.log('Sent invite email', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending invite email', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendTeacherStudentJoinedCourse(params: {
|
||||||
|
to: Array<string>;
|
||||||
|
courseName: string;
|
||||||
|
}) {
|
||||||
|
const { data } = await supabase.auth.getSession();
|
||||||
|
const accessToken = data.session?.access_token || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/email/course/teacher_student_joined', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: accessToken
|
||||||
|
},
|
||||||
|
body: JSON.stringify(params)
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(await response.text());
|
||||||
|
}
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
console.log('Sent invite email', result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending invite email', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-3
@@ -1,7 +1,14 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { getCurrentOrg } from '$lib/utils/services/org';
|
import { getCurrentOrg } from '$lib/utils/services/org';
|
||||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
import { getSupabase, supabase } from '$lib/utils/functions/supabase';
|
||||||
|
import { getProfile } from '$lib/utils/functions/user';
|
||||||
|
|
||||||
|
if (!supabase) {
|
||||||
|
getSupabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// we need to know if the email exists or not.
|
||||||
|
// with this we can only ask the user to accept
|
||||||
export const load = async ({ params = { hash: '' } }) => {
|
export const load = async ({ params = { hash: '' } }) => {
|
||||||
try {
|
try {
|
||||||
const hashData = atob(decodeURIComponent(params.hash));
|
const hashData = atob(decodeURIComponent(params.hash));
|
||||||
@@ -9,13 +16,16 @@ export const load = async ({ params = { hash: '' } }) => {
|
|||||||
|
|
||||||
const { orgId, email, orgSiteName } = JSON.parse(hashData);
|
const { orgId, email, orgSiteName } = JSON.parse(hashData);
|
||||||
|
|
||||||
getSupabase();
|
|
||||||
const _currentOrg = await getCurrentOrg(orgSiteName, true);
|
const _currentOrg = await getCurrentOrg(orgSiteName, true);
|
||||||
|
|
||||||
|
const profile = await getProfile(email);
|
||||||
|
console.log('profile data', profile);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
orgId,
|
orgId,
|
||||||
email,
|
email,
|
||||||
currentOrg: _currentOrg
|
currentOrg: _currentOrg,
|
||||||
|
profile
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error decoding invite params.hash', error);
|
console.error('Error decoding invite params.hash', error);
|
||||||
@@ -10,11 +10,11 @@
|
|||||||
} from '$lib/utils/functions/validator';
|
} from '$lib/utils/functions/validator';
|
||||||
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
|
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
|
||||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||||
import { setTheme } from '$lib/utils/functions/theme';
|
import { setTheme } from '$lib/utils/functions/theme';
|
||||||
import type { CurrentOrg } from '$lib/utils/types/org.js';
|
import type { CurrentOrg } from '$lib/utils/types/org.js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import Error from '../../+error.svelte';
|
import type { Profile } from '$lib/components/Course/components/People/types';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
@@ -34,7 +34,10 @@
|
|||||||
let formRef: HTMLFormElement;
|
let formRef: HTMLFormElement;
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const validationRes = authValidation(fields);
|
const validationRes = authValidation({
|
||||||
|
...fields,
|
||||||
|
email: 'test@gmail.com' // validation for this ema
|
||||||
|
});
|
||||||
console.log('validationRes', validationRes);
|
console.log('validationRes', validationRes);
|
||||||
|
|
||||||
if (Object.keys(validationRes).length) {
|
if (Object.keys(validationRes).length) {
|
||||||
@@ -44,40 +47,44 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
// Signup
|
let profile: Profile | null = data.profile;
|
||||||
const { data: signupData, error } = await supabase.auth.signUp({
|
|
||||||
email: data.email,
|
|
||||||
password: fields.password
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Signup', signupData);
|
if (!data.profile) {
|
||||||
if (error) throw error;
|
// Signup
|
||||||
|
const { data: signupData, error } = await supabase.auth.signUp({
|
||||||
|
email: data.email,
|
||||||
|
password: fields.password
|
||||||
|
});
|
||||||
|
|
||||||
if (!signupData.user) {
|
console.log('Signup', signupData);
|
||||||
throw 'User cannot be created ';
|
if (error) throw error;
|
||||||
|
|
||||||
|
if (!signupData.user) {
|
||||||
|
throw 'User cannot be created ';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert profile
|
||||||
|
const userId = signupData.user.id;
|
||||||
|
const profileRes = await supabase
|
||||||
|
.from('profile')
|
||||||
|
.insert({
|
||||||
|
id: userId,
|
||||||
|
username: fields.name.toLowerCase().replace(' ', '-') + new Date().getTime(),
|
||||||
|
fullname: fields.name,
|
||||||
|
email: data.email
|
||||||
|
})
|
||||||
|
.select();
|
||||||
|
|
||||||
|
console.log('Insert profile', profileRes.data);
|
||||||
|
|
||||||
|
if (profileRes.error) {
|
||||||
|
throw profileRes.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
profile = profileRes.data[0] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert profile
|
if (!profile?.id) {
|
||||||
const userId = signupData.user.id;
|
|
||||||
const profileRes = await supabase
|
|
||||||
.from('profile')
|
|
||||||
.insert({
|
|
||||||
id: userId,
|
|
||||||
username: fields.name.toLowerCase().replace(' ', '-') + new Date().getTime(),
|
|
||||||
fullname: fields.name,
|
|
||||||
email: data.email
|
|
||||||
})
|
|
||||||
.select();
|
|
||||||
|
|
||||||
console.log('Insert profile', profileRes.data);
|
|
||||||
|
|
||||||
if (profileRes.error) {
|
|
||||||
throw profileRes.error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = profileRes.data[0] || {};
|
|
||||||
|
|
||||||
if (!profile.id) {
|
|
||||||
throw 'Unable to create profile';
|
throw 'Unable to create profile';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,12 +98,23 @@
|
|||||||
.match({ email: data.email, organization_id: data.currentOrg?.id });
|
.match({ email: data.email, organization_id: data.currentOrg?.id });
|
||||||
|
|
||||||
console.log('Update member response', updateMemberRes);
|
console.log('Update member response', updateMemberRes);
|
||||||
formRef?.reset();
|
|
||||||
|
|
||||||
return goto('/login');
|
const res = await supabase.auth.signInWithPassword({
|
||||||
|
email: data.email,
|
||||||
|
password: fields.password
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.error) {
|
||||||
|
throw res.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
formRef?.reset();
|
||||||
|
return goto($currentOrgPath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
submitError = error.message;
|
submitError = error.message;
|
||||||
|
} else {
|
||||||
|
submitError = error?.toString() || '';
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
loading = false;
|
loading = false;
|
||||||
@@ -116,6 +134,7 @@
|
|||||||
$: disableSubmit = getDisableSubmit(fields);
|
$: disableSubmit = getDisableSubmit(fields);
|
||||||
|
|
||||||
$: setCurOrg(data.currentOrg as CurrentOrg);
|
$: setCurOrg(data.currentOrg as CurrentOrg);
|
||||||
|
$: console.log('data.profile', data.profile);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -132,7 +151,13 @@
|
|||||||
bind:formRef
|
bind:formRef
|
||||||
>
|
>
|
||||||
<div class="mt-4 w-full">
|
<div class="mt-4 w-full">
|
||||||
<p class="dark:text-white text-lg font-semibold mb-6">Create a free account</p>
|
<p class="dark:text-white text-lg font-semibold mb-6">
|
||||||
|
{#if data.profile}
|
||||||
|
Log in to join
|
||||||
|
{:else}
|
||||||
|
Create a free account to join
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
<TextField
|
<TextField
|
||||||
label="Your Email"
|
label="Your Email"
|
||||||
value={data.email}
|
value={data.email}
|
||||||
@@ -142,18 +167,20 @@
|
|||||||
inputClassName="w-full"
|
inputClassName="w-full"
|
||||||
isDisabled={true}
|
isDisabled={true}
|
||||||
/>
|
/>
|
||||||
<TextField
|
{#if !data.profile}
|
||||||
label="Full Name"
|
<TextField
|
||||||
bind:value={fields.name}
|
label="Full Name"
|
||||||
type="text"
|
bind:value={fields.name}
|
||||||
autoFocus={true}
|
type="text"
|
||||||
placeholder="e.g Joke Silva"
|
autoFocus={true}
|
||||||
className="mb-6"
|
placeholder="e.g Joke Silva"
|
||||||
inputClassName="w-full"
|
className="mb-6"
|
||||||
isDisabled={loading}
|
inputClassName="w-full"
|
||||||
errorMessage={errors.name}
|
isDisabled={loading}
|
||||||
isRequired
|
errorMessage={errors.name}
|
||||||
/>
|
isRequired
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<TextField
|
<TextField
|
||||||
label="Your Password"
|
label="Your Password"
|
||||||
bind:value={fields.password}
|
bind:value={fields.password}
|
||||||
@@ -166,17 +193,19 @@
|
|||||||
helperMessage="Password must be more than 6 characters"
|
helperMessage="Password must be more than 6 characters"
|
||||||
isRequired
|
isRequired
|
||||||
/>
|
/>
|
||||||
<TextField
|
{#if !data.profile}
|
||||||
label="Confirm Password"
|
<TextField
|
||||||
bind:value={fields.confirmPassword}
|
label="Confirm Password"
|
||||||
type="password"
|
bind:value={fields.confirmPassword}
|
||||||
placeholder="************"
|
type="password"
|
||||||
className="mb-6"
|
placeholder="************"
|
||||||
inputClassName="w-full"
|
className="mb-6"
|
||||||
isDisabled={loading}
|
inputClassName="w-full"
|
||||||
errorMessage={errors.confirmPassword}
|
isDisabled={loading}
|
||||||
isRequired
|
errorMessage={errors.confirmPassword}
|
||||||
/>
|
isRequired
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{#if submitError}
|
{#if submitError}
|
||||||
<p class="text-sm text-red-500">{submitError}</p>
|
<p class="text-sm text-red-500">{submitError}</p>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -184,7 +213,7 @@
|
|||||||
|
|
||||||
<div class="my-4 w-full flex justify-end items-center">
|
<div class="my-4 w-full flex justify-end items-center">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Create and Join"
|
label="Accept Invite"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="sm:w-full w-full"
|
className="sm:w-full w-full"
|
||||||
isDisabled={disableSubmit || loading}
|
isDisabled={disableSubmit || loading}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
import TextField from '$lib/components/Form/TextField.svelte';
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
@@ -14,6 +15,9 @@
|
|||||||
let loading = false;
|
let loading = false;
|
||||||
let errors = {};
|
let errors = {};
|
||||||
|
|
||||||
|
let query = new URLSearchParams($page.url.search);
|
||||||
|
let redirect = query.get('redirect');
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
const validationRes = authValidation(fields);
|
const validationRes = authValidation(fields);
|
||||||
console.log('validationRes', validationRes);
|
console.log('validationRes', validationRes);
|
||||||
@@ -31,6 +35,13 @@
|
|||||||
});
|
});
|
||||||
console.log('data', data);
|
console.log('data', data);
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
|
// reload page on org site cause for some reason the authlistener on +layout.svelte doesn't run
|
||||||
|
// on org site
|
||||||
|
if ($currentOrg.id && redirect) {
|
||||||
|
console.log('Forcing full page reload for auth listener');
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
submitError = error.error_description || error.message;
|
submitError = error.error_description || error.message;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -199,10 +199,6 @@
|
|||||||
$: progress = Math.round((step / maxSteps) * 100);
|
$: progress = Math.round((step / maxSteps) * 100);
|
||||||
$: fields.siteName = generateSitename(fields.siteName || '');
|
$: fields.siteName = generateSitename(fields.siteName || '');
|
||||||
$: setOrgSiteName(fields.orgName, isSiteNameTouched);
|
$: setOrgSiteName(fields.orgName, isSiteNameTouched);
|
||||||
|
|
||||||
$: {
|
|
||||||
console.log('currentOrg', $currentOrg);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $profile.id}
|
{#if $profile.id}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import WelcomeModal from '$lib/components/WelcomeModal/WelcomeModal.svelte';
|
import WelcomeModal from '$lib/components/WelcomeModal/WelcomeModal.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Add } from 'carbon-icons-svelte';
|
import { Add } from 'carbon-icons-svelte';
|
||||||
|
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants.js';
|
||||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||||
|
|
||||||
@@ -201,13 +202,13 @@
|
|||||||
<div class="flex items-center justify-between mb-10">
|
<div class="flex items-center justify-between mb-10">
|
||||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Dashboard</h1>
|
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Dashboard</h1>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
{#if $isMobile}
|
<PrimaryButton variant={VARIANTS.OUTLINED} onClick={createCourse} isDisabled={!$isOrgAdmin}>
|
||||||
<PrimaryButton onClick={createCourse}>
|
{#if $isMobile}
|
||||||
<Add size={24} />
|
<Add size={24} />
|
||||||
</PrimaryButton>
|
{:else}
|
||||||
{:else}
|
Create Course
|
||||||
<PrimaryButton variant={VARIANTS.OUTLINED} label="Create Course" onClick={createCourse} />
|
{/if}
|
||||||
{/if}
|
</PrimaryButton>
|
||||||
|
|
||||||
<VisitOrgSiteButton />
|
<VisitOrgSiteButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import { currentOrg } from '$lib/utils/store/org';
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
import { Add } from 'carbon-icons-svelte';
|
import { Add } from 'carbon-icons-svelte';
|
||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
|
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
let { allCourses, cantFetch } = data;
|
let { allCourses, cantFetch } = data;
|
||||||
@@ -51,11 +52,15 @@
|
|||||||
<div class="flex items-center justify-between mb-10">
|
<div class="flex items-center justify-between mb-10">
|
||||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Courses</h1>
|
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Courses</h1>
|
||||||
{#if $isMobile}
|
{#if $isMobile}
|
||||||
<PrimaryButton onClick={() => ($createCourseModal.open = true)}>
|
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={() => ($createCourseModal.open = true)}>
|
||||||
<Add size={24} />
|
<Add size={24} />
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
{:else}
|
{:else}
|
||||||
<PrimaryButton label="Create Course" onClick={() => ($createCourseModal.open = true)} />
|
<PrimaryButton
|
||||||
|
label="Create Course"
|
||||||
|
isDisabled={!$isOrgAdmin}
|
||||||
|
onClick={() => ($createCourseModal.open = true)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { page } from '$app/stores';
|
||||||
import TextField from '$lib/components/Form/TextField.svelte';
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||||
@@ -10,15 +11,20 @@
|
|||||||
} from '$lib/utils/functions/validator';
|
} from '$lib/utils/functions/validator';
|
||||||
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
|
import { SIGNUP_FIELDS } from '$lib/utils/constants/authentication';
|
||||||
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
import AuthUI from '$lib/components/AuthUI/index.svelte';
|
||||||
|
import { profile } from '$lib/utils/store/user';
|
||||||
|
import { currentOrg } from '$lib/utils/store/org';
|
||||||
|
|
||||||
let supabase = getSupabase();
|
let supabase = getSupabase();
|
||||||
let fields = Object.assign({}, SIGNUP_FIELDS);
|
let fields = Object.assign({}, SIGNUP_FIELDS);
|
||||||
let loading = false;
|
let loading = false;
|
||||||
let success = false;
|
let success = false;
|
||||||
let errors = {};
|
let errors = {};
|
||||||
let submitError;
|
let submitError: string;
|
||||||
let disableSubmit = false;
|
let disableSubmit = false;
|
||||||
let formRef;
|
let formRef: HTMLFormElement;
|
||||||
|
|
||||||
|
let query = new URLSearchParams($page.url.search);
|
||||||
|
let redirect = query.get('redirect');
|
||||||
|
|
||||||
async function handleSubmit(e) {
|
async function handleSubmit(e) {
|
||||||
const validationRes = authValidation(fields);
|
const validationRes = authValidation(fields);
|
||||||
@@ -31,18 +37,58 @@
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
loading = true;
|
loading = true;
|
||||||
const { data, error } = await supabase.auth.signUp({
|
|
||||||
|
const {
|
||||||
|
data: { session },
|
||||||
|
error
|
||||||
|
} = await supabase.auth.signUp({
|
||||||
email: fields.email,
|
email: fields.email,
|
||||||
password: fields.password
|
password: fields.password
|
||||||
});
|
});
|
||||||
console.log('data', data);
|
console.log('session', session);
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
formRef?.reset();
|
const { user: authUser } = session || {};
|
||||||
|
if (!authUser) {
|
||||||
|
throw 'Error creating user';
|
||||||
|
}
|
||||||
|
|
||||||
return goto('/login');
|
if (!$currentOrg.id) return;
|
||||||
// success = true;
|
|
||||||
// fields = Object.assign({}, SIGNUP_FIELDS);
|
const [regexUsernameMatch] = [...(authUser.email?.matchAll(/(.*)@/g) || [])];
|
||||||
|
const response = await fetch('https://api.ipregistry.co/?key=tryout');
|
||||||
|
const metadata = await response.json();
|
||||||
|
|
||||||
|
const profileRes = await supabase
|
||||||
|
.from('profile')
|
||||||
|
.insert({
|
||||||
|
id: authUser.id,
|
||||||
|
username: regexUsernameMatch[1] + `${new Date().getTime()}`,
|
||||||
|
fullname: regexUsernameMatch[1],
|
||||||
|
email: authUser.email,
|
||||||
|
metadata
|
||||||
|
})
|
||||||
|
.select();
|
||||||
|
console.log('profileRes', profileRes);
|
||||||
|
|
||||||
|
if (profileRes.error) {
|
||||||
|
throw profileRes.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setting profile
|
||||||
|
console.log('setting profile', profileRes.data[0]);
|
||||||
|
profile.set(profileRes.data[0]);
|
||||||
|
|
||||||
|
if (redirect) {
|
||||||
|
goto(redirect);
|
||||||
|
} else {
|
||||||
|
goto('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
formRef?.reset();
|
||||||
|
success = true;
|
||||||
|
fields = Object.assign({}, SIGNUP_FIELDS);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
submitError = error.error_description || error.message;
|
submitError = error.error_description || error.message;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user