form pernikahan

This commit is contained in:
Farhaan4 2025-10-07 10:36:44 +07:00
parent 54c237d211
commit ce4e10f3ac
45 changed files with 6940 additions and 1177 deletions

View File

@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Pelanggan;
use App\Models\Template;
use Illuminate\Http\Request;
class PelangganApiController extends Controller
{
public function store(Request $request)
{
$template = Template::findOrFail($request->input('template_id'));
$pelanggan = Pelanggan::create([
'nama_pemesan' => $request->nama_pemesan,
'email' => $request->email,
'no_tlpn' => $request->no_tlpn,
'template_id' => $template->id,
'form' => $request->input('form'),
'harga' => $template->harga,
'status' => 'menunggu',
]);
return response()->json([
'success' => true,
'message' => 'Pesanan berhasil dibuat',
'data' => $pelanggan
], 201);
}
}

View File

@ -38,4 +38,29 @@ class TemplateApiController extends Controller
'foto' => $template->foto ? asset('storage/' . $template->foto) : null, 'foto' => $template->foto ? asset('storage/' . $template->foto) : null,
]); ]);
} }
public function getByCategory($id)
{
$templates = Template::with('fiturs', 'kategori')
->where('kategori_id', $id)
->get();
$transformed = $templates->map(function($template) {
return [
'id' => $template->id,
'nama_template' => $template->nama_template,
'harga' => $template->harga,
'paket' => $template->paket,
'kategori' => $template->kategori ? [
'id' => $template->kategori->id,
'nama' => $template->kategori->nama
] : null,
'foto' => $template->foto ? asset('storage/' . $template->foto) : null,
'fiturs' => $template->fiturs ?? [],
];
});
return response()->json($transformed);
}
} }

View File

@ -4,6 +4,7 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\TemplateApiController; use App\Http\Controllers\Api\TemplateApiController;
use App\Http\Controllers\Api\KategoriApiController; use App\Http\Controllers\Api\KategoriApiController;
use App\Http\Controllers\Api\PelangganApiController;
Route::get('kategoris', [KategoriApiController::class, 'index']); Route::get('kategoris', [KategoriApiController::class, 'index']);
Route::get('kategoris/{kategori}', [KategoriApiController::class, 'show']); Route::get('kategoris/{kategori}', [KategoriApiController::class, 'show']);
@ -12,3 +13,6 @@ Route::get('/templates', [TemplateApiController::class, 'index']);
Route::get('/templates/{template}', [TemplateApiController::class, 'show']); Route::get('/templates/{template}', [TemplateApiController::class, 'show']);
Route::get('/templates/category/{id}', [TemplateApiController::class, 'getByCategory']);
Route::post('/pelanggans', [PelangganApiController::class, 'store']);

View File

@ -1,289 +0,0 @@
<template>
<div class="max-w-5xl mx-auto p-8 bg-gradient-to-b from-green-50 to-blue-50 shadow-lg rounded-xl">
<!-- Judul Form -->
<div class="text-center mb-10">
<h1 class="text-3xl md:text-4xl font-extrabold text-green-700 drop-shadow-sm">
🕌 Form Pemesanan Undangan Khitan
</h1>
<p class="text-gray-600 mt-2">
Isi data berikut dengan lengkap untuk pemesanan undangan khitan.
</p>
</div>
<form @submit.prevent="submitForm" class="space-y-10">
<!-- Tema Undangan -->
<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 flex items-center gap-2">
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Tema Undangan
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<input :value="form.nama_template" type="text" placeholder="Nama Template" class="input-readonly" readonly />
<input :value="form.kategori" type="text" placeholder="Kategori" class="input-readonly" readonly />
<input :value="form.harga" type="text" placeholder="Harga" class="input-readonly" readonly />
<input :value="form.tanggal_pemesanan" type="text" placeholder="Tanggal Pemesanan" class="input-readonly"
readonly />
</div>
</section>
<!-- Pemesan Undangan -->
<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 flex items-center gap-2">
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Pemesan Undangan
</h2>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input v-model="form.nama_pemesan" type="text" placeholder="Nama Pemesan" class="input" required />
<input v-model="form.no_hp" type="text" placeholder="No. WhatsApp" class="input" required />
</div>
<input v-model="form.email" type="email" placeholder="Email" class="input" required />
</div>
</section>
<!-- Detail Khitan -->
<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 Khitan</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<input v-model="form.nama_lengkap_anak" type="text" placeholder="Nama Lengkap Anak" class="input" required />
<input v-model="form.nama_panggilan_anak" type="text" placeholder="Nama Panggilan Anak" class="input"
required />
<input v-model="form.bapak_anak" type="text" placeholder="Nama Bapak" class="input" />
<input v-model="form.ibu_anak" type="text" placeholder="Nama Ibu" class="input" />
</div>
</section>
<!-- Jadwal 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">Jadwal Acara</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<input v-model="form.hari_tanggal_acara" type="date" class="input" />
<input v-model="form.waktu_acara" type="text" placeholder="08.00 WIB" class="input" />
<textarea v-model="form.alamat_acara" placeholder="Alamat Acara" rows="3" class="input col-span-2"></textarea>
<input v-model="form.maps_acara" type="text" placeholder="Link Google Maps" class="input col-span-2" />
</div>
</section>
<!-- Rekening, Musik, Galeri -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-6">
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="text-lg font-bold text-gray-800">No. Rekening</h2>
<input v-model="form.no_rekening1" type="text" placeholder="Rekening 1" class="input" />
<input v-model="form.no_rekening2" type="text" placeholder="Rekening 2" class="input" />
</div>
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
<h2 class="text-lg font-bold text-gray-800">Musik</h2>
<input v-model="form.link_musik" type="text" placeholder="Link Musik" class="input" />
</div>
</div>
<div 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 (max 5 gambar)</h2>
<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"
aria-label="Hapus gambar">
&times;
</button>
</div>
<label v-if="previewImages.length < 5" for="gallery-upload"
class="flex items-center justify-center w-full aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
</div>
</div>
</section>
</form>
<!-- Pilihan Fitur -->
<section v-for="kategori in kategoriFiturs" :key="kategori.id"
class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
<h2 class="text-lg font-bold text-gray-800 mb-4">{{ kategori.nama }}</h2>
<!-- Radio -->
<div v-if="kategori.tipe === 'radio'" class="space-y-2">
<label v-for="fitur in kategori.fiturs" :key="fitur.id" class="flex items-center gap-2">
<input type="radio" :name="'fitur_' + kategori.id" :value="fitur.id"
v-model="form.selectedFiturs[kategori.id]" />
{{ fitur.deskripsi }}
</label>
</div>
<!-- Checkbox -->
<div v-else class="space-y-2">
<label v-for="fitur in kategori.fiturs" :key="fitur.id" class="flex items-center gap-2">
<input type="checkbox" :value="fitur.id" v-model="form.selectedFiturs[kategori.id]" />
{{ fitur.deskripsi }}
</label>
</div>
</section>
<!-- Submit -->
<div class="mt-10 text-center">
<button @click="submitForm"
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!
</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. Pastikan semua data yang wajib diisi sudah lengkap.
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const form = ref({
template_id: "",
nama_template: "",
kategori: "",
harga: "",
tanggal_pemesanan: new Date().toISOString().split("T")[0],
nama_pemesan: "",
no_hp: "",
email: "",
nama_lengkap_anak: "",
nama_panggilan_anak: "",
bapak_anak: "",
ibu_anak: "",
hari_tanggal_acara: "",
waktu_acara: "",
alamat_acara: "",
maps_acara: "",
no_rekening1: "",
no_rekening2: "",
link_musik: "",
galeri: [],
selectedFiturs: {}, // { kategori_id: [fitur_id] }
});
const previewImages = ref([]);
const loading = ref(false);
const success = ref(false);
const error = ref(false);
onMounted(async () => {
if (route.query.template_id) {
try {
const template = await $fetch(`http://localhost:8000/api/templates/${route.query.template_id}`);
form.value.template_id = template.id;
form.value.nama_template = template.nama_template;
form.value.kategori_id = template.kategori_id;
form.value.kategori = template.kategori?.nama || "-";
form.value.harga = template.harga;
// simpan kategori fitur
kategoriFiturs.value = template.kategori_fiturs || [];
} catch (err) {
console.error("Gagal ambil template", err);
}
}
});
// FUNGSI UNTUK MENAMBAH GAMBAR
const handleFileUpload = (event) => {
const newFiles = Array.from(event.target.files);
const combinedFiles = [...form.value.galeri, ...newFiles];
// Batasi total file menjadi 5
form.value.galeri = combinedFiles.slice(0, 5);
// Buat ulang array preview berdasarkan data file yang sudah final
previewImages.value = [];
form.value.galeri.forEach(file => {
const reader = new FileReader();
reader.onload = (e) => {
previewImages.value.push(e.target.result);
};
reader.readAsDataURL(file);
});
// Reset input agar bisa memilih file yang sama lagi
event.target.value = null;
};
// FUNGSI UNTUK MENGHAPUS GAMBAR (SEKARANG DI LUAR)
const removeImage = (index) => {
form.value.galeri.splice(index, 1);
previewImages.value.splice(index, 1);
};
const submitForm = async () => {
loading.value = true;
success.value = false;
error.value = false;
try {
const body = new FormData();
for (const key in form.value) {
if (key === "galeri") {
form.value.galeri.forEach((file) => body.append("galeri[]", file));
} else if (key !== "selectedFiturs") {
body.append(key, form.value[key]);
}
}
// kirim fitur sebagai array fitur_id
for (const kategoriId in form.value.selectedFiturs) {
const fiturs = Array.isArray(form.value.selectedFiturs[kategoriId])
? form.value.selectedFiturs[kategoriId]
: [form.value.selectedFiturs[kategoriId]];
fiturs.forEach(fiturId => body.append("fiturs[]", fiturId));
}
await $fetch("http://localhost:8000/api/form/khitan", {
method: "POST",
body,
});
success.value = true;
// WA notif (biar tetap jalan)
const adminNumber = "62895602603247";
const message = `
Halo Admin, ada pemesanan undangan khitan baru 🎉
Nama Pemesan: ${form.value.nama_pemesan}
No WA: ${form.value.no_hp}
Email: ${form.value.email}
Template: ${form.value.nama_template} (${form.value.kategori})
Harga: ${form.value.harga}
Tanggal Pemesanan: ${form.value.tanggal_pemesanan}
`;
window.location.href = `https://wa.me/${adminNumber}?text=${encodeURIComponent(message)}`;
} catch (err) {
console.error(err);
error.value = true;
} finally {
loading.value = false;
}
};
</script>

View File

@ -1,355 +0,0 @@
<template>
<div class="max-w-5xl mx-auto p-8 bg-gradient-to-b from-pink-50 to-red-50 shadow-lg rounded-xl">
<!-- Judul Form -->
<div class="text-center mb-10">
<h1 class="text-3xl md:text-4xl font-extrabold text-red-700 drop-shadow-sm">
💍 Form Pemesanan Undangan Pernikahan 💐
</h1>
<p class="text-gray-600 mt-2">
Silakan isi data berikut untuk melakukan pemesanan undangan pernikahan Anda.
</p>
</div>
<form @submit.prevent="submitForm" class="space-y-10">
<!-- Tema Undangan -->
<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 flex items-center gap-2">
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Tema Undangan
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<input :value="form.nama_template" type="text" placeholder="Nama Template" class="input-readonly" readonly />
<input :value="form.kategori" type="text" placeholder="Kategori" class="input-readonly" readonly />
<input :value="form.harga" type="text" placeholder="Harga" class="input-readonly" readonly />
<input :value="form.tanggal_pemesanan" type="text" placeholder="Tanggal Pemesanan" class="input-readonly"
readonly />
</div>
</section>
<!-- Pemesan Undangan -->
<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 flex items-center gap-2">
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Pemesan Undangan
</h2>
<div class="space-y-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<input v-model="form.nama_pemesan" type="text" placeholder="Nama" class="input" required />
<input v-model="form.no_hp" type="text" placeholder="No. WhatsApp" class="input" required />
</div>
<input v-model="form.email" type="email" placeholder="Email" class="input" required />
</div>
</section>
<!-- Mempelai -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="text-lg font-bold text-gray-800">Mempelai Pria</h2>
<input v-model="form.nama_lengkap_pria" type="text" placeholder="Nama Lengkap" class="input" required />
<input v-model="form.nama_panggilan_pria" type="text" placeholder="Nama Panggilan" class="input" required />
<input v-model="form.bapak_pria" type="text" placeholder="Nama Bapak" class="input" />
<input v-model="form.ibu_pria" type="text" placeholder="Nama Ibu" class="input" />
<input v-model="form.instagram_pria" type="text" placeholder="Instagram" class="input" />
<input v-model="form.facebook_pria" type="text" placeholder="Facebook" class="input" />
<input v-model="form.twitter_pria" type="text" placeholder="Twitter" class="input" />
</div>
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="text-lg font-bold text-gray-800">Mempelai Wanita</h2>
<input v-model="form.nama_lengkap_wanita" type="text" placeholder="Nama Lengkap" class="input" required />
<input v-model="form.nama_panggilan_wanita" type="text" placeholder="Nama Panggilan" class="input" required />
<input v-model="form.bapak_wanita" type="text" placeholder="Nama Bapak" class="input" />
<input v-model="form.ibu_wanita" type="text" placeholder="Nama Ibu" class="input" />
<input v-model="form.instagram_wanita" type="text" placeholder="Instagram" class="input" />
<input v-model="form.facebook_wanita" type="text" placeholder="Facebook" class="input" />
<input v-model="form.twitter_wanita" type="text" placeholder="Twitter" class="input" />
</div>
</section>
<!-- Cerita Kita -->
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
<h2 class="text-lg font-bold text-gray-800 mb-10">Cerita Kita</h2>
<textarea v-model="form.cerita_kita" placeholder="Tuliskan cerita indah kalian di sini..." rows="1"
class="w-full border border-gray-300 rounded-md px-3 py-3 focus:ring-2 focus:ring-blue-500 focus:outline-none transition resize-none"
@input="e => { e.target.style.height = 'auto'; e.target.style.height = e.target.scrollHeight + 'px' }" />
</section>
<!-- Akad & Resepsi -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="text-lg font-bold text-gray-800">Akad</h2>
<input v-model="form.hari_tanggal_akad" type="date" class="input" />
<input v-model="form.waktu_akad" type="text" placeholder="Waktu" class="input" />
<input v-model="form.alamat_akad" type="text" placeholder="Alamat" class="input" />
<input v-model="form.maps_akad" type="text" placeholder="Link Google Maps" class="input" />
</div>
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="text-lg font-bold text-gray-800">Resepsi</h2>
<input v-model="form.hari_tanggal_resepsi" type="date" class="input" />
<input v-model="form.waktu_resepsi" type="text" placeholder="Waktu" class="input" />
<input v-model="form.alamat_resepsi" type="text" placeholder="Alamat" class="input" />
<input v-model="form.maps_resepsi" type="text" placeholder="Link Google Maps" class="input" />
</div>
</section>
<!-- Rekening, Musik, Galeri -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="space-y-6">
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="text-lg font-bold text-gray-800">No. Rekening</h2>
<input v-model="form.no_rekening1" type="text" placeholder="Rekening 1" class="input" />
<input v-model="form.no_rekening2" type="text" placeholder="Rekening 2" class="input" />
</div>
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
<h2 class="text-lg font-bold text-gray-800">Musik</h2>
<input v-model="form.link_musik" type="text" placeholder="Link Musik" class="input" />
</div>
</div>
<div 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 (max 5 gambar)</h2>
<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"
aria-label="Hapus gambar">
&times;
</button>
</div>
<label v-if="previewImages.length < 5" for="gallery-upload"
class="flex items-center justify-center w-full aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
</div>
</div>
</section>
</form>
<!-- Kategori & Fitur -->
<section v-for="kategori in kategoriFiturs" :key="kategori.id"
class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
<h2 class="text-lg font-bold text-gray-800 mb-4">
{{ kategori.nama }}
</h2>
<!-- Radio -->
<div v-if="kategori.tipe === 'radio'">
<label v-for="fitur in kategori.fiturs" :key="fitur.id" class="flex items-center gap-2 mb-2">
<input type="radio" :name="'kategori_' + kategori.id" :value="fitur.id"
v-model="form.selectedFiturs[kategori.id]" />
{{ fitur.deskripsi }}
</label>
</div>
<!-- Checkbox -->
<div v-else>
<label v-for="fitur in kategori.fiturs" :key="fitur.id" class="flex items-center gap-2 mb-2">
<input type="checkbox" :value="fitur.id" v-model="form.selectedFiturs[kategori.id]" />
{{ fitur.deskripsi }}
</label>
</div>
</section>
<!-- Submit -->
<div class="mt-10 text-center">
<button @click="submitForm"
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! Silakan tunggu konfirmasi dari 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.
Pastikan semua data yang wajib diisi sudah lengkap.</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const form = ref({
template_id: "",
nama_template: "",
kategori: "",
harga: "",
tanggal_pemesanan: new Date().toLocaleDateString("id-ID", {
year: "numeric",
month: "long",
day: "numeric",
}),
nama_pemesan: "",
no_hp: "",
email: "",
nama_lengkap_pria: "",
nama_panggilan_pria: "",
bapak_pria: "",
ibu_pria: "",
instagram_pria: "",
facebook_pria: "",
twitter_pria: "",
nama_lengkap_wanita: "",
nama_panggilan_wanita: "",
bapak_wanita: "",
ibu_wanita: "",
instagram_wanita: "",
facebook_wanita: "",
twitter_wanita: "",
cerita_kita: "",
hari_tanggal_akad: "",
waktu_akad: "",
alamat_akad: "",
maps_akad: "",
hari_tanggal_resepsi: "",
waktu_resepsi: "",
alamat_resepsi: "",
maps_resepsi: "",
no_rekening1: "",
no_rekening2: "",
link_musik: "",
galeri: [],
selectedFiturs: {}, // { kategori_id: [fitur_id] }
});
const kategoriFiturs = ref([]); // 🆕 simpan kategori fitur
const previewImages = ref([]);
const loading = ref(false);
const success = ref(false);
const error = ref(false);
onMounted(async () => {
if (route.query.template_id) {
try {
const template = await $fetch(
`http://localhost:8000/api/templates/${route.query.template_id}`
);
form.value.template_id = template.id;
form.value.nama_template = template.nama_template;
form.value.kategori = template.kategori?.nama || "-";
form.value.harga = new Intl.NumberFormat("id-ID", {
style: "currency",
currency: "IDR",
}).format(template.harga);
// 🆕 simpan kategori fitur
kategoriFiturs.value = template.kategori_fiturs || [];
} catch (err) {
console.error("Gagal ambil template", err);
}
}
});
// FUNGSI UNTUK MENAMBAH GAMBAR
const handleFileUpload = (event) => {
const newFiles = Array.from(event.target.files);
const combinedFiles = [...form.value.galeri, ...newFiles];
form.value.galeri = combinedFiles.slice(0, 5);
previewImages.value = [];
form.value.galeri.forEach((file) => {
const reader = new FileReader();
reader.onload = (e) => {
previewImages.value.push(e.target.result);
};
reader.readAsDataURL(file);
});
event.target.value = null;
};
// FUNGSI UNTUK MENGHAPUS GAMBAR
const removeImage = (index) => {
form.value.galeri.splice(index, 1);
previewImages.value.splice(index, 1);
};
const submitForm = async () => {
loading.value = true;
success.value = false;
error.value = false;
try {
const body = new FormData();
// field umum
for (const key in form.value) {
if (key === "galeri" || key === "selectedFiturs") continue;
if (form.value[key] !== null && form.value[key] !== undefined) {
body.append(key, form.value[key]);
}
}
// galeri
form.value.galeri.forEach((file) => body.append("galeri[]", file));
// 🆕 fiturs
for (const kategoriId in form.value.selectedFiturs) {
const fiturs = Array.isArray(form.value.selectedFiturs[kategoriId])
? form.value.selectedFiturs[kategoriId]
: [form.value.selectedFiturs[kategoriId]];
fiturs.forEach((fiturId) => body.append("fiturs[]", fiturId));
}
await $fetch("http://localhost:8000/api/form/pernikahan", {
method: "POST",
body,
});
success.value = true;
const adminNumber = "62895602603247";
const message = `
Halo Admin, ada pemesanan undangan pernikahan baru 🎉
Nama Pemesan: ${form.value.nama_pemesan}
No WA: ${form.value.no_hp}
Email: ${form.value.email}
Mempelai Pria: ${form.value.nama_lengkap_pria} (${form.value.nama_panggilan_pria})
Mempelai Wanita: ${form.value.nama_lengkap_wanita} (${form.value.nama_panggilan_wanita})
Akad: ${form.value.hari_tanggal_akad} | ${form.value.waktu_akad}
Alamat: ${form.value.alamat_akad}
Resepsi: ${form.value.hari_tanggal_resepsi} | ${form.value.waktu_resepsi}
Alamat: ${form.value.alamat_resepsi}
Template: ${form.value.nama_template} (${form.value.kategori})
Harga: ${form.value.harga}
Tanggal Pemesanan: ${form.value.tanggal_pemesanan}
`;
window.location.href = `https://wa.me/${adminNumber}?text=${encodeURIComponent(
message
)}`;
} catch (err) {
console.error(err);
error.value = true;
} finally {
loading.value = false;
}
};
</script>

