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
@@ -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,40 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
|
||||
interface Activity {
|
||||
icon: Component;
|
||||
title: string;
|
||||
percentage: number | string;
|
||||
description: string;
|
||||
hidePercentage?: boolean;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
activity: Activity;
|
||||
}
|
||||
|
||||
let { activity }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-col items-center justify-center gap-5 rounded-xl border p-3 md:flex-row md:p-5 dark:border-neutral-600"
|
||||
>
|
||||
<div class="bg-primary-200 w-fit rounded-full p-4 text-black">
|
||||
<activity.icon />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-center text-sm font-medium text-gray-600 md:text-left dark:text-gray-200">
|
||||
{activity.title}
|
||||
</p>
|
||||
<p class="text-center text-2xl md:text-left">
|
||||
{activity.percentage}
|
||||
{#if !activity.hidePercentage}
|
||||
%
|
||||
{/if}
|
||||
</p>
|
||||
<p class="text-center text-xs text-gray-500 md:text-left md:text-sm dark:text-gray-300">
|
||||
{activity.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { UserAnalytics } from '$lib/utils/types/analytics';
|
||||
import AlarmClockIcon from '@lucide/svelte/icons/alarm-clock';
|
||||
import MailIcon from '@lucide/svelte/icons/mail';
|
||||
|
||||
interface Props {
|
||||
user: UserAnalytics['user'];
|
||||
}
|
||||
|
||||
let { user }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="rounded-md border p-5 dark:border-neutral-600">
|
||||
<div class="flex w-full flex-col items-center justify-start gap-4 text-start md:flex-row">
|
||||
<Avatar.Root class="h-16 w-16">
|
||||
<Avatar.Image
|
||||
src={user.avatarUrl ? user.avatarUrl : '/logo-192.png'}
|
||||
alt={user.fullName ? user.fullName : 'User'}
|
||||
/>
|
||||
<Avatar.Fallback>{user.fullName ? user.fullName : 'User'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class="flex flex-col space-y-2">
|
||||
<p class="text-center text-2xl md:text-left dark:text-white">
|
||||
{user.fullName}
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col items-center gap-1 md:flex-row md:gap-4">
|
||||
<p class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
|
||||
<MailIcon size={16} />
|
||||
{user.email}
|
||||
</p>
|
||||
|
||||
<p class="flex items-center gap-1 text-sm text-gray-500 dark:text-gray-300">
|
||||
<AlarmClockIcon size={16} />
|
||||
{$t('analytics.last_seen')}:
|
||||
<span class="italic">
|
||||
{user.lastSeen ? calDateDiff(user.lastSeen) : $t('analytics.a_while_ago')}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,12 +0,0 @@
|
||||
<script>
|
||||
import { Skeleton } from '@cio/ui/base/skeleton';
|
||||
</script>
|
||||
|
||||
<div class="mx-auto max-w-4xl pt-14">
|
||||
<Skeleton class="h-40 w-full" />
|
||||
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Skeleton class="h-40 w-[270px]" />
|
||||
<Skeleton class="h-40 w-[270px]" />
|
||||
<Skeleton class="h-40 w-[270px]" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -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,34 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
disableCenteredContent?: boolean;
|
||||
className?: string;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { disableCenteredContent = false, className = '', children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="root {!className.includes('opacity') && 'opacity-100'} {!disableCenteredContent && 'center'} {className}">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
position: fixed;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
color: #fff;
|
||||
z-index: 1201;
|
||||
transition: opacity 225ms cubic-bezier(0.4, 0, 0.2, 1) 0ms;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -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,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-up-right';
|
||||
import { currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { user } from '$lib/utils/store/user';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
isLMS?: boolean;
|
||||
}
|
||||
|
||||
let { className = '', isLMS = false }: Props = $props();
|
||||
|
||||
let href = $derived(isLMS && $user.isLoggedIn ? `${$currentOrgDomain}/home` : $currentOrgDomain);
|
||||
</script>
|
||||
|
||||
<a {href} target="_blank" class="{className} ml-2 hover:no-underline">
|
||||
<Button variant="default">
|
||||
{#if !$isMobile}
|
||||
{#if isLMS}
|
||||
{$t('dashboard.visit_site')}
|
||||
{:else}
|
||||
{$t('settings.subheadings.view_site')}
|
||||
{/if}
|
||||
{/if}
|
||||
<ArrowUpRightIcon class="custom" />
|
||||
</Button>
|
||||
</a>
|
||||
@@ -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,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
code?: string;
|
||||
}
|
||||
|
||||
let { code = '' }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="mb-3 rounded-md bg-gray-200 p-5 dark:bg-gray-500">
|
||||
{@html code}
|
||||
</div>
|
||||
@@ -1,64 +0,0 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import confetti from 'canvas-confetti';
|
||||
import { confettiStore } from './store';
|
||||
|
||||
let myCanvas = $state();
|
||||
let myConfetti;
|
||||
|
||||
function showConfetti(show) {
|
||||
if (!show) return;
|
||||
|
||||
const duration = 5 * 1000;
|
||||
const animationEnd = Date.now() + duration;
|
||||
const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 };
|
||||
|
||||
function randomInRange(min, max) {
|
||||
return Math.random() * (max - min) + min;
|
||||
}
|
||||
|
||||
const interval = setInterval(function () {
|
||||
const timeLeft = animationEnd - Date.now();
|
||||
|
||||
if (timeLeft <= 0) {
|
||||
return clearInterval(interval);
|
||||
}
|
||||
|
||||
const particleCount = 50 * (timeLeft / duration);
|
||||
// since particles fall down, start a bit higher than random
|
||||
myConfetti(
|
||||
Object.assign({}, defaults, {
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 }
|
||||
})
|
||||
);
|
||||
myConfetti(
|
||||
Object.assign({}, defaults, {
|
||||
particleCount,
|
||||
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 }
|
||||
})
|
||||
);
|
||||
}, 250);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
myConfetti = confetti.create(myCanvas, {
|
||||
resize: true,
|
||||
useWorker: true
|
||||
});
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
showConfetti($confettiStore.show);
|
||||
});
|
||||
</script>
|
||||
|
||||
<canvas bind:this={myCanvas}></canvas>
|
||||
|
||||
<style>
|
||||
canvas {
|
||||
position: absolute;
|
||||
width: 60%;
|
||||
height: 70%;
|
||||
}
|
||||
</style>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const confettiStore = writable({
|
||||
show: false
|
||||
});
|
||||
|
||||
export function toggleConfetti() {
|
||||
confettiStore.update((confetti) => ({
|
||||
...confetti,
|
||||
show: !confetti.show
|
||||
}));
|
||||
}
|
||||
@@ -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,13 +0,0 @@
|
||||
export const NAV_IDS = {
|
||||
NEWS_FEED: 'NEWS_FEED',
|
||||
LESSONS: 'LESSONS',
|
||||
ATTENDANCE: 'ATTENDANCE',
|
||||
SUBMISSIONS: 'SUBMISSIONS',
|
||||
MARKS: 'MARKS',
|
||||
CERTIFICATES: 'CERTIFICATES',
|
||||
LANDING_PAGE: 'LANDING_PAGE',
|
||||
PEOPLE: 'PEOPLE',
|
||||
ANALYTICS: 'ANALYTICS',
|
||||
SETTINGS: 'SETTINGS',
|
||||
SECTION: 'SECTION'
|
||||
};
|
||||
@@ -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,46 +0,0 @@
|
||||
<script>
|
||||
import { flip } from 'svelte/animate';
|
||||
import { settings } from './store';
|
||||
import { dndzone } from 'svelte-dnd-action';
|
||||
import GripVerticalIcon from '@lucide/svelte/icons/grip-vertical';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const flipDurationMs = 300;
|
||||
|
||||
function handleDndConsider(e) {
|
||||
$settings.tabs = e.detail.items;
|
||||
}
|
||||
|
||||
function handleDndFinalize(e) {
|
||||
$settings.tabs = e.detail.items;
|
||||
|
||||
dispatch('change');
|
||||
}
|
||||
</script>
|
||||
|
||||
<section
|
||||
use:dndzone={{
|
||||
items: $settings.tabs,
|
||||
flipDurationMs,
|
||||
dropTargetStyle: {
|
||||
border: '2px #1d4ed8 solid',
|
||||
'border-style': 'dashed'
|
||||
}
|
||||
}}
|
||||
onconsider={handleDndConsider}
|
||||
onfinalize={handleDndFinalize}
|
||||
class="flex w-fit gap-1 p-1 md:gap-3"
|
||||
>
|
||||
{#each $settings.tabs as item (item.id)}
|
||||
<div
|
||||
animate:flip={{ duration: flipDurationMs }}
|
||||
class="flex items-center justify-start gap-1 rounded-md bg-slate-100 p-2 text-center text-xs md:gap-2 md:text-base dark:bg-slate-700 dark:text-white"
|
||||
>
|
||||
<GripVerticalIcon size={16} />
|
||||
{$t(item.name)}
|
||||
</div>
|
||||
{/each}
|
||||
</section>
|
||||
@@ -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>
|
||||
@@ -1,19 +0,0 @@
|
||||
import { COURSE_TYPE } from '$lib/utils/types';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const settings = writable({
|
||||
logo: '',
|
||||
course_title: '',
|
||||
course_description: '',
|
||||
grading: false,
|
||||
type: COURSE_TYPE.LIVE_CLASS,
|
||||
allow_new_students: false,
|
||||
tabs: [
|
||||
{ id: 1, name: 'course.navItem.lessons.materials.tabs.note.title' },
|
||||
{ id: 2, name: 'course.navItem.lessons.materials.tabs.slide.title' },
|
||||
{ id: 3, name: 'course.navItem.lessons.materials.tabs.video.title' },
|
||||
{ id: 4, name: 'course.navItem.lessons.materials.tabs.document.title' }
|
||||
],
|
||||
lesson_download: false,
|
||||
is_published: false
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
-71
@@ -1,71 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import { untrack } from 'svelte';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { uploadImage } from '$lib/utils/services/upload';
|
||||
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
setter: (value: any, key: string) => void;
|
||||
}
|
||||
|
||||
let { course = $bindable(), setter }: Props = $props();
|
||||
|
||||
let avatar: File | undefined = $state();
|
||||
let templateUrl = $derived(get(course, 'metadata.certificate.templateUrl', '/images/certificate-template.svg'));
|
||||
let isUploading = $state(false);
|
||||
|
||||
let show = $derived(get(course, `metadata.sectionDisplay.${NAV_ITEM_KEY.CERTIFICATE}`) ?? true);
|
||||
|
||||
async function onTemplateChange(_avatar: File | undefined) {
|
||||
untrack(async () => {
|
||||
if (!_avatar || !course.id) return;
|
||||
isUploading = true;
|
||||
|
||||
const logo = await uploadImage(_avatar);
|
||||
|
||||
if (logo) {
|
||||
templateUrl = logo;
|
||||
setter(templateUrl, 'metadata.certificate.templateUrl');
|
||||
}
|
||||
|
||||
isUploading = false;
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onTemplateChange(avatar);
|
||||
});
|
||||
$effect(() => {
|
||||
setter(show, `metadata.sectionDisplay.${NAV_ITEM_KEY.CERTIFICATE}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="border-bottom-c mt-5">
|
||||
<p class="text-center font-bold">
|
||||
{$t('course.navItem.landing_page.editor.certificate_form.upload_template')}
|
||||
</p>
|
||||
<UploadImage bind:avatar src={templateUrl} shape="rounded-none" bind:isUploading />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
<p class="font-semibold">
|
||||
{$t('course.navItem.landing_page.editor.display_section')}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={show} />
|
||||
<Label class="text-gray-600">
|
||||
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
show?: boolean;
|
||||
children?: import('svelte').Snippet;
|
||||
}
|
||||
|
||||
let { show = true, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<section class="root z-10">
|
||||
{#if show}
|
||||
<div class="relative h-full bg-white dark:bg-black">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style lang="scss">
|
||||
.root {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 300px;
|
||||
max-width: 300px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border-right: 1px solid var(--border-color);
|
||||
background-color: rgb(250, 251, 252);
|
||||
}
|
||||
|
||||
.toggler {
|
||||
right: -15px;
|
||||
z-index: 10;
|
||||
border: 1px solid var(--border-color);
|
||||
top: 50px;
|
||||
height: fit-content;
|
||||
background: var(--border-color);
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
}
|
||||
</style>
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
setter: (value: any, key: string) => void;
|
||||
}
|
||||
|
||||
let { course = $bindable(), setter }: Props = $props();
|
||||
|
||||
let show = $derived(get(course, `metadata.sectionDisplay.${NAV_ITEM_KEY.DESCRIPTION}`) ?? true);
|
||||
|
||||
$effect(() => {
|
||||
setter(show, `metadata.sectionDisplay.${NAV_ITEM_KEY.DESCRIPTION}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<TextEditor content={course.metadata.description} onChange={(content) => setter(content, `metadata.description`)} />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
<p class="font-semibold">
|
||||
{$t('course.navItem.landing_page.editor.display_section')}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={show} />
|
||||
|
||||
<Label class="font-bold">
|
||||
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
setter: (value: any, key: string) => void;
|
||||
}
|
||||
|
||||
let { course = $bindable(), setter }: Props = $props();
|
||||
|
||||
let show = $derived(get(course, `metadata.sectionDisplay.${NAV_ITEM_KEY.GOALS}`) ?? true);
|
||||
|
||||
$effect(() => {
|
||||
setter(show, `metadata.sectionDisplay.${NAV_ITEM_KEY.GOALS}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="">
|
||||
<TextEditor content={course.metadata.goals} onChange={(content) => setter(content, `metadata.goals`)} />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
<p class="font-semibold">
|
||||
{$t('course.navItem.landing_page.editor.display_section')}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={show} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
<script lang="ts">
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { handleOpenWidget } from '../../store';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
let { course = $bindable() } = $props();
|
||||
|
||||
function widgetControl() {
|
||||
$handleOpenWidget.open = !$handleOpenWidget.open;
|
||||
}
|
||||
</script>
|
||||
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label={$t('course.navItem.landing_page.editor.header_form.title')}
|
||||
bind:value={course.title}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label={$t('course.navItem.landing_page.editor.header_form.description')}
|
||||
bind:value={course.description}
|
||||
rows={6}
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label={$t('course.navItem.landing_page.editor.header_form.short_video')}
|
||||
placeholder="www.youtube.com/watch?v=uYRq60G5XTk"
|
||||
helperMessage={$t('course.navItem.landing_page.editor.header_form.helper')}
|
||||
type="text"
|
||||
bind:value={course.metadata.videoUrl}
|
||||
/>
|
||||
<div class="mt-7">
|
||||
<p class="mb-3">{$t('course.navItem.landing_page.editor.header_form.replace_cover')}</p>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.landing_page.editor.header_form.replace')}
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={widgetControl}
|
||||
/>
|
||||
</div>
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
import { uploadImage } from '$lib/utils/services/upload';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
setter: (value: any, key: string) => void;
|
||||
}
|
||||
|
||||
let { course = $bindable(), setter }: Props = $props();
|
||||
|
||||
let name = $state(get(course, 'metadata.instructor.name'));
|
||||
let role = $state(get(course, 'metadata.instructor.role'));
|
||||
let imgUrl = $state(get(course, 'metadata.instructor.imgUrl'));
|
||||
let description = $state(get(course, 'metadata.instructor.description'));
|
||||
let courseNo = $state(get(course, 'metadata.instructor.courseNo'));
|
||||
let avatar: File | undefined = $state();
|
||||
let isUploading = $state(false);
|
||||
|
||||
async function onAvatarChange(_avatar: File | undefined) {
|
||||
if (!_avatar || !course.id) return;
|
||||
|
||||
isUploading = true;
|
||||
const logo = await uploadImage(_avatar);
|
||||
|
||||
if (!logo) return;
|
||||
|
||||
imgUrl = logo;
|
||||
isUploading = false;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setter(name, 'metadata.instructor.name');
|
||||
});
|
||||
$effect(() => {
|
||||
setter(role, 'metadata.instructor.role');
|
||||
});
|
||||
$effect(() => {
|
||||
setter(imgUrl, 'metadata.instructor.imgUrl');
|
||||
});
|
||||
$effect(() => {
|
||||
setter(description, 'metadata.instructor.description');
|
||||
});
|
||||
$effect(() => {
|
||||
setter(courseNo, 'metadata.instructor.courseNo');
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
onAvatarChange(avatar);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mt-5 w-full">
|
||||
<label for="upload" class="font-bold">
|
||||
{$t('course.navItem.landing_page.editor.instructor_form.upload')}
|
||||
</label>
|
||||
<div class="flex w-full justify-center">
|
||||
<UploadImage bind:avatar src={imgUrl} bind:isUploading />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label={$t('course.navItem.landing_page.editor.instructor_form.name')}
|
||||
placeholder={$t('course.navItem.landing_page.editor.instructor_form.name_placeholder')}
|
||||
bind:value={name}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label={$t('course.navItem.landing_page.editor.instructor_form.role')}
|
||||
placeholder="e.g Software developer"
|
||||
bind:value={role}
|
||||
/>
|
||||
|
||||
<TextArea
|
||||
label={$t('course.navItem.landing_page.editor.instructor_form.about')}
|
||||
placeholder={$t('course.navItem.landing_page.editor.instructor_form.about_placeholder')}
|
||||
rows={6}
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
bind:value={description}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label={$t('course.navItem.landing_page.editor.instructor_form.total')}
|
||||
type="number"
|
||||
bind:value={courseNo}
|
||||
/>
|
||||
-128
@@ -1,128 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import * as Select from '@cio/ui/base/select';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { isCourseFree } from '$lib/utils/functions/course';
|
||||
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
setter: (value: any, key: string) => void;
|
||||
}
|
||||
|
||||
let { course = $bindable(), setter }: Props = $props();
|
||||
|
||||
let discount = $derived(get(course, 'metadata.discount', 0));
|
||||
let paymentLink = $derived(get(course, 'metadata.paymentLink', ''));
|
||||
let showDiscount = $derived(get(course, 'metadata.showDiscount', false));
|
||||
let giftToggled = $derived(get(course, 'metadata.reward.show', false));
|
||||
|
||||
function handleChange(content: string) {
|
||||
setter(content, 'metadata.reward.description');
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setter(showDiscount, 'metadata.showDiscount');
|
||||
});
|
||||
$effect(() => {
|
||||
setter(paymentLink, 'metadata.paymentLink');
|
||||
});
|
||||
$effect(() => {
|
||||
setter(discount, 'metadata.discount');
|
||||
});
|
||||
$effect(() => {
|
||||
setter(giftToggled, 'metadata.reward.show');
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if typeof course !== 'undefined'}
|
||||
<div>
|
||||
<Label class="mb-2 font-bold">{$t('course.navItem.landing_page.editor.pricing_form.currency')}</Label>
|
||||
<Select.Root type="single" value={course.currency} onValueChange={(value) => setter(value, 'currency')}>
|
||||
<Select.Trigger class="w-full">
|
||||
<p>{course.currency === 'NGN' ? 'NGN' : 'USD'}</p>
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="NGN">NGN</Select.Item>
|
||||
<Select.Item value="USD">USD</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label={$t('course.navItem.landing_page.editor.pricing_form.cost')}
|
||||
type="number"
|
||||
bind:value={course.cost}
|
||||
/>
|
||||
|
||||
{#if !isCourseFree(course.cost || 0)}
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label={$t('course.navItem.landing_page.editor.pricing_form.payment')}
|
||||
helperMessage="Stripe, Lemon Squeezy or any payment link"
|
||||
bind:value={paymentLink}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="mb-2">
|
||||
<Label class="font-bold">{$t('course.navItem.landing_page.editor.pricing_form.discount')}</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={showDiscount} />
|
||||
<Label class="text-gray-600">
|
||||
{showDiscount
|
||||
? $t('course.navItem.landing_page.editor.pricing_form.yes')
|
||||
: $t('course.navItem.landing_page.editor.pricing_form.no')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showDiscount}
|
||||
<TextField
|
||||
className="mt-5"
|
||||
labelClassName="font-bold"
|
||||
label={$t('course.navItem.landing_page.editor.pricing_form.percent')}
|
||||
type="number"
|
||||
bind:value={discount}
|
||||
helperMessage="In Percentage %"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="mt-5">
|
||||
<div class="mb-2">
|
||||
<Label class="font-bold">Gift on Completion</Label>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={giftToggled} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{giftToggled
|
||||
? $t('course.navItem.landing_page.editor.pricing_form.yes')
|
||||
: $t('course.navItem.landing_page.editor.pricing_form.no')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if giftToggled}
|
||||
<p class="mt-5 dark:text-white">
|
||||
{$t('course.navItem.landing_page.editor.pricing_form.gift')}
|
||||
</p>
|
||||
|
||||
<div class="h-2/5">
|
||||
<TextEditor
|
||||
content={get(course, 'metadata.reward.description', '')}
|
||||
onChange={(content) => handleChange(content)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
-42
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import { Label } from '@cio/ui/base/label';
|
||||
import { Switch } from '@cio/ui/base/switch';
|
||||
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { NAV_ITEM_KEY } from '../../constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
import TextEditor from '$lib/components/TextEditor/index.svelte';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
setter: (value: any, key: string) => void;
|
||||
}
|
||||
|
||||
let { course = $bindable(), setter }: Props = $props();
|
||||
|
||||
let show = $derived(get(course, `metadata.sectionDisplay.${NAV_ITEM_KEY.REQUIREMENT}`) ?? true);
|
||||
|
||||
$effect(() => {
|
||||
setter(show, `metadata.sectionDisplay.${NAV_ITEM_KEY.REQUIREMENT}`);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<TextEditor content={course.metadata.requirements} onChange={(content) => setter(content, `metadata.requirements`)} />
|
||||
</div>
|
||||
|
||||
<div class="mt-5 flex flex-col gap-2">
|
||||
<p class="font-semibold">
|
||||
{$t('course.navItem.landing_page.editor.display_section')}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<Switch bind:checked={show} />
|
||||
|
||||
<Label class="text-gray-600">
|
||||
{show ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
-96
@@ -1,96 +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 { t } from '$lib/utils/functions/translations';
|
||||
import { uploadImage } from '$lib/utils/services/upload';
|
||||
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import TextArea from '$lib/components/Form/TextArea.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import UploadImage from '$lib/components/UploadImage/index.svelte';
|
||||
|
||||
let { reviews = $bindable([]), review = $bindable({}), errors = {}, onExpand = () => {} } = $props();
|
||||
|
||||
let avatar = $state<File | undefined>();
|
||||
|
||||
// function to delete review
|
||||
function deleteReviewData() {
|
||||
reviews = reviews.filter((r) => r.id !== review.id);
|
||||
}
|
||||
|
||||
async function onAvatarChange(_avatar: File | undefined) {
|
||||
if (!_avatar) return;
|
||||
|
||||
review.avatar_url = await uploadImage(_avatar);
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
onAvatarChange(avatar);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<UploadImage bind:avatar src={review.avatar_url} widthHeight="w-20 h-20" errorMessage={errors.avatar_url} />
|
||||
|
||||
<TextField
|
||||
className="mt-2 w-full"
|
||||
labelClassName="font-normal"
|
||||
label={$t('course.navItem.landing_page.editor.reviews_form.fullname')}
|
||||
placeholder=""
|
||||
type="text"
|
||||
bind:value={review.name}
|
||||
errorMessage={errors.name}
|
||||
/>
|
||||
|
||||
<!-- Description -->
|
||||
<TextArea
|
||||
label={$t('course.navItem.landing_page.editor.reviews_form.description')}
|
||||
rows={4}
|
||||
className="mt-2 w-full"
|
||||
labelClassName="font-normal"
|
||||
placeholder=""
|
||||
bind:value={review.description}
|
||||
errorMessage={errors.description}
|
||||
isAIEnabled={true}
|
||||
initAIPrompt="Generate a 20 word review from a student saying good things about me teaching"
|
||||
aiAlignPopover="end"
|
||||
/>
|
||||
|
||||
<div class="mt-2 flex w-full items-end justify-between">
|
||||
<!-- Rating -->
|
||||
<TextField
|
||||
className="mt-2 w-20"
|
||||
labelClassName="font-normal"
|
||||
label={$t('course.navItem.landing_page.editor.reviews_form.rating')}
|
||||
type="number"
|
||||
min={1}
|
||||
max={5}
|
||||
bind:value={review.rating}
|
||||
onInputChange={(e) => {
|
||||
review.rating = parseInt(e.target.value);
|
||||
}}
|
||||
errorMessage={errors.rating}
|
||||
/>
|
||||
|
||||
<!-- Toggle -->
|
||||
<div class="flex w-24 flex-col items-center gap-2 space-x-2">
|
||||
<Switch bind:checked={review.hide} />
|
||||
|
||||
<Label class="text-sm">
|
||||
{review.hide ? $t('settings.landing_page.show_section') : $t('settings.landing_page.hide_section')}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex w-full items-center justify-between">
|
||||
<IconButton contained={true} value="delete" onClick={deleteReviewData} size="large">
|
||||
<TrashIcon size={16} />
|
||||
</IconButton>
|
||||
<IconButton contained={true} value="save" onClick={() => onExpand(review.id)} size="large">
|
||||
<SaveIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import z from 'zod';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import ReviewFormEditor from './ReviewFormEditor.svelte';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import { processErrors } from '$lib/utils/functions/validator';
|
||||
import { shortenName } from '$lib/utils/functions/string';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
setter: (value: any, key: string) => void;
|
||||
}
|
||||
|
||||
let { course = $bindable(), setter }: Props = $props();
|
||||
|
||||
let reviews = $state(get(course, 'metadata.reviews', []));
|
||||
let reviewToExpand = $state<number | null>(null);
|
||||
let errors = $state({});
|
||||
|
||||
function addReviewForm() {
|
||||
const _review = {
|
||||
id: new Date().getTime(),
|
||||
hide: false,
|
||||
name: '',
|
||||
avatar_url: '',
|
||||
rating: 1,
|
||||
created_at: new Date().getTime(),
|
||||
description: ''
|
||||
};
|
||||
reviews = [...reviews, _review];
|
||||
reviewToExpand = _review.id;
|
||||
}
|
||||
|
||||
function validateReviews(id) {
|
||||
const review = reviews.find((r) => r.id === id);
|
||||
const reviewSchema = z.object({
|
||||
name: z.string().min(5, {
|
||||
message: `${$t('course.navItem.landing_page.editor.reviews_form.validations.name.min_char')}`
|
||||
}),
|
||||
avatar_url: z.string().min(6, {
|
||||
message: `${$t('course.navItem.landing_page.editor.reviews_form.validations.avatar_url.message')}`
|
||||
}),
|
||||
rating: z
|
||||
.number()
|
||||
.min(1, {
|
||||
message: `${$t('course.navItem.landing_page.editor.reviews_form.validations.rating.message')}`
|
||||
})
|
||||
.max(5, {
|
||||
message: `${$t('course.navItem.landing_page.editor.reviews_form.validations.rating.message')}`
|
||||
}),
|
||||
description: z.string().min(10, {
|
||||
message: `${$t('course.navItem.landing_page.editor.reviews_form.validations.description.min_char')}`
|
||||
})
|
||||
});
|
||||
|
||||
const { error } = reviewSchema.safeParse(review);
|
||||
return processErrors(error);
|
||||
}
|
||||
|
||||
function onExpand(id) {
|
||||
errors = {};
|
||||
|
||||
if (reviewToExpand) {
|
||||
const validationRes = validateReviews(reviewToExpand);
|
||||
if (Object.keys(validationRes).length) {
|
||||
errors = Object.assign(errors, validationRes);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (id === reviewToExpand) {
|
||||
reviewToExpand = null;
|
||||
return;
|
||||
}
|
||||
|
||||
reviewToExpand = id;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setter(reviews, 'metadata.reviews');
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- Sections - Reviews -->
|
||||
<section id="reviews">
|
||||
<div class="">
|
||||
{#each reviews || [] as review, index}
|
||||
<div
|
||||
id={String(review.id)}
|
||||
class="relative my-2.5 flex flex-col items-center rounded-lg border border-gray-300 p-2"
|
||||
>
|
||||
{#if review.id !== reviewToExpand}
|
||||
<!-- the headers -->
|
||||
<div class="flex w-full items-center justify-between">
|
||||
<Avatar.Root class="mt-1 h-10 w-10">
|
||||
<Avatar.Image
|
||||
src={review.avatar_url ? review.avatar_url : '/logo-192.png'}
|
||||
alt={review.name ? review.name : 'Reviewer'}
|
||||
/>
|
||||
<Avatar.Fallback>{shortenName(review.name) || 'R'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<p class="text-sm">{review.name}</p>
|
||||
|
||||
<IconButton value="expand" onClick={() => onExpand(review.id)} size={$isMobile ? 'large' : 'small'}>
|
||||
<ChevronDownIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- the body -->
|
||||
{#if review.id === reviewToExpand}
|
||||
<ReviewFormEditor bind:reviews bind:review={reviews[index]} {errors} courseId={course.id} {onExpand} />
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<!-- create reviews button -->
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.landing_page.editor.reviews_form.add_reviews')}
|
||||
variant={VARIANTS.CONTAINED}
|
||||
onClick={addReviewForm}
|
||||
className="w-5 rounded-md mt-8"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,291 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import set from 'lodash/set';
|
||||
import ArrowLeftIcon from '@lucide/svelte/icons/arrow-left';
|
||||
import ArrowUpRightIcon from '@lucide/svelte/icons/arrow-right';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import ChevronLeftIcon from '@lucide/svelte/icons/chevron-left';
|
||||
import { currentOrgDomain } from '$lib/utils/store/org';
|
||||
|
||||
import { IconButton } from '$lib/components/IconButton';
|
||||
import { CloseButton } from '$lib/components/Buttons/Close';
|
||||
import HeaderForm from './HeaderForm.svelte';
|
||||
import RequirementForm from './RequirementForm.svelte';
|
||||
import DescriptionForm from './DescriptionForm.svelte';
|
||||
import PricingForm from './PricingForm.svelte';
|
||||
import GoalsForm from './GoalsForm.svelte';
|
||||
import ReviewsForm from './ReviewsForm.svelte';
|
||||
import CertificateForm from './CertificateForm.svelte';
|
||||
import InstructorForm from './InstructorForm.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { updateCourse } from '$lib/utils/services/courses';
|
||||
import generateSlug from '$lib/utils/functions/generateSlug';
|
||||
|
||||
import { isMobile } from '$lib/utils/store/useMobile';
|
||||
// import CustomPromptBtn from '$lib/components/AI/AIButton/CustomPromptBtn.svelte';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
course: Course;
|
||||
courseId: string;
|
||||
syncCourseStore: (course: Course) => void;
|
||||
}
|
||||
|
||||
let { course = $bindable(), courseId, syncCourseStore }: Props = $props();
|
||||
|
||||
let borderBottomGrey = 'border-r-0 border-b border-l-0 border-gray-300';
|
||||
let loading = $state(false);
|
||||
let show = $state(false);
|
||||
|
||||
interface Section {
|
||||
key: number;
|
||||
path: string;
|
||||
title: string;
|
||||
enableAIWriter?: boolean;
|
||||
initPrompt?: string;
|
||||
}
|
||||
|
||||
const sections: Section[] = [
|
||||
{
|
||||
key: 1,
|
||||
path: '',
|
||||
title: $t('course.navItem.landing_page.editor.title.header')
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
path: 'metadata.requirements',
|
||||
title: $t('course.navItem.landing_page.editor.title.requirement'),
|
||||
enableAIWriter: true,
|
||||
initPrompt: $t('course.navItem.landing_page.editor.title.requirement')
|
||||
},
|
||||
{
|
||||
key: 3,
|
||||
path: 'metadata.description',
|
||||
title: $t('course.navItem.landing_page.editor.title.description'),
|
||||
enableAIWriter: true,
|
||||
initPrompt: 'Please write a detailed course description for this course:'
|
||||
},
|
||||
{
|
||||
key: 4,
|
||||
path: 'metadata.goals',
|
||||
title: $t('course.navItem.landing_page.editor.title.goals'),
|
||||
enableAIWriter: true,
|
||||
initPrompt: 'What should a student expect to learn from this course:'
|
||||
},
|
||||
{
|
||||
key: 5,
|
||||
path: '',
|
||||
title: $t('course.navItem.landing_page.editor.title.certificate')
|
||||
},
|
||||
{
|
||||
key: 6,
|
||||
path: '',
|
||||
title: $t('course.navItem.landing_page.editor.title.reviews')
|
||||
},
|
||||
{
|
||||
key: 7,
|
||||
path: '',
|
||||
title: $t('course.navItem.landing_page.editor.title.instructor')
|
||||
},
|
||||
{
|
||||
key: 8,
|
||||
path: '',
|
||||
title: $t('course.navItem.landing_page.editor.title.pricing')
|
||||
}
|
||||
];
|
||||
let selectedSection: Section | null = $state(null);
|
||||
|
||||
function handleClose() {
|
||||
if (!selectedSection) {
|
||||
goto(`/courses/${courseId}`);
|
||||
}
|
||||
|
||||
selectedSection = null;
|
||||
}
|
||||
|
||||
function handleSectionSelect(sectionKey: number) {
|
||||
return () => {
|
||||
selectedSection = sections.find((section) => section.key === sectionKey) || null;
|
||||
|
||||
if (selectedSection) {
|
||||
const sectionId = selectedSection.title.toLowerCase();
|
||||
|
||||
const sectionEl = document.getElementById(sectionId);
|
||||
if (sectionEl) {
|
||||
sectionEl.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
loading = true;
|
||||
course.slug = course.slug || generateSlug(course.title);
|
||||
|
||||
console.log('course', course);
|
||||
await updateCourse(courseId, undefined, {
|
||||
...course,
|
||||
id: courseId,
|
||||
attendance: undefined,
|
||||
group: undefined,
|
||||
lessons: undefined,
|
||||
lesson_section: undefined,
|
||||
polls: undefined,
|
||||
slug: course.slug
|
||||
});
|
||||
|
||||
loading = false;
|
||||
syncCourseStore(course);
|
||||
}
|
||||
|
||||
async function handlePreview() {
|
||||
const link = `${$currentOrgDomain}/course/${course.slug}`;
|
||||
window.open(link, '_blank');
|
||||
}
|
||||
|
||||
function setter(value: any, setterKey: string) {
|
||||
if (typeof value === 'undefined') return;
|
||||
|
||||
// Untrack the course change to avoid unnecessary re-renders
|
||||
const _course = untrack(() => cloneDeep(course));
|
||||
set(_course, setterKey, value);
|
||||
|
||||
course = _course;
|
||||
}
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class={`${
|
||||
show ? 'fixed z-[50] -translate-x-[100%] md:absolute' : 'fixed z-[50] translate-x-0 md:relative'
|
||||
}left-0 border-r-1 z-[50] h-full w-[90vw] min-w-[300px] max-w-[350px] border border-b-0 border-l-0 border-t-0 bg-gray-100 transition dark:bg-neutral-800`}
|
||||
>
|
||||
<div class="toggler absolute rounded-full shadow-lg">
|
||||
<IconButton
|
||||
value="toggle"
|
||||
onClick={() => (show = !show)}
|
||||
size={$isMobile ? 'large' : 'small'}
|
||||
color="text-black"
|
||||
toolTipProps={$isMobile
|
||||
? undefined
|
||||
: {
|
||||
title: 'Toggle editor',
|
||||
direction: 'right',
|
||||
hotkeys: []
|
||||
}}
|
||||
>
|
||||
{#if show}
|
||||
<ChevronRightIcon size={16} />
|
||||
{:else}
|
||||
<ChevronLeftIcon size={16} />
|
||||
{/if}
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class="flex h-full flex-col">
|
||||
{#if !selectedSection}
|
||||
<div class="flex w-full items-center justify-between px-2">
|
||||
<CloseButton onClick={handleClose} />
|
||||
<div class="flex items-center">
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.landing_page.editor.save')}
|
||||
type="button"
|
||||
className="mr-1"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={handleSave}
|
||||
isLoading={loading}
|
||||
/>
|
||||
<IconButton onClick={handlePreview} disabled={loading || !course.slug}>
|
||||
<ArrowUpRightIcon size={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2 flex w-full items-center justify-between px-2">
|
||||
<h3 class="dark:text-white">{$t('course.navItem.landing_page.editor.page_builder')}</h3>
|
||||
</div>
|
||||
{#each sections as section, index}
|
||||
<button
|
||||
class="flex w-full items-center justify-between border border-l-0 px-2 py-3 {index + 1 < sections.length &&
|
||||
'border-b-0'} border-gray-300"
|
||||
onclick={handleSectionSelect(section.key)}
|
||||
>
|
||||
<p class="mr-2 dark:text-white">
|
||||
{section.title}
|
||||
{$t('course.navItem.landing_page.editor.section')}
|
||||
</p>
|
||||
<ChevronRightIcon size={16} />
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
<!-- Title -->
|
||||
<div class="flex items-center {borderBottomGrey} w-full">
|
||||
<IconButton onClick={handleClose}>
|
||||
<ArrowLeftIcon size={16} />
|
||||
</IconButton>
|
||||
<div class=" flex items-center">
|
||||
<h3 class="dark:text-white">
|
||||
{selectedSection.title}
|
||||
</h3>
|
||||
<!-- {#if selectedSection.enableAIWriter}
|
||||
<CustomPromptBtn
|
||||
className="w-fit ml-2"
|
||||
alignPopover="bottom-left"
|
||||
defaultPrompt={`${selectedSection.initPrompt} ${course.title}. Please format in html`}
|
||||
isHTML={true}
|
||||
handleInsert={(v) => {
|
||||
if (!selectedSection) return;
|
||||
const _course = cloneDeep(course);
|
||||
set(_course, selectedSection.path, v);
|
||||
course = _course;
|
||||
}}
|
||||
/>
|
||||
{/if} -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="title-content overflow-y-auto p-2">
|
||||
{#if selectedSection.key === 1}
|
||||
<HeaderForm bind:course />
|
||||
{:else if selectedSection.key === 2}
|
||||
<RequirementForm bind:course {setter} />
|
||||
{:else if selectedSection.key === 3}
|
||||
<DescriptionForm bind:course {setter} />
|
||||
{:else if selectedSection.key === 4}
|
||||
<GoalsForm bind:course {setter} />
|
||||
{:else if selectedSection.key === 5}
|
||||
<CertificateForm bind:course {setter} />
|
||||
{:else if selectedSection.key === 6}
|
||||
<ReviewsForm bind:course {setter} />
|
||||
{:else if selectedSection.key === 7}
|
||||
<InstructorForm bind:course {setter} />
|
||||
{:else if selectedSection.key === 8}
|
||||
<PricingForm bind:course {setter} />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.title-content {
|
||||
height: 90%;
|
||||
}
|
||||
.toggler {
|
||||
right: -15px;
|
||||
z-index: 10;
|
||||
border: 1px solid var(--border-color);
|
||||
top: 50px;
|
||||
height: fit-content;
|
||||
background: var(--border-color);
|
||||
}
|
||||
@media screen and (max-width: 767px) {
|
||||
.toggler {
|
||||
right: -25px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
export function observeIntersection(node, callback, options = {}) {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
callback(true);
|
||||
} else {
|
||||
callback(false);
|
||||
}
|
||||
});
|
||||
}, options);
|
||||
|
||||
observer.observe(node);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
observer.unobserve(node);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { preventDefault } from '$lib/utils/functions/svelte';
|
||||
import { coursePaymentValidation } from '$lib/utils/functions/validator';
|
||||
import { triggerSendEmail, NOTIFICATION_NAME } from '$lib/utils/services/notification/notification';
|
||||
|
||||
import Modal from '$lib/components/Modal/index.svelte';
|
||||
import TextField from '$lib/components/Form/TextField.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
|
||||
interface Props {
|
||||
open?: boolean;
|
||||
paymentLink?: string;
|
||||
teacherEmail?: string;
|
||||
courseName?: string;
|
||||
}
|
||||
|
||||
let { open = $bindable(false), paymentLink = '', teacherEmail = '', courseName = '' }: Props = $props();
|
||||
|
||||
let fields = $state({
|
||||
fullname: '',
|
||||
email: ''
|
||||
});
|
||||
|
||||
let errors: Record<string, string> = $state({
|
||||
fullname: '',
|
||||
email: ''
|
||||
});
|
||||
|
||||
const STEPS = {
|
||||
STEP_1: 'ENTER_DETAILS',
|
||||
STEP_2: 'GO_TO_PAYMENT'
|
||||
};
|
||||
|
||||
let step = $state(STEPS.STEP_1);
|
||||
|
||||
function onSubmit() {
|
||||
errors = coursePaymentValidation(fields);
|
||||
console.log('coursePayment errors', errors);
|
||||
|
||||
if (Object.keys(errors).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (paymentLink) {
|
||||
step = STEPS.STEP_2;
|
||||
|
||||
// Send email to first added teacher of student wanting to join
|
||||
triggerSendEmail(NOTIFICATION_NAME.SEND_TEACHER_STUDENT_BUY_REQUEST, {
|
||||
to: teacherEmail,
|
||||
courseName,
|
||||
studentEmail: fields.email,
|
||||
studentFullname: fields.fullname
|
||||
});
|
||||
} else {
|
||||
open = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onClickPaymentLink() {
|
||||
// Send email to student to send proof of payment to teacher
|
||||
triggerSendEmail(NOTIFICATION_NAME.STUDENT_PROOVE_COURSE_PAYMENT, {
|
||||
to: fields.email,
|
||||
courseName,
|
||||
teacherEmail,
|
||||
studentFullname: fields.fullname,
|
||||
orgName: $currentOrg.name
|
||||
});
|
||||
|
||||
fields.fullname = '';
|
||||
fields.email = '';
|
||||
open = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal onClose={() => (open = false)} bind:open width="w-96" modalHeading="Process course payment">
|
||||
{#if step === STEPS.STEP_1}
|
||||
<form onsubmit={preventDefault(onSubmit)}>
|
||||
<TextField
|
||||
label="Your Fullname"
|
||||
bind:value={fields.fullname}
|
||||
autoFocus={true}
|
||||
placeholder="John Doe"
|
||||
className="mb-4"
|
||||
isRequired={true}
|
||||
autoComplete={false}
|
||||
errorMessage={errors.fullname}
|
||||
/>
|
||||
<TextField
|
||||
label="Your Email"
|
||||
bind:value={fields.email}
|
||||
placeholder="[email protected]"
|
||||
className="mb-4"
|
||||
isRequired={true}
|
||||
autoComplete={false}
|
||||
errorMessage={errors.email}
|
||||
/>
|
||||
|
||||
<div class="mt-5 flex flex-row-reverse items-center">
|
||||
<PrimaryButton className="px-6 py-3" label={paymentLink ? 'Next' : 'Finish'} type="submit" />
|
||||
</div>
|
||||
</form>
|
||||
{:else if step === STEPS.STEP_2}
|
||||
<p>You will now be taken to a payment page, once you've paid send a proof of payment to the course admin</p>
|
||||
<div class="mt-5 flex flex-row-reverse items-center">
|
||||
<a href={paymentLink} target="_blank" onclick={onClickPaymentLink} class="text-sm font-semibold">Go to payment</a>
|
||||
</div>
|
||||
{/if}
|
||||
</Modal>
|
||||
@@ -1,270 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||
import { isCourseFree, getStudentInviteLink, calcCourseDiscount } from '$lib/utils/functions/course';
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { goto } from '$app/navigation';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import PaymentModal from './PaymentModal.svelte';
|
||||
import type { Course } from '$lib/utils/types';
|
||||
import { ROLE } from '@cio/utils/constants';
|
||||
import { capturePosthogEvent } from '$lib/utils/services/posthog';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
editMode?: boolean;
|
||||
courseData: Course;
|
||||
startCoursePayment?: boolean;
|
||||
mobile?: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
className = '',
|
||||
editMode = false,
|
||||
courseData,
|
||||
startCoursePayment = $bindable(false),
|
||||
mobile = false
|
||||
}: Props = $props();
|
||||
|
||||
let openModal = $state(false);
|
||||
let formatter: Intl.NumberFormat | undefined = $state();
|
||||
|
||||
const discount = $derived(get(courseData, 'metadata.discount', 0));
|
||||
const calculatedCost = $derived(
|
||||
calcCourseDiscount(discount, courseData.cost || 0, !!courseData.metadata.showDiscount)
|
||||
);
|
||||
const isFree = $derived(isCourseFree(calculatedCost));
|
||||
|
||||
function handleJoinCourse() {
|
||||
if (editMode) return;
|
||||
|
||||
capturePosthogEvent('join_course', {
|
||||
course_id: courseData.id,
|
||||
course_title: courseData.title,
|
||||
course_cost: courseData.cost,
|
||||
course_free: isFree
|
||||
});
|
||||
|
||||
if (isFree) {
|
||||
const link = getStudentInviteLink(courseData, $currentOrg.siteName, $currentOrgDomain);
|
||||
|
||||
goto(link);
|
||||
} else {
|
||||
openModal = true;
|
||||
}
|
||||
|
||||
startCoursePayment = false;
|
||||
}
|
||||
|
||||
function setFormatter(currency: string | undefined) {
|
||||
if (!currency) return;
|
||||
formatter = getCurrencyFormatter(currency);
|
||||
}
|
||||
|
||||
function getTeacherEmail(group: Course['group']) {
|
||||
const firstTutor = group?.members?.find((m) => m.role_id === ROLE.TUTOR);
|
||||
|
||||
return firstTutor?.profile?.email || '';
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
setFormatter(courseData.currency);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
startCoursePayment && handleJoinCourse();
|
||||
});
|
||||
</script>
|
||||
|
||||
<PaymentModal
|
||||
bind:open={openModal}
|
||||
paymentLink={get(courseData, 'metadata.paymentLink', '')}
|
||||
courseName={courseData.title}
|
||||
teacherEmail={getTeacherEmail(courseData.group)}
|
||||
/>
|
||||
|
||||
<!-- Pricing Details -->
|
||||
{#if mobile}
|
||||
<div
|
||||
class="sticky bottom-0 flex h-fit w-full items-center justify-center bg-gray-50 transition duration-300 lg:hidden dark:bg-neutral-800"
|
||||
>
|
||||
<aside
|
||||
class="price-container sticky lg:hidden {editMode
|
||||
? 'lg:bottom-2'
|
||||
: 'lg:top-10'} m-h-fit z-0 bg-gray-50 lg:rounded-lg lg:shadow-2xl dark:bg-neutral-800 {className}"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-3 px-3 py-3">
|
||||
<!-- Pricing -->
|
||||
<div class=" text-center">
|
||||
{#if courseData?.metadata?.allowNewStudent}
|
||||
<p class="flex items-center gap-1 text-sm font-medium dark:text-white">
|
||||
{formatter?.format(calculatedCost) || calculatedCost}
|
||||
{#if isFree}
|
||||
<span class="text-xs">
|
||||
({$t('course.navItem.landing_page.pricing_section.free')})
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
{#if courseData?.metadata?.showDiscount}
|
||||
<p class="text-sm font-light text-gray-500 dark:text-white">
|
||||
{discount}% {$t('course.navItem.landing_page.pricing_section.discount')}.
|
||||
<span class="line-through">
|
||||
{formatter?.format(courseData?.cost || 0) || courseData.cost}
|
||||
</span>
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-lg dark:text-white">
|
||||
{$t('course.navItem.landing_page.pricing_section.not_accepting')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Call To Action Buttons -->
|
||||
<div class="flex h-full w-full flex-col items-center">
|
||||
<PrimaryButton
|
||||
label={isFree
|
||||
? $t('course.navItem.landing_page.pricing_section.enroll')
|
||||
: $t('course.navItem.landing_page.pricing_section.buy')}
|
||||
className="w-full sm:w-full h-[40px]"
|
||||
onClick={handleJoinCourse}
|
||||
isDisabled={!courseData.metadata.allowNewStudent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
{:else}
|
||||
<aside
|
||||
class="price-container lg:sticky {editMode
|
||||
? 'lg:top-0'
|
||||
: 'lg:top-10'} m-h-fit lg:rounded-lg lg:shadow-2xl dark:bg-neutral-800 {className}"
|
||||
>
|
||||
<div class="p-2 lg:p-10">
|
||||
<!-- Pricing -->
|
||||
<div class="mb-6">
|
||||
{#if courseData?.metadata?.allowNewStudent}
|
||||
<p class="text-lg font-medium dark:text-white">
|
||||
{formatter?.format(calculatedCost) || calculatedCost}
|
||||
{#if isFree}
|
||||
<span class="text-sm">
|
||||
({$t('course.navItem.landing_page.pricing_section.free')})
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
{#if courseData?.metadata?.showDiscount}
|
||||
<p class="text-sm font-light text-gray-500 dark:text-white">
|
||||
{discount}% {$t('course.navItem.landing_page.pricing_section.discount')}.
|
||||
<span class="line-through">
|
||||
{formatter?.format(courseData?.cost || 0) || courseData.cost}
|
||||
</span>
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="text-lg dark:text-white">
|
||||
{$t('course.navItem.landing_page.pricing_section.not_accepting')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Call To Action Buttons -->
|
||||
<div class="flex w-full flex-col items-center">
|
||||
<PrimaryButton
|
||||
label={isFree
|
||||
? $t('course.navItem.landing_page.pricing_section.enroll')
|
||||
: $t('course.navItem.landing_page.pricing_section.buy')}
|
||||
className="w-full sm:w-full py-3 mb-3"
|
||||
onClick={handleJoinCourse}
|
||||
isDisabled={!courseData.metadata.allowNewStudent}
|
||||
/>
|
||||
{#if courseData?.metadata?.showDiscount && courseData.metadata.allowNewStudent}
|
||||
<p class="text-sm font-light text-gray-500 dark:text-white">
|
||||
{$t('course.navItem.landing_page.pricing_section.bird')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gift Container -->
|
||||
{#if courseData?.metadata?.reward?.show}
|
||||
<div class="flex flex-col items-center border-b border-t border-gray-300 p-10">
|
||||
<HtmlRender>{@html get(courseData, 'metadata.reward.description', '')}</HtmlRender>
|
||||
</div>
|
||||
{/if}
|
||||
</aside>
|
||||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.banner {
|
||||
background-color: #040f2d;
|
||||
min-height: 472px;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #7888b7;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
max-width: 559px;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.active {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.active::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: var(--main-primary-color);
|
||||
display: block;
|
||||
bottom: -14px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.price-container {
|
||||
width: 90%;
|
||||
max-width: 405px;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.price-container {
|
||||
width: 50%;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1024px) {
|
||||
.price-container {
|
||||
width: 350px;
|
||||
min-width: 250px;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: 1280px) {
|
||||
.price-container {
|
||||
width: 405px;
|
||||
min-width: 330px;
|
||||
height: fit-content;
|
||||
}
|
||||
}
|
||||
.course-content {
|
||||
max-width: 608px;
|
||||
}
|
||||
|
||||
:global(.list ul li) {
|
||||
margin-left: 1rem;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
:global(.plyr) {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
@@ -1,50 +0,0 @@
|
||||
<script lang="ts">
|
||||
import CircleCheckIcon from '@lucide/svelte/icons/circle-check';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { Lesson } from '$lib/utils/types';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
lessonCount: number;
|
||||
exerciseCount: number;
|
||||
lessons: Lesson[];
|
||||
}
|
||||
|
||||
let { title, lessonCount, exerciseCount, lessons }: Props = $props();
|
||||
|
||||
let expand = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="my-3 w-full rounded-md">
|
||||
<!-- lesson header -->
|
||||
<button
|
||||
class="flex w-full items-center justify-between bg-[#F7F7F7] py-3 pl-3 pr-4 text-sm font-medium dark:bg-neutral-700"
|
||||
onclick={() => (expand = !expand)}
|
||||
>
|
||||
<span class="flex items-center gap-2">
|
||||
{#if expand}
|
||||
<ChevronUpIcon size={16} />
|
||||
{:else}
|
||||
<ChevronDownIcon size={16} />
|
||||
{/if}
|
||||
{title}
|
||||
</span>
|
||||
<span class="text-xs font-normal">
|
||||
{pluralize($t('course.navItem.landing_page.lessons'), lessonCount, true)},
|
||||
{pluralize($t('course.navItem.landing_page.exercises'), exerciseCount, true)}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{#if expand}
|
||||
{#each lessons as lesson}
|
||||
<div class="lesson-section flex items-center gap-2 py-3 pl-3 text-[13px] text-[#656565]">
|
||||
<CircleCheckIcon size={16} />
|
||||
{lesson.title}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -1,41 +0,0 @@
|
||||
export const NAV_ITEM_KEY = {
|
||||
HEADER: 'header',
|
||||
REQUIREMENT: '#requirement',
|
||||
DESCRIPTION: '#description',
|
||||
GOALS: '#goals',
|
||||
CERTIFICATE: '#certificate',
|
||||
LESSONS: '#lessons',
|
||||
REVIEWS: '#reviews',
|
||||
INSTRUCTOR: '#instructor'
|
||||
};
|
||||
|
||||
export const NAV_ITEMS = [
|
||||
{
|
||||
key: NAV_ITEM_KEY.REQUIREMENT,
|
||||
label: 'course.navItem.landing_page.editor.title.requirement'
|
||||
},
|
||||
{
|
||||
key: NAV_ITEM_KEY.DESCRIPTION,
|
||||
label: 'course.navItem.landing_page.editor.title.description'
|
||||
},
|
||||
{
|
||||
key: NAV_ITEM_KEY.GOALS,
|
||||
label: 'course.navItem.landing_page.editor.title.goals'
|
||||
},
|
||||
{
|
||||
key: NAV_ITEM_KEY.CERTIFICATE,
|
||||
label: 'course.navItem.landing_page.editor.title.certificate'
|
||||
},
|
||||
{
|
||||
key: NAV_ITEM_KEY.LESSONS,
|
||||
label: 'course.navItems.nav_content'
|
||||
},
|
||||
{
|
||||
key: NAV_ITEM_KEY.REVIEWS,
|
||||
label: 'course.navItem.landing_page.editor.title.reviews'
|
||||
},
|
||||
{
|
||||
key: NAV_ITEM_KEY.INSTRUCTOR,
|
||||
label: 'course.navItem.landing_page.editor.title.instructor'
|
||||
}
|
||||
];
|
||||
@@ -1,579 +0,0 @@
|
||||
<script lang="ts">
|
||||
import get from 'lodash/get';
|
||||
import pluralize from 'pluralize';
|
||||
import { page } from '$app/state';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import PlayIcon from '@lucide/svelte/icons/play';
|
||||
|
||||
import { getLectureNo } from '../Course/function';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { NAV_ITEM_KEY, NAV_ITEMS } from './constants';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { calDateDiff } from '$lib/utils/functions/date';
|
||||
import { handleOpenWidget, reviewsModalStore } from './store';
|
||||
import { COURSE_VERSION, type Course } from '$lib/utils/types';
|
||||
import { course, sortLesson } from '$lib/components/Course/store';
|
||||
import { VARIANTS } from '$lib/components/PrimaryButton/constants';
|
||||
import { getEmbedId } from '$lib/utils/functions/formatYoutubeVideo';
|
||||
import { getExerciseCount, getLessonSections, getTotalLessons, filterNavItems } from './utils';
|
||||
|
||||
import Chip from '../Chip/index.svelte';
|
||||
import Modal from '../Modal/index.svelte';
|
||||
import ImageRenderer from '../Org/ImageRenderer.svelte';
|
||||
import * as Avatar from '@cio/ui/base/avatar';
|
||||
import PricingSection from './components/PricingSection.svelte';
|
||||
import { PoweredBy } from '$lib/features/ui';
|
||||
import { shortenName } from '$lib/utils/functions/string';
|
||||
import SectionsDisplay from './components/SectionsDisplay.svelte';
|
||||
import UploadWidget from '$lib/components/UploadWidget/index.svelte';
|
||||
import HtmlRender from '$lib/components/HTMLRender/HTMLRender.svelte';
|
||||
import PrimaryButton from '$lib/components/PrimaryButton/index.svelte';
|
||||
import { observeIntersection } from './components/IntersectionObserver';
|
||||
|
||||
interface Props {
|
||||
editMode?: boolean;
|
||||
courseData: Course;
|
||||
}
|
||||
|
||||
let { editMode = false, courseData = $bindable() }: Props = $props();
|
||||
|
||||
const reviews = $derived(get(courseData, 'metadata.reviews') || []);
|
||||
const video = $derived(get(courseData, 'metadata.videoUrl'));
|
||||
|
||||
const lessons = $derived.by(() => {
|
||||
const _lessons = get(courseData, 'lessons', []);
|
||||
return sortLesson([..._lessons]);
|
||||
});
|
||||
const totalRatings = $derived(reviews?.reduce((acc = 0, review) => acc + (review?.rating || 0), 0));
|
||||
const averageRating = $derived(totalRatings / reviews?.length);
|
||||
const expandDescription = $derived(Array(reviews?.length ?? 0).fill(false));
|
||||
|
||||
const allowNewStudent = $derived(get(courseData, 'metadata.allowNewStudent'));
|
||||
const bannerImage = $derived(get(courseData, 'logo'));
|
||||
const instructor = $derived(get(courseData, 'metadata.instructor') || {});
|
||||
const certificate: Course['metadata']['certificate'] = $derived(
|
||||
get(courseData, 'metadata.certificate', {
|
||||
templateUrl: '/images/certificate-template.svg'
|
||||
})
|
||||
);
|
||||
|
||||
const navItems = $derived(filterNavItems(courseData, reviews));
|
||||
const navItemKeys = $derived(navItems.map((item) => item.key));
|
||||
|
||||
let player: any = $state();
|
||||
let startCoursePayment = $state(false);
|
||||
let isVisible = $state(false);
|
||||
let observer: { destroy: () => void };
|
||||
|
||||
let activeNav = $state(NAV_ITEMS[0].key);
|
||||
|
||||
const lessonSections = getLessonSections(courseData);
|
||||
const totalLessons = getTotalLessons(lessonSections);
|
||||
|
||||
function locationHashChanged() {
|
||||
activeNav = window.location.hash;
|
||||
}
|
||||
|
||||
function initPlyr(_player: any, _video: string | undefined) {
|
||||
if (!player) return;
|
||||
|
||||
// @ts-ignore
|
||||
const plyr = new Plyr('#player', {});
|
||||
// @ts-ignore
|
||||
window.player = plyr;
|
||||
}
|
||||
|
||||
function toggleDescription(id: number) {
|
||||
expandDescription[id] = !expandDescription[id];
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
window.onhashchange = locationHashChanged;
|
||||
|
||||
const targetNode = document.querySelector('.target-component');
|
||||
observer = observeIntersection(targetNode, (inView: boolean) => {
|
||||
isVisible = inView;
|
||||
});
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
observer?.destroy();
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
initPlyr(player, video);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if !editMode}
|
||||
<PoweredBy />
|
||||
{/if}
|
||||
|
||||
<div class="flex w-full flex-col items-center bg-white dark:bg-black">
|
||||
<!-- Header Section -->
|
||||
<header id="header" class="banner p- flex w-full items-center justify-center">
|
||||
<div class="flex w-full flex-col-reverse items-center justify-between md:w-5/6 md:flex-row">
|
||||
<!-- Course Description -->
|
||||
<div class="w-11/12 py-10 md:w-2/5">
|
||||
<h1 class="my-4 text-5xl text-white dark:text-white">
|
||||
{get(courseData, 'title', '')}
|
||||
</h1>
|
||||
<p class="text-md mb-6 text-white dark:text-white">
|
||||
{get(courseData, 'description', '')}
|
||||
</p>
|
||||
|
||||
<p class="author my-3 text-sm dark:text-white">
|
||||
{get(courseData, 'metadata.instructor.name', '')}
|
||||
</p>
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.landing_page.start_course')}
|
||||
className="px-6 py-5 mt-6 sm:w-fit hidden md:block"
|
||||
onClick={() => {
|
||||
if (editMode) return;
|
||||
startCoursePayment = true;
|
||||
}}
|
||||
isDisabled={!allowNewStudent}
|
||||
/>
|
||||
{#if $handleOpenWidget.open}
|
||||
<UploadWidget bind:imageURL={$course.logo} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Banner Image getEmbedId(videoUrl) -->
|
||||
{#if video}
|
||||
<div class="banner-image flex w-full md:w-2/3">
|
||||
<div bind:this={player} id="player" data-plyr-provider="youtube" data-plyr-embed-id={getEmbedId(video)}></div>
|
||||
</div>
|
||||
<!-- <div class="container">
|
||||
<div
|
||||
bind:this={player}
|
||||
id="player"
|
||||
class="banner-image w-2/3 h-96 relative"
|
||||
data-plyr-provider="youtube"
|
||||
data-plyr-embed-id="bTqVqk7FSmY"
|
||||
/>
|
||||
</div> -->
|
||||
{:else}
|
||||
<div class="banner-image relative overflow-hidden rounded-md md:w-2/3">
|
||||
<img
|
||||
style="min-width:280px; min-height:200px"
|
||||
alt="About us"
|
||||
src={bannerImage ? bannerImage : '/images/classroomio-course-img-template.jpg'}
|
||||
class="relative mt-2 h-full max-h-[400px] w-full max-w-[500px] rounded-md md:mt-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- <div
|
||||
class="banner-image w-2/3 h-96 relative cursor-pointer"
|
||||
on:click={playVideo}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="absolute top-0 left-0 backdrop w-full h-full"
|
||||
/>
|
||||
<img
|
||||
src="/images/course-banner-image.png"
|
||||
alt="banner"
|
||||
class="w-full h-full"
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full z-50 flex items-center justify-center"
|
||||
>
|
||||
<img src="/images/play_video.png" alt="play video" />
|
||||
</div>
|
||||
</div> -->
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="w-full bg-white dark:bg-black">
|
||||
<div
|
||||
class="mx-0 my-2 flex w-full max-w-[1200px] flex-col-reverse items-center justify-between lg:m-auto lg:w-11/12 lg:flex-row lg:items-start lg:py-8"
|
||||
>
|
||||
<!-- Course Details -->
|
||||
<div class="course-content w-full p-3 lg:mr-10 lg:w-10/12">
|
||||
<!-- Navigation -->
|
||||
<nav
|
||||
class="sticky top-0 flex items-center border-b border-gray-300 py-3 {!editMode &&
|
||||
'lg:top-11'} bg-white dark:bg-neutral-800"
|
||||
>
|
||||
{#each navItems as navItem}
|
||||
<a
|
||||
href="{page.url.pathname}{navItem.key}"
|
||||
class="{navItem.key === activeNav &&
|
||||
'active text-primary-700'} z-0 mr-6 rounded-lg px-2 font-normal text-slate-700 hover:bg-gray-200 dark:text-white dark:hover:text-slate-900"
|
||||
>
|
||||
{$t(navItem.label)}
|
||||
</a>
|
||||
{/each}
|
||||
</nav>
|
||||
|
||||
<!-- Sections - Requirement -->
|
||||
|
||||
{#if navItemKeys.includes(NAV_ITEM_KEY.REQUIREMENT)}
|
||||
<section
|
||||
id="requirement"
|
||||
transition:fade={{ delay: 250, duration: 300 }}
|
||||
class="mt-8 border-b border-gray-300 pb-10"
|
||||
>
|
||||
<h3 class="mb-3 mt-0 text-2xl">
|
||||
{$t('course.navItem.landing_page.requirement')}
|
||||
</h3>
|
||||
|
||||
<ul class="list font-light">
|
||||
<HtmlRender>{@html get(courseData, 'metadata.requirements', '')}</HtmlRender>
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Sections - Course Description -->
|
||||
{#if navItemKeys.includes(NAV_ITEM_KEY.DESCRIPTION)}
|
||||
<section
|
||||
id="description"
|
||||
transition:fade={{ delay: 250, duration: 300 }}
|
||||
class="mt-8 border-b border-gray-300 pb-10"
|
||||
>
|
||||
<h3 class="mb-3 mt-0 text-2xl">
|
||||
{$t('course.navItem.landing_page.description')}
|
||||
</h3>
|
||||
|
||||
<HtmlRender className="dark:text-white text-sm font-light">
|
||||
{@html get(courseData, 'metadata.description', '')}
|
||||
</HtmlRender>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Sections - Goal -->
|
||||
{#if navItemKeys.includes(NAV_ITEM_KEY.GOALS)}
|
||||
<section id="goals" transition:fade={{ delay: 250, duration: 300 }} class="mt-8 pb-10">
|
||||
<h3 class="mb-3 mt-0 text-2xl">{$t('course.navItem.landing_page.learn')}</h3>
|
||||
<ul class="list font-light">
|
||||
<HtmlRender>{@html get(courseData, 'metadata.goals', '')}</HtmlRender>
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Sections - Certificate -->
|
||||
{#if navItemKeys.includes(NAV_ITEM_KEY.CERTIFICATE)}
|
||||
<section
|
||||
id="certificate"
|
||||
transition:fade={{ delay: 250, duration: 300 }}
|
||||
class="mt-8 border-b border-gray-300 pb-10"
|
||||
>
|
||||
<h3 class="mt-0 text-2xl">{$t('course.navItem.landing_page.certificate')}</h3>
|
||||
<p class="mb-3 text-sm font-light dark:text-white">
|
||||
{$t('course.navItem.landing_page.certificate_text')}
|
||||
</p>
|
||||
|
||||
<ImageRenderer
|
||||
src={certificate?.templateUrl}
|
||||
alt="certificate template"
|
||||
className="certificate-img max-h-[215px]"
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Sections - Lessons -->
|
||||
{#if courseData.version === COURSE_VERSION.V1}
|
||||
<section id="lessons" class="mt-8 border-b border-gray-300 pb-10">
|
||||
<div class="mb-3 flex w-full items-center justify-between">
|
||||
<h3 class="mb-3 mt-0 text-2xl">
|
||||
{$t('course.navItem.landing_page.content')}
|
||||
</h3>
|
||||
<p class="text-sm font-light dark:text-white">
|
||||
{pluralize('lesson', lessons.length, true)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
{#each lessons as lesson, index}
|
||||
<div class="m-2 rounded border px-2 py-1">
|
||||
<Chip value={getLectureNo(index + 1, '0')} className="bg-primary-100 text-primary-700 inline " />
|
||||
<p class="ml-2 inline text-xs font-light dark:text-white">
|
||||
{lesson.title}
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
{:else if courseData.version === COURSE_VERSION.V2}
|
||||
<section id="lessons">
|
||||
<!-- header -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h1>{$t('course.navItem.landing_page.course_content')}</h1>
|
||||
<span class="text-xs font-normal">
|
||||
{pluralize($t('course.navItem.landing_page.modules'), lessonSections.length, true)},
|
||||
{pluralize($t('course.navItem.landing_page.lessons'), totalLessons, true)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#each lessonSections as section}
|
||||
<SectionsDisplay
|
||||
exerciseCount={getExerciseCount(section.lessons)}
|
||||
lessonCount={section.lessons?.length}
|
||||
lessons={section.lessons}
|
||||
title={section.title}
|
||||
/>
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Sections - Reviews -->
|
||||
{#if navItemKeys.includes(NAV_ITEM_KEY.REVIEWS)}
|
||||
<section id="reviews" transition:fade={{ delay: 250, duration: 300 }}>
|
||||
<h2 class="my-16 mb-6 ml-0 mr-0 font-semibold">
|
||||
{$t('course.navItem.landing_page.reviews')}
|
||||
</h2>
|
||||
<div class="flex flex-wrap">
|
||||
{#each reviews.slice(0, 4) as review, id}
|
||||
{#if !review.hide}
|
||||
<!-- review -->
|
||||
<div class="item-start my-2 flex w-2/4 flex-row">
|
||||
<!-- image container -->
|
||||
{#if review.avatar_url}
|
||||
<Avatar.Root class="mt-1 size-10">
|
||||
<Avatar.Image
|
||||
src={review.avatar_url ? review.avatar_url : '/logo-192.png'}
|
||||
alt={review.name ? review.name : 'Avatar'}
|
||||
/>
|
||||
<Avatar.Fallback>{shortenName(review.name) || 'AV'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
{/if}
|
||||
|
||||
<!-- profile content -->
|
||||
<div class="w-11/12 pl-2.5">
|
||||
<p class="mb-0.5 font-medium">{review.name}</p>
|
||||
<!-- ratings -->
|
||||
<div class="flex flex-row items-center">
|
||||
{#if review.rating}
|
||||
<img src="/images/rating-{review.rating}.svg" class="mr-4 mt-1 w-24" alt="" />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="read-more-content mb-2" style="max-height: {expandDescription[id] ? 'none' : '50px'}">
|
||||
<p class="my-2 text-sm leading-5 text-gray-600">
|
||||
{review.description}
|
||||
</p>
|
||||
</div>
|
||||
{#if !expandDescription[id] && review.description.split(' ').length > 9}
|
||||
<button
|
||||
class="text-primary-700 mt-2 font-normal underline"
|
||||
onclick={() => toggleDescription(id)}>See More</button
|
||||
>
|
||||
{/if}
|
||||
{#if expandDescription[id]}
|
||||
<button
|
||||
class="text-primary-700 mt-2 font-normal underline"
|
||||
onclick={() => toggleDescription(id)}>See Less</button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if reviews.length > 4}
|
||||
<PrimaryButton
|
||||
label={$t('course.navItem.landing_page.see_all')}
|
||||
className="w-3/12 p-4 mt-2"
|
||||
variant={VARIANTS.OUTLINED}
|
||||
onClick={() => ($reviewsModalStore.open = true)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Reviews Modal -->
|
||||
<Modal
|
||||
onClose={() => ($reviewsModalStore.open = false)}
|
||||
bind:open={$reviewsModalStore.open}
|
||||
width="w-9/12"
|
||||
modalHeading={$t('course.navItem.landing_page.reviews_modal.title')}
|
||||
>
|
||||
<div class="flex">
|
||||
<!-- ratings -->
|
||||
<div class="w-1/3">
|
||||
<h2 class="text-xl">
|
||||
{averageRating}
|
||||
{$t('course.navItem.landing_page.reviews_modal.rating')}
|
||||
</h2>
|
||||
<h2 class="mt-2 text-lg font-semibold">
|
||||
{reviews.length}
|
||||
{$t('course.navItem.landing_page.reviews_modal.ratings')}
|
||||
</h2>
|
||||
</div>
|
||||
<!-- reviews -->
|
||||
<div class="flex w-4/6 flex-wrap">
|
||||
{#each reviews as review, id}
|
||||
<!-- review -->
|
||||
<div class="item-start my-2 flex w-full flex-row">
|
||||
<!-- image container -->
|
||||
{#if review.avatar_url}
|
||||
<Avatar.Root class="mt-1 h-10 w-10">
|
||||
<Avatar.Image
|
||||
src={review.avatar_url ? review.avatar_url : '/logo-192.png'}
|
||||
alt={review.name ? review.name : 'Avatar'}
|
||||
/>
|
||||
<Avatar.Fallback>{shortenName(review.name) || 'AV'}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
{/if}
|
||||
|
||||
<!-- profile content -->
|
||||
<div class="w-11/12 pl-2.5">
|
||||
<p class="mb-0.5 font-medium">{review.name}</p>
|
||||
<!-- ratings -->
|
||||
<div class="flex flex-row">
|
||||
<img src="/images/rating-full.svg" alt="" class="mr-2" />
|
||||
<p class="text-xs text-gray-600">
|
||||
{calDateDiff(review.created_at)}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="read-more-content mb-2"
|
||||
style="max-height: {expandDescription[id] ? 'none' : '50px'}"
|
||||
>
|
||||
<p class="my-2 text-sm leading-5 text-gray-600">
|
||||
{review.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- Sections - Instructor -->
|
||||
<section id="instructor" class="mt-8 pb-10">
|
||||
<h3 class="mb-3 mt-0 text-2xl">
|
||||
{$t('course.navItem.landing_page.instructor')}
|
||||
</h3>
|
||||
<div class="mb-4 flex items-center">
|
||||
<img
|
||||
alt="Author Avatar"
|
||||
class="mr-3 block h-20 w-20 rounded-full"
|
||||
src={get(instructor, 'imgUrl', $currentOrg.avatar_url || '/logo-512.png')}
|
||||
/>
|
||||
<div>
|
||||
<p class="text-md font-light dark:text-white">
|
||||
{get(instructor, 'name', $currentOrg.name)}
|
||||
</p>
|
||||
<p class="text-xs font-light dark:text-white">
|
||||
{get(instructor, 'role', '')}
|
||||
</p>
|
||||
<p class="text-md flex items-center font-light dark:text-white">
|
||||
<PlayIcon size={16} class="filled" />
|
||||
<span class="ml-1"
|
||||
>{get(instructor, 'courseNo', '')}
|
||||
{$t('course.navItem.landing_page.courses')}</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-sm font-light dark:text-white">
|
||||
{get(instructor, 'description', '')}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Pricing Details -->
|
||||
<PricingSection {courseData} {editMode} bind:startCoursePayment className="target-component" />
|
||||
</div>
|
||||
{#if !isVisible}
|
||||
<PricingSection {courseData} {editMode} bind:startCoursePayment mobile={true} className="w-full" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.banner {
|
||||
background-color: #040f2d;
|
||||
min-height: 472px;
|
||||
}
|
||||
|
||||
.author {
|
||||
color: #7888b7;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
max-width: 559px;
|
||||
}
|
||||
|
||||
.backdrop {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
:global(.certificate-img) {
|
||||
width: unset !important;
|
||||
}
|
||||
|
||||
.active {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.active::after {
|
||||
position: absolute;
|
||||
content: '';
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: var(--main-primary-color);
|
||||
display: block;
|
||||
bottom: -14px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.price-container {
|
||||
width: 405px;
|
||||
min-width: 330px;
|
||||
height: fit-content;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.course-content {
|
||||
max-width: 608px;
|
||||
min-width: 60%;
|
||||
}
|
||||
|
||||
nav {
|
||||
overflow: auto;
|
||||
margin: 0;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
:global(.list ul li) {
|
||||
margin-left: 1rem;
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
:global(.plyr) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.read-more-content {
|
||||
max-height: 100px; /* Adjust this value to set the maximum height for the truncated content */
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.read-more-button {
|
||||
cursor: pointer;
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.lesson-section:not(:last-child) {
|
||||
border-bottom: 1px solid #f7f7f7;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1023px) {
|
||||
.course-content {
|
||||
min-width: 80%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,65 +0,0 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const landingPageStore = writable({
|
||||
header: {
|
||||
title: 'Online Business Masterclass: Sell Your Own Digital Products',
|
||||
description:
|
||||
'Create, Launch & Market Your Own Online Business w/ Digital Products such as Online Courses, Coaching, eBooks, Webinars+',
|
||||
duration: 'Estimated 8 weeks',
|
||||
cost: 300,
|
||||
currency: '$',
|
||||
buttonLabel: 'Enrol'
|
||||
},
|
||||
requirement: {
|
||||
content:
|
||||
"You don't need any experience creating an online business or digital product. \nYou don't need an online presence, website, or following - but it will help kickstart your online business if you do."
|
||||
},
|
||||
description: {
|
||||
content:
|
||||
"### Have you ever dreamed of starting your own online business?. \n\nOf course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. \n### Enroll today and get instant access to. \n\nOf course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though.\n ### You will learn many types of digital marketing:\n\n Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though. Hello, thank you for reply. Of course I know that :-) I was just thinking if there is some way to do same as getStaticProps for other components too. I don't think such feature is completely necessary, though."
|
||||
},
|
||||
syllabus: {
|
||||
lessons: [
|
||||
{
|
||||
title: 'What is FinTech?'
|
||||
},
|
||||
{
|
||||
title: 'Payments, Cryptocurrencies and Blockchain'
|
||||
},
|
||||
{
|
||||
title: 'Digital Finance and Alternative Finance'
|
||||
},
|
||||
{
|
||||
title: 'FinTech Regulation and RegTech'
|
||||
},
|
||||
{
|
||||
title: 'Data & TechFin'
|
||||
}
|
||||
]
|
||||
},
|
||||
aboutUs: {
|
||||
companyName: 'The Brad School',
|
||||
logoUrl: 'https://img-c.udemycdn.com/user/200_H/19701574_95d1_13.jpg',
|
||||
description:
|
||||
'*Learn creative skills, from absolute beginner to advanced mastery.*\nVideo School exists to help you succeed in life. Each course has been hand-tailored to teach a specific skill from photography and video to art, design and business.\n**Whether you’re trying to learn a new skill from scratch, or want to refresh your memory on something you’ve learned in the past, you’ve come to the right place.**\nEducation makes the world a better place. Make your world better with new skills!\nOur courses can be watched 24/7 wherever you are. Most are fully downloadable so you can take them with you. You can also view them on mobile devices with the Udemy mobile app.\nAll courses have a 30-day money-back guarantee so that you can check it out, make sure it’s the right course for you, and get a refund if it’s not!'
|
||||
},
|
||||
reviews: [
|
||||
{
|
||||
id: 1,
|
||||
show: true,
|
||||
name: 'Adeyemi Adetunji',
|
||||
avatar_url: '/logo-192.png',
|
||||
rating: 1,
|
||||
created_at: new Date(),
|
||||
description: 'rice ventore officiis maxime laudantium recusandae sit dolor.'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export const reviewsModalStore = writable({
|
||||
open: false
|
||||
});
|
||||
|
||||
export let handleOpenWidget = writable({
|
||||
open: false
|
||||
});
|
||||
@@ -1,45 +0,0 @@
|
||||
import { NAV_ITEMS, NAV_ITEM_KEY } from './constants';
|
||||
import type { Course, Review } from '$lib/utils/types';
|
||||
import get from 'lodash/get';
|
||||
|
||||
export function getExerciseCount(lessons) {
|
||||
return lessons.reduce((total, lesson) => total + (lesson.totalExercises?.[0]?.count || 0), 0);
|
||||
}
|
||||
|
||||
export function getLessonSections(data) {
|
||||
const sections =
|
||||
data?.lesson_section?.map((section) => {
|
||||
return {
|
||||
...section,
|
||||
lessons: data.lessons.filter((lesson) => lesson.section_id === section.id)
|
||||
};
|
||||
}) || [];
|
||||
|
||||
return sections || [];
|
||||
}
|
||||
|
||||
export function getTotalLessons(sections) {
|
||||
return sections.reduce((total, section) => {
|
||||
return total + section.lessons.length;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function filterNavItems(course: Course, reviews: Review[]) {
|
||||
const rules = get(course, 'metadata.sectionDisplay', {});
|
||||
|
||||
return NAV_ITEMS.filter((item) => {
|
||||
const key = item.key;
|
||||
|
||||
if (typeof rules[key] === 'boolean') {
|
||||
return !!rules[key];
|
||||
}
|
||||
|
||||
if (key === NAV_ITEM_KEY.REVIEWS) {
|
||||
// Show only if there are reviews added
|
||||
return reviews.length > 0;
|
||||
}
|
||||
|
||||
// In case they are undefined
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -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,17 +0,0 @@
|
||||
<span class="button-trailing">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon icon-tabler icon-tabler-arrow-up-right"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<line x1="17" y1="7" x2="7" y2="17" /><polyline points="8 7 17 7 17 16" />
|
||||
</svg>
|
||||
</span>
|
||||
@@ -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,53 +0,0 @@
|
||||
<script lang="ts">
|
||||
// import CircleCheckIcon from '@lucide/svelte/icons/circle-check';
|
||||
|
||||
interface Props {
|
||||
filled?: Boolean;
|
||||
size?: number;
|
||||
}
|
||||
let { filled, size = 16 }: Props = $props();
|
||||
</script>
|
||||
|
||||
<!-- <CircleCheckIcon
|
||||
{size}
|
||||
class={filled ? 'filled' : ''}
|
||||
fill={filled ? 'red' : 'none'}
|
||||
strokeWidth={filled ? 0 : undefined}
|
||||
/> -->
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class="custom lucide-icon lucide lucide-circle-check {filled ? 'filled' : ''}"
|
||||
><!----><circle cx="12" cy="12" r="10"></circle><!----><path d="m9 12 2 2 4-4"></path><!----><!----><!----></svg
|
||||
>
|
||||
|
||||
<style>
|
||||
svg circle {
|
||||
stroke: var(--icon-stroke-color, currentColor); /* Circle border */
|
||||
fill: none; /* Circle is notsvg by default */
|
||||
}
|
||||
|
||||
/* Target the path (checkmark) specifically */
|
||||
svg path {
|
||||
stroke: var(--icon-stroke-color, currentColor); /* Checkmark color */
|
||||
fill: none; /* Checkmark is not filled, just stroked */
|
||||
}
|
||||
|
||||
svg.filled circle {
|
||||
fill: var(--icon-fill-color, currentColor); /* Fill the circle with color */
|
||||
stroke: var(--icon-fill-color, currentColor); /* Make the circle's border match its fill */
|
||||
}
|
||||
|
||||
svg.filled path {
|
||||
stroke: white; /* Make the checkmark white so it's visible against the fill */
|
||||
fill: none; /* Ensure the checkmark path itself is not filled */
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user