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 <[email protected]>
This commit is contained in:
co-authored by
Rotimi Best
parent
2145782385
commit
cbf6ae7e63
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user