kategori templat

This commit is contained in:
Farhaan4 2025-10-09 15:02:52 +07:00
parent 53aa75b809
commit 23c8e909ca
3 changed files with 212 additions and 98 deletions

View File

@ -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, 3, 4, 5, 2, 7, 8, 9] const selectedIds = [1, 3, 4, 5, 6, 7, 8, 9]
// State dropdown // State dropdown
const openDropdownId = ref(null) const openDropdownId = ref(null)
@ -10,10 +10,10 @@ const toggleDropdown = (templateId) => {
openDropdownId.value = openDropdownId.value === templateId ? null : templateId openDropdownId.value = openDropdownId.value === templateId ? null : templateId
} }
// Paket & fitur hardcode
const paketData = [ const paketData = [
// Paket Starter (Undangan Minimalis / Pernikahan Starter)
{ {
paket: 'Starter', paket: 'starter',
fiturs: [ fiturs: [
'1x Acara', '1x Acara',
'Masa Aktif 3 Bulan', 'Masa Aktif 3 Bulan',
@ -22,21 +22,10 @@ const paketData = [
'Request Musik' 'Request Musik'
] ]
}, },
// Paket Premium Pernikahan
{ {
paket: 'Basic', paket: 'premium',
fiturs: [
'1x Acara',
'6 Galeri Foto',
'Hitung Mundur Waktu Acara',
'Buku Tamu + Data Kehadiran',
'Masa Aktif 6 Bulan',
'Nama Tamu Personal',
'Maks. 200 Tamu',
'Request Musik'
]
},
{
paket: 'Premium',
fiturs: [ fiturs: [
'Maksimal 3x Acara (Akad, Resepsi, Syukuran)', 'Maksimal 3x Acara (Akad, Resepsi, Syukuran)',
'Unlimited Galeri Foto', 'Unlimited Galeri Foto',
@ -51,6 +40,40 @@ const paketData = [
'Nama Tamu Personal Unlimited Tamu', 'Nama Tamu Personal Unlimited Tamu',
'Request Musik' 'Request Musik'
] ]
},
// Paket Premium Ulang Tahun
{
paket: 'premium',
fiturs: [
'1x Acara',
'Unlimited Galeri Foto',
'Timeline Story',
'Google Maps',
'Reminder Google Calendar',
'Amplop Digital',
'Placement Video Cinematic',
'Masa Aktif 12 Bulan',
'Nama Tamu Personal Unlimited Tamu',
'Request Musik'
]
},
// Paket Premium Khitan
{
paket: 'premium',
fiturs: [
'1x Acara',
'Unlimited Galeri Foto',
'Timeline Story',
'Google Maps',
'Reminder Google Calendar',
'Amplop Digital',
'Placement Video Cinematic',
'Masa Aktif 12 Bulan',
'Nama Tamu Personal Unlimited Tamu',
'Request Musik'
]
} }
] ]
@ -67,17 +90,27 @@ const formMapping = {
const { data: templatesData, error } = await useFetch('http://localhost:8000/api/templates') const { data: templatesData, error } = await useFetch('http://localhost:8000/api/templates')
// Mapping template: gabungkan backend + paket & fitur hardcode // Mapping template: gabungkan backend + paket & fitur hardcode
const paketMapping = {
'Undangan Minimalis': 'starter',
'Undangan Pernikahan Premium': 'premium',
'Undangan Ulang Tahun Premium': 'premium',
'Undangan Khitan Premium': 'premium'
}
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) => {
const paketKey = paketMapping[t.nama_template] || 'starter';
const paketInfo = paketData.find(p => p.paket.toLowerCase() === paketKey.toLowerCase()) || paketData[0];
return { return {
id: t.id, id: t.id,
nama_template: t.nama_template, nama_template: t.nama_template,
harga: t.harga, harga: t.harga,
foto: t.foto || '/default.jpg', foto: t.foto || '/default.jpg',
paket: paketData[index % paketData.length].paket, paket: paketInfo.paket,
fiturs: paketData[index % paketData.length].fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })), fiturs: paketInfo.fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })),
kategori: t.kategori, kategori: t.kategori,
formPath: t.slug formPath: t.slug
} }
@ -149,7 +182,7 @@ const templates = computed(() =>
Order Order
</NuxtLink> </NuxtLink>
</div> </div>
</div>1 </div>
</div> </div>
</div> </div>
@ -166,7 +199,7 @@ const templates = computed(() =>
</template> </template>
<style> <style>
/* animasi dropdown smooth */
.fade-enter-active, .fade-enter-active,
.fade-leave-active { .fade-leave-active {
transition: all 0.3s ease; transition: all 0.3s ease;

View File

@ -34,9 +34,9 @@
<div v-for="category in categories" :key="category.id + '-' + category.foto" <div v-for="category in categories" :key="category.id + '-' + category.foto"
@click="onCategoryClick(category)" @click="onCategoryClick(category)"
class="group cursor-pointer relative overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300 w-72"> class="group cursor-pointer relative overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300 w-72">
<img :src="category.foto || '/fallback.png'" :alt="category.nama" <img :src="category.foto || '/ABBAUF.png'" :alt="category.nama"
class="w-full h-96 object-cover transition-transform duration-300 group-hover:scale-110" class="w-full h-96 object-cover transition-transform duration-300 group-hover:scale-110"
@error="(e) => e.target.src = '/fallback.png'"> @error="(e) => e.target.src = '/ABBAUF.png'">
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent"></div> <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent"></div>
<div class="absolute inset-0 flex flex-col justify-center items-start px-4 text-white"> <div class="absolute inset-0 flex flex-col justify-center items-start px-4 text-white">
<h3 class="text-xl font-semibold mb-2">{{ category.nama }}</h3> <h3 class="text-xl font-semibold mb-2">{{ category.nama }}</h3>
@ -71,8 +71,8 @@
<div v-for="t in templatesWithFeatures" :key="t.id" <div v-for="t in templatesWithFeatures" :key="t.id"
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300"> class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
<!-- Image --> <!-- Image -->
<img :src="t.foto || '/fallback.png'" :alt="t.nama" class="w-full h-48 object-cover" <img :src="t.foto || '/logo1.png'" :alt="t.nama" class="w-full h-48 object-cover"
@error="(e) => e.target.src = '/fallback.png'" /> @error="(e) => e.target.src = '/logo1.png'" />
<!-- Body --> <!-- Body -->
<div class="p-5 text-center"> <div class="p-5 text-center">
@ -84,34 +84,33 @@
<!-- Dropdown fitur --> <!-- Dropdown fitur -->
<div v-if="t.fiturs && t.fiturs.length > 0" class="relative mb-4"> <div v-if="t.fiturs && t.fiturs.length > 0" class="relative mb-4">
<button <button @click="toggleDropdown(t.id)"
@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-start">
class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 inline-flex justify-between items-start" <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" fill="currentColor"
<span class="mx-auto text-gray-700 font-semibold">FITUR YANG TERSEDIA</span> viewBox="0 0 20 20">
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"> <path fill-rule="evenodd"
<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"
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" />
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 <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" 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"
<svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 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" />
</svg> </svg>
{{ f.deskripsi }} {{ f.deskripsi }}
</li> </li>
</ul> </ul>
</div> </div>
</transition> </transition>
</div> </div>
<!-- Buttons --> <!-- Buttons -->
<div class="flex items-center gap-3 mt-6"> <div class="flex items-center gap-3 mt-6">
@ -120,8 +119,7 @@
@click="onTemplateClick(t)"> @click="onTemplateClick(t)">
Preview Preview
</button> </button>
<NuxtLink <NuxtLink :to="`${t.formPath}?template_id=${t.id}`"
:to="`/form/${t.kategori ? t.kategori.toLowerCase().replace(/ /g, '-') : ''}` + `?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>
@ -149,6 +147,13 @@ const isLoading = ref(true)
const error = ref(null) const error = ref(null)
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',
}
// state dropdown fitur // state dropdown fitur
const openDropdownId = ref(null) const openDropdownId = ref(null)
@ -240,13 +245,15 @@ const templatesWithFeatures = computed(() =>
id: t.id, id: t.id,
nama: t.nama_template, nama: t.nama_template,
harga: t.harga, harga: t.harga,
foto: t.foto, foto: t.foto || '/logo1.png',
kategori: t.kategori, kategori: t.kategori,
paket: paketData[index % paketData.length].paket, paket: paketData[index % paketData.length].paket,
fiturs: paketData[index % paketData.length].fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })) fiturs: paketData[index % paketData.length].fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })),
formPath: formMapping[t.nama_template] || `/form/lainny` // 🔥 ambil path form sesuai mapping
})) }))
) )
onMounted(() => { onMounted(() => {
fetchCategories() fetchCategories()
fetchTemplates() fetchTemplates()

View File

@ -25,31 +25,27 @@
</div> </div>
<!-- Grid Template --> <!-- Grid Template -->
<div v-else-if="templates.length > 0" <div v-else-if="templates.length > 0" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
<div v-for="tpl in templates" :key="tpl.id" <div v-for="tpl in templates" :key="tpl.id"
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300"> class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
<!-- Image --> <!-- Gambar -->
<img <img :src="tpl.foto" :alt="tpl.nama_template" class="w-full h-48 object-cover"
:src="tpl.foto || '/fallback.png'" @error="(e) => e.target.src = '/logo2.png'" />
:alt="tpl.nama_template"
class="w-full h-48 object-cover"
@error="(e) => e.target.src = '/fallback.png'"
/>
<!-- Body --> <!-- Body -->
<div class="p-5 text-center"> <div class="p-5 text-center">
<h4 class="text-xl font-bold text-gray-800 mb-2">{{ tpl.nama_template }}</h4> <h4 class="text-xl font-bold text-gray-800 mb-2">{{ tpl.nama_template }}</h4>
<p class="text-green-600 font-semibold text-xl mb-4"> <p class="text-green-600 font-semibold text-xl mb-1">
Rp {{ (tpl.harga ?? 0).toLocaleString('id-ID') }} Rp {{ Number(tpl.harga ?? 0).toLocaleString('id-ID') }}
</p> </p>
<p class="text-gray-500 mb-4 font-medium">Paket: {{ tpl.paket }}</p>
<!-- Dropdown Fitur --> <!-- Dropdown Fitur -->
<div v-if="tpl.fiturs?.length > 0" class="relative mb-4"> <div v-if="tpl.fiturs && tpl.fiturs.length" class="relative mb-4">
<button @click="toggleDropdown(tpl.id)" <button @click="toggleDropdown(tpl.id)"
class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 inline-flex justify-between items-center text-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" <svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor"> fill="currentColor">
@ -59,26 +55,29 @@
</svg> </svg>
</button> </button>
<div v-if="openDropdownId === tpl.id"> <transition name="fade">
<ul class="mt-4 space-y-2 text-gray-600 text-left"> <div v-if="openDropdownId === tpl.id" class="mt-4">
<li v-for="item_fitur in tpl.fiturs" :key="item_fitur.id" class="flex items-center"> <ul
<svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"> 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path> <li v-for="f in tpl.fiturs" :key="f.id" class="flex items-center">
</svg> <svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{{ item_fitur.deskripsi }} <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</li> </svg>
</ul> {{ f.deskripsi }}
</div> </li>
</ul>
</div>
</transition>
</div> </div>
<!-- Buttons --> <!-- Buttons -->
<div class="mt-6 flex flex-col gap-3"> <div class="flex items-center gap-3 mt-6">
<a :href="tpl.preview_link || '#'" target="_blank" <a :href="tpl.preview_link || '#'"
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 text-center block"> 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 text-center">
Preview Preview
</a> </a>
<NuxtLink <NuxtLink :to="`${tpl.formPath}?template_id=${tpl.id}`"
:to="`/form/${tpl.kategori?.id || tpl.id}?template_id=${tpl.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>
@ -86,6 +85,7 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else class="text-center py-10 text-gray-500"> <div v-else class="text-center py-10 text-gray-500">
@ -95,7 +95,7 @@
</template> </template>
<script setup> <script setup>
import { ref, watch, onMounted } from 'vue' import { ref, watch, onMounted, computed } from 'vue'
const props = defineProps({ const props = defineProps({
category: { type: String, required: true }, category: { type: String, required: true },
@ -113,6 +113,51 @@ const toggleDropdown = (templateId) => {
openDropdownId.value = openDropdownId.value === templateId ? null : templateId openDropdownId.value = openDropdownId.value === templateId ? null : templateId
} }
// Mapping form untuk tiap template
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',
}
// Hardcode fitur per paket
// Mapping paket -> fitur (pastikan key sesuai format paket)
const fiturPerPaket = {
Starter: [
'1x Acara',
'Masa Aktif 3 Bulan',
'Nama Tamu Personal',
'Maks. 100 Tamu',
'Request Musik'
],
Basic: [
'1x Acara',
'6 Galeri Foto',
'Hitung Mundur Waktu Acara',
'Buku Tamu + Data Kehadiran',
'Masa Aktif 6 Bulan',
'Nama Tamu Personal',
'Maks. 200 Tamu',
'Request Musik'
],
Premium: [
'Maksimal 3x Acara (Akad, Resepsi, Syukuran)',
'Unlimited Galeri Foto',
'Timeline Story',
'Google Maps',
'Reminder Google Calendar',
'Link Instagram Live Streaming',
'Amplop Digital',
'Placement Video Cinematic',
'Bonus Undangan Image Post Story',
'Masa Aktif 12 Bulan',
'Nama Tamu Personal Unlimited Tamu',
'Request Musik'
]
}
// Fetch templates dari API
const fetchTemplates = async (categoryId) => { const fetchTemplates = async (categoryId) => {
isLoading.value = true isLoading.value = true
error.value = null error.value = null
@ -120,15 +165,26 @@ const fetchTemplates = async (categoryId) => {
const res = await $fetch(`/api/templates/category/${categoryId}`, { const res = await $fetch(`/api/templates/category/${categoryId}`, {
baseURL: 'http://localhost:8000' baseURL: 'http://localhost:8000'
}) })
templates.value = res.map(tpl => ({
id: tpl.id, templates.value = res.map(tpl => {
nama_template: tpl.nama_template, // Pastikan nama paket konsisten: 'Starter', 'Basic', 'Premium'
harga: tpl.harga, const paketKey = tpl.paket ? tpl.paket.charAt(0).toUpperCase() + tpl.paket.slice(1).toLowerCase() : 'Starter'
kategori: tpl.kategori,
foto: tpl.foto ?? null, return {
fiturs: tpl.fiturs ?? [], id: tpl.id,
preview_link: tpl.preview_link ?? null nama_template: tpl.nama_template,
})) harga: tpl.harga,
kategori: tpl.kategori,
foto: tpl.foto ?? '/logo2.png',
paket: paketKey,
fiturs: (fiturPerPaket[paketKey] || []).map((f, i) => ({
id: i + 1,
deskripsi: f
})),
preview_link: tpl.preview_link ?? null,
formPath: formMapping[tpl.nama_template] || '/form/lainny'
}
})
} catch (err) { } catch (err) {
console.error(err) console.error(err)
error.value = 'Gagal memuat template.' error.value = 'Gagal memuat template.'
@ -138,11 +194,29 @@ const fetchTemplates = async (categoryId) => {
} }
} }
// Fetch saat mount
onMounted(() => fetchTemplates(props.id_category)) onMounted(() => fetchTemplates(props.id_category))
// Watch id_category untuk fetch ulang saat berubah
watch(() => props.id_category, (newId) => { watch(() => props.id_category, (newId) => {
if (newId) fetchTemplates(newId) if (newId) fetchTemplates(newId)
}) })
</script> </script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-5px);
}
.fade-enter-to,
.fade-leave-from {
opacity: 1;
transform: translateY(0);
}
</style>