featured templates
This commit is contained in:
parent
3da77dad53
commit
47ad559659
@ -14,6 +14,23 @@ class TemplateSeeder extends Seeder
|
|||||||
$k2 = Kategori::firstOrCreate(['nama' => 'ulang_tahun']);
|
$k2 = Kategori::firstOrCreate(['nama' => 'ulang_tahun']);
|
||||||
$k3 = Kategori::firstOrCreate(['nama' => 'khitan']);
|
$k3 = Kategori::firstOrCreate(['nama' => 'khitan']);
|
||||||
|
|
||||||
|
|
||||||
|
Template::create([
|
||||||
|
'nama_template' => 'Undangan Minimalis',
|
||||||
|
'harga' => 100000,
|
||||||
|
'paket' => 'starter',
|
||||||
|
'form' => [
|
||||||
|
'fields' => [
|
||||||
|
['name' => 'nama_pengantin', 'label' => 'Nama Pengantin', 'type' => 'text', 'required' => true],
|
||||||
|
['name' => 'tanggal_acara', 'label' => 'Tanggal Acara', 'type' => 'date', 'required' => true],
|
||||||
|
['name' => 'lokasi', 'label' => 'Lokasi', 'type' => 'text'],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'kategori_id' => $k1->id,
|
||||||
|
'foto' => 'templates/Pernikahan.jpg', // taruh di storage/app/public/templates/
|
||||||
|
]);
|
||||||
|
|
||||||
|
|
||||||
// Template Pernikahan Premium
|
// Template Pernikahan Premium
|
||||||
Template::create([
|
Template::create([
|
||||||
'nama_template' => 'Undangan Pernikahan Premium',
|
'nama_template' => 'Undangan Pernikahan Premium',
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
|
|
||||||
// ID template yang mau ditampilkan
|
// ID template yang mau ditampilkan
|
||||||
const selectedIds = [1, 2, 3, 5, 6]
|
const selectedIds = [3, 4, 5, 6, 7, 8, 9]
|
||||||
|
|
||||||
// State dropdown
|
// State dropdown
|
||||||
const openDropdownId = ref(null)
|
const openDropdownId = ref(null)
|
||||||
@ -54,6 +54,15 @@ const paketData = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
// 🔥 Mapping nama_template ke form path (cukup tambah di sini kalau ada form baru)
|
||||||
|
const formMapping = {
|
||||||
|
'Undangan Pernikahan Premium': '/form/pernikahan/b',
|
||||||
|
'Undangan Minimalis': '/form/pernikahan/a',
|
||||||
|
'Undangan Ulang Tahun Premium': '/form/ulang-tahun/a',
|
||||||
|
'Undangan Khitan Premium': '/form/khitan/a',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Fetch data template dari backend (nama_template, harga, kategori, foto)
|
// Fetch data template dari backend (nama_template, harga, kategori, foto)
|
||||||
const { data: templatesData, error } = await useFetch('http://localhost:8000/api/templates')
|
const { data: templatesData, error } = await useFetch('http://localhost:8000/api/templates')
|
||||||
|
|
||||||
@ -61,15 +70,18 @@ const { data: templatesData, error } = await useFetch('http://localhost:8000/api
|
|||||||
const templates = computed(() =>
|
const templates = computed(() =>
|
||||||
(templatesData.value || [])
|
(templatesData.value || [])
|
||||||
.filter(t => selectedIds.includes(t.id))
|
.filter(t => selectedIds.includes(t.id))
|
||||||
.map((t, index) => ({
|
.map((t, index) => {
|
||||||
id: t.id,
|
return {
|
||||||
nama_template: t.nama_template,
|
id: t.id,
|
||||||
harga: t.harga,
|
nama_template: t.nama_template,
|
||||||
foto: t.foto || '/default.jpg', // fallback jika foto kosong
|
harga: t.harga,
|
||||||
paket: paketData[index % paketData.length].paket,
|
foto: t.foto || '/default.jpg',
|
||||||
fiturs: paketData[index % paketData.length].fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })),
|
paket: paketData[index % paketData.length].paket,
|
||||||
kategori: t.kategori
|
fiturs: paketData[index % paketData.length].fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })),
|
||||||
}))
|
kategori: t.kategori,
|
||||||
|
formPath: formMapping[t.nama_template] || '/form/lainny' // 🔥 ambil path dari mapping
|
||||||
|
}
|
||||||
|
})
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -85,7 +97,8 @@ const templates = computed(() =>
|
|||||||
|
|
||||||
<!-- Grid Template -->
|
<!-- Grid Template -->
|
||||||
<div v-if="templates.length" class="grid gap-8 max-w-[1100px] mx-auto grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
<div v-if="templates.length" class="grid gap-8 max-w-[1100px] mx-auto grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<div v-for="t in templates" :key="t.id" class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
|
<div v-for="t in templates" :key="t.id"
|
||||||
|
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
|
||||||
<!-- Gambar -->
|
<!-- Gambar -->
|
||||||
<img :src="`http://localhost:8000${t.foto}`" :alt="t.nama_template" class="w-full h-48 object-cover" />
|
<img :src="`http://localhost:8000${t.foto}`" :alt="t.nama_template" class="w-full h-48 object-cover" />
|
||||||
|
|
||||||
@ -102,14 +115,18 @@ const templates = computed(() =>
|
|||||||
<button @click="toggleDropdown(t.id)"
|
<button @click="toggleDropdown(t.id)"
|
||||||
class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 inline-flex justify-between items-center">
|
class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 inline-flex justify-between items-center">
|
||||||
<span class="mx-auto text-gray-700 font-semibold">FITUR YANG TERSEDIA</span>
|
<span class="mx-auto text-gray-700 font-semibold">FITUR YANG TERSEDIA</span>
|
||||||
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
fill="currentColor">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="fade">
|
<transition name="fade">
|
||||||
<div v-if="openDropdownId === t.id" class="mt-4">
|
<div v-if="openDropdownId === t.id" class="mt-4">
|
||||||
<ul class="space-y-2 text-gray-600 text-left max-h-60 overflow-y-auto px-3 py-2 border border-gray-200 rounded-md shadow-inner bg-gray-50">
|
<ul
|
||||||
|
class="space-y-2 text-gray-600 text-left max-h-60 overflow-y-auto px-3 py-2 border border-gray-200 rounded-md shadow-inner bg-gray-50">
|
||||||
<li v-for="f in t.fiturs" :key="f.id" class="flex items-center">
|
<li v-for="f in t.fiturs" :key="f.id" class="flex items-center">
|
||||||
<svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
||||||
@ -123,15 +140,16 @@ const templates = computed(() =>
|
|||||||
|
|
||||||
<!-- Tombol -->
|
<!-- Tombol -->
|
||||||
<div class="flex items-center gap-3 mt-6">
|
<div class="flex items-center gap-3 mt-6">
|
||||||
<button class="w-full bg-white border border-gray-300 text-gray-800 font-semibold py-2 px-4 rounded-lg hover:bg-gray-100 transition-colors">
|
<button
|
||||||
|
class="w-full bg-white border border-gray-300 text-gray-800 font-semibold py-2 px-4 rounded-lg hover:bg-gray-100 transition-colors">
|
||||||
Preview
|
Preview
|
||||||
</button>
|
</button>
|
||||||
<NuxtLink :to="`/form/${t.kategori ? t.kategori.toLowerCase().replace(/ /g, '-') : 'lainnya'}?template_id=${t.id}`"
|
<NuxtLink :to="`${t.formPath}?template_id=${t.id}`"
|
||||||
class="w-full bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors text-center">
|
class="w-full bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors text-center">
|
||||||
Order
|
Order
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>1
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -149,14 +167,19 @@ const templates = computed(() =>
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* animasi dropdown smooth */
|
/* animasi dropdown smooth */
|
||||||
.fade-enter-active, .fade-leave-active {
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
.fade-enter-from, .fade-leave-to {
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-5px);
|
transform: translateY(-5px);
|
||||||
}
|
}
|
||||||
.fade-enter-to, .fade-leave-from {
|
|
||||||
|
.fade-enter-to,
|
||||||
|
.fade-leave-from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,349 +0,0 @@
|
|||||||
[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 />
|
|
||||||
|
|
||||||
|
|
||||||
</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.params.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>
|
|
||||||
14
proyek-frontend/app/pages/form/khitan/b.vue
Normal file
14
proyek-frontend/app/pages/form/khitan/b.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-10 max-w-2xl mx-auto">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Form Pemesanan - Khitan B</h1>
|
||||||
|
<form class="space-y-4">
|
||||||
|
<input type="text" placeholder="Nama Pemesan" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<input type="text" placeholder="Nama Anak" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<input type="date" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<textarea placeholder="Alamat Acara" class="border px-4 py-2 rounded w-full"></textarea>
|
||||||
|
<button class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
|
||||||
|
Kirim
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -9,7 +9,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form @submit.prevent="submitForm" class="space-y-10">
|
<form @submit.prevent="submitForm" class="space-y-10">
|
||||||
|
|
||||||
<!-- Paket Starter (Informasi) -->
|
<!-- Paket Starter (Informasi) -->
|
||||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Paket Undangan</h2>
|
<h2 class="text-lg font-bold text-gray-800 mb-4">Paket Undangan</h2>
|
||||||
@ -99,7 +98,6 @@
|
|||||||
<label class="block text-sm mb-1">Lokasi</label>
|
<label class="block text-sm mb-1">Lokasi</label>
|
||||||
<input v-model="acara.lokasi" type="text" class="input" required />
|
<input v-model="acara.lokasi" type="text" class="input" required />
|
||||||
</div>
|
</div>
|
||||||
<!-- Request Lagu -->
|
|
||||||
<div class="md:col-span-2">
|
<div class="md:col-span-2">
|
||||||
<label class="block text-sm mb-1">Request Lagu</label>
|
<label class="block text-sm mb-1">Request Lagu</label>
|
||||||
<input v-model="acara.request_lagu" type="text" class="input"
|
<input v-model="acara.request_lagu" type="text" class="input"
|
||||||
@ -130,11 +128,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from "vue"
|
import { ref, onMounted } from "vue"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
selectedPaket: "starter",
|
selectedPaket: "starter",
|
||||||
nama_template: "undangan minimalis",
|
nama_template: "Undangan Minimalis",
|
||||||
kategori: "Pernikahan",
|
kategori: "Pernikahan",
|
||||||
harga: "100.000",
|
harga: "100.000",
|
||||||
tanggal_pemesanan: new Date().toLocaleDateString("id-ID"),
|
tanggal_pemesanan: new Date().toLocaleDateString("id-ID"),
|
||||||
@ -154,62 +153,77 @@ const error = ref(false)
|
|||||||
const template = ref(null)
|
const template = ref(null)
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
|
const adminWaNumber = "6281234567890" // ganti dengan nomor WA admin
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
try {
|
try {
|
||||||
const templateId = route.query.template_id // ⬅️ ambil dari query string
|
const templateId = route.query.template_id
|
||||||
if (!templateId) throw new Error("Template ID tidak ditemukan di URL")
|
if (!templateId) throw new Error("Template ID tidak ditemukan di URL")
|
||||||
|
|
||||||
const res = await fetch(`http://localhost:8000/api/templates/${templateId}`)
|
const res = await fetch(`http://localhost:8000/api/templates/${templateId}`)
|
||||||
if (!res.ok) throw new Error("Gagal ambil template")
|
if (!res.ok) throw new Error("Gagal ambil template")
|
||||||
template.value = await res.json()
|
template.value = await res.json()
|
||||||
|
|
||||||
// isi form info template
|
|
||||||
form.value.nama_template = template.value.nama_template
|
form.value.nama_template = template.value.nama_template
|
||||||
form.value.kategori = template.value.kategori?.nama || ""
|
form.value.kategori = template.value.kategori?.nama || "Pernikahan"
|
||||||
form.value.harga = template.value.harga
|
form.value.harga = new Intl.NumberFormat('id-ID').format(template.value.harga)
|
||||||
form.value.tanggal_pemesanan = new Date().toLocaleDateString("id-ID")
|
form.value.tanggal_pemesanan = new Date().toLocaleDateString("id-ID")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Error fetch template:", err)
|
console.error("Error fetch template:", err)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
try {
|
try {
|
||||||
|
if (!template.value) {
|
||||||
|
throw new Error("Template belum dimuat, tidak bisa submit!")
|
||||||
|
}
|
||||||
loading.value = true
|
loading.value = true
|
||||||
success.value = false
|
success.value = false
|
||||||
error.value = false
|
error.value = false
|
||||||
|
|
||||||
const res = await fetch("http://localhost:8000/api/pelanggans", {
|
const res = await fetch("http://localhost:8000/api/pelanggans", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: { "Content-Type": "application/json" },
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
nama_pemesan: form.value.nama_pemesan,
|
nama_pemesan: form.value.nama_pemesan,
|
||||||
email: form.value.email,
|
email: form.value.email,
|
||||||
no_tlpn: form.value.no_hp,
|
no_tlpn: form.value.no_hp, // 🔥 ganti no_hp -> no_tlpn
|
||||||
template_id: template.value.id,
|
template_id: template.value.id,
|
||||||
form: {
|
form: JSON.stringify({ // 🔥 pastikan dikirim string, bukan object
|
||||||
nama_pengantin: form.value.nama_pria + " & " + form.value.nama_wanita,
|
nama_pengantin: form.value.nama_pria + " & " + form.value.nama_wanita,
|
||||||
tanggal_acara: form.value.acaras[0].tanggal,
|
tanggal_acara: form.value.acaras[0].tanggal,
|
||||||
lokasi: form.value.acaras[0].lokasi
|
lokasi: form.value.acaras[0].lokasi
|
||||||
}
|
})
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
// parse JSON dulu
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
console.log("Respon backend:", data)
|
if (!res.ok) throw new Error(data.message || "Gagal simpan data")
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
console.error("Status:", res.status)
|
|
||||||
console.error("Error full:", data)
|
|
||||||
console.error("Error detail:", data.errors)
|
|
||||||
throw new Error(data.message || "Gagal simpan data")
|
|
||||||
}
|
|
||||||
|
|
||||||
success.value = true
|
success.value = true
|
||||||
|
|
||||||
|
// --- Langsung buka WhatsApp Admin ---
|
||||||
|
const f = form.value
|
||||||
|
const pesan = `
|
||||||
|
Halo Admin, saya ingin memesan undangan.
|
||||||
|
Nama Pemesan: ${f.nama_pemesan}
|
||||||
|
No. WA: ${f.no_hp}
|
||||||
|
Email: ${f.email}
|
||||||
|
|
||||||
|
Template: ${f.nama_template}
|
||||||
|
Kategori: ${f.kategori}
|
||||||
|
Harga: ${f.harga}
|
||||||
|
|
||||||
|
Nama Pengantin: ${f.nama_pria} & ${f.nama_wanita}
|
||||||
|
Orang Tua Pria: ${f.ortu_pria}
|
||||||
|
Orang Tua Wanita: ${f.ortu_wanita}
|
||||||
|
|
||||||
|
Acara:
|
||||||
|
${f.acaras.map(a => `- ${a.nama}, ${a.tanggal}, ${a.jam || "-"} WIB, Lokasi: ${a.lokasi}, Request Lagu: ${a.request_lagu || "-"}`).join("\n")}
|
||||||
|
`
|
||||||
|
window.open(`https://wa.me/${adminWaNumber}?text=${encodeURIComponent(pesan)}`, "_blank")
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Catch error:", err)
|
console.error("Catch error:", err)
|
||||||
error.value = true
|
error.value = true
|
||||||
@ -217,8 +231,6 @@ const submitForm = async () => {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
14
proyek-frontend/app/pages/form/pernikahan/b.vue
Normal file
14
proyek-frontend/app/pages/form/pernikahan/b.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-10 max-w-2xl mx-auto">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Form Pemesanan - Wedding B</h1>
|
||||||
|
<form class="space-y-4">
|
||||||
|
<input type="text" placeholder="Nama Pemesan" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<input type="text" placeholder="Nama Pasangan" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<input type="date" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<textarea placeholder="Alamat Acara" class="border px-4 py-2 rounded w-full"></textarea>
|
||||||
|
<button class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
|
||||||
|
Kirim
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@ -1,275 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="max-w-6xl mx-auto p-8 bg-gradient-to-b from-blue-50 to-indigo-50 shadow-lg rounded-xl">
|
|
||||||
<!-- Judul -->
|
|
||||||
<div class="text-center mb-10">
|
|
||||||
<h1 class="text-3xl md:text-4xl font-extrabold text-indigo-700 drop-shadow-sm">
|
|
||||||
🎂 Form Pemesanan Undangan Ulang Tahun 🎉
|
|
||||||
</h1>
|
|
||||||
<p class="text-gray-600 mt-2">Isi data berikut untuk membuat undangan ulang tahun anak Anda</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form @submit.prevent="submitForm" class="space-y-10">
|
|
||||||
|
|
||||||
<!-- Pilih Paket -->
|
|
||||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
|
||||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Pilih Paket</h2>
|
|
||||||
<div class="flex flex-col md:flex-row gap-4">
|
|
||||||
<label v-for="paket in paketList" :key="paket.id"
|
|
||||||
class="flex-1 p-4 border rounded-xl cursor-pointer hover:shadow-lg transition-all"
|
|
||||||
:class="{'border-blue-600 bg-blue-50': form.selectedPaket === paket.id}">
|
|
||||||
<input type="radio" class="hidden" v-model="form.selectedPaket" :value="paket.id" />
|
|
||||||
<div class="font-semibold text-gray-800">{{ paket.nama }}</div>
|
|
||||||
<div class="text-gray-600">{{ paket.deskripsi }}</div>
|
|
||||||
<div class="font-bold mt-2 text-blue-700">{{ formatRupiah(paket.harga) }}</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Info Template -->
|
|
||||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
|
||||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Template</h2>
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
||||||
<input v-model="form.nama_template" type="text" placeholder="Nama Template" class="input-readonly" readonly />
|
|
||||||
<input v-model="form.kategori" type="text" placeholder="Kategori" class="input-readonly" readonly />
|
|
||||||
<input v-model="form.harga" type="text" placeholder="Harga" class="input-readonly" readonly />
|
|
||||||
<input v-model="form.tanggal_pemesanan" type="text" placeholder="Tanggal Pemesanan" class="input-readonly" readonly />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Data Pemesan -->
|
|
||||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
|
||||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Data Pemesan</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-gray-600 mb-1">Nama Pemesan</label>
|
|
||||||
<input v-model="form.nama_pemesan" type="text" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-gray-600 mb-1">No. WhatsApp</label>
|
|
||||||
<input v-model="form.no_hp" type="text" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="block text-sm text-gray-600 mb-1">Email</label>
|
|
||||||
<input v-model="form.email" type="email" class="input" required />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Data Anak -->
|
|
||||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
|
||||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Data Anak</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm mb-1">Nama Lengkap Anak</label>
|
|
||||||
<input v-model="form.nama_lengkap_anak" type="text" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm mb-1">Nama Panggilan Anak</label>
|
|
||||||
<input v-model="form.nama_panggilan_anak" type="text" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm mb-1">Nama Bapak</label>
|
|
||||||
<input v-model="form.bapak_anak" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm mb-1">Nama Ibu</label>
|
|
||||||
<input v-model="form.ibu_anak" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm mb-1">Ulang Tahun ke-</label>
|
|
||||||
<input v-model="form.umur_dirayakan" type="number" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm mb-1">Anak ke-</label>
|
|
||||||
<input v-model="form.anak_ke" type="number" class="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Detail Acara -->
|
|
||||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
|
||||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Detail Acara</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm mb-1">Hari & Tanggal</label>
|
|
||||||
<input v-model="form.hari_tanggal_acara" type="date" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm mb-1">Waktu Acara</label>
|
|
||||||
<input v-model="form.waktu_acara" type="text" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="block text-sm mb-1">Alamat Lengkap</label>
|
|
||||||
<textarea v-model="form.alamat_acara" class="input"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="md:col-span-2">
|
|
||||||
<label class="block text-sm mb-1">Link Google Maps (opsional)</label>
|
|
||||||
<input v-model="form.maps_acara" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Galeri Foto -->
|
|
||||||
<section v-if="form.selectedPaket === 'basic' || form.selectedPaket === 'premium'" class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
|
||||||
<h2 class="text-lg font-bold text-gray-800 mb-4">
|
|
||||||
Galeri Foto
|
|
||||||
<span v-if="form.selectedPaket === 'basic'">(max 6 gambar)</span>
|
|
||||||
<span v-if="form.selectedPaket === 'premium'">(unlimited)</span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<label for="gallery-upload"
|
|
||||||
class="cursor-pointer bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg text-sm font-medium inline-block mb-4">
|
|
||||||
+ Tambah Foto
|
|
||||||
</label>
|
|
||||||
<input type="file" multiple accept="image/*" @change="handleFileUpload" class="hidden" id="gallery-upload" />
|
|
||||||
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
||||||
<div v-for="(img, i) in previewImages" :key="i"
|
|
||||||
class="relative w-full aspect-square rounded-lg overflow-hidden shadow-sm group">
|
|
||||||
<img :src="img" alt="Preview" class="object-cover w-full h-full" />
|
|
||||||
<button type="button" @click="removeImage(i)"
|
|
||||||
class="absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity font-bold">
|
|
||||||
×
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Upload Video (Premium Only) -->
|
|
||||||
<section v-if="form.selectedPaket === 'premium'" class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
|
||||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Video Ucapan (Premium)</h2>
|
|
||||||
<input type="file" accept="video/*" @change="handleVideoUpload" class="block w-full text-sm text-gray-600" />
|
|
||||||
<div v-if="previewVideo" class="mt-4">
|
|
||||||
<video controls class="w-full rounded-lg shadow">
|
|
||||||
<source :src="previewVideo" type="video/mp4" />
|
|
||||||
Browser Anda tidak mendukung video.
|
|
||||||
</video>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Submit -->
|
|
||||||
<div class="mt-10 text-center">
|
|
||||||
<button type="submit"
|
|
||||||
class="bg-blue-600 text-white px-10 py-3 rounded-xl font-semibold shadow-md hover:bg-blue-700 hover:shadow-lg transition"
|
|
||||||
:disabled="loading">
|
|
||||||
{{ loading ? "Mengirim..." : "Kirim & Konfirmasi Admin" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Alert -->
|
|
||||||
<div v-if="success" class="mt-6 p-4 text-green-800 bg-green-100 rounded-lg text-center font-medium">
|
|
||||||
✅ Form berhasil dikirim! Tunggu konfirmasi admin.
|
|
||||||
</div>
|
|
||||||
<div v-if="error" class="mt-6 p-4 text-red-800 bg-red-100 rounded-lg text-center font-medium">
|
|
||||||
❌ Gagal mengirim form. Cek kembali inputan Anda.
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref } from "vue"
|
|
||||||
|
|
||||||
const paketList = ref([
|
|
||||||
{ id: "starter", nama: "Starter", deskripsi: "Fitur dasar (tanpa galeri)", harga: 100000 },
|
|
||||||
{ id: "basic", nama: "Basic", deskripsi: "Tambahan galeri (max 6 foto)", harga: 200000 },
|
|
||||||
{ id: "premium", nama: "Premium", deskripsi: "Unlimited galeri + upload video", harga: 350000 },
|
|
||||||
])
|
|
||||||
|
|
||||||
const form = ref({
|
|
||||||
selectedPaket: "",
|
|
||||||
nama_template: "",
|
|
||||||
kategori: "Ulang Tahun",
|
|
||||||
harga: "",
|
|
||||||
tanggal_pemesanan: new Date().toLocaleDateString("id-ID"),
|
|
||||||
nama_pemesan: "",
|
|
||||||
no_hp: "",
|
|
||||||
email: "",
|
|
||||||
nama_lengkap_anak: "",
|
|
||||||
nama_panggilan_anak: "",
|
|
||||||
bapak_anak: "",
|
|
||||||
ibu_anak: "",
|
|
||||||
umur_dirayakan: "",
|
|
||||||
anak_ke: "",
|
|
||||||
hari_tanggal_acara: "",
|
|
||||||
waktu_acara: "",
|
|
||||||
alamat_acara: "",
|
|
||||||
maps_acara: "",
|
|
||||||
galeri: [],
|
|
||||||
video: null
|
|
||||||
})
|
|
||||||
|
|
||||||
const previewImages = ref([])
|
|
||||||
const previewVideo = ref(null)
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const success = ref(false)
|
|
||||||
const error = ref(false)
|
|
||||||
|
|
||||||
const formatRupiah = (num) => new Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR" }).format(num)
|
|
||||||
|
|
||||||
const handleFileUpload = (event) => {
|
|
||||||
const files = Array.from(event.target.files)
|
|
||||||
|
|
||||||
if (form.value.selectedPaket === "basic" && (form.value.galeri.length + files.length) > 6) {
|
|
||||||
alert("Paket Basic hanya bisa upload maksimal 6 foto!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
form.value.galeri.push(...files)
|
|
||||||
files.forEach(file => {
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = e => previewImages.value.push(e.target.result)
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleVideoUpload = (event) => {
|
|
||||||
const file = event.target.files[0]
|
|
||||||
if (file) {
|
|
||||||
form.value.video = file
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = e => previewVideo.value = e.target.result
|
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeImage = (index) => {
|
|
||||||
form.value.galeri.splice(index, 1)
|
|
||||||
previewImages.value.splice(index, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitForm = async () => {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
success.value = false
|
|
||||||
error.value = false
|
|
||||||
|
|
||||||
console.log("Data dikirim:", form.value)
|
|
||||||
await new Promise(res => setTimeout(res, 1000))
|
|
||||||
|
|
||||||
success.value = true
|
|
||||||
} catch (err) {
|
|
||||||
error.value = true
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.input {
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
.input-readonly {
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
padding: 0.5rem 0.75rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
background-color: #f9fafb;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
14
proyek-frontend/app/pages/form/ulang-tahun/b.vue
Normal file
14
proyek-frontend/app/pages/form/ulang-tahun/b.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-10 max-w-2xl mx-auto">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">Form Pemesanan - Ulang Tahun B</h1>
|
||||||
|
<form class="space-y-4">
|
||||||
|
<input type="text" placeholder="Nama Pemesan" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<input type="text" placeholder="Nama Anak / Orang yang Berulang Tahun" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<input type="date" class="border px-4 py-2 rounded w-full" />
|
||||||
|
<textarea placeholder="Alamat Acara" class="border px-4 py-2 rounded w-full"></textarea>
|
||||||
|
<button class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
|
||||||
|
Kirim
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue
Block a user