View File

@ -1,231 +1,190 @@
<template> <template>
<div class="max-w-5xl mx-auto p-8 bg-gradient-to-b from-pink-50 to-blue-50 shadow-lg rounded-xl"> <div class="max-w-6xl mx-auto p-8 bg-gradient-to-b from-blue-50 to-indigo-50 shadow-lg rounded-xl">
<!-- Judul Form --> <!-- Judul -->
<div class="text-center mb-10"> <div class="text-center mb-10">
<h1 class="text-3xl md:text-4xl font-extrabold text-purple-700 drop-shadow-sm"> <h1 class="text-3xl md:text-4xl font-extrabold text-indigo-700 drop-shadow-sm">
🎂 Form Pemesanan Undangan Ulang Tahun 🎉 🎂 Form Pemesanan Undangan Ulang Tahun 🎉
</h1> </h1>
<p class="text-gray-600 mt-2"> <p class="text-gray-600 mt-2">Isi data berikut untuk membuat undangan ulang tahun anak Anda</p>
Isi data berikut dengan lengkap untuk melakukan pemesanan undangan ulang tahun.
</p>
</div> </div>
<form @submit.prevent="submitForm" class="space-y-10"> <form @submit.prevent="submitForm" class="space-y-10">
<!-- Tema Undangan --> <!-- Pilih Paket -->
<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 flex items-center gap-2"> <h2 class="text-lg font-bold text-gray-800 mb-4">Pilih Paket</h2>
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Tema Undangan <div class="flex flex-col md:flex-row gap-4">
</h2> <label v-for="paket in paketList" :key="paket.id"
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> class="flex-1 p-4 border rounded-xl cursor-pointer hover:shadow-lg transition-all"
<input :value="form.nama_template" type="text" placeholder="Nama Template" class="input-readonly" readonly /> :class="{'border-blue-600 bg-blue-50': form.selectedPaket === paket.id}">
<input :value="form.kategori" type="text" placeholder="Kategori" class="input-readonly" readonly /> <input type="radio" class="hidden" v-model="form.selectedPaket" :value="paket.id" />
<input :value="form.harga" type="text" placeholder="Harga" class="input-readonly" readonly /> <div class="font-semibold text-gray-800">{{ paket.nama }}</div>
<input :value="form.tanggal_pemesanan" type="text" placeholder="Tanggal Pemesanan" class="input-readonly" <div class="text-gray-600">{{ paket.deskripsi }}</div>
readonly /> <div class="font-bold mt-2 text-blue-700">{{ formatRupiah(paket.harga) }}</div>
</label>
</div> </div>
</section> </section>
<!-- Pemesan --> <!-- Info Template -->
<!-- Pemesan --> <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">Template</h2>
<h2 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Pemesan Undangan <input v-model="form.nama_template" type="text" placeholder="Nama Template" class="input-readonly" readonly />
</h2> <input v-model="form.kategori" type="text" placeholder="Kategori" class="input-readonly" readonly />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <input v-model="form.harga" type="text" placeholder="Harga" class="input-readonly" readonly />
<div class="relative"> <input v-model="form.tanggal_pemesanan" type="text" placeholder="Tanggal Pemesanan" class="input-readonly" readonly />
<input v-model="form.nama_pemesan" type="text" id="nama_pemesan" placeholder=" " required </div>
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> </section>
<label for="nama_pemesan"
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
Nama Pemesan
</label>
</div>
<div class="relative">
<input v-model="form.no_hp" type="text" id="no_hp" placeholder=" " required
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<label for="no_hp"
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
No. WhatsApp
</label>
</div>
<div class="relative md:col-span-2">
<input v-model="form.email" type="email" id="email" placeholder=" " required
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<label for="email"
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
Email
</label>
</div>
</div>
</section>
<!-- Data Anak --> <!-- Data Pemesan -->
<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 flex items-center gap-2"> <h2 class="text-lg font-bold text-gray-800 mb-4">Data Pemesan</h2>
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Data Anak <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
</h2> <div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <label class="block text-sm text-gray-600 mb-1">Nama Pemesan</label>
<div class="relative"> <input v-model="form.nama_pemesan" type="text" class="input" required />
<input v-model="form.nama_lengkap_anak" type="text" id="nama_lengkap_anak" placeholder=" " required </div>
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> <div>
<label for="nama_lengkap_anak" <label class="block text-sm text-gray-600 mb-1">No. WhatsApp</label>
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600"> <input v-model="form.no_hp" type="text" class="input" required />
Nama Lengkap Anak </div>
</label> <div class="md:col-span-2">
</div> <label class="block text-sm text-gray-600 mb-1">Email</label>
<div class="relative"> <input v-model="form.email" type="email" class="input" required />
<input v-model="form.nama_panggilan_anak" type="text" id="nama_panggilan_anak" placeholder=" " required </div>
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div>
<label for="nama_panggilan_anak" </section>
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
Nama Panggilan Anak
</label>
</div>
<div class="relative">
<input v-model="form.bapak_anak" type="text" id="bapak_anak" placeholder=" " required
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<label for="bapak_anak"
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
Nama Bapak
</label>
</div>
<div class="relative">
<input v-model="form.ibu_anak" type="text" id="ibu_anak" placeholder=" " required
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<label for="ibu_anak"
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
Nama Ibu
</label>
</div>
<div class="relative">
<input v-model="form.umur_dirayakan" type="text" id="umur_dirayakan" placeholder=" " required
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<label for="umur_dirayakan"
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
Ulang Tahun ke-
</label>
</div>
<div class="relative">
<input v-model="form.anak_ke" type="text" id="anak_ke" placeholder=" " required
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" />
<label for="anak_ke"
class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-blue-600">
Anak ke-
</label>
</div>
</div>
</section>
<!-- Jadwal Acara --> <!-- Data Anak -->
<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 flex items-center gap-2"> <h2 class="text-lg font-bold text-gray-800 mb-4">Data Anak</h2>
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Jadwal Acara <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
</h2> <div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <label class="block text-sm mb-1">Nama Lengkap Anak</label>
<div class="relative"> <input v-model="form.nama_lengkap_anak" type="text" class="input" required />
<input v-model="form.hari_tanggal_acara" type="date" id="hari_tanggal_acara" placeholder=" " required </div>
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> <div>
<label for="hari_tanggal_acara" <label class="block text-sm mb-1">Nama Panggilan Anak</label>
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Hari & Tanggal</label> <input v-model="form.nama_panggilan_anak" type="text" class="input" required />
</div> </div>
<div class="relative"> <div>
<input v-model="form.waktu_acara" type="text" id="waktu_acara" placeholder=" " required <label class="block text-sm mb-1">Nama Bapak</label>
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> <input v-model="form.bapak_anak" type="text" class="input" />
<label for="waktu_acara" </div>
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Waktu Acara</label> <div>
</div> <label class="block text-sm mb-1">Nama Ibu</label>
<div class="relative md:col-span-2"> <input v-model="form.ibu_anak" type="text" class="input" />
<textarea v-model="form.alamat_acara" id="alamat_acara" rows="3" placeholder=" " required </div>
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"></textarea> <div>
<label for="alamat_acara" <label class="block text-sm mb-1">Ulang Tahun ke-</label>
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Alamat Lengkap</label> <input v-model="form.umur_dirayakan" type="number" class="input" required />
</div> </div>
<div class="relative md:col-span-2"> <div>
<input v-model="form.maps_acara" type="text" id="maps_acara" placeholder=" " <label class="block text-sm mb-1">Anak ke-</label>
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> <input v-model="form.anak_ke" type="number" class="input" />
<label for="maps_acara" </div>
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Link Google Maps (Opsional)</label> </div>
</div> </section>
</div>
</section>
<!-- Informasi Tambahan --> <!-- Detail Acara -->
<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 flex items-center gap-2"> <h2 class="text-lg font-bold text-gray-800 mb-4">Detail Acara</h2>
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Informasi Tambahan <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
</h2> <div>
<div class="relative"> <label class="block text-sm mb-1">Hari & Tanggal</label>
<input v-model="form.link_musik" type="text" id="link_musik" placeholder=" " <input v-model="form.hari_tanggal_acara" type="date" class="input" required />
class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" /> </div>
<label for="link_musik" <div>
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Link Musik (Opsional)</label> <label class="block text-sm mb-1">Waktu Acara</label>
</div> <input v-model="form.waktu_acara" type="text" class="input" required />
</section> </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 --> <!-- Galeri Foto -->
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200"> <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 (max 5 gambar)</h2> <h2 class="text-lg font-bold text-gray-800 mb-4">
<input type="file" multiple accept="image/*" @change="handleFileUpload" class="hidden" id="gallery-upload" /> Galeri Foto
<div class="grid grid-cols-2 md:grid-cols-3 gap-3"> <span v-if="form.selectedPaket === 'basic'">(max 6 gambar)</span>
<div v-for="(img, i) in previewImages" :key="i" <span v-if="form.selectedPaket === 'premium'">(unlimited)</span>
class="relative w-full aspect-square rounded-lg overflow-hidden shadow-sm group"> </h2>
<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"
aria-label="Hapus gambar">
&times;
</button>
</div>
<label v-if="previewImages.length < 5" for="gallery-upload"
class="flex items-center justify-center w-full aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
</div>
</div>
<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">
&times;
</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> </form>
<!-- Submit -->
<div class="mt-10 text-center">
<button @click="submitForm"
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!</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.
Pastikan semua data yang wajib diisi sudah lengkap.</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref } from "vue"
import { useRoute } from "vue-router";
const route = useRoute(); 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 },
])
// 1. STRUKTUR DATA DISESUAIKAN DENGAN BACKEND
const form = ref({ const form = ref({
template_id: "", selectedPaket: "",
nama_template: "", nama_template: "",
kategori: "", kategori: "Ulang Tahun",
harga: "", harga: "",
tanggal_pemesanan: new Date().toLocaleDateString('id-ID', { year: 'numeric', month: 'long', day: 'numeric' }), tanggal_pemesanan: new Date().toLocaleDateString("id-ID"),
nama_pemesan: "", nama_pemesan: "",
no_hp: "", no_hp: "",
email: "", email: "",
nama_lengkap_anak: "", nama_lengkap_anak: "",
nama_panggilan_anak: "", nama_panggilan_anak: "",
bapak_anak: "", bapak_anak: "",
@ -236,116 +195,81 @@ const form = ref({
waktu_acara: "", waktu_acara: "",
alamat_acara: "", alamat_acara: "",
maps_acara: "", maps_acara: "",
link_musik: "",
galeri: [], galeri: [],
}); video: null
})
const previewImages = ref([]); const previewImages = ref([])
const loading = ref(false); const previewVideo = ref(null)
const success = ref(false);
const error = ref(false);
onMounted(async () => { const loading = ref(false)
if (route.query.template_id) { const success = ref(false)
try { const error = ref(false)
const template = await $fetch(`http://localhost:8000/api/templates/${route.query.template_id}`);
form.value.template_id = template.id; const formatRupiah = (num) => new Intl.NumberFormat("id-ID", { style: "currency", currency: "IDR" }).format(num)
form.value.nama_template = template.nama_template;
form.value.kategori = template.kategori?.nama || "-";
form.value.harga = new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR' }).format(template.harga);
} catch (err) {
console.error("Gagal ambil template", err);
}
}
});
// FUNGSI UNTUK MENAMBAH GAMBAR
const handleFileUpload = (event) => { const handleFileUpload = (event) => {
const newFiles = Array.from(event.target.files); const files = Array.from(event.target.files)
const combinedFiles = [...form.value.galeri, ...newFiles];
// Batasi total file menjadi 5 if (form.value.selectedPaket === "basic" && (form.value.galeri.length + files.length) > 6) {
form.value.galeri = combinedFiles.slice(0, 5); alert("Paket Basic hanya bisa upload maksimal 6 foto!")
return
}
// Buat ulang array preview berdasarkan data file yang sudah final form.value.galeri.push(...files)
previewImages.value = []; files.forEach(file => {
form.value.galeri.forEach(file => { const reader = new FileReader()
const reader = new FileReader(); reader.onload = e => previewImages.value.push(e.target.result)
reader.onload = (e) => { reader.readAsDataURL(file)
previewImages.value.push(e.target.result); })
}; }
reader.readAsDataURL(file);
});
// Reset input agar bisa memilih file yang sama lagi const handleVideoUpload = (event) => {
event.target.value = null; 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)
}
}
// FUNGSI UNTUK MENGHAPUS GAMBAR (SEKARANG DI LUAR)
const removeImage = (index) => { const removeImage = (index) => {
form.value.galeri.splice(index, 1); form.value.galeri.splice(index, 1)
previewImages.value.splice(index, 1); previewImages.value.splice(index, 1)
}; }
const submitForm = async () => { const submitForm = async () => {
loading.value = true;
success.value = false;
error.value = false;
try { try {
const body = new FormData(); loading.value = true
for (const key in form.value) { success.value = false
if (key === "galeri") { error.value = false
form.value.galeri.forEach((file) => body.append("galeri[]", file));
} else if (form.value[key] !== null && form.value[key] !== undefined) {
body.append(key, form.value[key]);
}
}
// 2. ENDPOINT API DIPERBAIKI console.log("Data dikirim:", form.value)
await $fetch("http://localhost:8000/api/form/ulang-tahun", { await new Promise(res => setTimeout(res, 1000))
method: "POST",
body,
});
success.value = true;
const adminNumber = "62895602603247";
// Susun pesan WA
const message = `
Halo Admin, ada pesanan undangan ulang tahun baru 🎉
Nama Pemesan: ${form.value.nama_pemesan}
No HP: ${form.value.no_hp}
Email: ${form.value.email}
Nama Anak: ${form.value.nama_lengkap_anak} (${form.value.nama_panggilan_anak})
Orang Tua: ${form.value.bapak_anak} & ${form.value.ibu_anak}
Umur Dirayakan: ${form.value.umur_dirayakan}
Anak ke: ${form.value.anak_ke}
Acara: ${form.value.hari_tanggal_acara} | ${form.value.waktu_acara}
Alamat: ${form.value.alamat_acara}
Google Maps: ${form.value.maps_acara || "-"}
Template: ${form.value.nama_template} (${form.value.kategori})
Harga: ${form.value.harga}
Tanggal Pemesanan: ${form.value.tanggal_pemesanan}
`;
// Redirect ke WhatsApp
const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(message)}`;
window.location.href = waUrl;
success.value = true
} catch (err) { } catch (err) {
console.error(err); error.value = true
error.value = true;
} finally { } finally {
loading.value = false; loading.value = false
} }
}; }
</script> </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>

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, 2, 3, 5, 6, 8] const selectedIds = [1, 2, 3, 4, 5, 6]
// state dropdown // state dropdown
const openDropdownId = ref(null) const openDropdownId = ref(null)
@ -10,12 +10,65 @@ const toggleDropdown = (templateId) => {
openDropdownId.value = openDropdownId.value === templateId ? null : templateId openDropdownId.value = openDropdownId.value === templateId ? null : templateId
} }
// fetch API dari Laravel // Paket & fitur hardcode (tidak tergantung kategori backend)
const { data: templatesData, error } = await useFetch('http://localhost:8000/api/templates') const paketData = [
{
paket: 'Starter',
fiturs: [
'1x Acara',
'Masa Aktif 3 Bulan',
'Nama Tamu Personal',
'Maks. 100 Tamu',
'Request Musik'
]
},
{
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',
fiturs: [
'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'
]
}
]
// filter hanya id tertentu // fetch API dari backend (hanya untuk nama template & harga)
const { data: templatesData } = await useFetch('http://localhost:8000/api/templates')
// mapping template: nama_template & harga dari backend, paket & fiturs hardcode
const templates = computed(() => const templates = computed(() =>
(templatesData.value || []).filter(t => selectedIds.includes(t.id)) (templatesData.value || [])
.filter(t => selectedIds.includes(t.id))
.map((t, index) => ({
id: t.id,
nama_template: t.nama_template,
harga: t.harga,
paket: paketData[index % paketData.length].paket,
fiturs: paketData[index % paketData.length].fiturs.map((f, i) => ({ id: i + 1, deskripsi: f })),
kategori: t.kategori // tetap disimpan agar bisa untuk routing Order
}))
) )
</script> </script>
@ -31,60 +84,56 @@ 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 <div v-for="t in templates" :key="t.id"
v-for="t in templates" class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
:key="t.id"
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300"
>
<!-- Image --> <!-- Image -->
<img <img :src="t.foto" :alt="t.nama_template" class="w-full h-48 object-cover" />
:src="`http://localhost:8000${t.foto}`"
: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">
<h4 class="text-xl font-bold text-gray-800 mb-2">{{ t.nama }}</h4> <h4 class="text-xl font-bold text-gray-800 mb-2">{{ t.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 {{ Number(t.harga).toLocaleString('id-ID') }} Rp {{ Number(t.harga).toLocaleString('id-ID') }}
</p> </p>
<p class="text-gray-500 mb-4 font-medium">Paket: {{ t.paket }}</p>
<!-- 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-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>
<div v-if="openDropdownId === t.id"> <transition name="fade">
<ul class="mt-4 space-y-2 text-gray-600 text-left"> <div v-if="openDropdownId === t.id" class="mt-4">
<li v-for="f in t.fiturs" :key="f.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"/> <li v-for="f in t.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">
{{ f.deskripsi }} <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</li> </svg>
</ul> {{ f.deskripsi }}
</div> </li>
</ul>
</div>
</transition>
</div> </div>
<!-- Buttons --> <!-- Buttons -->
<div class="flex items-center gap-3 mt-6"> <div class="flex items-center gap-3 mt-6">
<button <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" 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 <NuxtLink
:to="`/form/${t.kategori.nama.toLowerCase().replace(/ /g, '-')}` + `?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>
</div> </div>
@ -92,17 +141,29 @@ const templates = computed(() =>
</div> </div>
</div> </div>
<!-- Jika error --> <!-- Jika tidak ada template -->
<div v-else class="text-gray-500">Tidak ada template yang bisa ditampilkan</div> <div v-else class="text-gray-500">Tidak ada template yang bisa ditampilkan</div>
<!-- See more --> <!-- See more -->
<div class="mt-8 text-right max-w-[1100px] mx-auto"> <div class="mt-8 text-right max-w-[1100px] mx-auto">
<NuxtLink <NuxtLink to="/template" class="text-blue-600 font-medium hover:underline">
to="/template"
class="text-blue-600 font-medium hover:underline"
>
Lihat Selengkapnya... Lihat Selengkapnya...
</NuxtLink> </NuxtLink>
</div> </div>
</section> </section>
</template> </template>
<style>
/* animasi dropdown smooth */
.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>

View File

@ -0,0 +1,67 @@
<template>
<div class="flex items-center justify-center mt-10">
<span class="w-20 h-px bg-orange-300"></span>
<span class="mx-2 text-orange-400 text-2xl">🌸</span>
<span class="w-20 h-px bg-orange-300"></span>
</div>
<div class="text-center py-10 bg-[#FFF8F0]">
<!-- Judul -->
<h2 class="text-2xl font-bold text-orange-500 mb-2">We Are Getting Married</h2>
<p class="italic text-gray-700 mb-4">
Assalamualaikum Warrohmatullah Wabarrokatuhu
</p>
<p class="text-gray-500 max-w-xl mx-auto mb-6">
By asking for the grace and blessing of Allah SWT. We intend to hold a wedding
celebration, which Allah SWT willing will be held on
</p>
<!-- Tanggal -->
<h3 class="text-3xl font-semibold text-gray-700 mb-2">{{ formattedDate }}</h3>
<p class="text-gray-400 mb-8">{{ location }}</p>
<!-- Countdown -->
<div class="flex justify-center space-x-4">
<div v-for="(time, label) in countdown" :key="label"
class="bg-white rounded-xl shadow-md w-20 h-24 flex flex-col items-center justify-center">
<span class="text-2xl font-bold text-orange-500">{{ time }}</span>
<span class="text-sm text-orange-400 uppercase">{{ label }}</span>
</div>
</div>
<!-- Garis dekorasi bawah -->
<div class="flex items-center justify-center mt-10">
<span class="w-24 h-px bg-gray-300"></span>
<span class="mx-2 text-orange-400 text-2xl">🌸</span>
<span class="w-24 h-px bg-gray-300"></span>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// lokasi & tanggal target
const location = "Bride's house"
const targetDate = new Date("2025-12-31T23:59:59").getTime()
const countdown = ref({
H: 0, D: 0, M: 0, S: 0
})
const formattedDate = new Date("2025-12-31").toLocaleDateString("en-GB", {
day: "2-digit", month: "long", year: "numeric"
})
onMounted(() => {
setInterval(() => {
const now = new Date().getTime()
const distance = targetDate - now
countdown.value.H = Math.floor(distance / (1000 * 60 * 60 * 24))
countdown.value.D = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
countdown.value.M = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))
countdown.value.S = Math.floor((distance % (1000 * 60)) / 1000)
}, 1000)
})
</script>

View File

@ -0,0 +1,58 @@
<template>
<section class="bg-[#FFF8EC] py-12 text-center">
<!-- Title -->
<div class="mb-8">
<div class="flex items-center justify-center gap-2 mb-2">
<span class="h-[1px] w-12 bg-yellow-400"></span>
<span class="text-yellow-600 text-2xl">🌸</span>
<span class="h-[1px] w-12 bg-yellow-400"></span>
</div>
<h2 class="text-2xl md:text-3xl font-bold text-yellow-700">Gallery</h2>
</div>
<!-- Gallery Grid -->
<div class="grid grid-cols-2 md:grid-cols-3 gap-3 max-w-4xl mx-auto px-4">
<img v-for="(img, index) in images" :key="index" :src="img" alt="gallery"
class="rounded-lg object-cover w-full h-full shadow-md" :class="getGridClass(index)" />
</div>
<!-- Quote -->
<div class="max-w-3xl mx-auto mt-8 px-6">
<p class="text-gray-600 italic">
"And among His verses is that He has created for you wives of your own kind,
so that you may feel comfortable in them, and He has made between you mawadah and mercy.
Verily in that are signs for the people who think"
</p>
<p class="mt-4 text-gray-700 font-semibold">- AR-RUM 21 -</p>
</div>
<!-- Bottom Decoration -->
<div class="flex items-center justify-center gap-2 mt-6">
<span class="h-[1px] w-12 bg-yellow-400"></span>
<span class="text-yellow-600 text-2xl">🌸</span>
<span class="h-[1px] w-12 bg-yellow-400"></span>
</div>
</section>
</template>
<script setup>
const images = [
"/logo1.png", // kiri atas tinggi
"/logo2.png", // atas tengah
"/pria.jpg", // atas kanan
"/wanita.jpg", // bawah kiri
"/iphone.png", // bawah tengah lebar
"/templat.jpg" // bawah kanan
]
// kasih ukuran custom seperti masonry
const getGridClass = (index) => {
switch (index) {
case 0: return "row-span-2 h-[400px]" // tinggi besar
case 4: return "col-span-2 h-[250px]" // lebar besar
case 3: return "col-span-2 h-[250px]" // melebar
default: return "h-[200px]"
}
}
</script>

View File

@ -0,0 +1,70 @@
<template>
<section class="bg-[#FFF8EC] py-12 px-4">
<!-- Judul -->
<div class="flex items-center justify-center gap-2 mb-8">
<span class="h-[1px] w-12 bg-yellow-400"></span>
<span class="text-yellow-600 text-2xl">💌</span>
<span class="h-[1px] w-12 bg-yellow-400"></span>
</div>
<h2 class="text-2xl md:text-3xl font-bold text-yellow-600 text-center mb-10">
Guest Book
</h2>
<!-- Layout Form + Ucapan -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 max-w-4xl mx-auto">
<!-- Form -->
<div class="bg-white shadow-lg rounded-2xl p-6">
<h3 class="text-xl font-semibold text-yellow-600 mb-4">Say Something!</h3>
<form class="space-y-4">
<!-- Nama -->
<input
type="text"
placeholder="Your Name"
class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-yellow-400 focus:outline-none"
/>
<!-- Ucapan -->
<textarea
rows="3"
placeholder="Write your message..."
class="w-full p-3 border rounded-lg focus:ring-2 focus:ring-yellow-400 focus:outline-none"
></textarea>
<!-- Tombol -->
<button
type="submit"
class="w-full bg-yellow-500 hover:bg-yellow-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-200"
>
Send Now!
</button>
</form>
</div>
<!-- Daftar Ucapan -->
<div class="bg-white shadow-lg rounded-2xl p-6">
<div class="flex items-center gap-2 mb-4">
<span class="text-gray-500">💬</span>
<span class="text-gray-700 font-medium">04 Messages</span>
</div>
<!-- List Ucapan -->
<div class="space-y-3">
<div class="p-3 bg-gray-100 rounded-lg">
<p class="font-semibold text-gray-800">Tia SMAN6BDG</p>
<p class="text-sm text-gray-600">Congrats!!</p>
</div>
<div class="p-3 bg-gray-100 rounded-lg">
<p class="font-semibold text-gray-800">Muthia Rahma</p>
<p class="text-sm text-gray-600">Happy wedd my sisstaa!</p>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
// nanti tinggal ganti data dummy jadi fetch API
</script>

View File

@ -0,0 +1,9 @@
<template>
<div class="w-full h-64">
<iframe class="w-full h-full rounded-lg shadow"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d997.5010040241291!2d100.3676!3d-0.9471!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x2fd4b8c1e5b1a1d5%3A0xabcdef!2sPadang!5e0!3m2!1sen!2sid!4v1672500000000"
:allowfullscreen="true" loading="lazy" referrerpolicy="no-referrer-when-downgrade">
</iframe>
</div>
</template>

View File

@ -0,0 +1,13 @@
<template>
<div class="p-6 bg-white shadow rounded-lg flex flex-col items-center">
<h2 class="text-xl font-bold mb-4">Music Player</h2>
<audio controls autoplay loop class="w-full">
<source src="/music/wedding-song.mp3" type="audio/mpeg" />
Browser Anda tidak mendukung pemutar musik.
</audio>
</div>
</template>
<script setup>
// cukup minimalis, bisa ditambahkan playlist kalau mau
</script>

View File

@ -0,0 +1,130 @@
<template>
<section class="bg-[#FFF8EC] py-12 px-4">
<!-- Title -->
<div class="text-center mb-12" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-serif text-rose-800 mb-4">Ucapan</h2>
<p class="text-gray-600">Berikan ucapan & doa restu untuk kami</p>
</div>
<!-- Layout -->
<div class="max-w-4xl mx-auto grid md:grid-cols-2 gap-8">
<!-- Left Form -->
<div class="bg-white rounded-2xl shadow-lg p-6">
<h3 class="text-2xl font-bold text-yellow-500 mb-4">Say Something!</h3>
<form @submit.prevent="handleSubmit" class="space-y-4">
<!-- Name -->
<input
v-model="form.name"
type="text"
placeholder="Name"
class="w-full p-3 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-yellow-400"
/>
<!-- Message -->
<textarea
v-model="form.message"
placeholder="Message"
rows="3"
class="w-full p-3 rounded-xl border border-gray-200 focus:outline-none focus:ring-2 focus:ring-yellow-400"
></textarea>
<!-- Attendance -->
<div>
<label class="block text-gray-600 mb-2">Attendance</label>
<div class="flex gap-3 justify-between bg-gray-50 rounded-full p-2">
<button
type="button"
:class="attendanceClass('Yes')"
@click="form.attendance = 'Yes'"
>
Yes
</button>
<button
type="button"
:class="attendanceClass('Maybe')"
@click="form.attendance = 'Maybe'"
>
Maybe
</button>
<button
type="button"
:class="attendanceClass('No')"
@click="form.attendance = 'No'"
>
No
</button>
</div>
</div>
<!-- Submit -->
<button
type="submit"
class="w-full bg-yellow-400 text-white py-3 rounded-full font-semibold shadow hover:bg-yellow-500 transition"
>
Send Now!
</button>
</form>
</div>
<!-- Right Comments -->
<div class="bg-white rounded-2xl shadow-lg p-6">
<div class="flex items-center gap-2 mb-4 text-gray-600 font-medium">
<span class="text-xl">💬</span>
<span>{{ comments.length }}</span>
</div>
<div v-for="(c, i) in comments" :key="i" class="mb-4">
<div class="flex items-center justify-between mb-1">
<span class="font-semibold text-gray-800">{{ c.name }}</span>
<span
class="px-3 py-1 text-xs font-medium rounded-full"
:class="{
'bg-green-100 text-green-600': c.attendance === 'Yes',
'bg-yellow-100 text-yellow-600': c.attendance === 'Maybe',
'bg-gray-200 text-gray-600': c.attendance === 'No'
}"
>
{{ c.attendance }}
</span>
</div>
<p class="text-gray-600 text-sm">{{ c.message }}</p>
<hr class="mt-3" />
</div>
</div>
</div>
</section>
</template>
<script setup>
import { reactive, ref } from "vue";
const form = reactive({
name: "",
message: "",
attendance: "",
});
const comments = ref([
{ name: "Tia SMAN6BDG", message: "Congrats!", attendance: "Yes" },
{ name: "Muthia Rahma", message: "Happy wedd my sisstaa!", attendance: "Maybe" },
]);
const handleSubmit = () => {
if (form.name && form.message && form.attendance) {
comments.value.push({ ...form });
form.name = "";
form.message = "";
form.attendance = "";
}
};
const attendanceClass = (val) => {
return [
"flex-1 text-center py-2 rounded-full font-medium transition",
form.attendance === val
? "bg-yellow-400 text-white shadow"
: "bg-white text-gray-500 border border-gray-200 hover:bg-gray-100",
];
};
</script>

View File

@ -5,12 +5,11 @@
<div class="max-w-7xl mx-auto px-4 py-8"> <div class="max-w-7xl mx-auto px-4 py-8">
<!-- Back button --> <!-- Back button -->
<div class="mb-8"> <div class="mb-8">
<NuxtLink <NuxtLink to="/" class="text-blue-600 hover:text-blue-800 font-semibold inline-flex items-center">
to="/"
class="text-blue-600 hover:text-blue-800 font-semibold inline-flex items-center"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" /> <path fill-rule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clip-rule="evenodd" />
</svg> </svg>
Kembali ke Beranda Kembali ke Beranda
</NuxtLink> </NuxtLink>
@ -30,39 +29,27 @@
Gagal memuat kategori. Gagal memuat kategori.
</div> </div>
<!-- Kategori Grid --> <!-- Kategori Grid -->
<div <div v-else-if="categories.length > 0" class="mt-12 flex flex-wrap justify-center gap-6">
v-else-if="categories.length > 0" <div v-for="category in categories" :key="category.id + '-' + category.foto"
class="mt-12 flex flex-wrap justify-center gap-6" @click="onCategoryClick(category)"
> class="group cursor-pointer relative overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300 w-72">
<div <img :src="category.foto || '/fallback.png'" :alt="category.nama"
v-for="category in categories" class="w-full h-96 object-cover transition-transform duration-300 group-hover:scale-110"
:key="category.id + '-' + category.foto" @error="(e) => e.target.src = '/fallback.png'">
@click="onCategoryClick(category)" <div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent"></div>
class="group cursor-pointer relative overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300 w-72" <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>
<img <p class="text-lg font-normal leading-snug whitespace-normal break-words max-w-[90%]">
v-if="category.foto" {{ category.deskripsi }}
:src="`http://localhost:8000${category.foto}`" </p>
:alt="category.nama" </div>
class="w-full h-96 object-cover transition-transform duration-300 group-hover:scale-110" </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">
<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">
Belum ada kategori.
</div>
<div v-else class="mt-12 text-center text-gray-500">
Belum ada kategori.
</div>
<!-- Header Templates --> <!-- Header Templates -->
<div class="mt-20 text-center"> <div class="mt-20 text-center">
@ -74,74 +61,68 @@
</p> </p>
</div> </div>
<!-- Templates Grid --> <!-- Semua template (paket & fitur hardcode per kategori) -->
<div v-if="!isLoadingRandom" class="mt-12"> <div v-if="!isLoadingTemplates" class="mt-12">
<!-- Kalau kosong --> <div v-if="templatesWithFeatures.length === 0" class="text-center text-gray-500">
<div v-if="randomTemplates.length === 0" class="text-center text-gray-500 ">
Belum ada template tersedia. Belum ada template tersedia.
</div> </div>
<!-- Kalau ada --> <div v-else class="grid gap-8 max-w-[1100px] mx-auto grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<div <div v-for="t in templatesWithFeatures" :key="t.id"
v-else class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 items-start"
>
<div
v-for="t in randomTemplates"
:key="t.id"
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300 items-start"
>
<!-- Image --> <!-- Image -->
<img <img :src="t.foto || '/fallback.png'" :alt="t.nama" class="w-full h-48 object-cover"
:src="t.foto ? `http://localhost:8000${t.foto}` : '/fallback.png'" @error="(e) => e.target.src = '/fallback.png'" />
:alt="t.nama"
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">{{ t.nama }}</h4> <h4 class="text-xl font-bold text-gray-800 mb-2">{{ t.nama }}</h4>
<p class="text-green-600 font-semibold text-xl mb-4"> <p class="text-green-600 font-semibold text-xl mb-1">
Rp {{ Number(t.harga).toLocaleString('id-ID') }} Rp {{ Number(t.harga).toLocaleString('id-ID') }}
</p> </p>
<p class="text-gray-500 mb-4 font-medium">Paket: {{ t.paket }}</p>
<!-- 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-center" 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> <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" 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" /> <path fill-rule="evenodd"
</svg> 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"
</button> clip-rule="evenodd" />
</svg>
</button>
<div v-if="openDropdownId === t.id"> <transition name="fade">
<ul class="mt-4 space-y-2 text-gray-600 text-left"> <div v-if="openDropdownId === t.id" class="mt-4">
<li v-for="f in t.fiturs" :key="f.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"/> >
</svg> <li v-for="f in t.fiturs" :key="f.id" class="flex items-center">
{{ f.deskripsi }} <svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
</li> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</ul> </svg>
</div> {{ f.deskripsi }}
</div> </li>
</ul>
</div>
</transition>
</div>
<!-- Buttons --> <!-- Buttons -->
<div class="flex items-center gap-3 mt-6"> <div class="flex items-center gap-3 mt-6">
<button <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" 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)" @click="onTemplateClick(t)">
>
Preview Preview
</button> </button>
<NuxtLink <NuxtLink
:to="`/form/${t.kategori?.nama?.toLowerCase().replace(/ /g, '-')}` + `?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>
</div> </div>
@ -149,6 +130,7 @@
</div> </div>
</div> </div>
</div> </div>
<!-- END Templates -->
</div> </div>
</main> </main>
@ -158,7 +140,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, onActivated } from 'vue' import { ref, computed, onMounted, onActivated } from 'vue'
const emit = defineEmits(['category-selected', 'template-selected']) const emit = defineEmits(['category-selected', 'template-selected'])
@ -166,16 +148,61 @@ const categories = ref([])
const isLoading = ref(true) const isLoading = ref(true)
const error = ref(null) const error = ref(null)
const randomTemplates = ref([])
const isLoadingRandom = ref(true)
// dropdown fitur
// state dropdown fitur
const openDropdownId = ref(null) const openDropdownId = ref(null)
const toggleDropdown = (templateId) => { const toggleDropdown = (templateId) => {
openDropdownId.value = openDropdownId.value === templateId ? null : templateId openDropdownId.value = openDropdownId.value === templateId ? null : templateId
} }
// Fetch kategori
// Paket & fitur hardcode
const paketData = [
{
paket: 'Starter',
fiturs: [
'1x Acara',
'Masa Aktif 3 Bulan',
'Nama Tamu Personal',
'Maks. 100 Tamu',
'Request Musik'
]
},
{
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',
fiturs: [
'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 kategori
const fetchCategories = async () => { const fetchCategories = async () => {
isLoading.value = true isLoading.value = true
error.value = null error.value = null
@ -190,26 +217,44 @@ const fetchCategories = async () => {
} }
} }
// Fetch random template // fetch semua template
const fetchRandomTemplates = async () => { const templatesRaw = ref([])
isLoadingRandom.value = true const isLoadingTemplates = ref(true)
const fetchTemplates = async () => {
isLoadingTemplates.value = true
try { try {
const res = await $fetch('http://localhost:8000/api/templates/random') const res = await $fetch('http://localhost:8000/api/templates')
randomTemplates.value = res templatesRaw.value = res
} catch (err) { } catch (err) {
console.error('Gagal fetch random templates', err) console.error('Gagal fetch templates', err)
templatesRaw.value = []
} finally { } finally {
isLoadingRandom.value = false isLoadingTemplates.value = false
} }
} }
// mapping template dengan paket & fitur hardcode
const templatesWithFeatures = computed(() =>
(templatesRaw.value || []).map((t, index) => ({
id: t.id,
nama: t.nama_template,
harga: t.harga,
foto: t.foto,
kategori: t.kategori,
paket: paketData[index % paketData.length].paket,
fiturs: paketData[index % paketData.length].fiturs.map((f, i) => ({ id: i + 1, deskripsi: f }))
}))
)
onMounted(() => { onMounted(() => {
fetchCategories() fetchCategories()
fetchRandomTemplates() fetchTemplates()
}) })
onActivated(() => { onActivated(() => {
fetchCategories() fetchCategories()
fetchRandomTemplates() fetchTemplates()
}) })
const onCategoryClick = (category) => { const onCategoryClick = (category) => {

View File

@ -1,5 +1,6 @@
<template> <template>
<div> <div class="flex flex-col min-h-screen">
<!-- Header & Back Button -->
<div class="flex items-center mb-8"> <div class="flex items-center mb-8">
<button @click="$emit('back')" <button @click="$emit('back')"
class="text-blue-600 hover:text-blue-800 font-semibold inline-flex items-center mr-4"> class="text-blue-600 hover:text-blue-800 font-semibold inline-flex items-center mr-4">
@ -15,27 +16,38 @@
</h1> </h1>
</div> </div>
<!-- Loading & Error -->
<div v-if="isLoading" class="text-center py-10"> <div v-if="isLoading" class="text-center py-10">
<p>Memuat template...</p> <p>Memuat template...</p>
</div> </div>
<div v-else-if="error" class="text-center py-10 text-red-600"> <div v-else-if="error" class="text-center py-10 text-red-600">
<p>Gagal memuat template.</p> <p>{{ error }}</p>
</div> </div>
<div v-else-if="templates && templates.length > 0" <!-- Grid Template -->
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 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" <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">
<img :src="`http://localhost:8000${tpl.foto}`" :alt="tpl.nama" class="w-full h-48 object-cover">
<!-- 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'"
/>
<!-- 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 }}</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-4">
Rp {{ tpl.harga.toLocaleString('id-ID') }} Rp {{ (tpl.harga ?? 0).toLocaleString('id-ID') }}
</p> </p>
<div v-if="tpl.fitur && tpl.fitur.length > 0" class="relative mb-4"> <!-- Dropdown Fitur -->
<div v-if="tpl.fiturs?.length > 0" 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 text-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>
@ -49,40 +61,33 @@
<div v-if="openDropdownId === tpl.id"> <div v-if="openDropdownId === tpl.id">
<ul class="mt-4 space-y-2 text-gray-600 text-left"> <ul class="mt-4 space-y-2 text-gray-600 text-left">
<li v-for="item_fitur in tpl.fitur" :key="item_fitur.id" class="flex items-center"> <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"> <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> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg> </svg>
{{ item_fitur.deskripsi }} {{ item_fitur.deskripsi }}
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="mt-6"> <!-- Buttons -->
<div class="flex items-center gap-3"> <div class="mt-6 flex flex-col gap-3">
<!-- Tombol Preview (masih sama) --> <a :href="tpl.preview_link || '#'" target="_blank"
<a :href="tpl.id === 1 ? 'https://www.figma.com/proto/T3EQf6Ip0dZIBZMvaKiefE/Mockup-Ivitation?node-id=272-1270&t=bbfeDM0cefEB4xRt-0&scaling=scale-down&content-scaling=fixed&page-id=272%3A228&starting-point-node-id=285%3A273&show-proto-sidebar=1' : 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">
'https://www.figma.com/proto/T3EQf6Ip0dZIBZMvaKiefE/Mockup-Ivitation?node-id=285-273&t=bbfeDM0cefEB4xRt-0&scaling=scale-down&content-scaling=fixed&page-id=272%3A228&starting-point-node-id=285%3A273&show-proto-sidebar=1'" Preview
target="_blank" </a>
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"> <NuxtLink
Preview :to="`/form/${tpl.kategori?.id || tpl.id}?template_id=${tpl.id}`"
</a> 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
<!-- Tombol Order langsung ke form Khitan --> </NuxtLink>
<NuxtLink
:to="`/form/${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>
</div>
</div> </div>
</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">
<p>Belum ada template untuk kategori ini.</p> <p>Belum ada template untuk kategori ini.</p>
</div> </div>
@ -90,38 +95,54 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue'; import { ref, watch, onMounted } from 'vue'
// State untuk melacak ID dropdown yang sedang terbuka
const openDropdownId = ref(null);
// Fungsi untuk membuka/menutup dropdown
const toggleDropdown = (templateId) => {
if (openDropdownId.value === templateId) {
// Jika dropdown yang sama diklik lagi, tutup
openDropdownId.value = null;
} else {
// Jika dropdown lain diklik, buka yang baru
openDropdownId.value = templateId;
}
};
const props = defineProps({ const props = defineProps({
category: { type: String, required: true }, category: { type: String, required: true },
id_category: { type: Number, required: true }, id_category: { type: [Number, String], required: true },
}); })
defineEmits(['back']); defineEmits(['back'])
const { data: templates, pending: isLoading, error } = useFetch( const templates = ref([])
() => `/api/templates/category/${props.id_category}`, const isLoading = ref(true)
{ const error = ref(null)
baseURL: 'http://localhost:8000', const openDropdownId = ref(null)
key: () => `templates-${props.id_category}`,
transform: (response) => { const toggleDropdown = (templateId) => {
if (!response || !Array.isArray(response)) return []; openDropdownId.value = openDropdownId.value === templateId ? null : templateId
return response; }
}
const fetchTemplates = async (categoryId) => {
isLoading.value = true
error.value = null
try {
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
}))
} catch (err) {
console.error(err)
error.value = 'Gagal memuat template.'
templates.value = []
} finally {
isLoading.value = false
} }
); }
</script>
// 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>

View File

@ -0,0 +1,27 @@
<template>
<section class="w-full max-w-6xl mx-auto text-center">
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-8">
Acara Ulang Tahun
</h1>
<div class="bg-yellow-300/60 rounded-3xl p-8 shadow-xl">
<p class="text-orange-800 text-lg md:text-xl mb-4">
Insya Allah akan dilaksanakan pada:
</p>
<p class="text-orange-700 text-2xl md:text-3xl font-bold mb-6">
Selasa, 11 Juni 2025<br> Pukul 11.00 WIB
</p>
<p class="text-orange-800 text-lg md:text-xl mb-6">
Bertempat di:<br>
Jl. Andara Raya No.123, Jakarta Selatan
</p>
<div class="mt-6">
<iframe
class="w-full h-64 rounded-2xl shadow-lg"
src="https://www.google.com/maps/embed?pb=!1m18!1m12!1m3..."
allowfullscreen
loading="lazy">
</iframe>
</div>
</div>
</section>
</template>

View File

@ -0,0 +1,15 @@
<template>
<section class="w-full max-w-6xl mx-auto text-center">
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-8">
Galeri Foto
</h1>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
<img src="/logo1.png" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/logo2.png" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/pria.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/wanita.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/templat.jpg" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
<img src="/iphone.png" alt="gallery" class="rounded-xl shadow-lg object-cover h-48 w-full">
</div>
</section>
</template>

View File

@ -0,0 +1,70 @@
<template>
<section class="w-full max-w-4xl mx-auto text-center">
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-8">
Buku Tamu
</h1>
<!-- Form -->
<div class="bg-yellow-300/60 rounded-3xl p-6 shadow-xl mb-8">
<form @submit.prevent="submitMessage" class="space-y-4">
<input v-model="form.name" type="text" placeholder="Nama"
class="w-full px-4 py-2 rounded-xl border-2 border-orange-400 focus:ring-2 focus:ring-orange-500">
<textarea v-model="form.message" placeholder="Ucapan"
class="w-full px-4 py-2 rounded-xl border-2 border-orange-400 focus:ring-2 focus:ring-orange-500"></textarea>
<select v-model="form.attendance"
class="w-full px-4 py-2 rounded-xl border-2 border-orange-400 focus:ring-2 focus:ring-orange-500">
<option value="yes">Hadir</option>
<option value="no">Tidak Hadir</option>
<option value="maybe">Mungkin</option>
</select>
<button type="submit"
class="bg-orange-600 hover:bg-orange-700 text-white px-6 py-3 rounded-xl font-semibold shadow-lg">
Kirim
</button>
</form>
</div>
<!-- Messages -->
<div class="space-y-4">
<div v-for="msg in messages" :key="msg.id" class="bg-white rounded-xl shadow-md p-4 text-left">
<div class="flex items-center justify-between mb-2">
<span class="font-bold text-orange-800">{{ msg.name }}</span>
<span :class="getAttendanceClass(msg.attendance)" class="text-sm px-2 py-1 rounded-lg">
{{ msg.attendance }}
</span>
</div>
<p class="text-gray-700">{{ msg.message }}</p>
</div>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
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' }
])
const form = ref({ name: '', message: '', attendance: 'yes' })
const submitMessage = () => {
if (form.value.name && form.value.message) {
messages.value.push({
id: Date.now(),
...form.value
})
form.value = { name: '', message: '', attendance: 'yes' }
}
}
const getAttendanceClass = (attendance) => {
switch (attendance) {
case 'yes': return 'bg-green-100 text-green-700'
case 'no': return 'bg-red-100 text-red-700'
case 'maybe': return 'bg-yellow-100 text-yellow-700'
default: return 'bg-gray-100 text-gray-700'
}
}
</script>

