feat: migrate dashboard to svelte 5 (#577)

* chore: migration to SvelteKit 2

* stage 1: migrate to svelte 5 and fix rerendering issues

* stage 2: fix more $: reactivity across dashboard

* fix merge conflicts

* chore: migrate code in main to svelte 5

* chore: update preventDefault

* chore: simplify tsconfig and remove unused mobileMenuRef in MobileMenu.svelte

* chore: enforce .ts across dashboard

* rename from classroomio-com to website

* stage-3: Replace carbon icon svelte with lucide icon

* update lock file

* chore: finish icon replacement

* fix: settings and setup page multiple rerender

* stage 3: More icon and rerendering fixes

* feat: checkpoint

* feat: update editor

* [not working] connect everything with ui build

* storybook + ui + dashboard all working independently

* add bg-black to +page.svelte

* feat: add editor to storybook

* chore: animate lucid icons (#590)

* v1 animations

* feat: change some course navigation icons & animate org navigation icons

* chore: change settings icon

* chore: use css instead of js for hover

* rename animations

---------

Co-authored-by: tunny17 <[email protected]>

* chore: update path to node_modules types in api tsconfig

* fix: update import path for DropdownMenu component

* fix: add rollupOptions to build configuration in vite.config.ts

* fix: remove rollupOptions from build configuration and add @floating-ui/dom dependency

* install

---------

Co-authored-by: tunny17 <[email protected]>
This commit is contained in:
Rotimi Best
2025-11-01 16:18:29 +01:00
committed by GitHub
co-authored by tunny17
parent 0fb66a3782
commit f5a4156c8b
159 changed files with 15225 additions and 3722 deletions
@@ -23,7 +23,7 @@
import { snackbar } from '$lib/components/Snackbar/store';
import TabContent from '$lib/components/TabContent/index.svelte';
import Tabs from '$lib/components/Tabs/index.svelte';
import TextEditor from '$lib/components/TextEditor/index.svelte';
// import TextEditor from '$lib/components/TextEditor/index.svelte';
import MODES from '$lib/utils/constants/mode';
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
import { supabase } from '$lib/utils/functions/supabase';
@@ -67,16 +67,27 @@
toggleMode = () => {}
}: Props = $props();
// let content = $state<EdraEditorProps['Content']>();
// let editor = $state<EdraEditorProps['Editor']>();
// function onUpdate() {
// content = editor?.getHTML();
// console.log('content updated:', content);
// if (mode === MODES.view) return;
// $lessonByTranslation[lessonId][$lesson.locale] = content;
// try {
// localStorage.setItem(`lesson-${lessonId}-${$lesson.locale}`, content);
// } catch (error) {}
// $isLessonDirty = true;
// }
let localeExists: Record<string, boolean> = {};
// let prevContent = '';
let timeoutId: NodeJS.Timeout;
let errors: Record<string, string> = {};
let editorWindowRef: Window | undefined = $state();
let aiButtonRef: HTMLDivElement | undefined = $state();
let openPopover = $state(false);
let player: HTMLVideoElement | undefined = $state();
let aiButtonClass =
'flex items-center gap-2 px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2';
// let editorWindowRef: Window | undefined = $state();
('flex items-center gap-2 px-5 py-2 border border-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md w-full mb-2');
const lessonTitle = $derived($lesson.title);
@@ -324,7 +335,7 @@
initPlyr(player, $lesson.materials.videos);
});
let editorValue = $derived(
let _editorValue = $derived(
lessonFallbackNote($lesson.materials.note, $lessonByTranslation[lessonId], $lesson.locale)
);
</script>
@@ -403,7 +414,26 @@
</div>
<div class="mt-5 h-[60vh]">
<TextEditor
<!-- <div class="bg-background z-50 mt-12 size-full max-w-5xl rounded-md border border-dashed">
{#if editor && !editor.isDestroyed}
<EdraToolBar
class="bg-secondary/50 flex w-full items-center overflow-x-auto border-b border-dashed p-0.5"
{editor}
/>
{:else}
<div>
Debug: Editor state - {editor ? 'exists' : 'undefined'}, destroyed: {editor?.isDestroyed || 'N/A'}
</div>
{/if}
<EdraEditor
bind:editor
content={editorValue}
autofocus={true}
class="h-[300px] outline-none"
{onUpdate}
/>
</div> -->
<!-- <TextEditor
id={lessonId}
bind:editorWindowRef
value={editorValue}
@@ -419,7 +449,7 @@
$isLessonDirty = true;
}}
placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')}
/>
/> -->
</div>
</TabContent>
@@ -79,7 +79,7 @@
<div class={className}>
<button
class="item relative flex items-center {btnPadding} mb-1 ml-2 {NavClasses.item} {isGroupActive &&
class="item group relative flex items-center {btnPadding} mb-1 ml-2 {NavClasses.item} {isGroupActive &&
!isLoading &&
NavClasses.active} w-[95%]"
tabindex="0"
@@ -1,15 +1,15 @@
<script lang="ts">
import TableOfContentsIcon from '@lucide/svelte/icons/table-of-contents'; // overview
import TableOfContentsIcon from '@lucide/svelte/icons/table-of-contents';
import BookCheckIcon from '@lucide/svelte/icons/book-check';
import ListTodoIcon from '@lucide/svelte/icons/list-todo';
import UsersIcon from '@lucide/svelte/icons/users';
import ScreenShareIcon from '@lucide/svelte/icons/screen-share';
import AwardIcon from '@lucide/svelte/icons/award';
import ScrollTextIcon from '@lucide/svelte/icons/scroll-text';
import SettingsIcon from '@lucide/svelte/icons/settings';
import MegaphoneIcon from '@lucide/svelte/icons/megaphone';
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
import ListIcon from '@lucide/svelte/icons/list';
import SquareLibrary from '@lucide/svelte/icons/square-library';
import BookOpenCheck from '@lucide/svelte/icons/book-open-check';
import PanelTop from '@lucide/svelte/icons/panel-top';
import { NAV_IDS } from './constants';
interface Props {
@@ -20,25 +20,25 @@
</script>
{#if name === NAV_IDS.SECTION}
<TableOfContentsIcon size={16} />
<TableOfContentsIcon size={16} class="group-hover:animate-flip-book" />
{:else if name === NAV_IDS.NEWS_FEED}
<MegaphoneIcon size={16} />
<MegaphoneIcon size={16} class="group-hover:animate-shake-announcement" />
{:else if name === NAV_IDS.LESSONS}
<ScrollTextIcon size={16} />
<ScrollTextIcon size={16} class="group-hover:animate-scroll-unroll" />
{:else if name === NAV_IDS.ATTENDANCE}
<ListIcon size={16} />
<BookOpenCheck size={16} class="group-hover:animate-check-items" />
{:else if name === NAV_IDS.SUBMISSIONS}
<ListTodoIcon size={16} />
<SquareLibrary size={16} class="group-hover:animate-task-complete" />
{:else if name === NAV_IDS.MARKS}
<BookCheckIcon size={16} />
<BookCheckIcon size={16} class="group-hover:animate-grade-glow" />
{:else if name === NAV_IDS.PEOPLE}
<UsersIcon size={16} />
<UsersIcon size={16} class="group-hover:animate-people-wave" />
{:else if name === NAV_IDS.ANALYTICS}
<ChartLineIcon size={16} />
<ChartLineIcon size={16} class="group-hover:animate-chart-grow" />
{:else if name === NAV_IDS.LANDING_PAGE}
<ScreenShareIcon size={16} />
<PanelTop size={16} class="group-hover:animate-screen-glow" />
{:else if name === NAV_IDS.CERTIFICATES}
<AwardIcon size={16} />
<AwardIcon size={16} class="group-hover:animate-bounce" />
{:else if name === NAV_IDS.SETTINGS}
<SettingsIcon size={16} />
<SettingsIcon size={16} class="group-hover:animate-spin" />
{/if}
@@ -35,7 +35,7 @@
}
});
const result = (await response.json());
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || $t('courses.copy_course.error.failed_to_clone'));
@@ -34,11 +34,6 @@
emptyDescription = $t('courses.course_card.empty_description'),
isExplore = false
}: Props = $props();
export let courses: Course[] = [];
export let emptyTitle = $t('courses.course_card.empty_title');
export let emptyDescription = $t('courses.course_card.empty_description');
export let isExplore = false;
function calcProgressRate(progressRate?: number, totalLessons?: number): number {
if (!progressRate || !totalLessons) {
@@ -55,19 +50,19 @@
try {
await deleteCourse($deleteCourseModal.id);
// Remove the course from the courses store
$coursesStore = $coursesStore.filter(course => course.id !== $deleteCourseModal.id);
$coursesStore = $coursesStore.filter((course) => course.id !== $deleteCourseModal.id);
// Show success message
snackbar.success('snackbar.course_deleted');
// Close modal and reset state
deleteCourseModal.set(deleteCourseModalInitialState);
} catch (error) {
console.error('Error deleting course:', error);
snackbar.error('snackbar.course_settings.error.went_wrong');
// Stop deleting state on error
$deleteCourseModal.isDeleting = false;
}
@@ -75,8 +70,8 @@
</script>
<CopyCourseModal />
<DeleteModal
onDelete={handleDeleteCourse}
<DeleteModal
onDelete={handleDeleteCourse}
bind:open={$deleteCourseModal.open}
isLoading={$deleteCourseModal.isDeleting}
/>
@@ -0,0 +1,6 @@
<script lang="ts">
import { Editor } from '@cio/ui/custom/editor';
import defaultContent from './default-content';
</script>
<Editor content={defaultContent} />
@@ -0,0 +1,825 @@
export default {
type: 'doc',
content: [
{
type: 'heading',
attrs: {
textAlign: 'center',
level: 1
},
content: [
{
type: 'text',
text: 'Introducing ClassroomIO'
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: 'justify',
class: null
},
content: [
{
type: 'text',
text: 'The AI native LMS. We have '
},
{
type: 'text',
marks: [
{
type: 'bold'
}
],
text: 'bold'
},
{
type: 'text',
text: ', '
},
{
type: 'text',
marks: [
{
type: 'italic'
}
],
text: 'italic'
},
{
type: 'text',
text: ', '
},
{
type: 'text',
marks: [
{
type: 'strike'
}
],
text: 'strike'
},
{
type: 'text',
text: ', '
},
{
type: 'text',
marks: [
{
type: 'underline'
}
],
text: 'underline'
},
{
type: 'text',
text: ', '
},
{
type: 'text',
marks: [
{
type: 'link',
attrs: {
href: 'https://github.com',
target: '_blank',
rel: 'noopener noreferrer',
class: null
}
}
],
text: 'link'
},
{
type: 'text',
text: ', '
},
{
type: 'text',
marks: [
{
type: 'code'
}
],
text: 'code'
},
{
type: 'text',
text: ', Superscript A'
},
{
type: 'text',
marks: [
{
type: 'superscript'
}
],
text: 'b'
},
{
type: 'text',
text: ', subscript A'
},
{
type: 'text',
marks: [
{
type: 'subscript'
}
],
text: 'b'
},
{
type: 'text',
text: ', '
},
{
type: 'text',
marks: [
{
type: 'highlight',
attrs: {
color: '#800080'
}
}
],
text: 'highlight1'
},
{
type: 'text',
marks: [
{
type: 'highlight',
attrs: {
color: '#1a1600'
}
}
],
text: ','
},
{
type: 'text',
text: ' '
},
{
type: 'text',
marks: [
{
type: 'highlight',
attrs: {
color: '#0000FF'
}
}
],
text: 'highlight2'
},
{
type: 'text',
text: ', '
},
{
type: 'text',
marks: [
{
type: 'textStyle',
attrs: {
color: '#008000',
fontSize: ''
}
}
],
text: 'textcolor1'
},
{
type: 'text',
marks: [
{
type: 'textStyle',
attrs: {
color: 'rgb(78, 211, 21)',
fontSize: ''
}
}
],
text: ', '
},
{
type: 'text',
marks: [
{
type: 'textStyle',
attrs: {
color: '#FFA500',
fontSize: ''
}
}
],
text: 'textcolor2'
}
]
},
{
type: 'blockquote',
content: [
{
type: 'paragraph',
attrs: {
textAlign: 'justify',
class: null
},
content: [
{
type: 'text',
text: 'This is a block Quote'
}
]
}
]
},
{
type: 'codeBlock',
attrs: {
language: 'javascript'
},
content: [
{
type: 'text',
text: 'console.log("Hello World")'
}
]
},
{
type: 'horizontalRule'
},
{
type: 'heading',
attrs: {
textAlign: 'left',
level: 2
},
content: [
{
type: 'text',
text: 'Support For Media Placeholders and Medias'
}
]
},
{
type: 'image-placeholder'
},
{
type: 'image',
attrs: {
src: 'https://placehold.co/800x400/6A00F5/white',
alt: null,
title: 'A Sample Image',
width: 658,
height: null,
align: 'center'
}
},
{
type: 'video-placeholder'
},
{
type: 'video',
attrs: {
src: 'https://videos.pexels.com/video-files/2491284/2491284-uhd_2732_1440_24fps.mp4',
alt: null,
title: 'A Sample Video',
width: 673,
height: null,
align: 'center'
}
},
{
type: 'audio-placeholder'
},
{
type: 'audio',
attrs: {
src: 'https://cdn.pixabay.com/audio/2024/10/30/audio_42e6870f29.mp3',
alt: null,
title: 'A Sample Audio',
width: 670,
height: null,
align: 'center'
}
},
{
type: 'horizontalRule'
},
{
type: 'heading',
attrs: {
textAlign: null,
level: 2
},
content: [
{
type: 'text',
text: 'IFrames'
}
]
},
{
type: 'iframe-placeholder'
},
{
type: 'iframe',
attrs: {
src: 'https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3748.5645078258503!2d75.17451197505285!3d20.026784381383496!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x3bdb93bd138ae4bd%3A0x574c6482cf0b89cf!2sEllora%20Caves!5e0!3m2!1sen!2sin!4v1742466514831!5m2!1sen!2sin',
alt: null,
title: 'Ellora Caves, Maharastra, India',
width: 621,
height: null,
align: 'center'
}
},
{
type: 'iframe',
attrs: {
src: 'https://www.youtube.com/embed/jfKfPfyJRdk?si=SYBE0PkI5v79-54W',
alt: null,
title: 'Lofi Hip Hop Youtube Streaming Video',
width: 623,
height: null,
align: 'center'
}
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
}
},
{
type: 'horizontalRule'
},
{
type: 'heading',
attrs: {
textAlign: null,
level: 2
},
content: [
{
type: 'text',
text: 'Typography'
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Smilipicker, type '
},
{
type: 'text',
marks: [
{
type: 'code'
}
],
text: ':)'
},
{
type: 'text',
text: ' and it becomes 🙂 or 😉. Typography support e.g. type '
},
{
type: 'text',
marks: [
{
type: 'code'
}
],
text: '!='
},
{
type: 'text',
text: ' and it becomes ≠. Similarly (c) becomes ©, -> becomes → and many more like 1×2, ½. We also have color visualizer. #FFF, #000, #FF00FF can be visualized.'
}
]
},
{
type: 'horizontalRule'
},
{
type: 'heading',
attrs: {
textAlign: null,
level: 2
},
content: [
{
type: 'text',
text: 'Tables'
}
]
},
{
type: 'table',
content: [
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: [323],
style: null
},
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Hello World'
}
]
}
]
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: [328],
style: null
},
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Hello World'
}
]
}
]
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: [330],
style: null
},
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Hello World'
}
]
}
]
}
]
},
{
type: 'tableRow',
content: [
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: [323],
style: null
},
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Hello World'
}
]
}
]
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: [328],
style: null
},
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Hello World'
}
]
}
]
},
{
type: 'tableCell',
attrs: {
colspan: 1,
rowspan: 1,
colwidth: [330],
style: null
},
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Hello World'
}
]
}
]
}
]
}
]
},
{
type: 'heading',
attrs: {
textAlign: null,
level: 3
},
content: [
{
type: 'text',
marks: [
{
type: 'textStyle',
attrs: {
color: 'var(--tw-prose-headings)',
fontSize: '1.5em'
}
}
],
text: 'Rending Math and '
},
{
type: 'inlineMath',
attrs: {
latex: '\\LaTeX',
evaluate: 'no',
display: 'no'
}
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'The editor supports the '
},
{
type: 'inlineMath',
attrs: {
latex: '\\LaTeX',
evaluate: 'no',
display: 'no'
}
},
{
type: 'text',
text: ' rendering. e.g.'
}
]
},
{
type: 'orderedList',
attrs: {
start: 1
},
content: [
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Functions like, '
},
{
type: 'inlineMath',
attrs: {
latex: 'sin²\\theta + cos²\\theta=1, a²+b²=c²',
evaluate: 'no',
display: 'no'
}
}
]
}
]
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Matrix like unit matrix'
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'inlineMath',
attrs: {
latex: '\\begin{pmatrix} 1 & 0 \\\\ 0 & 1 \\end{pmatrix}',
evaluate: 'no',
display: 'no'
}
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'or more matrices like'
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'inlineMath',
attrs: {
latex:
'\\begin{pmatrix} 1 & 0 & \\cdots & 0 \\\\ 0 & 1 & \\cdots & 0 \\\\ \\vdots & \\vdots & \\ddots & \\vdots \\\\ 0 & 0 & \\cdots & 1 \\end{pmatrix}',
evaluate: 'no',
display: 'no'
}
}
]
}
]
},
{
type: 'listItem',
content: [
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'text',
text: 'Integrals and differential'
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'inlineMath',
attrs: {
latex:
'\\int_{0}^{\\frac{\\pi}{2}} \\sin(x) \\,dx = \\left[-\\cos(x)\\right]_0^{\\frac{\\pi}{2}} = -\\cos\\left(\\frac{\\pi}{2}\\right) + \\cos(0)',
evaluate: 'no',
display: 'no'
}
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
},
content: [
{
type: 'inlineMath',
attrs: {
latex: '\\frac{\\mathrm{d}}{\\mathrm{d}x}(\\sin x) = \\cos x',
evaluate: 'no',
display: 'no'
}
}
]
}
]
}
]
},
{
type: 'heading',
attrs: {
textAlign: null,
level: 3
},
content: [
{
type: 'text',
text: 'Explore More…'
}
]
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
}
},
{
type: 'paragraph',
attrs: {
textAlign: null,
class: null
}
}
]
};
@@ -11,11 +11,6 @@
}
let { open = $bindable(false), onDelete = () => {}, isLoading = false }: Props = $props();
function handleDelete() {
onDelete();
open = false;
}
</script>
<Modal
@@ -1,17 +1,14 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { browser } from '$app/environment';
import BellIcon from '@lucide/svelte/icons/bell';
import MoonIcon from '@lucide/svelte/icons/moon';
import SunIcon from '@lucide/svelte/icons/sun';
import MenuIcon from '@lucide/svelte/icons/menu';
import XIcon from '@lucide/svelte/icons/x';
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
import { ModeSwitcher } from '@cio/ui/base/dark-mode';
import { IconButton } from '$lib/components/IconButton';
import { globalStore } from '$lib/utils/store/app';
import { currentOrgPath } from '$lib/utils/store/org';
import { toggleBodyByMode } from '$lib/utils/functions/app';
import { sideBar } from '../Org/store';
import { t } from '$lib/utils/functions/translations';
@@ -27,16 +24,6 @@
$sideBar.hidden = !$sideBar.hidden;
};
function toggleDarkMode() {
$globalStore.isDark = !$globalStore.isDark;
toggleBodyByMode($globalStore.isDark);
if (browser) {
localStorage.setItem('mode', $globalStore.isDark ? 'dark' : '');
}
}
let coursesPath = $derived(
$globalStore.isOrgSite ? '/lms/mylearning' : isCoursePage ? `${$currentOrgPath}/courses` : $currentOrgPath
);
@@ -91,13 +78,14 @@
<BellIcon class="custom text-white" size={16} />
</li>
<li>
<IconButton size="small" onClick={toggleDarkMode}>
<!-- <IconButton size="small" onClick={toggleDarkMode}>
{#if $globalStore.isDark}
<SunIcon size={16} class="custom" />
{:else}
<MoonIcon class="custom text-white" size={16} />
{/if}
</IconButton>
</IconButton> -->
<ModeSwitcher />
</li>
<li></li>
@@ -4,6 +4,7 @@
import { isCoursePage } from '$lib/utils/functions/app';
import { t } from '$lib/utils/functions/translations';
import type { TCustomLinks } from './types';
import { ModeSwitcher } from '@cio/ui/base/dark-mode';
import Logo from './Logo.svelte';
import CustomLinks from './CustomLinks.svelte';
@@ -76,6 +77,8 @@
{:else if !isOrgSite && !$page.url.pathname?.includes('/404')}
<AuthButtons {disableSignup} {redirect} />
{/if}
<ModeSwitcher />
</ul>
{#if showLinks}
@@ -1,9 +1,10 @@
<script>
import { Popover } from 'carbon-components-svelte';
import Menu from './Menu.svelte';
import Modal from '$lib/components/Modal/index.svelte';
import { isMobile } from '$lib/utils/store/useMobile';
import { profileMenu } from '../store';
import { clickOutside } from '$lib/utils/functions/routes/dashboard';
</script>
{#if $isMobile}
@@ -16,15 +17,12 @@
>
<Menu />
</Modal>
{:else}
<Popover
bind:open={$profileMenu.open}
align="right-bottom"
caret={true}
on:click:outside={({ detail }) => {
$profileMenu.open = !!$profileMenu.ref?.contains(detail.target);
}}
{:else if $profileMenu.open}
<div
use:clickOutside
on:click:outside={() => ($profileMenu.open = false)}
class="absolute bottom-28 left-0 z-[300] w-full border bg-black p-2 dark:bg-white"
>
<Menu />
</Popover>
</div>
{/if}
@@ -76,10 +76,11 @@
if (error) throw error;
} catch (error) {
let message = error instanceof Error ? error.message : `${error}`;
if (message.includes('profile_username_key')) {
message = $t('snackbar.lms.error.username_exists');
}
snackbar.error(`${$t('snackbar.lms.error.update')}: ${message}`);
loading = false;
@@ -5,7 +5,7 @@
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
import UsersIcon from '@lucide/svelte/icons/users';
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
import SettingsIcon from '@lucide/svelte/icons/settings';
import Settings2 from '@lucide/svelte/icons/settings-2';
import MessageSquareMoreIcon from '@lucide/svelte/icons/message-square-more';
import BadgeHelpIcon from '@lucide/svelte/icons/badge-help';
import Dice6Icon from '@lucide/svelte/icons/dice-6';
@@ -98,24 +98,24 @@
{#if menuItem.show}
<a href="{$currentOrgPath}{menuItem.path}" class="text-black no-underline" onclick={toggleSidebar}>
<li
class="mb-1 flex items-center gap-2.5 px-2.5 py-2 {NavClasses.item} {menuItem.isActive
class="group mb-1 flex items-center gap-2.5 px-2.5 py-2 {NavClasses.item} {menuItem.isActive
? NavClasses.active
: 'dark:text-white'}"
>
{#if menuItem.path === ''}
<LayoutDashboardIcon size={16} />
<LayoutDashboardIcon size={16} class="group-hover:animate-gather" />
{:else if menuItem.path === '/courses'}
<LibraryBigIcon size={16} />
<LibraryBigIcon size={16} class="group-hover:animate-library-expand" />
{:else if menuItem.path === '/site'}
<LayoutTemplateIcon size={16} />
<LayoutTemplateIcon size={16} class="group-hover:animate-template-morph" />
{:else if menuItem.path === '/community'}
<MessageSquareMoreIcon size={16} />
<MessageSquareMoreIcon size={16} class="group-hover:animate-ripple" />
{:else if menuItem.path === '/quiz'}
<Dice6Icon size={16} />
<Dice6Icon size={16} class="group-hover:animate-roll" />
{:else if menuItem.path === '/audience'}
<UsersIcon size={16} />
<UsersIcon size={16} class="group-hover:animate-gather" />
{:else if menuItem.path === '/setup'}
<SettingsIcon size={16} />
<Settings2 size={16} class="group-hover:animate-setup-configure" />
{/if}
<p class="text-sm font-medium">{menuItem.label}</p>
</li>
@@ -52,3 +52,19 @@ export const formatDate = (_selectedDate: string | undefined): string => {
return selectedDate.format('ddd, MMM D');
};
export function clickOutside(node) {
const handleClick = (event) => {
if (node && !node.contains(event.target) && !event.defaultPrevented) {
node.dispatchEvent(new CustomEvent('click:outside', { detail: event.target }));
}
};
document.addEventListener('click', handleClick, true);
return {
destroy() {
document.removeEventListener('click', handleClick, true);
}
};
}
@@ -158,9 +158,7 @@ export const orgLandingpageValidation = (fields = {}) => {
export const onboardingValidation = (fields = {}, step) => {
const onboardingValidationSchema = {
stepOne: z.object({
fullname: z
.string()
.min(5, { message: `${t.get('validations.onboarding.step_one.full_name.min_char')}` }),
fullname: z.string().min(5, { message: `${t.get('validations.onboarding.step_one.full_name.min_char')}` }),
orgName: getOrgNameValidation(),
siteName: getSiteNameValidation()
}),
@@ -178,8 +176,7 @@ export const onboardingValidation = (fields = {}, step) => {
})
};
const schema =
step === 1 ? onboardingValidationSchema.stepOne : onboardingValidationSchema.stepTwo;
const schema = step === 1 ? onboardingValidationSchema.stepOne : onboardingValidationSchema.stepTwo;
const { error } = schema.safeParse(fields);
return processErrors(error);