feat: 🚀 😃 convert repo into a monorepo 🚀 😃 (#51)

* Merge main into turbo-monrepo

* feat: rename apps

* feat: remove supbase videos and serve all from project

* feat: move supabase to root

* feat: update README

* docs: update link
This commit is contained in:
Rotimi Best
2023-11-25 21:56:02 +00:00
committed by GitHub
parent 03ca379094
commit 41e29014ef
806 changed files with 85853 additions and 5746 deletions
@@ -0,0 +1,144 @@
<script lang="ts">
import Modal from '$lib/components/Modal/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { profile } from '$lib/utils/store/user';
import { generateSitename } from '$lib/utils/functions/org';
import { getOrganizations } from '$lib/utils/services/org';
import { blockedSubdomain } from '$lib/utils/constants/app';
import { newOrgModal } from '../store';
import { snackbar } from '$lib/components/Snackbar/store';
import { createOrgValidation } from '$lib/utils/functions/validator';
import { goto } from '$app/navigation';
type Error = {
orgName: string;
siteName: string;
};
let loading = false;
let orgName = '';
let siteName = '';
let errors: Error = {
orgName: '',
siteName: ''
};
function resetForm() {
orgName = '';
siteName = '';
loading = false;
errors = {
orgName: '',
siteName: ''
};
}
async function createNewOrg() {
errors = createOrgValidation({
orgName,
siteName
}) as Error;
if (Object.values(errors).length) {
loading = false;
return;
}
// Validate if domain is among our seculeded subdomains
if (blockedSubdomain.includes(siteName || '')) {
errors.siteName = 'Sitename already exists.';
loading = false;
return;
}
const { data: org, error } = await supabase
.from('organization')
.insert({
name: orgName,
siteName: siteName
})
.select();
console.log('Create organisation', org);
if (error) {
console.log('Error: create organisation', error);
errors.siteName = 'Sitename already exists.';
loading = false;
return;
}
if (Array.isArray(org) && org.length) {
const orgData = org[0];
const { data, error } = await supabase
.from('organizationmember')
.insert({
organization_id: orgData.id,
profile_id: $profile.id,
role_id: 1
})
.select();
console.log('Create organisation member', data);
if (error) {
console.log('Error: create organisation member', error);
errors.siteName =
'Something went wrong while creating this organization. Please reload and try again';
// Delete organization so it can be recreated.
await supabase.from('organization').delete().match({ siteName });
loading = false;
return;
}
snackbar.success();
await getOrganizations($profile.id);
resetForm();
goto(`/org/${siteName}`);
$newOrgModal.open = false;
}
}
$: siteName = generateSitename(orgName);
</script>
<Modal
onClose={() => ($newOrgModal.open = false)}
bind:open={$newOrgModal.open}
width="w-96"
modalHeading="Create Organization"
>
<form on:submit|preventDefault={createNewOrg} class="px-2">
<TextField
label="Name of Organization"
bind:value={orgName}
autoFocus={true}
placeholder="e.g Pepsi Co"
className="mb-4"
isRequired={true}
errorMessage={errors.orgName}
autoComplete={false}
/>
<!-- Org Site Name -->
<TextField
label="Organisation Site name"
helperMessage={`https://${siteName || ''}.classroomio.com`}
bind:value={siteName}
name="sitename"
type="text"
placeholder="e.g edforall"
className="mb-5 w-full"
labelClassName="text-lg font-normal"
errorMessage={errors.siteName}
isRequired={true}
/>
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton className="px-6 py-3" label="Create" type="submit" isLoading={loading} />
</div>
</form>
</Modal>
@@ -0,0 +1,47 @@
<script>
import {
DataTable,
Toolbar,
ToolbarContent,
ToolbarSearch,
Pagination,
DataTableSkeleton,
} from 'carbon-components-svelte';
import { orgAudience } from '$lib/utils/store/org';
export let isLoading = false;
const headers = [
{ key: 'name', value: 'Name' },
{ key: 'email', value: 'Email' },
{ key: 'date_joined', value: 'Date Joined' },
];
let pageSize = 5;
let page = 1;
let filteredRowIds = [];
let searchValue = '';
</script>
{#if isLoading}
<DataTableSkeleton {headers} rows={pageSize} />
{:else}
<DataTable size="tall" {headers} rows={$orgAudience} {pageSize} {page}>
<Toolbar>
<ToolbarContent>
<ToolbarSearch
persistent
value={searchValue}
shouldFilterRows
bind:filteredRowIds
/>
</ToolbarContent>
</Toolbar>
</DataTable>
{/if}
<Pagination
bind:pageSize
bind:page
totalItems={filteredRowIds.length}
pageSizeInputDisabled
/>
@@ -0,0 +1,33 @@
<script>
import Box from '$lib/components/Box/index.svelte';
import AudienceList from './AudienceList.svelte';
import AudienceEmptyIcon from '$lib/components/Icons/AudienceEmptyIcon.svelte';
import { getOrgAudience } from '$lib/utils/services/org';
import { orgAudience, currentOrg } from '$lib/utils/store/org';
let isLoading = false;
async function fetchInitData(orgId) {
if (!orgId) return;
isLoading = true;
await getOrgAudience(orgId);
isLoading = false;
}
$: fetchInitData($currentOrg.id);
</script>
<div class="flex items-center justify-center lg:justify-start flex-wrap my-4 m-auto">
{#if $orgAudience.length || isLoading}
<AudienceList {isLoading} />
{:else}
<Box>
<AudienceEmptyIcon />
<h3 class="dark:text-white text-2xl text-center my-5">No audience to manage!</h3>
<p class="dark:text-white w-1/3 text-center">
Manage all your students here and stay connected with them.
</p>
</Box>
{/if}
</div>
@@ -0,0 +1,30 @@
<script>
import Modal from '$lib/components/Modal/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
export let onDelete = () => {};
export let onCancel = () => {};
export let open = false;
</script>
<Modal onClose={() => (open = false)} bind:open width="w-96" modalHeading="Delete comment">
<div>
<h1 class="dark:text-white text-lg">Are you sure you want to delete this comment?</h1>
<div class="mt-5 flex items-center justify-between">
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="No, keep it"
onClick={onCancel}
/>
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.CONTAINED}
label="Yes, delete"
onClick={onDelete}
/>
</div>
</div>
</Modal>
@@ -0,0 +1,25 @@
<script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
</script>
<div
class="h-[70px] w-full border border-gray-100 dark:bg-neutral-800 mr-4 mb-4 mt-2 rounded-md transition ease-in-out relative flex pt-3 px-3"
>
<div class="w-10 flex justify-evenly flex-col px-1 mr-2 rounded-md">
<SkeletonPlaceholder style="width: 100%; height: 40%; border-radius: 4px;" />
<SkeletonPlaceholder style="width: 100%; height: 40%; border-radius: 4px;" />
</div>
<div class="w-9/12 mt-1.5 flex justify-evenly flex-col rounded-md">
<!-- first text -->
<div class="w-3/6 h-full">
<SkeletonText style="height: 50%; border-radius: 4px;" />
</div>
<!-- second text -->
<SkeletonText style="width: 10%; height: 50%; border-radius: 4px;" />
</div>
<div class="w-10 justify-evenly flex-col px-1 mt-1.5 ml-auto rounded-md">
<SkeletonPlaceholder style="width: 100%; height: 41%; border-radius: 4px;" />
</div>
</div>
@@ -0,0 +1,117 @@
<script>
import { goto } from '$app/navigation';
import AddCommentIcon from 'carbon-icons-svelte/lib/AddComment.svelte';
import CommunityLoader from './Loader.svelte';
import Vote from '$lib/components/Vote/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import Space from '$lib/components/Space/index.svelte';
import { currentOrgPath, currentOrg } from '$lib/utils/store/org';
import { supabase } from '$lib/utils/functions/supabase';
import { calDateDiff } from '$lib/utils/functions/date';
export let isLMS = false;
let isLoading = false;
let discussions = [];
async function fetchCommunityQuestions(orgId) {
if (!orgId) return;
isLoading = true;
const { data, error } = await supabase
.from('community_question')
.select(
`
organization_id,
title,
votes,
created_at,
slug,
comments:community_answer(count),
author:organizationmember!community_question_author_id_fkey!inner(
profile!inner(fullname)
)
`
)
.eq('organization_id', orgId);
console.log('data', data);
console.log('error', error);
isLoading = false;
if (error) {
console.error('Error loading community', error);
return goto(isLMS ? '/lms' : $currentOrgPath);
}
discussions =
data?.map((discussion) => ({
title: discussion.title,
slug: discussion.slug,
author: discussion?.author?.profile?.fullname,
comments: discussion.comments?.[0]?.count || 0,
votes: discussion.votes,
createdAt: calDateDiff(discussion.created_at)
})) || [];
}
$: fetchCommunityQuestions($currentOrg.id);
</script>
<div
class="flex items-center justify-center lg:justify-start flex-wrap my-4 m-auto border-c rounded bg-gray-100 dark:bg-neutral-800"
>
{#if isLoading}
<CommunityLoader />
<CommunityLoader />
<CommunityLoader />
<CommunityLoader />
{:else}
{#each discussions as discussion}
<div class="w-full flex border-bottom-c p-5">
<Vote value={discussion.votes} />
<div class="discussion-topic-author">
<h4>
<a
class="text-black dark:text-white"
href="{isLMS ? '/lms' : $currentOrgPath}/community/{discussion.slug}"
>
{discussion.title}
</a>
</h4>
<span class="text-gray-600 dark:text-white">
{discussion.author} asked {discussion.createdAt}
</span>
</div>
<Space />
<div class="flex items-center">
<AddCommentIcon size={20} />
<span class="ml-1">{discussion.comments}</span>
</div>
</div>
{:else}
<Box className="w-screen">
<CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Questions asked</h3>
<p class="dark:text-white w-1/3 text-center">Ask a question to the community</p>
</Box>
{/each}
{/if}
</div>
<style>
h4 {
font-size: 16px;
font-weight: 900;
word-break: break-word;
overflow-wrap: break-word;
margin: 0;
}
.discussion-topic-author {
margin-right: 20px;
}
.discussion-topic-author span {
font-size: 12px;
}
</style>
@@ -0,0 +1,21 @@
<script>
import { Search } from 'carbon-components-svelte';
let searchValue = '';
</script>
<section class="bg-[#040f2d] min-h-[337px] w-full flex items-center justify-center p-">
<div class="w-full flex items-center justify-center flex-col-reverse md:flex-row">
<!-- Course Description -->
<div class="course-descr w-11/12 py-10">
<h1 class="dark:text-white text-5xl text-white text-center my-4 font-bold">
Explore courses
</h1>
<p class="dark:text-white text-md text-white mb-6 text-center">
Find courses you will love from best teachers all over the world🌎.
</p>
<Search placeholder="Find a course..." bind:value={searchValue} />
</div>
</div>
</section>
@@ -0,0 +1,28 @@
<script>
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte';
</script>
<section class="w-screen h-screen p-5">
<!-- navbar -->
<div class="flex items-center justify-between w-full rounded-md overflow-hidden">
<SkeletonText style="width:80%; margin-right:10rem;border-radius:10px" />
<div class="flex items-center justify-between w-[20%]">
<SkeletonText style="width:2%; margin-right:1rem; border-radius:10px" />
<SkeletonText style="width:2%; margin-right:1rem; border-radius:10px" />
<SkeletonText style="width:2%; border-radius:10px" />
</div>
</div>
<!-- hero -->
<div
class="flex flex-row gap-5 items-start justify-center flex-wrap w-full my-8 rounded-md overflow-hidden"
>
<SkeletonPlaceholder style="width:100%; height:20rem" />
</div>
<!-- cards -->
<div class="flex gap-5 items-center justify-center flex-wrap my-4 rounded-md overflow-hidden">
<CardLoader />
<CardLoader />
<CardLoader />
</div>
</section>
@@ -0,0 +1,582 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import get from 'lodash/get';
import { onMount } from 'svelte';
import LogoFacebook from 'carbon-icons-svelte/lib/LogoFacebook.svelte';
import LogoTwitter from 'carbon-icons-svelte/lib/LogoTwitter.svelte';
import LogoLinkedin from 'carbon-icons-svelte/lib/LogoLinkedin.svelte';
import { Accordion, AccordionItem, Grid, Row, Column } from 'carbon-components-svelte';
import LocationFilled from 'carbon-icons-svelte/lib/LocationFilled.svelte';
import Phone from 'carbon-icons-svelte/lib/Phone.svelte';
import Email from 'carbon-icons-svelte/lib/Email.svelte';
import Rocket from 'carbon-icons-svelte/lib/Rocket.svelte';
import PageLoader from './PageLoader.svelte';
import { orgLandingpageValidation } from '$lib/utils/functions/validator';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Navigation from '$lib/components/Navigation/index.svelte';
import Box from '$lib/components/Box/index.svelte';
import Card from '$lib/components/Courses/components/Card/index.svelte';
import CardLoader from '$lib/components/Courses/components/Card/Loader.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
import { getCourseBySiteName } from '$lib/utils/services/org';
import { currentOrg } from '$lib/utils/store/org';
import { validateEmail } from '$lib/utils/functions/validateEmail';
import TextArea from '$lib/components/Form/TextArea.svelte';
import { goto } from '$app/navigation';
import { landingPageSettings } from '$lib/components/Org/Settings/store';
export let orgSiteName = '';
export let org = {};
let email: string | undefined;
let isAdding = false;
let success = false;
let successContactSaved = false;
let viewAll = false;
let isContactSubmiting = false;
let player;
let contact = {
name: '',
email: '',
phone: '',
message: ''
};
let contactError = {};
const supabase = getSupabase();
async function handleSubmit() {
if (!email || !validateEmail(email)) return;
isAdding = true;
const { error } = await supabase.from('organization_emaillist').insert({
email,
organization_id: org.id
});
if (error) {
console.error('Error', error);
}
success = true;
setTimeout(() => {
isAdding = false;
success = false;
email = undefined;
}, 5000);
}
async function handleContactSubmit() {
isContactSubmiting = true;
contactError = orgLandingpageValidation(contact);
if (Object.keys(contactError).length) {
isContactSubmiting = false;
return;
}
// Save to db
const { error } = await supabase.from('organization_contacts').insert({
name: contact.name,
email: contact.email,
phone: contact.phone,
message: contact.message,
organization_id: org.id
});
if (error) {
console.error('Something went wrong', error, '\n\nContact', contact);
isContactSubmiting = false;
}
isContactSubmiting = false;
successContactSaved = true;
contact = {
name: '',
email: '',
phone: '',
message: ''
};
}
function isYouTubeLink(link: string) {
const youtubeRegex =
/^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
return youtubeRegex.test(link.trim());
}
onMount(async () => {
if (!orgSiteName) return;
try {
console.log('sitename', orgSiteName);
$courseMetaDeta.isLoading = true;
const coursesResult = await getCourseBySiteName(orgSiteName);
courses.set(coursesResult);
$courseMetaDeta.isLoading = false;
} catch (error) {
console.log('error', error);
}
});
function initPlyr(_player: any, _video: string | undefined) {
if (!player) return;
// @ts-ignore
const plyr = new Plyr('#player', { width: '400px', height: '300px', borderRadius: '10px' });
// @ts-ignore
window.player = plyr;
}
// Use default data in store if user hasn't added their own content to landing page
function setDefault(landingpage) {
if (landingpage && Object.keys(landingpage).length) {
if (!landingpage?.header?.banner) {
landingpage.header.banner = $landingPageSettings.header.banner;
}
$landingPageSettings = {
...landingpage
};
}
}
$: initPlyr(player, $landingPageSettings.header?.banner?.video);
$: setDefault(org.landingpage);
</script>
<svelte:head>
<title>
{!org.name ? '' : `${org.name}'s `}Landing Page
</title>
</svelte:head>
{#if !org.landingpage}
<PageLoader />
{:else}
<main>
<!-- Header Section -->
{#if $landingPageSettings.header.show}
<header id="header" class="banner w-full h-[100vh] md:h-[90vh] mb-10 relative">
<Navigation logo={org.avatar_url} orgName={org.name} disableSignup={true} />
<div class="absolute h-[100vh] md:h-[90vh] top-0 w-full opacity-80 z-10 bg-white" />
{#if $landingPageSettings.header.banner.show}
<div class="flex items-center justify-center md:h-full py-2">
<div
class="md:w-11/12 lg:w-5/6 w-full flex items-center justify-center md:justify-between flex-col-reverse md:flex-row z-20 relative"
>
<!-- Course Description -->
<div class="md:w-2/5 w-11/12 py-10 flex flex-col items-center md:items-start mr-3">
<p class=" text-primary-600 text-2xl font-semibold capitalize">
{org.name}
</p>
<h1
class="text-4xl md:text-5xl lg:text-6xl text-center md:text-start my-4 font-bold"
>
{$landingPageSettings.header.title} <br /><span class="text-primary-600"
>{$landingPageSettings.header.titleHighlight}</span
>
</h1>
<p class="text-md mb-3 md:mb-5 text-xl text-center md:text-start">
{$landingPageSettings.header.subtitle}
</p>
<PrimaryButton
label={$landingPageSettings.header.action.label}
className="mt-2 md:mt-5 px-10 w-fit"
onClick={() => {
$landingPageSettings.header.action.redirect &&
goto($landingPageSettings.header.action.link);
}}
/>
</div>
<div class="w-5/6 md:w-1/2 md:max-w-[650px] flex">
{#if isYouTubeLink($landingPageSettings.header?.banner?.video) && $landingPageSettings.header.banner.type === 'video'}
<div bind:this={player} id="player" style="width:100%; border-radius:12px">
<iframe
title="Header video"
src={$landingPageSettings.header?.banner?.video}
allowfullscreen
allowtransparency
allow="autoplay"
/>
</div>
{:else}
<!-- <video class="w-full rounded-xl" controls loop autoplay>
<source src={$landingPageSettings.header?.banner?.video} type="video/mp4" />
<source src="/path/to/video.webm" type="video/webm" />
Captions are optional
<track kind="captions" />
</video> -->
<img
class="rounded-md"
src={$landingPageSettings.header?.banner?.image}
alt="landing page banner"
/>
{/if}
</div>
</div>
</div>
{:else}
<div class="w-full h-full flex items-center justify-center md:flex-row z-20 relative">
<div class="max-w-[600px] mx-auto w-11/12 py-10 flex flex-col items-center">
<p class=" text-primary-600 text-2xl font-semibold capitalize">{org.name}</p>
<h1 class="text-4xl md:text-5xl lg:text-6xl text-center my-4 font-bold">
{$landingPageSettings.header.title} <br /><span class="text-primary-600"
>{$landingPageSettings.header.titleHighlight}</span
>
</h1>
<p class="text-md mb-6 text-center text-xl">
{$landingPageSettings.header.subtitle}
</p>
<PrimaryButton
label={$landingPageSettings.header.action.label}
className="mt-5 px-10 w-fit"
onClick={() => {
$landingPageSettings.header.action.redirect &&
goto($landingPageSettings.header.action.link);
}}
/>
</div>
</div>
{/if}
</header>
{/if}
<!-- Our Story section -->
{#if $landingPageSettings.aboutUs.show}
<section id="aboutus" class="m-h-[400px] my-10 mx-auto max-w-6xl w-full">
<div class="mx-4 flex items-center justify-evenly flex-col lg:flex-row">
<div class="max-w-[600px] lg:w-2/5 mr-5 mb-5 lg:mb-0">
<h2 class="text-4xl md:text-5xl lg:text-6xl">{$landingPageSettings.aboutUs.title}</h2>
<p class="mb-2">
{$landingPageSettings.aboutUs.content}
</p>
</div>
<div class="image">
<img
src={$landingPageSettings.aboutUs.imageUrl}
alt="Our Story"
class=" rounded-2xl max-h-[450px]"
/>
</div>
</div>
</section>
{/if}
<!-- Courses Section -->
{#if $landingPageSettings.courses.show}
<section id="courses" transition:fade class="my-10 mx-auto max-w-6xl w-full">
<div class="w-full">
<div class="max-w-[500px] mx-auto w-11/12 py-10">
<h1 class="text-4xl md:text-5xl lg:text-6xl text-center my-4 font-bold">
{$landingPageSettings.courses.title}
<span class="text-primary-600">{$landingPageSettings.courses.titleHighlight}</span>
</h1>
<p class="text-md text-center">
{$landingPageSettings.courses.subtitle}
</p>
</div>
</div>
{#if $courseMetaDeta.isLoading}
<div class="cards-container my-4 mx-2">
<CardLoader />
<CardLoader />
<CardLoader />
</div>
{:else if $courses.length > 0}
<div class="cards-container my-4 mx-2">
{#each $courses.slice(0, viewAll ? $courses.length : 3) as courseData}
<Card
id={courseData.id}
slug={courseData.slug}
bannerImage={courseData.logo || '/images/classroomio-course-img-template.jpg'}
title={courseData.title}
description={courseData.description}
role_id={courseData.role_id}
isPublished={courseData.is_published}
cost={courseData.cost}
currency={courseData.currency}
totalLessons={get(courseData, 'lessons[0].count', 0)}
isOnLandingPage={true}
/>
{/each}
</div>
{:else}
<Box>
<CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Course Published</h3>
<p class="dark:text-white w-1/3 text-center">
We've got great courses coming your way, stay tuned!!!
</p>
</Box>
{/if}
{#if $courses.length > 3}
<div class="w-full mt-3 flex justify-center">
<PrimaryButton
variant={VARIANTS.OUTLINED}
onClick={() => (viewAll = !viewAll)}
label={viewAll ? 'View Less' : 'View All'}
className="px-10 py-5 w-fit"
/>
</div>
{/if}
</section>
{/if}
<!-- FAQ Section -->
{#if $landingPageSettings.faq.show}
<section id="faq" transition:fade class="my-10 mx-auto max-w-[700px] w-full">
<div class="py-10">
<h1 class="text-4xl md:text-5xl lg:text-6xl text-center my-4 font-bold">
{$landingPageSettings.faq.title}
</h1>
</div>
<div class="mx-2">
<Accordion size="xl">
{#each $landingPageSettings.faq.questions as faq}
<AccordionItem title={faq.title} class="text-3xl">
<p class="text-lg">
{faq.content}
</p>
</AccordionItem>
{/each}
</Accordion>
</div>
</section>
{/if}
<!-- Contact Section-->
{#if $landingPageSettings.contact.show}
<section id="contact" transition:fade class="my-10 w-full bg-primary-50">
<div class="mx-auto max-w-6xl w-full">
<div class="max-w-[500px] mx-auto w-11/12 py-10">
<h1 class="text-4xl md:text-5xl lg:text-6xl text-center my-4 font-bold">
{$landingPageSettings.contact.title}
<span class="text-primary-600">{$landingPageSettings.contact.titleHighlight}</span>
</h1>
<p class="text-md text-center">{$landingPageSettings.contact.subtitle}</p>
</div>
<Grid class="max-w-[700px] pb-10">
<!-- Contact Details -->
<Row>
<Column
class="flex items-center flex-col justify-center break-all text-center cursor-pointer hover:shadow-xl rounded-lg transition-all duration-500 py-2 mx-2"
>
<LocationFilled size={32} />
<p class="text-xs md:text-sm mt-3 max-w-[200px]">
{$landingPageSettings.contact.address}
</p>
</Column>
<Column
class="flex items-center flex-col justify-center break-all text-center cursor-pointer hover:shadow-xl rounded-lg transition-all duration-500 py-2 mx-2"
>
<Phone size={32} />
<p class="text-xs md:text-sm mt-3">{$landingPageSettings.contact.phone}</p>
</Column>
<Column
class="flex items-center flex-col justify-center break-all text-center cursor-pointer hover:shadow-xl rounded-lg transition-all duration-500 py-2 mx-2"
>
<Email size={32} />
<p class="text-xs md:text-sm mt-3">{$landingPageSettings.contact.email}</p>
</Column>
</Row>
<!-- Contact Form -->
<div class="mt-8 bg-white p-7 rounded-lg">
{#if successContactSaved}
<div class="w-full flex items-center justify-center">
Thank you for dropping a message, we will get back to you shortly
</div>
{:else}
<form on:submit|preventDefault={handleContactSubmit}>
<div class="w-full flex justify-between flex-col md:flex-row">
<div class="w-full md:w-2/4 mr-5">
<TextField
label="Your Name"
bind:value={contact.name}
errorMessage={contactError.name}
className="mb-5"
labelClassName="font-bold"
placeholder="Elon Musk"
/>
<TextField
label="Your Email"
bind:value={contact.email}
errorMessage={contactError.email}
className="text-xs font-normal mb-5"
placeholder="[email protected]"
/>
<TextField
label="Your Phone"
bind:value={contact.phone}
errorMessage={contactError.phone}
className="text-xs font-normal mb-5"
placeholder="+1194802480"
/>
</div>
<div class="w-full md:w-2/4">
<TextArea
label="Your Message"
bind:value={contact.message}
errorMessage={contactError.message}
rows="9"
maxRows={15}
placeholder="Your message here"
/>
</div>
</div>
<PrimaryButton
className="w-full mx-auto mt-5 md:mt-0"
type="submit"
isLoading={isContactSubmiting}
>
<span class="mr-2 text-md">Submit</span>
<Rocket size={24} />
</PrimaryButton>
</form>
{/if}
</div>
</Grid>
</div>
</section>
{/if}
<!-- Waitlist Section -->
{#if $landingPageSettings.mailinglist.show}
<section id="waitlist" transition:fade class="my-10 mx-auto max-w-6xl w-[95%]">
<div
class="bg-primary-700 rounded-lg flex flex-col lg:flex-row lg:items-center px-4 md:px-10 py-14"
>
<div class="w-full md:w-[65%] md:mr-4">
<h1 class="text-4xl font-bold mb-5 mt-0 text-white">
{$landingPageSettings.mailinglist.title}
</h1>
<p class="text-lg text-white">
{$landingPageSettings.mailinglist.subtitle}
</p>
</div>
<form on:submit|preventDefault={handleSubmit} class="my-4 w-full md:w-fit">
<div class="flex items-center flex-col sm:flex-row">
{#if success}
<p class="text-white">You have been added successfully. Thanks for subscribing.</p>
{:else}
<TextField
bind:value={email}
type="email"
placeholder="Enter your email.."
className="sm:mr-3 my-0 w-full md:w-fit"
isRequired={true}
isDisabled={isAdding}
inputClassName="py-2"
/>
<PrimaryButton
className="my-1 w-full mt-2"
variant={VARIANTS.CONTAINED_LIGHT}
type="submit"
isLoading={isAdding}>{$landingPageSettings.mailinglist.buttonLabel}</PrimaryButton
>
{/if}
</div>
</form>
</div>
</section>
{/if}
<!-- Footer Section -->
{#if $landingPageSettings.footer.show}
<footer
id="footer"
class="flex justify-center flex-col mt-10 w-full px-5 py-10 md:py-3 border-b-0 border-r-0 border-t border-l-0 border-gray-300"
>
<ul class="flex w-11/12 items-center flex-col sm:flex-row">
<div class="logo">
<a
href="/"
title={`Go to ${org.name} Home`}
id="logo"
data-hveid="8"
class="flex items-center"
>
<img
src={org.avatar_url || '/logo-192.png'}
alt={`${org.name} logo`}
class="rounded h-10 w-10 inline-block mx-auto"
data-atf="1"
/>
<h3 class="text-black ml-3 text-xl">{org.name}</h3>
</a>
</div>
<span class="flex-grow" />
<div class="flex mt-5 sm:mt-0">
{#if $landingPageSettings.footer.facebook}
<li class="mx-2">
<a
href={$landingPageSettings.footer.facebook}
target="_blank"
title="Facebook"
id="logo-fb"
data-hveid="8"
>
<LogoFacebook size={24} />
</a>
</li>
{/if}
{#if $landingPageSettings.footer.twitter}
<li class="mx-2">
<a
href={$landingPageSettings.footer.twitter}
target="_blank"
title="Twitter"
id="logo-tw"
data-hveid="8"
>
<LogoTwitter size={24} />
</a>
</li>
{/if}
{#if $landingPageSettings.footer.linkedin}
<li class="mx-2">
<a
href={$landingPageSettings.footer.linkedin}
target="_blank"
title="Linkedin"
id="logo-ln"
data-hveid="8"
>
<LogoLinkedin size={24} />
</a>
</li>
{/if}
</div>
</ul>
<p class="text-center mt-5">
Powered by
<a class="text-primary-600 underline" href="https://classroomio.com" target="_blank"
>ClassroomIO</a
>
</p>
</footer>
{/if}
</main>
{/if}
<style>
.banner {
background: url('/images/org-landingpage-banner.jpeg') no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
</style>
@@ -0,0 +1,66 @@
<script>
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Modal from '$lib/components/Modal/index.svelte';
import { deleteModal, quizesStore } from '$lib/utils/store/org';
import { supabase } from '$lib/utils/functions/supabase';
export let onDelete = () => {};
function closeModal() {
deleteModal.update((m) => ({
...m,
open: false,
id: null,
isQuestion: false
}));
}
async function deleteQuiz() {
const { data, error } = await supabase.from('quiz').delete().match({ id: $deleteModal.id });
console.log('data', data);
console.log('error', error);
if (error) {
return;
}
$quizesStore = $quizesStore.filter((q) => q.id !== $deleteModal.id);
closeModal();
}
</script>
<Modal
onClose={closeModal}
bind:open={$deleteModal.open}
width="w-2/5"
modalHeading={$deleteModal.isQuestion ? 'Delete Question' : 'Delete Quiz'}
size="sm"
>
<div class="w-full flex flex-col items-center my-10">
<h1 class="dark:text-white text-lg text-center">
{#if $deleteModal.isQuestion}
Are you sure you want to delete this question?
{:else}
Are you sure you want to delete this quiz?
{/if}
You will lose your content and this action can’t be undone.
</h1>
<div class="mt-5">
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.CONTAINED}
label="Delete"
onClick={() => {
if ($deleteModal.isQuestion) {
onDelete();
$deleteModal.open = false;
} else {
deleteQuiz();
}
}}
/>
</div>
</div>
</Modal>
@@ -0,0 +1,21 @@
<script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
</script>
<div
class="root w-full border border-gray-100 dark:bg-neutral-800 mr-4 mb-4 rounded-md transition ease-in-out relative flex p-3"
>
<SkeletonPlaceholder style="width: 20%; height: 100%;" />
<div class="w-full mx-5 flex justify-evenly flex-col">
<!-- Title -->
<SkeletonText style="width: 50%;" />
<!-- Description -->
<SkeletonText style="width: 50%; height: 4rem;" />
</div>
</div>
<style>
.root {
height: 180px;
}
</style>
@@ -0,0 +1,131 @@
<script>
import { goto } from '$app/navigation';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import { supabase } from '$lib/utils/functions/supabase';
import { currentOrg, createQuizModal, currentOrgPath, quizesStore } from '$lib/utils/store/org';
import { createQuizValidation } from '$lib/utils/functions/validator';
let open = false;
let errors = {
title: ''
};
let isLoading = false;
function handleClose() {
$createQuizModal.id = null;
$createQuizModal.title = '';
$createQuizModal.open = false;
$createQuizModal.openEdit = false;
}
function errorNotification() {
snackbar.error();
}
async function createQuiz() {
errors = createQuizValidation($createQuizModal);
console.log('createQuiz|updateQuiz errors', errors);
if (Object.keys(errors).length) {
return;
}
isLoading = true;
if ($createQuizModal.openEdit) {
const { error } = await supabase
.from('quiz')
.update({ title: $createQuizModal.title, updated_at: new Date() })
.match({ id: $createQuizModal.id });
isLoading = false;
if (error) {
return errorNotification();
} else {
$quizesStore = $quizesStore.map((s) => {
if (s.id === $createQuizModal.id) {
s.title = $createQuizModal.title;
}
return s;
});
handleClose();
}
} else {
const { data, error } = await supabase
.from('quiz')
.insert({
title: $createQuizModal.title,
organization_id: $currentOrg.id,
questions: [
{
id: 1,
label: '',
type: 'multichoice',
options: [
{
id: 'circle',
label: '',
isCorrect: false
},
{
id: 'triangle',
label: '',
isCorrect: false
}
]
}
]
})
.select();
isLoading = false;
if (error) {
return errorNotification();
} else {
$quizesStore = [...$quizesStore, data[0]];
handleClose();
goto(`${$currentOrgPath}/quiz/${data[0].id}`);
}
}
}
$: open = $createQuizModal.openEdit || $createQuizModal.open;
</script>
<svelte:head>
<title>Quizzes - ClassroomIO</title>
</svelte:head>
<Modal
onClose={handleClose}
bind:open
width="w-4/5 md:w-2/5"
modalHeading="{$createQuizModal.openEdit ? 'Update' : 'Create'} a Quiz"
>
<form on:submit|preventDefault={createQuiz}>
<TextField
label="Quiz Title"
bind:value={$createQuizModal.title}
autofocus={true}
placeholder="Your quiz title"
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
<div class="mt-5 flex items-center justify-end">
<PrimaryButton
className="px-6 py-3"
label={$createQuizModal.openEdit ? 'Save Changes' : 'Continue'}
type="submit"
isDisabled={isLoading}
{isLoading}
/>
</div>
</form>
</Modal>
@@ -0,0 +1,54 @@
<script>
import { onMount } from 'svelte';
import { SyncLoader } from 'svelte-loading-spinners';
import PlayContainer from './Container.svelte';
import PlayHeader from './Header/index.svelte';
import { quizStore, playQuizStore } from '$lib/utils/store/org';
import { STEPS } from '$lib/utils/constants/quiz';
import { genQuizPin } from '$lib/utils/functions/org';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
let isGettingPin = true;
function getPin() {
setTimeout(() => {
$quizStore.pin = genQuizPin();
isGettingPin = false;
}, 3000);
}
function goToPlayersStep() {
$playQuizStore.step = STEPS.WAIT_FOR_PLAYERS;
}
onMount(() => {
getPin();
});
</script>
<PlayContainer>
<div slot="header">
<PlayHeader startCount={true} showCountDown={true} />
</div>
<div slot="body" class="w-full rounded-md bg-white dark:bg-neutral-800 py-7 px-5">
<div class="mb-3">
<p>1. Visit</p>
<h3>play.classroomio.com</h3>
</div>
<div class="">
<p>2. Enter Pin</p>
{#if isGettingPin}
<SyncLoader size="50" color="#1d4ed8" unit="px" duration="1s" />
{:else}
<h3>{$quizStore.pin}</h3>
{/if}
</div>
</div>
<div slot="footer" class="flex justify-center items-center">
<p class="font-bold mr-3">Let's go</p>
<PrimaryButton label="View Players" variant={VARIANTS.OUTLINED} onClick={goToPlayersStep} />
</div>
</PlayContainer>
@@ -0,0 +1,43 @@
<div class="root flex flex-col justify-between h-full m-auto">
{#if $$slots.header}
<!-- HEADER -->
<div class="header">
<slot name="header" />
</div>
{/if}
{#if $$slots.body}
<!-- BODY -->
<div class="body mb-3">
<slot name="body" />
</div>
{/if}
{#if $$slots.footer}
<!-- FOOTER -->
<div class="footer bg-white dark:bg-neutral-800 py-6 px-2 m-auto">
<slot name="footer" />
</div>
{/if}
</div>
<style>
.root {
max-width: 680px;
width: 80%;
height: 95vh;
}
.header {
height: 15%;
}
.body {
min-height: 60%;
height: fit-content;
}
.footer {
max-width: 400px;
}
</style>
@@ -0,0 +1,7 @@
<script>
import CircleContent from './CircleContent.svelte';
export let answers = 0;
</script>
<CircleContent value={answers} label="Answer(s)" />
@@ -0,0 +1,45 @@
<script>
import { slide } from 'svelte/transition';
export let value = false;
export let label = '';
export let className = '';
function fadeSlide(node, options) {
const slideTrans = slide(node, options);
return {
duration: options.duration,
css: (t) => `
${slideTrans.css(t)}
opacity: ${t};
`
};
}
</script>
<div class="circle flex items-center flex-col justify-center {className}">
{#key value}
<h3 class="text-primary-600" transition:fadeSlide={{ duration: 500 }}>
{value}
</h3>
{/key}
<p class="text-primary-600 text-md">{label}</p>
</div>
<style>
.circle {
width: 100px;
height: 100px;
line-height: 100px;
border-radius: 50%;
font-size: 50px;
text-align: center;
background: #fff;
}
.circle h3 {
font-size: 40px;
margin: 0;
padding: 0;
line-height: 1;
}
</style>
@@ -0,0 +1,39 @@
<script>
import { onMount } from 'svelte';
import CircleContent from './CircleContent.svelte';
export let startCount = false;
export let timelimit = '';
let countDownNo = 0;
let started = false;
function triggerCountDown(start) {
if (!start || started) return;
const getNum = (k) => parseInt(timelimit.replace(k, ''));
if (timelimit.includes('s')) {
countDownNo = getNum('s');
} else if (timelimit.includes('m')) {
countDownNo = getNum('m') * 60;
}
window.refreshIntervalId = setInterval(() => {
if (--countDownNo <= 0) {
clearInterval(window.refreshIntervalId);
}
}, 1000);
started = true;
}
onMount(() => {
if (startCount) {
triggerCountDown(startCount);
}
});
$: triggerCountDown(startCount);
</script>
<CircleContent value={countDownNo} label="Second(s)" className="mr-3" />
@@ -0,0 +1,18 @@
<script>
import { quizStore } from '$lib/utils/store/org';
import Answer from './Answer.svelte';
import Timer from './Timer.svelte';
export let startCount = false;
export let showCountDown = false;
</script>
<div class="flex justify-between items-center w-full">
<h3 class="text-white">{$quizStore.title}</h3>
{#if showCountDown}
<div class="flex">
<Timer {startCount} timelimit={$quizStore.timelimit} />
<Answer />
</div>
{/if}
</div>
@@ -0,0 +1 @@
players
@@ -0,0 +1 @@
Podium
@@ -0,0 +1,102 @@
<script>
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import ChevronLeftIcon from 'carbon-icons-svelte/lib/ChevronLeft.svelte';
import ChevronRightIcon from 'carbon-icons-svelte/lib/ChevronRight.svelte';
import QuizQuestion from '../QuizQuestion.svelte';
import PlayContainer from './Container.svelte';
import PlayHeader from './Header/index.svelte';
import { quizStore } from '$lib/utils/store/org';
import { themeImages } from '$lib/utils/constants/quiz';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import IconButton from '$lib/components/IconButton/index.svelte';
export let exitPreview = () => {};
let bgImg;
let totalQ = $quizStore.questions.length;
let curQId = 0;
let currentQuestion = $quizStore.questions[0] || {};
function handlePrev() {
const prevQId = curQId - 1;
console.log('prevQId', prevQId);
const prev = $quizStore.questions[prevQId];
console.log('prev', prev);
if (prev) {
currentQuestion = prev;
curQId = prevQId;
}
}
function handleNext() {
const nextQId = curQId + 1;
console.log('nextqid', nextQId);
const next = $quizStore.questions[nextQId];
console.log('next', next);
if (next) {
currentQuestion = next;
curQId = nextQId;
}
}
onMount(() => {
bgImg = themeImages[$quizStore.theme].play;
});
</script>
{#if bgImg}
<div
class="absolute inset-0 z-50 bg-white h-screen w-screen"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<div
class="p-5 h-full w-full"
style="background: url({bgImg}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;min-height: 100vh;height: fit-content;"
>
<PlayContainer>
<div slot="header">
<PlayHeader startCount={true} showCountDown={true} />
</div>
<div slot="body" class="quiz-body">
{#if currentQuestion}
<QuizQuestion {currentQuestion} isPreview={true} />
{:else}
<h2>No question added</h2>
{/if}
</div>
<div slot="footer" class="flex justify-center flex-col">
{#if currentQuestion}
<PrimaryButton
label="Exit Preview"
variant={VARIANTS.TEXT}
onClick={exitPreview}
className="w-fit"
/>
<div class="flex justify-center items-center">
<IconButton onClick={handlePrev} size="small">
<ChevronLeftIcon size={16} class="carbon-icon dark:text-white" />
</IconButton>
<p class="mx-3">{curQId + 1} / {totalQ}</p>
<IconButton onClick={handleNext} size="small">
<ChevronRightIcon size={16} class="carbon-icon dark:text-white" />
</IconButton>
</div>
{/if}
</div>
</PlayContainer>
</div>
</div>
{/if}
<style>
.quiz-body {
width: 500px;
margin: auto;
}
</style>
@@ -0,0 +1 @@
Question
@@ -0,0 +1 @@
Scoreboard
@@ -0,0 +1,47 @@
<script>
import { onMount } from 'svelte';
import { fly } from 'svelte/transition';
import { SyncLoader } from 'svelte-loading-spinners';
import { themeImages, STEPS } from '$lib/utils/constants/quiz';
import { quizStore, playQuizStore } from '$lib/utils/store/org';
import ConnectToPlay from './ConnectToPlay.svelte';
import Players from './Players.svelte';
import Question from './Question.svelte';
import Scoreboard from './Scoreboard.svelte';
import Podium from './Podium.svelte';
let bgImg;
onMount(() => {
bgImg = themeImages[$quizStore.theme].play;
});
</script>
{#if bgImg}
<div
class="absolute inset-0 z-50 bg-white h-screen w-screen"
in:fly={{ y: 500, duration: 500 }}
out:fly={{ y: 500, duration: 500 }}
>
<div
class="p-5 h-full w-full"
style="background: url({bgImg}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;min-height: 100vh;height: fit-content;"
>
{#if $playQuizStore.step === STEPS.CONNECT_TO_PLAY}
<ConnectToPlay />
{:else if $playQuizStore.step === STEPS.WAIT_FOR_PLAYERS}
<Players />
{:else if $playQuizStore.step === STEPS.SHOW_NEXT_QUESTION}
<Question />
{:else if $playQuizStore.step === STEPS.SCOREBOARD}
<Scoreboard />
{:else if $playQuizStore.step === STEPS.PODIUM}
<Podium />
{:else}
<div class="w-full m-auto">
<SyncLoader size="20" color="#1d4ed8" unit="px" duration="1s" />
</div>
{/if}
</div>
</div>
{/if}
@@ -0,0 +1,98 @@
<script>
import pluralize from 'pluralize';
import { ImageLoader } from 'carbon-components-svelte';
import OverflowMenuHorizontalIcon from 'carbon-icons-svelte/lib/OverflowMenuHorizontal.svelte';
import { currentOrgPath } from '$lib/utils/store/org';
import Dropdown from '$lib/components/Dropdown/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { onRename, onDelete } from '$lib/utils/services/org/quiz';
import { themeImages } from '$lib/utils/constants/quiz';
import { calDateDiff } from '$lib/utils/functions/date';
export let quiz;
export let totalQuestions;
function getOptions() {
return [
// {
// label: 'Edit',
// onClick: onEdit(quiz),
// },
// {
// label: 'Share',
// onClick: onShare(quiz),
// },
// {
// label: 'Report',
// onClick: onReport(quiz),
// },
{
label: 'Rename',
onClick() {
onRename(quiz);
}
},
// {
// label: 'Make a copy',
// onClick() {
// onMakeCopy(quiz);
// },
// },
{
label: 'Delete',
onClick() {
onDelete(quiz);
}
}
];
}
function startQuiz() {}
</script>
{#if quiz}
<div
class="root w-full border hover:shadow-2xl transition ease-in-out rounded-lg bg-gray-100 dark:bg-black mb-5 relative flex flex-col lg:flex-row p-3"
>
<ImageLoader
src={themeImages[quiz.theme]?.card || themeImages.standard.card}
alt="quiz-card"
style="max-width:300px, min-width: 200px;"
/>
<div class="p-2 md:p-5 flex flex-col justify-between w-full">
<div class="flex justify-between">
<h4 class="dark:text-white title text-md font-bold capitalize">
<a href="{$currentOrgPath}/quiz/{quiz.id}">{quiz.title}</a>
</h4>
<Dropdown options={getOptions()} classNames="absolute top-4 right-4" isIcon={true}>
<div class="p-1 rounded-full bg-gray-200 dark:bg-gray-600">
<OverflowMenuHorizontalIcon size={20} class="carbon-icon active" />
</div>
</Dropdown>
</div>
<div class="flex md:flex-row flex-col justify-between">
<div>
<p class="mb-2">{pluralize('question', totalQuestions, true)}</p>
<p class="mb-2 md:mb-0">Updated {calDateDiff(quiz.updated_at)}</p>
</div>
<PrimaryButton
className="px-6 py-3"
variant={VARIANTS.OUTLINED}
label="Start Quiz"
onClick={startQuiz}
/>
</div>
</div>
</div>
{/if}
<style>
.root {
min-height: 180px;
}
</style>
@@ -0,0 +1,62 @@
<script>
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
import { optionImage } from '$lib/utils/constants/quiz';
import TextField from '$lib/components/Form/TextField.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
export let currentQuestion = {
label: ''
};
export let currentError = {};
export let optionHasError = () => false;
export let isPreview = false;
</script>
{#if isPreview}
<h1 class="text-white mb-5 font-bold">{currentQuestion.label}</h1>
{:else}
<TextField
placeholder="Start typing your question"
bind:value={currentQuestion.label}
className="w-full my-4"
errorMessage={currentError.isLabelEmpty && 'This field is required'}
isRequired={true}
bgColor="bg-white"
/>
{/if}
<!-- Options -->
<div class="flex justify-between w-full flex-wrap">
{#each currentQuestion.options || [] as option}
<div class="w-2/5 bg-white dark:bg-neutral-800 rounded p-5 mb-5 relative">
<img src={optionImage[option.id]} alt={option.id} />
{#if isPreview}
<p class="mt-5">{option.label}</p>
{:else}
<TextField
bind:value={option.label}
placeholder="Type answer"
bgColor="bg-white"
className="mt-3"
isDisabled={currentQuestion.type === 'boolean'}
errorMessage={optionHasError(option.id, currentError.options) && 'Please enter a label'}
/>
<div class="flex justify-end absolute top-2 right-2">
<IconButton
value={option.id}
onClick={() => (option.isCorrect = !option.isCorrect)}
buttonClassName={option.isCorrect && 'success'}
>
{#if option.isCorrect}
<CheckmarkFilledIcon size={24} class="carbon-icon dark:text-white" />
{:else}
<CheckmarkOutlineIcon size={24} class="carbon-icon dark:text-white" />
{/if}
</IconButton>
</div>
{/if}
</div>
{/each}
</div>
@@ -0,0 +1,53 @@
<script>
import { onMount } from 'svelte';
import Box from '$lib/components/Box/index.svelte';
import QuizLoader from './Loader.svelte';
import QuizCard from './QuizCard.svelte';
import CoursesEmptyIcon from '$lib/components/Icons/CoursesEmptyIcon.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import { supabase } from '$lib/utils/functions/supabase';
import { currentOrg, quizesStore } from '$lib/utils/store/org';
let isLoading = false;
async function fetchQuizes(id) {
if (!id) return;
isLoading = true;
const { data, error } = await supabase
.from('quiz')
.select(`*`)
.eq('organization_id', id)
.order('updated_at', { ascending: false });
if (error) {
snackbar.error();
return;
}
console.log('data', data);
quizesStore.set(data);
isLoading = false;
}
$: fetchQuizes($currentOrg.id);
</script>
<div class="flex items-center justify-center lg:justify-start flex-wrap my-4 m-auto">
{#if isLoading}
<QuizLoader />
<QuizLoader />
<QuizLoader />
{:else}
{#each $quizesStore as quiz}
<QuizCard {quiz} totalQuestions={quiz.questions?.length || 0} />
{:else}
<Box>
<CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">No Quiz Created</h3>
<p class="dark:text-white w-1/3 text-center">
Create interactive quizzes with scoreboard for your students.
</p>
</Box>
{/each}
{/if}
</div>
@@ -0,0 +1,80 @@
<!-- ResizableSidebar.svelte -->
<script>
let expanding = null;
let startX = 0;
let initialX = 0;
let initialWidth = 300; // Initial width
let width = initialWidth;
let x = 20; // Initial x-position
const grabberWidth = 10;
const MIN_WIDTH = 300; // Minimum width
const MAX_WIDTH = 500; // Maximum width
function startExpand(type, event) {
expanding = type;
startX = event.pageX;
initialX = x;
initialWidth = width;
}
function stopExpand() {
expanding = null;
}
function expand(event) {
if (!expanding) return;
const deltaX = event.pageX - startX;
const newWidth = initialWidth + deltaX;
// Ensure the width stays within the specified range
if (newWidth >= MIN_WIDTH && newWidth <= MAX_WIDTH) {
width = newWidth;
x = initialX + deltaX;
}
}
</script>
<svelte:fragment on:mouseup={stopExpand} />
<svg viewBox="0 0 100 40" height="44" width="300" on:mousemove={expand} class:expanding>
<rect {x} y="2" {width} height="36" class="step" />
<rect
{x}
y="2"
width={grabberWidth}
height="36"
fill="red"
class:active={expanding === 'left' ? 'grip active' : 'grip'}
on:mousedown={startExpand.bind(this, 'left')}
/>
<rect
x={x + width - grabberWidth}
y="2"
width={grabberWidth}
height="36"
fill="blue"
class:active={expanding === 'right' ? 'grip active' : 'grip'}
on:mousedown={startExpand.bind(this, 'right')}
/>
</svg>
<style>
.step {
fill: #8884;
stroke: #222a;
rx: 2;
}
.expanding .step {
stroke: #228e;
}
.grip {
cursor: col-resize;
fill: #fff0;
}
.grip.active,
.grip:hover {
fill: #2288;
}
</style>
@@ -0,0 +1,3 @@
<p class="dark:text-white font-bold">
<slot />
</p>
@@ -0,0 +1,105 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { Grid, Row, Column } from 'carbon-components-svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { currentOrg } from '$lib/utils/store/org';
import { supabase } from '$lib/utils/functions/supabase';
import { blockedSubdomain } from '$lib/utils/constants/app';
import { snackbar } from '$lib/components/Snackbar/store';
import SectionTitle from '../SectionTitle.svelte';
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
let siteName = '';
let errors = '';
let isLoading = false;
async function handleChangeDomain() {
if (blockedSubdomain.includes(siteName || '')) {
errors = 'Sitename already exists.';
return;
}
isLoading = true;
const { data: org, error } = await supabase
.from('organization')
.update({ siteName })
.match({ id: $currentOrg.id });
console.log('Updating organisation', org);
if (error) {
console.log('Error: create organisation', error);
errors = 'Sitename already exists.';
} else {
snackbar.success();
$currentOrg.siteName = siteName;
goto(`/org/${$currentOrg.siteName}/settings/domains`);
}
isLoading = false;
}
function setSiteName(_siteName: string) {
if (!siteName) {
siteName = _siteName;
}
}
function resetErrors(_siteName: string) {
if (errors) {
errors = '';
}
}
$: resetErrors(siteName);
$: setSiteName($currentOrg.siteName);
</script>
<Grid class="border rounded border-gray-200 w-full mt-5">
<Row class="py-7 border-bottom-c">
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Add</SectionTitle></Column>
<Column sm={2} md={6} lg={8}>
<p class="text-md text-gray-500 dark:text-white mb-5">
Add your team mates or collaborators to your organization. Start working together
</p>
<div>
<!-- Org Site Name -->
<TextField
label="URL"
helperMessage={`https://${siteName || ''}.classroomio.com`}
bind:value={siteName}
type="text"
placeholder="e.g traversymedia"
className="mb-5 w-full"
labelClassName=""
errorMessage={errors}
/>
<div class="flex items-center mb-6">
<PrimaryButton
label="Update Domain"
className="py-4"
variant={VARIANTS.OUTLINED}
onClick={handleChangeDomain}
isDisabled={isLoading}
/>
<VisitOrgSiteButton />
</div>
</div>
</Column>
</Row>
<Row class="py-7 border-bottom-c">
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Custom domain</SectionTitle></Column>
<Column sm={2} md={6} lg={8}>
<div class="flex items-center">
<p class="dark:text-white mr-3">Add your own domain name</p>
<ComingSoon />
</div>
</Column>
</Row>
</Grid>
@@ -0,0 +1,94 @@
<script lang="ts">
import { Grid, Column, Row } from 'carbon-components-svelte';
import CheckmarkFilled from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { profile } from '$lib/utils/store/user';
import { snackbar } from '$lib/components/Snackbar/store';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import SectionTitle from '../SectionTitle.svelte';
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
let chatId: number | null;
async function addChatId() {
if (!chatId || `${chatId}`.length < 5) return;
const { error } = await supabase
.from('profile')
.update({
telegram_chat_id: chatId
})
.match({ id: $profile.id });
if (error) {
console.log(error);
snackbar.error('Integration Failed, please try again later');
} else {
$profile.telegram_chat_id = chatId;
chatId = null;
snackbar.success('Integration successful');
}
}
async function deleteChatId() {
const { error } = await supabase
.from('profile')
.update({ telegram_chat_id: null })
.match({ id: $profile.id });
if (error) {
snackbar.error('Deletion Failed, please try again later');
console.log(error);
} else {
$profile.telegram_chat_id = null;
snackbar.success('Integration deleted');
}
}
</script>
<Grid class="border-c rounded border-gray-200 w-full mt-5">
<Row class="flex lg:flex-row flex-col justify-center lg:justify-start py-7 border-bottom-c">
<Column sm={4} md={4} lg={4} class="flex items-center justify-center">
<img src="/telegram-svg.svg" alt="" class="w-10 mr-2" />
<SectionTitle>Telegram</SectionTitle></Column
>
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0 flex flex-col items-center lg:items-start">
{#if $profile.telegram_chat_id !== null}
<div class="flex flex-col items-center w-full">
<div class="flex items-center">
<CheckmarkFilled size={32} class="mr-2" style="fill: green;" />
<SectionTitle>Integration Successful</SectionTitle>
</div>
<PrimaryButton className="mt-3" variant={VARIANTS.CONTAINED_DANGER} onClick={deleteChatId}
>Disconnect</PrimaryButton
>
</div>
{:else}
<div>
<h3 class="font-normal text-lg">
Connect your account to telegram to get notified of any change within the application.
</h3>
<h4 class="font-normal">Step 1: Authenticate your account via the ClassroomIO bot.</h4>
<PrimaryButton
className="mb-5"
variant={VARIANTS.OUTLINED}
onClick={() => window.open('https://t.me/classroomio_bot', '_blank')}
><span class="mr-2">Open Bot</span> <ArrowUpRightIcon /></PrimaryButton
>
<h4 class="font-normal">Step 2: Enter the Chat ID given to you by the bot.</h4>
<TextField
bind:value={chatId}
placeholder="360111"
className="w-full lg:w-60 mb-5"
type="number"
isRequired
/>
<PrimaryButton onClick={addChatId}>Connect</PrimaryButton>
</div>
{/if}
</Column>
</Row>
</Grid>
@@ -0,0 +1,503 @@
<script lang="ts">
import { Grid, Row, Column, RadioButtonGroup, RadioButton } from 'carbon-components-svelte';
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Save from 'carbon-icons-svelte/lib/Save.svelte';
import IconButton from '$lib/components/IconButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import SectionTitle from '../SectionTitle.svelte';
import { Toggle } from 'carbon-components-svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import TextArea from '$lib/components/Form/TextArea.svelte';
import { currentOrg } from '$lib/utils/store/org';
import { getSupabase } from '$lib/utils/functions/supabase';
import { snackbar } from '$lib/components/Snackbar/store';
import { landingPageSettings } from './store';
import type { OrgLandingPageJson } from './store';
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
let isSaving = false;
let creatingNewQuestion = false;
let widgetKey = '';
const banner = [
{ value: 'video', label: 'Video' },
{ value: 'image', label: 'Image' }
];
const supabase = getSupabase();
let newQuestion = {
title: '',
content: ''
};
function widgetControl(key: string) {
widgetKey = key;
$handleOpenWidget.open = true;
}
function createNewFaq() {
newQuestion = {
title: '',
content: ''
};
creatingNewQuestion = true;
}
function saveNewFAQ() {
if (newQuestion.title !== '' && newQuestion.content !== '') {
$landingPageSettings.faq.questions = [
...$landingPageSettings.faq.questions,
{
id: new Date().getTime(),
title: newQuestion.title,
content: newQuestion.content
}
];
creatingNewQuestion = false;
}
}
function cancelNewFAQ() {
creatingNewQuestion = false;
}
function deleteFaq(id: number) {
let faqs = $landingPageSettings.faq.questions;
const filteredFaq = faqs.filter((faq) => faq.id !== id);
$landingPageSettings.faq.questions = filteredFaq;
}
const checkPrefix = (inputValue: string) => {
if (!inputValue) return;
if (inputValue.trim() !== '') {
if (!inputValue.startsWith('https://')) {
inputValue = 'https://' + inputValue;
}
}
return inputValue;
};
async function handleSave() {
isSaving = true;
$landingPageSettings.footer.twitter = checkPrefix($landingPageSettings.footer.twitter) || '';
$landingPageSettings.footer.linkedin = checkPrefix($landingPageSettings.footer.linkedin) || '';
$landingPageSettings.footer.facebook = checkPrefix($landingPageSettings.footer.facebook) || '';
const { error } = await supabase
.from('organization')
.update({ landingpage: $landingPageSettings })
.match({ id: $currentOrg.id });
if (error) {
const message = error?.message || 'Please try again';
snackbar.error(`Update failed: ${message}`);
} else {
$currentOrg.landingpage = $landingPageSettings;
snackbar.success();
}
isSaving = false;
}
function setDefault(landingpage: OrgLandingPageJson) {
if (landingpage && Object.keys(landingpage).length) {
// Added new key, support backward compatibility
if (!landingpage?.header?.banner) {
landingpage.header.banner = $landingPageSettings.header.banner;
}
$landingPageSettings = {
...landingpage
};
}
}
$: setDefault($currentOrg?.landingpage as OrgLandingPageJson);
</script>
<Grid class="border-c rounded border-gray-200 w-full mt-5 relative">
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}>
<SectionTitle>Landing Page</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.header.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span>
<span slot="labelB" style="color: gray">Show section</span>
</Toggle>
</Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField
label="Title"
placeholder="Write your title here"
bind:value={$landingPageSettings.header.title}
className="w-full mb-5"
/>
<TextField
label="Title-Highlight"
placeholder="Write your titlehighlight here"
className="w-full mb-5"
bind:value={$landingPageSettings.header.titleHighlight}
/>
<TextArea
label="Subtitle"
labelClassName="font-light"
placeholder="Write your subtitle here"
className="w-full mb-5"
bind:value={$landingPageSettings.header.subtitle}
isAIEnabled={true}
/>
<SectionTitle>Actions</SectionTitle>
<TextField
label="Label"
placeholder="Write your label here"
className="w-full mt-3 mb-5"
bind:value={$landingPageSettings.header.action.label}
/>
<div class="gap-2 mb-5">
<TextField
label="Link"
placeholder="Write your link here"
bind:value={$landingPageSettings.header.action.link}
/>
<Toggle bind:toggled={$landingPageSettings.header.action.redirect} size="sm">
<span slot="labelA" style="color: gray">No redirect</span>
<span slot="labelB" style="color: gray">Redirect</span>
</Toggle>
</div>
<div />
<RadioButtonGroup
legendText="Banner Type"
bind:selected={$landingPageSettings.header.banner.type}
class="mt-10 mb-5"
>
{#each banner as item}
<RadioButton value={item.value} labelText={item.label} />
{/each}
</RadioButtonGroup>
{#if $landingPageSettings.header.banner.type === 'video'}
<div class="gap-2 mt-3 mb-5">
<TextField
label="Link"
placeholder="Write your video link here"
bind:value={$landingPageSettings.header.banner.video}
/>
</div>
{:else}
<PrimaryButton
variant={VARIANTS.OUTLINED}
label="Select Image"
className="mt-3"
onClick={() => widgetControl('banner')}
/>
{/if}
{#if $landingPageSettings.header.banner.image && $landingPageSettings.header.banner.type === 'image'}
<img
alt="bannerImage"
src={$landingPageSettings.header.banner.image}
class="mt-2 rounded-md w-full"
/>
{/if}
<Toggle bind:toggled={$landingPageSettings.header.banner.show} size="sm">
<span slot="labelA" style="color: gray">Hide banner</span>
<span slot="labelB" style="color: gray">Show banner</span>
</Toggle>
{#if $handleOpenWidget.open && widgetKey === 'banner'}
<UploadWidget bind:imageURL={$landingPageSettings.header.banner.image} />
{/if}
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}
><SectionTitle>About</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.aboutUs.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span>
<span slot="labelB" style="color: gray">Show section</span>
</Toggle></Column
>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField
label="Title"
placeholder="Write your title here"
bind:value={$landingPageSettings.aboutUs.title}
className="mb-5"
/>
<TextArea
label="Content"
labelClassName="font-light"
placeholder="Write your Content here"
className="mb-5"
bind:value={$landingPageSettings.aboutUs.content}
isAIEnabled={true}
/>
<div>
<p class="font-bold">Upload an image</p>
<PrimaryButton
variant={VARIANTS.OUTLINED}
label="Select Image"
className="mt-3"
onClick={() => widgetControl('about-us')}
/>
{#if $landingPageSettings.aboutUs.imageUrl}
<img
alt="About us"
src={$landingPageSettings.aboutUs.imageUrl}
class="mt-2 rounded-md w-full"
/>
{/if}
{#if $handleOpenWidget.open && widgetKey === 'about-us'}
<UploadWidget bind:imageURL={$landingPageSettings.aboutUs.imageUrl} />
{/if}
</div>
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}
><SectionTitle>Courses</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.courses.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span>
<span slot="labelB" style="color: gray">Show section</span>
</Toggle>
</Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField
label="Title"
placeholder="Write your title here"
bind:value={$landingPageSettings.courses.title}
/>
<TextField
label="Title-Highlight"
placeholder="Write your title highlight here"
bind:value={$landingPageSettings.courses.titleHighlight}
/>
<TextArea
label="Subtitle"
labelClassName="font-light"
placeholder="Write your subtitle here"
bind:value={$landingPageSettings.courses.subtitle}
isAIEnabled={true}
/>
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}
><SectionTitle>Frequently Asked Questions</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.faq.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span>
<span slot="labelB" style="color: gray">Show section</span>
</Toggle>
</Column>
<Column sm={8} md={8} lg={8}>
<TextField
label="Title"
placeholder="Write your title here"
bind:value={$landingPageSettings.faq.title}
className="mb-5"
/>
{#each $landingPageSettings.faq.questions as item (item.id)}
<div class="mb-3">
<TextField
label="Question"
placeholder="Write your question here"
bind:value={item.title}
className="mb-5"
/>
<TextArea
label="Answer"
labelClassName="font-light"
placeholder="Write your answer here"
bind:value={item.content}
className="mb-5"
isAIEnabled={true}
/>
<IconButton onClick={() => deleteFaq(item.id)}>
<TrashCan size={24} class="fill-red-700" />
</IconButton>
</div>
{/each}
{#if creatingNewQuestion}
<TextField
label="Question"
placeholder="Write your question here"
bind:value={newQuestion.title}
className="mb-5"
/>
<TextArea
label="Answer"
labelClassName="font-light"
placeholder="Write your answer here"
bind:value={newQuestion.content}
className="mb-5"
isAIEnabled={true}
/>
<div class="flex items-center gap-2">
<PrimaryButton variant={VARIANTS.OUTLINED} label="Save" onClick={saveNewFAQ} />
<PrimaryButton variant={VARIANTS.OUTLINED} label="Cancel" onClick={cancelNewFAQ} />
</div>
{:else}
<PrimaryButton variant={VARIANTS.OUTLINED} label="Add New Faq" onClick={createNewFaq} />
{/if}
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}
><SectionTitle>Contact us</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.contact.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span>
<span slot="labelB" style="color: gray">Show section</span>
</Toggle>
</Column>
<Column sm={8} md={8} lg={8}>
<TextField
label="Title"
placeholder="Write your title here"
bind:value={$landingPageSettings.contact.title}
className="mb-5"
/>
<TextField
label="Title-highlight"
labelClassName="font-light"
placeholder="Write your title highlight here"
className="mb-5"
bind:value={$landingPageSettings.contact.titleHighlight}
/>
<TextArea
label="Subtitle"
labelClassName="font-light"
placeholder="Write your subtitle here"
className="mt-3 mb-5"
bind:value={$landingPageSettings.contact.subtitle}
isAIEnabled={true}
/>
<TextField
label="Phone number"
placeholder="Write your phone number here"
className="mt-3 mb-5"
bind:value={$landingPageSettings.contact.phone}
/>
<TextField
label="Email"
placeholder="Write your email here"
className="mt-3 mb-5"
bind:value={$landingPageSettings.contact.email}
/>
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}
><SectionTitle>Mailing List</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.mailinglist.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span>
<span slot="labelB" style="color: gray">Show section</span>
</Toggle>
</Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField
label="Title"
placeholder="Write your title here"
bind:value={$landingPageSettings.mailinglist.title}
className="mb-5"
/>
<TextArea
label="Subtitle"
labelClassName="font-light"
placeholder="Write your subtitle here"
className="mb-5"
bind:value={$landingPageSettings.mailinglist.subtitle}
isAIEnabled={true}
/>
<TextField
label="Button-label"
placeholder="Write your label here"
className="mb-5"
bind:value={$landingPageSettings.mailinglist.buttonLabel}
/>
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7">
<Column sm={4} md={4} lg={4}
><SectionTitle>Footer</SectionTitle>
<Toggle bind:toggled={$landingPageSettings.footer.show} size="sm">
<span slot="labelA" style="color: gray">Hide section</span>
<span slot="labelB" style="color: gray">Show section</span>
</Toggle>
</Column>
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
<TextField
label="Facebook"
placeholder="Write your Facebook link here"
bind:value={$landingPageSettings.footer.facebook}
className="mb-5"
/>
<!-- <TextField
label="Instagram"
placeholder="Write your Instagram link here"
className="mb-5"
bind:value={$landingPageSettings.footer.instagram}
/> -->
<TextField
label="Twitter"
placeholder="Write your Twitter link here"
className="mb-5"
bind:value={$landingPageSettings.footer.twitter}
/>
<TextField
label="Linkedin"
placeholder="Write your Linkedin link here"
className="mb-5"
bind:value={$landingPageSettings.footer.linkedin}
/>
</Column>
</Row>
<div class="sticky desktop float-right bottom-12 mr-2 z-[120]">
<PrimaryButton
label="Save Changes"
isLoading={isSaving}
isDisabled={isSaving}
onClick={handleSave}
/>
</div>
</Grid>
<div
class="absolute
mobile right-6 bottom-8 z-[120]"
>
<span>
<IconButton onClick={handleSave} disabled={isSaving}>
<Save size={40} class=" bg-blue-700 p-1 rounded-full" />
</IconButton>
</span>
</div>
<style>
@media screen and (min-width: 769px) {
.desktop {
display: block;
}
.mobile {
display: none;
}
}
@media screen and (max-width: 768px) {
.desktop {
display: none;
}
.mobile {
display: flex;
}
}
</style>
@@ -0,0 +1,199 @@
<script>
import { goto } from '$app/navigation';
import { Grid, Row, Column } from 'carbon-components-svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import UploadImage from '$lib/components/UploadImage/index.svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { snackbar } from '$lib/components/Snackbar/store';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import SectionTitle from '../SectionTitle.svelte';
let avatar;
let loading = false;
const themes = {
rose: 'theme-rose',
green: 'theme-green',
orange: 'theme-orange',
violet: 'theme-violet',
default: ''
};
function handleChangeTheme(t = '') {
return async () => {
document.body.className = document.body.className
.split(' ')
.filter((c) => !c.includes('theme'))
.join(' ')
.concat(!!t ? ' ' : '', t);
$currentOrg.theme = t;
const res = await supabase
.from('organization')
.update({ theme: t })
.match({ id: $currentOrg.id });
console.log('Update theme', res);
};
}
async function handleUpdate() {
try {
loading = true;
const updates = {
name: $currentOrg.name
};
if (avatar) {
const filename = `user/${$currentOrg.name + Date.now()}.webp`;
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
cacheControl: '3600',
upsert: false
});
if (data) {
const { data: response } = supabase.storage.from('avatars').getPublicUrl(filename);
updates.avatar_url = response.publicUrl;
$currentOrg.avatar_url = response.publicUrl;
}
avatar = undefined;
}
let { error } = await supabase
.from('organization')
.update(updates)
.match({ id: $currentOrg.id });
currentOrg.update((_currentOrg) => ({
..._currentOrg,
...updates
}));
snackbar.success('Update successful');
if (error) throw error;
} catch (error) {
let message = error.message;
if (message.includes('profile_username_key')) {
message = 'username already exists';
}
snackbar.error(`Update failed: ${message}`);
loading = false;
} finally {
loading = false;
}
}
function gotoSetting(pathname) {
goto(`${$currentOrgPath}/settings${pathname}`);
}
</script>
<Grid class="border-c rounded border-gray-200 w-full mt-5">
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Organization Profile</SectionTitle></Column>
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0 flex flex-col items-center lg:items-start">
<TextField
label="Organization Name"
bind:value={$currentOrg.name}
className="w-full lg:w-60 mb-5"
/>
<UploadImage
bind:avatar
src={$currentOrg.avatar_url}
shape="rounded-md"
widthHeight="w-24 h-24"
/>
<PrimaryButton
label="Update Organization"
className="px-6 py-3 lg:mr-5 mt-5"
isLoading={loading}
isDisabled={loading}
onClick={handleUpdate}
/>
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Theme</SectionTitle></Column>
<Column sm={8} md={8} lg={8}>
<h4 class="dark:text-white lg:mt-0">Set your brand color</h4>
<div class="flex gap-2">
<button
class="rounded-full border-2 {$currentOrg.theme === themes.default &&
'border-[#1d4ee2]'} mr-3 flex items-center justify-center"
on:click={handleChangeTheme(themes.default)}
>
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#1d4ee2] rounded-full m-1" />
</button>
<button
class="rounded-full border-2 {$currentOrg.theme === themes.rose &&
'border-[#be1241]'} mr-3 flex items-center justify-center"
on:click={handleChangeTheme(themes.rose)}
>
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#be1241] rounded-full m-1" />
</button>
<button
class="rounded-full border-2 {$currentOrg.theme === themes.green &&
'border-[#0c891b]'} mr-3 flex items-center justify-center"
on:click={handleChangeTheme(themes.green)}
>
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#0c891b] rounded-full m-1" />
</button>
<button
class="rounded-full border-2 {$currentOrg.theme === themes.orange &&
'border-[#cc4902]'} mr-3 flex items-center justify-center"
on:click={handleChangeTheme(themes.orange)}
>
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#cc4902] rounded-full m-1" />
</button>
<button
class="rounded-full border-2 {$currentOrg.theme === themes.violet &&
'border-[#cf00ce]'} mr-3 flex items-center justify-center"
on:click={handleChangeTheme(themes.violet)}
>
<div class="w-3 h-3 md:w-6 md:h-6 bg-[#cf00ce] rounded-full m-1" />
</button>
</div>
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Custom Domain</SectionTitle></Column>
<Column sm={8} md={8} lg={8}>
<h4 class="dark:text-white lg:mt-0">Customise your Domain</h4>
<p class="text-sm text-gray-500 dark:text-white">
Create a custom URL so your audience can get to your organization easily
</p>
<PrimaryButton
label="Edit domain"
className="my-7 py-5 px-10 text-primary-700"
variant={VARIANTS.OUTLINED}
onClick={() => gotoSetting('/domains')}
/>
</Column>
</Row>
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}><SectionTitle>Team</SectionTitle></Column>
<Column sm={8} md={8} lg={8}>
<h4 class="dark:text-white lg:mt-0">Set up your website</h4>
<p class="text-sm text-gray-500 dark:text-white">
Add team mates to your organization so you can both collaborate on projects.
</p>
<PrimaryButton
label="Manage Team"
className="my-7 py-5 px-10 text-primary-700"
variant={VARIANTS.OUTLINED}
onClick={() => gotoSetting('/teams')}
/>
</Column>
</Row>
</Grid>
@@ -0,0 +1,93 @@
<script>
import { Grid, Row, Column } from 'carbon-components-svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import SectionTitle from '../SectionTitle.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import UploadImage from '$lib/components/UploadImage/index.svelte';
import { supabase } from '$lib/utils/functions/supabase';
import { profile } from '$lib/utils/store/user';
import { snackbar } from '$lib/components/Snackbar/store';
import LogoutButton from '$lib/components/Buttons/Logout/index.svelte';
import generateUUID from '$lib/utils/functions/generateUUID';
let avatar = '';
let loading = false;
async function handleUpdate() {
try {
loading = true;
const updates = {
fullname: $profile.fullname,
username: $profile.username,
email: $profile.email
};
if (avatar) {
const filename = `user/${generateUUID()}.webp`;
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
cacheControl: '3600',
upsert: false
});
if (data) {
const { data: response } = await supabase.storage.from('avatars').getPublicUrl(filename);
updates.avatar_url = response.publicUrl;
$profile.avatar_url = response.publicUrl;
}
avatar = undefined;
}
let { error } = await supabase.from('profile').update(updates).match({ id: $profile.id });
profile.update((_profile) => ({
..._profile,
...updates
}));
snackbar.success('Update successful');
if (error) throw error;
} catch (error) {
let message = error.message;
if (message.includes('profile_username_key')) {
message = 'username already exists';
}
snackbar.success(`Update failed: ${message}`);
loading = false;
} finally {
loading = false;
}
}
</script>
<Grid class="border-c rounded border-gray-200 w-full mt-5">
<Row class="flex flex-col lg:flex-row items-center lg:items-start py-7 border-bottom-c ">
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
<SectionTitle>Profile Picture</SectionTitle>
</Column>
<Column sm={2} md={4} lg={8} class="mt-2 lg:mt-0">
<UploadImage bind:avatar src={$profile.avatar_url} widthHeight="w-16 h-16 lg:w-24 lg:h-24" />
</Column>
</Row>
<Row class="flex flex-col lg:flex-row py-7 border-bottom-c">
<Column sm={4} md={4} lg={4}>
<SectionTitle>Personal Information</SectionTitle>
</Column>
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0">
<TextField label="Full Name" bind:value={$profile.fullname} className="w-full lg:w-60 mb-4" />
<TextField label="Username" bind:value={$profile.username} className="w-full lg:w-60 mb-4" />
<TextField label="Email" bind:value={$profile.email} className="w-full lg:w-60 mb-4" />
</Column>
</Row>
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
<PrimaryButton
label="Update Profile"
className="mr-5"
isLoading={loading}
isDisabled={loading}
onClick={handleUpdate}
/>
<LogoutButton />
</Row>
</Grid>
@@ -0,0 +1,192 @@
<script>
import { Grid, Row, Column, Select, SelectItem } from 'carbon-components-svelte';
import { Moon } from 'svelte-loading-spinners';
import TextField from '$lib/components/Form/TextField.svelte';
import TextChip from '$lib/components/Chip/Text.svelte';
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { validateEmailInString } from '$lib/utils/functions/validator';
import { ROLE_LABEL, ROLE } from '$lib/utils/constants/roles';
import { currentOrg, orgTeam } from '$lib/utils/store/org';
import { snackbar } from '$lib/components/Snackbar/store';
import { getOrgTeam } from '$lib/utils/services/org';
import { profile } from '$lib/utils/store/user';
import { supabase } from '$lib/utils/functions/supabase';
import SectionTitle from '../SectionTitle.svelte';
import {
triggerSendEmail,
NOTIFICATION_NAME
} from '$lib/utils/services/notification/notification';
let emailsStr = '';
let errorMessage = '';
let role = ROLE.TUTOR;
let isFetching = false;
let isLoading = false;
let isRemoving = false;
async function onSendInvite() {
const { hasError, error: _error, emails } = validateEmailInString(emailsStr);
if (hasError) {
errorMessage = _error;
return;
}
isLoading = true;
let apiError = '';
emails.forEach(async (email, index) => {
if (apiError) return;
const { data, error } = await supabase
.from('organizationmember')
.insert({
organization_id: $currentOrg.id,
email,
role_id: role,
verified: false
})
.select();
console.log('data', data);
if (error) {
apiError = error;
console.error('onSendInvite:', error);
snackbar.error(`Failed to send invite, please try again`);
isLoading = false;
return;
}
const [newMember] = data || [];
if (newMember) {
orgTeam.update((team) => [
{
id: newMember.id,
email: newMember?.email,
verified: newMember?.verified,
role: ROLE_LABEL[newMember?.role_id] || '',
isAdmin: newMember?.role_id === ROLE.ADMIN
},
...team
]);
}
triggerSendEmail(NOTIFICATION_NAME.INVITE_TEACHER, {
email: newMember.email,
org: {
id: $currentOrg.id,
name: $currentOrg.name,
siteName: $currentOrg.siteName
}
});
const isLast = index === emails.length - 1;
if (isLast) {
snackbar.success('Success');
emailsStr = '';
isLoading = false;
}
});
}
async function onRemove(id) {
console.log('onRemove called');
isRemoving = true;
const { data, error } = await supabase.from('organizationmember').delete().match({ id });
if (error) {
console.error('onRemove:', error);
snackbar.error('Failed to remove user, please try again');
} else {
orgTeam.update((team) => [...team.filter((member) => member.id !== id)]);
}
isRemoving = false;
}
const fetchTeam = async (id) => {
if (!id) return;
isFetching = true;
await getOrgTeam(id);
isFetching = false;
};
const isTeamMemberAdmin = (members, profileId) => {
return members.some((member) => member.profileId === profileId && member.isAdmin);
};
$: fetchTeam($currentOrg.id);
</script>
<Grid class="border rounded border-gray-200 w-full mt-5">
<Row class="py-7 border-bottom-c">
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Add</SectionTitle></Column>
<Column sm={2} md={6} lg={8}>
<p class="text-md text-gray-500 dark:text-white mb-5">
Add your team mates or collaborators to your organization. Start working together
</p>
<div class="">
<TextField
label="Invite"
placeholder="email comma separated"
bind:value={emailsStr}
className="mb-3"
{errorMessage}
/>
<Select labelText="Role" bind:selected={role} class="mb-5 w-40">
<!-- <SelectItem value={ROLE.ADMIN} text={ROLE_LABEL[ROLE.ADMIN]} /> -->
<SelectItem value={ROLE.TUTOR} text={ROLE_LABEL[ROLE.TUTOR]} />
</Select>
<PrimaryButton
label="Send Invite"
onClick={onSendInvite}
{isLoading}
isDisabled={isLoading}
/>
</div>
</Column>
</Row>
<Row class="py-7 border-bottom-c">
<Column sm={2} md={2} lg={4} class="text-lg"><SectionTitle>Members</SectionTitle></Column>
<Column sm={2} md={6} lg={8}>
{#if isFetching}
<Moon />
{:else}
{#each $orgTeam as teamMember}
<div class="flex justify-between items-center mb-5">
<div class="flex">
<p class="text-sm text-gray-500 dark:text-white mr-3">
{teamMember.email}
</p>
<TextChip value={teamMember.role} className="text-xs mr-3" size="sm" />
{#if !teamMember.verified}
<TextChip
value="Invite Sent"
className="text-xs bg-yellow-200 text-yellow-700"
size="sm"
/>
{:else if teamMember.profileId === $profile.id}
<ComingSoon label="You" />
{/if}
</div>
{#if teamMember.profileId !== $profile.id && isTeamMemberAdmin($orgTeam, $profile.id)}
<PrimaryButton
label="Remove"
variant={VARIANTS.TEXT_DANGER}
onClick={() => onRemove(teamMember.id)}
isLoading={isRemoving}
isDisabled={isRemoving}
/>
{/if}
</div>
{/each}
{/if}
</Column>
</Row>
</Grid>
@@ -0,0 +1,98 @@
<script>
import { page } from '$app/stores';
import { Tabs, Tab, TabContent } from 'carbon-components-svelte';
import Profile from './Profile.svelte';
import LandingpageSettings from './LandingpageSettings.svelte';
// import Account from './Account.svelte';
import OrgSettings from './OrgSettings.svelte';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
import { goto } from '$app/navigation';
import { isOrgAdmin } from '$lib/utils/store/org';
import Integrations from './Integrations.svelte';
let selected = 0;
let query = new URLSearchParams($page.url.search);
let tabKey = query.get('tab') || '';
let tabs = [];
function getSelectedByTab(tabKey = '') {
const tab = tabs.find((t) => t.tabKey === tabKey);
return tab ? tab.key : 0;
}
function changeRouteOnTabChange(key = 0) {
const tab = tabs.find((t) => t.key === key);
if (tab) {
return goto(tab.href);
}
}
onMount(() => {
selected = getSelectedByTab(tabKey);
});
$: {
if (browser) {
changeRouteOnTabChange(selected);
}
}
$: {
tabs = [
{
key: 0,
label: 'Profile',
tabKey: '',
href: $page.url.pathname,
disabled: false
},
{
key: 1,
label: 'Organization',
tabKey: 'org',
href: `${$page.url.pathname}?tab=org`,
disabled: !$isOrgAdmin
},
{
key: 2,
label: 'LandingPage',
tabKey: 'landingpage',
href: `${$page.url.pathname}?tab=landingpage`,
disabled: !$isOrgAdmin
},
{
key: 3,
label: 'Integrations',
tabKey: 'integrations',
href: `${$page.url.pathname}?tab=integrations`,
disabled: false
}
];
}
</script>
<Tabs autoWidth bind:selected>
{#each tabs as tab}
<Tab label={tab.label} href={tab.href} disabled={tab.disabled} />
{/each}
<!-- <Tab label="Account" /> -->
<svelte:fragment slot="content">
<TabContent class="w-full p-0">
<Profile />
</TabContent>
<!-- <TabContent>
<Account />
</TabContent> -->
<TabContent class="w-full p-0">
<OrgSettings />
</TabContent>
<TabContent class="w-full p-0">
<LandingpageSettings />
</TabContent>
<TabContent class="w-full p-0">
<Integrations />
</TabContent>
</svelte:fragment>
</Tabs>
@@ -0,0 +1,146 @@
import { writable } from 'svelte/store';
import type { Writable } from 'svelte/store';
export interface OrgLandingPageJson {
header: {
title: string;
titleHighlight: string;
subtitle: string;
action: {
label: string;
link: string;
redirect: boolean;
};
banner: {
video: string;
image: string;
type: string;
show: boolean;
};
show: boolean;
};
aboutUs: {
title: string;
content: string;
imageUrl: string;
show: boolean;
};
courses: {
title: string;
titleHighlight: string;
subtitle: string;
show: boolean;
};
faq: {
title: string;
questions: Array<{
id: number;
title: string;
content: string;
}>;
show: boolean;
};
contact: {
title: string;
titleHighlight: string;
subtitle: string;
address: string;
phone: string;
email: string;
show: boolean;
};
mailinglist: {
title: string;
subtitle: string;
buttonLabel: string;
show: boolean;
};
footer: {
facebook: string;
instagram: string;
twitter: string;
linkedin: string;
show: boolean;
};
}
export let landingPageSettings: Writable<OrgLandingPageJson> = writable({
header: {
title: 'Web Development Courses for',
titleHighlight: 'Everyone',
subtitle:
'Practical project-based courses that are easy to understand and straight to the point',
action: {
label: 'Start learning Today',
link: '#contact',
redirect: false
},
banner: {
video: 'https://www.youtube.com/watch?v=qqAYW7uxErI',
image: '',
type: 'video',
show: true
},
show: true
},
aboutUs: {
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:
'https://tapaozmyjsuykgerrfkt.supabase.co/storage/v1/object/public/avatars/landingpage/riverside.jpeg',
show: true
},
courses: {
title: 'Explore our',
titleHighlight: 'Courses',
subtitle: 'Find courses you will love from best teachers all over the world🌎.',
show: true
},
faq: {
title: 'Frequently Asked Questions.',
questions: [
{
id: 1,
title: 'Lorem ipsum dolor sit, amet con',
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.'
},
{
id: 2,
title: 'Lorem ipsum dolor sit, amet con',
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.'
},
{
id: 3,
title: 'Lorem ipsum dolor sit, amet con',
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.'
}
],
show: true
},
contact: {
title: 'Have you got any',
titleHighlight: 'Question?',
subtitle: 'Reach out to us with any question you might have.',
address: 'Ashwoodfield House Farm, Kidderminster Rd, Stourbridge, Kingswinford DY6 0AA',
phone: '+4407339904995',
email: '[email protected]',
show: true
},
mailinglist: {
title: 'Join our mailing list',
subtitle:
'We are constantly releasing new courses and sharing them with our email list. Subscribe to get notified once we release a new course',
buttonLabel: 'Subscribe',
show: true
},
footer: {
facebook: '#',
instagram: '#',
twitter: '#',
linkedin: '#',
show: true
}
});
@@ -0,0 +1,144 @@
<script lang="ts">
import { page } from '$app/stores';
import HelpIcon from 'carbon-icons-svelte/lib/Help.svelte';
import ForumIcon from 'carbon-icons-svelte/lib/Forum.svelte';
import TextChip from '$lib/components/Chip/Text.svelte';
import OrgSelector from '$lib/components/OrgSelector/OrgSelector.svelte';
import HomeIcon from '$lib/components/Icons/HomeIcon.svelte';
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
import QuizIcon from '$lib/components/Icons/QuizIcon.svelte';
import SiteSettingsIcon from '$lib/components/Icons/SiteSettingsIcon.svelte';
import AudienceIcon from '$lib/components/Icons/AudienceIcon.svelte';
import Avatar from '$lib/components/Avatar/index.svelte';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import { NavClasses } from '$lib/utils/constants/reusableClass';
import { sideBar } from './store';
const menuItems = [
{
path: '',
label: 'Dashboard'
},
// {
// path: '/quiz',
// label: 'Quizzes'
// },
{
path: '/courses',
label: 'Courses'
},
{
path: '/community',
label: 'Community'
},
// {
// path: '/site',
// label: 'Site settings',
// },
{
path: '/audience',
label: 'Audience'
}
];
function isActive(pagePath: string, itemPath: string) {
const pageLinkItems = pagePath.split('/');
const itemLinkItems = itemPath.split('/');
if (itemLinkItems.length !== pageLinkItems.length) {
return false;
}
return pagePath.includes(itemPath);
}
const toggleSidebar = () => {
$sideBar.hidden = !$sideBar.hidden;
};
</script>
<aside
class={`${
$sideBar.hidden
? '-translate-x-[100%] absolute md:translate-x-0 md:relative z-40'
: 'translate-x-0 absolute md:relative z-40'
} overflow-y-auto transition w-[250px] min-w-[250px] bg-gray-100 dark:bg-neutral-900 h-[calc(100vh-48px)] border border-l-0 border-t-0 border-b-0 border-r-1`}
>
<div class="h-full flex flex-col">
<div class="border-b border-gray-200 pt-5 px-4">
{#if $currentOrg.avatar_url && $currentOrg.name}
<Avatar
src={$currentOrg.avatar_url}
name={$currentOrg.name}
shape="rounded-md"
width="w-7"
height="h-7"
/>
{:else if $currentOrg.shortName}
<TextChip
value={$currentOrg.shortName}
className="bg-primary-200 dark:text-black font-bold"
/>
{/if}
<OrgSelector />
<ul class="my-5">
{#each menuItems as menuItem}
<a
href="{$currentOrgPath}{menuItem.path}"
class="text-black no-underline"
on:click={toggleSidebar}
>
<li
class="flex items-center py-3 px-4 mb-2 {NavClasses.item} {isActive(
$page.url.pathname,
`${$currentOrgPath}${menuItem.path}`
)
? NavClasses.active
: 'dark:text-white'}"
>
{#if menuItem.path === ''}
<HomeIcon />
{:else if menuItem.path === '/courses'}
<CourseIcon />
{:else if menuItem.path === '/site'}
<SiteSettingsIcon />
{:else if menuItem.path === '/community'}
<ForumIcon size={24} class="carbon-icon dark:fill-[#fff] fill-[#000]" />
{:else if menuItem.path === '/quiz'}
<QuizIcon />
{:else if menuItem.path === '/audience'}
<AudienceIcon />
{/if}
<p class=" ml-2">{menuItem.label}</p>
</li>
</a>
{/each}
</ul>
</div>
<span class="flex-grow" />
<ul class="my-5 pb-5 px-4">
<a href={$currentOrgPath} class="text-black no-underline" on:click={toggleSidebar}>
<li class="flex items-center py-3 px-4 mb-2 rounded">
<HelpIcon size={20} class="carbon-icon dark:text-white" />
<p class="dark:text-white ml-2">Help</p>
</li>
</a>
<a href="{$currentOrgPath}/settings" class="text-black no-underline" on:click={toggleSidebar}>
<li
class="flex items-center py-3 px-4 mb-2 {NavClasses.item} {isActive(
$page.url.pathname,
`${$currentOrgPath}/settings`
)
? NavClasses.active
: 'dark:text-white'}"
>
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" />
<p class="ml-2">Settings</p>
</li>
</a>
</ul>
</div>
</aside>
@@ -0,0 +1,9 @@
import { writable } from 'svelte/store';
export const newOrgModal = writable({
open: false
});
export const sideBar = writable({
hidden: false
});