View File

@ -0,0 +1,37 @@
<template>
<section class="w-full max-w-6xl mx-auto">
<div class="flex flex-col lg:flex-row items-center justify-between gap-8">
<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>
</div>
<div class="flex-1">
<div class="bg-yellow-300/60 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>
</section>
</template>
<script setup>
defineProps({
age: Number,
childName: String,
childOrder: Number,
parentNames: String,
childPhoto: String
})
</script>

View File

@ -0,0 +1,46 @@
<template>
<section class="w-full max-w-6xl mx-auto">
<div class="flex flex-col lg:flex-row items-center justify-between gap-8">
<!-- Left Side -->
<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="$emit('open-invitation')"
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 -->
<div class="flex-1 relative">
<div class="relative w-full max-w-md mx-auto">
<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>
</div>
</div>
</div>
</section>
</template>
<script setup>
defineProps({
childName: String,
guestName: String
})
</script>

View File

@ -0,0 +1,20 @@
<template>
<section class="w-full max-w-3xl mx-auto text-center">
<h1 class="text-orange-700 text-3xl md:text-4xl font-bold mb-6">
Terima Kasih
</h1>
<p class="text-orange-800 text-lg md:text-xl mb-6">
Kehadiran dan doa restu Bapak/Ibu/Saudara/i merupakan kebahagiaan besar bagi kami.
</p>
<div class="bg-yellow-300/60 rounded-3xl p-6 shadow-xl">
<p class="text-orange-700 font-bold text-xl">Raffi Ahmad & Nagita Slavina</p>
<p class="text-orange-800">Orang Tua {{ childName }}</p>
</div>
</section>
</template>
<script setup>
defineProps({
childName: String
})
</script>

