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/">
|
<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>
|
<h1 align="center">ClassroomIO.com</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
The Open Source Teaching Platform
|
The Open Source Teaching Platform for Bootcamps
|
||||||
<br />
|
<br />
|
||||||
<a href="https://classroomio.com">Website</a> | <a href="https://dub.sh/ciodiscord">Join Discord community</a>
|
<a href="https://classroomio.com">Website</a> | <a href="https://dub.sh/ciodiscord">Join Discord community</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<a href="https://classroomio.com/">
|
<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>
|
<h1 align="center">ClassroomIO</h1>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
The Open Source Teaching Platform
|
The Open Source Teaching Platform for Bootcamps
|
||||||
|
|
||||||
## Tools
|
## Tools
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
"rehype-slug": "^6.0.0",
|
"rehype-slug": "^6.0.0",
|
||||||
"remark-toc": "^9.0.0",
|
"remark-toc": "^9.0.0",
|
||||||
"remark-unwrap-images": "^4.0.0",
|
"remark-unwrap-images": "^4.0.0",
|
||||||
|
"shared-constants": "workspace:*",
|
||||||
"shiki": "^0.14.7"
|
"shiki": "^0.14.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
<meta property="og:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
<meta property="og:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
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:type" content="website" />
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="twitter:description"
|
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" />
|
<meta name="twitter:image" content="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||||
<!-- <link rel="stylesheet" href="global.css"> -->
|
<!-- <link rel="stylesheet" href="global.css"> -->
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
/>
|
/>
|
||||||
<meta
|
<meta
|
||||||
name="twitter:description"
|
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" />
|
<meta name="twitter:image" content="https://classroomio.com/classroomio-opengraph-image.png" />
|
||||||
<!-- <link rel="stylesheet" href="global.css"> -->
|
<!-- <link rel="stylesheet" href="global.css"> -->
|
||||||
|
|||||||
@@ -2,29 +2,29 @@
|
|||||||
import { faqs } from './faqs';
|
import { faqs } from './faqs';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-[5%] mb-[15%]">
|
<div class="mb-[15%] mt-[5%]">
|
||||||
<header class="text-center mb-[5%] px-[8%]">
|
<header class="mb-[5%] px-[8%] text-center">
|
||||||
<h1 class="mt-2 text-4xl lg:text-4xl font-medium tracking-tight text-gray-900">
|
<h1 class="mt-2 text-4xl font-medium tracking-tight text-gray-900 lg:text-4xl">
|
||||||
Questions & Answers
|
Questions & Answers
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<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?
|
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
|
>Shoot us an email</a
|
||||||
> and we’ll get back to you ASAP.
|
> and we’ll get back to you ASAP.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</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}
|
{#each faqs as faq}
|
||||||
<div class="max-w-[100%] px-6 lg:p-0">
|
<div class="max-w-[100%] px-6 lg:p-0">
|
||||||
<h1 class="text:md lg:text-md font-semibold leading-8 text-gray-800">
|
<h1 class="text:md lg:text-md font-semibold leading-8 text-gray-800">
|
||||||
{faq.question}
|
{faq.question}
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<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}
|
{faq.answer}
|
||||||
</p>
|
</p>
|
||||||
@@ -33,16 +33,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
<h1 class="font-display mt-3 text-3xl font-bold tracking-tight text-white lg:mt-2 lg:text-4xl">
|
||||||
Supercharge your Classroom Today
|
Launch Your Bootcamp Today
|
||||||
</h1>
|
</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.
|
Try before you buy. No credit card required.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<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-config={"{'layout':'month_view'}"}
|
||||||
data-cal-link="classroomio/demo"
|
data-cal-link="classroomio/demo"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import LogoFacebook from 'carbon-icons-svelte/lib/LogoFacebook.svelte';
|
import LogoFacebook from 'carbon-icons-svelte/lib/LogoFacebook.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="px-[2%] lg:px-[14%] border-t-[1px]">
|
<div class="border-t-[1px] px-[2%] lg:px-[14%]">
|
||||||
<div class="flex flex-col lg:flex-row justify-between items-center py-[10%] pl-5 lg:pl-0">
|
<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">
|
<div class="w-full lg:w-2/4">
|
||||||
<img
|
<img
|
||||||
width="32"
|
width="32"
|
||||||
@@ -12,14 +12,14 @@
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
src="/logo-192.png"
|
src="/logo-192.png"
|
||||||
alt="classroomio logo"
|
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
|
Launch Your Online Bootcamp In Minutes
|
||||||
<br />
|
<br />
|
||||||
Making teaching and learning seamless for tech bootcamps
|
The easiest way to launch and scale your online bootcamp
|
||||||
</p>
|
</p>
|
||||||
<a
|
<!-- <a
|
||||||
href="https://www.producthunt.com/posts/classroomio?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-classroomio"
|
href="https://www.producthunt.com/posts/classroomio?utm_source=badge-featured&utm_medium=badge&utm_souce=badge-classroomio"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
><img
|
><img
|
||||||
@@ -30,11 +30,11 @@
|
|||||||
width="250"
|
width="250"
|
||||||
height="54"
|
height="54"
|
||||||
/></a
|
/></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">
|
<a href="https://www.facebook.com/classroomiohq" target="_blank" aria-label="Facebook Link">
|
||||||
<LogoFacebook
|
<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}
|
size={20}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -45,12 +45,12 @@
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
src="/twitter_logo.png"
|
src="/twitter_logo.png"
|
||||||
alt=""
|
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>
|
||||||
<a href="https://www.youtube.com/@ClassroomIO" target="_blank" aria-label="Youtube Link">
|
<a href="https://www.youtube.com/@ClassroomIO" target="_blank" aria-label="Youtube Link">
|
||||||
<LogoYoutube
|
<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}
|
size={20}
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -58,64 +58,64 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav
|
<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>
|
<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
|
Product
|
||||||
</p>
|
</p>
|
||||||
<ul class="space-y-2 lg:space-y-4">
|
<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>
|
<a href="/#coursemanagement"> Course Management </a>
|
||||||
</li>
|
</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>
|
<a href="/#customization"> Customizations </a>
|
||||||
</li>
|
</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>
|
<a href="/#collaboration"> Forum </a>
|
||||||
</li>
|
</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>
|
<a href="/#ai"> AI Lesson Planner </a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
Resources
|
||||||
</p>
|
</p>
|
||||||
<ul class="space-y-2 lg:space-y-4">
|
<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>
|
<a href="/blog"> Blog </a>
|
||||||
</li>
|
</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>
|
<a target="_blank" href="https://classroomio.com/docs/"> Help center </a>
|
||||||
</li>
|
</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">
|
<a target="_blank" href="https://classroomio.com/docs/what-is-classroomio">
|
||||||
What is ClassroomIO?
|
What is ClassroomIO?
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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">
|
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup">
|
||||||
Getting Started
|
Getting Started
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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>
|
<a href="/oss-friends"> OSS Friends </a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
Company
|
||||||
</p>
|
</p>
|
||||||
<ul class="space-y-2 lg:space-y-4">
|
<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>
|
<a href="mailto:[email protected]" target="_blank"> Contact us </a>
|
||||||
</li>
|
</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
|
<a
|
||||||
href="https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594"
|
href="https://app.enzuzo.com/policies/privacy/958fc978-5477-11ee-a03b-7b111830c594"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -123,12 +123,12 @@
|
|||||||
Privacy policy
|
Privacy policy
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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">
|
<a target="_blank" href="https://classroomio.com/docs/quickstart/signup">
|
||||||
Getting Started
|
Getting Started
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</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
|
<a
|
||||||
href="https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594"
|
href="https://app.enzuzo.com/policies/tos/958fc978-5477-11ee-a03b-7b111830c594"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -141,12 +141,12 @@
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<footer
|
<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>All rights reserved.</p>
|
||||||
<p class="flex items-center">
|
<p class="flex items-center">
|
||||||
ClassroomIO © {new Date().getFullYear()} by
|
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
|
>Rotimi Best</a
|
||||||
>
|
>
|
||||||
from
|
from
|
||||||
|
|||||||
@@ -42,7 +42,8 @@
|
|||||||
<h1
|
<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"
|
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"
|
<span class="relative text-blue-700"
|
||||||
>In Minutes
|
>In Minutes
|
||||||
<svg
|
<svg
|
||||||
@@ -59,8 +60,8 @@
|
|||||||
</h1>
|
</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">
|
<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
|
Meet a better learning management system (LMS) for teachers and students. ClassroomIO allows you
|
||||||
allows you build and scale your own teaching platform with ease.
|
to launch and scale your online bootcamp with ease.
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-10 flex flex-col items-center justify-center gap-4 md:flex-row">
|
<div class="mt-10 flex flex-col items-center justify-center gap-4 md:flex-row">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,158 +1,141 @@
|
|||||||
<script>
|
<script>
|
||||||
import Checkmark from 'carbon-icons-svelte/lib/Checkmark.svelte';
|
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() {
|
function toggleIsYearlyPlan() {
|
||||||
prices = !prices;
|
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>
|
</script>
|
||||||
|
|
||||||
<section id="pricing" class="py-[12%]">
|
<section id="pricing" class="py-[12%]">
|
||||||
<div class="px-[6%] flex flex-col justify-center items-center">
|
<div class="flex flex-col items-center justify-center px-[6%]">
|
||||||
<div class="text-center mb-[3%]">
|
<div class="mb-[3%] text-center">
|
||||||
<h1 id="pricing" class="text-blue-700 text-md font-semibold tracking-wide">Pricing</h1>
|
<h1 id="pricing" class="text-md font-semibold tracking-wide text-blue-700">Pricing</h1>
|
||||||
<h2 class="my-5 lg:my-4 font-semibold tracking-tight text-gray-900 text-3xl lg:text-4xl">
|
<h2 class="my-5 text-3xl font-semibold tracking-tight text-gray-900 lg:my-4 lg:text-4xl">
|
||||||
For teams and individuals
|
For teams and individuals
|
||||||
</h2>
|
</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...
|
Customizable LMS, AI integration for productive educators and many more...
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
<button
|
||||||
style="background-color: {prices ? 'initial' : '#1D4EE2'}; color: {prices
|
style="background-color: {isYearlyPlan ? 'initial' : '#1D4EE2'}; color: {isYearlyPlan
|
||||||
? '#5e636b'
|
? '#5e636b'
|
||||||
: '#fff'}"
|
: '#fff'}"
|
||||||
class="bg-blue-700 text-white rounded-[30px] py-1 px-3 lg:py-2 lg:px-4 text-xs"
|
class="rounded-[30px] bg-blue-700 px-3 py-1 text-xs text-white lg:px-4 lg:py-2"
|
||||||
on:click={handlePriceChange}>Monthly</button
|
on:click={toggleIsYearlyPlan}>Monthly</button
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
style="background-color: {prices ? '#1D4EE2' : ''}; color: {prices ? '#fff' : '#5e636b'}"
|
style="background-color: {isYearlyPlan ? '#1D4EE2' : ''}; color: {isYearlyPlan
|
||||||
class="text-white rounded-[30px] py-1 lg:py-2 px-3 lg:px-4 text-xs"
|
? '#fff'
|
||||||
on:click={handlePriceChange}>Annually</button
|
: '#5e636b'}"
|
||||||
|
class="rounded-[30px] px-3 py-1 text-xs text-white lg:px-4 lg:py-2"
|
||||||
|
on:click={toggleIsYearlyPlan}>Annually</button
|
||||||
>
|
>
|
||||||
<div
|
<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
|
Save 2 months
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="isolate mt-10 grid gap-10 grid-cols-1 lg:grid-cols-3">
|
<div class="isolate mt-10 grid grid-cols-1 gap-10 lg:grid-cols-3">
|
||||||
<!-- card 1 -->
|
<!-- Card 1 -->
|
||||||
<div class="ring-gray-200 rounded-3xl p-8 ring-1 xl:p-9 max-w-xl lg:max-w-sm">
|
<div class="max-w-xl rounded-3xl p-8 ring-1 ring-gray-200 lg:max-w-sm xl:p-9">
|
||||||
<p class="text-gray-900 text-lg lg:text-xl font-semibold leading-8">Hobby</p>
|
<p class="text-lg font-semibold leading-8 text-gray-900 lg:text-xl">
|
||||||
<p class="text-gray-500 font-light mt-4 text-sm lg:text-md leading-6 lg:leading-6">
|
{PLANS.BASIC.NAME}
|
||||||
For personal use. Hobby plan is free for up to 5 courses forever, no credit card required.
|
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-6 flex items-baseline gap-x-1 text-4xl lg:text-4xl font-medium">
|
<p class="lg:text-md mt-4 text-sm font-light leading-6 text-gray-500 lg:leading-6">
|
||||||
$0 <sub class="text-base">/mo</sub>
|
{PLANS.BASIC.DESCRIPTION}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<p class="mt-6 flex items-baseline gap-x-1 text-4xl font-medium lg:text-4xl">
|
||||||
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"
|
${PLANS.BASIC.PRICE.MONTHLY}
|
||||||
data-cal-config={"{'layout':'month_view'}"}
|
<!-- <sub class="text-base">/mo</sub> -->
|
||||||
data-cal-link="classroomio/demo"
|
</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
|
{PLANS.BASIC.CTA.LABEL}
|
||||||
</button>
|
</a>
|
||||||
<ul class="text-gray-600 mt-8 space-y-2 lg:space-y-5 text-sm xl:mt-10">
|
<ul class="mt-8 space-y-2 text-sm text-gray-600 lg:space-y-5 xl:mt-10">
|
||||||
{#each freeFeatures as freeFeature}
|
{#each PLANS.BASIC.FEATURES as feature}
|
||||||
<li class="flex items-center">
|
<li class="flex items-center">
|
||||||
<Checkmark
|
<Checkmark
|
||||||
size={24}
|
size={24}
|
||||||
fill="#1D4EE2"
|
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>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- card 2 -->
|
<!-- Card 2 -->
|
||||||
<div class="bg-gray-900 rounded-3xl p-8 xl:p-9 max-w-xl lg:max-w-lg">
|
<div class="max-w-xl rounded-3xl bg-gray-900 p-8 lg:max-w-lg xl:p-9">
|
||||||
<p class="text-white text-lg lg:text-xl font-semibold leading-8">Pro</p>
|
<p class="text-lg font-semibold leading-8 text-white lg:text-xl">
|
||||||
<p class="text-white font-light mt-4 text-sm lg:text-md leading-6 lg:leading-6">
|
{PLANS.EARLY_ADOPTER.NAME}
|
||||||
Advanced features for small teaching businesses. Built for scale.
|
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-6 text-white flex items-baseline gap-x-1 text-4xl lg:text-4xl font-medium">
|
<p class="lg:text-md mt-4 text-sm font-light leading-6 text-white lg:leading-6">
|
||||||
${prices ? '12' : '15'} <sub class="text-base">/member</sub>
|
{PLANS.EARLY_ADOPTER.DESCRIPTION}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<p class="mt-6 flex items-baseline gap-x-1 text-4xl font-medium text-white lg:text-4xl">
|
||||||
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"
|
${isYearlyPlan ? PLANS.EARLY_ADOPTER.PRICE.YEARLY : PLANS.EARLY_ADOPTER.PRICE.MONTHLY}
|
||||||
data-cal-config={"{'layout':'month_view'}"}
|
<!-- <sub class="text-base">/mo</sub> -->
|
||||||
data-cal-link="classroomio/demo"
|
</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
|
{PLANS.EARLY_ADOPTER.CTA.LABEL}
|
||||||
</button>
|
</a>
|
||||||
<ul class="text-white mt-8 space-y-3 font-light lg:space-y-5 text-sm xl:mt-10">
|
<ul class="mt-8 space-y-3 text-sm font-light text-white lg:space-y-5 xl:mt-10">
|
||||||
{#each proFeatures as proFeature}
|
{#each PLANS.EARLY_ADOPTER.FEATURES as features}
|
||||||
<li class="flex items-center">
|
<li class="flex items-center">
|
||||||
<Checkmark size={24} fill="#fff" class="mr-2 lg:mr-3 scale-[70%] lg:scale-[100%]" />
|
<Checkmark size={24} fill="#fff" class="mr-2 scale-[70%] lg:mr-3 lg:scale-[100%]" />
|
||||||
{proFeature}
|
{features}
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- card 3 -->
|
<!-- Card 3 -->
|
||||||
<div class="ring-gray-200 rounded-3xl p-8 ring-1 xl:p-9 max-w-xl lg:max-w-sm">
|
<div class="max-w-xl rounded-3xl p-8 ring-1 ring-gray-200 lg:max-w-sm xl:p-9">
|
||||||
<p class="text-gray-900 text-lg lg:text-xl font-semibold leading-8">Team</p>
|
<p class="text-lg font-semibold leading-8 text-gray-900 lg:text-xl">
|
||||||
<p class="text-gray-500 font-light mt-4 text-sm lg:text-md leading-6 lg:leading-6">
|
{PLANS.ENTERPRISE.NAME}
|
||||||
Best suited for teams and larger businesses that need more control.
|
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-6 lg:mt-6 flex items-baseline gap-x-1 text-4xl lg:text-4xl font-medium">
|
<p class="lg:text-md mt-4 text-sm font-light leading-6 text-gray-500 lg:leading-6">
|
||||||
${prices ? '42' : '50'} <sub class="text-base">/member</sub>
|
{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>
|
</p>
|
||||||
<button
|
<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-config={"{'layout':'month_view'}"}
|
||||||
data-cal-link="classroomio/demo"
|
data-cal-link="classroomio/enterprise"
|
||||||
>
|
>
|
||||||
Get started
|
{PLANS.ENTERPRISE.CTA.LABEL}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<ul class="text-gray-600 mt-8 space-y-2 lg:space-y-5 text-sm xl:mt-10">
|
<ul class="mt-8 space-y-2 text-sm text-gray-600 lg:space-y-5 xl:mt-10">
|
||||||
{#each teamFeatures as teamFeature}
|
{#each PLANS.ENTERPRISE.FEATURES as features}
|
||||||
<li class="flex items-center">
|
<li class="flex items-center">
|
||||||
<Checkmark
|
<Checkmark
|
||||||
size={24}
|
size={24}
|
||||||
fill="#1D4EE2"
|
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>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -172,7 +155,7 @@
|
|||||||
class="m-0 lg:mt-1 text-md leading-7 lg:leading-6 text-gray-600"
|
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
|
For personal use. Hobby plan is free for up to 5 courses forever, no
|
||||||
credit card required.
|
credit Card required.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ import { sveltekit } from '@sveltejs/kit/vite';
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()]
|
plugins: [sveltekit()]
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ PUBLIC_TINYMCE_API_KEY=
|
|||||||
VITE_SENTRY_AUTH_TOKEN=
|
VITE_SENTRY_AUTH_TOKEN=
|
||||||
VITE_SENTRY_ORG_NAME=
|
VITE_SENTRY_ORG_NAME=
|
||||||
VITE_SENTRY_PROJECT_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 setup - Optional if you want to use OpenAI
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ yarn-error.log
|
|||||||
.svelte-kit/
|
.svelte-kit/
|
||||||
.env
|
.env
|
||||||
/dist/
|
/dist/
|
||||||
|
/cert/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|
||||||
# Local Netlify folder
|
# Local Netlify folder
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<a href="https://classroomio.com/">
|
<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>
|
<h1 align="center">ClassroomIO</h1>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
The Open Source Teaching Platform
|
The Open Source Teaching Platform for Bootcamps
|
||||||
<br />
|
<br />
|
||||||
<a href="https://classroomio.com">Website</a> | <a href="https://dub.sh/ciodiscord">Join Discord community</a>
|
<a href="https://classroomio.com">Website</a> | <a href="https://dub.sh/ciodiscord">Join Discord community</a>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"@aws-sdk/client-s3": "^3.386.0",
|
"@aws-sdk/client-s3": "^3.386.0",
|
||||||
"@carbon/charts-svelte": "^1.13.6",
|
"@carbon/charts-svelte": "^1.13.6",
|
||||||
"@defer/client": "^2.2.0",
|
"@defer/client": "^2.2.0",
|
||||||
|
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
|
||||||
"@sentry/browser": "^7.61.0",
|
"@sentry/browser": "^7.61.0",
|
||||||
"@sentry/integrations": "^7.61.0",
|
"@sentry/integrations": "^7.61.0",
|
||||||
"@sentry/vite-plugin": "^2.6.2",
|
"@sentry/vite-plugin": "^2.6.2",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"canvas-confetti": "^1.6.0",
|
"canvas-confetti": "^1.6.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"copy-to-clipboard": "^3.3.3",
|
"copy-to-clipboard": "^3.3.3",
|
||||||
|
"crypto": "^1.0.1",
|
||||||
"d3": "^7.8.5",
|
"d3": "^7.8.5",
|
||||||
"d3-cloud": "^1.2.7",
|
"d3-cloud": "^1.2.7",
|
||||||
"d3-sankey": "^0.12.3",
|
"d3-sankey": "^0.12.3",
|
||||||
@@ -49,7 +51,9 @@
|
|||||||
"openai-edge": "^1.2.2",
|
"openai-edge": "^1.2.2",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
"posthog-js": "^1.116.6",
|
"posthog-js": "^1.116.6",
|
||||||
|
"shared-constants": "workspace:*",
|
||||||
"sirv": "^2.0.3",
|
"sirv": "^2.0.3",
|
||||||
|
"stripe": "^14.22.0",
|
||||||
"svelte-email": "^0.0.4",
|
"svelte-email": "^0.0.4",
|
||||||
"svelte-ui": "workspace:*",
|
"svelte-ui": "workspace:*",
|
||||||
"unsplash-js": "^7.0.18",
|
"unsplash-js": "^7.0.18",
|
||||||
@@ -97,5 +101,8 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18.x"
|
"node": "18.x"
|
||||||
|
},
|
||||||
|
"browser": {
|
||||||
|
"crypto": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,12 @@
|
|||||||
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
|
<link rel="stylesheet" href="https://cdn.plyr.io/3.7.3/plyr.css" />
|
||||||
|
|
||||||
<title>ClassroomIO – Launch Your Online Bootcamp In Minutes</title>
|
<title>ClassroomIO – Launch Your Online Bootcamp In Minutes</title>
|
||||||
<meta name="description"
|
<meta name="description" content="ClassroomIO is the easiest place to launch and scale your online bootcamp." />
|
||||||
content="ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease." />
|
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<meta name="theme-color" content="#ffffff" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<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:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||||
<meta property="og:description"
|
<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:type" content="website" />
|
||||||
<meta property="og:image:type" content="image/png" />
|
<meta property="og:image:type" content="image/png" />
|
||||||
<meta property="og:image:width" content="1920" />
|
<meta property="og:image:width" content="1920" />
|
||||||
@@ -29,9 +28,7 @@
|
|||||||
<meta name="twitter:creator" content="@rotimi_best" />
|
<meta name="twitter:creator" content="@rotimi_best" />
|
||||||
<meta name="twitter:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
<meta name="twitter:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||||
<meta name="twitter:description"
|
<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." />
|
||||||
|
|
||||||
<script async type="text/javascript" src="https://static.senja.io/dist/platform.js"></script>
|
|
||||||
|
|
||||||
<!-- <link rel="stylesheet" href="global.css"> -->
|
<!-- <link rel="stylesheet" href="global.css"> -->
|
||||||
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
|
<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 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%
|
%sveltekit.head%
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|||||||
@@ -149,7 +149,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
{#each poll.options as option, index}
|
{#each poll.options as option, index}
|
||||||
<button
|
<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,
|
option,
|
||||||
currentUserId
|
currentUserId
|
||||||
) &&
|
) &&
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
{#each tabs as tab}
|
{#each tabs as tab}
|
||||||
<button
|
<button
|
||||||
class="mr-3 pb-2 border-t-0 border-l-0 border-r-0 {selectedTab === tab.value
|
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'}"
|
: 'border-b-0'}"
|
||||||
on:click={() => (selectedTab = tab.value)}
|
on:click={() => (selectedTab = tab.value)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@
|
|||||||
|
|
||||||
function getAppClass(appName: string, selected: string | undefined) {
|
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 ${
|
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>
|
</script>
|
||||||
|
|
||||||
<div
|
<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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import posthog from 'posthog-js';
|
import { logout } from '$lib/utils/functions/logout';
|
||||||
import { goto } from '$app/navigation';
|
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
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>
|
</script>
|
||||||
|
|
||||||
<PrimaryButton variant={VARIANTS.OUTLINED} label="Log out" onClick={logout} />
|
<PrimaryButton variant={VARIANTS.OUTLINED} label="Log out" onClick={logout} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
||||||
import { currentOrgDomain } from '$lib/utils/store/org';
|
import { currentOrgDomain } from '$lib/utils/store/org';
|
||||||
import { isMobile } from '$lib/utils/store/useMobile';
|
import { isMobile } from '$lib/utils/store/useMobile';
|
||||||
@@ -8,7 +9,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a href={$currentOrgDomain} target="_blank" class="{className} ml-2 hover:no-underline">
|
<a href={$currentOrgDomain} target="_blank" class="{className} ml-2 hover:no-underline">
|
||||||
<PrimaryButton>
|
<PrimaryButton variant={VARIANTS.CONTAINED_DARK}>
|
||||||
{#if !$isMobile}
|
{#if !$isMobile}
|
||||||
View site
|
View site
|
||||||
{/if}
|
{/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 { updateCourse } from '$lib/utils/services/courses';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
|
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||||
import Professional from './certificates/Professional.svelte';
|
import Professional from './certificates/Professional.svelte';
|
||||||
import Plain from './certificates/Plain.svelte';
|
import Plain from './certificates/Plain.svelte';
|
||||||
import { course } from '$lib/components/Course/store';
|
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 { globalStore } from '$lib/utils/store/app';
|
||||||
|
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||||
|
|
||||||
const studentNamePlaceholder = 'Name of student';
|
const studentNamePlaceholder = 'Name of student';
|
||||||
const themes = ['professional', 'plain'];
|
const themes = ['professional', 'plain'];
|
||||||
@@ -44,14 +46,20 @@
|
|||||||
/>
|
/>
|
||||||
</svelte:head>
|
</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">
|
<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">
|
<section class="w-full lg:w-2/5 h-full">
|
||||||
<strong class="my-2 text-base font-semibold text-black dark:text-gray-100"
|
<strong class="my-2 text-base font-semibold text-black dark:text-gray-100"
|
||||||
>Certificate settings</strong
|
>Certificate settings</strong
|
||||||
>
|
>
|
||||||
<p class="text-xs font-normal my-4 dark:text-gray-100">Choose a theme</p>
|
<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}
|
{#each themes as theme}
|
||||||
<div class="flex mr-3">
|
<div class="flex mr-3">
|
||||||
<RadioButton value={theme} />
|
<RadioButton value={theme} />
|
||||||
@@ -71,11 +79,12 @@
|
|||||||
>Settings > Organisation settings</strong
|
>Settings > Organisation settings</strong
|
||||||
> and upload your brand logo
|
> and upload your brand logo
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Go to Settings"
|
label="Go to Settings"
|
||||||
variant={VARIANTS.OUTLINED}
|
variant={VARIANTS.OUTLINED}
|
||||||
className="rounded-md mt-3"
|
className="rounded-md mt-3"
|
||||||
onClick={() => goto(`org/${$currentOrg.siteName}/settings`)}
|
onClick={() => goto(`/org/${$currentOrg.siteName}/settings`)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span class="my-4">
|
<span class="my-4">
|
||||||
@@ -86,6 +95,7 @@
|
|||||||
bgColor="bg-gray-100"
|
bgColor="bg-gray-100"
|
||||||
bind:value={$course.description}
|
bind:value={$course.description}
|
||||||
errorMessage={errors.description}
|
errorMessage={errors.description}
|
||||||
|
disabled={$isFreePlan}
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
<Toggle
|
<Toggle
|
||||||
@@ -93,6 +103,7 @@
|
|||||||
bind:toggled={$course.is_certificate_downloadable}
|
bind:toggled={$course.is_certificate_downloadable}
|
||||||
class="my-4"
|
class="my-4"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
disabled={$isFreePlan}
|
||||||
>
|
>
|
||||||
<span slot="labelA" style={$globalStore.isDark ? 'color: white' : 'color: #161616'}
|
<span slot="labelA" style={$globalStore.isDark ? 'color: white' : 'color: #161616'}
|
||||||
>Locked</span
|
>Locked</span
|
||||||
@@ -115,11 +126,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="h-1/5">
|
<div class="h-1/5">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Save Changes"
|
className="rounded-md flex gap-2 items-center"
|
||||||
className="rounded-md"
|
variant={VARIANTS.CONTAINED_DARK}
|
||||||
onClick={saveCertificate}
|
onClick={saveCertificate}
|
||||||
isLoading={isSaving}
|
isLoading={isSaving}
|
||||||
/>
|
isDisabled={$isFreePlan}
|
||||||
|
>
|
||||||
|
{#if $isFreePlan}
|
||||||
|
<FlashFilled size={16} class="text-blue-700" />
|
||||||
|
{/if}
|
||||||
|
Save Changes
|
||||||
|
</PrimaryButton>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
|
|
||||||
<section class="flex flex-wrap items-start gap-2 mb-10 -ml-4 -mr-4">
|
<section class="flex flex-wrap items-start gap-2 mb-10 -ml-4 -mr-4">
|
||||||
{#each CertificateReportData as data (data.id)}
|
{#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>
|
<p class="text-xs font-normal">{data.title}</p>
|
||||||
<bold class="text-2xl font-medium">{data.value}</bold>
|
<bold class="text-2xl font-medium">{data.value}</bold>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
modalHeading="Add New Lesson"
|
modalHeading="Add New Lesson"
|
||||||
>
|
>
|
||||||
<div
|
<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">
|
<div class="mr-5">
|
||||||
<TextChip
|
<TextChip
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
class="w-[261px] h-[240px] p-5 rounded-md dark:bg-neutral-700 border-2 {option.type ===
|
class="w-[261px] h-[240px] p-5 rounded-md dark:bg-neutral-700 border-2 {option.type ===
|
||||||
type
|
type
|
||||||
? 'border-primary-400'
|
? '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'
|
!option.isDisabled && 'hover:scale-95'
|
||||||
}`} flex flex-col {option.isDisabled &&
|
}`} flex flex-col {option.isDisabled &&
|
||||||
'cursor-not-allowed opacity-60'} transition-all ease-in-out"
|
'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 ===
|
class="w-[161px] h-[140px] hover:scale-95 p-5 rounded-md dark:bg-neutral-700 border-2 {template.id ===
|
||||||
selectedTemplateId
|
selectedTemplateId
|
||||||
? 'border-primary-400'
|
? '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"
|
type="button"
|
||||||
on:click={() => (selectedTemplateId = template.id)}
|
on:click={() => (selectedTemplateId = template.id)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import LocalVideo from './LocalVideo.svelte';
|
import LocalVideo from './LocalVideo.svelte';
|
||||||
import EmbedLink from './EmbedLink.svelte';
|
import EmbedLink from './EmbedLink.svelte';
|
||||||
import * as CONSTANTS from './constants';
|
import * as CONSTANTS from './constants';
|
||||||
|
import { isFreePlan } from '$lib/utils/store/org';
|
||||||
|
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||||
|
|
||||||
export let lessonId = '';
|
export let lessonId = '';
|
||||||
|
|
||||||
@@ -23,14 +25,18 @@
|
|||||||
class={`w-full px-4 py-3 my-1 border ${
|
class={`w-full px-4 py-3 my-1 border ${
|
||||||
currentTab === item.value
|
currentTab === item.value
|
||||||
? 'border border-[#0233BD] bg-[#F5F8FE] dark:text-black'
|
? '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`}
|
} rounded-md cursor-pointer flex flex-row items-center justify-start gap-2 whitespace-nowrap`}
|
||||||
>
|
>
|
||||||
<svelte:component
|
{#if $isFreePlan && item.value === 3}
|
||||||
this={item.icon}
|
<FlashFilled size={20} class="text-blue-700" />
|
||||||
size={20}
|
{:else}
|
||||||
color={`${currentTab === item.value ? 'dark:invert-0' : 'dark:invert'}`}
|
<svelte:component
|
||||||
/>
|
this={item.icon}
|
||||||
|
size={20}
|
||||||
|
color={`${currentTab === item.value ? 'dark:invert-0' : 'dark:invert'}`}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
<p>{item.title}</p>
|
<p>{item.title}</p>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { PUBLIC_SERVER_URL } from '$env/static/public';
|
import { PUBLIC_SERVER_URL } from '$env/static/public';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
@@ -10,11 +10,14 @@
|
|||||||
import { onMount, onDestroy } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { ProgressBar } from 'carbon-components-svelte';
|
import { ProgressBar } from 'carbon-components-svelte';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
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 value = 0;
|
||||||
let max = 100;
|
let max = 100;
|
||||||
let status = 'active';
|
let status = 'active';
|
||||||
let fileSize;
|
let fileSize;
|
||||||
|
let isDisabled = false;
|
||||||
|
|
||||||
export let lessonId = '';
|
export let lessonId = '';
|
||||||
|
|
||||||
@@ -61,7 +64,7 @@
|
|||||||
console.log('Upload res', formRes);
|
console.log('Upload res', formRes);
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
isLoaded = false;
|
isLoaded = false;
|
||||||
} catch (err) {
|
} catch (err: any) {
|
||||||
console.error('Error uploading video', err, '\n\n', err.response);
|
console.error('Error uploading video', err, '\n\n', err.response);
|
||||||
if (err.response) {
|
if (err.response) {
|
||||||
formRes = err.response.data;
|
formRes = err.response.data;
|
||||||
@@ -115,14 +118,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: isDoneUploading(formRes);
|
$: isDoneUploading(formRes);
|
||||||
|
$: isDisabled = isLoading || !PUBLIC_SERVER_URL || $isFreePlan;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<UpgradeBanner className="mb-3" onClick={() => ($uploadCourseVideoStore.isModalOpen = false)}>
|
||||||
|
Upgrade your plan to upload vidoes
|
||||||
|
</UpgradeBanner>
|
||||||
|
|
||||||
{#if !isLoaded}
|
{#if !isLoaded}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
on:click={() => (fileInput && !isLoading ? fileInput.click() : null)}
|
on:click={() => (fileInput && !isLoading ? fileInput.click() : null)}
|
||||||
class="h-full w-full"
|
class="h-full w-full {isDisabled && 'hover:cursor-not-allowed opacity-50'}"
|
||||||
disabled={isLoading || !PUBLIC_SERVER_URL}
|
disabled={isDisabled}
|
||||||
>
|
>
|
||||||
<form
|
<form
|
||||||
class="border-primary-300 flex h-full w-full flex-col items-center justify-center rounded-xl border border-dashed"
|
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 { SkeletonText } from 'carbon-components-svelte';
|
||||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||||
|
import FlashFilled from 'carbon-icons-svelte/lib/FlashFilled.svelte';
|
||||||
import { handleAddLessonWidget } from './store';
|
import { handleAddLessonWidget } from './store';
|
||||||
import NavIcons from './NavIcons.svelte';
|
import NavIcons from './NavIcons.svelte';
|
||||||
|
import { isFreePlan } from '$lib/utils/store/org';
|
||||||
|
|
||||||
export let handleClick = () => {};
|
export let handleClick = () => {};
|
||||||
export let label = '';
|
export let label = '';
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
export let isLoading = true;
|
export let isLoading = true;
|
||||||
export let isLesson = false;
|
export let isLesson = false;
|
||||||
export let isStudent = true;
|
export let isStudent = true;
|
||||||
|
export let isPaidFeature = false;
|
||||||
// export let subMenuItems = [];
|
// export let subMenuItems = [];
|
||||||
|
|
||||||
function addLesson() {
|
function addLesson() {
|
||||||
@@ -54,6 +57,9 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<span class="grow" />
|
<span class="grow" />
|
||||||
|
|
||||||
|
{#if isPaidFeature && $isFreePlan}
|
||||||
|
<FlashFilled size={20} class="text-blue-700" />
|
||||||
|
{/if}
|
||||||
{#if isLesson && !isLoading}
|
{#if isLesson && !isLoading}
|
||||||
{#if !isStudent}
|
{#if !isStudent}
|
||||||
<IconButton onClick={() => addLesson()} size="small">
|
<IconButton onClick={() => addLesson()} size="small">
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { sideBar } from '$lib/components/Org/store';
|
import { sideBar } from '$lib/components/Org/store';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
import { getIsLessonComplete } from '../Lesson/functions';
|
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 path: string;
|
||||||
export let isStudent: boolean = false;
|
export let isStudent: boolean = false;
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
to: string;
|
to: string;
|
||||||
hideSortIcon: boolean;
|
hideSortIcon: boolean;
|
||||||
isLesson?: boolean;
|
isLesson?: boolean;
|
||||||
|
isPaidFeature: boolean;
|
||||||
show?: () => boolean;
|
show?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,23 +149,27 @@
|
|||||||
{
|
{
|
||||||
label: 'News Feed',
|
label: 'News Feed',
|
||||||
to: getNavItemRoute($course.id),
|
to: getNavItemRoute($course.id),
|
||||||
hideSortIcon: true
|
hideSortIcon: true,
|
||||||
|
isPaidFeature: false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Lessons',
|
label: 'Lessons',
|
||||||
to: getLessonsRoute($course.id),
|
to: getLessonsRoute($course.id),
|
||||||
hideSortIcon: false,
|
hideSortIcon: false,
|
||||||
|
isPaidFeature: false,
|
||||||
isLesson: true
|
isLesson: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Attendance',
|
label: 'Attendance',
|
||||||
to: getNavItemRoute($course.id, 'attendance'),
|
to: getNavItemRoute($course.id, 'attendance'),
|
||||||
|
isPaidFeature: false,
|
||||||
hideSortIcon: true
|
hideSortIcon: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Submissions',
|
label: 'Submissions',
|
||||||
to: getNavItemRoute($course.id, 'submissions'),
|
to: getNavItemRoute($course.id, 'submissions'),
|
||||||
hideSortIcon: true,
|
hideSortIcon: true,
|
||||||
|
isPaidFeature: false,
|
||||||
show() {
|
show() {
|
||||||
return !isStudent;
|
return !isStudent;
|
||||||
}
|
}
|
||||||
@@ -172,27 +177,33 @@
|
|||||||
{
|
{
|
||||||
label: 'Marks',
|
label: 'Marks',
|
||||||
to: getNavItemRoute($course.id, 'marks'),
|
to: getNavItemRoute($course.id, 'marks'),
|
||||||
|
isPaidFeature: false,
|
||||||
hideSortIcon: true
|
hideSortIcon: true
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// label: 'Scoreboard',
|
|
||||||
// to: getNavItemRoute($course.id, 'scoreboard'),
|
|
||||||
// hideSortIcon: true,
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
label: 'People',
|
label: 'People',
|
||||||
to: getNavItemRoute($course.id, 'people'),
|
to: getNavItemRoute($course.id, 'people'),
|
||||||
|
isPaidFeature: false,
|
||||||
hideSortIcon: true
|
hideSortIcon: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Certificates',
|
label: 'Certificates',
|
||||||
to: getNavItemRoute($course.id, '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',
|
label: 'Landing Page',
|
||||||
to: getNavItemRoute($course.id, 'landingpage'),
|
to: getNavItemRoute($course.id, 'landingpage'),
|
||||||
hideSortIcon: true,
|
hideSortIcon: true,
|
||||||
|
isPaidFeature: false,
|
||||||
show() {
|
show() {
|
||||||
return !isStudent;
|
return !isStudent;
|
||||||
}
|
}
|
||||||
@@ -201,6 +212,7 @@
|
|||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
to: getNavItemRoute($course.id, 'settings'),
|
to: getNavItemRoute($course.id, 'settings'),
|
||||||
hideSortIcon: true,
|
hideSortIcon: true,
|
||||||
|
isPaidFeature: false,
|
||||||
show() {
|
show() {
|
||||||
return !isStudent;
|
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)]
|
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`}
|
} overflow-y-auto border border-l-0 border-t-0 border-b-0 border-r-1`}
|
||||||
style={$sideBar.hidden === true ? 'width:0' : 'width:300px'}
|
style={$sideBar.hidden === true ? 'width:0' : 'width:300px'}
|
||||||
bind:this={sidebarRef}
|
bind:this={sidebarRef}
|
||||||
>
|
>
|
||||||
<div class="sidebar-contenth-full flex flex-col">
|
<div class="flex flex-col">
|
||||||
<ul
|
<ul
|
||||||
class="sidebar-content my-5"
|
class="sidebar-content my-5"
|
||||||
bind:this={menuContentRef}
|
bind:this={menuContentRef}
|
||||||
@@ -240,6 +252,7 @@
|
|||||||
total={navItem.isLesson ? ($lessons || []).length : 0}
|
total={navItem.isLesson ? ($lessons || []).length : 0}
|
||||||
isLoading={!$course.id}
|
isLoading={!$course.id}
|
||||||
isLesson={navItem.isLesson}
|
isLesson={navItem.isLesson}
|
||||||
|
isPaidFeature={navItem.isPaidFeature}
|
||||||
{isStudent}
|
{isStudent}
|
||||||
>
|
>
|
||||||
{#if navItem.isLesson}
|
{#if navItem.isLesson}
|
||||||
|
|||||||
@@ -100,7 +100,7 @@
|
|||||||
id={feed.id}
|
id={feed.id}
|
||||||
class="flex flex-col gap-5 {isActive
|
class="flex flex-col gap-5 {isActive
|
||||||
? 'border-2 border-primary-700'
|
? '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>
|
<section>
|
||||||
<div class="p-3 pb-0">
|
<div class="p-3 pb-0">
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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}
|
{#if feed.comment.length > 0}
|
||||||
<button
|
<button
|
||||||
on:click={expandComment}
|
on:click={expandComment}
|
||||||
@@ -222,7 +222,7 @@
|
|||||||
bind:value={comment}
|
bind:value={comment}
|
||||||
on:keydown={handleAddNewComment}
|
on:keydown={handleAddNewComment}
|
||||||
placeholder="Add class comment"
|
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,13 +14,14 @@
|
|||||||
import { course } from '$lib/components/Course/store';
|
import { course } from '$lib/components/Course/store';
|
||||||
import type { Course } from '$lib/utils/types';
|
import type { Course } from '$lib/utils/types';
|
||||||
import { updateCourse, deleteCourse } from '$lib/utils/services/courses';
|
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 { isObject } from '$lib/utils/functions/isObject';
|
||||||
import { lessons } from '../Lesson/store/lessons';
|
import { lessons } from '../Lesson/store/lessons';
|
||||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||||
import { snackbar } from '$lib/components/Snackbar/store';
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||||
|
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||||
|
|
||||||
import { Restart } from 'carbon-icons-svelte';
|
import { Restart } from 'carbon-icons-svelte';
|
||||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||||
@@ -186,7 +187,7 @@
|
|||||||
$: setDefault($course);
|
$: setDefault($course);
|
||||||
</script>
|
</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">
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
<Column sm={8} md={8} lg={8}>
|
<Column sm={8} md={8} lg={8}>
|
||||||
<SectionTitle>Cover Image</SectionTitle>
|
<SectionTitle>Cover Image</SectionTitle>
|
||||||
@@ -287,10 +288,14 @@
|
|||||||
<p>Make the Lesson available for Download in pdf for registered students</p>
|
<p>Make the Lesson available for Download in pdf for registered students</p>
|
||||||
</Column>
|
</Column>
|
||||||
<Column sm={8} md={8} lg={8}>
|
<Column sm={8} md={8} lg={8}>
|
||||||
<Toggle size="sm" bind:toggled={$settings.lesson_download}>
|
{#if $isFreePlan}
|
||||||
<span slot="labelA" style="color: gray">Disabled</span>
|
<UpgradeBanner>Upgrade to download lessons</UpgradeBanner>
|
||||||
<span slot="labelB" style="color: gray">Enabled</span>
|
{:else}
|
||||||
</Toggle>
|
<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>
|
</Column>
|
||||||
</Row>
|
</Row>
|
||||||
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
<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>
|
<p>Make the Course available for Download for registered students</p>
|
||||||
</Column>
|
</Column>
|
||||||
<Column sm={8} md={8} lg={8}>
|
<Column sm={8} md={8} lg={8}>
|
||||||
<PrimaryButton
|
{#if $isFreePlan}
|
||||||
variant={VARIANTS.OUTLINED}
|
<UpgradeBanner>Upgrade to download course</UpgradeBanner>
|
||||||
label="Download"
|
{:else}
|
||||||
onClick={downloadCourse}
|
<PrimaryButton
|
||||||
isDisabled={isLoading || !PUBLIC_SERVER_URL}
|
variant={VARIANTS.OUTLINED}
|
||||||
{isLoading}
|
label="Download"
|
||||||
/>
|
onClick={downloadCourse}
|
||||||
|
isDisabled={isLoading || !PUBLIC_SERVER_URL}
|
||||||
|
{isLoading}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
</Column>
|
</Column>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
<script lang="ts">
|
<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 getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import CopyFile from "carbon-icons-svelte/lib/CopyFile.svelte";
|
import CopyFile from 'carbon-icons-svelte/lib/CopyFile.svelte';
|
||||||
import Share from "carbon-icons-svelte/lib/Share.svelte";
|
import Share from 'carbon-icons-svelte/lib/Share.svelte';
|
||||||
import UserFollow from "carbon-icons-svelte/lib/UserFollow.svelte";
|
import UserFollow from 'carbon-icons-svelte/lib/UserFollow.svelte';
|
||||||
|
|
||||||
export let bannerImage: string | undefined;
|
export let bannerImage: string | undefined;
|
||||||
export let id = '';
|
export let id = '';
|
||||||
@@ -52,7 +59,7 @@
|
|||||||
<ContextMenuOption indented labelText="Share" icon={Share} on:click={handleShareCourse} />
|
<ContextMenuOption indented labelText="Share" icon={Share} on:click={handleShareCourse} />
|
||||||
<ContextMenuOption indented labelText="Invite" icon={UserFollow} on:click={handleInvite} />
|
<ContextMenuOption indented labelText="Invite" icon={UserFollow} on:click={handleInvite} />
|
||||||
<ContextMenuDivider />
|
<ContextMenuDivider />
|
||||||
<ContextMenuOption kind="danger" labelText="Delete" on:click={handleDeleteCourse} />
|
<ContextMenuOption kind="danger" labelText="Delete" on:click={handleDeleteCourse} />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -60,14 +67,14 @@
|
|||||||
rel="prefetch"
|
rel="prefetch"
|
||||||
bind:this={target}
|
bind:this={target}
|
||||||
href={isOnLandingPage ? `/course/${slug}` : `/courses/${id}`}
|
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="p-4">
|
||||||
<div class=" mb-5">
|
<div class=" mb-5">
|
||||||
<ImageLoader
|
<ImageLoader
|
||||||
src={bannerImage}
|
src={bannerImage}
|
||||||
alt="Course Logo"
|
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">
|
<svelte:fragment slot="loading">
|
||||||
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
|
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
|
||||||
@@ -83,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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'}"
|
'items-center'}"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
<span
|
<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
|
'hover:cursor-not-allowed opacity-50'} dark:text-black p-3 mt-1 block w-full {bgColor} {errorMessage
|
||||||
? 'border-red-600'
|
? 'border-red-600'
|
||||||
: ''}"
|
: ''}"
|
||||||
|
|||||||
@@ -59,7 +59,7 @@
|
|||||||
bind:value
|
bind:value
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
required={isRequired}
|
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'}"
|
{bgColor} {!!errorMessage ? 'border-red-500' : 'border-gray-300'}"
|
||||||
on:change={onChange}
|
on:change={onChange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<input
|
<input
|
||||||
use:typeAction
|
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
|
'hover:cursor-not-allowed opacity-50'} dark:text-black p-3 mt-1 block w-full {bgColor} {errorMessage
|
||||||
? 'border-red-600'
|
? 'border-red-600'
|
||||||
: ''}"
|
: ''}"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<svg
|
<svg
|
||||||
width="24"
|
width="18"
|
||||||
height="24"
|
height="18"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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>
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="18"
|
||||||
height="24"
|
height="18"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
width="18"
|
||||||
height="24"
|
height="18"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<svg
|
<svg
|
||||||
width="24"
|
width="18"
|
||||||
height="24"
|
height="18"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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
|
<svg
|
||||||
width="24"
|
width="18"
|
||||||
height="24"
|
height="18"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
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)]`}
|
} 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="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">
|
<div class="w-full flex flex-col items-center">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={$profile.avatar_url}
|
src={$profile.avatar_url}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
import { Grid, Row, Column } from 'carbon-components-svelte';
|
||||||
import TextField from '$lib/components/Form/TextField.svelte';
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.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 UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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 ">
|
<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">
|
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
|
||||||
<p class="dark:text-white font-bold">Profile Picture</p>
|
<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">
|
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Update Profile"
|
label="Update Profile"
|
||||||
|
variant={VARIANTS.CONTAINED_DARK}
|
||||||
className="mr-5"
|
className="mr-5"
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
isDisabled={loading}
|
isDisabled={loading}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
{#if open}
|
{#if open}
|
||||||
<div
|
<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}
|
on:click={onClose}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
@@ -32,12 +32,14 @@
|
|||||||
<div
|
<div
|
||||||
class="{maxWidth || 'container'} {size === 'sm'
|
class="{maxWidth || 'container'} {size === 'sm'
|
||||||
? 'small'
|
? '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()}
|
on:click={(e) => e.stopPropagation()}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div
|
||||||
<p class="dark:text-white text-md font-medium m-0">
|
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}
|
{modalHeading}
|
||||||
</p>
|
</p>
|
||||||
{#if isCloseable}
|
{#if isCloseable}
|
||||||
@@ -47,7 +49,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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 />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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">
|
<div class="w-10 flex justify-evenly flex-col px-1 mr-2 rounded-md">
|
||||||
<SkeletonPlaceholder style="width: 100%; height: 40%; border-radius: 4px;" />
|
<SkeletonPlaceholder style="width: 100%; height: 40%; border-radius: 4px;" />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<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%;" />
|
<SkeletonPlaceholder style="width: 20%; height: 100%;" />
|
||||||
<div class="w-full mx-5 flex justify-evenly flex-col">
|
<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);
|
$: setSiteName($currentOrg.siteName);
|
||||||
</script>
|
</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">
|
<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={2} lg={4} class="text-lg"><SectionTitle>Add</SectionTitle></Column>
|
||||||
<Column sm={2} md={6} lg={8}>
|
<Column sm={2} md={6} lg={8}>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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">
|
<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">
|
<Column sm={4} md={4} lg={4} class="flex items-center justify-center">
|
||||||
<img src="/telegram-svg.svg" alt="" class="w-10 mr-2" />
|
<img src="/telegram-svg.svg" alt="" class="w-10 mr-2" />
|
||||||
|
|||||||
@@ -117,7 +117,7 @@
|
|||||||
$: setDefault($currentOrg?.landingpage as OrgLandingPageJson);
|
$: setDefault($currentOrg?.landingpage as OrgLandingPageJson);
|
||||||
</script>
|
</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">
|
<Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||||
<Column sm={4} md={4} lg={4}>
|
<Column sm={4} md={4} lg={4}>
|
||||||
<SectionTitle>Landing Page</SectionTitle>
|
<SectionTitle>Landing Page</SectionTitle>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { Grid, Row, Column } from 'carbon-components-svelte';
|
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 TextField from '$lib/components/Form/TextField.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
@@ -9,6 +10,7 @@
|
|||||||
import { snackbar } from '$lib/components/Snackbar/store';
|
import { snackbar } from '$lib/components/Snackbar/store';
|
||||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||||
import SectionTitle from '../SectionTitle.svelte';
|
import SectionTitle from '../SectionTitle.svelte';
|
||||||
|
import { isFreePlan } from '$lib/utils/store/org';
|
||||||
|
|
||||||
let avatar;
|
let avatar;
|
||||||
let loading = false;
|
let loading = false;
|
||||||
@@ -94,7 +96,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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">
|
<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={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">
|
<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>
|
</p>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Edit domain"
|
label="Edit domain"
|
||||||
className="my-7 py-5 px-10 text-primary-700"
|
className="my-7 py-5 px-10"
|
||||||
variant={VARIANTS.OUTLINED}
|
variant={VARIANTS.OUTLINED}
|
||||||
onClick={() => gotoSetting('/domains')}
|
onClick={() => gotoSetting('/domains')}
|
||||||
/>
|
/>
|
||||||
@@ -189,11 +191,15 @@
|
|||||||
Add team mates to your organization so you can both collaborate on projects.
|
Add team mates to your organization so you can both collaborate on projects.
|
||||||
</p>
|
</p>
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Manage Team"
|
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||||
className="my-7 py-5 px-10 text-primary-700"
|
|
||||||
variant={VARIANTS.OUTLINED}
|
variant={VARIANTS.OUTLINED}
|
||||||
onClick={() => gotoSetting('/teams')}
|
onClick={() => gotoSetting('/teams')}
|
||||||
/>
|
>
|
||||||
|
{#if $isFreePlan}
|
||||||
|
<FlashFilled size={16} class="text-blue-700" />
|
||||||
|
{/if}
|
||||||
|
Manage Team
|
||||||
|
</PrimaryButton>
|
||||||
</Column>
|
</Column>
|
||||||
</Row>
|
</Row>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import TextField from '$lib/components/Form/TextField.svelte';
|
import TextField from '$lib/components/Form/TextField.svelte';
|
||||||
import SectionTitle from '../SectionTitle.svelte';
|
import SectionTitle from '../SectionTitle.svelte';
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.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 UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
@@ -61,7 +62,7 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</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 ">
|
<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">
|
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
|
||||||
<SectionTitle>Profile Picture</SectionTitle>
|
<SectionTitle>Profile Picture</SectionTitle>
|
||||||
@@ -83,6 +84,7 @@
|
|||||||
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
|
<Row class="m-5 w-full flex items-center gap-2 lg:justify-center">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Update Profile"
|
label="Update Profile"
|
||||||
|
variant={VARIANTS.CONTAINED_DARK}
|
||||||
className="mr-5"
|
className="mr-5"
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
isDisabled={loading}
|
isDisabled={loading}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
triggerSendEmail,
|
triggerSendEmail,
|
||||||
NOTIFICATION_NAME
|
NOTIFICATION_NAME
|
||||||
} from '$lib/utils/services/notification/notification';
|
} from '$lib/utils/services/notification/notification';
|
||||||
|
import { isFreePlan } from '$lib/utils/store/org';
|
||||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||||
|
|
||||||
let emailsStr = '';
|
let emailsStr = '';
|
||||||
@@ -132,7 +133,7 @@
|
|||||||
$: fetchTeam($currentOrg.id);
|
$: fetchTeam($currentOrg.id);
|
||||||
</script>
|
</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">
|
<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={2} lg={4} class="text-lg"><SectionTitle>Add</SectionTitle></Column>
|
||||||
<Column sm={2} md={6} lg={8}>
|
<Column sm={2} md={6} lg={8}>
|
||||||
@@ -146,10 +147,11 @@
|
|||||||
placeholder="email comma separated"
|
placeholder="email comma separated"
|
||||||
bind:value={emailsStr}
|
bind:value={emailsStr}
|
||||||
className="mb-3"
|
className="mb-3"
|
||||||
|
isDisabled={$isFreePlan}
|
||||||
{errorMessage}
|
{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.ADMIN} text={ROLE_LABEL[ROLE.ADMIN]} /> -->
|
||||||
<SelectItem value={ROLE.TUTOR} text={ROLE_LABEL[ROLE.TUTOR]} />
|
<SelectItem value={ROLE.TUTOR} text={ROLE_LABEL[ROLE.TUTOR]} />
|
||||||
</Select>
|
</Select>
|
||||||
@@ -158,7 +160,7 @@
|
|||||||
label="Send Invite"
|
label="Send Invite"
|
||||||
onClick={onSendInvite}
|
onClick={onSendInvite}
|
||||||
{isLoading}
|
{isLoading}
|
||||||
isDisabled={isLoading}
|
isDisabled={isLoading || $isFreePlan}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Column>
|
</Column>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import LandingpageSettings from './LandingpageSettings.svelte';
|
import LandingpageSettings from './LandingpageSettings.svelte';
|
||||||
// import Account from './Account.svelte';
|
// import Account from './Account.svelte';
|
||||||
import OrgSettings from './OrgSettings.svelte';
|
import OrgSettings from './OrgSettings.svelte';
|
||||||
|
import Billing from './Billing.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
@@ -64,6 +65,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 3,
|
key: 3,
|
||||||
|
label: 'Billing',
|
||||||
|
tabKey: 'billing',
|
||||||
|
href: `${$page.url.pathname}?tab=billing`,
|
||||||
|
disabled: !$isOrgAdmin
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 4,
|
||||||
label: 'Integrations',
|
label: 'Integrations',
|
||||||
tabKey: 'integrations',
|
tabKey: 'integrations',
|
||||||
href: `${$page.url.pathname}?tab=integrations`,
|
href: `${$page.url.pathname}?tab=integrations`,
|
||||||
@@ -91,6 +99,9 @@
|
|||||||
<TabContent class="w-full p-0">
|
<TabContent class="w-full p-0">
|
||||||
<LandingpageSettings />
|
<LandingpageSettings />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
<TabContent class="w-full p-0">
|
||||||
|
<Billing />
|
||||||
|
</TabContent>
|
||||||
<TabContent class="w-full p-0">
|
<TabContent class="w-full p-0">
|
||||||
<Integrations />
|
<Integrations />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import HelpIcon from 'carbon-icons-svelte/lib/Help.svelte';
|
import HelpIcon from 'carbon-icons-svelte/lib/Help.svelte';
|
||||||
import ForumIcon from 'carbon-icons-svelte/lib/Forum.svelte';
|
import ForumIcon from 'carbon-icons-svelte/lib/Forum.svelte';
|
||||||
|
import { SettingsAdjust } from 'carbon-icons-svelte';
|
||||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import OrgSelector from '$lib/components/OrgSelector/OrgSelector.svelte';
|
import OrgSelector from '$lib/components/OrgSelector/OrgSelector.svelte';
|
||||||
import HomeIcon from '$lib/components/Icons/HomeIcon.svelte';
|
import HomeIcon from '$lib/components/Icons/HomeIcon.svelte';
|
||||||
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
|
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
|
||||||
@@ -11,37 +11,18 @@
|
|||||||
import SiteSettingsIcon from '$lib/components/Icons/SiteSettingsIcon.svelte';
|
import SiteSettingsIcon from '$lib/components/Icons/SiteSettingsIcon.svelte';
|
||||||
import AudienceIcon from '$lib/components/Icons/AudienceIcon.svelte';
|
import AudienceIcon from '$lib/components/Icons/AudienceIcon.svelte';
|
||||||
import Avatar from '$lib/components/Avatar/index.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 { profile } from '$lib/utils/store/user';
|
||||||
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
import { NavClasses } from '$lib/utils/constants/reusableClass';
|
||||||
import { sideBar } from './store';
|
import { sideBar } from './store';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
const menuItems = [
|
let menuItems: {
|
||||||
{
|
path: string;
|
||||||
path: '',
|
label: string;
|
||||||
label: 'Dashboard'
|
show: boolean;
|
||||||
},
|
}[] = [];
|
||||||
// {
|
|
||||||
// path: '/quiz',
|
|
||||||
// label: 'Quizzes'
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: '/courses',
|
|
||||||
label: 'Courses'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/community',
|
|
||||||
label: 'Community'
|
|
||||||
},
|
|
||||||
// {
|
|
||||||
// path: '/site',
|
|
||||||
// label: 'Site settings',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
path: '/audience',
|
|
||||||
label: 'Audience'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function isActive(pagePath: string, itemPath: string) {
|
function isActive(pagePath: string, itemPath: string) {
|
||||||
const pageLinkItems = pagePath.split('/');
|
const pageLinkItems = pagePath.split('/');
|
||||||
@@ -56,87 +37,137 @@
|
|||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
$sideBar.hidden = !$sideBar.hidden;
|
$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>
|
</script>
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
class={`${
|
class={`${
|
||||||
$sideBar.hidden
|
$sideBar.hidden
|
||||||
? '-translate-x-[100%] absolute md:translate-x-0 md:relative z-40'
|
? 'absolute z-40 -translate-x-[100%] md:relative md:translate-x-0'
|
||||||
: 'translate-x-0 absolute md:relative z-40'
|
: 'absolute z-40 translate-x-0 md:relative'
|
||||||
} 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`}
|
} 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="flex h-full flex-col">
|
||||||
<div class="border-b border-gray-200 pt-5 px-4">
|
<div class="">
|
||||||
{#if $currentOrg.avatar_url && $currentOrg.name}
|
|
||||||
<Avatar
|
|
||||||
src={$currentOrg.avatar_url}
|
|
||||||
name={$currentOrg.name}
|
|
||||||
shape="rounded-md"
|
|
||||||
width="w-7"
|
|
||||||
height="h-7"
|
|
||||||
/>
|
|
||||||
{:else if $currentOrg.shortName}
|
|
||||||
<TextChip
|
|
||||||
value={$currentOrg.shortName}
|
|
||||||
className="bg-primary-200 dark:text-black font-bold"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<OrgSelector />
|
<OrgSelector />
|
||||||
|
|
||||||
<ul class="my-5">
|
<ul class="mt-4 my-2 px-4">
|
||||||
{#each menuItems as menuItem}
|
{#each menuItems as menuItem}
|
||||||
<a
|
{#if menuItem.show}
|
||||||
href="{$currentOrgPath}{menuItem.path}"
|
<a
|
||||||
class="text-black no-underline"
|
href="{$currentOrgPath}{menuItem.path}"
|
||||||
on:click={toggleSidebar}
|
class="text-black no-underline"
|
||||||
>
|
on:click={toggleSidebar}
|
||||||
<li
|
|
||||||
class="flex items-center py-3 px-4 mb-2 {NavClasses.item} {isActive(
|
|
||||||
$page.url.pathname,
|
|
||||||
`${$currentOrgPath}${menuItem.path}`
|
|
||||||
)
|
|
||||||
? NavClasses.active
|
|
||||||
: 'dark:text-white'}"
|
|
||||||
>
|
>
|
||||||
{#if menuItem.path === ''}
|
<li
|
||||||
<HomeIcon />
|
class="mb-1 flex items-center gap-2.5 px-2.5 py-2 {NavClasses.item} {isActive(
|
||||||
{:else if menuItem.path === '/courses'}
|
$page.url.pathname,
|
||||||
<CourseIcon />
|
`${$currentOrgPath}${menuItem.path}`
|
||||||
{:else if menuItem.path === '/site'}
|
)
|
||||||
<SiteSettingsIcon />
|
? NavClasses.active
|
||||||
{:else if menuItem.path === '/community'}
|
: 'dark:text-white'}"
|
||||||
<ForumIcon size={24} class="carbon-icon dark:fill-[#fff] fill-[#000]" />
|
>
|
||||||
{:else if menuItem.path === '/quiz'}
|
{#if menuItem.path === ''}
|
||||||
<QuizIcon />
|
<HomeIcon />
|
||||||
{:else if menuItem.path === '/audience'}
|
{:else if menuItem.path === '/courses'}
|
||||||
<AudienceIcon />
|
<CourseIcon />
|
||||||
{/if}
|
{:else if menuItem.path === '/site'}
|
||||||
<p class=" ml-2">{menuItem.label}</p>
|
<SiteSettingsIcon />
|
||||||
</li>
|
{:else if menuItem.path === '/community'}
|
||||||
</a>
|
<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}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<span class="flex-grow" />
|
<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}>
|
<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" />
|
<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>
|
</li>
|
||||||
</a>
|
</a>
|
||||||
<a href="{$currentOrgPath}/settings" class="text-black no-underline" on:click={toggleSidebar}>
|
<a href="{$currentOrgPath}/settings" class="text-black no-underline" on:click={toggleSidebar}>
|
||||||
<li
|
<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,
|
$page.url.pathname,
|
||||||
`${$currentOrgPath}/settings`
|
`${$currentOrgPath}/settings`
|
||||||
)
|
)
|
||||||
? NavClasses.active
|
? NavClasses.active
|
||||||
: 'dark:text-white'}"
|
: 'dark:text-white'}"
|
||||||
>
|
>
|
||||||
<Avatar src={$profile.avatar_url} name={$profile.username} width="w-7" height="h-7" />
|
<Avatar
|
||||||
<p class="ml-2">Settings</p>
|
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>
|
</li>
|
||||||
</a>
|
</a>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Popover, SkeletonText } from 'carbon-components-svelte';
|
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 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 { goto } from '$app/navigation';
|
||||||
import { newOrgModal } from '$lib/components/Org/store';
|
import { newOrgModal } from '$lib/components/Org/store';
|
||||||
|
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||||
import type { CurrentOrg } from '$lib/utils/types/org';
|
import type { CurrentOrg } from '$lib/utils/types/org';
|
||||||
import { setTheme } from '$lib/utils/functions/theme';
|
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;
|
export let canAddOrg = true;
|
||||||
|
|
||||||
let open = false;
|
let open = false;
|
||||||
|
|
||||||
function handleAddOrg() {
|
function handleAddOrg() {
|
||||||
@@ -19,33 +24,60 @@
|
|||||||
function onClick(org: CurrentOrg) {
|
function onClick(org: CurrentOrg) {
|
||||||
localStorage.setItem('classroomio_org_sitename', org.siteName);
|
localStorage.setItem('classroomio_org_sitename', org.siteName);
|
||||||
currentOrg.set(org);
|
currentOrg.set(org);
|
||||||
|
|
||||||
setTheme(org.theme);
|
setTheme(org.theme);
|
||||||
goto($currentOrgPath);
|
goto($currentOrgPath);
|
||||||
|
|
||||||
open = false;
|
open = false;
|
||||||
}
|
}
|
||||||
</script>
|
</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}
|
{#if $currentOrg.name}
|
||||||
<button
|
<button
|
||||||
class="flex items-center cursor-pointer max-w-[219px]"
|
class="flex items-center gap-2 w-full"
|
||||||
on:click={(e) => {
|
on:click={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
open = !open;
|
open = !open;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p class="dark:text-white text-lg font-bold whitespace-nowrap truncate">
|
{#if $currentOrg.avatar_url && $currentOrg.name}
|
||||||
{$currentOrg.name}
|
<Avatar
|
||||||
</p>
|
src={$currentOrg.avatar_url}
|
||||||
<CaretDown size={16} class="ml-2" />
|
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>
|
</button>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="w-[219px] h-[30px]">
|
<div class="w-[219px] h-[30px]">
|
||||||
<SkeletonText style="width: 100%; height: 100%;" />
|
<SkeletonText style="width: 100%; height: 100%;" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if canAddOrg}
|
{#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}
|
{#each $orgs as org}
|
||||||
<OrgSelectorItem
|
<OrgSelectorItem
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -62,3 +94,9 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:global(.org-selector .bx--popover-contents) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -15,9 +15,10 @@
|
|||||||
|
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<button
|
<button
|
||||||
class="{disabled &&
|
class="w-full {disabled &&
|
||||||
'opacity-25'} flex items-center text-start w-[14rem] py-2 px-3 {hasDivider &&
|
'opacity-25'} flex items-center text-start w-[14rem] py-1.5 px-2.5 {hasDivider &&
|
||||||
'border-b border-gray-100'} {active && 'bg-primary-100 dark:bg-gray-500'}"
|
'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}
|
on:click={disabled ? undefined : onClick}
|
||||||
>
|
>
|
||||||
{#if avatar}
|
{#if avatar}
|
||||||
@@ -25,14 +26,14 @@
|
|||||||
src={avatar}
|
src={avatar}
|
||||||
name={text}
|
name={text}
|
||||||
shape="rounded-md"
|
shape="rounded-md"
|
||||||
width="w-7"
|
width="w-[1.2rem]"
|
||||||
height="h-7"
|
height="h-[1.2rem]"
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
/>
|
/>
|
||||||
{:else if avatarText}
|
{:else if avatarText}
|
||||||
<TextChip
|
<TextChip
|
||||||
value={avatarText}
|
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}
|
{size}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export const VARIANTS = {
|
export const VARIANTS = {
|
||||||
CONTAINED: 'CONTAINED',
|
CONTAINED: 'CONTAINED',
|
||||||
|
CONTAINED_DARK: 'CONTAINED_DARK',
|
||||||
CONTAINED_LIGHT: 'CONTAINED_LIGHT',
|
CONTAINED_LIGHT: 'CONTAINED_LIGHT',
|
||||||
CONTAINED_INFO: 'CONTAINED_INFO',
|
CONTAINED_INFO: 'CONTAINED_INFO',
|
||||||
CONTAINED_WHITE: 'CONTAINED_WHITE',
|
CONTAINED_WHITE: 'CONTAINED_WHITE',
|
||||||
@@ -14,6 +15,8 @@ export const VARIANTS = {
|
|||||||
|
|
||||||
export const VARIANTS_CLASS = {
|
export const VARIANTS_CLASS = {
|
||||||
[VARIANTS.CONTAINED]: 'border-none bg-primary-700 hover:bg-primary-900 text-white font-bold',
|
[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]:
|
[VARIANTS.CONTAINED_LIGHT]:
|
||||||
'border-none bg-primary-600 hover:bg-primary-900 text-white font-bold',
|
'border-none bg-primary-600 hover:bg-primary-900 text-white font-bold',
|
||||||
[VARIANTS.CONTAINED_WHITE]:
|
[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_SUCCESS]: 'bg-green-700 hover:bg-green-900 text-white',
|
||||||
[VARIANTS.CONTAINED_DANGER]: 'bg-red-700 hover:bg-red-900 text-white',
|
[VARIANTS.CONTAINED_DANGER]: 'bg-red-700 hover:bg-red-900 text-white',
|
||||||
[VARIANTS.OUTLINED]:
|
[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]:
|
[VARIANTS.NONE]:
|
||||||
'border-none hover:border-black-300 hover:bg-gray-200 dark:hover:bg-neutral-800 text-black',
|
'border-none hover:border-black-300 hover:bg-gray-200 dark:hover:bg-neutral-800 text-black',
|
||||||
[VARIANTS.TEXT]: 'text-black hover:underline',
|
[VARIANTS.TEXT]: 'text-black hover:underline',
|
||||||
|
|||||||
@@ -17,24 +17,21 @@
|
|||||||
<button
|
<button
|
||||||
class="{isLoading || isDisabled
|
class="{isLoading || isDisabled
|
||||||
? 'opacity-25 cursor-not-allowed'
|
? '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
|
isLoading ? VARIANTS.OUTLINED : variant
|
||||||
]} {!disablePadding &&
|
]} {!disablePadding &&
|
||||||
'py-[0.5rem] px-6'} rounded-md {className} w-fit min-h-[36px] justify-center sm:w-auto {isLoading &&
|
'py-[0.5rem] px-6'} rounded-md {className} w-fit min-h-[36px] justify-center sm:w-auto {variant !==
|
||||||
'py-[1rem] border-primary-700'} {variant !== VARIANTS.TEXT &&
|
VARIANTS.TEXT && 'hover:shadow-xl'} transition delay-150 duration-300 ease-in-out"
|
||||||
'hover:shadow-xl'} transition delay-150 duration-300 ease-in-out {variant !==
|
|
||||||
VARIANTS.CONTAINED_WHITE
|
|
||||||
? 'dark:text-white'
|
|
||||||
: ''}"
|
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
{name}
|
{name}
|
||||||
{type}
|
{type}
|
||||||
disabled={isLoading || isDisabled}
|
disabled={isLoading || isDisabled}
|
||||||
>
|
>
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<Loading withOverlay={false} small />
|
<Loading withOverlay={false} small class="mr-2" />
|
||||||
<!-- <Moon size="20" color="#1d4ed8" unit="px" duration="1s" /> -->
|
<!-- <Moon size="20" color="#1d4ed8" unit="px" duration="1s" /> -->
|
||||||
{:else if !!label}
|
{/if}
|
||||||
|
{#if !!label}
|
||||||
{label}
|
{label}
|
||||||
{:else}
|
{:else}
|
||||||
<slot />
|
<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">
|
<section class="width-fit p-3 flex flex-col items-center">
|
||||||
<div
|
<div
|
||||||
class="avatar-container {widthHeight ||
|
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}
|
{#if src}
|
||||||
<img class="w-full h-full {shape}" {src} alt="Avatar" />
|
<img class="w-full h-full {shape}" {src} alt="Avatar" />
|
||||||
|
|||||||
@@ -9,25 +9,24 @@
|
|||||||
} from '$lib/utils/services/notification/notification';
|
} from '$lib/utils/services/notification/notification';
|
||||||
import { profile } from '$lib/utils/store/user';
|
import { profile } from '$lib/utils/store/user';
|
||||||
|
|
||||||
let query = new URLSearchParams($page.url.search);
|
let open = false;
|
||||||
let welcomePopup = query.get('welcomePopup');
|
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
triggerSendEmail(NOTIFICATION_NAME.WELCOME_TO_APP, {
|
triggerSendEmail(NOTIFICATION_NAME.WELCOME_TO_APP, {
|
||||||
to: $profile.email,
|
to: $profile.email,
|
||||||
name: $profile.fullname
|
name: $profile.fullname
|
||||||
});
|
});
|
||||||
goto($currentOrgPath + '/courses?create=true');
|
|
||||||
|
goto($currentOrgPath + '/setup');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$: {
|
||||||
|
const query = new URLSearchParams($page.url.search);
|
||||||
|
open = query.get('welcomePopup') === 'true';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal
|
<Modal onClose={closeModal} {open} width="w-9/12" maxWidth="w-[800px]" modalHeading="Welcome">
|
||||||
onClose={closeModal}
|
|
||||||
open={welcomePopup === 'true'}
|
|
||||||
width="w-9/12"
|
|
||||||
maxWidth="w-[800px]"
|
|
||||||
modalHeading="Welcome"
|
|
||||||
>
|
|
||||||
<p class="text-black dark:text-white text-sm md:text-base lg:text-lg">
|
<p class="text-black dark:text-white text-sm md:text-base lg:text-lg">
|
||||||
We at <a
|
We at <a
|
||||||
href="https://app.classroomio.com/"
|
href="https://app.classroomio.com/"
|
||||||
|
|||||||
@@ -23,5 +23,6 @@ export const blockedSubdomain = [
|
|||||||
'academy',
|
'academy',
|
||||||
'schools',
|
'schools',
|
||||||
'training',
|
'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 {
|
export function genQuizPin(): number {
|
||||||
var minm = 100000;
|
var minm = 100000;
|
||||||
var maxm = 999999;
|
var maxm = 999999;
|
||||||
@@ -10,3 +12,13 @@ export function generateSitename(orgName: string): string {
|
|||||||
.replace(/\s+/g, '-')
|
.replace(/\s+/g, '-')
|
||||||
.replace(/[^a-zA-Z0-9-]/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 { get } from 'svelte/store';
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { supabase } from '$lib/utils/functions/supabase';
|
import { supabase } from '$lib/utils/functions/supabase';
|
||||||
import { orgs, currentOrg, orgAudience, orgTeam } from '$lib/utils/store/org';
|
import { orgs, currentOrg, orgAudience, orgTeam } from '$lib/utils/store/org';
|
||||||
import { ROLE, ROLE_LABEL } from '$lib/utils/constants/roles';
|
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) {
|
export async function getOrgTeam(orgId: string) {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
@@ -79,7 +79,12 @@ export async function getOrganizations(userId: string) {
|
|||||||
avatar_url,
|
avatar_url,
|
||||||
landingpage,
|
landingpage,
|
||||||
theme,
|
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,
|
avatar_url: orgMember?.organization?.avatar_url,
|
||||||
memberId: orgMember?.id,
|
memberId: orgMember?.id,
|
||||||
role_id: orgMember?.role_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) {
|
export async function getCurrentOrg(siteName: string, justGet = false) {
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('organization')
|
.from('organization')
|
||||||
.select(
|
.select(
|
||||||
@@ -210,7 +215,11 @@ export async function getCurrentOrg(siteName: string, justGet = false) {
|
|||||||
siteName,
|
siteName,
|
||||||
avatar_url,
|
avatar_url,
|
||||||
landingpage,
|
landingpage,
|
||||||
theme
|
theme,
|
||||||
|
organization_plan(
|
||||||
|
plan_name,
|
||||||
|
is_active
|
||||||
|
)
|
||||||
`
|
`
|
||||||
)
|
)
|
||||||
.eq('siteName', siteName)
|
.eq('siteName', siteName)
|
||||||
@@ -218,7 +227,6 @@ export async function getCurrentOrg(siteName: string, justGet = false) {
|
|||||||
console.log('data =', data);
|
console.log('data =', data);
|
||||||
console.log('error =', error);
|
console.log('error =', error);
|
||||||
|
|
||||||
|
|
||||||
const isDataEmpty = !data?.[0];
|
const isDataEmpty = !data?.[0];
|
||||||
|
|
||||||
if (!justGet && (error || isDataEmpty)) {
|
if (!justGet && (error || isDataEmpty)) {
|
||||||
@@ -234,3 +242,32 @@ export async function getCurrentOrg(siteName: string, justGet = false) {
|
|||||||
return data[0];
|
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 type { CurrentOrg, OrgTeamMember, OrgAudience } from '../types/org';
|
||||||
import { ROLE } from '$lib/utils/constants/roles';
|
import { ROLE } from '$lib/utils/constants/roles';
|
||||||
import type { UserLessonDataType } from '$lib/utils/types';
|
import type { UserLessonDataType } from '$lib/utils/types';
|
||||||
|
import { PLAN } from 'shared-constants/src/plans/constants';
|
||||||
|
|
||||||
export const defaultCurrentOrgState: CurrentOrg = {
|
export const defaultCurrentOrgState: CurrentOrg = {
|
||||||
id: '',
|
id: '',
|
||||||
@@ -15,7 +16,8 @@ export const defaultCurrentOrgState: CurrentOrg = {
|
|||||||
memberId: '',
|
memberId: '',
|
||||||
role_id: '',
|
role_id: '',
|
||||||
landingpage: {},
|
landingpage: {},
|
||||||
theme: ''
|
theme: '',
|
||||||
|
organization_plan: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export const orgs = writable<CurrentOrg[]>([]);
|
export const orgs = writable<CurrentOrg[]>([]);
|
||||||
@@ -26,6 +28,9 @@ export const isOrgAdmin = derived(
|
|||||||
currentOrg,
|
currentOrg,
|
||||||
($currentOrg) => parseInt($currentOrg.role_id) === ROLE.ADMIN
|
($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) =>
|
export const currentOrgPath = derived(currentOrg, ($currentOrg) =>
|
||||||
$currentOrg.siteName ? `/org/${$currentOrg.siteName}` : ''
|
$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
|
// Quiz
|
||||||
export const createQuizModal = writable({
|
export const createQuizModal = writable({
|
||||||
open: false,
|
open: false,
|
||||||
|
|||||||
@@ -105,6 +105,17 @@ export interface Organization {
|
|||||||
name: any; // type unknown;
|
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 {
|
export interface Profile {
|
||||||
id: string /* primary key */;
|
id: string /* primary key */;
|
||||||
fullname: string;
|
fullname: string;
|
||||||
|
|||||||
@@ -7,9 +7,14 @@ export interface CurrentOrg {
|
|||||||
memberId: string;
|
memberId: string;
|
||||||
role_id: string;
|
role_id: string;
|
||||||
landingpage: {
|
landingpage: {
|
||||||
[key: string]: any;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
theme: string;
|
theme: string;
|
||||||
|
organization_plan: {
|
||||||
|
subscriptionId: string;
|
||||||
|
plan_name: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OrgTeamMember {
|
export interface OrgTeamMember {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
import shouldRedirectOnAuth from '$lib/utils/functions/routes/shouldRedirectOnAuth';
|
import shouldRedirectOnAuth from '$lib/utils/functions/routes/shouldRedirectOnAuth';
|
||||||
import { identifyPosthogUser, initPosthog } from '$lib/utils/services/posthog';
|
import { identifyPosthogUser, initPosthog } from '$lib/utils/services/posthog';
|
||||||
import { initSentry, setSentryUser } from '$lib/utils/services/sentry';
|
import { initSentry, setSentryUser } from '$lib/utils/services/sentry';
|
||||||
|
import UpgradeModal from '$lib/components/Upgrade/Modal.svelte';
|
||||||
|
|
||||||
import '../app.postcss';
|
import '../app.postcss';
|
||||||
|
|
||||||
@@ -281,6 +282,7 @@
|
|||||||
|
|
||||||
<Theme bind:theme={carbonTheme} />
|
<Theme bind:theme={carbonTheme} />
|
||||||
|
|
||||||
|
<UpgradeModal />
|
||||||
<Snackbar />
|
<Snackbar />
|
||||||
|
|
||||||
{#if data.skipAuth}
|
{#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 { 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!)
|
// Create an OpenAI API client (that's edge friendly!)
|
||||||
const openAIConfig = new Configuration({
|
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 PageNav from '$lib/components/PageNav/index.svelte';
|
||||||
import PageBody from '$lib/components/PageBody/index.svelte';
|
import PageBody from '$lib/components/PageBody/index.svelte';
|
||||||
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/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 IssueCertificateModal from '$lib/components/Course/components/Ceritficate/IssueCertificateModal.svelte';
|
||||||
// import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
|
// import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
|
||||||
import Design from '$lib/components/Course/components/Ceritficate/Design.svelte';
|
import Design from '$lib/components/Course/components/Ceritficate/Design.svelte';
|
||||||
@@ -40,7 +39,7 @@
|
|||||||
<StudentCertificate />
|
<StudentCertificate />
|
||||||
</RoleBasedSecurity>
|
</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="Design" />
|
||||||
<Tab label="Reports" />
|
<Tab label="Reports" />
|
||||||
<svelte:fragment slot="content">
|
<svelte:fragment slot="content">
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
>
|
>
|
||||||
{#each $lessons as lesson (lesson.id)}
|
{#each $lessons as lesson (lesson.id)}
|
||||||
<div
|
<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 -->
|
<!-- Number Chip -->
|
||||||
<div class="mr-5">
|
<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 { isQuizPage } from '$lib/utils/functions/app';
|
||||||
import OrgSideBar from '$lib/components/Org/SideBar.svelte';
|
import OrgSideBar from '$lib/components/Org/SideBar.svelte';
|
||||||
import VerifyEmailModal from '$lib/components/Org/VerifyEmail/VerifyEmailModal.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>
|
</script>
|
||||||
|
|
||||||
<AddOrgModal />
|
<AddOrgModal />
|
||||||
@@ -13,6 +22,10 @@
|
|||||||
<OrgSideBar />
|
<OrgSideBar />
|
||||||
{/if}
|
{/if}
|
||||||
<div class="org-slot bg-white dark:bg-black w-full">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -184,7 +184,7 @@
|
|||||||
|
|
||||||
<WelcomeModal />
|
<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">
|
<div class="flex items-center justify-between mb-10">
|
||||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold mb-3">
|
<h1 class="dark:text-white text-2xl md:text-3xl font-bold mb-3">
|
||||||
{getGreeting()}
|
{getGreeting()}
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
<!-- <div class="flex items-start flex-wrap">
|
<!-- <div class="flex items-start flex-wrap">
|
||||||
{#each boxes as box}
|
{#each boxes as box}
|
||||||
<div
|
<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 mb-2 text-sm">{box.label}</p>
|
||||||
<p class="dark:text-white text-xl font-bold">{box.value}</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">
|
<div class="w-full xl:w-auto container">
|
||||||
<p class="dark:text-white font-bold mt-7 mb-4">Your Schedule</p>
|
<p class="dark:text-white font-bold mt-7 mb-4">Your Schedule</p>
|
||||||
<div
|
<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}
|
{#if !$isMobile}
|
||||||
<div id="calendar" class="flex justify-center mt-5 w-2/5 min-w-[250px]">
|
<div id="calendar" class="flex justify-center mt-5 w-2/5 min-w-[250px]">
|
||||||
@@ -326,10 +326,10 @@
|
|||||||
<!-- <div class="w-full">
|
<!-- <div class="w-full">
|
||||||
<p class="dark:text-white font-bold mb-7">Your Activities</p>
|
<p class="dark:text-white font-bold mb-7">Your Activities</p>
|
||||||
<div
|
<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}
|
{#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" />
|
<Avatar src={activity.avatar} name="avatar" />
|
||||||
<div class="ml-2 flex flex-col xl:flex-row">
|
<div class="ml-2 flex flex-col xl:flex-row">
|
||||||
<div class="mr-2">
|
<div class="mr-2">
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||||
import Audience from '$lib/components/Org/Audience/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;
|
let isLoading = false;
|
||||||
|
|
||||||
function exportAudience() {
|
function exportAudience() {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
alert('This feature is coming soon');
|
alert('This feature is coming soon');
|
||||||
@@ -14,13 +18,23 @@
|
|||||||
<title>Audience - ClassroomIO</title>
|
<title>Audience - ClassroomIO</title>
|
||||||
</svelte:head>
|
</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="py-10 px-5">
|
||||||
<div class="flex items-center justify-between mb-10">
|
<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} />
|
<PrimaryButton label="Export" onClick={exportAudience} isDisabled={isLoading} {isLoading} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if $orgAudience.length >= $currentOrgMaxAudience}
|
||||||
|
<UpgradeBanner>Upgrade your plan to invite more students</UpgradeBanner>
|
||||||
|
{/if}
|
||||||
<Audience />
|
<Audience />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||||
import type { Course } from '$lib/utils/types';
|
import type { Course } from '$lib/utils/types';
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
let { cantFetch } = data;
|
let { cantFetch } = data;
|
||||||
@@ -93,6 +94,7 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
label="Create Course"
|
label="Create Course"
|
||||||
|
variant={VARIANTS.CONTAINED_DARK}
|
||||||
isDisabled={!$isOrgAdmin}
|
isDisabled={!$isOrgAdmin}
|
||||||
onClick={() => ($createCourseModal.open = true)}
|
onClick={() => ($createCourseModal.open = true)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>Settings - ClassroomIO</title>
|
<title>Settings - ClassroomIO</title>
|
||||||
</svelte:head>
|
</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="py-10 px-5">
|
||||||
<div class="flex items-center justify-between mb-10">
|
<div class="flex items-center justify-between mb-10">
|
||||||
<h1 class="dark:text-white text-3xl font-bold">Settings</h1>
|
<h1 class="dark:text-white text-3xl font-bold">Settings</h1>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<title>Custom domain - ClassroomIO</title>
|
<title>Custom domain - ClassroomIO</title>
|
||||||
</svelte:head>
|
</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">
|
<div class="py-10 px-3 md:px-5">
|
||||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||||
>Back to Settings</a
|
>Back to Settings</a
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import TeamMembers from '$lib/components/Org/Settings/TeamMembers.svelte';
|
import TeamMembers from '$lib/components/Org/Settings/TeamMembers.svelte';
|
||||||
import { currentOrgPath } from '$lib/utils/store/org';
|
import { currentOrgPath } from '$lib/utils/store/org';
|
||||||
|
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Teams - ClassroomIO</title>
|
<title>Teams - ClassroomIO</title>
|
||||||
</svelte:head>
|
</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">
|
<div class="py-10 px-3 md:px-5">
|
||||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||||
>Back to Settings</a
|
>Back to Settings</a
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
<h1 class="dark:text-white text-3xl font-bold">Team members</h1>
|
<h1 class="dark:text-white text-3xl font-bold">Team members</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<UpgradeBanner>Upgrade your plan to add team members</UpgradeBanner>
|
||||||
<TeamMembers />
|
<TeamMembers />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||||
import { sveltekit } from '@sveltejs/kit/vite';
|
import { sveltekit } from '@sveltejs/kit/vite';
|
||||||
import { defineConfig, loadEnv } from 'vite';
|
import { defineConfig, loadEnv } from 'vite';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
export default ({ mode }) => {
|
export default ({ mode }) => {
|
||||||
process.env = { ...process.env, ...loadEnv(mode, process.cwd()) };
|
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({
|
return defineConfig({
|
||||||
plugins: [
|
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: {
|
build: {
|
||||||
sourcemap: true
|
sourcemap: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
<img
|
<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"
|
src="https://classroomio.com/classroomio-opengraph-image.png"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const config: DocsThemeConfig = {
|
|||||||
<title>ClassroomIO – Launch Your Online Bootcamp In Minutes</title>
|
<title>ClassroomIO – Launch Your Online Bootcamp In Minutes</title>
|
||||||
<meta
|
<meta
|
||||||
name="description"
|
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" />
|
<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:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||||
<meta
|
<meta
|
||||||
property="og:description"
|
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" />
|
<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:title" content="ClassroomIO – Launch Your Online Bootcamp In Minutes" />
|
||||||
<meta
|
<meta
|
||||||
name="twitter:description"
|
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 name="twitter:creator" content="@classroomio" />
|
||||||
<meta
|
<meta
|
||||||
@@ -88,8 +88,7 @@ const config: DocsThemeConfig = {
|
|||||||
openGraph: {
|
openGraph: {
|
||||||
siteName: 'ClassroomIO Docs',
|
siteName: 'ClassroomIO Docs',
|
||||||
title: 'ClassroomIO Documentation',
|
title: 'ClassroomIO Documentation',
|
||||||
description:
|
description: 'ClassroomIO is the easiest place to launch and scale your online bootcamp.',
|
||||||
'ClassroomIO is a no-code tool that allows you build and scale your own teaching platform with ease.',
|
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: `${process.env.imagePath}/img/logo-16.png`,
|
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);
|
||||||
|
}
|
||||||