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 <[email protected]> * revamp setup * improve the upgrade experience * migration * feat: add lemonsqueezy customer portal --------- Co-authored-by: emmanuel <[email protected]>
This commit is contained in:
@@ -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
|
||||
|
||||
+2
-2
@@ -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)}
|
||||
>
|
||||
|
||||
+12
-6
@@ -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}
|
||||
|
||||
+12
-4
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user