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:
@@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { page } from '$app/stores';
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import AddOrgModal from '$lib/components/Org/AddOrgModal/AddOrgModal.svelte';
|
||||
import { isQuizPage } from '$lib/utils/functions/app';
|
||||
import OrgSideBar from '$lib/components/Org/SideBar.svelte';
|
||||
@@ -8,29 +8,31 @@
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
let { data, children } = $props();
|
||||
|
||||
let ref = null;
|
||||
|
||||
$: if ($currentOrg.id && data.orgName === '*') {
|
||||
const newUrl = $page.url.pathname.replace('*', $currentOrg.siteName);
|
||||
goto(newUrl + $page.url.search);
|
||||
function redirect(siteName: string) {
|
||||
const newUrl = page.url.pathname.replace('*', siteName);
|
||||
goto(newUrl + page.url.search);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
data.orgName === '*' && redirect($currentOrg.siteName);
|
||||
});
|
||||
</script>
|
||||
|
||||
<AddOrgModal />
|
||||
|
||||
<VerifyEmailModal />
|
||||
|
||||
<div class="org-root w-full flex items-center justify-between">
|
||||
{#if !isQuizPage($page.url?.pathname)}
|
||||
<div class="org-root flex w-full items-center justify-between">
|
||||
{#if !isQuizPage(page.url?.pathname)}
|
||||
<OrgSideBar />
|
||||
{/if}
|
||||
<div class="org-slot bg-white dark:bg-black w-full">
|
||||
<div class="org-slot w-full bg-white dark:bg-black">
|
||||
{#if data.orgName === '*'}
|
||||
<Box>Taking you to your organization...</Box>
|
||||
{:else}
|
||||
<slot />
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { ActivityCard } from '$lib/components/Analytics';
|
||||
import CourseIcon from '$lib/components/Icons/CourseIcon.svelte';
|
||||
import Progress from '$lib/components/Progress/index.svelte';
|
||||
import UserProfile from 'carbon-icons-svelte/lib/UserProfile.svelte';
|
||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||
import UserIcon from '@lucide/svelte/icons/user';
|
||||
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
@@ -11,10 +11,11 @@
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { OrganisationAnalytics } from '$lib/utils/types/analytics';
|
||||
import Add from 'carbon-icons-svelte/lib/Add.svelte';
|
||||
import Book from 'carbon-icons-svelte/lib/Book.svelte';
|
||||
import CurrencyDollar from 'carbon-icons-svelte/lib/CurrencyDollar.svelte';
|
||||
import UserMultiple from 'carbon-icons-svelte/lib/UserMultiple.svelte';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
|
||||
import BookIcon from '@lucide/svelte/icons/book';
|
||||
import DollarSignIcon from '@lucide/svelte/icons/dollar-sign';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
@@ -27,16 +28,17 @@
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { Grid, Link, SkeletonPlaceholder } from 'carbon-components-svelte';
|
||||
|
||||
// export let data;
|
||||
|
||||
let dashAnalytics: OrganisationAnalytics;
|
||||
let dashAnalytics: OrganisationAnalytics | undefined = $state();
|
||||
let hasFetched = $state(false);
|
||||
|
||||
function createCourse() {
|
||||
goto(`${$currentOrgPath}/courses?create=true`);
|
||||
}
|
||||
|
||||
async function fetchDashAnalytics(orgId: string) {
|
||||
if (!orgId) return;
|
||||
if (!orgId || hasFetched) return;
|
||||
|
||||
hasFetched = true;
|
||||
|
||||
const accessToken = await getAccessToken();
|
||||
|
||||
@@ -61,31 +63,33 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: fetchDashAnalytics($currentOrg.id);
|
||||
$effect(() => {
|
||||
fetchDashAnalytics($currentOrg.id);
|
||||
});
|
||||
|
||||
$: cards = [
|
||||
let cards = $derived([
|
||||
{
|
||||
icon: CurrencyDollar,
|
||||
icon: DollarSignIcon,
|
||||
title: `${$t('dashboard.revenue')} ($)`,
|
||||
percentage: dashAnalytics?.revenue ?? 0,
|
||||
description: $t('dashboard.revenue_description'),
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: Book,
|
||||
icon: BookIcon,
|
||||
title: $t('dashboard.no_of_courses'),
|
||||
percentage: dashAnalytics?.numberOfCourses ?? 0,
|
||||
description: $t('dashboard.no_courses_description'),
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: UserMultiple,
|
||||
icon: UsersIcon,
|
||||
title: $t('dashboard.total_students'),
|
||||
percentage: dashAnalytics?.totalStudents ?? 0,
|
||||
description: $t('dashboard.total_students_description'),
|
||||
hidePercentage: true
|
||||
}
|
||||
];
|
||||
]);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -96,7 +100,7 @@
|
||||
|
||||
<div class="w-full max-w-5xl px-5 py-10 md:mx-auto">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h1 class="mb-3 text-2xl font-bold dark:text-white md:text-3xl">
|
||||
<h1 class="mb-3 text-2xl dark:text-white">
|
||||
{$t(getGreeting())}
|
||||
{$profile.fullname}!
|
||||
</h1>
|
||||
@@ -108,7 +112,7 @@
|
||||
className="min-h-[36px]"
|
||||
>
|
||||
{#if $isMobile}
|
||||
<Add size={24} />
|
||||
<PlusIcon size={16} />
|
||||
{:else}
|
||||
{$t('dashboard.create_course')}
|
||||
{/if}
|
||||
@@ -123,10 +127,7 @@
|
||||
<div class="mt-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{#each cards as card}
|
||||
{#if !dashAnalytics}
|
||||
<SkeletonPlaceholder
|
||||
style="width: 100%; min-width: 300px; height: 10rem;"
|
||||
class="rounded-md"
|
||||
/>
|
||||
<SkeletonPlaceholder style="width: 100%; min-width: 300px; height: 10rem;" class="rounded-md" />
|
||||
{:else}
|
||||
<ActivityCard activity={card} />
|
||||
{/if}
|
||||
@@ -136,9 +137,7 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div
|
||||
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 dark:border-neutral-600 md:p-5"
|
||||
>
|
||||
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600">
|
||||
<h3 class="mt-0 text-2xl font-bold">
|
||||
{$t('dashboard.top_courses')}
|
||||
</h3>
|
||||
@@ -173,7 +172,7 @@
|
||||
{:else}
|
||||
<div class="flex flex-col h-full items-center justify-center p-3">
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<CourseIcon width="30" height="30" />
|
||||
<LibraryBigIcon size={30} />
|
||||
</div>
|
||||
<div class="my-4 text-center">
|
||||
<p class=" text-xl font-semibold">
|
||||
@@ -183,20 +182,14 @@
|
||||
{$t('dashboard.create_first_course_description')}
|
||||
</p>
|
||||
</div>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={createCourse}
|
||||
label={$t('dashboard.create_course')}
|
||||
/>
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} onClick={createCourse} label={$t('dashboard.create_course')} />
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 dark:border-neutral-600 md:p-5"
|
||||
>
|
||||
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600">
|
||||
<h3 class="mt-0 text-2xl font-bold">
|
||||
{$t('dashboard.recent_enrollments')}
|
||||
</h3>
|
||||
@@ -210,12 +203,7 @@
|
||||
{#each dashAnalytics.enrollments as enrollment}
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Avatar
|
||||
src={enrollment.avatarUrl}
|
||||
name={enrollment.name}
|
||||
width="w-6"
|
||||
height="h-6"
|
||||
/>
|
||||
<Avatar src={enrollment.avatarUrl} name={enrollment.name} width="w-6" height="h-6" />
|
||||
|
||||
<div class="min-h-[45px] space-y-1">
|
||||
<p class="text-sm font-medium capitalize leading-none">{enrollment.name}</p>
|
||||
@@ -238,7 +226,7 @@
|
||||
{:else}
|
||||
<div class="flex flex-col h-full items-center justify-center p-3">
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<UserProfile size={24} />
|
||||
<UserIcon size={16} />
|
||||
</div>
|
||||
<div class="my-4 text-center">
|
||||
<p class=" text-xl font-semibold">
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
import Audience from '$lib/components/Org/Audience/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { orgAudience, currentOrgPlan, currentOrgMaxAudience } from '$lib/utils/store/org';
|
||||
import { PLAN } from 'shared/src/plans/constants';
|
||||
import { PLAN } from '@cio/utils/plans';
|
||||
import UpgradeBanner from '$lib/components/Upgrade/Banner.svelte';
|
||||
|
||||
let isLoading = false;
|
||||
let isLoading = $state(false);
|
||||
|
||||
function exportAudience() {
|
||||
isLoading = true;
|
||||
@@ -19,23 +19,18 @@
|
||||
<title>Audience - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<section class="mx-auto w-full max-w-4xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<div class="flex items-end">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold m-0">{$t('audience.title')}</h1>
|
||||
<h1 class="m-0 text-2xl font-bold md:text-3xl dark:text-white">{$t('audience.title')}</h1>
|
||||
{#if $currentOrgPlan?.plan_name !== PLAN.ENTERPRISE}
|
||||
<span class="ml-2">
|
||||
({$orgAudience.length} / {$currentOrgMaxAudience})
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<PrimaryButton
|
||||
label={$t('audience.export')}
|
||||
onClick={exportAudience}
|
||||
isDisabled={isLoading}
|
||||
{isLoading}
|
||||
/>
|
||||
<PrimaryButton label={$t('audience.export')} onClick={exportAudience} isDisabled={isLoading} {isLoading} />
|
||||
</div>
|
||||
|
||||
{#if $orgAudience.length >= $currentOrgMaxAudience}
|
||||
|
||||
@@ -7,18 +7,18 @@
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import type { UserAnalytics } from '$lib/utils/types/analytics';
|
||||
import { Grid, Tag } from 'carbon-components-svelte';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import Notebook from 'carbon-icons-svelte/lib/Notebook.svelte';
|
||||
import Report from 'carbon-icons-svelte/lib/Report.svelte';
|
||||
import RowExpand from 'carbon-icons-svelte/lib/RowExpand.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
||||
import UnfoldVerticalIcon from '@lucide/svelte/icons/unfold-vertical';
|
||||
import { onMount } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let userAnalytics: UserAnalytics;
|
||||
let userAnalytics: UserAnalytics | undefined = $state();
|
||||
|
||||
let courseFilter = 'all';
|
||||
let courseFilter = $state('all');
|
||||
function toggleCourseFilter(filter: 'completed' | 'incomplete') {
|
||||
if (courseFilter === filter) {
|
||||
courseFilter = 'all';
|
||||
@@ -50,51 +50,50 @@
|
||||
fetchUserAnalytics();
|
||||
});
|
||||
|
||||
$: userMetrics = [
|
||||
let userMetrics = $derived([
|
||||
{
|
||||
icon: Notebook,
|
||||
icon: BookOpenIcon,
|
||||
title: $t('analytics.enrolled_courses'),
|
||||
description: $t('analytics.enrolled_courses_description'),
|
||||
percentage: userAnalytics?.courses?.length,
|
||||
hidePercentage: true
|
||||
},
|
||||
{
|
||||
icon: Report,
|
||||
icon: ChartLineIcon,
|
||||
title: $t('analytics.overall_course_progress'),
|
||||
description: $t('analytics.overall_course_progress_description'),
|
||||
percentage: userAnalytics?.overallCourseProgress
|
||||
},
|
||||
{
|
||||
icon: RowExpand,
|
||||
icon: UnfoldVerticalIcon,
|
||||
title: $t('analytics.total_average_grade'),
|
||||
description: $t('analytics.total_average_grade_description'),
|
||||
percentage: userAnalytics?.overallAverageGrade
|
||||
}
|
||||
];
|
||||
]);
|
||||
|
||||
$: completedCourses = userAnalytics?.courses?.filter(
|
||||
(course) => course.lessons_count === course.lessons_completed
|
||||
)?.length;
|
||||
$: incompleteCourses = userAnalytics?.courses?.filter(
|
||||
(course) => course.lessons_count !== course.lessons_completed
|
||||
)?.length;
|
||||
let completedCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => course.lessons_count === course.lessons_completed)?.length
|
||||
);
|
||||
let incompleteCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => course.lessons_count !== course.lessons_completed)?.length
|
||||
);
|
||||
|
||||
$: filteredCourses = userAnalytics?.courses?.filter((course) => {
|
||||
if (courseFilter === 'all') {
|
||||
return true;
|
||||
}
|
||||
return (course.lessons_count === course.lessons_completed) === (courseFilter === 'completed');
|
||||
});
|
||||
let filteredCourses = $derived(
|
||||
userAnalytics?.courses?.filter((course) => {
|
||||
if (courseFilter === 'all') {
|
||||
return true;
|
||||
}
|
||||
return (course.lessons_count === course.lessons_completed) === (courseFilter === 'completed');
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if userAnalytics}
|
||||
<section class="w-full md:mx-auto md:max-w-5xl">
|
||||
<div class="p-5">
|
||||
<a
|
||||
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||
href={`${$currentOrgPath}/audience`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/audience`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import Add from 'carbon-icons-svelte/lib/Add.svelte';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
function askCommunity() {
|
||||
@@ -17,20 +17,16 @@
|
||||
<title>Community - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl md:mx-auto">
|
||||
<div class="w-full py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10 w-full">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('community.title')}</h1>
|
||||
<section class="w-full md:mx-auto md:max-w-4xl">
|
||||
<div class="w-full px-5 py-10">
|
||||
<div class="mb-10 flex w-full items-center justify-between">
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('community.title')}</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton onClick={askCommunity}>
|
||||
<Add size={24} />
|
||||
<PlusIcon size={16} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
label={$t('community.ask_button')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={askCommunity}
|
||||
/>
|
||||
<PrimaryButton label={$t('community.ask_button')} variant={VARIANTS.CONTAINED_DARK} onClick={askCommunity} />
|
||||
{/if}
|
||||
</div>
|
||||
<Community />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
import Avatar from '$lib/components/Avatar/index.svelte';
|
||||
import { courses } from '$lib/components/Courses/store';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import DeleteModal from '$lib/components/Org/Community/DeleteModal.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
@@ -14,21 +14,18 @@
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import {
|
||||
askCommunityValidation,
|
||||
commentInCommunityValidation
|
||||
} from '$lib/utils/functions/validator';
|
||||
import { askCommunityValidation, commentInCommunityValidation } from '$lib/utils/functions/validator';
|
||||
import { fetchCourses } from '$lib/utils/services/courses';
|
||||
import { currentOrg, currentOrgPath, isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { Dropdown, SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import CheckmarkOutlineIcon from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||
import TrashCanIcon from 'carbon-icons-svelte/lib/TrashCan.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
const { slug } = data;
|
||||
|
||||
interface Comment {
|
||||
@@ -56,11 +53,11 @@
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
let question: Question;
|
||||
let comment = '';
|
||||
let question: Question | undefined = $state();
|
||||
let comment = $state('');
|
||||
let errors: {
|
||||
title?: string;
|
||||
} = {};
|
||||
} = $state({});
|
||||
let isValidAnswer = false; // V2 allow admin mark an answer as accepted
|
||||
let resetInput = 1;
|
||||
let voted: {
|
||||
@@ -68,26 +65,26 @@
|
||||
comment: {
|
||||
[key: string]: boolean;
|
||||
};
|
||||
} = { question: false, comment: {} };
|
||||
let isEditMode = false;
|
||||
let deleteComment = {
|
||||
} = $state({ question: false, comment: {} });
|
||||
let isEditMode = $state(false);
|
||||
let deleteComment = $state({
|
||||
shouldDelete: false,
|
||||
commentId: '',
|
||||
isDeleting: false
|
||||
};
|
||||
let deleteQuestion = {
|
||||
});
|
||||
let deleteQuestion = $state({
|
||||
shouldDelete: false,
|
||||
questionId: '',
|
||||
isDeleting: false
|
||||
};
|
||||
let editContent = {
|
||||
});
|
||||
let editContent = $state({
|
||||
title: '',
|
||||
body: '',
|
||||
courseId: ''
|
||||
};
|
||||
});
|
||||
|
||||
let editorInstance = false;
|
||||
let fetchedCourses: Course[] = [];
|
||||
let editorInstance = $state(false);
|
||||
let fetchedCourses: Course[] = $state([]);
|
||||
|
||||
function mapResToQuestion(data): Question {
|
||||
return {
|
||||
@@ -115,14 +112,22 @@
|
||||
};
|
||||
}
|
||||
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
if ($courses.length) {
|
||||
fetchedCourses = [...$courses];
|
||||
return;
|
||||
}
|
||||
let hasFetched = $state(false);
|
||||
function getCourses(userId: string | null, orgId: string) {
|
||||
if (hasFetched) return;
|
||||
|
||||
const coursesResults = await fetchCourses(userId, orgId);
|
||||
fetchedCourses = coursesResults?.allCourses || [];
|
||||
untrack(async () => {
|
||||
if ($courses.length) {
|
||||
fetchedCourses = [...$courses];
|
||||
hasFetched = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const coursesResults = await fetchCourses(userId, orgId);
|
||||
fetchedCourses = coursesResults?.allCourses || [];
|
||||
|
||||
hasFetched = true;
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchCommunityQuestion(slug: string) {
|
||||
@@ -157,18 +162,20 @@
|
||||
|
||||
if (error) {
|
||||
console.error('[ORG] Error loading community', error);
|
||||
return goto(`${currentOrgPath}`);
|
||||
return goto(`${$currentOrgPath}`);
|
||||
}
|
||||
|
||||
question = mapResToQuestion(data);
|
||||
question.totalComments = question.comments.length;
|
||||
untrack(() => {
|
||||
question = mapResToQuestion(data);
|
||||
question.totalComments = question.comments.length;
|
||||
});
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
errors = commentInCommunityValidation({ comment });
|
||||
console.log('submitComment errors', errors);
|
||||
|
||||
if (Object.keys(errors).length) {
|
||||
if (Object.keys(errors).length || !question) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,6 +220,8 @@
|
||||
}
|
||||
|
||||
async function upvoteQuestion(type: string, commentId?: string) {
|
||||
if (!question) return;
|
||||
|
||||
const isQuestion = type === 'question';
|
||||
|
||||
if (isQuestion && voted.question) return;
|
||||
@@ -248,6 +257,8 @@
|
||||
}
|
||||
|
||||
async function handleQuestionEdit() {
|
||||
if (!question) return;
|
||||
|
||||
if (isEditMode) {
|
||||
errors = askCommunityValidation(editContent);
|
||||
console.log('handleQuestionEdit errors', errors);
|
||||
@@ -295,13 +306,12 @@
|
||||
}
|
||||
|
||||
async function handleDelete(isQuestion: boolean) {
|
||||
if (!question) return;
|
||||
|
||||
if (!isQuestion) {
|
||||
deleteComment.isDeleting = true;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('community_answer')
|
||||
.delete()
|
||||
.match({ id: deleteComment.commentId });
|
||||
const { error } = await supabase.from('community_answer').delete().match({ id: deleteComment.commentId });
|
||||
|
||||
deleteComment.isDeleting = false;
|
||||
|
||||
@@ -350,12 +360,14 @@
|
||||
deleteQuestion.isDeleting = false;
|
||||
}
|
||||
|
||||
$: browser && fetchCommunityQuestion(slug);
|
||||
$: {
|
||||
$effect(() => {
|
||||
fetchCommunityQuestion(slug);
|
||||
});
|
||||
$effect(() => {
|
||||
if ($profile.id && $currentOrg.id) {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -364,7 +376,7 @@
|
||||
|
||||
<DeleteModal
|
||||
bind:open={deleteQuestion.shouldDelete}
|
||||
bind:isDeleting={deleteQuestion.isDeleting}
|
||||
isDeleting={deleteQuestion.isDeleting}
|
||||
onCancel={() => {
|
||||
deleteQuestion.shouldDelete = false;
|
||||
deleteQuestion.questionId = '';
|
||||
@@ -391,20 +403,13 @@
|
||||
</div>
|
||||
{:else}
|
||||
<div class="px-5 py-10">
|
||||
<a
|
||||
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||
href={`${$currentOrgPath}/community`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/community`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="my-5 flex items-center justify-between">
|
||||
{#if isEditMode}
|
||||
<TextField
|
||||
bind:value={editContent.title}
|
||||
className="w-full mr-2"
|
||||
errorMessage={errors.title}
|
||||
/>
|
||||
<TextField bind:value={editContent.title} className="w-full mr-2" errorMessage={errors.title} />
|
||||
<Dropdown
|
||||
class="h-full w-[25%]"
|
||||
size="xl"
|
||||
@@ -414,11 +419,7 @@
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex items-center">
|
||||
<Vote
|
||||
value={question.votes}
|
||||
upVote={() => upvoteQuestion('question')}
|
||||
disabled={voted.question}
|
||||
/>
|
||||
<Vote value={question.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
|
||||
<h2 class="text-3xl">{question.title}</h2>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -444,12 +445,7 @@
|
||||
<div class="border-1 border-gray my-1 rounded-lg border px-1">
|
||||
<header class="flex items-center justify-between p-2 leading-none">
|
||||
<div class="flex items-center text-black no-underline hover:underline">
|
||||
<Avatar
|
||||
src={question.author.avatar}
|
||||
name={question.author.name}
|
||||
width="w-7"
|
||||
height="h-7"
|
||||
/>
|
||||
<Avatar src={question.author.avatar} name={question.author.name} width="w-7" height="h-7" />
|
||||
<p class="ml-2 text-sm dark:text-white">{question.author.name}</p>
|
||||
<p class="ml-2 text-sm text-gray-500 dark:text-white">
|
||||
{question.createdAt}
|
||||
@@ -459,11 +455,13 @@
|
||||
<IconButton
|
||||
value="delete-question"
|
||||
onClick={() => {
|
||||
if (!question) return;
|
||||
|
||||
deleteQuestion.shouldDelete = true;
|
||||
deleteQuestion.questionId = question.id;
|
||||
}}
|
||||
>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
@@ -504,7 +502,7 @@
|
||||
</div>
|
||||
|
||||
{#if isValidAnswer}
|
||||
<CheckmarkOutlineIcon size={20} />
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
|
||||
{#if comment.authorId === $profile.id || $isOrgAdmin}
|
||||
@@ -515,7 +513,7 @@
|
||||
deleteComment.commentId = comment.id;
|
||||
}}
|
||||
>
|
||||
<TrashCanIcon size={16} class="carbon-icon dark:text-white" />
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
@@ -15,24 +15,28 @@
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { Dropdown } from 'carbon-components-svelte';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
|
||||
let errors: {
|
||||
title?: string;
|
||||
courseId?: string;
|
||||
body?: string;
|
||||
} = {};
|
||||
let fields = {
|
||||
} = $state({});
|
||||
let fields = $state({
|
||||
title: '',
|
||||
body: '',
|
||||
courseId: ''
|
||||
};
|
||||
});
|
||||
|
||||
let fetchedCourses: Course[] = [];
|
||||
let hasFetched = $state(false);
|
||||
let fetchedCourses: Course[] = $state([]);
|
||||
|
||||
async function getCourses(userId: string | null, orgId: string) {
|
||||
if (hasFetched) return;
|
||||
|
||||
if ($courses.length) {
|
||||
fetchedCourses = [...$courses];
|
||||
hasFetched = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -40,6 +44,7 @@
|
||||
if (!coursesResults) return;
|
||||
|
||||
fetchedCourses = coursesResults.allCourses;
|
||||
hasFetched = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
@@ -75,11 +80,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$effect(() => {
|
||||
if ($profile.id && $currentOrg.id) {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -88,20 +93,13 @@
|
||||
|
||||
<section class="w-full md:mx-auto md:max-w-3xl">
|
||||
<div class="p-5">
|
||||
<a
|
||||
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||
href={`${$currentOrgPath}/community`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" />
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/community`}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
{$t('community.ask.go_back')}
|
||||
</a>
|
||||
<div class="flex items-center justify-between gap-12">
|
||||
<h1 class="text-2xl font-bold dark:text-white md:text-3xl">{$t('community.ask.ask_the')}</h1>
|
||||
<PrimaryButton
|
||||
label={$t('community.ask.publish')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('community.ask.ask_the')}</h1>
|
||||
<PrimaryButton label={$t('community.ask.publish')} variant={VARIANTS.CONTAINED_DARK} onClick={handleSave} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,27 +7,28 @@
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { courses, courseMetaDeta } from '$lib/components/Courses/store';
|
||||
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
|
||||
import { Add } from 'carbon-icons-svelte';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { browser } from '$app/environment';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import IconButton from '$lib/components/IconButton/index.svelte';
|
||||
import Grid from 'carbon-icons-svelte/lib/Grid.svelte';
|
||||
import List from 'carbon-icons-svelte/lib/List.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import GridIcon from '@lucide/svelte/icons/grid-2x2';
|
||||
import ListIcon from '@lucide/svelte/icons/list';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
|
||||
let { cantFetch } = data;
|
||||
let searchValue = '';
|
||||
let selectedId: string;
|
||||
let filteredCourses: Course[];
|
||||
let searchValue = $state('');
|
||||
let selectedId: string = $state('0');
|
||||
let hasFetched = false;
|
||||
|
||||
const filteredCourses: Course[] = $derived(filterCourses(searchValue, selectedId, $courses));
|
||||
|
||||
async function getCourses(userId: string | undefined, orgId: string) {
|
||||
if (cantFetch && typeof cantFetch === 'boolean' && orgId && !hasFetched) {
|
||||
// only show is loading when fetching for the first time
|
||||
@@ -56,7 +57,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
filteredCourses = courses.filter((course) => {
|
||||
const filteredCourses = courses.filter((course) => {
|
||||
if (!searchValue || course.title.toLowerCase().includes(searchValue.toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
@@ -65,14 +66,14 @@
|
||||
});
|
||||
|
||||
if (_selectedId === '0') {
|
||||
filteredCourses = filteredCourses.sort(
|
||||
(a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
|
||||
);
|
||||
return filteredCourses.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
||||
} else if (_selectedId === '1') {
|
||||
filteredCourses = filteredCourses.sort((a, b) => b.is_published - a.is_published);
|
||||
return filteredCourses.sort((a, b) => (b.is_published ? 0 : 1) - (a.is_published ? 0 : 1));
|
||||
} else if (_selectedId === '2') {
|
||||
filteredCourses = filteredCourses.sort((a, b) => b.total_lessons - a.total_lessons);
|
||||
return filteredCourses.sort((a, b) => (b.total_lessons ?? 0) - (a.total_lessons ?? 0));
|
||||
}
|
||||
|
||||
return filteredCourses;
|
||||
}
|
||||
|
||||
const setViewPreference = (preference: 'grid' | 'list') => {
|
||||
@@ -92,21 +93,22 @@
|
||||
}
|
||||
});
|
||||
|
||||
$: filterCourses(searchValue, selectedId, $courses);
|
||||
$: getCourses($profile.id, $currentOrg.id);
|
||||
$effect(() => {
|
||||
getCourses($profile.id, $currentOrg.id);
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Courses - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-6xl md:mx-auto">
|
||||
<div class="py-2 md:py-10 px-2 md:px-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('courses.heading')}</h1>
|
||||
<section class="w-full md:mx-auto md:max-w-6xl">
|
||||
<div class="px-2 py-2 md:px-5 md:py-10">
|
||||
<div class="mb-5 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('courses.heading')}</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={openNewCourseModal}>
|
||||
<Add size={24} />
|
||||
<PlusIcon size={16} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
@@ -117,7 +119,7 @@
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex flex-row-reverse mb-5">
|
||||
<div class="mb-5 flex flex-row-reverse">
|
||||
<div class="filter-containter flex items-end justify-start">
|
||||
<Search
|
||||
placeholder={$t('courses.search_placeholder')}
|
||||
@@ -136,18 +138,18 @@
|
||||
/>
|
||||
{#if $courseMetaDeta.view === 'list'}
|
||||
<IconButton onClick={() => setViewPreference('grid')}>
|
||||
<Grid size={24} />
|
||||
<GridIcon size={16} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<IconButton onClick={() => setViewPreference('list')}>
|
||||
<List size={24} />
|
||||
<ListIcon size={16} />
|
||||
</IconButton>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<NewCourseModal />
|
||||
<Courses bind:courses={filteredCourses} />
|
||||
<Courses courses={filteredCourses} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -5,20 +5,20 @@
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { createQuizModal } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { Add } from 'carbon-icons-svelte';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Interactive Quizzes - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full max-w-6xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">Interactive Quizzes</h1>
|
||||
<section class="mx-auto w-full max-w-6xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">Interactive Quizzes</h1>
|
||||
{#if $isMobile}
|
||||
<PrimaryButton onClick={() => ($createQuizModal.open = true)}>
|
||||
<Add size={24} />
|
||||
<PlusIcon size={16} />
|
||||
</PrimaryButton>
|
||||
{:else}
|
||||
<PrimaryButton label="Create Quiz" onClick={() => ($createQuizModal.open = true)} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import ArrowLeftIcon from 'carbon-icons-svelte/lib/ArrowLeft.svelte';
|
||||
import CheckmarkFilledIcon from 'carbon-icons-svelte/lib/CheckmarkFilled.svelte';
|
||||
import WarningFilledIcon from 'carbon-icons-svelte/lib/WarningFilled.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import CircleAlertIcon from '@lucide/svelte/icons/circle-alert';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import isBoolean from 'lodash/isBoolean';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -32,34 +32,36 @@
|
||||
options: QuizOption[];
|
||||
}
|
||||
|
||||
export let data;
|
||||
let { data } = $props();
|
||||
const { quizId } = data;
|
||||
|
||||
// Questionnaire State
|
||||
let currentQuestion: QuizQuestion = $quizStore.questions[0] || {
|
||||
id: 0,
|
||||
label: '',
|
||||
type: 'multichoice',
|
||||
options: []
|
||||
};
|
||||
let currentQuestion: QuizQuestion = $state(
|
||||
$quizStore.questions[0] || {
|
||||
id: 0,
|
||||
label: '',
|
||||
type: 'multichoice',
|
||||
options: []
|
||||
}
|
||||
);
|
||||
|
||||
// Behavioural State
|
||||
let openPreview = false;
|
||||
let type = 'multichoice';
|
||||
let openPreview = $state(false);
|
||||
let type = $state('multichoice');
|
||||
let errors: Array<{
|
||||
isLabelEmpty: boolean;
|
||||
hasOneAnswer: boolean;
|
||||
id: number;
|
||||
options: Array<{ id: number; error: boolean }>;
|
||||
}> = [];
|
||||
}> = $state([]);
|
||||
let currentError: {
|
||||
isLabelEmpty?: boolean;
|
||||
hasOneAnswer?: boolean;
|
||||
id?: number;
|
||||
options?: Array<{ id: number; error: boolean }>;
|
||||
} = {};
|
||||
let isFocused = false;
|
||||
let selectEl: Select | null = null;
|
||||
} = $state({});
|
||||
let isFocused = $state(false);
|
||||
let selectEl: Select | null = $state(null);
|
||||
|
||||
function activeClass(q, cq) {
|
||||
if (q.id === cq.id) {
|
||||
@@ -200,9 +202,7 @@
|
||||
|
||||
function qHasError(qId, _errs) {
|
||||
return _errs.some((qe) => {
|
||||
return (
|
||||
(qId ? qe.id === qId : true) && (qe.isLabelEmpty || !qe.hasOneAnswer || !!qe.options.length)
|
||||
);
|
||||
return (qId ? qe.id === qId : true) && (qe.isLabelEmpty || !qe.hasOneAnswer || !!qe.options.length);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -239,11 +239,8 @@
|
||||
<!-- Questions list -->
|
||||
<aside class="root h-full w-1/5 bg-gray-100 p-4 dark:bg-neutral-800">
|
||||
<div class="flex h-full flex-col">
|
||||
<a
|
||||
class="text-md flex items-center text-gray-500 dark:text-white"
|
||||
href={`${$currentOrgPath}/quiz`}
|
||||
>
|
||||
<ArrowLeftIcon size={24} class="carbon-icon dark:text-white" /> Back to Quizzes
|
||||
<a class="text-md flex items-center text-gray-500 dark:text-white" href={`${$currentOrgPath}/quiz`}>
|
||||
<ArrowLeftIcon size={16} /> Back to Quizzes
|
||||
</a>
|
||||
|
||||
<h3 class="my-3">Quiz</h3>
|
||||
@@ -255,11 +252,11 @@
|
||||
question,
|
||||
currentQuestion
|
||||
)}"
|
||||
on:click={() => {
|
||||
onclick={() => {
|
||||
currentQuestion = question;
|
||||
type = question.type;
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
currentQuestion = question;
|
||||
type = question.type;
|
||||
@@ -269,7 +266,7 @@
|
||||
Question {i + 1}
|
||||
|
||||
{#if qHasError(question.id, errors)}
|
||||
<WarningFilledIcon size={20} class="carbon-icon error" />
|
||||
<CircleAlertIcon size={16} class="filled" />
|
||||
{/if}
|
||||
</button>
|
||||
{/each}
|
||||
@@ -325,18 +322,8 @@
|
||||
<aside class="settings h-full w-1/5 bg-gray-100 p-4 dark:bg-neutral-800">
|
||||
<div class="py-5">
|
||||
<h5>Quiz settings</h5>
|
||||
<PrimaryButton
|
||||
label="Save Changes"
|
||||
variant={VARIANTS.CONTAINED}
|
||||
onClick={saveQuiz}
|
||||
className="my-3"
|
||||
/>
|
||||
<PrimaryButton
|
||||
label="Preview Quiz"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={previewQuiz}
|
||||
className="my-3"
|
||||
/>
|
||||
<PrimaryButton label="Save Changes" variant={VARIANTS.CONTAINED} onClick={saveQuiz} className="my-3" />
|
||||
<PrimaryButton label="Preview Quiz" variant={VARIANTS.OUTLINED} onClick={previewQuiz} className="my-3" />
|
||||
<PrimaryButton
|
||||
label="Delete question"
|
||||
variant={VARIANTS.TEXT}
|
||||
@@ -377,11 +364,7 @@
|
||||
</Select>
|
||||
|
||||
<!-- -->
|
||||
<Select
|
||||
labelText="Time limit"
|
||||
bind:selected={$quizStore.timelimit}
|
||||
class="mb-3 flex items-center"
|
||||
>
|
||||
<Select labelText="Time limit" bind:selected={$quizStore.timelimit} class="mb-3 flex items-center">
|
||||
<SelectItem value="10 seconds" text="10s" />
|
||||
<SelectItem value="20 seconds" text="20s" />
|
||||
<SelectItem value="30 seconds" text="30s" />
|
||||
@@ -397,23 +380,19 @@
|
||||
|
||||
{#each allThemes as _theme}
|
||||
<div
|
||||
class="theme relative mb-5 w-full cursor-pointer rounded-md border {$quizStore.theme ===
|
||||
_theme.id && 'border-primary-700'}"
|
||||
class="theme relative mb-5 w-full cursor-pointer rounded-md border {$quizStore.theme === _theme.id &&
|
||||
'border-primary-700'}"
|
||||
>
|
||||
{#if $quizStore.theme === _theme.id}
|
||||
<CheckmarkFilledIcon
|
||||
size={24}
|
||||
class="carbon-icon absolute right-4 top-4"
|
||||
style="fill:white;"
|
||||
/>
|
||||
<CircleCheckIcon size={16} filled />
|
||||
{/if}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
class="flex h-full w-full flex-col-reverse rounded-md border"
|
||||
style="background: url({themeImages[_theme.id]?.card});"
|
||||
on:click={() => ($quizStore.theme = _theme.id)}
|
||||
on:keydown={(e) => {
|
||||
onclick={() => ($quizStore.theme = _theme.id)}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
$quizStore.theme = _theme.id;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
<title>Settings - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-5">
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('settings.heading')}</h1>
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-5 py-10">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('settings.heading')}</h1>
|
||||
|
||||
<ViewSiteBtn />
|
||||
</div>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<title>Customize LMS - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-3 md:px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">
|
||||
{$t('components.settings.customize_lms.title')}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
<title>Custom domain - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-3 md:px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('components.settings.domains.title')}</h1>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('components.settings.domains.title')}</h1>
|
||||
</div>
|
||||
|
||||
<Domains />
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
<title>Teams - ClassroomIO</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="w-full md:max-w-4xl mx-auto">
|
||||
<div class="py-10 px-3 md:px-5">
|
||||
<a class="text-gray-500 dark:text-white text-md" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-3 py-10 md:px-5">
|
||||
<a class="text-md text-gray-500 dark:text-white" href={`${$currentOrgPath}/settings?tab=org`}
|
||||
>{$t('upgrade.back')}</a
|
||||
>
|
||||
<div class="flex items-center justify-between mb-10">
|
||||
<h1 class="dark:text-white text-3xl font-bold">{$t('upgrade.team_members')}</h1>
|
||||
<div class="mb-10 flex items-center justify-between">
|
||||
<h1 class="text-3xl font-bold dark:text-white">{$t('upgrade.team_members')}</h1>
|
||||
</div>
|
||||
|
||||
<UpgradeBanner>{$t('upgrade.team')}</UpgradeBanner>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import Chip from '$lib/components/Chip/Text.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import CheckmarkOutline from 'carbon-icons-svelte/lib/CheckmarkOutline.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -10,9 +10,17 @@
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
export let data;
|
||||
let setupList = data.setup || [];
|
||||
let completed = 0;
|
||||
let { data } = $props();
|
||||
const setupList = $derived(
|
||||
(data.setup || []).map((item) => {
|
||||
if (item.id === 'profile') {
|
||||
item.is_completed = !$profile.avatar_url.includes('avatars/avatar.png');
|
||||
}
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
const completed = $derived(setupList.filter((list) => list.is_completed).length);
|
||||
|
||||
const StepsEnum = {
|
||||
UPDATE_PROFILE: 'profile',
|
||||
@@ -70,21 +78,12 @@
|
||||
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">
|
||||
<section class="mx-auto w-full md:max-w-4xl">
|
||||
<div class="px-2 py-2 md:px-5 md:py-10">
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="dark:text-white text-2xl md:text-3xl font-bold">{$t('setup.get_started')}</h1>
|
||||
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('setup.get_started')}</h1>
|
||||
<Chip
|
||||
value={`${completed}/${setupList.length}`}
|
||||
className="text-[10px] font-semibold px-3 !py-1"
|
||||
@@ -95,18 +94,14 @@
|
||||
<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"
|
||||
class="flex w-full flex-col justify-between gap-2 border-b border-gray-300 py-8 lg:flex-row lg:items-center"
|
||||
>
|
||||
<div class={`space-y-1 flex-1 ${list.is_completed ? 'opacity-50' : ''} lg:max-w-[50%]`}>
|
||||
<div class={`flex-1 space-y-1 ${list.is_completed ? 'opacity-50' : ''} lg:max-w-[50%]`}>
|
||||
<div class="flex items-center gap-3">
|
||||
<Chip
|
||||
value={i + 1}
|
||||
className={`text-[10px] font-semibold !py-1 `}
|
||||
shape="rounded-full"
|
||||
/>
|
||||
<p class="font-medium text-lg">{$t(list.title)}</p>
|
||||
<Chip value={i + 1} className={`text-[10px] font-semibold !py-1 `} shape="rounded-full" />
|
||||
<p class="text-lg font-medium">{$t(list.title)}</p>
|
||||
</div>
|
||||
<p class={`text-sm`}>
|
||||
<p class="text-sm">
|
||||
{$t(list.desc)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -118,7 +113,7 @@
|
||||
isDisabled={list.is_completed}
|
||||
>
|
||||
{#if list.is_completed}
|
||||
<CheckmarkOutline />
|
||||
<CircleCheckIcon size={16} />
|
||||
{/if}
|
||||
{$t(list.button_label)}
|
||||
</PrimaryButton>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import get from 'lodash/get';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
|
||||
const supabase = getSupabase();
|
||||
|
||||
@@ -53,9 +53,7 @@ async function getGenericData(orgSlug: string): Promise<{
|
||||
};
|
||||
}
|
||||
|
||||
async function getIsLessonCreated(
|
||||
orgSlug: string
|
||||
): Promise<{ isLessonCreated: boolean; lessonData: any }> {
|
||||
async function getIsLessonCreated(orgSlug: string): Promise<{ isLessonCreated: boolean; lessonData: any }> {
|
||||
const { data } = await supabase
|
||||
.from('lesson')
|
||||
.select(
|
||||
@@ -104,9 +102,7 @@ async function getIsExerciseCreated(orgSlug: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
export const load = async ({ params = { slug: '' } }) => {
|
||||
const { isCourseCreated, isCoursePublished, orgHasAvatarUrl, courseData } = await getGenericData(
|
||||
params.slug
|
||||
);
|
||||
const { isCourseCreated, isCoursePublished, orgHasAvatarUrl, courseData } = await getGenericData(params.slug);
|
||||
const { isLessonCreated, lessonData } = await getIsLessonCreated(params.slug);
|
||||
const isExerciseCreated = await getIsExerciseCreated(params.slug);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user