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 -6
View File
@@ -28,8 +28,6 @@
import { globalStore } from '$lib/utils/store/app';
import { currentOrg } from '$lib/utils/store/org';
import { isMobile } from '$lib/utils/store/useMobile';
// import { Theme } from 'carbon-components-svelte';
// import type { CarbonTheme } from 'carbon-components-svelte/types/Theme/Theme.svelte';
import merge from 'lodash/merge';
import { onMount } from 'svelte';
import { MetaTags } from 'svelte-meta-tags';
@@ -46,7 +44,6 @@
let supabase = getSupabase();
let path = $derived($page.url?.pathname?.replace('/', ''));
let queryParam = $page.url?.search;
// let carbonTheme: CarbonTheme = $derived($globalStore.isDark ? 'g100' : 'white');
function handleResize() {
isMobile.update(() => window.innerWidth <= 760);
@@ -125,8 +122,6 @@
<MetaTags {...metaTags} />
<!-- <Theme bind:theme={carbonTheme} /> -->
<UpgradeModal />
<Snackbar />
@@ -138,7 +133,7 @@
{:else if data.isOrgSite && !path}
<OrgLandingPage orgSiteName={data.orgSiteName} org={data.org} />
{:else}
<main class="font-roboto dark:bg-black">
<main class="z-20000 dark:bg-black">
{#if !hideNavByRoute($page.url?.pathname)}
{#if isOrgPage($page.url?.pathname) || $page.url?.pathname.includes('profile') || isCoursesPage(path)}
<OrgNavigation bind:title={$course.title} isCoursePage={isCoursesPage(path)} />
+7 -8
View File
@@ -1,15 +1,15 @@
<script>
import { preventDefault } from '$lib/utils/functions/svelte';
import { onMount } from 'svelte';
import BotIcon from '@lucide/svelte/icons/bot';
import DoorOpenIcon from '@lucide/svelte/icons/door-open';
import ChartAreaIcon from '@lucide/svelte/icons/chart-area';
import BotIcon from '@lucide/svelte/icons/bot';
import { getSupabase } from '$lib/utils/functions/supabase';
import { preventDefault } from '$lib/utils/functions/svelte';
import { validateEmail } from '$lib/utils/functions/validateEmail';
import TextField from '$lib/components/Form/TextField.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { getSupabase } from '$lib/utils/functions/supabase';
import { validateEmail } from '$lib/utils/functions/validateEmail';
import Edra from '$lib/components/Edra/EdraRoot.svelte';
let email = $state('');
let isAdding = $state(false);
@@ -58,7 +58,7 @@
<title>ClassroomIO - The classroom software</title>
</svelte:head>
<div class="m-2 flex w-screen flex-col items-center justify-center font-sans sm:m-0 md:min-h-[93vh] dark:bg-black">
<div class="m-2 flex w-screen flex-col items-center justify-center sm:m-0 md:min-h-[93vh] dark:bg-black">
<!--
<img
src="/logo-192.png"
@@ -74,7 +74,6 @@
<p class="text-center text-lg dark:text-white">The operating system for classroooms of the future 🚀🚀🚀.</p>
</div>
<Edra />
<form onsubmit={preventDefault(handleSubmit)} class="my-4 hidden">
<div class="flex hidden flex-col items-center sm:flex-row">
{#if success}
@@ -1,30 +1,27 @@
<script lang="ts">
import Box from '$lib/components/Box/index.svelte';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { getLectureNo } from '$lib/components/Course/function.js';
import { course, group } from '$lib/components/Course/store';
import { CourseContainer } from '$lib/components/CourseContainer';
import { PageBody, PageNav } from '$lib/components/Page';
import { snackbar } from '$lib/components/Snackbar/store';
import { ROLE } from '$lib/utils/constants/roles';
import { t } from '$lib/utils/functions/translations';
import { takeAttendance } from '$lib/utils/services/attendance';
import { globalStore } from '$lib/utils/store/app';
import { attendance } from '$lib/utils/store/attendance';
import { profile } from '$lib/utils/store/user';
import type { GroupPerson, Lesson } from '$lib/utils/types/index';
import {
Checkbox,
Pagination,
Search,
StructuredList,
StructuredListBody,
StructuredListCell,
StructuredListHead,
StructuredListRow
} from 'carbon-components-svelte';
import { Input } from '@cio/ui/base/input';
import * as Table from '@cio/ui/base/table';
import Search from '@lucide/svelte/icons/search';
import { Checkbox } from '@cio/ui/base/checkbox';
import * as Pagination from '@cio/ui/base/pagination';
import BadgeXIcon from '@lucide/svelte/icons/badge-x';
import { profile } from '$lib/utils/store/user';
import { ROLE } from '$lib/utils/constants/roles';
import { globalStore } from '$lib/utils/store/app';
import { t } from '$lib/utils/functions/translations';
import { attendance } from '$lib/utils/store/attendance';
import { snackbar } from '$lib/components/Snackbar/store';
import { course, group } from '$lib/components/Course/store';
import { takeAttendance } from '$lib/utils/services/attendance';
import { getLectureNo } from '$lib/components/Course/function.js';
import type { GroupPerson, Lesson } from '$lib/utils/types/index';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import Box from '$lib/components/Box/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import { CourseContainer } from '$lib/components/CourseContainer';
let { data = $bindable() } = $props();
interface CourseData {
@@ -44,6 +41,12 @@
: $group.people.filter((person) => !!person.profile && person.role_id === ROLE.STUDENT)
);
let searchValue = $state('');
let currentPage = $state(1);
let pageSize = $state(10);
const filteredStudents = $derived(searchStudents(searchValue, students));
const totalStudents = $derived(filteredStudents.length);
const paginatedStudents = $derived(filteredStudents.slice((currentPage - 1) * pageSize, currentPage * pageSize));
function setAttendance(courseData: CourseData) {
for (const attendanceItem of courseData.attendance) {
@@ -144,70 +147,67 @@
<PageBody width="w-full max-w-6xl md:w-11/12">
<section class="mx-2 my-5 flex items-center lg:mx-9">
<div class="flex w-full flex-col items-start justify-between gap-2 lg:flex-row lg:items-center">
<div class="flex">
<p class="mr-5 flex items-center">
<Checkbox checked disabled />
{$t('course.navItem.attendance.present')}
</p>
<p class="flex items-center">
<div class="flex gap-5">
<p class="flex items-center gap-2">
<Checkbox disabled />
{$t('course.navItem.attendance.absent')}
<span>{$t('course.navItem.attendance.present')}</span>
</p>
<p class="flex items-center gap-2">
<Checkbox disabled />
<span>{$t('course.navItem.attendance.absent')}</span>
</p>
</div>
<div>
<Search
class="dark:text-slate-950"
placeholder={$t('course.navItem.attendance.search_students')}
bind:value={searchValue}
/>
<div class="relative w-full">
<Input type="text" placeholder={$t('course.navItem.attendance.search_students')} bind:value={searchValue} />
<Search class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2" />
</div>
</div>
</section>
<section class="mx-2 my-5 lg:mx-9">
<StructuredList class="relative m-0">
<!-- Moved the lesson headers outside the students loop -->
<StructuredListHead class="bg-primary-100">
<StructuredListRow head class="mx-7">
<StructuredListCell head class="text-primary-600 py-3"
>{$t('course.navItem.attendance.student')}</StructuredListCell
>
{#each $lessons as _lesson, index}
<StructuredListCell head class="text-primary-600 py-3"
>{$t('course.navItem.attendance.lesson')} 0{getLectureNo(index + 1)}</StructuredListCell
>
{/each}
</StructuredListRow>
</StructuredListHead>
{#each searchStudents(searchValue, students) as student}
<StructuredListBody>
<StructuredListRow>
<StructuredListCell>
<div class="flex w-1/4 items-center">
<p class="font-semibold dark:text-white">
{student.profile.fullname}
</p>
</div>
</StructuredListCell>
{#each $lessons as lesson}
<StructuredListCell class="">
<Checkbox
class={$globalStore.isStudent ? 'cursor-not-allowed' : ''}
disabled={$globalStore.isStudent}
checked={$attendance[student.id]
? $attendance[student.id][lesson.id]
? $attendance[student.id][lesson.id].is_present
: false
: false}
on:change={(e) => handleAttendanceChange(e, student, lesson)}
/>
</StructuredListCell>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head class="w-1/4">{$t('course.navItem.attendance.student')}</Table.Head>
{#each $lessons as _lesson, index}
<Table.Head class="text-center"
>{$t('course.navItem.attendance.lesson')} 0{getLectureNo(index + 1)}</Table.Head
>
{/each}
</StructuredListRow>
</StructuredListBody>
{/each}
</StructuredList>
</Table.Row>
</Table.Header>
<Table.Body>
{#each paginatedStudents as student}
<Table.Row>
<Table.Cell class="font-semibold">
{student.profile.fullname}
</Table.Cell>
{#each $lessons as lesson}
<Table.Cell class="text-center">
<div class="flex justify-center">
<Checkbox
disabled={$globalStore.isStudent}
checked={$attendance[student.id]
? $attendance[student.id][lesson.id]
? $attendance[student.id][lesson.id].is_present
: false
: false}
onCheckedChange={(checked) => {
const e = { target: { checked } };
handleAttendanceChange(e, student, lesson);
}}
/>
</div>
</Table.Cell>
{/each}
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
{#if students.length === 0}
<Box className="h-[300px] w-full">
<BadgeXIcon size={48} />
@@ -216,8 +216,39 @@
</h3>
</Box>
{/if}
{#if students.length !== 0}
<Pagination totalItems={10} pageSizes={[10, 15, 20]} />
{#if totalStudents > 0}
<div class="mt-4">
<Pagination.Root
count={totalStudents}
perPage={pageSize}
page={currentPage}
onPageChange={(page) => (currentPage = page)}
>
{#snippet children({ pages, currentPage: activePage })}
<Pagination.Content>
<Pagination.Item>
<Pagination.PrevButton />
</Pagination.Item>
{#each pages as page (page.key)}
{#if page.type === 'ellipsis'}
<Pagination.Item>
<Pagination.Ellipsis />
</Pagination.Item>
{:else}
<Pagination.Item>
<Pagination.Link {page} isActive={activePage === page.value}>
{page.value}
</Pagination.Link>
</Pagination.Item>
{/if}
{/each}
<Pagination.Item>
<Pagination.NextButton />
</Pagination.Item>
</Pagination.Content>
{/snippet}
</Pagination.Root>
</div>
{/if}
</section>
</PageBody>
@@ -7,8 +7,6 @@
import Design from '$lib/components/Course/components/Ceritficate/Design.svelte';
// import Reports from '$lib/components/Course/components/Ceritficate/Reports.svelte';
// import { issueCertificateModal } from '$lib/components/Course/components/Ceritficate/store';
// import { Tabs, Tab } from 'carbon-components-svelte';
// import TabContent from 'carbon-components-svelte/src/Tabs/TabContent.svelte';
import StudentCertificate from '$lib/components/Course/components/Ceritficate/StudentCertificate/Index.svelte';
import { t } from '$lib/utils/functions/translations';
@@ -1,36 +1,37 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { CourseContainer } from '$lib/components/CourseContainer';
import { checkExercisesComplete, fetchLesson, updateLessonCompletion } from '$lib/utils/services/courses';
import { globalStore } from '$lib/utils/store/app';
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import Save from '@lucide/svelte/icons/save';
import * as Select from '@cio/ui/base/select';
import Pencil from '@lucide/svelte/icons/pencil';
import HistoryIcon from '@lucide/svelte/icons/history';
import ListChecksIcon from '@lucide/svelte/icons/list-checks';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
import PencilIcon from '@lucide/svelte/icons/pencil';
import SaveIcon from '@lucide/svelte/icons/save';
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import HistoryIcon from '@lucide/svelte/icons/history';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
import { browser } from '$app/environment';
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions';
import LessonVersionHistory from '$lib/components/Course/components/Lesson/LessonVersionHistory.svelte';
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
import { lesson, setLesson, lessons, lessonSections } from '$lib/components/Course/components/Lesson/store/lessons';
import { getGroupMemberId } from '$lib/components/Course/function';
import MODES from '$lib/utils/constants/mode';
import { profile } from '$lib/utils/store/user';
import { globalStore } from '$lib/utils/store/app';
import { t } from '$lib/utils/functions/translations';
import { snackbar } from '$lib/components/Snackbar/store';
import { course, group } from '$lib/components/Course/store';
import { LANGUAGES } from '$lib/utils/constants/translation';
import { getGroupMemberId } from '$lib/components/Course/function';
import { getIsLessonComplete } from '$lib/components/Course/components/Lesson/functions';
import { COURSE_VERSION, LOCALE, type Lesson, type LessonCompletion } from '$lib/utils/types';
import LessonVersionHistory from '$lib/components/Course/components/Lesson/LessonVersionHistory.svelte';
import { checkExercisesComplete, fetchLesson, updateLessonCompletion } from '$lib/utils/services/courses';
import { lesson, setLesson, lessons, lessonSections } from '$lib/components/Course/components/Lesson/store/lessons';
import { IconButton } from '$lib/components/IconButton';
import { PageBody, PageNav } from '$lib/components/Page';
import { CourseContainer } from '$lib/components/CourseContainer';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import MODES from '$lib/utils/constants/mode';
import { LANGUAGES } from '$lib/utils/constants/translation';
import { t } from '$lib/utils/functions/translations';
import { profile } from '$lib/utils/store/user';
import { COURSE_VERSION, LOCALE, type Lesson, type LessonCompletion } from '$lib/utils/types';
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import Exercises from '$lib/components/Course/components/Lesson/Exercises/index.svelte';
import Materials from '$lib/components/Course/components/Lesson/Materials/index.svelte';
let { data = $bindable() } = $props();
@@ -233,7 +234,7 @@
</IconButton>
{/if}
<div class="hidden flex-row items-center lg:flex">
<div class="flex-row items-center lg:flex">
<IconButton
onClick={() => {
toggleMode();
@@ -241,27 +242,27 @@
disabled={isSaving}
>
{#if mode === MODES.edit}
<SaveIcon size={20} />
<Save size={20} />
{:else}
<PencilIcon size={20} />
<Pencil size={20} />
{/if}
</IconButton>
</div>
<div>
<DropdownMenu.Root>
<DropdownMenu.Trigger>Open</DropdownMenu.Trigger>
<DropdownMenu.Content>
<DropdownMenu.Group>
{#each LANGUAGES as lang}
<DropdownMenu.Item onclick={() => ($lesson.locale = lang.id as LOCALE)}>
{lang.text}
</DropdownMenu.Item>
{/each}
</DropdownMenu.Group>
</DropdownMenu.Content>
</DropdownMenu.Root>
</div>
<Select.Root type="single" bind:value={$lesson.locale}>
<Select.Trigger class="h-9 w-[120px]">
{LANGUAGES.find((lang) => lang.id === $lesson.locale)?.text || 'Language'}
</Select.Trigger>
<Select.Content>
<Select.Group>
{#each LANGUAGES as lang}
<Select.Item value={lang.id}>
{lang.text}
</Select.Item>
{/each}
</Select.Group>
</Select.Content>
</Select.Root>
{/if}
</RoleBasedSecurity>
</div>
@@ -1,25 +1,29 @@
<script lang="ts">
import jsPDF from 'jspdf';
import Papa from 'papaparse';
import { goto } from '$app/navigation';
import Box from '$lib/components/Box/index.svelte';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
import { getLectureNo } from '$lib/components/Course/function.js';
import { course } from '$lib/components/Course/store';
import { CourseContainer } from '$lib/components/CourseContainer';
import { PageBody, PageNav } from '$lib/components/Page';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import { t } from '$lib/utils/functions/translations';
import { fetchExercisesByMarks } from '$lib/utils/services/courses';
import { fetchMarks } from '$lib/utils/services/marks';
import { currentOrg } from '$lib/utils/store/org';
import type { GroupPerson } from '$lib/utils/types';
import type { CurrentOrg } from '$lib/utils/types/org';
import { Loading, OverflowMenu, OverflowMenuItem } from 'carbon-components-svelte';
import autoTable from 'jspdf-autotable';
import { Button } from '@cio/ui/base/button';
import { Progress } from '@cio/ui/base/progress';
import BadgeXIcon from '@lucide/svelte/icons/badge-x';
import DownloadIcon from '@lucide/svelte/icons/download';
import jsPDF from 'jspdf';
import autoTable from 'jspdf-autotable';
import Papa from 'papaparse';
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
import { currentOrg } from '$lib/utils/store/org';
import type { GroupPerson } from '$lib/utils/types';
import { t } from '$lib/utils/functions/translations';
import { fetchMarks } from '$lib/utils/services/marks';
import type { CurrentOrg } from '$lib/utils/types/org';
import { snackbar } from '$lib/components/Snackbar/store';
import { getLectureNo } from '$lib/components/Course/function.js';
import { fetchExercisesByMarks } from '$lib/utils/services/courses';
import Box from '$lib/components/Box/index.svelte';
import { course } from '$lib/components/Course/store';
import { PageBody, PageNav } from '$lib/components/Page';
import { CourseContainer } from '$lib/components/CourseContainer';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { lessons } from '$lib/components/Course/components/Lesson/store/lessons';
let { data } = $props();
@@ -217,20 +221,25 @@
<PageNav title={$t('course.navItem.marks.title')}>
{#snippet widget()}
<RoleBasedSecurity allowedRoles={[1, 2]}>
<OverflowMenu flipped style="border-radius: 50px" class="bg-gray-100 dark:bg-neutral-800">
<div slot="menu" style="">
<div style="">
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<Button variant="ghost" size="icon" class="rounded-full">
{#if isDownloading}
<Loading withOverlay={false} small />
<Progress />
{:else}
<DownloadIcon size={16} />
{/if}
</div>
</div>
<OverflowMenuItem text={$t('course.navItem.marks.export.csv')} on:click={downloadCSV} />
<OverflowMenuItem text={$t('course.navItem.marks.export.pdf')} on:click={downloadPDF} />
</OverflowMenu>
</Button>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="end">
<DropdownMenu.Item onclick={downloadCSV}>
{$t('course.navItem.marks.export.csv')}
</DropdownMenu.Item>
<DropdownMenu.Item onclick={downloadPDF}>
{$t('course.navItem.marks.export.pdf')}
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</RoleBasedSecurity>
{/snippet}
</PageNav>
@@ -1,38 +1,38 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import Avatar from '$lib/components/Avatar/index.svelte';
import { goto } from '$app/navigation';
import { Input } from '@cio/ui/base/input';
import { Button } from '@cio/ui/base/button';
import * as Table from '@cio/ui/base/table';
import CopyIcon from '@lucide/svelte/icons/copy';
import TrashIcon from '@lucide/svelte/icons/trash';
import CheckIcon from '@lucide/svelte/icons/check';
import SearchIcon from '@lucide/svelte/icons/search';
import TextChip from '$lib/components/Chip/Text.svelte';
import Avatar from '$lib/components/Avatar/index.svelte';
import ComingSoon from '$lib/components/ComingSoon/index.svelte';
import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
import InvitationModal from '$lib/components/Course/components/People/InvitationModal.svelte';
import { deleteMemberModal } from '$lib/components/Course/components/People/store';
import type { ProfileRole } from '$lib/components/Course/components/People/types';
import { group } from '$lib/components/Course/store';
import Select from '$lib/components/Form/Select.svelte';
import { IconButton } from '$lib/components/IconButton';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { ROLE_LABEL, ROLES } from '$lib/utils/constants/roles';
import { t } from '$lib/utils/functions/translations';
import { deleteGroupMember } from '$lib/utils/services/courses';
import InvitationModal from '$lib/components/Course/components/People/InvitationModal.svelte';
import DeleteConfirmation from '$lib/components/Course/components/People/DeleteConfirmation.svelte';
import { profile } from '$lib/utils/store/user';
import type { GroupPerson } from '$lib/utils/types';
import {
CopyButton,
Search,
StructuredList,
StructuredListBody,
StructuredListCell,
StructuredListHead,
StructuredListRow
} from 'carbon-components-svelte';
import TrashIcon from '@lucide/svelte/icons/trash';
import { group } from '$lib/components/Course/store';
import { t } from '$lib/utils/functions/translations';
import Select from '$lib/components/Form/Select.svelte';
import { IconButton } from '$lib/components/IconButton';
import { ROLE_LABEL, ROLES } from '$lib/utils/constants/roles';
import { deleteGroupMember } from '$lib/utils/services/courses';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import type { ProfileRole } from '$lib/components/Course/components/People/types';
import { deleteMemberModal } from '$lib/components/Course/components/People/store';
let member: { id?: string; email?: string; profile?: { email: string } } = $state({});
let filterBy: ProfileRole = $state(ROLES[0]);
let searchValue = $state('');
let copiedEmail = $state<string | null>(null);
const people: Array<GroupPerson> = $derived(sortAndFilterPeople($group.people, filterBy));
@@ -80,6 +80,18 @@
function gotoPerson(person) {
goto(`${page.url.href}/${person.profile_id}`);
}
async function copyToClipboard(text: string) {
try {
await navigator.clipboard.writeText(text);
copiedEmail = text;
setTimeout(() => {
copiedEmail = null;
}, 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
}
</script>
<InvitationModal />
@@ -88,12 +100,10 @@
<section class="mx-2 my-5 md:mx-9">
<div class="flex-end mb-7 flex flex-col items-start justify-end gap-2 md:flex-row md:items-center">
<div class="max-w-[320px]">
<Search
class="w-full border-0 bg-zinc-100 dark:text-slate-950"
placeholder={$t('course.navItem.people.search')}
bind:value={searchValue}
/>
<div class="relative max-w-[320px]">
<Input type="text" placeholder={$t('course.navItem.people.search')} bind:value={searchValue} class="w-full" />
<SearchIcon class="absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2" />
</div>
<div class="mb-3">
<Select
@@ -101,131 +111,134 @@
options={ROLES.map((role) => ({ label: $t(role.label), value: role.value }))}
className="dark:text-black mt-3 max-w-[80px]"
/>
<!-- <select bind:value={filterBy} class="mt-3">
{#each ROLES as option}
<option value={option.value}>{option.label}</option>
{/each}
</select> -->
</div>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<p class="hidden w-20 text-lg lg:block dark:text-white"></p>
</RoleBasedSecurity>
</div>
<StructuredList class="m-0">
<StructuredListHead class="bg-slate-100 dark:border-2 dark:border-neutral-800 dark:bg-neutral-800">
<StructuredListRow head class="mx-7">
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>{$t('course.navItem.people.name')}</StructuredListCell
>
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>{$t('course.navItem.people.role')}</StructuredListCell
>
<StructuredListCell head class="text-primary-700 py-3 dark:text-white"
>{$t('course.navItem.people.action')}</StructuredListCell
>
<RoleBasedSecurity allowedRoles={[1, 2]}>
<p class="hidden w-20 text-lg lg:block dark:text-white"></p>
</RoleBasedSecurity>
</StructuredListRow>
</StructuredListHead>
{#each filterPeople(searchValue, people) as person}
<StructuredListBody>
<StructuredListRow class="relative">
<!-- first column -->
<StructuredListCell class="w-4/6 md:w-3/6">
{#if person.profile}
<div class="flex items-start lg:items-center">
<Avatar
src={person.profile.avatar_url}
name={person.profile.fullname}
width="w-8"
height="h-8"
className="mr-3"
/>
<div class="flex flex-col items-start lg:flex-row lg:items-center">
<div class="mr-2">
<p class="text-base font-normal dark:text-white">
{person.profile.fullname}
</p>
<p class="text-primary-600 line-clamp-1 text-xs">
{obscureEmail(getEmail(person))}
</p>
</div>
<div class="flex items-center">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<CopyButton text={getEmail(person)} feedback="Copied Email to clipboard" />
</RoleBasedSecurity>
{#if person.profile_id == $profile.id}
<ComingSoon label={$t('course.navItem.people.you')} />
{/if}
<div class="rounded-md border">
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>{$t('course.navItem.people.name')}</Table.Head>
<Table.Head>{$t('course.navItem.people.role')}</Table.Head>
<Table.Head>{$t('course.navItem.people.action')}</Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each filterPeople(searchValue, people) as person}
<Table.Row>
<!-- first column -->
<Table.Cell class="w-4/6 md:w-3/6">
{#if person.profile}
<div class="flex items-start lg:items-center">
<Avatar
src={person.profile.avatar_url}
name={person.profile.fullname}
width="w-8"
height="h-8"
className="mr-3"
/>
<div class="flex flex-col items-start lg:flex-row lg:items-center">
<div class="mr-2">
<p class="text-base font-normal dark:text-white">
{person.profile.fullname}
</p>
<p class="text-primary-600 line-clamp-1 text-xs">
{obscureEmail(getEmail(person))}
</p>
</div>
<div class="flex items-center">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
onclick={() => copyToClipboard(getEmail(person))}
>
{#if copiedEmail === getEmail(person)}
<CheckIcon size={16} class="text-green-600" />
{:else}
<CopyIcon size={16} />
{/if}
</Button>
</RoleBasedSecurity>
{#if person.profile_id == $profile.id}
<ComingSoon label={$t('course.navItem.people.you')} />
{/if}
</div>
</div>
</div>
</div>
{:else}
<div class="flex w-2/4 items-start lg:items-center">
<TextChip
value={person.email.substring(0, 2).toUpperCase()}
className="bg-primary-200 text-black font-semibold text-xs mr-3"
shape="rounded-full"
/>
<a href="mailto:{person.email}" class="text-md text-primary-600 mr-2 dark:text-white">
{person.email}
</a>
<div class="flex items-center justify-between">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<CopyButton text={getEmail(person)} feedback={$t('course.navItem.people.feedback')} />
</RoleBasedSecurity>
{:else}
<div class="flex w-2/4 items-start lg:items-center">
<TextChip
value={$t('course.navItem.people.pending')}
className="text-xs bg-yellow-200 text-yellow-700 h-fit"
size="sm"
value={person.email.substring(0, 2).toUpperCase()}
className="bg-primary-200 text-black font-semibold text-xs mr-3"
shape="rounded-full"
/>
<a href="mailto:{person.email}" class="text-md text-primary-600 mr-2 dark:text-white">
{person.email}
</a>
<div class="flex items-center justify-between">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
onclick={() => copyToClipboard(getEmail(person))}
>
{#if copiedEmail === getEmail(person)}
<CheckIcon size={16} class="text-green-600" />
{:else}
<CopyIcon size={16} />
{/if}
</Button>
</RoleBasedSecurity>
<TextChip
value={$t('course.navItem.people.pending')}
className="text-xs bg-yellow-200 text-yellow-700 h-fit"
size="sm"
/>
</div>
</div>
</div>
{/if}
</StructuredListCell>
{/if}
</Table.Cell>
<!-- second column -->
<StructuredListCell class="w-1/4">
<p class=" w-1/4 text-center text-base font-normal dark:text-white">
{$t(ROLE_LABEL[person.role_id])}
</p>
</StructuredListCell>
<!-- second column -->
<Table.Cell class="w-1/4">
<p class=" w-1/4 text-center text-base font-normal dark:text-white">
{$t(ROLE_LABEL[person.role_id])}
</p>
</Table.Cell>
<!-- third column -->
<StructuredListCell class="w-1/4 p-0">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden space-x-2 sm:flex sm:items-center">
{#if person.profile_id !== $profile.id}
<IconButton
onClick={() => {
member = person;
$deleteMemberModal.open = true;
}}
>
<TrashIcon size={16} />
</IconButton>
<!-- <IconButton
onClick={() => gotoPerson(person)}
>
<TrashIcon size={16} />
</IconButton> -->
<!-- third column -->
<Table.Cell class="w-1/4">
<RoleBasedSecurity allowedRoles={[1, 2]}>
<div class="hidden space-x-2 sm:flex sm:items-center">
{#if person.profile_id !== $profile.id}
<IconButton
onClick={() => {
member = person;
$deleteMemberModal.open = true;
}}
>
<TrashIcon size={16} />
</IconButton>
<PrimaryButton
variant={VARIANTS.OUTLINED}
label={$t('course.navItem.people.view')}
onClick={() => gotoPerson(person)}
/>
{/if}
</div>
</RoleBasedSecurity>
</StructuredListCell>
</StructuredListRow>
</StructuredListBody>
{/each}
</StructuredList>
<!-- <Pagination totalItems={10} pageSizes={[10, 15, 20]} /> -->
<PrimaryButton
variant={VARIANTS.OUTLINED}
label={$t('course.navItem.people.view')}
onClick={() => gotoPerson(person)}
/>
{/if}
</div>
</RoleBasedSecurity>
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
</section>
@@ -1,16 +1,19 @@
<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 type { UserCourseAnalytics } from '$lib/utils/types/analytics';
import { Grid, Tag } from 'carbon-components-svelte';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { Badge } from '@cio/ui/base/badge';
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 { t } from '$lib/utils/functions/translations';
import { snackbar } from '$lib/components/Snackbar/store';
import { getAccessToken } from '$lib/utils/functions/supabase';
import type { UserCourseAnalytics } from '$lib/utils/types/analytics';
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';
let { data } = $props();
@@ -99,7 +102,7 @@
<section class="px-1">
<HeroProfileCard user={userCourseAnalytics.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 learningActivities as activity}
<ActivityCard {activity} />
@@ -108,7 +111,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.exercises')}
</h3>
@@ -126,26 +129,22 @@
</div>
<Progress value={getPercentage(completedExercises, userCourseAnalytics?.userExercisesStats?.length)} />
<div class="flex items-center justify-between">
<Tag
interactive
filter={exerciseFilter === 'incomplete'}
type={exerciseFilter === 'incomplete' ? 'gray' : 'outline'}
<Badge
type={exerciseFilter === 'incomplete' ? 'secondary' : 'outline'}
class="lowercase text-yellow-700 dark:text-yellow-500"
on:click={() => toggleExerciseFilter('incomplete')}
onclick={() => toggleExerciseFilter('incomplete')}
>
{incompleteExercises}
{$t('analytics.incomplete')}
</Tag>
<Tag
interactive
filter={exerciseFilter === 'completed'}
</Badge>
<Badge
type={exerciseFilter === 'completed' ? 'gray' : 'outline'}
class="lowercase text-green-700 dark:text-green-500"
on:click={() => toggleExerciseFilter('completed')}
onclick={() => toggleExerciseFilter('completed')}
>
{completedExercises}
{$t('analytics.complete')}
</Tag>
</Badge>
</div>
</div>
@@ -181,17 +180,17 @@
Score: {exercise.score}/{exercise.totalPoints}
{#if exercise.isCompleted}
<Tag type={exercise.status === 3 ? 'high-contrast' : 'outline'} size="sm">
<Badge type={exercise.status === 3 ? 'secondary' : 'outline'}>
{exercise.status === 3 ? $t('analytics.graded') : $t('analytics.not_graded')}
</Tag>
</Badge>
{/if}
</p>
</div>
</div>
<Tag class={`${exercise.isCompleted ? 'bg-green-200 text-green-700' : 'bg-yellow-200 text-yellow-700'}`}>
<Badge class={`${exercise.isCompleted ? 'bg-green-200 text-green-700' : 'bg-yellow-200 text-yellow-700'}`}>
{exercise.isCompleted ? $t('analytics.completed') : $t('analytics.incomplete')}
</Tag>
</Badge>
</div>
{/key}
{/each}
@@ -1,29 +1,31 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import Chip from '$lib/components/Chip/index.svelte';
import MarkExerciseModal from '$lib/components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
import { formatAnswers } from '$lib/components/Course/function.js';
import { course } from '$lib/components/Course/store';
import { CourseContainer } from '$lib/components/CourseContainer';
import { PageBody, PageNav } from '$lib/components/Page';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import { snackbar } from '$lib/components/Snackbar/store';
import formatDate from '$lib/utils/functions/formatDate';
import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly';
import { t } from '$lib/utils/functions/translations';
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification';
import { flip } from 'svelte/animate';
import { goto } from '$app/navigation';
import { dndzone } from 'svelte-dnd-action';
import {
deleteSubmission,
fetchSubmissions,
updateQuestionAnswer,
updateSubmission
} from '$lib/utils/services/submissions';
import { course } from '$lib/components/Course/store';
import { t } from '$lib/utils/functions/translations';
import formatDate from '$lib/utils/functions/formatDate';
import { snackbar } from '$lib/components/Snackbar/store';
import { formatAnswers } from '$lib/components/Course/function.js';
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
import isSubmissionEarly from '$lib/utils/functions/isSubmissionEarly';
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification';
import type { SubmissionIdData, SubmissionItem, SubmissionSection } from '$lib/utils/types/submission';
import { SkeletonPlaceholder } from 'carbon-components-svelte';
import { dndzone } from 'svelte-dnd-action';
import { flip } from 'svelte/animate';
import { Skeleton } from '@cio/ui/base/skeleton';
import Chip from '$lib/components/Chip/index.svelte';
import { PageBody, PageNav } from '$lib/components/Page';
import { CourseContainer } from '$lib/components/CourseContainer';
import RoleBasedSecurity from '$lib/components/RoleBasedSecurity/index.svelte';
import MarkExerciseModal from '$lib/components/Course/components/Lesson/Exercise/MarkExerciseModal.svelte';
let { data = $bindable() } = $props();
const { courseId } = data;
@@ -374,12 +376,12 @@
>
<div class="mb-2 flex items-center">
<Chip value={items.length} className="bg-set dark:bg-neutral-800" />
<p class="ml-2 font-bold dark:text-white">{title}</p>
<p class="ml-2 dark:text-white">{title}</p>
</div>
{#if fetching}
<SkeletonPlaceholder style="width: 100%; height: 170px;" class="my-2 rounded-md" />
<SkeletonPlaceholder style="width: 100%; height: 170px;" class="my-2 rounded-md" />
<SkeletonPlaceholder style="width: 100%; height: 170px;" class="my-2 rounded-md" />
<Skeleton class="my-2 h-[170px] w-full rounded-md" />
<Skeleton class="my-2 h-[170px] w-full rounded-md" />
<Skeleton class="my-2 h-[170px] w-full rounded-md" />
{:else}
<div
class="content mb-3 overflow-y-auto pr-2"
@@ -407,7 +409,7 @@
{item.student.username}
</p>
</a>
<a class="text-primary-700 text-md font-bold" href="{page.url.pathname}?submissionId={item.id}">
<a class="text-primary-700 text-md" href="{page.url.pathname}?submissionId={item.id}">
{item.exercise.title}
</a>
<a
@@ -1,5 +1,6 @@
<script lang="ts">
import LMSSideBar from '$lib/components/LMS/SideBar.svelte';
interface Props {
children?: import('svelte').Snippet;
}
+2 -2
View File
@@ -52,7 +52,7 @@
<section class="mx-auto max-w-6xl gap-5">
<div class="m-5">
<div class="mb-10 flex items-center justify-between">
<h1 class="text-2xl font-bold md:text-3xl dark:text-white">
<h1 class="text-2xl md:text-3xl dark:text-white">
{$t(getGreeting())}
{$profile.fullname}!
</h1>
@@ -115,7 +115,7 @@
</p>
{/if}
</span>
<h1 class="my-0 whitespace-nowrap text-5xl font-bold text-[#262626] lg:text-6xl dark:text-white">
<h1 class="my-0 whitespace-nowrap text-5xl text-[#262626] lg:text-6xl dark:text-white">
{progressPercentage} %
</h1>
</div>
@@ -16,7 +16,7 @@
<section class="mx-auto w-full 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('community.title')}</h1>
<h1 class="text-3xl dark:text-white">{$t('community.title')}</h1>
<PrimaryButton label={$t('community.ask_button')} onClick={askCommunity} />
</div>
@@ -1,32 +1,32 @@
<script lang="ts">
import { goto } from '$app/navigation';
import pluralize from 'pluralize';
import TrashIcon from '@lucide/svelte/icons/trash';
import { Dropdown, SkeletonPlaceholder, SkeletonText } from 'carbon-components-svelte';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import Vote from '$lib/components/Vote/index.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import Avatar from '$lib/components/Avatar/index.svelte';
import { IconButton } from '$lib/components/IconButton';
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
import { currentOrg, isOrgAdmin } from '$lib/utils/store/org';
import { profile } from '$lib/utils/store/user';
import { supabase } from '$lib/utils/functions/supabase';
import { askCommunityValidation, commentInCommunityValidation } from '$lib/utils/functions/validator';
import { snackbar } from '$lib/components/Snackbar/store';
import TextField from '$lib/components/Form/TextField.svelte';
import DeleteModal from '$lib/components/Org/Community/DeleteModal.svelte';
import TextEditor from '$lib/components/TextEditor/index.svelte';
import { calDateDiff } from '$lib/utils/functions/date';
import { untrack } from 'svelte';
import { fetchCourses } from '$lib/utils/services/courses';
import pluralize from 'pluralize';
import { goto } from '$app/navigation';
import * as Select from '@cio/ui/base/select';
import { Skeleton } from '@cio/ui/base/skeleton';
import TrashIcon from '@lucide/svelte/icons/trash';
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 type { Course } from '$lib/utils/types';
import { calDateDiff } from '$lib/utils/functions/date';
import { supabase } from '$lib/utils/functions/supabase';
import { snackbar } from '$lib/components/Snackbar/store';
import { fetchCourses } from '$lib/utils/services/courses';
import { currentOrg, isOrgAdmin } from '$lib/utils/store/org';
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
import { askCommunityValidation, commentInCommunityValidation } from '$lib/utils/functions/validator';
let { data } = $props();
const { slug } = data;
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: {
@@ -390,9 +393,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">
@@ -403,13 +406,22 @@
<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="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
: 'Select Course'}
</p>
</Select.Trigger>
<Select.Content>
{#each fetchedCourses as course}
{#if course.id}
<Select.Item value={course.id}>{course.title}</Select.Item>
{/if}
{/each}
</Select.Content>
</Select.Root>
{:else}
<div class="flex items-center">
<Vote value={question.votes} upVote={() => upvoteQuestion('question')} disabled={voted.question} />
@@ -461,19 +473,19 @@
{#if isEditMode && editorInstance}
<div class="my-2">
<TextEditor
bind:value={editContent.body}
placeholder="Give an answer"
onChange={(html) => (editContent.body = html)}
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>
@@ -510,7 +522,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>
@@ -521,7 +533,7 @@
<div>
{#if !editorInstance}
<TextEditor bind:value={comment} placeholder="Give an answer" onChange={(html) => (comment = html)} />
<TextEditor placeholder="Give an answer" content={comment} onChange={(content) => (comment = content)} />
{/if}
<div class="mr-2 flex justify-end">
@@ -1,20 +1,22 @@
<script lang="ts">
import { goto } from '$app/navigation';
import * as Select from '@cio/ui/base/select';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import { Dropdown } from 'carbon-components-svelte';
import { currentOrg } from '$lib/utils/store/org';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
import { askCommunityValidation } from '$lib/utils/functions/validator';
import { supabase } from '$lib/utils/functions/supabase';
import { snackbar } from '$lib/components/Snackbar/store';
import generateSlug from '$lib/utils/functions/generateSlug';
import TextEditor from '$lib/components/TextEditor/index.svelte';
import TextField from '$lib/components/Form/TextField.svelte';
import type { Course } from '$lib/utils/types';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
import { courses } from '$lib/components/Courses/store';
import type { Course } from '$lib/utils/types';
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 { 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;
@@ -93,7 +95,7 @@
{$t('community.ask.go_back')}
</a>
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold dark:text-white">{$t('community.ask.ask_the')}</h1>
<h1 class="text-3xl dark:text-white">{$t('community.ask.ask_the')}</h1>
<PrimaryButton label={$t('community.ask.publish')} onClick={handleSave} />
</div>
</div>
@@ -105,20 +107,34 @@
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}
{#if course.id}
<Select.Item value={course.id}>{course.title}</Select.Item>
{/if}
{/each}
</Select.Content>
</Select.Root>
{#if errors.courseId}
<p class="mt-1 text-sm text-red-500">
{errors.courseId}
</p>
{/if}
</div>
</div>
<TextEditor
bind:value={fields.body}
placeholder={$t('community.ask.ask_community')}
onChange={(html) => (fields.body = html)}
content={fields.body}
onChange={(content) => (fields.body = content)}
/>
</section>
@@ -129,7 +129,7 @@
<section class="mx-auto w-full max-w-6xl">
<div class="p-5">
<div class="mb-10 flex items-center justify-between">
<h1 class="text-3xl font-bold dark:text-white">{$t('exercises.heading')}</h1>
<h1 class="text-3xl dark:text-white">{$t('exercises.heading')}</h1>
</div>
<div>
@@ -139,7 +139,7 @@
class="mr-3 h-[70vh] min-w-[355px] max-w-[355px] overflow-hidden rounded-md border border-gray-50 bg-gray-100 p-3 dark:border-neutral-700 dark:bg-black"
>
<div class="mb-2 flex items-center gap-2">
<p class="ml-2 font-bold dark:text-white">{title}</p>
<p class="ml-2 dark:text-white">{title}</p>
<Chip value={items.length} {className} />
</div>
<div class="h-full overflow-y-auto pb-3 pr-2">
@@ -148,7 +148,7 @@
<a class="text-primary-600 mb-2 flex w-full cursor-pointer items-center" href={item.courseURL}>
<p class="text-xs">{item.courseTitle}</p>
</a>
<a class="text-md font-bold text-black dark:text-white" href={item.exerciseURL}>
<a class="text-md text-black dark:text-white" href={item.exerciseURL}>
{#if id === 3}
({item.grade}) -
{/if}
@@ -1,24 +1,35 @@
<script lang="ts">
import { onMount, untrack } from 'svelte';
import { Search, Dropdown } from 'carbon-components-svelte';
import { profile } from '$lib/utils/store/user';
import { fetchExploreCourses } from '$lib/utils/services/courses';
import Courses from '$lib/components/Courses/index.svelte';
import { Input } from '@cio/ui/base/input';
import * as Select from '@cio/ui/base/select';
import ListIcon from '@lucide/svelte/icons/list';
import GridIcon from '@lucide/svelte/icons/grid-2x2';
import SearchIcon from '@lucide/svelte/icons/search';
import { courseMetaDeta } from '$lib/components/Courses/store';
import { currentOrg } from '$lib/utils/store/org';
import type { Course } from '$lib/utils/types';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { t } from '$lib/utils/functions/translations';
import Courses from '$lib/components/Courses/index.svelte';
import { courseMetaDeta } from '$lib/components/Courses/store';
import { fetchExploreCourses } from '$lib/utils/services/courses';
import { IconButton } from '$lib/components/IconButton';
import GridIcon from '@lucide/svelte/icons/grid-2x2';
import ListIcon from '@lucide/svelte/icons/list';
let searchValue = $state('');
let selectedId: string | undefined = $state();
let hasFetched = false;
let exploreCourseList: Course[] = $state([]);
const filterOptions = $derived([
{ value: '0', label: $t('courses.course_filter.date_created') },
{ value: '1', label: $t('courses.course_filter.lessons') }
]);
const selectedLabel = $derived(
filterOptions.find((opt) => opt.value === selectedId)?.label || filterOptions[0].label
);
const filteredExploreCourses: Course[] = $derived(filterCourses(searchValue, selectedId || '', exploreCourseList));
function getCourses(userId?: string, orgId?: string) {
@@ -85,24 +96,23 @@
<section class="w-full md:mx-auto md:max-w-6xl">
<div class="px-2 py-2 md:px-5 md:py-10">
<h1 class="text-3xl font-bold dark:text-white">{$t('explore.heading')}</h1>
<h1 class="text-3xl dark:text-white">{$t('explore.heading')}</h1>
<div class="flex flex-row-reverse">
<div class="filter-container 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"
size="xl"
bind:selectedId
items={[
{ id: '0', text: $t('courses.course_filter.date_created') },
{ id: '1', text: $t('courses.course_filter.lessons') }
]}
/>
<div class="filter-container flex items-end justify-start gap-2">
<div class="relative">
<SearchIcon class="text-muted-foreground absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2" />
<Input type="text" placeholder={$t('courses.search_placeholder')} bind:value={searchValue} class="pl-9" />
</div>
<Select.Root type="single" bind:value={selectedId}>
<Select.Trigger class="min-w-[150px]">
<p>{selectedLabel}</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')}>
@@ -1,16 +1,20 @@
<script lang="ts">
import { Search } from 'carbon-components-svelte';
import { untrack } from 'svelte';
import { Input } from '@cio/ui/base/input';
import SearchIcon from '@lucide/svelte/icons/search';
import Tabs from '$lib/components/Tabs/index.svelte';
import TabContent from '$lib/components/TabContent/index.svelte';
import Courses from '$lib/components/Courses/index.svelte';
import { fetchCourses } from '$lib/utils/services/courses';
import TabContent from '$lib/components/TabContent/index.svelte';
import { profile } from '$lib/utils/store/user';
import { currentOrg } from '$lib/utils/store/org';
import { courses, courseMetaDeta, coursesComplete, coursesInProgress } from '$lib/components/Courses/store';
import { untrack } from 'svelte';
import { t } from '$lib/utils/functions/translations';
import { courses, courseMetaDeta, coursesComplete, coursesInProgress } from '$lib/components/Courses/store';
let hasFetched = false;
let searchValue = $state('');
function onChange(tab) {
return () => (currentTab = tab);
@@ -60,8 +64,9 @@
<section class="mx-auto max-w-6xl">
<div class="m-2 md:m-5">
<div role="searchbox" class=" w-full bg-gray-100 md:w-[60%] lg:w-[30%]">
<Search placeholder={$t('my_learning.search')} class="dark:text-black" />
<div role="searchbox" class="relative w-full md:w-[60%] lg:w-[30%]">
<Input type="text" placeholder={$t('my_learning.search')} bind:value={searchValue} />
<SearchIcon class="text-muted-foreground absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2" />
</div>
<h1 class="my-4 text-3xl font-semibold">{$t('my_learning.heading')}</h1>
<Tabs {tabs} {currentTab} {onChange}>
@@ -1,22 +1,23 @@
<script>
import { page } from '$app/state';
import { Tabs, Tab, TabContent } from 'carbon-components-svelte';
import Profile from '$lib/components/LMS/components/Profile.svelte';
import Integrations from '$lib/components/Org/Settings/Integrations.svelte';
import * as Tabs from '@cio/ui/base/tabs';
import { t } from '$lib/utils/functions/translations';
let selected = $state(0);
import Profile from '$lib/components/LMS/components/Profile.svelte';
import Integrations from '$lib/components/Org/Settings/Integrations.svelte';
let selectedTab = $state('profile');
const tabs = $derived([
{
key: 0,
key: 'profile',
label: $t('settings.tabs.profile_tab'),
tabKey: '',
href: page.url.pathname
},
{
key: 1,
key: 'integrations',
label: $t('settings.tabs.integrations_tab'),
tabKey: 'integrations',
href: `${page.url.pathname}?tab=integrations`,
disabled: false
}
@@ -26,23 +27,23 @@
<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-3xl font-bold dark:text-white">{$t('settings.heading')}</h1>
<h1 class="text-3xl dark:text-white">{$t('settings.heading')}</h1>
</div>
<div class="">
<Tabs autoWidth bind:selected>
{#each tabs as tab}
<Tab label={tab.label} href={tab.href} />
{/each}
<svelte:fragment slot="content">
<TabContent>
<Profile />
</TabContent>
<TabContent>
<Integrations />
</TabContent>
</svelte:fragment>
</Tabs>
<Tabs.Root bind:value={selectedTab}>
<Tabs.List>
{#each tabs as tab}
<Tabs.Trigger value={tab.key}>{tab.label}</Tabs.Trigger>
{/each}
</Tabs.List>
<Tabs.Content value="profile">
<Profile />
</Tabs.Content>
<Tabs.Content value="integrations">
<Integrations />
</Tabs.Content>
</Tabs.Root>
</div>
</div>
</section>
@@ -1,7 +1,7 @@
<script lang="ts">
import { env } from '$env/dynamic/public';
import { goto } from '$app/navigation';
import { Dropdown } from 'carbon-components-svelte';
import * as Select from '@cio/ui/base/select';
import TextField from '$lib/components/Form/TextField.svelte';
import UserProfileIcon from '$lib/components/Icons/UserProfileIcon.svelte';
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
@@ -43,19 +43,18 @@
let step = $state(1);
let loading = $state(false);
let isSiteNameTouched = $state(false);
let selectedId = 'en';
let dropdownItems = [
{ id: 'de', text: 'German' },
{ id: 'en', text: 'English' },
{ id: 'es', text: 'Spanish' },
{ id: 'fr', text: 'French' },
{ id: 'hi', text: 'Hindi' },
{ id: 'pl', text: 'Polish' },
{ id: 'pt', text: 'Portuguese' },
{ id: 'ru', text: 'Russian' },
{ id: 'vi', text: 'Vietnamese' },
{ id: 'da', text: 'Danish' }
{ id: 'de' as LOCALE, text: 'German' },
{ id: 'en' as LOCALE, text: 'English' },
{ id: 'es' as LOCALE, text: 'Spanish' },
{ id: 'fr' as LOCALE, text: 'French' },
{ id: 'hi' as LOCALE, text: 'Hindi' },
{ id: 'pl' as LOCALE, text: 'Polish' },
{ id: 'pt' as LOCALE, text: 'Portuguese' },
{ id: 'ru' as LOCALE, text: 'Russian' },
{ id: 'vi' as LOCALE, text: 'Vietnamese' },
{ id: 'da' as LOCALE, text: 'Danish' }
];
const progress = $derived(Math.round((step / maxSteps) * 100));
@@ -102,11 +101,6 @@
}
];
function handleSelect(event) {
const newSelectedId = event.detail.selectedId;
fields.locale = newSelectedId;
}
async function setMetaData() {
if (!env.PUBLIC_IP_REGISTRY_KEY) return;
@@ -363,8 +357,17 @@
<!-- Language Picker -->
<div class="mt-10">
<span>{$t('content.toggle_label')}: </span>
<Dropdown items={dropdownItems} {selectedId} on:select={handleSelect} class="w-full" />
<span class="dark:text-white">{$t('content.toggle_label')}: </span>
<Select.Root type="single" bind:value={fields.locale}>
<Select.Trigger class="w-full">
<p>{fields.locale}</p>
</Select.Trigger>
<Select.Content>
{#each dropdownItems as item}
<Select.Item value={item.id}>{item.text}</Select.Item>
{/each}
</Select.Content>
</Select.Root>
</div>
</div>
</div>
@@ -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"