2023-01-03 09:26:08 +01:00
|
|
|
<script lang="ts">
|
2025-12-22 02:36:48 +01:00
|
|
|
import type { Component } from 'svelte';
|
|
|
|
|
import get from 'lodash/get';
|
|
|
|
|
import { resolve } from '$app/paths';
|
2025-11-21 12:18:04 +01:00
|
|
|
import { Badge } from '@cio/ui/base/badge';
|
|
|
|
|
import UserIcon from '@lucide/svelte/icons/user';
|
|
|
|
|
import CircleDotIcon from '@lucide/svelte/icons/circle-dot';
|
|
|
|
|
import TrendingUpIcon from '@lucide/svelte/icons/trending-up';
|
2025-12-22 02:36:48 +01:00
|
|
|
import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
|
2025-11-21 12:18:04 +01:00
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
import { Button } from '@cio/ui/base/button';
|
|
|
|
|
import { Separator } from '@cio/ui/base/separator';
|
|
|
|
|
import * as Item from '@cio/ui/base/item';
|
|
|
|
|
import { Progress } from '@cio/ui/base/progress';
|
2025-11-21 12:18:04 +01:00
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
import { Image } from '$features/ui';
|
|
|
|
|
import { COURSE_TYPE, type Course } from '$lib/utils/types';
|
2025-11-21 12:18:04 +01:00
|
|
|
import { t } from '$lib/utils/functions/translations';
|
2024-12-26 07:24:41 +01:00
|
|
|
import { calcCourseDiscount } from '$lib/utils/functions/course';
|
|
|
|
|
import getCurrencyFormatter from '$lib/utils/functions/getCurrencyFormatter';
|
2025-12-22 02:36:48 +01:00
|
|
|
import { calcProgressRate } from '$features/course/utils/functions';
|
|
|
|
|
import CardDropdown from './card-dropdown.svelte';
|
2024-04-20 14:40:44 +01:00
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
export interface Props {
|
|
|
|
|
course: Course;
|
2025-11-01 15:49:29 +01:00
|
|
|
isOnLandingPage?: boolean;
|
|
|
|
|
isLMS?: boolean;
|
|
|
|
|
isExplore?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
let { course, isOnLandingPage, isLMS, isExplore }: Props = $props();
|
|
|
|
|
|
|
|
|
|
$inspect('isLMS', isLMS);
|
|
|
|
|
|
2025-11-01 15:49:29 +01:00
|
|
|
let {
|
|
|
|
|
bannerImage,
|
|
|
|
|
id = '',
|
|
|
|
|
slug = '',
|
|
|
|
|
title = '',
|
|
|
|
|
description = '',
|
|
|
|
|
isPublished = false,
|
|
|
|
|
totalLessons = 0,
|
|
|
|
|
totalStudents = 0,
|
|
|
|
|
currency = 'USD',
|
|
|
|
|
progressRate = 45,
|
|
|
|
|
type,
|
|
|
|
|
pricingData = {
|
2025-12-22 02:36:48 +01:00
|
|
|
cost: 0,
|
|
|
|
|
discount: 0,
|
|
|
|
|
showDiscount: false
|
2025-11-01 15:49:29 +01:00
|
|
|
}
|
2025-12-22 02:36:48 +01:00
|
|
|
} = $derived({
|
|
|
|
|
id: course.id,
|
|
|
|
|
slug: course.slug,
|
|
|
|
|
bannerImage: course.logo || '/images/classroomio-course-img-template.jpg',
|
|
|
|
|
title: course.title,
|
|
|
|
|
type: course.type,
|
|
|
|
|
description: course.description,
|
|
|
|
|
isPublished: course.is_published,
|
|
|
|
|
pricingData: {
|
|
|
|
|
cost: course.cost,
|
|
|
|
|
discount: course.metadata?.discount || 0,
|
|
|
|
|
showDiscount: course.metadata?.showDiscount || false
|
|
|
|
|
},
|
|
|
|
|
currency: course.currency,
|
|
|
|
|
totalLessons: get(course, 'lessons[0].count', 0),
|
|
|
|
|
progressRate: calcProgressRate(course.progress_rate, course.total_lessons),
|
|
|
|
|
totalStudents: course.total_students
|
|
|
|
|
});
|
2025-11-01 15:49:29 +01:00
|
|
|
|
|
|
|
|
let formatter = $derived(getCurrencyFormatter(currency));
|
2023-12-20 18:44:00 +01:00
|
|
|
|
2024-05-21 00:14:47 +01:00
|
|
|
const COURSE_TAG: Record<
|
|
|
|
|
string,
|
|
|
|
|
{
|
|
|
|
|
style: string;
|
|
|
|
|
label: string;
|
2025-12-22 02:36:48 +01:00
|
|
|
icon: Component;
|
2024-05-21 00:14:47 +01:00
|
|
|
iconStyle?: string;
|
|
|
|
|
}
|
|
|
|
|
> = {
|
|
|
|
|
[COURSE_TYPE.LIVE_CLASS]: {
|
|
|
|
|
style: '',
|
|
|
|
|
label: $t('course.navItem.settings.live_class'),
|
2025-11-01 15:49:29 +01:00
|
|
|
icon: CircleDotIcon,
|
|
|
|
|
iconStyle: 'custom text-red-700'
|
2024-05-21 00:14:47 +01:00
|
|
|
},
|
|
|
|
|
[COURSE_TYPE.SELF_PACED]: {
|
|
|
|
|
style: '',
|
|
|
|
|
label: $t('course.navItem.settings.self_paced'),
|
2025-11-01 15:49:29 +01:00
|
|
|
icon: UserIcon,
|
2025-12-22 02:36:48 +01:00
|
|
|
iconStyle: 'custom ui:text-primary'
|
2024-05-21 00:14:47 +01:00
|
|
|
},
|
|
|
|
|
SPECIALIZATION: {
|
|
|
|
|
style: '',
|
|
|
|
|
label: $t('specialization.course_tag'),
|
2025-11-01 15:49:29 +01:00
|
|
|
icon: TrendingUpIcon
|
2024-05-21 00:14:47 +01:00
|
|
|
}
|
|
|
|
|
};
|
2024-09-09 01:14:55 +01:00
|
|
|
|
2025-11-01 15:49:29 +01:00
|
|
|
let cost = $derived(calcCourseDiscount(pricingData.discount, pricingData.cost ?? 0, !!pricingData.showDiscount));
|
2024-12-23 23:20:52 +01:00
|
|
|
|
2025-11-01 15:49:29 +01:00
|
|
|
let courseUrl = $derived(
|
|
|
|
|
isOnLandingPage || isExplore ? `/course/${slug}` : `/courses/${id}${isLMS ? '/lessons?next=true' : ''}`
|
|
|
|
|
);
|
2021-08-29 07:23:19 +03:00
|
|
|
</script>
|
|
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
<Item.Root variant="outline" class="p-3! group relative max-w-[320px]">
|
|
|
|
|
{#snippet child({ props })}
|
|
|
|
|
<a href={resolve(courseUrl, {})} {...props}>
|
2024-12-23 23:20:52 +01:00
|
|
|
{#if !isLMS && !isOnLandingPage}
|
2025-12-22 02:36:48 +01:00
|
|
|
<CardDropdown {id} {title} {description} />
|
2024-12-23 23:20:52 +01:00
|
|
|
{/if}
|
|
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
<Item.Header>
|
|
|
|
|
<Item.Media variant="image" class="h-50! w-full! relative">
|
|
|
|
|
<Image src={bannerImage} alt="Course banner image" className="w-full h-full rounded-sm object-cover" />
|
2025-11-21 12:18:04 +01:00
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
{#if type}
|
|
|
|
|
{@const tag = COURSE_TAG[type]}
|
|
|
|
|
<Badge class="rounded-md! absolute bottom-2 left-2 z-10 flex items-center capitalize" variant="secondary">
|
|
|
|
|
<tag.icon class={tag.iconStyle} />
|
|
|
|
|
{tag.label}
|
|
|
|
|
</Badge>
|
|
|
|
|
{/if}
|
|
|
|
|
</Item.Media>
|
|
|
|
|
</Item.Header>
|
2023-05-15 15:13:42 +00:00
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
<Item.Content>
|
|
|
|
|
<Item.Title class="text-base! line-clamp-1 min-h-[44px]">
|
|
|
|
|
{title}
|
|
|
|
|
</Item.Title>
|
2023-09-27 04:23:45 +01:00
|
|
|
|
2025-12-22 02:36:48 +01:00
|
|
|
<Item.Description class="min-h-[63px]">{description}</Item.Description>
|
|
|
|
|
|
|
|
|
|
<Separator class="my-3" />
|
|
|
|
|
|
|
|
|
|
<div class="flex justify-between {isLMS && 'items-center'} w-full">
|
|
|
|
|
<div class="w-[50%]">
|
|
|
|
|
<p class="text-xs {!isLMS && 'pl-2'} dark:text-white">
|
|
|
|
|
{totalLessons}
|
|
|
|
|
{$t('courses.course_card.lessons_number')}
|
|
|
|
|
</p>
|
|
|
|
|
<div class="py-2 text-xs">
|
|
|
|
|
{#if isOnLandingPage}
|
|
|
|
|
<span class="px-2">
|
|
|
|
|
{#if !cost}
|
|
|
|
|
{$t('course.navItem.landing_page.pricing_section.free')}
|
|
|
|
|
{:else if pricingData.showDiscount}
|
|
|
|
|
{formatter.format(cost)}
|
|
|
|
|
<span class="line-through">
|
|
|
|
|
{formatter?.format(pricingData?.cost)}
|
|
|
|
|
</span>
|
|
|
|
|
{:else}
|
|
|
|
|
{formatter.format(cost)}
|
|
|
|
|
{/if}
|
|
|
|
|
</span>
|
|
|
|
|
{:else if isLMS}
|
|
|
|
|
{#if !isExplore}
|
|
|
|
|
<div class="flex w-3/4 items-center gap-2">
|
|
|
|
|
<Progress value={50} />
|
|
|
|
|
<p class="ui:text-muted-foreground text-xs">{progressRate}%</p>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
{:else}
|
|
|
|
|
<Badge variant={isPublished ? 'default' : 'outline'}>
|
|
|
|
|
{#if isPublished}
|
|
|
|
|
{$t('courses.course_card.published')}
|
|
|
|
|
{:else}
|
|
|
|
|
{$t('courses.course_card.unpublished')}
|
|
|
|
|
{/if}
|
|
|
|
|
</Badge>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{#if isLMS}
|
|
|
|
|
<Button variant="outline">
|
|
|
|
|
{isExplore ? $t('courses.course_card.learn_more') : $t('courses.course_card.continue_course')}
|
|
|
|
|
|
|
|
|
|
<ArrowRightIcon class="custom" />
|
|
|
|
|
</Button>
|
|
|
|
|
{:else if !isOnLandingPage}
|
|
|
|
|
<div class="flex flex-col justify-between">
|
|
|
|
|
<p class="pl-2 text-xs dark:text-white">
|
|
|
|
|
{totalStudents}
|
|
|
|
|
{$t('courses.course_card.students')}
|
|
|
|
|
</p>
|
2023-09-28 11:45:50 +01:00
|
|
|
</div>
|
2024-07-09 21:17:34 +01:00
|
|
|
{/if}
|
2025-12-22 02:36:48 +01:00
|
|
|
</div>
|
|
|
|
|
</Item.Content>
|
|
|
|
|
</a>
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Item.Root>
|