form pernikahan
This commit is contained in:
parent
54c237d211
commit
ce4e10f3ac
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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']);
|
||||||
|
|||||||
@ -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">
|
|
||||||
×
|
|
||||||
</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>
|
|
||||||
@ -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">
|
|
||||||
×
|
|
||||||
</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>
|
|
||||||
@ -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">
|
|
||||||
×
|
|
||||||
</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">
|
||||||
|
×
|
||||||
|
</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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
67
proyek-frontend/app/components/shared/CountdownTimer.vue
Normal file
67
proyek-frontend/app/components/shared/CountdownTimer.vue
Normal 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>
|
||||||
58
proyek-frontend/app/components/shared/Gallery.vue
Normal file
58
proyek-frontend/app/components/shared/Gallery.vue
Normal 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>
|
||||||
70
proyek-frontend/app/components/shared/GuestBook.vue
Normal file
70
proyek-frontend/app/components/shared/GuestBook.vue
Normal 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>
|
||||||
9
proyek-frontend/app/components/shared/Maps.vue
Normal file
9
proyek-frontend/app/components/shared/Maps.vue
Normal 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>
|
||||||
13
proyek-frontend/app/components/shared/MusicPlayer.vue
Normal file
13
proyek-frontend/app/components/shared/MusicPlayer.vue
Normal 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>
|
||||||
130
proyek-frontend/app/components/shared/RSVP.vue
Normal file
130
proyek-frontend/app/components/shared/RSVP.vue
Normal 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>
|
||||||
@ -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) => {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
27
proyek-frontend/app/components/templates/Ultah/Event.vue
Normal file
27
proyek-frontend/app/components/templates/Ultah/Event.vue
Normal 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>
|
||||||
15
proyek-frontend/app/components/templates/Ultah/Gallery.vue
Normal file
15
proyek-frontend/app/components/templates/Ultah/Gallery.vue
Normal 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>
|
||||||
70
proyek-frontend/app/components/templates/Ultah/GuestBook.vue
Normal file
70
proyek-frontend/app/components/templates/Ultah/GuestBook.vue
Normal 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>
|
||||||
@ -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>
|
||||||
46
proyek-frontend/app/components/templates/Ultah/Landing.vue
Normal file
46
proyek-frontend/app/components/templates/Ultah/Landing.vue
Normal 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>
|
||||||
20
proyek-frontend/app/components/templates/Ultah/ThankYou.vue
Normal file
20
proyek-frontend/app/components/templates/Ultah/ThankYou.vue
Normal 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>
|
||||||
769
proyek-frontend/app/components/templates/Ultah/UltahA.vue
Normal file
769
proyek-frontend/app/components/templates/Ultah/UltahA.vue
Normal 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>
|
||||||
247
proyek-frontend/app/components/templates/khitan/Event.vue
Normal file
247
proyek-frontend/app/components/templates/khitan/Event.vue
Normal 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>
|
||||||
351
proyek-frontend/app/components/templates/khitan/Gallery.vue
Normal file
351
proyek-frontend/app/components/templates/khitan/Gallery.vue
Normal 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>
|
||||||
450
proyek-frontend/app/components/templates/khitan/GuestBook.vue
Normal file
450
proyek-frontend/app/components/templates/khitan/GuestBook.vue
Normal 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>
|
||||||
@ -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>
|
||||||
248
proyek-frontend/app/components/templates/khitan/KhitanA.vue
Normal file
248
proyek-frontend/app/components/templates/khitan/KhitanA.vue
Normal 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>
|
||||||
105
proyek-frontend/app/components/templates/khitan/ThankYou.vue
Normal file
105
proyek-frontend/app/components/templates/khitan/ThankYou.vue
Normal 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>
|
||||||
630
proyek-frontend/app/components/templates/wedding/WeddingA.vue
Normal file
630
proyek-frontend/app/components/templates/wedding/WeddingA.vue
Normal 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>
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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">×</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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
×
|
||||||
|
</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>
|
||||||
|
|||||||
21
proyek-frontend/app/pages/preview/[id].vue
Normal file
21
proyek-frontend/app/pages/preview/[id].vue
Normal 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>
|
||||||
89
proyek-frontend/app/pages/preview/khitan-a.vue
Normal file
89
proyek-frontend/app/pages/preview/khitan-a.vue
Normal 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>
|
||||||
79
proyek-frontend/app/pages/preview/ultah-a.vue
Normal file
79
proyek-frontend/app/pages/preview/ultah-a.vue
Normal 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>
|
||||||
35
proyek-frontend/app/pages/preview/wedding-a.vue
Normal file
35
proyek-frontend/app/pages/preview/wedding-a.vue
Normal 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>
|
||||||
@ -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'
|
||||||
|
|||||||
1917
proyek-frontend/package-lock.json
generated
1917
proyek-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
proyek-frontend/public/music/wedding-song.mp3
Normal file
BIN
proyek-frontend/public/music/wedding-song.mp3
Normal file
Binary file not shown.
BIN
proyek-frontend/public/pria.jpg
Normal file
BIN
proyek-frontend/public/pria.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
BIN
proyek-frontend/public/wanita.jpg
Normal file
BIN
proyek-frontend/public/wanita.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
BIN
proyek-frontend/public/wedding1.png
Normal file
BIN
proyek-frontend/public/wedding1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.5 MiB |
14
proyek-frontend/tailwind.config.js
Normal file
14
proyek-frontend/tailwind.config.js
Normal 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: [],
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user