Files
classroomio/packages/ui/src/custom/widget-layouts/carousel.svelte
T

169 lines
5.2 KiB
Svelte

<script lang="ts">
import { onDestroy, onMount } from 'svelte';
import type { TWidgetPayload } from '@cio/utils/validation/widget';
import Stars from './atoms/stars.svelte';
import TypeBadge from './atoms/type-badge.svelte';
import Thumb from './atoms/thumb.svelte';
import { formatLessonsLine } from './utils';
let { payload }: { payload: TWidgetPayload } = $props();
const options = $derived(payload.design.layoutOptions.carousel);
const courses = $derived(payload.courses.slice(0, options.maxCount));
const visible = $derived(Number(options.visibleCards));
const maxIndex = $derived(Math.max(courses.length - visible, 0));
let position = $state(0);
let timer: ReturnType<typeof setInterval> | null = null;
const cardBasis = $derived(`calc((100% - ${(visible - 1) * 12}px) / ${visible})`);
const translatePct = $derived(courses.length > 0 ? `calc(-${position} * (100% + 12px) / ${visible})` : '0');
function go(index: number) {
if (maxIndex === 0) {
position = 0;
return;
}
if (options.loop) {
position = ((index % (maxIndex + 1)) + (maxIndex + 1)) % (maxIndex + 1);
} else {
position = Math.max(0, Math.min(index, maxIndex));
}
}
function startAutoplay() {
if (!options.autoPlay || maxIndex === 0) return;
timer = setInterval(() => go(position + 1), Math.max(options.autoPlayIntervalMs, 3000));
}
function stopAutoplay() {
if (timer) {
clearInterval(timer);
timer = null;
}
}
onMount(() => {
startAutoplay();
});
onDestroy(() => {
stopAutoplay();
});
$effect(() => {
if (position > maxIndex) position = maxIndex;
});
</script>
{#if courses.length === 0}
<div class="cio-empty">No courses to display.</div>
{:else}
<div
class="cio-cr-wrap"
onmouseenter={stopAutoplay}
onmouseleave={startAutoplay}
role="region"
aria-roledescription="carousel"
>
<div class="cio-cr-outer">
<div class="cio-cr-track" style={`transform: translateX(${translatePct})`}>
{#each courses as course (course.id)}
<a
class="cio-cr-card"
href={course.url}
target="_blank"
rel="noreferrer noopener"
style={`flex:0 0 ${cardBasis}`}
>
{#if payload.design.content.showCourseImage}
<div class="cio-cr-img">
<Thumb src={course.imageUrl} alt={course.title} />
</div>
{/if}
<div class="cio-cr-body">
<h3 class="cio-cr-title">{course.title}</h3>
<div class="cio-cr-rating-row">
<Stars rating={course.rating} ratingCount={course.ratingCount} showCount={false} />
</div>
{#if payload.design.content.showCourseTypeBadge}
<div class="cio-cr-tags-row">
<TypeBadge courseType={course.courseType} />
</div>
{/if}
<div class="cio-cr-meta">{formatLessonsLine(course, payload.design.content.showLessonsCount)}</div>
</div>
</a>
{/each}
</div>
</div>
{#if maxIndex > 0 && (options.showDots || options.showArrows)}
<div class="cio-cr-nav">
{#if options.showDots}
<div class="cio-cr-dots">
{#each Array(maxIndex + 1) as _, i (i)}
<button
type="button"
class="cio-cr-dot"
class:cio-on={i === position}
aria-label={`Go to slide ${i + 1}`}
onclick={() => go(i)}
></button>
{/each}
</div>
{:else}
<span></span>
{/if}
{#if options.showArrows}
<div class="cio-cr-arrows">
<button
type="button"
class="cio-cr-btn"
onclick={() => go(position - 1)}
disabled={!options.loop && position === 0}
aria-label="Previous"
>
<svg
class="cio-cr-btn-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m15 18-6-6 6-6" />
</svg>
</button>
<button
type="button"
class="cio-cr-btn"
onclick={() => go(position + 1)}
disabled={!options.loop && position === maxIndex}
aria-label="Next"
>
<svg
class="cio-cr-btn-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="m9 18 6-6-6-6" />
</svg>
</button>
</div>
{:else}
<span></span>
{/if}
</div>
{/if}
</div>
{/if}