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]>
@@ -1,8 +1,8 @@
|
||||
<a href="https://classroomio.com/">
|
||||
<img alt="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease." src="https://raw.githubusercontent.com/rotimi-best/classroomio/main/apps/classroomio-com/static/classroomio-opengraph-image.png" />
|
||||
<img alt="ClassroomIO is a no-code tool that allows you build and scale your online bootcamp with ease." src="https://raw.githubusercontent.com/rotimi-best/classroomio/main/apps/classroomio-com/static/classroomio-opengraph-image.png" />
|
||||
<h1 align="center">ClassroomIO.com</h1>
|
||||
<p align="center">
|
||||
The Open Source Teaching Platform
|
||||
The Open Source Teaching Platform for Bootcamps
|
||||
<br />
|
||||
<a href="https://classroomio.com">Website</a> | <a href="https://dub.sh/ciodiscord">Join Discord community</a>
|
||||
</p>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<a href="https://classroomio.com/">
|
||||
<img alt="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease." src="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||
<img alt="ClassroomIO is the easiest place to launch and scale your online bootcamp." src="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||
<h1 align="center">ClassroomIO</h1>
|
||||
</a>
|
||||
|
||||
The Open Source Teaching Platform
|
||||
The Open Source Teaching Platform for Bootcamps
|
||||
|
||||
## Tools
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-toc": "^9.0.0",
|
||||
"remark-unwrap-images": "^4.0.0",
|
||||
"shared-constants": "workspace:*",
|
||||
"shiki": "^0.14.7"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<meta property="og:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease."
|
||||
content="ClassroomIO is the easiest place to launch and scale your online bootcamp."
|
||||
/>
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
@@ -44,7 +44,7 @@
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease."
|
||||
content="ClassroomIO is the easiest place to launch and scale your online bootcamp."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||
<!-- <link rel="stylesheet" href="global.css"> -->
|
||||
@@ -60,7 +60,7 @@
|
||||
/>
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease."
|
||||
content="ClassroomIO is the easiest place to launch and scale your online bootcamp."
|
||||
/>
|
||||
<meta name="twitter:image" content="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||
<!-- <link rel="stylesheet" href="global.css"> -->
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
import { faqs } from './faqs';
|
||||
</script>
|
||||
|
||||
<div class="mt-[5%] mb-[15%]">
|
||||
<header class="text-center mb-[5%] px-[8%]">
|
||||
<h1 class="mt-2 text-4xl lg:text-4xl font-medium tracking-tight text-gray-900">
|
||||
<div class="mb-[15%] mt-[5%]">
|
||||
<header class="mb-[5%] px-[8%] text-center">
|
||||
<h1 class="mt-2 text-4xl font-medium tracking-tight text-gray-900 lg:text-4xl">
|
||||
Questions & Answers
|
||||
</h1>
|
||||
<p
|
||||
class="lg:px-[20%] mt-4 text-lg lg:text-xl text-gray-600 font-normal tracking-normal lg:tracking-wide"
|
||||
class="mt-4 text-lg font-normal tracking-normal text-gray-600 lg:px-[20%] lg:text-xl lg:tracking-wide"
|
||||
>
|
||||
Can’t find the answer you’re looking for?
|
||||
<a class="text-blue-700 font-semibold" href="mailto:[email protected]" target="_blank"
|
||||
<a class="font-semibold text-blue-700" href="mailto:[email protected]" target="_blank"
|
||||
>Shoot us an email</a
|
||||
> and we’ll get back to you ASAP.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-10 lg:gap-20 lg:px-[13%] mt-[20%] lg:m-0">
|
||||
<div class="mt-[20%] grid grid-cols-1 gap-10 lg:m-0 lg:grid-cols-3 lg:gap-20 lg:px-[13%]">
|
||||
{#each faqs as faq}
|
||||
<div class="max-w-[100%] px-6 lg:p-0">
|
||||
<h1 class="text:md lg:text-md font-semibold leading-8 text-gray-800">
|
||||
{faq.question}
|
||||
</h1>
|
||||
<p
|
||||
class="mt-2 text:md lg:text-md font-light leading-7 lg:leading-9 text-gray-600 tracking-wide"
|
||||
class="text:md lg:text-md mt-2 font-light leading-7 tracking-wide text-gray-600 lg:leading-9"
|
||||
>
|
||||
{faq.answer}
|
||||
</p>
|
||||
@@ -33,16 +33,16 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="floating relative mt-[10%] grad w-[100%] h-[61vh] lg:h-[54vh] flex items-center flex-col justify-center lg:w-[70%] mx-auto py-[12%] px-[6%] text-center shadow-2xl lg:rounded-3xl"
|
||||
class="floating grad relative mx-auto mt-[10%] flex h-[61vh] w-[100%] flex-col items-center justify-center px-[6%] py-[12%] text-center shadow-2xl lg:h-[54vh] lg:w-[70%] lg:rounded-3xl"
|
||||
>
|
||||
<h1 class="mt-3 lg:mt-2 text-3xl lg:text-4xl font-bold font-display tracking-tight text-white">
|
||||
Supercharge your Classroom Today
|
||||
<h1 class="font-display mt-3 text-3xl font-bold tracking-tight text-white lg:mt-2 lg:text-4xl">
|
||||
Launch Your Bootcamp Today
|
||||
</h1>
|
||||
<p class="mt-6 text-xl lg:text-lg text-gray-300">
|
||||
<p class="mt-6 text-xl text-gray-300 lg:text-lg">
|
||||
Try before you buy. No credit card required.
|
||||
</p>
|
||||
<button
|
||||
class="font-semibold text-md lg:text-lg text-gray-900 bg-white px-6 lg:px-7 py-4 lg:py-3 rounded-md mt-10"
|
||||
class="text-md mt-10 rounded-md bg-white px-6 py-4 font-semibold text-gray-900 lg:px-7 lg:py-3 lg:text-lg"
|
||||
data-cal-config={"{'layout':'month_view'}"}
|
||||
data-cal-link="classroomio/demo"
|
||||
>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import LogoFacebook from 'carbon-icons-svelte/lib/LogoFacebook.svelte';
|
||||
</script>
|
||||
|
||||
<div class="px-[2%] lg:px-[14%] border-t-[1px]">
|
||||
<div class="flex flex-col lg:flex-row justify-between items-center py-[10%] pl-5 lg:pl-0">
|
||||
<div class="border-t-[1px] px-[2%] lg:px-[14%]">
|
||||
<div class="flex flex-col items-center justify-between py-[10%] pl-5 lg:flex-row lg:pl-0">
|
||||
<div class="w-full lg:w-2/4">
|
||||
<img
|
||||
width="32"
|
||||
@@ -12,14 +12,14 @@
|
||||
loading="lazy"
|
||||
src="/logo-192.png"
|
||||
alt="classroomio logo"
|
||||
class="w-[10%] lg:w-[5%] mb-5 lg:mb-10"
|
||||
class="mb-5 w-[10%] lg:mb-10 lg:w-[5%]"
|
||||
/>
|
||||
<p class="text-sm lg:text-md tracking-wide leading-7 lg:leading-7 text-gray-600 mb-5 mr-2">
|
||||
<p class="lg:text-md mb-5 mr-2 text-sm leading-7 tracking-wide text-gray-600 lg:leading-7">
|
||||
Launch Your Online Bootcamp In Minutes
|
||||
<br />
|
||||
Making teaching and learning seamless for tech bootcamps
|
||||
The easiest way to launch and scale your online bootcamp
|
||||
</p>
|
||||
<a
|
||||
<!-- <a
|
||||
href="https://www.producthunt.com/posts/classroomio?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-classroomio"
|
||||
target="_blank"
|
||||
><img
|
||||
@@ -30,11 +30,11 @@
|
||||
width="250"
|
||||
height="54"
|
||||
/></a
|
||||
>
|
||||
<div class="flex items-center mt-5 lg:mt-10 gap-3 lg:gap-7">
|
||||
> -->
|
||||
<div class="mt-5 flex items-center gap-3 lg:mt-10 lg:gap-7">
|
||||
<a href="https://www.facebook.com/classroomiohq" target="_blank" aria-label="Facebook Link">
|
||||
<LogoFacebook
|
||||
class="fill-gray-400 hover:fill-gray-500 hover:scale-110 scale-90 lg:scale-150 transition-all cursor-pointer"
|
||||
class="scale-90 cursor-pointer fill-gray-400 transition-all hover:scale-110 hover:fill-gray-500 lg:scale-150"
|
||||
size={20}
|
||||
/>
|
||||
</a>
|
||||
@@ -45,12 +45,12 @@
|
||||
loading="lazy"
|
||||
src="/twitter_logo.png"
|
||||
alt=""
|
||||
class="w-3 hover:fill-gray-500 hover:scale-110 scale-90 lg:scale-150 transition-all cursor-pointer"
|
||||
class="w-3 scale-90 cursor-pointer transition-all hover:scale-110 hover:fill-gray-500 lg:scale-150"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://www.youtube.com/@ClassroomIO" target="_blank" aria-label="Youtube Link">
|
||||
<LogoYoutube
|
||||
class="fill-gray-400 hover:fill-gray-500 hover:scale-110 scale-90 lg:scale-150 transition-all cursor-pointer"
|
||||
class="scale-90 cursor-pointer fill-gray-400 transition-all hover:scale-110 hover:fill-gray-500 lg:scale-150"
|
||||
size={20}
|
||||
/>
|
||||
</a>
|
||||
@@ -58,64 +58,64 @@
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="flex justify-between items-start flex-col lg:flex-row w-full lg:w-2/4 gap-3 mt-16 lg:mt-0 space-y-6 lg:space-y-0"
|
||||
class="mt-16 flex w-full flex-col items-start justify-between gap-3 space-y-6 lg:mt-0 lg:w-2/4 lg:flex-row lg:space-y-0"
|
||||
>
|
||||
<div>
|
||||
<p class="mb-3 lg:mb-10 text-base lg:text-md font-semibold leading-6 text-gray-900">
|
||||
<p class="lg:text-md mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10">
|
||||
Product
|
||||
</p>
|
||||
<ul class="space-y-2 lg:space-y-4">
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a href="/#coursemanagement"> Course Management </a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a href="/#customization"> Customizations </a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a href="/#collaboration"> Forum </a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a href="/#ai"> AI Lesson Planner </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-3 lg:mb-10 text-base lg:text-md font-semibold leading-6 text-gray-900">
|
||||
<p class="lg:text-md mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10">
|
||||
Resources
|
||||
</p>
|
||||
<ul class="space-y-2 lg:space-y-4">
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a href="/blog"> Blog </a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a target="_blank" href="https://classroomio.com/docs/"> Help center </a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a target="_blank" href="https://classroomio.com/docs/what-is-classroomio">
|
||||
What is ClassroomIO?
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup">
|
||||
Getting Started
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a href="/oss-friends"> OSS Friends </a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="mb-3 lg:mb-10 text-base lg:text-md font-semibold leading-6 text-gray-900">
|
||||
<p class="lg:text-md mb-3 text-base font-semibold leading-6 text-gray-900 lg:mb-10">
|
||||
Company
|
||||
</p>
|
||||
<ul class="space-y-2 lg:space-y-4">
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a href="mailto:[email protected]" target="_blank"> Contact us </a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a
|
||||
href="https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594"
|
||||
target="_blank"
|
||||
@@ -123,12 +123,12 @@
|
||||
Privacy policy
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup">
|
||||
Getting Started
|
||||
</a>
|
||||
</li>
|
||||
<li class="text-sm lg:text-md tracking-wide leading-6 text-gray-600 hover:text-gray-900">
|
||||
<li class="lg:text-md text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900">
|
||||
<a
|
||||
href="https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594"
|
||||
target="_blank"
|
||||
@@ -141,12 +141,12 @@
|
||||
</nav>
|
||||
</div>
|
||||
<footer
|
||||
class="border-t-[1px] py-[3%] text-xs lg:text-md leading-5 font-light text-gray-500 text-center lg:text-left"
|
||||
class="lg:text-md border-t-[1px] py-[3%] text-center text-xs font-light leading-5 text-gray-500 lg:text-left"
|
||||
>
|
||||
<p>All rights reserved.</p>
|
||||
<p class="flex items-center">
|
||||
ClassroomIO © {new Date().getFullYear()} by
|
||||
<a href="https://twitter.com/rotimi_best" target="_blank" class="underline text-blue-700 mx-1"
|
||||
<a href="https://twitter.com/rotimi_best" target="_blank" class="mx-1 text-blue-700 underline"
|
||||
>Rotimi Best</a
|
||||
>
|
||||
from
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
<h1
|
||||
class="mx-auto flex flex-col items-center text-3xl font-normal leading-[1.5] text-slate-900 md:text-4xl lg:text-6xl"
|
||||
>
|
||||
<span>Launch Your Online Bootcamp</span>
|
||||
<span class="mb-2">Launch <span class="relative text-blue-700">Your</span> Online Bootcamp</span
|
||||
>
|
||||
<span class="relative text-blue-700"
|
||||
>In Minutes
|
||||
<svg
|
||||
@@ -59,8 +60,8 @@
|
||||
</h1>
|
||||
|
||||
<p class="text-md mx-auto mt-10 px-4 text-slate-700 md:text-lg lg:mt-6 lg:w-2/4 lg:p-0">
|
||||
Meet a better collaboration tool for teachers and students. ClassroomIO is a no-code tool that
|
||||
allows you build and scale your own teaching platform with ease.
|
||||
Meet a better learning management system (LMS) for teachers and students. ClassroomIO allows you
|
||||
to launch and scale your online bootcamp with ease.
|
||||
</p>
|
||||
<div class="mt-10 flex flex-col items-center justify-center gap-4 md:flex-row">
|
||||
<a
|
||||
|
||||
@@ -1,158 +1,141 @@
|
||||
<script>
|
||||
import Checkmark from 'carbon-icons-svelte/lib/Checkmark.svelte';
|
||||
import PLANS from 'shared-constants/src/plans/data.json';
|
||||
|
||||
let prices = false;
|
||||
let isYearlyPlan = false;
|
||||
|
||||
function handlePriceChange() {
|
||||
prices = !prices;
|
||||
function toggleIsYearlyPlan() {
|
||||
isYearlyPlan = !isYearlyPlan;
|
||||
}
|
||||
|
||||
const freeFeatures = [
|
||||
'Unlimited Courses',
|
||||
'Unlimited Conversations',
|
||||
'50 Students',
|
||||
'AI Course Builder (no video upload)',
|
||||
'Course Landing Page',
|
||||
'Live chat & Live polls',
|
||||
'20 AI Prompts'
|
||||
];
|
||||
|
||||
const proFeatures = [
|
||||
'Everything in Free',
|
||||
'1000 Students',
|
||||
'Advanced AI Course Builder (video upload)',
|
||||
'Lesson/Course PDF Download',
|
||||
'Certificates',
|
||||
'2000 AI Prompts',
|
||||
'Remove ClassroomIO Branding',
|
||||
'Email Support'
|
||||
];
|
||||
const teamFeatures = [
|
||||
'Everything in Pro',
|
||||
'Unlimited students',
|
||||
'Unlimited AI Prompts',
|
||||
'White Labelling',
|
||||
'Priority Support'
|
||||
];
|
||||
</script>
|
||||
|
||||
<section id="pricing" class="py-[12%]">
|
||||
<div class="px-[6%] flex flex-col justify-center items-center">
|
||||
<div class="text-center mb-[3%]">
|
||||
<h1 id="pricing" class="text-blue-700 text-md font-semibold tracking-wide">Pricing</h1>
|
||||
<h2 class="my-5 lg:my-4 font-semibold tracking-tight text-gray-900 text-3xl lg:text-4xl">
|
||||
<div class="flex flex-col items-center justify-center px-[6%]">
|
||||
<div class="mb-[3%] text-center">
|
||||
<h1 id="pricing" class="text-md font-semibold tracking-wide text-blue-700">Pricing</h1>
|
||||
<h2 class="my-5 text-3xl font-semibold tracking-tight text-gray-900 lg:my-4 lg:text-4xl">
|
||||
For teams and individuals
|
||||
</h2>
|
||||
<p class="mt-4 text-lg text-gray-500 tracking-normal lg:tracking-wide">
|
||||
<p class="mt-4 text-lg tracking-normal text-gray-500 lg:tracking-wide">
|
||||
Customizable LMS, AI integration for productive educators and many more...
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex items-center border-[2px] rounded-[30px] p-[2px] relative lg:scale-100 mt-10 lg:mt-0"
|
||||
class="relative mt-10 flex items-center rounded-[30px] border-[2px] p-[2px] lg:mt-0 lg:scale-100"
|
||||
>
|
||||
<button
|
||||
style="background-color: {prices ? 'initial' : '#1D4EE2'}; color: {prices
|
||||
style="background-color: {isYearlyPlan ? 'initial' : '#1D4EE2'}; color: {isYearlyPlan
|
||||
? '#5e636b'
|
||||
: '#fff'}"
|
||||
class="bg-blue-700 text-white rounded-[30px] py-1 px-3 lg:py-2 lg:px-4 text-xs"
|
||||
on:click={handlePriceChange}>Monthly</button
|
||||
class="rounded-[30px] bg-blue-700 px-3 py-1 text-xs text-white lg:px-4 lg:py-2"
|
||||
on:click={toggleIsYearlyPlan}>Monthly</button
|
||||
>
|
||||
<button
|
||||
style="background-color: {prices ? '#1D4EE2' : ''}; color: {prices ? '#fff' : '#5e636b'}"
|
||||
class="text-white rounded-[30px] py-1 lg:py-2 px-3 lg:px-4 text-xs"
|
||||
on:click={handlePriceChange}>Annually</button
|
||||
style="background-color: {isYearlyPlan ? '#1D4EE2' : ''}; color: {isYearlyPlan
|
||||
? '#fff'
|
||||
: '#5e636b'}"
|
||||
class="rounded-[30px] px-3 py-1 text-xs text-white lg:px-4 lg:py-2"
|
||||
on:click={toggleIsYearlyPlan}>Annually</button
|
||||
>
|
||||
<div
|
||||
class="absolute top-[-85%] right-[-40%] lg:top-[-75%] lg:right-[-43%] bg-[#006600] text-xs text-white px-3.5 py-1.5 rounded-full scale-[90%] lg:scale-100"
|
||||
class="absolute right-[-40%] top-[-85%] scale-[90%] rounded-full bg-[#006600] px-3.5 py-1.5 text-xs text-white lg:right-[-43%] lg:top-[-75%] lg:scale-100"
|
||||
>
|
||||
Save 2 months
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="isolate mt-10 grid gap-10 grid-cols-1 lg:grid-cols-3">
|
||||
<!-- card 1 -->
|
||||
<div class="ring-gray-200 rounded-3xl p-8 ring-1 xl:p-9 max-w-xl lg:max-w-sm">
|
||||
<p class="text-gray-900 text-lg lg:text-xl font-semibold leading-8">Hobby</p>
|
||||
<p class="text-gray-500 font-light mt-4 text-sm lg:text-md leading-6 lg:leading-6">
|
||||
For personal use. Hobby plan is free for up to 5 courses forever, no credit card required.
|
||||
<div class="isolate mt-10 grid grid-cols-1 gap-10 lg:grid-cols-3">
|
||||
<!-- Card 1 -->
|
||||
<div class="max-w-xl rounded-3xl p-8 ring-1 ring-gray-200 lg:max-w-sm xl:p-9">
|
||||
<p class="text-lg font-semibold leading-8 text-gray-900 lg:text-xl">
|
||||
{PLANS.BASIC.NAME}
|
||||
</p>
|
||||
<p class="mt-6 flex items-baseline gap-x-1 text-4xl lg:text-4xl font-medium">
|
||||
$0 <sub class="text-base">/mo</sub>
|
||||
<p class="lg:text-md mt-4 text-sm font-light leading-6 text-gray-500 lg:leading-6">
|
||||
{PLANS.BASIC.DESCRIPTION}
|
||||
</p>
|
||||
<button
|
||||
class="font-medium lg:font-semibold text-md lg:text-lg text-white py-3 lg:py-3 w-full bg-slate-900 hover:bg-slate-700 rounded-md lg:rounded-md mt-10"
|
||||
data-cal-config={"{'layout':'month_view'}"}
|
||||
data-cal-link="classroomio/demo"
|
||||
<p class="mt-6 flex items-baseline gap-x-1 text-4xl font-medium lg:text-4xl">
|
||||
${PLANS.BASIC.PRICE.MONTHLY}
|
||||
<!-- <sub class="text-base">/mo</sub> -->
|
||||
</p>
|
||||
<a
|
||||
class="text-md mt-10 block w-full rounded-md bg-slate-900 py-3 text-center font-medium text-white hover:bg-slate-700 lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold"
|
||||
href={PLANS.BASIC.CTA.LINK}
|
||||
target="_blank"
|
||||
>
|
||||
Get started
|
||||
</button>
|
||||
<ul class="text-gray-600 mt-8 space-y-2 lg:space-y-5 text-sm xl:mt-10">
|
||||
{#each freeFeatures as freeFeature}
|
||||
{PLANS.BASIC.CTA.LABEL}
|
||||
</a>
|
||||
<ul class="mt-8 space-y-2 text-sm text-gray-600 lg:space-y-5 xl:mt-10">
|
||||
{#each PLANS.BASIC.FEATURES as feature}
|
||||
<li class="flex items-center">
|
||||
<Checkmark
|
||||
size={24}
|
||||
fill="#1D4EE2"
|
||||
class="mr-2 lg:mr-3 scale-[70%] lg:scale-[100%]"
|
||||
class="mr-2 scale-[70%] lg:mr-3 lg:scale-[100%]"
|
||||
/>
|
||||
{freeFeature}
|
||||
{feature}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- card 2 -->
|
||||
<div class="bg-gray-900 rounded-3xl p-8 xl:p-9 max-w-xl lg:max-w-lg">
|
||||
<p class="text-white text-lg lg:text-xl font-semibold leading-8">Pro</p>
|
||||
<p class="text-white font-light mt-4 text-sm lg:text-md leading-6 lg:leading-6">
|
||||
Advanced features for small teaching businesses. Built for scale.
|
||||
<!-- Card 2 -->
|
||||
<div class="max-w-xl rounded-3xl bg-gray-900 p-8 lg:max-w-lg xl:p-9">
|
||||
<p class="text-lg font-semibold leading-8 text-white lg:text-xl">
|
||||
{PLANS.EARLY_ADOPTER.NAME}
|
||||
</p>
|
||||
<p class="mt-6 text-white flex items-baseline gap-x-1 text-4xl lg:text-4xl font-medium">
|
||||
${prices ? '12' : '15'} <sub class="text-base">/member</sub>
|
||||
<p class="lg:text-md mt-4 text-sm font-light leading-6 text-white lg:leading-6">
|
||||
{PLANS.EARLY_ADOPTER.DESCRIPTION}
|
||||
</p>
|
||||
<button
|
||||
class="font-medium lg:font-semibold text-md lg:text-lg text-slate-900 py-3 lg:py-3 w-full bg-white hover:bg-indigo-50 rounded-md lg:rounded-md mt-10"
|
||||
data-cal-config={"{'layout':'month_view'}"}
|
||||
data-cal-link="classroomio/demo"
|
||||
<p class="mt-6 flex items-baseline gap-x-1 text-4xl font-medium text-white lg:text-4xl">
|
||||
${isYearlyPlan ? PLANS.EARLY_ADOPTER.PRICE.YEARLY : PLANS.EARLY_ADOPTER.PRICE.MONTHLY}
|
||||
<!-- <sub class="text-base">/mo</sub> -->
|
||||
</p>
|
||||
<a
|
||||
class="text-md mt-10 block w-full rounded-md bg-white py-3 text-center font-medium text-slate-900 hover:bg-indigo-50 lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold"
|
||||
href={PLANS.EARLY_ADOPTER.CTA.LINK}
|
||||
target="_blank"
|
||||
>
|
||||
Get Started
|
||||
</button>
|
||||
<ul class="text-white mt-8 space-y-3 font-light lg:space-y-5 text-sm xl:mt-10">
|
||||
{#each proFeatures as proFeature}
|
||||
{PLANS.EARLY_ADOPTER.CTA.LABEL}
|
||||
</a>
|
||||
<ul class="mt-8 space-y-3 text-sm font-light text-white lg:space-y-5 xl:mt-10">
|
||||
{#each PLANS.EARLY_ADOPTER.FEATURES as features}
|
||||
<li class="flex items-center">
|
||||
<Checkmark size={24} fill="#fff" class="mr-2 lg:mr-3 scale-[70%] lg:scale-[100%]" />
|
||||
{proFeature}
|
||||
<Checkmark size={24} fill="#fff" class="mr-2 scale-[70%] lg:mr-3 lg:scale-[100%]" />
|
||||
{features}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- card 3 -->
|
||||
<div class="ring-gray-200 rounded-3xl p-8 ring-1 xl:p-9 max-w-xl lg:max-w-sm">
|
||||
<p class="text-gray-900 text-lg lg:text-xl font-semibold leading-8">Team</p>
|
||||
<p class="text-gray-500 font-light mt-4 text-sm lg:text-md leading-6 lg:leading-6">
|
||||
Best suited for teams and larger businesses that need more control.
|
||||
<!-- Card 3 -->
|
||||
<div class="max-w-xl rounded-3xl p-8 ring-1 ring-gray-200 lg:max-w-sm xl:p-9">
|
||||
<p class="text-lg font-semibold leading-8 text-gray-900 lg:text-xl">
|
||||
{PLANS.ENTERPRISE.NAME}
|
||||
</p>
|
||||
<p class="mt-6 lg:mt-6 flex items-baseline gap-x-1 text-4xl lg:text-4xl font-medium">
|
||||
${prices ? '42' : '50'} <sub class="text-base">/member</sub>
|
||||
<p class="lg:text-md mt-4 text-sm font-light leading-6 text-gray-500 lg:leading-6">
|
||||
{PLANS.ENTERPRISE.DESCRIPTION}
|
||||
</p>
|
||||
<p class="mt-6 flex items-baseline gap-x-1 text-xl font-medium lg:mt-6 lg:text-xl">
|
||||
Request Pricing
|
||||
</p>
|
||||
<button
|
||||
class="font-medium lg:font-semibold text-md lg:text-lg text-white py-3 lg:py-3 w-full bg-slate-900 hover:bg-slate-700 rounded-md lg:rounded-md mt-10"
|
||||
class="text-md mt-10 w-full rounded-md bg-slate-900 py-3 font-medium text-white hover:bg-slate-700 lg:rounded-md lg:py-3 lg:text-lg lg:font-semibold"
|
||||
data-cal-config={"{'layout':'month_view'}"}
|
||||
data-cal-link="classroomio/demo"
|
||||
data-cal-link="classroomio/enterprise"
|
||||
>
|
||||
Get started
|
||||
{PLANS.ENTERPRISE.CTA.LABEL}
|
||||
</button>
|
||||
|
||||
<ul class="text-gray-600 mt-8 space-y-2 lg:space-y-5 text-sm xl:mt-10">
|
||||
{#each teamFeatures as teamFeature}
|
||||
<ul class="mt-8 space-y-2 text-sm text-gray-600 lg:space-y-5 xl:mt-10">
|
||||
{#each PLANS.ENTERPRISE.FEATURES as features}
|
||||
<li class="flex items-center">
|
||||
<Checkmark
|
||||
size={24}
|
||||
fill="#1D4EE2"
|
||||
class="mr-2 lg:mr-3 scale-[70%] lg:scale-[100%]"
|
||||
class="mr-2 scale-[70%] lg:mr-3 lg:scale-[100%]"
|
||||
/>
|
||||
{teamFeature}
|
||||
{features}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
@@ -172,7 +155,7 @@
|
||||
class="m-0 lg:mt-1 text-md leading-7 lg:leading-6 text-gray-600"
|
||||
>
|
||||
For personal use. Hobby plan is free for up to 5 courses forever, no
|
||||
credit card required.
|
||||
credit Card required.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -2,5 +2,5 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [sveltekit()]
|
||||
});
|
||||
|
||||
@@ -21,6 +21,8 @@ PUBLIC_TINYMCE_API_KEY=
|
||||
VITE_SENTRY_AUTH_TOKEN=
|
||||
VITE_SENTRY_ORG_NAME=
|
||||
VITE_SENTRY_PROJECT_NAME=
|
||||
# Configure https locally https://stackoverflow.com/a/76525335
|
||||
USE_HTTPS_ON_LOCALHOST=false
|
||||
|
||||
# OpenAI setup - Optional if you want to use OpenAI
|
||||
OPENAI_API_KEY=
|
||||
@@ -29,4 +31,4 @@ OPENAI_API_KEY=
|
||||
PUBLIC_SERVER_URL=
|
||||
|
||||
# For getting metadata for signup users.
|
||||
PUBLIC_IP_REGISTRY_KEY=
|
||||
PUBLIC_IP_REGISTRY_KEY=
|
||||
|
||||
@@ -6,6 +6,7 @@ yarn-error.log
|
||||
.svelte-kit/
|
||||
.env
|
||||
/dist/
|
||||
/cert/
|
||||
package-lock.json
|
||||
|
||||
# Local Netlify folder
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<a href="https://classroomio.com/">
|
||||
<img alt="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease." src="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||
<img alt="ClassroomIO is the easiest place to launch and scale your online bootcamp." src="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||
<h1 align="center">ClassroomIO</h1>
|
||||
<p align="center">
|
||||
The Open Source Teaching Platform
|
||||
The Open Source Teaching Platform for Bootcamps
|
||||
<br />
|
||||
<a href="https://classroomio.com">Website</a> | <a href="https://dub.sh/ciodiscord">Join Discord community</a>
|
||||
</p>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"@aws-sdk/client-s3": "^3.386.0",
|
||||
"@carbon/charts-svelte": "^1.13.6",
|
||||
"@defer/client": "^2.2.0",
|
||||
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||
"@sentry/browser": "^7.61.0",
|
||||
"@sentry/integrations": "^7.61.0",
|
||||
"@sentry/vite-plugin": "^2.6.2",
|
||||
@@ -37,6 +38,7 @@
|
||||
"canvas-confetti": "^1.6.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"crypto": "^1.0.1",
|
||||
"d3": "^7.8.5",
|
||||
"d3-cloud": "^1.2.7",
|
||||
"d3-sankey": "^0.12.3",
|
||||
@@ -49,7 +51,9 @@
|
||||
"openai-edge": "^1.2.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "^1.116.6",
|
||||
"shared-constants": "workspace:*",
|
||||
"sirv": "^2.0.3",
|
||||
"stripe": "^14.22.0",
|
||||
"svelte-email": "^0.0.4",
|
||||
"svelte-ui": "workspace:*",
|
||||
"unsplash-js": "^7.0.18",
|
||||
@@ -97,5 +101,8 @@
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "18.x"
|
||||
},
|
||||
"browser": {
|
||||
"crypto": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,12 @@
|
||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
|
||||
|
||||
<title>ClassroomIO – Launch Your Online Bootcamp In Minutes</title>
|
||||
<meta name="description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease." />
|
||||
<meta name="description" content="ClassroomIO is the easiest place to launch and scale your online bootcamp." />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta property="og:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||
<meta property="og:description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease." />
|
||||
content="ClassroomIO is the easiest place to launch and scale your online bootcamp." />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:image:type" content="image/png" />
|
||||
<meta property="og:image:width" content="1920" />
|
||||
@@ -29,9 +28,7 @@
|
||||
<meta name="twitter:creator" content="@rotimi_best" />
|
||||
<meta name="twitter:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||
<meta name="twitter:description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease." />
|
||||
|
||||
<script async type="text/javascript" src="https://static.senja.io/dist/platform.js"></script>
|
||||
content="ClassroomIO is the easiest place to launch and scale your online bootcamp." />
|
||||
|
||||
<!-- <link rel="stylesheet" href="global.css"> -->
|
||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
||||
@@ -45,6 +42,8 @@
|
||||
-->
|
||||
<script src="https://cdn.plyr.io/3.7.3/plyr.js"></script>
|
||||
|
||||
<script async type="text/javascript" src="https://static.senja.io/dist/platform.js"></script>
|
||||
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -23,5 +23,6 @@ export const blockedSubdomain = [
|
||||
'academy',
|
||||
'schools',
|
||||
'training',
|
||||
'cloud'
|
||||
'cloud',
|
||||
'launchweek'
|
||||
];
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import posthog from 'posthog-js';
|
||||
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';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export 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');
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export function genQuizPin(): number {
|
||||
var minm = 100000;
|
||||
var maxm = 999999;
|
||||
@@ -10,3 +12,13 @@ export function generateSitename(orgName: string): string {
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-zA-Z0-9-]/g, '');
|
||||
}
|
||||
|
||||
export function openUpgradeModal() {
|
||||
const url = new URL(window.location.href);
|
||||
|
||||
url.searchParams.set('upgrade', 'true');
|
||||
|
||||
const searchParams = url.searchParams.toString();
|
||||
|
||||
goto(window.location.pathname + `?${searchParams}`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { LEMON_SQUEEZY_API_KEY } from '$env/static/private';
|
||||
import { lemonSqueezySetup } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
|
||||
export function configureLemonSqueezy() {
|
||||
return lemonSqueezySetup({
|
||||
apiKey: LEMON_SQUEEZY_API_KEY,
|
||||
onError: (error) => {
|
||||
throw new Error(`Lemon Squeezy API error: ${error.message}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import axios from 'axios';
|
||||
|
||||
interface SubscriptionData {
|
||||
productId: string;
|
||||
email: string;
|
||||
name: string;
|
||||
triggeredBy: string;
|
||||
orgId: string;
|
||||
}
|
||||
|
||||
interface SubscriptionResponse {
|
||||
success: boolean;
|
||||
checkoutURL: string;
|
||||
}
|
||||
|
||||
export async function subscribeToProduct(data: SubscriptionData): Promise<SubscriptionResponse> {
|
||||
const response = await axios({
|
||||
method: 'POST',
|
||||
url: '/api/lmz/subscribe',
|
||||
data
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import isEmpty from 'lodash/isEmpty';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { orgs, currentOrg, orgAudience, orgTeam } from '$lib/utils/store/org';
|
||||
import { ROLE, ROLE_LABEL } from '$lib/utils/constants/roles';
|
||||
import type { CurrentOrg, OrgTeamMember, OrgAudience } from '$lib/utils/types/org';
|
||||
import type { CurrentOrg, OrgTeamMember } from '$lib/utils/types/org';
|
||||
import type { OrganizationPlan } from '$lib/utils/types';
|
||||
|
||||
export async function getOrgTeam(orgId: string) {
|
||||
const { data, error } = await supabase
|
||||
@@ -79,7 +79,12 @@ export async function getOrganizations(userId: string) {
|
||||
avatar_url,
|
||||
landingpage,
|
||||
theme,
|
||||
created_at
|
||||
created_at,
|
||||
organization_plan(
|
||||
plan_name,
|
||||
is_active,
|
||||
subscriptionId:lmz_data->id
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
@@ -108,7 +113,8 @@ export async function getOrganizations(userId: string) {
|
||||
avatar_url: orgMember?.organization?.avatar_url,
|
||||
memberId: orgMember?.id,
|
||||
role_id: orgMember?.role_id,
|
||||
landingpage: orgMember?.organization?.landingpage
|
||||
landingpage: orgMember?.organization?.landingpage,
|
||||
organization_plan: orgMember?.organization?.organization_plan
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,7 +206,6 @@ export async function getCourseBySiteName(siteName: string) {
|
||||
}
|
||||
|
||||
export async function getCurrentOrg(siteName: string, justGet = false) {
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('organization')
|
||||
.select(
|
||||
@@ -210,14 +215,17 @@ export async function getCurrentOrg(siteName: string, justGet = false) {
|
||||
siteName,
|
||||
avatar_url,
|
||||
landingpage,
|
||||
theme
|
||||
theme,
|
||||
organization_plan(
|
||||
plan_name,
|
||||
is_active
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('siteName', siteName)
|
||||
.returns<CurrentOrg[]>();
|
||||
console.log('data =', data);
|
||||
console.log('error =', error);
|
||||
|
||||
|
||||
const isDataEmpty = !data?.[0];
|
||||
|
||||
@@ -234,3 +242,32 @@ export async function getCurrentOrg(siteName: string, justGet = false) {
|
||||
return data[0];
|
||||
}
|
||||
}
|
||||
|
||||
export async function createOrgPlan(params: {
|
||||
orgId: string;
|
||||
planName: string;
|
||||
triggeredBy: number;
|
||||
data: OrganizationPlan['lmz_data'];
|
||||
}) {
|
||||
return await supabase.from('organization_plan').insert({
|
||||
activated_at: new Date().toDateString(),
|
||||
org_id: params.orgId,
|
||||
triggered_by: params.triggeredBy,
|
||||
plan_name: params.planName,
|
||||
is_active: true,
|
||||
lmz_data: params.data
|
||||
});
|
||||
}
|
||||
|
||||
export async function cancelOrgPlan(params: { orgId: string; planName: string }) {
|
||||
return await supabase
|
||||
.from('organization_plan')
|
||||
.update({
|
||||
is_active: false,
|
||||
deactivated_at: new Date().toDateString()
|
||||
})
|
||||
.match({
|
||||
plan_name: params.planName,
|
||||
org_id: params.orgId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Writable } from 'svelte/store';
|
||||
import type { CurrentOrg, OrgTeamMember, OrgAudience } from '../types/org';
|
||||
import { ROLE } from '$lib/utils/constants/roles';
|
||||
import type { UserLessonDataType } from '$lib/utils/types';
|
||||
import { PLAN } from 'shared-constants/src/plans/constants';
|
||||
|
||||
export const defaultCurrentOrgState: CurrentOrg = {
|
||||
id: '',
|
||||
@@ -15,7 +16,8 @@ export const defaultCurrentOrgState: CurrentOrg = {
|
||||
memberId: '',
|
||||
role_id: '',
|
||||
landingpage: {},
|
||||
theme: ''
|
||||
theme: '',
|
||||
organization_plan: []
|
||||
};
|
||||
|
||||
export const orgs = writable<CurrentOrg[]>([]);
|
||||
@@ -26,6 +28,9 @@ export const isOrgAdmin = derived(
|
||||
currentOrg,
|
||||
($currentOrg) => parseInt($currentOrg.role_id) === ROLE.ADMIN
|
||||
);
|
||||
export const currentOrgPlan = derived(currentOrg, ($currentOrg) =>
|
||||
$currentOrg.organization_plan.find((p) => p.is_active)
|
||||
);
|
||||
export const currentOrgPath = derived(currentOrg, ($currentOrg) =>
|
||||
$currentOrg.siteName ? `/org/${$currentOrg.siteName}` : ''
|
||||
);
|
||||
@@ -38,6 +43,21 @@ export const currentOrgDomain = derived(currentOrg, ($currentOrg) => {
|
||||
: '';
|
||||
});
|
||||
|
||||
// Utility org store
|
||||
export const isFreePlan = derived(
|
||||
currentOrgPlan,
|
||||
($plan) => !$plan || $plan.plan_name === PLAN.BASIC
|
||||
);
|
||||
export const currentOrgMaxAudience = derived(currentOrgPlan, ($plan) =>
|
||||
!$plan
|
||||
? 50
|
||||
: $plan.plan_name === PLAN.EARLY_ADOPTER
|
||||
? 10000
|
||||
: $plan.plan_name === PLAN.ENTERPRISE
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: 50
|
||||
);
|
||||
|
||||
// Quiz
|
||||
export const createQuizModal = writable({
|
||||
open: false,
|
||||
|
||||
@@ -105,6 +105,17 @@ export interface Organization {
|
||||
name: any; // type unknown;
|
||||
}
|
||||
|
||||
export interface OrganizationPlan {
|
||||
id: number;
|
||||
activated_at: string;
|
||||
deactivated_at: string;
|
||||
org_id: string;
|
||||
plan_name: string;
|
||||
is_active: boolean;
|
||||
updated_at: string;
|
||||
lmz_data: unknown;
|
||||
}
|
||||
|
||||
export interface Profile {
|
||||
id: string /* primary key */;
|
||||
fullname: string;
|
||||
|
||||
@@ -7,9 +7,14 @@ export interface CurrentOrg {
|
||||
memberId: string;
|
||||
role_id: string;
|
||||
landingpage: {
|
||||
[key: string]: any;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
theme: string;
|
||||
organization_plan: {
|
||||
subscriptionId: string;
|
||||
plan_name: string;
|
||||
is_active: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface OrgTeamMember {
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
import shouldRedirectOnAuth from '$lib/utils/functions/routes/shouldRedirectOnAuth';
|
||||
import { identifyPosthogUser, initPosthog } from '$lib/utils/services/posthog';
|
||||
import { initSentry, setSentryUser } from '$lib/utils/services/sentry';
|
||||
import UpgradeModal from '$lib/components/Upgrade/Modal.svelte';
|
||||
|
||||
import '../app.postcss';
|
||||
|
||||
@@ -281,6 +282,7 @@
|
||||
|
||||
<Theme bind:theme={carbonTheme} />
|
||||
|
||||
<UpgradeModal />
|
||||
<Snackbar />
|
||||
|
||||
{#if data.skipAuth}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Configuration, OpenAIApi } from 'openai-edge';
|
||||
import { OpenAIStream, StreamingTextResponse } from 'ai';
|
||||
import { OPENAI_API_KEY } from '$env/static/private';
|
||||
import { OpenAIStream, StreamingTextResponse } from 'ai';
|
||||
import { Configuration, OpenAIApi } from 'openai-edge';
|
||||
|
||||
// Create an OpenAI API client (that's edge friendly!)
|
||||
const openAIConfig = new Configuration({
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { createCheckout } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import { LEMON_SQUEEZY_STORE_ID } from '$env/static/private';
|
||||
import { configureLemonSqueezy } from '$lib/utils/services/lemonsqueezy';
|
||||
|
||||
export async function POST({ request }) {
|
||||
const { email, name, orgId, productId, triggeredBy } = await request.json();
|
||||
|
||||
await configureLemonSqueezy();
|
||||
|
||||
if (!productId) {
|
||||
return json({ success: false, message: 'Product Id is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const checkoutData = {
|
||||
email: email,
|
||||
name: name,
|
||||
custom: {
|
||||
org_id: orgId,
|
||||
triggered_by: `${triggeredBy}`
|
||||
}
|
||||
};
|
||||
|
||||
console.log({ checkoutData });
|
||||
|
||||
const { data } = await createCheckout(LEMON_SQUEEZY_STORE_ID, productId, {
|
||||
checkoutData
|
||||
});
|
||||
|
||||
const checkoutURL = data?.data.attributes.url;
|
||||
|
||||
console.log({ checkoutURL });
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
checkoutURL
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { getSubscription } from '@lemonsqueezy/lemonsqueezy.js';
|
||||
import { configureLemonSqueezy } from '$lib/utils/services/lemonsqueezy';
|
||||
|
||||
export async function GET({ url }) {
|
||||
const subscriptionId = url.searchParams.get('subscriptionId');
|
||||
|
||||
await configureLemonSqueezy();
|
||||
|
||||
if (!subscriptionId) {
|
||||
return json({ success: false, message: 'Subscription id is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await getSubscription(subscriptionId);
|
||||
const customerPortal = response?.data?.data?.attributes?.urls?.customer_portal;
|
||||
|
||||
console.log({ customerPortal });
|
||||
|
||||
return json({
|
||||
success: true,
|
||||
customerPortal
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import crypto from 'crypto';
|
||||
import { LEMON_SQUEEZY_WEBHOOK_SECRET } from '$env/static/private';
|
||||
import { createOrgPlan, cancelOrgPlan } from '$lib/utils/services/org';
|
||||
import { PLAN } from 'shared-constants/src/plans/constants';
|
||||
|
||||
export async function POST({ request }) {
|
||||
try {
|
||||
// Catch the event type
|
||||
const clonedReq = request.clone();
|
||||
const eventType = request.headers.get('X-Event-Name');
|
||||
const body = await request.json();
|
||||
|
||||
// Check signature
|
||||
const secret = LEMON_SQUEEZY_WEBHOOK_SECRET;
|
||||
const hmac = crypto.createHmac('sha256', secret);
|
||||
const digest = Buffer.from(hmac.update(await clonedReq.text()).digest('hex'), 'utf8');
|
||||
const signature = Buffer.from(request.headers.get('X-Signature') || '', 'utf8');
|
||||
|
||||
if (!crypto.timingSafeEqual(digest, signature)) {
|
||||
throw new Error('Invalid signature.');
|
||||
}
|
||||
|
||||
console.log(body);
|
||||
|
||||
// Logic according to event
|
||||
if (eventType === 'subscription_created' || eventType === 'subscription_resumed') {
|
||||
const orgId = body.meta.custom_data.org_id;
|
||||
const triggeredBy = body.meta.custom_data.triggered_by;
|
||||
const isSuccessful = body.data.attributes.status === 'active';
|
||||
|
||||
console.log({
|
||||
orgId,
|
||||
triggeredBy,
|
||||
isSuccessful
|
||||
});
|
||||
|
||||
if (isSuccessful && orgId) {
|
||||
const { data, error } = await createOrgPlan({
|
||||
orgId,
|
||||
triggeredBy: parseInt(triggeredBy),
|
||||
planName: PLAN.EARLY_ADOPTER,
|
||||
data: body.data
|
||||
});
|
||||
|
||||
if (error) console.error('Error creating org plan', error);
|
||||
console.log('Create plan', data);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
['subscription_cancelled', 'subscription_expired', 'subscription_payment_refunded'].includes(
|
||||
`${eventType}`
|
||||
)
|
||||
) {
|
||||
const orgId = body.meta.custom_data.org_id;
|
||||
const { data, error } = await cancelOrgPlan({
|
||||
orgId,
|
||||
planName: PLAN.EARLY_ADOPTER
|
||||
});
|
||||
|
||||
if (error) console.error('Error canceling org plan', error);
|
||||
console.log('Cancel plan', data);
|
||||
}
|
||||
|
||||
return json({ message: 'Webhook received' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return json({ message: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export function GET() {
|
||||
return json({
|
||||
success: true
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export async function POST({ request }) {
|
||||
try {
|
||||
// Catch the event type
|
||||
// const clonedReq = request.clone();
|
||||
// const eventType = request.headers.get('X-Event-Name');
|
||||
const body = await request.json();
|
||||
|
||||
// // Check signature
|
||||
// const secret = LEMON_SQUEEZY_WEBHOOK_SECRET;
|
||||
// const hmac = crypto.createHmac('sha256', secret);
|
||||
// const digest = Buffer.from(hmac.update(await clonedReq.text()).digest('hex'), 'utf8');
|
||||
// const signature = Buffer.from(request.headers.get('X-Signature') || '', 'utf8');
|
||||
|
||||
// if (!crypto.timingSafeEqual(digest, signature)) {
|
||||
// throw new Error('Invalid signature.');
|
||||
// }
|
||||
|
||||
console.log(body);
|
||||
|
||||
// Logic according to event
|
||||
// if (eventType === 'order_created') {
|
||||
// const orgId = body.meta.custom_data.org_id;
|
||||
// const triggeredBy = body.meta.custom_data.triggered_by;
|
||||
// const isSuccessful = body.data.attributes.status === 'paid';
|
||||
|
||||
// console.log({
|
||||
// orgId,
|
||||
// triggeredBy,
|
||||
// isSuccessful
|
||||
// });
|
||||
|
||||
// if (isSuccessful) {
|
||||
// const { error } = await createOrgPlan({
|
||||
// orgId,
|
||||
// triggeredBy,
|
||||
// planName: PLAN.EARLY_ADOPTER,
|
||||
// data: body.data
|
||||
// });
|
||||
|
||||
// if (error) console.error('Error creating org plan', error);
|
||||
// }
|
||||
// }
|
||||
|
||||
return json({ message: 'Webhook received' });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return json({ message: 'Server error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export function GET() {
|
||||
return json({
|
||||
success: true
|
||||
});
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
import PageNav from '$lib/components/PageNav/index.svelte';
|
||||
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
|
||||
import { setCourse, course } from '$lib/components/Course/store';
|
||||
import IssueCertificateModal from '$lib/components/Course/components/Ceritficate/IssueCertificateModal.svelte';
|
||||
// import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
|
||||
import Design from '$lib/components/Course/components/Ceritficate/Design.svelte';
|
||||
@@ -40,7 +39,7 @@
|
||||
<StudentCertificate />
|
||||
</RoleBasedSecurity>
|
||||
|
||||
<!-- <Tabs autoWidth class="border-b border-gray-200">
|
||||
<!-- <Tabs autoWidth class="border-b border-gray-200 dark:border-neutral-600">
|
||||
<Tab label="Design" />
|
||||
<Tab label="Reports" />
|
||||
<svelte:fragment slot="content">
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
>
|
||||
{#each $lessons as lesson (lesson.id)}
|
||||
<div
|
||||
class="relative m-auto mb-4 flex max-w-xl items-center rounded-md border-2 border-gray-200 p-5 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
class="relative m-auto mb-4 flex max-w-xl items-center rounded-md border-2 border-gray-200 dark:border-neutral-600 p-5 dark:border-neutral-700 dark:bg-neutral-800"
|
||||
>
|
||||
<!-- Number Chip -->
|
||||
<div class="mr-5">
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { logout } from '$lib/utils/functions/logout';
|
||||
|
||||
onMount(() => {
|
||||
logout();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="app-background w-full min-h-screen flex items-center justify-center">
|
||||
<div>Logging you out...</div>
|
||||
</div>
|
||||
@@ -0,0 +1,5 @@
|
||||
export const load = ({ params = { slug: '' } }) => {
|
||||
return {
|
||||
orgName: params.slug
|
||||
};
|
||||
};
|
||||
@@ -4,6 +4,15 @@
|
||||
import { isQuizPage } from '$lib/utils/functions/app';
|
||||
import OrgSideBar from '$lib/components/Org/SideBar.svelte';
|
||||
import VerifyEmailModal from '$lib/components/Org/VerifyEmail/VerifyEmailModal.svelte';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
|
||||
$: if ($currentOrg.id && data.orgName === '*') {
|
||||
goto(`/org/${$currentOrg.siteName}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<AddOrgModal />
|
||||
@@ -13,6 +22,10 @@
|
||||
<OrgSideBar />
|
||||
{/if}
|
||||
<div class="org-slot bg-white dark:bg-black w-full">
|
||||
<slot />
|
||||
{#if data.orgName === '*'}
|
||||
<Box>Taking you to your organization...</Box>
|
||||
{:else}
|
||||
<slot />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -184,7 +184,7 @@
|
||||
|
||||
<WelcomeModal />
|
||||
|
||||
<div class="py-10 px-5 w-full max-w-7xl mx-auto">
|
||||
<div class="py-10 px-5 w-full max-w-5xl md:mx-auto">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold mb-3">
|
||||
{getGreeting()}
|
||||
@@ -232,7 +232,7 @@
|
||||
<!-- <div class="flex items-start flex-wrap">
|
||||
{#each boxes as box}
|
||||
<div
|
||||
class="w-full md:w-[246px] h-[165px] flex flex-col rounded border border-gray-200 justify-center px-5 md:mr-5 mb-5"
|
||||
class="w-full md:w-[246px] h-[165px] flex flex-col rounded border border-gray-200 dark:border-neutral-600 justify-center px-5 md:mr-5 mb-5"
|
||||
>
|
||||
<p class="dark:text-white mb-2 text-sm">{box.label}</p>
|
||||
<p class="dark:text-white text-xl font-bold">{box.value}</p>
|
||||
@@ -245,7 +245,7 @@
|
||||
<div class="w-full xl:w-auto container">
|
||||
<p class="dark:text-white font-bold mt-7 mb-4">Your Schedule</p>
|
||||
<div
|
||||
class="rounded border border-gray-200 gap-3 flex flex-col md:flex-row items-start px-2 md:px-5 py-5 w-full"
|
||||
class="rounded border border-gray-200 dark:border-neutral-600 gap-3 flex flex-col md:flex-row items-start px-2 md:px-5 py-5 w-full"
|
||||
>
|
||||
{#if !$isMobile}
|
||||
<div id="calendar" class="flex justify-center mt-5 w-2/5 min-w-[250px]">
|
||||
@@ -326,10 +326,10 @@
|
||||
<!-- <div class="w-full">
|
||||
<p class="dark:text-white font-bold mb-7">Your Activities</p>
|
||||
<div
|
||||
class="rounded border border-gray-200 md:min-w-[450px] activities-box py-4 px-2 md:px-5 w-full"
|
||||
class="rounded border border-gray-200 dark:border-neutral-600 md:min-w-[450px] activities-box py-4 px-2 md:px-5 w-full"
|
||||
>
|
||||
{#each activities as activity}
|
||||
<div class="flex mb-3 pb-3 border-b border-gray-200">
|
||||
<div class="flex mb-3 pb-3 border-b border-gray-200 dark:border-neutral-600">
|
||||
<Avatar src={activity.avatar} name="avatar" />
|
||||
<div class="ml-2 flex flex-col xl:flex-row">
|
||||
<div class="mr-2">
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
<script>
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Audience from '$lib/components/Org/Audience/index.svelte';
|
||||
import { orgAudience, currentOrgPlan, currentOrgMaxAudience } from '$lib/utils/store/org';
|
||||
import { PLAN } from 'shared-constants/src/plans/constants';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
|
||||
let isLoading = false;
|
||||
|
||||
function exportAudience() {
|
||||
isLoading = true;
|
||||
alert('This feature is coming soon');
|
||||
@@ -14,13 +18,23 @@
|
||||
<title>Audience - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<section class="w-full max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Audience</h1>
|
||||
<div class="flex items-end">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold m-0">Audience</h1>
|
||||
{#if $currentOrgPlan?.plan_name !== PLAN.ENTERPRISE}
|
||||
<span class="ml-2">
|
||||
({$orgAudience.length} / {$currentOrgMaxAudience})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<PrimaryButton label="Export" onClick={exportAudience} isDisabled={isLoading} {isLoading} />
|
||||
</div>
|
||||
|
||||
{#if $orgAudience.length >= $currentOrgMaxAudience}
|
||||
<UpgradeBanner>Upgrade your plan to invite more students</UpgradeBanner>
|
||||
{/if}
|
||||
<Audience />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { browser } from '$app/environment';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
export let data;
|
||||
let { cantFetch } = data;
|
||||
@@ -93,6 +94,7 @@
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
label="Create Course"
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
isDisabled={!$isOrgAdmin}
|
||||
onClick={() => ($createCourseModal.open = true)}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<title>Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">Settings</h1>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<title>Custom domain - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-3 md:px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>Back to Settings</a
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script>
|
||||
import TeamMembers from '$lib/components/Org/Settings/TeamMembers.svelte';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Teams - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-3 md:px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>Back to Settings</a
|
||||
@@ -16,6 +17,7 @@
|
||||
<h1 class="dark:text-white text-3xl font-bold">Team members</h1>
|
||||
</div>
|
||||
|
||||
<UpgradeBanner>Upgrade your plan to add team members</UpgradeBanner>
|
||||
<TeamMembers />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
<script>
|
||||
import Chip from '$lib/components/Chip/Text.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import StepDoneIcon from '$lib/components/Icons/StepDoneIcon.svelte';
|
||||
import CheckmarkOutline from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
import { snackbar } from '$lib/components/Snackbar/store.js';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
|
||||
export let data;
|
||||
let setupList = data.setup || [];
|
||||
let completed = 0;
|
||||
|
||||
const StepsEnum = {
|
||||
UPDATE_PROFILE: 'Upload a profile picture and update username',
|
||||
UPDATE_ORG_PROFILE: 'Update organisation profile picture',
|
||||
CREATE_COURSE: 'Create Course',
|
||||
CREATE_LESSON: 'Create a lesson',
|
||||
CREATE_EXERCISE: 'Create an exercise',
|
||||
PUBLISH_COURSE: 'Publish a course'
|
||||
};
|
||||
|
||||
const isCompleted = (id) => {
|
||||
return setupList?.find((c) => c.id === id).is_completed == true;
|
||||
};
|
||||
|
||||
const handleClick = (title) => {
|
||||
switch (title) {
|
||||
case StepsEnum.CREATE_COURSE:
|
||||
goto(`/org/${$currentOrg.siteName}/courses?create=true`);
|
||||
break;
|
||||
|
||||
case StepsEnum.CREATE_LESSON:
|
||||
if (isCompleted('course')) {
|
||||
const courseId = data?.courses[0].id;
|
||||
goto(`/courses/${courseId}/lessons`);
|
||||
} else {
|
||||
snackbar.info('You need to create a course');
|
||||
}
|
||||
break;
|
||||
|
||||
case StepsEnum.CREATE_EXERCISE:
|
||||
if (isCompleted('lesson')) {
|
||||
const courseId = data?.courses[0].id;
|
||||
const lessonId = data?.lessons[0].id;
|
||||
goto(`/courses/${courseId}/lessons/${lessonId}`);
|
||||
} else {
|
||||
snackbar.info('You Need to Create a lesson');
|
||||
}
|
||||
break;
|
||||
|
||||
case StepsEnum.PUBLISH_COURSE:
|
||||
if (isCompleted('course')) {
|
||||
const courseId = data?.courses[0].id;
|
||||
goto(`/courses/${courseId}/settings`);
|
||||
} else {
|
||||
snackbar.info('You need to create a course');
|
||||
}
|
||||
break;
|
||||
|
||||
case StepsEnum.UPDATE_PROFILE:
|
||||
goto(`/org/${$currentOrg.siteName}/settings`);
|
||||
break;
|
||||
|
||||
case StepsEnum.UPDATE_ORG_PROFILE:
|
||||
goto(`/org/${$currentOrg.siteName}/settings?tab=org`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
$: setupList = setupList.map((item) => {
|
||||
if (item.id === 'profile') {
|
||||
item.is_completed = !$profile.avatar_url.includes('avatars/avatar.png');
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
$: completed = setupList.filter((list) => list.is_completed).length;
|
||||
</script>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-2 md:py-10 px-2 md:px-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Get Started</h1>
|
||||
<Chip
|
||||
value={`${completed}/${setupList.length}`}
|
||||
className="text-[10px] font-semibold px-3 !py-1"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<section class="px-4">
|
||||
{#each setupList as list, i}
|
||||
<div
|
||||
class="flex flex-col lg:flex-row lg:items-center justify-between gap-2 w-full py-8 border-b border-gray-300"
|
||||
>
|
||||
<div class={`space-y-2 flex-1 ${list.is_completed ? 'opacity-50' : ''} max-w-[50%]`}>
|
||||
<div class="flex items-center gap-2">
|
||||
<Chip
|
||||
value={i + 1}
|
||||
className={`text-[10px] font-semibold !py-1 `}
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<p class="font-medium text-lg">{list.title}</p>
|
||||
</div>
|
||||
<p class={`text-sm`}>{list.desc}</p>
|
||||
</div>
|
||||
<div class="w-[30%]">
|
||||
<PrimaryButton
|
||||
variant={list.is_completed ? VARIANTS.CONTAINED_DARK : VARIANTS.OUTLINED}
|
||||
className="!w-full font-normal text-sm flex items-center gap-2"
|
||||
onClick={() => handleClick(list.title)}
|
||||
isDisabled={list.is_completed}
|
||||
>
|
||||
{#if list.is_completed}
|
||||
<CheckmarkOutline />
|
||||
{/if}
|
||||
{list.button_label}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,146 @@
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import get from 'lodash/get';
|
||||
|
||||
const supabase = getSupabase();
|
||||
|
||||
async function getGenericData(orgSlug: string): Promise<{
|
||||
isCoursePublished: boolean;
|
||||
isCourseCreated: boolean;
|
||||
orgHasAvatarUrl: boolean;
|
||||
courseData: any;
|
||||
}> {
|
||||
const { data } = await supabase
|
||||
.from('course')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
is_published,
|
||||
group:group_id!inner(
|
||||
organization!inner(
|
||||
avatar_url,
|
||||
siteName
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('group.organization.siteName', orgSlug);
|
||||
|
||||
const result = data || [];
|
||||
|
||||
console.log('courseData', result);
|
||||
|
||||
return {
|
||||
isCoursePublished: result.some((c) => c.is_published === true),
|
||||
isCourseCreated: result.length > 0,
|
||||
orgHasAvatarUrl: !!get(result[0], 'group.organization.avatar_url', ''),
|
||||
courseData: result
|
||||
};
|
||||
}
|
||||
|
||||
async function getIsLessonCreated(
|
||||
orgSlug: string
|
||||
): Promise<{ isLessonCreated: boolean; lessonData: any }> {
|
||||
const { data } = await supabase
|
||||
.from('lesson')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
course:course_id!inner(
|
||||
group:group_id!inner(
|
||||
organization!inner(
|
||||
siteName
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('course.group.organization.siteName', orgSlug);
|
||||
console.log('lessonsData', data);
|
||||
|
||||
return {
|
||||
isLessonCreated: Array.isArray(data) ? data.length > 0 : false,
|
||||
lessonData: data
|
||||
};
|
||||
}
|
||||
|
||||
async function getIsExerciseCreated(orgSlug: string): Promise<boolean> {
|
||||
const { data } = await supabase
|
||||
.from('exercise')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
lesson:lesson_id!inner(
|
||||
course:course_id!inner(
|
||||
group:group_id!inner(
|
||||
organization!inner(
|
||||
siteName
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('lesson.course.group.organization.siteName', orgSlug);
|
||||
|
||||
return Array.isArray(data) ? data?.length > 0 : false;
|
||||
}
|
||||
|
||||
export const load = async ({ params = { slug: '' } }) => {
|
||||
const { isCourseCreated, isCoursePublished, orgHasAvatarUrl, courseData } = await getGenericData(
|
||||
params.slug
|
||||
);
|
||||
const { isLessonCreated, lessonData } = await getIsLessonCreated(params.slug);
|
||||
const isExerciseCreated = await getIsExerciseCreated(params.slug);
|
||||
|
||||
const data = [
|
||||
{
|
||||
id: 'profile',
|
||||
title: 'Upload a profile picture and update username',
|
||||
desc: 'Personalize and a human touch making interactions more personal and memorable',
|
||||
is_completed: false,
|
||||
button_label: 'Update Profile'
|
||||
},
|
||||
{
|
||||
id: 'organization',
|
||||
title: 'Update organisation profile picture',
|
||||
desc: 'Establish a professional and recognizable identity for your organization',
|
||||
is_completed: orgHasAvatarUrl,
|
||||
button_label: 'Update Org Profile'
|
||||
},
|
||||
{
|
||||
id: 'course',
|
||||
title: 'Create Course',
|
||||
desc: 'Create a course that you will share with your students',
|
||||
is_completed: isCourseCreated,
|
||||
button_label: 'Create Course'
|
||||
},
|
||||
{
|
||||
id: 'lesson',
|
||||
title: 'Create a lesson',
|
||||
desc: 'Break your course into lesson that your students can easily understand',
|
||||
is_completed: isLessonCreated,
|
||||
button_label: 'Create Lesson'
|
||||
},
|
||||
{
|
||||
id: 'exercise',
|
||||
title: 'Create an exercise',
|
||||
desc: 'Test your students allow them to demonstarte their understanding of the subject matter',
|
||||
is_completed: isExerciseCreated,
|
||||
button_label: 'Create Assignment'
|
||||
},
|
||||
{
|
||||
id: 'publish',
|
||||
title: 'Publish a course',
|
||||
desc: 'Make your course public and purchaseable ',
|
||||
is_completed: isCoursePublished,
|
||||
button_label: 'Publish Course'
|
||||
}
|
||||
];
|
||||
|
||||
return {
|
||||
orgSiteName: params.slug,
|
||||
setup: data,
|
||||
courses: courseData,
|
||||
lessons: lessonData
|
||||
};
|
||||
};
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -1,10 +1,16 @@
|
||||
import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig, loadEnv } from 'vite';
|
||||
import fs from 'fs';
|
||||
|
||||
export default ({ mode }) => {
|
||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
|
||||
const { VITE_SENTRY_AUTH_TOKEN, VITE_SENTRY_ORG_NAME, VITE_SENTRY_PROJECT_NAME } = process.env;
|
||||
const {
|
||||
VITE_SENTRY_AUTH_TOKEN,
|
||||
VITE_SENTRY_ORG_NAME,
|
||||
VITE_SENTRY_PROJECT_NAME,
|
||||
VITE_USE_HTTPS_ON_LOCALHOST
|
||||
} = process.env;
|
||||
|
||||
return defineConfig({
|
||||
plugins: [
|
||||
@@ -20,6 +26,15 @@ export default ({ mode }) => {
|
||||
: {}
|
||||
)
|
||||
],
|
||||
server:
|
||||
VITE_USE_HTTPS_ON_LOCALHOST === 'true'
|
||||
? {
|
||||
https: {
|
||||
key: fs.readFileSync(`${__dirname}/cert/key.pem`),
|
||||
cert: fs.readFileSync(`${__dirname}/cert/cert.pem`)
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
build: {
|
||||
sourcemap: true
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<br />
|
||||
<img
|
||||
alt="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease."
|
||||
alt="ClassroomIO is the easiest place to launch and scale your online bootcamp."
|
||||
src="https://classroomio.com/classroomio-opengraph-image.png"
|
||||
/>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ const config: DocsThemeConfig = {
|
||||
<title>ClassroomIO – Launch Your Online Bootcamp In Minutes</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease."
|
||||
content="ClassroomIO is the easiest place to launch and scale your online bootcamp."
|
||||
/>
|
||||
|
||||
<meta property="og:url" content="https://classroomio.com/docs" />
|
||||
@@ -33,7 +33,7 @@ const config: DocsThemeConfig = {
|
||||
<meta property="og:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||
<meta
|
||||
property="og:description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease."
|
||||
content="ClassroomIO is the easiest place to launch and scale your online bootcamp."
|
||||
/>
|
||||
<meta property="og:image" content="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||
|
||||
@@ -43,7 +43,7 @@ const config: DocsThemeConfig = {
|
||||
<meta name="twitter:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease."
|
||||
content="ClassroomIO is the easiest place to launch and scale your online bootcamp."
|
||||
/>
|
||||
<meta name="twitter:creator" content="@classroomio" />
|
||||
<meta
|
||||
@@ -88,8 +88,7 @@ const config: DocsThemeConfig = {
|
||||
openGraph: {
|
||||
siteName: 'ClassroomIO Docs',
|
||||
title: 'ClassroomIO Documentation',
|
||||
description:
|
||||
'ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease.',
|
||||
description: 'ClassroomIO is the easiest place to launch and scale your online bootcamp.',
|
||||
images: [
|
||||
{
|
||||
url: `${process.env.imagePath}/img/logo-16.png`,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "shared-constants",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"tsconfig": "workspace:*"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
export const PLAN = {
|
||||
BASIC: 'BASIC',
|
||||
EARLY_ADOPTER: 'EARLY_ADOPTER',
|
||||
ENTERPRISE: 'ENTERPRISE'
|
||||
};
|
||||
|
||||
export const PLAN_NAMES = {
|
||||
[PLAN.BASIC]: 'Free',
|
||||
[PLAN.EARLY_ADOPTER]: 'Early Adopter',
|
||||
[PLAN.ENTERPRISE]: 'Enterprise',
|
||||
}
|
||||
|
||||
export const FEATURES = {
|
||||
BACIC_STUDENTS_50: 'BACIC_STUDENTS_50',
|
||||
EA_STUDENTS_10000: 'EA_STUDENTS_10000',
|
||||
EA_VIDEO_UPLOAD: 'EA_VIDEO_UPLOAD',
|
||||
EA_CERTIFICATE: 'EA_CERTIFICATE',
|
||||
EA_UPCOMING_FEATURES: 'EA_UPCOMING_FEATURES',
|
||||
ENTERPRISE_STUDENTS_UNLIMITED: 'ENTERPRISE_STUDENTS_UNLIMITED',
|
||||
ENTERPRISE_CUSTOM_DOMAIN: 'ENTERPRISE_CUSTOM_DOMAIN'
|
||||
};
|
||||
|
||||
export const BASIC_FEATURES = [FEATURES.BACIC_STUDENTS_50];
|
||||
|
||||
export const EARYL_ADOPTER_FEATURES = [
|
||||
...BASIC_FEATURES,
|
||||
FEATURES.EA_STUDENTS_10000,
|
||||
FEATURES.EA_VIDEO_UPLOAD,
|
||||
FEATURES.EA_CERTIFICATE,
|
||||
FEATURES.EA_UPCOMING_FEATURES
|
||||
];
|
||||
|
||||
export const ENTERPRISE_FEATURES = [
|
||||
...EARYL_ADOPTER_FEATURES,
|
||||
FEATURES.ENTERPRISE_STUDENTS_UNLIMITED,
|
||||
FEATURES.ENTERPRISE_CUSTOM_DOMAIN
|
||||
];
|
||||
|
||||
export const PLANS_BY_FEATURE = {
|
||||
[PLAN.BASIC]: BASIC_FEATURES,
|
||||
[PLAN.EARLY_ADOPTER]: EARYL_ADOPTER_FEATURES,
|
||||
[PLAN.ENTERPRISE]: ENTERPRISE_FEATURES
|
||||
};
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"BASIC": {
|
||||
"NAME": "Basic",
|
||||
"DESCRIPTION": "Free tier plan for personal use, no credit card required.",
|
||||
"PRICE": {
|
||||
"CURRENCY": "$",
|
||||
"MONTHLY": "0",
|
||||
"YEARLY": "0",
|
||||
"IS_PREMIUM": false
|
||||
},
|
||||
"FEATURES": [
|
||||
"Unlimited Courses",
|
||||
"Unlimited Q/A",
|
||||
"50 Students",
|
||||
"AI Course Builder (no video upload)",
|
||||
"Live chat & polls"
|
||||
],
|
||||
"CTA": {
|
||||
"LABEL": "Signup Now",
|
||||
"LINK": "/signup?plan=free",
|
||||
"DASHBOARD_LABEL": "Downgrade",
|
||||
"DASHBOARD_LINK": "#disabled",
|
||||
"IS_DISABLED": true
|
||||
}
|
||||
},
|
||||
"EARLY_ADOPTER": {
|
||||
"NAME": "Early Adopters",
|
||||
"DESCRIPTION": "For fast growing teaching businesses that aim to scale.",
|
||||
"PRICE": {
|
||||
"CURRENCY": "$",
|
||||
"MONTHLY": "35",
|
||||
"YEARLY": "350",
|
||||
"IS_PREMIUM": false
|
||||
},
|
||||
"FEATURES": [
|
||||
"Everything in Basic",
|
||||
"Unlimted Teachers",
|
||||
"10K Students",
|
||||
"Advanced AI Course Builder (Video upload & Certificate)",
|
||||
"Custom Branding",
|
||||
"Includes all Upcoming Features"
|
||||
],
|
||||
"CTA": {
|
||||
"LABEL": "I want in 😍",
|
||||
"LINK": "/signup?plan=early-adopter",
|
||||
"DASHBOARD_LABEL": "Support us 😍",
|
||||
"DASHBOARD_LINK": "",
|
||||
"IS_DISABLED": false,
|
||||
"PRODUCT_ID": "297248"
|
||||
}
|
||||
},
|
||||
"ENTERPRISE": {
|
||||
"NAME": "Enterprise",
|
||||
"DESCRIPTION": "Best suited for larger businesses that need more control",
|
||||
"PRICE": {
|
||||
"CURRENCY": "",
|
||||
"MONTHLY": "Request Pricing",
|
||||
"YEARLY": "Request Pricing",
|
||||
"IS_PREMIUM": true
|
||||
},
|
||||
"FEATURES": [
|
||||
"Everything in Early Adopters plus:",
|
||||
"Unlimited students",
|
||||
"Custom Domain",
|
||||
"24/7 Support"
|
||||
],
|
||||
"CTA": {
|
||||
"LABEL": "Contact Us",
|
||||
"LINK": "https://cal.com/classroomio/enterprise",
|
||||
"DASHBOARD_LABEL": "Contact Us",
|
||||
"DASHBOARD_LINK": "https://cal.com/classroomio/enterprise",
|
||||
"IS_DISABLED": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PLANS_BY_FEATURE } from './constants';
|
||||
|
||||
export function hasFeature(featureName: string, userPlan: string) {
|
||||
const features = PLANS_BY_FEATURE[userPlan];
|
||||
|
||||
return features.includes(featureName);
|
||||
}
|
||||