Undangan/proyek-frontend/app/pages/form/[id].vue
2025-09-19 14:33:03 +07:00

349 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[id].vue
<template>
<div class="max-w-3xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-4">Form Pemesanan</h1>
<!-- Error umum -->
<div v-if="error" class="bg-red-100 text-red-600 p-3 rounded mb-4">
{{ error }}
</div>
<!-- Loading -->
<div v-else-if="loading">Memuat data template...</div>
<!-- Form -->
<div v-else>
<div class="mb-4">
<p class="text-green-600 font-bold text-lg">Rp {{ Number(template.harga).toLocaleString('id-ID') }}</p>
<img v-if="template.thumbnail" :src="template.thumbnail" alt="Preview Template" class="w-64 h-40 object-cover rounded" />
</div>
<!-- Fitur dinamis -->
<div class="mt-4">
<h3 class="font-medium mb-2">Isi Data Fitur:</h3>
<div v-for="fitur in template.fiturs" :key="fitur.id" class="mb-4">
<label class="block font-medium mb-1">{{ fitur.deskripsi }}</label>
<!-- Jika fitur adalah galeri -->
<div v-if="isGallery(fitur.deskripsi)">
<input
type="file"
:multiple="true"
:accept="acceptedImageTypes"
@change="handleGalleryChange($event, fitur.id, parseGalleryMax(fitur.deskripsi))"
class="w-full"
:ref="`fileInput_${fitur.id}`"
/>
<p class="text-sm text-gray-500 mt-1">
Maks. {{ parseGalleryMax(fitur.deskripsi) || defaultGalleryMax }} file.
(Terpilih: {{ (files[`fitur_${fitur.id}`] || []).length }})
</p>
<p v-if="fileErrors[`fitur_${fitur.id}`]" class="text-sm text-red-600 mt-1">
{{ fileErrors[`fitur_${fitur.id}`] }}
</p>
<!-- Preview gambar yang dipilih -->
<div v-if="files[`fitur_${fitur.id}`] && files[`fitur_${fitur.id}`].length > 0" class="mt-2 flex flex-wrap gap-2">
<div v-for="(file, index) in files[`fitur_${fitur.id}`]" :key="index" class="relative">
<img :src="getFilePreview(file)" alt="Preview" class="w-20 h-20 object-cover rounded border" />
<button
@click="removeFile(`fitur_${fitur.id}`, index)"
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 text-xs flex items-center justify-center"
type="button"
>
×
</button>
</div>
</div>
</div>
<!-- Jika fitur adalah tanggal -->
<input
v-else-if="fitur.deskripsi.toLowerCase().includes('tanggal')"
type="date"
v-model="formFields[fieldNameById(fitur.id)]"
class="w-full border rounded px-3 py-2"
/>
<!-- default text -->
<input
v-else
type="text"
v-model="formFields[fieldNameById(fitur.id)]"
placeholder="Isi data..."
class="w-full border rounded px-3 py-2"
/>
</div>
</div>
<!-- Data Pemesan -->
<div class="mt-6">
<label class="block font-medium">Nama Pemesan *</label>
<input v-model="baseForm.nama_pemesan" type="text" class="w-full border rounded px-3 py-2 mb-3" required />
<label class="block font-medium">Email *</label>
<input v-model="baseForm.email" type="email" class="w-full border rounded px-3 py-2 mb-3" required />
<label class="block font-medium">Nomor HP *</label>
<input v-model="baseForm.no_hp" type="text" class="w-full border rounded px-3 py-2 mb-3" required />
<label class="block font-medium">Catatan</label>
<textarea v-model="baseForm.catatan" class="w-full border rounded px-3 py-2 mb-3"></textarea>
</div>
<!-- Submit -->
<button @click="submitForm" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50" :disabled="isSubmitting">
{{ isSubmitting ? 'Mengirim...' : 'Kirim Pesanan' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const templateId = route.query.template_id
const template = ref({ fiturs: [] })
const loading = ref(true)
const error = ref(null)
const isSubmitting = ref(false)
// base form fields (pemesan)
const baseForm = ref({
nama_pemesan: '',
email: '',
no_hp: '',
catatan: ''
})
// formFields menyimpan nilai text/date untuk tiap fitur keyed by fieldNameById(fitur.id)
const formFields = ref({}) // { "fitur_1": "Budi", "fitur_2": "2025-09-20", ... }
// files menyimpan File[] per fitur.id
const files = ref({}) // { "fitur_3": [File, File], ... }
const fileErrors = ref({}) // error message per gallery field
const defaultGalleryMax = 10
const acceptedImageTypes = 'image/*'
/** ---------- helper ---------- **/
const slugify = (text) =>
text.toString().toLowerCase().trim().replace(/\s+/g, '_').replace(/[^\w\-]/g, '')
// fieldName derived from description (used for backend field name logic)
const fieldName = (deskripsi) => slugify(deskripsi)
// fieldNameById used to link formFields stored by fitur.id
const fieldNameById = (id) => `fitur_${id}`
// cek apakah deskripsi adalah gallery
const isGallery = (deskripsi) => deskripsi.toLowerCase().includes('galeri') || deskripsi.toLowerCase().includes('gallery')
// parse number from "Galeri 3" -> 3. Returns null if not found
const parseGalleryMax = (deskripsi) => {
const m = deskripsi.match(/(\d+)/)
return m ? parseInt(m[1], 10) : null
}
// create preview URL for file
const getFilePreview = (file) => {
return URL.createObjectURL(file)
}
// remove file from selection
const removeFile = (fiturKey, index) => {
if (files.value[fiturKey]) {
files.value[fiturKey].splice(index, 1)
}
}
/** ---------- fetch template ---------- **/
onMounted(async () => {
try {
const res = await fetch(`http://localhost:8000/api/templates/${templateId}`)
if (!res.ok) throw new Error('Gagal memuat data template')
const data = await res.json()
// backend might wrap as {template:..., fiturs:...} or return template object directly.
// normalize: if data.template exists, use it, else assume data is template
const tpl = data.template ?? data
template.value = tpl
// setup empty formFields and files for each fitur
tpl.fiturs?.forEach(f => {
formFields.value[fieldNameById(f.id)] = ''
// gallery init
if (isGallery(f.deskripsi)) {
files.value[fieldNameById(f.id)] = []
fileErrors.value[fieldNameById(f.id)] = ''
}
})
} catch (err) {
console.error(err)
error.value = err.message || 'Gagal memuat template'
} finally {
loading.value = false
}
})
/** ---------- handle gallery selection ---------- **/
function handleGalleryChange(event, fiturId, maxAllowed) {
const selected = Array.from(event.target.files || [])
const allowed = maxAllowed || defaultGalleryMax
const fiturKey = fieldNameById(fiturId)
// reset error
fileErrors.value[fiturKey] = ''
// client-side validation for file count
if (selected.length > allowed) {
fileErrors.value[fiturKey] = `Jumlah file melebihi batas (${allowed}). Pilih maksimal ${allowed} file.`
files.value[fiturKey] = []
event.target.value = '' // reset input
return
}
// optional: validate file types and sizes (e.g., < 10MB)
const tooLarge = selected.find(f => f.size > 10 * 1024 * 1024) // 10MB
if (tooLarge) {
fileErrors.value[fiturKey] = 'Satu atau lebih file melebihi 10MB.'
files.value[fiturKey] = []
event.target.value = ''
return
}
// validate image types
const invalidType = selected.find(f => !f.type.startsWith('image/'))
if (invalidType) {
fileErrors.value[fiturKey] = 'File harus berupa gambar (JPG, PNG, GIF, dll).'
files.value[fiturKey] = []
event.target.value = ''
return
}
// all good
files.value[fiturKey] = selected
}
/** ---------- submit ---------- **/
const submitForm = async () => {
if (isSubmitting.value) return
error.value = null
isSubmitting.value = true
// client basic check: required base fields
if (!baseForm.value.nama_pemesan || !baseForm.value.no_hp || !baseForm.value.email) {
error.value = 'Nama pemesan, No HP, dan Email wajib diisi.'
isSubmitting.value = false
return
}
// validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(baseForm.value.email)) {
error.value = 'Format email tidak valid.'
isSubmitting.value = false
return
}
try {
// use FormData because there may be files
const fd = new FormData()
fd.append('template_id', templateId)
fd.append('nama_pemesan', baseForm.value.nama_pemesan)
fd.append('email', baseForm.value.email)
fd.append('no_hp', baseForm.value.no_hp)
fd.append('catatan', baseForm.value.catatan || '')
// append feature values (text/date) and files
template.value.fiturs?.forEach(fitur => {
const fiturKey = fieldNameById(fitur.id)
const fieldNameForBackend = fieldName(fitur.deskripsi)
// for gallery fields, append files
if (isGallery(fitur.deskripsi)) {
const fileArray = files.value[fiturKey] || []
console.log(`Appending ${fileArray.length} files for ${fieldNameForBackend}`)
if (fileArray.length > 0) {
// append files with proper array notation for Laravel
fileArray.forEach((file, index) => {
fd.append(`${fieldNameForBackend}[]`, file)
console.log(`Added file ${index}:`, file.name, file.type, file.size)
})
}
} else {
// for text/date fields
const value = formFields.value[fiturKey]
if (value !== undefined && value !== null && String(value).trim() !== '') {
fd.append(fieldNameForBackend, value)
}
}
})
// Debug: log FormData contents
console.log('FormData contents:')
for (let pair of fd.entries()) {
console.log(pair[0] + ':', pair[1])
}
// fetch POST (do NOT set Content-Type — browser sets multipart boundary)
const res = await fetch('http://localhost:8000/api/form', {
method: 'POST',
body: fd,
})
const data = await res.json()
console.log('Response:', data)
if (!res.ok) {
if (data.errors) {
// flatten errors object from Laravel
const errorMessages = Object.entries(data.errors).map(([field, messages]) => {
return `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`
})
error.value = errorMessages.join(' | ')
} else {
error.value = data.message || 'Gagal mengirim form'
}
return
}
alert('Pesanan berhasil dikirim!')
// reset inputs
resetForm()
} catch (err) {
console.error('Submit error:', err)
error.value = 'Terjadi kesalahan saat mengirim form.'
} finally {
isSubmitting.value = false
}
}
// reset form function
const resetForm = () => {
baseForm.value = { nama_pemesan: '', email: '', no_hp: '', catatan: '' }
// reset feature fields and files
template.value.fiturs?.forEach(f => {
const fiturKey = fieldNameById(f.id)
formFields.value[fiturKey] = ''
if (isGallery(f.deskripsi)) {
files.value[fiturKey] = []
fileErrors.value[fiturKey] = ''
}
})
// reset file inputs
document.querySelectorAll('input[type="file"]').forEach(input => {
input.value = ''
})
}
</script>
<style scoped>
/* Custom styling if needed */
</style>