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:
co-authored by
Rotimi Best
parent
2145782385
commit
cbf6ae7e63
@@ -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)} />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user