tes design template
This commit is contained in:
parent
9faed9a803
commit
9be255c4d8
@ -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:
|
||||
</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>
|
||||
<p class="text-orange-800 text-lg md:text-xl mb-6">
|
||||
Bertempat di:<br>
|
||||
Jl. Andara Raya No.123, Jakarta Selatan
|
||||
</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>
|
||||
|
||||
<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-sm font-bold text-orange-800 uppercase">
|
||||
{{ eventDayName || 'TANGGAL TIDAK VALID' }}<br />
|
||||
{{ eventMonthYear || '' }}
|
||||
</p>
|
||||
</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>
|
||||
<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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
Terima Kasih
|
||||
</h1>
|
||||
<p class="text-orange-800 text-lg md:text-xl mb-6">
|
||||
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>
|
||||
<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-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-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>
|
||||
|
||||
@ -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>
|
||||
@ -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({
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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',
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -31,5 +31,5 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import WeddingA from '~/components/templates/wedding/WeddingA.vue'
|
||||
import WeddingA from '~/components/templates/wedding/weddingA.vue'
|
||||
</script>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user