Merge branch 'baru' of https://git.abbauf.com/Magang-2025/Undangan into baru
This commit is contained in:
commit
e9c33e0fc9
@ -1,27 +1,191 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="w-full max-w-6xl mx-auto text-center">
|
<section
|
||||||
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-8">
|
class="w-full bg-gradient-to-b from-yellow-300 to-yellow-100 py-12 px-4 text-center relative overflow-hidden">
|
||||||
Acara Ulang Tahun
|
<!-- Judul -->
|
||||||
|
<h1 class="text-3xl md:text-4xl font-extrabold text-orange-700 mb-10">
|
||||||
|
🎉 BIRTHDAY PARTY 🎉
|
||||||
</h1>
|
</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">
|
<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">
|
||||||
Insya Allah akan dilaksanakan pada:
|
<!-- Kolom Kiri - Google Map -->
|
||||||
</p>
|
<div class="flex flex-col items-center justify-center">
|
||||||
<p class="text-orange-700 text-2xl md:text-3xl font-bold mb-6">
|
<iframe v-if="mapEmbed" class="w-full h-72 rounded-2xl shadow-lg" :src="mapEmbed" allowfullscreen
|
||||||
Selasa, 11 Juni 2025<br> Pukul 11.00 WIB
|
loading="lazy"></iframe>
|
||||||
</p>
|
<button
|
||||||
<p class="text-orange-800 text-lg md:text-xl mb-6">
|
class="mt-4 bg-orange-700 hover:bg-orange-800 text-white font-semibold py-2 px-6 rounded-full shadow-md transition"
|
||||||
Bertempat di:<br>
|
@click="openMap">
|
||||||
Jl. Andara Raya No.123, Jakarta Selatan
|
Direction
|
||||||
</p>
|
</button>
|
||||||
<div class="mt-6">
|
</div>
|
||||||
<iframe
|
|
||||||
class="w-full h-64 rounded-2xl shadow-lg"
|
<!-- Kolom Kanan - Detail Acara -->
|
||||||
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3..."
|
<div class="text-center">
|
||||||
allowfullscreen
|
<h2 class="text-2xl md:text-3xl font-extrabold text-blue-900 mb-4">
|
||||||
loading="lazy">
|
BIRTHDAY PARTY
|
||||||
</iframe>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Gambar di bawah -->
|
||||||
|
<img src="/ABBAUF.png" alt="Logo" class="mx-auto mt-10 w-56 md:w-64" />
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</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>
|
<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">
|
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-8">
|
||||||
Galeri Foto
|
Galeri Foto
|
||||||
</h1>
|
</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">
|
<div v-if="images && images.length" class="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
<img src="/logo2.png" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
|
<img
|
||||||
<img src="/pria.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
|
v-for="(img, index) in images"
|
||||||
<img src="/wanita.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
|
:key="index"
|
||||||
<img src="/templat.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
|
:src="img"
|
||||||
<img src="/iphone.png" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
|
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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</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>
|
<script setup>
|
||||||
defineProps({
|
defineProps({
|
||||||
age: Number,
|
age: [Number,String],
|
||||||
childName: String,
|
childName: String,
|
||||||
childOrder: Number,
|
childOrder: [Number,String],
|
||||||
parentNames: String,
|
parentNames: String,
|
||||||
childPhoto: String
|
childPhoto: [String, Array]
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,20 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<section class="w-full max-w-3xl mx-auto text-center">
|
<section
|
||||||
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-6">
|
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"
|
||||||
Terima Kasih
|
>
|
||||||
</h1>
|
<!-- Background Ornamen -->
|
||||||
<p class="text-orange-800 text-lg md:text-xl mb-6">
|
<div class="absolute inset-0 opacity-10 pointer-events-none">
|
||||||
Kehadiran dan doa restu Bapak/Ibu/Saudara/i merupakan kebahagiaan besar bagi kami.
|
<div class="absolute top-10 left-10 w-24 h-24 bg-orange-500 rounded-full blur-2xl"></div>
|
||||||
</p>
|
<div class="absolute bottom-10 right-10 w-32 h-32 bg-yellow-600 rounded-full blur-3xl"></div>
|
||||||
<div class="bg-yellow-300/60 rounded-3xl p-6 shadow-xl">
|
</div>
|
||||||
<p class="text-orange-700 font-bold text-xl">Raffi Ahmad & Nagita Slavina</p>
|
|
||||||
<p class="text-orange-800">Orang Tua {{ childName }}</p>
|
<!-- 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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
defineProps({
|
defineProps({
|
||||||
childName: String
|
jsonData: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<div class="min-h-screen bg-gradient-to-br from-yellow-300 via-yellow-400 to-yellow-500 relative overflow-hidden">
|
<div class="space-y-24">
|
||||||
<!-- Background Pattern -->
|
<Introduction
|
||||||
<div class="absolute inset-0 opacity-10">
|
:age="age"
|
||||||
<div class="absolute top-10 left-10 w-20 h-20 bg-yellow-600 rounded-full"></div>
|
:childName="childName"
|
||||||
<div class="absolute top-32 right-20 w-16 h-16 bg-yellow-600 rounded-full"></div>
|
:childOrder="childOrder"
|
||||||
<div class="absolute bottom-20 left-20 w-12 h-12 bg-yellow-600 rounded-full"></div>
|
:parentNames="parentNames"
|
||||||
<div class="absolute bottom-40 right-40 w-24 h-24 bg-yellow-600 rounded-full"></div>
|
:childPhoto="childPhoto"
|
||||||
</div>
|
/>
|
||||||
|
<Event />
|
||||||
<!-- Navigation -->
|
<Gallery />
|
||||||
<nav class="relative z-20 bg-transparent border-b border-yellow-600/20">
|
<GuestBook />
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
<ThankYou />
|
||||||
<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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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
|
defineProps({
|
||||||
const childName = ref('Rayyanza Malik Ahmad')
|
age: Number,
|
||||||
const age = ref(4)
|
childName: String,
|
||||||
const childOrder = ref(2)
|
childOrder: Number,
|
||||||
const parentNames = ref('Raffi Ahmad & Nagita Slavina')
|
parentNames: String,
|
||||||
const guestName = ref('Gempita Nora Marten')
|
childPhoto: String
|
||||||
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!` }
|
|
||||||
]
|
|
||||||
})
|
})
|
||||||
</script>
|
</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">
|
<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>
|
<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">
|
<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>
|
</h1>
|
||||||
|
|
||||||
<p class="text-lg mb-8 animate-fade-in-delay">{{ formatDate(data.eventDate) }}</p>
|
<p class="text-lg mb-8 animate-fade-in-delay">{{ formatDate(data.eventDate) }}</p>
|
||||||
<button @click="openInvitation"
|
<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">
|
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 'aos/dist/aos.css'
|
||||||
|
|
||||||
// Import shared components
|
// Import shared components
|
||||||
import CountdownTimer from '~/components/shared/CountdownTimer.vue'
|
import CountdownTimer from '~/components/templates/wedding/CountdownTimer.vue'
|
||||||
import Gallery from '~/components/shared/Gallery.vue'
|
import Gallery from '~/components/templates/wedding/Gallery.vue'
|
||||||
import Maps from '~/components/shared/Maps.vue'
|
import Maps from '~/components/templates/wedding/Maps.vue'
|
||||||
import RSVP from '~/components/shared/RSVP.vue'
|
import RSVP from '~/components/templates/wedding/RSVP.vue'
|
||||||
import GuestBook from '~/components/shared/GuestBook.vue'
|
import GuestBook from '~/components/templates/wedding/GuestBook.vue'
|
||||||
import MusicPlayer from '~/components/shared/MusicPlayer.vue'
|
import MusicPlayer from '~/components/templates/wedding/MusicPlayer.vue'
|
||||||
|
|
||||||
// Props untuk menerima data dari parent/API
|
// Props untuk menerima data dari parent/API
|
||||||
const props = defineProps({
|
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>
|
||||||
312
proyek-frontend/app/pages/form/undangan-khitan-basic.vue
Normal file
312
proyek-frontend/app/pages/form/undangan-khitan-basic.vue
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 py-10 px-6">
|
||||||
|
<div class="max-w-4xl mx-auto bg-white rounded-2xl shadow-lg p-8">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-800">
|
||||||
|
Form Undangan Khitan Basic
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500 text-sm mb-8">
|
||||||
|
Isi semua data dengan lengkap dan benar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Data Pemesan -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📋 Data Pemesan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.nama_pemesan"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Pemesan"
|
||||||
|
required
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.no_tlpn"
|
||||||
|
type="text"
|
||||||
|
placeholder="No Telepon"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Data Anak -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">👦 Data Anak yang Dikhitan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_lengkap"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Lengkap"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_panggilan"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Panggilan"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_bapak"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Bapak"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_ibu"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Ibu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Detail Acara -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📅 Detail Acara</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.form.hari_tanggal_acara"
|
||||||
|
type="date"
|
||||||
|
placeholder="Hari & Tanggal Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.waktu"
|
||||||
|
type="text"
|
||||||
|
placeholder="Waktu Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="form.form.alamat"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Alamat Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 mt-3 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"
|
||||||
|
></textarea>
|
||||||
|
<input
|
||||||
|
v-model="form.form.link_gmaps"
|
||||||
|
type="text"
|
||||||
|
placeholder="Link Google Maps"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 mt-3 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Say Something -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">💬 Say Something</h2>
|
||||||
|
<textarea
|
||||||
|
v-model="form.form.say_something"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Kata-kata atau ucapan terima kasih..."
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"
|
||||||
|
></textarea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Hitung Mundur -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">⏳ Hitung Mundur</h2>
|
||||||
|
<input
|
||||||
|
v-model="form.form.hitung_mundur"
|
||||||
|
type="datetime-local"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Rekening -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">💳 Rekening</h2>
|
||||||
|
<input
|
||||||
|
v-model="form.form.rekening_1"
|
||||||
|
type="text"
|
||||||
|
placeholder="Rekening 1"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Musik -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🎵 Musik</h2>
|
||||||
|
<input
|
||||||
|
v-model="form.form.link_music"
|
||||||
|
type="text"
|
||||||
|
placeholder="Link Musik (opsional)"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Galeri Foto -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🖼️ Galeri Foto</h2>
|
||||||
|
<div
|
||||||
|
class="border-2 border-dashed border-gray-300 rounded-xl p-8 flex flex-col justify-center items-center text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="gallery"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
<label v-if="!previews.length" for="gallery" class="cursor-pointer flex flex-col items-center">
|
||||||
|
<span class="text-4xl font-bold">+</span>
|
||||||
|
<span class="text-sm mt-2">Pilih Foto (maks. 4, JPEG/PNG, maks. 2MB)</span>
|
||||||
|
</label>
|
||||||
|
<div v-else class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(src, i) in previews"
|
||||||
|
:key="i"
|
||||||
|
class="relative group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
class="w-24 h-24 object-cover rounded-lg border shadow"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeFile(i)"
|
||||||
|
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="previews.length < 4"
|
||||||
|
for="gallery"
|
||||||
|
class="cursor-pointer flex flex-col items-center justify-center w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<span class="text-3xl font-bold">+</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tombol -->
|
||||||
|
<div class="text-end mt-6">
|
||||||
|
<button
|
||||||
|
@click="batal"
|
||||||
|
class="bg-gray-600 text-white font-semibold px-6 py-2 rounded-lg transition mr-2"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="konfirmasi"
|
||||||
|
class="bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Konfirmasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
nama_pemesan: '',
|
||||||
|
email: '',
|
||||||
|
no_tlpn: '',
|
||||||
|
form: {
|
||||||
|
nama_lengkap: '',
|
||||||
|
nama_panggilan: '',
|
||||||
|
nama_bapak: '',
|
||||||
|
nama_ibu: '',
|
||||||
|
hari_tanggal_acara: '',
|
||||||
|
waktu: '',
|
||||||
|
alamat: '',
|
||||||
|
link_gmaps: '',
|
||||||
|
say_something: '',
|
||||||
|
hitung_mundur: '',
|
||||||
|
rekening_1: '',
|
||||||
|
link_music: ''
|
||||||
|
},
|
||||||
|
foto: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const previews = ref([])
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
const total = form.value.foto.length + files.length
|
||||||
|
if (total > 4) {
|
||||||
|
alert('Maksimal 4 foto!')
|
||||||
|
e.target.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
alert(`${file.name} terlalu besar! Maks 2MB.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) {
|
||||||
|
alert(`${file.name} harus berupa JPEG atau PNG.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.value.foto.push(file)
|
||||||
|
previews.value.push(URL.createObjectURL(file))
|
||||||
|
})
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
form.value.foto.splice(index, 1)
|
||||||
|
previews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const konfirmasi = async () => {
|
||||||
|
try {
|
||||||
|
if (!form.value.nama_pemesan || !form.value.email) {
|
||||||
|
alert('Harap isi Nama Pemesan dan Email!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('nama_pemesan', form.value.nama_pemesan)
|
||||||
|
data.append('email', form.value.email)
|
||||||
|
data.append('no_tlpn', form.value.no_tlpn)
|
||||||
|
data.append('template_slug', 'undangan-khitan-basic')
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(form.value.form)) {
|
||||||
|
data.append(`form[${key}]`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.foto.forEach((file, index) => {
|
||||||
|
data.append(`foto[${index}]`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 422) {
|
||||||
|
const errors = Object.values(result.errors || {}).flat().join('\n')
|
||||||
|
throw new Error(errors || result.message)
|
||||||
|
}
|
||||||
|
throw new Error(result.message || 'Gagal mengirim data')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(result.message || 'Data berhasil disimpan!')
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert('Terjadi kesalahan: ' + err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batal = () => router.back()
|
||||||
|
</script>
|
||||||
@ -131,18 +131,27 @@
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
<input v-model="form.form.rekening_1" placeholder="Rekening 1"
|
<input v-model="form.form.rekening_1" placeholder="Rekening 1"
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2
|
class="w-full border border-gray-300 rounded-lg px-3 py-2
|
||||||
focus:ring-2 focus:ring-blue-400 focus:border-blue-400
|
focus:ring-2 focus:ring-blue-400 focus:border-blue-400
|
||||||
outline-none transition" />
|
outline-none transition" />
|
||||||
<input v-model="form.form.rekening_2" placeholder="Rekening 2"
|
<input v-model="form.form.rekening_2" placeholder="Rekening 2"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2
|
||||||
|
focus:ring-2 focus:ring-blue-400 focus:border-blue-400
|
||||||
|
outline-none transition" />
|
||||||
|
<input v-model="form.form.rekening_3" placeholder="Rekening 3"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2
|
||||||
|
focus:ring-2 focus:ring-blue-400 focus:border-blue-400
|
||||||
|
outline-none transition" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Musik -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🎵 Musik</h2>
|
||||||
|
<input v-model="form.form.link_music" placeholder="Link Music (opsional)"
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2
|
class="w-full border border-gray-300 rounded-lg px-3 py-2
|
||||||
focus:ring-2 focus:ring-blue-400 focus:border-blue-400
|
focus:ring-2 focus:ring-blue-400 focus:border-blue-400
|
||||||
outline-none transition" />
|
outline-none transition" />
|
||||||
<input v-model="form.form.rekening_3" placeholder="Rekening 3"
|
</section>
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2
|
|
||||||
focus:ring-2 focus:ring-blue-400 focus:border-blue-400
|
|
||||||
outline-none transition" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Foto Upload -->
|
<!-- Foto Upload -->
|
||||||
<section class="mb-8">
|
<section class="mb-8">
|
||||||
@ -178,14 +187,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Musik -->
|
|
||||||
<section class="mb-8">
|
|
||||||
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🎵 Musik</h2>
|
|
||||||
<input v-model="form.form.link_music" placeholder="Link Music (opsional)"
|
|
||||||
class="w-full border border-gray-300 rounded-lg px-3 py-2
|
|
||||||
focus:ring-2 focus:ring-blue-400 focus:border-blue-400
|
|
||||||
outline-none transition" />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Tombol -->
|
<!-- Tombol -->
|
||||||
<div class="text-end mt-6">
|
<div class="text-end mt-6">
|
||||||
|
|||||||
257
proyek-frontend/app/pages/form/undangan-khitan-starter.vue
Normal file
257
proyek-frontend/app/pages/form/undangan-khitan-starter.vue
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 py-10 px-6">
|
||||||
|
<div class="max-w-5xl mx-auto bg-white rounded-2xl shadow-lg p-8">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-800">
|
||||||
|
Form Undangan Khitan Starter
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500 text-sm mb-8">
|
||||||
|
Isi semua data berikut dengan lengkap dan benar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Data Pemesan -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📋 Data Pemesan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.nama_pemesan"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Pemesan"
|
||||||
|
required
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.no_tlpn"
|
||||||
|
type="text"
|
||||||
|
placeholder="No Telepon"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Data Anak -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">👦 Data Anak yang Dikhitan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_lengkap"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Lengkap Anak"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_bapak"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Bapak"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_ibu"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Ibu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Detail Acara -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📅 Detail Acara</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.form.hari_tanggal_acara"
|
||||||
|
type="date"
|
||||||
|
placeholder="Hari & Tanggal Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.waktu"
|
||||||
|
type="text"
|
||||||
|
placeholder="Waktu Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<textarea
|
||||||
|
v-model="form.form.alamat"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Alamat Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 mt-3 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"
|
||||||
|
></textarea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Galeri Foto -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🖼️ Galeri Foto</h2>
|
||||||
|
<div
|
||||||
|
class="border-2 border-dashed border-gray-300 rounded-xl p-8 flex flex-col justify-center items-center text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="gallery"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
<label v-if="!previews.length" for="gallery" class="cursor-pointer flex flex-col items-center">
|
||||||
|
<span class="text-4xl font-bold">+</span>
|
||||||
|
<span class="text-sm mt-2">Pilih Foto (maks. 2, JPEG/PNG, maks. 2MB)</span>
|
||||||
|
</label>
|
||||||
|
<div v-else class="grid grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(src, i) in previews"
|
||||||
|
:key="i"
|
||||||
|
class="relative group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
class="w-24 h-24 object-cover rounded-lg border shadow"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeFile(i)"
|
||||||
|
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition"
|
||||||
|
title="Hapus foto"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="previews.length < 2"
|
||||||
|
for="gallery"
|
||||||
|
class="cursor-pointer flex flex-col items-center justify-center w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<span class="text-3xl font-bold">+</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tombol -->
|
||||||
|
<div class="text-end mt-6">
|
||||||
|
<button
|
||||||
|
@click="batal"
|
||||||
|
class="bg-gray-600 text-white font-semibold px-6 py-2 rounded-lg transition mr-2"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="konfirmasi"
|
||||||
|
class="bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Konfirmasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
nama_pemesan: '',
|
||||||
|
email: '',
|
||||||
|
no_tlpn: '',
|
||||||
|
form: {
|
||||||
|
nama_lengkap: '',
|
||||||
|
nama_bapak: '',
|
||||||
|
nama_ibu: '',
|
||||||
|
hari_tanggal_acara: '',
|
||||||
|
waktu: '',
|
||||||
|
alamat: ''
|
||||||
|
},
|
||||||
|
foto: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const previews = ref([])
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
const totalFiles = form.value.foto.length + files.length
|
||||||
|
|
||||||
|
if (totalFiles > 2) {
|
||||||
|
alert('Maksimal 2 foto!')
|
||||||
|
e.target.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
alert(`File ${file.name} terlalu besar! Maksimal 2MB.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) {
|
||||||
|
alert(`File ${file.name} harus berupa JPEG atau PNG!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.value.foto.push(file)
|
||||||
|
previews.value.push(URL.createObjectURL(file))
|
||||||
|
})
|
||||||
|
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
form.value.foto.splice(index, 1)
|
||||||
|
previews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const konfirmasi = async () => {
|
||||||
|
try {
|
||||||
|
if (!form.value.nama_pemesan || !form.value.email) {
|
||||||
|
alert('Harap isi semua kolom wajib (Nama Pemesan, Email)!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('nama_pemesan', form.value.nama_pemesan)
|
||||||
|
data.append('email', form.value.email)
|
||||||
|
data.append('no_tlpn', form.value.no_tlpn)
|
||||||
|
data.append('template_slug', 'undangan-khitan-starter')
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(form.value.form)) {
|
||||||
|
data.append(`form[${key}]`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.foto.forEach((file, index) => {
|
||||||
|
data.append(`foto[${index}]`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 422) {
|
||||||
|
const errors = result.errors || {}
|
||||||
|
const errorMessages = Object.values(errors).flat().join('\n')
|
||||||
|
throw new Error(errorMessages || result.message || 'Validasi gagal')
|
||||||
|
}
|
||||||
|
if (res.status === 404) {
|
||||||
|
throw new Error(result.message || 'Template tidak ditemukan')
|
||||||
|
}
|
||||||
|
throw new Error(result.message || 'Gagal mengirim data')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(result.message || 'Data berhasil disimpan!')
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert('Terjadi kesalahan: ' + err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batal = () => router.back()
|
||||||
|
</script>
|
||||||
381
proyek-frontend/app/pages/form/undangan-pernikahan-basic.vue
Normal file
381
proyek-frontend/app/pages/form/undangan-pernikahan-basic.vue
Normal file
@ -0,0 +1,381 @@
|
|||||||
|
```vue
|
||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 py-10 px-6">
|
||||||
|
<div class="max-w-5xl mx-auto bg-white rounded-2xl shadow-lg p-8">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-800">
|
||||||
|
Form Undangan Pernikahan Basic
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500 text-sm mb-8">
|
||||||
|
Isi semua data berikut dengan lengkap dan benar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Data Pemesan -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📋 Data Pemesan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.nama_pemesan"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Pemesan"
|
||||||
|
required
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.no_tlpn"
|
||||||
|
type="text"
|
||||||
|
placeholder="No Telepon"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Data Mempelai -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">💍 Data Mempelai</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Mempelai Pria -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium text-gray-700 mb-2">Mempelai Pria</h3>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_lengkap_pria"
|
||||||
|
placeholder="Nama Lengkap"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_bapak_pria"
|
||||||
|
placeholder="Nama Bapak"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_ibu_pria"
|
||||||
|
placeholder="Nama Ibu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mempelai Wanita -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium text-gray-700 mb-2">Mempelai Wanita</h3>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_lengkap_wanita"
|
||||||
|
placeholder="Nama Lengkap"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-pink-400 focus:border-pink-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_bapak_wanita"
|
||||||
|
placeholder="Nama Bapak"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-pink-400 focus:border-pink-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_ibu_wanita"
|
||||||
|
placeholder="Nama Ibu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-pink-400 focus:border-pink-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Detail Acara -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📅 Detail Acara</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Akad -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium text-gray-700 mb-2">Akad</h3>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<input
|
||||||
|
v-model="form.form.hari_tanggal_akad"
|
||||||
|
type="date"
|
||||||
|
placeholder="Hari & Tanggal Akad"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.waktu_akad"
|
||||||
|
type="time"
|
||||||
|
placeholder="Waktu Akad"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-model="form.form.alamat_akad"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Alamat Akad"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"
|
||||||
|
></textarea>
|
||||||
|
<input
|
||||||
|
v-model="form.form.link_gmaps_akad"
|
||||||
|
type="text"
|
||||||
|
placeholder="Link Google Maps Akad"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Resepsi -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium text-gray-700 mb-2">Resepsi</h3>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<input
|
||||||
|
v-model="form.form.hari_tanggal_resepsi"
|
||||||
|
type="date"
|
||||||
|
placeholder="Hari & Tanggal Resepsi"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.waktu_resepsi"
|
||||||
|
type="time"
|
||||||
|
placeholder="Waktu Resepsi"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-model="form.form.alamat_resepsi"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Alamat Resepsi"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"
|
||||||
|
></textarea>
|
||||||
|
<input
|
||||||
|
v-model="form.form.link_gmaps_resepsi"
|
||||||
|
type="text"
|
||||||
|
placeholder="Link Google Maps Resepsi"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Hitung Mundur -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium text-gray-700 mb-2">Hitung Mundur</h3>
|
||||||
|
<input
|
||||||
|
v-model="form.form.hitung_mundur"
|
||||||
|
type="datetime-local"
|
||||||
|
placeholder="Hitung Mundur Waktu Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Say Something -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">💬 Say Something</h2>
|
||||||
|
<textarea
|
||||||
|
v-model="form.form.say_something"
|
||||||
|
rows="4"
|
||||||
|
placeholder="Kata-kata spesial atau pesan untuk tamu..."
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"
|
||||||
|
></textarea>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Rekening & Musik -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">💳 Rekening & Musik</h2>
|
||||||
|
<div class="grid md:grid-cols-3 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.form.rekening_1"
|
||||||
|
placeholder="Rekening 1"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
v-model="form.form.link_music"
|
||||||
|
placeholder="Link Music (opsional)"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 mt-4 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Foto Upload -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🖼️ Galeri Foto</h2>
|
||||||
|
<div
|
||||||
|
class="border-2 border-dashed border-gray-300 rounded-xl p-8 flex flex-col justify-center items-center text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="gallery"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
<label v-if="!previews.length" for="gallery" class="cursor-pointer flex flex-col items-center">
|
||||||
|
<span class="text-4xl font-bold">+</span>
|
||||||
|
<span class="text-sm mt-2">Pilih Foto (maks. 4, JPEG/PNG, maks. 2MB)</span>
|
||||||
|
</label>
|
||||||
|
<div v-else class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(src, i) in previews"
|
||||||
|
:key="i"
|
||||||
|
class="relative group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
class="w-24 h-24 object-cover rounded-lg border shadow"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeFile(i)"
|
||||||
|
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition"
|
||||||
|
title="Hapus foto"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="previews.length < 4"
|
||||||
|
for="gallery"
|
||||||
|
class="cursor-pointer flex flex-col items-center justify-center w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<span class="text-3xl font-bold">+</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tombol -->
|
||||||
|
<div class="text-end mt-6">
|
||||||
|
<button
|
||||||
|
@click="batal"
|
||||||
|
class="bg-gray-600 text-white font-semibold px-6 py-2 rounded-lg transition mr-2"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="konfirmasi"
|
||||||
|
class="bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Konfirmasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
nama_pemesan: '',
|
||||||
|
email: '',
|
||||||
|
no_tlpn: '',
|
||||||
|
form: {
|
||||||
|
nama_lengkap_pria: '',
|
||||||
|
nama_bapak_pria: '',
|
||||||
|
nama_ibu_pria: '',
|
||||||
|
nama_lengkap_wanita: '',
|
||||||
|
nama_bapak_wanita: '',
|
||||||
|
nama_ibu_wanita: '',
|
||||||
|
hari_tanggal_akad: '',
|
||||||
|
waktu_akad: '',
|
||||||
|
alamat_akad: '',
|
||||||
|
link_gmaps_akad: '',
|
||||||
|
hari_tanggal_resepsi: '',
|
||||||
|
waktu_resepsi: '',
|
||||||
|
alamat_resepsi: '',
|
||||||
|
link_gmaps_resepsi: '',
|
||||||
|
hitung_mundur: '',
|
||||||
|
say_something: '',
|
||||||
|
rekening_1: '',
|
||||||
|
link_music: ''
|
||||||
|
},
|
||||||
|
foto: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const previews = ref([])
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
const totalFiles = form.value.foto.length + files.length
|
||||||
|
|
||||||
|
if (totalFiles > 4) {
|
||||||
|
alert('Maksimal 4 foto!')
|
||||||
|
e.target.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
// Validate file size (2MB) and type
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
alert(`File ${file.name} terlalu besar! Maksimal 2MB.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) {
|
||||||
|
alert(`File ${file.name} harus berupa JPEG atau PNG!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.value.foto.push(file)
|
||||||
|
previews.value.push(URL.createObjectURL(file))
|
||||||
|
})
|
||||||
|
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
form.value.foto.splice(index, 1)
|
||||||
|
previews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const konfirmasi = async () => {
|
||||||
|
try {
|
||||||
|
// Basic client-side validation
|
||||||
|
if (!form.value.nama_pemesan || !form.value.email) {
|
||||||
|
alert('Harap isi semua kolom wajib (Nama Pemesan, Email)!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('nama_pemesan', form.value.nama_pemesan)
|
||||||
|
data.append('email', form.value.email)
|
||||||
|
data.append('no_tlpn', form.value.no_tlpn)
|
||||||
|
data.append('template_slug', 'undangan-pernikahan-basic')
|
||||||
|
|
||||||
|
// Append form fields individually to ensure Laravel receives them as an array
|
||||||
|
for (const [key, value] of Object.entries(form.value.form)) {
|
||||||
|
data.append(`form[${key}]`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.foto.forEach((file, index) => {
|
||||||
|
data.append(`foto[${index}]`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log([...data]) // untuk debugging
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 422) {
|
||||||
|
const errors = result.errors || {}
|
||||||
|
const errorMessages = Object.values(errors).flat().join('\n')
|
||||||
|
throw new Error(errorMessages || result.message || 'Validasi gagal')
|
||||||
|
}
|
||||||
|
if (res.status === 404) {
|
||||||
|
throw new Error(result.message || 'Template tidak ditemukan')
|
||||||
|
}
|
||||||
|
throw new Error(result.message || 'Gagal mengirim data')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(result.message || 'Data berhasil disimpan!')
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert('Terjadi kesalahan: ' + err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batal = () => router.back()
|
||||||
|
</script>
|
||||||
|
```
|
||||||
290
proyek-frontend/app/pages/form/undangan-pernikahan-starter.vue
Normal file
290
proyek-frontend/app/pages/form/undangan-pernikahan-starter.vue
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 py-10 px-6">
|
||||||
|
<div class="max-w-5xl mx-auto bg-white rounded-2xl shadow-lg p-8">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-800">
|
||||||
|
Form Undangan Pernikahan Starter
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500 text-sm mb-8">
|
||||||
|
Isi semua data berikut dengan lengkap dan benar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Data Pemesan -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📋 Data Pemesan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.nama_pemesan"
|
||||||
|
type="text"
|
||||||
|
placeholder="Nama Pemesan"
|
||||||
|
required
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="Email"
|
||||||
|
required
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.no_tlpn"
|
||||||
|
type="text"
|
||||||
|
placeholder="No Telepon"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Data Mempelai -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">💍 Data Mempelai</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<!-- Mempelai Pria -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium text-gray-700 mb-2">Mempelai Pria</h3>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_lengkap_pria"
|
||||||
|
placeholder="Nama Lengkap"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_bapak_pria"
|
||||||
|
placeholder="Nama Bapak"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_ibu_pria"
|
||||||
|
placeholder="Nama Ibu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mempelai Wanita -->
|
||||||
|
<div>
|
||||||
|
<h3 class="font-medium text-gray-700 mb-2">Mempelai Wanita</h3>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_lengkap_wanita"
|
||||||
|
placeholder="Nama Lengkap"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-pink-400 focus:border-pink-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_bapak_wanita"
|
||||||
|
placeholder="Nama Bapak"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-pink-400 focus:border-pink-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.nama_ibu_wanita"
|
||||||
|
placeholder="Nama Ibu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-pink-400 focus:border-pink-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Detail Acara -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📅 Detail Acara</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<input
|
||||||
|
v-model="form.form.hari_tanggal_acara"
|
||||||
|
type="date"
|
||||||
|
placeholder="Hari & Tanggal Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
v-model="form.form.waktu"
|
||||||
|
type="time"
|
||||||
|
placeholder="Waktu Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition"
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
v-model="form.form.alamat"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Alamat Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Foto Upload -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🖼️ Galeri Foto</h2>
|
||||||
|
<div
|
||||||
|
class="border-2 border-dashed border-gray-300 rounded-xl p-8 flex flex-col justify-center items-center text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="gallery"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
<label v-if="!previews.length" for="gallery" class="cursor-pointer flex flex-col items-center">
|
||||||
|
<span class="text-4xl font-bold">+</span>
|
||||||
|
<span class="text-sm mt-2">Pilih Foto (maks. 2, JPEG/PNG, maks. 2MB)</span>
|
||||||
|
</label>
|
||||||
|
<div v-else class="grid grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(src, i) in previews"
|
||||||
|
:key="i"
|
||||||
|
class="relative group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
class="w-24 h-24 object-cover rounded-lg border shadow"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeFile(i)"
|
||||||
|
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition"
|
||||||
|
title="Hapus foto"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="previews.length < 2"
|
||||||
|
for="gallery"
|
||||||
|
class="cursor-pointer flex flex-col items-center justify-center w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<span class="text-3xl font-bold">+</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tombol -->
|
||||||
|
<div class="text-end mt-6">
|
||||||
|
<button
|
||||||
|
@click="batal"
|
||||||
|
class="bg-gray-600 text-white font-semibold px-6 py-2 rounded-lg transition mr-2"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="konfirmasi"
|
||||||
|
class="bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition"
|
||||||
|
>
|
||||||
|
Konfirmasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
nama_pemesan: '',
|
||||||
|
email: '',
|
||||||
|
no_tlpn: '',
|
||||||
|
form: {
|
||||||
|
nama_lengkap_pria: '',
|
||||||
|
nama_bapak_pria: '',
|
||||||
|
nama_ibu_pria: '',
|
||||||
|
nama_lengkap_wanita: '',
|
||||||
|
nama_bapak_wanita: '',
|
||||||
|
nama_ibu_wanita: '',
|
||||||
|
hari_tanggal_acara: '',
|
||||||
|
waktu: '',
|
||||||
|
alamat: ''
|
||||||
|
},
|
||||||
|
foto: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const previews = ref([])
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
const totalFiles = form.value.foto.length + files.length
|
||||||
|
|
||||||
|
if (totalFiles > 2) {
|
||||||
|
alert('Maksimal 2 foto!')
|
||||||
|
e.target.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
// Validate file size (2MB) and type
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
alert(`File ${file.name} terlalu besar! Maksimal 2MB.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) {
|
||||||
|
alert(`File ${file.name} harus berupa JPEG atau PNG!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.value.foto.push(file)
|
||||||
|
previews.value.push(URL.createObjectURL(file))
|
||||||
|
})
|
||||||
|
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
form.value.foto.splice(index, 1)
|
||||||
|
previews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const konfirmasi = async () => {
|
||||||
|
try {
|
||||||
|
// Basic client-side validation
|
||||||
|
if (!form.value.nama_pemesan || !form.value.email) {
|
||||||
|
alert('Harap isi semua kolom wajib (Nama Pemesan, Email)!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('nama_pemesan', form.value.nama_pemesan)
|
||||||
|
data.append('email', form.value.email)
|
||||||
|
data.append('no_tlpn', form.value.no_tlpn)
|
||||||
|
data.append('template_slug', 'undangan-pernikahan-starter')
|
||||||
|
|
||||||
|
// Append form fields individually to ensure Laravel receives them as an array
|
||||||
|
for (const [key, value] of Object.entries(form.value.form)) {
|
||||||
|
data.append(`form[${key}]`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.foto.forEach((file, index) => {
|
||||||
|
data.append(`foto[${index}]`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log([...data]) // untuk debugging
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 422) {
|
||||||
|
const errors = result.errors || {}
|
||||||
|
const errorMessages = Object.values(errors).flat().join('\n')
|
||||||
|
throw new Error(errorMessages || result.message || 'Validasi gagal')
|
||||||
|
}
|
||||||
|
if (res.status === 404) {
|
||||||
|
throw new Error(result.message || 'Template tidak ditemukan')
|
||||||
|
}
|
||||||
|
throw new Error(result.message || 'Gagal mengirim data')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(result.message || 'Data berhasil disimpan!')
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert('Terjadi kesalahan: ' + err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batal = () => router.back()
|
||||||
|
</script>
|
||||||
252
proyek-frontend/app/pages/form/undangan-ulang-tahun-basic.vue
Normal file
252
proyek-frontend/app/pages/form/undangan-ulang-tahun-basic.vue
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 py-10 px-6">
|
||||||
|
<div class="max-w-5xl mx-auto bg-white rounded-2xl shadow-lg p-8">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-800">
|
||||||
|
Form Undangan Ulang Tahun Basic
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500 text-sm mb-8">
|
||||||
|
Isi semua data berikut dengan lengkap dan benar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Data Pemesan -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📋 Data Pemesan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<input v-model="form.nama_pemesan" type="text" placeholder="Nama Pemesan"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.email" type="email" placeholder="Email"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.no_tlpn" type="text" placeholder="No Telepon"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Data Anak -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🎉 Data Anak</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<input v-model="form.form.nama_lengkap" placeholder="Nama Lengkap"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.nama_panggilan" placeholder="Nama Panggilan"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.nama_bapak" placeholder="Nama Bapak"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.nama_ibu" placeholder="Nama Ibu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.umur_yang_dirayakan" type="number" placeholder="Umur Yang Dirayakan"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Detail Acara -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📅 Detail Acara</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<input v-model="form.form.hari_tanggal_acara" type="date" placeholder="Hari & Tanggal Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.waktu" type="text" placeholder="Waktu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<textarea v-model="form.form.alamat" rows="4" placeholder="Alamat"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"></textarea>
|
||||||
|
<input v-model="form.form.link_gmaps" type="text" placeholder="Link Gmaps"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.hitung_mundur" type="datetime-local" placeholder="Hitung Mundur Waktu Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Pesan & Rekening -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">💌 Pesan & Rekening</h2>
|
||||||
|
<textarea v-model="form.form.say_something" rows="4" placeholder="Say Something..."
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"></textarea>
|
||||||
|
<div class="grid md:grid-cols-1 gap-4 mt-4">
|
||||||
|
<input v-model="form.form.rekening_1" placeholder="Rekening 1"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.link_music" placeholder="Link Music (opsional)"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Foto Upload -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🖼️ Galeri Foto</h2>
|
||||||
|
<div
|
||||||
|
class="border-2 border-dashed border-gray-300 rounded-xl p-8 flex flex-col justify-center items-center text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="gallery"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
<label v-if="!previews.length" for="gallery" class="cursor-pointer flex flex-col items-center">
|
||||||
|
<span class="text-4xl font-bold">+</span>
|
||||||
|
<span class="text-sm mt-2">Pilih Foto (maks. 4, JPEG/PNG, maks. 2MB)</span>
|
||||||
|
</label>
|
||||||
|
<div v-else class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(src, i) in previews"
|
||||||
|
:key="i"
|
||||||
|
class="relative group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
class="w-24 h-24 object-cover rounded-lg border shadow"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeFile(i)"
|
||||||
|
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition"
|
||||||
|
title="Hapus foto"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="previews.length < 4"
|
||||||
|
for="gallery"
|
||||||
|
class="cursor-pointer flex flex-col items-center justify-center w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<span class="text-3xl font-bold">+</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tombol -->
|
||||||
|
<div class="text-end mt-6">
|
||||||
|
<button @click="batal"
|
||||||
|
class="bg-gray-600 text-white font-semibold px-6 py-2 rounded-lg transition mr-2">
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button @click="konfirmasi"
|
||||||
|
class="bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition">
|
||||||
|
Konfirmasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
nama_pemesan: '',
|
||||||
|
email: '',
|
||||||
|
no_tlpn: '',
|
||||||
|
form: {
|
||||||
|
nama_lengkap: '',
|
||||||
|
nama_panggilan: '',
|
||||||
|
nama_bapak: '',
|
||||||
|
nama_ibu: '',
|
||||||
|
umur_yang_dirayakan: '',
|
||||||
|
hari_tanggal_acara: '',
|
||||||
|
waktu: '',
|
||||||
|
alamat: '',
|
||||||
|
link_gmaps: '',
|
||||||
|
hitung_mundur: '',
|
||||||
|
say_something: '',
|
||||||
|
rekening_1: '',
|
||||||
|
link_music: ''
|
||||||
|
},
|
||||||
|
foto: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const previews = ref([])
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
const totalFiles = form.value.foto.length + files.length
|
||||||
|
|
||||||
|
if (totalFiles > 4) {
|
||||||
|
alert('Maksimal 4 foto!')
|
||||||
|
e.target.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
// Validate file size (2MB) and type
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
alert(`File ${file.name} terlalu besar! Maksimal 2MB.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) {
|
||||||
|
alert(`File ${file.name} harus berupa JPEG atau PNG!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.value.foto.push(file)
|
||||||
|
previews.value.push(URL.createObjectURL(file))
|
||||||
|
})
|
||||||
|
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
form.value.foto.splice(index, 1)
|
||||||
|
previews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const konfirmasi = async () => {
|
||||||
|
try {
|
||||||
|
// Basic client-side validation
|
||||||
|
if (!form.value.nama_pemesan || !form.value.email) {
|
||||||
|
alert('Harap isi kolom wajib (Nama Pemesan, Email)!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('nama_pemesan', form.value.nama_pemesan)
|
||||||
|
data.append('email', form.value.email)
|
||||||
|
data.append('no_tlpn', form.value.no_telepon)
|
||||||
|
data.append('template_slug', 'undangan-ulang-tahun-basic')
|
||||||
|
|
||||||
|
// Append form fields individually to ensure Laravel receives them as an array
|
||||||
|
for (const [key, value] of Object.entries(form.value.form)) {
|
||||||
|
data.append(`form[${key}]`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.foto.forEach((file, index) => {
|
||||||
|
data.append(`foto[${index}]`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log([...data]) // untuk debugging
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 422) {
|
||||||
|
const errors = result.errors || {}
|
||||||
|
const errorMessages = Object.values(errors).flat().join('\n')
|
||||||
|
throw new Error(errorMessages || result.message || 'Validasi gagal')
|
||||||
|
}
|
||||||
|
if (res.status === 404) {
|
||||||
|
throw new Error(result.message || 'Template tidak ditemukan')
|
||||||
|
}
|
||||||
|
throw new Error(result.message || 'Gagal mengirim data')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(result.message || 'Data berhasil disimpan!')
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert('Terjadi kesalahan: ' + err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batal = () => router.back()
|
||||||
|
</script>
|
||||||
@ -1,434 +1,174 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="max-w-4xl mx-auto p-6">
|
<div class="min-h-screen bg-gray-50 py-10 px-6">
|
||||||
<div class="bg-white rounded-lg shadow-lg p-8">
|
<div class="max-w-5xl mx-auto bg-white rounded-2xl shadow-lg p-8">
|
||||||
<h2 class="text-3xl font-bold text-gray-800 mb-2">
|
<h1 class="text-2xl font-bold text-center text-gray-800">
|
||||||
Undangan Ulang Tahun Premium
|
Form Undangan Ulang Tahun Premium
|
||||||
</h2>
|
</h1>
|
||||||
<p class="text-gray-600 mb-6">Harga: Rp 200.000</p>
|
<p class="text-center text-gray-500 text-sm mb-8">
|
||||||
|
Isi semua data berikut dengan lengkap dan benar.
|
||||||
|
</p>
|
||||||
|
|
||||||
<form @submit.prevent="submitForm" class="space-y-6">
|
|
||||||
<!-- Data Pemesan -->
|
<!-- Data Pemesan -->
|
||||||
<div class="border-b pb-6">
|
<section class="mb-8">
|
||||||
<h3 class="text-xl font-semibold text-gray-700 mb-4">Data Pemesan</h3>
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📋 Data Pemesan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<input v-model="form.nama_pemesan" type="text" placeholder="Nama Pemesan"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.email" type="email" placeholder="Email"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.no_tlpn" type="text" placeholder="No Telepon"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<!-- Data Anak -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🎉 Data Anak</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
<div class="grid gap-2">
|
||||||
Nama Pemesan <span class="text-red-500">*</span>
|
<input v-model="form.form.nama_lengkap" placeholder="Nama Lengkap"
|
||||||
</label>
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
<input
|
<input v-model="form.form.nama_panggilan" placeholder="Nama Panggilan"
|
||||||
v-model="formData.nama_pemesan"
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
type="text"
|
<input v-model="form.form.nama_bapak" placeholder="Nama Bapak"
|
||||||
required
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
<input v-model="form.form.nama_ibu" placeholder="Nama Ibu"
|
||||||
placeholder="Masukkan nama pemesan"
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
/>
|
<input v-model="form.form.umur_yang_dirayakan" type="number" placeholder="Umur Yang Dirayakan"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.anak_ke" type="number" placeholder="Anak Ke"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
<div class="grid gap-2">
|
||||||
Email <span class="text-red-500">*</span>
|
<input v-model="form.form.instagram" placeholder="Link Instagram"
|
||||||
</label>
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
<input
|
<input v-model="form.form.facebook" placeholder="Link Facebook"
|
||||||
v-model="formData.email"
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
type="email"
|
<input v-model="form.form.twitter" placeholder="Link Twitter"
|
||||||
required
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
</div>
|
||||||
placeholder="contoh@email.com"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
No Telepon
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.no_telepon"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="08xxxxxxxxxx"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Data Yang Berulang Tahun -->
|
|
||||||
<div class="border-b pb-6">
|
|
||||||
<h3 class="text-xl font-semibold text-gray-700 mb-4">Data Yang Berulang Tahun</h3>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Nama Lengkap
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.nama_lengkap"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Nama lengkap"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Nama Panggilan
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.nama_panggilan"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Nama panggilan"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Nama Bapak
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.nama_bapak"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Nama bapak"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Nama Ibu
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.nama_ibu"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Nama ibu"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Umur Yang Dirayakan
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model.number="formData.umur_yang_dirayakan"
|
|
||||||
type="number"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Contoh: 7"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Anak Ke
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model.number="formData.anak_ke"
|
|
||||||
type="number"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Contoh: 1"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Media Sosial -->
|
|
||||||
<div class="border-b pb-6">
|
|
||||||
<h3 class="text-xl font-semibold text-gray-700 mb-4">Media Sosial</h3>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Link Instagram
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.instagram"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="https://instagram.com/username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Link Facebook
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.facebook"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="https://facebook.com/username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Link Twitter
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.twitter"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="https://twitter.com/username"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Detail Acara -->
|
<!-- Detail Acara -->
|
||||||
<div class="border-b pb-6">
|
<section class="mb-8">
|
||||||
<h3 class="text-xl font-semibold text-gray-700 mb-4">Detail Acara</h3>
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📅 Detail Acara</h2>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
<div>
|
<input v-model="form.form.hari_tanggal_acara" type="date" placeholder="Hari & Tanggal Acara"
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
Hari & Tanggal Acara
|
<input v-model="form.form.waktu" type="text" placeholder="Waktu"
|
||||||
</label>
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
<input
|
<textarea v-model="form.form.alamat" rows="4" placeholder="Alamat"
|
||||||
v-model="formData.hari_tanggal_acara"
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"></textarea>
|
||||||
type="date"
|
<input v-model="form.form.link_gmaps" type="text" placeholder="Link Gmaps"
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
/>
|
<input v-model="form.form.hitung_mundur" type="datetime-local" placeholder="Hitung Mundur Waktu Acara"
|
||||||
</div>
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.link_live_streaming" type="text" placeholder="Link Live Streaming"
|
||||||
<div>
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Waktu
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.waktu"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Contoh: 14.00 - 16.00 WIB"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Alamat
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.alamat"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Alamat lengkap acara"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Link Google Maps
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.link_gmaps"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="https://maps.google.com/..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Say Something
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
v-model="formData.say_something"
|
|
||||||
rows="4"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Pesan atau kata-kata untuk undangan"
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Rekening -->
|
<!-- Pesan & Rekening -->
|
||||||
<div class="border-b pb-6">
|
<section class="mb-8">
|
||||||
<h3 class="text-xl font-semibold text-gray-700 mb-4">Rekening (Opsional)</h3>
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">💌 Pesan & Rekening</h2>
|
||||||
|
<textarea v-model="form.form.say_something" rows="4" placeholder="Say Something..."
|
||||||
<div class="grid grid-cols-1 gap-4">
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"></textarea>
|
||||||
<div>
|
<div class="grid md:grid-cols-3 gap-4 mt-4">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
<input v-model="form.form.rekening_1" placeholder="Rekening 1"
|
||||||
Rekening 1
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
</label>
|
<input v-model="form.form.rekening_2" placeholder="Rekening 2"
|
||||||
<input
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
v-model="formData.rekening_1"
|
<input v-model="form.form.rekening_3" placeholder="Rekening 3"
|
||||||
type="text"
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Contoh: BCA - 1234567890 - Nama Pemilik"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Rekening 2
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.rekening_2"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Contoh: Mandiri - 9876543210 - Nama Pemilik"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Rekening 3
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
v-model="formData.rekening_3"
|
|
||||||
type="text"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
placeholder="Contoh: BNI - 5555555555 - Nama Pemilik"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<input v-model="form.form.link_music" placeholder="Link Music (opsional)"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 mt-4 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Upload Foto -->
|
<!-- Foto Upload -->
|
||||||
<div class="border-b pb-6">
|
<section class="mb-8">
|
||||||
<h3 class="text-xl font-semibold text-gray-700 mb-4">Upload Foto</h3>
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🖼️ Galeri Foto</h2>
|
||||||
|
<div
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
class="border-2 border-dashed border-gray-300 rounded-xl p-8 flex flex-col justify-center items-center text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
<div v-for="i in 5" :key="i">
|
>
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Foto {{ i }}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
@change="handleFileUpload($event, `foto_${i}`)"
|
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
<p v-if="formData[`foto_${i}`]" class="text-sm text-green-600 mt-1">
|
|
||||||
✓ File dipilih: {{ formData[`foto_${i}`].name }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Link Music -->
|
|
||||||
<div class="pb-6">
|
|
||||||
<h3 class="text-xl font-semibold text-gray-700 mb-4">Background Music</h3>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Link Music
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
v-model="formData.link_music"
|
id="gallery"
|
||||||
type="text"
|
type="file"
|
||||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
multiple
|
||||||
placeholder="Link YouTube atau file musik"
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileChange"
|
||||||
/>
|
/>
|
||||||
|
<label v-if="!previews.length" for="gallery" class="cursor-pointer flex flex-col items-center">
|
||||||
|
<span class="text-4xl font-bold">+</span>
|
||||||
|
<span class="text-sm mt-2">Pilih Foto (maks. 8, JPEG/PNG, maks. 2MB)</span>
|
||||||
|
</label>
|
||||||
|
<div v-else class="grid grid-cols-3 sm:grid-cols-4 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(src, i) in previews"
|
||||||
|
:key="i"
|
||||||
|
class="relative group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
class="w-24 h-24 object-cover rounded-lg border shadow"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeFile(i)"
|
||||||
|
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition"
|
||||||
|
title="Hapus foto"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="previews.length < 8"
|
||||||
|
for="gallery"
|
||||||
|
class="cursor-pointer flex flex-col items-center justify-center w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<span class="text-3xl font-bold">+</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
<!-- Submit Button -->
|
<!-- Tombol -->
|
||||||
<div class="flex gap-4">
|
<div class="text-end mt-6">
|
||||||
<button
|
<button @click="batal"
|
||||||
type="submit"
|
class="bg-gray-600 text-white font-semibold px-6 py-2 rounded-lg transition mr-2">
|
||||||
:disabled="loading"
|
Batal
|
||||||
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 px-6 rounded-lg transition duration-200 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{{ loading ? 'Mengirim...' : 'Kirim Pesanan' }}
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button @click="konfirmasi"
|
||||||
type="button"
|
class="bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition">
|
||||||
@click="resetForm"
|
Konfirmasi
|
||||||
class="px-6 py-3 border border-gray-300 text-gray-700 font-semibold rounded-lg hover:bg-gray-50 transition duration-200"
|
|
||||||
>
|
|
||||||
Reset
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const formData = ref({
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
nama_pemesan: '',
|
nama_pemesan: '',
|
||||||
email: '',
|
email: '',
|
||||||
no_telepon: '',
|
no_tlpn: '',
|
||||||
nama_lengkap: '',
|
form: {
|
||||||
nama_panggilan: '',
|
|
||||||
nama_bapak: '',
|
|
||||||
nama_ibu: '',
|
|
||||||
umur_yang_dirayakan: null,
|
|
||||||
anak_ke: null,
|
|
||||||
instagram: '',
|
|
||||||
facebook: '',
|
|
||||||
twitter: '',
|
|
||||||
hari_tanggal_acara: '',
|
|
||||||
waktu: '',
|
|
||||||
alamat: '',
|
|
||||||
link_gmaps: '',
|
|
||||||
say_something: '',
|
|
||||||
rekening_1: '',
|
|
||||||
rekening_2: '',
|
|
||||||
rekening_3: '',
|
|
||||||
foto_1: null,
|
|
||||||
foto_2: null,
|
|
||||||
foto_3: null,
|
|
||||||
foto_4: null,
|
|
||||||
foto_5: null,
|
|
||||||
link_music: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const handleFileUpload = (event, fieldName) => {
|
|
||||||
const file = event.target.files[0]
|
|
||||||
if (file) {
|
|
||||||
formData.value[fieldName] = file
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formDataToSend = new FormData()
|
|
||||||
|
|
||||||
// Append semua data ke FormData
|
|
||||||
Object.keys(formData.value).forEach(key => {
|
|
||||||
if (formData.value[key] !== null && formData.value[key] !== '') {
|
|
||||||
formDataToSend.append(key, formData.value[key])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Kirim ke API Laravel
|
|
||||||
const response = await $fetch('/api/orders/ulang-tahun-premium', {
|
|
||||||
method: 'POST',
|
|
||||||
body: formDataToSend
|
|
||||||
})
|
|
||||||
|
|
||||||
alert('Pesanan berhasil dikirim!')
|
|
||||||
resetForm()
|
|
||||||
|
|
||||||
// Redirect atau tindakan lainnya
|
|
||||||
// navigateTo('/success')
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error:', error)
|
|
||||||
alert('Terjadi kesalahan saat mengirim pesanan')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
formData.value = {
|
|
||||||
nama_pemesan: '',
|
|
||||||
email: '',
|
|
||||||
no_telepon: '',
|
|
||||||
nama_lengkap: '',
|
nama_lengkap: '',
|
||||||
nama_panggilan: '',
|
nama_panggilan: '',
|
||||||
nama_bapak: '',
|
nama_bapak: '',
|
||||||
nama_ibu: '',
|
nama_ibu: '',
|
||||||
umur_yang_dirayakan: null,
|
umur_yang_dirayakan: '',
|
||||||
anak_ke: null,
|
anak_ke: '',
|
||||||
instagram: '',
|
instagram: '',
|
||||||
facebook: '',
|
facebook: '',
|
||||||
twitter: '',
|
twitter: '',
|
||||||
@ -436,29 +176,102 @@ const resetForm = () => {
|
|||||||
waktu: '',
|
waktu: '',
|
||||||
alamat: '',
|
alamat: '',
|
||||||
link_gmaps: '',
|
link_gmaps: '',
|
||||||
|
hitung_mundur: '',
|
||||||
|
link_live_streaming: '',
|
||||||
say_something: '',
|
say_something: '',
|
||||||
rekening_1: '',
|
rekening_1: '',
|
||||||
rekening_2: '',
|
rekening_2: '',
|
||||||
rekening_3: '',
|
rekening_3: '',
|
||||||
foto_1: null,
|
|
||||||
foto_2: null,
|
|
||||||
foto_3: null,
|
|
||||||
foto_4: null,
|
|
||||||
foto_5: null,
|
|
||||||
link_music: ''
|
link_music: ''
|
||||||
|
},
|
||||||
|
foto: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const previews = ref([])
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
const totalFiles = form.value.foto.length + files.length
|
||||||
|
|
||||||
|
if (totalFiles > 8) {
|
||||||
|
alert('Maksimal 8 foto!')
|
||||||
|
e.target.value = ''
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset file inputs
|
files.forEach(file => {
|
||||||
const fileInputs = document.querySelectorAll('input[type="file"]')
|
// Validate file size (2MB) and type
|
||||||
fileInputs.forEach(input => {
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
input.value = ''
|
alert(`File ${file.name} terlalu besar! Maksimal 2MB.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) {
|
||||||
|
alert(`File ${file.name} harus berupa JPEG atau PNG!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.value.foto.push(file)
|
||||||
|
previews.value.push(URL.createObjectURL(file))
|
||||||
})
|
})
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
e.target.value = ''
|
||||||
input:focus,
|
|
||||||
textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
form.value.foto.splice(index, 1)
|
||||||
|
previews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const konfirmasi = async () => {
|
||||||
|
try {
|
||||||
|
// Basic client-side validation
|
||||||
|
if (!form.value.nama_pemesan || !form.value.email) {
|
||||||
|
alert('Harap isi kolom wajib (Nama Pemesan, Email)!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('nama_pemesan', form.value.nama_pemesan)
|
||||||
|
data.append('email', form.value.email)
|
||||||
|
data.append('no_tlpn', form.value.no_tlpn)
|
||||||
|
data.append('template_slug', 'undangan-ulang-tahun-premium')
|
||||||
|
|
||||||
|
// Append form fields individually to ensure Laravel receives them as an array
|
||||||
|
for (const [key, value] of Object.entries(form.value.form)) {
|
||||||
|
data.append(`form[${key}]`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.foto.forEach((file, index) => {
|
||||||
|
data.append(`foto[${index}]`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log([...data]) // untuk debugging
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 422) {
|
||||||
|
const errors = result.errors || {}
|
||||||
|
const errorMessages = Object.values(errors).flat().join('\n')
|
||||||
|
throw new Error(errorMessages || result.message || 'Validasi gagal')
|
||||||
|
}
|
||||||
|
if (res.status === 404) {
|
||||||
|
throw new Error(result.message || 'Template tidak ditemukan')
|
||||||
|
}
|
||||||
|
throw new Error(result.message || 'Gagal mengirim data')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(result.message || 'Data berhasil disimpan!')
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert('Terjadi kesalahan: ' + err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batal = () => router.back()
|
||||||
|
</script>
|
||||||
221
proyek-frontend/app/pages/form/undangan-ulang-tahun-starter.vue
Normal file
221
proyek-frontend/app/pages/form/undangan-ulang-tahun-starter.vue
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<template>
|
||||||
|
<div class="min-h-screen bg-gray-50 py-10 px-6">
|
||||||
|
<div class="max-w-5xl mx-auto bg-white rounded-2xl shadow-lg p-8">
|
||||||
|
<h1 class="text-2xl font-bold text-center text-gray-800">
|
||||||
|
Form Undangan Ulang Tahun Starter
|
||||||
|
</h1>
|
||||||
|
<p class="text-center text-gray-500 text-sm mb-8">
|
||||||
|
Isi semua data berikut dengan lengkap dan benar.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Data Pemesan -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📋 Data Pemesan</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<input v-model="form.nama_pemesan" type="text" placeholder="Nama Pemesan"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.email" type="email" placeholder="Email"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.no_tlpn" type="text" placeholder="No Telepon"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Data Anak -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🎉 Data Anak</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<input v-model="form.form.nama_lengkap" placeholder="Nama Lengkap"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.umur_yang_dirayakan" type="number" placeholder="Umur Yang Dirayakan"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Detail Acara -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">📅 Detail Acara</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<input v-model="form.form.hari_tanggal_acara" type="date" placeholder="Hari & Tanggal Acara"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<input v-model="form.form.waktu" type="text" placeholder="Waktu"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" />
|
||||||
|
<textarea v-model="form.form.alamat" rows="4" placeholder="Alamat"
|
||||||
|
class="w-full border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none"></textarea>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Foto Upload -->
|
||||||
|
<section class="mb-8">
|
||||||
|
<h2 class="font-semibold text-blue-600 mb-3 border-b pb-1">🖼️ Galeri Foto</h2>
|
||||||
|
<div
|
||||||
|
class="border-2 border-dashed border-gray-300 rounded-xl p-8 flex flex-col justify-center items-center text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="gallery"
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
class="hidden"
|
||||||
|
@change="handleFileChange"
|
||||||
|
/>
|
||||||
|
<label v-if="!previews.length" for="gallery" class="cursor-pointer flex flex-col items-center">
|
||||||
|
<span class="text-4xl font-bold">+</span>
|
||||||
|
<span class="text-sm mt-2">Pilih Foto (maks. 2, JPEG/PNG, maks. 2MB)</span>
|
||||||
|
</label>
|
||||||
|
<div v-else class="grid grid-cols-2 sm:grid-cols-2 gap-4">
|
||||||
|
<div
|
||||||
|
v-for="(src, i) in previews"
|
||||||
|
:key="i"
|
||||||
|
class="relative group"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="src"
|
||||||
|
class="w-24 h-24 object-cover rounded-lg border shadow"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
@click="removeFile(i)"
|
||||||
|
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition"
|
||||||
|
title="Hapus foto"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
v-if="previews.length < 2"
|
||||||
|
for="gallery"
|
||||||
|
class="cursor-pointer flex flex-col items-center justify-center w-24 h-24 border-2 border-dashed border-gray-300 rounded-lg text-gray-400 hover:border-blue-400 hover:text-blue-500 transition"
|
||||||
|
>
|
||||||
|
<span class="text-3xl font-bold">+</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tombol -->
|
||||||
|
<div class="text-end mt-6">
|
||||||
|
<button @click="batal"
|
||||||
|
class="bg-gray-600 text-white font-semibold px-6 py-2 rounded-lg transition mr-2">
|
||||||
|
Batal
|
||||||
|
</button>
|
||||||
|
<button @click="konfirmasi"
|
||||||
|
class="bg-blue-700 text-white font-semibold px-6 py-2 rounded-lg transition">
|
||||||
|
Konfirmasi
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
nama_pemesan: '',
|
||||||
|
email: '',
|
||||||
|
no_tlpn: '',
|
||||||
|
form: {
|
||||||
|
nama_lengkap: '',
|
||||||
|
umur_yang_dirayakan: '',
|
||||||
|
hari_tanggal_acara: '',
|
||||||
|
waktu: '',
|
||||||
|
alamat: ''
|
||||||
|
},
|
||||||
|
foto: []
|
||||||
|
})
|
||||||
|
|
||||||
|
const previews = ref([])
|
||||||
|
|
||||||
|
const handleFileChange = (e) => {
|
||||||
|
const files = Array.from(e.target.files)
|
||||||
|
const totalFiles = form.value.foto.length + files.length
|
||||||
|
|
||||||
|
if (totalFiles > 2) {
|
||||||
|
alert('Maksimal 2 foto!')
|
||||||
|
e.target.value = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
// Validate file size (2MB) and type
|
||||||
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
|
alert(`File ${file.name} terlalu besar! Maksimal 2MB.`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)) {
|
||||||
|
alert(`File ${file.name} harus berupa JPEG atau PNG!`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.value.foto.push(file)
|
||||||
|
previews.value.push(URL.createObjectURL(file))
|
||||||
|
})
|
||||||
|
|
||||||
|
e.target.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeFile = (index) => {
|
||||||
|
form.value.foto.splice(index, 1)
|
||||||
|
previews.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const konfirmasi = async () => {
|
||||||
|
try {
|
||||||
|
// Basic client-side validation
|
||||||
|
if (!form.value.nama_pemesan || !form.value.email) {
|
||||||
|
alert('Harap isi kolom wajib (Nama Pemesan, Email)!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('nama_pemesan', form.value.nama_pemesan)
|
||||||
|
data.append('email', form.value.email)
|
||||||
|
data.append('no_tlpn', form.value.no_telepon)
|
||||||
|
data.append('template_slug', 'undangan-ulang-tahun-starter')
|
||||||
|
|
||||||
|
// Append form fields individually to ensure Laravel receives them as an array
|
||||||
|
for (const [key, value] of Object.entries(form.value.form)) {
|
||||||
|
data.append(`form[${key}]`, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
form.value.foto.forEach((file, index) => {
|
||||||
|
data.append(`foto[${index}]`, file)
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log([...data]) // untuk debugging
|
||||||
|
|
||||||
|
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await res.json()
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
if (res.status === 422) {
|
||||||
|
const errors = result.errors || {}
|
||||||
|
const errorMessages = Object.values(errors).flat().join('\n')
|
||||||
|
throw new Error(errorMessages || result.message || 'Validasi gagal')
|
||||||
|
}
|
||||||
|
if (res.status === 404) {
|
||||||
|
throw new Error(result.message || 'Template tidak ditemukan')
|
||||||
|
}
|
||||||
|
throw new Error(result.message || 'Gagal mengirim data')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(result.message || 'Data berhasil disimpan!')
|
||||||
|
router.push('/')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
alert('Terjadi kesalahan: ' + err.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const batal = () => router.back()
|
||||||
|
</script>
|
||||||
@ -1,5 +1,7 @@
|
|||||||
<template>
|
<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 -->
|
<!-- Loading State -->
|
||||||
<div v-if="pending" class="text-center">
|
<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>
|
<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 -->
|
<!-- Error State -->
|
||||||
<div v-else-if="error || !data" class="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
|
<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">
|
<svg class="w-12 h-12 mx-auto text-red-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||||
<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>
|
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>
|
</svg>
|
||||||
<p class="text-lg font-semibold text-gray-800 mb-2">Undangan Tidak Ditemukan</p>
|
<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>
|
<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
|
Kembali ke Beranda
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
@ -50,7 +56,10 @@ const { data, pending, error } = await useAsyncData(
|
|||||||
async () => {
|
async () => {
|
||||||
try {
|
try {
|
||||||
const response = await $fetch(`${backendUrl}/api/pelanggans/code/${route.params.code}`)
|
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
|
// Check if the API response indicates failure
|
||||||
if (!response.success) {
|
if (!response.success) {
|
||||||
throw createError({
|
throw createError({
|
||||||
@ -94,6 +103,12 @@ const { data, pending, error } = await useAsyncData(
|
|||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
'undangan-minimalis': defineAsyncComponent(() => import('~/components/undangan/undangan-minimalis.vue')),
|
'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
|
// Add more mappings as templates are developed
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +117,14 @@ const dynamicComponent = computed(() => {
|
|||||||
return componentMap[data.value.template.slug] || null
|
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
|
// Set meta tags only if data exists
|
||||||
useHead(() => ({
|
useHead(() => ({
|
||||||
title: data.value?.nama_pemesan || 'Undangan Tak Bernama',
|
title: data.value?.nama_pemesan || 'Undangan Tak Bernama',
|
||||||
|
|||||||
@ -31,5 +31,5 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import WeddingA from '~/components/templates/wedding/WeddingA.vue'
|
import WeddingA from '~/components/templates/wedding/weddingA.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user