feat: 🚀 😃 convert repo into a monorepo 🚀 😃 (#51)

* Merge main into turbo-monrepo

* feat: rename apps

* feat: remove supbase videos and serve all from project

* feat: move supabase to root

* feat: update README

* docs: update link
This commit is contained in:
Rotimi Best
2023-11-25 21:56:02 +00:00
committed by GitHub
parent 03ca379094
commit 41e29014ef
806 changed files with 85853 additions and 5746 deletions
@@ -0,0 +1,22 @@
import { supabase } from '$lib/utils/functions/supabase';
export async function fetchCourses(profileId, orgId) {
if (!orgId || !profileId) return;
// Gets courses for a particular organisation where the current logged in user is a groupmember
const { data: allCourses } = await supabase
.rpc('get_courses', {
org_id_arg: orgId,
profile_id_arg: profileId
})
.eq('profile_id', profileId);
console.log(`allCourses`, allCourses);
if (!Array.isArray(allCourses)) {
return {
allCourses: []
};
}
return { allCourses };
}
@@ -0,0 +1,28 @@
<script lang="ts">
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
</script>
<div
class="root w-72 border bg-gray-100 dark:bg-black mr-4 mb-4 rounded-md transition ease-in-out relative"
>
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
<div class="p-3 pb-0">
<!-- Title -->
<SkeletonText style="width: 50%;" />
<!-- Description -->
<SkeletonText style="width: 100%; margin-top: 1rem;" />
<SkeletonText style="width: 100%; margin-bottom: 1rem;" />
<div class="flex justify-between mb-3">
<SkeletonText style="width: 25%" />
<SkeletonText style="width: 25%" />
</div>
<SkeletonText style="width: 25%" />
</div>
</div>
<style>
.root {
min-height: 370px;
}
</style>
@@ -0,0 +1,108 @@
<script lang="ts">
import { Tag, ImageLoader, SkeletonPlaceholder } from 'carbon-components-svelte';
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
export let bannerImage: string | undefined;
export let id = '';
export let slug = '';
export let title = '';
export let description = '';
export let isPublished = false;
export let cost = 0;
export let totalLessons = 0;
export let currency = 'NGN';
export let isOnLandingPage = false;
export let isLMS = false;
export let progressRate = 45;
$: formatter = getCurrencyFormatter(currency);
</script>
<a
rel="prefetch"
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"
>
<div class="p-4">
<div class=" mb-5">
<ImageLoader
src={bannerImage}
alt="Course Logo"
class="h-[200px] w-full rounded dark:border dark:border-white"
>
<svelte:fragment slot="loading">
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
</svelte:fragment>
<svelte:fragment slot="error">An error occurred.</svelte:fragment>
</ImageLoader>
</div>
<h3 class="text-xl dark:text-white title">{title}</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-300 description">
{description}
</p>
</div>
<div
class="px-4 border border-gray border-b-0 border-l-0 border-r-0 flex justify-between items-center"
>
<div>
<p class="text-xs pt-2 {!isLMS && 'pl-2'} dark:text-white">{totalLessons} lessons</p>
<p class="text-xs py-2">
{#if isOnLandingPage}
<span class="px-2">{!cost ? 'Free' : formatter.format(cost)}</span>
{:else if isLMS}
<div class="flex items-center gap-2">
<div class=" relative bg-[#EAEAEA] w-[50px] h-1">
<div
style="width:{progressRate}%"
class={`absolute top-0 left-0 bg-primary-700 h-full`}
/>
</div>
<p class="text-xs text-[#656565] dark:text-white">{progressRate}%</p>
</div>
{:else}
<Tag type={isPublished ? 'green' : 'cool-gray'}>
{#if isPublished}
Published
{:else}
Unpublished
{/if}
</Tag>
{/if}
</p>
</div>
{#if isLMS}
<PrimaryButton
label={'Continue Course'}
variant={VARIANTS.OUTLINED}
className="rounded-none text-primary-600"
/>
{/if}
</div>
</a>
<style>
a,
a:hover {
text-decoration: none;
}
.title,
.description {
height: 42px;
line-height: 20px;
display: -webkit-box;
-webkit-box-orient: vertical;
-moz-box-orient: vertical;
-ms-box-orient: vertical;
box-orient: vertical;
-webkit-line-clamp: 2;
-moz-line-clamp: 2;
-ms-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
</style>
@@ -0,0 +1,61 @@
<script>
import { goto } from '$app/navigation';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import TextField from '$lib/components/Form/TextField.svelte';
import { copyCourseModalInitialState, copyCourseModal } from '$lib/components/Courses/store';
import { cloneCourse } from '$lib/utils/services/courses/clone';
// clone, show spinner and redirect to new course
async function createCourse() {
console.log('Create course');
$copyCourseModal.isSaving = true;
try {
const newCourse = await cloneCourse($copyCourseModal.id, $copyCourseModal.title);
goto(`/courses/${newCourse.id}`);
copyCourseModal.set(copyCourseModalInitialState);
} catch (error) {
console.error(error);
// IIFI of snackbar to Show notification error
(() => {
snackbar.error();
})();
// Stop loader
$copyCourseModal.isSaving = false;
}
}
</script>
<Modal
onClose={() => ($copyCourseModal.open = false)}
bind:open={$copyCourseModal.open}
width="w-96"
modalHeading="Create a Course"
>
<form on:submit|preventDefault={createCourse}>
<TextField
label="Course name"
bind:value={$copyCourseModal.title}
autoFocus={true}
placeholder="Your course name"
className="mb-4"
isRequired={true}
autoComplete={false}
/>
<div class="mt-5 flex items-center flex-row-reverse">
<PrimaryButton
className="px-6 py-3"
label="Create"
type="submit"
isLoading={$copyCourseModal.isSaving}
/>
</div>
</form>
</Modal>
@@ -0,0 +1,141 @@
<script>
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import TextArea from '$lib/components/Form/TextArea.svelte';
import { courses, createCourseModal } from '$lib/components/Courses/store';
import { validateForm } from '$lib/components/Courses/functions';
import { ROLE } from '$lib/utils/constants/roles';
import { addGroupMember } from '$lib/utils/services/courses';
import { supabase } from '$lib/utils/functions/supabase';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { goto } from '$app/navigation';
import { capturePosthogEvent } from '$lib/utils/services/posthog';
let isLoading = false;
let errors = {
title: '',
description: ''
};
let userPrompt = '';
function onClose() {
createCourseModal.update(() => ({
open: false,
title: '',
description: '',
emails: '',
tutors: '',
students: ''
}));
}
async function createCourse() {
isLoading = true;
const { hasError, fieldErrors } = validateForm($createCourseModal);
errors = fieldErrors;
if (hasError) return;
const { title, description } = $createCourseModal;
// 1. Create group
const { data: newGroup } = await supabase
.from('group')
.insert({ name: title, description, organization_id: $currentOrg.id })
.select();
console.log(`newGroup`, newGroup);
if (!newGroup) return;
const { id: group_id } = newGroup[0];
// Set userPrompt to the description
userPrompt = description;
// 2. Create course with group_id
const { data: newCourse } = await supabase
.from('course')
.insert({
title,
description,
group_id
})
.select();
console.log(`newCourse`, newCourse);
if (!newCourse) return;
courses.update((_courses) => [..._courses, newCourse[0]]);
capturePosthogEvent('course_created', {
course_id: newCourse[0]?.id,
course_title: newCourse[0]?.title,
course_description: newCourse[0]?.description,
organization_id: $currentOrg.id,
organization_name: $currentOrg.name,
user_id: $profile.id,
user_email: $profile.email
});
// 3. Add group members
await addGroupMember({
profile_id: $profile.id,
email: $profile.email,
group_id,
role_id: ROLE.TUTOR
});
if (newCourse[0] != null && newCourse[0].id) {
goto(`/courses/${newCourse[0]?.id}`);
}
onClose();
isLoading = false;
}
</script>
<svelte:head>
<title>Create a new course</title>
</svelte:head>
<Modal
{onClose}
bind:open={$createCourseModal.open}
width="w-4/5 md:w-2/5"
modalHeading="Create a Course"
>
<form on:submit|preventDefault={createCourse}>
<TextField
label="Course name"
bind:value={$createCourseModal.title}
placeholder="Your course name"
className="mb-4"
isRequired={true}
errorMessage={errors.title}
autoComplete={false}
/>
<TextArea
label="Short description"
bind:value={$createCourseModal.description}
rows={4}
placeholder="A little description about this course"
className="mb-4"
isRequired={true}
errorMessage={errors.description}
isAIEnabled={true}
initAIPrompt="Write a 30 word description for a course titled: {$createCourseModal.title}"
/>
<div class="mt-5 flex items-center">
<PrimaryButton
className="px-6 py-3"
label="Finish"
type="submit"
isDisabled={isLoading}
{isLoading}
/>
</div>
</form>
</Modal>
@@ -0,0 +1,10 @@
export const statuses = [
{
chipClassName: 'bg-green-600 text-white',
chipLabel: 'In progress'
},
{
chipClassName: 'bg-red-600 text-white',
chipLabel: 'Completed'
}
];
@@ -0,0 +1,25 @@
import { validateEmail } from '$lib/utils/functions/validateEmail';
export function validateForm(fields) {
let hasError = false;
const { title, description } = fields;
const fieldErrors = {
title: '',
description: ''
};
if (!title) {
fieldErrors.title = 'Title is required';
hasError = true;
}
if (!description) {
fieldErrors.description = 'Description is required';
hasError = true;
}
return {
hasError,
fieldErrors
};
}
@@ -0,0 +1,58 @@
<script lang="ts">
import Box from '../Box/index.svelte';
import Card from './components/Card/index.svelte';
import CardLoader from './components/Card/Loader.svelte';
import CoursesEmptyIcon from '../Icons/CoursesEmptyIcon.svelte';
import { courseMetaDeta } from './store';
import type { Course } from '$lib/utils/types';
import { appStore } from '$lib/utils/store/app';
export let courses: Course[] = [];
export let emptyTitle = 'No Courses Created';
export let emptyDescription =
'Share your knowledge with the world by creating engaging courses for your students.';
function calcProgressRate(progressRate?: number, totalLessons?: number): number {
if (!progressRate || !totalLessons) {
return 0;
}
return Math.round((progressRate / totalLessons) * 100);
}
</script>
<!-- <CopyCourseModal /> -->
<div class={`w-full ${$courseMetaDeta.isLoading || courses ? 'cards-container' : ''} my-4 mx-auto`}>
{#if $courseMetaDeta.isLoading}
<CardLoader />
<CardLoader />
<CardLoader />
{:else}
{#each courses as courseData}
{#key courseData.id}
<Card
id={courseData.id}
bannerImage={courseData.logo || '/images/classroomio-course-img-template.jpg'}
title={courseData.title}
description={courseData.description}
isPublished={courseData.is_published}
cost={courseData.cost}
currency={courseData.currency}
totalLessons={courseData.total_lessons}
isLMS={$appStore.isOrgSite}
progressRate={calcProgressRate(courseData.progress_rate, courseData.total_lessons)}
/>
{/key}
{/each}
{/if}
</div>
{#if !$courseMetaDeta.isLoading && !courses.length}
<Box className="w-full">
<CoursesEmptyIcon />
<h3 class="dark:text-white text-2xl my-5">{emptyTitle}</h3>
<p class="dark:text-white w-1/3 text-center">
{emptyDescription}
</p>
</Box>
{/if}
@@ -0,0 +1,37 @@
import { writable, derived } from 'svelte/store';
import type { Course } from '$lib/utils/types';
export const courses = writable<Course[]>([]);
export const coursesInProgress = derived(courses, ($courses) =>
$courses.length > 0 ? $courses.slice(0, 3) : []
);
export const coursesComplete = derived(courses, ($courses) =>
$courses.length > 0
? $courses.filter((course) => {
return course.total_lessons === course.progress_rate;
})
: []
);
export const courseMetaDeta = writable({
isLoading: true
});
export const createCourseModal = writable({
open: false,
title: '',
description: '',
emails: '',
tutors: '',
students: ''
});
export const copyCourseModalInitialState = {
open: false,
courseIdToCopy: null,
id: '',
title: '',
isSaving: false,
error: null
};
export const copyCourseModal = writable({ ...copyCourseModalInitialState });