feat: migrate course UI from supabase (#615)
* chore: fix storybook build * chore: implement theming * community list not showing properly * refactor: migrate Chip component to @cio/ui/custom/chip - Updated imports for the Chip component across multiple files to use the new location in the @cio/ui package. - Replaced the previous Chip implementation with a new version that utilizes a Badge for better styling and functionality. - Adjusted related components to ensure consistent usage of the updated Chip component. * fix: align course sidebar design to match organization design (#611) * fix: align course sidebar design to match organization design * feat: add hideSearch prop to AppHeader and update sidebar layout --------- Co-authored-by: Rotimi Best <[email protected]> * refactor: replace PrimaryButton with Button component across multiple files - Updated various components to use the new Button component from @cio/ui/base/button instead of the deprecated PrimaryButton. - Ensured consistent button usage throughout the application for improved styling and functionality. - Removed the PrimaryButton component and its associated constants from the codebase. * refactor: standardize button and input usage across components - Replaced instances of deprecated components such as TextField, TextArea, and IconButton with their updated counterparts (InputField, TextareaField, and updated IconButton). - Ensured consistent event handling by changing `onClick` to `onclick` for better alignment with Svelte's event binding conventions. - Removed unused components and streamlined imports to enhance code maintainability and readability. * chore: themeing, course card, use $features * refactor: centralize UI components and update modal implementations - Refactored various components to centralize UI elements, improving discoverability and maintainability. - Replaced deprecated modal components with the new Dialog component for consistent modal behavior across the application. - Removed unused components and streamlined imports to enhance code clarity and reduce complexity. * feat: add new documentation and refactor org and lms UI components - Introduced new documentation files for Auth Flow, Breadcrumbs Refactoring, Page and Route Creation, and Route Page Setup to enhance developer onboarding and reference. - Refactored various UI components, including the Course Sidebar and Lesson Modal, to improve layout and functionality. - Removed deprecated components and streamlined imports for better maintainability. - Updated translations across multiple languages to ensure consistency in user experience. * refactor: update UI components and improve styling consistency - Refactored various components to enhance styling consistency, including updates to button and text color classes. - Removed deprecated components and streamlined imports for better maintainability. - Replaced instances of PageBody with a more flexible Page component structure across multiple routes. - Updated translations and UI elements to align with the new design system. - Deleted unused components to reduce code complexity and improve clarity. * update output.css --------- Co-authored-by: Abdulmuizz Hamzat <[email protected]>
This commit is contained in:
co-authored by
Abdulmuizz Hamzat
parent
e6a3cf9332
commit
9fb6c7cbaa
+384
-993
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover" class="">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
<div style="display: contents" class="theme-container">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -71,7 +71,7 @@
|
||||
<div class="{className} relative z-[100]">
|
||||
<IconButton
|
||||
contained={true}
|
||||
color="text-primary-700 dark:bg-neutral-500 dark:text-white"
|
||||
color="ui:text-primary dark:bg-neutral-500 dark:text-white"
|
||||
size="large"
|
||||
onClick={() => (openPopover = !openPopover)}
|
||||
>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as ActivityCard } from './ActivityCard.svelte';
|
||||
export { default as HeroProfileCard } from './HeroProfileCard.svelte';
|
||||
export { default as LoadingPage } from './LoadingPage.svelte';
|
||||
@@ -1,14 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
className?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { className = '', children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="{className} flex h-[517px] w-full flex-col items-center justify-center rounded border border-gray-200 dark:border-neutral-600"
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import IconButton, { type Props as IconButtonProps } from '$lib/components/IconButton/index.svelte';
|
||||
|
||||
interface Props {
|
||||
onClick?: IconButtonProps['onClick'];
|
||||
contained?: boolean;
|
||||
isClosing?: boolean;
|
||||
color?: string;
|
||||
size?: IconButtonProps['size'];
|
||||
toolTipProps?: IconButtonProps['toolTipProps'];
|
||||
}
|
||||
|
||||
let {
|
||||
onClick = () => {},
|
||||
contained = false,
|
||||
isClosing = false,
|
||||
color = 'text-primary-700 bg-primary-200 dark:bg-neutral-500',
|
||||
size = 'small'
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<IconButton {onClick} {color} {contained} {size} disabled={isClosing}>
|
||||
{#if isClosing}
|
||||
<Spinner class="text-blue-700! size-5" />
|
||||
{:else}
|
||||
<XIcon size={size === 'large' ? 24 : 16} class="dark:text-black" />
|
||||
{/if}
|
||||
</IconButton>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as CloseButton } from './index.svelte';
|
||||
@@ -1,10 +0,0 @@
|
||||
<script lang="ts">
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
|
||||
let { onClick = () => {} } = $props();
|
||||
</script>
|
||||
|
||||
<IconButton {onClick}>
|
||||
<XIcon size={16} class="dark:text-white" />
|
||||
</IconButton>
|
||||
@@ -1,10 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SendHorizontalIcon from '@lucide/svelte/icons/send-horizontal';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
|
||||
let { onClick } = $props();
|
||||
</script>
|
||||
|
||||
<IconButton {onClick}>
|
||||
<SendHorizontalIcon size={16} class="filled" />
|
||||
</IconButton>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
name?: string;
|
||||
}
|
||||
|
||||
let { name = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<p class="w-fit rounded-md border border-blue-700 px-1.5 text-center text-xs font-semibold text-blue-700">
|
||||
{name}
|
||||
</p>
|
||||
@@ -1,24 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
className?: string;
|
||||
value?: string | number;
|
||||
size?: string;
|
||||
shape?: string;
|
||||
}
|
||||
|
||||
let { className = '', value = '', size = '', shape = 'rounded-md' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<p
|
||||
class="{className} {size === 'sm' ? 'px-1 py-1' : 'px-2 py-2'} org-avatar {className.includes('bg-')
|
||||
? ''
|
||||
: 'bg-gray-200 dark:text-black'} {shape} flex items-center justify-center text-center"
|
||||
>
|
||||
{value}
|
||||
</p>
|
||||
|
||||
<style>
|
||||
.org-avatar {
|
||||
width: fit-content;
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
value?: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
let { value = 0, className = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<p class={className ? className : 'bg-set mt-2'}>{value}</p>
|
||||
|
||||
<style>
|
||||
p {
|
||||
display: inline-block;
|
||||
min-width: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 18px;
|
||||
text-align: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 2em;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.bg-set {
|
||||
background-color: #d1d5da80;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +0,0 @@
|
||||
<script>
|
||||
import { PageNav } from '$lib/components/Page';
|
||||
</script>
|
||||
|
||||
<PageNav title="Attendance" />
|
||||
All attendance will be here with table
|
||||
@@ -7,18 +7,17 @@
|
||||
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { saveCertificateValidation } from '$lib/utils/functions/validator';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
import Plain from './templates/Plain.svelte';
|
||||
import Professional from './templates/Professional.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import { UpgradeBanner } from '$lib/features/ui';
|
||||
import { TextareaField } from '@cio/ui/custom/textarea-field';
|
||||
import { UpgradeBanner } from '$features/ui';
|
||||
import BlueBadgePattern from './templates/BlueBadgePattern.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import PurpleBadgePattern from './templates/PurpleBadgePattern.svelte';
|
||||
import BlueProfessionalBadge from './templates/BlueProfessionalBadge.svelte';
|
||||
import PurpleProfessionalBadge from './templates/PurpleProfessionalBadge.svelte';
|
||||
@@ -122,18 +121,15 @@
|
||||
{$t('course.navItem.certificates.and_upload')}
|
||||
</p>
|
||||
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.certificates.goto_settings')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
className="rounded-md mt-3"
|
||||
onClick={() => goto(`/org/${$currentOrg.siteName}/settings`)}
|
||||
/>
|
||||
<Button variant="outline" class="mt-3" onclick={() => goto(`/org/${$currentOrg.siteName}/settings`)}>
|
||||
{$t('course.navItem.certificates.goto_settings')}
|
||||
</Button>
|
||||
</div>
|
||||
<span class="my-4">
|
||||
<p class="mb-2 mt-4 text-xs font-normal dark:text-gray-100">
|
||||
{$t('course.navItem.certificates.description')}
|
||||
</p>
|
||||
<TextArea
|
||||
<TextareaField
|
||||
rows={6}
|
||||
placeholder={$t('course.navItem.certificates.placeholder')}
|
||||
bind:value={$course.description}
|
||||
@@ -173,18 +169,12 @@
|
||||
</section>
|
||||
</div>
|
||||
<div class="h-1/5">
|
||||
<PrimaryButton
|
||||
className="rounded-md flex gap-2 items-center"
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={saveCertificate}
|
||||
isLoading={isSaving}
|
||||
isDisabled={$isFreePlan}
|
||||
>
|
||||
<Button variant="secondary" onclick={saveCertificate} loading={isSaving} disabled={$isFreePlan}>
|
||||
{#if $isFreePlan}
|
||||
<ZapIcon size={16} class="filled" />
|
||||
{/if}
|
||||
{$t('course.navItem.certificates.save')}
|
||||
</PrimaryButton>
|
||||
</Button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
+19
-17
@@ -5,10 +5,10 @@
|
||||
import { issueCertificateModal, resetForm } from './store';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { TextareaField } from '@cio/ui/custom/textarea-field';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
let isAutomatic = $state(false);
|
||||
|
||||
@@ -17,14 +17,17 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={resetForm}
|
||||
<Dialog.Root
|
||||
bind:open={$issueCertificateModal.open}
|
||||
width="w-3/5"
|
||||
maxWidth=""
|
||||
modalHeading="Send Certificate"
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) resetForm();
|
||||
}}
|
||||
>
|
||||
<main>
|
||||
<Dialog.Content class="w-3/5">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Send Certificate</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<main>
|
||||
<div>
|
||||
<div class="mb-4 flex items-center space-x-2">
|
||||
<Switch id="auto-certificate" bind:checked={isAutomatic} />
|
||||
@@ -37,25 +40,23 @@
|
||||
<p class="my-4 text-xs font-normal text-gray-500">or send a personalised/ custom certificate below:</p>
|
||||
<form onsubmit={preventDefault(issueCertificate)}>
|
||||
<div class="flex w-full flex-col gap-2 md:flex-row">
|
||||
<TextField
|
||||
<InputField
|
||||
label="Email address of the student"
|
||||
className="w-full my-4"
|
||||
labelClassName="text-xs font-normal"
|
||||
inputClassName="text-sm placeholder:text-sm placeholder:font-medium "
|
||||
placeholder="email,comma seperated"
|
||||
bind:value={$issueCertificateModal.email}
|
||||
/>
|
||||
<TextField
|
||||
<InputField
|
||||
label="Schedule date"
|
||||
className="w-full my-4"
|
||||
labelClassName="text-xs font-normal"
|
||||
inputClassName="text-sm placeholder:text-sm placeholder:font-medium "
|
||||
placeholder="12/06/2023"
|
||||
bind:value={$issueCertificateModal.date}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
<TextareaField
|
||||
label="Add a personalized message"
|
||||
labelClassName="text-xs font-normal"
|
||||
bind:value={$issueCertificateModal.message}
|
||||
@@ -65,8 +66,9 @@
|
||||
/>
|
||||
|
||||
<div class="mt-5 flex w-full items-end justify-end">
|
||||
<PrimaryButton className="px-6 py-3 rounded-md text-sm font-medium" label="Issue certificate" type="submit" />
|
||||
<Button type="submit">Issue certificate</Button>
|
||||
</div>
|
||||
</form>
|
||||
</main>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-sm">{student.issued}</span>
|
||||
{#if student.peronalized}
|
||||
<Badge variant="secondary" class="bg-primary-100 text-primary-600">personalized</Badge>
|
||||
<Badge variant="secondary" class="bg-primary-100 ui:text-primary">personalized</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
</Table.Cell>
|
||||
|
||||
+10
-13
@@ -1,17 +1,14 @@
|
||||
<script>
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import Empty from '@cio/ui/custom/empty/empty.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import LockIcon from '@lucide/svelte/icons/lock';
|
||||
</script>
|
||||
|
||||
<Box>
|
||||
<div class="flex h-full w-max flex-col items-center justify-center gap-2">
|
||||
<img src="/images/student-certificate-preview.png" alt="Certificate" class="max-w-[218px]" />
|
||||
<p class="text-center text-xl font-normal">
|
||||
{$t('course.navItem.certificates.locked_certificate')}
|
||||
</p>
|
||||
<p class="text-center text-sm font-normal">
|
||||
{$t('course.navItem.certificates.instructor_unlock')} <br />
|
||||
{$t('course.navItem.certificates.contact_instructor')}
|
||||
</p>
|
||||
</div>
|
||||
</Box>
|
||||
<div class="w-full flex-1">
|
||||
<Empty
|
||||
title={$t('course.navItem.certificates.locked_certificate')}
|
||||
description={`${$t('course.navItem.certificates.instructor_unlock')} ${$t('course.navItem.certificates.contact_instructor')}`}
|
||||
icon={LockIcon}
|
||||
variant="page"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+10
-24
@@ -5,13 +5,12 @@
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import Empty from '@cio/ui/custom/empty/empty.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { fetchProfileCourseProgress } from '$lib/utils/services/courses';
|
||||
import type { ProfileCourseProgress } from '$lib/utils/types';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { classroomio } from '$lib/utils/services/api';
|
||||
|
||||
let isLoading = $state(false);
|
||||
@@ -19,7 +18,7 @@
|
||||
let progress: ProfileCourseProgress | undefined;
|
||||
|
||||
const downLoadCertificate = async () => {
|
||||
if (!isCourseComplete) return;
|
||||
if (!isCourseComplete || !$course.id) return;
|
||||
|
||||
isLoading = true;
|
||||
try {
|
||||
@@ -85,24 +84,11 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<Box>
|
||||
<div class="flex h-full w-max flex-col items-center justify-center gap-5">
|
||||
<img src="/images/student-certificate-preview.png" alt="Certificate" class="max-w-[218px]" />
|
||||
<p class="text-center text-xl font-normal">
|
||||
{$t(title)}
|
||||
</p>
|
||||
<p class="max-w-md text-center text-sm font-normal">
|
||||
{$t(subtitle)}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
className="flex items-center gap-2"
|
||||
onClick={downLoadCertificate}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
isDisabled={!isCourseComplete}
|
||||
{isLoading}
|
||||
>
|
||||
<div class="flex-1">
|
||||
<Empty title={$t(title)} description={$t(subtitle)} icon={DownloadIcon} variant="page">
|
||||
<Button onclick={downLoadCertificate} disabled={!isCourseComplete} loading={isLoading}>
|
||||
<DownloadIcon size={16} />
|
||||
{$t('course.navItem.certificates.download_certificate')}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</Box>
|
||||
</Button>
|
||||
</Empty>
|
||||
</div>
|
||||
|
||||
+26
-21
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { course } from '../../store';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
@@ -34,14 +33,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
<Dialog.Root
|
||||
bind:open
|
||||
width="w-[80%] md:w-[65%]"
|
||||
maxWidth="max-w-xl"
|
||||
modalHeading={$t(`course.navItem.lessons.section_prompt.header`)}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) handleClose();
|
||||
}}
|
||||
>
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<Dialog.Content class="w-[80%] md:w-[65%] max-w-xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t(`course.navItem.lessons.section_prompt.header`)}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<div class="mb-8">
|
||||
<h3 class="text-center text-2xl">
|
||||
{$t('course.navItem.lessons.section_prompt.title')}
|
||||
@@ -52,16 +54,19 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('course.navItem.lessons.section_prompt.cancel')}
|
||||
onClick={handleClose}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.section_prompt.activate')}
|
||||
onClick={activate}
|
||||
isLoading={isActivating}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={handleClose}
|
||||
>
|
||||
{$t('course.navItem.lessons.section_prompt.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onclick={activate}
|
||||
loading={isActivating}
|
||||
>
|
||||
{$t('course.navItem.lessons.section_prompt.activate')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
+25
-22
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
@@ -17,31 +16,35 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={() => (openDeleteModal = false)}
|
||||
<Dialog.Root
|
||||
bind:open={openDeleteModal}
|
||||
width="w-96"
|
||||
containerClass="px-6 pt-2 pb-6"
|
||||
modalHeading={$t('delete_modal.label')}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) openDeleteModal = false;
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<Dialog.Content class="w-96 px-6 pt-2 pb-6">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('delete_modal.label')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div>
|
||||
<h1 class="text-lg dark:text-white">
|
||||
{$t('delete_modal.content')}?
|
||||
</h1>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('delete_modal.no')}
|
||||
onClick={() => (openDeleteModal = false)}
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('delete_modal.yes')}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => (openDeleteModal = false)}
|
||||
>
|
||||
{$t('delete_modal.no')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={handleDelete}
|
||||
>
|
||||
{$t('delete_modal.yes')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
+21
-22
@@ -1,44 +1,43 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { deleteConfirmation } from '../store/exercise';
|
||||
|
||||
let { onDelete = () => {}, onCancel = () => {} } = $props();
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={() => ($deleteConfirmation.open = false)}
|
||||
<Dialog.Root
|
||||
bind:open={$deleteConfirmation.open}
|
||||
width="w-96"
|
||||
modalHeading={$t('course.navItem.lessons.exercises.all_exercises.delete_confirmation.title')}
|
||||
>
|
||||
<div>
|
||||
<Dialog.Content class="w-96">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('course.navItem.lessons.exercises.all_exercises.delete_confirmation.title')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div>
|
||||
<p class="mt-0 text-base dark:text-white">
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.delete_confirmation.sure')}
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('course.navItem.lessons.exercises.all_exercises.delete_confirmation.no')}
|
||||
onClick={() => {
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
$deleteConfirmation.open = false;
|
||||
onCancel();
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.CONTAINED}
|
||||
label={$t('course.navItem.lessons.exercises.all_exercises.delete_confirmation.yes')}
|
||||
onClick={() => {
|
||||
>
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.delete_confirmation.no')}
|
||||
</Button>
|
||||
<Button
|
||||
onclick={() => {
|
||||
onDelete();
|
||||
$deleteConfirmation.open = false;
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.delete_confirmation.yes')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
+49
-62
@@ -16,22 +16,20 @@
|
||||
} from '../store/exercise';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import { deleteExercise } from '$lib/utils/services/courses';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { QUESTION_TYPE, QUESTION_TYPES } from '$lib/components/Question/constants';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
import { lesson } from '../store/lessons';
|
||||
import OrderModal from './OrderModal.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import Checkbox from '$lib/components/Form/Checkbox.svelte';
|
||||
import RadioItem from '$lib/components/Form/RadioItem.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import { TextareaField } from '@cio/ui/custom/textarea-field';
|
||||
import { CheckboxField } from '@cio/ui/custom/checkbox-field';
|
||||
import { RadioItem } from '@cio/ui/custom/radio-item';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import DeleteConfirmationModal from './DeleteConfirmation.svelte';
|
||||
import ErrorMessage from '$lib/components/ErrorMessage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import { CircleCheckIcon } from '$features/ui/icons';
|
||||
import QuestionContainer from '$lib/components/QuestionContainer/index.svelte';
|
||||
|
||||
const initialQuestionsLength = $questionnaire.questions.length;
|
||||
@@ -90,49 +88,43 @@
|
||||
|
||||
<OrderModal />
|
||||
|
||||
<Modal
|
||||
onClose={() => (shouldDeleteExercise = false)}
|
||||
<Dialog.Root
|
||||
bind:open={shouldDeleteExercise}
|
||||
width="w-2/4"
|
||||
modalHeading={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.delete_modal')}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) shouldDeleteExercise = false;
|
||||
}}
|
||||
>
|
||||
<form onsubmit={preventDefault()}>
|
||||
<h1 class="text-xl dark:text-white">
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.edit_mode.sure')}
|
||||
</h1>
|
||||
<Dialog.Content class="w-2/4">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('course.navItem.lessons.exercises.all_exercises.edit_mode.delete_modal')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<form onsubmit={preventDefault()}>
|
||||
<h1 class="text-xl dark:text-white">
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.edit_mode.sure')}
|
||||
</h1>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.no')}
|
||||
type="submit"
|
||||
onClick={() => (shouldDeleteExercise = false)}
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.CONTAINED}
|
||||
label={isDeleting
|
||||
? $t('course.navItem.lessons.exercises.all_exercises.edit_mode.deleting')
|
||||
: $t('course.navItem.lessons.exercises.all_exercises.edit_mode.yes')}
|
||||
isDisabled={isDeleting}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<Button variant="outline" type="submit" onclick={() => (shouldDeleteExercise = false)}>
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.edit_mode.no')}
|
||||
</Button>
|
||||
<Button disabled={isDeleting} onclick={handleDelete} loading={isDeleting}>
|
||||
{isDeleting
|
||||
? $t('course.navItem.lessons.exercises.all_exercises.edit_mode.deleting')
|
||||
: $t('course.navItem.lessons.exercises.all_exercises.edit_mode.yes')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
{#snippet optionActions(question, option)}
|
||||
<div data-name="option-action" class="ml-2 flex items-center gap-2">
|
||||
<IconButton value={`${option.id}`} onClick={handleRemoveOption(question.id, option.id)} size="small" bordered>
|
||||
<IconButton onclick={handleRemoveOption(question.id, option.id)}>
|
||||
<TrashIcon size={18} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
value={`${option.id}`}
|
||||
onClick={handleAnswerSelect(question.id, option.id)}
|
||||
buttonClassName={option.is_correct ? 'success' : ''}
|
||||
size="small"
|
||||
bordered
|
||||
onclick={handleAnswerSelect(question.id, option.id)}
|
||||
class={option.is_correct ? 'text-green-500! fill-green-500!' : ''}
|
||||
>
|
||||
<CircleCheckIcon size={18} filled={option.is_correct} />
|
||||
</IconButton>
|
||||
@@ -142,7 +134,7 @@
|
||||
<div class="mb-20 w-full">
|
||||
{#if Object.values(errors).length}
|
||||
<div class="mb-4 flex w-full justify-center">
|
||||
<ErrorMessage message={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.error')} />
|
||||
<p class="text-sm text-red-500">{$t('course.navItem.lessons.exercises.all_exercises.edit_mode.error')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="questions px-6 pt-6">
|
||||
@@ -160,11 +152,11 @@
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="mr-5 w-3/5">
|
||||
<TextField
|
||||
<InputField
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.question')}
|
||||
bind:value={question.title}
|
||||
isRequired={true}
|
||||
onChange={() => {
|
||||
onchange={() => {
|
||||
question.is_dirty = true;
|
||||
}}
|
||||
/>
|
||||
@@ -194,12 +186,12 @@
|
||||
|
||||
{#if typeof question.code === 'string'}
|
||||
<div class="my-3 flex w-3/5 items-center justify-between">
|
||||
<TextArea
|
||||
<TextareaField
|
||||
bind:value={question.code}
|
||||
rows={2}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.edit_mode.write')}
|
||||
/>
|
||||
<IconButton value="write-code" onClick={() => handleCode(question.id, false)}>
|
||||
<IconButton onclick={() => handleCode(question.id, false)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -213,45 +205,40 @@
|
||||
isEditable={true}
|
||||
name={question.title || 'radio-name'}
|
||||
bind:label={option.label}
|
||||
onChange={addDynamicValue(question.id, option.id)}
|
||||
onchange={addDynamicValue(question.id, option.id)}
|
||||
>
|
||||
{@render optionActions(question, option)}
|
||||
</RadioItem>
|
||||
{:else if QUESTION_TYPE.CHECKBOX === question.question_type.id}
|
||||
<Checkbox
|
||||
<CheckboxField
|
||||
isEditable={true}
|
||||
name={question?.name || 'checkbox-name'}
|
||||
bind:label={option.label}
|
||||
onChange={addDynamicValue(question.id, option.id)}
|
||||
onchange={addDynamicValue(question.id, option.id)}
|
||||
>
|
||||
{@render optionActions(question, option)}
|
||||
</Checkbox>
|
||||
</CheckboxField>
|
||||
{/if}
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#if QUESTION_TYPE.TEXTAREA === question.question_type.id}
|
||||
<TextArea bind:value={question.value} disabled={true} />
|
||||
<TextareaField bind:value={question.value} disabled={true} />
|
||||
{/if}
|
||||
|
||||
{#if getQuestionErrorMsg(errors, question, 'option')}
|
||||
<ErrorMessage message={getQuestionErrorMsg(errors, question, 'option')} />
|
||||
<p class="text-sm text-red-500">{getQuestionErrorMsg(errors, question, 'option')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if QUESTION_TYPE.TEXTAREA !== question.question_type.id}
|
||||
<div class="mt-3 flex items-center">
|
||||
<PrimaryButton
|
||||
disablePadding={true}
|
||||
className="p-2"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleAddOption(question.id)}
|
||||
>
|
||||
<Button variant="outline" onclick={handleAddOption(question.id)}>
|
||||
<CirclePlusIcon size={16} />
|
||||
<p class="ml-2 dark:text-white">
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.edit_mode.option')}
|
||||
</p>
|
||||
</PrimaryButton>
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</QuestionContainer>
|
||||
|
||||
+31
-38
@@ -7,14 +7,13 @@
|
||||
|
||||
import { STATUS } from './constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import type { SubmissionIdData } from '$lib/utils/types/submission';
|
||||
|
||||
import Preview from './Preview.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { TextareaField } from '@cio/ui/custom/textarea-field';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
// import { useCompletion } from 'ai/svelte';
|
||||
// import { QUESTION_TYPE } from '$lib/components/Question/constants';
|
||||
@@ -231,16 +230,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
modalHeading={data.title}
|
||||
<Dialog.Root
|
||||
bind:open
|
||||
{onClose}
|
||||
width="w-4/5 h-[90%]"
|
||||
containerClass="flex items-start !max-h-full h-[85%] py-0 px-4"
|
||||
headerClass="py-2"
|
||||
labelClass="text-base font-semibold"
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) onClose();
|
||||
}}
|
||||
>
|
||||
<div class="mt-2 h-full w-full">
|
||||
<Dialog.Content class="w-4/5 h-[90%] flex items-start !max-h-full h-[85%] py-0 px-4">
|
||||
<Dialog.Header class="py-2">
|
||||
<Dialog.Title class="text-base font-semibold">{data.title}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="mt-2 h-full w-full">
|
||||
{#if openDeletePrompt}
|
||||
<div class="mx-auto w-96 rounded-md border border-gray-300 p-3">
|
||||
<h1 class="text-lg dark:text-white">
|
||||
@@ -248,20 +248,12 @@
|
||||
</h1>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('delete_modal.no')}
|
||||
onClick={() => (openDeletePrompt = false)}
|
||||
isDisabled={isDeleting}
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('delete_modal.yes')}
|
||||
onClick={handleDeleteSubmission}
|
||||
isLoading={isDeleting}
|
||||
/>
|
||||
<Button variant="outline" onclick={() => (openDeletePrompt = false)} disabled={isDeleting}>
|
||||
{$t('delete_modal.no')}
|
||||
</Button>
|
||||
<Button variant="outline" onclick={handleDeleteSubmission} loading={isDeleting}>
|
||||
{$t('delete_modal.yes')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -383,7 +375,7 @@
|
||||
<p class="font-semibold text-gray-500 dark:text-white">
|
||||
{$t('course.navItem.submissions.grading_modal.add_comment')}:
|
||||
</p>
|
||||
<TextArea
|
||||
<TextareaField
|
||||
bgColor="bg-gray-100 dark:bg-neutral-700"
|
||||
className="font-semibold"
|
||||
placeholder={$t('course.navItem.submissions.grading_modal.add_comment_placeholder')}
|
||||
@@ -392,22 +384,22 @@
|
||||
</div>
|
||||
|
||||
<div class="flex w-full flex-col space-y-3 px-3 py-2">
|
||||
<PrimaryButton onClick={gradeWithAI} variant={VARIANTS.OUTLINED} className="space-x-3 py-3 px-8 w-full ">
|
||||
<Button variant="outline" onclick={gradeWithAI} class="w-full">
|
||||
<img src="/ai.svg" alt="ai" />
|
||||
<p class="text-sm font-semibold">
|
||||
{$t('course.navItem.submissions.grading_modal.grade_with_ai')}
|
||||
</p>
|
||||
</PrimaryButton>
|
||||
<PrimaryButton
|
||||
onClick={() => {
|
||||
</Button>
|
||||
<Button
|
||||
onclick={() => {
|
||||
handleSave(data);
|
||||
// onClose();
|
||||
}}
|
||||
isLoading={isSaving}
|
||||
label={$t('course.navItem.submissions.grading_modal.submit_grades')}
|
||||
variant={VARIANTS.CONTAINED}
|
||||
className="py-3 px-8 w-full"
|
||||
/>
|
||||
loading={isSaving}
|
||||
class="w-full"
|
||||
>
|
||||
{$t('course.navItem.submissions.grading_modal.submit_grades')}
|
||||
</Button>
|
||||
</div>
|
||||
<!-- <div class="flex items-center text-sm p-3">
|
||||
<p class="dark:text-white w-1/2">Teacher</p>
|
||||
@@ -415,7 +407,8 @@
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
|
||||
+13
-8
@@ -6,7 +6,7 @@
|
||||
import TablePropertiesIcon from '@lucide/svelte/icons/table-properties';
|
||||
|
||||
import { flip } from 'svelte/animate';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
|
||||
import { questionnaireOrder, questionnaire } from '../store/exercise';
|
||||
import { filterOutDeleted } from './functions';
|
||||
@@ -60,13 +60,17 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
<Dialog.Root
|
||||
bind:open={$questionnaireOrder.open}
|
||||
width="w-96"
|
||||
modalHeading={$t('course.navItem.lessons.exercises.all_exercises.order_questions')}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) handleClose();
|
||||
}}
|
||||
>
|
||||
<section
|
||||
<Dialog.Content class="w-96">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('course.navItem.lessons.exercises.all_exercises.order_questions')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<section
|
||||
use:dndzone={{
|
||||
items,
|
||||
flipDurationMs,
|
||||
@@ -82,7 +86,7 @@
|
||||
{#each items as item (item.id)}
|
||||
<div
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
class="border-primary-600 flex items-center rounded-md border p-4"
|
||||
class="ui:border-primary flex items-center rounded-md border p-4"
|
||||
>
|
||||
{#if item.type === 1}
|
||||
<SquareCheckIcon size={16} />
|
||||
@@ -95,7 +99,8 @@
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<style>
|
||||
section {
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@
|
||||
{#each tabs as tab}
|
||||
<button
|
||||
class="relative {currentTab === tab.value
|
||||
? 'text-primary-700'
|
||||
? 'ui:text-primary'
|
||||
: 'dark:bg-gray-500 dark:text-white'} mr-4 w-fit px-2 py-3 text-center font-semibold focus:outline-none dark:bg-transparent"
|
||||
onclick={onChange(tab.value)}
|
||||
>
|
||||
|
||||
+11
-12
@@ -2,9 +2,8 @@
|
||||
import { questionnaire } from '../store/exercise';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import DateTime from '$lib/components/Form/DateTime.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { TextEditor } from '$features/ui';
|
||||
import QuestionContainer from '$lib/components/QuestionContainer/index.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -13,9 +12,9 @@
|
||||
|
||||
let { preview }: Props = $props();
|
||||
|
||||
function getTotalPossibleGrade(questions: { points: string }[]) {
|
||||
function getTotalPossibleGrade(questions: { points: number | string }[]) {
|
||||
return questions.reduce((acc, question) => {
|
||||
acc += parseFloat(question.points, 10);
|
||||
acc += typeof question.points === 'string' ? parseFloat(question.points) : question.points;
|
||||
return acc;
|
||||
}, 0);
|
||||
}
|
||||
@@ -24,18 +23,18 @@
|
||||
<div class="mb-5 {!preview ? 'px-6' : 'px-2'}">
|
||||
<QuestionContainer isTitle={true}>
|
||||
{#if !preview}
|
||||
<TextField
|
||||
<InputField
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.description.title')}
|
||||
bind:value={$questionnaire.title}
|
||||
className="mb-2"
|
||||
onChange={() => ($questionnaire.is_title_dirty = true)}
|
||||
onchange={() => ($questionnaire.is_title_dirty = true)}
|
||||
/>
|
||||
<DateTime
|
||||
<InputField
|
||||
label={$t('course.navItem.lessons.exercises.all_exercises.view_mode.due')}
|
||||
className="w-50"
|
||||
value={$questionnaire.due_by}
|
||||
onInput={(e) => {
|
||||
$questionnaire.due_by = e.target.value;
|
||||
type="datetime-local"
|
||||
bind:value={$questionnaire.due_by}
|
||||
onchange={() => {
|
||||
$questionnaire.is_due_by_dirty = true;
|
||||
}}
|
||||
/>
|
||||
@@ -80,7 +79,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<article class="preview prose prose-sm mt-3 p-2 sm:prose">
|
||||
<article class="preview prose prose-sm sm:prose mt-3 p-2">
|
||||
{@html $questionnaire.description || $t('course.navItem.lessons.exercises.all_exercises.description.no')}
|
||||
</article>
|
||||
{/if}
|
||||
|
||||
+19
-15
@@ -8,10 +8,11 @@
|
||||
import RadioQuestion from '$lib/components/Question/RadioQuestion/index.svelte';
|
||||
import CheckboxQuestion from '$lib/components/Question/CheckboxQuestion/index.svelte';
|
||||
import TextareaQuestion from '$lib/components/Question/TextareaQuestion/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import Progress from '$lib/components/Progress/index.svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { RoleBasedSecurity } from '$features/ui';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import FileQuestionIcon from '@lucide/svelte/icons/file-question';
|
||||
import { Progress } from '@cio/ui/base/progress';
|
||||
import { removeDuplicate } from '$lib/utils/functions/removeDuplicate';
|
||||
import { QUESTION_TYPE } from '$lib/components/Question/constants';
|
||||
import { STATUS } from './constants';
|
||||
@@ -64,7 +65,7 @@
|
||||
const submissionLink = `${baseUrl}/submissions`;
|
||||
const content = `
|
||||
<p>Hello ${teacherFullname},</p>
|
||||
<p>A student ${student.profile.fullname} just submitted an exercise <a href=${exerciseLink}>${$questionnaire.title}</a>
|
||||
<p>A student ${student.profile.fullname} just submitted an exercise <a href=${exerciseLink}>${$questionnaire.title}</a>
|
||||
<p>You can get started grading by clicking "Open Submissions"</p>
|
||||
<div>
|
||||
<a class="button" href=${submissionLink}>Open Submissions</a>
|
||||
@@ -259,15 +260,18 @@
|
||||
<Preview questions={filterOutDeleted($questionnaire.questions)} questionnaireMetaData={$questionnaireMetaData} />
|
||||
</RoleBasedSecurity>
|
||||
{:else if !$questionnaire.questions.length}
|
||||
<Box>
|
||||
<img src="/images/empty-exercise-icon.svg" alt="Exercise svg" class="my-2.5" />
|
||||
<h2 class="my-1.5 text-xl">No question added for this exercise</h2>
|
||||
<p class="px-44 text-center text-sm">
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
Click the <span class="text-primary-700">Edit</span> button to add.
|
||||
</RoleBasedSecurity>
|
||||
</p>
|
||||
</Box>
|
||||
<Empty
|
||||
title="No question added for this exercise"
|
||||
description="Click the Edit button to add."
|
||||
icon={FileQuestionIcon}
|
||||
variant="page"
|
||||
>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<p class="ui:text-primary text-center text-sm">
|
||||
Click the <span class="font-semibold">Edit</span> button to add.
|
||||
</p>
|
||||
</RoleBasedSecurity>
|
||||
</Empty>
|
||||
{:else if $questionnaireMetaData.currentQuestionIndex === 0}
|
||||
<RoleBasedSecurity allowedRoles={[3]}>
|
||||
<div>
|
||||
@@ -296,7 +300,7 @@
|
||||
{@html $questionnaire.description || 'No desription'}
|
||||
</article>
|
||||
|
||||
<PrimaryButton onClick={handleStart} label="Start" className="my-5 float-right" type="button" />
|
||||
<Button onclick={handleStart} type="button" class="float-right my-5">Start</Button>
|
||||
</div>
|
||||
</RoleBasedSecurity>
|
||||
{:else if $questionnaireMetaData.isFinished}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { QUESTION_TYPE } from '$lib/components/Question/constants';
|
||||
import { toggleConfetti } from '$lib/components/Confetti/store';
|
||||
import { toggleConfetti } from '$features/ui/confetti/store';
|
||||
|
||||
export const isAnswerCorrect = (options, answer) => {
|
||||
return options.some((option) => option.is_correct && option.value === answer);
|
||||
|
||||
@@ -18,18 +18,14 @@
|
||||
} from '../store/exercise';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { upsertExercise } from '$lib/utils/services/courses';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
import EditMode from './EditMode.svelte';
|
||||
import ViewMode from './ViewMode.svelte';
|
||||
import { PageBody } from '$lib/components/Page';
|
||||
import Analytics from './Submissions/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import UpdateDescription from './UpdateDescription.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { RoleBasedSecurity } from '$features/ui';
|
||||
|
||||
interface Props {
|
||||
exerciseId?: string;
|
||||
@@ -106,7 +102,7 @@
|
||||
// });
|
||||
</script>
|
||||
|
||||
<PageBody isPageNavHidden={$globalStore.isStudent} padding="px-4 overflow-x-hidden">
|
||||
<div class="overflow-x-hidden px-4">
|
||||
<div class="sticky top-0 z-10 mb-3 bg-gray-100 p-2 dark:bg-neutral-800">
|
||||
<Breadcrumb.Root>
|
||||
<Breadcrumb.List>
|
||||
@@ -139,31 +135,20 @@
|
||||
{#if selectedTab === 'questions'}
|
||||
<div class="right-0 flex w-full items-center justify-end">
|
||||
<div class="flex items-center">
|
||||
<PrimaryButton
|
||||
className="mr-2"
|
||||
variant={VARIANTS.CONTAINED}
|
||||
label={$t('course.navItem.lessons.exercises.all_exercises.save')}
|
||||
onClick={handleSave}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
<Button class="mr-2" onclick={handleSave} loading={isSaving}>
|
||||
{$t('course.navItem.lessons.exercises.all_exercises.save')}
|
||||
</Button>
|
||||
<IconButton
|
||||
onClick={() => (preview = !preview)}
|
||||
contained={preview}
|
||||
toolTipProps={{
|
||||
title: $t('course.navItem.lessons.exercises.all_exercises.preview'),
|
||||
direction: 'bottom',
|
||||
hotkeys: []
|
||||
}}
|
||||
onclick={() => (preview = !preview)}
|
||||
tooltip={$t('course.navItem.lessons.exercises.all_exercises.preview')}
|
||||
tooltipSide="bottom"
|
||||
>
|
||||
<EyeIcon size={20} class={preview ? 'filled' : ''} />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleAddQuestion}
|
||||
toolTipProps={{
|
||||
title: $t('course.navItem.lessons.exercises.all_exercises.add_question'),
|
||||
direction: 'bottom',
|
||||
hotkeys: []
|
||||
}}
|
||||
onclick={handleAddQuestion}
|
||||
tooltip={$t('course.navItem.lessons.exercises.all_exercises.add_question')}
|
||||
tooltipSide="bottom"
|
||||
>
|
||||
<CirclePlusIcon size={20} />
|
||||
</IconButton>
|
||||
@@ -205,4 +190,4 @@
|
||||
{:else if selectedTab === 'submissions'}
|
||||
<Analytics bind:exerciseId />
|
||||
{/if}
|
||||
</PageBody>
|
||||
</div>
|
||||
|
||||
+60
-68
@@ -2,16 +2,14 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { Badge } from '@cio/ui/base/badge';
|
||||
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import Confetti from '$lib/components/Confetti/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { ComingSoon } from '$lib/features/ui';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Confetti, ComingSoon } from '$features/ui';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { CircleCheckIcon } from '$features/ui/icons';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { ExerciseTemplate } from '$lib/utils/types';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { type GeneratedTemplates, getAllTemplates, TAGS } from '$lib/mocks';
|
||||
|
||||
interface Props {
|
||||
@@ -126,17 +124,20 @@
|
||||
// const note = $derived(browser ? getTextFromHTML(content) : '');
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={handleCancelAddExercise}
|
||||
<Dialog.Root
|
||||
bind:open
|
||||
modalHeading={$t('course.navItem.lessons.exercises.new_exercise_modal.heading')}
|
||||
maxWidth="max-w-2xl"
|
||||
width="w-4/5"
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) handleCancelAddExercise();
|
||||
}}
|
||||
>
|
||||
{#if !isLoading && isAIStarted}
|
||||
<Confetti />
|
||||
{/if}
|
||||
{#if step === 0}
|
||||
<Dialog.Content class="max-w-2xl w-4/5">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('course.navItem.lessons.exercises.new_exercise_modal.heading')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
{#if !isLoading && isAIStarted}
|
||||
<Confetti />
|
||||
{/if}
|
||||
{#if step === 0}
|
||||
<div>
|
||||
<h2 class="my-5 text-2xl font-medium">
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.how')}?
|
||||
@@ -171,11 +172,9 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex flex-row-reverse items-center">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.next')}
|
||||
onClick={handleNext}
|
||||
/>
|
||||
<Button onclick={handleNext}>
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.next')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else if step === 1}
|
||||
@@ -185,7 +184,7 @@
|
||||
<h2 class="my-5 text-2xl font-medium">
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.title')}
|
||||
</h2>
|
||||
<TextField
|
||||
<InputField
|
||||
bind:value={title}
|
||||
autoFocus={true}
|
||||
placeholder={$t('course.navItem.lessons.exercises.new_exercise_modal.title_placeholder')}
|
||||
@@ -193,17 +192,12 @@
|
||||
/>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleBack}
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
|
||||
onClick={handleAddExercise}
|
||||
/>
|
||||
<Button variant="outline" onclick={handleBack}>
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
|
||||
</Button>
|
||||
<Button onclick={handleAddExercise}>
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -258,18 +252,13 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleBack}
|
||||
/>
|
||||
<PrimaryButton
|
||||
isDisabled={!selectedTemplateId || !allTemplates}
|
||||
className="px-6 py-3"
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
|
||||
isLoading={isTemplateFinishedLoading}
|
||||
onClick={async () => {
|
||||
<Button variant="outline" onclick={handleBack}>
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!selectedTemplateId || !allTemplates}
|
||||
loading={isTemplateFinishedLoading}
|
||||
onclick={async () => {
|
||||
isTemplateFinishedLoading = true;
|
||||
const template = allTemplates?.[selectedTag]?.find((t) => t.id === selectedTemplateId);
|
||||
|
||||
@@ -281,7 +270,9 @@
|
||||
|
||||
isTemplateFinishedLoading = true;
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -294,7 +285,7 @@
|
||||
<p class="mb-4 text-sm">
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.choose_questions')}
|
||||
</p>
|
||||
<TextField
|
||||
<InputField
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.how_many_questions')}
|
||||
type="number"
|
||||
bind:value={questionNumber}
|
||||
@@ -302,7 +293,7 @@
|
||||
className="mb-2"
|
||||
isRequired
|
||||
/>
|
||||
<TextField
|
||||
<InputField
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.how_many_options')}
|
||||
type="number"
|
||||
bind:value={optionNumber}
|
||||
@@ -313,14 +304,14 @@
|
||||
<h3>{$t('course.navItem.lessons.exercises.new_exercise_modal.add_note')}</h3>
|
||||
{/if}
|
||||
<div class="mt-5 flex flex-row-reverse items-center">
|
||||
<PrimaryButton
|
||||
onClick={callAI}
|
||||
{isLoading}
|
||||
isDisabled={isLoading || !note}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
<Button
|
||||
onclick={callAI}
|
||||
loading={isLoading}
|
||||
disabled={isLoading || !note}
|
||||
variant="outline"
|
||||
>
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.generate')}
|
||||
</PrimaryButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
@@ -340,20 +331,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
|
||||
variant={VARIANTS.TEXT}
|
||||
onClick={handleBack}
|
||||
/>
|
||||
<PrimaryButton
|
||||
isDisabled={isLoading || !note}
|
||||
className="px-6 py-3"
|
||||
label={$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
|
||||
onClick={handleAddExercise}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onclick={handleBack}
|
||||
>
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.back')}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isLoading || !note}
|
||||
onclick={handleAddExercise}
|
||||
>
|
||||
{$t('course.navItem.lessons.exercises.new_exercise_modal.finish')}
|
||||
</Button>
|
||||
</div>
|
||||
</div> -->
|
||||
{/if}
|
||||
{/if}
|
||||
</Modal>
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -13,13 +13,11 @@
|
||||
import { isQuestionnaireFetching, questionnaire } from '../store/exercise';
|
||||
import { createExercise, createExerciseFromTemplate } from '$lib/utils/services/courses';
|
||||
|
||||
import { PageBody } from '$lib/components/Page';
|
||||
import Exercise from '../Exercise/index.svelte';
|
||||
import Backdrop from '$lib/components/Backdrop/index.svelte';
|
||||
import { Backdrop, RoleBasedSecurity } from '$features/ui';
|
||||
import * as Empty from '@cio/ui/base/empty';
|
||||
import { QUESTION_TYPES } from '$lib/components/Question/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import NewExerciseModal from '$lib/components/Course/components/Lesson/Exercises/NewExerciseModal.svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -175,7 +173,7 @@
|
||||
bind:title={newExercise.title}
|
||||
/>
|
||||
|
||||
<PageBody isPageNavHidden={$globalStore.isStudent}>
|
||||
<div>
|
||||
{#snippet header()}
|
||||
<slot:fragment>
|
||||
<Breadcrumb.Root class="my-2">
|
||||
@@ -188,11 +186,9 @@
|
||||
</Breadcrumb.List>
|
||||
</Breadcrumb.Root>
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<PrimaryButton
|
||||
className="mr-2 my-2"
|
||||
label={$t('course.navItem.lessons.exercises.add_button')}
|
||||
onClick={() => (open = !open)}
|
||||
/>
|
||||
<Button class="my-2 mr-2" onclick={() => (open = !open)}>
|
||||
{$t('course.navItem.lessons.exercises.add_button')}
|
||||
</Button>
|
||||
</RoleBasedSecurity>
|
||||
</slot:fragment>
|
||||
{/snippet}
|
||||
@@ -224,5 +220,5 @@
|
||||
</Empty.Root>
|
||||
{/each}
|
||||
</div>
|
||||
</PageBody>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
import ListChecksIcon from '@lucide/svelte/icons/list-checks';
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { Chip } from '@cio/ui/custom/chip';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { RoleBasedSecurity } from '$features/ui';
|
||||
import * as Empty from '@cio/ui/base/empty';
|
||||
import { lessons, handleSaveLesson } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getLessonOrder(id: string) {
|
||||
function getLessonOrder(id: string): number | string {
|
||||
const index = $lessons.findIndex((lesson) => lesson.id === id);
|
||||
|
||||
if (index < 9) {
|
||||
@@ -98,19 +98,14 @@
|
||||
>
|
||||
<!-- Number Chip -->
|
||||
<div class="mr-5">
|
||||
<TextChip
|
||||
value={getLessonOrder(lesson.id)}
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
/>
|
||||
<Chip value={getLessonOrder(lesson.id)} />
|
||||
</div>
|
||||
|
||||
<!-- Lesson Content -->
|
||||
<div class="w-4/5">
|
||||
<!-- Lesson Title -->
|
||||
{#if lessonEditing === lesson.id}
|
||||
<TextField bind:value={lesson.title} autoFocus={true} className="max-w-lg" errorMessage={errors?.title} />
|
||||
<InputField bind:value={lesson.title} autoFocus={true} className="max-w-lg" errorMessage={errors?.title} />
|
||||
{:else}
|
||||
<h3 class="m-0 flex items-center text-lg dark:text-white">
|
||||
<a
|
||||
|
||||
+27
-34
@@ -7,16 +7,14 @@
|
||||
import EllipsisVerticalIcon from '@lucide/svelte/icons/ellipsis-vertical';
|
||||
import * as DropdownMenu from '@cio/ui/base/dropdown-menu';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { Chip } from '@cio/ui/custom/chip';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { handleAddLessonWidget } from '$lib/components/Course/components/Lesson/store';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import {
|
||||
lessonSections,
|
||||
handleDelete,
|
||||
@@ -27,7 +25,7 @@
|
||||
import { updateLesson, updateLessonSection } from '$lib/utils/services/courses';
|
||||
import type { Lesson } from '$lib/utils/types';
|
||||
import DeleteLessonConfirmation from '$lib/components/Course/components/Lesson/DeleteLessonConfirmation.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { RoleBasedSecurity } from '$features/ui';
|
||||
|
||||
type CrudParam = {
|
||||
sectionId?: string;
|
||||
@@ -200,7 +198,7 @@
|
||||
class="mb-2 flex min-h-[50px] items-center justify-between rounded-tl-md rounded-tr-md border-b bg-gray-50 px-3 py-1 dark:bg-neutral-700"
|
||||
>
|
||||
{#if lessonEditing === section.id}
|
||||
<TextField className="w-4/6" bind:value={section.title} errorMessage={errors?.title} />
|
||||
<InputField className="w-4/6" bind:value={section.title} errorMessage={errors?.title} />
|
||||
{:else}
|
||||
<p class="w-4/6 font-semibold">{section.title}</p>
|
||||
{/if}
|
||||
@@ -208,20 +206,20 @@
|
||||
<RoleBasedSecurity allowedRoles={[1, 2]}>
|
||||
<div class="flex items-center">
|
||||
{#if lessonEditing === section.id}
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.cancel')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => {
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
section.title = prevTitle ?? section.title;
|
||||
resetEdit();
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.save')}
|
||||
onClick={() => onSave({ sectionId: section.id })}
|
||||
/>
|
||||
>
|
||||
{$t('course.navItem.lessons.add_lesson.cancel')}
|
||||
</Button>
|
||||
<Button onclick={() => onSave({ sectionId: section.id })}>
|
||||
{$t('course.navItem.lessons.add_lesson.save')}
|
||||
</Button>
|
||||
{:else}
|
||||
<IconButton contained size="small" onClick={() => handleAddLesson(section.id)} disabled={!!lessonEditing}>
|
||||
<IconButton onclick={() => handleAddLesson(section.id)} disabled={!!lessonEditing}>
|
||||
<PlusIcon size={16} />
|
||||
</IconButton>
|
||||
<DropdownMenu.Root>
|
||||
@@ -272,15 +270,10 @@
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
>
|
||||
{#if lessonEditing === lesson.id}
|
||||
<TextField className="w-4/6" bind:value={lesson.title} errorMessage={errors?.title} />
|
||||
<InputField className="w-4/6" bind:value={lesson.title} errorMessage={errors?.title} />
|
||||
{:else}
|
||||
<div class="flex w-4/5 items-center gap-2">
|
||||
<TextChip
|
||||
value={getLessonOrder(section.lessons, lesson.id)}
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
/>
|
||||
<Chip value={getLessonOrder(section.lessons, lesson.id)} />
|
||||
<div>
|
||||
<a
|
||||
href={$globalStore.isStudent && !lesson.is_unlocked
|
||||
@@ -309,18 +302,18 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- IS EDITING -->
|
||||
{#if lessonEditing === lesson.id}
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.cancel')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => {
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
lesson.title = prevTitle ?? lesson.title;
|
||||
resetEdit();
|
||||
}}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.add_lesson.save')}
|
||||
onClick={() => onSave({ lessonId: lesson.id }, lesson)}
|
||||
/>
|
||||
>
|
||||
{$t('course.navItem.lessons.add_lesson.cancel')}
|
||||
</Button>
|
||||
<Button onclick={() => onSave({ lessonId: lesson.id }, lesson)}>
|
||||
{$t('course.navItem.lessons.add_lesson.save')}
|
||||
</Button>
|
||||
{:else}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
|
||||
+10
-9
@@ -3,8 +3,7 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import PlayIcon from '@lucide/svelte/icons/play';
|
||||
import { fetchLesssonLanguageHistory } from '$lib/utils/services/courses';
|
||||
import { diffLines } from 'diff';
|
||||
@@ -12,7 +11,7 @@
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import type { TLocale } from '@cio/db/types';
|
||||
|
||||
interface LessonHistory {
|
||||
@@ -172,14 +171,15 @@
|
||||
<div class="panel bg-white dark:bg-black">
|
||||
<div class="w-full p-10 pr-80">
|
||||
<div class="flex items-start gap-x-10">
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} onClick={handleDrawerClose}>
|
||||
<Button variant="outline" onclick={handleDrawerClose}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
</PrimaryButton>
|
||||
</Button>
|
||||
|
||||
{#if selectedVersionIndex != 0}
|
||||
<div class="">
|
||||
<PrimaryButton isLoading={contentRestoreLoading} onClick={restoreSelectedVersion}
|
||||
>{$t('course.navItem.lessons.version_history.restore_version')}</PrimaryButton
|
||||
<Button loading={contentRestoreLoading} onclick={restoreSelectedVersion}>
|
||||
{$t('course.navItem.lessons.version_history.restore_version')}
|
||||
</Button>
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -230,8 +230,9 @@
|
||||
</button>
|
||||
{/each}
|
||||
<div class="mt-2 flex h-10 items-center justify-start px-10">
|
||||
<PrimaryButton className="h-full" isLoading={isMoreHistoryLoading} onClick={loadMoreHistory}
|
||||
>{$t('course.navItem.lessons.version_history.fetch_more_versions')}</PrimaryButton
|
||||
<Button loading={isMoreHistoryLoading} onclick={loadMoreHistory}>
|
||||
{$t('course.navItem.lessons.version_history.fetch_more_versions')}
|
||||
</Button>
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+15
-26
@@ -1,16 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { lessonDocUpload, resetDocumentUploadStore } from '../../store/lessons';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
||||
import { DocumentUploader } from '$lib/utils/services/courses/presign';
|
||||
import { onDestroy, untrack } from 'svelte';
|
||||
import { UpgradeBanner } from '$lib/features/ui';
|
||||
import { UpgradeBanner, CloseButton } from '$features/ui';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { lesson } from '../../store/lessons';
|
||||
import { CloseButton } from '$lib/components/Buttons/Close';
|
||||
|
||||
let fileInput: HTMLInputElement | undefined = $state();
|
||||
let selectedFile: File | null = $state(null);
|
||||
@@ -246,12 +244,9 @@
|
||||
class="hidden"
|
||||
/>
|
||||
<div class="flex justify-center">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.document.choose_file')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
{isDisabled}
|
||||
onClick={() => fileInput?.click()}
|
||||
/>
|
||||
<Button variant="outline" disabled={isDisabled} onclick={() => fileInput?.click()}>
|
||||
{$t('course.navItem.lessons.materials.tabs.document.choose_file')}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -277,11 +272,9 @@
|
||||
></div>
|
||||
</div>
|
||||
<div class="mt-3 flex justify-center">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.document.cancel_upload')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={cancelUpload}
|
||||
/>
|
||||
<Button variant="outline" onclick={cancelUpload}>
|
||||
{$t('course.navItem.lessons.materials.tabs.document.cancel_upload')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -289,16 +282,12 @@
|
||||
<!-- Upload Button -->
|
||||
{#if selectedFile && !$lessonDocUpload.isUploading}
|
||||
<div class="mt-6 flex justify-end space-x-3">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.document.cancel')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={removeSelectedFile}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.document.upload_document')}
|
||||
onClick={uploadDocument}
|
||||
isLoading={$lessonDocUpload.isUploading}
|
||||
/>
|
||||
<Button variant="outline" onclick={removeSelectedFile}>
|
||||
{$t('course.navItem.lessons.materials.tabs.document.cancel')}
|
||||
</Button>
|
||||
<Button onclick={uploadDocument} loading={$lessonDocUpload.isUploading} disabled={$lessonDocUpload.isUploading}>
|
||||
{$t('course.navItem.lessons.materials.tabs.document.upload_document')}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
+6
-9
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import MODES from '$lib/utils/constants/mode';
|
||||
import FileTextIcon from '@lucide/svelte/icons/file-text';
|
||||
@@ -40,11 +39,9 @@
|
||||
|
||||
{#if mode === MODES.edit}
|
||||
<div class="mb-4">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.document.add_document')}
|
||||
onClick={openDocumentUploadModal}
|
||||
className="mb-4"
|
||||
/>
|
||||
<Button onclick={openDocumentUploadModal} class="mb-4">
|
||||
{$t('course.navItem.lessons.materials.tabs.document.add_document')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if !isEmpty(displayDocuments)}
|
||||
@@ -115,10 +112,10 @@
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
{#if document.type === 'pdf'}
|
||||
<PrimaryButton variant={VARIANTS.CONTAINED_DARK} onClick={() => handleViewPDF(document)} {isLoading}>
|
||||
<Button onclick={() => handleViewPDF(document)} loading={isLoading}>
|
||||
<EyeIcon size={16} />
|
||||
{$t('course.navItem.lessons.materials.tabs.document.view_pdf')}
|
||||
</PrimaryButton>
|
||||
</Button>
|
||||
{:else}
|
||||
<a
|
||||
href={isLoading ? '#' : document.link}
|
||||
|
||||
+11
-14
@@ -8,9 +8,9 @@
|
||||
import { isLessonDirty, lesson } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { copyToClipboard, getVideoUrls, removeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
let genericLinks = $state('');
|
||||
let error = $state('');
|
||||
@@ -56,20 +56,17 @@
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5">
|
||||
<TextField
|
||||
<InputField
|
||||
label={$t('course.navItem.lessons.materials.tabs.video.embed_link')}
|
||||
bind:value={genericLinks}
|
||||
className="flex-1 text-left "
|
||||
inputClassName="text-sm"
|
||||
onChange={() => ($isLessonDirty = true)}
|
||||
className="flex-1"
|
||||
onchange={() => ($isLessonDirty = true)}
|
||||
placeholder="https://www.videoplayer.com/"
|
||||
errorMessage={error}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.video.add_video.add_video')}
|
||||
className="rounded-md"
|
||||
onClick={addVideo}
|
||||
/>
|
||||
<Button onclick={addVideo}>
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.add_video')}
|
||||
</Button>
|
||||
</div>
|
||||
<p class="mt-4 pl-2 text-sm">
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.videos_added')}:
|
||||
@@ -84,10 +81,10 @@
|
||||
<Badge class="max-w-md truncate" variant="secondary">
|
||||
{video.link}
|
||||
</Badge>
|
||||
<IconButton value="copy-video" onClick={() => copyToClipboard(video.link)}>
|
||||
<IconButton onclick={() => copyToClipboard(video.link)}>
|
||||
<CopyIcon size={16} />
|
||||
</IconButton>
|
||||
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
|
||||
<IconButton onclick={() => removeVideo(index)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
+14
-14
@@ -1,14 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { Progress } from '@cio/ui/base/progress';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import { UpgradeBanner } from '$lib/features/ui';
|
||||
import { UpgradeBanner } from '$features/ui';
|
||||
import { lesson, lessonVideoUpload, cancelVideoUpload } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { VideoUploader } from '$lib/utils/services/courses/presign';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
interface Props {
|
||||
lessonId?: string;
|
||||
@@ -144,11 +143,9 @@
|
||||
<Progress class="w-full" value={$lessonVideoUpload.uploadProgress} max={100} />
|
||||
<p class="text-sm">{helperText}</p>
|
||||
<div class="mt-3">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.video.add_video.cancel_upload')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={cancelUpload}
|
||||
/>
|
||||
<Button variant="outline" onclick={cancelUpload}>
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.cancel_upload')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -187,7 +184,9 @@
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.maximum_size')}
|
||||
</p>
|
||||
</span>
|
||||
<PrimaryButton label={$t('course.navItem.lessons.materials.tabs.video.add_video.button')} onClick={tryAgain} />
|
||||
<Button onclick={tryAgain}>
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.button')}
|
||||
</Button>
|
||||
</div>
|
||||
{:else if formRes?.status !== 200}
|
||||
<div class="flex h-full w-full flex-col items-center justify-center rounded-xl">
|
||||
@@ -201,7 +200,9 @@
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.format')}
|
||||
</p>
|
||||
</span>
|
||||
<PrimaryButton label={$t('course.navItem.lessons.materials.tabs.video.add_video.try_again')} onClick={tryAgain} />
|
||||
<Button onclick={tryAgain}>
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.try_again')}
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class=" w-full rounded-md border px-8 py-3">
|
||||
@@ -214,10 +215,9 @@
|
||||
</video>
|
||||
<p>{fileInput?.files?.[0]?.name || fileName}</p>
|
||||
</div>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.button_done')}
|
||||
onClick={() => ($lessonVideoUpload.isModalOpen = false)}
|
||||
/>
|
||||
<Button onclick={() => ($lessonVideoUpload.isModalOpen = false)}>
|
||||
{$t('course.navItem.lessons.materials.button_done')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
+11
-14
@@ -4,13 +4,13 @@
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import type { LessonVideoType } from '$lib/utils/types';
|
||||
import { copyToClipboard, getVideoUrls, removeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { lesson, isLessonDirty } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
let youtubeLinks = $state('');
|
||||
let error = $state('');
|
||||
@@ -45,20 +45,17 @@
|
||||
</script>
|
||||
|
||||
<div class="flex w-full items-{error ? 'center' : 'end'} justify-between gap-5">
|
||||
<TextField
|
||||
<InputField
|
||||
label={$t('course.navItem.lessons.materials.tabs.video.add_video.youtube_link')}
|
||||
bind:value={youtubeLinks}
|
||||
className="flex-1 text-left "
|
||||
inputClassName="text-sm"
|
||||
onChange={() => ($isLessonDirty = true)}
|
||||
className="flex-1 text-left"
|
||||
onchange={() => ($isLessonDirty = true)}
|
||||
placeholder="https://www.youtube.com/watch?v="
|
||||
errorMessage={error}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.video.add_video.add_video')}
|
||||
className="rounded-md"
|
||||
onClick={addVideo}
|
||||
/>
|
||||
<Button onclick={addVideo}>
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.add_video')}
|
||||
</Button>
|
||||
</div>
|
||||
<p class="mt-4 pl-2 text-sm">
|
||||
{$t('course.navItem.lessons.materials.tabs.video.add_video.videos_added')}:
|
||||
@@ -73,10 +70,10 @@
|
||||
<Badge class="max-w-md truncate" variant="secondary">
|
||||
{video.link}
|
||||
</Badge>
|
||||
<IconButton value="copy-video" onClick={() => copyToClipboard(video.link)}>
|
||||
<IconButton onclick={() => copyToClipboard(video.link)}>
|
||||
<CopyIcon size={16} />
|
||||
</IconButton>
|
||||
<IconButton value="delete-video" onClick={() => removeVideo(index)}>
|
||||
<IconButton onclick={() => removeVideo(index)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
+19
-31
@@ -6,18 +6,17 @@
|
||||
|
||||
import { group } from '$lib/components/Course/store';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import { TextareaField } from '@cio/ui/custom/textarea-field';
|
||||
import { shortenName } from '$lib/utils/functions/string';
|
||||
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { DeleteModal } from '$features/ui';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { lesson, lessonCommentsChannel } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import type { GroupPerson, LessonComment, LessonCommentInsertPayload } from '$lib/utils/types';
|
||||
|
||||
const supabase = getSupabase();
|
||||
@@ -63,7 +62,7 @@
|
||||
id: 0,
|
||||
comment: comment,
|
||||
name: $t('course.navItem.lessons.comments.you'),
|
||||
avatar: $profile.avatarUrl,
|
||||
avatar: $profile.avatarUrl || 'AV',
|
||||
commentAt: new Date(),
|
||||
groupmember_id: groupmember.id
|
||||
},
|
||||
@@ -279,7 +278,7 @@
|
||||
<Avatar.Fallback>{shortenName($profile.fullname) || 'U'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="h-full w-full">
|
||||
<TextArea
|
||||
<TextareaField
|
||||
label={$t('course.navItem.lessons.comments.text_area_title')}
|
||||
placeholder={$t('course.navItem.lessons.comments.placeholder')}
|
||||
bind:value={comment}
|
||||
@@ -288,12 +287,9 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex flex-row-reverse">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.comments.comment_btn')}
|
||||
onClick={handleSend}
|
||||
isDisabled={!comment}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
<Button onclick={handleSend} disabled={!comment} loading={isSaving}>
|
||||
{$t('course.navItem.lessons.comments.comment_btn')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -344,22 +340,17 @@
|
||||
</div>
|
||||
|
||||
{#if editCommentId === commentItem.id}
|
||||
<TextArea
|
||||
<TextareaField
|
||||
placeholder={$t('course.navItem.lessons.comments.placeholder')}
|
||||
bind:value={commentItem.comment}
|
||||
/>
|
||||
<div class="mt-2 flex flex-row-reverse items-center gap-2">
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('course.navItem.lessons.comments.cancel_btn')}
|
||||
onClick={() => (editCommentId = null)}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.comments.comment_btn')}
|
||||
onClick={() => handleUpdate(commentItem)}
|
||||
isDisabled={!commentItem.comment}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
<Button variant="outline" onclick={() => (editCommentId = null)}>
|
||||
{$t('course.navItem.lessons.comments.cancel_btn')}
|
||||
</Button>
|
||||
<Button onclick={() => handleUpdate(commentItem)} disabled={!commentItem.comment} loading={isSaving}>
|
||||
{$t('course.navItem.lessons.comments.comment_btn')}
|
||||
</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<article class="prose sm:prose-sm max-w-[300px] dark:text-white">
|
||||
@@ -372,12 +363,9 @@
|
||||
</div>
|
||||
{#if pagination.hasMore}
|
||||
<div class="mt-2 flex items-center justify-center">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.comments.load_more_btn')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => fetchComments($group.people)}
|
||||
isLoading={isFetching}
|
||||
/>
|
||||
<Button variant="outline" onclick={() => fetchComments($group.people)} loading={isFetching}>
|
||||
{$t('course.navItem.lessons.comments.load_more_btn')}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+7
-23
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { lesson, deleteLessonDocument } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { CloseButton } from '$lib/components/Buttons/Close';
|
||||
import { CloseButton } from '$features/ui';
|
||||
import { lessonDocUpload } from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import MODES from '$lib/utils/constants/mode';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import ZoomInIcon from '@lucide/svelte/icons/zoom-in';
|
||||
@@ -12,7 +12,7 @@
|
||||
import DocumentList from '../Document/DocumentList.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { LessonDocument } from '$lib/utils/types';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
|
||||
let { mode = MODES.view } = $props();
|
||||
|
||||
@@ -334,30 +334,18 @@
|
||||
<!-- Navigation Controls -->
|
||||
{#if !isLoading && !error}
|
||||
<div class="flex items-center space-x-1">
|
||||
<IconButton
|
||||
onClick={prevPage}
|
||||
disabled={pageNum <= 1}
|
||||
toolTipProps={{ title: 'Previous page (←)', hotkeys: ['ArrowLeft'] }}
|
||||
>
|
||||
<IconButton onclick={prevPage} disabled={pageNum <= 1} tooltip="Previous page (←)">
|
||||
<ChevronLeftIcon size={16} />
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
onClick={nextPage}
|
||||
disabled={pageNum >= pageCount}
|
||||
toolTipProps={{ title: 'Next page (→)', hotkeys: ['ArrowRight'] }}
|
||||
>
|
||||
<IconButton onclick={nextPage} disabled={pageNum >= pageCount} tooltip="Next page (→)">
|
||||
<ChevronRightIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
<!-- Zoom Controls -->
|
||||
<div class="flex items-center space-x-1">
|
||||
<IconButton
|
||||
onClick={zoomOut}
|
||||
disabled={scale <= 0.5}
|
||||
toolTipProps={{ title: 'Zoom out (-)', hotkeys: ['-'] }}
|
||||
>
|
||||
<IconButton onclick={zoomOut} disabled={scale <= 0.5} tooltip="Zoom out (-)">
|
||||
<ZoomOutIcon size={16} />
|
||||
</IconButton>
|
||||
|
||||
@@ -365,11 +353,7 @@
|
||||
{Math.round(scale * 100)}%
|
||||
</span>
|
||||
|
||||
<IconButton
|
||||
onClick={zoomIn}
|
||||
disabled={scale >= 3.0}
|
||||
toolTipProps={{ title: 'Zoom in (+)', hotkeys: ['+'] }}
|
||||
>
|
||||
<IconButton onclick={zoomIn} disabled={scale >= 3.0} tooltip="Zoom in (+)">
|
||||
<ZoomInIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
+200
-186
@@ -18,7 +18,7 @@
|
||||
import type { Content } from '@cio/ui/custom/editor';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import type { LessonPage } from '$lib/utils/types';
|
||||
import { isHtmlValueEmpty } from '$lib/utils/functions/toHtml';
|
||||
import {
|
||||
@@ -30,23 +30,23 @@
|
||||
lessonVideoUpload,
|
||||
lessonDocUpload
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { lessonFallbackNote, t } from '$lib/utils/functions/translations';
|
||||
import { formatYoutubeVideo } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { Chip } from '@cio/ui/custom/chip';
|
||||
import Loader from './Loader.svelte';
|
||||
import Box from '$lib/components/Box/index.svelte';
|
||||
import Comments from './components/Comments.svelte';
|
||||
import Tabs from '$lib/components/Tabs/index.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import * as UnderlineTabs from '@cio/ui/custom/underline-tabs';
|
||||
import { Empty } from '@cio/ui/custom/empty';
|
||||
import VideoIcon from '@lucide/svelte/icons/video';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import ComponentNote from './components/ComponentNote.svelte';
|
||||
import ComponentSlide from './components/ComponentSlide.svelte';
|
||||
import ComponentVideo from './components/ComponentVideo.svelte';
|
||||
import TabContent from '$lib/components/TabContent/index.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import { TextEditor } from '$features/ui';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import ComponentDocument from './components/ComponentDocument.svelte';
|
||||
import AddVideoToLesson from '$lib/components/Course/components/Lesson/Materials/Video/AddVideoToLesson.svelte';
|
||||
import AddDocumentToLesson from '$lib/components/Course/components/Lesson/Materials/Document/AddDocumentToLesson.svelte';
|
||||
@@ -106,7 +106,7 @@
|
||||
});
|
||||
});
|
||||
const componentsToRender = $derived(getComponentOrder(tabs));
|
||||
let currentTab = $derived(tabs[0].value);
|
||||
let currentTab = $state<string>('');
|
||||
|
||||
function onEditorChange(content: Content) {
|
||||
console.log('content updated:', content);
|
||||
@@ -124,11 +124,11 @@
|
||||
$isLessonDirty = true;
|
||||
}
|
||||
|
||||
const onChange = (tab) => {
|
||||
return () => {
|
||||
currentTab = tab;
|
||||
};
|
||||
};
|
||||
$effect(() => {
|
||||
if (tabs.length > 0 && !currentTab) {
|
||||
currentTab = String(tabs[0].value);
|
||||
}
|
||||
});
|
||||
|
||||
const getValue = (label: string) => {
|
||||
const tabValue = tabs.find((tab) => tab.label === label)?.value;
|
||||
@@ -353,23 +353,33 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
{onClose}
|
||||
<Dialog.Root
|
||||
bind:open={$lessonVideoUpload.isModalOpen}
|
||||
width="w-4/5 w-[90%] h-[80%] md:h-[566px]"
|
||||
modalHeading={$t('course.navItem.lessons.materials.tabs.video.add_video.title')}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) onClose();
|
||||
}}
|
||||
>
|
||||
<AddVideoToLesson {lessonId} />
|
||||
</Modal>
|
||||
<Dialog.Content class="max-w-4/5 h-[80%] w-[90%] md:h-[566px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('course.navItem.lessons.materials.tabs.video.add_video.title')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<AddVideoToLesson {lessonId} />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Modal
|
||||
onClose={onDocumentClose}
|
||||
<Dialog.Root
|
||||
bind:open={$lessonDocUpload.isModalOpen}
|
||||
width="w-4/5 w-[90%] h-[80%] md:h-[566px]"
|
||||
modalHeading={$t('course.navItem.lessons.materials.tabs.document.upload_title')}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) onDocumentClose();
|
||||
}}
|
||||
>
|
||||
<AddDocumentToLesson />
|
||||
</Modal>
|
||||
<Dialog.Content class="max-w-4/5 h-[80%] w-[90%] md:h-[566px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('course.navItem.lessons.materials.tabs.document.upload_title')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<AddDocumentToLesson />
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<HtmlRender className="m-auto text-center">
|
||||
<h1 class="mt-0 text-2xl capitalize md:text-4xl">
|
||||
@@ -380,151 +390,156 @@
|
||||
{#if $lesson.isFetching}
|
||||
<Loader />
|
||||
{:else if mode === MODES.edit}
|
||||
<Tabs {tabs} {currentTab} {onChange}>
|
||||
{#snippet content()}
|
||||
<slot:fragment>
|
||||
<TabContent value={getValue('course.navItem.lessons.materials.tabs.note.title')} index={currentTab}>
|
||||
<div class="flex justify-end gap-1">
|
||||
<!-- Update this when ai-sdk is updated -->
|
||||
<div class="hidden flex-row-reverse">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<PrimaryButton
|
||||
className="flex items-center relative"
|
||||
onClick={() => {
|
||||
openPopover = !openPopover;
|
||||
}}
|
||||
isLoading={$isLoading}
|
||||
isDisabled={$isLoading}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
disableScale
|
||||
>
|
||||
<WandSparklesIcon size={16} />
|
||||
AI
|
||||
</PrimaryButton>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start" class="w-80">
|
||||
<div class="p-2">
|
||||
<button class={aiButtonClass} onclick={() => callAI('outline')}>
|
||||
<ListChecksIcon size={16} />
|
||||
{$t('course.navItem.lessons.materials.tabs.note.ai.outline')}
|
||||
</button>
|
||||
<button class={aiButtonClass} onclick={() => callAI('note')}>
|
||||
<NotepadTextIcon size={16} />
|
||||
{$t('course.navItem.lessons.materials.tabs.note.ai.note')}
|
||||
</button>
|
||||
<button class={aiButtonClass} onclick={() => callAI('activities')}>
|
||||
<ListTodoIcon size={16} />
|
||||
{$t('course.navItem.lessons.materials.tabs.note.ai.activities')}
|
||||
</button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 h-[60vh]">
|
||||
<TextEditor
|
||||
content={_editorValue}
|
||||
onChange={(content) => onEditorChange(content)}
|
||||
onReady={() =>
|
||||
// editor
|
||||
{
|
||||
// editorInstance = editor;
|
||||
// editorWindowRef = editor.view.dom.ownerDocument.defaultView;
|
||||
<UnderlineTabs.Root bind:value={currentTab}>
|
||||
<UnderlineTabs.List>
|
||||
{#each tabs as tab}
|
||||
<UnderlineTabs.Trigger value={String(tab.value)}>
|
||||
{#if tab.icon}
|
||||
<tab.icon size={16} />
|
||||
{/if}
|
||||
{$t(tab.label)}
|
||||
{#if typeof tab.badgeValue === 'number' && tab.badgeValue > 0}
|
||||
<Chip value={`${tab.badgeValue}`} className="ml-1" />
|
||||
{/if}
|
||||
</UnderlineTabs.Trigger>
|
||||
{/each}
|
||||
</UnderlineTabs.List>
|
||||
<UnderlineTabs.Content value={String(getValue('course.navItem.lessons.materials.tabs.note.title') || '')}>
|
||||
<div class="flex justify-end gap-1">
|
||||
<!-- Update this when ai-sdk is updated -->
|
||||
<div class="hidden flex-row-reverse">
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => {
|
||||
openPopover = !openPopover;
|
||||
}}
|
||||
placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
</TabContent>
|
||||
loading={$isLoading}
|
||||
disabled={$isLoading}
|
||||
>
|
||||
<WandSparklesIcon size={16} />
|
||||
AI
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start" class="w-80">
|
||||
<div class="p-2">
|
||||
<button class={aiButtonClass} onclick={() => callAI('outline')}>
|
||||
<ListChecksIcon size={16} />
|
||||
{$t('course.navItem.lessons.materials.tabs.note.ai.outline')}
|
||||
</button>
|
||||
<button class={aiButtonClass} onclick={() => callAI('note')}>
|
||||
<NotepadTextIcon size={16} />
|
||||
{$t('course.navItem.lessons.materials.tabs.note.ai.note')}
|
||||
</button>
|
||||
<button class={aiButtonClass} onclick={() => callAI('activities')}>
|
||||
<ListTodoIcon size={16} />
|
||||
{$t('course.navItem.lessons.materials.tabs.note.ai.activities')}
|
||||
</button>
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TabContent value={getValue('course.navItem.lessons.materials.tabs.slide.title')} index={currentTab}>
|
||||
{#if mode === MODES.edit}
|
||||
<TextField
|
||||
label={$t('course.navItem.lessons.materials.tabs.slide.slide_link')}
|
||||
bind:value={$lesson.materials.slide_url}
|
||||
onInputChange={() => ($isLessonDirty = true)}
|
||||
helperMessage={$t('course.navItem.lessons.materials.tabs.slide.helper_message')}
|
||||
/>
|
||||
{/if}
|
||||
</TabContent>
|
||||
<TabContent value={getValue('course.navItem.lessons.materials.tabs.video.title')} index={currentTab}>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.tabs.video.button')}
|
||||
onClick={openAddVideoModal}
|
||||
className="mb-2"
|
||||
/>
|
||||
{#if $lesson.materials.videos.length}
|
||||
<div class="flex h-full w-full flex-col items-start">
|
||||
{#each $lesson.materials.videos as video, index}
|
||||
{#if mode === MODES.edit}
|
||||
<div class="ml-auto">
|
||||
<IconButton value="delete-video" contained={true} onClick={() => deleteLessonVideo(index)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex h-full w-full flex-col gap-2 overflow-hidden">
|
||||
{#key video.link}
|
||||
<div class="mb-5">
|
||||
{#if video.type === 'youtube'}
|
||||
<iframe
|
||||
width="100%"
|
||||
height="569"
|
||||
class="iframe"
|
||||
src={formatYoutubeVideo(video.link, errors)}
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
{:else if video.type === 'generic'}
|
||||
<iframe
|
||||
width="100%"
|
||||
height="569"
|
||||
class="iframe"
|
||||
src={video.link}
|
||||
title="Embeded Video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
{:else if video.metadata?.svid}
|
||||
<div style="position:relative;padding-bottom:51.416579%">
|
||||
<iframe
|
||||
src="https://muse.ai/embed/{video.metadata
|
||||
?.svid}?logo=https://app.classroomio.com/logo-512.png&subtitles=auto&cover_play_position=center"
|
||||
style="width:100%;height:100%;position:absolute;left:0;top:0"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
title="Muse AI Video Embed"
|
||||
></iframe>
|
||||
</div>
|
||||
{:else}
|
||||
<video
|
||||
bind:this={player}
|
||||
class="plyr-video-trigger"
|
||||
style="max-height: 569px;"
|
||||
playsinline
|
||||
controls
|
||||
>
|
||||
<source src={video.link} type="video/mp4" />
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{/if}
|
||||
<div class="mt-5 h-[60vh]">
|
||||
<TextEditor
|
||||
content={_editorValue}
|
||||
onChange={(content) => onEditorChange(content)}
|
||||
onReady={() =>
|
||||
// editor
|
||||
{
|
||||
// editorInstance = editor;
|
||||
// editorWindowRef = editor.view.dom.ownerDocument.defaultView;
|
||||
}}
|
||||
placeholder={$t('course.navItem.lessons.materials.tabs.note.placeholder')}
|
||||
/>
|
||||
</div>
|
||||
</UnderlineTabs.Content>
|
||||
|
||||
<UnderlineTabs.Content value={String(getValue('course.navItem.lessons.materials.tabs.slide.title') || '')}>
|
||||
{#if mode === MODES.edit}
|
||||
<InputField
|
||||
label={$t('course.navItem.lessons.materials.tabs.slide.slide_link')}
|
||||
bind:value={$lesson.materials.slide_url}
|
||||
onInputChange={() => ($isLessonDirty = true)}
|
||||
helperMessage={$t('course.navItem.lessons.materials.tabs.slide.helper_message')}
|
||||
/>
|
||||
{/if}
|
||||
</UnderlineTabs.Content>
|
||||
<UnderlineTabs.Content value={String(getValue('course.navItem.lessons.materials.tabs.video.title') || '')}>
|
||||
<Button onclick={openAddVideoModal} class="mb-2">
|
||||
{$t('course.navItem.lessons.materials.tabs.video.button')}
|
||||
</Button>
|
||||
{#if $lesson.materials.videos.length}
|
||||
<div class="flex h-full w-full flex-col items-start">
|
||||
{#each $lesson.materials.videos as video, index}
|
||||
{#if mode === MODES.edit}
|
||||
<div class="ml-auto">
|
||||
<IconButton onclick={() => deleteLessonVideo(index)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex h-full w-full flex-col gap-2 overflow-hidden">
|
||||
{#key video.link}
|
||||
<div class="mb-5">
|
||||
{#if video.type === 'youtube'}
|
||||
<iframe
|
||||
width="100%"
|
||||
height="569"
|
||||
class="iframe"
|
||||
src={formatYoutubeVideo(video.link, errors)}
|
||||
title="YouTube video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
{:else if video.type === 'generic'}
|
||||
<iframe
|
||||
width="100%"
|
||||
height="569"
|
||||
class="iframe"
|
||||
src={video.link}
|
||||
title="Embeded Video player"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
></iframe>
|
||||
{:else if video.metadata?.svid}
|
||||
<div style="position:relative;padding-bottom:51.416579%">
|
||||
<iframe
|
||||
src="https://muse.ai/embed/{video.metadata
|
||||
?.svid}?logo=https://app.classroomio.com/logo-512.png&subtitles=auto&cover_play_position=center"
|
||||
style="width:100%;height:100%;position:absolute;left:0;top:0"
|
||||
frameborder="0"
|
||||
allowfullscreen
|
||||
title="Muse AI Video Embed"
|
||||
></iframe>
|
||||
</div>
|
||||
{/key}
|
||||
{:else}
|
||||
<video
|
||||
bind:this={player}
|
||||
class="plyr-video-trigger"
|
||||
style="max-height: 569px;"
|
||||
playsinline
|
||||
controls
|
||||
>
|
||||
<source src={video.link} type="video/mp4" />
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</TabContent>
|
||||
<TabContent value={getValue('course.navItem.lessons.materials.tabs.document.title')} index={currentTab}>
|
||||
<ComponentDocument {mode} />
|
||||
</TabContent>
|
||||
</slot:fragment>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</UnderlineTabs.Content>
|
||||
<UnderlineTabs.Content value={String(getValue('course.navItem.lessons.materials.tabs.document.title') || '')}>
|
||||
<ComponentDocument {mode} />
|
||||
</UnderlineTabs.Content>
|
||||
</UnderlineTabs.Root>
|
||||
{:else if !isMaterialsEmpty($lesson.materials, $lessonByTranslation[lessonId])}
|
||||
{#key lessonId}
|
||||
<div class="mb-20 flex w-full flex-col gap-6" in:fade={{ delay: 500 }} out:fade>
|
||||
@@ -534,29 +549,28 @@
|
||||
|
||||
{#if $currentOrg.customization?.apps?.comments}
|
||||
<hr class="my-5" />
|
||||
{$currentOrg.customization}
|
||||
|
||||
<Comments {lessonId} />
|
||||
{/if}
|
||||
</div>
|
||||
{/key}
|
||||
{:else}
|
||||
<Box className="text-center">
|
||||
<img src="/no-video.svg" alt="Video not found" />
|
||||
<h3 class="py-2 text-xl font-normal dark:text-white">
|
||||
{$t('course.navItem.lessons.materials.body_heading')}
|
||||
</h3>
|
||||
|
||||
<Empty
|
||||
title={$t('course.navItem.lessons.materials.body_heading')}
|
||||
description={$t('course.navItem.lessons.materials.body_content') +
|
||||
' ' +
|
||||
$t('course.navItem.lessons.materials.get_started') +
|
||||
' ' +
|
||||
$t('course.navItem.lessons.materials.button') +
|
||||
'.'}
|
||||
icon={VideoIcon}
|
||||
variant="page"
|
||||
class="text-center"
|
||||
>
|
||||
{#if !isStudent}
|
||||
<p class="py-2 text-center text-sm font-normal">
|
||||
{$t('course.navItem.lessons.materials.body_content')}
|
||||
<strong>{$t('course.navItem.lessons.materials.get_started')}</strong>
|
||||
{$t('course.navItem.lessons.materials.button')}.
|
||||
</p>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.lessons.materials.get_started')}
|
||||
className="rounded-md"
|
||||
onClick={toggleMode}
|
||||
/>
|
||||
<Button onclick={toggleMode}>
|
||||
{$t('course.navItem.lessons.materials.get_started')}
|
||||
</Button>
|
||||
{/if}
|
||||
</Box>
|
||||
</Empty>
|
||||
{/if}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
// import Select from '$lib/components/Form/Select.svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { InputField } from '@cio/ui/custom/input-field';
|
||||
import {
|
||||
lessons,
|
||||
lessonSections,
|
||||
@@ -11,7 +10,7 @@
|
||||
handleSaveLessonSection
|
||||
} from '$lib/components/Course/components/Lesson/store/lessons';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { goto } from '$app/navigation';
|
||||
import { handleAddLessonWidget } from './store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
@@ -107,35 +106,39 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={handleClose}
|
||||
<Dialog.Root
|
||||
bind:open={$handleAddLessonWidget.open}
|
||||
width="w-[80%] md:w-[65%]"
|
||||
maxWidth="max-w-2xl"
|
||||
containerClass="overflow-hidden"
|
||||
modalHeading={$t(
|
||||
`course.navItem.lessons.add_lesson.${$handleAddLessonWidget.isSection ? 'modal_heading_section' : 'modal_heading'}`
|
||||
)}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) handleClose();
|
||||
}}
|
||||
>
|
||||
<form
|
||||
onsubmit={preventDefault(handleSave)}
|
||||
class="relative m-auto mb-2 flex flex-wrap items-center px-2 py-2 md:mb-4 md:px-5 md:py-3 dark:bg-neutral-800"
|
||||
>
|
||||
<div class="w-full">
|
||||
<TextField
|
||||
label={$t(
|
||||
`course.navItem.lessons.add_lesson.${
|
||||
$handleAddLessonWidget.isSection ? 'lesson_section_title' : 'lesson_title'
|
||||
}`
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{$t(
|
||||
`course.navItem.lessons.add_lesson.${$handleAddLessonWidget.isSection ? 'modal_heading_section' : 'modal_heading'}`
|
||||
)}
|
||||
bind:value={lesson.title}
|
||||
autoFocus={true}
|
||||
className="flex-1 min-w-lg max-w-lg"
|
||||
isRequired={true}
|
||||
errorMessage={errors.title}
|
||||
/>
|
||||
{#if $course.type == COURSE_TYPE.LIVE_CLASS}
|
||||
<!-- <div
|
||||
</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<form
|
||||
onsubmit={preventDefault(handleSave)}
|
||||
class="relative mb-2 flex w-full flex-wrap items-center px-2 py-2 md:mb-4 md:px-5 md:py-3"
|
||||
>
|
||||
<div class="w-full">
|
||||
<InputField
|
||||
label={$t(
|
||||
`course.navItem.lessons.add_lesson.${
|
||||
$handleAddLessonWidget.isSection ? 'lesson_section_title' : 'lesson_title'
|
||||
}`
|
||||
)}
|
||||
bind:value={lesson.title}
|
||||
autoFocus={true}
|
||||
className="w-full"
|
||||
isRequired={true}
|
||||
errorMessage={errors.title}
|
||||
/>
|
||||
{#if $course.type == COURSE_TYPE.LIVE_CLASS}
|
||||
<!-- <div
|
||||
class="flex items-start justify-evenly gap-1 flex-col lg:flex-row lg:items-center mt-2 w-4/5"
|
||||
>
|
||||
<div class="lg:mb-0">
|
||||
@@ -156,14 +159,17 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center mb-3 lg:mb-0">
|
||||
<TextField className="w-[179px]" placeholder="https://meet.google.com/mga-dsjs-fmb" />
|
||||
<InputField className="w-[179px]" placeholder="https://meet.google.com/mga-dsjs-fmb" />
|
||||
</div>
|
||||
</div> -->
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="flex flex-row-reverse">
|
||||
<PrimaryButton label={$t('course.navItem.lessons.add_lesson.save')} onClick={handleSave} />
|
||||
</div>
|
||||
</Modal>
|
||||
<div class="flex flex-row-reverse">
|
||||
<Button onclick={handleSave}>
|
||||
{$t('course.navItem.lessons.add_lesson.save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { RealtimeChannel } from '@supabase/supabase-js';
|
||||
import type { TLocale } from '@cio/db/types';
|
||||
import type { Writable } from 'svelte/store';
|
||||
import { lessonValidation } from '$lib/utils/functions/validator';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const lessonVideoUpload = writable({
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import { goto } from '$app/navigation';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||
import LockIcon from '@lucide/svelte/icons/lock';
|
||||
import * as Collapsible from '@cio/ui/base/collapsible';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import AwardIcon from '@lucide/svelte/icons/award';
|
||||
import PanelTop from '@lucide/svelte/icons/panel-top';
|
||||
import SettingsIcon from '@lucide/svelte/icons/settings';
|
||||
import MegaphoneIcon from '@lucide/svelte/icons/megaphone';
|
||||
import ChartLineIcon from '@lucide/svelte/icons/chart-line';
|
||||
import BookCheckIcon from '@lucide/svelte/icons/book-check';
|
||||
import ScrollTextIcon from '@lucide/svelte/icons/scroll-text';
|
||||
import SquareLibrary from '@lucide/svelte/icons/square-library';
|
||||
import BookOpenCheck from '@lucide/svelte/icons/book-open-check';
|
||||
import TableOfContentsIcon from '@lucide/svelte/icons/table-of-contents';
|
||||
|
||||
import { NAV_IDS } from './constants';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { handleAddLessonWidget } from '../Lesson/store';
|
||||
import { getIsLessonComplete } from '../Lesson/functions';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { COURSE_TYPE, COURSE_VERSION } from '$lib/utils/types';
|
||||
import { lessons, lessonSections } from '../Lesson/store/lessons';
|
||||
import { getNavItemRoute, getLessonsRoute, getLectureNo } from '$lib/components/Course/function';
|
||||
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
isStudent?: boolean;
|
||||
}
|
||||
|
||||
let { path, isStudent = false }: Props = $props();
|
||||
|
||||
const navItems = $derived(
|
||||
[
|
||||
{
|
||||
id: NAV_IDS.NEWS_FEED,
|
||||
title: $t('course.navItems.nav_news_feed'),
|
||||
url: getNavItemRoute($course.id),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id),
|
||||
show() {
|
||||
return isStudent ? $currentOrg.customization?.['course']?.['newsfeed'] : true;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.NEWS_FEED)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.LESSONS,
|
||||
title: $t('course.navItems.nav_content'),
|
||||
url: getLessonsRoute($course.id),
|
||||
isActive:
|
||||
(path || page.url.pathname).includes('/lessons') ||
|
||||
(path || page.url.pathname) === getLessonsRoute($course.id),
|
||||
isLesson: true,
|
||||
icon: getNavIcon(NAV_IDS.LESSONS),
|
||||
items:
|
||||
$course.version === COURSE_VERSION.V1
|
||||
? ($lessons || []).map((lesson, index) => ({
|
||||
title: lesson.title,
|
||||
url: getLessonsRoute($course.id, lesson.id),
|
||||
lesson,
|
||||
index,
|
||||
isV1: true
|
||||
}))
|
||||
: $lessonSections.flatMap((section) =>
|
||||
section.lessons.map((lesson) => ({
|
||||
title: lesson.title,
|
||||
url: getLessonsRoute($course.id, lesson.id),
|
||||
lesson,
|
||||
sectionTitle: section.title,
|
||||
isV1: false
|
||||
}))
|
||||
)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.ANALYTICS,
|
||||
title: $t('course.navItems.nav_analytics'),
|
||||
url: getNavItemRoute($course.id, 'analytics'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'analytics'),
|
||||
show() {
|
||||
return !isStudent;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.ANALYTICS)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.ATTENDANCE,
|
||||
title: $t('course.navItems.nav_attendance'),
|
||||
url: getNavItemRoute($course.id, 'attendance'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'attendance'),
|
||||
show() {
|
||||
if ($course.type !== COURSE_TYPE.LIVE_CLASS) return false;
|
||||
return true;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.ATTENDANCE)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.SUBMISSIONS,
|
||||
title: $t('course.navItems.nav_submissions'),
|
||||
url: getNavItemRoute($course.id, 'submissions'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'submissions'),
|
||||
show() {
|
||||
if (isStudent) return false;
|
||||
return true;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.SUBMISSIONS)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.MARKS,
|
||||
title: $t('course.navItems.nav_marks'),
|
||||
url: getNavItemRoute($course.id, 'marks'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'marks'),
|
||||
show() {
|
||||
if ($course.type == COURSE_TYPE.LIVE_CLASS) {
|
||||
return isStudent ? ($currentOrg.customization?.['course']?.['grading'] ?? false) : true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.MARKS)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.CERTIFICATES,
|
||||
title: $t('course.navItems.nav_certificates'),
|
||||
url: getNavItemRoute($course.id, 'certificates'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'certificates'),
|
||||
show() {
|
||||
if (isStudent && $isFreePlan) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.CERTIFICATES)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.LANDING_PAGE,
|
||||
title: $t('course.navItems.nav_landing_page'),
|
||||
url: getNavItemRoute($course.id, 'landingpage'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'landingpage'),
|
||||
show() {
|
||||
return !isStudent;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.LANDING_PAGE)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.PEOPLE,
|
||||
title: $t('course.navItems.nav_people'),
|
||||
url: getNavItemRoute($course.id, 'people'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'people'),
|
||||
show() {
|
||||
return !isStudent;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.PEOPLE)
|
||||
},
|
||||
{
|
||||
id: NAV_IDS.SETTINGS,
|
||||
title: $t('course.navItems.nav_settings'),
|
||||
url: getNavItemRoute($course.id, 'settings'),
|
||||
isActive: (path || page.url.pathname) === getNavItemRoute($course.id, 'settings'),
|
||||
show() {
|
||||
return !isStudent;
|
||||
},
|
||||
icon: getNavIcon(NAV_IDS.SETTINGS)
|
||||
}
|
||||
].filter((item) => !item.show || item.show())
|
||||
);
|
||||
|
||||
function addLesson(isSection: boolean, id: string) {
|
||||
goto('/courses/' + $course.id + '/lessons');
|
||||
$handleAddLessonWidget.open = true;
|
||||
|
||||
if ($course.version === COURSE_VERSION.V2) {
|
||||
$handleAddLessonWidget.isSection = isSection ? false : true;
|
||||
|
||||
if (isSection) {
|
||||
$handleAddLessonWidget.id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getNavIcon(id: string) {
|
||||
if (!id) return null;
|
||||
|
||||
if (id === NAV_IDS.SECTION) {
|
||||
return TableOfContentsIcon;
|
||||
} else if (id === NAV_IDS.NEWS_FEED) {
|
||||
return MegaphoneIcon;
|
||||
} else if (id === NAV_IDS.LESSONS) {
|
||||
return ScrollTextIcon;
|
||||
} else if (id === NAV_IDS.ATTENDANCE) {
|
||||
return BookOpenCheck;
|
||||
} else if (id === NAV_IDS.SUBMISSIONS) {
|
||||
return SquareLibrary;
|
||||
} else if (id === NAV_IDS.MARKS) {
|
||||
return BookCheckIcon;
|
||||
} else if (id === NAV_IDS.PEOPLE) {
|
||||
return UsersIcon;
|
||||
} else if (id === NAV_IDS.ANALYTICS) {
|
||||
return ChartLineIcon;
|
||||
} else if (id === NAV_IDS.LANDING_PAGE) {
|
||||
return PanelTop;
|
||||
} else if (id === NAV_IDS.CERTIFICATES) {
|
||||
return AwardIcon;
|
||||
} else if (id === NAV_IDS.SETTINGS) {
|
||||
return SettingsIcon;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function toggleSidebarOnMobile() {}
|
||||
</script>
|
||||
|
||||
<Sidebar.Provider class="flex w-fit items-start gap-2">
|
||||
<Sidebar.Root collapsible="icon">
|
||||
<Sidebar.Content>
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel>Course Navigation</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each navItems as item (item.id)}
|
||||
<Collapsible.Root open={item.isActive} class="group/collapsible">
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuItem {...props}>
|
||||
{#if item.isLesson && item.items}
|
||||
<Collapsible.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<a href={item.url}>
|
||||
<Sidebar.MenuButton
|
||||
{...props}
|
||||
tooltipContent={item.title}
|
||||
class="flex w-full items-center gap-4 py-2 pl-1.5"
|
||||
>
|
||||
{@const Icon = item.icon}
|
||||
<Icon size={16} class="nav-icon group-hover:animate-{item.id}" />
|
||||
{item.title}
|
||||
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<Plus
|
||||
size={20}
|
||||
class="rounded-full p-1 hover:bg-gray-200"
|
||||
onclick={() => addLesson(false, item.id)}
|
||||
/>
|
||||
|
||||
<ChevronRightIcon
|
||||
size={20}
|
||||
class="rounded-full p-1 transition-transform duration-200 hover:bg-gray-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</div>
|
||||
</Sidebar.MenuButton>
|
||||
</a>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub>
|
||||
{#if $course.version === COURSE_VERSION.V1}
|
||||
{#each item.items as lessonItem}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton
|
||||
isActive={(path || page.url.pathname).includes(lessonItem.lesson.id)}
|
||||
>
|
||||
{#snippet child({ props })}
|
||||
<a
|
||||
href={isStudent && !lessonItem.lesson.is_unlocked
|
||||
? page.url.pathname
|
||||
: lessonItem.url}
|
||||
onclick={toggleSidebarOnMobile}
|
||||
aria-disabled={!lessonItem.lesson.is_unlocked}
|
||||
title={lessonItem.lesson.title}
|
||||
class="flex w-full items-center gap-2 {isStudent && !lessonItem.lesson.is_unlocked
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''}"
|
||||
{...props}
|
||||
>
|
||||
{#if lessonItem.isV1 && 'index' in lessonItem}
|
||||
<TextChip
|
||||
value={getLectureNo(lessonItem.index + 1)}
|
||||
className="bg-primary-200 text-primary-600 text-xs"
|
||||
size="sm"
|
||||
shape="rounded-full"
|
||||
/>
|
||||
{/if}
|
||||
<span class="flex-1 truncate">{lessonItem.lesson.title}</span>
|
||||
{#if !lessonItem.lesson.is_unlocked}
|
||||
<LockIcon size={16} class="shrink-0" />
|
||||
{:else if getIsLessonComplete(lessonItem.lesson.lesson_completion, $profile.id)}
|
||||
<span class="shrink-0">
|
||||
<CircleCheckIcon size={16} filled />
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each $lessonSections as section}
|
||||
<Collapsible.Root open={true} class="group/section">
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuSubItem {...props}>
|
||||
<Collapsible.Trigger>
|
||||
{#snippet child({ props })}
|
||||
<Sidebar.MenuSubButton
|
||||
{...props}
|
||||
class="flex w-full items-center gap-2 font-medium"
|
||||
>
|
||||
{@const Icon = getNavIcon(NAV_IDS.SECTION)}
|
||||
<Icon size={14} />
|
||||
<span class="flex-1 truncate">{section.title}</span>
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<Plus
|
||||
size={20}
|
||||
class="rounded-full p-1 hover:bg-gray-200"
|
||||
onclick={() => addLesson(true, section.id)}
|
||||
/>
|
||||
|
||||
<ChevronRightIcon
|
||||
size={20}
|
||||
class="rounded-full p-1 transition-transform duration-200 hover:bg-gray-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</div>
|
||||
</Sidebar.MenuSubButton>
|
||||
{/snippet}
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub class="ml-2">
|
||||
{#each section.lessons as lesson}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton
|
||||
isActive={(path || page.url.pathname).includes(lesson.id)}
|
||||
>
|
||||
{#snippet child({ props })}
|
||||
<a
|
||||
href={isStudent && !lesson.is_unlocked
|
||||
? page.url.pathname
|
||||
: getLessonsRoute($course.id, lesson.id)}
|
||||
onclick={toggleSidebarOnMobile}
|
||||
aria-disabled={!lesson.is_unlocked}
|
||||
title={lesson.title}
|
||||
class="flex w-full items-center gap-2 {isStudent && !lesson.is_unlocked
|
||||
? 'cursor-not-allowed opacity-50'
|
||||
: ''}"
|
||||
{...props}
|
||||
>
|
||||
<span class="flex-1 truncate">{lesson.title}</span>
|
||||
{#if !lesson.is_unlocked}
|
||||
<LockIcon size={16} class="shrink-0" />
|
||||
{:else if getIsLessonComplete(lesson.lesson_completion, $profile.id)}
|
||||
<span class="shrink-0">
|
||||
<CircleCheckIcon size={16} filled />
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/snippet}
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
</Sidebar.MenuSub>
|
||||
</Collapsible.Content>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/snippet}
|
||||
</Collapsible.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Sidebar.MenuSub>
|
||||
</Collapsible.Content>
|
||||
{:else}
|
||||
<a href={item.url} onclick={toggleSidebarOnMobile}>
|
||||
<Sidebar.MenuButton
|
||||
tooltipContent={item.title}
|
||||
class="flex w-full cursor-pointer items-center gap-4 px-1.5 py-2 {item.isActive
|
||||
? 'ui:bg-accent ui:text-accent-foreground'
|
||||
: ''}"
|
||||
>
|
||||
{@const Icon = item.icon}
|
||||
<Icon size={16} class="nav-icon group-hover:animate-{item.id}" />
|
||||
{item.title}
|
||||
</Sidebar.MenuButton>
|
||||
</a>
|
||||
{/if}
|
||||
</Sidebar.MenuItem>
|
||||
{/snippet}
|
||||
</Collapsible.Root>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
</Sidebar.Content>
|
||||
<Sidebar.Rail />
|
||||
</Sidebar.Root>
|
||||
|
||||
<Sidebar.Inset>
|
||||
<Sidebar.Trigger />
|
||||
</Sidebar.Inset>
|
||||
</Sidebar.Provider>
|
||||
|
||||
<style>
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
+21
-14
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
interface Props {
|
||||
openDeleteModal?: boolean;
|
||||
@@ -17,18 +15,27 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal onClose={() => (openDeleteModal = false)} bind:open={openDeleteModal} width="w-96" modalHeading="Delete Feed">
|
||||
<div>
|
||||
<Dialog.Root
|
||||
bind:open={openDeleteModal}
|
||||
>
|
||||
<Dialog.Content class="w-96">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Delete Feed</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div>
|
||||
<h1 class="text-lg dark:text-white">Are you sure you want to delete this feed?</h1>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label="No"
|
||||
onClick={() => (openDeleteModal = false)}
|
||||
/>
|
||||
<PrimaryButton className="px-6 py-3" variant={VARIANTS.OUTLINED} label="Yes" onClick={handleDelete} />
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => (openDeleteModal = false)}
|
||||
>
|
||||
No
|
||||
</Button>
|
||||
<Button variant="outline" onclick={handleDelete}>
|
||||
Yes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import type { Feed, Author } from '$lib/utils/types/feed';
|
||||
import { createNewFeed } from '$lib/utils/services/newsfeed';
|
||||
import { getTextFromHTML } from '$lib/utils/functions/toHtml';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { createNewsfeedValidation } from '$lib/utils/functions/validator';
|
||||
import { isNewFeedModal } from '$lib/components/Course/components/NewsFeed/store';
|
||||
import { NOTIFICATION_NAME, triggerSendEmail } from '$lib/utils/services/notification/notification';
|
||||
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { TextEditor } from '$features/ui';
|
||||
|
||||
interface Props {
|
||||
author?: Author | any;
|
||||
@@ -113,16 +112,21 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={resetEditor}
|
||||
<Dialog.Root
|
||||
bind:open={$isNewFeedModal.open}
|
||||
width="w-4/5"
|
||||
maxWidth="max-w-lg"
|
||||
modalHeading={edit === true
|
||||
? $t('course.navItem.news_feed.heading_button.edit_post')
|
||||
: $t('course.navItem.news_feed.heading_button.make_a_post')}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) resetEditor();
|
||||
}}
|
||||
>
|
||||
<section class="w-2/ flex h-full flex-col rounded-xl pb-3">
|
||||
<Dialog.Content class="w-4/5 max-w-lg">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>
|
||||
{edit === true
|
||||
? $t('course.navItem.news_feed.heading_button.edit_post')
|
||||
: $t('course.navItem.news_feed.heading_button.make_a_post')}
|
||||
</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<section class="w-2/ flex h-full flex-col rounded-xl pb-3">
|
||||
<TextEditor
|
||||
content={newPost}
|
||||
onChange={(text) => {
|
||||
@@ -136,13 +140,17 @@
|
||||
{/if}
|
||||
<div class="flex items-center justify-end py-2">
|
||||
<div class="flex gap-2">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.news_feed.heading_button.cancel')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={resetEditor}
|
||||
/>
|
||||
<PrimaryButton {isLoading} label={$t('course.navItem.news_feed.heading_button.post')} onClick={onPost} />
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={resetEditor}
|
||||
>
|
||||
{$t('course.navItem.news_feed.heading_button.cancel')}
|
||||
</Button>
|
||||
<Button loading={isLoading} onclick={onPost}>
|
||||
{$t('course.navItem.news_feed.heading_button.post')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -14,10 +14,10 @@
|
||||
import { addNewsfeedCommentValidation } from '$lib/utils/functions/validator';
|
||||
import { isNewFeedModal } from '$lib/components/Course/components/NewsFeed/store';
|
||||
|
||||
import Chip from '$lib/components/Chip/index.svelte';
|
||||
import { Chip } from '@cio/ui/custom/chip';
|
||||
import DeleteFeedConfirmation from './DeleteFeedConfirmation.svelte';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import { RoleBasedSecurity } from '$lib/features/ui';
|
||||
import { RoleBasedSecurity } from '$features/ui';
|
||||
|
||||
interface Props {
|
||||
feed: Feed;
|
||||
@@ -102,7 +102,7 @@
|
||||
const usersReacted = feed.reaction[reactionType] || [];
|
||||
|
||||
return usersReacted.includes(author.id)
|
||||
? 'bg-primary-200 border-primary-600 pl-2'
|
||||
? 'bg-primary-200 ui:border-primary pl-2'
|
||||
: 'bg-gray-200 border-gray-600 pl-2';
|
||||
};
|
||||
|
||||
|
||||
+22
-24
@@ -1,8 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { deleteMemberModal } from './store';
|
||||
|
||||
@@ -14,32 +12,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={() => ($deleteMemberModal.open = false)}
|
||||
<Dialog.Root
|
||||
bind:open={$deleteMemberModal.open}
|
||||
width="w-96"
|
||||
containerClass="pt-3"
|
||||
headerClass="py-2 px-5"
|
||||
modalHeading={$t('course.navItem.people.delete_confirmation.title')}
|
||||
>
|
||||
<div>
|
||||
<Dialog.Content class="w-96 pt-3">
|
||||
<Dialog.Header class="py-2 px-5">
|
||||
<Dialog.Title>{$t('course.navItem.people.delete_confirmation.title')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div>
|
||||
<p class="mt-0 text-base dark:text-white">
|
||||
{$t('course.navItem.people.delete_confirmation.sure')} <strong>{email}</strong>?
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('course.navItem.people.delete_confirmation.no')}
|
||||
onClick={() => ($deleteMemberModal.open = false)}
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('course.navItem.people.delete_confirmation.yes')}
|
||||
onClick={handleDelete}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={() => ($deleteMemberModal.open = false)}
|
||||
>
|
||||
{$t('course.navItem.people.delete_confirmation.no')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onclick={handleDelete}
|
||||
>
|
||||
{$t('course.navItem.people.delete_confirmation.yes')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
+103
-100
@@ -13,9 +13,9 @@
|
||||
|
||||
import { qrInviteNodeStore } from './store';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
import { orgApi } from '$lib/features/org/api/org.svelte';
|
||||
import { orgApi } from '$features/org/api/org.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { snackbar } from '$features/ui/snackbar/store';
|
||||
import type { OrgTeamMember } from '$lib/utils/types/org';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import { course, setCourse } from '$lib/components/Course/store';
|
||||
@@ -25,8 +25,8 @@
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
|
||||
import ShareQrImage from './ShareQRImage.svelte';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
|
||||
interface Tutor {
|
||||
id: number;
|
||||
@@ -176,112 +176,115 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={() => closeModal()}
|
||||
<Dialog.Root
|
||||
open={addPeopleParm === 'true'}
|
||||
width="w-4/5 md:w-2/5"
|
||||
maxWidth="max-w-lg"
|
||||
modalHeading={$t('course.navItem.people.invite_modal.title')}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) closeModal();
|
||||
}}
|
||||
>
|
||||
<form onsubmit={preventDefault(onSubmit)}>
|
||||
<div class="mb-8">
|
||||
<p class="mb-1 text-base font-semibold">{$t('course.navItem.people.invite_modal.invite')}</p>
|
||||
<Select.Root type="multiple" bind:value={selectedIds} disabled={orgApi.isLoading}>
|
||||
<Select.Trigger class="w-full">
|
||||
<div>
|
||||
{#if selectedTutors.length > 0}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each selectedTutors as tutor}
|
||||
<Badge variant="secondary" class="flex items-center gap-1">
|
||||
{tutor.text}
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectedIds = selectedIds.filter((id) => id !== tutor.id.toString());
|
||||
}}
|
||||
class="rounded-sm hover:bg-gray-200"
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{$t('course.navItem.people.invite_modal.select')}
|
||||
{/if}
|
||||
<Dialog.Content class="w-4/5 max-w-lg md:w-2/5">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('course.navItem.people.invite_modal.title')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<form onsubmit={preventDefault(onSubmit)}>
|
||||
<div class="mb-8">
|
||||
<p class="mb-1 text-base font-semibold">{$t('course.navItem.people.invite_modal.invite')}</p>
|
||||
<Select.Root type="multiple" bind:value={selectedIds} disabled={orgApi.isLoading}>
|
||||
<Select.Trigger class="w-full">
|
||||
<div>
|
||||
{#if selectedTutors.length > 0}
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each selectedTutors as tutor}
|
||||
<Badge variant="secondary" class="flex items-center gap-1">
|
||||
{tutor.text}
|
||||
<button
|
||||
type="button"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
selectedIds = selectedIds.filter((id) => id !== tutor.id.toString());
|
||||
}}
|
||||
class="rounded-sm hover:bg-gray-200"
|
||||
>
|
||||
<XIcon size={14} />
|
||||
</button>
|
||||
</Badge>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
{$t('course.navItem.people.invite_modal.select')}
|
||||
{/if}
|
||||
</div>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each tutors as tutor}
|
||||
<Select.Item value={tutor.id.toString()}>
|
||||
{tutor.text}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if orgApi.isLoading}
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<Spinner class="h-4 w-4" />
|
||||
<span class="text-sm">Loading...</span>
|
||||
</div>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each tutors as tutor}
|
||||
<Select.Item value={tutor.id.toString()}>
|
||||
{tutor.text}
|
||||
</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if orgApi.isLoading}
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<Spinner class="h-4 w-4" />
|
||||
<span class="text-sm">Loading...</span>
|
||||
{:else}
|
||||
<span class="mt-2 block text-sm">
|
||||
{$t('course.navItem.people.invite_modal.to_add')}
|
||||
<a href={`/org/${$currentOrg.siteName}/settings/teams`} class="ui:text-primary underline">
|
||||
{$t('course.navItem.people.invite_modal.go_to')}
|
||||
</a>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-8 flex w-full items-center justify-between">
|
||||
<div class="w-3/5">
|
||||
<p class="mb-1 text-base font-semibold">
|
||||
{$t('course.navItem.people.invite_modal.invite_students')}
|
||||
</p>
|
||||
<p class=" text-sm">{$t('course.navItem.people.invite_modal.via_link')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="mt-2 block text-sm">
|
||||
{$t('course.navItem.people.invite_modal.to_add')}
|
||||
<a href={`/org/${$currentOrg.siteName}/settings/teams`} class="text-primary-600 underline">
|
||||
{$t('course.navItem.people.invite_modal.go_to')}
|
||||
</a>
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-8 flex w-full items-center justify-between">
|
||||
<div class="w-3/5">
|
||||
<p class="mb-1 text-base font-semibold">
|
||||
{$t('course.navItem.people.invite_modal.invite_students')}
|
||||
</p>
|
||||
<p class=" text-sm">{$t('course.navItem.people.invite_modal.via_link')}</p>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<button type="button" onclick={copyLink} class="text-primary-800 cursor-pointer capitalize underline">
|
||||
{$t('course.navItem.people.invite_modal.copy_link')}
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start" class="w-auto">
|
||||
<div class="p-1 text-sm">{$t('course.navItem.people.invite_modal.success')}</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
|
||||
<Popover.Root>
|
||||
<Popover.Trigger>
|
||||
<button type="button" onclick={copyLink} class="text-primary-800 cursor-pointer capitalize underline">
|
||||
{$t('course.navItem.people.invite_modal.copy_link')}
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content align="start" class="w-auto">
|
||||
<div class="p-1 text-sm">{$t('course.navItem.people.invite_modal.success')}</div>
|
||||
</Popover.Content>
|
||||
</Popover.Root>
|
||||
</div>
|
||||
<div
|
||||
class="flex w-full flex-col-reverse items-center justify-between gap-5 rounded-md border p-4 md:flex-row md:items-stretch"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-between gap-3 md:items-start">
|
||||
<span class="text-sm font-medium">
|
||||
{$t('course.navItem.people.invite_modal.via_qr')}
|
||||
</span>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-col-reverse items-center justify-between gap-5 rounded-md border p-4 md:flex-row md:items-stretch"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-between gap-3 md:items-start">
|
||||
<span class="text-sm font-medium">
|
||||
{$t('course.navItem.people.invite_modal.via_qr')}
|
||||
</span>
|
||||
<Button variant="outline" loading={isLoadingQRDownload} onclick={handleQRDownload}>
|
||||
{$t('course.navItem.people.invite_modal.download_qr')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<PrimaryButton
|
||||
isLoading={isLoadingQRDownload}
|
||||
onClick={handleQRDownload}
|
||||
label={$t('course.navItem.people.invite_modal.download_qr')}
|
||||
className="font-medium"
|
||||
/>
|
||||
<div class="w-full border-4 border-[#f7f7f7] p-1 md:w-28">
|
||||
<img src={qrImage} alt="link qrcode" class="h-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full border-4 border-[#f7f7f7] p-1 md:w-28">
|
||||
<img src={qrImage} alt="link qrcode" class="h-full w-full" />
|
||||
<div class="w-160 absolute left-[-1000px]">
|
||||
<ShareQrImage {qrImage} course={$course} currentOrg={$currentOrg} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-160 absolute left-[-1000px]">
|
||||
<ShareQrImage {qrImage} course={$course} currentOrg={$currentOrg} />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex flex-row-reverse items-center">
|
||||
<PrimaryButton className="px-6 py-3" label={$t('course.navItem.people.invite_modal.finish')} type="submit" />
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
<div class="mt-5 flex flex-row-reverse items-center">
|
||||
<Button variant="secondary" type="submit">
|
||||
{$t('course.navItem.people.invite_modal.finish')}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { AccountOrg } from '$lib/features/app/types';
|
||||
import type { AccountOrg } from '$features/app/types';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { isFreePlan } from '$lib/utils/store/org';
|
||||
import { qrInviteNodeStore } from './store';
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="my-4 rounded-xl bg-gray-100 p-2 text-xl">Scan QR</div>
|
||||
<img src={qrImage} alt="qrcode" />
|
||||
<div class="pb-4 pt-1">
|
||||
<p class="text-primary-600 mt-2 text-2xl">{course.title}</p>
|
||||
<p class="ui:text-primary mt-2 text-2xl">{course.title}</p>
|
||||
<p class="mt-1 text-lg font-semibold text-black">{currentOrg.name}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,480 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import * as RadioGroup from '@cio/ui/base/radio-group';
|
||||
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
|
||||
import DragAndDrop from './DragAndDrop.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { Row, Grid, Column } from '$lib/components/Org/Settings/Layout';
|
||||
import SectionTitle from '$lib/components/Org/SectionTitle.svelte';
|
||||
import { UpgradeBanner } from '$lib/features/ui';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
|
||||
import { settings } from './store';
|
||||
import Copy from '@lucide/svelte/icons/copy';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import { lessons } from '../Lesson/store/lessons';
|
||||
import { course } from '$lib/components/Course/store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { isObject } from '$lib/utils/functions/isObject';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import DeleteModal from '$lib/components/Modal/DeleteModal.svelte';
|
||||
import { deleteCourse, updateCourse } from '$lib/utils/services/courses';
|
||||
import { copyToClipboard } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
import { currentOrg, currentOrgDomain, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
let isSaving = $state(false);
|
||||
let isLoading = $state(false);
|
||||
let isDeleting = $state(false);
|
||||
let errors: {
|
||||
title: string | undefined;
|
||||
description: string | undefined;
|
||||
} = $state({
|
||||
title: undefined,
|
||||
description: undefined
|
||||
});
|
||||
let avatar: string | undefined;
|
||||
let hasUnsavedChanges = $state(false);
|
||||
let openDeleteModal = $state(false);
|
||||
|
||||
function widgetControl() {
|
||||
$handleOpenWidget.open = !$handleOpenWidget.open;
|
||||
}
|
||||
|
||||
function getLessonOrder(id: string) {
|
||||
const index = $lessons.findIndex((lesson) => lesson.id === id);
|
||||
if (index < 9) {
|
||||
return '0' + (index + 1);
|
||||
} else {
|
||||
return index + 1;
|
||||
}
|
||||
}
|
||||
|
||||
const downloadCourse = async () => {
|
||||
alert('Coming soon');
|
||||
return;
|
||||
|
||||
isLoading = true;
|
||||
|
||||
try {
|
||||
const lessonsList = $lessons.map((lesson) => ({
|
||||
lessonTitle: lesson.title,
|
||||
lessonNumber: getLessonOrder(lesson.id),
|
||||
lessonNote: lesson.note,
|
||||
slideUrl: lesson.slide_url || '',
|
||||
video: lesson.videos || ''
|
||||
}));
|
||||
|
||||
const response = await fetch('/downloadCourse', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
courseTitle: $course.title,
|
||||
orgName: $currentOrg.name,
|
||||
orgTheme: $currentOrg.theme || '',
|
||||
lessons: lessonsList
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
const data = await response.blob();
|
||||
const file = new Blob([data], { type: 'application/pdf' });
|
||||
const fileURL = URL.createObjectURL(file);
|
||||
|
||||
let a = document.createElement('a');
|
||||
document.body.append(a);
|
||||
a.download = $course.title + ' - ' + 'Course ';
|
||||
a.href = fileURL;
|
||||
a.click();
|
||||
a.remove();
|
||||
|
||||
snackbar.success('snackbar.course_settings.success.download');
|
||||
} catch (error) {
|
||||
snackbar.error('snackbar.course_settings.error.not_right');
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
};
|
||||
|
||||
const deleteBannerImage = () => {
|
||||
$settings.logo = '';
|
||||
hasUnsavedChanges = true;
|
||||
};
|
||||
|
||||
async function handleDeleteCourse() {
|
||||
isDeleting = true;
|
||||
|
||||
try {
|
||||
await deleteCourse($course.id);
|
||||
goto($currentOrgPath + '/courses');
|
||||
} catch (error) {
|
||||
snackbar.error('snackbar.course_settings.error.went_wrong');
|
||||
}
|
||||
|
||||
isDeleting = false;
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!$settings.course_title) {
|
||||
errors.title = $t('snackbar.course_settings.error.title');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$settings.course_description) {
|
||||
errors.description = $t('snackbar.course_settings.error.description');
|
||||
return;
|
||||
}
|
||||
|
||||
isSaving = true;
|
||||
|
||||
try {
|
||||
const updatedCourse = {
|
||||
title: $settings.course_title,
|
||||
description: $settings.course_description,
|
||||
type: $settings.type,
|
||||
logo: $settings.logo,
|
||||
is_published: $settings.is_published,
|
||||
metadata: {
|
||||
...(isObject($course.metadata) ? $course.metadata : {}),
|
||||
lessonTabsOrder: $settings.tabs,
|
||||
grading: $settings.grading,
|
||||
lessonDownload: $settings.lesson_download,
|
||||
allowNewStudent: $settings.allow_new_students
|
||||
},
|
||||
slug: $course.slug
|
||||
};
|
||||
await updateCourse($course.id, avatar, updatedCourse);
|
||||
|
||||
$course = {
|
||||
...$course,
|
||||
...updatedCourse
|
||||
};
|
||||
|
||||
snackbar.success('snackbar.course_settings.success.saved');
|
||||
|
||||
hasUnsavedChanges = false;
|
||||
} catch (error) {
|
||||
snackbar.error();
|
||||
}
|
||||
|
||||
isSaving = false;
|
||||
};
|
||||
|
||||
const generateNewCourseLink = () => {
|
||||
$course.slug = generateSlug($course.title);
|
||||
hasUnsavedChanges = true;
|
||||
};
|
||||
|
||||
async function setDefault(course: Course) {
|
||||
if (!course || !Object.keys(course).length) return;
|
||||
|
||||
untrack(() => {
|
||||
settings.set({
|
||||
course_title: course.title,
|
||||
type: course.type,
|
||||
course_description: course.description,
|
||||
logo: course.logo || '',
|
||||
tabs: course.metadata.lessonTabsOrder || $settings.tabs,
|
||||
grading: !!course.metadata.grading,
|
||||
lesson_download: !!course.metadata.lessonDownload,
|
||||
is_published: !!course.is_published,
|
||||
allow_new_students: course.metadata.allowNewStudent
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setDefault($course);
|
||||
});
|
||||
|
||||
let courseLink = $derived(`${$currentOrgDomain}/course/${$course.slug}`);
|
||||
</script>
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
|
||||
<DeleteModal onDelete={handleDeleteCourse} bind:open={openDeleteModal} />
|
||||
|
||||
<Grid class="border-c rounded border-gray-200 dark:border-neutral-600">
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.cover_image')}</SectionTitle>
|
||||
<p>
|
||||
{$t('course.navItem.settings.optional_image')}
|
||||
</p>
|
||||
<span class="flex items-center justify-start">
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
label={$t('course.navItem.settings.replace')}
|
||||
className="mr-2"
|
||||
onClick={widgetControl}
|
||||
/>
|
||||
<PrimaryButton variant={VARIANTS.OUTLINED} label={$t('ai.reset')} onClick={deleteBannerImage} />
|
||||
</span>
|
||||
{#if $handleOpenWidget.open}
|
||||
<UploadWidget
|
||||
bind:imageURL={$settings.logo}
|
||||
onchange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</Column>
|
||||
|
||||
<Column sm={8} md={8} lg={6}>
|
||||
<div class="relative z-[20] w-fit">
|
||||
<img
|
||||
style="min-width:280px; min-height:200px"
|
||||
alt="About us"
|
||||
src={$settings.logo ? $settings.logo : '/images/classroomio-course-img-template.jpg'}
|
||||
class="relative mt-2 h-[200px] w-[280px] rounded-md md:mt-0"
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row id="share" class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.course_details')}</SectionTitle>
|
||||
</Column>
|
||||
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<TextField
|
||||
label={$t('course.navItem.settings.course_title')}
|
||||
placeholder="Write the course title here"
|
||||
className="w-full mb-5"
|
||||
isRequired
|
||||
bind:value={$settings.course_title}
|
||||
errorMessage={errors?.title}
|
||||
onInputChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
/>
|
||||
<TextArea
|
||||
label={$t('course.navItem.settings.course_description')}
|
||||
placeholder={$t('course.navItem.settings.placeholder')}
|
||||
className="w-full mb-5"
|
||||
isRequired
|
||||
bind:value={$settings.course_description}
|
||||
errorMessage={errors?.description}
|
||||
onChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
/>
|
||||
<div id="share">
|
||||
<p class="text-md mb-2 flex items-center gap-2">
|
||||
{$t('course.navItem.settings.link')}
|
||||
<IconButton contained={true} size="small" onClick={generateNewCourseLink}>
|
||||
<RotateCcwIcon size={16} />
|
||||
</IconButton>
|
||||
<span class="grow"></span>
|
||||
<IconButton contained={true} size="small" onClick={() => goto(courseLink)}>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
</IconButton>
|
||||
</p>
|
||||
<div class="flex items-center justify-between rounded-md border p-3">
|
||||
{#if $course.slug}
|
||||
<p class="text-sm">{courseLink}</p>
|
||||
|
||||
<IconButton
|
||||
contained={true}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
copyToClipboard(courseLink);
|
||||
}}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</IconButton>
|
||||
{:else}
|
||||
<p class="text-sm">Setup landing page to get course link</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
<!-- <Row class="flex lg:flex-row flex-col py-7 border-bottom-c">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.grading')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.reports')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<Toggle size="sm" bind:toggled={$settings.grading}>
|
||||
<span slot="labelA" style="color: gray">{$t('course.navItem.settings.disabled')}</span>
|
||||
<span slot="labelB" style="color: gray">{$t('course.navItem.settings.enabled')}</span>
|
||||
</Toggle>
|
||||
</Column>
|
||||
</Row> -->
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.order')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.drag')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<DragAndDrop
|
||||
on:change={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.lesson_download')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.available')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
{#if $isFreePlan}
|
||||
<UpgradeBanner>{$t('upgrade.download_lessons')}</UpgradeBanner>
|
||||
{:else}
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="lesson-download"
|
||||
bind:checked={$settings.lesson_download}
|
||||
onCheckedChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
/>
|
||||
<Label for="lesson-download" class="text-sm text-gray-500">
|
||||
{$settings.lesson_download ? $t('course.navItem.settings.enabled') : $t('course.navItem.settings.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.course_download')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.course_avail')}</p>
|
||||
</Column>
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
{#if $isFreePlan}
|
||||
<UpgradeBanner>{$t('upgrade.download_course')}</UpgradeBanner>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('course.navItem.settings.download')}
|
||||
onClick={downloadCourse}
|
||||
isDisabled={isLoading}
|
||||
{isLoading}
|
||||
/>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.type')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.course_type_desc')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<RadioGroup.Root
|
||||
value={$settings.type}
|
||||
onValueChange={(value) => {
|
||||
$settings.type = value as COURSE_TYPE;
|
||||
if (hasUnsavedChanges) return;
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
>
|
||||
<div class="mb-3 flex items-center space-x-2">
|
||||
<RadioGroup.Item value={COURSE_TYPE.LIVE_CLASS} id="live-class" />
|
||||
<Label for="live-class">{$t('course.navItem.settings.live_class')}</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value={COURSE_TYPE.SELF_PACED} id="self-paced" />
|
||||
<Label for="self-paced">{$t('course.navItem.settings.self_paced')}</Label>
|
||||
</div>
|
||||
</RadioGroup.Root>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.allow')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.access')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="allow-new-students"
|
||||
bind:checked={$settings.allow_new_students}
|
||||
onCheckedChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
/>
|
||||
<Label for="allow-new-students" class="text-sm text-gray-500">
|
||||
{$settings.allow_new_students
|
||||
? $t('course.navItem.settings.enabled')
|
||||
: $t('course.navItem.settings.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<!-- Publish Course -->
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.publish')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.determines')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
id="is-published"
|
||||
bind:checked={$settings.is_published}
|
||||
onCheckedChange={(checked) => {
|
||||
hasUnsavedChanges = true;
|
||||
$settings.allow_new_students = checked;
|
||||
if (!$course.slug) {
|
||||
generateNewCourseLink();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label for="is-published" class="text-sm text-gray-500">
|
||||
{$settings.is_published ? $t('course.navItem.settings.published') : $t('course.navItem.settings.unpublished')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row id="delete" class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<SectionTitle>{$t('course.navItem.settings.delete')}</SectionTitle>
|
||||
<p>{$t('course.navItem.settings.delete_text')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.CONTAINED_DANGER}
|
||||
label={$t('course.navItem.settings.delete')}
|
||||
onClick={() => (openDeleteModal = true)}
|
||||
isLoading={isDeleting}
|
||||
isDisabled={isDeleting}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<div class="flex w-full items-center justify-end p-5">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.settings.save')}
|
||||
isLoading={isSaving}
|
||||
isDisabled={isSaving}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
@@ -2,16 +2,12 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Spinner } from '@cio/ui/base/spinner';
|
||||
import Navigation from '../Course/components/Navigation/index.svelte';
|
||||
import Backdrop from '$lib/components/Backdrop/index.svelte';
|
||||
import { Backdrop, Confetti } from '$features/ui';
|
||||
import { course, group, courseStore } from '../Course/store';
|
||||
import Confetti from '../Confetti/index.svelte';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import type { GroupPerson } from '$lib/utils/types';
|
||||
|
||||
@@ -28,10 +24,8 @@
|
||||
|
||||
let {
|
||||
courseId,
|
||||
path = '',
|
||||
isExercisePage = false,
|
||||
isFetching = $bindable(false),
|
||||
containerClass = '',
|
||||
onFetchingChange = (_v: boolean) => {},
|
||||
children,
|
||||
renderOnlyChildren = false
|
||||
@@ -69,50 +63,38 @@
|
||||
</Backdrop>
|
||||
{/if}
|
||||
|
||||
<Modal open={!isPermitted} width="w-96" modalHeading={$t('course.not_permitted.header')}>
|
||||
<div>
|
||||
<p class="text-md text-center dark:text-white">
|
||||
{$t('course.not_permitted.body')}
|
||||
</p>
|
||||
<Dialog.Root open={!isPermitted}>
|
||||
<Dialog.Content class="w-96">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{$t('course.not_permitted.header')}</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div>
|
||||
<p class="text-md text-center dark:text-white">
|
||||
{$t('course.not_permitted.body')}
|
||||
</p>
|
||||
|
||||
<div class="mt-5 flex justify-center">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
label={$t('course.not_permitted.button')}
|
||||
onClick={() => {
|
||||
goto('/org/*');
|
||||
}}
|
||||
/>
|
||||
<div class="mt-5 flex justify-center">
|
||||
<Button
|
||||
onclick={() => {
|
||||
goto('/org/*');
|
||||
}}
|
||||
>
|
||||
{$t('course.not_permitted.button')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
{#if renderOnlyChildren}
|
||||
{@render children?.()}
|
||||
{:else}
|
||||
<div class="root">
|
||||
<Navigation {path} isStudent={$globalStore.isStudent} />
|
||||
<div class="rightBar {containerClass}" class:isMobile={$isMobile}>
|
||||
{#if isExercisePage}
|
||||
<Confetti />
|
||||
{/if}
|
||||
{#if isExercisePage}
|
||||
<Confetti />
|
||||
{/if}
|
||||
|
||||
<!-- Show only if permitted -->
|
||||
{#if isPermitted}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Show only if permitted -->
|
||||
{#if isPermitted}
|
||||
{@render children?.()}
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.root {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rightBar {
|
||||
flex-grow: 1;
|
||||
width: calc(100% - 360px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
options?: any;
|
||||
classNames?: string;
|
||||
isIcon?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { options = [], classNames = 'relative', isIcon = false, children }: Props = $props();
|
||||
|
||||
let open = $state(false);
|
||||
|
||||
const notIconClass = 'border rounded-lg border-grey p-3 focus:outline-none focus:border-gray-400 focus:bg-gray-200';
|
||||
|
||||
function isOptionLast(index) {
|
||||
return index === options.length - 1;
|
||||
}
|
||||
|
||||
function onClick(option) {
|
||||
return () => {
|
||||
open = !open;
|
||||
if (option && option.onClick) {
|
||||
option.onClick();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
// In case an option was clicked, let's wait for the on:click of that button before we close
|
||||
setTimeout(() => (open = false), 100);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="{classNames} z-10 {!options.length && 'hidden'}">
|
||||
<button
|
||||
class="flex flex-row items-center {!isIcon && notIconClass}"
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
open = !open;
|
||||
}}
|
||||
onblur={onBlur}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
|
||||
{#if open}
|
||||
<div class="dropdown border-grey absolute mt-2 whitespace-nowrap rounded-lg border bg-white py-3 dark:bg-black">
|
||||
{#each options as option, index}
|
||||
<button
|
||||
class="hover:bg-primary-100 block px-4 py-3 text-left {!isOptionLast(index) &&
|
||||
'border-b'} w-full dark:text-white dark:hover:text-black"
|
||||
onclick={onClick(option)}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.dropdown {
|
||||
box-shadow:
|
||||
0px 5px 5px -3px rgb(0 0 0 / 20%),
|
||||
0px 8px 10px 1px rgb(0 0 0 / 14%),
|
||||
0px 3px 14px 2px rgb(0 0 0 / 12%);
|
||||
right: 0px;
|
||||
top: 20px;
|
||||
left: unset;
|
||||
}
|
||||
</style>
|
||||
@@ -1,5 +0,0 @@
|
||||
<script lang="ts">
|
||||
let { message } = $props();
|
||||
</script>
|
||||
|
||||
<p class="text-sm text-red-500">{message}</p>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||
import LinkIcon from '@lucide/svelte/icons/link';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
title?: string;
|
||||
titleClass?: string;
|
||||
disableContainerPadding?: boolean;
|
||||
rootClass?: string;
|
||||
supportsLink?: boolean;
|
||||
isExpanded?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
id = '',
|
||||
title = '',
|
||||
titleClass = '',
|
||||
disableContainerPadding = false,
|
||||
rootClass = '',
|
||||
supportsLink = false,
|
||||
isExpanded = $bindable(true),
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
function handleClick(e) {
|
||||
e.stopPropagation();
|
||||
|
||||
isExpanded = !isExpanded;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div {id} class="w-full bg-white dark:bg-black {rootClass}">
|
||||
<button
|
||||
class="relative flex w-full items-center justify-between {!disableContainerPadding && 'p-5'}"
|
||||
tabindex="0"
|
||||
onclick={handleClick}
|
||||
>
|
||||
<p class="flex items-center dark:text-white {titleClass}">
|
||||
{title}
|
||||
{#if supportsLink}
|
||||
<a class="ml-2" href="{page.url.pathname}#{id}" onclick={(e) => e.stopPropagation()}>
|
||||
<LinkIcon size={16} />
|
||||
</a>
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
{#if isExpanded}
|
||||
<ChevronUpIcon size={16} />
|
||||
{:else}
|
||||
<ChevronDownIcon size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
{#if isExpanded}
|
||||
<div class="flex flex-col p-5">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,17 +0,0 @@
|
||||
<script>
|
||||
</script>
|
||||
|
||||
<!-- markup (zero or more items) goes here -->
|
||||
<div class="my-1">
|
||||
<hr />
|
||||
<div class="flex w-full justify-center">
|
||||
<span class="py-3 text-sm text-gray-700"
|
||||
>Created with love by
|
||||
<a class="text-primary-700 underline" href="https://twitter.com/rotimi_best" target="_blank">Rotimi Best</a></span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* your styles go here */
|
||||
</style>
|
||||
@@ -1,83 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label?: string;
|
||||
value: string | number | null;
|
||||
placeholder?: string;
|
||||
inputClassName?: string;
|
||||
labelClassName?: string;
|
||||
isDisabled?: boolean;
|
||||
isRequired?: boolean;
|
||||
bgColor?: string;
|
||||
errorMessage?: string;
|
||||
helperMessage?: string;
|
||||
onInput?: any;
|
||||
onKeyDown?: any;
|
||||
onInputChange?: any;
|
||||
onChange?: any; // This is to know if element is 'dirty'
|
||||
}
|
||||
|
||||
let {
|
||||
label = '',
|
||||
value,
|
||||
placeholder = '',
|
||||
inputClassName = '',
|
||||
labelClassName = 'font-light',
|
||||
isDisabled = false,
|
||||
isRequired = false,
|
||||
bgColor = 'bg-gray-100 focus:bg-primary-50 dark:bg-neutral-800 dark:text-white',
|
||||
errorMessage = '',
|
||||
helperMessage = '',
|
||||
onInput = (e) => {},
|
||||
onKeyDown = (e) => {},
|
||||
onInputChange = (e) => {},
|
||||
onChange = () => {}
|
||||
}: Props = $props();
|
||||
|
||||
let focusClass = $state('');
|
||||
</script>
|
||||
|
||||
{#if label}
|
||||
<p class="m-0 text-left text-sm dark:text-white {labelClassName}">
|
||||
{label}
|
||||
|
||||
{#if isRequired}
|
||||
<span class="text-red-700">*</span>
|
||||
{/if}
|
||||
</p>
|
||||
{/if}
|
||||
<span
|
||||
class="form-input custom-placeholder form-input mt-2 block min-h-[50px] w-full resize overflow-hidden rounded-t-md border border-b-2 border-l-0 border-r-0 border-t-0 border-gray-200 p-3 pt-3.5 leading-5 outline-none focus:border-b-2 focus:border-l-0 focus:border-r-0 focus:border-t-0 dark:border-neutral-600 dark:bg-white dark:text-black {inputClassName} {focusClass} {isDisabled &&
|
||||
'opacity-50 hover:cursor-not-allowed'} mt-1 block w-full p-3 dark:text-black {bgColor} {errorMessage
|
||||
? 'border-red-600'
|
||||
: ''}"
|
||||
role="textbox"
|
||||
tabindex={0}
|
||||
contenteditable
|
||||
oninput={onInput}
|
||||
onkeydown={onKeyDown}
|
||||
onchange={onInputChange}
|
||||
onblur={(e) => {
|
||||
if (focusClass.includes('border-primary-600')) {
|
||||
focusClass = focusClass.replace('border-primary-600', '');
|
||||
}
|
||||
onChange(e);
|
||||
}}
|
||||
onfocus={() => (focusClass += ' border-primary-600')}
|
||||
style="--placeholder: '{placeholder}'">{value}</span
|
||||
>
|
||||
{#if errorMessage}
|
||||
<p class="text-sm text-red-500">{errorMessage}</p>
|
||||
{:else if helperMessage}
|
||||
<p class="text-sm dark:text-white">
|
||||
{helperMessage}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.custom-placeholder:empty::before {
|
||||
content: var(--placeholder, 'Default Placeholder');
|
||||
}
|
||||
.form-input {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
|
||||
interface Props {
|
||||
label?: string | null;
|
||||
value?: string;
|
||||
checked?: boolean;
|
||||
name?: string;
|
||||
isEditable?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onChange?: any;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
label = $bindable(''),
|
||||
value = '',
|
||||
checked = $bindable(false),
|
||||
name = '',
|
||||
isEditable = false,
|
||||
disabled = false,
|
||||
className = '',
|
||||
onChange = () => {},
|
||||
children
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="{className} group inline-flex w-full items-center {disabled ? 'cursor-not-allowed' : 'cursor-pointer'}">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 focus:ring-offset-0"
|
||||
{name}
|
||||
{value}
|
||||
disabled={disabled || isEditable}
|
||||
bind:checked
|
||||
/>
|
||||
{#if isEditable}
|
||||
<div class="w-2/4">
|
||||
<TextField bind:value={label} placeholder="Your option" className="ml-1" type="text" {onChange} />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="ml-2 dark:text-white">{label}</span>
|
||||
{/if}
|
||||
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
className?: string;
|
||||
onChange?: any; // This is to know if element is 'dirty'
|
||||
}
|
||||
|
||||
let {
|
||||
label = '',
|
||||
placeholder = '',
|
||||
value = $bindable(''),
|
||||
className = '',
|
||||
onChange = (e?: Event) => {}
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<label class="block {className}">
|
||||
<span class="text-gray-700 dark:bg-neutral-800 dark:text-white">{label}</span>
|
||||
<input
|
||||
bind:value
|
||||
{placeholder}
|
||||
type="date"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-800 dark:text-white"
|
||||
onblur={onChange}
|
||||
/>
|
||||
</label>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string | Date;
|
||||
className?: string;
|
||||
onInput?: any;
|
||||
isRequired?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
label = '',
|
||||
placeholder = '',
|
||||
value = $bindable(''),
|
||||
className = '',
|
||||
onInput = () => {},
|
||||
isRequired = false
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<label class="block {className}">
|
||||
<span class="text-gray-700 dark:text-white"
|
||||
>{label}
|
||||
{#if isRequired}
|
||||
<span class="text-red-700">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
<input
|
||||
bind:value
|
||||
oninput={onInput}
|
||||
{placeholder}
|
||||
type="datetime-local"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 dark:bg-neutral-800 dark:text-white"
|
||||
/>
|
||||
</label>
|
||||
@@ -1,40 +0,0 @@
|
||||
<script lang="ts">
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
|
||||
interface Props {
|
||||
label?: string | null;
|
||||
value?: string;
|
||||
checked?: boolean;
|
||||
name?: string;
|
||||
isEditable?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
onChange?: any; // This is to know if element is 'dirty'
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
label = $bindable(''),
|
||||
value = '',
|
||||
checked = false,
|
||||
name = '',
|
||||
isEditable = false,
|
||||
disabled = false,
|
||||
className = '',
|
||||
onChange = () => {},
|
||||
children
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="{className} group inline-flex w-full items-center {disabled ? 'cursor-not-allowed' : 'cursor-pointer'}">
|
||||
<input class="form-radio" type="radio" {checked} {name} {value} disabled={disabled || isEditable} />
|
||||
{#if isEditable}
|
||||
<div class="w-2/4">
|
||||
<TextField bind:value={label} placeholder="Your option" className="ml-1" type="text" {onChange} />
|
||||
</div>
|
||||
{:else}
|
||||
<span class="ml-2 dark:text-white">{label}</span>
|
||||
{/if}
|
||||
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,61 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
label?: string;
|
||||
value: any;
|
||||
options?: any;
|
||||
labelKey?: string;
|
||||
isRequired?: boolean;
|
||||
className?: string;
|
||||
selectClassName?: string;
|
||||
onChange?: any; // This is to know if element is 'dirty'
|
||||
}
|
||||
|
||||
let {
|
||||
label = '',
|
||||
value = $bindable(),
|
||||
options = [],
|
||||
labelKey = 'label',
|
||||
isRequired = false,
|
||||
className = '',
|
||||
selectClassName = '',
|
||||
onChange = () => {}
|
||||
}: Props = $props();
|
||||
|
||||
// onMount(() => {
|
||||
// value = options[0];
|
||||
// });
|
||||
</script>
|
||||
|
||||
<label class="block {className}">
|
||||
{#if label}
|
||||
<span class="mr-2 text-gray-700 dark:text-white">{label}</span>
|
||||
{/if}
|
||||
<select
|
||||
bind:value
|
||||
class="form-select mt-1 block w-auto rounded-md border-2 px-2 py-1.5 dark:bg-neutral-700 dark:text-white {selectClassName}"
|
||||
required={isRequired}
|
||||
onblur={() => {}}
|
||||
onchange={onChange}
|
||||
class:customColor={!!selectClassName}
|
||||
>
|
||||
{#each options as option}
|
||||
<option class="bg-white text-black" value={option}>
|
||||
{option[labelKey]}
|
||||
</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<style>
|
||||
select.customColor {
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' class='text-white' viewBox='0 0 20 20'%3e%3cpath stroke='white' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
select.customColor option {
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
}
|
||||
</style>
|
||||
@@ -1,105 +0,0 @@
|
||||
<script lang="ts">
|
||||
// import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte';
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: any;
|
||||
value?: string;
|
||||
rows?: number;
|
||||
// export let maxRows = 3;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
bgColor?: string;
|
||||
helperMessage?: string;
|
||||
errorMessage?: string;
|
||||
isRequired?: boolean;
|
||||
onChange?: any; // This is to know if element is 'dirty'
|
||||
ref?: HTMLTextAreaElement | null;
|
||||
isAIEnabled?: boolean;
|
||||
initAIPrompt?: string;
|
||||
aiAlignPopover?: PopoverPrimitive.ContentProps['align'];
|
||||
iconbutton?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
label = '',
|
||||
disabled = false,
|
||||
placeholder = $t('course.navItem.lessons.exercises.all_exercises.write_your_answer'),
|
||||
value = $bindable(),
|
||||
rows = 3,
|
||||
className = '',
|
||||
labelClassName = '',
|
||||
bgColor = 'bg-gray-50 focus:bg-primary-50 dark:bg-neutral-700 dark:text-white',
|
||||
helperMessage = '',
|
||||
errorMessage = '',
|
||||
isRequired = false,
|
||||
onChange = () => {},
|
||||
ref = $bindable(null),
|
||||
// isAIEnabled = false,
|
||||
initAIPrompt = $bindable(''),
|
||||
// aiAlignPopover = 'left',
|
||||
iconbutton
|
||||
}: Props = $props();
|
||||
|
||||
// $: maxHeight = maxRows ? `${1 + maxRows * 1.2}em` : `auto`;
|
||||
</script>
|
||||
|
||||
<label for="text-field" class="relative block w-full {className}">
|
||||
{#if label}
|
||||
<p class="flex w-full items-center justify-between text-left dark:text-white {labelClassName}">
|
||||
<span>
|
||||
{label}
|
||||
{#if isRequired}
|
||||
<span class="text-red-700">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
|
||||
<!-- {#if isAIEnabled}
|
||||
<CustomPromptBtn
|
||||
bind:defaultPrompt={initAIPrompt}
|
||||
alignPopover={aiAlignPopover}
|
||||
handleInsert={(v) => {
|
||||
value = v;
|
||||
}}
|
||||
/>
|
||||
{/if} -->
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<!-- <pre
|
||||
aria-hidden="true"
|
||||
style="min-height: {minHeight}; max-height: {maxHeight}">
|
||||
{value + '\n'}
|
||||
</pre> -->
|
||||
|
||||
<textarea
|
||||
{disabled}
|
||||
{rows}
|
||||
{placeholder}
|
||||
bind:value
|
||||
bind:this={ref}
|
||||
required={isRequired}
|
||||
class="form-textarea focus:border-primary-600 mt-1 block w-full rounded-t-md border-b-2 border-l-0 border-r-0 border-t-0 border-gray-200 p-3 focus:border-b-2 focus:border-l-0 focus:border-r-0 focus:border-t-0 dark:border-neutral-600
|
||||
{bgColor} {errorMessage ? 'border-red-500' : 'border-gray-300'}"
|
||||
onchange={onChange}
|
||||
></textarea>
|
||||
{#if !!errorMessage}
|
||||
<p class="text-sm text-red-500">
|
||||
{errorMessage}
|
||||
</p>
|
||||
{:else if helperMessage}
|
||||
<p class="text-sm text-gray-500 dark:text-white">
|
||||
{helperMessage}
|
||||
</p>
|
||||
{/if}
|
||||
{@render iconbutton?.()}
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.form-textarea {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,156 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import PasswordEye from '../Icons/PasswordEye.svelte';
|
||||
|
||||
interface Props {
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
value?: string | number | null;
|
||||
name?: string;
|
||||
onKeyDown?: any;
|
||||
className?: string;
|
||||
inputClassName?: string;
|
||||
labelClassName?: string;
|
||||
bgColor?: string;
|
||||
type?: string;
|
||||
isPassword?: boolean;
|
||||
autoFocus?: boolean;
|
||||
isRequired?: boolean;
|
||||
isDisabled?: boolean;
|
||||
min?: string | number | null | undefined;
|
||||
max?: string | number | null | undefined;
|
||||
errorMessage?: string;
|
||||
helperMessage?: string;
|
||||
autoComplete?: boolean;
|
||||
onChange?: any; // This is to know if element is 'dirty'
|
||||
onInputChange?: any;
|
||||
}
|
||||
|
||||
let {
|
||||
label = '',
|
||||
placeholder = '',
|
||||
value = $bindable(),
|
||||
name = '',
|
||||
onKeyDown = (_e) => {},
|
||||
className = '',
|
||||
inputClassName = '',
|
||||
labelClassName = 'font-light',
|
||||
bgColor = 'bg-gray-100 focus:bg-primary-50 dark:bg-neutral-700 dark:text-white',
|
||||
type = $bindable('text'),
|
||||
isPassword = $bindable(false),
|
||||
autoFocus = false,
|
||||
isRequired = false,
|
||||
isDisabled = false,
|
||||
min = undefined,
|
||||
max = undefined,
|
||||
errorMessage = '',
|
||||
helperMessage = '',
|
||||
autoComplete = true,
|
||||
onChange = () => {},
|
||||
onInputChange = () => {}
|
||||
}: Props = $props();
|
||||
|
||||
let ref: HTMLInputElement | undefined = $state(undefined);
|
||||
let fieldNode: HTMLInputElement | undefined = undefined;
|
||||
let focusClass = $state('');
|
||||
|
||||
function typeAction(node: HTMLInputElement | undefined) {
|
||||
if (!node) return;
|
||||
|
||||
node.type = type || 'text';
|
||||
|
||||
if (isRequired) {
|
||||
node.required = true;
|
||||
}
|
||||
|
||||
fieldNode = node;
|
||||
}
|
||||
|
||||
function handlePasswordEye() {
|
||||
type = type === 'password' ? 'text' : 'password';
|
||||
typeAction(fieldNode);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (autoFocus && ref) {
|
||||
ref.focus();
|
||||
}
|
||||
|
||||
if (type === 'password') {
|
||||
isPassword = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<label class="relative block {className}">
|
||||
{#if label}
|
||||
<label for="text-field" class="m-0 text-left text-sm dark:text-white {labelClassName}">
|
||||
{label}
|
||||
|
||||
{#if isRequired}
|
||||
<span class="text-red-700">*</span>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
<input
|
||||
use:typeAction
|
||||
class="form-input rounded-t-md border-b-2 border-l-0 border-r-0 border-t-0 border-gray-200 focus:border-b-2 focus:border-l-0 focus:border-r-0 focus:border-t-0 dark:border-neutral-600 {inputClassName} {focusClass} {isDisabled &&
|
||||
'opacity-50 hover:cursor-not-allowed'} mt-1 block w-full p-3 dark:text-black {bgColor} {errorMessage
|
||||
? 'border-red-600'
|
||||
: ''}"
|
||||
onkeydown={onKeyDown}
|
||||
onchange={onInputChange}
|
||||
{placeholder}
|
||||
bind:value
|
||||
bind:this={ref}
|
||||
required={isRequired}
|
||||
disabled={isDisabled}
|
||||
autocomplete={autoComplete ? 'on' : 'off'}
|
||||
onblur={(e) => {
|
||||
if (focusClass.includes('border-primary-600')) {
|
||||
focusClass = focusClass.replace('border-primary-600', '');
|
||||
}
|
||||
onChange(e);
|
||||
}}
|
||||
onfocus={() => (focusClass += ' border-primary-600')}
|
||||
{name}
|
||||
{min}
|
||||
{max}
|
||||
/>
|
||||
{#if isPassword}
|
||||
<span class="password-eye">
|
||||
<IconButton value="write-code" onClick={handlePasswordEye}>
|
||||
<PasswordEye isClosed={type === 'password' ? true : false} />
|
||||
</IconButton>
|
||||
</span>
|
||||
{/if}
|
||||
{#if errorMessage}
|
||||
<p class="text-sm text-red-500">{errorMessage}</p>
|
||||
{:else if helperMessage}
|
||||
<p class="text-sm opacity-70 dark:text-white">
|
||||
{helperMessage}
|
||||
</p>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<style>
|
||||
.form-input {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
.password-eye {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 6px;
|
||||
}
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox */
|
||||
/* input[type='number'] {
|
||||
-moz-appearance: textfield;
|
||||
} */
|
||||
</style>
|
||||
@@ -1,9 +0,0 @@
|
||||
<script lang="ts">
|
||||
let { tags = [] } = $props();
|
||||
</script>
|
||||
|
||||
<div class="break-all">
|
||||
{#each tags as tag}
|
||||
<a class="text-primary-700 dark:text-primary-500 mr-5 text-sm" href="?filter={tag}">#{tag.toLowerCase()}</a>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -1,20 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { children }: Props = $props();
|
||||
let hovering;
|
||||
|
||||
function enter() {
|
||||
hovering = true;
|
||||
}
|
||||
|
||||
function leave() {
|
||||
hovering = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div onmouseenter={enter} onmouseleave={leave}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,100 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ToolTip from '../ToolTip/index.svelte';
|
||||
|
||||
export interface Props {
|
||||
onClick?: any;
|
||||
stopPropagation?: boolean;
|
||||
disabled?: boolean;
|
||||
selected?: boolean;
|
||||
buttonClassName?: string;
|
||||
contained?: boolean;
|
||||
bordered?: boolean;
|
||||
value?: string;
|
||||
type?: 'button' | 'submit' | 'reset' | null | undefined;
|
||||
size?: 'small' | 'large';
|
||||
color?: string;
|
||||
toolTipProps?: {
|
||||
title: string;
|
||||
hotkeys: string[];
|
||||
direction?: string;
|
||||
};
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
onClick = (_v: string) => {},
|
||||
stopPropagation = false,
|
||||
disabled = false,
|
||||
selected = false,
|
||||
buttonClassName = '',
|
||||
contained = false,
|
||||
bordered = false,
|
||||
value = '',
|
||||
type = 'button',
|
||||
size = 'large',
|
||||
color = '',
|
||||
toolTipProps = {
|
||||
title: '',
|
||||
hotkeys: [],
|
||||
direction: ''
|
||||
},
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
function handleClick(e: Event) {
|
||||
if (stopPropagation) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
onClick(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<ToolTip title={toolTipProps.title} hotkeys={disabled ? [] : toolTipProps.hotkeys} direction={toolTipProps.direction}>
|
||||
<button
|
||||
class="root {color} {selected && 'active'} {disabled && 'disabled'} {size} {contained &&
|
||||
'contained dark:bg-neutral-700'} {bordered && 'border'} {buttonClassName}"
|
||||
{disabled}
|
||||
{type}
|
||||
onclick={handleClick}
|
||||
>
|
||||
{@render children?.()}
|
||||
</button>
|
||||
</ToolTip>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
flex: 0 0 auto;
|
||||
padding: 12px;
|
||||
font-size: 1.5rem;
|
||||
text-align: center;
|
||||
transition: background-color 150ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
outline: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
-webkit-appearance: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
width: fit-content;
|
||||
|
||||
&.disabled {
|
||||
cursor: not-allowed;
|
||||
color: rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.root.small {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.contained {
|
||||
background-color: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as IconButton } from './index.svelte';
|
||||
@@ -1,27 +0,0 @@
|
||||
<svg width="121" height="120" viewBox="0 0 121 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="79.7922" cy="32.4026" r="12.5649" stroke="#0F62FE" stroke-width="1.67532" />
|
||||
<path
|
||||
d="M69.7402 40.7794L74.2444 37.7766C77.6914 35.4786 82.0846 35.1362 85.8461 36.8722L90.6818 39.1041"
|
||||
stroke="#0F62FE"
|
||||
stroke-width="1.67532"
|
||||
/>
|
||||
<circle cx="79.4199" cy="28.68" r="3.81602" stroke="#0F62FE" stroke-width="1.67532" />
|
||||
<circle cx="106.597" cy="62.5588" r="12.5649" stroke="#0F62FE" stroke-width="1.67532" />
|
||||
<path
|
||||
d="M96.5455 70.9357L101.05 67.9329C104.497 65.6349 108.89 65.2924 112.651 67.0285L117.487 69.2603"
|
||||
stroke="#0F62FE"
|
||||
stroke-width="1.67532"
|
||||
/>
|
||||
<circle cx="106.225" cy="58.8363" r="3.81602" stroke="#0F62FE" stroke-width="1.67532" />
|
||||
<circle cx="71.4156" cy="79.3128" r="12.5649" stroke="#0F62FE" stroke-width="1.67532" />
|
||||
<path
|
||||
d="M61.3636 87.6896L65.8678 84.6868C69.3148 82.3888 73.708 82.0463 77.4695 83.7824L82.3052 86.0143"
|
||||
stroke="#0F62FE"
|
||||
stroke-width="1.67532"
|
||||
/>
|
||||
<circle cx="71.0433" cy="75.5902" r="3.81602" stroke="#0F62FE" stroke-width="1.67532" />
|
||||
<rect x="5.5" y="49.5" width="24.5" height="5" rx="2.5" stroke="#1339A6" />
|
||||
<rect x="5.5" y="61.5" width="44" height="5" rx="2.5" stroke="#1339A6" />
|
||||
<rect x="5.5" y="73.5" width="44" height="5" rx="2.5" stroke="#1339A6" />
|
||||
<rect x="5.5" y="85.5" width="44" height="5" rx="2.5" stroke="#1339A6" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.4 KiB |
@@ -1,16 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
color?: string;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}
|
||||
|
||||
let { color = 'currentColor', width = '18', height = '18' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg {width} {height} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="dark">
|
||||
<path
|
||||
d="M18 22.5H6C5.60232 22.4995 5.22105 22.3414 4.93984 22.0602C4.65864 21.7789 4.50046 21.3977 4.5 21V3C4.5004 2.6023 4.65856 2.221 4.93978 1.93978C5.221 1.65856 5.6023 1.5004 6 1.5H18C18.3977 1.50046 18.779 1.65864 19.0602 1.93984C19.3414 2.22105 19.4995 2.60232 19.5 3V15.4636L15.75 13.5886L12 15.4636V3H6V21H18V18H19.5V21C19.4995 21.3977 19.3413 21.7789 19.0601 22.0601C18.7789 22.3413 18.3977 22.4995 18 22.5ZM15.75 11.9114L18 13.0364V3H13.5V13.0364L15.75 11.9114Z"
|
||||
fill={color}
|
||||
/>
|
||||
</svg>
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { color = 'currentColor' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="dark">
|
||||
<path
|
||||
d="M16.9999 2H8.99991C8.46948 2 7.96077 2.21071 7.58569 2.58579C7.21062 2.96086 6.99991 3.46957 6.99991 4V20C6.99991 20.5304 7.21062 21.0391 7.58569 21.4142C7.96077 21.7893 8.46948 22 8.99991 22H20.9999C21.5303 22 22.039 21.7893 22.4141 21.4142C22.7892 21.0391 22.9999 20.5304 22.9999 20V8L16.9999 2Z"
|
||||
stroke={color}
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path d="M16.9999 2V8H22.9999" stroke={color} stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M18.9999 13H10.9999" stroke={color} stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M18.9999 17H10.9999" stroke={color} stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M12.9999 9H11.9999H10.9999" stroke={color} stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
isClosed?: boolean;
|
||||
}
|
||||
|
||||
let { isClosed = true }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if isClosed}
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="dark">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.6 11.3L3.3 10.6C2.6 9.8 1.9 9 1.5 8C2.5 5.5 5.3 3.5 8 3.5C8.7 3.5 9.4 3.6 10 3.9L10.8 3.1C9.9 2.7 9 2.5 8 2.5C4.7 2.6 1.7 4.7 0.5 7.8V8.1C1 9.3 1.7 10.4 2.6 11.3ZM12.6 4.1C13.9 5 14.9 6.3 15.5 7.8V8.2C14.3 11.3 11.3 13.4 8 13.5C6.7 13.4 5.4 13.1 4.3 12.4L1.7 15L1 14.3L14.3 1L15 1.7L12.6 4.1ZM7.8 6.1C6.9 6.1 6.1 6.9 6 7.9L5.1 8.8C5 8.3 5 7.7 5.1 7.2C5.5 5.6 7.2 4.7 8.7 5.2L7.8 6.1ZM10 8C10 9.1 9.1 10 8 10C7.7 10 7.3 9.9 7 9.7L9.7 7C9.9 7.3 10 7.6 10 8ZM5 11.7C5.9 12.2 7 12.5 8 12.5C10.6 12.5 13.4 10.5 14.5 8C13.9 6.7 13 5.6 11.9 4.8L10.5 6.2C11.2 7.2 11.2 8.6 10.5 9.6C9.5 11 7.7 11.3 6.3 10.4L5 11.7Z"
|
||||
fill="#ADADAD"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" class="dark">
|
||||
<path
|
||||
d="M0.666748 8.00008C0.666748 8.00008 3.33341 2.66675 8.00008 2.66675C12.6667 2.66675 15.3334 8.00008 15.3334 8.00008C15.3334 8.00008 12.6667 13.3334 8.00008 13.3334C3.33341 13.3334 0.666748 8.00008 0.666748 8.00008Z"
|
||||
stroke="#ADADAD"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M8 10C9.10457 10 10 9.10457 10 8C10 6.89543 9.10457 6 8 6C6.89543 6 6 6.89543 6 8C6 9.10457 6.89543 10 8 10Z"
|
||||
stroke="#ADADAD"
|
||||
stroke-width="1.33333"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
@@ -1,24 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
color?: string;
|
||||
}
|
||||
|
||||
let { color = 'currentColor' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="dark">
|
||||
<g clip-path="url(#clip0_3647_18153)">
|
||||
<path
|
||||
d="M24 15C24 15.5304 23.7893 16.0391 23.4142 16.4142C23.0391 16.7893 22.5304 17 22 17H10L6 21V5C6 4.46957 6.21071 3.96086 6.58579 3.58579C6.96086 3.21071 7.46957 3 8 3H22C22.5304 3 23.0391 3.21071 23.4142 3.58579C23.7893 3.96086 24 4.46957 24 5V15Z"
|
||||
stroke={color}
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_3647_18153">
|
||||
<rect width="24" height="24" fill={color} transform="translate(0.258789)" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
@@ -1,6 +0,0 @@
|
||||
<svg width="61" height="61" viewBox="0 0 61 61" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M30.5 0C13.6564 0 0 13.6564 0 30.5C0 47.3436 13.6564 61 30.5 61C47.3436 61 61 47.3436 61 30.5C61 13.6564 47.3436 0 30.5 0ZM44.6291 22.0629L30.0196 44.3927C30.0158 44.3966 30.0082 44.4004 30.0082 44.4042C29.9319 44.5186 29.89 44.6482 29.7947 44.7549C29.6612 44.8998 29.4897 44.9761 29.3372 45.0866C29.2991 45.1133 29.2609 45.1438 29.219 45.1705C28.9826 45.3192 28.7386 45.4183 28.4718 45.4831C28.3841 45.506 28.304 45.5289 28.2125 45.5441C28.0002 45.5733 27.785 45.5759 27.572 45.5518C27.4216 45.5422 27.2723 45.5193 27.1259 45.4831C26.9749 45.4376 26.8282 45.3789 26.6875 45.3078C26.5655 45.2506 26.4359 45.2353 26.3215 45.1629C26.2376 45.1095 26.1919 45.0256 26.1194 44.9608C26.0889 44.9341 26.0508 44.9265 26.0203 44.8998L17.5146 37.0346C17.0105 36.5505 16.7163 35.8879 16.6955 35.1893C16.6746 34.4906 16.9287 33.8117 17.403 33.2984C17.8774 32.785 18.5343 32.4783 19.2324 32.4441C19.9305 32.4099 20.6142 32.6509 21.1365 33.1154L27.2937 38.8113L40.1609 19.1426C40.5481 18.55 41.1549 18.1356 41.8477 17.9905C42.5406 17.8453 43.2627 17.9814 43.8552 18.3686C44.4477 18.7559 44.8621 19.3627 45.0073 20.0555C45.1524 20.7483 45.0164 21.4704 44.6291 22.0629Z"
|
||||
fill="#0c891b"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,117 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import type { Component } from 'svelte';
|
||||
import * as Sidebar from '@cio/ui/base/sidebar';
|
||||
import UsersIcon from '@lucide/svelte/icons/users';
|
||||
import MapPlusIcon from '@lucide/svelte/icons/map-plus';
|
||||
import ListTodoIcon from '@lucide/svelte/icons/list-todo';
|
||||
import LibraryBigIcon from '@lucide/svelte/icons/library-big';
|
||||
import LayoutDashboardIcon from '@lucide/svelte/icons/layout-dashboard';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import { SidebarFooterMenu } from '$lib/features/ui/sidebar/footer';
|
||||
|
||||
interface SideLinks {
|
||||
name: string;
|
||||
icon: Component;
|
||||
link: string;
|
||||
show?: () => boolean;
|
||||
}
|
||||
|
||||
const user = $derived.by(() => ({
|
||||
name: $profile.fullname,
|
||||
email: $profile.email,
|
||||
avatar: $profile.avatarUrl
|
||||
}));
|
||||
|
||||
function isActive(pagePath: string, itemPath: string) {
|
||||
const pageLinkItems = pagePath.split('/');
|
||||
const itemLinkItems = itemPath.split('/');
|
||||
|
||||
if (itemLinkItems.length !== pageLinkItems.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pagePath.includes(itemPath);
|
||||
}
|
||||
|
||||
const sideLinks: SideLinks[] = $derived.by(() =>
|
||||
[
|
||||
{
|
||||
name: $t('lms_navigation.home'),
|
||||
icon: LayoutDashboardIcon,
|
||||
link: '/lms'
|
||||
},
|
||||
{
|
||||
name: $t('lms_navigation.my_learning'),
|
||||
icon: LibraryBigIcon,
|
||||
link: '/lms/mylearning'
|
||||
},
|
||||
{
|
||||
name: $t('lms_navigation.exercise'),
|
||||
icon: ListTodoIcon,
|
||||
link: '/lms/exercises',
|
||||
show() {
|
||||
return $currentOrg?.customization?.dashboard?.exercise;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: $t('lms_navigation.community'),
|
||||
icon: UsersIcon,
|
||||
link: '/lms/community',
|
||||
show() {
|
||||
return $currentOrg?.customization?.dashboard?.community;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: $t('lms_navigation.explore'),
|
||||
icon: MapPlusIcon,
|
||||
link: '/lms/explore'
|
||||
}
|
||||
].filter((link) => (link.show ? link.show() : true))
|
||||
);
|
||||
</script>
|
||||
|
||||
<Sidebar.Provider class="w-fit">
|
||||
<Sidebar.Root collapsible="icon">
|
||||
<Sidebar.Content>
|
||||
<Sidebar.Group>
|
||||
<Sidebar.GroupLabel>LMS Navigation</Sidebar.GroupLabel>
|
||||
<Sidebar.Menu>
|
||||
{#each sideLinks as item (item.link)}
|
||||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton tooltipContent={item.name}>
|
||||
<a
|
||||
href={item.link}
|
||||
class="flex w-full items-center gap-4 {isActive(page.url.pathname, item.link)
|
||||
? 'ui:bg-accent ui:text-accent-foreground rounded-md px-3 py-2'
|
||||
: ''}"
|
||||
>
|
||||
<item.icon size={16} />
|
||||
{item.name}
|
||||
</a>
|
||||
</Sidebar.MenuButton>
|
||||
</Sidebar.MenuItem>
|
||||
{/each}
|
||||
</Sidebar.Menu>
|
||||
</Sidebar.Group>
|
||||
</Sidebar.Content>
|
||||
|
||||
{#if user}
|
||||
<Sidebar.Footer>
|
||||
<SidebarFooterMenu />
|
||||
</Sidebar.Footer>
|
||||
{/if}
|
||||
|
||||
<Sidebar.Rail />
|
||||
</Sidebar.Root>
|
||||
|
||||
<!-- TODO: FIGURE OUT WHY THIS APPEARANCE IS BUGGY -->
|
||||
<!-- TODO: THE SIDEBAR COLLAPSE VIEW IS BAD -->
|
||||
<!-- <Sidebar.Inset>
|
||||
<Sidebar.Trigger class="-ml-1" />
|
||||
</Sidebar.Inset> -->
|
||||
</Sidebar.Provider>
|
||||
@@ -1,119 +0,0 @@
|
||||
<script>
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { Row, Grid, Column } from '$lib/components/Org/Settings/Layout';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import LanguagePicker from '$lib/components/Org/Settings/LanguagePicker.svelte';
|
||||
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import generateUUID from '$lib/utils/functions/generateUUID';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { handleLocaleChange } from '$lib/utils/functions/translations';
|
||||
|
||||
let avatar = $state('');
|
||||
let loading = $state(false);
|
||||
let hasLangChanged = $state(false);
|
||||
let locale = $derived($profile.locale);
|
||||
|
||||
async function handleUpdate() {
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
const updates = {
|
||||
fullname: $profile.fullname,
|
||||
username: $profile.username,
|
||||
email: $profile.email,
|
||||
locale
|
||||
};
|
||||
|
||||
if (avatar) {
|
||||
const filename = `user/${generateUUID()}.webp`;
|
||||
|
||||
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
});
|
||||
if (data) {
|
||||
const { data: response } = await supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
updates.avatar_url = response.publicUrl;
|
||||
$profile.avatar_url = response.publicUrl;
|
||||
}
|
||||
avatar = undefined;
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('profile').update(updates).match({ id: $profile.id });
|
||||
|
||||
profile.update((_profile) => ({
|
||||
..._profile,
|
||||
...updates
|
||||
}));
|
||||
snackbar.success();
|
||||
|
||||
if (hasLangChanged) {
|
||||
handleLocaleChange(locale);
|
||||
}
|
||||
|
||||
if (error) throw error;
|
||||
} catch (error) {
|
||||
let message = error instanceof Error ? error.message : `${error}`;
|
||||
if (message.includes('profile_username_key')) {
|
||||
message = $t('username already exists');
|
||||
}
|
||||
|
||||
snackbar.error(`${$t('snackbar.update_failed')}: ${message}`);
|
||||
|
||||
loading = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c mt-5 w-full rounded border-gray-200 dark:border-neutral-600">
|
||||
<Row class="border-bottom-c flex flex-col items-center py-7 lg:flex-row lg:items-start ">
|
||||
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
|
||||
<p class="font-bold dark:text-white">{$t('settings.profile.profile_picture.heading')}</p>
|
||||
</Column>
|
||||
<Column sm={2} md={4} lg={8} class="mt-2 lg:mt-0">
|
||||
<UploadImage bind:avatar src={$profile.avatar_url} widthHeight="w-16 h-16 lg:w-24 lg:h-24" />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<p class="font-bold dark:text-white">{$t('settings.profile.personal_information.heading')}</p>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0">
|
||||
<TextField
|
||||
label={$t('settings.profile.personal_information.full_name')}
|
||||
bind:value={$profile.fullname}
|
||||
className="w-full lg:w-60 mb-4"
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.profile.personal_information.username')}
|
||||
bind:value={$profile.username}
|
||||
className="w-full lg:w-60 mb-4"
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.profile.personal_information.email')}
|
||||
bind:value={$profile.email}
|
||||
className="w-full lg:w-60 mb-4"
|
||||
/>
|
||||
|
||||
<LanguagePicker bind:hasLangChanged bind:value={$profile.locale} className="w-full lg:w-60 mb-4" />
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="m-5 flex w-full items-center gap-2 lg:justify-center">
|
||||
<PrimaryButton
|
||||
label={$t('settings.profile.update_profile')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
className="mr-5"
|
||||
isLoading={loading}
|
||||
isDisabled={loading}
|
||||
onClick={handleUpdate}
|
||||
/>
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -1,54 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let { value = $bindable(), placeholder = ``, preview } = $props();
|
||||
const activeClassName = 'border-b-2 border-primary-700';
|
||||
let isWriteMode = $state(true);
|
||||
|
||||
const handleTabClick = (tab) => (event) => {
|
||||
isWriteMode = tab === 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="root">
|
||||
<div class="flex justify-center">
|
||||
<button onclick={handleTabClick(1)} class="p-2 focus:outline-none {isWriteMode && `${activeClassName}`}">
|
||||
{$t('markdown_editor.write')}
|
||||
</button>
|
||||
<button onclick={handleTabClick(2)} class="p-2 focus:outline-none {!isWriteMode && `${activeClassName}`}">
|
||||
{$t('markdown_editor.preview')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="m-2 mb-0 p-0">
|
||||
{#if isWriteMode}
|
||||
<textarea bind:value {placeholder} class="border-gray rounded-md border p-2 dark:bg-gray-500"></textarea>
|
||||
{:else}
|
||||
<article class="preview prose prose-sm sm:prose m-auto p-2">
|
||||
{@render preview?.()}
|
||||
</article>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
width: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.preview {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
.octicon {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import Modal from './index.svelte';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
onDelete?: () => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
let { open = $bindable(false), onDelete = () => {}, isLoading = false }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
onClose={() => !isLoading && (open = false)}
|
||||
bind:open
|
||||
width="w-96"
|
||||
containerClass="p-6"
|
||||
headerClass="p-2"
|
||||
modalHeading={$t('delete_modal.label')}
|
||||
>
|
||||
<div>
|
||||
<p class="text-center text-xl dark:text-white">
|
||||
{$t('delete_modal.content')}
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('delete_modal.no')}
|
||||
onClick={() => (open = false)}
|
||||
{isLoading}
|
||||
/>
|
||||
<PrimaryButton
|
||||
className="px-6 py-3"
|
||||
variant={VARIANTS.CONTAINED_DANGER}
|
||||
label={$t('delete_modal.yes')}
|
||||
onClick={onDelete}
|
||||
{isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -1,92 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CloseButton from '../Buttons/Close/index.svelte';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
onClose?: any;
|
||||
modalHeading?: string;
|
||||
headerClass?: string;
|
||||
labelClass?: string;
|
||||
width?: string;
|
||||
maxWidth?: string;
|
||||
containerClass?: string;
|
||||
size?: string;
|
||||
isCloseable?: boolean;
|
||||
isClosing?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
onClose = () => {},
|
||||
modalHeading = '',
|
||||
headerClass = '',
|
||||
labelClass = '',
|
||||
width = '',
|
||||
maxWidth = '',
|
||||
containerClass = '',
|
||||
size = '',
|
||||
isCloseable = true,
|
||||
isClosing = false,
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (open) {
|
||||
document.getElementsByTagName('body')[0].style.overflow = 'hidden';
|
||||
} else {
|
||||
document.getElementsByTagName('body')[0].style.overflow = 'auto';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="dialog z-100 fixed inset-0 flex items-center justify-center overflow-x-hidden bg-gray-100 bg-opacity-50"
|
||||
onclick={onClose}
|
||||
role="presentation"
|
||||
>
|
||||
<div aria-hidden="true" class="backdrop"></div>
|
||||
<div
|
||||
class="{maxWidth || 'container'} {size === 'sm'
|
||||
? 'small'
|
||||
: ''} mx-auto bg-white dark:bg-neutral-800 {width} max-h-4/5 overflow-y-auto rounded-md shadow-lg"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
role="presentation"
|
||||
>
|
||||
<div
|
||||
class="{headerClass} sticky top-0 z-10 flex items-center justify-between border border-l-0 border-r-0 border-t-0 border-gray-100 bg-white p-4 px-5 dark:border-neutral-600 dark:bg-neutral-800"
|
||||
>
|
||||
<p class="text-md m-0 font-medium dark:text-white {labelClass}">
|
||||
{modalHeading}
|
||||
</p>
|
||||
{#if isCloseable}
|
||||
<div class="rounded-full">
|
||||
<CloseButton onClick={onClose} contained={true} {isClosing} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="body p-6 {containerClass} ">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.dialog {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
}
|
||||
/* .body {
|
||||
max-height: 60vh;
|
||||
} */
|
||||
|
||||
.small {
|
||||
max-width: 388px;
|
||||
}
|
||||
</style>
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Modal } from './index.svelte';
|
||||
@@ -17,18 +17,17 @@
|
||||
|
||||
{#if customLinks}
|
||||
<div class={isMobile ? 'space-y-4' : 'mr-4 hidden items-center space-x-6 lg:flex'}>
|
||||
{#each customLinks.links as link}
|
||||
{#each customLinks.links as link (link.url)}
|
||||
<li>
|
||||
<Button
|
||||
<a
|
||||
href={link.url}
|
||||
target={link.openInNewTab ? '_blank' : '_self'}
|
||||
rel={link.openInNewTab ? 'noopener noreferrer' : ''}
|
||||
variant="link"
|
||||
size="sm"
|
||||
onclick={handleLinkClick}
|
||||
>
|
||||
{link.label}
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onclick={handleLinkClick}>
|
||||
{link.label}
|
||||
</Button>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import BellIcon from '@lucide/svelte/icons/bell';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import { globalStore } from '$lib/utils/store/app';
|
||||
import { currentOrgPath } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
@@ -26,7 +26,7 @@
|
||||
<div class="flex items-center text-white">
|
||||
{#if isCoursePage}
|
||||
<li class="hidden md:block">
|
||||
<IconButton onClick={() => goto(coursesPath)}>
|
||||
<IconButton onclick={() => goto(coursesPath)}>
|
||||
<ArrowLeftIcon size={16} class="custom" />
|
||||
</IconButton>
|
||||
</li>
|
||||
|
||||
@@ -45,12 +45,12 @@
|
||||
<ul class="flex w-full items-center">
|
||||
<Logo {logo} {orgName} />
|
||||
|
||||
<span class="flex-grow"></span>
|
||||
<span class="grow"></span>
|
||||
|
||||
<!-- Mobile Menu Button - Only show when custom links exist -->
|
||||
{#if isOrgSite && showLinks}
|
||||
<button
|
||||
class="mobile-menu-btn hover:text-primary-600 rounded-md p-2 text-gray-700 transition-colors duration-200 hover:bg-gray-100 lg:hidden"
|
||||
class="mobile-menu-btn hover:ui:text-primary rounded-md p-2 text-gray-700 transition-colors duration-200 hover:bg-gray-100 lg:hidden"
|
||||
onclick={toggleMobileMenu}
|
||||
aria-label="Toggle mobile menu"
|
||||
>
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
<script>
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { currentOrgPlan, isFreePlan } from '$lib/utils/store/org';
|
||||
|
||||
import { Row, Grid, Column } from './Layout';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
let isRedirecting = $state(false);
|
||||
|
||||
async function onOpenBilling() {
|
||||
console.log({
|
||||
plan: $currentOrgPlan
|
||||
});
|
||||
|
||||
try {
|
||||
if ($currentOrgPlan?.provider === 'polar') {
|
||||
const url = new URL('/api/polar/portal', window.location.origin);
|
||||
url.searchParams.set('customerId', $currentOrgPlan?.customerId || '');
|
||||
|
||||
window.open(url.toString(), '_blank');
|
||||
}
|
||||
|
||||
if ($currentOrgPlan?.provider === 'lmz') {
|
||||
isRedirecting = true;
|
||||
const url = new URL('/api/lmz/subscription', window.location.origin);
|
||||
url.searchParams.set('subscriptionId', $currentOrgPlan?.subscriptionId);
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
const result = await response.json();
|
||||
|
||||
isRedirecting = false;
|
||||
|
||||
window.open(result.customerPortal, '_blank');
|
||||
}
|
||||
} catch (error) {
|
||||
isRedirecting = false;
|
||||
|
||||
console.error(error);
|
||||
snackbar.error($t('settings.billing.error'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="mt-5 w-full">
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}><SectionTitle>{$t('settings.billing.sub_title')}</SectionTitle></Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="lg:mt-0 dark:text-white">{$t('settings.billing.manage')}</h4>
|
||||
{#if $isFreePlan}
|
||||
{$t('settings.billing.active')}
|
||||
{:else}
|
||||
<p class="mb-3">{$t('settings.billing.provider')}</p>
|
||||
|
||||
<PrimaryButton
|
||||
label={$t('settings.billing.open_billing')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
onClick={onOpenBilling}
|
||||
isLoading={isRedirecting}
|
||||
/>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -1,187 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import SaveIcon from '@lucide/svelte/icons/save';
|
||||
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
|
||||
import { Row, Grid, Column } from './Layout';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
const supabase = getSupabase();
|
||||
|
||||
let widgetKey = $state('');
|
||||
let isSaving = $state(false);
|
||||
|
||||
function widgetControl(key: string) {
|
||||
widgetKey = key;
|
||||
$handleOpenWidget.open = true;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('organization')
|
||||
.update({ customization: $currentOrg.customization })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
if (error) {
|
||||
const message = error?.message || 'snackbar.lms.error.try_again';
|
||||
console.error('Error updating customizations', message);
|
||||
snackbar.error('snackbar.update_failed');
|
||||
} else {
|
||||
snackbar.success('snackbar.success_update');
|
||||
}
|
||||
|
||||
isSaving = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="border-c relative mt-5 w-full rounded border-gray-200 dark:border-neutral-600">
|
||||
<Row class="border-bottom-c flex flex-col items-start py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('components.settings.customize_lms.dashboard.title')}</SectionTitle>
|
||||
</Column>
|
||||
|
||||
<Column sm={8} md={8} lg={8} class="space-y-2">
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.dashboard.community')}</p>
|
||||
<div class="mb-3 flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.dashboard.community} />
|
||||
|
||||
<Label class="text-sm">
|
||||
{$currentOrg.customization.dashboard.community
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.dashboard.exercises')}</p>
|
||||
<div class="mb-3 flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.dashboard.exercise} />
|
||||
|
||||
<Label class="text-sm">
|
||||
{$currentOrg.customization.dashboard.exercise
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.dashboard.banner_image')}</p>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('components.settings.customize_lms.dashboard.banner_image_btn')}
|
||||
className="mt-3"
|
||||
onClick={() => widgetControl('banner-image')}
|
||||
/>
|
||||
{#if $currentOrg.customization.dashboard.bannerImage}
|
||||
<img alt="Banner" src={$currentOrg.customization.dashboard.bannerImage} class="mt-2 w-full rounded-md" />
|
||||
{/if}
|
||||
{#if $handleOpenWidget.open && widgetKey === 'banner-image'}
|
||||
<UploadWidget bind:imageURL={$currentOrg.customization.dashboard.bannerImage} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>{$t('components.settings.customize_lms.dashboard.banner_text')}</p>
|
||||
<TextField
|
||||
label={$t('components.settings.customize_lms.dashboard.banner_text_label')}
|
||||
placeholder={$t('components.settings.customize_lms.dashboard.banner_text_placeholder')}
|
||||
bind:value={$currentOrg.customization.dashboard.bannerText}
|
||||
/>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('components.settings.customize_lms.course.title')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={8} md={8} lg={8} class="space-y-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.course.newsfeed')}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.course.newsfeed} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$currentOrg.customization.course.newsfeed
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.course.grading')}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.course.grading} />
|
||||
<Label class="text-gray-600">
|
||||
{$currentOrg.customization.course.grading
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('components.settings.customize_lms.apps.title')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={8} md={8} lg={8} class="space-y-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.apps.poll')}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.apps.poll} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$currentOrg.customization.apps.poll
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3">
|
||||
<p class="text-sm">{$t('components.settings.customize_lms.apps.live_comment')}</p>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$currentOrg.customization.apps.comments} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$currentOrg.customization.apps.comments
|
||||
? $t('components.settings.customize_lms.enabled')
|
||||
: $t('components.settings.customize_lms.disabled')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<div class="sticky bottom-12 z-[120] float-right mr-2 hidden md:block">
|
||||
<PrimaryButton
|
||||
label={$t('components.settings.customize_lms.save')}
|
||||
onClick={handleSave}
|
||||
isLoading={isSaving}
|
||||
isDisabled={isSaving}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<div class="absolute bottom-8 right-6 z-[120] block md:hidden">
|
||||
<PrimaryButton onClick={handleSave} isLoading={isSaving} isDisabled={isSaving}>
|
||||
<SaveIcon size={16} />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
@@ -1,400 +0,0 @@
|
||||
<script lang="ts">
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
import RotateCcwIcon from '@lucide/svelte/icons/rotate-ccw';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import Copy from '@lucide/svelte/icons/copy';
|
||||
import isValidDomain from 'is-valid-domain';
|
||||
import { goto } from '$app/navigation';
|
||||
import { untrack } from 'svelte';
|
||||
import { parse } from 'tldts';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { blockedSubdomain } from '$lib/utils/constants/app';
|
||||
import { currentOrg, isFreePlan } from '$lib/utils/store/org';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { copyToClipboard } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { updateOrgSiteNameValidation } from '$lib/utils/functions/validator';
|
||||
import { sanitizeDomain, sendDomainRequest } from '$lib/utils/functions/domain';
|
||||
|
||||
import { Row, Grid, Column } from './Layout';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextChip from '$lib/components/Chip/Text.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { ComingSoon } from '$lib/features/ui';
|
||||
import { UpgradeBanner } from '$lib/features/ui';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import VisitOrgSiteButton from '$lib/components/Buttons/VisitOrgSite.svelte';
|
||||
|
||||
let siteName = $derived($currentOrg.siteName);
|
||||
let customDomain = $state('');
|
||||
let customCode = $state('');
|
||||
let favicon = $state('');
|
||||
let isLoading = $state(false);
|
||||
let isCustomDomainLoading = $state(false);
|
||||
let isRefreshing = $state(false);
|
||||
|
||||
const isDomainValid = $derived(isValidDomain(sanitizeDomain(customDomain), { subdomain: true }));
|
||||
|
||||
type Error = {
|
||||
siteName: string;
|
||||
customDomain: string;
|
||||
};
|
||||
let errors: Error = $state({
|
||||
siteName: '',
|
||||
customDomain: ''
|
||||
});
|
||||
|
||||
async function handleSaveSiteName() {
|
||||
errors = updateOrgSiteNameValidation(siteName) as Error;
|
||||
|
||||
if (Object.values(errors).length) {
|
||||
isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (blockedSubdomain.includes(siteName || '')) {
|
||||
errors.siteName = 'Sitename already exists.';
|
||||
return;
|
||||
}
|
||||
isLoading = true;
|
||||
|
||||
const { data: org, error } = await supabase.from('organization').update({ siteName }).match({ id: $currentOrg.id });
|
||||
|
||||
console.log('Updating organisation', org);
|
||||
if (error) {
|
||||
console.log('Error: create organisation', error);
|
||||
errors.siteName = $t('add_org.sitename');
|
||||
} else {
|
||||
snackbar.success();
|
||||
$currentOrg.siteName = siteName;
|
||||
|
||||
goto(`/org/${$currentOrg.siteName}/settings/domains`);
|
||||
}
|
||||
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
async function handleSaveCustomDomain() {
|
||||
if (!isDomainValid) return;
|
||||
|
||||
const sanitizedDomain = sanitizeDomain(customDomain);
|
||||
|
||||
const details = parse(sanitizedDomain);
|
||||
if (!details.subdomain) {
|
||||
errors.customDomain = $t('components.settings.domains.custom_domain_error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (sanitizedDomain.includes('classroomio.com')) {
|
||||
errors.customDomain = $t('components.settings.domains.custom_domain_not_classroomio');
|
||||
return;
|
||||
}
|
||||
|
||||
isCustomDomainLoading = true;
|
||||
const { error } = await supabase
|
||||
.from('organization')
|
||||
.update({ customDomain: sanitizedDomain })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
if (error) {
|
||||
console.log('Error: create organisation', error);
|
||||
errors.customDomain = error.message;
|
||||
isCustomDomainLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await sendDomainRequest('add_domain', sanitizedDomain);
|
||||
const data = await response.json();
|
||||
console.log('added domain to vercel', data);
|
||||
} catch (error) {
|
||||
console.log('Error: adding domain to vercel', error);
|
||||
snackbar.error(error as string);
|
||||
isCustomDomainLoading = false;
|
||||
return;
|
||||
}
|
||||
snackbar.success('components.settings.domains.custom_domain_success');
|
||||
$currentOrg.customDomain = sanitizedDomain;
|
||||
|
||||
isCustomDomainLoading = false;
|
||||
}
|
||||
|
||||
async function handleRemoveCustomDomain() {
|
||||
if (!$currentOrg.customDomain) return;
|
||||
|
||||
isCustomDomainLoading = true;
|
||||
const { error } = await supabase
|
||||
.from('organization')
|
||||
.update({ customDomain: null, isCustomDomainVerified: false })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
if (error) {
|
||||
console.log('Error: updating organisation', error);
|
||||
snackbar.error(error.message);
|
||||
isCustomDomainLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await sendDomainRequest('remove_domain', $currentOrg.customDomain);
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
throw new Error(data.message);
|
||||
}
|
||||
console.log('removed domain from vercel', data);
|
||||
} catch (error) {
|
||||
console.log('Error: removing domain from vercel', error);
|
||||
snackbar.error(error as string);
|
||||
isCustomDomainLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
snackbar.success();
|
||||
|
||||
$currentOrg.customDomain = '';
|
||||
$currentOrg.isCustomDomainVerified = false;
|
||||
isCustomDomainLoading = false;
|
||||
}
|
||||
|
||||
async function handleRefreshCustomDomain() {
|
||||
isRefreshing = true;
|
||||
|
||||
try {
|
||||
const response = await sendDomainRequest('verify_domain', $currentOrg.customDomain || '');
|
||||
const data = await response.json();
|
||||
|
||||
console.log('data', data);
|
||||
if (data.verified && !$currentOrg.isCustomDomainVerified) {
|
||||
$currentOrg.isCustomDomainVerified = true;
|
||||
|
||||
await supabase.from('organization').update({ isCustomDomainVerified: true }).match({ id: $currentOrg.id });
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Error: refreshing domain', error);
|
||||
snackbar.error(error as string);
|
||||
}
|
||||
|
||||
isRefreshing = false;
|
||||
}
|
||||
|
||||
function resetErrors(_siteName: string, _customDomain: string) {
|
||||
untrack(() => {
|
||||
if (errors.siteName) {
|
||||
errors.siteName = '';
|
||||
}
|
||||
if (errors.customDomain) {
|
||||
errors.customDomain = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getSubdomain() {
|
||||
const details = parse($currentOrg.customDomain || '');
|
||||
return details.subdomain;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
resetErrors(siteName, customDomain);
|
||||
});
|
||||
</script>
|
||||
|
||||
<Grid class="mt-5 w-full rounded border border-gray-200 dark:border-neutral-600">
|
||||
<Row class="border-bottom-c py-7">
|
||||
<Column sm={4} md={4} lg={4} class="text-lg">
|
||||
<SectionTitle>{$t('components.settings.domains.add')}</SectionTitle>
|
||||
</Column>
|
||||
|
||||
<Column sm={8} md={8} lg={8} class="space-y-2">
|
||||
<p class="text-md mb-5 text-gray-500 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.custom_domain.body')}
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<!-- Org Site Name -->
|
||||
<TextField
|
||||
label="URL"
|
||||
helperMessage={`https://${siteName || ''}.classroomio.com`}
|
||||
bind:value={siteName}
|
||||
type="text"
|
||||
placeholder="e.g traversymedia"
|
||||
className="mb-5 w-full"
|
||||
labelClassName=""
|
||||
errorMessage={errors.siteName}
|
||||
/>
|
||||
<div class="mb-6 flex items-center">
|
||||
<PrimaryButton
|
||||
label={$t('components.settings.domains.update')}
|
||||
className="py-2"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleSaveSiteName}
|
||||
isDisabled={isLoading}
|
||||
/>
|
||||
|
||||
<VisitOrgSiteButton />
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="py-7">
|
||||
<UpgradeBanner className="mb-5">{$t('upgrade.domain')}</UpgradeBanner>
|
||||
<Column sm={4} md={4} lg={4} class="text-lg">
|
||||
<SectionTitle>
|
||||
{$t('components.settings.domains.custom')}
|
||||
</SectionTitle>
|
||||
</Column>
|
||||
|
||||
<Column sm={8} md={8} lg={8} class="space-y-2">
|
||||
<div class="border-bottom-c pb-10">
|
||||
<!-- DNS Configuration -->
|
||||
{#if $currentOrg.customDomain}
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-md flex items-center gap-2 font-medium">
|
||||
{$currentOrg.customDomain}
|
||||
|
||||
<IconButton contained={true} size="small" onClick={() => goto(`https://${$currentOrg.customDomain}`)}>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
</IconButton>
|
||||
</p>
|
||||
|
||||
<div
|
||||
class="mt-1 h-2 w-2 rounded-full bg-{$currentOrg.isCustomDomainVerified ? 'green' : 'yellow'}-400"
|
||||
></div>
|
||||
</div>
|
||||
{#if $currentOrg.isCustomDomainVerified}
|
||||
<TextChip value="Verified" className="bg-green-500 text-white text-xs px-3" size="sm" />
|
||||
{:else}
|
||||
<TextChip value="Pending verification" className="bg-yellow-500 text-white text-xs px-3" size="sm" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mb-4 text-sm text-gray-500">
|
||||
{$t('components.settings.domains.dns_description')}
|
||||
</div>
|
||||
|
||||
<div class="mb-4 flex items-center gap-10 rounded-md border px-4 py-2">
|
||||
<div class="flex h-[72px] flex-col justify-evenly">
|
||||
<p class="text-sm font-light">{$t('components.settings.domains.dns_type')}</p>
|
||||
<p class="flex h-[40px] items-center">CNAME</p>
|
||||
</div>
|
||||
|
||||
<div class="flex h-[72px] flex-col justify-evenly">
|
||||
<p class="text-sm font-light">{$t('components.settings.domains.dns_name')}</p>
|
||||
<p class="flex h-[40px] items-center">
|
||||
{getSubdomain()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex h-[72px] flex-col justify-evenly">
|
||||
<p class="text-sm font-light">{$t('components.settings.domains.dns_value')}</p>
|
||||
<p class=" flex items-center gap-1">
|
||||
cname.vercel-dns.com
|
||||
<IconButton onClick={() => copyToClipboard('cname.vercel-dns.com')}>
|
||||
<p>cname.vercel-dns.com</p>
|
||||
|
||||
<Copy />
|
||||
</IconButton>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
className="py-2 flex items-center gap-2"
|
||||
onClick={handleRefreshCustomDomain}
|
||||
isLoading={isRefreshing}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
>
|
||||
{#if !isRefreshing}
|
||||
<RotateCcwIcon size={16} />
|
||||
{/if}
|
||||
{$t('components.settings.domains.refresh')}
|
||||
</PrimaryButton>
|
||||
|
||||
<PrimaryButton
|
||||
className="py-2 flex items-center gap-2"
|
||||
onClick={handleRemoveCustomDomain}
|
||||
isLoading={isCustomDomainLoading}
|
||||
variant={VARIANTS.CONTAINED_DANGER}
|
||||
>
|
||||
{#if !isCustomDomainLoading}
|
||||
<TrashIcon size={16} />
|
||||
{/if}
|
||||
{$t('components.settings.domains.remove')}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Add Custom Domain -->
|
||||
<div class="mb-4 flex items-center gap-5">
|
||||
<p class="font-bold">{$t('components.settings.domains.your_domain')}</p>
|
||||
</div>
|
||||
<TextField
|
||||
bind:value={customDomain}
|
||||
type="text"
|
||||
placeholder="courses.yourwebsite.com"
|
||||
helperMessage="https://{customDomain || 'course.yourwebsite.com'}"
|
||||
errorMessage={errors.customDomain}
|
||||
className="w-4/5"
|
||||
isDisabled={$isFreePlan}
|
||||
/>
|
||||
|
||||
<div class="mt-5 flex items-center">
|
||||
<PrimaryButton
|
||||
label={$t('components.settings.domains.save')}
|
||||
className="py-2"
|
||||
onClick={$isFreePlan ? () => {} : handleSaveCustomDomain}
|
||||
isLoading={isCustomDomainLoading}
|
||||
isDisabled={isLoading || !isDomainValid}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="border-bottom-c py-10">
|
||||
<div class="mb-4 flex items-center gap-5">
|
||||
<p class="font-bold">{$t('components.settings.domains.custom_favicon')}</p>
|
||||
<ComingSoon />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<UploadImage
|
||||
shape="rounded-md"
|
||||
bind:avatar={favicon}
|
||||
src={$currentOrg.favicon ?? '/logo-512.png'}
|
||||
widthHeight="w-16 h-16 lg:w-24 lg:h-24"
|
||||
flexDirection="flex-row"
|
||||
isDisabled={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="py-10">
|
||||
<div class="mb-4 flex items-center gap-5">
|
||||
<p class="font-bold">{$t('components.settings.domains.custom_code')}</p>
|
||||
<ComingSoon />
|
||||
</div>
|
||||
|
||||
<TextArea
|
||||
bind:value={customCode}
|
||||
placeholder="e.g <link rel='stylesheet' href='https://example.com/style.css' />"
|
||||
className="w-4/5"
|
||||
rows={7}
|
||||
disabled={true}
|
||||
/>
|
||||
</div>
|
||||
<!-- <div class="flex items-center">
|
||||
<p class="dark:text-white mr-3">
|
||||
{$t('components.settings.domains.domain')}
|
||||
</p>
|
||||
|
||||
<ComingSoon />
|
||||
</div> -->
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -1,94 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
|
||||
import { Row, Grid, Column } from './Layout';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import ArrowUpRightIcon from '$lib/components/Icons/ArrowTopRight.svelte';
|
||||
import CircleCheckIcon from '$lib/components/Icons/CircleCheckIcon.svelte';
|
||||
|
||||
let chatId: number | null = $state(null);
|
||||
|
||||
async function addChatId() {
|
||||
if (!chatId || `${chatId}`.length < 5) return;
|
||||
|
||||
const { error } = await supabase
|
||||
.from('profile')
|
||||
.update({
|
||||
telegram_chat_id: chatId
|
||||
})
|
||||
.match({ id: $profile.id });
|
||||
|
||||
if (error) {
|
||||
console.log(error);
|
||||
snackbar.error('snackbar.course_settings.error.failed_integration');
|
||||
} else {
|
||||
$profile.telegramChatId = chatId;
|
||||
chatId = null;
|
||||
snackbar.success('snackbar.course_settings.success.successful_integration');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteChatId() {
|
||||
const { error } = await supabase.from('profile').update({ telegram_chat_id: null }).match({ id: $profile.id });
|
||||
|
||||
if (error) {
|
||||
snackbar.error('snackbar.course_settings.error.failed_deletion');
|
||||
console.log(error);
|
||||
} else {
|
||||
$profile.telegramChatId = null;
|
||||
snackbar.success('snackbar.course_settings.success.successful_deletion');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Grid class="mt-5 w-full">
|
||||
<Row class="border-bottom-c flex flex-col justify-center py-7 lg:flex-row lg:justify-start">
|
||||
<Column sm={4} md={4} lg={4} class="flex items-center justify-center">
|
||||
<img src="/telegram-svg.svg" alt="" class="mr-2 w-10" />
|
||||
<SectionTitle>{$t('settings.integrations.heading')}</SectionTitle></Column
|
||||
>
|
||||
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 flex flex-col items-center lg:mt-0 lg:items-start">
|
||||
{#if $profile.telegramChatId !== null}
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<div class="flex items-center">
|
||||
<CircleCheckIcon size={16} filled />
|
||||
<SectionTitle>{$t('settings.integrations.success_message')}</SectionTitle>
|
||||
</div>
|
||||
<PrimaryButton className="mt-3" variant={VARIANTS.CONTAINED_DANGER} onClick={deleteChatId}
|
||||
>{$t('settings.integrations.disconnect')}</PrimaryButton
|
||||
>
|
||||
</div>
|
||||
{:else}
|
||||
<div>
|
||||
<h3 class="text-lg font-normal">
|
||||
{$t('settings.integrations.sub_heading')}
|
||||
</h3>
|
||||
<h4 class="font-normal">{$t('settings.integrations.step_authenticate')}</h4>
|
||||
<PrimaryButton
|
||||
className="mb-5"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => window.open('https://t.me/classroomio_bot', '_blank')}
|
||||
><span class="mr-2">{$t('settings.integrations.open_bot_button')}</span>
|
||||
<ArrowUpRightIcon /></PrimaryButton
|
||||
>
|
||||
<h4 class="font-normal">{$t('settings.integrations.step_chatId')}</h4>
|
||||
<TextField
|
||||
bind:value={chatId}
|
||||
placeholder="360111"
|
||||
className="w-full lg:w-60 mb-5"
|
||||
type="number"
|
||||
isRequired
|
||||
/>
|
||||
<PrimaryButton onClick={addChatId}>{$t('settings.integrations.connect_button')}</PrimaryButton>
|
||||
</div>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
@@ -1,813 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import SaveIcon from '@lucide/svelte/icons/save';
|
||||
import TrashIcon from '@lucide/svelte/icons/trash';
|
||||
import * as RadioGroup from '@cio/ui/base/radio-group';
|
||||
|
||||
import { landingPageSettings } from './store';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { getSupabase } from '$lib/utils/functions/supabase';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { handleOpenWidget } from '$lib/components/CourseLandingPage/store';
|
||||
|
||||
import { Row, Grid, Column } from './Layout';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
import type { OrgLandingPageJson } from '$lib/utils/types/org';
|
||||
|
||||
let isSaving = $state(false);
|
||||
let creatingNewQuestion = $state(false);
|
||||
let creatingNewCustomLink = $state(false);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
let widgetKey = $state('');
|
||||
const banner = [
|
||||
{ value: 'video', label: `${$t('settings.landing_page.actions.banner_type.video')}` },
|
||||
{ value: 'image', label: `${$t('settings.landing_page.actions.banner_type.image')}` }
|
||||
];
|
||||
const supabase = getSupabase();
|
||||
|
||||
let newQuestion = $state({
|
||||
title: '',
|
||||
content: ''
|
||||
});
|
||||
|
||||
let newCustomLink = $state({
|
||||
label: '',
|
||||
url: '',
|
||||
openInNewTab: false
|
||||
});
|
||||
|
||||
function widgetControl(key: string) {
|
||||
widgetKey = key;
|
||||
$handleOpenWidget.open = true;
|
||||
}
|
||||
|
||||
function createNewFaq() {
|
||||
newQuestion = {
|
||||
title: '',
|
||||
content: ''
|
||||
};
|
||||
creatingNewQuestion = true;
|
||||
}
|
||||
|
||||
function saveNewFAQ() {
|
||||
if (newQuestion.title !== '' && newQuestion.content !== '') {
|
||||
$landingPageSettings.faq.questions = [
|
||||
...$landingPageSettings.faq.questions,
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
title: newQuestion.title,
|
||||
content: newQuestion.content
|
||||
}
|
||||
];
|
||||
creatingNewQuestion = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelNewFAQ() {
|
||||
creatingNewQuestion = false;
|
||||
}
|
||||
|
||||
function deleteFaq(id: number) {
|
||||
let faqs = $landingPageSettings.faq.questions;
|
||||
const filteredFaq = faqs.filter((faq) => faq.id !== id);
|
||||
$landingPageSettings.faq.questions = filteredFaq;
|
||||
}
|
||||
|
||||
function createNewCustomLink() {
|
||||
newCustomLink = {
|
||||
label: '',
|
||||
url: '',
|
||||
openInNewTab: false
|
||||
};
|
||||
creatingNewCustomLink = true;
|
||||
}
|
||||
|
||||
function saveNewCustomLink() {
|
||||
if (newCustomLink.label !== '' && newCustomLink.url !== '') {
|
||||
$landingPageSettings.customLinks.links = [
|
||||
...$landingPageSettings.customLinks.links,
|
||||
{
|
||||
id: new Date().getTime(),
|
||||
label: newCustomLink.label,
|
||||
url: newCustomLink.url,
|
||||
openInNewTab: newCustomLink.openInNewTab
|
||||
}
|
||||
];
|
||||
creatingNewCustomLink = false;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelNewCustomLink() {
|
||||
creatingNewCustomLink = false;
|
||||
}
|
||||
|
||||
function deleteCustomLink(id: number) {
|
||||
let customLinks = $landingPageSettings.customLinks.links;
|
||||
const filteredCustomLinks = customLinks.filter((link) => link.id !== id);
|
||||
$landingPageSettings.customLinks.links = filteredCustomLinks;
|
||||
}
|
||||
|
||||
const checkPrefix = (inputValue: string) => {
|
||||
if (!inputValue) return;
|
||||
|
||||
if (inputValue.trim() !== '') {
|
||||
if (!inputValue.startsWith('https://')) {
|
||||
inputValue = 'https://' + inputValue;
|
||||
}
|
||||
}
|
||||
return inputValue;
|
||||
};
|
||||
|
||||
async function handleSave() {
|
||||
isSaving = true;
|
||||
$landingPageSettings.footer.twitter = checkPrefix($landingPageSettings.footer.twitter) || '';
|
||||
$landingPageSettings.footer.linkedin = checkPrefix($landingPageSettings.footer.linkedin) || '';
|
||||
$landingPageSettings.footer.facebook = checkPrefix($landingPageSettings.footer.facebook) || '';
|
||||
|
||||
const { error } = await supabase
|
||||
.from('organization')
|
||||
.update({ landingpage: $landingPageSettings })
|
||||
.match({ id: $currentOrg.id });
|
||||
|
||||
if (error) {
|
||||
const message = error?.message || $t('snackbar.lms.error.try_again');
|
||||
snackbar.error(`${$t('snackbar.update_failed')}: ${message}`);
|
||||
} else {
|
||||
$currentOrg.landingpage = $landingPageSettings as any;
|
||||
snackbar.success('snackbar.success_update');
|
||||
hasUnsavedChanges = false;
|
||||
}
|
||||
|
||||
isSaving = false;
|
||||
}
|
||||
|
||||
// Set default from store
|
||||
currentOrg.subscribe((cOrg) => {
|
||||
if (cOrg.landingpage && Object.keys(cOrg.landingpage).length) {
|
||||
const landingpage = { ...(cOrg.landingpage as unknown as OrgLandingPageJson) };
|
||||
|
||||
// Fallbacks for new keys we added into JSON, in case a user already saved the old JSON
|
||||
if (!landingpage?.header?.banner) {
|
||||
landingpage.header.banner = $landingPageSettings.header.banner;
|
||||
}
|
||||
|
||||
if (!landingpage?.header?.background) {
|
||||
landingpage.header.background = $landingPageSettings.header.background;
|
||||
}
|
||||
|
||||
if (!landingpage.customLinks) {
|
||||
landingpage.customLinks = $landingPageSettings.customLinks;
|
||||
}
|
||||
$landingPageSettings = {
|
||||
...landingpage
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
|
||||
<Grid class="relative mt-5 w-full">
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.header.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.header.show
|
||||
? $t('settings.landing_page.show_section')
|
||||
: $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.title')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.header.title}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="w-full mb-5"
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.title_highlight')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
|
||||
className="w-full mb-5"
|
||||
bind:value={$landingPageSettings.header.titleHighlight}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label={$t('settings.landing_page.subtitle')}
|
||||
labelClassName="font-light"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
|
||||
className="w-full mb-5"
|
||||
bind:value={$landingPageSettings.header.subtitle}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<SectionTitle>{$t('settings.landing_page.actions.heading')}</SectionTitle>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.actions.label')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.label')}
|
||||
className="w-full mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.header.action.label}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<div class="mb-5 gap-2">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.actions.link')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.link')}
|
||||
bind:value={$landingPageSettings.header.action.link}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<div class="mt-3 flex items-center space-x-2">
|
||||
<Switch
|
||||
bind:checked={$landingPageSettings.header.action.redirect}
|
||||
onCheckedChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.header.action.redirect
|
||||
? $t('settings.landing_page.actions.redirect')
|
||||
: $t('settings.landing_page.actions.no_redirect')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-5 mt-10">
|
||||
<Label class="mb-3 block font-medium">{$t('settings.landing_page.actions.banner_type.heading')}</Label>
|
||||
<RadioGroup.Root
|
||||
bind:value={$landingPageSettings.header.banner.type}
|
||||
onValueChange={() => {
|
||||
if (hasUnsavedChanges) return;
|
||||
hasUnsavedChanges = true;
|
||||
}}
|
||||
>
|
||||
{#each banner as item}
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value={item.value} id={item.value} />
|
||||
<Label for={item.value} class="cursor-pointer">{item.label}</Label>
|
||||
</div>
|
||||
{/each}
|
||||
</RadioGroup.Root>
|
||||
</div>
|
||||
{#if $landingPageSettings.header.banner.type === 'video'}
|
||||
<div class="mb-5 mt-3 gap-2">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.actions.link')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.video')}
|
||||
bind:value={$landingPageSettings.header.banner.video}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.about.select_image')}
|
||||
className="mt-3"
|
||||
onClick={() => widgetControl('banner')}
|
||||
/>
|
||||
{/if}
|
||||
{#if $landingPageSettings.header.banner.image && $landingPageSettings.header.banner.type === 'image'}
|
||||
<img alt="bannerImage" src={$landingPageSettings.header.banner.image} class="mt-2 w-full rounded-md" />
|
||||
{/if}
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
bind:checked={$landingPageSettings.header.banner.show}
|
||||
onCheckedChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.header.banner.show
|
||||
? $t('settings.landing_page.actions.show_banner')
|
||||
: $t('settings.landing_page.actions.hide_banner')}
|
||||
</Label>
|
||||
</div>
|
||||
{#if $handleOpenWidget.open && widgetKey === 'banner'}
|
||||
<UploadWidget
|
||||
bind:imageURL={$landingPageSettings.header.banner.image}
|
||||
onchange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- background -->
|
||||
|
||||
<div class="mt-4">
|
||||
<p class="mb-4">{$t('settings.landing_page.background.title')}</p>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.about.select_image')}
|
||||
className="mt-3"
|
||||
onClick={() => widgetControl('background')}
|
||||
/>
|
||||
|
||||
{#if $landingPageSettings.header.background?.image}
|
||||
<img
|
||||
alt="backgroundImage"
|
||||
src={$landingPageSettings.header.background.image}
|
||||
class="mt-2 w-full rounded-md"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="mt-2 flex items-center space-x-2">
|
||||
<Switch
|
||||
checked={$landingPageSettings.header.background?.show}
|
||||
onCheckedChange={() => {
|
||||
hasUnsavedChanges = true;
|
||||
if (!$landingPageSettings.header.background) {
|
||||
$landingPageSettings.header.background = {
|
||||
image: '',
|
||||
show: true
|
||||
};
|
||||
} else {
|
||||
$landingPageSettings.header.background.show = !$landingPageSettings.header.background.show;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.header.background?.show
|
||||
? $t('settings.landing_page.background.show_background')
|
||||
: $t('settings.landing_page.background.hide_background')}
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
{#if $handleOpenWidget.open && widgetKey === 'background'}
|
||||
<UploadWidget
|
||||
imageURL={$landingPageSettings.header.background?.image}
|
||||
onchange={(imgUrl) => {
|
||||
hasUnsavedChanges = true;
|
||||
|
||||
if (!$landingPageSettings.header.background) {
|
||||
$landingPageSettings.header.background = {
|
||||
image: imgUrl,
|
||||
show: true
|
||||
};
|
||||
} else {
|
||||
$landingPageSettings.header.background.image = imgUrl;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.about.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.aboutUs.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.aboutUs.show
|
||||
? $t('settings.landing_page.show_section')
|
||||
: $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.about.title')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.aboutUs.title}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="mb-5"
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label={$t('settings.landing_page.about.content')}
|
||||
labelClassName="font-light"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.content')}
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.aboutUs.content}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p class="font-bold">{$t('settings.landing_page.about.upload_an_image')}</p>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.about.select_image')}
|
||||
className="mt-3"
|
||||
onClick={() => widgetControl('about-us')}
|
||||
/>
|
||||
{#if $landingPageSettings.aboutUs.imageUrl}
|
||||
<img alt="About us" src={$landingPageSettings.aboutUs.imageUrl} class="mt-2 w-full rounded-md" />
|
||||
{/if}
|
||||
{#if $handleOpenWidget.open && widgetKey === 'about-us'}
|
||||
<UploadWidget bind:imageURL={$landingPageSettings.aboutUs.imageUrl} />
|
||||
{/if}
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.courses.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.courses.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.courses.show
|
||||
? $t('settings.landing_page.courses.show_section')
|
||||
: $t('settings.landing_page.courses.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.courses.title')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.courses.title}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.courses.title_highlight')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
|
||||
bind:value={$landingPageSettings.courses.titleHighlight}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<TextArea
|
||||
label={$t('settings.landing_page.courses.subtitle')}
|
||||
labelClassName="font-light"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
|
||||
bind:value={$landingPageSettings.courses.subtitle}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.faq.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.faq.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.faq.show
|
||||
? $t('settings.landing_page.faq.show_section')
|
||||
: $t('settings.landing_page.faq.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.faq.title')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.faq.title}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="mb-5"
|
||||
/>
|
||||
{#each $landingPageSettings.faq.questions as item (item.id)}
|
||||
<div class="mb-3">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.faq.question')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_question_here')}
|
||||
bind:value={item.title}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="mb-5"
|
||||
/>
|
||||
<TextArea
|
||||
label={$t('settings.landing_page.faq.answer')}
|
||||
labelClassName="font-light"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_answer_here')}
|
||||
bind:value={item.content}
|
||||
className="mb-5"
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<IconButton onClick={() => deleteFaq(item.id)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
{/each}
|
||||
{#if creatingNewQuestion}
|
||||
<TextField
|
||||
label={$t('settings.landing_page.faq.question')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_question_here')}
|
||||
bind:value={newQuestion.title}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="mb-5"
|
||||
/>
|
||||
<TextArea
|
||||
label={$t('settings.landing_page.faq.answer')}
|
||||
labelClassName="font-light"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_answer_here')}
|
||||
bind:value={newQuestion.content}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="mb-5"
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<div class="flex items-center gap-2">
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.faq.save')}
|
||||
onClick={saveNewFAQ}
|
||||
/>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.faq.cancel')}
|
||||
onClick={cancelNewFAQ}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.faq.button')}
|
||||
onClick={createNewFaq}
|
||||
/>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.contact_us.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.contact.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.contact.show
|
||||
? $t('settings.landing_page.contact_us.show_section')
|
||||
: $t('settings.landing_page.contact_us.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.contact_us.title')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.contact.title}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="mb-5"
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.contact_us.title_highlight')}
|
||||
labelClassName="font-light"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.highlight')}
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.contact.titleHighlight}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<TextArea
|
||||
label={$t('settings.landing_page.contact_us.subtitle')}
|
||||
labelClassName="font-light"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
|
||||
className="mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.contact.subtitle}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.contact_us.address')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.address')}
|
||||
className="mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.contact.address}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.contact_us.phone_number')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.number')}
|
||||
className="mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.contact.phone}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.contact_us.email')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.email')}
|
||||
className="mt-3 mb-5"
|
||||
bind:value={$landingPageSettings.contact.email}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.mailing_list.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch
|
||||
bind:checked={$landingPageSettings.mailinglist.show}
|
||||
onCheckedChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.mailinglist.show
|
||||
? $t('settings.landing_page.mailing_list.show_section')
|
||||
: $t('settings.landing_page.mailing_list.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.mailing_list.title')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_title')}
|
||||
bind:value={$landingPageSettings.mailinglist.title}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="mb-5"
|
||||
/>
|
||||
<TextArea
|
||||
label={$t('settings.landing_page.mailing_list.subtitle')}
|
||||
labelClassName="font-light"
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.write_your_subtitle')}
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.mailinglist.subtitle}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
isAIEnabled={true}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label={$t('settings.landing_page.mailing_list.button_label')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.label')}
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.mailinglist.buttonLabel}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.custom_links.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.customLinks.show} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.customLinks.show
|
||||
? $t('settings.landing_page.custom_links.show_links')
|
||||
: $t('settings.landing_page.custom_links.hide_links')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<p class="mb-4 text-sm text-gray-600">
|
||||
{$t('settings.landing_page.custom_links.description')}
|
||||
</p>
|
||||
|
||||
{#each $landingPageSettings.customLinks.links as link (link.id)}
|
||||
<div class="mb-4 rounded-lg border border-gray-200 p-4">
|
||||
<div class="mb-3 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.custom_links.label')}
|
||||
placeholder={$t('settings.landing_page.custom_links.label_placeholder')}
|
||||
bind:value={link.label}
|
||||
className="mb-3"
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.custom_links.url')}
|
||||
placeholder={$t('settings.landing_page.custom_links.url_placeholder')}
|
||||
bind:value={link.url}
|
||||
className="mb-3"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" bind:checked={link.openInNewTab} class="mr-2" />
|
||||
<span class="text-sm">{$t('settings.landing_page.custom_links.new_tab')}</span>
|
||||
</label>
|
||||
<IconButton onClick={() => deleteCustomLink(link.id)}>
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if creatingNewCustomLink}
|
||||
<div class="mb-4 rounded-lg border border-gray-200 bg-gray-50 p-4">
|
||||
<div class="mb-3 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.custom_links.label')}
|
||||
placeholder={$t('settings.landing_page.custom_links.label_placeholder')}
|
||||
bind:value={newCustomLink.label}
|
||||
className="mb-3"
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.custom_links.url')}
|
||||
placeholder={$t('settings.landing_page.custom_links.url_placeholder')}
|
||||
bind:value={newCustomLink.url}
|
||||
className="mb-3"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" bind:checked={newCustomLink.openInNewTab} class="mr-2" />
|
||||
<span class="text-sm">{$t('settings.landing_page.custom_links.new_tab')}</span>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.custom_links.save')}
|
||||
onClick={saveNewCustomLink}
|
||||
className="text-sm px-3 py-1"
|
||||
/>
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.custom_links.cancel')}
|
||||
onClick={cancelNewCustomLink}
|
||||
className="text-sm px-3 py-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<PrimaryButton
|
||||
variant={VARIANTS.OUTLINED}
|
||||
label={$t('settings.landing_page.custom_links.add')}
|
||||
onClick={createNewCustomLink}
|
||||
className="mt-2"
|
||||
/>
|
||||
{/if}
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.landing_page.footer.heading')}</SectionTitle>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={$landingPageSettings.footer.show} onCheckedChange={() => (hasUnsavedChanges = true)} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{$landingPageSettings.footer.show
|
||||
? $t('settings.landing_page.footer.show_section')
|
||||
: $t('settings.landing_page.footer.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-4 lg:mt-0">
|
||||
<TextField
|
||||
label={$t('settings.landing_page.footer.facebook')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.facebook')}
|
||||
bind:value={$landingPageSettings.footer.facebook}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="mb-5"
|
||||
/>
|
||||
<!-- <TextField
|
||||
label="Instagram"
|
||||
placeholder="Write your Instagram link here"
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.footer.instagram}
|
||||
/> -->
|
||||
<TextField
|
||||
label={$t('settings.landing_page.footer.twitter')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.twitter')}
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.footer.twitter}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.landing_page.footer.linkedin')}
|
||||
placeholder={$t('course.navItem.lessons.exercises.all_exercises.linkedin')}
|
||||
className="mb-5"
|
||||
bind:value={$landingPageSettings.footer.linkedin}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<div class="desktop sticky bottom-12 z-[120] float-right mr-2">
|
||||
<PrimaryButton
|
||||
label={$t('settings.landing_page.save_changes')}
|
||||
isLoading={isSaving}
|
||||
isDisabled={isSaving}
|
||||
onClick={handleSave}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
|
||||
<div
|
||||
class="mobile
|
||||
absolute bottom-8 right-6 z-[120]"
|
||||
>
|
||||
<span>
|
||||
<IconButton onClick={handleSave} disabled={isSaving}>
|
||||
<SaveIcon size={16} />
|
||||
</IconButton>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@media screen and (min-width: 769px) {
|
||||
.desktop {
|
||||
display: block;
|
||||
}
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 768px) {
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
.mobile {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,41 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
sm?: number;
|
||||
md?: number;
|
||||
lg?: number;
|
||||
class?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { sm = 8, md = 8, lg = 8, class: className = '', children }: Props = $props();
|
||||
|
||||
const getColSpan = (cols: number) => {
|
||||
const spanMap = {
|
||||
1: 'col-span-1',
|
||||
2: 'col-span-2',
|
||||
3: 'col-span-3',
|
||||
4: 'col-span-4',
|
||||
5: 'col-span-5',
|
||||
6: 'col-span-6',
|
||||
7: 'col-span-7',
|
||||
8: 'col-span-8',
|
||||
9: 'col-span-9',
|
||||
10: 'col-span-10',
|
||||
11: 'col-span-11',
|
||||
12: 'col-span-12',
|
||||
13: 'col-span-13',
|
||||
14: 'col-span-14',
|
||||
15: 'col-span-15',
|
||||
16: 'col-span-16'
|
||||
};
|
||||
return spanMap[cols] || 'col-span-full';
|
||||
};
|
||||
|
||||
const smClass = getColSpan(sm);
|
||||
const mdClass = `md:${getColSpan(md)}`;
|
||||
const lgClass = `lg:${getColSpan(lg)}`;
|
||||
</script>
|
||||
|
||||
<div class="{smClass} {mdClass} {lgClass} {className} grid">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
class?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { class: className = '', children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="w-full {className}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,13 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
id?: string;
|
||||
class?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { id, class: className = '', children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div {id} class="lg:grid-cols-16 grid grid-cols-1 gap-5 px-3 {className}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -1,3 +0,0 @@
|
||||
export { default as Grid } from './Grid.svelte';
|
||||
export { default as Row } from './Row.svelte';
|
||||
export { default as Column } from './Column.svelte';
|
||||
@@ -1,353 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import ZapIcon from '@lucide/svelte/icons/zap';
|
||||
import debounce from 'lodash/debounce';
|
||||
import ColorPicker from 'svelte-awesome-color-picker';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { supabase } from '$lib/utils/functions/supabase';
|
||||
import { snackbar } from '$lib/components/Snackbar/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { updateOrgNameValidation } from '$lib/utils/functions/validator';
|
||||
import { currentOrg, currentOrgPath, isFreePlan } from '$lib/utils/store/org';
|
||||
import { injectCustomTheme, setCustomTheme, setTheme } from '$lib/utils/functions/theme';
|
||||
|
||||
import { Row, Grid, Column } from './Layout';
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
|
||||
let avatar = $state<string | undefined>();
|
||||
let hasUnsavedChanges = $state(false);
|
||||
|
||||
type Error = {
|
||||
orgName: string;
|
||||
};
|
||||
|
||||
let errors: Error = $state({
|
||||
orgName: ''
|
||||
});
|
||||
let loading = $state(false);
|
||||
let hex = $state('');
|
||||
|
||||
$effect(() => {
|
||||
const theme = $currentOrg.theme;
|
||||
|
||||
// if not a custom theme, don't update hex
|
||||
if (!theme || theme.includes('theme-')) return;
|
||||
|
||||
// only update hex with value of `theme` when theme changes not when HEX changes
|
||||
untrack(() => {
|
||||
hex = theme;
|
||||
});
|
||||
});
|
||||
|
||||
const themes = {
|
||||
rose: 'theme-rose',
|
||||
green: 'theme-green',
|
||||
orange: 'theme-orange',
|
||||
violet: 'theme-violet',
|
||||
default: ''
|
||||
};
|
||||
|
||||
const saveTheme = debounce(async (theme) => {
|
||||
const { error, data } = await supabase.from('organization').update({ theme }).match({ id: $currentOrg.id });
|
||||
|
||||
console.log('Debounced update theme', data);
|
||||
|
||||
if (error) {
|
||||
snackbar.error('Failed to update theme: ' + error.message);
|
||||
}
|
||||
}, 700);
|
||||
|
||||
function handleChangeTheme(t = '') {
|
||||
return async () => {
|
||||
document.body.className = document.body.className
|
||||
.split(' ')
|
||||
.filter((c) => !c.includes('theme'))
|
||||
.join(' ')
|
||||
.concat(t ? ' ' : '', t);
|
||||
$currentOrg.theme = t;
|
||||
|
||||
hex = '';
|
||||
|
||||
setTheme(t);
|
||||
|
||||
saveTheme(t);
|
||||
};
|
||||
}
|
||||
|
||||
async function handleCustomTheme() {
|
||||
if (!hex) return;
|
||||
|
||||
injectCustomTheme(hex);
|
||||
setCustomTheme('theme-custom');
|
||||
|
||||
$currentOrg.theme = hex;
|
||||
|
||||
saveTheme(hex);
|
||||
}
|
||||
|
||||
async function handleUpdate() {
|
||||
errors = updateOrgNameValidation($currentOrg.name) as Error;
|
||||
|
||||
if (Object.values(errors).length) {
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
loading = true;
|
||||
|
||||
const updates: Record<string, string> = {
|
||||
name: $currentOrg.name
|
||||
};
|
||||
|
||||
if (avatar) {
|
||||
const filename = `user/${$currentOrg.name + Date.now()}.webp`;
|
||||
|
||||
const { data } = await supabase.storage.from('avatars').upload(filename, avatar, {
|
||||
cacheControl: '3600',
|
||||
upsert: false
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const { data: response } = supabase.storage.from('avatars').getPublicUrl(filename);
|
||||
|
||||
updates.avatarUrl = response.publicUrl;
|
||||
$currentOrg.avatarUrl = response.publicUrl;
|
||||
}
|
||||
avatar = undefined;
|
||||
}
|
||||
|
||||
let { error } = await supabase.from('organization').update(updates).match({ id: $currentOrg.id });
|
||||
|
||||
currentOrg.update((_currentOrg) => ({
|
||||
..._currentOrg,
|
||||
...updates
|
||||
}));
|
||||
|
||||
snackbar.success('snackbar.course_settings.success.update_successful');
|
||||
hasUnsavedChanges = false;
|
||||
if (error) throw error;
|
||||
} catch (error) {
|
||||
let message = error as string;
|
||||
if (message.includes('profile_username_key')) {
|
||||
message = $t('snackbar.lms.error.username_exists');
|
||||
}
|
||||
|
||||
snackbar.error(`${$t('snackbar.update_failed')}: ${message}`);
|
||||
loading = false;
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function gotoSettings(pathname) {
|
||||
goto(`${$currentOrgPath}/settings${pathname}`);
|
||||
}
|
||||
|
||||
let isCustomTheme = $derived(hex && !hex.includes('theme-'));
|
||||
</script>
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
|
||||
<Grid class="mt-5 w-full">
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>
|
||||
{$t('settings.organization.organization_profile.heading')}
|
||||
</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 flex flex-col items-center lg:mt-0 lg:items-start">
|
||||
<TextField
|
||||
label={$t('settings.organization.organization_profile.organization_name')}
|
||||
bind:value={$currentOrg.name}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
className="w-full lg:w-60 mb-5"
|
||||
errorMessage={errors.orgName}
|
||||
/>
|
||||
<UploadImage
|
||||
bind:avatar
|
||||
src={$currentOrg.avatarUrl}
|
||||
shape="rounded-md"
|
||||
widthHeight="w-24 h-24"
|
||||
change={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
|
||||
<PrimaryButton
|
||||
label={$t('settings.organization.organization_profile.update_organization')}
|
||||
className="px-6 py-3 lg:mr-5 mt-5"
|
||||
isLoading={loading}
|
||||
isDisabled={loading}
|
||||
onClick={handleUpdate}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="border-bottom-c relative flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.organization.organization_profile.theme.heading')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="lg:mt-0 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.theme.sub_heading')}
|
||||
</h4>
|
||||
|
||||
<div class="flex items-center gap-5">
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.default &&
|
||||
'border-[#1d4ee2]'} flex h-fit items-center justify-center"
|
||||
onclick={handleChangeTheme(themes.default)}
|
||||
aria-label="Default blue theme"
|
||||
>
|
||||
<div class="m-1 h-6 w-6 rounded-full bg-[#1d4ee2] md:h-6 md:w-6"></div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.rose &&
|
||||
'border-[#be1241]'} flex h-fit items-center justify-center"
|
||||
onclick={handleChangeTheme(themes.rose)}
|
||||
aria-label="Rose theme"
|
||||
>
|
||||
<div class="m-1 h-6 w-6 rounded-full bg-[#be1241] md:h-6 md:w-6"></div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.green &&
|
||||
'border-[#0c891b]'} flex h-fit items-center justify-center"
|
||||
onclick={handleChangeTheme(themes.green)}
|
||||
aria-label="Green theme"
|
||||
>
|
||||
<div class="m-1 h-6 w-6 rounded-full bg-[#0c891b] md:h-6 md:w-6"></div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.orange &&
|
||||
'border-[#cc4902]'} flex h-fit items-center justify-center"
|
||||
onclick={handleChangeTheme(themes.orange)}
|
||||
aria-label="Orange theme"
|
||||
>
|
||||
<div class="m-1 h-6 w-6 rounded-full bg-[#cc4902] md:h-6 md:w-6"></div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="rounded-full border-2 {$currentOrg.theme === themes.violet &&
|
||||
'border-[#cf00ce]'} flex h-fit items-center justify-center"
|
||||
onclick={handleChangeTheme(themes.violet)}
|
||||
aria-label="Violet theme"
|
||||
>
|
||||
<div class="m-1 h-6 w-6 rounded-full bg-[#cf00ce] md:h-6 md:w-6"></div>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="h-auto w-fit border-2 {isCustomTheme
|
||||
? 'border-primary-700'
|
||||
: 'dark:border-neutral-700'} group relative rounded-full"
|
||||
>
|
||||
<!-- plus icon positioned over the color picker -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-0 flex items-center justify-center transition-opacity duration-200"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6 text-{isCustomTheme ? 'white' : 'black'} z-10 opacity-100 dark:text-white"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<ColorPicker position="responsive" label="" bind:hex on:input={handleCustomTheme} />
|
||||
</div>
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>
|
||||
{$t('settings.organization.organization_profile.customize_lms.heading')}
|
||||
</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="lg:mt-0 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.customize_lms.sub_heading')}
|
||||
</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.customize_lms.body')}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSettings('/customize-lms')}
|
||||
>
|
||||
{$t('settings.organization.organization_profile.customize_lms.button')}
|
||||
</PrimaryButton>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.organization.organization_profile.custom_domain.heading')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="lg:mt-0 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.custom_domain.sub_heading')}
|
||||
</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.custom_domain.body')}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSettings('/domains')}
|
||||
>
|
||||
{#if $isFreePlan}
|
||||
<ZapIcon size={16} class="filled" />
|
||||
{/if}
|
||||
{$t('settings.organization.organization_profile.custom_domain.button')}
|
||||
</PrimaryButton>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}
|
||||
><SectionTitle>{$t('settings.organization.organization_profile.team.heading')}</SectionTitle></Column
|
||||
>
|
||||
<Column sm={8} md={8} lg={8}>
|
||||
<h4 class="lg:mt-0 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.team.sub_heading')}
|
||||
</h4>
|
||||
<p class="text-sm text-gray-500 dark:text-white">
|
||||
{$t('settings.organization.organization_profile.team.body')}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
className="my-7 py-5 px-10 flex items-center gap-2 justify-center"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => gotoSettings('/teams')}
|
||||
>
|
||||
{#if $isFreePlan}
|
||||
<ZapIcon size={16} class="filled" />
|
||||
{/if}
|
||||
{$t('settings.organization.organization_profile.team.button')}
|
||||
</PrimaryButton>
|
||||
</Column>
|
||||
</Row>
|
||||
</Grid>
|
||||
|
||||
<style>
|
||||
:global(.dark) {
|
||||
--cp-text-color: #fff;
|
||||
--cp-border-color: white;
|
||||
--cp-text-color: white;
|
||||
--cp-input-color: #555;
|
||||
--cp-button-hover-color: #777;
|
||||
}
|
||||
|
||||
:global(.dark .alpha) {
|
||||
background: #333 !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,165 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { TLocale } from '@cio/db/types';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { CircleCheckBig } from '@lucide/svelte';
|
||||
import { profileApi } from '$lib/features/auth/api/profile.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { Row, Grid, Column } from './Layout';
|
||||
|
||||
import SectionTitle from '../SectionTitle.svelte';
|
||||
import LanguagePicker from './LanguagePicker.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import UnsavedChanges from '$lib/components/UnsavedChanges/index.svelte';
|
||||
|
||||
let avatar = $state<string | File | undefined>();
|
||||
let hasLangChanged = $state(false);
|
||||
let locale = $derived<TLocale | undefined>($profile.locale || undefined);
|
||||
let hasUnsavedChanges = $state(false);
|
||||
let email = $derived($profile.email || '');
|
||||
let isChangingEmail = $state(false);
|
||||
let emailChangeInitiated = $state(false);
|
||||
|
||||
async function handleUpdate() {
|
||||
await profileApi.submit(
|
||||
{
|
||||
fullname: $profile.fullname,
|
||||
username: $profile.username,
|
||||
locale,
|
||||
avatar
|
||||
},
|
||||
$profile,
|
||||
hasLangChanged,
|
||||
locale
|
||||
);
|
||||
|
||||
if (profileApi.success) {
|
||||
hasUnsavedChanges = false;
|
||||
avatar = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleEmailChange() {
|
||||
if (email === $profile.email) {
|
||||
return;
|
||||
}
|
||||
|
||||
await profileApi.changeEmail({
|
||||
newEmail: email,
|
||||
callbackURL: window.location.href
|
||||
});
|
||||
|
||||
if (profileApi.success) {
|
||||
isChangingEmail = false;
|
||||
emailChangeInitiated = true;
|
||||
// Reset email field to current email after successful change request
|
||||
email = $profile.email || '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<UnsavedChanges bind:hasUnsavedChanges />
|
||||
|
||||
<Grid class="mt-0 w-full md:mt-5">
|
||||
<Row class="border-bottom-c flex flex-col items-center lg:flex-row lg:items-start lg:py-7 ">
|
||||
<Column sm={4} md={8} lg={4} class="mt-2 md:mt-0">
|
||||
<SectionTitle>{$t('settings.profile.profile_picture.heading')}</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0">
|
||||
<UploadImage
|
||||
bind:avatar
|
||||
src={$profile.avatarUrl}
|
||||
widthHeight="w-16 h-16 lg:w-24 lg:h-24"
|
||||
isDisabled={profileApi.isLoading}
|
||||
change={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
<Row class="border-bottom-c flex flex-col py-7 lg:flex-row">
|
||||
<Column sm={4} md={4} lg={4}>
|
||||
<SectionTitle>{$t('settings.profile.personal_information.heading')}</SectionTitle>
|
||||
</Column>
|
||||
<Column sm={8} md={8} lg={8} class="mt-2 lg:mt-0">
|
||||
<TextField
|
||||
label={$t('settings.profile.personal_information.full_name')}
|
||||
bind:value={$profile.fullname}
|
||||
className="w-full lg:w-60 mb-4"
|
||||
errorMessage={profileApi.errors.fullname ? $t(profileApi.errors.fullname) : undefined}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<TextField
|
||||
label={$t('settings.profile.personal_information.username')}
|
||||
bind:value={$profile.username}
|
||||
className="w-full lg:w-60 mb-4"
|
||||
errorMessage={profileApi.errors.username ? $t(profileApi.errors.username) : undefined}
|
||||
onChange={() => (hasUnsavedChanges = true)}
|
||||
/>
|
||||
<div class="mb-4 w-full lg:w-60">
|
||||
<TextField
|
||||
label={$t('settings.profile.personal_information.email')}
|
||||
bind:value={email}
|
||||
className="w-full"
|
||||
type="email"
|
||||
isDisabled={profileApi.isLoading}
|
||||
errorMessage={profileApi.errors.newEmail ? $t(profileApi.errors.newEmail) : undefined}
|
||||
onChange={() => {
|
||||
if (email !== $profile.email) {
|
||||
isChangingEmail = true;
|
||||
emailChangeInitiated = false;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if isChangingEmail && email !== $profile.email}
|
||||
<div class="mt-2 flex gap-2">
|
||||
<PrimaryButton
|
||||
label={$t('settings.profile.personal_information.confirm')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
className="text-sm"
|
||||
isLoading={profileApi.isLoading}
|
||||
isDisabled={profileApi.isLoading}
|
||||
onClick={handleEmailChange}
|
||||
/>
|
||||
<PrimaryButton
|
||||
label={$t('settings.profile.personal_information.cancel')}
|
||||
variant={VARIANTS.NONE}
|
||||
className="text-sm text-primary-700"
|
||||
isDisabled={profileApi.isLoading}
|
||||
onClick={() => {
|
||||
email = $profile.email || '';
|
||||
isChangingEmail = false;
|
||||
emailChangeInitiated = false;
|
||||
profileApi.errors.newEmail = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if emailChangeInitiated}
|
||||
<div
|
||||
class="ml-2 mt-2 flex w-4/5 items-center gap-2 rounded-md border border-gray-200 p-2 text-sm text-amber-500"
|
||||
>
|
||||
<CircleCheckBig />
|
||||
<p>{$t('settings.profile.personal_information.email_change_verification_note')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<LanguagePicker
|
||||
change={() => (hasUnsavedChanges = true)}
|
||||
bind:hasLangChanged
|
||||
bind:value={locale}
|
||||
className="w-full lg:w-60 mb-4"
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
|
||||
<div class="m-5 flex w-full items-center gap-2 lg:justify-center">
|
||||
<PrimaryButton
|
||||
label={$t('settings.profile.update_profile')}
|
||||
variant={VARIANTS.CONTAINED_DARK}
|
||||
className="mr-5 w-fit"
|
||||
isLoading={profileApi.isLoading}
|
||||
onClick={handleUpdate}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
@@ -1,101 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import * as Tabs from '@cio/ui/base/tabs';
|
||||
|
||||
import Profile from './Profile.svelte';
|
||||
import Billing from './Billing.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import OrgSettings from './OrgSettings.svelte';
|
||||
import Integrations from './Integrations.svelte';
|
||||
import LandingpageSettings from './LandingpageSettings.svelte';
|
||||
// import Account from './Account.svelte';
|
||||
|
||||
import { isOrgAdmin } from '$lib/utils/store/org';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface TabItem {
|
||||
key: number;
|
||||
label: string;
|
||||
tabKey: string;
|
||||
href: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
|
||||
let query = $derived(new URLSearchParams(page.url.search));
|
||||
let tabKey = $derived(query.get('tab') || '');
|
||||
|
||||
let tabs: TabItem[] = $derived([
|
||||
{
|
||||
key: 0,
|
||||
label: $t('settings.tabs.profile_tab'),
|
||||
tabKey: '',
|
||||
href: page.url.pathname,
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
label: $t('settings.tabs.organization_tab'),
|
||||
tabKey: 'org',
|
||||
href: `${page.url.pathname}?tab=org`,
|
||||
disabled: !$isOrgAdmin
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
label: $t('settings.tabs.landing_page_tab'),
|
||||
tabKey: 'landingpage',
|
||||
href: `${page.url.pathname}?tab=landingpage`,
|
||||
disabled: !$isOrgAdmin
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
label: $t('settings.tabs.billing_tab'),
|
||||
tabKey: 'billing',
|
||||
href: `${page.url.pathname}?tab=billing`,
|
||||
disabled: !$isOrgAdmin
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
label: $t('settings.tabs.integrations_tab'),
|
||||
tabKey: 'integrations',
|
||||
href: `${page.url.pathname}?tab=integrations`,
|
||||
disabled: false
|
||||
}
|
||||
]);
|
||||
|
||||
function onTabChange(value: string) {
|
||||
const tab = tabs.find((t) => t.tabKey === value);
|
||||
if (tab) {
|
||||
goto(tab.href);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Tabs.Root value={tabKey} onValueChange={onTabChange} class="w-full p-0">
|
||||
<Tabs.List class="w-full">
|
||||
{#each tabs as tab}
|
||||
<Tabs.Trigger value={tab.tabKey} disabled={tab.disabled}>
|
||||
{tab.label}
|
||||
</Tabs.Trigger>
|
||||
{/each}
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Content value="" class="w-full p-0">
|
||||
<Profile />
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="org" class="w-full p-0">
|
||||
<OrgSettings />
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="landingpage" class="w-full p-0">
|
||||
<LandingpageSettings />
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="billing" class="w-full p-0">
|
||||
<Billing />
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="integrations" class="w-full p-0">
|
||||
<Integrations />
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
@@ -1,57 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
width?: string;
|
||||
padding?: string;
|
||||
className?: string;
|
||||
headerClassName?: string;
|
||||
onClick?: any;
|
||||
isPageNavHidden?: boolean;
|
||||
header?: import('svelte').Snippet;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
width = 'max-w-4xl w-full lg:w-11/12',
|
||||
padding = $bindable('pb-5 px-4'),
|
||||
className = '',
|
||||
headerClassName = '',
|
||||
onClick = () => {},
|
||||
isPageNavHidden,
|
||||
header,
|
||||
children
|
||||
}: Props = $props();
|
||||
|
||||
function handleKeydown(event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
onClick();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div
|
||||
tabindex="0"
|
||||
class="overflow-y-auto {isPageNavHidden
|
||||
? 'h-[calc(100vh-65px)] lg:h-[calc(100vh-127px)]'
|
||||
: 'h-[calc(100vh-127px)]'} mx-auto mt-4 {width} {className} relative {isPageNavHidden ? 'px-4 pb-20' : padding}"
|
||||
onclick={onClick}
|
||||
onkeydown={handleKeydown}
|
||||
>
|
||||
{#if header}
|
||||
<div
|
||||
class="head sticky right-0 flex w-full items-center justify-between px-3 dark:bg-neutral-800 {headerClassName}"
|
||||
>
|
||||
{@render header?.()}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.head {
|
||||
top: 0px;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -1,141 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import Chip from '../Chip/index.svelte';
|
||||
import TextField from '../Form/TextField.svelte';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
overidableStyle?: string;
|
||||
navItems?: any;
|
||||
disableSticky?: boolean;
|
||||
isTitleEditable?: boolean;
|
||||
onEditComplete?: any;
|
||||
paddingClass?: string;
|
||||
hideOnMobile?: boolean;
|
||||
image?: import('svelte').Snippet;
|
||||
widget?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let {
|
||||
title = $bindable(''),
|
||||
overidableStyle = '',
|
||||
navItems = [],
|
||||
disableSticky = false,
|
||||
isTitleEditable = false,
|
||||
onEditComplete = () => {},
|
||||
paddingClass = 'px-2',
|
||||
hideOnMobile = false,
|
||||
image,
|
||||
widget
|
||||
}: Props = $props();
|
||||
|
||||
let enterEditTitleMode = $state(false);
|
||||
|
||||
const dynamicRootClass = $derived(Array.isArray(navItems) && navItems.length > 4 ? 'bring-down' : '');
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="{hideOnMobile &&
|
||||
'hideOnMobile'} header border-b border-gray-200 bg-white dark:border-neutral-600 dark:bg-black {!disableSticky &&
|
||||
'sticky'} {dynamicRootClass}"
|
||||
style={overidableStyle}
|
||||
>
|
||||
<div
|
||||
class="{hideOnMobile
|
||||
? 'hidden lg:flex'
|
||||
: ''} {paddingClass} m-auto flex min-h-[61px] max-w-4xl items-center justify-between"
|
||||
>
|
||||
{#if !!title}
|
||||
{#if isTitleEditable}
|
||||
{#if !enterEditTitleMode}
|
||||
<button class="w-full" onclick={() => (enterEditTitleMode = true)}>
|
||||
<h4 class="editable-title overflow-ellipsis rounded-md px-3 hover:bg-gray-100 dark:bg-black">
|
||||
{title}
|
||||
</h4>
|
||||
</button>
|
||||
{:else}
|
||||
<TextField
|
||||
bind:value={title}
|
||||
placeholder={$t('course.navItem.settings.course_title')}
|
||||
onChange={() => {
|
||||
enterEditTitleMode = false;
|
||||
|
||||
if (typeof onEditComplete === 'function') {
|
||||
onEditComplete();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="flex w-full items-center" {title}>
|
||||
{@render image?.()}
|
||||
<h4 class="{image ? 'ml-2' : ''} truncate whitespace-nowrap text-lg font-semibold dark:text-white">
|
||||
{title}
|
||||
</h4>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if Array.isArray(navItems) && navItems.length}
|
||||
<div class="flex items-center justify-evenly">
|
||||
{#each navItems as item}
|
||||
<a
|
||||
class="relative mr-5 flex items-center pb-2 text-sm {item.isActive
|
||||
? 'text-primary-700'
|
||||
: 'text-black dark:text-white'} hover:no-underline"
|
||||
href={item.href}
|
||||
>
|
||||
{item.label}
|
||||
|
||||
{#if typeof item.badgeValue === 'number'}
|
||||
<Chip value={item.badgeValue} className="ml-1 bg-gray-300 dark:text-black" />
|
||||
{/if}
|
||||
<span
|
||||
class="bg-primary-700 absolute bottom-0 left-0 h-1 transition-all duration-500 ease-in-out {item.isActive
|
||||
? 'w-full'
|
||||
: 'w-0'}"
|
||||
></span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{@render widget?.()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
min-height: 61px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header.sticky {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.header.bring-down {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.editable-title {
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
/* justify-content: center; */
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.title {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.hideOnMobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
interface Props {
|
||||
isOrg?: boolean;
|
||||
}
|
||||
|
||||
let { isOrg = false }: Props = $props();
|
||||
|
||||
function handleClick() {
|
||||
if (!isOrg) {
|
||||
return goto('/');
|
||||
}
|
||||
|
||||
return goto('https://classroomio.com');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="root flex w-screen flex-col items-center justify-center">
|
||||
<img src="https://brand.cdn.clsrio.com/404.png" alt="classroomio_error_image" />
|
||||
<div class="flex w-4/5 flex-col items-center justify-center gap-3 lg:w-2/5 dark:text-white">
|
||||
<p class="mb-5 text-4xl font-semibold dark:text-white">
|
||||
{#if isOrg}
|
||||
Organization doesn't exist!
|
||||
{:else}
|
||||
Page not found
|
||||
{/if}
|
||||
</p>
|
||||
<p class=" mb-5 text-center text-lg text-gray-700 dark:text-white">
|
||||
The page you are looking for doesn't exist or has been moved. Please go back to the homepage.
|
||||
</p>
|
||||
<PrimaryButton variant={VARIANTS.CONTAINED_DARK} label="Go Home" className="text-lg" onClick={handleClick} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
height: calc(100vh - 45px);
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user