feat: added product page

This commit is contained in:
Tobias Klemp
2025-11-29 22:27:37 +01:00
parent 90280fd436
commit 6d03984e21
85 changed files with 1869 additions and 436 deletions

View File

@@ -0,0 +1,98 @@
<script lang="ts">
import type { ImageCarouselItem } from '.'
import * as Carousel from '../carousel'
import ImgCarouselItem from './ImageCarouselItem.svelte'
import type { CarouselAPI } from '$lib/components/ui/carousel/context.js'
import { Button } from '../button'
import { ChevronLeftIcon, ChevronRightIcon } from '@lucide/svelte'
const {
items,
preview = false,
buttonPosition = 'default',
}: {
items: ImageCarouselItem[]
preview?: boolean
buttonPosition?: 'contained' | 'default' | 'hidden'
} = $props()
let api = $state<CarouselAPI>()
let currentImageIndex = $state(0)
$effect(() => {
if (!api) return
api.on('select', () => {
if (!api) return
currentImageIndex = api.selectedScrollSnap()
})
})
function onPreviewImageClick(toIndex: number) {
if (!api) return
api.scrollTo(toIndex, false)
}
function prevImage() {
if (!api) return
api.scrollPrev()
}
function nextImage() {
if (!api) return
api.scrollNext()
}
</script>
<div>
<Carousel.Root setApi={(emblaApi) => (api = emblaApi)}>
<Carousel.Content>
{#each items as item}
<ImgCarouselItem {item} />
{/each}
</Carousel.Content>
{#if buttonPosition === 'default'}
<Carousel.Next />
{:else if buttonPosition === 'contained'}
<Button
onclick={prevImage}
variant="outline"
size="icon"
class="absolute top-1/2 -translate-y-1/2 left-4 rounded-full"
><ChevronLeftIcon /></Button
>
{/if}
{#if buttonPosition === 'default'}
<Carousel.Previous />
{:else if buttonPosition === 'contained'}
<Button
onclick={nextImage}
variant="outline"
size="icon"
class="absolute top-1/2 -translate-y-1/2 right-4 rounded-full"
><ChevronRightIcon /></Button
>
{/if}
</Carousel.Root>
{#if preview}
<Carousel.Root
opts={{
align: 'start',
}}
class="hidden md:block"
>
<Carousel.Content>
{#each items as item, i}
<ImgCarouselItem
onclick={() => onPreviewImageClick(i)}
{item}
class="md:basis-1/3 lg:basis-1/4 mt-4"
isSelected={i === currentImageIndex}
/>
{/each}
</Carousel.Content>
</Carousel.Root>
{/if}
</div>

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import { cn } from '$lib/utils'
import type { ImageCarouselItem } from '.'
import * as Carousel from '../carousel'
const {
item,
class: className,
isSelected,
onclick,
}: {
item: ImageCarouselItem
class?: string
isSelected?: boolean
onclick?: () => void
} = $props()
const itemClass = $derived(
cn(
'object-cover rounded-lg',
isSelected
? 'border-2 transition-all border-blue-600 dark:border-blue-500'
: 'border-2 transition-all border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600',
!item.aspect || item.aspect === '1:1' ? 'aspect-square' : '',
item.aspect === '16:9' ? 'aspect-video' : '',
item.aspect === '9:16' ? 'aspect-9/16' : '',
item.aspect === '4:3' ? 'aspect-4/3' : '',
),
)
</script>
<Carousel.Item {onclick} class={className}>
<img src={item.image} alt={item.alt} class={itemClass} />
</Carousel.Item>

View File

@@ -0,0 +1,9 @@
import ImageCarousel from './ImageCarousel.svelte'
type ImageCarouselItem = {
image: string
alt: string
aspect?: '1:1' | '16:9' | '9:16' | '4:3'
}
export { ImageCarousel, type ImageCarouselItem }