Last active
February 15, 2025 04:58
-
-
Save maietta/c01c56bc2a9a2a60dfdc86f1b3b66616 to your computer and use it in GitHub Desktop.
SwiperJS with Svelte 5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { onMount } from 'svelte'; | |
import Swiper from 'swiper'; | |
import { register } from 'swiper/element/bundle'; | |
import 'swiper/css'; | |
import 'swiper/css/navigation'; | |
import 'swiper/css/pagination'; | |
import 'swiper/css/thumbs'; | |
interface ImageSizes { | |
large: string; | |
medium: string; | |
original: string; | |
small: string; | |
thumb: string; | |
} | |
interface ImageData { | |
caption: string; | |
dominantColor: string; | |
height: number; | |
width: number; | |
images: ImageSizes; | |
objectId: string; | |
} | |
type ImageCollection = ImageData[]; | |
register(); | |
// Props and state | |
let data = $props<{ photos: ImageCollection }>(); | |
let photos = $derived(data.photos); | |
const baseURL = 'https://sjc1.vultrobjects.com/m685'; | |
const viewportWidth = typeof window !== 'undefined' ? window.innerWidth : 0; | |
const isMobile = viewportWidth < 768; | |
const desiredSize = isMobile ? 'medium' : 'large'; | |
// Helper function | |
const getPhotoUrl = (photo: { images: ImageSizes }, size: keyof ImageSizes) => { | |
const url = photo.images[size]; | |
return url ? `${baseURL}/${url}` : ''; | |
}; | |
// Derived state | |
let photoUrls = $derived( | |
photos.map((photo: ImageData) => ({ | |
url: getPhotoUrl(photo, desiredSize as keyof ImageSizes), | |
thumb: getPhotoUrl(photo, 'thumb'), | |
caption: photo.caption || '', // Assuming caption exists in ImageData; if not, add it | |
dominantColor: photo.dominantColor || '' | |
})) | |
); | |
// Swiper instances | |
let mainSwiper: Swiper; | |
let modalSwiper: Swiper; | |
let showModal = $state(false); | |
let backgroundImage = $state(''); | |
let backgroundAccent = $state(''); | |
const openModal = (index: number) => { | |
showModal = true; | |
setTimeout(() => { | |
modalSwiper = new Swiper('.swiper-modal', { | |
initialSlide: index, | |
navigation: { | |
nextEl: '.modal-nav.swiper-button-next', | |
prevEl: '.modal-nav.swiper-button-prev' | |
} | |
}); | |
}, 100); | |
}; | |
const closeModal = () => { | |
showModal = false; | |
if (modalSwiper) modalSwiper.destroy(true, true); | |
}; | |
onMount(() => { | |
const thumbnailSwiper = new Swiper('.swiper_thumbnails', { | |
slidesPerView: 1, | |
// spaceBetween: 2, | |
freeMode: false | |
}); | |
mainSwiper = new Swiper('.swiper_main', { | |
loop: true, | |
autoHeight: true, | |
autoplay: { | |
delay: 4000, | |
pauseOnMouseEnter: true | |
}, | |
navigation: { | |
nextEl: '.swiper-button-next', | |
prevEl: '.swiper-button-prev' | |
}, | |
thumbs: { | |
swiper: thumbnailSwiper | |
}, | |
pagination: { | |
el: '.swiper-pagination', | |
clickable: true | |
}, | |
on: { | |
slideChange: () => { | |
// backgroundImage = photoUrls[mainSwiper.activeIndex].url; | |
// backgroundAccent = photoUrls[mainSwiper.activeIndex].dominantColor; | |
} | |
} | |
}); | |
return () => { | |
mainSwiper.destroy(true, true); | |
if (modalSwiper) modalSwiper.destroy(true, true); | |
}; | |
}); | |
</script> | |
<!-- TODO: Add keyboard navigation for main swiper --> | |
<!-- TODO: Pause autoplay when photos are not in main view and resume when they are. This is to prevent content from jumping around --> | |
<!-- Main Swiper --> | |
<div class="swiper swiper_main"> | |
<div class="swiper-wrapper max-h[calc(100vh-100px)]"> | |
{#each photoUrls as { url, caption }, index} | |
<div class="swiper-slide"> | |
<button | |
class=" focus:outline-hidden cursor-pointer" | |
aria-label="View photo ${index + 1} of ${photoUrls.length}" | |
onclick={() => openModal(index)} | |
> | |
<img class="w-full" alt={caption} src={url} /> | |
</button> | |
</div> | |
{/each} | |
</div> | |
<div class="swiper-button-prev"></div> | |
<div class="swiper-button-next"></div> | |
</div> | |
<!-- Thumbnails Swiper --> | |
<div class="swiper swiper_thumbnails"> | |
<div class="swiper-wrapper flex max-h-24"> | |
{#each photoUrls as { thumb }, index} | |
<div class="swiper-slide w-full cursor-pointer"> | |
<img src={thumb} alt="Thumbnail ${index + 1}" /> | |
</div> | |
{/each} | |
</div> | |
</div> | |
{#if showModal} | |
<div | |
class="modal fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-75" | |
role="dialog" | |
aria-modal="true" | |
aria-labelledby="modal-title" | |
aria-describedby="modal-description" | |
> | |
<div class="swiper swiper-modal h-full w-full"> | |
<h2 id="modal-title" class="sr-only">Photo Viewer</h2> | |
<p id="modal-description" class="sr-only"> | |
Use the next and previous buttons to navigate through photos. | |
</p> | |
<div class="swiper-wrapper"> | |
{#each photoUrls as { url }, index} | |
<div class="swiper-slide"> | |
<img class="h-full w-full object-contain" alt="Photo ${index + 1}" src={url} /> | |
</div> | |
{/each} | |
</div> | |
<button class="swiper-button-prev modal-nav" aria-label="Previous photo"></button> | |
<button class="swiper-button-next modal-nav" aria-label="Next photo"></button> | |
</div> | |
<button | |
class="absolute right-4 top-4 z-50 text-white" | |
aria-label="Close modal" | |
onclick={closeModal} | |
> | |
<svg | |
xmlns="http://www.w3.org/2000/svg" | |
class="h-24 w-24" | |
fill="none" | |
viewBox="0 0 24 24" | |
stroke="currentColor" | |
> | |
<path | |
stroke-linecap="round" | |
stroke-linejoin="round" | |
stroke-width="2" | |
d="M6 18L18 6M6 6l12 12" | |
/> | |
</svg> | |
</button> | |
</div> | |
{/if} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- Above the fold --> | |
<section class="container m-0 mx-auto flex p-0"> | |
<div class="w-full md:w-1/2 lg:w-2/3"> | |
{#if availableTabs.length > 1} | |
<Tabs.Root value="photos" class="relative z-0 border-2 p-4"> | |
<!-- Tabs List --> | |
<Tabs.List class="absolute z-10 m-5"> | |
{#each Object.values(availableTabs) as tab} | |
<Tabs.Trigger value={tab.value}> | |
{tab.label} | |
</Tabs.Trigger> | |
{/each} | |
</Tabs.List> | |
<!-- Tabs Content --> | |
{#each Object.values(availableTabs) as tab} | |
<Tabs.Content value={tab.value} class="m-0"> | |
{#if tab.value === 'photos'} | |
<PhotoGallery {photos} /> | |
{/if} | |
{#if tab.value === 'virtual-tour'} | |
<VirtualTour /> | |
{/if} | |
{#if tab.value === 'drone-tour'} | |
<DroneTour /> | |
{/if} | |
</Tabs.Content> | |
{/each} | |
</Tabs.Root> | |
{:else} | |
<!-- Show Photos Component Only --> | |
<div class="w-full"> | |
<PhotoGallery {photos} /> | |
</div> | |
{/if} | |
</div> | |
<!-- Secondary Content --> | |
<div class="w-full md:w-1/2 lg:w-1/3"> | |
<!-- Property Inquiry Form --> | |
<div class="container sticky top-0"> | |
<!-- <InqueryForm mlsid={mls.SystemID} {address} /> --> | |
</div> | |
</div> | |
</section> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment