mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-08-05 05:05:17 +02:00
feat: update CardCarousel component to handle images, name, and icon props across various cards
This commit is contained in:
parent
7a61ba2d22
commit
d53991e3f9
8 changed files with 76 additions and 67 deletions
|
@ -1,43 +1,33 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Location, Lodging, Transportation } from '$lib/types';
|
|
||||||
import ImageDisplayModal from './ImageDisplayModal.svelte';
|
import ImageDisplayModal from './ImageDisplayModal.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { ContentImage } from '$lib/types';
|
||||||
export let adventures: Location[] | Transportation[] | Lodging[] = [];
|
export let images: ContentImage[] = [];
|
||||||
|
export let name: string = '';
|
||||||
|
export let icon: string = '';
|
||||||
|
|
||||||
let currentSlide = 0;
|
let currentSlide = 0;
|
||||||
let showImageModal = false;
|
let showImageModal = false;
|
||||||
let modalInitialIndex = 0;
|
let modalInitialIndex = 0;
|
||||||
|
|
||||||
$: adventure_images = adventures.flatMap((adventure) =>
|
$: sortedImages = [...images].sort((a, b) => {
|
||||||
adventure.images.map((image) => ({
|
if (a.is_primary && !b.is_primary) {
|
||||||
image: image.image,
|
return -1;
|
||||||
adventure: adventure,
|
} else if (!a.is_primary && b.is_primary) {
|
||||||
is_primary: image.is_primary
|
return 1;
|
||||||
}))
|
} else {
|
||||||
);
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (adventure_images.length > 0) {
|
if (sortedImages.length > 0) {
|
||||||
currentSlide = 0;
|
currentSlide = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
|
||||||
// sort so that any image in adventure_images .is_primary is first
|
|
||||||
adventure_images.sort((a, b) => {
|
|
||||||
if (a.is_primary && !b.is_primary) {
|
|
||||||
return -1;
|
|
||||||
} else if (!a.is_primary && b.is_primary) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeSlide(direction: string) {
|
function changeSlide(direction: string) {
|
||||||
if (direction === 'next' && currentSlide < adventure_images.length - 1) {
|
if (direction === 'next' && currentSlide < sortedImages.length - 1) {
|
||||||
currentSlide = currentSlide + 1;
|
currentSlide = currentSlide + 1;
|
||||||
} else if (direction === 'prev' && currentSlide > 0) {
|
} else if (direction === 'prev' && currentSlide > 0) {
|
||||||
currentSlide = currentSlide - 1;
|
currentSlide = currentSlide - 1;
|
||||||
|
@ -54,16 +44,17 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if showImageModal && adventure_images.length > 0}
|
{#if showImageModal && sortedImages.length > 0}
|
||||||
<ImageDisplayModal
|
<ImageDisplayModal
|
||||||
images={adventure_images}
|
images={sortedImages}
|
||||||
initialIndex={modalInitialIndex}
|
initialIndex={modalInitialIndex}
|
||||||
on:close={closeImageModal}
|
on:close={closeImageModal}
|
||||||
|
{name}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<figure>
|
<figure>
|
||||||
{#if adventure_images && adventure_images.length > 0}
|
{#if sortedImages && sortedImages.length > 0}
|
||||||
<div class="carousel w-full relative">
|
<div class="carousel w-full relative">
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<div class="carousel-item w-full block">
|
<div class="carousel-item w-full block">
|
||||||
|
@ -75,17 +66,17 @@
|
||||||
class="cursor-pointer relative group"
|
class="cursor-pointer relative group"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={adventure_images[currentSlide].image}
|
src={sortedImages[currentSlide].image}
|
||||||
class="w-full h-48 object-cover transition-all group-hover:brightness-110"
|
class="w-full h-48 object-cover transition-all group-hover:brightness-110"
|
||||||
alt={adventure_images[currentSlide].adventure.name}
|
alt={name || 'Image'}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Overlay indicator for multiple images -->
|
<!-- Overlay indicator for multiple images -->
|
||||||
<!-- {#if adventure_images.length > 1}
|
<!-- {#if sortedImages.length > 1}
|
||||||
<div
|
<div
|
||||||
class="absolute top-3 right-3 bg-black/60 text-white px-2 py-1 rounded-lg text-xs font-medium"
|
class="absolute top-3 right-3 bg-black/60 text-white px-2 py-1 rounded-lg text-xs font-medium"
|
||||||
>
|
>
|
||||||
{currentSlide + 1} / {adventure_images.length}
|
{currentSlide + 1} / {sortedImages.length}
|
||||||
</div>
|
</div>
|
||||||
{/if} -->
|
{/if} -->
|
||||||
|
|
||||||
|
@ -113,7 +104,7 @@
|
||||||
</div> -->
|
</div> -->
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
{#if adventure_images.length > 1}
|
{#if sortedImages.length > 1}
|
||||||
<div class="absolute inset-0 flex items-center justify-between pointer-events-none">
|
<div class="absolute inset-0 flex items-center justify-between pointer-events-none">
|
||||||
{#if currentSlide > 0}
|
{#if currentSlide > 0}
|
||||||
<button
|
<button
|
||||||
|
@ -134,7 +125,7 @@
|
||||||
<div class="w-12"></div>
|
<div class="w-12"></div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if currentSlide < adventure_images.length - 1}
|
{#if currentSlide < sortedImages.length - 1}
|
||||||
<button
|
<button
|
||||||
on:click|stopPropagation={() => changeSlide('next')}
|
on:click|stopPropagation={() => changeSlide('next')}
|
||||||
class="btn btn-circle btn-sm mr-2 pointer-events-auto bg-neutral border-none text-neutral-content shadow-lg"
|
class="btn btn-circle btn-sm mr-2 pointer-events-auto bg-neutral border-none text-neutral-content shadow-lg"
|
||||||
|
@ -155,9 +146,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Dot indicators at bottom -->
|
<!-- Dot indicators at bottom -->
|
||||||
<!-- {#if adventure_images.length > 1}
|
<!-- {#if sortedImages.length > 1}
|
||||||
<div class="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2">
|
<div class="absolute bottom-3 left-1/2 -translate-x-1/2 flex gap-2">
|
||||||
{#each adventure_images as _, index}
|
{#each sortedImages as _, index}
|
||||||
<button
|
<button
|
||||||
on:click|stopPropagation={() => (currentSlide = index)}
|
on:click|stopPropagation={() => (currentSlide = index)}
|
||||||
class="w-2 h-2 rounded-full transition-all pointer-events-auto {index ===
|
class="w-2 h-2 rounded-full transition-all pointer-events-auto {index ===
|
||||||
|
@ -173,14 +164,21 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- add a figure with a gradient instead -->
|
<!-- Fallback with emoji icon as main image -->
|
||||||
<div class="w-full h-48 bg-gradient-to-r from-success via-base to-primary relative">
|
<div class="w-full h-48 relative flex items-center justify-center">
|
||||||
<!-- subtle button bottom left text
|
{#if icon}
|
||||||
<div
|
<!-- Clean background with emoji as the focal point -->
|
||||||
class="absolute bottom-0 left-0 px-2 py-1 text-md font-medium bg-neutral rounded-tr-lg shadow-md"
|
<div
|
||||||
>
|
class="w-full h-full bg-gradient-to-r from-success via-base to-primary flex items-center justify-center"
|
||||||
{$t('adventures.no_image_found')}
|
>
|
||||||
</div> -->
|
<div class="text-8xl select-none">
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Original gradient fallback when no icon -->
|
||||||
|
<div class="w-full h-full bg-gradient-to-r from-success via-base to-primary"></div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</figure>
|
</figure>
|
||||||
|
|
|
@ -91,7 +91,11 @@
|
||||||
>
|
>
|
||||||
<!-- Image Carousel -->
|
<!-- Image Carousel -->
|
||||||
<div class="relative overflow-hidden rounded-t-2xl">
|
<div class="relative overflow-hidden rounded-t-2xl">
|
||||||
<CardCarousel adventures={collection.locations} />
|
<CardCarousel
|
||||||
|
images={collection.locations.flatMap((location) => location.images)}
|
||||||
|
name={collection.name}
|
||||||
|
icon="📚"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Badge Overlay -->
|
<!-- Badge Overlay -->
|
||||||
<div class="absolute top-4 left-4 flex flex-col gap-2">
|
<div class="absolute top-4 left-4 flex flex-col gap-2">
|
||||||
|
|
|
@ -3,15 +3,15 @@
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
let modal: HTMLDialogElement;
|
let modal: HTMLDialogElement;
|
||||||
import type { Location } from '$lib/types';
|
|
||||||
|
|
||||||
export let images: { image: string; adventure: any | null }[] = [];
|
|
||||||
export let initialIndex: number = 0;
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { ContentImage } from '$lib/types';
|
||||||
|
export let images: ContentImage[] = [];
|
||||||
|
export let initialIndex: number = 0;
|
||||||
|
export let name: string = '';
|
||||||
|
export let location: string = '';
|
||||||
|
|
||||||
let currentIndex = initialIndex;
|
let currentIndex = initialIndex;
|
||||||
let currentImage = images[currentIndex]?.image || '';
|
let currentImage = images[currentIndex]?.image || '';
|
||||||
let currentAdventure = images[currentIndex]?.adventure || null;
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||||
|
@ -48,7 +48,6 @@
|
||||||
function updateCurrentSlide(index: number) {
|
function updateCurrentSlide(index: number) {
|
||||||
currentIndex = index;
|
currentIndex = index;
|
||||||
currentImage = images[currentIndex]?.image || '';
|
currentImage = images[currentIndex]?.image || '';
|
||||||
currentAdventure = images[currentIndex]?.adventure || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextSlide() {
|
function nextSlide() {
|
||||||
|
@ -86,7 +85,7 @@
|
||||||
on:keydown={handleKeydown}
|
on:keydown={handleKeydown}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
{#if currentAdventure && currentImage}
|
{#if images.length > 0 && currentImage}
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div
|
<div
|
||||||
class="top-0 z-10 bg-base-100/90 backdrop-blur-lg border-b border-base-300 -mx-6 -mt-6 px-6 py-4 mb-6"
|
class="top-0 z-10 bg-base-100/90 backdrop-blur-lg border-b border-base-300 -mx-6 -mt-6 px-6 py-4 mb-6"
|
||||||
|
@ -110,7 +109,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold text-primary">
|
<h1 class="text-2xl font-bold text-primary">
|
||||||
{currentAdventure.name}
|
{name}
|
||||||
</h1>
|
</h1>
|
||||||
{#if images.length > 1}
|
{#if images.length > 1}
|
||||||
<p class="text-sm text-base-content/60">
|
<p class="text-sm text-base-content/60">
|
||||||
|
@ -174,7 +173,7 @@
|
||||||
<div class="flex justify-center items-center max-w-full">
|
<div class="flex justify-center items-center max-w-full">
|
||||||
<img
|
<img
|
||||||
src={currentImage}
|
src={currentImage}
|
||||||
alt={currentAdventure.name}
|
alt={name}
|
||||||
class="max-w-full max-h-[75vh] object-contain rounded-lg shadow-lg"
|
class="max-w-full max-h-[75vh] object-contain rounded-lg shadow-lg"
|
||||||
style="max-width: 100%; max-height: 75vh; object-fit: contain;"
|
style="max-width: 100%; max-height: 75vh; object-fit: contain;"
|
||||||
/>
|
/>
|
||||||
|
@ -210,11 +209,7 @@
|
||||||
: 'border-base-300 hover:border-base-400'}"
|
: 'border-base-300 hover:border-base-400'}"
|
||||||
on:click={() => goToSlide(index)}
|
on:click={() => goToSlide(index)}
|
||||||
>
|
>
|
||||||
<img
|
<img src={imageData.image} alt={name} class="w-full h-full object-cover" />
|
||||||
src={imageData.image}
|
|
||||||
alt={imageData.adventure?.name || 'Image'}
|
|
||||||
class="w-full h-full object-cover"
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -227,7 +222,7 @@
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-sm text-base-content/60">
|
<div class="text-sm text-base-content/60">
|
||||||
{#if currentAdventure.location}
|
{#if location}
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
|
@ -243,7 +238,7 @@
|
||||||
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
{currentAdventure.location}
|
{location}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -190,7 +190,7 @@
|
||||||
>
|
>
|
||||||
<!-- Image Section with Overlay -->
|
<!-- Image Section with Overlay -->
|
||||||
<div class="relative overflow-hidden rounded-t-2xl">
|
<div class="relative overflow-hidden rounded-t-2xl">
|
||||||
<CardCarousel adventures={[adventure]} />
|
<CardCarousel images={adventure.images} icon={adventure.category?.icon} name={adventure.name} />
|
||||||
|
|
||||||
<!-- Status Overlay -->
|
<!-- Status Overlay -->
|
||||||
<div class="absolute top-4 left-4 flex flex-col gap-2">
|
<div class="absolute top-4 left-4 flex flex-col gap-2">
|
||||||
|
|
|
@ -99,7 +99,7 @@
|
||||||
>
|
>
|
||||||
<!-- Image Section with Overlay -->
|
<!-- Image Section with Overlay -->
|
||||||
<div class="relative overflow-hidden rounded-t-2xl">
|
<div class="relative overflow-hidden rounded-t-2xl">
|
||||||
<CardCarousel adventures={[lodging]} />
|
<CardCarousel images={lodging.images} icon={getLodgingIcon(lodging.type)} name={lodging.name} />
|
||||||
|
|
||||||
<!-- Category Badge -->
|
<!-- Category Badge -->
|
||||||
{#if lodging.type}
|
{#if lodging.type}
|
||||||
|
|
|
@ -118,7 +118,11 @@
|
||||||
>
|
>
|
||||||
<!-- Image Section with Overlay -->
|
<!-- Image Section with Overlay -->
|
||||||
<div class="relative overflow-hidden rounded-t-2xl">
|
<div class="relative overflow-hidden rounded-t-2xl">
|
||||||
<CardCarousel adventures={[transportation]} />
|
<CardCarousel
|
||||||
|
images={transportation.images}
|
||||||
|
icon={getTransportationIcon(transportation.type)}
|
||||||
|
name={transportation.name}
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Privacy Indicator -->
|
<!-- Privacy Indicator -->
|
||||||
<div class="absolute top-4 right-4">
|
<div class="absolute top-4 right-4">
|
||||||
|
|
|
@ -1224,7 +1224,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if orderedItem.type === 'adventure' && orderedItem.item && 'images' in orderedItem.item}
|
{#if orderedItem.type === 'adventure' && orderedItem.item && 'visits' in orderedItem.item}
|
||||||
<LocationCard
|
<LocationCard
|
||||||
user={data.user}
|
user={data.user}
|
||||||
on:edit={editAdventure}
|
on:edit={editAdventure}
|
||||||
|
@ -1293,7 +1293,11 @@
|
||||||
{#if isPopupOpen}
|
{#if isPopupOpen}
|
||||||
<Popup openOn="click" offset={[0, -10]} on:close={() => (isPopupOpen = false)}>
|
<Popup openOn="click" offset={[0, -10]} on:close={() => (isPopupOpen = false)}>
|
||||||
{#if adventure.images && adventure.images.length > 0}
|
{#if adventure.images && adventure.images.length > 0}
|
||||||
<CardCarousel adventures={[adventure]} />
|
<CardCarousel
|
||||||
|
images={adventure.images}
|
||||||
|
name={adventure.name}
|
||||||
|
icon={adventure?.category?.icon}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="text-lg text-black font-bold">{adventure.name}</div>
|
<div class="text-lg text-black font-bold">{adventure.name}</div>
|
||||||
<p class="font-semibold text-black text-md">
|
<p class="font-semibold text-black text-md">
|
||||||
|
|
|
@ -208,7 +208,11 @@
|
||||||
<div class="min-w-64 max-w-sm">
|
<div class="min-w-64 max-w-sm">
|
||||||
{#if adventure.images && adventure.images.length > 0}
|
{#if adventure.images && adventure.images.length > 0}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<CardCarousel adventures={[adventure]} />
|
<CardCarousel
|
||||||
|
images={adventure.images}
|
||||||
|
name={adventure.name}
|
||||||
|
icon={adventure?.category?.icon}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue