[feat] Link undangan frontend
This commit is contained in:
parent
992b3e735a
commit
5f7c976cb4
@ -82,12 +82,13 @@ class PelangganApiController extends Controller
|
|||||||
{
|
{
|
||||||
$pelanggan = Pelanggan::with('template')
|
$pelanggan = Pelanggan::with('template')
|
||||||
->where('invitation_code', $code)
|
->where('invitation_code', $code)
|
||||||
|
->where('status', 'diterima')
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if (!$pelanggan) {
|
if (!$pelanggan) {
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Data pelanggan dengan kode undangan tidak ditemukan.',
|
'message' => 'Data undangan tidak ditemukan.',
|
||||||
], 404);
|
], 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
116
backend-baru/database/factories/PelangganFactory.php
Normal file
116
backend-baru/database/factories/PelangganFactory.php
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Pelanggan;
|
||||||
|
use App\Models\Template;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Pelanggan>
|
||||||
|
*/
|
||||||
|
class PelangganFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Pelanggan::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
// Get a random template
|
||||||
|
$template = Template::inRandomOrder()->first() ?? Template::factory()->create();
|
||||||
|
|
||||||
|
// Generate form data based on template's form fields
|
||||||
|
$formData = $this->generateFormData($template->form['fields'] ?? []);
|
||||||
|
|
||||||
|
// Generate unique invitation code
|
||||||
|
$invitationCode = 'INV-' . strtoupper(Str::random(6));
|
||||||
|
while (Pelanggan::where('invitation_code', $invitationCode)->exists()) {
|
||||||
|
$invitationCode = 'INV-' . strtoupper(Str::random(6));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nama_pemesan' => $this->faker->name(),
|
||||||
|
'email' => $this->faker->unique()->safeEmail(),
|
||||||
|
'no_tlpn' => $this->faker->phoneNumber(),
|
||||||
|
'template_id' => $template->id,
|
||||||
|
'form' => $formData,
|
||||||
|
'harga' => $template->harga,
|
||||||
|
'status' => $this->faker->randomElement(['menunggu', 'diterima', 'ditolak']),
|
||||||
|
'invitation_code' => $invitationCode,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate form data based on template fields
|
||||||
|
*
|
||||||
|
* @param array $fields
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function generateFormData(array $fields): array
|
||||||
|
{
|
||||||
|
$formData = [];
|
||||||
|
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$name = $field['name'];
|
||||||
|
$type = $field['type'] ?? 'text';
|
||||||
|
|
||||||
|
switch ($type) {
|
||||||
|
case 'text':
|
||||||
|
$formData[$name] = $this->generateTextField($name);
|
||||||
|
break;
|
||||||
|
case 'email':
|
||||||
|
$formData[$name] = $this->faker->safeEmail();
|
||||||
|
break;
|
||||||
|
case 'date':
|
||||||
|
$formData[$name] = $this->faker->date('Y-m-d', 'now +1 month');
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
$formData[$name] = $this->faker->numberBetween(1, 100);
|
||||||
|
break;
|
||||||
|
case 'textarea':
|
||||||
|
$formData[$name] = $this->faker->paragraph();
|
||||||
|
break;
|
||||||
|
case 'file':
|
||||||
|
$formData[$name] = 'files/' . $this->faker->uuid() . '.jpg';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$formData[$name] = $this->faker->word();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate text field data based on field name
|
||||||
|
*
|
||||||
|
* @param string $name
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function generateTextField(string $name): string
|
||||||
|
{
|
||||||
|
if (str_contains($name, 'nama_')) {
|
||||||
|
return $this->faker->name();
|
||||||
|
}
|
||||||
|
if (str_contains($name, 'alamat')) {
|
||||||
|
return $this->faker->address();
|
||||||
|
}
|
||||||
|
if (str_contains($name, 'link_gmaps')) {
|
||||||
|
return 'https://maps.google.com/?q=' . $this->faker->latitude() . ',' . $this->faker->longitude();
|
||||||
|
}
|
||||||
|
if (str_contains($name, 'instagram') || str_contains($name, 'facebook') || str_contains($name, 'twitter')) {
|
||||||
|
return 'https://' . str_replace('_', '.', $name) . '/' . $this->faker->userName();
|
||||||
|
}
|
||||||
|
if (str_contains($name, 'waktu')) {
|
||||||
|
return $this->faker->time('H:i');
|
||||||
|
}
|
||||||
|
if (str_contains($name, 'rekening')) {
|
||||||
|
return $this->faker->bankAccountNumber();
|
||||||
|
}
|
||||||
|
if (str_contains($name, 'link_music')) {
|
||||||
|
return 'https://music.example.com/' . $this->faker->uuid();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->faker->word();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,52 +10,52 @@ class PelangganSeeder extends Seeder
|
|||||||
{
|
{
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// contoh beberapa pelanggan
|
Pelanggan::factory()->count(100)->create();
|
||||||
$pelanggans = [
|
// $pelanggans = [
|
||||||
[
|
// [
|
||||||
'nama_pemesan' => 'Arief Dwi Wicaksono',
|
// 'nama_pemesan' => 'Arief Dwi Wicaksono',
|
||||||
'email' => 'arief@example.com',
|
// 'email' => 'arief@example.com',
|
||||||
'no_tlpn' => '081234567890',
|
// 'no_tlpn' => '081234567890',
|
||||||
'template_id' => 1, // pastikan ada template_id valid
|
// 'template_id' => 1, // pastikan ada template_id valid
|
||||||
'form' => json_encode([
|
// 'form' => json_encode([
|
||||||
'nama_pria' => 'Arief',
|
// 'nama_pria' => 'Arief',
|
||||||
'nama_wanita' => 'Nisa',
|
// 'nama_wanita' => 'Nisa',
|
||||||
'alamat' => 'Malang',
|
// 'alamat' => 'Malang',
|
||||||
]),
|
// ]),
|
||||||
'harga' => 150000,
|
// 'harga' => 150000,
|
||||||
'status' => 'menunggu',
|
// 'status' => 'menunggu',
|
||||||
],
|
// ],
|
||||||
[
|
// [
|
||||||
'nama_pemesan' => 'Rizky Ramadhan',
|
// 'nama_pemesan' => 'Rizky Ramadhan',
|
||||||
'email' => 'rizky@example.com',
|
// 'email' => 'rizky@example.com',
|
||||||
'no_tlpn' => '081298765432',
|
// 'no_tlpn' => '081298765432',
|
||||||
'template_id' => 2,
|
// 'template_id' => 2,
|
||||||
'form' => json_encode([
|
// 'form' => json_encode([
|
||||||
'nama_pria' => 'Rizky',
|
// 'nama_pria' => 'Rizky',
|
||||||
'nama_wanita' => 'Dinda',
|
// 'nama_wanita' => 'Dinda',
|
||||||
'alamat' => 'Surabaya',
|
// 'alamat' => 'Surabaya',
|
||||||
]),
|
// ]),
|
||||||
'harga' => 250000,
|
// 'harga' => 250000,
|
||||||
'status' => 'diterima',
|
// 'status' => 'diterima',
|
||||||
],
|
// ],
|
||||||
[
|
// [
|
||||||
'nama_pemesan' => 'Siti Rahmawati',
|
// 'nama_pemesan' => 'Siti Rahmawati',
|
||||||
'email' => 'siti@example.com',
|
// 'email' => 'siti@example.com',
|
||||||
'no_tlpn' => '081212341234',
|
// 'no_tlpn' => '081212341234',
|
||||||
'template_id' => 3,
|
// 'template_id' => 3,
|
||||||
'form' => json_encode([
|
// 'form' => json_encode([
|
||||||
'nama_pria' => 'Andi',
|
// 'nama_pria' => 'Andi',
|
||||||
'nama_wanita' => 'Siti',
|
// 'nama_wanita' => 'Siti',
|
||||||
'alamat' => 'Jakarta',
|
// 'alamat' => 'Jakarta',
|
||||||
]),
|
// ]),
|
||||||
'harga' => 300000,
|
// 'harga' => 300000,
|
||||||
'status' => 'menunggu',
|
// 'status' => 'menunggu',
|
||||||
],
|
// ],
|
||||||
];
|
// ];
|
||||||
|
|
||||||
foreach ($pelanggans as $data) {
|
// foreach ($pelanggans as $data) {
|
||||||
$data['invitation_code'] = 'INV-' . strtoupper(Str::random(6)); // 🟢 generate code unik
|
// $data['invitation_code'] = 'INV-' . strtoupper(Str::random(6)); // 🟢 generate code unik
|
||||||
Pelanggan::create($data);
|
// Pelanggan::create($data);
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,36 +1,110 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-100">
|
<div class="min-h-screen bg-gradient-to-br from-rose-50 via-pink-50 to-purple-50 flex items-center justify-center p-4">
|
||||||
<div class="relative max-w-md w-full bg-white rounded-lg shadow-lg overflow-hidden">
|
<!-- Main Card -->
|
||||||
<!-- Background Image -->
|
<div class="max-w-lg w-full">
|
||||||
<div
|
<!-- Floating Animation Container -->
|
||||||
class="absolute inset-0 bg-cover bg-center opacity-20"
|
<div class="animate-float">
|
||||||
:style="{ backgroundImage: `url(${imageUrl})` }"
|
<div class="relative bg-white rounded-3xl shadow-2xl overflow-hidden">
|
||||||
></div>
|
<!-- Decorative Top Wave -->
|
||||||
|
<div class="absolute top-0 left-0 right-0 h-32 bg-gradient-to-r from-rose-400 to-pink-400 opacity-10">
|
||||||
<!-- Content -->
|
<svg class="absolute bottom-0" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
|
||||||
<div class="relative z-10 p-8 text-center">
|
<path fill="#ffffff" fill-opacity="1" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,112C672,96,768,96,864,112C960,128,1056,160,1152,160C1248,160,1344,128,1392,112L1440,96L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"></path>
|
||||||
<h1 class="text-3xl font-serif font-bold text-gray-800 mb-4">
|
</svg>
|
||||||
{{ data.nama_pengantin || 'Undangan Pernikahan' }}
|
</div>
|
||||||
</h1>
|
|
||||||
|
<!-- Header Image Section -->
|
||||||
<div class="mb-6">
|
<div class="relative h-64 overflow-hidden">
|
||||||
<h2 class="text-xl font-semibold text-gray-700">Tanggal Acara</h2>
|
<div class="absolute inset-0 bg-gradient-to-b from-transparent to-white z-10"></div>
|
||||||
<p class="text-gray-600">
|
<img
|
||||||
{{ formatDate(data.tanggal_acara) || 'Tanggal belum ditentukan' }}
|
:src="imageUrl"
|
||||||
</p>
|
alt="Wedding Template"
|
||||||
</div>
|
class="w-full h-full object-cover transform hover:scale-110 transition-transform duration-700"
|
||||||
|
/>
|
||||||
<div class="mb-6">
|
<!-- Ornamental Corner -->
|
||||||
<h2 class="text-xl font-semibold text-gray-700">Lokasi</h2>
|
<div class="absolute top-4 left-4 w-16 h-16 border-l-4 border-t-4 border-rose-300 rounded-tl-2xl"></div>
|
||||||
<p class="text-gray-600">{{ data.lokasi || 'Lokasi belum ditentukan' }}</p>
|
<div class="absolute top-4 right-4 w-16 h-16 border-r-4 border-t-4 border-rose-300 rounded-tr-2xl"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-8">
|
<!-- Content Section -->
|
||||||
<p class="text-sm text-gray-500 italic">
|
<div class="relative px-8 py-10 space-y-8">
|
||||||
{{ data.template.nama_template }} - {{ data.template.paket.toUpperCase() }}
|
<!-- Divider Line -->
|
||||||
</p>
|
<div class="flex items-center justify-center mb-8">
|
||||||
|
<div class="h-px w-12 bg-gradient-to-r from-transparent to-rose-300"></div>
|
||||||
|
<div class="mx-4">
|
||||||
|
<svg class="w-8 h-8 text-rose-400" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fill-rule="evenodd" d="M3.172 5.172a4 4 0 015.656 0L10 6.343l1.172-1.171a4 4 0 115.656 5.656L10 17.657l-6.828-6.829a4 4 0 010-5.656z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="h-px w-12 bg-gradient-to-l from-transparent to-rose-300"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Names -->
|
||||||
|
<div class="text-center space-y-2">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-serif font-bold bg-gradient-to-r from-rose-600 to-pink-600 bg-clip-text text-transparent animate-fade-in">
|
||||||
|
{{ formData.nama_pengantin || 'Undangan Pernikahan' }}
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 text-sm tracking-widest uppercase">The Wedding Of</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Section -->
|
||||||
|
<div class="bg-gradient-to-r from-rose-50 to-pink-50 rounded-2xl p-6 shadow-inner transform hover:scale-105 transition-transform duration-300">
|
||||||
|
<div class="flex items-center justify-center space-x-3 mb-2">
|
||||||
|
<svg class="w-6 h-6 text-rose-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
|
||||||
|
</svg>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700">Save The Date</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-2xl font-serif text-gray-800">
|
||||||
|
{{ formatDate(formData.tanggal_acara) || 'Tanggal belum ditentukan' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Location Section -->
|
||||||
|
<div class="bg-gradient-to-r from-purple-50 to-pink-50 rounded-2xl p-6 shadow-inner transform hover:scale-105 transition-transform duration-300">
|
||||||
|
<div class="flex items-center justify-center space-x-3 mb-2">
|
||||||
|
<svg class="w-6 h-6 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||||
|
</svg>
|
||||||
|
<h2 class="text-lg font-semibold text-gray-700">Lokasi Acara</h2>
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-xl text-gray-800 font-medium">
|
||||||
|
{{ formData.lokasi || 'Lokasi belum ditentukan' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decorative Quote -->
|
||||||
|
<div class="text-center py-4">
|
||||||
|
<p class="text-sm text-gray-400 italic font-serif">
|
||||||
|
"Cinta adalah persahabatan yang telah terbakar"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer Badge -->
|
||||||
|
<div class="flex items-center justify-center pt-4">
|
||||||
|
<div class="bg-gradient-to-r from-rose-100 to-pink-100 px-6 py-2 rounded-full">
|
||||||
|
<p class="text-xs text-gray-600 font-medium">
|
||||||
|
{{ data.template?.nama_template }} •
|
||||||
|
<span class="uppercase">{{ data.template?.paket }}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decorative Bottom Corners -->
|
||||||
|
<div class="absolute bottom-4 left-4 w-16 h-16 border-l-4 border-b-4 border-rose-300 rounded-bl-2xl"></div>
|
||||||
|
<div class="absolute bottom-4 right-4 w-16 h-16 border-r-4 border-b-4 border-rose-300 rounded-br-2xl"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Floating Hearts Animation -->
|
||||||
|
<div class="fixed inset-0 pointer-events-none overflow-hidden -z-10">
|
||||||
|
<div class="heart-float" style="left: 10%; animation-delay: 0s;">❤️</div>
|
||||||
|
<div class="heart-float" style="left: 30%; animation-delay: 2s;">💕</div>
|
||||||
|
<div class="heart-float" style="left: 50%; animation-delay: 4s;">💖</div>
|
||||||
|
<div class="heart-float" style="left: 70%; animation-delay: 1s;">💗</div>
|
||||||
|
<div class="heart-float" style="left: 90%; animation-delay: 3s;">💝</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -43,27 +117,93 @@ const props = defineProps({
|
|||||||
data: {
|
data: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
validator: (data) => 'template' in data && 'slug' in data.template,
|
validator: (data) => {
|
||||||
|
return data && typeof data === 'object' && 'template' in data
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const backendUrl = config.public.apiBaseUrl
|
const backendUrl = config.public.apiBaseUrl
|
||||||
|
|
||||||
|
const formData = computed(() => props.data.form || {})
|
||||||
|
|
||||||
const imageUrl = computed(() => {
|
const imageUrl = computed(() => {
|
||||||
return props.data.template.foto
|
const foto = props.data.template?.foto
|
||||||
? `${backendUrl}/storage/${props.data.template.foto}`
|
return foto
|
||||||
: 'https://via.placeholder.com/400x600'
|
? `${backendUrl}/storage/${foto}`
|
||||||
|
: 'https://images.unsplash.com/photo-1519741497674-611481863552?w=800&h=600&fit=crop'
|
||||||
})
|
})
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
const formatDate = (dateString) => {
|
||||||
if (!dateString) return null
|
if (!dateString) return null
|
||||||
const date = new Date(dateString)
|
try {
|
||||||
return date.toLocaleDateString('id-ID', {
|
const date = new Date(dateString)
|
||||||
weekday: 'long',
|
if (isNaN(date.getTime())) return null
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
return date.toLocaleDateString('id-ID', {
|
||||||
day: 'numeric',
|
weekday: 'long',
|
||||||
})
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error formatting date:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes float {
|
||||||
|
0%, 100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes heart-float {
|
||||||
|
0% {
|
||||||
|
transform: translateY(100vh) rotate(0deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
90% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(-100vh) rotate(360deg);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-float {
|
||||||
|
animation: float 6s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fade-in 1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heart-float {
|
||||||
|
position: absolute;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
animation: heart-float 15s linear infinite;
|
||||||
|
bottom: -50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,19 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen flex items-center justify-center bg-gray-100">
|
<div class="min-h-screen flex items-center justify-center bg-gray-100">
|
||||||
|
<!-- Loading State -->
|
||||||
<div v-if="pending" class="text-center">
|
<div v-if="pending" class="text-center">
|
||||||
<div class="animate-spin rounded-full h-12 w-12 border-t-4 border-blue-500 mx-auto"></div>
|
<div class="animate-spin rounded-full h-12 w-12 border-t-4 border-blue-500 mx-auto"></div>
|
||||||
<p class="mt-4 text-gray-600">Loading invitation...</p>
|
<p class="mt-4 text-gray-600">Loading invitation...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="error" class="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
|
<!-- Error State -->
|
||||||
<p class="text-red-600">{{ error }}</p>
|
<div v-else-if="error || !data" class="max-w-md w-full bg-white rounded-lg shadow-lg p-8 text-center">
|
||||||
|
<svg class="w-12 h-12 mx-auto text-red-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||||
|
</svg>
|
||||||
|
<p class="text-lg font-semibold text-gray-800 mb-2">Undangan Tidak Ditemukan</p>
|
||||||
|
<p class="text-gray-600 mb-6">Maaf, undangan yang Anda cari tidak tersedia atau sudah tidak berlaku.</p>
|
||||||
|
<NuxtLink to="/" class="inline-block bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 transition-colors">
|
||||||
|
Kembali ke Beranda
|
||||||
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else>
|
<!-- Data Loaded Successfully -->
|
||||||
<component v-if="dynamicComponent" :is="dynamicComponent" :data="data" />
|
<div v-else-if="data && data.template">
|
||||||
|
<!-- Dynamic Component for Known Slugs -->
|
||||||
|
<component v-if="dynamicComponent" :is="dynamicComponent" :data="data" />
|
||||||
|
|
||||||
<div v-else class="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
|
<!-- Fallback for Unknown Slugs -->
|
||||||
<h1 class="text-2xl font-bold text-gray-800 mb-4">Invitation Data</h1>
|
<div v-else class="w-full bg-white rounded-lg shadow-lg p-8">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-800 mb-1">Invitation Data</h1>
|
||||||
|
<div class="text-sm mb-4 text-gray-500">Tampilan ini muncul saat komponen template belum tersedia</div>
|
||||||
<div v-for="(value, key) in data" :key="key" class="mb-4">
|
<div v-for="(value, key) in data" :key="key" class="mb-4">
|
||||||
<h2 class="text-lg font-semibold text-gray-700 capitalize">{{ key.replace('_', ' ') }}</h2>
|
<h2 class="text-lg font-semibold text-gray-700 capitalize">{{ key.replace('_', ' ') }}</h2>
|
||||||
<p v-if="typeof value !== 'object'" class="text-gray-600">{{ value }}</p>
|
<p v-if="typeof value !== 'object'" class="text-gray-600">{{ value }}</p>
|
||||||
@ -25,40 +38,82 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { defineAsyncComponent, computed } from 'vue'
|
||||||
|
import { useRoute, useRuntimeConfig, useAsyncData, createError } from '#app'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const backendUrl = config.public.apiBaseUrl
|
const backendUrl = config.public.apiBaseUrl
|
||||||
|
|
||||||
const error = ref(null)
|
const { data, pending, error } = await useAsyncData(
|
||||||
|
'invitation',
|
||||||
|
async () => {
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`${backendUrl}/api/pelanggans/code/${route.params.code}`)
|
||||||
|
|
||||||
|
// Check if the API response indicates failure
|
||||||
|
if (!response.success) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 404,
|
||||||
|
message: response.message || 'Undangan tidak ditemukan',
|
||||||
|
fatal: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const { data, pending } = await useAsyncData('invitation', async () => {
|
// Validate data structure
|
||||||
try {
|
if (!response.data || !response.data.template) {
|
||||||
const response = await $fetch(`${backendUrl}/api/pelanggans/code/${route.params.code}`)
|
throw createError({
|
||||||
if (!response.data || !response.data.template || !response.data.template.slug) {
|
statusCode: 404,
|
||||||
throw new Error('Invalid API response structure')
|
message: 'Data undangan tidak valid',
|
||||||
|
fatal: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.data
|
||||||
|
} catch (err) {
|
||||||
|
// Handle network errors or other exceptions
|
||||||
|
if (err.statusCode) {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: err.statusCode || 500,
|
||||||
|
message: 'Undangan tidak ditemukan',
|
||||||
|
fatal: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return response.data
|
},
|
||||||
} catch (err) {
|
{
|
||||||
error.value = err.message || 'Failed to load invitation data'
|
// Prevent automatic error propagation
|
||||||
return null
|
lazy: false,
|
||||||
|
server: true,
|
||||||
|
// Transform function to ensure consistent data structure
|
||||||
|
transform: (data) => data
|
||||||
}
|
}
|
||||||
})
|
)
|
||||||
|
|
||||||
const componentMap = {
|
const componentMap = {
|
||||||
minimalis: defineAsyncComponent(() => import('~/components/undangan/undangan-minimalis.vue')),
|
'undangan-minimalis': defineAsyncComponent(() => import('~/components/undangan/undangan-minimalis.vue')),
|
||||||
/// impory
|
// Add more mappings as templates are developed
|
||||||
}
|
}
|
||||||
|
|
||||||
const dynamicComponent = computed(() => {
|
const dynamicComponent = computed(() => {
|
||||||
return data.value?.template?.slug ? componentMap[data.value.template.slug] || null : null
|
if (!data.value?.template?.slug) return null
|
||||||
|
return componentMap[data.value.template.slug] || null
|
||||||
})
|
})
|
||||||
|
|
||||||
useHead({
|
// Set meta tags only if data exists
|
||||||
title: data.value?.nama_pelanggan || 'Undangan Pernikahan',
|
useHead(() => ({
|
||||||
|
title: data.value?.nama_pemesan || 'Undangan Tak Bernama',
|
||||||
meta: [
|
meta: [
|
||||||
{ name: 'description', content: `Undangan pernikahan untuk ${data.value?.nama_pengantin || 'tamu undangan'}` },
|
{
|
||||||
|
name: 'description',
|
||||||
|
content: data.value
|
||||||
|
? `${data.value.nama_pemesan} mengundang Anda untuk menghadiri acara berikut!`
|
||||||
|
: 'Undangan Digital'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
}))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user