feat(gallery): use UModal and UCarousel

This commit is contained in:
2025-12-31 02:10:47 +01:00
parent 400a35021f
commit 79850f0715
3 changed files with 113 additions and 136 deletions

View File

@@ -1,3 +1,5 @@
<template>
<UApp>
<NuxtPage />
</UApp>
</template>

View File

@@ -0,0 +1,99 @@
<script setup lang="ts">
type ImageMetadata = {
url: string;
date: string;
camera: string;
lens: string;
settings?: {
aperture?: string;
shutter?: string;
iso?: string;
focalLength?: string;
};
};
const props = defineProps<{
images: ImageMetadata[];
initialIndex: number;
}>();
const emit = defineEmits<{ close: [] }>();
const currentIndex = ref(props.initialIndex);
</script>
<template>
<UModal fullscreen :ui="{ body: 'bg-black', header: 'hidden' }">
<template #body>
<div
class="relative w-full h-full flex flex-col items-center justify-center"
>
<UButton
icon="i-lucide-x"
color="neutral"
variant="link"
size="xl"
class="absolute top-4 right-4 z-10"
aria-label="Close modal"
@click="emit('close')"
/>
<UCarousel
v-slot="{ item }"
:items="images"
:ui="{
item: 'flex-shrink-0 flex flex-col items-center justify-center select-none',
}"
arrows
:prev="{
variant: 'link',
icon: 'i-lucide-chevron-left',
class: 'left-4!',
ui: { leadingIcon: 'size-8' },
}"
:next="{
variant: 'link',
icon: 'i-lucide-chevron-right',
class: 'right-4!',
ui: { leadingIcon: 'size-8' },
}"
class="w-full h-full mt-10"
@select="(index) => (currentIndex = index)"
>
<img
:src="item.url"
class="max-w-[90vw] h-[80vh] object-contain rounded-sm"
/>
<!-- metadata -->
<div
class="bg-black px-4 py-2 text-sm font-mono text-white flex flex-wrap items-center justify-center gap-2"
>
<span>{{ item.date }}</span>
<span class="text-white/50">|</span>
<span>{{ item.camera }}</span>
<span class="text-white/50">|</span>
<span>{{ item.lens }}</span>
<template v-if="item.settings">
<span class="text-white/50">|</span>
<span v-if="item.settings.aperture">{{
item.settings.aperture
}}</span>
<span v-if="item.settings.shutter">{{
item.settings.shutter
}}</span>
<span v-if="item.settings.iso">ISO {{ item.settings.iso }}</span>
<span v-if="item.settings.focalLength">{{
item.settings.focalLength
}}</span>
</template>
</div>
</UCarousel>
<div class="text-white/70 text-sm">
{{ currentIndex + 1 }} / {{ images.length }}
</div>
</div>
</template>
</UModal>
</template>

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import { LazyGalleryModal } from "#components";
type ImageMetadata = {
url: string;
date: string;
@@ -148,10 +150,6 @@ const images: ImageMetadata[] = [
];
const columnCount = ref(3);
const isModalOpen = ref(false);
const currentImageIndex = ref(0);
const currentImage = computed(() => images[currentImageIndex.value]!);
const getColumnImages = (columnNumber: number) =>
images.filter((_, index) => index % columnCount.value === columnNumber - 1);
@@ -166,48 +164,26 @@ const updateColumns = () => {
}
};
const openModal = (image: ImageMetadata) => {
const overlay = useOverlay();
const galleryModal = overlay.create(LazyGalleryModal);
const openModal = async (image: ImageMetadata) => {
const index = images.indexOf(image);
if (index !== -1) {
currentImageIndex.value = index;
isModalOpen.value = true;
}
};
const closeModal = () => {
isModalOpen.value = false;
};
const nextImage = () => {
currentImageIndex.value = (currentImageIndex.value + 1) % images.length;
};
const prevImage = () => {
currentImageIndex.value =
(currentImageIndex.value - 1 + images.length) % images.length;
};
const handleKeydown = (event: KeyboardEvent) => {
if (!isModalOpen.value) return;
if (event.key === "Escape") {
closeModal();
} else if (event.key === "ArrowRight") {
nextImage();
} else if (event.key === "ArrowLeft") {
prevImage();
galleryModal.open({
images,
initialIndex: index,
});
}
};
onMounted(() => {
updateColumns();
window.addEventListener("resize", updateColumns);
window.addEventListener("keydown", handleKeydown);
});
onUnmounted(() => {
window.removeEventListener("resize", updateColumns);
window.removeEventListener("keydown", handleKeydown);
});
</script>
@@ -243,7 +219,7 @@ onUnmounted(() => {
<div
v-for="(image, idx) in getColumnImages(col)"
:key="idx"
class="relative overflow-hidden rounded-sm bg-black cursor-pointer"
class="relative overflow-hidden rounded-sm bg-black cursor-pointer transition-transform"
@click="openModal(image)"
>
<img
@@ -253,105 +229,5 @@ onUnmounted(() => {
</div>
</div>
</div>
<Teleport to="body">
<Transition name="modal">
<div
v-if="isModalOpen"
class="fixed inset-0 z-50 flex items-center font-[JetBrains_Mono] justify-center bg-black/95 backdrop-blur-sm"
@click="closeModal"
>
<!-- close -->
<UButton
icon="i-lucide-x"
color="neutral"
variant="link"
:ui="{ leadingIcon: 'size-8' }"
class="absolute top-6 right-6 z-10"
aria-label="Close modal"
@click="closeModal"
/>
<!-- previous -->
<UButton
icon="i-lucide-chevron-left"
color="neutral"
variant="link"
:ui="{ leadingIcon: 'size-8' }"
class="absolute left-6 z-10 hidden md:block"
aria-label="Previous image"
@click.stop="prevImage"
/>
<div
class="flex flex-col items-center justify-center gap-3 max-w-[95vw] max-h-[90vh] px-4 md:px-20"
@click.stop
>
<img
:key="currentImageIndex"
:src="currentImage.url"
class="max-w-[90vw] h-[80vh] object-contain rounded-sm"
alt="Gallery image"
/>
<!-- metadata -->
<div
class="bg-black px-4 py-2 text-sm text-white flex flex-wrap items-center justify-center gap-2"
>
<span>{{ currentImage.date }}</span>
<span class="text-white/50">|</span>
<span>{{ currentImage.camera }}</span>
<span class="text-white/50">|</span>
<span>{{ currentImage.lens }}</span>
<template v-if="currentImage.settings">
<span class="text-white/50">|</span>
<span v-if="currentImage.settings.aperture">{{
currentImage.settings.aperture
}}</span>
<span v-if="currentImage.settings.shutter">{{
currentImage.settings.shutter
}}</span>
<span v-if="currentImage.settings.iso"
>ISO {{ currentImage.settings.iso }}</span
>
<span v-if="currentImage.settings.focalLength">{{
currentImage.settings.focalLength
}}</span>
</template>
</div>
</div>
<!-- next -->
<UButton
icon="i-lucide-chevron-right"
color="neutral"
variant="link"
:ui="{ leadingIcon: 'size-8' }"
class="absolute right-6 z-10 hidden md:block"
aria-label="Next image"
@click.stop="nextImage"
/>
<!-- TODO: mobile navigation (use md:hidden) -->
<div
class="absolute bottom-6 left-1/2 transform -translate-x-1/2 text-white/70 text-sm"
>
{{ currentImageIndex + 1 }} / {{ images.length }}
</div>
</div>
</Transition>
</Teleport>
</div>
</template>
<style scoped>
.modal-enter-active,
.modal-leave-active {
transition: opacity 0.3s ease;
}
.modal-enter-from,
.modal-leave-to {
opacity: 0;
}
</style>