185 lines
4.8 KiB
Vue
185 lines
4.8 KiB
Vue
<script setup lang="ts">
|
|
import { ChevronLeft, ChevronRight } from '@lucide/vue';
|
|
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from '@/components/ui/dialog';
|
|
|
|
const open = defineModel<boolean>('open', { default: false });
|
|
|
|
const emit = defineEmits<{
|
|
close: [];
|
|
'update:url': [url: string];
|
|
}>();
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
url: string | null;
|
|
urls?: string[];
|
|
title?: string;
|
|
titles?: string[];
|
|
stocks?: string[];
|
|
}>(),
|
|
{
|
|
urls: () => [],
|
|
title: 'Pratinjau Foto',
|
|
titles: () => [],
|
|
stocks: () => [],
|
|
}
|
|
);
|
|
|
|
|
|
const activeUrl = ref<string | null>(props.url);
|
|
|
|
watch(
|
|
() => props.url,
|
|
(newUrl) => {
|
|
activeUrl.value = newUrl;
|
|
}
|
|
);
|
|
|
|
watch(open, (val) => {
|
|
if (!val) {
|
|
emit('close');
|
|
} else {
|
|
activeUrl.value = props.url;
|
|
}
|
|
});
|
|
|
|
const allUrls = computed(() => {
|
|
if (props.urls && props.urls.length > 0) {
|
|
return props.urls;
|
|
}
|
|
|
|
return props.url ? [props.url] : [];
|
|
});
|
|
|
|
const currentIndex = computed(() => {
|
|
if (!activeUrl.value) {
|
|
return -1;
|
|
}
|
|
|
|
return allUrls.value.indexOf(activeUrl.value);
|
|
});
|
|
|
|
const hasMultiple = computed(() => allUrls.value.length > 1);
|
|
|
|
const activeTitle = computed(() => {
|
|
if (props.titles && props.titles.length > 0 && currentIndex.value >= 0) {
|
|
return props.titles[currentIndex.value] ?? props.title ?? 'Pratinjau Foto';
|
|
}
|
|
|
|
return props.title ?? 'Pratinjau Foto';
|
|
});
|
|
|
|
const activeStock = computed(() => {
|
|
if (props.stocks && props.stocks.length > 0 && currentIndex.value >= 0) {
|
|
return props.stocks[currentIndex.value] ?? null;
|
|
}
|
|
|
|
return null;
|
|
});
|
|
|
|
function nextImage() {
|
|
if (!hasMultiple.value) {
|
|
return;
|
|
}
|
|
|
|
const nextIdx = (currentIndex.value + 1) % allUrls.value.length;
|
|
const newUrl = allUrls.value[nextIdx];
|
|
activeUrl.value = newUrl;
|
|
emit('update:url', newUrl);
|
|
}
|
|
|
|
function prevImage() {
|
|
if (!hasMultiple.value) {
|
|
return;
|
|
}
|
|
|
|
const prevIdx = (currentIndex.value - 1 + allUrls.value.length) % allUrls.value.length;
|
|
const newUrl = allUrls.value[prevIdx];
|
|
activeUrl.value = newUrl;
|
|
emit('update:url', newUrl);
|
|
}
|
|
|
|
function handleKeyDown(e: KeyboardEvent) {
|
|
if (!open.value || !hasMultiple.value) {
|
|
return;
|
|
}
|
|
|
|
if (e.key === 'ArrowRight') {
|
|
nextImage();
|
|
} else if (e.key === 'ArrowLeft') {
|
|
prevImage();
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('keydown', handleKeyDown);
|
|
});
|
|
|
|
let touchStartX = 0;
|
|
let touchEndX = 0;
|
|
|
|
function handleTouchStart(e: TouchEvent) {
|
|
touchStartX = e.changedTouches[0].screenX;
|
|
}
|
|
|
|
function handleTouchEnd(e: TouchEvent) {
|
|
touchEndX = e.changedTouches[0].screenX;
|
|
const diff = touchStartX - touchEndX;
|
|
|
|
if (Math.abs(diff) > 50) {
|
|
if (diff > 0) {
|
|
nextImage();
|
|
} else {
|
|
prevImage();
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog v-model:open="open">
|
|
<DialogContent class="sm:max-w-2xl p-4 overflow-hidden gap-4">
|
|
<DialogHeader class="pb-2 border-b">
|
|
<DialogTitle class="text-base font-semibold">{{ activeTitle }}</DialogTitle>
|
|
<p v-if="activeStock" class="text-sm text-muted-foreground">{{ activeStock }}</p>
|
|
</DialogHeader>
|
|
|
|
<div class="relative flex items-center justify-center min-h-[300px] select-none"
|
|
@touchstart="handleTouchStart" @touchend="handleTouchEnd">
|
|
<img v-if="activeUrl" :key="activeUrl" :src="activeUrl" :alt="title ?? 'Pratinjau gambar'"
|
|
class="max-h-[70vh] w-full rounded-md object-contain transition-all duration-300">
|
|
</div>
|
|
|
|
<!-- Bottom Navigation Bar -->
|
|
<div v-if="hasMultiple" class="mt-4 flex items-center justify-between gap-4 border-t pt-3">
|
|
<Button type="button" variant="outline" size="sm" class="flex items-center gap-1 shrink-0"
|
|
@click.stop="prevImage">
|
|
<ChevronLeft class="size-4" />
|
|
Sebelumnya
|
|
</Button>
|
|
|
|
<div class="text-sm text-muted-foreground font-medium select-none">
|
|
{{ currentIndex + 1 }} / {{ allUrls.length }}
|
|
</div>
|
|
|
|
<Button type="button" variant="outline" size="sm" class="flex items-center gap-1 shrink-0"
|
|
@click.stop="nextImage">
|
|
Berikutnya
|
|
<ChevronRight class="size-4" />
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|