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> <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 --> <!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div> <div class="absolute inset-0 bg-pattern opacity-20"></div>
<!-- Decorative Lanterns --> <!-- Decorative Lanterns -->
<div class="absolute top-8 left-8 animate-sway"> <div class="absolute top-4 left-4 md:top-8 md:left-8 animate-sway">
<div class="w-16 h-20 relative"> <Lantern />
<!-- 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>
<div class="absolute top-4 right-4 md:top-8 md:right-8 animate-sway animation-delay-1000">
<div class="absolute top-8 right-8 animate-sway animation-delay-1000"> <Lantern />
<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>
</div> </div>
<!-- Main Content --> <!-- 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-6xl mx-auto"> <div class="text-center max-w-5xl mx-auto">
<!-- Bismillah --> <!-- Bismillah -->
<div class="mb-8 animate-fade-in-down"> <div class="mb-10 animate-fade-in-down">
<h1 class="text-yellow-400 text-2xl md:text-3xl font-bold mb-6 arabic-text"> <h1 class="text-yellow-400 text-xl md:text-3xl lg:text-4xl font-bold mb-6 arabic-text">
بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ
</h1> </h1>
</div> </div>
<!-- Event Description --> <!-- Event Description -->
<div class="mb-8 animate-fade-in-up animation-delay-300"> <div class="mb-12 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"> <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> Dengan memohon rahmat dan ridho Allah Subhanahu wa Ta'ala, kami<br>
mengundang Bapak/Ibu/Saudara/i untuk hadir dan berbagi kebahagiaan<br> mengundang Bapak/Ibu/Saudara/i untuk hadir dan berbagi kebahagiaan<br>
dalam acara Khitanan putra kami tercinta, yang insya Allah akan<br> dalam acara Khitanan putra kami tercinta, yang insya Allah akan<br>
diselenggarakan pada : diselenggarakan pada:
</p> </p>
</div> </div>
<!-- Event Details Container --> <!-- Event Details -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 md:gap-8 mb-12">
<!-- Acara Khitanan -->
<!-- Date & Countdown -->
<div class="animate-fade-in-left animation-delay-600"> <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"> <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-2xl font-bold mb-6 font-script"> <h3 class="text-yellow-400 text-xl md:text-2xl font-bold mb-4 font-script">
Jum'at/Sabtu Khitanan
</h3> </h3>
<div class="text-white text-2xl md:text-3xl lg:text-4xl font-bold mb-6">
<div class="text-white text-3xl md:text-4xl font-bold mb-8"> {{ eventDate }} ({{ eventDay }})
{{ eventDate }}
</div> </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"> <div class="text-white mb-6">
<p class="text-lg mb-2">Pukul {{ eventTime }}</p> <p class="text-sm md:text-base mb-2">🕓 {{ eventTime }}</p>
<p class="text-lg mb-2">{{ eventLocation }}</p> <p class="text-sm md:text-base mb-2">📍 {{ eventLocation }}</p>
<p class="text-base opacity-80">{{ eventAddress }}</p>
</div> </div>
<!-- Map -->
<!-- Map Placeholder --> <div class="bg-gray-300 rounded-lg h-40 md:h-48 relative overflow-hidden mb-6">
<div class="bg-gray-300 rounded-lg h-32 mb-6 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-blue-400 to-blue-600 opacity-50"></div> <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"> <div class="absolute inset-0 flex items-center justify-center">
<Icon name="lucide:map-pin" class="w-8 h-8 text-white" /> <Icon name="lucide:map-pin" class="w-8 h-8 text-white" />
</div> </div>
<img <img
src="https://via.placeholder.com/400x200/4299e1/ffffff?text=Map+Preview" :src="eventMap"
alt="Location Map" alt="Location Map"
class="w-full h-full object-cover opacity-80" class="w-full h-full object-cover opacity-80"
/> />
</div> </div>
<!-- Countdown -->
<!-- Direction Button --> <div class="grid grid-cols-4 gap-3 md:gap-4 mb-6">
<button <div
@click="openMap" v-for="(item, index) in countdownItemsAcara"
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" :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"
> >
Direction Add to Calendar
</button> </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> </div>
</div> </div>
</div> </div>
<!-- Decorative Elements --> <!-- Decorative Element -->
<div class="absolute inset-0 pointer-events-none overflow-hidden"> <div class="absolute inset-0 pointer-events-none overflow-hidden">
<div class="absolute top-0 left-1/2 transform -translate-x-1/2"> <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"> <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"/> <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> </svg>
</div> </div>
</div> </div>
@ -162,86 +175,188 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue' import { ref, onMounted, onUnmounted, computed } from 'vue'
// Props // Props sesuai dengan parent dan JSON
const props = defineProps({ const props = defineProps({
data: { hari_tanggal_acara: { type: String, default: '' },
type: Object, waktu: { type: String, default: '' },
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') 🎯 EVENT KHITAN
const eventTime = computed(() => props.data.eventTime || '09.00 WIB s.d Selesai') ========================== */
const eventLocation = computed(() => props.data.eventLocation || 'TMC Mangrove, Tanjung Pasir') const eventDate = computed(() => props.hari_tanggal_acara || '2025-10-31')
const eventAddress = computed(() => props.data.eventAddress || 'Desa Tanjung Pasir') 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') 🎉 SYUKURAN
const childTitle = computed(() => props.data.childTitle || 'SATRIA HUDA DINATA') ========================== */
const childSubtitle = computed(() => props.data.childSubtitle || 'Putra Ke Dua Dari') const syukuranDate = computed(() => props.hari_tanggal_syukuran || '')
const fatherName = computed(() => props.data.fatherName || 'Bpk H. Munawar Huda, S.H.') const syukuranDay = computed(() => syukuranDate.value ? new Date(syukuranDate.value).toLocaleDateString('id-ID', { weekday: 'long' }) : '')
const motherName = computed(() => props.data.motherName || 'Ibu Hj. Dinah, A.M.Keb') 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 }) COUNTDOWN
let countdownInterval = null ========================== */
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(() => [ const countdownItemsAcara = computed(() => [
{ value: String(countdown.value.days).padStart(2, '0'), label: 'D' }, { value: String(countdownAcara.value.days).padStart(2, '0'), label: 'D' },
{ value: String(countdown.value.hours).padStart(2, '0'), label: 'H' }, { value: String(countdownAcara.value.hours).padStart(2, '0'), label: 'H' },
{ value: String(countdown.value.minutes).padStart(2, '0'), label: 'M' }, { value: String(countdownAcara.value.minutes).padStart(2, '0'), label: 'M' },
{ value: String(countdown.value.seconds).padStart(2, '0'), label: 'S' } { 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 updateCountdown = () => {
const eventDateObj = new Date('2025-06-20T09:00:00')
const now = new Date() const now = new Date()
const diff = eventDateObj.getTime() - now.getTime()
if (diff > 0) { // Countdown untuk acara khitanan
countdown.value = { if (props.hitung_mundur_mulai) {
days: Math.floor(diff / (1000 * 60 * 60 * 24)), const eventDateObj = new Date(props.hitung_mundur_mulai)
hours: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)), const diff = eventDateObj - now
minutes: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)), if (diff > 0) {
seconds: Math.floor((diff % (1000 * 60)) / 1000) countdownAcara.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 {
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 }
} }
} else {
countdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
if (countdownInterval) clearInterval(countdownInterval)
} }
} }
/* ==========================
📅 ACTIONS
========================== */
const addToCalendar = () => { const addToCalendar = () => {
const eventDetails = { const title = `Tasyakuran Khitan ${props.nama_panggilan || 'Putra Kami'}`
title: `Tasyakuran Khitan ${childName.value}`, const start = new Date(props.hitung_mundur_mulai).toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
start: '20250620T090000Z', const end = new Date(new Date(props.hitung_mundur_mulai).getTime() + 3 * 60 * 60 * 1000).toISOString().replace(/[-:]/g, '').split('.')[0] + 'Z'
end: '20250621T170000Z',
description: 'Undangan Tasyakuran Khitan', const url = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(title)}&dates=${start}/${end}&location=${encodeURIComponent(eventLocation.value)}`
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)}`
window.open(url, '_blank') window.open(url, '_blank')
} }
const openMap = () => { /* ==========================
const location = encodeURIComponent(eventLocation.value) LIFECYCLE
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'
}
onMounted(() => { onMounted(() => {
updateCountdown() updateCountdown()
countdownInterval = setInterval(updateCountdown, 1000) countdownInterval.value = setInterval(updateCountdown, 1000)
}) })
onUnmounted(() => { onUnmounted(() => {
if (countdownInterval) clearInterval(countdownInterval) if (countdownInterval.value) {
clearInterval(countdownInterval.value)
}
}) })
</script> </script>
<style scoped> <style scoped>
/* ... tetap sama dengan punyamu, kecuali hapus selector .grid-cols-1.lg\:grid-cols-2 */ /* Animations & bg-pattern */
</style> .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> <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 --> <!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div> <div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Main Content --> <!-- 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 --> <!-- Gallery Title -->
<div class="text-center mb-8 animate-fade-in-down"> <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"> <h1 class="text-yellow-400 text-4xl md:text-5xl font-bold mb-6 font-script">
@ -18,54 +16,58 @@
berharga dalam hidupku ini. berharga dalam hidupku ini.
</p> </p>
</div> </div>
<!-- Photo Gallery Grid --> <!-- Photo Gallery Grid -->
<div class="flex-1 max-w-4xl mx-auto w-full animate-fade-in-up animation-delay-300"> <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"> <div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<!-- Foto pertama (besar) -->
<!-- Large Photo (spans 2 rows on medium+ screens) --> <div
<div class="md:col-span-1 md:row-span-2 relative group cursor-pointer" @click="openModal(0)"> v-if="images.length > 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"> class="md:col-span-1 md:row-span-2 relative group cursor-pointer"
<img @click="openModal(0)"
:src="galleryImages[0]?.src || placeholderImage" >
:alt="galleryImages[0]?.alt || 'Gallery Image 1'" <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="images[0]"
:alt="`Gallery Image 1`"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
@error="handleImageError" @error="handleImageError"
/> />
<!-- Overlay --> <div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 flex items-center justify-center transition-colors duration-300">
<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" /> <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> </div>
</div> </div>
<!-- Medium Photos --> <!-- Foto lainnya -->
<div <div
v-for="(image, index) in galleryImages.slice(1, 5)" v-for="(image, index) in images.slice(1)"
:key="index + 1" :key="index + 1"
class="relative group cursor-pointer" class="relative group cursor-pointer"
@click="openModal(index + 1)" @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 <img
:src="image.src || placeholderImage" :src="image"
:alt="image.alt || `Gallery Image ${index + 2}`" :alt="`Gallery Image ${index + 2}`"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
@error="handleImageError" @error="handleImageError"
/> />
<!-- Overlay --> <div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 flex items-center justify-center transition-colors duration-300">
<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" /> <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>
<!-- 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>
</div> </div>
<!-- Modal for Image Preview --> <!-- Modal Preview -->
<Teleport to="body"> <Teleport to="body">
<div <div
v-if="selectedImage !== null" v-if="selectedImage !== null"
@ -73,22 +75,23 @@
@click="closeModal" @click="closeModal"
> >
<div class="relative max-w-4xl max-h-full" @click.stop> <div class="relative max-w-4xl max-h-full" @click.stop>
<!-- Close Button --> <!-- Tombol close -->
<button <button
@click="closeModal" @click="closeModal"
class="absolute -top-12 right-0 text-white hover:text-yellow-400 transition-colors duration-300" 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" /> <Icon name="lucide:x" class="w-8 h-8" />
</button> </button>
<!-- Image --> <!-- Gambar besar -->
<img <img
:src="galleryImages[selectedImage]?.src || placeholderImage" :src="images[selectedImage]"
:alt="galleryImages[selectedImage]?.alt || 'Gallery Image'" :alt="`Gallery Image ${selectedImage + 1}`"
class="max-w-full max-h-full object-contain rounded-lg shadow-2xl" class="max-w-full max-h-full object-contain rounded-lg shadow-2xl"
@error="handleImageError"
/> />
<!-- Navigation Arrows --> <!-- Navigasi kiri-kanan -->
<button <button
v-if="selectedImage > 0" v-if="selectedImage > 0"
@click.stop="navigateImage(-1)" @click.stop="navigateImage(-1)"
@ -96,90 +99,40 @@
> >
<Icon name="lucide:chevron-left" class="w-6 h-6" /> <Icon name="lucide:chevron-left" class="w-6 h-6" />
</button> </button>
<button <button
v-if="selectedImage < galleryImages.length - 1" v-if="selectedImage < images.length - 1"
@click.stop="navigateImage(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" 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" /> <Icon name="lucide:chevron-right" class="w-6 h-6" />
</button> </button>
<!-- Image Counter --> <!-- 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"> <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 }} / {{ galleryImages.length }} {{ selectedImage + 1 }} / {{ images.length }}
</div> </div>
</div> </div>
</div> </div>
</Teleport> </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> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
// Props
const props = defineProps({ const props = defineProps({
data: { images: {
type: Object, type: Array,
default: () => ({}) default: () => []
} }
}) })
// Reactive data // State
const selectedImage = ref(null) 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 // Modal Control
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) => { const openModal = (index) => {
selectedImage.value = index selectedImage.value = index
document.body.style.overflow = 'hidden' document.body.style.overflow = 'hidden'
@ -192,7 +145,7 @@ const closeModal = () => {
const navigateImage = (direction) => { const navigateImage = (direction) => {
const newIndex = selectedImage.value + direction const newIndex = selectedImage.value + direction
if (newIndex >= 0 && newIndex < galleryImages.value.length) { if (newIndex >= 0 && newIndex < props.images.length) {
selectedImage.value = newIndex selectedImage.value = newIndex
} }
} }
@ -201,33 +154,19 @@ const handleImageError = (event) => {
event.target.src = placeholderImage event.target.src = placeholderImage
} }
// Keyboard Control
const handleKeyPress = (event) => { const handleKeyPress = (event) => {
if (selectedImage.value !== null) { if (selectedImage.value !== null) {
switch (event.key) { switch (event.key) {
case 'Escape': case 'Escape': closeModal(); break
closeModal() case 'ArrowLeft': navigateImage(-1); break
break case 'ArrowRight': navigateImage(1); break
case 'ArrowLeft':
navigateImage(-1)
break
case 'ArrowRight':
navigateImage(1)
break
} }
} }
} }
// Lifecycle // Lifecycle
onMounted(() => { 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) window.addEventListener('keydown', handleKeyPress)
}) })
@ -238,114 +177,7 @@ onUnmounted(() => {
</script> </script>
<style scoped> <style scoped>
/* Background Pattern */
.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="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>'); 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> </style>

View File

@ -1,63 +1,91 @@
<template> <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 --> <!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div> <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 --> <!-- 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 --> <!-- Child Photo -->
<div class="w-48 h-60 overflow-hidden rounded-t-3xl mx-auto mb-6"> <div class="w-48 h-60 overflow-hidden rounded-t-3xl mx-auto mb-6 shadow-lg">
<img :src="formData.childPhoto || '/pria.jpg'" alt="Child" class="w-full h-full object-cover"/> <img
:src="getFullPhotoUrl(childPhoto)"
alt="Child"
class="w-full h-full object-cover"
/>
</div> </div>
<!-- Child Name --> <!-- Child Name -->
<div class="bg-blue-800/50 border border-yellow-400 rounded-md px-6 py-2 mb-4 inline-block"> <div
<h1 class="text-yellow-400 font-bold text-xl">{{ formData.nama_panggilan || 'SATRIA HUDA DINATA' }}</h1> 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> </div>
<!-- Child Order & Parents --> <!-- Child Order -->
<div class="text-white/80 text-lg mb-2"> <div v-if="form.anak_ke" class="text-white/80 text-lg mb-2">
Putra Ke Dua Dari Putra ke {{ form.anak_ke }} dari
</div> </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"> <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> </div>
</div>
</div>
</template> </template>
<script setup> <script setup>
import { computed } from 'vue'
import { useRuntimeConfig } from '#app'
const props = defineProps({ const props = defineProps({
data: { form: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
} }
}) })
const formData = computed(() => {
const result = {} const config = useRuntimeConfig()
props.data?.fields?.forEach(f => { const backendUrl = config.public.apiBaseUrl
result[f.name] = f.value
}) // ============ FOTO ANAK ============
return result const childPhoto = computed(() => {
}) const fotoPertama = props.form?.foto?.[3]
onMounted(() => { return fotoPertama || ''
console.log("🧾 Introduction Data:", props.data)
}) })
// ============ 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> </script>
<style scoped> <style scoped>
/* Background Islamic Pattern */
.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="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>'); 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> <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-blue-900 overflow-hidden">
<!-- Background Islamic Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div> <div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Decorative Elements -->
<div class="absolute top-0 left-0 w-full h-full"> <div class="absolute top-0 left-0 w-full h-full">
<!-- Top decorative curves -->
<div class="absolute top-0 left-0 w-full"> <div class="absolute top-0 left-0 w-full">
<svg viewBox="0 0 1200 300" class="w-full h-auto"> <svg viewBox="0 0 1200 300" class="w-full h-auto">
<defs> <defs>
@ -20,7 +16,6 @@
</svg> </svg>
</div> </div>
<!-- Bottom decorative curves -->
<div class="absolute bottom-0 left-0 w-full"> <div class="absolute bottom-0 left-0 w-full">
<svg viewBox="0 0 1200 300" class="w-full h-auto"> <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"/> <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>
</div> </div>
<!-- Main Content -->
<div class="relative z-10 h-full flex items-center justify-center px-6"> <div class="relative z-10 h-full flex items-center justify-center px-6">
<div class="text-center max-w-4xl mx-auto"> <div class="text-center max-w-4xl mx-auto">
<!-- Islamic Symbol -->
<div class="mb-8 animate-fade-in-down"> <div class="mb-8 animate-fade-in-down">
<div class="mx-auto w-32 h-32 relative"> <div class="mx-auto w-32 h-32 relative">
<!-- Crescent and Star -->
<div class="absolute inset-0 flex items-center justify-center"> <div class="absolute inset-0 flex items-center justify-center">
<svg viewBox="0 0 100 100" class="w-full h-full text-yellow-400"> <svg viewBox="0 0 100 100" class="w-full h-full text-yellow-400">
<defs> <defs>
@ -46,16 +38,13 @@
</feMerge> </feMerge>
</filter> </filter>
</defs> </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)"/> <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)"/> <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> </svg>
</div> </div>
</div> </div>
</div> </div>
<!-- Invitation Title -->
<div class="mb-12 animate-fade-in-up animation-delay-300"> <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"> <h1 class="text-yellow-400 text-3xl md:text-4xl font-bold mb-4 tracking-wide">
Undangan Undangan
@ -68,37 +57,30 @@
</h2> </h2>
</div> </div>
<!-- Guest Information -->
<div class="mb-12 animate-fade-in-up animation-delay-600"> <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"> <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-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>
</div> </div>
<!-- Open Invitation Button -->
<div class="animate-fade-in-up animation-delay-900"> <div class="animate-fade-in-up animation-delay-900">
<button <button
@click="$emit('next-page')" @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" 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> <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 class="relative flex items-center space-x-2">
<span>OPEN INVITATION</span> <span>OPEN INVITATION</span>
<Icon name="lucide:chevron-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform duration-300" /> <Icon name="lucide:chevron-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform duration-300" />
</span> </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> <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> </button>
</div> </div>
<!-- Decorative Elements Around Button -->
<div class="absolute inset-0 pointer-events-none"> <div class="absolute inset-0 pointer-events-none">
<!-- Floating particles -->
<div class="absolute top-1/4 left-1/4 animate-float"> <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 class="w-2 h-2 bg-yellow-400 rounded-full opacity-60"></div>
</div> </div>
@ -112,7 +94,6 @@
</div> </div>
</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 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 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> <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> <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 --> <!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div> <div class="absolute inset-0 bg-pattern opacity-20"></div>
<!-- Main Content --> <!-- 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"> <div class="text-center max-w-4xl mx-auto">
<!-- Title --> <!-- Title -->
<div class="mb-12 animate-fade-in-down"> <div class="mb-10 animate-fade-in-down">
<h1 class="text-yellow-400 text-4xl md:text-6xl font-bold mb-6 font-script">Terimakasih</h1> <h1 class="text-yellow-400 text-3xl md:text-5xl lg:text-6xl font-bold mb-4 md:mb-6 font-script">
<p class="text-white text-lg md:text-xl leading-relaxed max-w-3xl mx-auto"> Terima Kasih
Kami mengucapkan terima kasih atas kehadiran serta doa restu </h1>
yang diberikan. Semoga Allah SWT senantiasa melimpahkan <p class="text-white text-sm md:text-base lg:text-lg leading-relaxed max-w-3xl mx-auto">
rahmat dan keberkahan kepada kita semua. 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> </p>
</div> </div>
<!-- Family Info --> <!-- Family Info -->
<div class="mb-12 animate-fade-in-up animation-delay-300"> <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> <h2 class="text-yellow-400 text-xl md:text-2xl lg:text-3xl font-bold mb-6 md:mb-8">
<div class="space-y-6"> Kami Keluarga Besar Dari
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-yellow-400/30 animate-fade-in-up animation-delay-500"> </h2>
<div class="text-yellow-400 text-lg font-semibold mb-2"> <div class="space-y-4 md:space-y-6">
{{ data.fatherName || 'Bpk H. Munawar Huda, S.H.' }} & {{ data.motherName || 'Ibu Hj. Dinah, A.M.Keb' }} <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> <div class="text-yellow-400 text-base md:text-lg font-semibold mb-2">
<div class="text-white/80 text-sm"> {{ jsonData.nama_bapak || 'Bpk H. Munawar Huda, S.H.' }} & {{ jsonData.nama_ibu || 'Ibu Hj. Dinah, A.M.Keb' }}
{{ data.fatherDescription || '(Anggota DPRD Provinsi Banten Fraksi Partai Demokrat)' }}
</div> </div>
</div> </div>
</div> </div>
@ -33,26 +35,45 @@
<!-- Final Message --> <!-- Final Message -->
<div class="animate-fade-in-up animation-delay-1200"> <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"> <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-lg md:text-xl font-medium mb-4 italic"> <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..." "Dan Allah telah mengeluarkan kamu dari perut ibumu dalam keadaan tidak mengetahui sesuatupun..."
</p> </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>
</div> </div>
<!-- Social --> <!-- Social Media Links -->
<div class="mt-12 animate-fade-in-up animation-delay-1500 flex justify-center space-x-6"> <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 href="https://instagram.com/abbauftech" target="_blank" rel="noopener noreferrer" <a
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" v-if="jsonData.link_instagram"
aria-label="Instagram"> :href="jsonData.link_instagram"
<Icon name="lucide:instagram" class="w-6 h-6 text-yellow-400" /> 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>
<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"> <a
<Icon name="lucide:facebook" class="w-6 h-6 text-yellow-400" /> 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>
<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"> <a
<Icon name="lucide:message-circle" class="w-6 h-6 text-yellow-400" /> 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> </a>
</div> </div>
</div> </div>
@ -66,10 +87,14 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { computed } from 'vue'
const props = defineProps({ const props = defineProps({
data: { childName: {
type: String,
default: ''
},
jsonData: {
type: Object, type: Object,
default: () => ({}) 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>"); 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 { .animate-fade-in-up {
animation: fadeInUp 1s forwards; animation: fadeInUp 1s ease-out forwards;
} }
.animate-fade-in-down { .animate-fade-in-down {
animation: fadeInDown 1s forwards; animation: fadeInDown 1s ease-out forwards;
} }
@keyframes fadeInUp { @keyframes fadeInUp {
@ -102,4 +126,21 @@ const currentYear = computed(() => new Date().getFullYear())
from { opacity: 0; transform: translateY(-20px); } from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); } 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> </style>
```

View File

@ -1,6 +1,8 @@
<!-- components/undangan/undangan-khitan-premium.vue --> <!-- components/undangan/undangan-khitan-premium.vue -->
<template> <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 ================= --> <!-- ================= NAVIGATION ================= -->
<nav <nav
v-if="currentSection !== 'landing'" v-if="currentSection !== 'landing'"
@ -9,21 +11,11 @@
<ul <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" 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> <li><button @click="switchSection('introduction')" :class="navClass('introduction')">Intro</button></li>
<button @click="switchSection('introduction')" :class="navClass('introduction')">Intro</button> <li><button @click="switchSection('event')" :class="navClass('event')">Event</button></li>
</li> <li><button @click="switchSection('gallery')" :class="navClass('gallery')">Gallery</button></li>
<li> <li><button @click="switchSection('say')" :class="navClass('say')">Guest Book</button></li>
<button @click="switchSection('event')" :class="navClass('event')">Event</button> <li><button @click="switchSection('thanks')" :class="navClass('thanks')">Thanks</button></li>
</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> </ul>
</nav> </nav>
@ -32,6 +24,7 @@
<button @click="toggleMusic" class="bg-blue-600 p-3 rounded-full text-white shadow-lg"> <button @click="toggleMusic" class="bg-blue-600 p-3 rounded-full text-white shadow-lg">
{{ isPlaying ? '⏸️' : '▶️' }} {{ isPlaying ? '⏸️' : '▶️' }}
</button> </button>
<audio ref="audioPlayer" :src="musicUrl" loop></audio>
</div> </div>
<!-- ================= MAIN CONTENT ================= --> <!-- ================= MAIN CONTENT ================= -->
@ -47,20 +40,28 @@
/> />
<!-- Introduction --> <!-- Introduction -->
<KhitanIntroduction <KhitanIntroduction
v-if="currentSection === 'introduction'" v-if="currentSection === 'introduction'"
:data="formData" :form="formData"
/> />
<!-- Event --> <!-- Event -->
<KhitanEvent <KhitanEvent
v-if="currentSection === 'event'" v-if="currentSection === 'event'"
:hari_tanggal_acara="formData.hari_tanggal_acara" :hari_tanggal_acara="formData.hari_tanggal_acara"
:waktu="formData.waktu" :waktu="formData.waktu"
:alamat="formData.alamat" :alamat="formData.alamat"
:link_gmaps="formData.link_gmaps" :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 --> <!-- Gallery -->
<KhitanGallery <KhitanGallery
@ -76,18 +77,15 @@
@addMessage="addMessage" @addMessage="addMessage"
/> />
<!-- Thank You --> <!-- Thank You -->
<KhitanThankYou <KhitanThankYou v-if="currentSection === 'thanks'" :childName="formData.nama_panggilan" :jsonData="formData" />
v-if="currentSection === 'thanks'"
:childName="formData.nama_panggilan"
:jsonData="formData"
/>
</main> </main>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, computed } from 'vue' import { ref, computed, watch } from 'vue'
import { useRuntimeConfig } from '#app' import { useRuntimeConfig } from '#app'
// ================== IMPORT KOMPONEN ================== // ================== IMPORT KOMPONEN ==================
@ -112,15 +110,27 @@ const formData = computed(() => props.data.form || {})
// ================== GALERI ================== // ================== GALERI ==================
const galleryImages = computed(() => { const galleryImages = computed(() => {
return [ const f = formData.value.foto
formData.value.foto_1,
formData.value.foto_2,
formData.value.foto_3, if (Array.isArray(f)) {
formData.value.foto_4, return f.map(img => `${backendUrl}/storage/${img}`)
formData.value.foto_5 }
]
.filter(Boolean) // Jika masih bentuk lama (foto_1, foto_2, dst.)
.map(f => `${backendUrl}/${f}`) return [
formData.value.foto_1,
formData.value.foto_2,
formData.value.foto_3,
formData.value.foto_4,
formData.value.foto_5
].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 ================== // ================== NAVIGASI SECTION ==================
@ -128,8 +138,21 @@ const currentSection = ref('landing')
const switchSection = (s) => (currentSection.value = s) const switchSection = (s) => (currentSection.value = s)
// ================== MUSIK ================== // ================== MUSIK ==================
const audioPlayer = ref(null)
const isPlaying = ref(false) 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 ================== // ================== GUEST BOOK ==================
const messages = ref([]) const messages = ref([])
@ -140,15 +163,9 @@ const navClass = (s) =>
currentSection.value === s currentSection.value === s
? 'text-blue-800 underline' ? 'text-blue-800 underline'
: 'hover:text-blue-700' : 'hover:text-blue-700'
onMounted(() => {
console.log("🧾 formData:", formData.value)
console.log("🖼️ galleryImages:", galleryImages.value)
})
</script> </script>
<style scoped> <style scoped>
/* Animasi transisi antar section */
main { main {
transition: all 0.7s ease-in-out; transition: all 0.7s ease-in-out;
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB