feat: setup drizzle (#594)
* chore: migration to SvelteKit 2 * stage 1: migrate to svelte 5 and fix rerendering issues * stage 2: fix more $: reactivity across dashboard * fix merge conflicts * chore: migrate code in main to svelte 5 * chore: update preventDefault * chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte * chore: enforce .ts across dashboard * rename from classroomio-com to website * stage-3: Replace carbon icon svelte with lucide icon * update lock file * chore: finish icon replacement * fix: settings and setup page multiple rerender * stage 3: More icon and rerendering fixes * feat: setup drizzle and better auth * fix: onboarding validation error showing translation key #586 (#593) * fix: onboarding validation error showing translation key #586 * fix: remove debug log for profile update errors * chore: add a clean command * fix: remove message from email verification response
This commit is contained in:
@@ -2,9 +2,7 @@
|
||||
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"
|
||||
>
|
||||
<div class="root relative mb-4 mr-4 w-72 rounded-md border bg-gray-100 transition ease-in-out dark:bg-black">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 10rem;" />
|
||||
<div class="p-3 pb-0">
|
||||
<!-- Title -->
|
||||
@@ -13,7 +11,7 @@
|
||||
<SkeletonText style="width: 100%; margin-top: 1rem;" />
|
||||
<SkeletonText style="width: 100%; margin-bottom: 1rem;" />
|
||||
|
||||
<div class="flex justify-between mb-3">
|
||||
<div class="mb-3 flex justify-between">
|
||||
<SkeletonText style="width: 25%" />
|
||||
<SkeletonText style="width: 25%" />
|
||||
</div>
|
||||
|
||||
@@ -6,41 +6,56 @@
|
||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import {
|
||||
ImageLoader,
|
||||
OverflowMenu,
|
||||
OverflowMenuItem,
|
||||
SkeletonPlaceholder,
|
||||
Tag
|
||||
} from 'carbon-components-svelte';
|
||||
import GrowthIcon from 'carbon-icons-svelte/lib/Growth.svelte';
|
||||
import RadioButtonChecked from 'carbon-icons-svelte/lib/RadioButtonChecked.svelte';
|
||||
import UserProfileIcon from 'carbon-icons-svelte/lib/UserProfile.svelte';
|
||||
import { ImageLoader, OverflowMenu, OverflowMenuItem, SkeletonPlaceholder, Tag } from 'carbon-components-svelte';
|
||||
import TrendingUpIcon from '@lucide/svelte/icons/trending-up';
|
||||
import CircleDotIcon from '@lucide/svelte/icons/circle-dot';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
import { copyCourseModal, deleteCourseModal } from '$lib/components/Courses/store';
|
||||
export let bannerImage: string | undefined;
|
||||
export let id = '';
|
||||
export let slug = '';
|
||||
export let title = '';
|
||||
export let description = '';
|
||||
export let isPublished = false;
|
||||
export let totalLessons = 0;
|
||||
export let totalStudents = 0;
|
||||
export let currency = 'USD';
|
||||
export let isOnLandingPage = false;
|
||||
export let isLMS = false;
|
||||
export let isExplore = false;
|
||||
export let progressRate = 45;
|
||||
export let type: COURSE_TYPE;
|
||||
export let pricingData: {
|
||||
cost: number;
|
||||
currency?: string;
|
||||
showDiscount?: boolean;
|
||||
discount?: number;
|
||||
} = {
|
||||
cost: 0
|
||||
};
|
||||
|
||||
$: formatter = getCurrencyFormatter(currency);
|
||||
interface Props {
|
||||
bannerImage: string | undefined;
|
||||
id?: string;
|
||||
slug?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
isPublished?: boolean;
|
||||
totalLessons?: number;
|
||||
totalStudents?: number;
|
||||
currency?: string;
|
||||
isOnLandingPage?: boolean;
|
||||
isLMS?: boolean;
|
||||
isExplore?: boolean;
|
||||
progressRate?: number;
|
||||
type: COURSE_TYPE;
|
||||
pricingData?: {
|
||||
cost: number;
|
||||
currency?: string;
|
||||
showDiscount?: boolean;
|
||||
discount?: number;
|
||||
};
|
||||
}
|
||||
|
||||
let {
|
||||
bannerImage,
|
||||
id = '',
|
||||
slug = '',
|
||||
title = '',
|
||||
description = '',
|
||||
isPublished = false,
|
||||
totalLessons = 0,
|
||||
totalStudents = 0,
|
||||
currency = 'USD',
|
||||
isOnLandingPage = false,
|
||||
isLMS = false,
|
||||
isExplore = false,
|
||||
progressRate = 45,
|
||||
type,
|
||||
pricingData = {
|
||||
cost: 0
|
||||
}
|
||||
}: Props = $props();
|
||||
|
||||
let formatter = $derived(getCurrencyFormatter(currency));
|
||||
|
||||
function handleCloneCourse() {
|
||||
$copyCourseModal.open = true;
|
||||
@@ -75,41 +90,36 @@
|
||||
[COURSE_TYPE.LIVE_CLASS]: {
|
||||
style: '',
|
||||
label: $t('course.navItem.settings.live_class'),
|
||||
icon: RadioButtonChecked,
|
||||
iconStyle: 'text-red-700'
|
||||
icon: CircleDotIcon,
|
||||
iconStyle: 'custom text-red-700'
|
||||
},
|
||||
[COURSE_TYPE.SELF_PACED]: {
|
||||
style: '',
|
||||
label: $t('course.navItem.settings.self_paced'),
|
||||
icon: UserProfileIcon,
|
||||
iconStyle: 'text-primary-700'
|
||||
icon: UserIcon,
|
||||
iconStyle: 'custom text-primary-700'
|
||||
},
|
||||
SPECIALIZATION: {
|
||||
style: '',
|
||||
label: $t('specialization.course_tag'),
|
||||
icon: GrowthIcon
|
||||
icon: TrendingUpIcon
|
||||
}
|
||||
};
|
||||
|
||||
$: cost = calcCourseDiscount(
|
||||
pricingData.discount,
|
||||
pricingData.cost ?? 0,
|
||||
!!pricingData.showDiscount
|
||||
);
|
||||
let cost = $derived(calcCourseDiscount(pricingData.discount, pricingData.cost ?? 0, !!pricingData.showDiscount));
|
||||
|
||||
$: courseUrl =
|
||||
isOnLandingPage || isExplore
|
||||
? `/course/${slug}`
|
||||
: `/courses/${id}${isLMS ? '/lessons?next=true' : ''}`;
|
||||
let courseUrl = $derived(
|
||||
isOnLandingPage || isExplore ? `/course/${slug}` : `/courses/${id}${isLMS ? '/lessons?next=true' : ''}`
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
on:click={(e) => {
|
||||
onclick={() => {
|
||||
goto(courseUrl);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
goto(courseUrl);
|
||||
}
|
||||
@@ -124,29 +134,10 @@
|
||||
size="sm"
|
||||
on:click={(e) => e.stopPropagation()}
|
||||
>
|
||||
<OverflowMenuItem
|
||||
text={$t('courses.course_card.context_menu.clone')}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCloneCourse();
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={$t('courses.course_card.context_menu.share')}
|
||||
on:click={handleShareCourse}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={$t('courses.course_card.context_menu.invite')}
|
||||
on:click={handleInvite}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('courses.course_card.context_menu.delete')}
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteCourse();
|
||||
}}
|
||||
/>
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.clone')} on:click={handleCloneCourse} />
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.share')} on:click={handleShareCourse} />
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.invite')} on:click={handleInvite} />
|
||||
<OverflowMenuItem danger text={$t('courses.course_card.context_menu.delete')} on:click={handleDeleteCourse} />
|
||||
</OverflowMenu>
|
||||
{/if}
|
||||
|
||||
@@ -158,14 +149,16 @@
|
||||
<svelte:fragment slot="loading">
|
||||
<SkeletonPlaceholder style="width: 100%; height: 200px;" />
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="error">{$t('courses.course_card.error_message')}</svelte:fragment>
|
||||
<svelte:fragment slot="errror">
|
||||
{$t('courses.course_card.error_message')}
|
||||
</svelte:fragment>
|
||||
</ImageLoader>
|
||||
{#if type}
|
||||
{@const tag = COURSE_TAG[type]}
|
||||
<span
|
||||
class="bg-primary-50 absolute bottom-2 left-2 z-10 flex items-center gap-1 rounded-sm p-1 font-mono text-xs capitalize"
|
||||
>
|
||||
<svelte:component this={tag.icon} size={16} class={tag.iconStyle} />
|
||||
<tag.icon class={tag.iconStyle} />
|
||||
{tag.label}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -204,10 +197,7 @@
|
||||
{#if !isExplore}
|
||||
<div class="flex items-center gap-2">
|
||||
<div class=" relative h-1 w-[50px] bg-[#EAEAEA]">
|
||||
<div
|
||||
style="width:{progressRate}%"
|
||||
class={`bg-primary-700 absolute left-0 top-0 h-full`}
|
||||
/>
|
||||
<div style="width:{progressRate}%" class="bg-primary-700 absolute left-0 top-0 h-full"></div>
|
||||
</div>
|
||||
<p class="text-xs text-[#656565] dark:text-white">{progressRate}%</p>
|
||||
</div>
|
||||
@@ -226,9 +216,7 @@
|
||||
|
||||
{#if isLMS}
|
||||
<PrimaryButton
|
||||
label={isExplore
|
||||
? $t('courses.course_card.learn_more')
|
||||
: $t('courses.course_card.continue_course')}
|
||||
label={isExplore ? $t('courses.course_card.learn_more') : $t('courses.course_card.continue_course')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
className="rounded-none"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
@@ -11,12 +13,6 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface CloneCourseResponse {
|
||||
success: boolean;
|
||||
course: { id: string };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
// clone, show spinner and redirect to new course
|
||||
async function createCourse() {
|
||||
// Prevent double submission (Option 1: Early Return Guard)
|
||||
@@ -39,7 +35,7 @@
|
||||
}
|
||||
});
|
||||
|
||||
const result = (await response.json()) as CloneCourseResponse;
|
||||
const result = (await response.json());
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(result.error || $t('courses.copy_course.error.failed_to_clone'));
|
||||
@@ -71,7 +67,7 @@
|
||||
width="w-96"
|
||||
modalHeading={$t('courses.copy_course.title')}
|
||||
>
|
||||
<form on:submit|preventDefault={createCourse}>
|
||||
<form onsubmit={preventDefault(createCourse)}>
|
||||
<TextField
|
||||
label={$t('courses.copy_course.course_name_label')}
|
||||
bind:value={$copyCourseModal.title}
|
||||
@@ -96,7 +92,6 @@
|
||||
label={$t('courses.copy_course.create_button')}
|
||||
type="submit"
|
||||
isLoading={$copyCourseModal.isSaving}
|
||||
isDisabled={$copyCourseModal.isSaving}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import { SkeletonText } from 'carbon-components-svelte';
|
||||
</script>
|
||||
|
||||
<div class="my-2">
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
StructuredListRow,
|
||||
StructuredListCell,
|
||||
Tag,
|
||||
OverflowMenuItem,
|
||||
OverflowMenu
|
||||
} from 'carbon-components-svelte';
|
||||
import { StructuredListRow, StructuredListCell, Tag, OverflowMenuItem, OverflowMenu } from 'carbon-components-svelte';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { goto } from '$app/navigation';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export let id = '';
|
||||
export let title = '';
|
||||
export let type = '';
|
||||
export let description = '';
|
||||
export let isPublished = false;
|
||||
export let totalLessons = 0;
|
||||
export let totalStudents = 0;
|
||||
interface Props {
|
||||
id?: string;
|
||||
title?: string;
|
||||
type?: string;
|
||||
description?: string;
|
||||
isPublished?: boolean;
|
||||
totalLessons?: number;
|
||||
totalStudents?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
id = '',
|
||||
title = '',
|
||||
type = '',
|
||||
description = '',
|
||||
isPublished = false,
|
||||
totalLessons = 0,
|
||||
totalStudents = 0
|
||||
}: Props = $props();
|
||||
|
||||
function handleCloneCourse(e) {
|
||||
e.stopPropagation();
|
||||
@@ -70,23 +76,10 @@
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<OverflowMenuItem
|
||||
text={$t('courses.course_card.context_menu.clone')}
|
||||
on:click={handleCloneCourse}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={$t('courses.course_card.context_menu.share')}
|
||||
on:click={handleShareCourse}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
text={$t('courses.course_card.context_menu.invite')}
|
||||
on:click={handleInvite}
|
||||
/>
|
||||
<OverflowMenuItem
|
||||
danger
|
||||
text={$t('courses.course_card.context_menu.delete')}
|
||||
on:click={handleDeleteCourse}
|
||||
/>
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.clone')} on:click={handleCloneCourse} />
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.share')} on:click={handleShareCourse} />
|
||||
<OverflowMenuItem text={$t('courses.course_card.context_menu.invite')} on:click={handleInvite} />
|
||||
<OverflowMenuItem danger text={$t('courses.course_card.context_menu.delete')} on:click={handleDeleteCourse} />
|
||||
</OverflowMenu>
|
||||
</StructuredListCell>
|
||||
</StructuredListRow>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/stores';
|
||||
import { page } from '$app/state';
|
||||
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
|
||||
import { validateForm } from '$lib/components/Courses/functions';
|
||||
import { courses, createCourseModal } from '$lib/components/Courses/store';
|
||||
@@ -17,15 +19,14 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { COURSE_TYPE, COURSE_VERSION } from '$lib/utils/types';
|
||||
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
||||
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
let isLoading = false;
|
||||
let errors = {
|
||||
let isLoading = $state(false);
|
||||
let errors = $state({
|
||||
title: '',
|
||||
description: ''
|
||||
};
|
||||
let step = 0;
|
||||
});
|
||||
let step = $state(0);
|
||||
|
||||
const options = [
|
||||
{
|
||||
@@ -43,7 +44,7 @@
|
||||
isDisabled: false
|
||||
}
|
||||
];
|
||||
let type = options[0].type;
|
||||
let type = $state(options[0].type);
|
||||
|
||||
function onClose(redirectTo) {
|
||||
goto(redirectTo);
|
||||
@@ -132,7 +133,7 @@
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
$: open = new URLSearchParams($page.url.search).get('create') === 'true';
|
||||
let open = $derived(new URLSearchParams(page.url.search).get('create') === 'true');
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -140,7 +141,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<Modal
|
||||
onClose={() => onClose($page.url.pathname)}
|
||||
onClose={() => onClose(page.url.pathname)}
|
||||
bind:open
|
||||
width="w-4/5 md:w-2/5 md:min-w-[600px]"
|
||||
containerClass="max-w-2xl mx-auto"
|
||||
@@ -155,25 +156,16 @@
|
||||
<div class="my-8 flex flex-col items-center justify-evenly gap-4 md:flex-row">
|
||||
{#each options as option}
|
||||
<button
|
||||
class="w-11/12 rounded-md border-2 p-5 dark:bg-neutral-700 md:h-[240px] md:w-[261px] {option.type ===
|
||||
type
|
||||
class="w-11/12 rounded-md border-2 p-5 md:h-[240px] md:w-[261px] dark:bg-neutral-700 {option.type === type
|
||||
? 'border-primary-400'
|
||||
: `border-gray-200 dark:border-neutral-600 ${
|
||||
!option.isDisabled && 'hover:scale-95'
|
||||
}`} flex flex-col {option.isDisabled &&
|
||||
'cursor-not-allowed opacity-60'} transition-all ease-in-out"
|
||||
}`} flex flex-col {option.isDisabled && 'cursor-not-allowed opacity-60'} transition-all ease-in-out"
|
||||
type="button"
|
||||
on:click={!option.isDisabled ? () => (type = option.type) : undefined}
|
||||
onclick={!option.isDisabled ? () => (type = option.type) : undefined}
|
||||
>
|
||||
<div class="flex h-[70%] w-full flex-row-reverse">
|
||||
{#if option.type === type}
|
||||
<CheckmarkFilledIcon
|
||||
size={16}
|
||||
class="carbon-icon text-primary-600 dark:text-primary-200"
|
||||
/>
|
||||
{:else if !option.isDisabled}
|
||||
<CheckmarkOutlineIcon size={16} class="carbon-icon" />
|
||||
{/if}
|
||||
<CircleCheckIcon size={16} filled={option.type === type} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -199,7 +191,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<form on:submit|preventDefault={createCourse}>
|
||||
<form onsubmit={preventDefault(createCourse)}>
|
||||
<div class="mb-4 flex items-end space-x-2">
|
||||
<TextField
|
||||
label={$t('courses.new_course_modal.course_name')}
|
||||
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
import { validateEmail } from '$lib/utils/functions/validateEmail';
|
||||
|
||||
export function validateForm(fields) {
|
||||
let hasError = false;
|
||||
const { title, description } = fields;
|
||||
@@ -20,6 +20,20 @@
|
||||
import { deleteCourse } from '$lib/utils/services/courses';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { deleteCourseModal, deleteCourseModalInitialState, courses as coursesStore } from './store';
|
||||
|
||||
interface Props {
|
||||
courses?: Course[];
|
||||
emptyTitle?: any;
|
||||
emptyDescription?: any;
|
||||
isExplore?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
courses = [],
|
||||
emptyTitle = $t('courses.course_card.empty_title'),
|
||||
emptyDescription = $t('courses.course_card.empty_description'),
|
||||
isExplore = false
|
||||
}: Props = $props();
|
||||
|
||||
export let courses: Course[] = [];
|
||||
export let emptyTitle = $t('courses.course_card.empty_title');
|
||||
@@ -96,7 +110,7 @@
|
||||
<StructuredListCell head>
|
||||
{$t('courses.course_card.list_view.published')}
|
||||
</StructuredListCell>
|
||||
<StructuredListCell head>{''}</StructuredListCell>
|
||||
<StructuredListCell head></StructuredListCell>
|
||||
</StructuredListRow>
|
||||
</StructuredListHead>
|
||||
<StructuredListBody>
|
||||
@@ -139,7 +153,7 @@
|
||||
</div>
|
||||
{#if !$courseMetaDeta.isLoading && !courses.length}
|
||||
<Box className="w-full">
|
||||
<CoursesEmptyIcon />
|
||||
<CoursesEmptyIcon size={16} />
|
||||
<h3 class="my-5 text-2xl dark:text-white">{emptyTitle}</h3>
|
||||
<p class="w-1/3 text-center dark:text-white">
|
||||
{emptyDescription}
|
||||
|
||||
Reference in New Issue
Block a user