2024-05-21 00:14:47 +01:00
|
|
|
<script lang="ts">
|
2025-11-01 15:49:29 +01:00
|
|
|
import { preventDefault } from '$lib/utils/functions/svelte';
|
|
|
|
|
|
2024-12-30 02:59:10 +01:00
|
|
|
import { goto } from '$app/navigation';
|
2025-11-01 15:49:29 +01:00
|
|
|
import { page } from '$app/state';
|
2025-11-24 01:00:50 +01:00
|
|
|
import { ComingSoon } from '$lib/features/ui';
|
2025-12-02 09:54:38 +01:00
|
|
|
import { validateForm } from '../utils/functions';
|
|
|
|
|
import { courses, createCourseModal } from '../utils/store';
|
2024-12-30 02:59:10 +01:00
|
|
|
import TextArea from '$lib/components/Form/TextArea.svelte';
|
|
|
|
|
import TextField from '$lib/components/Form/TextField.svelte';
|
2023-08-02 19:35:27 +01:00
|
|
|
import Modal from '$lib/components/Modal/index.svelte';
|
2024-12-30 02:59:10 +01:00
|
|
|
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
|
|
|
|
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
2025-11-24 01:00:50 +01:00
|
|
|
import { ROLE } from '@cio/utils/constants';
|
2024-12-30 02:59:10 +01:00
|
|
|
import { supabase } from '$lib/utils/functions/supabase';
|
|
|
|
|
import { t } from '$lib/utils/functions/translations';
|
|
|
|
|
import { addDefaultNewsFeed, addGroupMember } from '$lib/utils/services/courses';
|
|
|
|
|
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
|
|
|
|
import { currentOrg } from '$lib/utils/store/org';
|
|
|
|
|
import { profile } from '$lib/utils/store/user';
|
|
|
|
|
import { COURSE_TYPE, COURSE_VERSION } from '$lib/utils/types';
|
2025-11-01 15:49:29 +01:00
|
|
|
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
2021-12-30 22:30:30 +02:00
|
|
|
|
2025-11-01 15:49:29 +01:00
|
|
|
let isLoading = $state(false);
|
|
|
|
|
let errors = $state({
|
2023-09-18 17:03:56 +00:00
|
|
|
title: '',
|
|
|
|
|
description: ''
|
2025-11-01 15:49:29 +01:00
|
|
|
});
|
|
|
|
|
let step = $state(0);
|
2021-12-30 22:30:30 +02:00
|
|
|
|
2024-05-21 00:14:47 +01:00
|
|
|
const options = [
|
|
|
|
|
{
|
2025-11-21 12:18:04 +01:00
|
|
|
title: $t('new_course_modal.live_class_label'),
|
|
|
|
|
subtitle: $t('new_course_modal.live_class_subtitle'),
|
2024-05-21 00:14:47 +01:00
|
|
|
type: COURSE_TYPE.LIVE_CLASS,
|
|
|
|
|
isDisabled: false
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-11-21 12:18:04 +01:00
|
|
|
title: $t('new_course_modal.self_paced_label'),
|
|
|
|
|
subtitle: $t('new_course_modal.self_paced_subtitle'),
|
2024-05-21 00:14:47 +01:00
|
|
|
type: COURSE_TYPE.SELF_PACED,
|
|
|
|
|
isDisabled: false
|
|
|
|
|
}
|
|
|
|
|
];
|
2025-11-01 15:49:29 +01:00
|
|
|
let type = $state(options[0].type);
|
2023-09-24 07:08:37 +00:00
|
|
|
|
2024-07-11 09:03:35 +01:00
|
|
|
function onClose(redirectTo) {
|
2024-06-21 10:39:50 +01:00
|
|
|
goto(redirectTo);
|
2024-05-21 00:14:47 +01:00
|
|
|
|
2023-09-24 07:08:37 +00:00
|
|
|
createCourseModal.update(() => ({
|
|
|
|
|
title: '',
|
|
|
|
|
description: '',
|
2024-05-21 00:14:47 +01:00
|
|
|
type: '',
|
2023-09-24 07:08:37 +00:00
|
|
|
emails: '',
|
|
|
|
|
tutors: '',
|
|
|
|
|
students: ''
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2023-10-30 06:04:53 +00:00
|
|
|
async function createCourse() {
|
2023-10-03 12:29:06 +00:00
|
|
|
isLoading = true;
|
2023-09-18 17:03:56 +00:00
|
|
|
const { hasError, fieldErrors } = validateForm($createCourseModal);
|
2021-12-30 22:30:30 +02:00
|
|
|
|
|
|
|
|
errors = fieldErrors;
|
|
|
|
|
if (hasError) return;
|
|
|
|
|
|
|
|
|
|
const { title, description } = $createCourseModal;
|
|
|
|
|
// 1. Create group
|
|
|
|
|
const { data: newGroup } = await supabase
|
|
|
|
|
.from('group')
|
2023-08-02 19:35:27 +01:00
|
|
|
.insert({ name: title, description, organization_id: $currentOrg.id })
|
|
|
|
|
.select();
|
2021-12-30 22:30:30 +02:00
|
|
|
|
|
|
|
|
console.log(`newGroup`, newGroup);
|
2023-10-30 06:04:53 +00:00
|
|
|
|
|
|
|
|
if (!newGroup) return;
|
|
|
|
|
|
2021-12-30 22:30:30 +02:00
|
|
|
const { id: group_id } = newGroup[0];
|
|
|
|
|
|
|
|
|
|
// 2. Create course with group_id
|
2024-06-21 10:39:50 +01:00
|
|
|
const { data: newCourseData } = await supabase
|
2023-08-02 19:35:27 +01:00
|
|
|
.from('course')
|
|
|
|
|
.insert({
|
|
|
|
|
title,
|
|
|
|
|
description,
|
2024-05-21 00:14:47 +01:00
|
|
|
type: type,
|
2024-08-14 10:27:37 +01:00
|
|
|
version: COURSE_VERSION.V2,
|
2023-08-07 06:18:30 +00:00
|
|
|
group_id
|
2023-08-02 19:35:27 +01:00
|
|
|
})
|
|
|
|
|
.select();
|
2024-06-21 10:39:50 +01:00
|
|
|
console.log(`newCourse data`, newCourseData);
|
2021-12-30 22:30:30 +02:00
|
|
|
|
2024-06-21 10:39:50 +01:00
|
|
|
if (!newCourseData) return;
|
2023-10-30 06:04:53 +00:00
|
|
|
|
2024-06-21 10:39:50 +01:00
|
|
|
const newCourse = newCourseData[0];
|
|
|
|
|
courses.update((_courses) => [..._courses, newCourse]);
|
2021-12-30 22:30:30 +02:00
|
|
|
|
2023-10-30 06:04:53 +00:00
|
|
|
capturePosthogEvent('course_created', {
|
2024-06-21 10:39:50 +01:00
|
|
|
course_id: newCourse.id,
|
|
|
|
|
course_title: newCourse.title,
|
|
|
|
|
course_description: newCourse.description,
|
2023-10-30 06:04:53 +00:00
|
|
|
organization_id: $currentOrg.id,
|
|
|
|
|
organization_name: $currentOrg.name,
|
|
|
|
|
user_id: $profile.id,
|
|
|
|
|
user_email: $profile.email
|
|
|
|
|
});
|
2021-12-30 22:30:30 +02:00
|
|
|
|
2023-10-30 06:04:53 +00:00
|
|
|
// 3. Add group members
|
2024-02-28 03:00:31 +01:00
|
|
|
const { data } = await addGroupMember({
|
2021-12-30 22:30:30 +02:00
|
|
|
profile_id: $profile.id,
|
|
|
|
|
email: $profile.email,
|
|
|
|
|
group_id,
|
2023-08-07 06:18:30 +00:00
|
|
|
role_id: ROLE.TUTOR
|
2021-12-30 22:30:30 +02:00
|
|
|
});
|
2024-02-28 03:00:31 +01:00
|
|
|
|
|
|
|
|
// 4. Add default news feed.
|
|
|
|
|
if (Array.isArray(data) && data.length) {
|
|
|
|
|
const { id: authorId } = data[0];
|
|
|
|
|
console.log('Add news feed into course');
|
|
|
|
|
|
|
|
|
|
await addDefaultNewsFeed({
|
|
|
|
|
content: `<h2>Welcome to this course 🎉 </h2>
|
|
|
|
|
<p>Thank you for joining this course and I hope you get the best out of it.</p>`,
|
2024-06-21 10:39:50 +01:00
|
|
|
course_id: newCourse.id,
|
2024-02-28 03:00:31 +01:00
|
|
|
is_pinned: true,
|
|
|
|
|
author_id: authorId
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-21 10:39:50 +01:00
|
|
|
onClose(`/courses/${newCourse.id}`);
|
2023-10-03 12:29:06 +00:00
|
|
|
isLoading = false;
|
2021-12-30 22:30:30 +02:00
|
|
|
}
|
2024-05-21 00:14:47 +01:00
|
|
|
|
2025-11-01 15:49:29 +01:00
|
|
|
let open = $derived(new URLSearchParams(page.url.search).get('create') === 'true');
|
2021-12-30 22:30:30 +02:00
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<svelte:head>
|
2023-08-17 14:05:01 +01:00
|
|
|
<title>Create a new course</title>
|
2021-12-30 22:30:30 +02:00
|
|
|
</svelte:head>
|
|
|
|
|
|
|
|
|
|
<Modal
|
2025-11-01 15:49:29 +01:00
|
|
|
onClose={() => onClose(page.url.pathname)}
|
2024-05-21 00:14:47 +01:00
|
|
|
bind:open
|
|
|
|
|
width="w-4/5 md:w-2/5 md:min-w-[600px]"
|
2024-12-30 02:59:10 +01:00
|
|
|
containerClass="max-w-2xl mx-auto"
|
2024-04-19 05:45:48 +01:00
|
|
|
modalHeading={$t('courses.new_course_modal.heading')}
|
2021-12-30 22:30:30 +02:00
|
|
|
>
|
2024-05-21 00:14:47 +01:00
|
|
|
{#if step === 0}
|
|
|
|
|
<div>
|
2024-12-30 02:59:10 +01:00
|
|
|
<h2 class="my-5 text-xl font-medium">
|
2024-05-21 00:14:47 +01:00
|
|
|
{$t('courses.new_course_modal.type_selector_title')}
|
|
|
|
|
</h2>
|
2023-09-24 07:08:37 +00:00
|
|
|
|
2025-11-24 01:00:50 +01:00
|
|
|
<div class="my-8 flex flex-col items-center justify-evenly gap-4 md:flex-row">
|
2024-05-21 00:14:47 +01:00
|
|
|
{#each options as option}
|
|
|
|
|
<button
|
2025-11-21 12:18:04 +01:00
|
|
|
class="w-11/12 rounded-md border-2 p-5 md:h-60 md:w-[261px] dark:bg-neutral-700 {option.type === type
|
2024-05-21 00:14:47 +01:00
|
|
|
? 'border-primary-400'
|
|
|
|
|
: `border-gray-200 dark:border-neutral-600 ${
|
|
|
|
|
!option.isDisabled && 'hover:scale-95'
|
2025-11-01 15:49:29 +01:00
|
|
|
}`} flex flex-col {option.isDisabled && 'cursor-not-allowed opacity-60'} transition-all ease-in-out"
|
2024-05-21 00:14:47 +01:00
|
|
|
type="button"
|
2025-11-01 15:49:29 +01:00
|
|
|
onclick={!option.isDisabled ? () => (type = option.type) : undefined}
|
2024-05-21 00:14:47 +01:00
|
|
|
>
|
2024-12-30 02:59:10 +01:00
|
|
|
<div class="flex h-[70%] w-full flex-row-reverse">
|
2025-11-01 15:49:29 +01:00
|
|
|
<CircleCheckIcon size={16} filled={option.type === type} />
|
2024-05-21 00:14:47 +01:00
|
|
|
</div>
|
2021-12-30 22:30:30 +02:00
|
|
|
|
2024-05-21 00:14:47 +01:00
|
|
|
<div>
|
2025-11-21 12:18:04 +01:00
|
|
|
<p class="flex items-center text-start">
|
2024-05-21 00:14:47 +01:00
|
|
|
<span class="mr-2 text-sm">{option.title}</span>
|
|
|
|
|
{#if option.isDisabled}
|
|
|
|
|
<ComingSoon />
|
|
|
|
|
{/if}
|
|
|
|
|
</p>
|
2024-12-30 02:59:10 +01:00
|
|
|
<p class="text-start text-xs font-light">{option.subtitle}</p>
|
2024-05-21 00:14:47 +01:00
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
|
2024-12-30 02:59:10 +01:00
|
|
|
<div class="mt-8 flex flex-row-reverse items-center">
|
2024-05-21 00:14:47 +01:00
|
|
|
<PrimaryButton
|
|
|
|
|
className="px-6 py-3"
|
|
|
|
|
label={$t('courses.new_course_modal.next')}
|
|
|
|
|
onClick={() => (step = 1)}
|
|
|
|
|
isDisabled={!type}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2021-12-30 22:30:30 +02:00
|
|
|
</div>
|
2024-05-21 00:14:47 +01:00
|
|
|
{:else}
|
2025-11-01 15:49:29 +01:00
|
|
|
<form onsubmit={preventDefault(createCourse)}>
|
2024-12-30 02:59:10 +01:00
|
|
|
<div class="mb-4 flex items-end space-x-2">
|
2024-05-21 00:14:47 +01:00
|
|
|
<TextField
|
|
|
|
|
label={$t('courses.new_course_modal.course_name')}
|
|
|
|
|
bind:value={$createCourseModal.title}
|
|
|
|
|
placeholder={$t('courses.new_course_modal.course_name_placeholder')}
|
|
|
|
|
className="w-full "
|
|
|
|
|
isRequired={true}
|
|
|
|
|
errorMessage={errors.title}
|
|
|
|
|
autoComplete={false}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<TextArea
|
|
|
|
|
label={$t('courses.new_course_modal.short_description')}
|
|
|
|
|
bind:value={$createCourseModal.description}
|
|
|
|
|
rows={4}
|
|
|
|
|
placeholder={$t('courses.new_course_modal.short_description_placeholder')}
|
|
|
|
|
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 justify-between">
|
|
|
|
|
<PrimaryButton
|
|
|
|
|
className="px-6 py-3"
|
|
|
|
|
label={$t('courses.new_course_modal.back')}
|
|
|
|
|
variant={VARIANTS.OUTLINED}
|
|
|
|
|
onClick={() => (step = 0)}
|
|
|
|
|
/>
|
|
|
|
|
<PrimaryButton
|
|
|
|
|
className="px-6 py-3"
|
|
|
|
|
label={$t('courses.new_course_modal.button')}
|
|
|
|
|
type="submit"
|
|
|
|
|
isDisabled={isLoading}
|
|
|
|
|
{isLoading}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
{/if}
|
2021-12-30 22:30:30 +02:00
|
|
|
</Modal>
|