Files
classroomio/apps/dashboard/src/lib/features/course/components/card.svelte
T

228 lines
7.7 KiB
Svelte
Raw Normal View History

<script lang="ts">
import type { Component } from 'svelte';
import { resolve } from '$app/paths';
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';
import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
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';
2026-03-09 11:18:22 +01:00
import pluralize from 'pluralize';
import { Image } from '$features/ui';
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';
2026-03-09 11:18:22 +01:00
import { calcCourseProgress, calcProgressRate } from '$features/course/utils/functions';
import CardDropdown from './card-dropdown.svelte';
2026-03-09 11:18:22 +01:00
import type { OrgCourses, UserEnrolledCourses } from '$features/course/types';
import type { OrgPublicCourses } from '$features/org/utils/types';
2024-04-20 14:40:44 +01:00
export interface Props {
2026-03-09 11:18:22 +01:00
course: OrgCourses[number] | UserEnrolledCourses[number] | OrgPublicCourses[number];
2025-11-01 15:49:29 +01:00
isOnLandingPage?: boolean;
isLMS?: boolean;
isExplore?: boolean;
}
let { course, isOnLandingPage, isLMS, isExplore }: Props = $props();
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 = {
cost: 0,
discount: 0,
showDiscount: false
2025-11-01 15:49:29 +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,
2026-03-09 11:18:22 +01:00
isPublished: course.isPublished,
pricingData: {
cost: course.cost,
discount: course.metadata?.discount || 0,
showDiscount: course.metadata?.showDiscount || false
},
currency: course.currency,
2026-03-09 11:18:22 +01:00
totalLessons: course.lessonCount,
progressRate: (() => {
const c = course as { exerciseCount?: number; exercisesCompleted?: number };
if (typeof c.exerciseCount === 'number' && typeof c.exercisesCompleted === 'number') {
return calcCourseProgress({
lessonsCompleted: 'progressRate' in course ? (course.progressRate ?? 0) : 0,
totalLessons: course.lessonCount ?? 0,
exercisesCompleted: c.exercisesCompleted,
totalExercises: c.exerciseCount
});
}
return calcProgressRate('progressRate' in course ? course.progressRate : 0, course.lessonCount);
})(),
totalStudents: 'totalStudents' in course ? course.totalStudents : 0
});
2025-11-01 15:49:29 +01:00
let formatter = $derived(getCurrencyFormatter(currency));
2026-03-09 11:18:22 +01:00
const courseTags = $derived(
('tags' in course && Array.isArray(course.tags) ? course.tags : []) as Array<{
id: string;
name: string;
slug: string;
color?: string | null;
}>
);
const COURSE_TAG: Record<
string,
{
style: string;
label: string;
icon: Component;
iconStyle?: string;
}
> = {
2026-03-09 11:18:22 +01:00
['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'
},
2026-03-09 11:18:22 +01:00
['SELF_PACED']: {
style: '',
label: $t('course.navItem.settings.self_paced'),
2025-11-01 15:49:29 +01:00
icon: UserIcon,
iconStyle: 'custom ui:text-primary'
},
SPECIALIZATION: {
style: '',
label: $t('specialization.course_tag'),
2025-11-01 15:49:29 +01:00
icon: TrendingUpIcon
}
};
2025-11-01 15:49:29 +01:00
let cost = $derived(calcCourseDiscount(pricingData.discount, pricingData.cost ?? 0, !!pricingData.showDiscount));
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>
2026-03-09 11:18:22 +01:00
<Item.Root variant="outline" class="group relative max-w-[320px] p-3!">
{#snippet child({ props })}
<a href={resolve(courseUrl, {})} {...props}>
{#if !isLMS && !isOnLandingPage}
<CardDropdown {id} {title} {description} />
{/if}
<Item.Header>
2026-03-09 11:18:22 +01:00
<Item.Media variant="image" class="relative h-50! w-full!">
<Image src={bannerImage} alt="Course banner image" className="w-full h-full rounded-sm object-cover" />
{#if type}
{@const tag = COURSE_TAG[type]}
2026-03-09 11:18:22 +01:00
<Badge class="absolute bottom-2 left-2 z-10 flex items-center rounded-md! capitalize" variant="secondary">
<tag.icon class={tag.iconStyle} />
{tag.label}
</Badge>
{/if}
</Item.Media>
</Item.Header>
<Item.Content>
2026-03-09 11:18:22 +01:00
<Item.Title class="line-clamp-1 min-h-[44px] text-base!">
{title}
</Item.Title>
2023-09-27 04:23:45 +01:00
<Item.Description class="min-h-[63px]">{description}</Item.Description>
2026-03-09 11:18:22 +01:00
<div class="ui:text-muted-foreground flex min-h-[12px] flex-wrap items-center gap-1">
{#if !isLMS && courseTags.length > 0}
{#each courseTags as tag (tag.id)}
<Item.SubDescription class="border-border inline-flex items-center gap-1.5 rounded-full border px-2">
<span
class="ui:bg-primary/60 inline-block h-1.5 w-1.5 rounded-full"
style={tag.color ? `background-color: ${tag.color}` : undefined}
aria-hidden="true"
></span>
<span>{tag.name}</span>
</Item.SubDescription>
{/each}
{/if}
</div>
<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">
2026-03-09 11:18:22 +01:00
{formatter?.format(pricingData?.cost ?? 0)}
</span>
{:else}
{formatter.format(cost)}
{/if}
</span>
{:else if isLMS}
{#if !isExplore}
<div class="flex w-3/4 items-center gap-2">
2026-03-09 11:18:22 +01:00
<Progress value={progressRate} />
<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">
2026-03-09 11:18:22 +01:00
{pluralize($t('courses.course_card.students'), totalStudents, true)}
</p>
</div>
{/if}
</div>
</Item.Content>
</a>
{/snippet}
</Item.Root>