Merge branch 'main' of https://gitlab.com/thenextgreat/classroomio
This commit is contained in:
@@ -6,12 +6,15 @@
|
||||
import SendAlt from 'carbon-icons-svelte/lib/SendAlt.svelte';
|
||||
import ChartPie from 'carbon-icons-svelte/lib/ChartPie.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 LiveChat from './components/LiveChat/index.svelte';
|
||||
import Notes from './components/Notes/index.svelte';
|
||||
import Poll from './components/Poll/index.svelte';
|
||||
import APPS_CONSTANTS from './constants';
|
||||
import { OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
|
||||
|
||||
let selectedApp;
|
||||
|
||||
@@ -48,8 +51,21 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="apps">
|
||||
<div class={`root ${$apps.open ? 'open dark:bg-slate-800' : 'close dark:bg-slate-800'}`}>
|
||||
<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
|
||||
toolTipProps={{ title: 'Live Chat', hotkeys: ['A', '1'] }}
|
||||
value={APPS_CONSTANTS.APPS.LIVE_CHAT}
|
||||
@@ -104,7 +120,7 @@
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
height: 93vh;
|
||||
height: calc(100vh - 48px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
border-left: 1px solid var(--border-color);
|
||||
@@ -129,4 +145,25 @@
|
||||
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>
|
||||
|
||||
@@ -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 { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import GoogleIconColored from '../Icons/GoogleIconColored.svelte';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import type { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
export let supabase;
|
||||
export let handleSubmit;
|
||||
export let supabase: SupabaseClient;
|
||||
export let handleSubmit = () => {};
|
||||
export let isLogin = true;
|
||||
export let showOnlyContent = false;
|
||||
export let isLoading = false;
|
||||
export let showLogo = false;
|
||||
export let formRef;
|
||||
export let hideGoogleAuth = false;
|
||||
|
||||
async function signInWithGoogle() {
|
||||
if (isLoading) {
|
||||
@@ -19,12 +22,11 @@
|
||||
}
|
||||
try {
|
||||
console.log('signInWithGoogle');
|
||||
const { user, session, error } = await supabase.auth.signInWithOAuth({
|
||||
const { error, data } = await supabase.auth.signInWithOAuth({
|
||||
provider: 'google'
|
||||
});
|
||||
|
||||
console.log('user', user);
|
||||
console.log('session', session);
|
||||
console.log('data', data);
|
||||
console.log('error', error);
|
||||
} catch (error) {
|
||||
console.log('catch error', error);
|
||||
@@ -57,7 +59,7 @@
|
||||
>
|
||||
<slot />
|
||||
</form>
|
||||
{#if !showOnlyContent}
|
||||
{#if !showOnlyContent && !hideGoogleAuth}
|
||||
<div class="w-10/12 mb-3">
|
||||
<p class="dark:text-white text-sm mb-5">or sign up with:</p>
|
||||
<PrimaryButton
|
||||
@@ -75,10 +77,14 @@
|
||||
{#if !showOnlyContent}
|
||||
<div class="w-full p-6 border-t border-grey text-center">
|
||||
{#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}
|
||||
Already have an account? <a class="hover:underline text-primary-700" href="/login"
|
||||
>Log In</a
|
||||
Already have an account? <a
|
||||
class="hover:underline text-primary-700"
|
||||
href="/login{$page.url.search}">Log In</a
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
|
||||
export let className = '';
|
||||
</script>
|
||||
@@ -12,7 +13,9 @@
|
||||
class="{className} ml-2 hover:no-underline"
|
||||
>
|
||||
<PrimaryButton>
|
||||
View site
|
||||
{#if !$isMobile}
|
||||
View site
|
||||
{/if}
|
||||
<ArrowUpRightIcon />
|
||||
</PrimaryButton>
|
||||
</a>
|
||||
|
||||
@@ -45,8 +45,8 @@
|
||||
</svelte:head>
|
||||
|
||||
<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">
|
||||
<section class="w-full md:w-2/5 h-full">
|
||||
<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 lg:w-2/5 h-full">
|
||||
<strong class="my-2 text-base font-semibold text-black dark:text-gray-100"
|
||||
>Certificate settings</strong
|
||||
>
|
||||
@@ -99,7 +99,7 @@
|
||||
</div>
|
||||
</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">
|
||||
{#if $course.certificate_theme === 'professional'}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<Modal
|
||||
onClose={() => (openModal = false)}
|
||||
bind:open={openModal}
|
||||
width="w-[60%]"
|
||||
width="w-[80%] md:w-[60%]"
|
||||
containerClass="overflow-hidden"
|
||||
modalHeading="Add New Lesson"
|
||||
>
|
||||
|
||||
@@ -165,8 +165,8 @@
|
||||
<p class="dark:text-white mt-4 text-sm">Created Jul 3, 2021</p>
|
||||
</a>
|
||||
{:else}
|
||||
<Box className="mt-3">
|
||||
<div class="flex justify-between flex-col items-center w-96">
|
||||
<Box className="mt-3 text-center">
|
||||
<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" />
|
||||
<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">
|
||||
|
||||
@@ -11,13 +11,15 @@
|
||||
const onChange = (tab) => () => (currentTab = tab);
|
||||
</script>
|
||||
|
||||
<section class="w-full h-full flex flex-row items-start gap-3">
|
||||
<div class="flex-1 flex-col">
|
||||
<p class="text-sm font-normal text-start text-[#4F4B4B] dark:text-[#b0a9a9] mb-3">Add By</p>
|
||||
<section class="w-full h-full flex flex-col md:flex-row items-start gap-3">
|
||||
<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 w-full">
|
||||
Add By
|
||||
</p>
|
||||
{#each tabs as item (item.value)}
|
||||
<button
|
||||
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
|
||||
? 'border border-[#0233BD] bg-[#F5F8FE] dark:text-black'
|
||||
: 'border border-gray-200'
|
||||
|
||||
@@ -197,13 +197,13 @@
|
||||
<Modal
|
||||
{onClose}
|
||||
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"
|
||||
>
|
||||
<VideoUploader {lessonId} />
|
||||
</Modal>
|
||||
|
||||
<h1 class="text-4xl mt-0">
|
||||
<h1 class="text-4xl mt-0 capitalize">
|
||||
{lessonTitle}
|
||||
</h1>
|
||||
|
||||
@@ -386,7 +386,7 @@
|
||||
</article>
|
||||
{/if}
|
||||
{:else}
|
||||
<Box>
|
||||
<Box className="text-center">
|
||||
<img src="/no-video.svg" alt="Video not found" />
|
||||
<h3 class="text-xl font-normal dark:text-white py-2">
|
||||
No note, video or slide added for this lesson yet
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { menu } from '$lib/components/Org/store';
|
||||
|
||||
// export let lessonId;
|
||||
export let path;
|
||||
@@ -32,12 +33,14 @@
|
||||
let show = null;
|
||||
let isLessonActive = false;
|
||||
|
||||
const toggleSidebar = () => {
|
||||
$menu.hidden = !$menu.hidden;
|
||||
};
|
||||
|
||||
function handleMainGroupClick(href) {
|
||||
return () => {
|
||||
if ($isMobile) {
|
||||
setTimeout(() => (show = false), 500);
|
||||
}
|
||||
goto(href);
|
||||
toggleSidebar();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -146,145 +149,66 @@
|
||||
|
||||
<Settings />
|
||||
|
||||
<div class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}">
|
||||
{#if show}
|
||||
<div class="relative h-full dark:bg-gray-800">
|
||||
<!-- <PageNav title="Back to courses" paddingClass="pl-2" titleSize="text-md">
|
||||
<slot:fragment slot="image">
|
||||
<Avatar src={$course.logo} />
|
||||
<IconButton
|
||||
value="toggle"
|
||||
onClick={() => goto(`${$currentOrgPath}/courses`)}
|
||||
size="small"
|
||||
contained={true}
|
||||
<aside
|
||||
class={`${
|
||||
$menu.hidden
|
||||
? '-translate-x-[100%] absolute md:translate-x-0 md:relative z-[40]'
|
||||
: 'translate-x-0 absolute md:relative z-[40]'
|
||||
} 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`}
|
||||
>
|
||||
<div class="h-full flex flex-col">
|
||||
<ul class="my-5">
|
||||
{#each navItems as navItem}
|
||||
{#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" />
|
||||
</IconButton>
|
||||
</slot:fragment>
|
||||
<slot:fragment slot="widget">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<IconButton onClick={() => ($settingsDialog.open = !$settingsDialog.open)}>
|
||||
<SettingsIcon size={20} class="carbon-icon dark:text-white" />
|
||||
</IconButton>
|
||||
</RoleBasedSecurity>
|
||||
</slot:fragment>
|
||||
</PageNav> -->
|
||||
<div class="h-full py-3 overflow-y-auto">
|
||||
{#each navItems as navItem}
|
||||
{#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}
|
||||
>
|
||||
{#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}
|
||||
{#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={toggleSidebar}
|
||||
aria-disabled={!item.is_unlocked}
|
||||
>
|
||||
<div
|
||||
class="flex items-center py-3 px-4 {NavClasses.item} {(
|
||||
path || $page.url.pathname
|
||||
).includes(item.id) && NavClasses.active}"
|
||||
>
|
||||
<div
|
||||
class="flex items-center py-3 px-4 {NavClasses.item} {(
|
||||
path || $page.url.pathname
|
||||
).includes(item.id) && NavClasses.active}"
|
||||
>
|
||||
<TextChip
|
||||
value={getLectureNo(index + 1)}
|
||||
className="bg-primary-200 text-primary-600 text-xs mr-2"
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<span>{item.title}</span>
|
||||
<span class="grow" />
|
||||
{#if !item.is_unlocked}
|
||||
<span class="text-md ml-2" title="This lesson is locked.">
|
||||
<LockedIcon class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{:else if item.is_complete}
|
||||
<span class="ml-2" title="You have completed this lesson">
|
||||
<CheckmarkFilled class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/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>
|
||||
<TextChip
|
||||
value={getLectureNo(index + 1)}
|
||||
className="bg-primary-200 text-primary-600 text-xs mr-2"
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<span>{item.title}</span>
|
||||
<span class="grow" />
|
||||
{#if !item.is_unlocked}
|
||||
<span class="text-md ml-2" title="This lesson is locked.">
|
||||
<LockedIcon class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{:else if item.is_complete}
|
||||
<span class="ml-2" title="You have completed this lesson">
|
||||
<CheckmarkFilled class="carbon-icon dark:text-white" />
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
{/if}
|
||||
</NavExpandable>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</aside>
|
||||
|
||||
@@ -1,27 +1,21 @@
|
||||
<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 copy from 'copy-to-clipboard';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import { addGroupMember, fetchGroup } from '$lib/utils/services/courses';
|
||||
import { invitationModal, resetForm } from './store';
|
||||
import { validateForm } from '$lib/components/Courses/functions';
|
||||
import { course, setCourse } from '$lib/components/Course/store';
|
||||
import { MultiSelect, Loading } from 'carbon-components-svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { getOrgTeam } from '$lib/utils/services/org';
|
||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||
import { sendTeacherCourseWelcome } from './utils';
|
||||
|
||||
let errors: {
|
||||
students: string;
|
||||
tutors: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
} = {
|
||||
students: '',
|
||||
tutors: ''
|
||||
};
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { getStudentInviteLink } from '$lib/utils/functions/course';
|
||||
|
||||
interface Tutor {
|
||||
id: number;
|
||||
@@ -33,6 +27,8 @@
|
||||
let selectedIds: Array<number> = [];
|
||||
let selectedTutors: Tutor[] = [];
|
||||
let isLoadingTutors = false;
|
||||
let link = '';
|
||||
let copied = false;
|
||||
|
||||
function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
|
||||
return value !== null && value !== undefined;
|
||||
@@ -41,28 +37,14 @@
|
||||
i.length === 0 ? [] : i.map((id) => tutors.find((tutor) => tutor.id === id)).filter(notEmpty);
|
||||
|
||||
function onSubmit() {
|
||||
const { hasError, fieldErrors, students } = validateForm({
|
||||
...$invitationModal,
|
||||
title: 'sfdsd',
|
||||
description: 'dsfdsf',
|
||||
tutors: 'sgsg'
|
||||
});
|
||||
|
||||
errors = fieldErrors;
|
||||
if (hasError) return;
|
||||
if (!selectedTutors.length) {
|
||||
resetForm();
|
||||
return;
|
||||
}
|
||||
|
||||
let membersStack = [];
|
||||
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) {
|
||||
membersStack.push({
|
||||
profile_id: tutor.profileId,
|
||||
@@ -128,24 +110,34 @@
|
||||
isLoadingTutors = false;
|
||||
}
|
||||
|
||||
function copyLink() {
|
||||
copy(link);
|
||||
copied = true;
|
||||
setTimeout(() => {
|
||||
copied = false;
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
$: {
|
||||
selectedTutors = formatSelected(selectedIds);
|
||||
console.log('selectedTutors', selectedTutors);
|
||||
}
|
||||
|
||||
$: setTutors($currentOrg.id, $invitationModal.open);
|
||||
|
||||
$: link = getStudentInviteLink($course, $currentOrg.siteName);
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={resetForm}
|
||||
bind:open={$invitationModal.open}
|
||||
width="w-2/5"
|
||||
width="w-4/5 md:w-2/5"
|
||||
maxWidth="max-w-lg"
|
||||
modalHeading="Invite people"
|
||||
>
|
||||
<form on:submit|preventDefault={onSubmit}>
|
||||
<div class="mb-4">
|
||||
<p class="text-base mb-1">Invite Tutors</p>
|
||||
<div class="mb-8">
|
||||
<p class="text-base mb-1 font-semibold">Invite Tutors</p>
|
||||
<MultiSelect
|
||||
disabled={isLoadingTutors}
|
||||
label="Select tutors in organization..."
|
||||
@@ -169,19 +161,29 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
label="Invite Students"
|
||||
labelClassName="text-base"
|
||||
bind:value={$invitationModal.students}
|
||||
rows="2"
|
||||
maxRows={3}
|
||||
placeholder="[email protected], john@gmail.com"
|
||||
helperMessage="To invite people, add their emails separated by a comma"
|
||||
errorMessage={errors.students}
|
||||
className="mb-4"
|
||||
/>
|
||||
<div class="mb-8 w-[90%] flex justify-between items-center">
|
||||
<div class="w-3/5">
|
||||
<p class="text-base mb-1 font-semibold">Invite Students</p>
|
||||
<p class=" text-sm">You can invite students via an invite link</p>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -5,12 +5,10 @@ export const deleteMemberModal = writable({
|
||||
});
|
||||
|
||||
export const invitationModal = writable({
|
||||
open: false,
|
||||
students: ''
|
||||
open: false
|
||||
});
|
||||
|
||||
export const resetForm = () =>
|
||||
invitationModal.update(() => ({
|
||||
open: false,
|
||||
students: ''
|
||||
open: false
|
||||
}));
|
||||
|
||||
@@ -11,7 +11,7 @@ export async function sendTeacherCourseWelcome(params: {
|
||||
const accessToken = data.session?.access_token || '';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/email/course/welcome', {
|
||||
const response = await fetch('/api/email/course/teacher_welcome', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -11,6 +11,7 @@ export const course: Writable<Course> = writable({
|
||||
description: '',
|
||||
cost: 0,
|
||||
currency: '',
|
||||
status: 'ACTIVE',
|
||||
is_certificate_downloadable: false,
|
||||
certificate_theme: 'professional',
|
||||
metadata: {
|
||||
@@ -20,6 +21,7 @@ export const course: Writable<Course> = writable({
|
||||
videoUrl: '',
|
||||
showDiscount: false,
|
||||
discount: 0,
|
||||
paymentLink: '',
|
||||
reward: {
|
||||
show: false,
|
||||
description: ''
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
export let isFetching = false;
|
||||
|
||||
$: {
|
||||
const user = $group.people.find(
|
||||
(person) => person.profile_id === $profile.id
|
||||
);
|
||||
const user = $group.people.find((person) => person.profile_id === $profile.id);
|
||||
|
||||
if (user) {
|
||||
isStudent = user.role_id === 3;
|
||||
@@ -58,6 +56,6 @@
|
||||
}
|
||||
|
||||
.rightBar.isMobile {
|
||||
margin-left: 10px;
|
||||
/* margin-left: 10px; */
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,36 +7,12 @@
|
||||
export let show = true;
|
||||
</script>
|
||||
|
||||
<section
|
||||
class="root z-10 {!show && 'hide'} {$isMobile ? 'fixed shadow-xl' : 'sticky'}"
|
||||
>
|
||||
<section class="root z-10">
|
||||
{#if show}
|
||||
<div class="relative h-full bg-white dark:bg-gray-800">
|
||||
<slot />
|
||||
</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 editor',
|
||||
hotkeys: ['B'],
|
||||
direction: 'right',
|
||||
}}
|
||||
>
|
||||
{#if show}
|
||||
<ChevronLeftIcon size={16} />
|
||||
{:else}
|
||||
<ChevronRightIcon size={16} />
|
||||
{/if}
|
||||
</IconButton>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -50,11 +26,6 @@
|
||||
left: 0;
|
||||
border-right: 1px solid var(--border-color);
|
||||
background-color: rgb(250, 251, 252);
|
||||
|
||||
&.hide {
|
||||
min-width: 15px;
|
||||
max-width: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.toggler {
|
||||
@@ -67,10 +38,5 @@
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.root {
|
||||
width: 98%;
|
||||
min-width: unset;
|
||||
max-width: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,32 +1,36 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import get from 'lodash/get';
|
||||
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 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 showDiscount;
|
||||
let giftToggled;
|
||||
let paymentLink = '';
|
||||
let showDiscount = false;
|
||||
let giftToggled = false;
|
||||
|
||||
let hasBeenSet = false;
|
||||
|
||||
function setDefaults(course) {
|
||||
function setDefaults(course: Course) {
|
||||
console.log('\n\nsetDefaults course', course);
|
||||
if (isEmpty(course) || hasBeenSet) return;
|
||||
hasBeenSet = true;
|
||||
|
||||
paymentLink = get(course, 'metadata.paymentLink', '');
|
||||
discount = get(course, 'metadata.discount', 0);
|
||||
showDiscount = get(course, 'metadata.showDiscount', false);
|
||||
giftToggled = get(course, 'metadata.reward.show', false);
|
||||
}
|
||||
|
||||
function setter(value, setterKey) {
|
||||
console.log('\n\nsetter course', value);
|
||||
function setter(value: string | number | boolean, setterKey: string) {
|
||||
if (typeof value !== 'boolean' && !value) return;
|
||||
|
||||
const _course = cloneDeep(course);
|
||||
@@ -34,66 +38,76 @@
|
||||
course = _course;
|
||||
}
|
||||
|
||||
function handleChange(html) {
|
||||
function handleChange(html: string) {
|
||||
const _course = cloneDeep(course);
|
||||
set(_course, 'metadata.reward.description', html);
|
||||
course = _course;
|
||||
}
|
||||
|
||||
$: setter(showDiscount, 'metadata.showDiscount');
|
||||
$: setter(paymentLink, 'metadata.paymentLink');
|
||||
$: setter(discount, 'metadata.discount');
|
||||
$: setter(giftToggled, 'metadata.reward.show');
|
||||
|
||||
$: setDefaults(course);
|
||||
</script>
|
||||
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label="Currency"
|
||||
value={course.currency}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{#if typeof course !== 'undefined'}
|
||||
<Select labelText="Currency" bind:selected={course.currency}>
|
||||
<SelectItem value="NGN" />
|
||||
<SelectItem value="USD" />
|
||||
</Select>
|
||||
|
||||
<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
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label="Discout Percent"
|
||||
label="Cost"
|
||||
type="number"
|
||||
bind:value={discount}
|
||||
helperMessage="In Percentage %"
|
||||
bind:value={course.cost}
|
||||
/>
|
||||
{/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 !isCourseFree(course.cost || 0)}
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label="Payment Link"
|
||||
helperMessage="Stripe, Lemon Squeezy or any payment link"
|
||||
bind:value={paymentLink}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#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 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
|
||||
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}
|
||||
|
||||
@@ -18,13 +18,16 @@
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
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 courseId;
|
||||
|
||||
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
|
||||
let loading = false;
|
||||
|
||||
let show = false;
|
||||
const sections = [
|
||||
{
|
||||
key: 1,
|
||||
@@ -115,69 +118,112 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Container show={true}>
|
||||
<!-- Publish Header Section -->
|
||||
{#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 />
|
||||
<aside
|
||||
class={`${
|
||||
show ? '-translate-x-[100%] fixed z-[9999]' : 'translate-x-0 fixed z-[9999]'
|
||||
} 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`}
|
||||
>
|
||||
<div class="toggler rounded-full shadow-lg absolute">
|
||||
<IconButton
|
||||
value="toggle"
|
||||
onClick={() => (show = !show)}
|
||||
size={$isMobile ? 'large' : 'small'}
|
||||
color="text-black"
|
||||
toolTipProps={$isMobile
|
||||
? {}
|
||||
: {
|
||||
title: 'Toggle editor',
|
||||
direction: 'right'
|
||||
}}
|
||||
>
|
||||
{#if show}
|
||||
<ChevronRightIcon size={16} />
|
||||
{:else}
|
||||
<ChevronLeftIcon size={16} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</Container>
|
||||
</IconButton>
|
||||
</div>
|
||||
<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>
|
||||
.title-content {
|
||||
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>
|
||||
|
||||
@@ -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="[email protected]"
|
||||
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">
|
||||
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 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 courseData: Course = {
|
||||
id: '',
|
||||
title: '',
|
||||
description: ''
|
||||
};
|
||||
export let editMode = false;
|
||||
export let courseData: Course;
|
||||
export let startCoursePayment = false;
|
||||
|
||||
let calculatedCost = 0;
|
||||
let discount = 0;
|
||||
let formatter: Intl.NumberFormat | undefined;
|
||||
let isFree = false;
|
||||
|
||||
function calcDisc(percent: number, cost: number) {
|
||||
if (!percent) return cost;
|
||||
@@ -23,27 +27,51 @@
|
||||
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) {
|
||||
if (!currency) return;
|
||||
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);
|
||||
$: discount = get(courseData, 'metadata.discount', 0);
|
||||
$: calculatedCost = calcDisc(discount, courseData.cost || 0);
|
||||
$: isFree = isCourseFree(calculatedCost);
|
||||
</script>
|
||||
|
||||
<PaymentModal
|
||||
bind:open={startCoursePayment}
|
||||
paymentLink={get(courseData, 'metadata.paymentLink', '')}
|
||||
courseName={courseData.title}
|
||||
teacherEmail={getTeacherEmail(courseData.group)}
|
||||
/>
|
||||
|
||||
<!-- 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">
|
||||
<!-- Pricing -->
|
||||
<div class="mb-6">
|
||||
<p class="dark:text-white font-medium text-lg">
|
||||
{formatter?.format(calculatedCost) || calculatedCost}
|
||||
{#if isFree}
|
||||
<span class="text-sm">(Free)</span>
|
||||
{/if}
|
||||
</p>
|
||||
{#if courseData?.metadata?.showDiscount}
|
||||
<p class="dark:text-white font-light text-sm text-gray-500">
|
||||
@@ -58,15 +86,9 @@
|
||||
<!-- Call To Action Buttons -->
|
||||
<div class="flex flex-col w-full items-center">
|
||||
<PrimaryButton
|
||||
label="Add to Cart"
|
||||
label={isFree ? 'Join Course' : 'Buy Now'}
|
||||
className="w-full sm:w-full py-3 mb-3"
|
||||
onClick={addToCart}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label="Buy Now"
|
||||
className="w-full sm:w-full py-3 mb-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={addToCart}
|
||||
onClick={handleJoinCourse}
|
||||
/>
|
||||
<p class="dark:text-white font-light text-sm text-gray-500">Early bird offer. Buy ASAP</p>
|
||||
</div>
|
||||
@@ -120,9 +142,9 @@
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media screen and (min-width: 768px) and (max-width: 1024px) {
|
||||
.price-container {
|
||||
width: 100%;
|
||||
width: 405px;
|
||||
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);
|
||||
|
||||
export let courseData: Course = {
|
||||
id: '',
|
||||
title: '',
|
||||
description: ''
|
||||
};
|
||||
export let editMode = false;
|
||||
export let courseData: Course;
|
||||
const ratingsImg = [
|
||||
'/images/rating-1.svg',
|
||||
'/images/rating-2.svg',
|
||||
@@ -43,6 +40,7 @@
|
||||
let player: any;
|
||||
let averageRating = 0;
|
||||
let totalRatings = 0;
|
||||
let startCoursePayment = false;
|
||||
|
||||
// initialize the expandDescription array with 'false' values for each review.
|
||||
let expandDescription = Array(reviews.length).fill(false);
|
||||
@@ -61,16 +59,6 @@
|
||||
activeNav = window.location.hash;
|
||||
}
|
||||
|
||||
function addToCart() {}
|
||||
|
||||
function buyNow() {
|
||||
// if (courseData.cost) {
|
||||
// paystackApi.tra;
|
||||
// } else {
|
||||
// // Course is free
|
||||
// }
|
||||
}
|
||||
|
||||
function initPlyr(_player: any, _video: string | undefined) {
|
||||
if (!player) return;
|
||||
|
||||
@@ -119,7 +107,13 @@
|
||||
<p class="dark:text-white author my-3 text-sm">
|
||||
{get(courseData, 'metadata.instructor.name', '')}
|
||||
</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>
|
||||
|
||||
<!-- Banner Image getEmbedId(videoUrl) -->
|
||||
@@ -164,14 +158,16 @@
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
<PricingSection className="md:hidden" {courseData} />
|
||||
<PricingSection className="md:hidden" {courseData} {editMode} bind:startCoursePayment />
|
||||
<!-- Body -->
|
||||
<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 -->
|
||||
<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 -->
|
||||
<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}
|
||||
<a
|
||||
href="{$page.url.pathname}{navItem.key}"
|
||||
@@ -416,7 +412,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Pricing Details -->
|
||||
<PricingSection className="hidden md:block" {courseData} />
|
||||
<PricingSection {courseData} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -464,7 +460,7 @@
|
||||
}
|
||||
|
||||
.course-content {
|
||||
max-width: 608px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
nav {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import PasswordEye from '../Icons/PasswordEye.svelte';
|
||||
|
||||
export let label = '';
|
||||
export let placeholder = '';
|
||||
export let value = '';
|
||||
export let value: string | number;
|
||||
export let name = '';
|
||||
export let onKeyDown = () => {};
|
||||
export let className = '';
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
export let logo;
|
||||
export let orgName;
|
||||
export let isOrgSite = false;
|
||||
export let backgroundColor = 'bg-blue';
|
||||
export let backgroundColor = 'bg-white';
|
||||
|
||||
let navClass = '';
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
|
||||
let selected = 0;
|
||||
let query = new URLSearchParams($page.url.search);
|
||||
@@ -18,19 +19,22 @@
|
||||
key: 0,
|
||||
label: 'Profile',
|
||||
tabKey: '',
|
||||
href: $page.url.pathname
|
||||
href: $page.url.pathname,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
label: 'Organization',
|
||||
tabKey: 'org',
|
||||
href: `${$page.url.pathname}?tab=org`
|
||||
href: `${$page.url.pathname}?tab=org`,
|
||||
disabled: !$isOrgAdmin
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
label: '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>
|
||||
{#each tabs as tab}
|
||||
<Tab label={tab.label} href={tab.href} />
|
||||
<Tab label={tab.label} href={tab.href} disabled={tab.disabled} />
|
||||
{/each}
|
||||
<!-- <Tab label="Account" /> -->
|
||||
<svelte:fragment slot="content">
|
||||
|
||||
@@ -82,7 +82,8 @@ export let landingPageSettings: Writable<OrgLandingPageJson> = writable({
|
||||
title: 'Our Story',
|
||||
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.',
|
||||
imageUrl: '',
|
||||
imageUrl:
|
||||
'https://koxqonvbkeakwvmdegcf.supabase.co/storage/v1/object/public/avatars/landingpage/riverside.jpeg',
|
||||
show: true
|
||||
},
|
||||
courses: {
|
||||
|
||||
@@ -19,12 +19,12 @@
|
||||
const menuItems = [
|
||||
{
|
||||
path: '',
|
||||
label: 'Home'
|
||||
},
|
||||
{
|
||||
path: '/quiz',
|
||||
label: 'Quizzes'
|
||||
label: 'Dashboard'
|
||||
},
|
||||
// {
|
||||
// path: '/quiz',
|
||||
// label: 'Quizzes'
|
||||
// },
|
||||
{
|
||||
path: '/courses',
|
||||
label: 'Courses'
|
||||
|
||||
@@ -7,6 +7,7 @@ export const VARIANTS = {
|
||||
CONTAINED_SUCCESS: 'CONTAINED_SUCCESS',
|
||||
OUTLINED: 'OUTLINED',
|
||||
NONE: 'NONE',
|
||||
LINK: 'LINK',
|
||||
TEXT: 'TEXT',
|
||||
TEXT_DANGER: 'TEXT_DANGER'
|
||||
};
|
||||
@@ -25,6 +26,7 @@ export const VARIANTS_CLASS = {
|
||||
[VARIANTS.NONE]:
|
||||
'border-none hover:border-black-300 hover:bg-gray-200 dark:hover:bg-gray-500 text-black',
|
||||
[VARIANTS.TEXT]: 'text-black hover:underline',
|
||||
[VARIANTS.LINK]: 'text-primary-500 dark:text-primary-500 hover:underline',
|
||||
[VARIANTS.TEXT_DANGER]:
|
||||
'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 isLoading = false;
|
||||
|
||||
$: isDisabled = isLoading ? true : false;
|
||||
$: isDisabled = isLoading ? true : isDisabled;
|
||||
</script>
|
||||
|
||||
<button
|
||||
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[
|
||||
isLoading ? VARIANTS.OUTLINED : variant
|
||||
]} {!disablePadding &&
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
</script>
|
||||
|
||||
<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}
|
||||
{#if !tab.icon && !tab.badgeValue}
|
||||
<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"
|
||||
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}
|
||||
<svelte:component this={tab.icon} />
|
||||
{/if}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
|
||||
export let id = '';
|
||||
export let value: string;
|
||||
export let onChange = (html = '') => {};
|
||||
export let onChange = (html: string) => {};
|
||||
export let height = 300;
|
||||
export let placeholder = '';
|
||||
export let editorWindowRef;
|
||||
export let editorWindowRef = null;
|
||||
|
||||
const apiKey = PUBLIC_TINYMCE_API_KEY;
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<input
|
||||
style="display:none"
|
||||
type="file"
|
||||
accept=".jpg, .jpeg, .png"
|
||||
accept=".jpg, .jpeg, .png, .webp"
|
||||
on:change={(e) => onFileSelected(e)}
|
||||
bind:this={fileinput}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,8 @@ export const ROUTE = {
|
||||
COURSE: '/course',
|
||||
LOGIN: '/login',
|
||||
SIGN_UP: '/signup',
|
||||
INVITE_SIGNUP: '/invite',
|
||||
INVITE_TEACHER: '/invite/t',
|
||||
INVITE_STUDENT: '/invite/s',
|
||||
PROFILE: '/profile',
|
||||
PEOPLE: '/people',
|
||||
DISCUSSIONS: '/discussions',
|
||||
@@ -20,7 +21,7 @@ export const PUBLIC_ROUTES = [
|
||||
`^${ROUTE.HOME}$`,
|
||||
ROUTE.LOGIN,
|
||||
ROUTE.SIGN_UP,
|
||||
`^${ROUTE.INVITE_SIGNUP}/.*`,
|
||||
`^${ROUTE.INVITE_TEACHER}/.*`,
|
||||
ROUTE.FORGOT,
|
||||
ROUTE.RESET,
|
||||
`^${ROUTE.PAGES}/.*`,
|
||||
@@ -31,7 +32,8 @@ export const PUBLIC_ROUTES = [
|
||||
export const ROUTES_TO_HIDE_NAV = [
|
||||
ROUTE.LOGIN,
|
||||
ROUTE.SIGN_UP,
|
||||
`^${ROUTE.INVITE_SIGNUP}/.*`,
|
||||
`^${ROUTE.INVITE_TEACHER}/.*`,
|
||||
`^${ROUTE.INVITE_STUDENT}/.*`,
|
||||
ROUTE.FORGOT,
|
||||
ROUTE.RESET,
|
||||
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) => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
const locale = currency == 'NGN' ? 'en-NG' : 'en-US';
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: '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);
|
||||
};
|
||||
|
||||
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 = {}) => {
|
||||
const { error } = resetValidationSchema.safeParse(fields);
|
||||
|
||||
|
||||
@@ -125,14 +125,16 @@ export async function getOrgAudience(orgId) {
|
||||
email,
|
||||
avatar_url,
|
||||
created_at,
|
||||
organizationmember!inner(
|
||||
groupmember!inner(
|
||||
role_id,
|
||||
organization_id
|
||||
group_id:group!inner(
|
||||
organization_id
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('organizationmember.organization_id', orgId)
|
||||
.eq('organizationmember.role_id', 3); // is a student, tutor is 2 and admin is 1
|
||||
.eq('groupmember.group.organization_id', orgId)
|
||||
.eq('groupmember.role_id', ROLE.STUDENT);
|
||||
|
||||
console.log('data', data);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { MailService } from '@sendgrid/mail';
|
||||
import { SENDGRID_API_KEY } from '$env/static/private';
|
||||
|
||||
export const SENDGRID_FROM = 'Rotimi Best <[email protected]>';
|
||||
export const SENDGRID_FROM_NOTIFY = 'ClassroomIO <notif[email protected]>';
|
||||
export const SENDGRID_FROM_NOREPLY = 'ClassroomIO <norepl[email protected]>';
|
||||
|
||||
export let sendgrid: MailService;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { writable, derived } from 'svelte/store';
|
||||
import { STEPS } from '../constants/quiz';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type { CurrentOrg, OrgTeamMember } from '../types/org';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
|
||||
export const orgs = writable<CurrentOrg[]>([]);
|
||||
export const currentOrg: Writable<CurrentOrg> = writable({
|
||||
@@ -17,6 +18,10 @@ export const currentOrg: Writable<CurrentOrg> = writable({
|
||||
});
|
||||
export const orgAudience = writable([]);
|
||||
export const orgTeam = writable<OrgTeamMember[]>([]);
|
||||
export const isOrgAdmin = derived(
|
||||
currentOrg,
|
||||
($currentOrg) => parseInt($currentOrg.role_id) === ROLE.ADMIN
|
||||
);
|
||||
export const currentOrgPath = derived(currentOrg, ($currentOrg) =>
|
||||
$currentOrg.siteName ? `/org/${$currentOrg.siteName}` : ''
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ interface CourseMetadata {
|
||||
videoUrl?: '';
|
||||
showDiscount: false;
|
||||
discount: 0;
|
||||
paymentLink?: string;
|
||||
reward?: {
|
||||
show: boolean;
|
||||
description: string;
|
||||
@@ -64,6 +65,8 @@ export interface Group {
|
||||
updated_at?: string;
|
||||
organization_id?: string /* foreign key to organization.id */;
|
||||
organization?: Organization;
|
||||
tutors: Profile[];
|
||||
members: Groupmember[];
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
@@ -93,7 +96,7 @@ export interface Course {
|
||||
logo?: string;
|
||||
slug?: any; // type unknown;
|
||||
metadata?: CourseMetadata;
|
||||
cost?: number;
|
||||
cost: number;
|
||||
currency?: string;
|
||||
group?: Group;
|
||||
organization?: Organization;
|
||||
|
||||
Reference in New Issue
Block a user