tes design template

This commit is contained in:
Farhaan4 2025-10-13 15:41:26 +07:00
parent 9faed9a803
commit 9be255c4d8
19 changed files with 673 additions and 823 deletions

View File

@ -1,27 +1,191 @@
<template>
<section class="w-full max-w-6xl mx-auto text-center">
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-8">
Acara Ulang Tahun
<section
class="w-full bg-gradient-to-b from-yellow-300 to-yellow-100 py-12 px-4 text-center relative overflow-hidden">
<!-- Judul -->
<h1 class="text-3xl md:text-4xl font-extrabold text-orange-700 mb-10">
🎉 BIRTHDAY PARTY 🎉
</h1>
<div class="bg-yellow-300/60 rounded-3xl p-8 shadow-xl">
<p class="text-orange-800 text-lg md:text-xl mb-4">
Insya Allah akan dilaksanakan pada:
<div class="max-w-6xl mx-auto grid md:grid-cols-2 gap-8 bg-yellow-200/50 rounded-3xl p-8 shadow-xl relative z-10">
<!-- Kolom Kiri - Google Map -->
<div class="flex flex-col items-center justify-center">
<iframe v-if="mapEmbed" class="w-full h-72 rounded-2xl shadow-lg" :src="mapEmbed" allowfullscreen
loading="lazy"></iframe>
<button
class="mt-4 bg-orange-700 hover:bg-orange-800 text-white font-semibold py-2 px-6 rounded-full shadow-md transition"
@click="openMap">
Direction
</button>
</div>
<!-- Kolom Kanan - Detail Acara -->
<div class="text-center">
<h2 class="text-2xl md:text-3xl font-extrabold text-blue-900 mb-4">
BIRTHDAY PARTY
</h2>
<!-- Tanggal dan Waktu -->
<div class="flex justify-center items-center gap-6 mb-4">
<div>
<p class="text-4xl font-extrabold text-orange-700">
{{ eventDateNum || '-' }}
</p>
<p class="text-orange-700 text-2xl md:text-3xl font-bold mb-6">
Selasa, 11 Juni 2025<br> Pukul 11.00 WIB
<p class="text-sm font-bold text-orange-800 uppercase">
{{ eventDayName || 'TANGGAL TIDAK VALID' }}<br />
{{ eventMonthYear || '' }}
</p>
<p class="text-orange-800 text-lg md:text-xl mb-6">
Bertempat di:<br>
Jl. Andara Raya No.123, Jakarta Selatan
</div>
<div class="border-l-2 border-orange-500 h-10"></div>
<div class="text-left">
<p class="text-lg font-semibold text-orange-700">
{{ props.waktu || '00.00' }} WIB
</p>
<div class="mt-6">
<iframe
class="w-full h-64 rounded-2xl shadow-lg"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3..."
allowfullscreen
loading="lazy">
</iframe>
<p class="text-sm text-orange-800 font-bold">S.D SELESAI</p>
</div>
</div>
<!-- Lokasi -->
<p class="text-orange-900 font-medium mb-6 leading-relaxed">
{{ props.alamat || 'Belum ada alamat' }}
</p>
<!-- Countdown -->
<div v-if="countdownValid" class="flex justify-center gap-4 mb-6 text-white font-bold">
<div v-for="(value, label) in countdownDisplay" :key="label"
class="bg-orange-700 rounded-xl px-4 py-3 text-center shadow-md w-16">
<p class="text-2xl">{{ value }}</p>
<p class="text-xs uppercase">{{ label }}</p>
</div>
</div>
<!-- Tombol Kalender -->
<button
class="bg-orange-700 hover:bg-orange-800 text-white font-semibold py-2 px-6 rounded-full shadow-md transition"
@click="addToCalendar">
Add to Calendar
</button>
</div>
</div>
<!-- Gambar di bawah -->
<img src="/ABBAUF.png" alt="Logo" class="mx-auto mt-10 w-56 md:w-64" />
</section>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({
hari_tanggal_acara: String,
waktu: String,
alamat: String,
link_gmaps: String,
hitung_mundur: String,
})
// --- Format tanggal agar tidak invalid ---
const eventDate = computed(() => {
let dateString = props.hari_tanggal_acara
if (!dateString) return null
// Tambahkan waktu default agar format valid di semua browser
if (!dateString.includes('T')) {
dateString += 'T00:00:00Z'
}
const d = new Date(dateString)
return isNaN(d) ? null : d
})
const eventDateNum = computed(() => eventDate.value?.getDate() || null)
const eventDayName = computed(() =>
eventDate.value
? eventDate.value
.toLocaleDateString('id-ID', { weekday: 'long' })
.toUpperCase()
: null
)
const eventMonthYear = computed(() =>
eventDate.value
? eventDate.value
.toLocaleDateString('id-ID', { month: 'long', year: 'numeric' })
.toUpperCase()
: null
)
// --- Google Map Embed ---
const mapEmbed = computed(() => {
if (!props.link_gmaps || props.link_gmaps.trim() === '') return null
if (props.link_gmaps.includes('/embed?')) return props.link_gmaps
return `https://www.google.com/maps?q=${encodeURIComponent(
props.link_gmaps
)}&output=embed`
})
const openMap = () => {
if (props.link_gmaps && props.link_gmaps.trim() !== '') {
window.open(props.link_gmaps, '_blank')
}
}
// --- Countdown ---
const countdown = ref({ days: 0, hours: 0, minutes: 0, seconds: 0 })
const countdownValid = ref(false)
onMounted(() => {
let targetString = props.hitung_mundur
if (!targetString) return
if (!targetString.includes('T')) {
targetString += 'T00:00:00'
}
const target = new Date(targetString)
if (isNaN(target)) return
countdownValid.value = true
setInterval(() => {
const now = new Date().getTime()
const diff = target.getTime() - now
if (diff <= 0) return
countdown.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),
}
}, 1000)
})
const countdownDisplay = computed(() => ({
D: countdown.value.days,
H: countdown.value.hours,
M: countdown.value.minutes,
S: countdown.value.seconds,
}))
// --- Add to Calendar ---
const addToCalendar = () => {
const title = 'Birthday Party'
let dateString = props.hari_tanggal_acara
if (!dateString) return
if (!dateString.includes('T')) {
dateString += 'T00:00:00'
}
const startDate = new Date(dateString)
const endDate = new Date(startDate.getTime() + 2 * 60 * 60 * 1000) // +2 jam
const start = startDate.toISOString().replace(/-|:|\.\d+/g, '')
const end = endDate.toISOString().replace(/-|:|\.\d+/g, '')
const url = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(
title
)}&dates=${start}/${end}&details=${encodeURIComponent(
'Acara Ulang Tahun di ' + (props.alamat || '')
)}`
window.open(url, '_blank')
}
</script>
<style scoped>
section {
position: relative;
}
</style>

View File

@ -1,15 +1,36 @@
<template>
<section class="w-full max-w-6xl mx-auto text-center">
<section class="w-full max-w-6xl mx-auto text-center px-4">
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-8">
Galeri Foto
</h1>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<img src="/logo1.png" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/logo2.png" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/pria.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/wanita.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/templat.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/iphone.png" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<div v-if="images && images.length" class="grid grid-cols-2 md:grid-cols-3 gap-4">
<img
v-for="(img, index) in images"
:key="index"
:src="img"
alt="gallery"
class="rounded-xl shadow-lg object-cover h-48 w-full hover:scale-105 transition-transform duration-300"
/>
</div>
<div v-else class="text-orange-800 bg-yellow-100 py-8 rounded-2xl shadow-inner">
<p class="text-lg font-semibold">Belum ada foto untuk ditampilkan</p>
</div>
</section>
</template>
<script setup>
const props = defineProps({
images: {
type: Array,
required: true
}
})
</script>
<style scoped>
img {
border: 3px solid rgba(255, 255, 255, 0.6);
}
</style>

View File

@ -28,10 +28,10 @@
<script setup>
defineProps({
age: Number,
age: [Number,String],
childName: String,
childOrder: Number,
childOrder: [Number,String],
parentNames: String,
childPhoto: String
childPhoto: [String, Array]
})
</script>

View File

@ -1,20 +1,68 @@
<template>
<section class="w-full max-w-3xl mx-auto text-center">
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-6">
<section
class="min-h-screen flex flex-col justify-center items-center text-center bg-gradient-to-br from-yellow-200 via-yellow-300 to-yellow-400 px-6 py-12 relative overflow-hidden"
>
<!-- Background Ornamen -->
<div class="absolute inset-0 opacity-10 pointer-events-none">
<div class="absolute top-10 left-10 w-24 h-24 bg-orange-500 rounded-full blur-2xl"></div>
<div class="absolute bottom-10 right-10 w-32 h-32 bg-yellow-600 rounded-full blur-3xl"></div>
</div>
<!-- Konten Utama -->
<div
class="relative z-10 w-full max-w-3xl bg-white/60 backdrop-blur-md rounded-3xl shadow-2xl p-8 md:p-12 border border-yellow-200"
>
<h1 class="text-orange-700 text-4xl md:text-5xl font-extrabold mb-6 animate-fade-in">
Terima Kasih
</h1>
<p class="text-orange-800 text-lg md:text-xl mb-6">
<p
class="text-orange-800 text-lg md:text-xl mb-8 leading-relaxed animate-fade-in delay-100"
>
Kehadiran dan doa restu Bapak/Ibu/Saudara/i merupakan kebahagiaan besar bagi kami.
</p>
<div class="bg-yellow-300/60 rounded-3xl p-6 shadow-xl">
<p class="text-orange-700 font-bold text-xl">Raffi Ahmad & Nagita Slavina</p>
<p class="text-orange-800">Orang Tua {{ childName }}</p>
<div
class="bg-gradient-to-r from-yellow-300 to-yellow-400 rounded-3xl py-6 px-8 shadow-md animate-fade-in delay-200"
>
<p class="text-orange-700 font-bold text-2xl md:text-3xl mb-2">
{{ jsonData.nama_bapak }} & {{ jsonData.nama_ibu }}
</p>
<p class="text-orange-800 text-lg">
Orang Tua {{ jsonData.nama_panggilan }}
</p>
</div>
</div>
</section>
</template>
<script setup>
defineProps({
childName: String
jsonData: {
type: Object,
required: true
}
})
</script>
<style scoped>
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 0.8s ease-out forwards;
}
.delay-100 {
animation-delay: 0.1s;
}
.delay-200 {
animation-delay: 0.2s;
}
</style>

View File

@ -1,769 +1,31 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-yellow-300 via-yellow-400 to-yellow-500 relative overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-10 left-10 w-20 h-20 bg-yellow-600 rounded-full"></div>
<div class="absolute top-32 right-20 w-16 h-16 bg-yellow-600 rounded-full"></div>
<div class="absolute bottom-20 left-20 w-12 h-12 bg-yellow-600 rounded-full"></div>
<div class="absolute bottom-40 right-40 w-24 h-24 bg-yellow-600 rounded-full"></div>
</div>
<!-- Navigation -->
<nav class="relative z-20 bg-transparent border-b border-yellow-600/20">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-center items-center h-16">
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-8">
<a href="#introduction" @click="currentSection = 'introduction'"
:class="currentSection === 'introduction' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
class="px-3 py-2 text-sm font-medium transition-colors">
INTRODUCTION
</a>
<a href="#event" @click="currentSection = 'event'"
:class="currentSection === 'event' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
class="px-3 py-2 text-sm font-medium transition-colors">
EVENT
</a>
<a href="#galeri" @click="currentSection = 'galeri'"
:class="currentSection === 'galeri' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
class="px-3 py-2 text-sm font-medium transition-colors">
GALERI
</a>
<a href="#say" @click="currentSection = 'say'"
:class="currentSection === 'say' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
class="px-3 py-2 text-sm font-medium transition-colors">
SAY?
</a>
<a href="#thanks" @click="currentSection = 'thanks'"
:class="currentSection === 'thanks' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
class="px-3 py-2 text-sm font-medium transition-colors">
THANKS
</a>
</div>
</div>
</div>
</div>
</nav>
<!-- Mobile Navigation -->
<div class="md:hidden bg-yellow-400/90 backdrop-blur-sm">
<div class="px-2 pt-2 pb-3 space-y-1">
<a href="#introduction" @click="currentSection = 'introduction'"
:class="currentSection === 'introduction' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
INTRODUCTION
</a>
<a href="#event" @click="currentSection = 'event'"
:class="currentSection === 'event' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
EVENT
</a>
<a href="#galeri" @click="currentSection = 'galeri'"
:class="currentSection === 'galeri' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
GALERI
</a>
<a href="#say" @click="currentSection = 'say'"
:class="currentSection === 'say' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
SAY?
</a>
<a href="#thanks" @click="currentSection = 'thanks'"
:class="currentSection === 'thanks' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
THANKS
</a>
</div>
</div>
<!-- Music Icon -->
<div class="fixed bottom-4 left-4 z-30">
<button @click="toggleMusic" class="bg-orange-600 hover:bg-orange-700 text-white p-3 rounded-full shadow-lg transition-colors">
<svg v-if="isPlaying" class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
</svg>
<svg v-else class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</button>
</div>
<!-- Main Content -->
<main class="relative z-10 min-h-screen flex items-center justify-center p-4">
<!-- Landing Section -->
<section v-if="currentSection === 'landing'" class="w-full max-w-6xl mx-auto">
<div class="flex flex-col lg:flex-row items-center justify-between gap-8">
<!-- Left Side Content -->
<div class="flex-1 text-center lg:text-left">
<h1 class="text-orange-700 text-xl md:text-2xl font-semibold mb-4">
Celebrate With Us
</h1>
<h2 class="text-blue-600 text-4xl md:text-6xl font-bold mb-4">
{{ childName }}
</h2>
<h3 class="text-orange-700 text-2xl md:text-3xl font-bold mb-8">
Birthday Party
</h3>
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-2xl p-6 mb-8 inline-block">
<p class="text-orange-800 text-lg mb-2">Kepada Yth.</p>
<p class="text-orange-700 text-xl font-semibold">{{ guestName }}</p>
</div>
<button @click="openInvitation" class="bg-orange-600 hover:bg-orange-700 text-white px-8 py-4 rounded-full text-lg font-semibold shadow-lg transform hover:scale-105 transition-all">
Open Invitation
</button>
</div>
<!-- Right Side - Minions -->
<div class="flex-1 relative">
<div class="relative w-full max-w-md mx-auto">
<!-- Minions Placeholder -->
<div class="bg-yellow-400 rounded-full w-64 h-64 mx-auto flex items-center justify-center border-4 border-yellow-600">
<div class="text-center">
<div class="w-20 h-20 bg-white rounded-full mx-auto mb-4 flex items-center justify-center border-4 border-gray-800">
<div class="w-12 h-12 bg-yellow-600 rounded-full"></div>
</div>
<div class="text-gray-800 font-bold text-lg">Minions</div>
</div>
</div>
<!-- Additional minions -->
<div class="absolute -left-8 top-16 w-20 h-20 bg-yellow-400 rounded-full border-2 border-yellow-600 flex items-center justify-center">
<div class="w-6 h-6 bg-white rounded-full border border-gray-800"></div>
</div>
<div class="absolute -right-8 top-20 w-16 h-16 bg-yellow-400 rounded-full border-2 border-yellow-600 flex items-center justify-center">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
</div>
</div>
</div>
</div>
</section>
<!-- Introduction Section -->
<section v-if="currentSection === 'introduction'" class="w-full max-w-6xl mx-auto">
<div class="flex flex-col lg:flex-row items-center justify-between gap-8">
<!-- Left Side Content -->
<div class="flex-1 text-center lg:text-left">
<h1 class="text-orange-700 text-2xl md:text-3xl font-bold mb-6">
Ulang Tahun Ke -{{ age }}
</h1>
<h2 class="text-orange-800 text-3xl md:text-5xl font-bold mb-6">
{{ childName }}
</h2>
<h3 class="text-orange-700 text-xl md:text-2xl font-semibold mb-4">
Anak Ke -{{ childOrder }}
</h3>
<h4 class="text-orange-800 text-2xl md:text-3xl font-bold mb-8">
{{ parentNames }}
</h4>
<!-- Minions Animation Area -->
<div class="flex justify-center lg:justify-start gap-4 mb-8">
<div class="w-20 h-20 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600 transform hover:scale-110 transition-transform">
<div class="w-8 h-8 bg-white rounded-full border border-gray-800"></div>
</div>
<div class="w-20 h-20 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600 transform hover:scale-110 transition-transform">
<div class="w-8 h-8 bg-white rounded-full border border-gray-800"></div>
</div>
</div>
</div>
<!-- Right Side - Child Photo -->
<div class="flex-1">
<div class="relative w-full max-w-md mx-auto">
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl">
<img :src="childPhoto || '/assets/img/child-placeholder.jpg'"
:alt="childName"
class="w-full h-80 object-cover rounded-2xl shadow-lg">
</div>
</div>
</div>
</div>
</section>
<!-- Event Section -->
<section v-if="currentSection === 'event'" class="w-full max-w-6xl mx-auto">
<div class="flex flex-col lg:flex-row gap-8">
<!-- Left Side - Map -->
<div class="flex-1">
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-6 shadow-xl">
<div class="relative">
<!-- Minion Peeking -->
<div class="absolute -top-8 left-1/2 transform -translate-x-1/2 z-10">
<div class="w-16 h-8 bg-yellow-400 rounded-t-full border-2 border-yellow-600 flex items-end justify-center">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800 mb-1"></div>
</div>
</div>
<!-- Map Placeholder -->
<div class="bg-gray-200 rounded-2xl h-64 flex items-center justify-center">
<div class="text-center">
<div class="text-gray-500 text-lg mb-2">📍 Location Map</div>
<div class="text-gray-400 text-sm">Google Maps Integration</div>
</div>
</div>
<button class="absolute bottom-4 left-4 bg-orange-600 hover:bg-orange-700 text-white px-4 py-2 rounded-lg font-semibold">
Direction
</button>
</div>
</div>
</div>
<!-- Right Side - Event Details -->
<div class="flex-1">
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-6 shadow-xl">
<!-- Minion Peeking -->
<div class="absolute -top-8 right-8">
<div class="w-16 h-8 bg-yellow-400 rounded-t-full border-2 border-yellow-600 flex items-end justify-center">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800 mb-1"></div>
</div>
</div>
<div class="relative">
<h2 class="text-blue-600 text-2xl md:text-3xl font-bold mb-6 text-center">
BIRTHDAY PARTY
</h2>
<!-- Date and Time -->
<div class="mb-6">
<div class="flex items-center gap-4 mb-2">
<span class="text-orange-700 text-xl font-bold">{{ eventDay }}</span>
<span class="text-orange-700 text-lg">{{ eventDate }}</span>
<span class="text-orange-700 text-lg">{{ eventTime }}</span>
</div>
<div class="text-orange-600 font-medium">{{ eventLocation }}</div>
</div>
<!-- Countdown -->
<div class="grid grid-cols-4 gap-2 mb-6">
<div class="bg-orange-600 rounded-lg p-3 text-center text-white">
<div class="text-xl font-bold">{{ countdown.days }}</div>
<div class="text-xs">D</div>
</div>
<div class="bg-orange-600 rounded-lg p-3 text-center text-white">
<div class="text-xl font-bold">{{ countdown.hours }}</div>
<div class="text-xs">H</div>
</div>
<div class="bg-orange-600 rounded-lg p-3 text-center text-white">
<div class="text-xl font-bold">{{ countdown.minutes }}</div>
<div class="text-xs">M</div>
</div>
<div class="bg-orange-600 rounded-lg p-3 text-center text-white">
<div class="text-xl font-bold">{{ countdown.seconds }}</div>
<div class="text-xs">S</div>
</div>
</div>
<!-- Add to Calendar -->
<button class="w-full bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-lg font-semibold">
Add to Calendar
</button>
<!-- Minions at bottom -->
<div class="flex justify-center mt-6 gap-4">
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-3 h-3 bg-white rounded-full border border-gray-800"></div>
</div>
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-3 h-3 bg-white rounded-full border border-gray-800"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Gallery Section -->
<section v-if="currentSection === 'galeri'" class="w-full max-w-6xl mx-auto">
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl relative">
<h2 class="text-orange-700 text-3xl md:text-4xl font-bold text-center mb-8">
Galeri
</h2>
<!-- Photo Grid -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8">
<div v-for="(photo, index) in galleryPhotos" :key="index"
class="aspect-square bg-gray-200 rounded-xl overflow-hidden shadow-lg hover:scale-105 transition-transform">
<img :src="photo || '/assets/img/gallery-placeholder.jpg'"
:alt="`Gallery ${index + 1}`"
class="w-full h-full object-cover">
</div>
</div>
<!-- Minions around gallery -->
<div class="absolute -bottom-4 -left-4">
<div class="w-16 h-16 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-6 h-6 bg-white rounded-full border border-gray-800"></div>
</div>
</div>
<div class="absolute -top-4 -right-4">
<div class="w-16 h-16 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-6 h-6 bg-white rounded-full border border-gray-800"></div>
</div>
</div>
<div class="absolute bottom-8 right-8 flex gap-2">
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
</div>
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
</div>
</div>
</div>
</section>
<!-- Say Something Section -->
<section v-if="currentSection === 'say'" class="w-full max-w-6xl mx-auto">
<div class="flex flex-col lg:flex-row gap-8">
<!-- Left Side - Form -->
<div class="flex-1">
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl relative">
<!-- Minion Peeking -->
<div class="absolute -top-8 left-8">
<div class="w-16 h-8 bg-yellow-400 rounded-t-full border-2 border-yellow-600 flex items-end justify-center">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800 mb-1"></div>
</div>
</div>
<h3 class="text-orange-700 text-2xl font-bold mb-6">Say Something!</h3>
<form @submit.prevent="submitMessage" class="space-y-4">
<div>
<label class="text-orange-700 font-semibold mb-2 block">Nome</label>
<input v-model="guestMessage.name"
type="text"
class="w-full px-4 py-3 rounded-xl border-2 border-yellow-300 focus:border-orange-500 outline-none bg-white/80">
</div>
<div>
<label class="text-orange-700 font-semibold mb-2 block">Message</label>
<textarea v-model="guestMessage.message"
rows="4"
class="w-full px-4 py-3 rounded-xl border-2 border-yellow-300 focus:border-orange-500 outline-none bg-white/80 resize-none"></textarea>
</div>
<div>
<label class="text-orange-700 font-semibold mb-2 block">Attendance</label>
<div class="flex gap-2">
<button type="button"
@click="guestMessage.attendance = 'yes'"
:class="guestMessage.attendance === 'yes' ? 'bg-green-600 text-white' : 'bg-white text-gray-700 hover:bg-green-100'"
class="px-4 py-2 rounded-lg font-semibold transition-colors">
Yes
</button>
<button type="button"
@click="guestMessage.attendance = 'no'"
:class="guestMessage.attendance === 'no' ? 'bg-red-600 text-white' : 'bg-white text-gray-700 hover:bg-red-100'"
class="px-4 py-2 rounded-lg font-semibold transition-colors">
No
</button>
<button type="button"
@click="guestMessage.attendance = 'maybe'"
:class="guestMessage.attendance === 'maybe' ? 'bg-yellow-600 text-white' : 'bg-white text-gray-700 hover:bg-yellow-100'"
class="px-4 py-2 rounded-lg font-semibold transition-colors">
Maybe
</button>
</div>
</div>
<button type="submit"
class="w-full bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-lg font-semibold">
Confirmation
</button>
</form>
</div>
</div>
<!-- Right Side - Messages -->
<div class="flex-1">
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl relative">
<!-- Minion Peeking -->
<div class="absolute -top-8 right-8">
<div class="w-16 h-8 bg-yellow-400 rounded-t-full border-2 border-yellow-600 flex items-end justify-center">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800 mb-1"></div>
</div>
</div>
<div class="flex items-center gap-2 mb-6">
<span class="text-gray-600">💬</span>
<span class="text-orange-700 font-bold">{{ messages.length.toString().padStart(2, '0') }}</span>
</div>
<!-- Messages List -->
<div class="space-y-4 max-h-96 overflow-y-auto">
<div v-for="message in messages" :key="message.id"
class="bg-white/80 rounded-xl p-4">
<div class="flex justify-between items-start mb-2">
<span class="font-semibold text-orange-700">{{ message.name }}</span>
<span :class="getAttendanceClass(message.attendance)"
class="px-2 py-1 rounded-full text-xs font-semibold">
{{ message.attendance.charAt(0).toUpperCase() + message.attendance.slice(1) }}
</span>
</div>
<p class="text-gray-700">{{ message.message }}</p>
</div>
</div>
<!-- Bottom Minions -->
<div class="absolute -bottom-4 right-4 flex gap-2">
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
</div>
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Thanks Section -->
<section v-if="currentSection === 'thanks'" class="w-full max-w-6xl mx-auto">
<div class="flex flex-col lg:flex-row items-center justify-between gap-8">
<!-- Left Side Content -->
<div class="flex-1 text-center lg:text-left">
<p class="text-orange-700 text-lg md:text-xl leading-relaxed mb-8">
Merupakan suatu kebahagiaan dan kehormatan bagi kami, apabila teman-teman,
berkenan hadir dan memberikan do'a.
</p>
<h3 class="text-orange-700 text-xl md:text-2xl font-semibold mb-4">
Hormat Kami
</h3>
<h4 class="text-orange-800 text-2xl md:text-3xl font-bold mb-8">
{{ parentNames }}
</h4>
<!-- Minion -->
<div class="flex justify-center lg:justify-start">
<div class="w-24 h-24 bg-yellow-400 rounded-full flex items-center justify-center border-4 border-yellow-600 transform hover:scale-110 transition-transform">
<div class="w-10 h-10 bg-white rounded-full border-2 border-gray-800 flex items-center justify-center">
<div class="w-6 h-6 bg-yellow-600 rounded-full"></div>
</div>
</div>
</div>
</div>
<!-- Right Side - Family Photo -->
<div class="flex-1">
<div class="relative w-full max-w-md mx-auto">
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl">
<img :src="familyPhoto || '/assets/img/family-placeholder.jpg'"
:alt="parentNames"
class="w-full h-80 object-cover rounded-2xl shadow-lg">
</div>
<!-- Decorative minion -->
<div class="absolute -bottom-2 -left-2">
<div class="w-16 h-16 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
<div class="w-6 h-6 bg-white rounded-full border border-gray-800"></div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<div class="space-y-24">
<Introduction
:age="age"
:childName="childName"
:childOrder="childOrder"
:parentNames="parentNames"
:childPhoto="childPhoto"
/>
<Event />
<Gallery />
<GuestBook />
<ThankYou />
</div>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
import Introduction from './Introduction.vue'
import Event from './Event.vue'
import Gallery from './Gallery.vue'
import GuestBook from './GuestBook.vue'
import ThankYou from './ThankYou.vue'
// Props/Data yang bisa diisi dari parent atau API
const childName = ref('Rayyanza Malik Ahmad')
const age = ref(4)
const childOrder = ref(2)
const parentNames = ref('Raffi Ahmad & Nagita Slavina')
const guestName = ref('Gempita Nora Marten')
const eventDay = ref('MINGGU')
const eventDate = ref('11 JUNI 2025')
const eventTime = ref('11.00 WITA S.D SELESAI')
const eventLocation = ref('Jalan Andara No.17 Cilandak Pondok Labu, DKI Jakarta Green Andara Residence')
// Photos
const childPhoto = ref('')
const familyPhoto = ref('')
const galleryPhotos = ref([
'/assets/img/gallery1.jpg',
'/assets/img/gallery2.jpg',
'/assets/img/gallery3.jpg',
'/assets/img/gallery4.jpg',
'/assets/img/gallery5.jpg',
'/assets/img/gallery6.jpg'
])
// State management
const currentSection = ref('landing')
const isPlaying = ref(false)
// Countdown timer
const countdown = reactive({
days: 0,
hours: 0,
minutes: 0,
seconds: 0
})
// Guest message form
const guestMessage = reactive({
name: '',
message: '',
attendance: 'yes'
})
// Messages list
const messages = ref([
{
id: 1,
name: 'Gempita',
message: 'Selamat ulang tahun cipung',
attendance: 'yes'
},
{
id: 2,
name: 'Ayu Ting Ting',
message: 'Selamat ulang tahun anak mama gigi',
attendance: 'maybe'
}
])
// Methods
const openInvitation = () => {
currentSection.value = 'introduction'
}
const toggleMusic = () => {
isPlaying.value = !isPlaying.value
// TODO: Implement actual music toggle
}
const submitMessage = () => {
if (guestMessage.name && guestMessage.message) {
messages.value.push({
id: Date.now(),
name: guestMessage.name,
message: guestMessage.message,
attendance: guestMessage.attendance
})
// Reset form
guestMessage.name = ''
guestMessage.message = ''
guestMessage.attendance = 'yes'
}
}
const getAttendanceClass = (attendance) => {
switch (attendance) {
case 'yes':
case 'attend':
return 'bg-green-500 text-white'
case 'no':
return 'bg-red-500 text-white'
case 'maybe':
return 'bg-yellow-500 text-white'
default:
return 'bg-gray-500 text-white'
}
}
// Countdown timer function
const updateCountdown = () => {
const eventDate = new Date('2025-06-11T11:00:00')
const now = new Date()
const diff = eventDate - now
if (diff > 0) {
countdown.days = Math.floor(diff / (1000 * 60 * 60 * 24))
countdown.hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
countdown.minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
countdown.seconds = Math.floor((diff % (1000 * 60)) / 1000)
}
}
// Lifecycle
let countdownInterval = null
onMounted(() => {
updateCountdown()
countdownInterval = setInterval(updateCountdown, 1000)
})
onUnmounted(() => {
if (countdownInterval) {
clearInterval(countdownInterval)
}
})
// Meta
useHead({
title: `${childName.value} Birthday Invitation`,
meta: [
{ name: 'description', content: `Join us to celebrate ${childName.value}'s ${age.value}th birthday party!` }
]
defineProps({
age: Number,
childName: String,
childOrder: Number,
parentNames: String,
childPhoto: String
})
</script>
<style scoped>
/* Custom animations */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-10px); }
}
.float-animation {
animation: float 3s ease-in-out infinite;
}
/* Smooth transitions */
* {
transition: all 0.3s ease;
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #fbbf24;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #ea580c;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #c2410c;
}
/* Custom button hover effects */
button {
transition: all 0.2s ease;
}
button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
button:active {
transform: translateY(0);
}
/* Glass morphism effect */
.backdrop-blur-sm {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
/* Responsive typography */
@media (max-width: 768px) {
.text-6xl { font-size: 2.5rem; }
.text-5xl { font-size: 2rem; }
.text-4xl { font-size: 1.75rem; }
.text-3xl { font-size: 1.5rem; }
}
/* Custom grid for gallery */
.grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@media (min-width: 768px) {
.md\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}
/* Loading animation for images */
img {
transition: opacity 0.3s ease;
}
img[src=""], img:not([src]) {
opacity: 0;
}
/* Custom yellow gradient */
.bg-gradient-to-br {
background-image: linear-gradient(to bottom right, #fcd34d, #f59e0b, #d97706);
}
/* Minion eye animation */
@keyframes blink {
0%, 90%, 100% { transform: scaleY(1); }
95% { transform: scaleY(0.1); }
}
.minion-eye {
animation: blink 3s ease-in-out infinite;
}
/* Party confetti effect */
@keyframes confetti {
0% { transform: translateY(-100px) rotate(0deg); opacity: 1; }
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
}
.confetti {
animation: confetti 3s linear infinite;
}
/* Mobile menu slide */
.mobile-menu {
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.mobile-menu.open {
transform: translateY(0);
}
/* Card hover effects */
.card-hover {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.card-hover:hover {
transform: translateY(-5px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
/* Text glow effect */
.text-glow {
text-shadow: 0 0 10px rgba(251, 191, 36, 0.5);
}
/* Pulse animation for important elements */
@keyframes pulse-soft {
0%, 100% { opacity: 1; }
50% { opacity: 0.8; }
}
.pulse-soft {
animation: pulse-soft 2s ease-in-out infinite;
}
</style>

View File

@ -8,8 +8,9 @@
<div class="relative text-center text-white p-8">
<p class="text-sm uppercase tracking-widest mb-4 animate-fade-in">The Wedding of</p>
<h1 class="text-5xl md:text-7xl font-serif mb-6 animate-slide-up">
{{ data.bride.nickname }} & {{ data.groom.nickname }}
{{ data.bride?.nickname || 'Bride' }} & {{ data.groom?.nickname || 'Groom' }}
</h1>
<p class="text-lg mb-8 animate-fade-in-delay">{{ formatDate(data.eventDate) }}</p>
<button @click="openInvitation"
class="px-8 py-3 bg-white text-rose-600 rounded-full font-semibold hover:bg-rose-50 transition-all duration-300 animate-pulse-slow">
@ -424,12 +425,12 @@ import AOS from 'aos'
import 'aos/dist/aos.css'
// Import shared components
import CountdownTimer from '~/components/shared/CountdownTimer.vue'
import Gallery from '~/components/shared/Gallery.vue'
import Maps from '~/components/shared/Maps.vue'
import RSVP from '~/components/shared/RSVP.vue'
import GuestBook from '~/components/shared/GuestBook.vue'
import MusicPlayer from '~/components/shared/MusicPlayer.vue'
import CountdownTimer from '~/components/templates/wedding/CountdownTimer.vue'
import Gallery from '~/components/templates/wedding/Gallery.vue'
import Maps from '~/components/templates/wedding/Maps.vue'
import RSVP from '~/components/templates/wedding/RSVP.vue'
import GuestBook from '~/components/templates/wedding/GuestBook.vue'
import MusicPlayer from '~/components/templates/wedding/MusicPlayer.vue'
// Props untuk menerima data dari parent/API
const props = defineProps({

View File

@ -0,0 +1,154 @@
<!-- 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">
<!-- ================= NAVIGATION ================= -->
<nav
v-if="currentSection !== 'landing'"
class="absolute top-4 left-1/2 transform -translate-x-1/2 z-20"
>
<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>
</ul>
</nav>
<!-- ================= MUSIK CONTROL ================= -->
<div class="fixed bottom-4 left-4 z-30" v-if="currentSection !== 'landing'">
<button @click="toggleMusic" class="bg-blue-600 p-3 rounded-full text-white shadow-lg">
{{ isPlaying ? '⏸️' : '▶️' }}
</button>
</div>
<!-- ================= MAIN CONTENT ================= -->
<main
class="relative z-10 min-h-screen flex items-center justify-center p-4 transition-all duration-700 ease-in-out"
>
<!-- Landing Page -->
<KhitanA
v-if="currentSection === 'landing'"
:childName="formData.nama_panggilan"
:guestName="data.nama_tamu"
@next-page="switchSection('introduction')"
/>
<!-- Introduction -->
<KhitanIntroduction
v-if="currentSection === 'introduction'"
:childName="formData.nama_lengkap"
:parentsName="`${formData.nama_bapak} & ${formData.nama_ibu}`"
:childOrder="formData.anak_ke"
:childPhoto="`${backendUrl}/${formData.foto_1}`"
:age="formData.umur_yang_dirayakan"
/>
<!-- Event -->
<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"
/>
<!-- Gallery -->
<KhitanGallery
v-if="currentSection === 'gallery'"
:images="galleryImages"
/>
<!-- Guest Book -->
<KhitanSay
v-if="currentSection === 'say'"
:guestName="data.nama_tamu"
:messages="messages"
@addMessage="addMessage"
/>
<!-- Thank You -->
<KhitanThankYou
v-if="currentSection === 'thanks'"
:childName="formData.nama_panggilan"
:jsonData="formData"
/>
</main>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRuntimeConfig } from '#app'
// ================== IMPORT KOMPONEN ==================
import KhitanA from '~/components/templates/khitan/KhitanA.vue'
import KhitanIntroduction from '~/components/templates/khitan/Introduction.vue'
import KhitanEvent from '~/components/templates/khitan/Event.vue'
import KhitanGallery from '~/components/templates/khitan/Gallery.vue'
import KhitanSay from '~/components/templates/khitan/GuestBook.vue'
import KhitanThankYou from '~/components/templates/khitan/ThankYou.vue'
// ================== PROPS ==================
const props = defineProps({
data: { type: Object, required: true }
})
// ================== BACKEND CONFIG ==================
const config = useRuntimeConfig()
const backendUrl = config.public.apiBaseUrl
// ================== FORM DATA ==================
const formData = computed(() => props.data.form || {})
// ================== GALERI ==================
const galleryImages = computed(() => {
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}`)
})
// ================== NAVIGASI SECTION ==================
const currentSection = ref('landing')
const switchSection = (s) => (currentSection.value = s)
// ================== MUSIK ==================
const isPlaying = ref(false)
const toggleMusic = () => (isPlaying.value = !isPlaying.value)
// ================== GUEST BOOK ==================
const messages = ref([])
const addMessage = (msg) => messages.value.push(msg)
// ================== STYLE NAV ==================
const navClass = (s) =>
currentSection.value === s
? 'text-blue-800 underline'
: 'hover:text-blue-700'
</script>
<style scoped>
/* Animasi transisi antar section */
main {
transition: all 0.7s ease-in-out;
}
</style>

View File

@ -0,0 +1,69 @@
<template>
<div>
<!-- Pastikan data sudah ada -->
<WeddingA v-if="props.data" :data="formattedData" />
<div v-else class="text-center py-20">
<p class="text-red-600 font-semibold">Undangan tidak ditemukan 😢</p>
</div>
</div>
</template>
<script setup>
import WeddingA from '~/components/templates/wedding/weddingA.vue'
// Props dari parent (/p/[code].vue)
const props = defineProps({
data: {
type: Object,
required: true
}
})
// Format data dari backend agar cocok dengan struktur WeddingA.vue
const formattedData = computed(() => {
const f = props.data.form || {}
return {
id: props.data.id,
coverImage: f.cover_image || '/default-cover.jpg',
heroImage: f.hero_image || '/default-hero.jpg',
greeting: f.say_something || 'Dengan memohon rahmat dan ridho Allah SWT, kami bermaksud mengundang Anda...',
eventDate: f.hari_tanggal_resepsi,
bride: {
fullname: f.nama_lengkap_wanita,
nickname: f.nama_panggilan_wanita,
photo: f.foto_wanita ? `http://localhost:8000/storage/${f.foto_wanita}` : '/wanita.jpg',
fatherName: f.nama_ayah_wanita,
motherName: f.nama_ibu_wanita,
instagram: f.instagram_wanita || ''
},
groom: {
fullname: f.nama_lengkap_pria,
nickname: f.nama_panggilan_pria,
photo: f.foto_pria ? `http://localhost:8000/storage/${f.foto_pria}` : '/pria.jpg',
fatherName: f.nama_ayah_pria,
motherName: f.nama_ibu_pria,
instagram: f.instagram_pria || ''
},
akad: {
date: f.hari_tanggal_akad,
time: f.waktu_akad,
place: f.tempat_akad,
address: f.alamat_akad,
mapUrl: f.link_gmaps_akad
},
resepsi: {
date: f.hari_tanggal_resepsi,
time: f.waktu_resepsi,
place: f.tempat_resepsi,
address: f.alamat_resepsi,
mapUrl: f.link_gmaps_resepsi
},
gallery: [1, 2, 3, 4, 5, 6]
.map(i => f[`foto_${i}`])
.filter(Boolean)
.map(x => `http://localhost:8000/storage/${x}`),
musicUrl: f.link_music || ''
}
})
</script>

View File

@ -0,0 +1,108 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-yellow-300 via-yellow-400 to-yellow-500 relative overflow-hidden">
<!-- Navigation -->
<nav v-if="currentSection !== 'landing'" class="absolute top-4 left-1/2 transform -translate-x-1/2 z-20">
<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('galeri')" :class="navClass('galeri')">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>
<!-- Tombol Musik -->
<div class="fixed bottom-4 left-4 z-30" v-if="currentSection !== 'landing'">
<button @click="toggleMusic" class="bg-orange-600 p-3 rounded-full text-white shadow-lg">
{{ isPlaying ? '⏸️' : '▶️' }}
</button>
</div>
<main
class="relative z-10 min-h-screen flex items-center justify-center p-4 transition-all duration-700 ease-in-out">
<!-- Landing -->
<Landing v-if="currentSection === 'landing'" :childName="formData.nama_panggilan"
:guestName="data.nama_tamu" @open-invitation="switchSection('introduction')" />
<!-- Introduction -->
<Introduction v-if="currentSection === 'introduction'" :age="formData.umur_yang_dirayakan"
:childName="formData.nama_lengkap" :childOrder="formData.anak_ke"
:parentsName="`${formData.nama_bapak} & ${formData.nama_ibu}`"
:childPhoto="`${backendUrl}/${formData.foto_1}`" />
<!-- Event -->
<Event 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" />
<!-- Gallery -->
<Gallery v-if="currentSection === 'galeri'" :images="galleryImages" />
<!-- Guest Book -->
<GuestBook v-if="currentSection === 'say'" :guestName="data.nama_tamu" :messages="messages"
@addMessage="addMessage" />
<!-- Thank You -->
<ThankYou v-if="currentSection === 'thanks'" :childName="formData.nama_panggilan" :jsonData="formData" />
</main>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { useRuntimeConfig } from '#app'
// Komponen
import Landing from '~/components/templates/Ultah/Landing.vue'
import Introduction from '~/components/templates/Ultah/Introduction.vue'
import Event from '~/components/templates/Ultah/Event.vue'
import Gallery from '~/components/templates/Ultah/Gallery.vue'
import GuestBook from '~/components/templates/Ultah/GuestBook.vue'
import ThankYou from '~/components/templates/Ultah/ThankYou.vue'
// Props dari halaman /p/[code].vue
const props = defineProps({
data: { type: Object, required: true }
})
// Runtime config
const config = useRuntimeConfig()
const backendUrl = config.public.apiBaseUrl
// Data form dari backend
const formData = computed(() => props.data.form || {})
// Gabungkan semua foto jadi array untuk galeri
const galleryImages = computed(() => {
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}`)
})
// Navigasi antar section
const currentSection = ref('landing')
const switchSection = (s) => (currentSection.value = s)
// Musik
const isPlaying = ref(false)
const toggleMusic = () => (isPlaying.value = !isPlaying.value)
// Buku tamu
const messages = ref([])
const addMessage = (msg) => messages.value.push(msg)
// Dummy countdown (bisa diganti dinamis nanti)
const countdown = ref({ days: 3, hours: 12, minutes: 45, seconds: 20 })
// Style untuk navigasi aktif
const navClass = (s) => (currentSection.value === s ? 'text-orange-700 underline' : 'hover:text-orange-600')
</script>

View File

@ -1,5 +1,7 @@
<template>
<div class="min-h-screen flex items-center justify-center bg-gray-100">
<div class="w-full min-h--screen bg-gray-100">
<!-- Loading State -->
<div v-if="pending" class="text-center">
<div class="animate-spin rounded-full h-12 w-12 border-t-4 border-blue-500 mx-auto"></div>
@ -8,12 +10,16 @@
<!-- Error State -->
<div v-else-if="error || !data" class="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
<svg class="w-12 h-12 mx-auto text-red-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
<svg class="w-12 h-12 mx-auto text-red-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z">
</path>
</svg>
<p class="text-lg font-semibold text-gray-800 mb-2">Undangan Tidak Ditemukan</p>
<p class="text-gray-600 mb-6">Maaf, undangan yang Anda cari tidak tersedia atau sudah tidak berlaku.</p>
<NuxtLink to="/" class="inline-block bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 transition-colors">
<NuxtLink to="/"
class="inline-block bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 transition-colors">
Kembali ke Beranda
</NuxtLink>
</div>
@ -50,7 +56,10 @@ const { data, pending, error } = await useAsyncData(
async () => {
try {
const response = await $fetch(`${backendUrl}/api/pelanggans/code/${route.params.code}`)
console.log('✅ API response:', response)
console.log('🧾 response.data:', response.data)
console.log('🧩 Template data:', response.data?.template)
console.log('🔖 Template slug:', response.data?.template?.slug)
// Check if the API response indicates failure
if (!response.success) {
throw createError({
@ -94,6 +103,12 @@ const { data, pending, error } = await useAsyncData(
const componentMap = {
'undangan-minimalis': defineAsyncComponent(() => import('~/components/undangan/undangan-minimalis.vue')),
'undangan-ulang-tahun-premium': defineAsyncComponent(() => import('~/components/undangan/undangan-ulang-tahun-premium.vue')),
'undangan-pernikahan-premium': defineAsyncComponent(() => import('~/components/undangan/undangan-pernikahan-premium.vue')),
'undangan-khitan-premium': defineAsyncComponent(() => import('~/components/undangan/undangan-khitan-premium.vue')),
// Add more mappings as templates are developed
}
@ -102,6 +117,14 @@ const dynamicComponent = computed(() => {
return componentMap[data.value.template.slug] || null
})
// === DEBUG WATCHER ===
watchEffect(() => {
if (data.value) {
console.log('📦 Template slug:', data.value?.template?.slug)
console.log('🧩 Dynamic component:', dynamicComponent.value)
}
})
// Set meta tags only if data exists
useHead(() => ({
title: data.value?.nama_pemesan || 'Undangan Tak Bernama',

View File

@ -5,7 +5,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import WeddingA from '~/components/templates/wedding/WeddingA.vue'
import WeddingA from '~/components/templates/wedding/weddingA.vue'
const route = useRoute()
const invitationData = ref(null)

View File

@ -31,5 +31,5 @@
</template>
<script setup>
import WeddingA from '~/components/templates/wedding/WeddingA.vue'
import WeddingA from '~/components/templates/wedding/weddingA.vue'
</script>