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

* feat: remove tinymce & replace org sidebar

* chore: PR Fixes

* chore: move ui font into src dir

* remove files in vite config

* use google font import for loading font

* remove custom font

* more fonts update

* remove font bold

* more removals of font bold

* fix import

* checkpoint

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

* minor css improvements and try out font sans

* chore: replace carbon components with shadcn's

* chore: replace carbon components with shadcn _ v3

* chore: ui fixes & storybook update

* feat: UI cleanup

* fix: quick ui fixes

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

* chore: add isloading to button

* feat: add loading state to button stories

* chore: feedback fixes

* fix: feedback fixes

* chore: remove console

* fix: PR comments

* fix: PR comments

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
This commit is contained in:
Adeyemi Adetunji
2025-11-21 12:18:04 +01:00
committed by GitHub
co-authored by Rotimi Best
parent 2145782385
commit cbf6ae7e63
499 changed files with 19708 additions and 15603 deletions
@@ -1,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