View File

@ -0,0 +1,769 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-yellow-300 via-yellow-400 to-yellow-500 relative overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 opacity-10">
<div class="absolute top-10 left-10 w-20 h-20 bg-yellow-600 rounded-full"></div>
<div class="absolute top-32 right-20 w-16 h-16 bg-yellow-600 rounded-full"></div>
<div class="absolute bottom-20 left-20 w-12 h-12 bg-yellow-600 rounded-full"></div>
<div class="absolute bottom-40 right-40 w-24 h-24 bg-yellow-600 rounded-full"></div>
</div>
<!-- Navigation -->
<nav class="relative z-20 bg-transparent border-b border-yellow-600/20">
<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>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue'
// Props/Data yang bisa diisi dari parent atau API
const childName = ref('Rayyanza Malik Ahmad')
const age = ref(4)
const childOrder = ref(2)
const parentNames = ref('Raffi Ahmad & Nagita Slavina')
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>
<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>

View File

@ -0,0 +1,247 @@
<!-- components/forms/KhitanForm.vue -->
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Decorative Lanterns -->
<div class="absolute top-8 left-8 animate-sway">
<div class="w-16 h-20 relative">
<!-- Lantern Chain -->
<div class="absolute top-0 left-1/2 w-0.5 h-8 bg-yellow-400 transform -translate-x-1/2"></div>
<!-- Lantern Body -->
<div class="absolute bottom-0 w-full">
<svg viewBox="0 0 64 80" class="w-full h-full">
<defs>
<linearGradient id="lanternGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#FFD700"/>
<stop offset="50%" style="stop-color:#FFA500"/>
<stop offset="100%" style="stop-color:#FFD700"/>
</linearGradient>
</defs>
<ellipse cx="32" cy="15" rx="28" ry="12" fill="url(#lanternGradient)"/>
<rect x="8" y="12" width="48" height="40" rx="24" fill="url(#lanternGradient)" opacity="0.9"/>
<ellipse cx="32" cy="55" rx="28" ry="12" fill="url(#lanternGradient)"/>
<rect x="20" y="20" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
<rect x="20" y="30" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
<rect x="20" y="40" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
</svg>
</div>
</div>
</div>
<div class="absolute top-8 right-8 animate-sway animation-delay-1000">
<div class="w-16 h-20 relative">
<!-- Lantern Chain -->
<div class="absolute top-0 left-1/2 w-0.5 h-8 bg-yellow-400 transform -translate-x-1/2"></div>
<!-- Lantern Body -->
<div class="absolute bottom-0 w-full">
<svg viewBox="0 0 64 80" class="w-full h-full">
<!-- pakai gradient yang sama -->
<use href="#lanternGradient"></use>
<ellipse cx="32" cy="15" rx="28" ry="12" fill="url(#lanternGradient)"/>
<rect x="8" y="12" width="48" height="40" rx="24" fill="url(#lanternGradient)" opacity="0.9"/>
<ellipse cx="32" cy="55" rx="28" ry="12" fill="url(#lanternGradient)"/>
<rect x="20" y="20" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
<rect x="20" y="30" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
<rect x="20" y="40" width="24" height="4" rx="2" fill="#1e40af" opacity="0.6"/>
</svg>
</div>
</div>
</div>
<!-- Main Content -->
<div class="relative z-10 h-full flex items-center justify-center px-6">
<div class="text-center max-w-6xl mx-auto">
<!-- Bismillah -->
<div class="mb-8 animate-fade-in-down">
<h1 class="text-yellow-400 text-2xl md:text-3xl font-bold mb-6 arabic-text">
بِسْمِ اللَّهِ الرَّحْمَٰنِ الرَّحِيمِ
</h1>
</div>
<!-- Event Description -->
<div class="mb-8 animate-fade-in-up animation-delay-300">
<p class="text-white text-base md:text-lg leading-relaxed max-w-4xl mx-auto mb-6">
Dengan memohon rahmat dan ridho Allah Subhanahu wa Ta'ala, kami<br>
mengundang Bapak/Ibu/Saudara/i untuk hadir dan berbagi kebahagiaan<br>
dalam acara Khitanan putra kami tercinta, yang insya Allah akan<br>
diselenggarakan pada :
</p>
</div>
<!-- Event Details Container -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<!-- Date & Countdown -->
<div class="animate-fade-in-left animation-delay-600">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-yellow-400/30">
<h3 class="text-yellow-400 text-2xl font-bold mb-6 font-script">
Jum'at/Sabtu
</h3>
<div class="text-white text-3xl md:text-4xl font-bold mb-8">
{{ eventDate }}
</div>
<!-- Countdown Timer -->
<div class="grid grid-cols-4 gap-4 mb-6">
<div
v-for="(item, index) in countdownItems"
:key="index"
class="bg-blue-800/50 rounded-lg p-4 text-center border border-yellow-400/20"
>
<div class="text-yellow-400 text-2xl md:text-3xl font-bold">
{{ item.value }}
</div>
<div class="text-white text-sm uppercase tracking-wider">
{{ item.label }}
</div>
</div>
</div>
<!-- Add to Calendar Button -->
<button
@click="addToCalendar"
class="bg-yellow-400 hover:bg-yellow-500 text-blue-900 px-6 py-3 rounded-full font-semibold transition-all duration-300 transform hover:scale-105"
>
Add to Calendar
</button>
</div>
</div>
<!-- Location & Map -->
<div class="animate-fade-in-right animation-delay-600">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-yellow-400/30">
<div class="text-white mb-6">
<p class="text-lg mb-2">Pukul {{ eventTime }}</p>
<p class="text-lg mb-2">{{ eventLocation }}</p>
<p class="text-base opacity-80">{{ eventAddress }}</p>
</div>
<!-- Map Placeholder -->
<div class="bg-gray-300 rounded-lg h-32 mb-6 relative overflow-hidden">
<div class="absolute inset-0 bg-gradient-to-r from-blue-400 to-blue-600 opacity-50"></div>
<div class="absolute inset-0 flex items-center justify-center">
<Icon name="lucide:map-pin" class="w-8 h-8 text-white" />
</div>
<img
src="https://via.placeholder.com/400x200/4299e1/ffffff?text=Map+Preview"
alt="Location Map"
class="w-full h-full object-cover opacity-80"
/>
</div>
<!-- Direction Button -->
<button
@click="openMap"
class="bg-yellow-400 hover:bg-yellow-500 text-blue-900 px-6 py-3 rounded-full font-semibold transition-all duration-300 transform hover:scale-105"
>
Direction
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Decorative Elements -->
<div class="absolute inset-0 pointer-events-none overflow-hidden">
<div class="absolute top-0 left-1/2 transform -translate-x-1/2">
<svg viewBox="0 0 200 100" class="w-48 h-24 text-yellow-400 opacity-20">
<path d="M100 20 Q120 0 140 20 Q160 40 140 60 Q120 40 100 60 Q80 40 60 60 Q40 40 60 20 Q80 0 100 20" fill="currentColor"/>
</svg>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
// Props
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
// computed agar template tidak merah
const eventDate = computed(() => props.data.eventDate || '20-21 Juni 2025')
const eventTime = computed(() => props.data.eventTime || '09.00 WIB s.d Selesai')
const eventLocation = computed(() => props.data.eventLocation || 'TMC Mangrove, Tanjung Pasir')
const eventAddress = computed(() => props.data.eventAddress || 'Desa Tanjung Pasir')
const childPhoto = computed(() => props.data.childPhoto || '/images/khitan/child-photo.jpg')
const childName = computed(() => props.data.childName || 'Satria Huda Dinata')
const childTitle = computed(() => props.data.childTitle || 'SATRIA HUDA DINATA')
const childSubtitle = computed(() => props.data.childSubtitle || 'Putra Ke Dua Dari')
const fatherName = computed(() => props.data.fatherName || 'Bpk H. Munawar Huda, S.H.')
const motherName = computed(() => props.data.motherName || 'Ibu Hj. Dinah, A.M.Keb')
// countdown
const countdown = ref({ days: 7, hours: 11, minutes: 21, seconds: 45 })
let countdownInterval = null
const countdownItems = computed(() => [
{ value: String(countdown.value.days).padStart(2, '0'), label: 'D' },
{ value: String(countdown.value.hours).padStart(2, '0'), label: 'H' },
{ value: String(countdown.value.minutes).padStart(2, '0'), label: 'M' },
{ value: String(countdown.value.seconds).padStart(2, '0'), label: 'S' }
])
const updateCountdown = () => {
const eventDateObj = new Date('2025-06-20T09:00:00')
const now = new Date()
const diff = eventDateObj.getTime() - now.getTime()
if (diff > 0) {
countdown.value = {
days: Math.floor(diff / (1000 * 60 * 60 * 24)),
hours: Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)),
minutes: Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)),
seconds: Math.floor((diff % (1000 * 60)) / 1000)
}
} else {
countdown.value = { days: 0, hours: 0, minutes: 0, seconds: 0 }
if (countdownInterval) clearInterval(countdownInterval)
}
}
const addToCalendar = () => {
const eventDetails = {
title: `Tasyakuran Khitan ${childName.value}`,
start: '20250620T090000Z',
end: '20250621T170000Z',
description: 'Undangan Tasyakuran Khitan',
location: eventLocation.value
}
const url = `https://calendar.google.com/calendar/render?action=TEMPLATE&text=${encodeURIComponent(eventDetails.title)}&dates=${eventDetails.start}/${eventDetails.end}&details=${encodeURIComponent(eventDetails.description)}&location=${encodeURIComponent(eventDetails.location)}`
window.open(url, '_blank')
}
const openMap = () => {
const location = encodeURIComponent(eventLocation.value)
window.open(`https://maps.google.com/maps?q=${location}`, '_blank')
}
const handleImageError = (e) => {
e.target.src = 'https://via.placeholder.com/128x128/4299e1/ffffff?text=Photo'
}
onMounted(() => {
updateCountdown()
countdownInterval = setInterval(updateCountdown, 1000)
})
onUnmounted(() => {
if (countdownInterval) clearInterval(countdownInterval)
})
</script>
<style scoped>
/* ... tetap sama dengan punyamu, kecuali hapus selector .grid-cols-1.lg\:grid-cols-2 */
</style>

