351 lines
10 KiB
Vue
351 lines
10 KiB
Vue
<!-- components/shared/Gallery.vue -->
|
|
<template>
|
|
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
|
|
<!-- Background Pattern -->
|
|
<div class="absolute inset-0 bg-pattern opacity-30"></div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="relative z-10 h-full flex flex-col items-center justify-center px-6 py-12">
|
|
|
|
<!-- Gallery Title -->
|
|
<div class="text-center mb-8 animate-fade-in-down">
|
|
<h1 class="text-yellow-400 text-4xl md:text-5xl font-bold mb-6 font-script">
|
|
Galeri
|
|
</h1>
|
|
<p class="text-white text-base md:text-lg max-w-2xl mx-auto leading-relaxed">
|
|
Aku hadir ke dunia ini atas izin Allah, dan kini tiba waktuku untuk
|
|
menjalani salah satu sunnah-Nya. Mohon doa dan restunya di momen
|
|
berharga dalam hidupku ini.
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Photo Gallery Grid -->
|
|
<div class="flex-1 max-w-4xl mx-auto w-full animate-fade-in-up animation-delay-300">
|
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 h-full max-h-96">
|
|
|
|
<!-- Large Photo (spans 2 rows on medium+ screens) -->
|
|
<div class="md:col-span-1 md:row-span-2 relative group cursor-pointer" @click="openModal(0)">
|
|
<div class="bg-white/10 backdrop-blur-sm rounded-2xl overflow-hidden h-full border border-yellow-400/30 hover:border-yellow-400/60 transition-all duration-300">
|
|
<img
|
|
:src="galleryImages[0]?.src || placeholderImage"
|
|
:alt="galleryImages[0]?.alt || 'Gallery Image 1'"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
|
@error="handleImageError"
|
|
/>
|
|
<!-- Overlay -->
|
|
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300 flex items-center justify-center">
|
|
<Icon name="lucide:zoom-in" class="w-8 h-8 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Medium Photos -->
|
|
<div
|
|
v-for="(image, index) in galleryImages.slice(1, 5)"
|
|
:key="index + 1"
|
|
class="relative group cursor-pointer"
|
|
@click="openModal(index + 1)"
|
|
>
|
|
<div class="bg-white/10 backdrop-blur-sm rounded-xl overflow-hidden h-full border border-yellow-400/30 hover:border-yellow-400/60 transition-all duration-300">
|
|
<img
|
|
:src="image.src || placeholderImage"
|
|
:alt="image.alt || `Gallery Image ${index + 2}`"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
|
@error="handleImageError"
|
|
/>
|
|
<!-- Overlay -->
|
|
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300 flex items-center justify-center">
|
|
<Icon name="lucide:zoom-in" class="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- Modal for Image Preview -->
|
|
<Teleport to="body">
|
|
<div
|
|
v-if="selectedImage !== null"
|
|
class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"
|
|
@click="closeModal"
|
|
>
|
|
<div class="relative max-w-4xl max-h-full" @click.stop>
|
|
<!-- Close Button -->
|
|
<button
|
|
@click="closeModal"
|
|
class="absolute -top-12 right-0 text-white hover:text-yellow-400 transition-colors duration-300"
|
|
>
|
|
<Icon name="lucide:x" class="w-8 h-8" />
|
|
</button>
|
|
|
|
<!-- Image -->
|
|
<img
|
|
:src="galleryImages[selectedImage]?.src || placeholderImage"
|
|
:alt="galleryImages[selectedImage]?.alt || 'Gallery Image'"
|
|
class="max-w-full max-h-full object-contain rounded-lg shadow-2xl"
|
|
/>
|
|
|
|
<!-- Navigation Arrows -->
|
|
<button
|
|
v-if="selectedImage > 0"
|
|
@click.stop="navigateImage(-1)"
|
|
class="absolute left-4 top-1/2 transform -translate-y-1/2 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-300"
|
|
>
|
|
<Icon name="lucide:chevron-left" class="w-6 h-6" />
|
|
</button>
|
|
|
|
<button
|
|
v-if="selectedImage < galleryImages.length - 1"
|
|
@click.stop="navigateImage(1)"
|
|
class="absolute right-4 top-1/2 transform -translate-y-1/2 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-300"
|
|
>
|
|
<Icon name="lucide:chevron-right" class="w-6 h-6" />
|
|
</button>
|
|
|
|
<!-- Image Counter -->
|
|
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black/50 backdrop-blur-sm text-white px-3 py-1 rounded-full text-sm">
|
|
{{ selectedImage + 1 }} / {{ galleryImages.length }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<!-- Floating Decorative Elements -->
|
|
<div class="absolute inset-0 pointer-events-none overflow-hidden">
|
|
<!-- Top ornament -->
|
|
<div class="absolute top-8 left-1/2 transform -translate-x-1/2 opacity-20">
|
|
<svg viewBox="0 0 100 50" class="w-24 h-12 text-yellow-400">
|
|
<path d="M50 10 Q60 0 70 10 Q80 20 70 30 Q60 20 50 30 Q40 20 30 30 Q20 20 30 10 Q40 0 50 10" fill="currentColor"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Bottom ornament -->
|
|
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 opacity-20">
|
|
<svg viewBox="0 0 100 50" class="w-24 h-12 text-yellow-400">
|
|
<path d="M50 40 Q40 50 30 40 Q20 30 30 20 Q40 30 50 20 Q60 30 70 20 Q80 30 70 40 Q60 50 50 40" fill="currentColor"/>
|
|
</svg>
|
|
</div>
|
|
|
|
<!-- Side decorations -->
|
|
<div class="absolute top-1/2 left-4 transform -translate-y-1/2 opacity-10">
|
|
<div class="w-1 h-20 bg-yellow-400 rounded-full"></div>
|
|
</div>
|
|
<div class="absolute top-1/2 right-4 transform -translate-y-1/2 opacity-10">
|
|
<div class="w-1 h-20 bg-yellow-400 rounded-full"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
|
|
// Props
|
|
const props = defineProps({
|
|
data: {
|
|
type: Object,
|
|
default: () => ({})
|
|
}
|
|
})
|
|
|
|
// Reactive data
|
|
const selectedImage = ref(null)
|
|
const placeholderImage = 'https://via.placeholder.com/400x400/4299e1/ffffff?text=Photo'
|
|
|
|
// Gallery images - replace with actual images
|
|
const galleryImages = ref([
|
|
{
|
|
src: '/images/khitan/child-portrait-1.jpg',
|
|
alt: 'Satria Huda Dinata Portrait 1'
|
|
},
|
|
{
|
|
src: '/images/khitan/child-portrait-2.jpg',
|
|
alt: 'Satria Huda Dinata Portrait 2'
|
|
},
|
|
{
|
|
src: '/images/khitan/child-portrait-3.jpg',
|
|
alt: 'Satria Huda Dinata Portrait 3'
|
|
},
|
|
{
|
|
src: '/images/khitan/child-portrait-4.jpg',
|
|
alt: 'Satria Huda Dinata Portrait 4'
|
|
},
|
|
{
|
|
src: '/images/khitan/child-portrait-5.jpg',
|
|
alt: 'Satria Huda Dinata Portrait 5'
|
|
}
|
|
])
|
|
|
|
// Methods
|
|
const openModal = (index) => {
|
|
selectedImage.value = index
|
|
document.body.style.overflow = 'hidden'
|
|
}
|
|
|
|
const closeModal = () => {
|
|
selectedImage.value = null
|
|
document.body.style.overflow = ''
|
|
}
|
|
|
|
const navigateImage = (direction) => {
|
|
const newIndex = selectedImage.value + direction
|
|
if (newIndex >= 0 && newIndex < galleryImages.value.length) {
|
|
selectedImage.value = newIndex
|
|
}
|
|
}
|
|
|
|
const handleImageError = (event) => {
|
|
event.target.src = placeholderImage
|
|
}
|
|
|
|
const handleKeyPress = (event) => {
|
|
if (selectedImage.value !== null) {
|
|
switch (event.key) {
|
|
case 'Escape':
|
|
closeModal()
|
|
break
|
|
case 'ArrowLeft':
|
|
navigateImage(-1)
|
|
break
|
|
case 'ArrowRight':
|
|
navigateImage(1)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(() => {
|
|
// Initialize gallery images from props if available
|
|
if (props.data?.gallery && props.data.gallery.length > 0) {
|
|
galleryImages.value = props.data.gallery.map((src, index) => ({
|
|
src,
|
|
alt: `Gallery Image ${index + 1}`
|
|
}))
|
|
}
|
|
|
|
// Add keyboard listener
|
|
window.addEventListener('keydown', handleKeyPress)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
window.removeEventListener('keydown', handleKeyPress)
|
|
document.body.style.overflow = ''
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Background Pattern */
|
|
.bg-pattern {
|
|
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="islamic" x="0" y="0" width="25" height="25" patternUnits="userSpaceOnUse"><g fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"><path d="M12.5 0 L25 12.5 L12.5 25 L0 12.5 Z M6.25 6.25 L18.75 6.25 L18.75 18.75 L6.25 18.75 Z"/><circle cx="12.5" cy="12.5" r="3"/></g></pattern></defs><rect width="100%" height="100%" fill="url(%23islamic)"/></svg>');
|
|
}
|
|
|
|
/* Animations */
|
|
@keyframes fadeInDown {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes fadeInUp {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
.animate-fade-in-down {
|
|
animation: fadeInDown 0.8s ease-out forwards;
|
|
}
|
|
|
|
.animate-fade-in-up {
|
|
animation: fadeInUp 0.8s ease-out forwards;
|
|
}
|
|
|
|
/* Animation Delays */
|
|
.animation-delay-300 {
|
|
animation-delay: 0.3s;
|
|
opacity: 0;
|
|
animation-fill-mode: forwards;
|
|
}
|
|
|
|
/* Custom fonts */
|
|
.font-script {
|
|
font-family: 'Times New Roman', serif;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Gallery Grid Adjustments */
|
|
.grid {
|
|
height: 100%;
|
|
}
|
|
|
|
.grid > div {
|
|
min-height: 120px;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.grid > div:first-child {
|
|
min-height: 250px;
|
|
}
|
|
|
|
.grid > div:not(:first-child) {
|
|
min-height: 120px;
|
|
}
|
|
}
|
|
|
|
/* Modal Styles */
|
|
.fixed.inset-0 {
|
|
backdrop-filter: blur(8px);
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 640px) {
|
|
.text-4xl.md\:text-5xl {
|
|
font-size: 2rem;
|
|
}
|
|
|
|
.px-6 {
|
|
padding-left: 1rem;
|
|
padding-right: 1rem;
|
|
}
|
|
|
|
.gap-4 {
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.max-h-96 {
|
|
max-height: 20rem;
|
|
}
|
|
|
|
.grid > div {
|
|
min-height: 100px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.md\:col-span-1 {
|
|
grid-column: span 1;
|
|
}
|
|
|
|
.md\:row-span-2 {
|
|
grid-row: span 1;
|
|
}
|
|
}
|
|
</style> |