feat: dashboard billing (#193)
* feat: move plans into sharable json * remove unused code * update pnpm lock * update plans.json * implement early adopter pricing * docs: update meta title and description * Finished the UI for upgrading plans * Updated the pricing plans * Updated the cards ui * use relative import * improve styling of upgrade plan card * styling improvements, lemonsqueezy setup * trigger lemonsqueezy payment and add webhook * update pnpm lock * add logout route * feat: complete billing functionality * generate lock file * make crypto false on browser * feat: adding upgrade prompts to org * chore: add upgrade icon to upload video * feat: add upgrade prompt to certificate and settings * feat: implement onboarding setup page (#229) * working on the onboarding setup * working on the onboarding page * Almost done with the onboarding UI * finished the UI of the onBoarding setup * finished the UI of the onBoarding setup * Implementing the functions to carryout the logic of updating the UI onmount if a user has carried out a certain step or not * Implementing the functions to carryout the logic of updating the UI onmount if a user has carried out a certain step or not * making some chnages * finished the onBoarding task * updated some logic * Removed the uneeded svg from the static folder * fixed isCoursePublished not returning a boolean * improved the setup link protection * chore: make menuItems reactive * improve formatting * format supabase queries --------- Co-authored-by: Rotimi Best <[email protected]> * revamp setup * improve the upgrade experience * migration * feat: add lemonsqueezy customer portal --------- Co-authored-by: emmanuel <[email protected]>
This commit is contained in:
@@ -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
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user