View File

@ -0,0 +1,351 @@
<!-- components/shared/Gallery.vue -->
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Main Content -->
<div class="relative z-10 h-full flex flex-col items-center justify-center px-6 py-12">
<!-- Gallery Title -->
<div class="text-center mb-8 animate-fade-in-down">
<h1 class="text-yellow-400 text-4xl md:text-5xl font-bold mb-6 font-script">
Galeri
</h1>
<p class="text-white text-base md:text-lg max-w-2xl mx-auto leading-relaxed">
Aku hadir ke dunia ini atas izin Allah, dan kini tiba waktuku untuk
menjalani salah satu sunnah-Nya. Mohon doa dan restunya di momen
berharga dalam hidupku ini.
</p>
</div>
<!-- Photo Gallery Grid -->
<div class="flex-1 max-w-4xl mx-auto w-full animate-fade-in-up animation-delay-300">
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 h-full max-h-96">
<!-- Large Photo (spans 2 rows on medium+ screens) -->
<div class="md:col-span-1 md:row-span-2 relative group cursor-pointer" @click="openModal(0)">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl overflow-hidden h-full border border-yellow-400/30 hover:border-yellow-400/60 transition-all duration-300">
<img
:src="galleryImages[0]?.src || placeholderImage"
:alt="galleryImages[0]?.alt || 'Gallery Image 1'"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
@error="handleImageError"
/>
<!-- Overlay -->
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300 flex items-center justify-center">
<Icon name="lucide:zoom-in" class="w-8 h-8 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div>
</div>
</div>
<!-- Medium Photos -->
<div
v-for="(image, index) in galleryImages.slice(1, 5)"
:key="index + 1"
class="relative group cursor-pointer"
@click="openModal(index + 1)"
>
<div class="bg-white/10 backdrop-blur-sm rounded-xl overflow-hidden h-full border border-yellow-400/30 hover:border-yellow-400/60 transition-all duration-300">
<img
:src="image.src || placeholderImage"
:alt="image.alt || `Gallery Image ${index + 2}`"
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
@error="handleImageError"
/>
<!-- Overlay -->
<div class="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300 flex items-center justify-center">
<Icon name="lucide:zoom-in" class="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for Image Preview -->
<Teleport to="body">
<div
v-if="selectedImage !== null"
class="fixed inset-0 z-50 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4"
@click="closeModal"
>
<div class="relative max-w-4xl max-h-full" @click.stop>
<!-- Close Button -->
<button
@click="closeModal"
class="absolute -top-12 right-0 text-white hover:text-yellow-400 transition-colors duration-300"
>
<Icon name="lucide:x" class="w-8 h-8" />
</button>
<!-- Image -->
<img
:src="galleryImages[selectedImage]?.src || placeholderImage"
:alt="galleryImages[selectedImage]?.alt || 'Gallery Image'"
class="max-w-full max-h-full object-contain rounded-lg shadow-2xl"
/>
<!-- Navigation Arrows -->
<button
v-if="selectedImage > 0"
@click.stop="navigateImage(-1)"
class="absolute left-4 top-1/2 transform -translate-y-1/2 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-300"
>
<Icon name="lucide:chevron-left" class="w-6 h-6" />
</button>
<button
v-if="selectedImage < galleryImages.length - 1"
@click.stop="navigateImage(1)"
class="absolute right-4 top-1/2 transform -translate-y-1/2 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-300"
>
<Icon name="lucide:chevron-right" class="w-6 h-6" />
</button>
<!-- Image Counter -->
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-black/50 backdrop-blur-sm text-white px-3 py-1 rounded-full text-sm">
{{ selectedImage + 1 }} / {{ galleryImages.length }}
</div>
</div>
</div>
</Teleport>
<!-- Floating Decorative Elements -->
<div class="absolute inset-0 pointer-events-none overflow-hidden">
<!-- Top ornament -->
<div class="absolute top-8 left-1/2 transform -translate-x-1/2 opacity-20">
<svg viewBox="0 0 100 50" class="w-24 h-12 text-yellow-400">
<path d="M50 10 Q60 0 70 10 Q80 20 70 30 Q60 20 50 30 Q40 20 30 30 Q20 20 30 10 Q40 0 50 10" fill="currentColor"/>
</svg>
</div>
<!-- Bottom ornament -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 opacity-20">
<svg viewBox="0 0 100 50" class="w-24 h-12 text-yellow-400">
<path d="M50 40 Q40 50 30 40 Q20 30 30 20 Q40 30 50 20 Q60 30 70 20 Q80 30 70 40 Q60 50 50 40" fill="currentColor"/>
</svg>
</div>
<!-- Side decorations -->
<div class="absolute top-1/2 left-4 transform -translate-y-1/2 opacity-10">
<div class="w-1 h-20 bg-yellow-400 rounded-full"></div>
</div>
<div class="absolute top-1/2 right-4 transform -translate-y-1/2 opacity-10">
<div class="w-1 h-20 bg-yellow-400 rounded-full"></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// Props
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
// Reactive data
const selectedImage = ref(null)
const placeholderImage = 'https://via.placeholder.com/400x400/4299e1/ffffff?text=Photo'
// Gallery images - replace with actual images
const galleryImages = ref([
{
src: '/images/khitan/child-portrait-1.jpg',
alt: 'Satria Huda Dinata Portrait 1'
},
{
src: '/images/khitan/child-portrait-2.jpg',
alt: 'Satria Huda Dinata Portrait 2'
},
{
src: '/images/khitan/child-portrait-3.jpg',
alt: 'Satria Huda Dinata Portrait 3'
},
{
src: '/images/khitan/child-portrait-4.jpg',
alt: 'Satria Huda Dinata Portrait 4'
},
{
src: '/images/khitan/child-portrait-5.jpg',
alt: 'Satria Huda Dinata Portrait 5'
}
])
// Methods
const openModal = (index) => {
selectedImage.value = index
document.body.style.overflow = 'hidden'
}
const closeModal = () => {
selectedImage.value = null
document.body.style.overflow = ''
}
const navigateImage = (direction) => {
const newIndex = selectedImage.value + direction
if (newIndex >= 0 && newIndex < galleryImages.value.length) {
selectedImage.value = newIndex
}
}
const handleImageError = (event) => {
event.target.src = placeholderImage
}
const handleKeyPress = (event) => {
if (selectedImage.value !== null) {
switch (event.key) {
case 'Escape':
closeModal()
break
case 'ArrowLeft':
navigateImage(-1)
break
case 'ArrowRight':
navigateImage(1)
break
}
}
}
// Lifecycle
onMounted(() => {
// Initialize gallery images from props if available
if (props.data?.gallery && props.data.gallery.length > 0) {
galleryImages.value = props.data.gallery.map((src, index) => ({
src,
alt: `Gallery Image ${index + 1}`
}))
}
// Add keyboard listener
window.addEventListener('keydown', handleKeyPress)
})
onUnmounted(() => {
window.removeEventListener('keydown', handleKeyPress)
document.body.style.overflow = ''
})
</script>
<style scoped>
/* Background Pattern */
.bg-pattern {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="islamic" x="0" y="0" width="25" height="25" patternUnits="userSpaceOnUse"><g fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"><path d="M12.5 0 L25 12.5 L12.5 25 L0 12.5 Z M6.25 6.25 L18.75 6.25 L18.75 18.75 L6.25 18.75 Z"/><circle cx="12.5" cy="12.5" r="3"/></g></pattern></defs><rect width="100%" height="100%" fill="url(%23islamic)"/></svg>');
}
/* Animations */
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-down {
animation: fadeInDown 0.8s ease-out forwards;
}
.animate-fade-in-up {
animation: fadeInUp 0.8s ease-out forwards;
}
/* Animation Delays */
.animation-delay-300 {
animation-delay: 0.3s;
opacity: 0;
animation-fill-mode: forwards;
}
/* Custom fonts */
.font-script {
font-family: 'Times New Roman', serif;
font-style: italic;
}
/* Gallery Grid Adjustments */
.grid {
height: 100%;
}
.grid > div {
min-height: 120px;
}
@media (min-width: 768px) {
.grid > div:first-child {
min-height: 250px;
}
.grid > div:not(:first-child) {
min-height: 120px;
}
}
/* Modal Styles */
.fixed.inset-0 {
backdrop-filter: blur(8px);
}
/* Responsive */
@media (max-width: 640px) {
.text-4xl.md\:text-5xl {
font-size: 2rem;
}
.px-6 {
padding-left: 1rem;
padding-right: 1rem;
}
.gap-4 {
gap: 0.5rem;
}
.max-h-96 {
max-height: 20rem;
}
.grid > div {
min-height: 100px;
}
}
@media (max-width: 480px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
.md\:col-span-1 {
grid-column: span 1;
}
.md\:row-span-2 {
grid-row: span 1;
}
}
</style>

View File

