feat: make explore more visible in lms
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import * as Dialog from '@cio/ui/base/dialog';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { DEFAULT_COURSE_BANNER_IMAGE } from '@cio/ui';
|
||||
import BookOpenIcon from '@lucide/svelte/icons/book-open';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import { calcCourseDiscount, isCourseFree } from '$lib/utils/functions/course';
|
||||
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
||||
import type { RecommendedCourses } from '$features/course/types';
|
||||
import pluralize from 'pluralize';
|
||||
|
||||
interface Props {
|
||||
course: RecommendedCourses[number];
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
let { course, open = $bindable(false) }: Props = $props();
|
||||
|
||||
type CourseMetadata = {
|
||||
allowNewStudent?: boolean;
|
||||
discount?: number;
|
||||
showDiscount?: boolean;
|
||||
requirements?: string;
|
||||
};
|
||||
|
||||
const metadata = $derived(course.metadata as CourseMetadata | null);
|
||||
const discount = $derived(metadata?.discount ?? 0);
|
||||
const calculatedCost = $derived(calcCourseDiscount(discount, course.cost || 0, !!metadata?.showDiscount));
|
||||
const isFree = $derived(isCourseFree(calculatedCost));
|
||||
const allowNewStudent = $derived(metadata?.allowNewStudent !== false);
|
||||
const requirements = $derived(metadata?.requirements?.trim() || null);
|
||||
const formatter = $derived(getCurrencyFormatter(course.currency ?? 'USD'));
|
||||
|
||||
function handleJoinCourse() {
|
||||
if (!course.slug) return;
|
||||
|
||||
if (isFree) {
|
||||
goto(`/course/${course.slug}/enroll`);
|
||||
} else {
|
||||
goto(`/course/${course.slug}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="overflow-hidden p-0 sm:max-w-2xl">
|
||||
<div class="aspect-video w-full overflow-hidden">
|
||||
<img src={course.logo || DEFAULT_COURSE_BANNER_IMAGE} alt={course.title} class="h-full w-full object-cover" />
|
||||
</div>
|
||||
|
||||
<div class="max-h-80 overflow-y-auto px-6 py-4">
|
||||
<h2 class="text-xl font-semibold tracking-tight">{course.title}</h2>
|
||||
|
||||
{#if course.description}
|
||||
<p class="ui:text-muted-foreground mt-2 text-sm leading-relaxed">{course.description}</p>
|
||||
{/if}
|
||||
|
||||
<div class="ui:text-muted-foreground mt-4 flex items-center gap-4 text-sm">
|
||||
<span class="flex items-center gap-1.5">
|
||||
<BookOpenIcon class="size-4" />
|
||||
{course.lessonCount ?? 0}
|
||||
{$t('courses.course_card.lessons_number')}
|
||||
</span>
|
||||
<span>
|
||||
{pluralize($t('courses.course_card.exercise'), course.exerciseCount ?? 0, true)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if requirements}
|
||||
<div class="mt-4">
|
||||
<h3 class="text-sm font-semibold">{$t('course.navItem.landing_page.requirement')}</h3>
|
||||
<p class="ui:text-muted-foreground mt-1 text-sm leading-relaxed">{requirements}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="ui:border-t ui:bg-card sticky bottom-0 flex items-center justify-between px-6 py-4">
|
||||
<div>
|
||||
{#if allowNewStudent}
|
||||
<p class="text-lg font-bold">
|
||||
{formatter.format(calculatedCost)}
|
||||
{#if isFree}
|
||||
<span class="text-sm font-normal">
|
||||
({$t('course.navItem.landing_page.pricing_section.free')})
|
||||
</span>
|
||||
{/if}
|
||||
</p>
|
||||
{#if metadata?.showDiscount && !isFree}
|
||||
<p class="ui:text-muted-foreground text-sm line-through">
|
||||
{formatter.format(course.cost ?? 0)}
|
||||
</p>
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="ui:text-muted-foreground text-sm">
|
||||
{$t('course.navItem.landing_page.pricing_section.not_accepting')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Button onclick={handleJoinCourse} disabled={!allowNewStudent}>
|
||||
{isFree
|
||||
? $t('course.navItem.landing_page.pricing_section.enroll')
|
||||
: $t('course.navItem.landing_page.pricing_section.buy')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -17,7 +17,10 @@
|
||||
import { currentOrg, currentOrgDomain } from '$lib/utils/store/org';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { coursesApi } from '$features/course/api';
|
||||
import { CourseCard } from '$features/course/components';
|
||||
import CoursePublicBadge from '$features/course/components/course-public-badge.svelte';
|
||||
import CoursePreviewModal from '$features/lms/components/course-preview-modal.svelte';
|
||||
import type { RecommendedCourses } from '$features/course/types';
|
||||
import { IconButton } from '@cio/ui/custom/icon-button';
|
||||
import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
|
||||
import CopyIcon from '@lucide/svelte/icons/copy';
|
||||
@@ -34,6 +37,8 @@
|
||||
|
||||
let loginStreak = $state<number | null>(null);
|
||||
let hasLoadedLoginStreak = $state(false);
|
||||
let selectedCourse = $state<RecommendedCourses[number] | null>(null);
|
||||
let previewOpen = $state(false);
|
||||
|
||||
let totalCompleted = $derived(
|
||||
coursesApi.enrolledCourses.reduce((acc, course) => acc + getCourseCompletedItems(course), 0)
|
||||
@@ -58,6 +63,7 @@
|
||||
if (!$profile.id || !$currentOrg.id) return;
|
||||
|
||||
coursesApi.getEnrolledCourses();
|
||||
coursesApi.getRecommendedCourses({ limit: 6 });
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -395,4 +401,41 @@
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{#if coursesApi.isLoading || coursesApi.recommendedCourses.length > 0}
|
||||
<section class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="ui:text-muted-foreground text-sm font-semibold tracking-[0.14em] uppercase">
|
||||
{$t('dashboard.explore_more_courses')}
|
||||
</h2>
|
||||
<Button variant="outline" size="sm" onclick={() => goto('/lms/explore')}>
|
||||
{$t('dashboard.view_more')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if coursesApi.isLoading}
|
||||
<div class="ui:text-muted-foreground flex items-center justify-center py-8">
|
||||
<Spinner class="size-6" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid w-full grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{#each coursesApi.recommendedCourses as course (course.id)}
|
||||
<CourseCard
|
||||
{course}
|
||||
isExplore={true}
|
||||
isLMS={true}
|
||||
onExploreClick={() => {
|
||||
selectedCourse = course;
|
||||
previewOpen = true;
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if selectedCourse}
|
||||
<CoursePreviewModal course={selectedCourse} bind:open={previewOpen} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
import { coursesApi } from '$features/course/api';
|
||||
import type { RecommendedCourses } from '$features/course/types';
|
||||
import { CourseSortBy, DEFAULT_COURSE_SORT, parseCourseSortValue } from '$features/course/utils/constants';
|
||||
import CoursePreviewModal from '$features/lms/components/course-preview-modal.svelte';
|
||||
|
||||
let searchValue = $state('');
|
||||
let sortKey = $state<CourseSortBy>(DEFAULT_COURSE_SORT);
|
||||
let selectedCourse = $state<RecommendedCourses[number] | null>(null);
|
||||
let previewOpen = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
@@ -67,4 +70,12 @@
|
||||
isLoading={coursesApi.isLoading}
|
||||
bind:searchValue
|
||||
bind:sortKey
|
||||
onCardClick={(course) => {
|
||||
selectedCourse = course as RecommendedCourses[number];
|
||||
previewOpen = true;
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if selectedCourse}
|
||||
<CoursePreviewModal course={selectedCourse} bind:open={previewOpen} />
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import * as UnderlineTabs from '@cio/ui/custom/underline-tabs';
|
||||
import { Button } from '@cio/ui/base/button';
|
||||
import { CoursesPage } from '$features/course/pages';
|
||||
import { profile } from '$lib/utils/store/user';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
@@ -51,7 +52,11 @@
|
||||
emptyTitle={$t('my_learning.not_in_progress')}
|
||||
isLMS={true}
|
||||
isLoading={coursesApi.isLoading}
|
||||
/>
|
||||
>
|
||||
{#snippet emptyAction()}
|
||||
<Button href="/lms/explore">{$t('my_learning.find_courses')}</Button>
|
||||
{/snippet}
|
||||
</CoursesPage>
|
||||
</UnderlineTabs.Content>
|
||||
<UnderlineTabs.Content value={tabs[1].value}>
|
||||
<CoursesPage
|
||||
|
||||
Reference in New Issue
Block a user