This commit is contained in:
Farhaan4 2025-10-20 15:42:05 +07:00
parent 83f60371cd
commit b3c659bd58
7 changed files with 549 additions and 535 deletions

View File

@ -1,158 +1,171 @@
<!-- components/forms/KhitanForm.vue -->
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<div class="min-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>
<div class="absolute inset-0 bg-pattern opacity-20"></div>
<!-- Decorative Lanterns -->
<div class="absolute top-8 left-8 animate-sway">
<div class="w-16 h-20 relative">
<!-- Lantern Chain -->
<div class="absolute top-0 left-1/2 w-0.5 h-8 bg-yellow-400 transform -translate-x-1/2"></div>
<!-- Lantern Body -->
<div class="absolute bottom-0 w-full">
<svg viewBox="0 0 64 80" class="w-full h-full">
<defs>
<linearGradient id="lanternGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#FFD700"/>
<stop offset="50%" style="stop-color:#FFA500"/>
<stop offset="100%" style="stop-color:#FFD700"/>
</linearGradient>
</defs>
<ellipse cx="32" cy="15" rx="28" ry="12" fill="url(#lanternGradient)"/>
<rect x="8" y="12" width="48" height="40" rx="24" fill="url(#lanternGradient)" opacity="0.9"/>
<ellipse cx="32" cy="55" rx="28" ry="12" fill="url(#lanternGradient)"/>
<rect x="20" y="20" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
<rect x="20" y="30" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
<rect x="20" y="40" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
</svg>
</div>
</div>
</div>
<div class="absolute top-8 right-8 animate-sway animation-delay-1000">
<div class="w-16 h-20 relative">
<!-- Lantern Chain -->
<div class="absolute top-0 left-1/2 w-0.5 h-8 bg-yellow-400 transform -translate-x-1/2"></div>
<!-- Lantern Body -->
<div class="absolute bottom-0 w-full">
<svg viewBox="0 0 64 80" class="w-full h-full">
<!-- pakai gradient yang sama -->
<use href="#lanternGradient"></use>
<ellipse cx="32" cy="15" rx="28" ry="12" fill="url(#lanternGradient)"/>
<rect x="8" y="12" width="48" height="40" rx="24" fill="url(#lanternGradient)" opacity="0.9"/>
<ellipse cx="32" cy="55" rx="28" ry="12" fill="url(#lanternGradient)"/>
<rect x="20" y="20" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
<rect x="20" y="30" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
<rect x="20" y="40" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
</svg>
</div>
<div class="absolute top-4 left-4 md:top-8 md:left-8 animate-sway">
<Lantern />
</div>
<div class="absolute top-4 right-4 md:top-8 md:right-8 animate-sway animation-delay-1000">
<Lantern />
</div>
<!-- Main Content -->
<div class="relative z-10 h-full flex items-center justify-center px-6">
<div class="text-center max-w-6xl mx-auto">
<div class="relative z-10 min-h-screen flex items-center justify-center px-4 md:px-8 py-12">
<div class="text-center max-w-5xl mx-auto">
<!-- Bismillah -->
<div class="mb-8 animate-fade-in-down">
<h1 class="text-yellow-400 text-2xl md:text-3xl font-bold mb-6 arabic-text">
<div class="mb-10 animate-fade-in-down">
<h1 class="text-yellow-400 text-xl md:text-3xl lg:text-4xl font-bold mb-6 arabic-text">
بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ
</h1>
</div>
<!-- Event Description -->
<div class="mb-8 animate-fade-in-up animation-delay-300">
<p class="text-white text-base md:text-lg leading-relaxed max-w-4xl mx-auto mb-6">
<div class="mb-12 animate-fade-in-up animation-delay-300">
<p class="text-white text-sm md:text-base lg:text-lg leading-relaxed max-w-3xl mx-auto">
Dengan memohon rahmat dan ridho Allah Subhanahu wa Ta'ala, kami<br>
mengundang Bapak/Ibu/Saudara/i untuk hadir dan berbagi kebahagiaan<br>
dalam acara Khitanan putra kami tercinta, yang insya Allah akan<br>
diselenggarakan pada :
diselenggarakan pada:
</p>
</div>
<!-- Event Details Container -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Date & Countdown -->
<!-- Event Details -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 md:gap-8 mb-12">
<!-- Acara Khitanan -->
<div class="animate-fade-in-left animation-delay-600">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-yellow-400/30">
<h3 class="text-yellow-400 text-2xl font-bold mb-6 font-script">
Jum'at/Sabtu
<div class="bg-white/10 backdrop-blur-md rounded-2xl p-6 md:p-8 border border-yellow-400/30 shadow-lg">
<h3 class="text-yellow-400 text-xl md:text-2xl font-bold mb-4 font-script">
Khitanan
</h3>
<div class="text-white text-3xl md:text-4xl font-bold mb-8">
{{ eventDate }}
<div class="text-white text-2xl md:text-3xl lg:text-4xl font-bold mb-6">
{{ eventDate }} ({{ eventDay }})
</div>
<!-- Countdown Timer -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div
v-for="(item, index) in countdownItems"
:key="index"
class="bg-blue-800/50 rounded-lg p-4 text-center border border-yellow-400/20"
>
<div class="text-yellow-400 text-2xl md:text-3xl font-bold">
{{ item.value }}
</div>
<div class="text-white text-sm uppercase tracking-wider">
{{ item.label }}
</div>
</div>
</div>
<!-- Add to Calendar Button -->
<button
@click="addToCalendar"
class="bg-yellow-400 hover:bg-yellow-500 text-blue-900 px-6 py-3 rounded-full font-semibold transition-all duration-300 transform hover:scale-105"
>
Add to Calendar
</button>
</div>
</div>
<!-- Location & Map -->
<div class="animate-fade-in-right animation-delay-600">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-yellow-400/30">
<div class="text-white mb-6">
<p class="text-lg mb-2">Pukul {{ eventTime }}</p>
<p class="text-lg mb-2">{{ eventLocation }}</p>
<p class="text-base opacity-80">{{ eventAddress }}</p>
<p class="text-sm md:text-base mb-2">🕓 {{ eventTime }}</p>
<p class="text-sm md:text-base mb-2">📍 {{ eventLocation }}</p>
</div>
<!-- Map Placeholder -->
<div class="bg-gray-300 rounded-lg h-32 mb-6 relative overflow-hidden">
<!-- Map -->
<div class="bg-gray-300 rounded-lg h-40 md:h-48 relative overflow-hidden mb-6">
<div class="absolute inset-0 bg-gradient-to-r from-blue-400 to-blue-600 opacity-50"></div>
<div class="absolute inset-0 flex items-center justify-center">
<Icon name="lucide:map-pin" class="w-8 h-8 text-white" />
</div>
<img
src="https://via.placeholder.com/400x200/4299e1/ffffff?text=Map+Preview"
:src="eventMap"
alt="Location Map"
class="w-full h-full object-cover opacity-80"
/>
</div>
<!-- Direction Button -->
<button
@click="openMap"
class="bg-yellow-400 hover:bg-yellow-500 text-blue-900 px-6 py-3 rounded-full font-semibold transition-all duration-300 transform hover:scale-105"
<!-- Countdown -->
<div class="grid grid-cols-4 gap-3 md:gap-4 mb-6">
<div
v-for="(item, index) in countdownItemsAcara"
:key="index"
class="bg-blue-800/50 rounded-lg p-3 md:p-4 text-center border border-yellow-400/20"
>
Direction
<div class="text-yellow-400 text-xl md:text-2xl font-bold">
{{ item.value }}
</div>
<div class="text-white text-xs md:text-sm uppercase tracking-wider">
{{ item.label }}
</div>
</div>
</div>
<!-- Add to Calendar -->
<button
@click="addToCalendar"
class="bg-yellow-400 hover:bg-yellow-500 text-blue-900 px-6 py-2 md:py-3 rounded-full font-semibold transition-all duration-300 transform hover:scale-105"
>
Add to Calendar
</button>
<!-- Map Link -->
<div class="mt-4">
<a
v-if="eventAddress"
:href="eventAddress"
target="_blank"
class="text-yellow-400 text-sm md:text-base underline hover:text-yellow-300"
>
Lihat di Google Maps
</a>
</div>
</div>
</div>
<!-- Acara Syukuran -->
<div class="animate-fade-in-right animation-delay-600">
<div class="bg-white/10 backdrop-blur-md rounded-2xl p-6 md:p-8 border border-yellow-400/30 shadow-lg">
<h3 class="text-yellow-400 text-xl md:text-2xl font-bold mb-4 font-script">
Syukuran
</h3>
<div class="text-white text-2xl md:text-3xl lg:text-4xl font-bold mb-6">
{{ syukuranDate }} ({{ syukuranDay }})
</div>
<div class="text-white mb-6">
<p class="text-sm md:text-base mb-2">🕓 {{ syukuranTime }}</p>
<p class="text-sm md:text-base mb-2">📍 {{ syukuranAddress }}</p>
</div>
<!-- Map -->
<div class="bg-gray-300 rounded-lg h-40 md:h-48 relative overflow-hidden mb-6">
<div class="absolute inset-0 bg-gradient-to-r from-blue-400 to-blue-600 opacity-50"></div>
<div class="absolute inset-0 flex items-center justify-center">
<Icon name="lucide:map-pin" class="w-8 h-8 text-white" />
</div>
<img
:src="syukuranMapImage"
alt="Syukuran Location Map"
class="w-full h-full object-cover opacity-80"
/>
</div>
<!-- Countdown -->
<div class="grid grid-cols-4 gap-3 md:gap-4 mb-6">
<div
v-for="(item, index) in countdownItemsSyukuran"
:key="index"
class="bg-blue-800/50 rounded-lg p-3 md:p-4 text-center border border-yellow-400/20"
>
<div class="text-yellow-400 text-xl md:text-2xl font-bold">
{{ item.value }}
</div>
<div class="text-white text-xs md:text-sm uppercase tracking-wider">
{{ item.label }}
</div>
</div>
</div>
<!-- Add to Calendar -->
<button
@click="addToCalendar"
class="bg-yellow-400 hover:bg-yellow-500 text-blue-900 px-6 py-2 md:py-3 rounded-full font-semibold transition-all duration-300 transform hover:scale-105"
>
Add to Calendar
</button>
<!-- Map Link -->
<div class="mt-4">
<a
v-if="syukuranMap"
:href="syukuranMap"
target="_blank"
class="text-yellow-400 text-sm md:text-base underline hover:text-yellow-300"
>
Lihat di Google Maps
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Decorative Elements -->
<!-- Decorative Element -->
<div class="absolute inset-0 pointer-events-none overflow-hidden">
<div class="absolute top-0 left-1/2 transform -translate-x-1/2">
<svg viewBox="0 0 200 100" class="w-48 h-24 text-yellow-400 opacity-20">
<path d="M100 20 Q120 0 140 20 Q160 40 140 60 Q120 40 100 60 Q80 40 60 60 Q40 40 60 20 Q80 0 100 20" fill="currentColor"/>
<svg viewBox="0 0 200 100" class="w-40 md:w-48 h-20 md:h-24 text-yellow-400 opacity-20">
<path
d="M100 20 Q120 0 140 20 Q160 40 140 60 Q120 40 100 60 Q80 40 60 60 Q40 40 60 20 Q80 0 100 20"
fill="currentColor"
/>
</svg>
</div>
</div>
@ -162,86 +175,188 @@
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
// Props
// Props sesuai dengan parent dan JSON
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
hari_tanggal_acara: { type: String, default: '' },
waktu: { type: String, default: '' },
alamat: { type: String, default: '' },
link_gmaps: { type: String, default: '' },
hitung_mundur_mulai: { type: String, default: '' },
hari_tanggal_syukuran: { type: String, default: '' },
waktu_syukuran: { type: String, default: '' },
alamat_syukuran: { type: String, default: '' },
link_gmaps_syukuran: { type: String, default: '' },
hitung_mundur_selesai: { type: String, default: '' },
nama_panggilan: { type: String, default: '' }
})
// computed agar template tidak merah
const eventDate = computed(() => props.data.eventDate || '20-21 Juni 2025')
const eventTime = computed(() => props.data.eventTime || '09.00 WIB s.d Selesai')
const eventLocation = computed(() => props.data.eventLocation || 'TMC Mangrove, Tanjung Pasir')
const eventAddress = computed(() => props.data.eventAddress || 'Desa Tanjung Pasir')
/* ==========================
🎯 EVENT KHITAN
========================== */
const eventDate = computed(() => props.hari_tanggal_acara || '2025-10-31')
const eventDay = computed(() => new Date(eventDate.value).toLocaleDateString('id-ID', { weekday: 'long' }))
const eventTime = computed(() => props.waktu || '11:11')
const eventLocation = computed(() => props.alamat || 'Tempat belum ditentukan')
const eventAddress = computed(() => props.link_gmaps || '')
const eventMap = computed(() => `https://maps.googleapis.com/maps/api/staticmap?center=${encodeURIComponent(eventLocation.value)}&zoom=15&size=400x200&markers=color:red|${encodeURIComponent(eventLocation.value)}`)
const childPhoto = computed(() => props.data.childPhoto || '/images/khitan/child-photo.jpg')
const childName = computed(() => props.data.childName || 'Satria Huda Dinata')
const childTitle = computed(() => props.data.childTitle || 'SATRIA HUDA DINATA')
const childSubtitle = computed(() => props.data.childSubtitle || 'Putra Ke Dua Dari')
const fatherName = computed(() => props.data.fatherName || 'Bpk H. Munawar Huda, S.H.')
const motherName = computed(() => props.data.motherName || 'Ibu Hj. Dinah, A.M.Keb')
/* ==========================
🎉 SYUKURAN
========================== */
const syukuranDate = computed(() => props.hari_tanggal_syukuran || '')
const syukuranDay = computed(() => syukuranDate.value ? new Date(syukuranDate.value).toLocaleDateString('id-ID', { weekday: 'long' }) : '')
const syukuranTime = computed(() => props.waktu_syukuran || '')
const syukuranAddress = computed(() => props.alamat_syukuran || '')
const syukuranMap = computed(() => props.link_gmaps_syukuran || '')
const syukuranMapImage = computed(() => `https://maps.googleapis.com/maps/api/staticmap?center=${encodeURIComponent(syukuranAddress.value)}&zoom=15&size=400x200&markers=color:red|${encodeURIComponent(syukuranAddress.value)}`)
// countdown
const countdown = ref({ days: 7, hours: 11, minutes: 21, seconds: 45 })
let countdownInterval = null
/* ==========================
COUNTDOWN
========================== */
const countdownAcara = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 })
const countdownSyukuran = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 })
const countdownInterval = ref(null)
const countdownItems = computed(() => [
{ value: String(countdown.value.days).padStart(2, '0'), label: 'D' },
{ value: String(countdown.value.hours).padStart(2, '0'), label: 'H' },
{ value: String(countdown.value.minutes).padStart(2, '0'), label: 'M' },
{ value: String(countdown.value.seconds).padStart(2, '0'), label: 'S' }
const countdownItemsAcara = computed(() => [
{ value: String(countdownAcara.value.days).padStart(2, '0'), label: 'D' },
{ value: String(countdownAcara.value.hours).padStart(2, '0'), label: 'H' },
{ value: String(countdownAcara.value.minutes).padStart(2, '0'), label: 'M' },
{ value: String(countdownAcara.value.seconds).padStart(2, '0'), label: 'S' }
])
const countdownItemsSyukuran = computed(() => [
{ value: String(countdownSyukuran.value.days).padStart(2, '0'), label: 'D' },
{ value: String(countdownSyukuran.value.hours).padStart(2, '0'), label: 'H' },
{ value: String(countdownSyukuran.value.minutes).padStart(2, '0'), label: 'M' },
{ value: String(countdownSyukuran.value.seconds).padStart(2, '0'), label: 'S' }
])
const updateCountdown = () => {
const eventDateObj = new Date('2025-06-20T09:00:00')
const now = new Date()
const diff = eventDateObj.getTime() - now.getTime()
// Countdown untuk acara khitanan
if (props.hitung_mundur_mulai) {
const eventDateObj = new Date(props.hitung_mundur_mulai)
const diff = eventDateObj - now
if (diff > 0) {
countdown.value = {
countdownAcara.value = {
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((diff % (1000 * 60)) / 1000)
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
minutes: Math.floor((diff / (1000 * 60)) % 60),
seconds: Math.floor((diff / 1000) % 60)
}
} else {
countdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
if (countdownInterval) clearInterval(countdownInterval)
countdownAcara.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
}
}
// Countdown untuk acara syukuran
if (props.hitung_mundur_selesai) {
const syukuranDateObj = new Date(props.hitung_mundur_selesai)
const diff = syukuranDateObj - now
if (diff > 0) {
countdownSyukuran.value = {
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff / (1000 * 60 * 60)) % 24),
minutes: Math.floor((diff / (1000 * 60)) % 60),
seconds: Math.floor((diff / 1000) % 60)
}
} else {
countdownSyukuran.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
}
}
}
/* ==========================
📅 ACTIONS
========================== */
const addToCalendar = () => {
const eventDetails = {
title: `Tasyakuran Khitan ${childName.value}`,
start: '20250620T090000Z',
end: '20250621T170000Z',
description: 'Undangan Tasyakuran Khitan',
location: eventLocation.value
}
const url = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(eventDetails.title)}&dates=${eventDetails.start}/${eventDetails.end}&details=${encodeURIComponent(eventDetails.description)}&location=${encodeURIComponent(eventDetails.location)}`
const title = `Tasyakuran Khitan ${props.nama_panggilan || 'Putra Kami'}`
const start = new Date(props.hitung_mundur_mulai).toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
const end = new Date(new Date(props.hitung_mundur_mulai).getTime() + 3 * 60 * 60 * 1000).toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
const url = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(title)}&dates=${start}/${end}&location=${encodeURIComponent(eventLocation.value)}`
window.open(url, '_blank')
}
const openMap = () => {
const location = encodeURIComponent(eventLocation.value)
window.open(`https://maps.google.com/maps?q=${location}`, '_blank')
}
const handleImageError = (e) => {
e.target.src = 'https://via.placeholder.com/128x128/4299e1/ffffff?text=Photo'
}
/* ==========================
LIFECYCLE
========================== */
onMounted(() => {
updateCountdown()
countdownInterval = setInterval(updateCountdown, 1000)
countdownInterval.value = setInterval(updateCountdown, 1000)
})
onUnmounted(() => {
if (countdownInterval) clearInterval(countdownInterval)
if (countdownInterval.value) {
clearInterval(countdownInterval.value)
}
})
</script>
<style scoped>
/* ... tetap sama dengan punyamu, kecuali hapus selector .grid-cols-1.lg\:grid-cols-2 */
/* Animations & bg-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='pattern' x='0' y='0' width='20' height='20' patternUnits='userSpaceOnUse'><path d='M10 5 L15 10 L10 15 L5 10 Z' fill='none' stroke='rgba(255,255,255,0.1)' stroke-width='0.5'/></pattern></defs><rect width='100' height='100' fill='url(%23pattern)'/></svg>");
}
.animate-sway {
animation: sway 3s ease-in-out infinite;
}
@keyframes sway {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
.animate-fade-in-down {
animation: fadeInDown 1s ease-out forwards;
}
.animate-fade-in-up {
animation: fadeInUp 1s ease-out forwards;
}
.animate-fade-in-left {
animation: fadeInLeft 1s ease-out forwards;
}
.animate-fade-in-right {
animation: fadeInRight 1s ease-out forwards;
}
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInLeft {
from { opacity: 0; transform: translateX(-20px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes fadeInRight {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
.animation-delay-300 {
animation-delay: 0.3s;
}
.animation-delay-600 {
animation-delay: 0.6s;
}
.animation-delay-1000 {
animation-delay: 1s;
}
.arabic-text {
font-family: 'Noto Naskh Arabic', serif;
}
</style>

View File

@ -1,12 +1,10 @@
<!-- 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">
<div class="min-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">
<div class="relative z-10 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">
@ -21,51 +19,55 @@
<!-- 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">
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<!-- Foto pertama (besar) -->
<div
v-if="images.length > 0"
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 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'"
:src="images[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">
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 flex items-center justify-center transition-colors duration-300">
<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 -->
<!-- Foto lainnya -->
<div
v-for="(image, index) in galleryImages.slice(1, 5)"
v-for="(image, index) in images.slice(1)"
: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">
<div class="bg-white/10 backdrop-blur-sm rounded-xl overflow-hidden 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}`"
:src="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">
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 flex items-center justify-center transition-colors duration-300">
<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>
<!-- Jika tidak ada foto -->
<div v-if="images.length === 0" class="col-span-full text-center py-8 bg-white/10 rounded-xl text-white/70">
Belum ada foto untuk ditampilkan.
</div>
</div>
</div>
</div>
</div>
<!-- Modal for Image Preview -->
<!-- Modal Preview -->
<Teleport to="body">
<div
v-if="selectedImage !== null"
@ -73,7 +75,7 @@
@click="closeModal"
>
<div class="relative max-w-4xl max-h-full" @click.stop>
<!-- Close Button -->
<!-- Tombol close -->
<button
@click="closeModal"
class="absolute -top-12 right-0 text-white hover:text-yellow-400 transition-colors duration-300"
@ -81,14 +83,15 @@
<Icon name="lucide:x" class="w-8 h-8" />
</button>
<!-- Image -->
<!-- Gambar besar -->
<img
:src="galleryImages[selectedImage]?.src || placeholderImage"
:alt="galleryImages[selectedImage]?.alt || 'Gallery Image'"
:src="images[selectedImage]"
:alt="`Gallery Image ${selectedImage + 1}`"
class="max-w-full max-h-full object-contain rounded-lg shadow-2xl"
@error="handleImageError"
/>
<!-- Navigation Arrows -->
<!-- Navigasi kiri-kanan -->
<button
v-if="selectedImage > 0"
@click.stop="navigateImage(-1)"
@ -98,88 +101,38 @@
</button>
<button
v-if="selectedImage < galleryImages.length - 1"
v-if="selectedImage < images.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 }}
<!-- Counter -->
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black/50 text-white px-3 py-1 rounded-full text-sm">
{{ selectedImage + 1 }} / {{ images.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: () => ({})
images: {
type: Array,
default: () => []
}
})
// Reactive data
// State
const selectedImage = ref(null)
const placeholderImage = 'https://via.placeholder.com/400x400/4299e1/ffffff?text=Photo'
const placeholderImage = '/pria.jpg'
// 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
// Modal Control
const openModal = (index) => {
selectedImage.value = index
document.body.style.overflow = 'hidden'
@ -192,7 +145,7 @@ const closeModal = () => {
const navigateImage = (direction) => {
const newIndex = selectedImage.value + direction
if (newIndex >= 0 && newIndex < galleryImages.value.length) {
if (newIndex >= 0 && newIndex < props.images.length) {
selectedImage.value = newIndex
}
}
@ -201,33 +154,19 @@ const handleImageError = (event) => {
event.target.src = placeholderImage
}
// Keyboard Control
const handleKeyPress = (event) => {
if (selectedImage.value !== null) {
switch (event.key) {
case 'Escape':
closeModal()
break
case 'ArrowLeft':
navigateImage(-1)
break
case 'ArrowRight':
navigateImage(1)
break
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)
})
@ -238,114 +177,7 @@ onUnmounted(() => {
</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>

View File

@ -1,63 +1,91 @@
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<div
class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden"
>
<!-- Main Content -->
<div class="relative z-10 min-h-screen flex items-center justify-center px-4 md:px-8 py-12">
<div class="text-center max-w-4xl mx-auto">
<!-- Title -->
<div class="mb-10 animate-fade-in-down">
<h1 class="text-yellow-400 text-3xl md:text-5xl lg:text-6xl font-bold mb-4 md:mb-6 font-script">
Introduction
</h1>
</div>
<!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Top Navigation Tabs -->
<div class="absolute top-6 left-1/2 transform -translate-x-1/2 bg-white/20 backdrop-blur-sm rounded-full px-6 py-2 flex space-x-8 z-10">
<span class="text-yellow-400 font-semibold cursor-pointer">Introduction</span>
<span class="text-blue-100 cursor-pointer">Event</span>
<span class="text-blue-100 cursor-pointer">Gallery</span>
<span class="text-blue-100 cursor-pointer">Say</span>
<span class="text-blue-100 cursor-pointer">Thank You</span>
</div>
<!-- Main Content -->
<div class="relative z-10 h-full flex flex-col items-center justify-center px-6 text-center">
<div
class="relative z-10 h-full flex flex-col items-center justify-center px-6 text-center"
>
<!-- Child Photo -->
<div class="w-48 h-60 overflow-hidden rounded-t-3xl mx-auto mb-6">
<img :src="formData.childPhoto || '/pria.jpg'" alt="Child" class="w-full h-full object-cover"/>
<div class="w-48 h-60 overflow-hidden rounded-t-3xl mx-auto mb-6 shadow-lg">
<img
:src="getFullPhotoUrl(childPhoto)"
alt="Child"
class="w-full h-full object-cover"
/>
</div>
<!-- Child Name -->
<div class="bg-blue-800/50 border border-yellow-400 rounded-md px-6 py-2 mb-4 inline-block">
<h1 class="text-yellow-400 font-bold text-xl">{{ formData.nama_panggilan || 'SATRIA HUDA DINATA' }}</h1>
<div
class="bg-blue-800/50 border border-yellow-400 rounded-md px-6 py-2 mb-4 inline-block"
>
<h1 class="text-yellow-400 font-bold text-xl">
{{ form.nama_lengkap || 'Nama Anak' }}
</h1>
</div>
<!-- Child Order & Parents -->
<div class="text-white/80 text-lg mb-2">
Putra Ke Dua Dari
<!-- Child Order -->
<div v-if="form.anak_ke" class="text-white/80 text-lg mb-2">
Putra ke {{ form.anak_ke }} dari
</div>
<div v-else class="text-white/80 text-lg mb-2">
Putra dari
</div>
<!-- Parents -->
<div class="text-yellow-400 font-semibold text-xl">
{{ formData.fatherName || 'Bpk H. Munawar Huda, S.H.' }} & {{ formData.motherName || 'Ibu Hj. Dinah, A.M.Keb' }}
{{ form.nama_bapak || 'Nama Ayah' }} & {{ form.nama_ibu || 'Nama Ibu' }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useRuntimeConfig } from '#app'
const props = defineProps({
data: {
form: {
type: Object,
default: () => ({})
}
})
const formData = computed(() => {
const result = {}
props.data?.fields?.forEach(f => {
result[f.name] = f.value
})
return result
})
onMounted(() => {
console.log("🧾 Introduction Data:", props.data)
const config = useRuntimeConfig()
const backendUrl = config.public.apiBaseUrl
// ============ FOTO ANAK ============
const childPhoto = computed(() => {
const fotoPertama = props.form?.foto?.[3]
return fotoPertama || ''
})
// ============ FORMAT URL FOTO ============
const getFullPhotoUrl = (photo) => {
if (!photo) return '/pria.jpg'
if (photo.startsWith('http')) return photo
if (photo.startsWith('/storage')) return `${backendUrl}${photo}`
return `${backendUrl}/storage/${photo}`
}
</script>
<style scoped>
/* Background Islamic 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>');
}

View File

@ -1,12 +1,8 @@
<!-- components/templates/khitan/KhitanA.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 Islamic Pattern -->
<div class="h-screen w-full relative bg-blue-900 overflow-hidden">
<div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Decorative Elements -->
<div class="absolute top-0 left-0 w-full h-full">
<!-- Top decorative curves -->
<div class="absolute top-0 left-0 w-full">
<svg viewBox="0 0 1200 300" class="w-full h-auto">
<defs>
@ -20,7 +16,6 @@
</svg>
</div>
<!-- Bottom decorative curves -->
<div class="absolute bottom-0 left-0 w-full">
<svg viewBox="0 0 1200 300" class="w-full h-auto">
<path d="M0,150 C300,250 600,50 1200,150 L1200,300 L0,300 Z" fill="url(#goldGradient)" opacity="0.3"/>
@ -28,13 +23,10 @@
</div>
</div>
<!-- Main Content -->
<div class="relative z-10 h-full flex items-center justify-center px-6">
<div class="text-center max-w-4xl mx-auto">
<!-- Islamic Symbol -->
<div class="mb-8 animate-fade-in-down">
<div class="mx-auto w-32 h-32 relative">
<!-- Crescent and Star -->
<div class="absolute inset-0 flex items-center justify-center">
<svg viewBox="0 0 100 100" class="w-full h-full text-yellow-400">
<defs>
@ -46,16 +38,13 @@
</feMerge>
</filter>
</defs>
<!-- Crescent -->
<path d="M35 20 A 25 25 0 1 1 35 80 A 15 15 0 1 0 35 20" fill="currentColor" filter="url(#glow)"/>
<!-- Star -->
<polygon points="65,25 67,35 77,35 69,42 72,52 65,45 58,52 61,42 53,35 63,35" fill="currentColor" filter="url(#glow)"/>
</svg>
</div>
</div>
</div>
<!-- Invitation Title -->
<div class="mb-12 animate-fade-in-up animation-delay-300">
<h1 class="text-yellow-400 text-3xl md:text-4xl font-bold mb-4 tracking-wide">
Undangan
@ -68,37 +57,30 @@
</h2>
</div>
<!-- Guest Information -->
<div class="mb-12 animate-fade-in-up animation-delay-600">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 mb-6 border border-yellow-400/30">
<p class="text-yellow-400 text-lg mb-2">{{ data?.guestTitle || 'Kepada Yth.' }}</p>
<p class="text-white text-xl font-semibold">{{ data?.guestName || 'Muzaki Parsaoran' }}</p>
<p class="text-white text-xl font-semibold">{{ data?.guestName || '' }}</p>
</div>
</div>
<!-- Open Invitation Button -->
<div class="animate-fade-in-up animation-delay-900">
<button
@click="$emit('next-page')"
class="group relative px-8 py-4 bg-gradient-to-r from-yellow-400 to-yellow-500 text-blue-900 font-bold text-lg rounded-full shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300 overflow-hidden"
>
<!-- Button background animation -->
<div class="absolute inset-0 bg-gradient-to-r from-yellow-300 to-yellow-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- Button content -->
<span class="relative flex items-center space-x-2">
<span>OPEN INVITATION</span>
<Icon name="lucide:chevron-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform duration-300" />
</span>
<!-- Shine effect -->
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent transform -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-700"></div>
</button>
</div>
<!-- Decorative Elements Around Button -->
<div class="absolute inset-0 pointer-events-none">
<!-- Floating particles -->
<div class="absolute top-1/4 left-1/4 animate-float">
<div class="w-2 h-2 bg-yellow-400 rounded-full opacity-60"></div>
</div>
@ -112,7 +94,6 @@
</div>
</div>
<!-- Corner Decorations -->
<div class="absolute top-4 left-4 w-16 h-16 border-l-2 border-t-2 border-yellow-400 opacity-50"></div>
<div class="absolute top-4 right-4 w-16 h-16 border-r-2 border-t-2 border-yellow-400 opacity-50"></div>
<div class="absolute bottom-4 left-4 w-16 h-16 border-l-2 border-b-2 border-yellow-400 opacity-50"></div>

View File

@ -1,31 +1,33 @@
```vue
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<div class="min-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>
<div class="absolute inset-0 bg-pattern opacity-20"></div>
<!-- Main Content -->
<div class="relative z-10 h-full flex items-center justify-center px-6">
<div class="relative z-10 min-h-screen flex items-center justify-center px-4 md:px-8 py-12">
<div class="text-center max-w-4xl mx-auto">
<!-- Title -->
<div class="mb-12 animate-fade-in-down">
<h1 class="text-yellow-400 text-4xl md:text-6xl font-bold mb-6 font-script">Terimakasih</h1>
<p class="text-white text-lg md:text-xl leading-relaxed max-w-3xl mx-auto">
Kami mengucapkan terima kasih atas kehadiran serta doa restu
yang diberikan. Semoga Allah SWT senantiasa melimpahkan
rahmat dan keberkahan kepada kita semua.
<div class="mb-10 animate-fade-in-down">
<h1 class="text-yellow-400 text-3xl md:text-5xl lg:text-6xl font-bold mb-4 md:mb-6 font-script">
Terima Kasih
</h1>
<p class="text-white text-sm md:text-base lg:text-lg leading-relaxed max-w-3xl mx-auto">
Kami mengucapkan terima kasih atas kehadiran serta doa restu yang diberikan untuk
<span class="text-yellow-400 font-semibold">{{ jsonData.nama_panggilan || 'putra kami' }}</span>.
Semoga Allah SWT senantiasa melimpahkan rahmat dan keberkahan kepada kita semua.
</p>
</div>
<!-- Family Info -->
<div class="mb-12 animate-fade-in-up animation-delay-300">
<h2 class="text-yellow-400 text-2xl md:text-3xl font-bold mb-8">Kami Keluarga Besar Dari</h2>
<div class="space-y-6">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-yellow-400/30 animate-fade-in-up animation-delay-500">
<div class="text-yellow-400 text-lg font-semibold mb-2">
{{ data.fatherName || 'Bpk H. Munawar Huda, S.H.' }} & {{ data.motherName || 'Ibu Hj. Dinah, A.M.Keb' }}
</div>
<div class="text-white/80 text-sm">
{{ data.fatherDescription || '(Anggota DPRD Provinsi Banten Fraksi Partai Demokrat)' }}
<h2 class="text-yellow-400 text-xl md:text-2xl lg:text-3xl font-bold mb-6 md:mb-8">
Kami Keluarga Besar Dari
</h2>
<div class="space-y-4 md:space-y-6">
<div class="bg-white/10 backdrop-blur-md rounded-2xl p-4 md:p-6 border border-yellow-400/30 shadow-lg animate-fade-in-up animation-delay-500">
<div class="text-yellow-400 text-base md:text-lg font-semibold mb-2">
{{ jsonData.nama_bapak || 'Bpk H. Munawar Huda, S.H.' }} & {{ jsonData.nama_ibu || 'Ibu Hj. Dinah, A.M.Keb' }}
</div>
</div>
</div>
@ -33,26 +35,45 @@
<!-- Final Message -->
<div class="animate-fade-in-up animation-delay-1200">
<div class="bg-gradient-to-r from-yellow-400/20 to-yellow-600/20 backdrop-blur-sm rounded-2xl p-8 border border-yellow-400/50">
<p class="text-white text-lg md:text-xl font-medium mb-4 italic">
<div class="bg-gradient-to-r from-yellow-400/20 to-yellow-600/20 backdrop-blur-md rounded-2xl p-6 md:p-8 border border-yellow-400/50 shadow-lg">
<p class="text-white text-base md:text-lg font-medium mb-4 italic">
"Dan Allah telah mengeluarkan kamu dari perut ibumu dalam keadaan tidak mengetahui sesuatupun..."
</p>
<p class="text-yellow-400 font-semibold">- QS. An-Nahl: 78</p>
<p class="text-yellow-400 text-sm md:text-base font-semibold">- QS. An-Nahl: 78</p>
</div>
</div>
<!-- Social -->
<div class="mt-12 animate-fade-in-up animation-delay-1500 flex justify-center space-x-6">
<a href="https://instagram.com/abbauftech" target="_blank" rel="noopener noreferrer"
class="rounded-full p-3 backdrop-blur-sm border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110"
aria-label="Instagram">
<Icon name="lucide:instagram" class="w-6 h-6 text-yellow-400" />
<!-- Social Media Links -->
<div class="mt-10 md:mt-12 animate-fade-in-up animation-delay-1500 flex justify-center space-x-4 md:space-x-6">
<a
v-if="jsonData.link_instagram"
:href="jsonData.link_instagram"
target="_blank"
rel="noopener noreferrer"
class="rounded-full p-2 md:p-3 backdrop-blur-md border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110"
aria-label="Instagram"
>
<Icon name="lucide:instagram" class="w-5 h-5 md:w-6 md:h-6 text-yellow-400" />
</a>
<a href="#" class="rounded-full p-3 backdrop-blur-sm border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110" aria-label="Facebook">
<Icon name="lucide:facebook" class="w-6 h-6 text-yellow-400" />
<a
v-if="jsonData.link_facebook"
:href="jsonData.link_facebook"
target="_blank"
rel="noopener noreferrer"
class="rounded-full p-2 md:p-3 backdrop-blur-md border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110"
aria-label="Facebook"
>
<Icon name="lucide:facebook" class="w-5 h-5 md:w-6 md:h-6 text-yellow-400" />
</a>
<a href="#" class="rounded-full p-3 backdrop-blur-sm border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110" aria-label="WhatsApp">
<Icon name="lucide:message-circle" class="w-6 h-6 text-yellow-400" />
<a
v-if="jsonData.link_twitter"
:href="jsonData.link_twitter"
target="_blank"
rel="noopener noreferrer"
class="rounded-full p-2 md:p-3 backdrop-blur-md border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110"
aria-label="Twitter"
>
<Icon name="lucide:twitter" class="w-5 h-5 md:w-6 md:h-6 text-yellow-400" />
</a>
</div>
</div>
@ -66,10 +87,14 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { computed } from 'vue'
const props = defineProps({
data: {
childName: {
type: String,
default: ''
},
jsonData: {
type: Object,
default: () => ({})
}
@ -84,13 +109,12 @@ const currentYear = computed(() => new Date().getFullYear())
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><defs><pattern id='pattern' x='0' y='0' width='20' height='20' patternUnits='userSpaceOnUse'><path d='M10 5 L15 10 L10 15 L5 10 Z' fill='none' stroke='rgba(255,255,255,0.1)' stroke-width='0.5'/></pattern></defs><rect width='100' height='100' fill='url(%23pattern)'/></svg>");
}
/* Tambahkan animasi sesuai kebutuhan */
.animate-fade-in-up {
animation: fadeInUp 1s forwards;
animation: fadeInUp 1s ease-out forwards;
}
.animate-fade-in-down {
animation: fadeInDown 1s forwards;
animation: fadeInDown 1s ease-out forwards;
}
@keyframes fadeInUp {
@ -102,4 +126,21 @@ const currentYear = computed(() => new Date().getFullYear())
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.animation-delay-300 {
animation-delay: 0.3s;
}
.animation-delay-500 {
animation-delay: 0.5s;
}
.animation-delay-1200 {
animation-delay: 1.2s;
}
.animation-delay-1500 {
animation-delay: 1.5s;
}
</style>
```

View File

@ -1,6 +1,8 @@
<!-- components/undangan/undangan-khitan-premium.vue -->
<template>
<div class="min-h-screen bg-gradient-to-b from-blue-100 via-blue-200 to-blue-300 relative overflow-hidden">
<div
class="min-h-screen bg-gradient-to-b from-blue-100 via-blue-200 to-blue-300 relative overflow-hidden"
>
<!-- ================= NAVIGATION ================= -->
<nav
v-if="currentSection !== 'landing'"
@ -9,21 +11,11 @@
<ul
class="flex space-x-6 bg-white/40 backdrop-blur-md px-6 py-3 rounded-full shadow-md text-sm font-semibold text-gray-800"
>
<li>
<button @click="switchSection('introduction')" :class="navClass('introduction')">Intro</button>
</li>
<li>
<button @click="switchSection('event')" :class="navClass('event')">Event</button>
</li>
<li>
<button @click="switchSection('gallery')" :class="navClass('gallery')">Gallery</button>
</li>
<li>
<button @click="switchSection('say')" :class="navClass('say')">Guest Book</button>
</li>
<li>
<button @click="switchSection('thanks')" :class="navClass('thanks')">Thanks</button>
</li>
<li><button @click="switchSection('introduction')" :class="navClass('introduction')">Intro</button></li>
<li><button @click="switchSection('event')" :class="navClass('event')">Event</button></li>
<li><button @click="switchSection('gallery')" :class="navClass('gallery')">Gallery</button></li>
<li><button @click="switchSection('say')" :class="navClass('say')">Guest Book</button></li>
<li><button @click="switchSection('thanks')" :class="navClass('thanks')">Thanks</button></li>
</ul>
</nav>
@ -32,6 +24,7 @@
<button @click="toggleMusic" class="bg-blue-600 p-3 rounded-full text-white shadow-lg">
{{ isPlaying ? '⏸️' : '▶️' }}
</button>
<audio ref="audioPlayer" :src="musicUrl" loop></audio>
</div>
<!-- ================= MAIN CONTENT ================= -->
@ -49,18 +42,26 @@
<!-- Introduction -->
<KhitanIntroduction
v-if="currentSection === 'introduction'"
:data="formData"
:form="formData"
/>
<!-- Event -->
<KhitanEvent
<KhitanEvent
v-if="currentSection === 'event'"
:hari_tanggal_acara="formData.hari_tanggal_acara"
:waktu="formData.waktu"
:alamat="formData.alamat"
:link_gmaps="formData.link_gmaps"
:hitung_mundur="formData.hitung_mundur"
/>
:hitung_mundur_mulai="formData.hitung_mundur_mulai"
:hari_tanggal_syukuran="formData.hari_tanggal_syukuran"
:waktu_syukuran="formData.waktu_syukuran"
:alamat_syukuran="formData.alamat_syukuran"
:link_gmaps_syukuran="formData.link_gmaps_syukuran"
:hitung_mundur_selesai="formData.hitung_mundur_selesai"
/>
<!-- Gallery -->
<KhitanGallery
@ -76,18 +77,15 @@
@addMessage="addMessage"
/>
<!-- Thank You -->
<KhitanThankYou
v-if="currentSection === 'thanks'"
:childName="formData.nama_panggilan"
:jsonData="formData"
/>
<KhitanThankYou v-if="currentSection === 'thanks'" :childName="formData.nama_panggilan" :jsonData="formData" />
</main>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, watch } from 'vue'
import { useRuntimeConfig } from '#app'
// ================== IMPORT KOMPONEN ==================
@ -112,15 +110,27 @@ const formData = computed(() => props.data.form || {})
// ================== GALERI ==================
const galleryImages = computed(() => {
const f = formData.value.foto
if (Array.isArray(f)) {
return f.map(img => `${backendUrl}/storage/${img}`)
}
// Jika masih bentuk lama (foto_1, foto_2, dst.)
return [
formData.value.foto_1,
formData.value.foto_2,
formData.value.foto_3,
formData.value.foto_4,
formData.value.foto_5
]
.filter(Boolean)
.map(f => `${backendUrl}/${f}`)
].filter(Boolean).map(img => `${backendUrl}/${img}`)
})
// Foto utama diambil dari foto pertama
const mainPhoto = computed(() => {
const firstPhoto = formData.value.foto?.[0]
return firstPhoto ? `${backendUrl}/${firstPhoto}` : ''
})
// ================== NAVIGASI SECTION ==================
@ -128,8 +138,21 @@ const currentSection = ref('landing')
const switchSection = (s) => (currentSection.value = s)
// ================== MUSIK ==================
const audioPlayer = ref(null)
const isPlaying = ref(false)
const toggleMusic = () => (isPlaying.value = !isPlaying.value)
const musicUrl = computed(() =>
formData.value.link_music ? `${backendUrl}/${formData.value.link_music}` : ''
)
const toggleMusic = () => {
if (!audioPlayer.value) return
if (isPlaying.value) {
audioPlayer.value.pause()
} else {
audioPlayer.value.play()
}
isPlaying.value = !isPlaying.value
}
// ================== GUEST BOOK ==================
const messages = ref([])
@ -140,15 +163,9 @@ const navClass = (s) =>
currentSection.value === s
? 'text-blue-800 underline'
: 'hover:text-blue-700'
onMounted(() => {
console.log("🧾 formData:", formData.value)
console.log("🖼️ galleryImages:", galleryImages.value)
})
</script>
<style scoped>
/* Animasi transisi antar section */
main {
transition: all 0.7s ease-in-out;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB