update khitan premium template
This commit is contained in:
parent
1987f882dd
commit
cf35361085
@ -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>
|
||||
@ -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>
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
Loading…
Reference in New Issue
Block a user