feat: move away from carbon components and TinyMCE (#597)

* feat: remove tinymce & replace org sidebar

* chore: PR Fixes

* chore: move ui font into src dir

* remove files in vite config

* use google font import for loading font

* remove custom font

* more fonts update

* remove font bold

* more removals of font bold

* fix import

* checkpoint

* chore: checkpoint (add course & lms shadcn sidebar)

* minor css improvements and try out font sans

* chore: replace carbon components with shadcn's

* chore: replace carbon components with shadcn _ v3

* chore: ui fixes & storybook update

* feat: UI cleanup

* fix: quick ui fixes

* chore: update pnpm-lock.yaml with new dependencies and versions; fix output.css formatting

* chore: add isloading to button

* feat: add loading state to button stories

* chore: feedback fixes

* fix: feedback fixes

* chore: remove console

* fix: PR comments

* fix: PR comments

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Adeyemi Adetunji
2025-11-21 12:18:04 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 2145782385
commit cbf6ae7e63
499 changed files with 19708 additions and 15603 deletions
@@ -1,12 +1,23 @@
<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';
import VerifyEmailModal from '$lib/components/Org/VerifyEmail/VerifyEmailModal.svelte';
import Box from '$lib/components/Box/index.svelte';
import { currentOrg } from '$lib/utils/store/org';
import { goto } from '$app/navigation';
import { Skeleton } from '@cio/ui/base/skeleton';
import UsersIcon from '@lucide/svelte/icons/users';
import Settings2Icon from '@lucide/svelte/icons/settings-2';
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
import LayoutDashboardIcon from '@lucide/svelte/icons/layout-dashboard';
import MessageSquareMoreIcon from '@lucide/svelte/icons/message-square-more';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { isQuizPage } from '$lib/utils/functions/app';
import { t } from '$lib/utils/functions/translations';
import { orgs, isOrgAdmin } from '$lib/utils/store/org';
import Box from '$lib/components/Box/index.svelte';
import OrgSideBar from '$lib/components/Sidebar/index.svelte';
import AddOrgModal from '$lib/components/Org/AddOrgModal/AddOrgModal.svelte';
import VerifyEmailModal from '$lib/components/Org/VerifyEmail/VerifyEmailModal.svelte';
let { data, children } = $props();
@@ -15,6 +26,64 @@
goto(newUrl + page.url.search);
}
function isActive(pagePath: string, itemPath: string) {
const pageLinkItems = pagePath.split('/');
const itemLinkItems = itemPath.split('/');
if (itemLinkItems.length !== pageLinkItems.length) {
return false;
}
return pagePath.includes(itemPath);
}
const sidebarData = $derived({
teams: $orgs,
navMain: [
{
title: $t('org_navigation.dashboard'),
url: '/',
icon: LayoutDashboardIcon,
isActive: isActive(page.url.pathname, `/org/${data.orgName}`)
},
{
title: $t('org_navigation.courses'),
url: `/courses`,
icon: LibraryBigIcon,
isActive: isActive(page.url.pathname, `/org/${data.orgName}/courses`)
},
{
title: $t('org_navigation.community'),
url: `/community`,
icon: MessageSquareMoreIcon,
isActive: isActive(page.url.pathname, `/org/${data.orgName}/community`)
},
{
title: $t('org_navigation.audience'),
url: `/audience`,
icon: UsersIcon,
isActive: isActive(page.url.pathname, `/org/${data.orgName}/audience`)
},
...(!!$isOrgAdmin
? [
{
title: $t('org_navigation.setup'),
url: `/setup`,
icon: Settings2Icon,
isActive: isActive(page.url.pathname, `/org/${data.orgName}/setup`)
}
]
: [])
],
user: {
name: $profile.fullname,
email: $profile.email,
avatar: $profile.avatar_url
}
});
const isSidebarReady = $derived($orgs.length > 0 && $profile.fullname && $profile.email);
$effect(() => {
data.orgName === '*' && redirect($currentOrg.siteName);
});
@@ -25,14 +94,75 @@
<VerifyEmailModal />
<div class="org-root flex w-full items-center justify-between">
{#if !isQuizPage(page.url?.pathname)}
<OrgSideBar />
{/if}
<div class="org-slot w-full bg-white dark:bg-black">
{#if data.orgName === '*'}
<Box>Taking you to your organization...</Box>
<div class="org-slot flex w-full items-start bg-white dark:bg-black">
{#if !isQuizPage(page.url?.pathname)}
{#if isSidebarReady}
<OrgSideBar data={sidebarData} />
{:else}
<div class="bg-sidebar flex h-[calc(100vh-48px)] w-64 flex-col gap-2 border-r p-2">
<!-- org switcher -->
<div class="flex items-center gap-2 rounded-md p-2">
<Skeleton class="h-8 w-8 rounded-lg" />
<div class="flex flex-1 flex-col gap-1">
<Skeleton class="h-4 w-24" />
<Skeleton class="h-3 w-16" />
</div>
<Skeleton class="h-4 w-4" />
</div>
<!-- nav main -->
<div class="flex flex-col gap-1 py-2">
<Skeleton class="mb-2 h-3 w-16 px-2" />
{#each Array(5) as _, i (i)}
<div class="flex items-center gap-2 rounded-md p-2">
<Skeleton class="h-4 w-4" />
<Skeleton class="h-4 w-full" />
</div>
{/each}
</div>
<!-- spacer -->
<div class="flex-1"></div>
<!-- upgrade banner -->
<div class="mx-4 mb-2 flex flex-col items-center gap-3 rounded-md border p-4">
<Skeleton class="h-16 w-16 rounded-md" />
<div class="flex flex-col items-center gap-2">
<Skeleton class="h-4 w-32" />
<Skeleton class="h-3 w-40" />
</div>
<Skeleton class="h-9 w-24 rounded-md" />
</div>
<!-- nav user -->
<div class="flex items-center gap-2 rounded-md p-2">
<Skeleton class="h-8 w-8 rounded-lg" />
<div class="flex flex-1 flex-col gap-1">
<Skeleton class="h-4 w-24" />
<Skeleton class="h-3 w-20" />
</div>
<Skeleton class="h-4 w-4" />
</div>
</div>
{/if}
{:else}
{@render children?.()}
<div class="flex-1">
{#if data.orgName === '*'}
<Box>Taking you to your organization...</Box>
{:else}
{@render children?.()}
{/if}
</div>
{/if}
{#if !isQuizPage(page.url?.pathname)}
<div class="flex-1">
{#if data.orgName === '*'}
<Box>Taking you to your organization...</Box>
{:else}
{@render children?.()}
{/if}
</div>
{/if}
</div>
</div>
@@ -1,32 +1,32 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { ActivityCard } from '$lib/components/Analytics';
import Progress from '$lib/components/Progress/index.svelte';
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
import BookIcon from '@lucide/svelte/icons/book';
import { Skeleton } from '@cio/ui/base/skeleton';
import UserIcon from '@lucide/svelte/icons/user';
import UsersIcon from '@lucide/svelte/icons/users';
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
import DollarSignIcon from '@lucide/svelte/icons/dollar-sign';
import { snackbar } from '$lib/components/Snackbar/store';
import { profile } from '$lib/utils/store/user';
import PlusIcon from '@lucide/svelte/icons/plus';
import { isOrgAdmin } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
import { isMobile } from '$lib/utils/store/useMobile';
import { calDateDiff } from '$lib/utils/functions/date';
import { getGreeting } from '$lib/utils/functions/date';
import { snackbar } from '$lib/components/Snackbar/store';
import { getAccessToken } from '$lib/utils/functions/supabase';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import type { OrganisationAnalytics } from '$lib/utils/types/analytics';
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';
import { ActivityCard } from '$lib/components/Analytics';
import Progress from '$lib/components/Progress/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Grid from '$lib/components/Org/Settings/Layout/Grid.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import WelcomeModal from '$lib/components/WelcomeModal/WelcomeModal.svelte';
import { getGreeting } from '$lib/utils/functions/date';
import { t } from '$lib/utils/functions/translations';
import { isOrgAdmin } from '$lib/utils/store/org';
import { isMobile } from '$lib/utils/store/useMobile';
import { Grid, Link, SkeletonPlaceholder } from 'carbon-components-svelte';
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
let dashAnalytics: OrganisationAnalytics | undefined = $state();
let hasFetched = $state(false);
@@ -98,7 +98,7 @@
<WelcomeModal />
<div class="w-full max-w-5xl px-5 py-10 md:mx-auto">
<div class="w-full px-5 py-10 md:mx-auto">
<div class="mb-5 flex items-center justify-between">
<h1 class="mb-3 text-2xl dark:text-white">
{$t(getGreeting())}
@@ -123,11 +123,11 @@
</div>
<div class="mb-10 flex flex-wrap items-start">
<Grid class="px-0" fullWidth>
<Grid class="px-0">
<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" />
<Skeleton style="width: 100%; min-width: 300px; height: 10rem;" class="rounded-md" />
{:else}
<ActivityCard activity={card} />
{/if}
@@ -138,25 +138,25 @@
<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 md:p-5 dark:border-neutral-600 dark:text-white">
<h3 class="mt-0 text-2xl font-bold">
<h3 class="mt-0 text-2xl">
{$t('dashboard.top_courses')}
</h3>
<div class="h-full space-y-6">
{#if !dashAnalytics}
{#each Array(5) as _}
<SkeletonPlaceholder style="width: 100%; height: 40px;" class="rounded-md" />
<Skeleton class="h-10 w-full rounded-md" />
{/each}
{:else}
{#each dashAnalytics.topCourses as course}
<div class="flex items-center gap-2">
<div class="w-4/6 space-y-1">
<Link href={`/courses/${course.id}`}>
<a href={`/courses/${course.id}`}>
<p class="line-clamp-2 pb-[0.1rem] text-sm font-medium leading-none">
{course.title}
</p>
</Link>
<p class="text-muted-foreground text-sm">
</a>
<p class="text-sm">
{course.enrollments}
{$t(course.enrollments === 1 ? 'dashboard.student' : 'dashboard.students')}
</p>
@@ -190,14 +190,14 @@
</div>
<div class="flex min-h-[45vh] w-full flex-col rounded-md border p-3 md:p-5 dark:border-neutral-600 dark:text-white">
<h3 class="mt-0 text-2xl font-bold">
<h3 class="mt-0 text-2xl">
{$t('dashboard.recent_enrollments')}
</h3>
<div class="h-full space-y-6">
{#if !dashAnalytics}
{#each Array(5) as _}
<SkeletonPlaceholder style="width: 100%; height: 40px;" class="rounded-md" />
<Skeleton class="h-10 w-full rounded-md" />
{/each}
{:else}
{#each dashAnalytics.enrollments as enrollment}
@@ -216,11 +216,11 @@
</div>
<div class="w-2/4">
<Link href={`/courses/${enrollment.courseId}`}>
<a href={`/courses/${enrollment.courseId}`}>
<p class="line-clamp-2 pb-[0.1rem] text-sm font-medium leading-none">
{enrollment.course}
</p>
</Link>
</a>
</div>
</div>
{:else}
@@ -19,11 +19,11 @@
<title>Audience - ClassroomIO</title>
</svelte:head>
<section class="mx-auto w-full max-w-4xl">
<section class="mx-auto w-full max-w-6xl">
<div class="px-5 py-10">
<div class="mb-10 flex items-center justify-between">
<div class="flex items-end">
<h1 class="m-0 text-2xl font-bold md:text-3xl dark:text-white">{$t('audience.title')}</h1>
<h1 class="m-0 text-2xl md:text-3xl dark:text-white">{$t('audience.title')}</h1>
{#if $currentOrgPlan?.plan_name !== PLAN.ENTERPRISE}
<span class="ml-2">
({$orgAudience.length} / {$currentOrgMaxAudience})
@@ -1,18 +1,21 @@
<script lang="ts">
import { ActivityCard, HeroProfileCard, LoadingPage } from '$lib/components/Analytics';
import Progress from '$lib/components/Progress/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import { getAccessToken } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
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 '@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';
import { Badge } from '@cio/ui/base/badge';
import UnfoldVerticalIcon from '@lucide/svelte/icons/unfold-vertical';
import BookOpenIcon from '@lucide/svelte/icons/book-open';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
import Progress from '$lib/components/Progress/index.svelte';
import Grid from '$lib/components/Org/Settings/Layout/Grid.svelte';
import { ActivityCard, HeroProfileCard, LoadingPage } from '$lib/components/Analytics';
import { t } from '$lib/utils/functions/translations';
import { currentOrgPath } from '$lib/utils/store/org';
import { snackbar } from '$lib/components/Snackbar/store';
import { getAccessToken } from '$lib/utils/functions/supabase';
import type { UserAnalytics } from '$lib/utils/types/analytics';
let { data } = $props();
@@ -101,7 +104,7 @@
<div class="px-5 py-1">
<HeroProfileCard user={userAnalytics.user} />
<Grid class="mt-5 px-0" fullWidth>
<Grid class="mt-5 px-0">
<div class="mt-8 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
{#each userMetrics as activity}
<ActivityCard {activity} />
@@ -110,7 +113,7 @@
</Grid>
<div class="mt-5 rounded-md border p-3 md:p-5 dark:border-neutral-600">
<h3 class="text-2xl font-bold">
<h3 class="text-2xl">
{$t('analytics.courses')}
</h3>
@@ -128,26 +131,22 @@
</div>
<Progress value={userAnalytics.overallCourseProgress} />
<div class="flex items-center justify-between">
<Tag
interactive
filter={courseFilter === 'incomplete'}
type={courseFilter === 'incomplete' ? 'gray' : 'outline'}
<Badge
type={courseFilter === 'incomplete' ? 'secondary' : 'outline'}
class="text-yellow-700 dark:text-yellow-500"
on:click={() => toggleCourseFilter('incomplete')}
onclick={() => toggleCourseFilter('incomplete')}
>
{incompleteCourses}
{$t('analytics.incomplete')}
</Tag>
<Tag
interactive
filter={courseFilter === 'completed'}
type={courseFilter === 'completed' ? 'gray' : 'outline'}
</Badge>
<Badge
type={courseFilter === 'completed' ? 'secondary' : 'outline'}
class="text-green-700 dark:text-green-500"
on:click={() => toggleCourseFilter('completed')}
onclick={() => toggleCourseFilter('completed')}
>
{completedCourses}
{$t('analytics.complete')}
</Tag>
</Badge>
</div>
</div>
@@ -181,7 +180,7 @@
</div>
</div>
<Tag
<Badge
class={`${
course.lessons_count === course.lessons_completed
? 'bg-green-200 text-green-700'
@@ -191,7 +190,7 @@
{course.lessons_count === course.lessons_completed
? $t('analytics.completed')
: $t('analytics.incomplete')}
</Tag>
</Badge>
</div>
<div class="flex w-full items-center gap-1">
@@ -17,10 +17,10 @@
<title>Community - ClassroomIO</title>
</svelte:head>
<section class="w-full md:mx-auto md:max-w-4xl">
<section class="w-full md:mx-auto md:max-w-6xl">
<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>
<h1 class="text-2xl md:text-3xl dark:text-white">{$t('community.title')}</h1>
{#if $isMobile}
<PrimaryButton onClick={askCommunity}>
<PlusIcon size={16} />
@@ -1,32 +1,32 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { untrack } from 'svelte';
import Avatar from '$lib/components/Avatar/index.svelte';
import pluralize from 'pluralize';
import { goto } from '$app/navigation';
import { Skeleton } from '@cio/ui/base/skeleton';
import * as Select from '@cio/ui/base/select';
import TrashIcon from '@lucide/svelte/icons/trash';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import { t } from '$lib/utils/functions/translations';
import { courses } from '$lib/components/Courses/store';
import TextField from '$lib/components/Form/TextField.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';
import { snackbar } from '$lib/components/Snackbar/store';
import TextEditor from '$lib/components/TextEditor/index.svelte';
import Vote from '$lib/components/Vote/index.svelte';
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 { snackbar } from '$lib/components/Snackbar/store';
import { fetchCourses } from '$lib/utils/services/courses';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
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 '@lucide/svelte/icons/arrow-left';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
import TrashIcon from '@lucide/svelte/icons/trash';
import pluralize from 'pluralize';
import { askCommunityValidation, commentInCommunityValidation } from '$lib/utils/functions/validator';
let { data } = $props();
const { slug } = data;
import type { Course } from '$lib/utils/types';
import { profile } from '$lib/utils/store/user';
import Vote from '$lib/components/Vote/index.svelte';
import { IconButton } from '$lib/components/IconButton';
import Avatar from '$lib/components/Avatar/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import TextEditor from '$lib/components/TextEditor/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import DeleteModal from '$lib/components/Org/Community/DeleteModal.svelte';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
interface Comment {
id: string;
@@ -53,6 +53,9 @@
courseId: string;
}
let { data } = $props();
const { slug } = data;
let question: Question | undefined = $state();
let comment = $state('');
let errors: {
@@ -397,9 +400,9 @@
<section class="mx-auto max-w-3xl md:mx-10 lg:mb-20">
{#if !question}
<div class="mb-3 px-5 py-10">
<SkeletonText style="width: 25%;" />
<SkeletonText style="width: 100%; margin-bottom: 2rem" />
<SkeletonPlaceholder style="width: 100%; height: 20rem;" />
<Skeleton class="h-4 w-[25%]" />
<Skeleton class="mb-2 h-4 w-full" />
<Skeleton class="h-80 w-full" />
</div>
{:else}
<div class="px-5 py-10">
@@ -410,13 +413,20 @@
<div class="my-5 flex items-center justify-between">
{#if isEditMode}
<TextField bind:value={editContent.title} className="w-full mr-2" errorMessage={errors.title} />
<Dropdown
class="h-full w-[25%]"
size="xl"
label={$t('community.ask.select_course')}
items={fetchedCourses.map((course) => ({ id: course.id, text: course.title }))}
bind:selectedId={editContent.courseId}
/>
<Select.Root type="single" bind:value={editContent.courseId}>
<Select.Trigger class="h-full w-[25%]">
<p>
{editContent.courseId
? fetchedCourses.find((course) => course.id === editContent.courseId)?.title
: $t('community.ask.select_course')}
</p>
</Select.Trigger>
<Select.Content>
{#each fetchedCourses as course}
<Select.Item value={course.id || ''}>{course.title}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{:else}
<div class="flex items-center">
<Vote value={question.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
@@ -442,7 +452,7 @@
{/if}
{/if}
</div>
<div class="border-1 border-gray my-1 rounded-lg border px-1">
<div class="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" />
@@ -468,19 +478,19 @@
{#if isEditMode && editorInstance}
<div class="my-2">
<TextEditor
bind:value={editContent.body}
placeholder={$t('community.ask.give')}
onChange={(html) => (editContent.body = html)}
placeholder="Give an answer"
content={editContent.body}
onChange={(content) => (editContent.body = content)}
/>
</div>
{:else}
<section class="prose prose-sm sm:prose p-2">
<section class="prose prose-sm p-2 sm:prose">
{@html question.body}
</section>
{/if}
</div>
<div class="my-8 font-bold">
<div class="my-8">
{pluralize($t('community.answers'), question.totalComments, true)}
</div>
@@ -491,7 +501,7 @@
upVote={() => upvoteQuestion('comment', comment.id)}
disabled={voted.comment[comment.id]}
/>
<div class="border-1 border-gray w-full rounded-lg border">
<div class="border-gray w-full rounded-lg border">
<header class="flex items-center justify-between p-2 leading-none">
<div class="flex items-center text-black">
<Avatar src={comment.avatar} name={comment.name} width="w-7" height="h-7" />
@@ -517,7 +527,7 @@
</IconButton>
{/if}
</header>
<article class="prose prose-sm sm:prose p-2">
<article class="prose prose-sm p-2 sm:prose">
{@html comment.comment}
</article>
</div>
@@ -528,11 +538,7 @@
<div class="mt-4">
{#if !editorInstance}
<TextEditor
bind:value={comment}
placeholder={$t('community.ask.give')}
onChange={(html) => (comment = html)}
/>
<TextEditor placeholder="Give an answer" content={comment} onChange={(content) => (comment = content)} />
{/if}
<div class="mt-2 flex justify-end">
@@ -1,22 +1,24 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { courses } from '$lib/components/Courses/store';
import TextField from '$lib/components/Form/TextField.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import TextEditor from '$lib/components/TextEditor/index.svelte';
import generateSlug from '$lib/utils/functions/generateSlug';
import { supabase } from '$lib/utils/functions/supabase';
import { t } from '$lib/utils/functions/translations';
import { askCommunityValidation } from '$lib/utils/functions/validator';
import { fetchCourses } from '$lib/utils/services/courses';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import type { Course } from '$lib/utils/types';
import { Dropdown } from 'carbon-components-svelte';
import * as Select from '@cio/ui/base/select';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import type { Course } from '$lib/utils/types';
import { profile } from '$lib/utils/store/user';
import { t } from '$lib/utils/functions/translations';
import { courses } from '$lib/components/Courses/store';
import { supabase } from '$lib/utils/functions/supabase';
import { snackbar } from '$lib/components/Snackbar/store';
import { fetchCourses } from '$lib/utils/services/courses';
import generateSlug from '$lib/utils/functions/generateSlug';
import { currentOrg, currentOrgPath } from '$lib/utils/store/org';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { askCommunityValidation } from '$lib/utils/functions/validator';
import TextField from '$lib/components/Form/TextField.svelte';
import TextEditor from '$lib/components/TextEditor/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
let errors: {
title?: string;
courseId?: string;
@@ -98,7 +100,7 @@
{$t('community.ask.go_back')}
</a>
<div class="flex items-center justify-between gap-12">
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('community.ask.ask_the')}</h1>
<h1 class="text-2xl 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>
@@ -110,22 +112,30 @@
errorMessage={errors.title}
className="w-[75%]"
/>
<Dropdown
class="h-full w-[25%]"
size="xl"
label={$t('community.ask.select_course')}
invalid={!!errors.courseId}
invalidText={errors.courseId}
items={fetchedCourses.map((course) => ({ id: course.id, text: course.title }))}
bind:selectedId={fields.courseId}
/>
<div class="w-[25%]">
<Select.Root type="single" bind:value={fields.courseId}>
<Select.Trigger class="h-full w-full">
<p>
{fields.courseId
? fetchedCourses.find((course) => course.id === fields.courseId)?.title
: $t('community.ask.select_course')}
</p>
</Select.Trigger>
<Select.Content>
{#each fetchedCourses as course}
<Select.Item value={course.id}>{course.title}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{#if errors.courseId}
<p class="mt-1 text-sm text-red-500">
{errors.courseId}
</p>
{/if}
</div>
</div>
<div class="px-2">
<TextEditor
bind:value={fields.body}
placeholder={$t('community.ask.ask_community')}
onChange={(html) => (fields.body = html)}
/>
<TextEditor placeholder="Give an answer" content={fields.body} onChange={(content) => (fields.body = content)} />
{#if errors.body}
<p class="mt-2 text-sm text-red-500">{errors.body}</p>
@@ -1,5 +1,8 @@
<script lang="ts">
import { Search, Dropdown } from 'carbon-components-svelte';
import { Input } from '@cio/ui/base/input';
import * as Select from '@cio/ui/base/select';
import SearchIcon from '@lucide/svelte/icons/search';
import { profile } from '$lib/utils/store/user';
import { fetchCourses } from '$lib/utils/services/courses';
import Courses from '$lib/components/Courses/index.svelte';
@@ -27,6 +30,16 @@
let selectedId: string = $state('0');
let hasFetched = false;
const filterOptions = $derived([
{ value: '0', label: $t('courses.course_filter.date_created') },
{ value: '1', label: $t('courses.course_filter.published') },
{ value: '2', label: $t('courses.course_filter.lessons') }
]);
const selectedLabel = $derived(
filterOptions.find((opt) => opt.value === selectedId)?.label || filterOptions[0].label
);
const filteredCourses: Course[] = $derived(filterCourses(searchValue, selectedId, $courses));
async function getCourses(userId: string | undefined, orgId: string) {
@@ -105,7 +118,7 @@
<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>
<h1 class="text-2xl md:text-3xl dark:text-white">{$t('courses.heading')}</h1>
{#if $isMobile}
<PrimaryButton isDisabled={!$isOrgAdmin} onClick={openNewCourseModal}>
<PlusIcon size={16} />
@@ -120,22 +133,22 @@
{/if}
</div>
<div class="mb-5 flex flex-row-reverse">
<div class="filter-containter flex items-end justify-start">
<Search
placeholder={$t('courses.search_placeholder')}
bind:value={searchValue}
searchClass="mr-2"
class=" bg-gray-100 dark:bg-neutral-800"
/>
<Dropdown
class="h-full min-w-[150px]"
bind:selectedId
items={[
{ id: '0', text: $t('courses.course_filter.date_created') },
{ id: '1', text: $t('courses.course_filter.published') },
{ id: '2', text: $t('courses.course_filter.lessons') }
]}
/>
<div class="filter-containter flex items-end justify-start gap-2">
<div class="relative">
<Input type="text" placeholder={$t('courses.search_placeholder')} bind:value={searchValue} class="pl-9" />
<SearchIcon class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2" />
</div>
<Select.Root type="single" bind:value={selectedId}>
<Select.Trigger class="min-w-[150px]">
<p>{selectedId ? selectedLabel : filterOptions[0].label}</p>
</Select.Trigger>
<Select.Content>
{#each filterOptions as option}
<Select.Item value={option.value}>{option.label}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
{#if $courseMetaDeta.view === 'list'}
<IconButton onClick={() => setViewPreference('grid')}>
<GridIcon size={16} />
@@ -152,10 +165,3 @@
<Courses courses={filteredCourses} />
</div>
</section>
<style>
.filter-containter :global(.bx--dropdown) {
max-height: unset;
height: 100%;
}
</style>
@@ -15,7 +15,7 @@
<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>
<h1 class="text-2xl md:text-3xl dark:text-white">Interactive Quizzes</h1>
{#if $isMobile}
<PrimaryButton onClick={() => ($createQuizModal.open = true)}>
<PlusIcon size={16} />
@@ -1,23 +1,25 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
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';
import { Label } from '@cio/ui/base/label';
import * as Select from '@cio/ui/base/select';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import CircleAlertIcon from '@lucide/svelte/icons/circle-alert';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
import DeleteModal from '$lib/components/Org/Quiz/DeleteModal.svelte';
import Preview from '$lib/components/Org/Quiz/Play/Preview.svelte';
import QuizQuestion from '$lib/components/Org/Quiz/QuizQuestion.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import DeleteModal from '$lib/components/Org/Quiz/DeleteModal.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import { allOptions, allThemes, booleanOptions, themeImages } from '$lib/utils/constants/quiz';
import { supabase } from '$lib/utils/functions/supabase';
import QuizQuestion from '$lib/components/Org/Quiz/QuizQuestion.svelte';
import { t } from '$lib/utils/functions/translations';
import { supabase } from '$lib/utils/functions/supabase';
import { snackbar } from '$lib/components/Snackbar/store';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { currentOrgPath, deleteModal, quizStore, quizesStore } from '$lib/utils/store/org';
import { Select, SelectItem } from 'carbon-components-svelte';
import { allOptions, allThemes, booleanOptions, themeImages } from '$lib/utils/constants/quiz';
interface QuizOption {
id: number;
@@ -58,10 +60,9 @@
isLabelEmpty?: boolean;
hasOneAnswer?: boolean;
id?: number;
options?: Array<{ id: number; error: boolean }>;
options: Array<{ id: number; error: boolean }>;
} = $state({});
let isFocused = $state(false);
let selectEl: Select | null = $state(null);
function activeClass(q, cq) {
if (q.id === cq.id) {
@@ -248,7 +249,7 @@
<div class="mb-3">
{#each $quizStore.questions as question, i}
<button
class="mb-3 flex w-full justify-between rounded p-3 text-left font-bold text-gray-500 dark:text-white {activeClass(
class="mb-3 flex w-full justify-between rounded p-3 text-left text-gray-500 dark:text-white {activeClass(
question,
currentQuestion
)}"
@@ -285,7 +286,7 @@
?.editor}) no-repeat center center fixed; -webkit-background-size: cover;-moz-background-size: cover;-o-background-size: cover;background-size: cover;"
>
<div class="content m-auto">
<h1 class="my-5 font-bold text-white">{$quizStore.title}</h1>
<h1 class="my-5 text-white">{$quizStore.title}</h1>
<QuizQuestion {currentQuestion} {optionHasError} {currentError} />
@@ -339,39 +340,44 @@
<div class="flex flex-col justify-evenly">
<div class="my-3">
<!-- Question type -->
<Select
labelText="Question type"
bind:this={selectEl}
bind:selected={type}
class="mb-3 flex items-center"
on:focus={() => (isFocused = true)}
on:blur={() => (isFocused = false)}
on:change={() => {
if (!isFocused) return;
// Blur after change
const onBlur = selectEl?.$$?.callbacks?.blur?.[0];
const onFocus = selectEl?.$$?.callbacks?.focus?.[0];
setTimeout(() => {
onBlur();
onFocus();
}, 1000);
handleQuestionTypeChange(type);
}}
>
<SelectItem value="multichoice" text="Multi-choice answers" />
<SelectItem value="boolean" text="True or False" />
</Select>
<div class="mb-3">
<Label class="mb-2">Question type</Label>
<Select.Root
type="single"
bind:value={type}
onValueChange={(value) => {
if (value) {
handleQuestionTypeChange(value);
}
}}
>
<Select.Trigger class="w-full">
<p>{type === 'multichoice' ? 'Multi-choice answers' : 'True or False'}</p>
</Select.Trigger>
<Select.Content>
<Select.Item value="multichoice">Multi-choice answers</Select.Item>
<Select.Item value="boolean">True or False</Select.Item>
</Select.Content>
</Select.Root>
</div>
<!-- -->
<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" />
<SelectItem value="1 minute" text="1m" />
<SelectItem value="2 minute" text="2m" />
<SelectItem value="3 minute" text="3m" />
</Select>
<div class="mb-3">
<Label class="mb-2">Time limit</Label>
<Select.Root type="single" bind:value={$quizStore.timelimit}>
<Select.Trigger class="w-full">
<p>{$quizStore.timelimit || 'Select time limit'}</p>
</Select.Trigger>
<Select.Content>
<Select.Item value="10 seconds">10s</Select.Item>
<Select.Item value="20 seconds">20s</Select.Item>
<Select.Item value="30 seconds">30s</Select.Item>
<Select.Item value="1 minute">1m</Select.Item>
<Select.Item value="2 minute">2m</Select.Item>
<Select.Item value="3 minute">3m</Select.Item>
</Select.Content>
</Select.Root>
</div>
</div>
<!-- Theme settings -->
@@ -8,10 +8,10 @@
<title>Settings - ClassroomIO</title>
</svelte:head>
<section class="mx-auto w-full md:max-w-4xl">
<div class="px-5 py-10">
<section class="w-full md:max-w-6xl lg:mx-auto">
<div class="px-5 md:py-10">
<div class="mb-10 flex items-center justify-between">
<h1 class="text-3xl font-bold dark:text-white">{$t('settings.heading')}</h1>
<h1 class="text-3xl dark:text-white">{$t('settings.heading')}</h1>
<ViewSiteBtn />
</div>
@@ -14,7 +14,7 @@
>{$t('upgrade.back')}</a
>
<div class="mb-10 flex items-center justify-between">
<h1 class="text-3xl font-bold dark:text-white">
<h1 class="text-3xl dark:text-white">
{$t('components.settings.customize_lms.title')}
</h1>
</div>
@@ -14,7 +14,7 @@
>{$t('upgrade.back')}</a
>
<div class="mb-10 flex items-center justify-between">
<h1 class="text-3xl font-bold dark:text-white">{$t('components.settings.domains.title')}</h1>
<h1 class="text-3xl dark:text-white">{$t('components.settings.domains.title')}</h1>
</div>
<Domains />
@@ -15,7 +15,7 @@
>{$t('upgrade.back')}</a
>
<div class="mb-10 flex items-center justify-between">
<h1 class="text-3xl font-bold dark:text-white">{$t('upgrade.team_members')}</h1>
<h1 class="text-3xl dark:text-white">{$t('upgrade.team_members')}</h1>
</div>
<UpgradeBanner>{$t('upgrade.team')}</UpgradeBanner>
@@ -80,10 +80,10 @@
};
</script>
<section class="mx-auto w-full md:max-w-4xl">
<section class="mx-auto w-full md:max-w-3xl">
<div class="px-2 py-2 md:px-5 md:py-10">
<div class="flex items-center gap-2">
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">{$t('setup.get_started')}</h1>
<h1 class="text-2xl 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"