243 lines
9.8 KiB
Vue
243 lines
9.8 KiB
Vue
<template>
|
||
<section class="bg-gradient-to-b from-amber-50 to-white py-16 px-4 md:px-6 lg:px-8">
|
||
<!-- Full-width container -->
|
||
<div class="w-full">
|
||
<!-- Judul (tetap terpusat) -->
|
||
<div class="text-center mb-12">
|
||
<div class="flex items-center justify-center gap-2 mb-6">
|
||
<span class="h-[1px] w-12 bg-amber-400"></span>
|
||
<span class="text-amber-500 text-2xl">Flower</span>
|
||
<span class="h-[1px] w-12 bg-amber-400"></span>
|
||
</div>
|
||
<h2 class="text-3xl md:text-4xl font-bold text-amber-600">
|
||
Guest Book
|
||
</h2>
|
||
</div>
|
||
|
||
<!-- Layout Form + Ucapan (konten terpusat) -->
|
||
<div class="max-w-5xl mx-auto">
|
||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
|
||
|
||
<!-- Form -->
|
||
<div
|
||
class="bg-white shadow-xl rounded-2xl p-6 sm:p-8 border border-amber-100 order-2 md:order-1"
|
||
data-aos="fade-right"
|
||
>
|
||
<h3 class="text-xl font-semibold text-amber-600 mb-6 flex items-center gap-2">
|
||
<Icon name="mdi:message-text" class="text-2xl" />
|
||
Say Something!
|
||
</h3>
|
||
|
||
<form @submit.prevent="handleSubmit" class="space-y-5">
|
||
<!-- Nama -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">Your Name</label>
|
||
<input
|
||
v-model="form.name"
|
||
type="text"
|
||
placeholder="Enter your name"
|
||
required
|
||
maxlength="50"
|
||
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-400 focus:border-amber-400 focus:outline-none transition-all"
|
||
/>
|
||
</div>
|
||
|
||
<!-- Ucapan -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||
Your Message <span class="text-xs text-gray-500">({{ form.message.length }}/280)</span>
|
||
</label>
|
||
<textarea
|
||
v-model="form.message"
|
||
rows="4"
|
||
placeholder="Write your wishes and prayers..."
|
||
required
|
||
maxlength="280"
|
||
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-amber-400 focus:border-amber-400 focus:outline-none transition-all resize-none"
|
||
></textarea>
|
||
</div>
|
||
|
||
<!-- Attendance -->
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">Will you attend?</label>
|
||
<div class="flex gap-4 bg-amber-50 rounded-full p-2">
|
||
<label class="flex-1 flex items-center justify-center cursor-pointer">
|
||
<input
|
||
v-model="form.attendance"
|
||
type="radio"
|
||
value="yes"
|
||
class="hidden"
|
||
/>
|
||
<span :class="form.attendance === 'yes' ? 'bg-amber-500 text-white' : 'text-gray-700'"
|
||
class="px-4 py-2 rounded-full text-sm font-medium transition-all">
|
||
<Icon name="mdi:check-circle" class="inline mr-1 text-lg" /> Yes
|
||
</span>
|
||
</label>
|
||
<label class="flex-1 flex items-center justify-center cursor-pointer">
|
||
<input
|
||
v-model="form.attendance"
|
||
type="radio"
|
||
value="no"
|
||
class="hidden"
|
||
/>
|
||
<span :class="form.attendance === 'no' ? 'bg-red-500 text-white' : 'text-gray-700'"
|
||
class="px-4 py-2 rounded-full text-sm font-medium transition-all">
|
||
<Icon name="mdi:close-circle" class="inline mr-1 text-lg" /> No
|
||
</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tombol -->
|
||
<button
|
||
type="submit"
|
||
:disabled="isSubmitting"
|
||
class="w-full bg-amber-500 hover:bg-amber-600 text-white font-semibold py-3 px-6 rounded-lg transition duration-200 shadow-md disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
|
||
>
|
||
<Icon v-if="isSubmitting" name="mdi:loading" class="animate-spin" />
|
||
<span v-else><Icon name="mdi:send" class="inline" /> Send Message</span>
|
||
<span v-if="isSubmitting">Sending...</span>
|
||
</button>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Daftar Ucapan -->
|
||
<div
|
||
class="bg-white shadow-xl rounded-2xl p-6 sm:p-8 border border-amber-100 order-1 md:order-2"
|
||
data-aos="fade-left"
|
||
ref="messagesContainer"
|
||
>
|
||
<div class="flex items-center gap-2 mb-6">
|
||
<Icon name="mdi:chat-outline" class="text-amber-500 text-2xl" />
|
||
<span class="text-gray-700 font-semibold">{{ messages.length }} Messages</span>
|
||
</div>
|
||
|
||
<!-- List Ucapan dengan Scroll -->
|
||
<div class="space-y-4 max-h-96 overflow-y-auto pr-2 custom-scrollbar">
|
||
<template v-if="messages.length > 0">
|
||
<div
|
||
v-for="(msg, i) in messages"
|
||
:key="i"
|
||
class="p-4 bg-gradient-to-br from-amber-50 to-white rounded-xl border border-amber-100 hover:shadow-md transition-all slide-in"
|
||
>
|
||
<div class="flex items-start justify-between mb-2">
|
||
<div class="flex items-center gap-3">
|
||
<div class="w-9 h-9 bg-amber-400 rounded-full flex items-center justify-center text-white font-bold text-sm">
|
||
{{ msg.name.charAt(0).toUpperCase() }}
|
||
</div>
|
||
<div>
|
||
<p class="font-semibold text-gray-800 text-sm">{{ msg.name }}</p>
|
||
<p class="text-xs text-gray-500">{{ formatDate(msg.createdAt) }}</p>
|
||
</div>
|
||
</div>
|
||
<span
|
||
class="px-3 py-1 text-xs font-semibold rounded-full flex items-center gap-1"
|
||
:class="msg.attendance === 'yes' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
|
||
>
|
||
<Icon :name="msg.attendance === 'yes' ? 'mdi:check' : 'mdi:close'" class="text-sm" />
|
||
{{ msg.attendance === 'yes' ? 'Attending' : 'Can\'t attend' }}
|
||
</span>
|
||
</div>
|
||
<p class="text-sm text-gray-700 leading-relaxed ps-11">{{ msg.message }}</p>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Empty State -->
|
||
<div v-else class="text-center py-12">
|
||
<Icon name="mdi:message-outline" class="text-gray-300 text-6xl mb-4" />
|
||
<p class="text-gray-400 text-sm">No messages yet. Be the first to leave a message!</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, nextTick, watch } from 'vue'
|
||
|
||
const props = defineProps({
|
||
invitationId: { type: String, required: true }
|
||
})
|
||
const emit = defineEmits(['submitted'])
|
||
|
||
const form = ref({
|
||
name: '',
|
||
message: '',
|
||
attendance: 'yes'
|
||
})
|
||
|
||
const isSubmitting = ref(false)
|
||
const messagesContainer = ref(null)
|
||
|
||
const messages = ref([
|
||
{ name: 'Tia SMAN6BDG', message: 'Congratulations! Wishing you a lifetime of love!', attendance: 'yes', createdAt: new Date('2024-01-15') },
|
||
{ name: 'Muthia Rahma', message: 'Happy wedding! So happy for you both!', attendance: 'yes', createdAt: new Date('2024-01-14') },
|
||
{ name: 'Ahmad Fauzi', message: 'Barakallah! Semoga sakinah, mawaddah, warahmah', attendance: 'no', createdAt: new Date('2024-01-13') },
|
||
{ name: 'Sarah Amelia', message: 'Congrats! Can’t wait to celebrate!', attendance: 'yes', createdAt: new Date('2024-01-12') },
|
||
])
|
||
|
||
// Auto-scroll ke atas saat ada pesan baru
|
||
watch(messages, async () => {
|
||
await nextTick()
|
||
if (messagesContainer.value) {
|
||
messagesContainer.value.scrollTop = 0
|
||
}
|
||
}, { deep: true })
|
||
|
||
const handleSubmit = async () => {
|
||
if (!form.value.name.trim() || !form.value.message.trim()) {
|
||
alert('Please fill in name and message')
|
||
return
|
||
}
|
||
if (form.value.message.length > 280) {
|
||
alert('Message too long (max 280 characters)')
|
||
return
|
||
}
|
||
|
||
isSubmitting.value = true
|
||
|
||
try {
|
||
await new Promise(r => setTimeout(r, 800))
|
||
|
||
const newMsg = { ...form.value, createdAt: new Date() }
|
||
messages.value.unshift(newMsg)
|
||
emit('submitted', newMsg)
|
||
|
||
form.value = { name: '', message: '', attendance: 'yes' }
|
||
} catch (err) {
|
||
alert('Failed to send message')
|
||
} finally {
|
||
isSubmitting.value = false
|
||
}
|
||
}
|
||
|
||
const formatDate = (date) => {
|
||
const d = new Date(date)
|
||
const now = new Date()
|
||
const diffMins = Math.floor((now - d) / 60000)
|
||
const diffHours = Math.floor(diffMins / 60)
|
||
const diffDays = Math.floor(diffHours / 24)
|
||
|
||
if (diffMins < 1) return 'Just now'
|
||
if (diffMins < 60) return `${diffMins}m ago`
|
||
if (diffHours < 24) return `${diffHours}h ago`
|
||
if (diffDays < 7) return `${diffDays}d ago`
|
||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.custom-scrollbar::-webkit-scrollbar { width: 6px; }
|
||
.custom-scrollbar::-webkit-scrollbar-track { background: #fef3c7; border-radius: 10px; }
|
||
.custom-scrollbar::-webkit-scrollbar-thumb { background: #f59e0b; border-radius: 10px; }
|
||
.custom-scrollbar::-webkit-scrollbar-thumb:hover { background: #d97706; }
|
||
|
||
.slide-in { animation: slideIn 0.4s ease-out; }
|
||
@keyframes slideIn {
|
||
from { opacity: 0; transform: translateY(-10px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
</style> |