fix landing page
This commit is contained in:
parent
40e8b59001
commit
28d3155070
@ -14,13 +14,13 @@ class TemplateApiController extends Controller
|
|||||||
|
|
||||||
$transformedTemplates = $templates->map(function ($template) {
|
$transformedTemplates = $templates->map(function ($template) {
|
||||||
return [
|
return [
|
||||||
'id' => $template->id,
|
'id' => $template->id,
|
||||||
'nama_template' => $template->nama_template,
|
'nama_template' => $template->nama_template,
|
||||||
'slug' => $template->slug,
|
'slug' => $template->slug,
|
||||||
'harga' => $template->harga,
|
'harga' => $template->harga,
|
||||||
'paket' => $template->paket,
|
'paket' => $template->paket,
|
||||||
'kategori' => $template->kategori ? $template->kategori->nama : null,
|
'kategori' => $template->kategori ? $template->kategori->nama : null,
|
||||||
'foto' => $template->foto ? asset('storage/' . $template->foto) : null,
|
'foto' => $template->foto ? asset('storage/' . $template->foto) : null,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -34,14 +34,14 @@ class TemplateApiController extends Controller
|
|||||||
$formData = $this->addDummyValues($template->form ?? []);
|
$formData = $this->addDummyValues($template->form ?? []);
|
||||||
|
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'id' => $template->id,
|
'id' => $template->id,
|
||||||
'nama_template' => $template->nama_template,
|
'nama_template' => $template->nama_template,
|
||||||
'slug' => $template->slug,
|
'slug' => $template->slug,
|
||||||
'harga' => $template->harga,
|
'harga' => $template->harga,
|
||||||
'paket' => $template->paket,
|
'paket' => $template->paket,
|
||||||
'kategori' => $template->kategori ? $template->kategori->nama : null,
|
'kategori' => $template->kategori ? $template->kategori->nama : null,
|
||||||
'form' => $formData,
|
'form' => $formData,
|
||||||
'foto' => $template->foto ? asset('storage/' . $template->foto) : null,
|
'foto' => $template->foto ? asset('storage/' . $template->foto) : null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,22 +74,23 @@ class TemplateApiController extends Controller
|
|||||||
|
|
||||||
public function getByCategory($id)
|
public function getByCategory($id)
|
||||||
{
|
{
|
||||||
$templates = Template::with('fiturs', 'kategori')
|
$templates = Template::with('kategori')
|
||||||
->where('kategori_id', $id)
|
->where('kategori_id', $id)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
|
||||||
$transformed = $templates->map(function ($template) {
|
$transformed = $templates->map(function ($template) {
|
||||||
return [
|
return [
|
||||||
'id' => $template->id,
|
'id' => $template->id,
|
||||||
'nama_template' => $template->nama_template,
|
'nama_template' => $template->nama_template,
|
||||||
'harga' => $template->harga,
|
'harga' => $template->harga,
|
||||||
'paket' => $template->paket,
|
'paket' => $template->paket,
|
||||||
'kategori' => $template->kategori ? [
|
'kategori' => $template->kategori ? [
|
||||||
'id' => $template->kategori->id,
|
'id' => $template->kategori->id,
|
||||||
'nama' => $template->kategori->nama
|
'nama' => $template->kategori->nama
|
||||||
] : null,
|
] : null,
|
||||||
'foto' => $template->foto ? asset('storage/' . $template->foto) : null,
|
'foto' => $template->foto ? asset('storage/' . $template->foto) : null,
|
||||||
'fiturs' => $template->fiturs ?? [],
|
'fiturs' => $template->fiturs ?? [],
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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 = [3, 5, 7]
|
const selectedIds = [3, 6, 9]
|
||||||
|
|
||||||
// State dropdown
|
// State dropdown
|
||||||
const openDropdownId = ref(null)
|
const openDropdownId = ref(null)
|
||||||
@ -88,6 +88,7 @@ const formMapping = {
|
|||||||
|
|
||||||
// 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')
|
||||||
|
console.log(templatesData.value)
|
||||||
|
|
||||||
// Mapping template: gabungkan backend + paket & fitur hardcode
|
// Mapping template: gabungkan backend + paket & fitur hardcode
|
||||||
const paketMapping = {
|
const paketMapping = {
|
||||||
@ -129,11 +130,12 @@ const templates = computed(() =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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 items-start">
|
||||||
<div v-for="t in templates" :key="t.id"
|
<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">
|
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="t.foto ? (t.foto.startsWith('http') ? t.foto : `http://localhost:8000${t.foto}`) : '/default.jpg'" :alt="t.nama_template" class="w-full h-48 object-cover" />
|
||||||
|
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<div class="p-5 text-center">
|
<div class="p-5 text-center">
|
||||||
|
|||||||
@ -14,7 +14,7 @@
|
|||||||
Kembali ke Beranda
|
Kembali ke Beranda
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<h1 class="text-3xl md:text-4xl font-bold text-center text-gray-800">
|
<h1 class="text-3xl md:text-4xl font-bold text-center text-gray-800">
|
||||||
Pilih Kategori Favoritmu
|
Pilih Kategori Favoritmu
|
||||||
@ -29,27 +29,38 @@
|
|||||||
Gagal memuat kategori.
|
Gagal memuat kategori.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Kategori Grid -->
|
<!-- Kategori Grid -->
|
||||||
<div v-else-if="categories.length > 0" class="mt-12 flex flex-wrap justify-center gap-6">
|
<div
|
||||||
<div v-for="category in categories" :key="category.id + '-' + category.foto"
|
v-else-if="categories.length > 0"
|
||||||
@click="onCategoryClick(category)"
|
class="mt-12 flex flex-wrap justify-center gap-6"
|
||||||
class="group cursor-pointer relative overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300 w-72">
|
>
|
||||||
<img :src="category.foto || '/ABBAUF.png'" :alt="category.nama"
|
<div
|
||||||
class="w-full h-96 object-cover transition-transform duration-300 group-hover:scale-110"
|
v-for="category in categories"
|
||||||
@error="(e) => e.target.src = '/ABBAUF.png'">
|
:key="category.id + '-' + category.foto"
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent"></div>
|
@click="onCategoryClick(category)"
|
||||||
<div class="absolute inset-0 flex flex-col justify-center items-start px-4 text-white">
|
class="group cursor-pointer relative overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300 w-72"
|
||||||
<h3 class="text-xl font-semibold mb-2">{{ category.nama }}</h3>
|
>
|
||||||
<p class="text-lg font-normal leading-snug whitespace-normal break-words max-w-[90%]">
|
<img
|
||||||
{{ category.deskripsi }}
|
v-if="category.foto"
|
||||||
</p>
|
:src="`http://localhost:8000${category.foto}`"
|
||||||
</div>
|
:alt="category.nama"
|
||||||
</div>
|
class="w-full h-96 object-cover transition-transform duration-300 group-hover:scale-110"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<p class="text-lg font-normal leading-snug whitespace-normal break-words max-w-[90%]">
|
||||||
|
{{ category.deskripsi }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-else class="mt-12 text-center text-gray-500">
|
<div v-else class="mt-12 text-center text-gray-500">
|
||||||
Belum ada kategori.
|
Belum ada kategori.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Header Templates -->
|
<!-- Header Templates -->
|
||||||
<div class="mt-20 text-center">
|
<div class="mt-20 text-center">
|
||||||
@ -67,12 +78,14 @@
|
|||||||
Belum ada template tersedia.
|
Belum ada template tersedia.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="grid gap-8 max-w-[1100px] mx-auto grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
<div v-else class="grid gap-8 max-w-[1100px] mx-auto grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 items-start">
|
||||||
<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 || '/logo1.png'" :alt="t.nama" class="w-full h-48 object-cover"
|
<img
|
||||||
@error="(e) => e.target.src = '/logo1.png'" />
|
:src="t.foto ? (t.foto.startsWith('http') ? t.foto : `http://localhost:8000${t.foto}`) : '/default.jpg'"
|
||||||
|
:alt="t.nama_template" class="w-full h-48 object-cover" />
|
||||||
|
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<div class="p-5 text-center">
|
<div class="p-5 text-center">
|
||||||
@ -114,11 +127,12 @@
|
|||||||
|
|
||||||
<!-- Buttons -->
|
<!-- Buttons -->
|
||||||
<div class="flex items-center gap-3 mt-6">
|
<div class="flex items-center gap-3 mt-6">
|
||||||
<NuxtLink :to="`/preview/${t.id}`"
|
<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 text-center">
|
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"
|
||||||
|
@click="onTemplateClick(t)">
|
||||||
Preview
|
Preview
|
||||||
</NuxtLink>
|
</button>
|
||||||
<NuxtLink :to="`/form/${t.slug}`"
|
<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>
|
||||||
@ -206,7 +220,8 @@ const paketData = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
// fetch kategori
|
|
||||||
|
// Fetch kategori
|
||||||
const fetchCategories = async () => {
|
const fetchCategories = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
@ -243,7 +258,6 @@ const templatesWithFeatures = computed(() =>
|
|||||||
(templatesRaw.value || []).map((t, index) => ({
|
(templatesRaw.value || []).map((t, index) => ({
|
||||||
id: t.id,
|
id: t.id,
|
||||||
nama: t.nama_template,
|
nama: t.nama_template,
|
||||||
slug: t.slug,
|
|
||||||
harga: t.harga,
|
harga: t.harga,
|
||||||
foto: t.foto || '/logo1.png',
|
foto: t.foto || '/logo1.png',
|
||||||
kategori: t.kategori,
|
kategori: t.kategori,
|
||||||
|
|||||||
@ -31,8 +31,10 @@
|
|||||||
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">
|
||||||
|
|
||||||
<!-- Gambar -->
|
<!-- Gambar -->
|
||||||
<img :src="tpl.foto" :alt="tpl.nama_template" class="w-full h-48 object-cover"
|
<img
|
||||||
@error="(e) => e.target.src = '/logo2.png'" />
|
:src="tpl.foto ? (tpl.foto.startsWith('http') ? tpl.foto : `http://localhost:8000${tpl.foto}`) : '/default.jpg'"
|
||||||
|
:alt="tpl.nama_template" class="w-full h-48 object-cover" />
|
||||||
|
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body -->
|
||||||
<div class="p-5 text-center">
|
<div class="p-5 text-center">
|
||||||
|
|||||||
@ -1,31 +1,769 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-24">
|
<div class="min-h-screen bg-gradient-to-br from-yellow-300 via-yellow-400 to-yellow-500 relative overflow-hidden">
|
||||||
<Introduction
|
<!-- Background Pattern -->
|
||||||
:age="age"
|
<div class="absolute inset-0 opacity-10">
|
||||||
:childName="childName"
|
<div class="absolute top-10 left-10 w-20 h-20 bg-yellow-600 rounded-full"></div>
|
||||||
:childOrder="childOrder"
|
<div class="absolute top-32 right-20 w-16 h-16 bg-yellow-600 rounded-full"></div>
|
||||||
:parentNames="parentNames"
|
<div class="absolute bottom-20 left-20 w-12 h-12 bg-yellow-600 rounded-full"></div>
|
||||||
:childPhoto="childPhoto"
|
<div class="absolute bottom-40 right-40 w-24 h-24 bg-yellow-600 rounded-full"></div>
|
||||||
/>
|
</div>
|
||||||
<Event />
|
|
||||||
<Gallery />
|
<!-- Navigation -->
|
||||||
<GuestBook />
|
<nav class="relative z-20 bg-transparent border-b border-yellow-600/20">
|
||||||
<ThankYou />
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex justify-center items-center h-16">
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<div class="ml-10 flex items-baseline space-x-8">
|
||||||
|
<a href="#introduction" @click="currentSection = 'introduction'"
|
||||||
|
:class="currentSection === 'introduction' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
|
||||||
|
class="px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
INTRODUCTION
|
||||||
|
</a>
|
||||||
|
<a href="#event" @click="currentSection = 'event'"
|
||||||
|
:class="currentSection === 'event' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
|
||||||
|
class="px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
EVENT
|
||||||
|
</a>
|
||||||
|
<a href="#galeri" @click="currentSection = 'galeri'"
|
||||||
|
:class="currentSection === 'galeri' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
|
||||||
|
class="px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
GALERI
|
||||||
|
</a>
|
||||||
|
<a href="#say" @click="currentSection = 'say'"
|
||||||
|
:class="currentSection === 'say' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
|
||||||
|
class="px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
SAY?
|
||||||
|
</a>
|
||||||
|
<a href="#thanks" @click="currentSection = 'thanks'"
|
||||||
|
:class="currentSection === 'thanks' ? 'text-orange-600 font-bold border-b-2 border-orange-600' : 'text-orange-800 hover:text-orange-600'"
|
||||||
|
class="px-3 py-2 text-sm font-medium transition-colors">
|
||||||
|
THANKS
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Mobile Navigation -->
|
||||||
|
<div class="md:hidden bg-yellow-400/90 backdrop-blur-sm">
|
||||||
|
<div class="px-2 pt-2 pb-3 space-y-1">
|
||||||
|
<a href="#introduction" @click="currentSection = 'introduction'"
|
||||||
|
:class="currentSection === 'introduction' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
|
||||||
|
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
|
||||||
|
INTRODUCTION
|
||||||
|
</a>
|
||||||
|
<a href="#event" @click="currentSection = 'event'"
|
||||||
|
:class="currentSection === 'event' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
|
||||||
|
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
|
||||||
|
EVENT
|
||||||
|
</a>
|
||||||
|
<a href="#galeri" @click="currentSection = 'galeri'"
|
||||||
|
:class="currentSection === 'galeri' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
|
||||||
|
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
|
||||||
|
GALERI
|
||||||
|
</a>
|
||||||
|
<a href="#say" @click="currentSection = 'say'"
|
||||||
|
:class="currentSection === 'say' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
|
||||||
|
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
|
||||||
|
SAY?
|
||||||
|
</a>
|
||||||
|
<a href="#thanks" @click="currentSection = 'thanks'"
|
||||||
|
:class="currentSection === 'thanks' ? 'bg-orange-600 text-white' : 'text-orange-800 hover:bg-orange-600 hover:text-white'"
|
||||||
|
class="block px-3 py-2 text-base font-medium rounded-md transition-colors">
|
||||||
|
THANKS
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Music Icon -->
|
||||||
|
<div class="fixed bottom-4 left-4 z-30">
|
||||||
|
<button @click="toggleMusic" class="bg-orange-600 hover:bg-orange-700 text-white p-3 rounded-full shadow-lg transition-colors">
|
||||||
|
<svg v-if="isPlaying" class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>
|
||||||
|
</svg>
|
||||||
|
<svg v-else class="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<main class="relative z-10 min-h-screen flex items-center justify-center p-4">
|
||||||
|
|
||||||
|
<!-- Landing Section -->
|
||||||
|
<section v-if="currentSection === 'landing'" class="w-full max-w-6xl mx-auto">
|
||||||
|
<div class="flex flex-col lg:flex-row items-center justify-between gap-8">
|
||||||
|
|
||||||
|
<!-- Left Side Content -->
|
||||||
|
<div class="flex-1 text-center lg:text-left">
|
||||||
|
<h1 class="text-orange-700 text-xl md:text-2xl font-semibold mb-4">
|
||||||
|
Celebrate With Us
|
||||||
|
</h1>
|
||||||
|
<h2 class="text-blue-600 text-4xl md:text-6xl font-bold mb-4">
|
||||||
|
{{ childName }}
|
||||||
|
</h2>
|
||||||
|
<h3 class="text-orange-700 text-2xl md:text-3xl font-bold mb-8">
|
||||||
|
Birthday Party
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-2xl p-6 mb-8 inline-block">
|
||||||
|
<p class="text-orange-800 text-lg mb-2">Kepada Yth.</p>
|
||||||
|
<p class="text-orange-700 text-xl font-semibold">{{ guestName }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="openInvitation" class="bg-orange-600 hover:bg-orange-700 text-white px-8 py-4 rounded-full text-lg font-semibold shadow-lg transform hover:scale-105 transition-all">
|
||||||
|
Open Invitation
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side - Minions -->
|
||||||
|
<div class="flex-1 relative">
|
||||||
|
<div class="relative w-full max-w-md mx-auto">
|
||||||
|
<!-- Minions Placeholder -->
|
||||||
|
<div class="bg-yellow-400 rounded-full w-64 h-64 mx-auto flex items-center justify-center border-4 border-yellow-600">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-20 h-20 bg-white rounded-full mx-auto mb-4 flex items-center justify-center border-4 border-gray-800">
|
||||||
|
<div class="w-12 h-12 bg-yellow-600 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-800 font-bold text-lg">Minions</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Additional minions -->
|
||||||
|
<div class="absolute -left-8 top-16 w-20 h-20 bg-yellow-400 rounded-full border-2 border-yellow-600 flex items-center justify-center">
|
||||||
|
<div class="w-6 h-6 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute -right-8 top-20 w-16 h-16 bg-yellow-400 rounded-full border-2 border-yellow-600 flex items-center justify-center">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Introduction Section -->
|
||||||
|
<section v-if="currentSection === 'introduction'" class="w-full max-w-6xl mx-auto">
|
||||||
|
<div class="flex flex-col lg:flex-row items-center justify-between gap-8">
|
||||||
|
|
||||||
|
<!-- Left Side Content -->
|
||||||
|
<div class="flex-1 text-center lg:text-left">
|
||||||
|
<h1 class="text-orange-700 text-2xl md:text-3xl font-bold mb-6">
|
||||||
|
Ulang Tahun Ke -{{ age }}
|
||||||
|
</h1>
|
||||||
|
<h2 class="text-orange-800 text-3xl md:text-5xl font-bold mb-6">
|
||||||
|
{{ childName }}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<h3 class="text-orange-700 text-xl md:text-2xl font-semibold mb-4">
|
||||||
|
Anak Ke -{{ childOrder }}
|
||||||
|
</h3>
|
||||||
|
<h4 class="text-orange-800 text-2xl md:text-3xl font-bold mb-8">
|
||||||
|
{{ parentNames }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<!-- Minions Animation Area -->
|
||||||
|
<div class="flex justify-center lg:justify-start gap-4 mb-8">
|
||||||
|
<div class="w-20 h-20 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600 transform hover:scale-110 transition-transform">
|
||||||
|
<div class="w-8 h-8 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-20 h-20 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600 transform hover:scale-110 transition-transform">
|
||||||
|
<div class="w-8 h-8 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side - Child Photo -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="relative w-full max-w-md mx-auto">
|
||||||
|
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl">
|
||||||
|
<img :src="childPhoto || '/assets/img/child-placeholder.jpg'"
|
||||||
|
:alt="childName"
|
||||||
|
class="w-full h-80 object-cover rounded-2xl shadow-lg">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Event Section -->
|
||||||
|
<section v-if="currentSection === 'event'" class="w-full max-w-6xl mx-auto">
|
||||||
|
<div class="flex flex-col lg:flex-row gap-8">
|
||||||
|
|
||||||
|
<!-- Left Side - Map -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-6 shadow-xl">
|
||||||
|
<div class="relative">
|
||||||
|
<!-- Minion Peeking -->
|
||||||
|
<div class="absolute -top-8 left-1/2 transform -translate-x-1/2 z-10">
|
||||||
|
<div class="w-16 h-8 bg-yellow-400 rounded-t-full border-2 border-yellow-600 flex items-end justify-center">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800 mb-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Placeholder -->
|
||||||
|
<div class="bg-gray-200 rounded-2xl h-64 flex items-center justify-center">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-gray-500 text-lg mb-2">📍 Location Map</div>
|
||||||
|
<div class="text-gray-400 text-sm">Google Maps Integration</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="absolute bottom-4 left-4 bg-orange-600 hover:bg-orange-700 text-white px-4 py-2 rounded-lg font-semibold">
|
||||||
|
Direction
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side - Event Details -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-6 shadow-xl">
|
||||||
|
<!-- Minion Peeking -->
|
||||||
|
<div class="absolute -top-8 right-8">
|
||||||
|
<div class="w-16 h-8 bg-yellow-400 rounded-t-full border-2 border-yellow-600 flex items-end justify-center">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800 mb-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative">
|
||||||
|
<h2 class="text-blue-600 text-2xl md:text-3xl font-bold mb-6 text-center">
|
||||||
|
BIRTHDAY PARTY
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Date and Time -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-center gap-4 mb-2">
|
||||||
|
<span class="text-orange-700 text-xl font-bold">{{ eventDay }}</span>
|
||||||
|
<span class="text-orange-700 text-lg">{{ eventDate }}</span>
|
||||||
|
<span class="text-orange-700 text-lg">{{ eventTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-orange-600 font-medium">{{ eventLocation }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Countdown -->
|
||||||
|
<div class="grid grid-cols-4 gap-2 mb-6">
|
||||||
|
<div class="bg-orange-600 rounded-lg p-3 text-center text-white">
|
||||||
|
<div class="text-xl font-bold">{{ countdown.days }}</div>
|
||||||
|
<div class="text-xs">D</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-orange-600 rounded-lg p-3 text-center text-white">
|
||||||
|
<div class="text-xl font-bold">{{ countdown.hours }}</div>
|
||||||
|
<div class="text-xs">H</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-orange-600 rounded-lg p-3 text-center text-white">
|
||||||
|
<div class="text-xl font-bold">{{ countdown.minutes }}</div>
|
||||||
|
<div class="text-xs">M</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-orange-600 rounded-lg p-3 text-center text-white">
|
||||||
|
<div class="text-xl font-bold">{{ countdown.seconds }}</div>
|
||||||
|
<div class="text-xs">S</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add to Calendar -->
|
||||||
|
<button class="w-full bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-lg font-semibold">
|
||||||
|
Add to Calendar
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Minions at bottom -->
|
||||||
|
<div class="flex justify-center mt-6 gap-4">
|
||||||
|
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-3 h-3 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-3 h-3 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Gallery Section -->
|
||||||
|
<section v-if="currentSection === 'galeri'" class="w-full max-w-6xl mx-auto">
|
||||||
|
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl relative">
|
||||||
|
|
||||||
|
<h2 class="text-orange-700 text-3xl md:text-4xl font-bold text-center mb-8">
|
||||||
|
Galeri
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<!-- Photo Grid -->
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-8">
|
||||||
|
<div v-for="(photo, index) in galleryPhotos" :key="index"
|
||||||
|
class="aspect-square bg-gray-200 rounded-xl overflow-hidden shadow-lg hover:scale-105 transition-transform">
|
||||||
|
<img :src="photo || '/assets/img/gallery-placeholder.jpg'"
|
||||||
|
:alt="`Gallery ${index + 1}`"
|
||||||
|
class="w-full h-full object-cover">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Minions around gallery -->
|
||||||
|
<div class="absolute -bottom-4 -left-4">
|
||||||
|
<div class="w-16 h-16 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-6 h-6 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute -top-4 -right-4">
|
||||||
|
<div class="w-16 h-16 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-6 h-6 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="absolute bottom-8 right-8 flex gap-2">
|
||||||
|
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Say Something Section -->
|
||||||
|
<section v-if="currentSection === 'say'" class="w-full max-w-6xl mx-auto">
|
||||||
|
<div class="flex flex-col lg:flex-row gap-8">
|
||||||
|
|
||||||
|
<!-- Left Side - Form -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl relative">
|
||||||
|
<!-- Minion Peeking -->
|
||||||
|
<div class="absolute -top-8 left-8">
|
||||||
|
<div class="w-16 h-8 bg-yellow-400 rounded-t-full border-2 border-yellow-600 flex items-end justify-center">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800 mb-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-orange-700 text-2xl font-bold mb-6">Say Something!</h3>
|
||||||
|
|
||||||
|
<form @submit.prevent="submitMessage" class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="text-orange-700 font-semibold mb-2 block">Nome</label>
|
||||||
|
<input v-model="guestMessage.name"
|
||||||
|
type="text"
|
||||||
|
class="w-full px-4 py-3 rounded-xl border-2 border-yellow-300 focus:border-orange-500 outline-none bg-white/80">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-orange-700 font-semibold mb-2 block">Message</label>
|
||||||
|
<textarea v-model="guestMessage.message"
|
||||||
|
rows="4"
|
||||||
|
class="w-full px-4 py-3 rounded-xl border-2 border-yellow-300 focus:border-orange-500 outline-none bg-white/80 resize-none"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="text-orange-700 font-semibold mb-2 block">Attendance</label>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<button type="button"
|
||||||
|
@click="guestMessage.attendance = 'yes'"
|
||||||
|
:class="guestMessage.attendance === 'yes' ? 'bg-green-600 text-white' : 'bg-white text-gray-700 hover:bg-green-100'"
|
||||||
|
class="px-4 py-2 rounded-lg font-semibold transition-colors">
|
||||||
|
Yes
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
@click="guestMessage.attendance = 'no'"
|
||||||
|
:class="guestMessage.attendance === 'no' ? 'bg-red-600 text-white' : 'bg-white text-gray-700 hover:bg-red-100'"
|
||||||
|
class="px-4 py-2 rounded-lg font-semibold transition-colors">
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
<button type="button"
|
||||||
|
@click="guestMessage.attendance = 'maybe'"
|
||||||
|
:class="guestMessage.attendance === 'maybe' ? 'bg-yellow-600 text-white' : 'bg-white text-gray-700 hover:bg-yellow-100'"
|
||||||
|
class="px-4 py-2 rounded-lg font-semibold transition-colors">
|
||||||
|
Maybe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
class="w-full bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-lg font-semibold">
|
||||||
|
Confirmation
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side - Messages -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl relative">
|
||||||
|
<!-- Minion Peeking -->
|
||||||
|
<div class="absolute -top-8 right-8">
|
||||||
|
<div class="w-16 h-8 bg-yellow-400 rounded-t-full border-2 border-yellow-600 flex items-end justify-center">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800 mb-1"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2 mb-6">
|
||||||
|
<span class="text-gray-600">💬</span>
|
||||||
|
<span class="text-orange-700 font-bold">{{ messages.length.toString().padStart(2, '0') }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Messages List -->
|
||||||
|
<div class="space-y-4 max-h-96 overflow-y-auto">
|
||||||
|
<div v-for="message in messages" :key="message.id"
|
||||||
|
class="bg-white/80 rounded-xl p-4">
|
||||||
|
<div class="flex justify-between items-start mb-2">
|
||||||
|
<span class="font-semibold text-orange-700">{{ message.name }}</span>
|
||||||
|
<span :class="getAttendanceClass(message.attendance)"
|
||||||
|
class="px-2 py-1 rounded-full text-xs font-semibold">
|
||||||
|
{{ message.attendance.charAt(0).toUpperCase() + message.attendance.slice(1) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-700">{{ message.message }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom Minions -->
|
||||||
|
<div class="absolute -bottom-4 right-4 flex gap-2">
|
||||||
|
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-4 h-4 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Thanks Section -->
|
||||||
|
<section v-if="currentSection === 'thanks'" class="w-full max-w-6xl mx-auto">
|
||||||
|
<div class="flex flex-col lg:flex-row items-center justify-between gap-8">
|
||||||
|
|
||||||
|
<!-- Left Side Content -->
|
||||||
|
<div class="flex-1 text-center lg:text-left">
|
||||||
|
<p class="text-orange-700 text-lg md:text-xl leading-relaxed mb-8">
|
||||||
|
Merupakan suatu kebahagiaan dan kehormatan bagi kami, apabila teman-teman,
|
||||||
|
berkenan hadir dan memberikan do'a.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h3 class="text-orange-700 text-xl md:text-2xl font-semibold mb-4">
|
||||||
|
Hormat Kami
|
||||||
|
</h3>
|
||||||
|
<h4 class="text-orange-800 text-2xl md:text-3xl font-bold mb-8">
|
||||||
|
{{ parentNames }}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<!-- Minion -->
|
||||||
|
<div class="flex justify-center lg:justify-start">
|
||||||
|
<div class="w-24 h-24 bg-yellow-400 rounded-full flex items-center justify-center border-4 border-yellow-600 transform hover:scale-110 transition-transform">
|
||||||
|
<div class="w-10 h-10 bg-white rounded-full border-2 border-gray-800 flex items-center justify-center">
|
||||||
|
<div class="w-6 h-6 bg-yellow-600 rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side - Family Photo -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="relative w-full max-w-md mx-auto">
|
||||||
|
<div class="bg-yellow-300/60 backdrop-blur-sm rounded-3xl p-8 shadow-xl">
|
||||||
|
<img :src="familyPhoto || '/assets/img/family-placeholder.jpg'"
|
||||||
|
:alt="parentNames"
|
||||||
|
class="w-full h-80 object-cover rounded-2xl shadow-lg">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decorative minion -->
|
||||||
|
<div class="absolute -bottom-2 -left-2">
|
||||||
|
<div class="w-16 h-16 bg-yellow-400 rounded-full flex items-center justify-center border-2 border-yellow-600">
|
||||||
|
<div class="w-6 h-6 bg-white rounded-full border border-gray-800"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import Introduction from './Introduction.vue'
|
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||||
import Event from './Event.vue'
|
|
||||||
import Gallery from './Gallery.vue'
|
|
||||||
import GuestBook from './GuestBook.vue'
|
|
||||||
import ThankYou from './ThankYou.vue'
|
|
||||||
|
|
||||||
defineProps({
|
// Props/Data yang bisa diisi dari parent atau API
|
||||||
age: Number,
|
const childName = ref('Rayyanza Malik Ahmad')
|
||||||
childName: String,
|
const age = ref(4)
|
||||||
childOrder: Number,
|
const childOrder = ref(2)
|
||||||
parentNames: String,
|
const parentNames = ref('Raffi Ahmad & Nagita Slavina')
|
||||||
childPhoto: String
|
const guestName = ref('Gempita Nora Marten')
|
||||||
|
const eventDay = ref('MINGGU')
|
||||||
|
const eventDate = ref('11 JUNI 2025')
|
||||||
|
const eventTime = ref('11.00 WITA S.D SELESAI')
|
||||||
|
const eventLocation = ref('Jalan Andara No.17 Cilandak Pondok Labu, DKI Jakarta Green Andara Residence')
|
||||||
|
|
||||||
|
// Photos
|
||||||
|
const childPhoto = ref('')
|
||||||
|
const familyPhoto = ref('')
|
||||||
|
const galleryPhotos = ref([
|
||||||
|
'/assets/img/gallery1.jpg',
|
||||||
|
'/assets/img/gallery2.jpg',
|
||||||
|
'/assets/img/gallery3.jpg',
|
||||||
|
'/assets/img/gallery4.jpg',
|
||||||
|
'/assets/img/gallery5.jpg',
|
||||||
|
'/assets/img/gallery6.jpg'
|
||||||
|
])
|
||||||
|
|
||||||
|
// State management
|
||||||
|
const currentSection = ref('landing')
|
||||||
|
const isPlaying = ref(false)
|
||||||
|
|
||||||
|
// Countdown timer
|
||||||
|
const countdown = reactive({
|
||||||
|
days: 0,
|
||||||
|
hours: 0,
|
||||||
|
minutes: 0,
|
||||||
|
seconds: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Guest message form
|
||||||
|
const guestMessage = reactive({
|
||||||
|
name: '',
|
||||||
|
message: '',
|
||||||
|
attendance: 'yes'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Messages list
|
||||||
|
const messages = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'Gempita',
|
||||||
|
message: 'Selamat ulang tahun cipung',
|
||||||
|
attendance: 'yes'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: 'Ayu Ting Ting',
|
||||||
|
message: 'Selamat ulang tahun anak mama gigi',
|
||||||
|
attendance: 'maybe'
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const openInvitation = () => {
|
||||||
|
currentSection.value = 'introduction'
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleMusic = () => {
|
||||||
|
isPlaying.value = !isPlaying.value
|
||||||
|
// TODO: Implement actual music toggle
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitMessage = () => {
|
||||||
|
if (guestMessage.name && guestMessage.message) {
|
||||||
|
messages.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
name: guestMessage.name,
|
||||||
|
message: guestMessage.message,
|
||||||
|
attendance: guestMessage.attendance
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
guestMessage.name = ''
|
||||||
|
guestMessage.message = ''
|
||||||
|
guestMessage.attendance = 'yes'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAttendanceClass = (attendance) => {
|
||||||
|
switch (attendance) {
|
||||||
|
case 'yes':
|
||||||
|
case 'attend':
|
||||||
|
return 'bg-green-500 text-white'
|
||||||
|
case 'no':
|
||||||
|
return 'bg-red-500 text-white'
|
||||||
|
case 'maybe':
|
||||||
|
return 'bg-yellow-500 text-white'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-500 text-white'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Countdown timer function
|
||||||
|
const updateCountdown = () => {
|
||||||
|
const eventDate = new Date('2025-06-11T11:00:00')
|
||||||
|
const now = new Date()
|
||||||
|
const diff = eventDate - now
|
||||||
|
|
||||||
|
if (diff > 0) {
|
||||||
|
countdown.days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||||
|
countdown.hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||||
|
countdown.minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||||
|
countdown.seconds = Math.floor((diff % (1000 * 60)) / 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
let countdownInterval = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
updateCountdown()
|
||||||
|
countdownInterval = setInterval(updateCountdown, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
if (countdownInterval) {
|
||||||
|
clearInterval(countdownInterval)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
useHead({
|
||||||
|
title: `${childName.value} Birthday Invitation`,
|
||||||
|
meta: [
|
||||||
|
{ name: 'description', content: `Join us to celebrate ${childName.value}'s ${age.value}th birthday party!` }
|
||||||
|
]
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Custom animations */
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% { transform: translateY(0px); }
|
||||||
|
50% { transform: translateY(-10px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.float-animation {
|
||||||
|
animation: float 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions */
|
||||||
|
* {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbar styling */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #fbbf24;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #ea580c;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #c2410c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom button hover effects */
|
||||||
|
button {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glass morphism effect */
|
||||||
|
.backdrop-blur-sm {
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
-webkit-backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive typography */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.text-6xl { font-size: 2.5rem; }
|
||||||
|
.text-5xl { font-size: 2rem; }
|
||||||
|
.text-4xl { font-size: 1.75rem; }
|
||||||
|
.text-3xl { font-size: 1.5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom grid for gallery */
|
||||||
|
.grid-cols-2 {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:grid-cols-3 {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading animation for images */
|
||||||
|
img {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
img[src=""], img:not([src]) {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom yellow gradient */
|
||||||
|
.bg-gradient-to-br {
|
||||||
|
background-image: linear-gradient(to bottom right, #fcd34d, #f59e0b, #d97706);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Minion eye animation */
|
||||||
|
@keyframes blink {
|
||||||
|
0%, 90%, 100% { transform: scaleY(1); }
|
||||||
|
95% { transform: scaleY(0.1); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.minion-eye {
|
||||||
|
animation: blink 3s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Party confetti effect */
|
||||||
|
@keyframes confetti {
|
||||||
|
0% { transform: translateY(-100px) rotate(0deg); opacity: 1; }
|
||||||
|
100% { transform: translateY(100vh) rotate(720deg); opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.confetti {
|
||||||
|
animation: confetti 3s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile menu slide */
|
||||||
|
.mobile-menu {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu.open {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card hover effects */
|
||||||
|
.card-hover {
|
||||||
|
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text glow effect */
|
||||||
|
.text-glow {
|
||||||
|
text-shadow: 0 0 10px rgba(251, 191, 36, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse animation for important elements */
|
||||||
|
@keyframes pulse-soft {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.pulse-soft {
|
||||||
|
animation: pulse-soft 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -9,7 +9,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import WeddingA from '../templates/wedding/WeddingA.vue'
|
import WeddingA from '~/components/templates/wedding/weddingA.vue'
|
||||||
|
|
||||||
// Props dari parent (/p/[code].vue)
|
// Props dari parent (/p/[code].vue)
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|||||||
@ -33,7 +33,9 @@
|
|||||||
|
|
||||||
<!-- Event -->
|
<!-- Event -->
|
||||||
<Event v-if="currentSection === 'event'" :hari_tanggal_acara="formData.hari_tanggal_acara"
|
<Event v-if="currentSection === 'event'" :hari_tanggal_acara="formData.hari_tanggal_acara"
|
||||||
:waktu="formData.waktu" :alamat="formData.alamat" :link_gmaps="formData.link_gmaps"
|
:waktu="formData.waktu"
|
||||||
|
:alamat="formData.alamat"
|
||||||
|
:link_gmaps="formData.link_gmaps"
|
||||||
:hitung_mundur="formData.hitung_mundur" />
|
:hitung_mundur="formData.hitung_mundur" />
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -158,6 +158,10 @@ import { useRouter } from 'vue-router'
|
|||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Nomor WhatsApp admin (ganti sesuai kebutuhan)
|
||||||
|
const ADMIN_WA = '62895333053398' // format internasional tanpa "+"
|
||||||
|
|
||||||
|
// ======================= FORM ==========================
|
||||||
const form = ref({
|
const form = ref({
|
||||||
nama_pemesan: '',
|
nama_pemesan: '',
|
||||||
email: '',
|
email: '',
|
||||||
@ -187,6 +191,7 @@ const form = ref({
|
|||||||
foto: []
|
foto: []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ===================== FOTO PREVIEW =====================
|
||||||
const previews = ref([])
|
const previews = ref([])
|
||||||
|
|
||||||
const handleFileChange = (e) => {
|
const handleFileChange = (e) => {
|
||||||
@ -200,7 +205,6 @@ const handleFileChange = (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
// Validate file size (2MB) and type
|
|
||||||
if (file.size > 2 * 1024 * 1024) {
|
if (file.size > 2 * 1024 * 1024) {
|
||||||
alert(`File ${file.name} terlalu besar! Maksimal 2MB.`)
|
alert(`File ${file.name} terlalu besar! Maksimal 2MB.`)
|
||||||
return
|
return
|
||||||
@ -212,7 +216,6 @@ const handleFileChange = (e) => {
|
|||||||
form.value.foto.push(file)
|
form.value.foto.push(file)
|
||||||
previews.value.push(URL.createObjectURL(file))
|
previews.value.push(URL.createObjectURL(file))
|
||||||
})
|
})
|
||||||
|
|
||||||
e.target.value = ''
|
e.target.value = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,9 +224,9 @@ const removeFile = (index) => {
|
|||||||
previews.value.splice(index, 1)
|
previews.value.splice(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== KONFIRMASI ======================
|
||||||
const konfirmasi = async () => {
|
const konfirmasi = async () => {
|
||||||
try {
|
try {
|
||||||
// Basic client-side validation
|
|
||||||
if (!form.value.nama_pemesan || !form.value.email) {
|
if (!form.value.nama_pemesan || !form.value.email) {
|
||||||
alert('Harap isi kolom wajib (Nama Pemesan, Email)!')
|
alert('Harap isi kolom wajib (Nama Pemesan, Email)!')
|
||||||
return
|
return
|
||||||
@ -235,7 +238,6 @@ const konfirmasi = async () => {
|
|||||||
data.append('no_tlpn', form.value.no_tlpn)
|
data.append('no_tlpn', form.value.no_tlpn)
|
||||||
data.append('template_slug', 'undangan-ulang-tahun-premium')
|
data.append('template_slug', 'undangan-ulang-tahun-premium')
|
||||||
|
|
||||||
// Append form fields individually to ensure Laravel receives them as an array
|
|
||||||
for (const [key, value] of Object.entries(form.value.form)) {
|
for (const [key, value] of Object.entries(form.value.form)) {
|
||||||
data.append(`form[${key}]`, value)
|
data.append(`form[${key}]`, value)
|
||||||
}
|
}
|
||||||
@ -244,8 +246,6 @@ const konfirmasi = async () => {
|
|||||||
data.append(`foto[${index}]`, file)
|
data.append(`foto[${index}]`, file)
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log([...data]) // untuk debugging
|
|
||||||
|
|
||||||
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
const res = await fetch('http://localhost:8000/api/pelanggans', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: data
|
body: data
|
||||||
@ -265,7 +265,46 @@ const konfirmasi = async () => {
|
|||||||
throw new Error(result.message || 'Gagal mengirim data')
|
throw new Error(result.message || 'Gagal mengirim data')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ===================== SUKSES -> BUKA WHATSAPP ======================
|
||||||
alert(result.message || 'Data berhasil disimpan!')
|
alert(result.message || 'Data berhasil disimpan!')
|
||||||
|
|
||||||
|
const f = form.value
|
||||||
|
const child = f.form
|
||||||
|
|
||||||
|
const message = `
|
||||||
|
Halo Admin 👋,
|
||||||
|
Saya ingin membuat *Undangan Ulang Tahun Premium* 🎂
|
||||||
|
|
||||||
|
📋 *Data Pemesan*
|
||||||
|
- Nama: ${f.nama_pemesan}
|
||||||
|
- Email: ${f.email}
|
||||||
|
- No. Telepon: ${f.no_tlpn}
|
||||||
|
|
||||||
|
🎉 *Data Anak*
|
||||||
|
- Nama Lengkap: ${child.nama_lengkap}
|
||||||
|
- Nama Panggilan: ${child.nama_panggilan}
|
||||||
|
- Umur: ${child.umur_yang_dirayakan} tahun
|
||||||
|
- Anak ke: ${child.anak_ke}
|
||||||
|
- Bapak: ${child.nama_bapak}
|
||||||
|
- Ibu: ${child.nama_ibu}
|
||||||
|
|
||||||
|
📅 *Detail Acara*
|
||||||
|
- Tanggal: ${child.hari_tanggal_acara}
|
||||||
|
- Waktu: ${child.waktu}
|
||||||
|
- Alamat: ${child.alamat}
|
||||||
|
- Link Gmaps: ${child.link_gmaps || '-'}
|
||||||
|
|
||||||
|
💌 *Pesan Tambahan*
|
||||||
|
${child.say_something || '-'}
|
||||||
|
|
||||||
|
Terima kasih 🙏
|
||||||
|
`
|
||||||
|
|
||||||
|
const encodedMsg = encodeURIComponent(message)
|
||||||
|
const waUrl = `https://wa.me/${ADMIN_WA}?text=${encodedMsg}`
|
||||||
|
|
||||||
|
window.open(waUrl, '_blank')
|
||||||
|
|
||||||
router.push('/')
|
router.push('/')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
@ -274,4 +313,4 @@ const konfirmasi = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const batal = () => router.back()
|
const batal = () => router.back()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user