feat: dashboard billing (#193)
* feat: move plans into sharable json * remove unused code * update pnpm lock * update plans.json * implement early adopter pricing * docs: update meta title and description * Finished the UI for upgrading plans * Updated the pricing plans * Updated the cards ui * use relative import * improve styling of upgrade plan card * styling improvements, lemonsqueezy setup * trigger lemonsqueezy payment and add webhook * update pnpm lock * add logout route * feat: complete billing functionality * generate lock file * make crypto false on browser * feat: adding upgrade prompts to org * chore: add upgrade icon to upload video * feat: add upgrade prompt to certificate and settings * feat: implement onboarding setup page (#229) * working on the onboarding setup * working on the onboarding page * Almost done with the onboarding UI * finished the UI of the onBoarding setup * finished the UI of the onBoarding setup * Implementing the functions to carryout the logic of updating the UI onmount if a user has carried out a certain step or not * Implementing the functions to carryout the logic of updating the UI onmount if a user has carried out a certain step or not * making some chnages * finished the onBoarding task * updated some logic * Removed the uneeded svg from the static folder * fixed isCoursePublished not returning a boolean * improved the setup link protection * chore: make menuItems reactive * improve formatting * format supabase queries --------- Co-authored-by: Rotimi Best <best@classroomio.com> * revamp setup * improve the upgrade experience * migration * feat: add lemonsqueezy customer portal --------- Co-authored-by: emmanuel <chifez1@gmail.com>
@@ -149,7 +149,7 @@
|
||||
{:else}
|
||||
{#each poll.options as option, index}
|
||||
<button
|
||||
class="bg-white dark:bg-black rounded-md border-2 border-gray-100 h-[50px] {didUserSelect(
|
||||
class="bg-white dark:bg-black rounded-md border-2 border-gray-100 dark:border-neutral-600 h-[50px] {didUserSelect(
|
||||
option,
|
||||
currentUserId
|
||||
) &&
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
class="mr-3 pb-2 border-t-0 border-l-0 border-r-0 {selectedTab === tab.value
|
||||
? 'border-b-4 border-indigo-600 focus:border-indigo-600'
|
||||
? 'border-b-4 border-primary-600 focus:border-primary-600'
|
||||
: 'border-b-0'}"
|
||||
on:click={() => (selectedTab = tab.value)}
|
||||
>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
|
||||
function getAppClass(appName: string, selected: string | undefined) {
|
||||
return `transition duration-500 delay-150 ease-in-out border-t-0 border-b-0 border-r-0 ${
|
||||
appName === selected ? 'border-l-4 border-indigo-600 focus:border-indigo-600' : 'border-l-0'
|
||||
appName === selected ? 'border-l-4 border-primary-600 focus:border-primary-600' : 'border-l-0'
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="{className} w-full h-[517px] flex items-center justify-center flex-col border rounded border-gray-200"
|
||||
class="{className} w-full h-[517px] flex items-center justify-center flex-col border rounded border-gray-200 dark:border-neutral-600"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -1,29 +1,7 @@
|
||||
<script lang="ts">
|
||||
import posthog from 'posthog-js';
|
||||
import { goto } from '$app/navigation';
|
||||
import { logout } from '$lib/utils/functions/logout';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { currentOrg, orgs, defaultCurrentOrgState } from '$lib/utils/store/org';
|
||||
import { user, profile, defaultProfileState, defaultUserState } from '$lib/utils/store/user';
|
||||
|
||||
async function logout() {
|
||||
const { error } = await supabase.auth.signOut();
|
||||
|
||||
if (error) {
|
||||
console.error('Error logging out: ', error);
|
||||
}
|
||||
|
||||
currentOrg.set(defaultCurrentOrgState);
|
||||
orgs.set([]);
|
||||
user.set(defaultUserState);
|
||||
profile.set(defaultProfileState);
|
||||
|
||||
capturePosthogEvent('user_logged_out');
|
||||
posthog.reset();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} label="Log out" onClick={logout} />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
||||
import { currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
@@ -8,7 +9,7 @@
|
||||
</script>
|
||||
|
||||
<a href={$currentOrgDomain} target="_blank" class="{className} ml-2 hover:no-underline">
|
||||
<PrimaryButton>
|
||||
<PrimaryButton variant={VARIANTS.CONTAINED_DARK}>
|
||||
{#if !$isMobile}
|
||||
View site
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
export let name = '';
|
||||
</script>
|
||||
|
||||
<p
|
||||
class="w-fit text-xs border font-semibold px-1.5 rounded-md text-center text-blue-700 border-blue-700"
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
@@ -4,12 +4,14 @@
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import Professional from './certificates/Professional.svelte';
|
||||
import Plain from './certificates/Plain.svelte';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||
|
||||
const studentNamePlaceholder = 'Name of student';
|
||||
const themes = ['professional', 'plain'];
|
||||
@@ -44,14 +46,20 @@
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
<main class="md:-ml-3 md:-mr-3 px-2">
|
||||
<UpgradeBanner>Upgrade your plan to generate certificates</UpgradeBanner>
|
||||
|
||||
<main class="md:-ml-3 md:-mr-3 px-2 mt-2">
|
||||
<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
|
||||
>
|
||||
<p class="text-xs font-normal my-4 dark:text-gray-100">Choose a theme</p>
|
||||
<RadioButtonGroup bind:selected={$course.certificate_theme} class="mb-10">
|
||||
<RadioButtonGroup
|
||||
bind:selected={$course.certificate_theme}
|
||||
class="mb-10"
|
||||
disabled={$isFreePlan}
|
||||
>
|
||||
{#each themes as theme}
|
||||
<div class="flex mr-3">
|
||||
<RadioButton value={theme} />
|
||||
@@ -71,11 +79,12 @@
|
||||
>Settings > Organisation settings</strong
|
||||
> and upload your brand logo
|
||||
</p>
|
||||
|
||||
<PrimaryButton
|
||||
label="Go to Settings"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
className="rounded-md mt-3"
|
||||
onClick={() => goto(`org/${$currentOrg.siteName}/settings`)}
|
||||
onClick={() => goto(`/org/${$currentOrg.siteName}/settings`)}
|
||||
/>
|
||||
</div>
|
||||
<span class="my-4">
|
||||
@@ -86,6 +95,7 @@
|
||||
bgColor="bg-gray-100"
|
||||
bind:value={$course.description}
|
||||
errorMessage={errors.description}
|
||||
disabled={$isFreePlan}
|
||||
/>
|
||||
</span>
|
||||
<Toggle
|
||||
@@ -93,6 +103,7 @@
|
||||
bind:toggled={$course.is_certificate_downloadable}
|
||||
class="my-4"
|
||||
size="sm"
|
||||
disabled={$isFreePlan}
|
||||
>
|
||||
<span slot="labelA" style={$globalStore.isDark ? 'color: white' : 'color: #161616'}
|
||||
>Locked</span
|
||||
@@ -115,11 +126,17 @@
|
||||
</div>
|
||||
<div class="h-1/5">
|
||||
<PrimaryButton
|
||||
label="Save Changes"
|
||||
className="rounded-md"
|
||||
className="rounded-md flex gap-2 items-center"
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={saveCertificate}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
isDisabled={$isFreePlan}
|
||||
>
|
||||
{#if $isFreePlan}
|
||||
<FlashFilled size={16} class="text-blue-700" />
|
||||
{/if}
|
||||
Save Changes
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
|
||||
<section class="flex flex-wrap items-start gap-2 mb-10 -ml-4 -mr-4">
|
||||
{#each CertificateReportData as data (data.id)}
|
||||
<div class="box flex flex-col rounded border border-gray-200 justify-center px-2 mb-5">
|
||||
<div
|
||||
class="box flex flex-col rounded border border-gray-200 dark:border-neutral-600 justify-center px-2 mb-5"
|
||||
>
|
||||
<p class="text-xs font-normal">{data.title}</p>
|
||||
<bold class="text-2xl font-medium">{data.value}</bold>
|
||||
</div>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
modalHeading="Add New Lesson"
|
||||
>
|
||||
<div
|
||||
class="sm:min-h-[245px] md:min-h-[100px] lg:min-h-[190px] relative m-auto rounded-md border-2 border-gray-200 py-2 md:py-3 px-2 md:px-5 mb-2 md:mb-4 flex flex-wrap items-center dark:bg-neutral-800"
|
||||
class="sm:min-h-[245px] md:min-h-[100px] lg:min-h-[190px] relative m-auto rounded-md border-2 border-gray-200 dark:border-neutral-600 py-2 md:py-3 px-2 md:px-5 mb-2 md:mb-4 flex flex-wrap items-center dark:bg-neutral-800"
|
||||
>
|
||||
<div class="mr-5">
|
||||
<TextChip
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
class="w-[261px] h-[240px] p-5 rounded-md dark:bg-neutral-700 border-2 {option.type ===
|
||||
type
|
||||
? 'border-primary-400'
|
||||
: `border-gray-200 dark:border-gray-400 ${
|
||||
: `border-gray-200 dark:border-neutral-600 dark:border-gray-400 ${
|
||||
!option.isDisabled && 'hover:scale-95'
|
||||
}`} flex flex-col {option.isDisabled &&
|
||||
'cursor-not-allowed opacity-60'} transition-all ease-in-out"
|
||||
@@ -205,7 +205,7 @@
|
||||
class="w-[161px] h-[140px] hover:scale-95 p-5 rounded-md dark:bg-neutral-700 border-2 {template.id ===
|
||||
selectedTemplateId
|
||||
? 'border-primary-400'
|
||||
: `border-gray-200 dark:border-gray-400`} flex flex-col transition-all ease-in-out"
|
||||
: `border-gray-200 dark:border-neutral-600 dark:border-gray-400`} flex flex-col transition-all ease-in-out"
|
||||
type="button"
|
||||
on:click={() => (selectedTemplateId = template.id)}
|
||||
>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import LocalVideo from './LocalVideo.svelte';
|
||||
import EmbedLink from './EmbedLink.svelte';
|
||||
import * as CONSTANTS from './constants';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||
|
||||
export let lessonId = '';
|
||||
|
||||
@@ -23,14 +25,18 @@
|
||||
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'
|
||||
: 'border border-gray-200 dark:border-neutral-600'
|
||||
} rounded-md cursor-pointer flex flex-row items-center justify-start gap-2 whitespace-nowrap`}
|
||||
>
|
||||
<svelte:component
|
||||
this={item.icon}
|
||||
size={20}
|
||||
color={`${currentTab === item.value ? 'dark:invert-0' : 'dark:invert'}`}
|
||||
/>
|
||||
{#if $isFreePlan && item.value === 3}
|
||||
<FlashFilled size={20} class="text-blue-700" />
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={item.icon}
|
||||
size={20}
|
||||
color={`${currentTab === item.value ? 'dark:invert-0' : 'dark:invert'}`}
|
||||
/>
|
||||
{/if}
|
||||
<p>{item.title}</p>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import axios from 'axios';
|
||||
import { PUBLIC_SERVER_URL } from '$env/static/public';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
@@ -10,11 +10,14 @@
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { ProgressBar } from 'carbon-components-svelte';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
|
||||
let value = 0;
|
||||
let max = 100;
|
||||
let status = 'active';
|
||||
let fileSize;
|
||||
let isDisabled = false;
|
||||
|
||||
export let lessonId = '';
|
||||
|
||||
@@ -61,7 +64,7 @@
|
||||
console.log('Upload res', formRes);
|
||||
isLoading = false;
|
||||
isLoaded = false;
|
||||
} catch (err) {
|
||||
} catch (err: any) {
|
||||
console.error('Error uploading video', err, '\n\n', err.response);
|
||||
if (err.response) {
|
||||
formRes = err.response.data;
|
||||
@@ -115,14 +118,19 @@
|
||||
}
|
||||
|
||||
$: isDoneUploading(formRes);
|
||||
$: isDisabled = isLoading || !PUBLIC_SERVER_URL || $isFreePlan;
|
||||
</script>
|
||||
|
||||
<UpgradeBanner className="mb-3" onClick={() => ($uploadCourseVideoStore.isModalOpen = false)}>
|
||||
Upgrade your plan to upload vidoes
|
||||
</UpgradeBanner>
|
||||
|
||||
{#if !isLoaded}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (fileInput && !isLoading ? fileInput.click() : null)}
|
||||
class="h-full w-full"
|
||||
disabled={isLoading || !PUBLIC_SERVER_URL}
|
||||
class="h-full w-full {isDisabled && 'hover:cursor-not-allowed opacity-50'}"
|
||||
disabled={isDisabled}
|
||||
>
|
||||
<form
|
||||
class="border-primary-300 flex h-full w-full flex-col items-center justify-center rounded-xl border border-dashed"
|
||||
|
||||
@@ -6,8 +6,10 @@
|
||||
import { SkeletonText } from 'carbon-components-svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||
import { handleAddLessonWidget } from './store';
|
||||
import NavIcons from './NavIcons.svelte';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
export let handleClick = () => {};
|
||||
export let label = '';
|
||||
@@ -17,6 +19,7 @@
|
||||
export let isLoading = true;
|
||||
export let isLesson = false;
|
||||
export let isStudent = true;
|
||||
export let isPaidFeature = false;
|
||||
// export let subMenuItems = [];
|
||||
|
||||
function addLesson() {
|
||||
@@ -54,6 +57,9 @@
|
||||
{/if}
|
||||
<span class="grow" />
|
||||
|
||||
{#if isPaidFeature && $isFreePlan}
|
||||
<FlashFilled size={20} class="text-blue-700" />
|
||||
{/if}
|
||||
{#if isLesson && !isLoading}
|
||||
{#if !isStudent}
|
||||
<IconButton onClick={() => addLesson()} size="small">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import { sideBar } from '$lib/components/Org/store';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { getIsLessonComplete } from '../Lesson/functions';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
export let path: string;
|
||||
export let isStudent: boolean = false;
|
||||
@@ -25,6 +25,7 @@
|
||||
to: string;
|
||||
hideSortIcon: boolean;
|
||||
isLesson?: boolean;
|
||||
isPaidFeature: boolean;
|
||||
show?: () => boolean;
|
||||
}
|
||||
|
||||
@@ -148,23 +149,27 @@
|
||||
{
|
||||
label: 'News Feed',
|
||||
to: getNavItemRoute($course.id),
|
||||
hideSortIcon: true
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: false
|
||||
},
|
||||
{
|
||||
label: 'Lessons',
|
||||
to: getLessonsRoute($course.id),
|
||||
hideSortIcon: false,
|
||||
isPaidFeature: false,
|
||||
isLesson: true
|
||||
},
|
||||
{
|
||||
label: 'Attendance',
|
||||
to: getNavItemRoute($course.id, 'attendance'),
|
||||
isPaidFeature: false,
|
||||
hideSortIcon: true
|
||||
},
|
||||
{
|
||||
label: 'Submissions',
|
||||
to: getNavItemRoute($course.id, 'submissions'),
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: false,
|
||||
show() {
|
||||
return !isStudent;
|
||||
}
|
||||
@@ -172,27 +177,33 @@
|
||||
{
|
||||
label: 'Marks',
|
||||
to: getNavItemRoute($course.id, 'marks'),
|
||||
isPaidFeature: false,
|
||||
hideSortIcon: true
|
||||
},
|
||||
// {
|
||||
// label: 'Scoreboard',
|
||||
// to: getNavItemRoute($course.id, 'scoreboard'),
|
||||
// hideSortIcon: true,
|
||||
// },
|
||||
{
|
||||
label: 'People',
|
||||
to: getNavItemRoute($course.id, 'people'),
|
||||
isPaidFeature: false,
|
||||
hideSortIcon: true
|
||||
},
|
||||
{
|
||||
label: 'Certificates',
|
||||
to: getNavItemRoute($course.id, 'certificates'),
|
||||
hideSortIcon: true
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: true,
|
||||
show() {
|
||||
// Dont show students if org on free plan
|
||||
if (isStudent && $isFreePlan) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Landing Page',
|
||||
to: getNavItemRoute($course.id, 'landingpage'),
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: false,
|
||||
show() {
|
||||
return !isStudent;
|
||||
}
|
||||
@@ -201,6 +212,7 @@
|
||||
label: 'Settings',
|
||||
to: getNavItemRoute($course.id, 'settings'),
|
||||
hideSortIcon: true,
|
||||
isPaidFeature: false,
|
||||
show() {
|
||||
return !isStudent;
|
||||
}
|
||||
@@ -219,12 +231,12 @@
|
||||
transition w-[90vw] md:w-[300px] lg:w-[350px] bg-gray-100 dark:bg-black h-[calc(100vh-48px)]
|
||||
|
||||
${
|
||||
resize && 'border-r-8 border-r-blue-500'
|
||||
resize ? 'border-r-8 border-r-blue-500' : 'dark:border-r-neutral-600'
|
||||
} overflow-y-auto border border-l-0 border-t-0 border-b-0 border-r-1`}
|
||||
style={$sideBar.hidden === true ? 'width:0' : 'width:300px'}
|
||||
bind:this={sidebarRef}
|
||||
>
|
||||
<div class="sidebar-contenth-full flex flex-col">
|
||||
<div class="flex flex-col">
|
||||
<ul
|
||||
class="sidebar-content my-5"
|
||||
bind:this={menuContentRef}
|
||||
@@ -240,6 +252,7 @@
|
||||
total={navItem.isLesson ? ($lessons || []).length : 0}
|
||||
isLoading={!$course.id}
|
||||
isLesson={navItem.isLesson}
|
||||
isPaidFeature={navItem.isPaidFeature}
|
||||
{isStudent}
|
||||
>
|
||||
{#if navItem.isLesson}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
id={feed.id}
|
||||
class="flex flex-col gap-5 {isActive
|
||||
? 'border-2 border-primary-700'
|
||||
: 'border border-gray-200'} rounded-md mb-7 max-w-3xl"
|
||||
: 'border border-gray-200 dark:border-neutral-600'} rounded-md mb-7 max-w-3xl"
|
||||
>
|
||||
<section>
|
||||
<div class="p-3 pb-0">
|
||||
@@ -162,7 +162,7 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="border-t border-gray-200 p-3">
|
||||
<section class="border-t border-gray-200 dark:border-neutral-600 p-3">
|
||||
{#if feed.comment.length > 0}
|
||||
<button
|
||||
on:click={expandComment}
|
||||
@@ -222,7 +222,7 @@
|
||||
bind:value={comment}
|
||||
on:keydown={handleAddNewComment}
|
||||
placeholder="Add class comment"
|
||||
class="w-full bg-transparent border border-gray-200 rounded-3xl"
|
||||
class="w-full bg-transparent border border-gray-200 dark:border-neutral-600 rounded-3xl"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -14,13 +14,14 @@
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { updateCourse, deleteCourse } from '$lib/utils/services/courses';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import { currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
||||
import { isObject } from '$lib/utils/functions/isObject';
|
||||
import { lessons } from '../Lesson/store/lessons';
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
|
||||
import { Restart } from 'carbon-icons-svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
@@ -186,7 +187,7 @@
|
||||
$: setDefault($course);
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200">
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600">
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>Cover Image</SectionTitle>
|
||||
@@ -287,10 +288,14 @@
|
||||
<p>Make the Lesson available for Download in pdf for registered students</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<Toggle size="sm" bind:toggled={$settings.lesson_download}>
|
||||
<span slot="labelA" style="color: gray">Disabled</span>
|
||||
<span slot="labelB" style="color: gray">Enabled</span>
|
||||
</Toggle>
|
||||
{#if $isFreePlan}
|
||||
<UpgradeBanner>Upgrade to download lessons</UpgradeBanner>
|
||||
{:else}
|
||||
<Toggle size="sm" bind:toggled={$settings.lesson_download}>
|
||||
<span slot="labelA" style="color: gray">Disabled</span>
|
||||
<span slot="labelB" style="color: gray">Enabled</span>
|
||||
</Toggle>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
@@ -299,13 +304,17 @@
|
||||
<p>Make the Course available for Download for registered students</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label="Download"
|
||||
onClick={downloadCourse}
|
||||
isDisabled={isLoading || !PUBLIC_SERVER_URL}
|
||||
{isLoading}
|
||||
/>
|
||||
{#if $isFreePlan}
|
||||
<UpgradeBanner>Upgrade to download course</UpgradeBanner>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label="Download"
|
||||
onClick={downloadCourse}
|
||||
isDisabled={isLoading || !PUBLIC_SERVER_URL}
|
||||
{isLoading}
|
||||
/>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { Tag, ImageLoader, SkeletonPlaceholder, ContextMenu, ContextMenuDivider, ContextMenuOption, } from 'carbon-components-svelte';
|
||||
import {
|
||||
Tag,
|
||||
ImageLoader,
|
||||
SkeletonPlaceholder,
|
||||
ContextMenu,
|
||||
ContextMenuDivider,
|
||||
ContextMenuOption
|
||||
} from 'carbon-components-svelte';
|
||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import CopyFile from "carbon-icons-svelte/lib/CopyFile.svelte";
|
||||
import Share from "carbon-icons-svelte/lib/Share.svelte";
|
||||
import UserFollow from "carbon-icons-svelte/lib/UserFollow.svelte";
|
||||
import CopyFile from 'carbon-icons-svelte/lib/CopyFile.svelte';
|
||||
import Share from 'carbon-icons-svelte/lib/Share.svelte';
|
||||
import UserFollow from 'carbon-icons-svelte/lib/UserFollow.svelte';
|
||||
|
||||
export let bannerImage: string | undefined;
|
||||
export let id = '';
|
||||
@@ -52,7 +59,7 @@
|
||||
<ContextMenuOption indented labelText="Share" icon={Share} on:click={handleShareCourse} />
|
||||
<ContextMenuOption indented labelText="Invite" icon={UserFollow} on:click={handleInvite} />
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuOption kind="danger" labelText="Delete" on:click={handleDeleteCourse} />
|
||||
<ContextMenuOption kind="danger" labelText="Delete" on:click={handleDeleteCourse} />
|
||||
</ContextMenu>
|
||||
{/if}
|
||||
|
||||
@@ -60,14 +67,14 @@
|
||||
rel="prefetch"
|
||||
bind:this={target}
|
||||
href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`}
|
||||
class="text-black border border-gray rounded w-full max-w-[320px] relative hover:scale-95 transition-all ease-in-out"
|
||||
class="text-black border border-gray dark:border-neutral-600 rounded w-full max-w-[320px] relative hover:scale-95 transition-all ease-in-out"
|
||||
>
|
||||
<div class="p-4">
|
||||
<div class=" mb-5">
|
||||
<ImageLoader
|
||||
src={bannerImage}
|
||||
alt="Course Logo"
|
||||
class="h-[200px] w-full rounded dark:border dark:border-white"
|
||||
class="h-[200px] w-full rounded dark:border dark:border-neutral-600"
|
||||
>
|
||||
<svelte:fragment slot="loading">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
|
||||
@@ -83,7 +90,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="px-4 border border-gray border-b-0 border-l-0 border-r-0 flex justify-between {isLMS &&
|
||||
class="px-4 border border-gray dark:border-neutral-600 border-b-0 border-l-0 border-r-0 flex justify-between {isLMS &&
|
||||
'items-center'}"
|
||||
>
|
||||
<div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</p>
|
||||
{/if}
|
||||
<span
|
||||
class="form-input custom-placeholder w-full block overflow-hidden resize min-h-[50px] leading-5 border mt-2 p-3 pt-3.5 outline-none dark:text-black dark:bg-white form-input border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 {inputClassName} {focusClass} {isDisabled &&
|
||||
class="form-input custom-placeholder w-full block overflow-hidden resize min-h-[50px] leading-5 border mt-2 p-3 pt-3.5 outline-none dark:text-black dark:bg-white form-input border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 dark:border-neutral-600 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 {inputClassName} {focusClass} {isDisabled &&
|
||||
'hover:cursor-not-allowed opacity-50'} dark:text-black p-3 mt-1 block w-full {bgColor} {errorMessage
|
||||
? 'border-red-600'
|
||||
: ''}"
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
bind:value
|
||||
bind:this={ref}
|
||||
required={isRequired}
|
||||
class="form-textarea border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 focus:border-primary-600 mt-1 block w-full
|
||||
class="form-textarea border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 dark:border-neutral-600 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 focus:border-primary-600 mt-1 block w-full
|
||||
{bgColor} {!!errorMessage ? 'border-red-500' : 'border-gray-300'}"
|
||||
on:change={onChange}
|
||||
/>
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
{/if}
|
||||
<input
|
||||
use:typeAction
|
||||
class="form-input border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 {inputClassName} {focusClass} {isDisabled &&
|
||||
class="form-input border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 dark:border-neutral-600 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 {inputClassName} {focusClass} {isDisabled &&
|
||||
'hover:cursor-not-allowed opacity-50'} dark:text-black p-3 mt-1 block w-full {bgColor} {errorMessage
|
||||
? 'border-red-600'
|
||||
: ''}"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@@ -3,8 +3,8 @@
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,6 +1,6 @@
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg width="61" height="61" viewBox="0 0 61 61" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M30.5 0C13.6564 0 0 13.6564 0 30.5C0 47.3436 13.6564 61 30.5 61C47.3436 61 61 47.3436 61 30.5C61 13.6564 47.3436 0 30.5 0ZM44.6291 22.0629L30.0196 44.3927C30.0158 44.3966 30.0082 44.4004 30.0082 44.4042C29.9319 44.5186 29.89 44.6482 29.7947 44.7549C29.6612 44.8998 29.4897 44.9761 29.3372 45.0866C29.2991 45.1133 29.2609 45.1438 29.219 45.1705C28.9826 45.3192 28.7386 45.4183 28.4718 45.4831C28.3841 45.506 28.304 45.5289 28.2125 45.5441C28.0002 45.5733 27.785 45.5759 27.572 45.5518C27.4216 45.5422 27.2723 45.5193 27.1259 45.4831C26.9749 45.4376 26.8282 45.3789 26.6875 45.3078C26.5655 45.2506 26.4359 45.2353 26.3215 45.1629C26.2376 45.1095 26.1919 45.0256 26.1194 44.9608C26.0889 44.9341 26.0508 44.9265 26.0203 44.8998L17.5146 37.0346C17.0105 36.5505 16.7163 35.8879 16.6955 35.1893C16.6746 34.4906 16.9287 33.8117 17.403 33.2984C17.8774 32.785 18.5343 32.4783 19.2324 32.4441C19.9305 32.4099 20.6142 32.6509 21.1365 33.1154L27.2937 38.8113L40.1609 19.1426C40.5481 18.55 41.1549 18.1356 41.8477 17.9905C42.5406 17.8453 43.2627 17.9814 43.8552 18.3686C44.4477 18.7559 44.8621 19.3627 45.0073 20.0555C45.1524 20.7483 45.0164 21.4704 44.6291 22.0629Z"
|
||||
fill="#0c891b"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -56,7 +56,7 @@
|
||||
} overflow-y-auto transition w-[250px] min-w-[250px] bg-gray-100 dark:bg-neutral-900 h-[calc(100vh-48px)]`}
|
||||
>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="border-b border-gray-200 pt-5 px-4">
|
||||
<div class="border-b border-gray-200 dark:border-neutral-600 pt-5 px-4">
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<Avatar
|
||||
src={$profile.avatar_url}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
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 { profile } from '$lib/utils/store/user';
|
||||
@@ -61,7 +62,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5">
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 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">
|
||||
<p class="dark:text-white font-bold">Profile Picture</p>
|
||||
@@ -83,6 +84,7 @@
|
||||
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
|
||||
<PrimaryButton
|
||||
label="Update Profile"
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
className="mr-5"
|
||||
isLoading={loading}
|
||||
isDisabled={loading}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="dialog fixed overflow-x-hidden overflow-y-auto inset-0 flex justify-center items-center z-50 bg-gray-100 bg-opacity-50"
|
||||
class="dialog fixed inset-0 z-50 flex items-center justify-center overflow-y-auto overflow-x-hidden bg-gray-100 bg-opacity-50"
|
||||
on:click={onClose}
|
||||
role="presentation"
|
||||
>
|
||||
@@ -32,12 +32,14 @@
|
||||
<div
|
||||
class="{maxWidth || 'container'} {size === 'sm'
|
||||
? 'small'
|
||||
: ''} bg-white dark:bg-neutral-800 mx-auto {width} shadow-lg p-5 pt-2 rounded-md"
|
||||
: ''} mx-auto bg-white dark:bg-neutral-800 {width} rounded-md pt-2 shadow-lg"
|
||||
on:click={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<p class="dark:text-white text-md font-medium m-0">
|
||||
<div
|
||||
class="mb-4 flex items-center justify-between border border-l-0 border-r-0 border-t-0 border-gray-100 dark:border-neutral-600 p-4 px-5"
|
||||
>
|
||||
<p class="text-md m-0 font-medium dark:text-white">
|
||||
{modalHeading}
|
||||
</p>
|
||||
{#if isCloseable}
|
||||
@@ -47,7 +49,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="body p-1 h-4/5 overflow-y-auto {containerClass}">
|
||||
<div class="body h-4/5 overflow-y-auto p-6 {containerClass}">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="h-[70px] w-full border border-gray-100 dark:bg-neutral-800 mx-4 my-2 rounded-md transition ease-in-out relative flex pt-3 px-3"
|
||||
class="h-[70px] w-full border border-gray-100 dark:border-neutral-600 dark:bg-neutral-800 mx-4 my-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;" />
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
</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"
|
||||
class="root w-full border border-gray-100 dark:border-neutral-600 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">
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script>
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { currentOrgPlan, isFreePlan } from '$lib/utils/store/org';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
|
||||
let isRedirecting = false;
|
||||
|
||||
async function onOpenBilling() {
|
||||
console.log({
|
||||
plan: $currentOrgPlan
|
||||
});
|
||||
|
||||
if ($currentOrgPlan?.subscriptionId) {
|
||||
isRedirecting = true;
|
||||
fetch('/api/lmz/subscriptions?subscriptionId=' + $currentOrgPlan?.subscriptionId)
|
||||
.then((response) => response.json())
|
||||
.then((res) => {
|
||||
isRedirecting = false;
|
||||
window.open(res.customerPortal, '_blank');
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 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>Billing details</SectionTitle></Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="dark:text-white lg:mt-0">Manage your billing</h4>
|
||||
{#if $isFreePlan}
|
||||
You don't have any active billing
|
||||
{:else}
|
||||
<p class="mb-3">We use Lemonsqueezy to help manage your billing</p>
|
||||
|
||||
<PrimaryButton
|
||||
label="Open billing"
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={onOpenBilling}
|
||||
isLoading={isRedirecting}
|
||||
/>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -72,7 +72,7 @@
|
||||
$: setSiteName($currentOrg.siteName);
|
||||
</script>
|
||||
|
||||
<Grid class="border rounded border-gray-200 w-full mt-5">
|
||||
<Grid class="border rounded border-gray-200 dark:border-neutral-600 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}>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5">
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 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" />
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
$: setDefault($currentOrg?.landingpage as OrgLandingPageJson);
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5 relative">
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 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>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script>
|
||||
import { goto } from '$app/navigation';
|
||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
@@ -9,6 +10,7 @@
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
let avatar;
|
||||
let loading = false;
|
||||
@@ -94,7 +96,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5">
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 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">
|
||||
@@ -175,7 +177,7 @@
|
||||
</p>
|
||||
<PrimaryButton
|
||||
label="Edit domain"
|
||||
className="my-7 py-5 px-10 text-primary-700"
|
||||
className="my-7 py-5 px-10"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSetting('/domains')}
|
||||
/>
|
||||
@@ -189,11 +191,15 @@
|
||||
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"
|
||||
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSetting('/teams')}
|
||||
/>
|
||||
>
|
||||
{#if $isFreePlan}
|
||||
<FlashFilled size={16} class="text-blue-700" />
|
||||
{/if}
|
||||
Manage Team
|
||||
</PrimaryButton>
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import SectionTitle from '../SectionTitle.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 { profile } from '$lib/utils/store/user';
|
||||
@@ -61,7 +62,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 w-full mt-5">
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600 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>
|
||||
@@ -83,6 +84,7 @@
|
||||
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
|
||||
<PrimaryButton
|
||||
label="Update Profile"
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
className="mr-5"
|
||||
isLoading={loading}
|
||||
isDisabled={loading}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
triggerSendEmail,
|
||||
NOTIFICATION_NAME
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||
|
||||
let emailsStr = '';
|
||||
@@ -132,7 +133,7 @@
|
||||
$: fetchTeam($currentOrg.id);
|
||||
</script>
|
||||
|
||||
<Grid class="border rounded border-gray-200 w-full mt-5">
|
||||
<Grid class="border rounded border-gray-200 dark:border-neutral-600 w-full mt-5 relative">
|
||||
<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}>
|
||||
@@ -146,10 +147,11 @@
|
||||
placeholder="email comma separated"
|
||||
bind:value={emailsStr}
|
||||
className="mb-3"
|
||||
isDisabled={$isFreePlan}
|
||||
{errorMessage}
|
||||
/>
|
||||
|
||||
<Select labelText="Role" bind:selected={role} class="mb-5 w-40">
|
||||
<Select labelText="Role" bind:selected={role} class="mb-5 w-40" disabled={$isFreePlan}>
|
||||
<!-- <SelectItem value={ROLE.ADMIN} text={ROLE_LABEL[ROLE.ADMIN]} /> -->
|
||||
<SelectItem value={ROLE.TUTOR} text={ROLE_LABEL[ROLE.TUTOR]} />
|
||||
</Select>
|
||||
@@ -158,7 +160,7 @@
|
||||
label="Send Invite"
|
||||
onClick={onSendInvite}
|
||||
{isLoading}
|
||||
isDisabled={isLoading}
|
||||
isDisabled={isLoading || $isFreePlan}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import LandingpageSettings from './LandingpageSettings.svelte';
|
||||
// import Account from './Account.svelte';
|
||||
import OrgSettings from './OrgSettings.svelte';
|
||||
import Billing from './Billing.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -64,6 +65,13 @@
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
label: 'Billing',
|
||||
tabKey: 'billing',
|
||||
href: `${$page.url.pathname}?tab=billing`,
|
||||
disabled: !$isOrgAdmin
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
label: 'Integrations',
|
||||
tabKey: 'integrations',
|
||||
href: `${$page.url.pathname}?tab=integrations`,
|
||||
@@ -91,6 +99,9 @@
|
||||
<TabContent class="w-full p-0">
|
||||
<LandingpageSettings />
|
||||
</TabContent>
|
||||
<TabContent class="w-full p-0">
|
||||
<Billing />
|
||||
</TabContent>
|
||||
<TabContent class="w-full p-0">
|
||||
<Integrations />
|
||||
</TabContent>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
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 { SettingsAdjust } from 'carbon-icons-svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.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';
|
||||
@@ -11,37 +11,18 @@
|
||||
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 { currentOrgPath, currentOrgPlan, isFreePlan } from '$lib/utils/store/org';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||
import { sideBar } from './store';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
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'
|
||||
}
|
||||
];
|
||||
let menuItems: {
|
||||
path: string;
|
||||
label: string;
|
||||
show: boolean;
|
||||
}[] = [];
|
||||
|
||||
function isActive(pagePath: string, itemPath: string) {
|
||||
const pageLinkItems = pagePath.split('/');
|
||||
@@ -56,87 +37,137 @@
|
||||
const toggleSidebar = () => {
|
||||
$sideBar.hidden = !$sideBar.hidden;
|
||||
};
|
||||
|
||||
const openModal = () => {
|
||||
goto(window.location.pathname + '?upgrade=true');
|
||||
};
|
||||
|
||||
$: {
|
||||
menuItems = [
|
||||
{
|
||||
path: '',
|
||||
label: 'Dashboard',
|
||||
show: true
|
||||
},
|
||||
// {
|
||||
// path: '/quiz',
|
||||
// label: 'Quizzes'
|
||||
// },
|
||||
{
|
||||
path: '/courses',
|
||||
label: 'Courses',
|
||||
show: true
|
||||
},
|
||||
{
|
||||
path: '/community',
|
||||
label: 'Community',
|
||||
show: true
|
||||
},
|
||||
// {
|
||||
// path: '/site',
|
||||
// label: 'Site settings',
|
||||
// },
|
||||
{
|
||||
path: '/audience',
|
||||
label: 'Audience',
|
||||
show: true
|
||||
},
|
||||
{
|
||||
path: '/setup',
|
||||
label: 'Setup',
|
||||
show: $isOrgAdmin
|
||||
}
|
||||
];
|
||||
}
|
||||
</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`}
|
||||
? 'absolute z-40 -translate-x-[100%] md:relative md:translate-x-0'
|
||||
: 'absolute z-40 translate-x-0 md:relative'
|
||||
} border-r-1 h-[calc(100vh-48px)] w-[250px] min-w-[250px] overflow-y-auto border border-b-0 border-l-0 border-t-0 border-gray-100 dark:border-neutral-600 bg-gray-100 transition dark:bg-neutral-900`}
|
||||
>
|
||||
<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}
|
||||
|
||||
<div class="flex h-full flex-col">
|
||||
<div class="">
|
||||
<OrgSelector />
|
||||
|
||||
<ul class="my-5">
|
||||
<ul class="mt-4 my-2 px-4">
|
||||
{#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.show}
|
||||
<a
|
||||
href="{$currentOrgPath}{menuItem.path}"
|
||||
class="text-black no-underline"
|
||||
on:click={toggleSidebar}
|
||||
>
|
||||
{#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>
|
||||
<li
|
||||
class="mb-1 flex items-center gap-2.5 px-2.5 py-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={20} class="carbon-icon fill-[#000] dark:fill-[#fff]" />
|
||||
{:else if menuItem.path === '/quiz'}
|
||||
<QuizIcon />
|
||||
{:else if menuItem.path === '/audience'}
|
||||
<AudienceIcon />
|
||||
{:else if menuItem.path === '/setup'}
|
||||
<SettingsAdjust />
|
||||
{/if}
|
||||
<p class="text-sm font-medium">{menuItem.label}</p>
|
||||
</li>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
<span class="flex-grow" />
|
||||
<ul class="my-5 pb-5 px-4">
|
||||
|
||||
{#if $isFreePlan}
|
||||
<div
|
||||
class="border-primary-700 mx-4 flex flex-col items-center justify-center gap-4 rounded-md border px-2 py-6 text-center hover:scale-95 transition-all ease-in-out"
|
||||
>
|
||||
<img src="/upgrade.png" alt="upgrade" class="h-16 w-16" />
|
||||
<span class="flex flex-col gap-1">
|
||||
<p class="text-base font-semibold">Become an Early Adopter</p>
|
||||
<p class="text-xs">Unlock unlimited features and invest in our future</p>
|
||||
</span>
|
||||
<PrimaryButton label="Upgrade Now" onClick={openModal} className="font-normal" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<ul class="my-5 px-4 pb-5">
|
||||
<a href={$currentOrgPath} class="text-black no-underline" on:click={toggleSidebar}>
|
||||
<li class="flex items-center py-3 px-4 mb-2 rounded">
|
||||
<li class="mb-2 flex items-center rounded px-2.5 py-1.5">
|
||||
<HelpIcon size={20} class="carbon-icon dark:text-white" />
|
||||
<p class="dark:text-white ml-2">Help</p>
|
||||
<p class="ml-2.5 dark:text-white text-sm font-medium">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(
|
||||
class="mb-2 flex items-center px-2.5 py-1.5 {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>
|
||||
<Avatar
|
||||
src={$profile.avatar_url}
|
||||
name={$profile.username}
|
||||
width="w-[1.2rem]"
|
||||
height="h-[1.2rem]"
|
||||
/>
|
||||
<p class="ml-2.5 text-sm font-medium">Settings</p>
|
||||
</li>
|
||||
</a>
|
||||
</ul>
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Popover, SkeletonText } from 'carbon-components-svelte';
|
||||
import CaretDown from 'carbon-icons-svelte/lib/CaretDown.svelte';
|
||||
import ChevronSort from 'carbon-icons-svelte/lib/ChevronSort.svelte';
|
||||
import OrgSelectorItem from './OrgSelectorItem.svelte';
|
||||
import { currentOrg, orgs, currentOrgPath } from '$lib/utils/store/org';
|
||||
import { currentOrg, orgs, currentOrgPath, currentOrgPlan } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
import { newOrgModal } from '$lib/components/Org/store';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||
import { setTheme } from '$lib/utils/functions/theme';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import Plan from '$lib/components/Chip/Plan.svelte';
|
||||
import { PLAN_NAMES } from 'shared-constants/src/plans/constants';
|
||||
|
||||
export let canAddOrg = true;
|
||||
|
||||
let open = false;
|
||||
|
||||
function handleAddOrg() {
|
||||
@@ -19,33 +24,60 @@
|
||||
function onClick(org: CurrentOrg) {
|
||||
localStorage.setItem('classroomio_org_sitename', org.siteName);
|
||||
currentOrg.set(org);
|
||||
|
||||
setTheme(org.theme);
|
||||
goto($currentOrgPath);
|
||||
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-5 relative" data-outline>
|
||||
<div
|
||||
class="org-selector px-4 py-3 border border-l-0 border-r-0 border-t-0 border-gray-200 dark:border-neutral-600 relative"
|
||||
data-outline
|
||||
>
|
||||
{#if $currentOrg.name}
|
||||
<button
|
||||
class="flex items-center cursor-pointer max-w-[219px]"
|
||||
class="flex items-center gap-2 w-full"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
open = !open;
|
||||
}}
|
||||
>
|
||||
<p class="dark:text-white text-lg font-bold whitespace-nowrap truncate">
|
||||
{$currentOrg.name}
|
||||
</p>
|
||||
<CaretDown size={16} class="ml-2" />
|
||||
{#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
|
||||
size="sm"
|
||||
value={$currentOrg.shortName}
|
||||
className="bg-primary-200 dark:text-black font-medium"
|
||||
/>
|
||||
{/if}
|
||||
<div class="flex items-center cursor-pointer max-w-[219px] justify-between w-full">
|
||||
<div class="flex flex-col items-start">
|
||||
<p class="dark:text-white text-sm font-medium whitespace-nowrap truncate mb-1">
|
||||
{$currentOrg.name}
|
||||
</p>
|
||||
|
||||
<Plan name={$currentOrgPlan ? PLAN_NAMES[$currentOrgPlan.plan_name] : PLAN_NAMES.BASIC} />
|
||||
</div>
|
||||
<ChevronSort size={16} />
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<div class="w-[219px] h-[30px]">
|
||||
<SkeletonText style="width: 100%; height: 100%;" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if canAddOrg}
|
||||
<Popover bind:open closeOnOutsideClick align="bottom-left">
|
||||
<Popover class="w-[95%] left-[2%] rounded-md" bind:open closeOnOutsideClick align="bottom-left">
|
||||
{#each $orgs as org}
|
||||
<OrgSelectorItem
|
||||
size="sm"
|
||||
@@ -62,3 +94,9 @@
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
:global(.org-selector .bx--popover-contents) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
|
||||
<div class="relative">
|
||||
<button
|
||||
class="{disabled &&
|
||||
'opacity-25'} flex items-center text-start w-[14rem] py-2 px-3 {hasDivider &&
|
||||
'border-b border-gray-100'} {active && 'bg-primary-100 dark:bg-gray-500'}"
|
||||
class="w-full {disabled &&
|
||||
'opacity-25'} flex items-center text-start w-[14rem] py-1.5 px-2.5 {hasDivider &&
|
||||
'border-b border-gray-100 dark:border-neutral-600'} {active &&
|
||||
'bg-gray-200 dark:bg-neutral-700 dark:text-white'}"
|
||||
on:click={disabled ? undefined : onClick}
|
||||
>
|
||||
{#if avatar}
|
||||
@@ -25,14 +26,14 @@
|
||||
src={avatar}
|
||||
name={text}
|
||||
shape="rounded-md"
|
||||
width="w-7"
|
||||
height="h-7"
|
||||
width="w-[1.2rem]"
|
||||
height="h-[1.2rem]"
|
||||
className="mr-2"
|
||||
/>
|
||||
{:else if avatarText}
|
||||
<TextChip
|
||||
value={avatarText}
|
||||
className="mr-2 bg-primary-200 font-bold dark:bg-gray-600"
|
||||
className="mr-2 bg-primary-200 font-medium text-xs dark:text-black"
|
||||
{size}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const VARIANTS = {
|
||||
CONTAINED: 'CONTAINED',
|
||||
CONTAINED_DARK: 'CONTAINED_DARK',
|
||||
CONTAINED_LIGHT: 'CONTAINED_LIGHT',
|
||||
CONTAINED_INFO: 'CONTAINED_INFO',
|
||||
CONTAINED_WHITE: 'CONTAINED_WHITE',
|
||||
@@ -14,6 +15,8 @@ export const VARIANTS = {
|
||||
|
||||
export const VARIANTS_CLASS = {
|
||||
[VARIANTS.CONTAINED]: 'border-none bg-primary-700 hover:bg-primary-900 text-white font-bold',
|
||||
[VARIANTS.CONTAINED_DARK]:
|
||||
'border-none bg-black hover:bg-neutral-600 text-white dark:bg-white dark:text-black font-bold',
|
||||
[VARIANTS.CONTAINED_LIGHT]:
|
||||
'border-none bg-primary-600 hover:bg-primary-900 text-white font-bold',
|
||||
[VARIANTS.CONTAINED_WHITE]:
|
||||
@@ -22,7 +25,7 @@ export const VARIANTS_CLASS = {
|
||||
[VARIANTS.CONTAINED_SUCCESS]: 'bg-green-700 hover:bg-green-900 text-white',
|
||||
[VARIANTS.CONTAINED_DANGER]: 'bg-red-700 hover:bg-red-900 text-white',
|
||||
[VARIANTS.OUTLINED]:
|
||||
'border border-primary-700 hover:border-black-300 hover:bg-primary-700 text-black dark:text-white',
|
||||
'border border-black hover:bg-neutral-600 hover:text-white text-black dark:text-white',
|
||||
[VARIANTS.NONE]:
|
||||
'border-none hover:border-black-300 hover:bg-gray-200 dark:hover:bg-neutral-800 text-black',
|
||||
[VARIANTS.TEXT]: 'text-black hover:underline',
|
||||
|
||||
@@ -17,24 +17,21 @@
|
||||
<button
|
||||
class="{isLoading || isDisabled
|
||||
? 'opacity-25 cursor-not-allowed'
|
||||
: 'cursor-pointer'} flex items-center h-auto {VARIANTS_CLASS[
|
||||
: 'cursor-pointer hover:scale-95 transition-all ease-in-out'} flex items-center h-auto {VARIANTS_CLASS[
|
||||
isLoading ? VARIANTS.OUTLINED : variant
|
||||
]} {!disablePadding &&
|
||||
'py-[0.5rem] px-6'} rounded-md {className} w-fit min-h-[36px] justify-center sm:w-auto {isLoading &&
|
||||
'py-[1rem] border-primary-700'} {variant !== VARIANTS.TEXT &&
|
||||
'hover:shadow-xl'} transition delay-150 duration-300 ease-in-out {variant !==
|
||||
VARIANTS.CONTAINED_WHITE
|
||||
? 'dark:text-white'
|
||||
: ''}"
|
||||
'py-[0.5rem] px-6'} rounded-md {className} w-fit min-h-[36px] justify-center sm:w-auto {variant !==
|
||||
VARIANTS.TEXT && 'hover:shadow-xl'} transition delay-150 duration-300 ease-in-out"
|
||||
on:click={onClick}
|
||||
{name}
|
||||
{type}
|
||||
disabled={isLoading || isDisabled}
|
||||
>
|
||||
{#if isLoading}
|
||||
<Loading withOverlay={false} small />
|
||||
<Loading withOverlay={false} small class="mr-2" />
|
||||
<!-- <Moon size="20" color="#1d4ed8" unit="px" duration="1s" /> -->
|
||||
{:else if !!label}
|
||||
{/if}
|
||||
{#if !!label}
|
||||
{label}
|
||||
{:else}
|
||||
<slot />
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { openUpgradeModal } from '$lib/utils/functions/org';
|
||||
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
export let className = '';
|
||||
export let onClick = () => {};
|
||||
</script>
|
||||
|
||||
{#if $isFreePlan}
|
||||
<button
|
||||
class="w-full border bg-blue-50 border-blue-700 flex items-center gap-2 px-2 py-2 rounded-md hover:scale-95 transition-all ease-in-out {className}"
|
||||
on:click={() => {
|
||||
onClick();
|
||||
openUpgradeModal();
|
||||
}}
|
||||
>
|
||||
<FlashFilled size={16} class="text-blue-700" />
|
||||
<p class="font-semibold text-blue-700 text-sm">
|
||||
<slot />
|
||||
</p>
|
||||
</button>
|
||||
{/if}
|
||||
@@ -0,0 +1,220 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import type { OrganizationPlan } from '$lib/utils/types';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { goto } from '$app/navigation';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import { page } from '$app/stores';
|
||||
import Checkmark from 'carbon-icons-svelte/lib/Checkmark.svelte';
|
||||
import PLANS from 'shared-constants/src/plans/data.json';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { subscribeToProduct } from '$lib/utils/services/lemonsqueezy/subscribe';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { Loading } from 'carbon-components-svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import StepDoneIcon from '$lib/components/Icons/StepDoneIcon.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import type { RealtimeChannel, RealtimePostgresChangesPayload } from '@supabase/supabase-js';
|
||||
import Confetti from '$lib/components/Confetti/index.svelte';
|
||||
import { toggleConfetti } from '$lib/components/Confetti/store';
|
||||
|
||||
const disabledClass = 'bg-gray-300 text-gray-400 hover:cursor-not-allowed';
|
||||
|
||||
let isLoadingPlan: string | null = null;
|
||||
let orgPlanChannel: RealtimeChannel;
|
||||
let open = false;
|
||||
let upgraded = false;
|
||||
|
||||
async function handleClick(plan, planName: string) {
|
||||
if (plan.CTA.IS_DISABLED || !$profile.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plan.CTA.PRODUCT_ID && plan.CTA.DASHBOARD_LINK) {
|
||||
window.open(plan.CTA.DASHBOARD_LINK, '_blank');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingPlan = planName;
|
||||
|
||||
try {
|
||||
const { checkoutURL } = await subscribeToProduct({
|
||||
productId: plan.CTA.PRODUCT_ID,
|
||||
email: $profile.email,
|
||||
name: $profile.fullname,
|
||||
triggeredBy: $currentOrg.memberId,
|
||||
orgId: $currentOrg.id
|
||||
});
|
||||
|
||||
if (!checkoutURL) {
|
||||
snackbar.error('Failed to generate success');
|
||||
return;
|
||||
}
|
||||
|
||||
window.open(checkoutURL, '_blank');
|
||||
} catch (error) {
|
||||
console.error('Error subscribing', error);
|
||||
|
||||
snackbar.error('Failed, please try again later');
|
||||
}
|
||||
|
||||
isLoadingPlan = null;
|
||||
}
|
||||
|
||||
async function handleInsert(payload: RealtimePostgresChangesPayload<OrganizationPlan>) {
|
||||
const newPlan = payload.new as OrganizationPlan;
|
||||
|
||||
console.log('A new plan was inserted');
|
||||
// If plan was successfully generated
|
||||
if (newPlan.org_id === $currentOrg.id && newPlan.is_active) {
|
||||
upgraded = true;
|
||||
toggleConfetti();
|
||||
|
||||
setTimeout(() => {
|
||||
toggleConfetti();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
if (upgraded) {
|
||||
window.location.href = $currentOrgPath;
|
||||
} else {
|
||||
goto($page.url.pathname);
|
||||
}
|
||||
}
|
||||
|
||||
function onLearnMore() {
|
||||
window.open('https://classroomio.com/blog/early-adopter', '_blank');
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
orgPlanChannel = supabase
|
||||
.channel('any')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{ event: 'INSERT', schema: 'public', table: 'organization_plan' },
|
||||
handleInsert
|
||||
)
|
||||
.subscribe();
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (orgPlanChannel) {
|
||||
supabase.removeChannel(orgPlanChannel);
|
||||
}
|
||||
});
|
||||
|
||||
$: {
|
||||
const query = new URLSearchParams($page.url.search);
|
||||
open = (query.get('upgrade') || '') === 'true';
|
||||
}
|
||||
$: planNames = Object.keys(PLANS);
|
||||
</script>
|
||||
|
||||
{#if upgraded}
|
||||
<Confetti />
|
||||
{/if}
|
||||
|
||||
<Modal
|
||||
{onClose}
|
||||
{open}
|
||||
width="w-4/5"
|
||||
maxWidth={upgraded ? 'max-w-[600px]' : undefined}
|
||||
modalHeading="Upgrade Plan"
|
||||
>
|
||||
{#if upgraded}
|
||||
<div class="flex flex-col items-center justify-center mb-4 w-full relative">
|
||||
<StepDoneIcon />
|
||||
<h4 class="text-2xl">Thank you for your support</h4>
|
||||
<p class="mb-4 text-center">
|
||||
Your plan comes with cool perks and benefits including access to all future features we
|
||||
release.
|
||||
</p>
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton label="Close" variant={VARIANTS.OUTLINED} onClick={onClose} />
|
||||
<PrimaryButton label="Learn more" variant={VARIANTS.CONTAINED_DARK} onClick={onLearnMore} />
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="isolate grid grid-cols-1 gap-3 lg:grid-cols-3">
|
||||
{#each planNames as planName}
|
||||
<div
|
||||
class="max-w-xl rounded-xl {planName === 'EARLY_ADOPTER' &&
|
||||
'bg-primary-900'} p-4 ring-1 ring-gray-200 lg:max-w-sm"
|
||||
>
|
||||
<p
|
||||
class="mb-2 text-lg font-semibold leading-8 {planName === 'EARLY_ADOPTER'
|
||||
? 'text-white'
|
||||
: 'text-gray-900'} dark:text-white lg:text-xl"
|
||||
>
|
||||
{PLANS[planName].NAME}
|
||||
</p>
|
||||
<p
|
||||
class="text-baseline flex items-baseline gap-x-1 font-medium {planName ===
|
||||
'EARLY_ADOPTER'
|
||||
? 'text-white'
|
||||
: 'text-black'} dark:text-gray-300"
|
||||
>
|
||||
{PLANS[planName].PRICE.CURRENCY}
|
||||
{PLANS[planName].PRICE.MONTHLY}
|
||||
{#if !PLANS[planName].PRICE.IS_PREMIUM}
|
||||
/month
|
||||
{/if}
|
||||
</p>
|
||||
<p
|
||||
class=" mt-4 text-sm font-light leading-6 {planName === 'EARLY_ADOPTER'
|
||||
? 'text-white'
|
||||
: 'text-black'} dark:text-gray-300 lg:leading-6"
|
||||
>
|
||||
{PLANS[planName].DESCRIPTION}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="text-md mt-4 block w-full rounded-md {isLoadingPlan === planName &&
|
||||
disabledClass} {planName === 'EARLY_ADOPTER'
|
||||
? 'bg-white text-slate-900 hover:bg-indigo-50'
|
||||
: PLANS[planName].CTA.IS_DISABLED
|
||||
? disabledClass
|
||||
: 'bg-primary-900 hover:bg-primary-700 text-white'} py-3 text-center font-medium hover:no-underline lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold flex items-center justify-center"
|
||||
on:click={() => {
|
||||
if (isLoadingPlan === planName) return;
|
||||
|
||||
handleClick(PLANS[planName], planName);
|
||||
}}
|
||||
>
|
||||
{#if isLoadingPlan === planName}
|
||||
<Loading withOverlay={false} small />
|
||||
{:else}
|
||||
{PLANS[planName].CTA.DASHBOARD_LABEL}
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<ul
|
||||
class="mt-4 space-y-2 text-sm {planName === 'EARLY_ADOPTER'
|
||||
? 'text-white'
|
||||
: 'text-black'} dark:text-gray-300"
|
||||
>
|
||||
{#each PLANS[planName].FEATURES as features}
|
||||
<li class="flex items-center">
|
||||
<div>
|
||||
<Checkmark
|
||||
size={16}
|
||||
fill={planName === 'EARLY_ADOPTER' ? '#fff' : '#1D4EE2'}
|
||||
class="mr-2 lg:mr-3"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-sm">
|
||||
{features}
|
||||
</p>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
@@ -24,7 +24,7 @@
|
||||
<section class="width-fit p-3 flex flex-col items-center">
|
||||
<div
|
||||
class="avatar-container {widthHeight ||
|
||||
'setwidthheight'} pointer border-2 border-gray-200 relative {shape}"
|
||||
'setwidthheight'} pointer border-2 border-gray-200 dark:border-neutral-600 relative {shape}"
|
||||
>
|
||||
{#if src}
|
||||
<img class="w-full h-full {shape}" {src} alt="Avatar" />
|
||||
|
||||
@@ -9,25 +9,24 @@
|
||||
} from '$lib/utils/services/notification/notification';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
|
||||
let query = new URLSearchParams($page.url.search);
|
||||
let welcomePopup = query.get('welcomePopup');
|
||||
let open = false;
|
||||
|
||||
const closeModal = () => {
|
||||
triggerSendEmail(NOTIFICATION_NAME.WELCOME_TO_APP, {
|
||||
to: $profile.email,
|
||||
name: $profile.fullname
|
||||
});
|
||||
goto($currentOrgPath + '/courses?create=true');
|
||||
|
||||
goto($currentOrgPath + '/setup');
|
||||
};
|
||||
|
||||
$: {
|
||||
const query = new URLSearchParams($page.url.search);
|
||||
open = query.get('welcomePopup') === 'true';
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={closeModal}
|
||||
open={welcomePopup === 'true'}
|
||||
width="w-9/12"
|
||||
maxWidth="w-[800px]"
|
||||
modalHeading="Welcome"
|
||||
>
|
||||
<Modal onClose={closeModal} {open} width="w-9/12" maxWidth="w-[800px]" modalHeading="Welcome">
|
||||
<p class="text-black dark:text-white text-sm md:text-base lg:text-lg">
|
||||
We at <a
|
||||
href="https://app.classroomio.com/"
|
||||
|
||||