@ -0,0 +1,450 @@
<!-- components/shared/GuestBook.vue -->
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Main Content -->
<div class="relative z-10 h-full flex items-center justify-center px-6 py-12">
<div class="w-full max-w-6xl mx-auto">
<!-- Title -->
<div class="text-center mb-8 animate-fade-in-down">
<h1 class="text-yellow-400 text-4xl md:text-5xl font-bold mb-4 font-script">
Do'a & Ucapan
</h1>
</div>
<!-- Main Content Grid -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 h-full max-h-[600px]">
<!-- Left Side - Input Form -->
<div class="animate-fade-in-left animation-delay-300">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-yellow-400/30 h-full">
<h2 class="text-yellow-400 text-2xl font-bold mb-6">Say Something!</h2>
<form @submit.prevent="submitMessage" class="space-y-6">
<!-- Name Input -->
<div>
<label class="block text-white text-sm font-medium mb-2">Name</label>
<input
v-model="form.name"
type="text"
required
class="w-full px-4 py-3 bg-white/90 text-blue-900 rounded-lg border border-transparent focus:border-yellow-400 focus:ring-2 focus:ring-yellow-400/20 transition-all duration-300 placeholder-blue-400"
placeholder="Enter your name"
/>
</div>
<!-- Message Input -->
<div>
<label class="block text-white text-sm font-medium mb-2">Message</label>
<textarea
v-model="form.message"
required
rows="4"
class="w-full px-4 py-3 bg-white/90 text-blue-900 rounded-lg border border-transparent focus:border-yellow-400 focus:ring-2 focus:ring-yellow-400/20 transition-all duration-300 resize-none placeholder-blue-400"
placeholder="Write your message or prayer..."
></textarea>
</div>
<!-- Attendance Selection -->
<div>
<label class="block text-white text-sm font-medium mb-3">Attendance</label>
<div class="flex space-x-2">
<button
v-for="option in attendanceOptions"
:key="option.value"
type="button"
@click="form.attendance = option.value"
:class="[
'px-6 py-2 rounded-full font-medium transition-all duration-300',
form.attendance === option.value
? 'bg-yellow-400 text-blue-900'
: 'bg-white/20 text-white hover:bg-white/30'
]"
>
{{ option.label }}
</button>
</div>
</div>
<!-- Submit Button -->
<button
type="submit"
:disabled="isSubmitting"
class="w-full bg-yellow-400 hover:bg-yellow-500 disabled:bg-yellow-400/50 text-blue-900 font-bold py-3 px-6 rounded-full transition-all duration-300 transform hover:scale-105 disabled:transform-none disabled:cursor-not-allowed"
>
<span v-if="!isSubmitting">Send Now !</span>
<span v-else class="flex items-center justify-center">
<svg class="animate-spin -ml-1 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
Sending...
</span>
</button>
</form>
<!-- Success Message -->
<div v-if="showSuccess" class="mt-4 p-3 bg-green-500/20 border border-green-400 rounded-lg">
<p class="text-green-400 text-sm">Thank you for your message!</p>
</div>
</div>
</div>
<!-- Right Side - Messages List -->
<div class="animate-fade-in-right animation-delay-300">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-8 border border-yellow-400/30 h-full overflow-hidden">
<div class="h-full flex flex-col">
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-yellow-400/50 scrollbar-track-transparent">
<div class="space-y-4">
<!-- Sample Messages -->
<div
v-for="message in messages"
:key="message.id"
class="bg-white/5 rounded-lg p-4 border border-white/10 hover:border-yellow-400/30 transition-all duration-300"
>
<div class="flex items-start justify-between mb-2">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-yellow-400 rounded-full flex items-center justify-center">
<span class="text-blue-900 text-sm font-bold">
{{ message.name.charAt(0).toUpperCase() }}
</span>
</div>
<div>
<h4 class="text-yellow-400 font-semibold">{{ message.name }}</h4>
<p class="text-white/60 text-xs">{{ message.time }}</p>
</div>
</div>
<span
:class="[
'px-2 py-1 rounded-full text-xs font-medium',
message.attendance === 'yes'
? 'bg-green-500/20 text-green-400 border border-green-400/50'
: message.attendance === 'maybe'
? 'bg-yellow-500/20 text-yellow-400 border border-yellow-400/50'
: 'bg-red-500/20 text-red-400 border border-red-400/50'
]"
>
{{ message.attendance === 'yes' ? 'Attend' : message.attendance === 'maybe' ? 'Maybe' : 'Can\'t' }}
</span>
</div>
<p class="text-white text-sm leading-relaxed">{{ message.message }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Decorative Elements -->
<div class="absolute inset-0 pointer-events-none overflow-hidden">
<!-- Top left decoration -->
<div class="absolute top-8 left-8 opacity-20">
<svg viewBox="0 0 50 50" class="w-12 h-12 text-yellow-400">
<circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" stroke-width="2"/>
<path d="M15 25 L25 15 L35 25 L25 35 Z" fill="currentColor"/>
</svg>
</div>
<!-- Top right decoration -->
<div class="absolute top-8 right-8 opacity-20">
<svg viewBox="0 0 50 50" class="w-12 h-12 text-yellow-400">
<rect x="10" y="10" width="30" height="30" fill="none" stroke="currentColor" stroke-width="2" rx="5"/>
<circle cx="25" cy="25" r="8" fill="currentColor"/>
</svg>
</div>
<!-- Bottom decorations -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 opacity-10">
<div class="flex space-x-2">
<div class="w-2 h-2 bg-yellow-400 rounded-full"></div>
<div class="w-2 h-2 bg-yellow-400 rounded-full"></div>
<div class="w-2 h-2 bg-yellow-400 rounded-full"></div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// Props
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
// Reactive data
const form = ref({
name: '',
message: '',
attendance: 'yes'
})
const isSubmitting = ref(false)
const showSuccess = ref(false)
const attendanceOptions = [
{ value: 'yes', label: 'Yes' },
{ value: 'maybe', label: 'Maybe' },
{ value: 'no', label: 'No' }
]
// Sample messages - replace with actual data from backend
const messages = ref([
{
id: 1,
name: 'HWD FRENDSS ^^',
message: 'Selamat ya! Semoga menjadi anak yang sholeh dan berbakti kepada orang tua.',
attendance: 'yes',
time: '2 hours ago'
},
{
id: 2,
name: 'Tio SMAN6BDG',
message: 'Congratsss!',
attendance: 'yes',
time: '3 hours ago'
},
{
id: 3,
name: 'Muthia Rahma',
message: 'Happy wedd my sisstaa!',
attendance: 'maybe',
time: '5 hours ago'
},
{
id: 4,
name: 'Alia Sena P',
message: 'Barakallahu fiikum. Semoga menjadi keluarga yang sakinah mawadah wa rahmah.',
attendance: 'yes',
time: '6 hours ago'
},
{
id: 5,
name: 'Ahmad Rizki',
message: 'Selamat menjalani sunnah Rasul! Semoga Allah senantiasa melindungi.',
attendance: 'yes',
time: '1 day ago'
},
{
id: 6,
name: 'Siti Aminah',
message: 'Alhamdulillah, semoga menjadi awal yang baik untuk masa depan yang cerah.',
attendance: 'maybe',
time: '1 day ago'
}
])
// Methods
const submitMessage = async () => {
if (!form.value.name.trim() || !form.value.message.trim()) {
return
}
isSubmitting.value = true
try {
// Simulate API call - replace with actual backend integration
await new Promise(resolve => setTimeout(resolve, 1500))
// Add new message to the list (in real implementation, this would come from backend)
const newMessage = {
id: Date.now(),
name: form.value.name,
message: form.value.message,
attendance: form.value.attendance,
time: 'Just now'
}
messages.value.unshift(newMessage)
// Reset form
form.value = {
name: '',
message: '',
attendance: 'yes'
}
// Show success message
showSuccess.value = true
setTimeout(() => {
showSuccess.value = false
}, 3000)
} catch (error) {
console.error('Error submitting message:', error)
} finally {
isSubmitting.value = false
}
}
// Lifecycle
onMounted(() => {
// Load messages from props if available
if (props.data?.messages && props.data.messages.length > 0) {
messages.value = props.data.messages
}
})
</script>
<style scoped>
/* Background Pattern */
.bg-pattern {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="islamic" x="0" y="0" width="25" height="25" patternUnits="userSpaceOnUse"><g fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"><path d="M12.5 0 L25 12.5 L12.5 25 L0 12.5 Z M6.25 6.25 L18.75 6.25 L18.75 18.75 L6.25 18.75 Z"/><circle cx="12.5" cy="12.5" r="3"/></g></pattern></defs><rect width="100%" height="100%" fill="url(%23islamic)"/></svg>');
}
/* Animations */
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInLeft {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes fadeInRight {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-fade-in-down {
animation: fadeInDown 0.8s ease-out forwards;
}
.animate-fade-in-left {
animation: fadeInLeft 0.8s ease-out forwards;
}
.animate-fade-in-right {
animation: fadeInRight 0.8s ease-out forwards;
}
/* Animation Delays */
.animation-delay-300 {
animation-delay: 0.3s;
opacity: 0;
animation-fill-mode: forwards;
}
/* Custom fonts */
.font-script {
font-family: 'Times New Roman', serif;
font-style: italic;
}
/* Custom Scrollbar */
.scrollbar-thin {
scrollbar-width: thin;
}
.scrollbar-thin::-webkit-scrollbar {
width: 4px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background-color: rgba(251, 191, 36, 0.5);
border-radius: 2px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background-color: rgba(251, 191, 36, 0.7);
}
/* Input Focus States */
input:focus,
textarea:focus {
outline: none;
}
/* Responsive */
@media (max-width: 1024px) {
.grid-cols-1.lg\:grid-cols-2 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
.max-h-\[600px\] {
max-height: none;
}
}
@media (max-width: 640px) {
.text-4xl.md\:text-5xl {
font-size: 2rem;
}
.px-8 {
padding-left: 1rem;
padding-right: 1rem;
}
.py-8 {
padding-top: 1rem;
padding-bottom: 1rem;
}
.space-x-2 > * + * {
margin-left: 0.25rem;
}
.px-6 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.flex.space-x-2 {
flex-wrap: wrap;
gap: 0.5rem;
}
.flex.space-x-2 > * {
margin-left: 0;
}
}
@media (max-width: 480px) {
.grid {
gap: 1rem;
}
.px-4 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Top Navigation Tabs -->
<div class="absolute top-6 left-1/2 transform -translate-x-1/2 bg-white/20 backdrop-blur-sm rounded-full px-6 py-2 flex space-x-8 z-10">
<span class="text-yellow-400 font-semibold cursor-pointer">Introduction</span>
<span class="text-blue-100 cursor-pointer">Event</span>
<span class="text-blue-100 cursor-pointer">Gallery</span>
<span class="text-blue-100 cursor-pointer">Say</span>
<span class="text-blue-100 cursor-pointer">Thank You</span>
</div>
<!-- Main Content -->
<div class="relative z-10 h-full flex flex-col items-center justify-center px-6 text-center">
<!-- Child Photo -->
<div class="w-48 h-60 overflow-hidden rounded-t-3xl mx-auto mb-6">
<img :src="data.childPhoto || '/pria.jpg'" alt="Child" class="w-full h-full object-cover"/>
</div>
<!-- Child Name -->
<div class="bg-blue-800/50 border border-yellow-400 rounded-md px-6 py-2 mb-4 inline-block">
<h1 class="text-yellow-400 font-bold text-xl">{{ data.childName || 'SATRIA HUDA DINATA' }}</h1>
</div>
<!-- Child Order & Parents -->
<div class="text-white/80 text-lg mb-2">
Putra Ke Dua Dari
</div>
<div class="text-yellow-400 font-semibold text-xl">
{{ data.fatherName || 'Bpk H. Munawar Huda, S.H.' }} & {{ data.motherName || 'Ibu Hj. Dinah, A.M.Keb' }}
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
</script>
<style scoped>
/* Background Islamic Pattern */
.bg-pattern {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="islamic" x="0" y="0" width="25" height="25" patternUnits="userSpaceOnUse"><g fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"><path d="M12.5 0 L25 12.5 L12.5 25 L0 12.5 Z M6.25 6.25 L18.75 6.25 L18.75 18.75 L6.25 18.75 Z"/><circle cx="12.5" cy="12.5" r="3"/></g></pattern></defs><rect width="100%" height="100%" fill="url(%23islamic)"/></svg>');
}
</style>

View File

@ -0,0 +1,248 @@
<!-- components/templates/khitan/KhitanA.vue -->
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<!-- Background Islamic Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Decorative Elements -->
<div class="absolute top-0 left-0 w-full h-full">
<!-- Top decorative curves -->
<div class="absolute top-0 left-0 w-full">
<svg viewBox="0 0 1200 300" class="w-full h-auto">
<defs>
<linearGradient id="goldGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#FFD700"/>
<stop offset="50%" style="stop-color:#FFA500"/>
<stop offset="100%" style="stop-color:#FFD700"/>
</linearGradient>
</defs>
<path d="M0,150 C300,50 600,250 1200,150 L1200,0 L0,0 Z" fill="url(#goldGradient)" opacity="0.3"/>
</svg>
</div>
<!-- Bottom decorative curves -->
<div class="absolute bottom-0 left-0 w-full">
<svg viewBox="0 0 1200 300" class="w-full h-auto">
<path d="M0,150 C300,250 600,50 1200,150 L1200,300 L0,300 Z" fill="url(#goldGradient)" opacity="0.3"/>
</svg>
</div>
</div>
<!-- Main Content -->
<div class="relative z-10 h-full flex items-center justify-center px-6">
<div class="text-center max-w-4xl mx-auto">
<!-- Islamic Symbol -->
<div class="mb-8 animate-fade-in-down">
<div class="mx-auto w-32 h-32 relative">
<!-- Crescent and Star -->
<div class="absolute inset-0 flex items-center justify-center">
<svg viewBox="0 0 100 100" class="w-full h-full text-yellow-400">
<defs>
<filter id="glow">
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Crescent -->
<path d="M35 20 A 25 25 0 1 1 35 80 A 15 15 0 1 0 35 20" fill="currentColor" filter="url(#glow)"/>
<!-- Star -->
<polygon points="65,25 67,35 77,35 69,42 72,52 65,45 58,52 61,42 53,35 63,35" fill="currentColor" filter="url(#glow)"/>
</svg>
</div>
</div>
</div>
<!-- Invitation Title -->
<div class="mb-12 animate-fade-in-up animation-delay-300">
<h1 class="text-yellow-400 text-3xl md:text-4xl font-bold mb-4 tracking-wide">
Undangan
</h1>
<h2 class="text-white text-4xl md:text-6xl font-bold mb-6 font-serif">
TASYAKURAN
</h2>
<h2 class="text-white text-4xl md:text-6xl font-bold mb-8 font-serif">
KHITAN
</h2>
</div>
<!-- Guest Information -->
<div class="mb-12 animate-fade-in-up animation-delay-600">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 mb-6 border border-yellow-400/30">
<p class="text-yellow-400 text-lg mb-2">{{ data?.guestTitle || 'Kepada Yth.' }}</p>
<p class="text-white text-xl font-semibold">{{ data?.guestName || 'Muzaki Parsaoran' }}</p>
</div>
</div>
<!-- Open Invitation Button -->
<div class="animate-fade-in-up animation-delay-900">
<button
@click="$emit('next-page')"
class="group relative px-8 py-4 bg-gradient-to-r from-yellow-400 to-yellow-500 text-blue-900 font-bold text-lg rounded-full shadow-lg hover:shadow-xl transform hover:scale-105 transition-all duration-300 overflow-hidden"
>
<!-- Button background animation -->
<div class="absolute inset-0 bg-gradient-to-r from-yellow-300 to-yellow-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<!-- Button content -->
<span class="relative flex items-center space-x-2">
<span>OPEN INVITATION</span>
<Icon name="lucide:chevron-right" class="w-5 h-5 group-hover:translate-x-1 transition-transform duration-300" />
</span>
<!-- Shine effect -->
<div class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent transform -skew-x-12 -translate-x-full group-hover:translate-x-full transition-transform duration-700"></div>
</button>
</div>
<!-- Decorative Elements Around Button -->
<div class="absolute inset-0 pointer-events-none">
<!-- Floating particles -->
<div class="absolute top-1/4 left-1/4 animate-float">
<div class="w-2 h-2 bg-yellow-400 rounded-full opacity-60"></div>
</div>
<div class="absolute top-1/3 right-1/4 animate-float animation-delay-500">
<div class="w-1 h-1 bg-yellow-300 rounded-full opacity-80"></div>
</div>
<div class="absolute bottom-1/3 left-1/3 animate-float animation-delay-1000">
<div class="w-3 h-3 bg-yellow-500 rounded-full opacity-40"></div>
</div>
</div>
</div>
</div>
<!-- Corner Decorations -->
<div class="absolute top-4 left-4 w-16 h-16 border-l-2 border-t-2 border-yellow-400 opacity-50"></div>
<div class="absolute top-4 right-4 w-16 h-16 border-r-2 border-t-2 border-yellow-400 opacity-50"></div>
<div class="absolute bottom-4 left-4 w-16 h-16 border-l-2 border-b-2 border-yellow-400 opacity-50"></div>
<div class="absolute bottom-4 right-4 w-16 h-16 border-r-2 border-b-2 border-yellow-400 opacity-50"></div>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue'
// Props
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
// Emits
const emit = defineEmits(['next-page'])
// Reactive data
const isVisible = ref(false)
// Methods
const animateIn = () => {
isVisible.value = true
}
// Lifecycle
onMounted(() => {
setTimeout(animateIn, 100)
})
</script>
<style scoped>
/* Background Pattern */
.bg-pattern {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="islamic" x="0" y="0" width="25" height="25" patternUnits="userSpaceOnUse"><g fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="0.5"><path d="M12.5 0 L25 12.5 L12.5 25 L0 12.5 Z M6.25 6.25 L18.75 6.25 L18.75 18.75 L6.25 18.75 Z"/><circle cx="12.5" cy="12.5" r="3"/></g></pattern></defs><rect width="100%" height="100%" fill="url(%23islamic)"/></svg>');
}
/* Animations */
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes float {
0%, 100% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
}
.animate-fade-in-down {
animation: fadeInDown 0.8s ease-out forwards;
}
.animate-fade-in-up {
animation: fadeInUp 0.8s ease-out forwards;
}
.animate-float {
animation: float 3s ease-in-out infinite;
}
/* Animation Delays */
.animation-delay-300 {
animation-delay: 0.3s;
opacity: 0;
animation-fill-mode: forwards;
}
.animation-delay-500 {
animation-delay: 0.5s;
}
.animation-delay-600 {
animation-delay: 0.6s;
opacity: 0;
animation-fill-mode: forwards;
}
.animation-delay-900 {
animation-delay: 0.9s;
opacity: 0;
animation-fill-mode: forwards;
}
.animation-delay-1000 {
animation-delay: 1s;
}
/* Custom fonts */
.font-serif {
font-family: 'Times New Roman', serif;
}
/* Responsive */
@media (max-width: 640px) {
.text-4xl.md\:text-6xl {
font-size: 2.25rem;
}
.text-3xl.md\:text-4xl {
font-size: 1.875rem;
}
.px-8 {
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
</style>

View File

@ -0,0 +1,105 @@
<template>
<div class="h-screen w-full relative bg-gradient-to-br from-blue-900 via-blue-800 to-blue-900 overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 bg-pattern opacity-30"></div>
<!-- Main Content -->
<div class="relative z-10 h-full flex items-center justify-center px-6">
<div class="text-center max-w-4xl mx-auto">
<!-- Title -->
<div class="mb-12 animate-fade-in-down">
<h1 class="text-yellow-400 text-4xl md:text-6xl font-bold mb-6 font-script">Terimakasih</h1>
<p class="text-white text-lg md:text-xl leading-relaxed max-w-3xl mx-auto">
Kami mengucapkan terima kasih atas kehadiran serta doa restu
yang diberikan. Semoga Allah SWT senantiasa melimpahkan
rahmat dan keberkahan kepada kita semua.
</p>
</div>
<!-- Family Info -->
<div class="mb-12 animate-fade-in-up animation-delay-300">
<h2 class="text-yellow-400 text-2xl md:text-3xl font-bold mb-8">Kami Keluarga Besar Dari</h2>
<div class="space-y-6">
<div class="bg-white/10 backdrop-blur-sm rounded-2xl p-6 border border-yellow-400/30 animate-fade-in-up animation-delay-500">
<div class="text-yellow-400 text-lg font-semibold mb-2">
{{ data.fatherName || 'Bpk H. Munawar Huda, S.H.' }} & {{ data.motherName || 'Ibu Hj. Dinah, A.M.Keb' }}
</div>
<div class="text-white/80 text-sm">
{{ data.fatherDescription || '(Anggota DPRD Provinsi Banten Fraksi Partai Demokrat)' }}
</div>
</div>
</div>
</div>
<!-- Final Message -->
<div class="animate-fade-in-up animation-delay-1200">
<div class="bg-gradient-to-r from-yellow-400/20 to-yellow-600/20 backdrop-blur-sm rounded-2xl p-8 border border-yellow-400/50">
<p class="text-white text-lg md:text-xl font-medium mb-4 italic">
"Dan Allah telah mengeluarkan kamu dari perut ibumu dalam keadaan tidak mengetahui sesuatupun..."
</p>
<p class="text-yellow-400 font-semibold">- QS. An-Nahl: 78</p>
</div>
</div>
<!-- Social -->
<div class="mt-12 animate-fade-in-up animation-delay-1500 flex justify-center space-x-6">
<a href="https://instagram.com/abbauftech" target="_blank" rel="noopener noreferrer"
class="rounded-full p-3 backdrop-blur-sm border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110"
aria-label="Instagram">
<Icon name="lucide:instagram" class="w-6 h-6 text-yellow-400" />
</a>
<a href="#" class="rounded-full p-3 backdrop-blur-sm border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110" aria-label="Facebook">
<Icon name="lucide:facebook" class="w-6 h-6 text-yellow-400" />
</a>
<a href="#" class="rounded-full p-3 backdrop-blur-sm border border-yellow-400/30 hover:border-yellow-400/60 hover:bg-white/20 transition-all duration-300 transform hover:scale-110" aria-label="WhatsApp">
<Icon name="lucide:message-circle" class="w-6 h-6 text-yellow-400" />
</a>
</div>
</div>
</div>
<!-- Copyright -->
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 z-10">
<p class="text-white/60 text-xs text-center">© {{ currentYear }} - Invitation Template</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
})
const currentYear = computed(() => new Date().getFullYear())
</script>
<style scoped>
/* Animations & bg-pattern */
.bg-pattern {
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><defs><pattern id='pattern' x='0' y='0' width='20' height='20' patternUnits='userSpaceOnUse'><path d='M10 5 L15 10 L10 15 L5 10 Z' fill='none' stroke='rgba(255,255,255,0.1)' stroke-width='0.5'/></pattern></defs><rect width='100' height='100' fill='url(%23pattern)'/></svg>");
}
/* Tambahkan animasi sesuai kebutuhan */
.animate-fade-in-up {
animation: fadeInUp 1s forwards;
}
.animate-fade-in-down {
animation: fadeInDown 1s forwards;
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeInDown {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
</style>

View File

@ -0,0 +1,630 @@
<!-- components/invitation/templates/wedding/WeddingA.vue -->
<template>
<div class="wedding-template-a min-h-screen bg-gradient-to-b from-rose-50 to-white">
<!-- Opening Cover -->
<section v-if="!isOpened" class="fixed inset-0 z-50 flex items-center justify-center bg-cover bg-center"
:style="{ backgroundImage: `url(${data.coverImage || '/wedding1.png'})` }">
<div class="absolute inset-0 bg-black/40"></div>
<div class="relative text-center text-white p-8">
<p class="text-sm uppercase tracking-widest mb-4 animate-fade-in">The Wedding of</p>
<h1 class="text-5xl md:text-7xl font-serif mb-6 animate-slide-up">
{{ data.bride.nickname }} & {{ data.groom.nickname }}
</h1>
<p class="text-lg mb-8 animate-fade-in-delay">{{ formatDate(data.eventDate) }}</p>
<button @click="openInvitation"
class="px-8 py-3 bg-white text-rose-600 rounded-full font-semibold hover:bg-rose-50 transition-all duration-300 animate-pulse-slow">
<Icon name="mdi:envelope-open-outline" class="mr-2" />
Buka Undangan
</button>
</div>
</section>
<!-- Main Content -->
<div v-show="isOpened" class="animate-fade-in">
<!-- Hero Section -->
<section class="relative h-screen flex items-center justify-center overflow-hidden">
<div class="absolute inset-0 bg-cover bg-center"
:style="{ backgroundImage: `url(${data.heroImage || '/wedding1.png'})` }">
<div class="absolute inset-0 bg-gradient-to-b from-transparent via-black/20 to-black/40"></div>
</div>
<div class="relative text-center text-white px-4 z-10">
<p class="text-sm uppercase tracking-widest mb-4" data-aos="fade-down">
{{ data.greeting || 'Dengan Memohon Rahmat dan Ridho Allah SWT' }}
</p>
<h2 class="text-4xl md:text-6xl font-serif mb-4" data-aos="zoom-in">
{{ data.bride.nickname }} & {{ data.groom.nickname }}
</h2>
<div class="w-24 h-0.5 bg-white mx-auto mb-4" data-aos="zoom-in" data-aos-delay="200"></div>
<p class="text-lg" data-aos="fade-up" data-aos-delay="300">
{{ formatDate(data.eventDate) }}
</p>
</div>
<!-- Scroll Indicator -->
<div class="absolute bottom-8 left-1/2 transform -translate-x-1/2 animate-bounce">
<Icon name="mdi:chevron-double-down" class="text-white text-3xl" />
</div>
</section>
<!-- Couple Section -->
<section class="py-20 bg-white rounded-2xl shadow-lg max-w-5xl mx-auto px-6">
<div class="text-center mb-12" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-bold text-yellow-600 mb-4">Meet The Happy Couple</h2>
<p class="text-gray-600 max-w-2xl mx-auto">
Glory be to Allah SWT who has created creatures in pairs. Ya Allah,
please accept and bless us
</p>
</div>
<div class="grid md:grid-cols-2 gap-12 items-center">
<!-- Groom -->
<div class="text-center" data-aos="fade-right">
<div class="relative inline-block mb-6">
<div
class="w-40 h-40 md:w-48 md:h-48 rounded-full border-4 border-yellow-400 mx-auto flex items-center justify-center overflow-hidden">
<img :src="data.groom.photo || '/pria.jpg'" :alt="data.groom.fullname"
class="w-full h-full object-cover" />
</div>
<!-- Ornament -->
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-4">
<Icon name="mdi:flower" class="text-yellow-500 text-5xl" />
</div>
</div>
<h3 class="text-2xl font-[GreatVibes] text-yellow-600 mb-2">{{ data.groom.fullname }}</h3>
<p class="text-gray-600">Son of</p>
<p class="text-gray-700 font-medium">
{{ data.groom.fatherName }} & {{ data.groom.motherName }}
</p>
<div class="flex justify-center mt-4 space-x-4">
<a v-if="data.groom.instagram" :href="`https://instagram.com/${data.groom.instagram}`"
target="_blank">
<Icon name="mdi:instagram"
class="text-yellow-600 text-2xl hover:text-yellow-700 transition-colors" />
</a>
<a v-if="data.groom.twitter" :href="`https://twitter.com/${data.groom.twitter}`"
target="_blank">
<Icon name="mdi:twitter"
class="text-yellow-600 text-2xl hover:text-yellow-700 transition-colors" />
</a>
<a v-if="data.groom.facebook" :href="`https://facebook.com/${data.groom.facebook}`"
target="_blank">
<Icon name="mdi:facebook"
class="text-yellow-600 text-2xl hover:text-yellow-700 transition-colors" />
</a>
</div>
</div>
<!-- Bride -->
<div class="text-center" data-aos="fade-left">
<div class="relative inline-block mb-6">
<div
class="w-40 h-40 md:w-48 md:h-48 rounded-full border-4 border-yellow-400 mx-auto flex items-center justify-center overflow-hidden">
<img :src="data.bride.photo || '/wanita.jpg'" :alt="data.bride.fullname"
class="w-full h-full object-cover" />
</div>
<!-- Ornament -->
<div class="absolute bottom-0 left-1/2 -translate-x-1/2 translate-y-4">
<Icon name="mdi:flower" class="text-yellow-500 text-5xl" />
</div>
</div>
<h3 class="text-2xl font-[GreatVibes] text-yellow-600 mb-2">{{ data.bride.fullname }}</h3>
<p class="text-gray-600">Daughter of</p>
<p class="text-gray-700 font-medium">
{{ data.bride.fatherName }} & {{ data.bride.motherName }}
</p>
<div class="flex justify-center mt-4 space-x-4">
<a v-if="data.bride.instagram" :href="`https://instagram.com/${data.bride.instagram}`"
target="_blank">
<Icon name="mdi:instagram"
class="text-yellow-600 text-2xl hover:text-yellow-700 transition-colors" />
</a>
<a v-if="data.bride.twitter" :href="`https://twitter.com/${data.bride.twitter}`"
target="_blank">
<Icon name="mdi:twitter"
class="text-yellow-600 text-2xl hover:text-yellow-700 transition-colors" />
</a>
<a v-if="data.bride.facebook" :href="`https://facebook.com/${data.bride.facebook}`"
target="_blank">
<Icon name="mdi:facebook"
class="text-yellow-600 text-2xl hover:text-yellow-700 transition-colors" />
</a>
</div>
</div>
</div>
<!-- Our Story Button -->
<div class="text-center mt-12">
<button
class="bg-yellow-500 hover:bg-yellow-600 text-white px-6 py-3 rounded-md font-medium transition-colors">
Our Story
</button>
</div>
</section>
<!-- Event Details -->
<section class="py-20 bg-rose-50">
<div class="max-w-4xl mx-auto px-4">
<div class="text-center mb-12" data-aos="fade-up">
<h2 class="text-3xl md:text-4xl font-serif text-rose-800 mb-4">Waktu & Tempat</h2>
<p class="text-gray-600">Merupakan suatu kehormatan dan kebahagiaan bagi kami apabila
Bapak/Ibu/Saudara/i berkenan hadir</p>
</div>
<!-- Akad Nikah -->
<div class="bg-white rounded-2xl shadow-lg p-8 mb-6" data-aos="fade-up">
<div class="text-center">
<div
class="inline-flex items-center justify-center w-16 h-16 bg-rose-100 rounded-full mb-4">
<Icon name="mdi:mosque" class="text-rose-600 text-2xl" />
</div>
<h3 class="text-2xl font-serif text-gray-800 mb-4">Akad Nikah</h3>
<div class="space-y-2 text-gray-600">
<p class="flex items-center justify-center">
<Icon name="mdi:calendar" class="mr-2" />
{{ formatDate(data.akad.date) }}
</p>
<p class="flex items-center justify-center">
<Icon name="mdi:clock-outline" class="mr-2" />
{{ data.akad.time }} - Selesai
</p>
<p class="flex items-center justify-center">
<Icon name="mdi:map-marker" class="mr-2" />
{{ data.akad.place }}
</p>
<p class="text-sm">{{ data.akad.address }}</p>
</div>
</div>
</div>
<!-- Resepsi -->
<div class="bg-white rounded-2xl shadow-lg p-8" data-aos="fade-up" data-aos-delay="200">
<div class="text-center">
<div
class="inline-flex items-center justify-center w-16 h-16 bg-blue-100 rounded-full mb-4">
<Icon name="mdi:party-popper" class="text-blue-600 text-2xl" />
</div>
<h3 class="text-2xl font-serif text-gray-800 mb-4">Resepsi</h3>
<div class="space-y-2 text-gray-600">
<p class="flex items-center justify-center">
<Icon name="mdi:calendar" class="mr-2" />
{{ formatDate(data.resepsi.date) }}
</p>
<p class="flex items-center justify-center">
<Icon name="mdi:clock-outline" class="mr-2" />
{{ data.resepsi.time }} - Selesai
</p>
<p class="flex items-center justify-center">
<Icon name="mdi:map-marker" class="mr-2" />
{{ data.resepsi.place }}
</p>
<p class="text-sm">{{ data.resepsi.address }}</p>
</div>
</div>
</div>
<!-- Countdown Timer -->
<div class="mt-12" data-aos="zoom-in">
<CountdownTimer :targetDate="data.eventDate" />
</div>
</div>
</section>
<!-- Gallery -->
<section class="py-20">
<div class="max-w-6xl mx-auto px-4">
<Gallery :images="data.gallery" />
</div>
</section>
<!-- Maps -->
<section class="bg-[#FFF8EC] py-12 text-center">
<div class="max-w-5xl mx-auto px-4 text-center">
<!-- Judul -->
<div class="mb-8">
<div class="flex items-center justify-center gap-2 mb-2">
<span class="h-[1px] w-12 bg-yellow-400"></span>
<span class="text-yellow-600 text-2xl">🌸</span>
<span class="h-[1px] w-12 bg-yellow-400"></span>
</div>
<h2 class="text-3xl md:text-4xl font-serif text-rose-800 mb-4">Lokasi Acara</h2>
</div>
<div class="grid md:grid-cols-2 gap-6">
<div data-aos="fade-right">
<h3 class="font-semibold text-gray-800 mb-3">Akad Nikah</h3>
<Maps :location="data.akad.mapUrl" />
<a :href="data.akad.mapUrl" target="_blank"
class="inline-block mt-4 px-6 py-2 bg-rose-600 text-white rounded-lg hover:bg-rose-700 transition-colors">
<Icon name="mdi:map" class="mr-2" />
Buka di Google Maps
</a>
</div>
<div data-aos="fade-left">
<h3 class="font-semibold text-gray-800 mb-3">Resepsi</h3>
<Maps :location="data.resepsi.mapUrl" />
<a :href="data.resepsi.mapUrl" target="_blank"
class="inline-block mt-4 px-6 py-2 bg-rose-600 text-white rounded-lg hover:bg-rose-700 transition-colors">
<Icon name="mdi:map" class="mr-2" />
Buka di Google Maps
</a>
</div>
</div>
</div>
</section>
<!-- Ucapan -->
<section class="py-20">
<div class="max-w-2xl mx-auto px-4">
<!-- RSVP Form -->
<RSVP :invitationId="data.id" @submitted="handleRSVPSubmit" />
</div>
</section>
<section>
<!-- Gift Section -->
<section class="py-20 bg-[#FFF8F0]">
<div class="max-w-5xl mx-auto px-4 text-center">
<!-- Judul -->
<div class="mb-10">
<div class="flex items-center justify-center mb-4">
<span class="w-20 h-px bg-orange-300"></span>
<span class="mx-2 text-orange-400 text-2xl">🌸</span>
<span class="w-20 h-px bg-orange-300"></span>
</div>
<h2 class="text-3xl md:text-4xl font-bold text-orange-500">Give a Gift</h2>
<p class="text-gray-600 mt-4 max-w-2xl mx-auto">
Bagi keluarga dan sahabat yang ingin berbagi tanda kasih, kami dengan senang hati
menerimanya sebagai bagian dari doa dan restu yang tulus. Terima kasih telah menjadi
bagian dari hari istimewa kami.
</p>
</div>
<!-- Card Container -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Digital Wallet -->
<div class="bg-white rounded-2xl shadow-md p-6 text-left">
<h3 class="text-xl font-semibold text-gray-700 mb-4">Digital Wallet</h3>
<p class="text-sm text-gray-400 mb-6">Note: Tap to copy bank number</p>
<!-- Akun 1 -->
<div class="mb-6">
<p class="font-semibold text-orange-500 flex items-center space-x-2">
<span>Asep Irawan</span>
<img src="/logo1.png" alt="BNI" class="h-5" />
</p>
<div class="flex items-center mt-2 bg-white shadow-md rounded-xl p-3">
<input class="flex-1 text-gray-700 font-mono outline-none"
value="009 - 0222 2444 21" readonly />
<button @click="copyToClipboard('009 - 0222 2444 21')">
<Icon name="mdi:content-copy" class="text-gray-500" />
</button>
</div>
</div>
<!-- Akun 2 -->
<div>
<p class="font-semibold text-orange-500 flex items-center space-x-2">
<span>Putri Amanda</span>
<img src="/logo2.png" alt="BNI" class="h-5" />
</p>
<div class="flex items-center mt-2 bg-white shadow-md rounded-xl p-3">
<input class="flex-1 text-gray-700 font-mono outline-none"
value="009 - 0222 2444 21" readonly />
<button @click="copyToClipboard('009 - 0222 2444 21')">
<Icon name="mdi:content-copy" class="text-gray-500" />
</button>
</div>
</div>
</div>
<!-- Offline Gift -->
<div class="bg-white rounded-2xl shadow-md p-6 text-left">
<h3 class="text-xl font-semibold text-gray-700 mb-4">Offline Gift</h3>
<p class="text-gray-600 mb-6">
Jl. Terusan Jakarta No.53, Cicaheum, Kec. Kiaracondong, Kota Bandung, Jawa Barat
40291
</p>
<div class="flex items-center space-x-4">
<Icon name="mdi:map-marker" class="text-orange-500 text-2xl" />
<a href="https://maps.google.com/?q=Jl.+Terusan+Jakarta+No.53,+Cicaheum"
target="_blank"
class="bg-orange-400 hover:bg-orange-500 text-white px-6 py-2 rounded-xl shadow transition">
Open Map
</a>
</div>
</div>
</div>
<!-- Garis Dekorasi Bawah -->
<div class="flex items-center justify-center mt-10">
<span class="w-20 h-px bg-orange-300"></span>
<span class="mx-2 text-orange-400 text-2xl">🌸</span>
<span class="w-20 h-px bg-orange-300"></span>
</div>
</div>
</section>
</section>
<!-- Closing -->
<section class="py-20 text-center px-4">
<div class="max-w-2xl mx-auto" data-aos="fade-up">
<p class="text-gray-600 mb-4">
Merupakan suatu kehormatan dan kebahagiaan bagi kami apabila Bapak/Ibu/Saudara/i
berkenan hadir untuk memberikan doa restu kepada kedua mempelai.
</p>
<p class="text-gray-600 mb-8">
Atas kehadiran serta doa restu Bapak/Ibu/Saudara/i, kami ucapkan terima kasih.
</p>
<p class="font-serif text-2xl text-rose-800 mb-2">Wassalamu'alaikum Wr. Wb.</p>
<p class="text-gray-700 font-semibold mt-8">
{{ data.bride.nickname }} & {{ data.groom.nickname }}
</p>
</div>
</section>
<!-- Footer -->
<footer class="relative text-center bg-cover bg-center bg-no-repeat"
style="background-image: url('/wedding1.png');">
<!-- Overlay gelap biar teks lebih kontras -->
<div class="absolute inset-0 bg-black/40"></div>
<!-- Konten footer -->
<div class="relative z-10">
<!-- Kotak Nama Mempelai -->
<div class="flex items-center justify-center space-x-4 -mt-12">
<div class="px-8 py-3 border border-white rounded-lg bg-white/20 backdrop-blur-sm shadow">
<p class="font-semibold italic text-2xl text-white">{{ data.groom.nickname }}</p>
</div>
<span class="text-3xl font-bold text-white">-</span>
<div class="px-8 py-3 border border-white rounded-lg bg-white/20 backdrop-blur-sm shadow">
<p class="font-semibold italic text-2xl text-white">{{ data.bride.nickname }}</p>
</div>
</div>
<!-- Ikon bunga -->
<div class="flex justify-center mt-6">
<span class="text-white text-3xl"></span>
</div>
<!-- Logo -->
<div class="flex justify-center mt-4">
<img src="/ABBAUF.png" alt="Logo" class="h-12 object-contain" />
</div>
<!-- Copyright -->
<p class="text-xs mt-4 text-white/80">
© 2024 All rights reserved
</p>
</div>
</footer>
<!-- Music Player -->
<MusicPlayer v-if="data.musicUrl" :url="data.musicUrl" :autoplay="true" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import AOS from 'aos'
import 'aos/dist/aos.css'
// Import shared components
import CountdownTimer from '~/components/shared/CountdownTimer.vue'
import Gallery from '~/components/shared/Gallery.vue'
import Maps from '~/components/shared/Maps.vue'
import RSVP from '~/components/shared/RSVP.vue'
import GuestBook from '~/components/shared/GuestBook.vue'
import MusicPlayer from '~/components/shared/MusicPlayer.vue'
// Props untuk menerima data dari parent/API
const props = defineProps({
data: {
type: Object,
required: true,
default: () => ({
id: '',
coverImage: '',
heroImage: '',
greeting: '',
eventDate: new Date(),
bride: {
fullname: '',
nickname: '',
photo: '',
orderChild: '',
fatherName: '',
motherName: '',
instagram: ''
},
groom: {
fullname: '',
nickname: '',
photo: '',
orderChild: '',
fatherName: '',
motherName: '',
instagram: ''
},
akad: {
date: new Date(),
time: '',
place: '',
address: '',
mapUrl: ''
},
resepsi: {
date: new Date(),
time: '',
place: '',
address: '',
mapUrl: ''
},
gallery: [],
gift: {
bankName: '',
accountNumber: '',
accountName: ''
},
musicUrl: ''
})
}
})
// State
const isOpened = ref(false)
const guestMessages = ref([])
// Methods
const openInvitation = () => {
isOpened.value = true
// Play music if available
if (props.data.musicUrl) {
// Music will autoplay through MusicPlayer component
}
}
const formatDate = (date) => {
if (!date) return ''
const options = {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
}
return new Date(date).toLocaleDateString('id-ID', options)
}
const copyToClipboard = async (text) => {
try {
await navigator.clipboard.writeText(text)
// Show toast notification
alert('Nomor rekening berhasil disalin!')
} catch (err) {
console.error('Failed to copy:', err)
}
}
const handleRSVPSubmit = (data) => {
// Handle RSVP submission
console.log('RSVP submitted:', data)
// Add to guest messages
if (data.message) {
guestMessages.value.unshift({
name: data.name,
message: data.message,
attendance: data.attendance,
createdAt: new Date()
})
}
}
// Load guest messages from API
const loadGuestMessages = async () => {
try {
// Fetch from your Laravel API
// const response = await $fetch(`/api/invitations/${props.data.id}/messages`)
// guestMessages.value = response.data
} catch (error) {
console.error('Error loading guest messages:', error)
}
}
// Lifecycle
onMounted(() => {
// Initialize AOS
AOS.init({
duration: 1000,
once: true,
offset: 100
})
// Load guest messages
loadGuestMessages()
})
</script>
<style scoped>
/* Custom animations */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slide-up {
from {
transform: translateY(30px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes pulse-slow {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
}
.animate-fade-in {
animation: fade-in 1s ease-out;
}
.animate-fade-in-delay {
animation: fade-in 1s ease-out 0.5s both;
}
.animate-slide-up {
animation: slide-up 1s ease-out;
}
.animate-pulse-slow {
animation: pulse-slow 2s ease-in-out infinite;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #f43f5e;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #e11d48;
}
</style>

View File

@ -102,8 +102,9 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
const route = useRoute() const route = useRoute()
const templateId = route.query.template_id const templateId = route.params.id
const template = ref({ fiturs: [] }) const template = ref({ fiturs: [] })
const loading = ref(true) const loading = ref(true)

View File

@ -1,8 +1,282 @@
<template> <template>
<div> <div class="max-w-5xl mx-auto p-8 bg-gradient-to-b from-green-50 to-blue-50 shadow-lg rounded-xl">
<FormsKhitanForm /> <!-- Judul Form -->
<div class="text-center mb-10">
<h1 class="text-3xl md:text-4xl font-extrabold text-green-700 drop-shadow-sm">
🕌 Form Pemesanan Undangan Khitan Premium
</h1>
<p class="text-gray-600 mt-2">
Isi data berikut dengan lengkap untuk pemesanan undangan khitan.
</p>
</div>
<!-- Info Paket Premium -->
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 mb-10">
<h2 class="text-lg font-bold text-gray-800 mb-4">Paket Premium (Informasi)</h2>
<ul class="list-disc pl-5 space-y-1 text-gray-700 text-sm">
<li>Maksimal 3x Acara (Akad, Resepsi, Syukuran)</li>
<li>Unlimited Galeri Foto</li>
<li>Timeline Story</li>
<li>Google Maps</li>
<li>Reminder Google Calendar</li>
<li>Link Instagram Live Streaming</li>
<li>Amplop Digital</li>
<li>Placement Video Cinematic</li>
<li>Bonus Undangan Image Post Story</li>
<li>Masa Aktif 12 Bulan</li>
<li>Fitur standar: Nama Tamu Personal Unlimited Tamu, Request Musik</li>
</ul>
</section>
<!-- Form Pemesanan -->
<form @submit.prevent="submitForm" class="space-y-10">
<!-- Tema Undangan (otomatis isi) -->
<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 flex items-center gap-2">
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Tema Undangan
</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<input :value="form.nama_template" type="text" placeholder="Nama Template" class="input-readonly" readonly />
<input :value="form.kategori" type="text" placeholder="Kategori" class="input-readonly" readonly />
<input :value="form.harga" type="text" placeholder="Harga" class="input-readonly" readonly />
<input :value="form.tanggal_pemesanan" type="text" placeholder="Tanggal Pemesanan" class="input-readonly" readonly />
</div>
</section>
<!-- Anak yang dikhitan -->
<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">Anak yang Dikhitan</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="relative">
<input v-model="form.nama_anak" id="nama_anak" type="text" placeholder=" " required class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<label for="nama_anak" class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">Nama Anak</label>
</div>
<div class="relative">
<input v-model="form.umur_anak" id="umur_anak" type="number" placeholder=" " class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<label for="umur_anak" class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">Umur Anak</label>
</div>
<div class="relative md:col-span-2">
<input v-model="form.tempat_lahir" id="tempat_lahir" type="text" placeholder=" " class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<label for="tempat_lahir" class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">Tempat Lahir</label>
</div>
<!-- Informasi Orang Tua -->
<div class="relative md:col-span-2">
<input v-model="form.nama_orang_tua" type="text" placeholder="Nama Orang Tua" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<label class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">Nama Orang Tua</label>
</div>
</div>
</section>
<!-- Jadwal Acara -->
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="text-lg font-bold text-gray-800">Jadwal Acara & Countdown</h2>
<div v-for="(acara, index) in form.jadwal_acara" :key="index" class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-3">
<input v-model="acara.nama_acara" type="text" placeholder="Nama Acara" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<input v-model="acara.tanggal" type="date" placeholder="Tanggal" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<input v-model="acara.waktu" type="text" placeholder="Waktu" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<div class="text-sm font-medium text-red-600 flex items-center justify-center">
{{ countdowns[index] }}
</div>
</div>
<button type="button" @click="addAcara" class="text-green-700 font-semibold">+ Tambah Acara</button>
</section>
<!-- Fitur Premium & Amplop Digital -->
<!-- Bagian Fitur Premium & Amplop Digital diperbarui -->
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
<h2 class="text-lg font-bold text-gray-800">Fitur Premium</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="relative">
<input v-model="form.maps_acara" type="url" placeholder="" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<label class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">Link Google Maps</label>
</div>
<div class="relative">
<input v-model="form.link_live" type="url" placeholder="" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<label class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">Link Instagram Live</label>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" v-model="form.amplop_digital" /> Amplop Digital
</div>
<div class="relative" v-if="form.amplop_digital">
<input v-model="form.no_rekening" type="text" placeholder="Nomor Rekening" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<label class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">No. Rekening</label>
</div>
<div class="flex items-center gap-2">
<input type="checkbox" v-model="form.placement_video" /> Placement Video Cinematic
</div>
<div class="flex items-center gap-2">
<input type="checkbox" v-model="form.bonus_image_post" /> Bonus Undangan Image Post Story
</div>
<!-- Reminder Google Calendar -->
<div class="flex items-center gap-2">
<input type="checkbox" v-model="form.reminder_calendar" /> Reminder Google Calendar
</div>
<div class="relative" v-if="form.reminder_calendar">
<input v-model="form.reminder_notes" type="text" placeholder="Catatan Reminder (opsional)" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500" />
<label class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">Catatan Reminder</label>
</div>
<div class="relative md:col-span-2">
<textarea v-model="form.timeline_story" rows="3" placeholder="" class="peer w-full border border-gray-300 rounded-lg px-3 pt-5 pb-2 text-sm focus:outline-none focus:ring-2 focus:ring-green-500 resize-none"></textarea>
<label class="absolute left-2 top-1 text-gray-500 text-xs transition-all duration-200 peer-placeholder-shown:top-5 peer-placeholder-shown:text-sm peer-placeholder-shown:text-gray-400 peer-focus:top-1 peer-focus:text-xs peer-focus:text-green-600">Timeline Story</label>
</div>
</div>
</section>
<!-- Galeri Upload -->
<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">Galeri</h2>
<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">&times;</button>
</div>
<label for="gallery-upload" class="flex items-center justify-center w-full aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
</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!
</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. Pastikan semua data yang wajib diisi sudah lengkap.
</div>
</form>
</div> </div>
</template> </template>
<script setup> <script setup>
</script> import { ref, onMounted } from "vue";
const form = ref({
template_id: "",
nama_template: "",
kategori: "",
harga: "",
tanggal_pemesanan: new Date().toISOString().split("T")[0],
nama_pemesan: "",
no_hp: "",
email: "",
nama_anak: "",
umur_anak: "",
tempat_lahir: "",
nama_orang_tua: "",
jadwal_acara: [{ nama_acara: "", tanggal: "", waktu: "" }],
maps_acara: "",
link_live: "",
amplop_digital: false,
no_rekening: "",
placement_video: false,
bonus_image_post: false,
timeline_story: "",
galeri: [],
selectedFiturs: {},
});
const previewImages = ref([]);
const loading = ref(false);
const success = ref(false);
const error = ref(false);
const countdowns = ref([""]); // array countdown tiap acara
const addAcara = () => {
if (form.value.jadwal_acara.length < 3) {
form.value.jadwal_acara.push({ nama_acara: "", tanggal: "", waktu: "" });
countdowns.value.push("");
}
};
// Countdown real-time
const updateCountdowns = () => {
form.value.jadwal_acara.forEach((acara, i) => {
if (!acara.tanggal) {
countdowns.value[i] = "-";
return;
}
const acaraTime = new Date(`${acara.tanggal}T${acara.waktu || "00:00"}`).getTime();
const now = Date.now();
const diff = acaraTime - now;
if (diff <= 0) {
countdowns.value[i] = "Acara sedang berlangsung / selesai";
} else {
const d = Math.floor(diff / (1000 * 60 * 60 * 24));
const h = Math.floor((diff / (1000 * 60 * 60)) % 24);
const m = Math.floor((diff / (1000 * 60)) % 60);
const s = Math.floor((diff / 1000) % 60);
countdowns.value[i] = `${d}d ${h}h ${m}m ${s}s`;
}
});
};
onMounted(() => {
setInterval(updateCountdowns, 1000);
});
const handleFileUpload = (event) => {
const newFiles = Array.from(event.target.files);
form.value.galeri.push(...newFiles);
previewImages.value = [];
form.value.galeri.forEach(file => {
const reader = new FileReader();
reader.onload = e => previewImages.value.push(e.target.result);
reader.readAsDataURL(file);
});
event.target.value = null;
};
const removeImage = (index) => {
form.value.galeri.splice(index, 1);
previewImages.value.splice(index, 1);
};
const submitForm = async () => {
loading.value = true;
success.value = false;
error.value = false;
try {
const body = new FormData();
for (const key in form.value) {
if (key === "galeri") form.value.galeri.forEach(f => body.append("galeri[]", f));
else if (key === "jadwal_acara") form.value.jadwal_acara.forEach(a => body.append("jadwal_acara[]", JSON.stringify(a)));
else if (key !== "selectedFiturs") body.append(key, form.value[key]);
}
for (const kategoriId in form.value.selectedFiturs) {
const fiturs = Array.isArray(form.value.selectedFiturs[kategoriId]) ? form.value.selectedFiturs[kategoriId] : [form.value.selectedFiturs[kategoriId]];
fiturs.forEach(fiturId => body.append("fiturs[]", fiturId));
}
await $fetch("http://localhost:8000/api/form", { method: "POST", body, headers: { Accept: "application/json" } });
success.value = true;
const adminNumber = "62895602603247";
const message = `Halo Admin, ada pemesanan undangan khitan baru 🎉\nNama Pemesan: ${form.value.nama_pemesan}\nNo WA: ${form.value.no_hp}\nEmail: ${form.value.email}\nTemplate: ${form.value.nama_template} (${form.value.kategori})\nHarga: ${form.value.harga}\nTanggal Pemesanan: ${form.value.tanggal_pemesanan}`;
window.location.href = `https://wa.me/${adminNumber}?text=${encodeURIComponent(message)}`;
} catch (err) {
console.error(err);
error.value = true;
} finally {
loading.value = false;
}
};
</script>

View File

@ -1,9 +1,240 @@
<template> <template>
<div> <div class="max-w-6xl mx-auto p-8 bg-gradient-to-b from-pink-50 to-red-50 shadow-lg rounded-xl">
<FormsPernikahanForm /> <!-- Judul -->
<div class="text-center mb-10">
<h1 class="text-3xl md:text-4xl font-extrabold text-red-700 drop-shadow-sm">
💍 Form Pemesanan Undangan Pernikahan 💐
</h1>
<p class="text-gray-600 mt-2">Isi data berikut untuk membuat undangan pernikahan Anda</p>
</div>
<form @submit.prevent="submitForm" class="space-y-10">
<!-- Paket Starter (Informasi) -->
<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>
<div class="p-4 border rounded-xl bg-blue-50">
<div class="font-semibold text-gray-800 mb-2">Starter</div>
<div class="text-gray-600 mb-2">Fitur:</div>
<ul class="list-disc list-inside text-gray-600">
<li>1x Acara</li>
<li>Masa Aktif 3 Bulan</li>
<li>Fitur standar: Nama Tamu Personal, maks. 100 Tamu, Request Musik</li>
</ul>
</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 & Detail Paket</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 Pengantin -->
<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 Pengantin</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm mb-1">Nama Pengantin Pria</label>
<input v-model="form.nama_pria" type="text" class="input" required />
</div>
<div>
<label class="block text-sm mb-1">Nama Pengantin Wanita</label>
<input v-model="form.nama_wanita" type="text" class="input" required />
</div>
<div>
<label class="block text-sm mb-1">Orang Tua Pengantin Pria</label>
<input v-model="form.ortu_pria" type="text" class="input" />
</div>
<div>
<label class="block text-sm mb-1">Orang Tua Pengantin Wanita</label>
<input v-model="form.ortu_wanita" type="text" class="input" />
</div>
</div>
</section>
<!-- Data 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 (1x Acara)</h2>
<div v-for="(acara, index) in form.acaras" :key="index" class="mb-6 border-b pb-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block text-sm mb-1">Nama Acara</label>
<input v-model="acara.nama" type="text" class="input" required />
</div>
<div>
<label class="block text-sm mb-1">Tanggal Acara</label>
<input v-model="acara.tanggal" type="date" class="input" required />
</div>
<div>
<label class="block text-sm mb-1">Jam</label>
<input v-model="acara.jam" type="time" class="input" />
</div>
<div>
<label class="block text-sm mb-1">Lokasi</label>
<input v-model="acara.lokasi" type="text" class="input" required />
</div>
<!-- Request Lagu -->
<div class="md:col-span-2">
<label class="block text-sm mb-1">Request Lagu</label>
<input v-model="acara.request_lagu" type="text" class="input"
placeholder="Contoh: Lagu pengantin, playlist, dsb." />
</div>
</div>
</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> </div>
</template> </template>
<script setup> <script setup>
import { ref } from "vue"
</script> const form = ref({
selectedPaket: "starter",
nama_template: "undangan minimalis",
kategori: "Pernikahan",
harga: "100.000",
tanggal_pemesanan: new Date().toLocaleDateString("id-ID"),
nama_pemesan: "",
no_hp: "",
email: "",
nama_pria: "",
nama_wanita: "",
ortu_pria: "",
ortu_wanita: "",
acaras: [{ nama: "", tanggal: "", jam: "", lokasi: "", request_lagu: "" }],
})
const loading = ref(false)
const success = ref(false)
const error = ref(false)
const template = ref(null)
const route = useRoute()
onMounted(async () => {
try {
const templateId = route.query.template_id // ambil dari query string
if (!templateId) throw new Error("Template ID tidak ditemukan di URL")
const res = await fetch(`http://localhost:8000/api/templates/${templateId}`)
if (!res.ok) throw new Error("Gagal ambil template")
template.value = await res.json()
// isi form info template
form.value.nama_template = template.value.nama_template
form.value.kategori = template.value.kategori?.nama || ""
form.value.harga = template.value.harga
form.value.tanggal_pemesanan = new Date().toLocaleDateString("id-ID")
} catch (err) {
console.error("Error fetch template:", err)
}
})
const submitForm = async () => {
try {
loading.value = true
success.value = false
error.value = false
const res = await fetch("http://localhost:8000/api/pelanggans", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
nama_pemesan: form.value.nama_pemesan,
email: form.value.email,
no_tlpn: form.value.no_hp,
template_id: template.value.id,
form: {
nama_pengantin: form.value.nama_pria + " & " + form.value.nama_wanita,
tanggal_acara: form.value.acaras[0].tanggal,
lokasi: form.value.acaras[0].lokasi
}
}),
})
// parse JSON dulu
const data = await res.json()
console.log("Respon backend:", 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
} catch (err) {
console.error("Catch error:", 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>

View File

@ -1,9 +1,275 @@
<template> <template>
<div> <div class="max-w-6xl mx-auto p-8 bg-gradient-to-b from-blue-50 to-indigo-50 shadow-lg rounded-xl">
<FormsUlangTahunForm /> <!-- 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">
&times;
</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> </div>
</template> </template>
<script setup> <script setup>
import { ref } from "vue"
</script> 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>

View File

@ -0,0 +1,21 @@
<template>
<WeddingA v-if="invitationData" :data="invitationData" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import WeddingA from '~/components/templates/wedding/WeddingA.vue'
const route = useRoute()
const invitationData = ref(null)
onMounted(async () => {
try {
const res = await fetch(`http://127.0.0.1:8000/api/pelanggan/${route.params.id}`)
invitationData.value = await res.json()
} catch (err) {
console.error('Gagal ambil data:', err)
}
})
</script>

View File

@ -0,0 +1,89 @@
<template>
<div>
<KhitanA
v-if="isCoverVisible"
:data="invitationData"
@next-page="openInvitation"
/>
<div v-else class="min-h-screen bg-blue-900 overflow-hidden">
<div class="fixed top-6 left-1/2 transform -translate-x-1/2 z-50">
<div class="flex space-x-2 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2">
<button
v-for="(page, index) in pages"
:key="index"
@click="goToPage(index)"
:class="[
'px-3 py-1 rounded-full text-sm font-medium transition-all duration-300',
currentPage === index
? 'bg-yellow-400 text-blue-900'
: 'text-white hover:bg-white/20'
]"
>
{{ page.title }}
</button>
</div>
</div>
<div class="relative h-screen">
<Transition :name="transitionName" mode="out-in">
<component
:is="pages[currentPage].component"
:key="currentPage"
:data="invitationData"
class="absolute inset-0"
/>
</Transition>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 3. Import SEMUA komponen, termasuk komponen sampul
import KhitanA from '~/components/templates/khitan/KhitanA.vue' // <-- IMPORT INI
import KhitanIntroduction from '~/components/templates/khitan/Introduction.vue'
import KhitanEvent from '~/components/templates/khitan/Event.vue'
import KhitanGallery from '~/components/templates/khitan/Gallery.vue'
import KhitanSay from '~/components/templates/khitan/GuestBook.vue'
import KhitanThankYou from '~/components/templates/khitan/ThankYou.vue'
// 4. Tambahkan state untuk mengontrol sampul
const isCoverVisible = ref(true) // <-- TAMBAHKAN INI
// --- Kode Anda yang sudah ada sebelumnya ---
const currentPage = ref(0)
const transitionName = ref('slide-right')
const pages = [
{ title: 'Introduction', component: KhitanIntroduction },
{ title: 'Event', component: KhitanEvent },
{ title: 'Gallery', component: KhitanGallery },
{ title: 'Say', component: KhitanSay },
{ title: 'Thank You', component: KhitanThankYou }
]
const invitationData = {
// Menambahkan guestName agar bisa ditampilkan di sampul
guestName: 'Tamu Undangan',
childName: 'Satria Huda Dinata',
fatherName: 'Bpk H. Munawar Huda, S.H.',
motherName: 'Ibu Hj. Dinah, A.M.Keb',
eventDate: '20-21 Juni 2025',
eventTime: '09:00 WIB s.d Selesai',
eventLocation: 'TMC Mangrove, Tanjung Pasir'
}
const goToPage = (index) => {
transitionName.value = index > currentPage.value ? 'slide-left' : 'slide-right'
currentPage.value = index
}
// 5. Tambahkan fungsi untuk membuka undangan
const openInvitation = () => {
isCoverVisible.value = false;
// Opsional: Anda bisa mulai putar musik di sini
}
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="min-h-screen bg-gradient-to-br from-yellow-300 via-yellow-400 to-yellow-500 relative overflow-hidden">
<!-- Navigation (hanya muncul kalau bukan di landing) -->
<nav v-if="currentSection !== 'landing'"
class="absolute top-4 left-1/2 transform -translate-x-1/2 z-20">
<ul class="flex space-x-6 bg-white/40 backdrop-blur-md px-6 py-3 rounded-full shadow-md text-sm font-semibold text-gray-800">
<li>
<button @click="currentSection='introduction'" :class="navClass('introduction')">Intro</button>
</li>
<li>
<button @click="currentSection='event'" :class="navClass('event')">Event</button>
</li>
<li>
<button @click="currentSection='galeri'" :class="navClass('galeri')">Gallery</button>
</li>
<li>
<button @click="currentSection='say'" :class="navClass('say')">Guest Book</button>
</li>
<li>
<button @click="currentSection='thanks'" :class="navClass('thanks')">Thanks</button>
</li>
</ul>
</nav>
<!-- Music Icon -->
<div class="fixed bottom-4 left-4 z-30" v-if="currentSection !== 'landing'">
<button @click="toggleMusic" class="bg-orange-600 p-3 rounded-full text-white shadow-lg">
{{ isPlaying ? '⏸️' : '▶️' }}
</button>
</div>
<!-- Main Content -->
<main class="relative z-10 min-h-screen flex items-center justify-center p-4">
<Landing v-if="currentSection==='landing'"
:childName="childName"
:guestName="guestName"
@open-invitation="currentSection='introduction'" />
<Introduction v-if="currentSection==='introduction'"
:age="age"
:childName="childName"
:childOrder="childOrder"
:parentNames="parentNames"
:childPhoto="childPhoto"/>
<Event v-if="currentSection==='event'" />
<Gallery v-if="currentSection==='galeri'" />
<GuestBook v-if="currentSection==='say'" />
<ThankYou v-if="currentSection==='thanks'" />
</main>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Landing from '~/components/templates/Ultah/Landing.vue'
import Introduction from '~/components/templates/Ultah/Introduction.vue'
import Event from '~/components/templates/Ultah/Event.vue'
import Gallery from '~/components/templates/Ultah/Gallery.vue'
import GuestBook from '~/components/templates/Ultah/GuestBook.vue'
import ThankYou from '~/components/templates/Ultah/ThankYou.vue'
const childName = ref('Rayyanza Malik Ahmad')
const guestName = ref('Gempita Nora Marten')
const age = ref(4)
const childOrder = ref(2)
const parentNames = ref('Raffi Ahmad & Nagita Slavina')
const childPhoto = ref('')
const isPlaying = ref(false)
const currentSection = ref('landing')
const toggleMusic = () => { isPlaying.value = !isPlaying.value }
const navClass = (section) => {
return currentSection.value === section
? 'text-orange-700 underline'
: 'hover:text-orange-600'
}
</script>

View File

@ -0,0 +1,35 @@
<template>
<div>
<WeddingA
namaPengantinPria="Ahmad"
namaPengantinWanita="Aisyah"
namaAyahPria="Budi"
namaIbuPria="Siti"
namaAyahWanita="Hendra"
namaIbuWanita="Nur"
tanggalAkad="Minggu, 15 Oktober 2025"
lokasiAkad="Masjid Raya Padang"
tanggalResepsi="Senin, 16 Oktober 2025"
lokasiResepsi="Gedung Serbaguna Padang"
fotoBackground="/img/background.jpg"
fotoPengantinPria="/img/groom.jpg"
fotoPengantinWanita="/img/bride.jpg"
linkMaps="https://maps.google.com"
:gallery="[
'/img/gallery1.jpg',
'/img/gallery2.jpg',
'/img/gallery3.jpg'
]"
rekeningDigital="BCA - 1234567890 a/n Ahmad"
alamatGift="Jl. Merpati No. 45, Padang"
:ucapanDoa="[
{ nama: 'Rizki', pesan: 'Selamat menempuh hidup baru!' },
{ nama: 'Dewi', pesan: 'Semoga menjadi keluarga sakinah mawaddah warahmah' }
]"
/>
</div>
</template>
<script setup>
import WeddingA from '~/components/templates/wedding/WeddingA.vue'
</script>

View File

@ -1,5 +1,6 @@
// https://nuxt.com/docs/api/configuration/nuxt-config // https://nuxt.com/docs/api/configuration/nuxt-config
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite"; // <-- Impor ini lagi
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2025-07-15', compatibilityDate: '2025-07-15',
devtools: { enabled: true }, devtools: { enabled: true },
@ -9,16 +10,20 @@ export default defineNuxtConfig({
'@nuxt/icon', '@nuxt/icon',
'@nuxt/image', '@nuxt/image',
'@nuxt/eslint', '@nuxt/eslint',
// Pastikan '@nuxtjs/tailwindcss' TIDAK ADA di sini
], ],
css: [ css: [
'~/assets/css/main.css' '~/assets/css/main.css'
], ],
// Tambahkan kembali blok vite ini
vite: { vite: {
plugins: [ plugins: [
tailwindcss(), tailwindcss(),
], ],
}, },
runtimeConfig: { runtimeConfig: {
public: { public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:8000' apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'

File diff suppressed because it is too large Load Diff

View File

@ -15,11 +15,16 @@
"@nuxt/icon": "^2.0.0", "@nuxt/icon": "^2.0.0",
"@nuxt/image": "^1.11.0", "@nuxt/image": "^1.11.0",
"@tailwindcss/vite": "^4.1.12", "@tailwindcss/vite": "^4.1.12",
"aos": "^2.3.4",
"eslint": "^9.34.0", "eslint": "^9.34.0",
"lucide-vue-next": "^0.542.0", "lucide-vue-next": "^0.542.0",
"nuxt": "^4.0.3", "nuxt": "^4.0.3",
"tailwindcss": "^4.1.12", "tailwindcss": "^4.1.12",
"vue": "^3.5.20", "vue": "^3.5.20",
"vue-router": "^4.5.1" "vue-router": "^4.5.1"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",
"@tailwindcss/postcss": "^4.1.14"
} }
} }

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

@ -0,0 +1,14 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./app/**/*.{vue,js,ts,jsx,tsx}", // scan semua file Nuxt 4 kamu
"./components/**/*.{vue,js,ts,jsx,tsx}", // kalau ada di luar folder app
"./layouts/**/*.{vue,js,ts,jsx,tsx}",
"./pages/**/*.{vue,js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}