update khitan premium template

This commit is contained in:
Farhaan4 2025-10-28 16:19:53 +07:00
parent 1987f882dd
commit cf35361085
4 changed files with 340 additions and 430 deletions

View File

@ -1,79 +1,40 @@
<template>
<!-- 🌌 Latar belakang fullscreen -->
<section
class="relative min-h-screen w-full flex flex-col items-center justify-center text-center overflow-hidden bg-gradient-to-br from-indigo-900 via-purple-900 to-blue-900 text-white p-6">
<!-- 🌟 Bintang Berkedip -->
<div class="absolute inset-0 overflow-hidden z-0">
<div v-for="(star, i) in stars" :key="i" class="absolute bg-white rounded-full animate-twinkle" :style="{
top: star.top + '%',
left: star.left + '%',
width: star.size + 'px',
height: star.size + 'px',
animationDelay: star.delay + 's',
opacity: star.opacity
}"></div>
</div>
<section class="w-full max-w-4xl mx-auto text-center">
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-8">
Buku Tamu
</h1>
<!-- 🌈 Cahaya lembut -->
<div
class="absolute w-[600px] h-[600px] bg-blue-400/10 blur-[200px] rounded-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2">
</div>
<!-- 💬 Konten utama -->
<div class="relative z-10 max-w-lg w-full space-y-6 animate-fade-in">
<h2 class="text-4xl font-extrabold text-yellow-300 drop-shadow-[0_0_20px_rgba(255,255,255,0.3)]">
💬 Buku Tamu
</h2>
<p class="text-blue-100">{{ saySomething }}</p>
<!-- 📝 Form -->
<form @submit.prevent="submitMessage"
class="bg-white/10 backdrop-blur-md border border-white/20 p-6 rounded-2xl shadow-lg space-y-4">
<input v-model="form.name" type="text" placeholder="Nama Anda"
class="w-full px-4 py-2 rounded-lg bg-white/20 text-white placeholder-gray-300 focus:ring-2 focus:ring-yellow-400 outline-none"
required />
<textarea v-model="form.message" rows="3" placeholder="Tulis ucapan Anda..."
class="w-full px-4 py-2 rounded-lg bg-white/20 text-white placeholder-gray-300 focus:ring-2 focus:ring-yellow-400 outline-none"
required></textarea>
<select v-model="form.attendance" class="w-full px-4 py-3 rounded-xl border-2 border-orange-400
focus:ring-2 focus:ring-orange-500 focus:border-orange-500
bg-gradient-to-r from-yellow-50 to-orange-50 text-gray-800
appearance-none transition-all duration-300 cursor-pointer">
<option value="hadir" class="bg-white text-gray-800"> Hadir</option>
<option value="tidak_hadir" class="bg-white text-gray-800"> Tidak Hadir</option>
<option value="mungkin" class="bg-white text-gray-800">🤔 Mungkin</option>
<!-- Form -->
<div class="bg-yellow-300/60 rounded-3xl p-6 shadow-xl mb-8">
<form @submit.prevent="submitMessage" class="space-y-4">
<input v-model="form.name" type="text" placeholder="Nama"
class="w-full px-4 py-2 rounded-xl border-2 border-orange-400 focus:ring-2 focus:ring-orange-500">
<textarea v-model="form.message" placeholder="Ucapan"
class="w-full px-4 py-2 rounded-xl border-2 border-orange-400 focus:ring-2 focus:ring-orange-500"></textarea>
<select v-model="form.attendance"
class="w-full px-4 py-2 rounded-xl border-2 border-orange-400 focus:ring-2 focus:ring-orange-500">
<option value="hadir">Hadir</option>
<option value="tidak_hadir">Tidak Hadir</option>
<option value="mungkin">Mungkin</option>
</select>
<button type="submit"
class="bg-yellow-400 text-gray-900 font-semibold px-6 py-2.5 rounded-full shadow-lg hover:bg-yellow-500 transition-all duration-300 transform hover:scale-105">
Kirim
class="bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-xl font-semibold shadow-lg">
Kirim
</button>
</form>
<!-- 💌 Pesan -->
<div v-if="messages.length" class="mt-10 space-y-4 text-left max-h-[300px] overflow-y-auto">
<transition-group name="fade" tag="div">
<div class="space-y-4">
<div v-for="msg in messages" :key="msg.id" class="bg-white rounded-xl shadow-md p-4 text-left">
<div class="flex items-center justify-between mb-2">
<span class="font-bold text-orange-800">{{ msg.nama }}</span>
<span :class="getAttendanceClass(msg.status_kehadiran)" class="text-sm px-2 py-1 rounded-lg">
{{ formatAttendance(msg.status_kehadiran) }}
</span>
</div>
<p class="text-gray-700">{{ msg.pesan }}</p>
</div>
</div>
</transition-group>
</div>
</div>
<!-- 🌠 Ornamen bawah -->
<div
class="absolute bottom-0 left-0 right-0 h-[200px] bg-gradient-to-t from-indigo-900 via-purple-800/30 to-transparent">
<!-- Messages -->
<div class="space-y-4">
<div v-for="msg in messages" :key="msg.id" class="bg-white rounded-xl shadow-md p-4 text-left">
<div class="flex items-center justify-between mb-2">
<span class="font-bold text-orange-800">{{ msg.nama }}</span>
<span :class="getAttendanceClass(msg.status_kehadiran)" class="text-sm px-2 py-1 rounded-lg">
{{ formatAttendance(msg.status_kehadiran) }}
</span>
</div>
<p class="text-gray-700">{{ msg.pesan }}</p>
</div>
</div>
</section>
</template>
@ -96,14 +57,11 @@ const props = defineProps({
}
})
const messages = ref([...props.messages])
const messages = ref([...props.messages])
const form = ref({ name: '', message: '', attendance: 'hadir' });
onMounted(() => {
messages.value = props.messages || []
})
// Fungsi untuk mengirim data RSVP ke backend
const submitMessage = async () => {
if (!form.value.name || !form.value.message) {
@ -111,12 +69,15 @@ const submitMessage = async () => {
return;
}
const payload = {
nama: form.value.name,
pesan: form.value.message,
status_kehadiran: form.value.attendance,
}
try {
console.log('POST to:', `${backendUrl}/api/rsvp/${invitationCode}`, {
nama: form.value.name,
pesan: form.value.message,
status_kehadiran: form.value.attendance,
});
console.log('POST to:', `${backendUrl}/api/rsvp/${invitationCode}`, payload)
const response = await fetch(`${backendUrl}/api/rsvp/${invitationCode}`, {
method: 'POST',
headers: {
@ -125,18 +86,20 @@ const submitMessage = async () => {
body: JSON.stringify({
nama: form.value.name,
pesan: form.value.message,
status_kehadiran: form.value.attendance,
status_kehadiran: form.value.attendance,
}),
});
if (!response.ok) {
throw new Error(`Gagal menyimpan RSVP`);
if (!response.ok) {
console.log(response)
throw new Error(`Gagal menyimpan RSVP,${response.errors}`);
}
const result = await response.json();
messages.value.push(result.data);
messages.value.push(result.data); // Tambahkan RSVP baru ke daftar
form.value = { name: '', message: '', attendance: 'hadir' };
alert(result.message);
alert(result.message); // Tampilkan pesan sukses dari backend
} catch (error) {
console.error('Error menyimpan RSVP:', error);
alert('Terjadi kesalahan saat menyimpan RSVP.');
@ -146,9 +109,9 @@ const submitMessage = async () => {
// Fungsi untuk mengatur kelas CSS berdasarkan status kehadiran
const getAttendanceClass = (attendance) => {
switch (attendance) {
case 'hadir':
case 'iya':
return 'bg-green-100 text-green-700';
case 'tidak_hadir':
case 'tidak':
return 'bg-red-100 text-red-700';
case 'mungkin':
return 'bg-yellow-100 text-yellow-700';
@ -160,9 +123,9 @@ const getAttendanceClass = (attendance) => {
// Fungsi untuk memformat teks kehadiran
const formatAttendance = (attendance) => {
switch (attendance) {
case 'hadir':
case 'iya':
return 'Hadir';
case 'tidak_hadir':
case 'tidak':
return 'Tidak Hadir';
case 'mungkin':
return 'Mungkin';
@ -170,63 +133,4 @@ const formatAttendance = (attendance) => {
return attendance;
}
};
</script>
<style scoped>
/* 🌟 Animasi bintang berkedip */
@keyframes twinkle {
0%,
100% {
opacity: 0.3;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.3);
}
}
.animate-twinkle {
animation: twinkle 3.5s ease-in-out infinite;
}
/* ✨ Fade animasi */
@keyframes fade-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fade-in 1.2s ease-out;
}
/* Pesan baru fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.4s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* Scrollbar halus */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 9999px;
}
</style>
</script>

View File

@ -101,7 +101,7 @@
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-yellow-400/50 scrollbar-track-transparent">
<div class="space-y-4">
<!-- Sample Messages -->
<!-- Messages from Backend -->
<div
v-for="message in messages"
:key="message.id"
@ -111,28 +111,26 @@
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<span class="text-blue-900 text-sm font-bold">
{{ message.name.charAt(0).toUpperCase() }}
{{ message.nama.charAt(0).toUpperCase() }}
</span>
</div>
<div>
<h4 class="text-yellow-400 font-semibold">{{ message.name }}</h4>
<p class="text-white/60 text-xs">{{ message.time }}</p>
<h4 class="text-yellow-400 font-semibold">{{ message.nama }}</h4>
<p class="text-white/60 text-xs">{{ formatTime(message.created_at) }}</p>
</div>
</div>
<span
:class="[
'px-2 py-1 rounded-full text-xs font-medium',
message.attendance === 'yes'
? 'bg-green-500/20 text-green-400 border border-green-400/50'
: message.attendance === 'maybe'
? 'bg-yellow-500/20 text-yellow-400 border border-yellow-400/50'
: 'bg-red-500/20 text-red-400 border border-red-400/50'
]"
:class="getAttendanceBadge(message.status_kehadiran)"
>
{{ message.attendance === 'yes' ? 'Attend' : message.attendance === 'maybe' ? 'Maybe' : 'Can\'t' }}
{{ formatAttendanceLabel(message.status_kehadiran) }}
</span>
</div>
<p class="text-white text-sm leading-relaxed">{{ message.message }}</p>
<p class="text-white text-sm leading-relaxed">{{ message.pesan }}</p>
</div>
<!-- Empty State -->
<div v-if="!messages.length" class="text-center py-8 text-white/50">
<p>Belum ada ucapan...</p>
</div>
</div>
@ -177,126 +175,143 @@
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute, useRuntimeConfig } from '#app'
// Config & Route
const route = useRoute()
const config = useRuntimeConfig()
const backendUrl = config.public.apiBaseUrl
const invitationCode = route.params.code || '';
const guest = route.query.code
// Props
const props = defineProps({
data: {
type: Object,
default: () => ({})
messages: {
type: Array,
required: false,
default: () => []
}
})
// Reactive data
// Reactive State
const form = ref({
name: '',
message: '',
attendance: 'yes'
attendance: 'hadir'
})
const isSubmitting = ref(false)
const showSuccess = ref(false)
const attendanceOptions = [
{ value: 'yes', label: 'Yes' },
{ value: 'maybe', label: 'Maybe' },
{ value: 'no', label: 'No' }
{ value: 'hadir', label: 'Hadir' },
{ value: 'mungkin', label: 'Mungkin' },
{ value: 'tidak_hadir', label: 'Tidak Hadir' }
]
// Sample messages - replace with actual data from backend
const messages = ref([
{
id: 1,
name: 'HWD FRENDSS ^^',
message: 'Selamat ya! Semoga menjadi anak yang sholeh dan berbakti kepada orang tua.',
attendance: 'yes',
time: '2 hours ago'
},
{
id: 2,
name: 'Tio SMAN6BDG',
message: 'Congratsss!',
attendance: 'yes',
time: '3 hours ago'
},
{
id: 3,
name: 'Muthia Rahma',
message: 'Happy wedd my sisstaa!',
attendance: 'maybe',
time: '5 hours ago'
},
{
id: 4,
name: 'Alia Sena P',
message: 'Barakallahu fiikum. Semoga menjadi keluarga yang sakinah mawadah wa rahmah.',
attendance: 'yes',
time: '6 hours ago'
},
{
id: 5,
name: 'Ahmad Rizki',
message: 'Selamat menjalani sunnah Rasul! Semoga Allah senantiasa melindungi.',
attendance: 'yes',
time: '1 day ago'
},
{
id: 6,
name: 'Siti Aminah',
message: 'Alhamdulillah, semoga menjadi awal yang baik untuk masa depan yang cerah.',
attendance: 'maybe',
time: '1 day ago'
}
])
const messages = ref([])
// Methods
const submitMessage = async () => {
if (!form.value.name.trim() || !form.value.message.trim()) {
return
// === HELPER FUNCTIONS ===
const formatTime = (timestamp) => {
if (!timestamp) return 'Baru saja'
const now = new Date()
const date = new Date(timestamp)
const diffMs = now - date
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMs / 3600000)
const diffDays = Math.floor(diffMs / 86400000)
if (diffMins < 1) return 'Baru saja'
if (diffMins < 60) return `${diffMins} menit lalu`
if (diffHours < 24) return `${diffHours} jam lalu`
if (diffDays < 7) return `${diffDays} hari lalu`
return date.toLocaleDateString('id-ID')
}
const getAttendanceBadge = (status) => {
const map = {
'hadir': 'px-2 py-1 rounded-full text-xs font-medium bg-green-500/20 text-green-400 border border-green-400/50',
'mungkin': 'px-2 py-1 rounded-full text-xs font-medium bg-yellow-500/20 text-yellow-400 border border-yellow-400/50',
'tidak_hadir': 'px-2 py-1 rounded-full text-xs font-medium bg-red-500/20 text-red-400 border border-red-400/50'
}
isSubmitting.value = true
return map[status] || 'px-2 py-1 rounded-full text-xs font-medium bg-gray-500/20 text-gray-400 border border-gray-400/50'
}
const formatAttendanceLabel = (status) => {
const map = {
'hadir': 'Hadir',
'mungkin': 'Mungkin',
'tidak_hadir': 'Tidak Hadir'
}
return map[status] || status
}
// === TAMBAHKAN FUNGSI FETCH ===
const fetchMessages = async () => {
if (!invitationCode) return
try {
// Simulate API call - replace with actual backend integration
await new Promise(resolve => setTimeout(resolve, 1500))
// Add new message to the list (in real implementation, this would come from backend)
const newMessage = {
id: Date.now(),
name: form.value.name,
message: form.value.message,
attendance: form.value.attendance,
time: 'Just now'
const response = await fetch(`${backendUrl}/api/rsvp/${invitationCode}`)
if (response.ok) {
const result = await response.json()
messages.value = result.data || []
}
messages.value.unshift(newMessage)
// Reset form
form.value = {
name: '',
message: '',
attendance: 'yes'
}
// Show success message
showSuccess.value = true
setTimeout(() => {
showSuccess.value = false
}, 3000)
} catch (error) {
console.error('Error submitting message:', error)
} finally {
isSubmitting.value = false
console.error('Gagal mengambil pesan:', error)
}
}
// Lifecycle
onMounted(() => {
// Load messages from props if available
if (props.data?.messages && props.data.messages.length > 0) {
// Fungsi untuk mengirim data RSVP ke backend
const submitMessage = async () => {
if (!form.value.name || !form.value.message) {
alert('Nama dan ucapan harus diisi!');
return;
}
try {
console.log('POST to:', `${backendUrl}/api/rsvp/${invitationCode}`, {
nama: form.value.name,
pesan: form.value.message,
status_kehadiran: form.value.attendance,
});
const response = await fetch(`${backendUrl}/api/rsvp/${invitationCode}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
nama: form.value.name,
pesan: form.value.message,
status_kehadiran: form.value.attendance,
}),
});
if (!response.ok) {
throw new Error(`Gagal menyimpan RSVP`);
}
const result = await response.json();
messages.value.push(result.data);
form.value = { name: '', message: '', attendance: 'hadir' };
alert(result.message);
} catch (error) {
console.error('Error menyimpan RSVP:', error);
alert('Terjadi kesalahan saat menyimpan RSVP.');
}
};
// === LIFECYCLE ===
onMounted(async () => {
// Gunakan props dulu (SSR)
if (props.data?.messages && Array.isArray(props.data.messages)) {
messages.value = props.data.messages
}
// Selalu fetch dari API
await fetchMessages()
})
</script>
@ -308,56 +323,22 @@ onMounted(() => {
/* Animations */
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
from { opacity: 0; transform: translateY(-30px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
from { opacity: 0; transform: translateX(-30px); }
to { opacity: 1; transform: translateX(0); }
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
.animate-fade-in-down {
animation: fadeInDown 0.8s ease-out forwards;
}
.animate-fade-in-left {
animation: fadeInLeft 0.8s ease-out forwards;
}
.animate-fade-in-right {
animation: fadeInRight 0.8s ease-out forwards;
}
/* Animation Delays */
.animation-delay-300 {
animation-delay: 0.3s;
opacity: 0;
animation-fill-mode: forwards;
}
.animate-fade-in-down { animation: fadeInDown 0.8s ease-out forwards; }
.animate-fade-in-left { animation: fadeInLeft 0.8s ease-out forwards; }
.animate-fade-in-right { animation: fadeInRight 0.8s ease-out forwards; }
.animation-delay-300 { animation-delay: 0.3s; opacity: 0; animation-fill-mode: forwards; }
/* Custom fonts */
.font-script {
@ -366,86 +347,31 @@ onMounted(() => {
}
/* Custom Scrollbar */
.scrollbar-thin {
scrollbar-width: thin;
}
.scrollbar-thin::-webkit-scrollbar {
width: 4px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin { scrollbar-width: thin; }
.scrollbar-thin::-webkit-scrollbar { width: 4px; }
.scrollbar-thin::-webkit-scrollbar-track { background: transparent; }
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgba(251, 191, 36, 0.5);
border-radius: 2px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: rgba(251, 191, 36, 0.7);
}
/* Input Focus States */
input:focus,
textarea:focus {
outline: none;
}
/* Responsive */
/* Responsive Fixes */
@media (max-width: 1024px) {
.grid-cols-1.lg\:grid-cols-2 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.max-h-\[600px\] {
max-height: none;
}
.grid-cols-1.lg\:grid-cols-2 { grid-template-columns: 1fr; }
.max-h-\[600px\] { max-height: none; }
}
@media (max-width: 640px) {
.text-4xl.md\:text-5xl {
font-size: 2rem;
}
.px-8 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-8 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.space-x-2 > * + * {
margin-left: 0.25rem;
}
.px-6 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.flex.space-x-2 {
flex-wrap: wrap;
gap: 0.5rem;
}
.flex.space-x-2 > * {
margin-left: 0;
}
.text-4xl.md\:text-5xl { font-size: 2rem; }
.px-8 { padding: 1rem; }
.py-8 { padding: 1rem 0; }
.flex.space-x-2 { flex-wrap: wrap; gap: 0.5rem; }
.px-6 { padding-left: 0.75rem; padding-right: 0.75rem; }
}
@media (max-width: 480px) {
.grid {
gap: 1rem;
}
.px-4 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.grid { gap: 1rem; }
.px-4 { padding-left: 0.75rem; padding-right: 0.75rem; }
}
</style>

View File

@ -59,8 +59,14 @@
<div class="mb-12 animate-fade-in-up animation-delay-600">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 mb-6 border border-yellow-400/30">
<p class="text-yellow-400 text-lg mb-2">{{ data?.guestTitle || 'Kepada Yth.' }}</p>
<p class="text-white text-xl font-semibold">{{ data?.guestName || '' }}</p>
<div
class="bg-white/10 border border-white/20 backdrop-blur-md py-3 px-8 rounded-full text-blue-100 shadow-lg mt-2"
>
Kepada Yth.
<span class="font-semibold text-yellow-300">{{
guestName || 'Tamu Undangan'
}}</span>
</div>
</div>
</div>
@ -104,12 +110,10 @@
<script setup>
import { onMounted, ref } from 'vue'
// Props
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
defineProps({
childName: String,
guestName: String,
musicUrl: { type: String, default: '' }
})
// Emits

View File

@ -11,17 +11,40 @@
<ul
class="flex space-x-6 bg-white/40 backdrop-blur-md px-6 py-3 rounded-full shadow-md text-sm font-semibold text-gray-800"
>
<li><button @click="switchSection('introduction')" :class="navClass('introduction')">Intro</button></li>
<li><button @click="switchSection('event')" :class="navClass('event')">Event</button></li>
<li><button @click="switchSection('gallery')" :class="navClass('gallery')">Gallery</button></li>
<li><button @click="switchSection('say')" :class="navClass('say')">Guest Book</button></li>
<li><button @click="switchSection('thanks')" :class="navClass('thanks')">Thanks</button></li>
<li>
<button @click="switchSection('introduction')" :class="navClass('introduction')">
Intro
</button>
</li>
<li>
<button @click="switchSection('event')" :class="navClass('event')">
Event
</button>
</li>
<li>
<button @click="switchSection('gallery')" :class="navClass('gallery')">
Gallery
</button>
</li>
<li>
<button @click="switchSection('say')" :class="navClass('say')">
Guest Book
</button>
</li>
<li>
<button @click="switchSection('thanks')" :class="navClass('thanks')">
Thanks
</button>
</li>
</ul>
</nav>
<!-- ================= 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">
<div class="fixed bottom-4 left-4 z-30" v-if="currentSection !== 'landing' && musicUrl">
<button
@click="toggleMusic"
class="bg-blue-600 p-3 rounded-full text-white shadow-lg hover:bg-blue-700 transition"
>
{{ isPlaying ? '⏸️' : '▶️' }}
</button>
<audio ref="audioPlayer" :src="musicUrl" loop></audio>
@ -31,18 +54,21 @@
<main
class="relative z-10 min-h-screen flex items-center justify-center p-4 transition-all duration-700 ease-in-out"
>
<!-- Landing -->
<KhitanA
v-if="currentSection === 'landing'"
:childName="formData.nama_panggilan"
:guestName="data.nama_tamu"
@next-page="switchSection('introduction')"
:guestName="guestName"
@next-page="goToIntroduction"
/>
<!-- Introduction -->
<KhitanIntroduction
v-if="currentSection === 'introduction'"
:form="formData"
/>
<!-- Event -->
<KhitanEvent
v-if="currentSection === 'event'"
:hari_tanggal_acara="formData.hari_tanggal_acara"
@ -57,18 +83,21 @@
:hitung_mundur_selesai="formData.hitung_mundur_selesai"
/>
<!-- Gallery -->
<KhitanGallery
v-if="currentSection === 'gallery'"
:images="galleryImages"
/>
<!-- Guest Book -->
<KhitanSay
v-if="currentSection === 'say'"
:guestName="data.nama_tamu"
:guestName="guestName"
:messages="messages"
@addMessage="addMessage"
/>
<!-- Thank You -->
<KhitanThankYou
v-if="currentSection === 'thanks'"
:childName="formData.nama_panggilan"
@ -79,7 +108,7 @@
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, nextTick, onMounted } from 'vue'
import { useRuntimeConfig } from '#app'
import KhitanA from '~/components/templates/khitan/KhitanA.vue'
import KhitanIntroduction from '~/components/templates/khitan/Introduction.vue'
@ -89,81 +118,128 @@ import KhitanSay from '~/components/templates/khitan/GuestBook.vue'
import KhitanThankYou from '~/components/templates/khitan/ThankYou.vue'
const props = defineProps({
data: { type: Object, required: true }
data: { type: Object, required: true },
tamu: { type: Object, required: true }
})
const config = useRuntimeConfig()
const backendUrl = config.public.apiBaseUrl
// Form Data
const formData = computed(() => props.data.form || {})
// Guest Name (dengan fallback)
const guestName = computed(() => props.tamu?.nama_tamu || 'Tamu Undangan')
// Gallery Images - fix logic
const galleryImages = computed(() => {
const f = formData.value.foto
if (Array.isArray(f)) {
return f.map(img => `${backendUrl}/storage/${img}`)
const fotos = formData.value.foto
const base = `${backendUrl}/storage/`
if (Array.isArray(fotos)) {
return fotos.filter(Boolean).map(img => `${base}${img}`)
}
return [
formData.value.foto_1,
formData.value.foto_2,
formData.value.foto_3,
formData.value.foto_4,
formData.value.foto_5
].filter(Boolean).map(img => `${backendUrl}/${img}`)
]
.filter(Boolean)
.map(img => `${backendUrl}/${img}`)
})
// Messages (HANYA SATU DEFINISI!)
const messages = ref(props.data.rsvp || [])
// Current Section
const currentSection = ref('landing')
const switchSection = (s) => (currentSection.value = s)
const audioPlayer = ref(null)
const isPlaying = ref(false)
const musicUrl = computed(() =>
formData.value.link_music ? `${backendUrl}/${formData.value.link_music}` : ''
)
const toggleMusic = () => {
if (!audioPlayer.value) return
if (isPlaying.value) {
audioPlayer.value.pause()
} else {
audioPlayer.value.play()
// Switch Section
const switchSection = (section) => {
currentSection.value = section
if (section === 'introduction') {
startMusicAuto() // Auto play saat masuk intro
}
isPlaying.value = !isPlaying.value
}
const messages = ref([])
const addMessage = (msg) => messages.value.push(msg)
const goToIntroduction = () => {
switchSection('introduction')
}
const navClass = (s) =>
currentSection.value === s
? 'text-blue-800 underline'
: 'hover:text-blue-700'
// Music
const audioPlayer = ref(null)
const isPlaying = ref(false)
const musicUrl = computed(() => {
const link = formData.value.link_music
return link ? `${backendUrl}/${link}` : ''
})
const toggleMusic = async () => {
if (!audioPlayer.value || !musicUrl.value) return
try {
if (isPlaying.value) {
audioPlayer.value.pause()
} else {
await audioPlayer.value.play()
}
isPlaying.value = !isPlaying.value
} catch (err) {
console.warn('Autoplay dicegah browser:', err)
isPlaying.value = false
}
}
const startMusicAuto = async () => {
if (!musicUrl.value || !audioPlayer.value || isPlaying.value) return
await nextTick() // Tunggu audio ter-mount
try {
await audioPlayer.value.play()
isPlaying.value = true
} catch (err) {
console.warn('Autoplay dicegah. User harus interaksi dulu.')
}
}
// Add Message
const addMessage = (msg) => {
messages.value.push(msg)
}
// Nav Active Class
const navClass = (section) =>
currentSection.value === section
? 'text-blue-800 underline decoration-2 underline-offset-4'
: 'hover:text-blue-700 transition'
// Optional: Auto play saat masuk intro (setelah user klik landing)
onMounted(() => {
if (currentSection.value === 'introduction') {
startMusicAuto()
}
})
</script>
<style scoped>
/* Pastikan benar-benar fullscreen */
html,
body,
:host,
div[min-h-screen],
.min-h-screen {
width: 100%;
/* Fullscreen Fix */
html, body, #__nuxt, [data-v-app] {
height: 100%;
margin: 0;
padding: 0;
}
/* Hilangkan white space akibat scroll */
body {
overflow-x: hidden;
background: transparent;
}
/* Pastikan gradient menutup seluruh layar */
div[min-h-screen] {
position: relative;
inset: 0;
.min-h-screen {
min-height: 100vh;
}
/* Transisi section tetap */
/* Smooth transition */
main {
transition: all 0.7s ease-in-out;
transition: all 0.7s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>
</style>