@ -1,121 +1,179 @@
< 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 >
< 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 . 2 MB ) < / span >
<!-- 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" >
<!-- Nama Pemesan -- >
< div class = "relative" >
< input v -model = " form.nama_pemesan " type = "text" placeholder = " " required
class = "peer w-full border border-gray-300 rounded-lg px-2.5 pt-4 pb-1.5 text-sm focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" / >
< label class = " absolute left -1 top -0 text -gray -400 text -xs transition -all
peer - placeholder - shown : top - 4 peer - placeholder - shown : left - 2 peer - placeholder - shown : text - gray - 400 peer - placeholder - shown : text - sm
peer - focus : top - 0 peer - focus : left - 1 peer - focus : text - [ 10 px ] peer - focus : text - blue - 500
peer - valid : top - 0 peer - valid : left - 1 peer - valid : text - [ 10 px ] peer - valid : text - gray - 500 " >
Nama Pemesan
< / 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 >
<!-- Email -- >
< div class = "relative" >
< input v -model = " form.email " type = "email" placeholder = " " required
class = "peer w-full border border-gray-300 rounded-lg px-2.5 pt-4 pb-1.5 text-sm focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" / >
< label class = " absolute left -1 top -0 text -gray -400 text -xs transition -all
peer - placeholder - shown : top - 4 peer - placeholder - shown : left - 2 peer - placeholder - shown : text - gray - 400 peer - placeholder - shown : text - sm
peer - focus : top - 0 peer - focus : left - 1 peer - focus : text - [ 10 px ] peer - focus : text - blue - 500
peer - valid : top - 0 peer - valid : left - 1 peer - valid : text - [ 10 px ] peer - valid : text - gray - 500 " >
Email
< / label >
< / div >
<!-- No Telepon -- >
< div class = "relative" >
< input v -model = " form.no_tlpn " type = "text" placeholder = " " required
class = "peer w-full border border-gray-300 rounded-lg px-2.5 pt-4 pb-1.5 text-sm focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" / >
< label class = " absolute left -1 top -0 text -gray -400 text -xs transition -all
peer - placeholder - shown : top - 4 peer - placeholder - shown : left - 2 peer - placeholder - shown : text - gray - 400 peer - placeholder - shown : text - sm
peer - focus : top - 0 peer - focus : left - 1 peer - focus : text - [ 10 px ] peer - focus : text - blue - 500
peer - valid : top - 0 peer - valid : left - 1 peer - valid : text - [ 10 px ] peer - valid : text - gray - 500 " >
No Telepon
< / label >
< / div >
< / 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 class = "grid gap-4" >
< template v -for = " ( field , i ) in [
{ key : 'nama_lengkap' , label : 'Nama Lengkap' } ,
{ key : 'nama_bapak' , label : 'Nama Bapak' } ,
{ key : 'nama_ibu' , label : 'Nama Ibu' } ,
{ key : 'umur_yang_dirayakan' , label : 'Umur yang Dirayakan' , type : 'number' } ,
{ key : 'anak_ke' , label : 'Anak Ke' , type : 'number' }
] " :key=" i " >
< div class = "relative" >
< input v -model = " form.form [ field.key ] " : type = "field.type || 'text'" required placeholder = " "
class = "peer w-full border border-gray-300 rounded-lg px-2.5 pt-4 pb-1.5 text-sm focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" / >
< label class = " absolute left -1 top -0 text -gray -400 text -xs transition -all
peer - placeholder - shown : top - 4 peer - placeholder - shown : left - 2 peer - placeholder - shown : text - gray - 400 peer - placeholder - shown : text - sm
peer - focus : top - 0 peer - focus : left - 1 peer - focus : text - [ 10 px ] peer - focus : text - blue - 500
peer - valid : top - 0 peer - valid : left - 1 peer - valid : text - [ 10 px ] peer - valid : text - gray - 500 " >
{ { field . label } }
< / label >
< / div >
< / template >
< / 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" >
< div class = "relative" >
< input v -model = " form.form.hari_tanggal_acara " type = "date" required placeholder = " "
class = "peer w-full border border-gray-300 rounded-lg px-2.5 pt-4 pb-1.5 text-sm focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" / >
< label class = " absolute left -2 text -gray -400 text -xs transition -all
peer - placeholder - shown : top - 4 peer - placeholder - shown : text - sm
peer - focus : top - 0 peer - focus : text - [ 10 px ] peer - focus : text - blue - 500
peer - valid : top - 0 peer - valid : text - [ 10 px ] peer - valid : text - gray - 500 " >
Hari & Tanggal Acara
< / label >
< / div >
< div class = "relative" >
< input v -model = " form.form.waktu " type = "time" required placeholder = " "
class = "peer w-full border border-gray-300 rounded-lg px-2.5 pt-4 pb-1.5 text-sm focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" / >
< label class = " absolute left -2 text -gray -400 text -xs transition -all
peer - placeholder - shown : top - 4 peer - placeholder - shown : text - sm
peer - focus : top - 0 peer - focus : text - [ 10 px ] peer - focus : text - blue - 500
peer - valid : top - 0 peer - valid : text - [ 10 px ] peer - valid : text - gray - 500 " >
Waktu
< / label >
< / div >
< div class = "relative md:col-span-2" >
< textarea v -model = " form.form.alamat " rows = "4" required placeholder = " "
class = "peer w-full border border-gray-300 rounded-lg px-2.5 pt-4 pb-1.5 text-sm focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition resize-none" / >
< label class = " absolute left -1 top -0 text -gray -400 text -xs transition -all
peer - placeholder - shown : top - 4 peer - placeholder - shown : left - 2 peer - placeholder - shown : text - gray - 400 peer - placeholder - shown : text - sm
peer - focus : top - 0 peer - focus : left - 1 peer - focus : text - [ 10 px ] peer - focus : text - blue - 500
peer - valid : top - 0 peer - valid : left - 1 peer - valid : text - [ 10 px ] peer - valid : text - gray - 500 " >
Alamat
< / label >
< / div >
< div class = "relative md:col-span-2" >
< input v -model = " form.form.link_gmaps " type = "text" required placeholder = " "
class = "peer w-full border border-gray-300 rounded-lg px-2.5 pt-4 pb-1.5 text-sm focus:ring-2 focus:ring-blue-400 focus:border-blue-400 outline-none transition" / >
< label class = " absolute left -1 top -0 text -gray -400 text -xs transition -all
peer - placeholder - shown : top - 4 peer - placeholder - shown : left - 2 peer - placeholder - shown : text - gray - 400 peer - placeholder - shown : text - sm
peer - focus : top - 0 peer - focus : left - 1 peer - focus : text - [ 10 px ] peer - focus : text - blue - 500
peer - valid : top - 0 peer - valid : left - 1 peer - valid : text - [ 10 px ] peer - valid : text - gray - 500 " >
Link Gmaps
< / label >
< / div >
< / 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 . 2 MB ) < / 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" >
X
< / 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'
import Swal from 'sweetalert2'
const router = useRouter ( )
const ADMIN _WA = '62895333053398' / / G a n t i d e n g a n n o m o r a d m i n y a n g b e n a r
const form = ref ( {
nama _pemesan : '' ,
@ -123,7 +181,10 @@ const form = ref({
no _tlpn : '' ,
form : {
nama _lengkap : '' ,
nama _bapak : '' ,
nama _ibu : '' ,
umur _yang _dirayakan : '' ,
anak _ke : '' ,
hari _tanggal _acara : '' ,
waktu : '' ,
alamat : ''
@ -138,19 +199,18 @@ const handleFileChange = (e) => {
const totalFiles = form . value . foto . length + files . length
if ( totalFiles > 2 ) {
alert( 'Maksimal 2 foto! ')
Swal. fire ( 'Oops!' , 'Maksimal 2 foto!' , 'warning ')
e . target . value = ''
return
}
files . forEach ( file => {
/ / V a l i d a t e f i l e s i z e ( 2 M B ) a n d t y p e
if ( file . size > 2 * 1024 * 1024 ) {
alert( ` File ${ file . name } terlalu besar! Maksimal 2MB. ` )
Swal. fire ( 'File Terlalu Besar!' , ` ${ file . name } melebihi 2MB ` , 'error' )
return
}
if ( ! [ 'image/jpeg' , 'image/png' , 'image/jpg' ] . includes ( file . type ) ) {
alert( ` File ${ file . name } harus berupa JPEG atau PNG! ` )
Swal. fire ( 'Format Salah!' , ` ${ file . name } harus JPEG/PNG ` , 'error' )
return
}
form . value . foto . push ( file )
@ -167,29 +227,26 @@ const removeFile = (index) => {
const konfirmasi = async ( ) => {
try {
/ / Ba s i c c l i e n t - s i d e v a l i d a t i o n
/ / Va l i d a s i w a j i b
if ( ! form . value . nama _pemesan || ! form . value . email ) {
alert( 'Harap isi kolom wajib (Nama Pemesan, Email)! ')
Swal. fire ( 'Data Kurang!' , 'Nama Pemesan dan Email wajib diisi!' , 'warning ')
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 _t e le po n)
data . append ( 'no_tlpn' , form . value . no _t lpn)
data . append ( 'template_slug' , 'undangan-ulang-tahun-starter' )
/ / A p p e n d f o r m f i e l d s i n d i v i d u a l l y t o e n s u r e L a r a v e l r e c e i v e s t h e m a s a n a r r a y
for ( const [ key , value ] of Object . entries ( form . value . form ) ) {
data . append ( ` form[ ${ key } ] ` , value )
data . append ( ` form[ ${ key } ] ` , value || '-' )
}
form . value . foto . forEach ( ( file , index ) => {
data . append ( ` foto[ ${ index } ] ` , file )
} )
console . log ( [ ... data ] ) / / u n t u k d e b u g g i n g
const res = await fetch ( 'http://localhost:8000/api/pelanggans' , {
method : 'POST' ,
body : data
@ -201,19 +258,73 @@ const konfirmasi = async () => {
if ( res . status === 422 ) {
const errors = result . errors || { }
const errorMessages = Object . values ( errors ) . flat ( ) . join ( '\n' )
throw new Error ( errorMessages || result . message || 'Validasi gagal' )
throw new Error ( errorMessages || 'Validasi gagal' )
}
if ( res . status === 404 ) {
throw new Error ( result . message || 'Template tidak ditemukan' )
throw new Error ( 'Template tidak ditemukan' )
}
throw new Error ( result . message || 'Gagal mengirim data' )
}
alert ( result . message || 'Data berhasil disimpan!' )
router . push ( '/' )
/ / = = = S U K S E S : T a m p i l k a n S w e e t A l e r t = = =
await Swal . fire ( {
title : 'Berhasil!' ,
text : result . message || 'Data undangan kamu berhasil disimpan.' ,
icon : 'success' ,
confirmButtonText : 'Lanjut ke WhatsApp' ,
confirmButtonColor : '#2563eb' ,
background : '#f9fafb' ,
color : '#111827' ,
showClass : { popup : 'animate__animated animate__fadeInDown' } ,
hideClass : { popup : 'animate__animated animate__fadeOutUp' }
} )
/ / = = = B U A T P E S A N W H A T S A P P = = =
const f = form . value
const child = f . form
const message = `
Halo Admin ,
Saya ingin membuat * Undangan Ulang Tahun Starter *
* Data Pemesan *
- Nama : $ { f . nama _pemesan }
- Email : $ { f . email }
- No . Telepon : $ { f . no _tlpn }
* Data Anak *
- Nama Lengkap : $ { child . nama _lengkap }
- Bapak : $ { child . nama _bapak || '-' }
- Ibu : $ { child . nama _ibu || '-' }
- Umur : $ { child . umur _yang _dirayakan } tahun
- Anak ke : $ { child . anak _ke || '-' }
* Detail Acara *
- Tanggal : $ { child . hari _tanggal _acara }
- Waktu : $ { child . waktu }
- Alamat : $ { child . alamat }
Terima kasih
` .trim()
const encodedMsg = encodeURIComponent ( message )
const waUrl = ` https://wa.me/ ${ ADMIN _WA } ?text= ${ encodedMsg } `
/ / = = = B U K A W H A T S A P P = = =
Swal . fire ( {
title : 'Membuka WhatsApp...' ,
text : 'Mohon tunggu sebentar' ,
timer : 1500 ,
timerProgressBar : true ,
showConfirmButton : false ,
willClose : ( ) => {
window . open ( waUrl , '_blank' )
router . push ( '/' )
}
} )
} catch ( err ) {
console . error ( err )
alert ( 'Terjadi kesalahan: ' + err . message )
Swal . fire ( 'Gagal' , err . message , 'error' )
}
}