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'
// 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
const openDropdownId = ref(null)
@ -10,10 +10,10 @@ const toggleDropdown = (templateId) => {
openDropdownId.value = openDropdownId.value === templateId ? null : templateId
}
// Paket & fitur hardcode
const paketData = [
// Paket Starter (Undangan Minimalis / Pernikahan Starter)
{
paket: 'Starter',
paket: 'starter',
fiturs: [
'1x Acara',
'Masa Aktif 3 Bulan',
@ -22,21 +22,10 @@ const paketData = [
'Request Musik'
]
},
// Paket Premium Pernikahan
{
paket: 'Basic',
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',
paket: 'premium',
fiturs: [
'Maksimal 3x Acara (Akad, Resepsi, Syukuran)',
'Unlimited Galeri Foto',
@ -51,6 +40,40 @@ const paketData = [
'Nama Tamu Personal Unlimited Tamu',
'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')
// 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(() =>
(templatesData.value || [])
.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 {
id: t.id,
nama_template: t.nama_template,
harga: t.harga,
foto: t.foto || '/default.jpg',
paket: paketData[index % paketData.length].paket,
fiturs: paketData[index % paketData.length].fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })),
paket: paketInfo.paket,
fiturs: paketInfo.fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })),
kategori: t.kategori,
formPath: t.slug
}
@ -149,7 +182,7 @@ const templates = computed(() =>
Order
</NuxtLink>
</div>
</div>1
</div>
</div>
</div>
@ -166,7 +199,7 @@ const templates = computed(() =>
</template>
<style>
/* animasi dropdown smooth */
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease;

View File

@ -34,9 +34,9 @@
<div v-for="category in categories" :key="category.id + '-' + category.foto"
@click="onCategoryClick(category)"
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"
@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 flex flex-col justify-center items-start px-4 text-white">
<h3 class="text-xl font-semibold mb-2">{{ category.nama }}</h3>
@ -71,8 +71,8 @@
<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">
<!-- Image -->
<img :src="t.foto || '/fallback.png'" :alt="t.nama" class="w-full h-48 object-cover"
@error="(e) => e.target.src = '/fallback.png'" />
<img :src="t.foto || '/logo1.png'" :alt="t.nama" class="w-full h-48 object-cover"
@error="(e) => e.target.src = '/logo1.png'" />
<!-- Body -->
<div class="p-5 text-center">
@ -84,34 +84,33 @@
<!-- Dropdown fitur -->
<div v-if="t.fiturs && t.fiturs.length > 0" class="relative mb-4">
<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-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" 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" />
</svg>
</button>
<div v-if="t.fiturs && t.fiturs.length > 0" class="relative mb-4">
<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-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"
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" />
</svg>
</button>
<transition name="fade">
<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"
>
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{{ f.deskripsi }}
</li>
</ul>
</div>
</transition>
</div>
<transition name="fade">
<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">
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
{{ f.deskripsi }}
</li>
</ul>
</div>
</transition>
</div>
<!-- Buttons -->
<div class="flex items-center gap-3 mt-6">
@ -120,8 +119,7 @@
@click="onTemplateClick(t)">
Preview
</button>
<NuxtLink
:to="`/form/${t.kategori ? t.kategori.toLowerCase().replace(/ /g, '-') : ''}` + `?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">
Order
</NuxtLink>
@ -149,6 +147,13 @@ const isLoading = ref(true)
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
const openDropdownId = ref(null)
@ -240,13 +245,15 @@ const templatesWithFeatures = computed(() =>
id: t.id,
nama: t.nama_template,
harga: t.harga,
foto: t.foto,
foto: t.foto || '/logo1.png',
kategori: t.kategori,
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(() => {
fetchCategories()
fetchTemplates()

View File

@ -25,31 +25,27 @@
</div>
<!-- Grid Template -->
<div v-else-if="templates.length > 0"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 items-start">
<div v-else-if="templates.length > 0" 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"
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
<!-- Image -->
<img
:src="tpl.foto || '/fallback.png'"
:alt="tpl.nama_template"
class="w-full h-48 object-cover"
@error="(e) => e.target.src = '/fallback.png'"
/>
<!-- Gambar -->
<img :src="tpl.foto" :alt="tpl.nama_template" class="w-full h-48 object-cover"
@error="(e) => e.target.src = '/logo2.png'" />
<!-- Body -->
<div class="p-5 text-center">
<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">
Rp {{ (tpl.harga ?? 0).toLocaleString('id-ID') }}
<p class="text-green-600 font-semibold text-xl mb-1">
Rp {{ Number(tpl.harga ?? 0).toLocaleString('id-ID') }}
</p>
<p class="text-gray-500 mb-4 font-medium">Paket: {{ tpl.paket }}</p>
<!-- 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)"
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>
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
fill="currentColor">
@ -59,26 +55,29 @@
</svg>
</button>
<div v-if="openDropdownId === tpl.id">
<ul class="mt-4 space-y-2 text-gray-600 text-left">
<li v-for="item_fitur in tpl.fiturs" :key="item_fitur.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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
{{ item_fitur.deskripsi }}
</li>
</ul>
</div>
<transition name="fade">
<div v-if="openDropdownId === tpl.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">
<li v-for="f in tpl.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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
{{ f.deskripsi }}
</li>
</ul>
</div>
</transition>
</div>
<!-- Buttons -->
<div class="mt-6 flex flex-col gap-3">
<a :href="tpl.preview_link || '#'" target="_blank"
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">
<div class="flex items-center gap-3 mt-6">
<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">
Preview
</a>
<NuxtLink
:to="`/form/${tpl.kategori?.id || tpl.id}?template_id=${tpl.id}`"
<NuxtLink :to="`${tpl.formPath}?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">
Order
</NuxtLink>
@ -86,6 +85,7 @@
</div>
</div>
</div>
<div v-else class="text-center py-10 text-gray-500">
@ -95,7 +95,7 @@
</template>
<script setup>
import { ref, watch, onMounted } from 'vue'
import { ref, watch, onMounted, computed } from 'vue'
const props = defineProps({
category: { type: String, required: true },
@ -113,6 +113,51 @@ const toggleDropdown = (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) => {
isLoading.value = true
error.value = null
@ -120,15 +165,26 @@ const fetchTemplates = async (categoryId) => {
const res = await $fetch(`/api/templates/category/${categoryId}`, {
baseURL: 'http://localhost:8000'
})
templates.value = res.map(tpl => ({
id: tpl.id,
nama_template: tpl.nama_template,
harga: tpl.harga,
kategori: tpl.kategori,
foto: tpl.foto ?? null,
fiturs: tpl.fiturs ?? [],
preview_link: tpl.preview_link ?? null
}))
templates.value = res.map(tpl => {
// Pastikan nama paket konsisten: 'Starter', 'Basic', 'Premium'
const paketKey = tpl.paket ? tpl.paket.charAt(0).toUpperCase() + tpl.paket.slice(1).toLowerCase() : 'Starter'
return {
id: tpl.id,
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) {
console.error(err)
error.value = 'Gagal memuat template.'
@ -138,11 +194,29 @@ const fetchTemplates = async (categoryId) => {
}
}
// Fetch saat mount
onMounted(() => fetchTemplates(props.id_category))
// Watch id_category untuk fetch ulang saat berubah
watch(() => props.id_category, (newId) => {
if (newId) fetchTemplates(newId)
})
</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>