Files
classroomio/packages/ui/src/custom/course-card/course-card.svelte
T

179 lines
5.3 KiB
Svelte

<script lang="ts">
import type { Component, Snippet } from 'svelte';
import { cn } from '../../tools';
import { Badge } from '../../base/badge';
import { Separator } from '../../base/separator';
import * as Item from '../../base/item';
import { DEFAULT_COURSE_BANNER_IMAGE } from './constants';
interface Props {
href?: string;
title: string;
description: string;
/** Used when `media` snippet is not provided */
bannerImage?: string;
bannerAlt?: string;
typeBadge?: {
label: string;
icon: Component;
iconClass?: string;
};
/** e.g. public-course indicator on the banner */
visibilityBadge?: {
label: string;
icon: Component;
iconClass?: string;
};
class?: string;
/** Custom banner area (e.g. optimized `Image`); overrides `bannerImage` */
media?: Snippet;
/** Optional overlay inside the link (e.g. dropdown), typically absolutely positioned */
overlay?: Snippet;
tags?: Snippet;
footer?: Snippet;
}
let {
href,
title,
description,
bannerImage,
bannerAlt = 'Course banner image',
typeBadge,
visibilityBadge,
class: className = '',
media,
overlay,
tags,
footer
}: Props = $props();
const resolvedBannerImage = $derived(bannerImage?.trim() ? bannerImage : DEFAULT_COURSE_BANNER_IMAGE);
</script>
<Item.Root variant="outline" class={cn('ui:group ui:relative ui:max-w-[320px] ui:p-3!', className)}>
{#snippet child({ props })}
{#if href}
<a {href} {...props} class={cn('ui:block', props.class as string)}>
{@render overlay?.()}
<Item.Header>
<Item.Media variant="image" class="ui:relative ui:h-50! ui:w-full!">
{#if media}
{@render media()}
{:else}
<img
src={resolvedBannerImage}
alt={bannerAlt}
class="ui:w-full ui:h-full ui:rounded-sm ui:object-cover"
/>
{/if}
{#if typeBadge}
{@const Icon = typeBadge.icon}
<Badge
class="ui:absolute ui:bottom-2 ui:left-2 ui:z-10 ui:flex ui:items-center ui:rounded-md! ui:capitalize"
variant="secondary"
>
<Icon class={typeBadge.iconClass} />
{typeBadge.label}
</Badge>
{/if}
{#if visibilityBadge}
{@const VIcon = visibilityBadge.icon}
<Badge
class={cn(
'ui:absolute ui:bottom-2 ui:z-10 ui:flex ui:items-center ui:rounded-md! ui:gap-1 ui:capitalize',
typeBadge ? 'ui:right-2 ui:left-auto' : 'ui:left-2'
)}
variant="secondary"
>
<VIcon class={visibilityBadge.iconClass} />
{visibilityBadge.label}
</Badge>
{/if}
</Item.Media>
</Item.Header>
<Item.Content>
<Item.Title class="ui:flex ui:w-full ui:min-h-14 ui:items-center ui:text-base!">
<span class="ui:line-clamp-2">{title}</span>
</Item.Title>
<Item.Description class="ui:min-h-[63px]">{description}</Item.Description>
{#if tags}
{@render tags()}
{/if}
{#if footer}
<Separator class="ui:my-3" />
{@render footer()}
{/if}
</Item.Content>
</a>
{:else}
<div {...props} class={cn('ui:block', props.class as string)}>
{@render overlay?.()}
<Item.Header>
<Item.Media variant="image" class="ui:relative ui:h-50! ui:w-full!">
{#if media}
{@render media()}
{:else}
<img
src={resolvedBannerImage}
alt={bannerAlt}
class="ui:w-full ui:h-full ui:rounded-sm ui:object-cover"
/>
{/if}
{#if typeBadge}
{@const Icon = typeBadge.icon}
<Badge
class="ui:absolute ui:bottom-2 ui:left-2 ui:z-10 ui:flex ui:items-center ui:rounded-md! ui:capitalize"
variant="secondary"
>
<Icon class={typeBadge.iconClass} />
{typeBadge.label}
</Badge>
{/if}
{#if visibilityBadge}
{@const VIcon = visibilityBadge.icon}
<Badge
class={cn(
'ui:absolute ui:bottom-2 ui:z-10 ui:flex ui:items-center ui:rounded-md! ui:gap-1 ui:capitalize',
typeBadge ? 'ui:right-2 ui:left-auto' : 'ui:left-2'
)}
variant="secondary"
>
<VIcon class={visibilityBadge.iconClass} />
{visibilityBadge.label}
</Badge>
{/if}
</Item.Media>
</Item.Header>
<Item.Content>
<Item.Title class="ui:flex ui:w-full ui:min-h-14 ui:items-center ui:text-base!">
<span class="ui:line-clamp-2">{title}</span>
</Item.Title>
<Item.Description class="ui:min-h-[63px]">{description}</Item.Description>
{#if tags}
{@render tags()}
{/if}
{#if footer}
<Separator class="ui:my-3" />
{@render footer()}
{/if}
</Item.Content>
</div>
{/if}
{/snippet}
</Item.Root>