Compare commits

..

11 Commits

Author SHA1 Message Date
d078aab4b0 new logo 2025-09-03 13:52:24 +07:00
213a3874d0 image 2025-09-03 11:02:31 +07:00
e20f1fa12f fix testi 2025-09-03 10:48:20 +07:00
ef91b38fa5 box shadow 2025-09-01 16:29:59 +07:00
Farhaan4
ddea7e321f fix 2025-09-01 15:23:46 +07:00
ddae4df823 fix 2025-09-01 12:31:25 +07:00
ff19646ba7 Styling 2025-09-01 11:58:46 +07:00
49ac69876c Cara membuat undangan 2025-09-01 11:54:24 +07:00
c11fdaf58f navlink 2025-09-01 10:22:10 +07:00
b28ee832d5 Update aboutsection.vue 2025-09-01 10:05:05 +07:00
720b968c6b Tentang Kami 2025-09-01 09:41:06 +07:00
77 changed files with 379 additions and 6897 deletions

View File

@ -1,41 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AdminAuthController extends Controller
{
public function showLogin()
{
return view('admin.auth.login');
}
public function login(Request $request)
{
$credentials = $request->validate([
'email' => ['required','email'],
'password' => ['required'],
]);
$remember = $request->boolean('remember');
if (Auth::guard('admin')->attempt($credentials, $remember)) {
$request->session()->regenerate();
return redirect()->intended(route('admin.dashboard'));
}
return back()->withErrors([
'email' => 'Email atau password salah.',
])->onlyInput('email');
}
public function logout(Request $request)
{
Auth::guard('admin')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('admin.login');
}
}

View File

@ -1,220 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Pelanggan;
use App\Models\PelangganDetail;
use App\Models\Template;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
class FormApiController extends Controller
{
/**
* Convert string to slug-like field name
*/
private function slugify($text)
{
return strtolower(
preg_replace('/[^A-Za-z0-9_]/', '_', trim($text))
);
}
public function store(Request $request)
{
try {
// Log incoming request for debugging
Log::info('Form submission received', [
'template_id' => $request->template_id,
'files' => array_keys($request->allFiles()),
'data_keys' => array_keys($request->except(['_token']))
]);
// ✅ Validasi dasar
$rules = [
'template_id' => 'required|exists:templates,id',
'nama_pemesan' => 'required|string|max:255',
'no_hp' => 'required|string|max:20',
'email' => 'required|email',
];
// ✅ Ambil template + fiturnya
$template = Template::with(['fiturs', 'kategori'])->findOrFail($request->template_id);
$galleryFields = []; // Track gallery field names
// ✅ Loop fitur → buat validasi dinamis
foreach ($template->fiturs as $fitur) {
$field = $this->slugify($fitur->deskripsi);
// default text input
$rules[$field] = 'nullable|string|max:255';
// tanggal
if (str_contains(strtolower($fitur->deskripsi), 'tanggal')) {
$rules[$field] = 'nullable|date';
}
// galeri (cek jumlah: Galeri 2, Galeri 5, dll.)
if (str_contains(strtolower($fitur->deskripsi), 'galeri') ||
str_contains(strtolower($fitur->deskripsi), 'gallery')) {
preg_match('/(\d+)/', $fitur->deskripsi, $matches);
$maxFiles = isset($matches[1]) ? (int) $matches[1] : 10;
// Add gallery field to tracking
$galleryFields[] = $field;
// Validation for gallery array
$rules[$field] = "nullable|array|max:$maxFiles";
$rules[$field . '.*'] = 'file|image|mimes:jpeg,png,jpg,gif,webp|max:10240'; // 10MB
}
}
Log::info('Validation rules generated', [
'rules' => $rules,
'gallery_fields' => $galleryFields
]);
// ✅ Jalankan validasi
$validatedData = $request->validate($rules);
// ✅ Process all gallery uploads
$allGalleryPaths = [];
foreach ($galleryFields as $galleryField) {
if ($request->hasFile($galleryField)) {
$galleryPaths = [];
$files = $request->file($galleryField);
Log::info("Processing files for field: $galleryField", [
'file_count' => is_array($files) ? count($files) : 1
]);
// Handle both single file and array of files
if (!is_array($files)) {
$files = [$files];
}
foreach ($files as $file) {
try {
$path = $file->store('gallery', 'public');
$galleryPaths[] = $path;
Log::info("File uploaded successfully", [
'original_name' => $file->getClientOriginalName(),
'path' => $path
]);
} catch (\Exception $e) {
Log::error("File upload failed", [
'file' => $file->getClientOriginalName(),
'error' => $e->getMessage()
]);
throw $e;
}
}
if (!empty($galleryPaths)) {
$allGalleryPaths[$galleryField] = $galleryPaths;
$validatedData[$galleryField] = $galleryPaths;
}
}
}
Log::info('All gallery uploads processed', [
'gallery_data' => $allGalleryPaths
]);
// ✅ Simpan ke tabel pelanggan
$pelanggan = Pelanggan::create([
'nama_pemesan' => $validatedData['nama_pemesan'],
'nama_template' => $template->nama_template,
'kategori' => $template->kategori->nama ?? '-',
'email' => $validatedData['email'],
'no_tlpn' => $validatedData['no_hp'],
'harga' => $template->harga,
'catatan' => $validatedData['catatan'] ?? null,
]);
// ✅ Simpan detail form (dinamis) - include gallery paths
PelangganDetail::create([
'pelanggan_id' => $pelanggan->id,
'detail_form' => $validatedData,
]);
Log::info('Form submitted successfully', [
'pelanggan_id' => $pelanggan->id,
'template_id' => $template->id
]);
return response()->json([
'success' => true,
'message' => 'Form berhasil dikirim sesuai fitur template',
'data' => $pelanggan->load('details'),
'gallery_info' => $allGalleryPaths
], 201);
} catch (\Illuminate\Validation\ValidationException $e) {
Log::error('Validation failed', [
'errors' => $e->errors(),
'input' => $request->except(['password', '_token'])
]);
return response()->json([
'success' => false,
'message' => 'Validasi gagal',
'errors' => $e->errors()
], 422);
} catch (\Exception $e) {
Log::error('Form submission failed', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
'input' => $request->except(['password', '_token'])
]);
return response()->json([
'success' => false,
'message' => 'Terjadi kesalahan internal server',
'error' => config('app.debug') ? $e->getMessage() : 'Internal server error'
], 500);
}
}
public function getFiturs($id)
{
try {
$template = Template::with(['fiturs', 'kategori'])->findOrFail($id);
return response()->json([
'success' => true,
'template' => [
'id' => $template->id,
'nama_template' => $template->nama_template,
'kategori' => $template->kategori->nama ?? '-',
'harga' => $template->harga,
'thumbnail' => $template->thumbnail ?? null,
],
'fiturs' => $template->fiturs->map(function ($fitur) {
return [
'id' => $fitur->id,
'deskripsi' => $fitur->deskripsi,
'harga' => $fitur->harga,
];
}),
]);
} catch (\Exception $e) {
Log::error('Failed to get template fiturs', [
'template_id' => $id,
'error' => $e->getMessage()
]);
return response()->json([
'success' => false,
'message' => 'Template tidak ditemukan',
'error' => config('app.debug') ? $e->getMessage() : 'Template not found'
], 404);
}
}
}

View File

@ -1,42 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Kategori;
class KategoriApiController extends Controller
{
// Ambil semua kategori
public function index()
{
// 1. Ambil semua kategori dari database
$kategoris = Kategori::all();
// 2. Ubah koleksi data untuk membuat URL foto yang benar
$transformedKategoris = $kategoris->map(function($kategori) {
return [
'id' => $kategori->id,
'nama' => $kategori->nama,
'deskripsi' => $kategori->deskripsi,
// Gunakan helper asset() untuk membuat URL lengkap
'foto' => $kategori->foto ? asset('storage/' . $kategori->foto) : null,
];
});
// 3. Kirim data yang sudah diubah sebagai JSON
return response()->json($transformedKategoris);
}
// Ambil detail satu kategori
public function show(Kategori $kategori)
{
// Sebaiknya detail juga diubah agar konsisten
return response()->json([
'id' => $kategori->id,
'nama' => $kategori->nama,
'deskripsi' => $kategori->deskripsi,
'foto' => asset('storage/' . $kategori->foto),
]);
}
}

View File

@ -1,80 +0,0 @@
<?php
// namespace App\Http\Controllers\Api;
// use App\Http\Controllers\Controller;
// use App\Models\Pelanggan;
// use App\Models\PelangganDetail;
// use App\Models\Template; // ✅ tambahkan ini
// use Illuminate\Http\Request;
// use Illuminate\Support\Arr;
// class KhitanApiController extends Controller
// {
// public function store(Request $request)
// {
// $data = $request->validate([
// 'template_id' => 'required|exists:templates,id',
// 'nama_pemesan' => 'required|string|max:255',
// 'no_hp' => 'required|string|max:20',
// 'email' => 'required|email',
// // Anak
// 'nama_lengkap_anak' => 'required|string|max:255',
// 'nama_panggilan_anak' => 'required|string|max:255',
// 'bapak_anak' => 'nullable|string|max:255',
// 'ibu_anak' => 'nullable|string|max:255',
// // Jadwal
// 'hari_tanggal_acara' => 'nullable|date',
// 'waktu_acara' => 'nullable|string',
// 'alamat_acara' => 'nullable|string',
// 'maps_acara' => 'nullable|string',
// // Tambahan
// 'no_rekening1' => 'nullable|string',
// 'no_rekening2' => 'nullable|string',
// 'link_musik' => 'nullable|string',
// 'galeri' => 'nullable|array|max:5',
// 'galeri.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
// ]);
// // --- PROSES UPLOAD GAMBAR ---
// $galleryPaths = [];
// if ($request->hasFile('galeri')) {
// foreach ($request->file('galeri') as $file) {
// // Simpan file ke storage/app/public/gallery dan dapatkan path-nya
// $path = $file->store('gallery', 'public');
// $galleryPaths[] = $path;
// }
// }
// // Tambahkan path gambar ke dalam data yang akan disimpan
// $data['galeri'] = $galleryPaths;
// // ✅ Ambil template dari database
// $template = Template::with('kategori')->findOrFail($data['template_id']);
// // ✅ Simpan ke tabel pelanggan
// $pelanggan = Pelanggan::create([
// 'nama_pemesan' => $data['nama_pemesan'],
// 'nama_template' => $template->nama_template,
// 'kategori' => $template->kategori->nama ?? 'khitan',
// 'email' => $data['email'],
// 'no_tlpn' => $data['no_hp'],
// 'harga' => $template->harga,
// ]);
// // ✅ Simpan detail form ke tabel pelanggan_details
// PelangganDetail::create([
// 'pelanggan_id' => $pelanggan->id,
// 'detail_form' => $data,
// ]);
// return response()->json([
// 'success' => true,
// 'message' => 'Form khitan berhasil dikirim',
// 'data' => $pelanggan->load('details')
// ], 201);
// }
// }

View File

@ -1,101 +0,0 @@
<?php
// namespace App\Http\Controllers\Api;
// use App\Http\Controllers\Controller;
// use App\Models\Pelanggan;
// use App\Models\PelangganDetail;
// use App\Models\Template; // ✅ tambahkan ini
// use Illuminate\Http\Request;
// use Illuminate\Support\Arr;
// class PernikahanApiController extends Controller
// {
// public function store(Request $request)
// {
// $data = $request->validate([
// 'template_id' => 'required|exists:templates,id',
// 'nama_pemesan' => 'required|string|max:255',
// 'no_hp' => 'required|string|max:20',
// 'email' => 'required|email',
// // Pria
// 'nama_lengkap_pria' => 'required|string|max:255',
// 'nama_panggilan_pria' => 'required|string|max:255',
// 'bapak_pria' => 'nullable|string|max:255',
// 'ibu_pria' => 'nullable|string|max:255',
// 'instagram_pria' => 'nullable|string',
// 'facebook_pria' => 'nullable|string',
// 'twitter_pria' => 'nullable|string',
// // Wanita
// 'nama_lengkap_wanita' => 'required|string|max:255',
// 'nama_panggilan_wanita' => 'required|string|max:255',
// 'bapak_wanita' => 'nullable|string|max:255',
// 'ibu_wanita' => 'nullable|string|max:255',
// 'instagram_wanita' => 'nullable|string',
// 'facebook_wanita' => 'nullable|string',
// 'twitter_wanita' => 'nullable|string',
// // Cerita
// 'cerita_kita' => 'nullable|string',
// // Akad
// 'hari_tanggal_akad' => 'nullable|date',
// 'waktu_akad' => 'nullable|string',
// 'alamat_akad' => 'nullable|string',
// 'maps_akad' => 'nullable|string',
// // Resepsi
// 'hari_tanggal_resepsi' => 'nullable|date',
// 'waktu_resepsi' => 'nullable|string',
// 'alamat_resepsi' => 'nullable|string',
// 'maps_resepsi' => 'nullable|string',
// // Tambahan
// 'no_rekening1' => 'nullable|string',
// 'no_rekening2' => 'nullable|string',
// 'link_musik' => 'nullable|string',
// 'galeri' => 'nullable|array|max:10',
// 'galeri.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
// ]);
// // --- PROSES UPLOAD GAMBAR ---
// $galleryPaths = [];
// if ($request->hasFile('galeri')) {
// foreach ($request->file('galeri') as $file) {
// // Simpan file ke storage/app/public/gallery dan dapatkan path-nya
// $path = $file->store('gallery', 'public');
// $galleryPaths[] = $path;
// }
// }
// // Ganti 'galeri' di $data dengan array path yang sudah disimpan
// $data['galeri'] = $galleryPaths;
// // ✅ Ambil template berdasarkan template_id
// $template = Template::with('kategori')->findOrFail($data['template_id']);
// // ✅ Simpan ke tabel pelanggan
// $pelanggan = Pelanggan::create([
// 'nama_pemesan' => $data['nama_pemesan'],
// 'nama_template' => $template->nama_template,
// 'kategori' => $template->kategori->nama ?? '-',
// 'email' => $data['email'],
// 'no_tlpn' => $data['no_hp'],
// 'harga' => $template->harga,
// ]);
// // ✅ Simpan detail form ke tabel pelanggan_details
// PelangganDetail::create([
// 'pelanggan_id' => $pelanggan->id,
// 'detail_form' => $data,
// ]);
// return response()->json([
// 'success' => true,
// 'message' => 'Form pernikahan berhasil dikirim',
// 'data' => $pelanggan->load('details')
// ], 201);
// }
// }

View File

@ -1,56 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\Review;
class ReviewController extends Controller
{
// Ambil semua ulasan
public function index()
{
$reviews = Review::all();
return response()->json($reviews, 200);
}
// Simpan ulasan baru
public function store(Request $request)
{
$validated = $request->validate([
'rating' => 'required|integer|min:1|max:5',
'message' => 'required|string',
'name' => 'required|string|max:100',
'city' => 'required|string|max:100',
]);
$review = Review::create($validated);
return response()->json([
'message' => 'Ulasan berhasil disimpan',
'data' => $review
], 201);
}
// Tampilkan ulasan tertentu
public function show($id)
{
$review = Review::findOrFail($id);
return response()->json($review, 200);
}
// Hapus ulasan
public function destroy($id)
{
$review = Review::findOrFail($id);
$review->delete();
return response()->json([
'message' => 'Ulasan berhasil dihapus'
], 200);
}
}

View File

@ -1,90 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Template;
class TemplateApiController extends Controller
{
// User hanya bisa lihat semua template
public function index()
{
$templates = Template::with(['kategori','fiturs'])
->get()
->map(function($t){
return [
'id' => $t->id,
'nama' => $t->nama_template,
'harga' => (float) $t->harga,
'foto' => asset('storage/' . $t->foto),
'kategori' => $t->kategori,
'fiturs' => $t->fiturs,
];
});
return response()->json($templates);
}
// User bisa lihat detail 1 template
public function show(Template $template)
{
// UBAH DI SINI: 'fitur' -> 'fiturs'
return response()->json($template->load(['kategori','fiturs']));
}
public function byCategory($id)
{
// UBAH DI SINI: 'fitur' -> 'fiturs'
$templates = Template::with(['kategori','fiturs'])
->where('kategori_id', (int)$id)
->get()
->map(function($t){
return [
'id' => $t->id,
'nama' => $t->nama_template,
'harga' => (float) $t->harga,
'foto' => asset('storage/' . $t->foto),
'kategori' => $t->kategori,
// UBAH DI SINI JUGA: $t->fitur -> $t->fiturs
'fitur' => $t->fiturs,
];
});
return response()->json($templates);
}
public function random()
{
try {
// Coba tanpa relationship dulu untuk debug
$templates = Template::inRandomOrder()
->take(8)
->get()
->map(function($t){
return [
'id' => $t->id,
'nama' => $t->nama_template,
'harga' => (float) $t->harga,
'foto' => asset('storage/' . $t->foto),
'kategori' => $t->kategori,
'fiturs' => $t->fiturs,
];
});
return response()->json($templates);
} catch (\Exception $e) {
return response()->json([
'error' => $e->getMessage(),
'line' => $e->getLine(),
'file' => $e->getFile()
], 500);
}
}
}

View File

@ -1,75 +0,0 @@
<?php
// namespace App\Http\Controllers\Api;
// use App\Http\Controllers\Controller;
// use App\Models\Pelanggan;
// use App\Models\PelangganDetail;
// use App\Models\Template; // ✅ tambahkan ini
// use Illuminate\Http\Request;
// class UlangTahunApiController extends Controller
// {
// public function store(Request $request)
// {
// $data = $request->validate([
// 'template_id' => 'required|exists:templates,id',
// 'nama_pemesan' => 'required|string|max:255',
// 'no_hp' => 'required|string|max:20',
// 'email' => 'required|email',
// // Data Anak
// 'nama_lengkap_anak' => 'required|string|max:255',
// 'nama_panggilan_anak' => 'required|string|max:100',
// 'bapak_anak' => 'required|string|max:255',
// 'ibu_anak' => 'required|string|max:255',
// 'umur_dirayakan' => 'required|string|max:10',
// 'anak_ke' => 'required|string|max:5',
// // Jadwal
// 'hari_tanggal_acara' => 'required|date',
// 'waktu_acara' => 'required|string|max:50',
// 'alamat_acara' => 'required|string',
// 'maps_acara' => 'nullable|string',
// 'link_musik' => 'nullable|string',
// // --- PERBAIKAN VALIDASI GALERI ---
// 'galeri' => 'nullable|array|max:5',
// 'galeri.*' => 'image|mimes:jpeg,png,jpg,gif|max:2048',
// ]);
// // --- PROSES UPLOAD GAMBAR ---
// $galleryPaths = [];
// if ($request->hasFile('galeri')) {
// foreach ($request->file('galeri') as $file) {
// $path = $file->store('gallery', 'public');
// $galleryPaths[] = $path;
// }
// }
// $data['galeri'] = $galleryPaths;
// // ✅ Ambil template berdasarkan template_id
// $template = Template::with('kategori')->findOrFail($data['template_id']);
// // ✅ Simpan ke tabel pelanggan
// $pelanggan = Pelanggan::create([
// 'nama_pemesan' => $data['nama_pemesan'],
// 'nama_template' => $template->nama_template,
// 'kategori' => $template->kategori->nama ?? 'ulang_tahun',
// 'email' => $data['email'],
// 'no_tlpn' => $data['no_hp'],
// 'harga' => $template->harga,
// ]);
// // ✅ Simpan detail form ke tabel pelanggan_details
// PelangganDetail::create([
// 'pelanggan_id' => $pelanggan->id,
// 'detail_form' => $data,
// ]);
// return response()->json([
// 'success' => true,
// 'message' => 'Form ulang tahun berhasil dikirim',
// 'data' => $pelanggan->load('details')
// ], 201);
// }
// }

View File

@ -1,31 +0,0 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Kategori;
use App\Models\Template;
use App\Models\Pelanggan;
use Carbon\Carbon;
class DashboardController extends Controller
{
public function index()
{
$totalKategori = Kategori::count();
$totalTemplate = Template::count();
$totalPelanggan = Pelanggan::count();
$today = Carbon::now()->translatedFormat('l, d F Y');
// ambil 5 pelanggan terbaru
$recentPelanggan = Pelanggan::latest()->paginate(5);
return view('admin.dashboard', compact(
'totalKategori',
'totalTemplate',
'totalPelanggan',
'today',
'recentPelanggan'
));
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Fitur;
use App\Models\KategoriFitur;
use Illuminate\Http\Request;
class FiturController extends Controller
{
public function index()
{
$fitur = Fitur::with('kategoriFitur')->paginate(5);
$kategoriFiturs = KategoriFitur::all();
return view('admin.fitur.index', compact('fitur', 'kategoriFiturs'));
}
public function store(Request $request)
{
$request->validate([
'deskripsi' => 'required|string|max:255',
'harga' => 'required|numeric|min:0',
'kategori_fitur_id' => 'nullable|exists:kategori_fitur,id',
]);
Fitur::create([
'deskripsi' => $request->deskripsi,
'harga' => $request->harga,
'kategori_fitur_id' => $request->kategori_fitur_id,
]);
return redirect()->route('admin.fitur.index')
->with('success', 'Fitur berhasil ditambahkan');
}
public function update(Request $request, $id)
{
$request->validate([
'deskripsi' => 'required|string|max:255',
'harga' => 'required|numeric|min:0',
'kategori_fitur_id' => 'nullable|exists:kategori_fitur,id',
]);
$fitur = Fitur::findOrFail($id);
$fitur->update([
'deskripsi' => $request->deskripsi,
'harga' => $request->harga,
'kategori_fitur_id' => $request->kategori_fitur_id,
]);
return redirect()->route('admin.fitur.index')
->with('success', 'Fitur berhasil diperbarui');
}
public function destroy($id)
{
$fitur = Fitur::findOrFail($id);
$fitur->delete();
return redirect()->route('admin.fitur.index')
->with('success', 'Fitur berhasil dihapus');
}
}

View File

@ -1,64 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Models\Kategori;
use Illuminate\Http\Request;
class KategoriController extends Controller
{
public function index()
{
$kategori = Kategori::all();
return view('admin.kategori.index', compact('kategori'));
}
public function create()
{
return view('admin.kategori.create');
}
public function store(Request $request)
{
$data = $request->validate([
'nama' => 'required|string|max:255',
'deskripsi' => 'nullable|string',
'foto' => 'nullable|image|mimes:jpg,jpeg,png,gif|max:5120',
]);
if ($request->hasFile('foto')) {
$data['foto'] = $request->file('foto')->store('kategori', 'public');
}
Kategori::create($data);
return redirect()->route('admin.kategori.index')->with('success', 'Kategori berhasil ditambahkan!');
}
public function edit(Kategori $kategori)
{
return view('admin.kategori.edit', compact('kategori'));
}
public function update(Request $request, Kategori $kategori)
{
$data = $request->validate([
'nama' => 'required|string|max:255',
'deskripsi' => 'nullable|string',
'foto' => 'nullable|image|mimes:jpg,jpeg,png,gif|max:5120',
]);
if ($request->hasFile('foto')) {
$data['foto'] = $request->file('foto')->store('kategori', 'public');
}
$kategori->update($data);
return redirect()->route('admin.kategori.index')->with('success', 'Kategori berhasil diperbarui!');
}
public function destroy(Kategori $kategori)
{
$kategori->delete();
return redirect()->route('admin.kategori.index')->with('success', 'Kategori berhasil dihapus!');
}
}

View File

@ -1,54 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\KategoriFitur;
use Illuminate\Http\Request;
class KategoriFiturController extends Controller
{
public function index()
{
$kategoriFitur = KategoriFitur::paginate(10);
return view('admin.kategori_fitur.index', compact('kategoriFitur'));
}
public function create()
{
return view('admin.kategori_fitur.create');
}
public function store(Request $request)
{
$request->validate([
'deskripsi' => 'required|string|max:255',
]);
KategoriFitur::create($request->all());
return redirect()->route('admin.kategori_fitur.index')->with('success', 'Kategori Fitur berhasil ditambahkan');
}
public function edit(KategoriFitur $kategori_fitur)
{
return view('admin.kategori_fitur.edit', compact('kategori_fitur'));
}
public function update(Request $request, KategoriFitur $kategori_fitur)
{
$request->validate([
'deskripsi' => 'required|string|max:255',
]);
$kategori_fitur->update($request->all());
return redirect()->route('admin.kategori_fitur.index')->with('success', 'Kategori Fitur berhasil diperbarui');
}
public function destroy(KategoriFitur $kategori_fitur)
{
$kategori_fitur->delete();
return redirect()->route('admin.kategori_fitur.index')->with('success', 'Kategori Fitur berhasil dihapus');
}
}

View File

@ -1,48 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Pelanggan;
use App\Models\Kategori;
use Illuminate\Http\Request;
class PelangganController extends Controller
{
public function index(Request $request)
{
$query = Pelanggan::query();
// Filter kategori
if ($request->filled('kategori')) {
$query->where('kategori', $request->kategori);
}
// Pencarian
if ($request->filled('search')) {
$search = $request->search;
$query->where(function ($q) use ($search) {
$q->where('nama_pemesan', 'like', "%$search%")
->orWhere('email', 'like', "%$search%")
->orWhere('no_tlpn', 'like', "%$search%");
});
}
// Pakai pagination
$pelanggans = $query->orderBy('created_at', 'desc')->paginate(10);
$kategoris = Kategori::all();
return view('admin.pelanggan.index', compact('pelanggans', 'kategoris'));
}
public function show($id)
{
$pelanggan = Pelanggan::with('details')->findOrFail($id);
return view('admin.pelanggan.show', compact('pelanggan'));
}
public function destroy(Pelanggan $pelanggan)
{
$pelanggan->delete();
return redirect()->route('admin.pelanggan.index')->with('success', 'Pelanggan berhasil dihapus!');
}
}

View File

@ -1,147 +0,0 @@
<?php
namespace App\Http\Controllers;
use App\Models\Template;
use App\Models\Kategori;
use App\Models\Fitur;
use App\Models\KategoriFitur; // <-- tambah ini
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class TemplateController extends Controller
{
public function index()
{
$templates = Template::with(['kategori', 'fiturs'])->paginate(5);
$kategoris = Kategori::all();
// semua fitur dengan relasi kategori
$fiturs = Fitur::with('kategoriFitur')->get();
// kategori fitur beserta fiturnya
$kategoriFiturs = KategoriFitur::with('fiturs')->get();
// fitur yang tidak punya kategori
$fitursTanpaKategori = Fitur::whereNull('kategori_fitur_id')->get();
return view('admin.templates.index', compact(
'templates',
'kategoris',
'fiturs',
'kategoriFiturs',
'fitursTanpaKategori'
));
}
public function show($id)
{
return Template::with('kategori')->findOrFail($id);
}
public function store(Request $request)
{
$data = $request->validate([
'nama_template' => 'required|string|max:255',
'kategori_id' => 'required|exists:kategoris,id',
'fitur_id' => 'required|array',
'fitur_id.*' => 'exists:fiturs,id',
'foto' => 'nullable|image|mimes:jpg,jpeg,png,gif|max:5120',
'harga' => 'required|string', // wajib string karena ada titik
]);
// hitung total harga dari fitur yang dipilih
$totalHarga = Fitur::whereIn('id', $data['fitur_id'])->sum('harga');
if ($request->hasFile('foto')) {
$data['foto'] = $request->file('foto')->store('templates', 'public');
}
// Bersihkan titik ribuan sebelum simpan
$hargaBersih = (int) str_replace('.', '', $request->harga);
// store
$template = Template::create([
'nama_template' => $data['nama_template'],
'kategori_id' => $data['kategori_id'],
'foto' => $data['foto'] ?? null,
'harga' => $hargaBersih,
]);
$template->fiturs()->sync($data['fitur_id']);
return redirect()->route('templates.index')->with('success', 'Template berhasil ditambahkan!');
}
public function update(Request $request, Template $template)
{
$data = $request->validate([
'nama_template' => 'required|string|max:255',
'kategori_id' => 'required|exists:kategoris,id',
'fitur_id' => 'required|array',
'fitur_id.*' => 'exists:fiturs,id',
'foto' => 'nullable|image|mimes:jpg,jpeg,png,gif|max:5120',
'harga' => 'required|string',
]);
// hitung ulang harga fitur
$totalHarga = Fitur::whereIn('id', $data['fitur_id'])->sum('harga');
if ($request->hasFile('foto')) {
if ($template->foto && Storage::disk('public')->exists($template->foto)) {
Storage::disk('public')->delete($template->foto);
}
$data['foto'] = $request->file('foto')->store('templates', 'public');
}
// Bersihkan titik ribuan
$hargaBersih = (int) str_replace('.', '', $request->harga);
// update
$template->update([
'nama_template' => $data['nama_template'],
'kategori_id' => $data['kategori_id'],
'foto' => $data['foto'] ?? $template->foto,
'harga' => $hargaBersih,
]);
$template->fiturs()->sync($data['fitur_id']);
return redirect()->route('templates.index')->with('success', 'Template berhasil diperbarui!');
}
public function destroy(Template $template)
{
if ($template->foto && Storage::disk('public')->exists($template->foto)) {
Storage::disk('public')->delete($template->foto);
}
$template->fiturs()->detach();
$template->delete();
return redirect()->route('templates.index')->with('success', 'Template berhasil dihapus!');
}
public function byKategori($id)
{
$kategori = Kategori::findOrFail($id);
$templates = Template::with(['kategori', 'fiturs'])
->where('kategori_id', $id)
->paginate(5);
$kategoris = Kategori::all();
$fiturs = Fitur::with('kategoriFitur')->get();
$kategoriFiturs = KategoriFitur::with('fiturs')->get();
$fitursTanpaKategori = Fitur::whereNull('kategori_fitur_id')->get(); // <-- tambahkan ini
return view('admin.templates.index', compact(
'templates',
'kategoris',
'fiturs',
'kategori',
'kategoriFiturs',
'fitursTanpaKategori' // <-- kirim ke view
));
}
}

View File

@ -1,15 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class Admin extends Authenticatable
{
use Notifiable;
protected $fillable = ['name','email','password'];
protected $hidden = ['password','remember_token'];
}

View File

@ -1,26 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Fitur extends Model
{
use HasFactory;
protected $table = 'fiturs';
protected $fillable = [
'deskripsi',
'harga',
'kategori_fitur_id',
];
public function kategoriFitur()
{
return $this->belongsTo(KategoriFitur::class, 'kategori_fitur_id');
}
}

View File

@ -1,15 +0,0 @@
<?php
// app/Models/Kategori.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Kategori extends Model
{
protected $fillable = ['nama', 'deskripsi', 'foto'];
public function templates()
{
return $this->hasMany(Template::class);
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class KategoriFitur extends Model
{
use HasFactory;
protected $table = 'kategori_fitur';
protected $fillable = [
'deskripsi',
];
// relasi: satu kategori punya banyak fitur
public function fiturs()
{
return $this->hasMany(Fitur::class, 'kategori_fitur_id');
}
}

View File

@ -1,35 +0,0 @@
<?php
// app/Models/Khitan.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Khitan extends Model
{
protected $fillable = [
'template_id',
'nama_pemesan',
'no_hp',
'email',
//Data
'nama_lengkap_anak',
'nama_panggilan_anak',
'bapak_anak',
'ibu_anak',
//Jadwal
'hari_tanggal_acara',
'waktu_acara',
'alamat_acara',
'maps_acara',
'no_rekening1',
'no_rekening2',
'link_musik',
'galeri',
];
public function template() {
return $this->belongsTo(Template::class);
}
}

View File

@ -1,25 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Pelanggan extends Model
{
use HasFactory;
protected $fillable = [
'nama_pemesan',
'nama_template',
'kategori',
'email',
'no_tlpn',
'harga',
];
public function details()
{
return $this->hasMany(PelangganDetail::class);
}
}

View File

@ -1,22 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PelangganDetail extends Model
{
use HasFactory;
protected $fillable = ['pelanggan_id', 'detail_form'];
protected $casts = [
'detail_form' => 'array',
];
public function pelanggan()
{
return $this->belongsTo(Pelanggan::class);
}
}

View File

@ -1,59 +0,0 @@
<?php
// app/Models/Pernikahan.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Pernikahan extends Model
{
protected $fillable = [
'template_id',
'nama_pemesan',
'no_hp',
'email',
// Pria
'nama_lengkap_pria',
'nama_panggilan_pria',
'bapak_pria',
'ibu_pria',
'instagram_pria',
'facebook_pria',
'twitter_pria',
// Wanita
'nama_lengkap_wanita',
'nama_panggilan_wanita',
'bapak_wanita',
'ibu_wanita',
'instagram_wanita',
'facebook_wanita',
'twitter_wanita',
// Cerita
'cerita_kita',
// Akad
'hari_tanggal_akad',
'waktu_akad',
'alamat_akad',
'maps_akad',
// Resepsi
'hari_tanggal_resepsi',
'waktu_resepsi',
'alamat_resepsi',
'maps_resepsi',
// Tambahan
'no_rekening1',
'no_rekening2',
'link_musik',
'galeri',
];
public function template() {
return $this->belongsTo(Template::class);
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Review extends Model
{
use HasFactory;
protected $fillable = [
'rating',
'message',
'name',
'city',
];
}

View File

@ -1,32 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Template extends Model
{
protected $fillable = [
'nama_template',
'kategori_id',
'foto',
'harga',
// jangan masukkan 'fitur_id' karena sudah di pivot
];
public function kategori()
{
return $this->belongsTo(Kategori::class);
}
// relasi many-to-many
public function fiturs()
{
return $this->belongsToMany(Fitur::class, 'fitur_template');
}
// relasi lainnya jika ada
public function pernikahan() { return $this->hasOne(Pernikahan::class); }
public function ulangTahun() { return $this->hasOne(UlangTahun::class); }
public function khitan() { return $this->hasOne(Khitan::class); }
}

View File

@ -1,36 +0,0 @@
<?php
// app/Models/UlangTahun.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UlangTahun extends Model
{
protected $fillable = [
'template_id',
'nama_pemesan',
'no_hp',
'email',
//Data
'nama_lengkap_anak',
'nama_panggilan_anak',
'bapak_anak',
'ibu_anak',
'umur_dirayakan',
'anak_ke',
//Jadwal
'hari_tanggal_acara',
'waktu_acara',
'alamat_acara',
'maps_acara',
'link_musik',
'galeri',
];
public function template() {
return $this->belongsTo(Template::class);
}
}

View File

@ -2,39 +2,94 @@
return [ return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [ 'defaults' => [
'guard' => 'web', 'guard' => 'web',
'passwords' => 'users', 'passwords' => 'users',
], ],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session"
|
*/
'guards' => [ 'guards' => [
// Guard untuk user biasa
'web' => [ 'web' => [
'driver' => 'session', 'driver' => 'session',
'provider' => 'users', 'provider' => 'users',
], ],
],
// Guard untuk admin /*
'admin' => [ |--------------------------------------------------------------------------
'driver' => 'session', | User Providers
'provider' => 'admins', |--------------------------------------------------------------------------
], |
], | All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [ 'providers' => [
// Provider untuk user biasa
'users' => [ 'users' => [
'driver' => 'eloquent', 'driver' => 'eloquent',
'model' => App\Models\User::class, 'model' => App\Models\User::class,
], ],
// Provider untuk admin // 'users' => [
'admins' => [ // 'driver' => 'database',
'driver' => 'eloquent', // 'table' => 'users',
'model' => App\Models\Admin::class, // ],
],
], ],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| You may specify multiple password reset configurations if you have more
| than one user table or model in the application and you want to have
| separate password reset settings based on the specific user types.
|
| The expire time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [ 'passwords' => [
'users' => [ 'users' => [
'provider' => 'users', 'provider' => 'users',
@ -42,14 +97,19 @@ return [
'expire' => 60, 'expire' => 60,
'throttle' => 60, 'throttle' => 60,
], ],
'admins' => [
'provider' => 'admins',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
], ],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| times out and the user is prompted to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => 10800, 'password_timeout' => 10800,
]; ];

View File

@ -1,23 +0,0 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Template>
*/
class TemplateFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@ -1,24 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('admins');
}
};

View File

@ -1,22 +0,0 @@
<?php
// database/migrations/2025_09_08_000001_create_kategoris_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('kategoris', function (Blueprint $table) {
$table->id();
$table->string('nama');
$table->text('deskripsi')->nullable();
$table->string('foto')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('kategoris');
}
};

View File

@ -1,24 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('reviews', function (Blueprint $table) {
$table->id();
$table->tinyInteger('rating'); // nilai bintang 15
$table->text('message');
$table->string('name');
$table->string('city');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('reviews');
}
};

View File

@ -1,20 +0,0 @@
<?php
// database/migrations/2025_09_08_000002_create_fiturs_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('fiturs', function (Blueprint $table) {
$table->id();
$table->text('deskripsi');
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('fiturs');
}
};

View File

@ -1,25 +0,0 @@
<?php
// database/migrations/2025_09_08_000003_create_templates_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
// database/migrations/2025_09_08_000003_create_templates_table.php
Schema::create('templates', function (Blueprint $table) {
$table->id();
$table->string('nama_template');
$table->foreignId('kategori_id')->constrained()->cascadeOnDelete();
$table->decimal('harga', 10, 2)->default(0); // ✅ harga template
$table->string('foto')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('templates');
}
};

View File

@ -1,26 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('pelanggans', function (Blueprint $table) {
$table->id();
$table->string('nama_pemesan');
$table->string('nama_template');
$table->string('kategori');
$table->string('email');
$table->string('no_tlpn');
$table->decimal('harga', 15, 2)->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('pelanggans');
}
};

View File

@ -1,24 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('pelanggan_details', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('pelanggan_id');
$table->json('detail_form'); // data sesuai kategori
$table->timestamps();
$table->foreign('pelanggan_id')->references('id')->on('pelanggans')->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('pelanggan_details');
}
};

View File

@ -1,65 +0,0 @@
<?php
// database/migrations/2025_09_08_000004_create_pernikahans_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('pernikahans', function (Blueprint $table) {
$table->id();
$table->foreignId('template_id')->constrained()->cascadeOnDelete();
$table->string('nama_pemesan');
$table->string('no_hp');
$table->string('email');
// Data Pria
$table->string('nama_lengkap_pria');
$table->string('nama_panggilan_pria');
$table->string('bapak_pria')->nullable();
$table->string('ibu_pria')->nullable();
$table->string('instagram_pria')->nullable();
$table->string('facebook_pria')->nullable();
$table->string('twitter_pria')->nullable();
// Data Wanita
$table->string('nama_lengkap_wanita');
$table->string('nama_panggilan_wanita');
$table->string('bapak_wanita')->nullable();
$table->string('ibu_wanita')->nullable();
$table->string('instagram_wanita')->nullable();
$table->string('facebook_wanita')->nullable();
$table->string('twitter_wanita')->nullable();
// Cerita
$table->text('cerita_kita')->nullable();
// Akad
$table->date('hari_tanggal_akad')->nullable();
$table->string('waktu_akad')->nullable();
$table->text('alamat_akad')->nullable();
$table->string('maps_akad')->nullable();
// Resepsi
$table->date('hari_tanggal_resepsi')->nullable();
$table->string('waktu_resepsi')->nullable();
$table->text('alamat_resepsi')->nullable();
$table->string('maps_resepsi')->nullable();
// Tambahan
$table->string('no_rekening1')->nullable();
$table->string('no_rekening2')->nullable();
$table->string('link_musik')->nullable();
$table->text('galeri')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('pernikahans');
}
};

View File

@ -1,40 +0,0 @@
<?php
// database/migrations/2025_09_08_000005_create_ulang_tahuns_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('ulang_tahuns', function (Blueprint $table) {
$table->id();
$table->foreignId('template_id')->constrained()->cascadeOnDelete();
$table->string('nama_pemesan');
$table->string('no_hp');
$table->string('email');
// Data anak
$table->string('nama_lengkap_anak');
$table->string('nama_panggilan_anak');
$table->string('bapak_anak');
$table->string('ibu_anak');
$table->string('umur_dirayakan');
$table->string('anak_ke');
// Jadwal acara
$table->date('hari_tanggal_acara');
$table->string('waktu_acara');
$table->text('alamat_acara');
$table->string('maps_acara')->nullable();
$table->string('link_musik')->nullable();
$table->string('galeri')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('ulang_tahuns');
}
};

View File

@ -1,43 +0,0 @@
<?php
// database/migrations/2025_09_08_000006_create_khitans_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('khitans', function (Blueprint $table) {
$table->id();
$table->foreignId('template_id')->constrained()->cascadeOnDelete();
$table->string('nama_pemesan');
$table->string('no_hp');
$table->string('email');
// Data Anak
$table->string('nama_lengkap_anak');
$table->string('nama_panggilan_anak');
$table->string('bapak_anak')->nullable();
$table->string('ibu_anak')->nullable();
// Jadwal
$table->date('hari_tanggal_acara')->nullable();
$table->string('waktu_acara')->nullable();
$table->text('alamat_acara')->nullable();
$table->string('maps_acara')->nullable();
// Tambahan
$table->string('no_rekening1')->nullable();
$table->string('no_rekening2')->nullable();
$table->string('link_musik')->nullable();
$table->text('galeri')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('khitans');
}
};

View File

@ -1,20 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('fitur_template', function (Blueprint $table) {
$table->id();
$table->foreignId('template_id')->constrained()->cascadeOnDelete();
$table->foreignId('fitur_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('fitur_template');
}
};

View File

@ -1,23 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::table('templates', function (Blueprint $table) {
// Hapus foreign key dulu (jika ada)
if (Schema::hasColumn('templates', 'fitur_id')) {
$table->dropForeign(['fitur_id']);
$table->dropColumn('fitur_id');
}
});
}
public function down(): void {
Schema::table('templates', function (Blueprint $table) {
$table->foreignId('fitur_id')->nullable()->constrained()->cascadeOnDelete();
});
}
};

View File

@ -1,22 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('fiturs', function (Blueprint $table) {
$table->decimal('harga', 10, 2)->default(0)->after('deskripsi');
});
}
public function down(): void
{
Schema::table('fiturs', function (Blueprint $table) {
$table->dropColumn('harga');
});
}
};

View File

@ -1,22 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('kategori_fitur', function (Blueprint $table) {
$table->id();
$table->string('deskripsi');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('kategori_fitur');
}
};

View File

@ -1,25 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('fiturs', function (Blueprint $table) {
$table->unsignedBigInteger('kategori_fitur_id')->nullable()->after('id');
$table->foreign('kategori_fitur_id')->references('id')->on('kategori_fitur')->onDelete('cascade');
});
}
public function down()
{
Schema::table('fiturs', function (Blueprint $table) {
$table->dropForeign(['kategori_fitur_id']);
$table->dropColumn('kategori_fitur_id');
});
}
};

View File

@ -2,20 +2,16 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Admin; use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class AdminSeeder extends Seeder class AdminSeeder extends Seeder
{ {
/**
* Run the database seeds.
*/
public function run(): void public function run(): void
{ {
Admin::updateOrCreate( //
['email' => 'admin@example.com'],
[
'name' => 'Super Admin',
'password' => Hash::make('password123'), // ganti setelah login
]
);
} }
} }

View File

@ -7,10 +7,16 @@ use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
/**
* Seed the application's database.
*/
public function run(): void public function run(): void
{ {
$this->call([ // \App\Models\User::factory(10)->create();
AdminSeeder::class,
]); // \App\Models\User::factory()->create([
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
} }
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

View File

@ -1,109 +0,0 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: #ffffff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.login-card {
background: #EFEFEF;
border-radius: 15px;
box-shadow: 0 0 25px rgb(0, 123, 255);
width: 100%;
max-width: 400px; /* batas lebar maksimum */
padding: 30px;
}
.login-logo {
display: block;
margin: 0 auto 15px;
max-width: 130px;
}
.btn-login {
background-color: #3bb9ff;
border: none;
border-radius: 8px;
}
.btn-login:hover {
background-color: #1a8edb;
}
.form-control {
border-radius: 10px;
}
/* Responsive padding */
@media (max-width: 576px) {
.login-card {
padding: 20px;
margin: 10px;
}
.login-logo {
max-width: 100px;
}
h4 {
font-size: 1.2rem;
}
p {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="text-center mb-3">
<!-- Logo -->
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="login-logo">
</div>
<h4 class="text-center mb-2 fw-bold">SELAMAT DATANG</h4>
<p class="text-center text-muted mb-4">Silakan masukkan email dan password anda.</p>
@if ($errors->any())
<div class="alert alert-danger">
{{ $errors->first() }}
</div>
@endif
<form action="{{ route('admin.login.post') }}" method="POST">
@csrf
<div class="mb-3">
<label class="form-label">Nama</label>
<input type="email" name="email" value="{{ old('email') }}" class="form-control"
placeholder="Masukkan nama Anda" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Kata Sandi</label>
<input type="password" name="password" class="form-control"
placeholder="Masukkan kata sandi Anda" required>
</div>
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember">
<label class="form-check-label" for="remember">Ingat saya</label>
</div>
<a href="#" class="text-decoration-none mt-2 mt-sm-0">Lupa kata sandi</a>
</div>
<button type="submit" class="btn btn-login text-white w-100">Login</button>
</form>
</div>
</body>
</html>

View File

@ -1,232 +0,0 @@
@extends('layouts.app')
@section('title', 'Halaman Dasbor')
@section('content')
<div class="container mx-auto py-4">
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Halaman Dasbor</h3>
<div class="flex items-center gap-3">
<div class="bg-blue-100 text-blue-600 px-3 py-2 rounded-lg flex items-center gap-2 text-sm">
<i class="bi bi-clock-history"></i>
{{ $today }}
</div>
</div>
</div>
<!-- Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
<div
class="bg-white rounded-lg shadow-md p-4 flex justify-between items-center hover:shadow-lg transition-transform duration-300 transform hover:-translate-y-2">
<div>
<h5 class="text-gray-500 text-sm">Kategori</h5>
<h3 class="font-bold text-xl">{{ $totalKategori }}</h3>
</div>
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center text-xl">
<i class="bi bi-diagram-3"></i>
</div>
</div>
<div
class="bg-white rounded-lg shadow-md p-4 flex justify-between items-center hover:shadow-lg transition-transform duration-300 transform hover:-translate-y-2">
<div>
<h5 class="text-gray-500 text-sm">Template</h5>
<h3 class="font-bold text-xl">{{ $totalTemplate }}</h3>
</div>
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center text-xl">
<i class="bi bi-card-list"></i>
</div>
</div>
<div
class="bg-white rounded-lg shadow-md p-4 flex justify-between items-center hover:shadow-lg transition-transform duration-300 transform hover:-translate-y-2">
<div>
<h5 class="text-gray-500 text-sm">Pelanggan</h5>
<h3 class="font-bold text-xl">{{ $totalPelanggan }}</h3>
</div>
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center text-xl">
<i class="bi bi-person"></i>
</div>
</div>
</div>
<!-- Recent Pelanggan -->
<div class="bg-white rounded-lg shadow-sm mt-6">
<div class="p-4 overflow-x-auto">
<h4 class="text-lg font-bold mb-3">Pelanggan Terbaru</h4>
<table class="w-full text-left border-collapse border border-gray-300">
<thead class="bg-gray-100">
<tr>
<th class="p-2 border border-gray-300 text-center w-16">Nomor</th>
<th class="p-2 border border-gray-300 text-center">Nama</th>
<th class="p-2 border border-gray-300 text-center">Template</th>
<th class="p-2 border border-gray-300 text-center">Kategori</th>
<th class="p-2 border border-gray-300 text-center">Email</th>
<th class="p-2 border border-gray-300 text-center">No. Telepon</th>
<th class="p-2 border border-gray-300 text-center">Harga</th>
<th class="p-2 border border-gray-300 text-center">Tanggal Pemesanan</th>
<th class="p-2 border border-gray-300 text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($recentPelanggan as $index => $pelanggan)
<tr>
<td class="p-2 py-4 border border-gray-300 text-center">
{{ $recentPelanggan->firstItem() + $index }}
</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->nama_pemesan }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->nama_template }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->kategori ?? '-' }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->email }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->no_tlpn ?? '-' }}</td>
<td class="py-3 px-2 border border-gray-300 text-center">
Rp {{ number_format($pelanggan->harga, 0, ',', '.') }}
</td>
<td class="py-3 px-2 border border-gray-300 text-center">
{{ \Carbon\Carbon::parse($pelanggan->created_at)->format('d M Y') }}
</td>
<td class="p-2 border border-gray-300 text-center">
<div class="flex justify-center space-x-2">
<a href="{{ route('admin.pelanggan.show', $pelanggan->id) }}"
class="text-blue-600 hover:underline flex items-center">
<i class="bi bi-eye mr-1"></i> Detail
</a>
<button class="text-red-600 hover:underline flex items-center openDeleteModalBtn"
data-id="{{ $pelanggan->id }}">
<i class="bi bi-trash mr-1"></i> Hapus
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="9" class="p-2 text-center text-gray-500 border border-gray-300">
Belum ada data pelanggan.
</td>
</tr>
@endforelse
</tbody>
</table>
<!-- Pagination -->
<div class="p-4 flex justify-center">
<div class="flex space-x-1">
{{-- Tombol Previous --}}
@if ($recentPelanggan->onFirstPage())
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Prev</span>
@else
<a href="{{ $recentPelanggan->previousPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Prev</a>
@endif
@php
$total = $recentPelanggan->lastPage();
$current = $recentPelanggan->currentPage();
@endphp
{{-- Selalu tampilkan halaman pertama --}}
@if ($current > 2)
<a href="{{ $recentPelanggan->url(1) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">1</a>
@if ($current > 3)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
@endif
{{-- Hanya tampilkan 3 halaman di tengah (current-1, current, current+1) --}}
@for ($i = max(1, $current - 1); $i <= min($total, $current + 1); $i++)
@if ($i == $current)
<span
class="px-3 py-1 rounded-lg bg-blue-600 text-white font-semibold">{{ $i }}</span>
@else
<a href="{{ $recentPelanggan->url($i) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $i }}</a>
@endif
@endfor
{{-- Selalu tampilkan halaman terakhir --}}
@if ($current < $total - 1)
@if ($current < $total - 2)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
<a href="{{ $recentPelanggan->url($total) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $total }}</a>
@endif
{{-- Tombol Next --}}
@if ($recentPelanggan->hasMorePages())
<a href="{{ $recentPelanggan->nextPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Next</a>
@else
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Next</span>
@endif
</div>
</div>
</div>
</div>
<!-- Modal Hapus Pelanggan -->
@foreach ($recentPelanggan as $pelanggan)
<div id="modalDelete{{ $pelanggan->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeDeleteOverlay" data-id="{{ $pelanggan->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Hapus Pelanggan</h5>
</div>
<div class="p-4">
<p>Apakah Anda yakin ingin menghapus pelanggan <strong>{{ $pelanggan->nama_pemesan }}</strong>?</p>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeDeleteBtn"
data-id="{{ $pelanggan->id }}">Batal</button>
<form action="{{ route('admin.pelanggan.destroy', $pelanggan->id) }}" method="POST"
class="inline">
@csrf
@method('DELETE')
<button class="bg-red-600 text-white px-3 py-1 rounded">Hapus</button>
</form>
</div>
</div>
</div>
@endforeach
<script>
document.addEventListener('DOMContentLoaded', () => {
const openDeleteBtns = document.querySelectorAll('.openDeleteModalBtn');
const closeDeleteBtns = document.querySelectorAll('.closeDeleteBtn');
const closeDeleteOverlays = document.querySelectorAll('.closeDeleteOverlay');
openDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
});
});
closeDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
closeDeleteOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
});
</script>
@endsection

View File

@ -1,374 +0,0 @@
@extends('layouts.app')
@section('title', 'Manajemen Fitur')
@section('content')
<div class="container mx-auto py-4">
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Daftar Fitur</h3>
<button id="openTambahModal" class="bg-blue-600 text-white px-3 py-2.5 rounded flex items-center">
<i class="bi bi-plus-lg mr-1"></i> Tambah Fitur
</button>
</div>
<!-- Flash Message -->
@if (session('success'))
<div id="toast-success" class="mb-4 p-3 rounded bg-green-100 text-green-800 border border-green-300 shadow">
{{ session('success') }}
</div>
<script>
setTimeout(() => document.getElementById('toast-success')?.remove(), 3000);
</script>
@endif
@if ($errors->any())
<div id="toast-error" class="mb-4 p-3 rounded bg-red-100 text-red-800 border border-red-300 shadow">
<ul class="list-disc ml-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
<script>
setTimeout(() => document.getElementById('toast-error')?.remove(), 5000);
</script>
@endif
<!-- Tabel Fitur -->
<div class="bg-white rounded-lg shadow-sm">
<div class="p-4 overflow-x-auto">
<table class="w-full table-fixed border border-gray-300 text-left">
<thead class="bg-gray-100">
<tr>
<th class="w-[7%] p-2 border border-gray-300 text-center">Nomor</th>
<th class="w-[25%] p-2 border border-gray-300 text-center">Kategori Fitur</th>
<th class="w-[30%] p-2 border border-gray-300 text-center">Fitur</th>
<th class="w-[18%] p-2 border border-gray-300 text-center">Harga</th>
<th class="w-[20%] p-2 border border-gray-300 text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($fitur as $key => $item)
<tr class="hover:bg-gray-50">
<!-- Nomor -->
<td class="py-5 px-2 border border-gray-300 text-center">
{{ $fitur->firstItem() + $key }}
</td>
<!-- Kategori -->
<td class="py-5 px-2 border border-gray-300 text-center">
{{ $item->kategoriFitur->deskripsi ?? '-' }}
</td>
<!-- Nama Fitur -->
<td class="py-5 px-2 border border-gray-300 truncate whitespace-nowrap">
{{ $item->deskripsi }}
</td>
<!-- Harga -->
<td class="py-5 px-2 border border-gray-300 text-center">
Rp {{ number_format($item->harga, 0, ',', '.') }}
</td>
<!-- Aksi -->
<td class="py-5 px-2 border border-gray-300 text-center">
<div class="flex justify-center space-x-2">
<button class="text-blue-600 flex items-center pr-4 openEditModalBtn"
data-id="{{ $item->id }}">
<i class="bi bi-pencil mr-1"></i> Ubah
</button>
<button class="text-red-600 flex items-center openDeleteModalBtn"
data-id="{{ $item->id }}">
<i class="bi bi-trash mr-1"></i> Hapus
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="p-2 text-center text-gray-500">Belum ada fitur</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="p-4 flex justify-center">
<div class="flex space-x-1">
{{-- Tombol Previous --}}
@if ($fitur->onFirstPage())
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Prev</span>
@else
<a href="{{ $fitur->previousPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Prev</a>
@endif
@php
$total = $fitur->lastPage();
$current = $fitur->currentPage();
@endphp
{{-- Selalu tampilkan halaman pertama --}}
@if ($current > 2)
<a href="{{ $fitur->url(1) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">1</a>
@if ($current > 3)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
@endif
{{-- Halaman tengah --}}
@for ($i = max(1, $current - 1); $i <= min($total, $current + 1); $i++)
@if ($i == $current)
<span class="px-3 py-1 rounded-lg bg-blue-600 text-white font-semibold">{{ $i }}</span>
@else
<a href="{{ $fitur->url($i) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $i }}</a>
@endif
@endfor
{{-- Selalu tampilkan halaman terakhir --}}
@if ($current < $total - 1)
@if ($current < $total - 2)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
<a href="{{ $fitur->url($total) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $total }}</a>
@endif
{{-- Tombol Next --}}
@if ($fitur->hasMorePages())
<a href="{{ $fitur->nextPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Next</a>
@else
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Next</span>
@endif
</div>
</div>
</div>
</div>
<!-- Modal Tambah -->
<div id="modalTambah" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50" id="closeTambahModal"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<form action="{{ route('admin.fitur.store') }}" method="POST">
@csrf
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Tambah Fitur</h5>
</div>
<div class="p-4 space-y-4">
{{-- Kategori --}}
@if($kategori_fitur ?? false)
<!-- Jika controller mengirim $kategori_fitur -->
<input type="hidden" name="kategori_fitur_id" value="{{ optional($kategori_fitur ?? null)->id }}">
<p class="text-sm text-gray-600">
Kategori: <strong>{{ optional($kategori_fitur ?? null)->deskripsi }}</strong>
</p>
@else
<!-- Kalau tidak ada kategori tertentu, tampilkan dropdown -->
<div>
<label class="block text-sm font-medium">Kategori Fitur</label>
<select name="kategori_fitur_id" class="w-full p-2 border rounded">
<option value="">-- Pilih Kategori --</option>
@foreach(\App\Models\KategoriFitur::all() as $kategori)
<option value="{{ $kategori->id }}">{{ $kategori->deskripsi }}</option>
@endforeach
</select>
</div>
@endif
{{-- Nama Fitur --}}
<div>
<label class="block text-sm font-medium">Nama Fitur</label>
<input type="text" name="deskripsi" class="w-full p-2 border rounded" required>
</div>
{{-- Harga --}}
<div>
<label class="block text-sm font-medium">Harga</label>
<input type="number" name="harga" step="100" min="0" class="w-full p-2 border rounded" required>
</div>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" id="closeTambahBtn"
class="bg-gray-300 text-black px-3 py-1 rounded">Batal</button>
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan</button>
</div>
</form>
</div>
</div>
<!-- Modal Edit -->
@foreach ($fitur as $item)
<div id="modalEdit{{ $item->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeEditOverlay" data-id="{{ $item->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<form action="{{ route('admin.fitur.update', $item->id) }}" method="POST">
@csrf
@method('PUT')
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Edit Fitur</h5>
</div>
<div class="p-4 space-y-4">
{{-- Kategori --}}
@if($kategori_fitur ?? false)
<!-- Jika controller mengirim $kategori_fitur -->
<input type="hidden" name="kategori_fitur_id" value="{{ optional($kategori_fitur ?? null)->id }}">
<p class="text-sm text-gray-600">
Kategori: <strong>{{ optional($kategori_fitur ?? null)->deskripsi }}</strong>
</p>
@else
<!-- Kalau tidak ada kategori tertentu, tampilkan dropdown -->
<div>
<label class="block text-sm font-medium">Kategori Fitur</label>
<select name="kategori_fitur_id" class="w-full p-2 border rounded">
<option value="">-- Pilih Kategori --</option>
@foreach(\App\Models\KategoriFitur::all() as $kategori)
<option value="{{ $kategori->id }}"
{{ $item->kategori_fitur_id == $kategori->id ? 'selected' : '' }}>
{{ $kategori->deskripsi }}
</option>
@endforeach
</select>
</div>
@endif
{{-- Nama Fitur --}}
<div>
<label class="block text-sm font-medium">Nama Fitur</label>
<input type="text" name="deskripsi" value="{{ $item->deskripsi }}"
class="w-full p-2 border rounded" required>
</div>
{{-- Harga --}}
<div>
<label class="block text-sm font-medium">Harga</label>
<input type="number" name="harga" value="{{ $item->harga }}"
step="100" min="0" class="w-full p-2 border rounded" required>
</div>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeEditBtn"
data-id="{{ $item->id }}">Batal</button>
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan Perubahan</button>
</div>
</form>
</div>
</div>
@endforeach
<!-- Modal Hapus -->
@foreach ($fitur as $item)
<div id="modalDelete{{ $item->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeDeleteOverlay" data-id="{{ $item->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Hapus Fitur</h5>
</div>
<div class="p-4">
<p>Apakah Anda yakin ingin menghapus fitur <strong>{{ $item->deskripsi }}</strong>?</p>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeDeleteBtn"
data-id="{{ $item->id }}">Batal</button>
<form action="{{ route('admin.fitur.destroy', $item->id) }}" method="POST" class="inline">
@csrf
@method('DELETE')
<button class="bg-red-600 text-white px-3 py-1 rounded">Hapus</button>
</form>
</div>
</div>
</div>
@endforeach
<script>
// Modal Tambah
const openTambahModal = document.getElementById('openTambahModal');
const modalTambah = document.getElementById('modalTambah');
const closeTambahBtn = document.getElementById('closeTambahBtn');
const closeTambahOverlay = document.getElementById('closeTambahModal');
openTambahModal.addEventListener('click', () => {
modalTambah.classList.remove('hidden');
modalTambah.classList.add('flex');
});
closeTambahBtn.addEventListener('click', () => {
modalTambah.classList.add('hidden');
modalTambah.classList.remove('flex');
});
closeTambahOverlay.addEventListener('click', () => {
modalTambah.classList.add('hidden');
modalTambah.classList.remove('flex');
});
// Modal Edit
const openEditBtns = document.querySelectorAll('.openEditModalBtn');
const closeEditBtns = document.querySelectorAll('.closeEditBtn');
const closeEditOverlays = document.querySelectorAll('.closeEditOverlay');
openEditBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.remove('hidden');
modal.classList.add('flex');
});
});
closeEditBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
closeEditOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
// Modal Delete
const openDeleteBtns = document.querySelectorAll('.openDeleteModalBtn');
const closeDeleteBtns = document.querySelectorAll('.closeDeleteBtn');
const closeDeleteOverlays = document.querySelectorAll('.closeDeleteOverlay');
openDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
modal.classList.remove('hidden');
modal.classList.add('flex');
});
});
closeDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
closeDeleteOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalDelete' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
</script>
@endsection

View File

@ -1,285 +0,0 @@
@extends('layouts.app')
@section('title', 'Manajemen Kategori')
@section('content')
<div class="container mx-auto py-4">
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Manajemen Kategori</h3>
<button id="openTambahModal" class="bg-blue-600 text-white px-3 py-2.5 rounded flex items-center">
<i class="bi bi-plus-lg mr-1"></i> Tambah Kategori
</button>
</div>
<!-- Flash Message -->
@if (session('success'))
<div id="toast-success" class="mb-4 p-3 rounded bg-green-100 text-green-800 border border-green-300 shadow">
{{ session('success') }}
</div>
<script>
setTimeout(() => document.getElementById('toast-success')?.remove(), 3000);
</script>
@endif
@if ($errors->any())
<div id="toast-error" class="mb-4 p-3 rounded bg-red-100 text-red-800 border border-red-300 shadow">
<ul class="list-disc ml-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
<script>
setTimeout(() => document.getElementById('toast-error')?.remove(), 5000);
</script>
@endif
<!-- Tabel Kategori -->
<div class="bg-white rounded-lg shadow-sm">
<div class="p-4 overflow-x-auto">
<table class="w-full table-fixed text-left border border-gray-300 border-collapse">
<thead class="bg-gray-100">
<tr>
<th class="p-2 border border-gray-300 w-[50px] text-center">Nomor</th>
<th class="p-2 border border-gray-300 w-[150px] text-center">Nama Kategori</th>
<th class="p-2 border border-gray-300 w-[300px] text-center">Keterangan</th>
<th class="p-2 border border-gray-300 w-[90px] text-center">Foto</th>
<th class="p-2 border border-gray-300 w-[110px] text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($kategori as $key => $item)
<tr>
<td class="p-2 border border-gray-300 text-center truncate">{{ $key + 1 }}</td>
<td class="p-2 border border-gray-300 truncate">{{ $item->nama }}</td>
<td class="p-2 border border-gray-300 truncate">{{ $item->deskripsi ?? '-' }}</td>
<td class="p-2 border border-gray-300 text-center">
<div class="w-12 h-12 overflow-hidden rounded bg-gray-100 flex items-center justify-center mx-auto">
<img src="{{ $item->foto ? asset('storage/' . $item->foto) : asset('default-image.png') }}"
alt="foto" class="max-w-full max-h-full object-contain">
</div>
</td>
<td class="p-2 border border-gray-300 text-center">
<div class="flex justify-center space-x-2">
<button
class="text-blue-600 hover:underline flex items-center pr-4 openEditModalBtn"
data-id="{{ $item->id }}">
<i class="bi bi-pencil mr-1"></i> Ubah
</button>
<button class="text-red-600 hover:underline flex items-center openDeleteModalBtn"
data-id="{{ $item->id }}">
<i class="bi bi-trash mr-1"></i> Hapus
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="5" class="p-2 text-center text-gray-500 border border-gray-300">
Belum ada kategori
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Tambah -->
<div id="modalTambah" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50" id="closeTambahModal"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<form action="{{ route('admin.kategori.store') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Tambah Kategori</h5>
</div>
<div class="p-4 space-y-4">
<div>
<label class="block text-sm font-medium">Nama</label>
<input type="text" name="nama" class="w-full p-2 border rounded" required>
</div>
<div>
<label class="block text-sm font-medium">Deskripsi</label>
<textarea name="deskripsi" class="w-full p-2 border rounded" rows="3"></textarea>
</div>
<div>
<label class="block text-sm font-medium">Foto</label>
<input type="file" name="foto" class="w-full p-2 border rounded" accept="image/*">
<small class="text-gray-500">Format yang didukung: JPG, PNG, GIF. Maksimal 5MB.</small>
</div>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" id="closeTambahBtn"
class="bg-gray-300 text-black px-3 py-1 rounded">Batal</button>
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan</button>
</div>
</form>
</div>
</div>
<!-- Modal Edit -->
@foreach ($kategori as $item)
<div id="modalEdit{{ $item->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeEditOverlay" data-id="{{ $item->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<form action="{{ route('admin.kategori.update', $item->id) }}" method="POST"
enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Edit Kategori</h5>
</div>
<div class="p-4 space-y-4">
<div>
<label class="block text-sm font-medium">Nama</label>
<input type="text" name="nama" value="{{ $item->nama }}"
class="w-full p-2 border rounded" required>
</div>
<div>
<label class="block text-sm font-medium">Deskripsi</label>
<textarea name="deskripsi" class="w-full p-2 border rounded" rows="3">{{ $item->deskripsi }}</textarea>
</div>
<div>
<label class="block text-sm font-medium">Foto</label>
<input type="file" name="foto" class="w-full p-2 border rounded" accept="image/*">
<small class="text-gray-500">Format yang didukung: JPG, PNG, GIF. Maksimal 5MB.</small>
@if ($item->foto)
<div class="mt-2">
<small class="text-gray-500">Foto saat ini:</small><br>
<div
class="w-20 h-20 mt-1 overflow-hidden rounded bg-gray-100 flex items-center justify-center border">
<img src="{{ asset('storage/' . $item->foto) }}" alt="foto"
class="max-w-full max-h-full object-contain">
</div>
</div>
@endif
</div>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeEditBtn"
data-id="{{ $item->id }}">Batal</button>
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan Perubahan</button>
</div>
</form>
</div>
</div>
@endforeach
<!-- Modal Hapus -->
@foreach ($kategori as $item)
<div id="modalDelete{{ $item->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeDeleteOverlay" data-id="{{ $item->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Hapus Kategori</h5>
</div>
<div class="p-4">
<p>Apakah Anda yakin ingin menghapus kategori <strong>{{ $item->nama }}</strong>?</p>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeDeleteBtn"
data-id="{{ $item->id }}">Batal</button>
<form action="{{ route('admin.kategori.destroy', $item->id) }}" method="POST" class="inline">
@csrf
@method('DELETE')
<button class="bg-red-600 text-white px-3 py-1 rounded">Hapus</button>
</form>
</div>
</div>
</div>
@endforeach
<script>
// Modal Tambah
const openTambahModal = document.getElementById('openTambahModal');
const modalTambah = document.getElementById('modalTambah');
const closeTambahBtn = document.getElementById('closeTambahBtn');
const closeTambahOverlay = document.getElementById('closeTambahModal');
openTambahModal.addEventListener('click', () => {
modalTambah.classList.remove('hidden');
modalTambah.classList.add('flex');
});
closeTambahBtn.addEventListener('click', () => {
modalTambah.classList.add('hidden');
modalTambah.classList.remove('flex');
});
closeTambahOverlay.addEventListener('click', () => {
modalTambah.classList.add('hidden');
modalTambah.classList.remove('flex');
});
// Modal Edit
const openEditBtns = document.querySelectorAll('.openEditModalBtn');
const closeEditBtns = document.querySelectorAll('.closeEditBtn');
const closeEditOverlays = document.querySelectorAll('.closeEditOverlay');
openEditBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.remove('hidden');
modal.classList.add('flex');
});
});
closeEditBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
closeEditOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
// Modal Hapus
const openDeleteBtns = document.querySelectorAll('.openDeleteModalBtn');
const closeDeleteBtns = document.querySelectorAll('.closeDeleteBtn');
const closeDeleteOverlays = document.querySelectorAll('.closeDeleteOverlay');
openDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
});
});
closeDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
closeDeleteOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
</script>
@endsection

View File

@ -1,18 +0,0 @@
{{-- @extends('layouts.app')
@section('title', 'Tambah Kategori Fitur')
@section('content')
<div class="container mx-auto py-4">
<h3 class="text-xl font-bold mb-4">Tambah Kategori Fitur</h3>
<form action="{{ route('admin.kategori_fitur.store') }}" method="POST">
@csrf
<div class="mb-4">
<label class="block">Deskripsi</label>
<input type="text" name="deskripsi" class="border rounded w-full px-3 py-2" required>
</div>
<button class="bg-blue-500 text-white px-4 py-2 rounded">Simpan</button>
<a href="{{ route('admin.kategori_fitur.index') }}" class="ml-2 text-gray-600">Batal</a>
</form>
</div>
@endsection --}}

View File

@ -1,19 +0,0 @@
{{-- @extends('layouts.app')
@section('title', 'Edit Kategori Fitur')
@section('content')
<div class="container mx-auto py-4">
<h3 class="text-xl font-bold mb-4">Edit Kategori Fitur</h3>
<form action="{{ route('admin.kategori_fitur.update', $kategori_fitur->id) }}" method="POST">
@csrf
@method('PUT')
<div class="mb-4">
<label class="block">Deskripsi</label>
<input type="text" name="deskripsi" value="{{ $kategori_fitur->deskripsi }}" class="border rounded w-full px-3 py-2" required>
</div>
<button class="bg-yellow-500 text-white px-4 py-2 rounded">Update</button>
<a href="{{ route('admin.kategori_fitur.index') }}" class="ml-2 text-gray-600">Batal</a>
</form>
</div>
@endsection --}}

View File

@ -1,290 +0,0 @@
@extends('layouts.app')
@section('title', 'Manajemen Kategori Fitur')
@section('content')
<div class="container mx-auto py-4">
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Daftar Kategori Fitur</h3>
<button id="openTambahModal" class="bg-blue-600 text-white px-3 py-2.5 rounded flex items-center">
<i class="bi bi-plus-lg mr-1"></i> Tambah Kategori
</button>
</div>
<!-- Flash Message -->
@if (session('success'))
<div id="toast-success" class="mb-4 p-3 rounded bg-green-100 text-green-800 border border-green-300 shadow">
{{ session('success') }}
</div>
<script>
setTimeout(() => document.getElementById('toast-success')?.remove(), 3000);
</script>
@endif
@if ($errors->any())
<div id="toast-error" class="mb-4 p-3 rounded bg-red-100 text-red-800 border border-red-300 shadow">
<ul class="list-disc ml-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
<script>
setTimeout(() => document.getElementById('toast-error')?.remove(), 5000);
</script>
@endif
<!-- Tabel Kategori -->
<div class="bg-white rounded-lg shadow-sm">
<div class="p-4 overflow-x-auto">
<table class="w-full table-fixed border border-gray-300 text-left">
<thead class="bg-gray-100">
<tr>
<th class="w-[10%] p-2 border border-gray-300 text-center">No</th>
<th class="w-[60%] p-2 border border-gray-300 text-center">Deskripsi</th>
<th class="w-[30%] p-2 border border-gray-300 text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($kategoriFitur as $index => $item)
<tr class="hover:bg-gray-50">
<td class="py-4 px-2 border border-gray-300 text-center">
{{ ($kategoriFitur->currentPage() - 1) * $kategoriFitur->perPage() + $loop->iteration }}
</td>
<td class="py-4 px-2 border border-gray-300">
{{ $item->deskripsi }}
</td>
<td class="py-4 px-2 border border-gray-300 text-center">
<div class="flex justify-center space-x-2">
<button class="text-blue-600 flex items-center pr-4 openEditModalBtn"
data-id="{{ $item->id }}">
<i class="bi bi-pencil mr-1"></i> Ubah
</button>
<button class="text-red-600 flex items-center openDeleteModalBtn"
data-id="{{ $item->id }}">
<i class="bi bi-trash mr-1"></i> Hapus
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="3" class="p-2 text-center text-gray-500">Belum ada kategori fitur</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="p-4 flex justify-center">
<div class="flex space-x-1">
{{-- Tombol Previous --}}
@if ($kategoriFitur->onFirstPage())
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Prev</span>
@else
<a href="{{ $kategoriFitur->previousPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Prev</a>
@endif
@php
$total = $kategoriFitur->lastPage();
$current = $kategoriFitur->currentPage();
@endphp
{{-- Selalu tampilkan halaman pertama --}}
@if ($current > 2)
<a href="{{ $kategoriFitur->url(1) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">1</a>
@if ($current > 3)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
@endif
{{-- Halaman tengah --}}
@for ($i = max(1, $current - 1); $i <= min($total, $current + 1); $i++)
@if ($i == $current)
<span class="px-3 py-1 rounded-lg bg-blue-600 text-white font-semibold">{{ $i }}</span>
@else
<a href="{{ $kategoriFitur->url($i) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $i }}</a>
@endif
@endfor
{{-- Selalu tampilkan halaman terakhir --}}
@if ($current < $total - 1)
@if ($current < $total - 2)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
<a href="{{ $kategoriFitur->url($total) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $total }}</a>
@endif
{{-- Tombol Next --}}
@if ($kategoriFitur->hasMorePages())
<a href="{{ $kategoriFitur->nextPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Next</a>
@else
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Next</span>
@endif
</div>
</div>
</div>
<!-- Modal Tambah -->
<div id="modalTambah" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50" id="closeTambahModal"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<form action="{{ route('admin.kategori_fitur.store') }}" method="POST">
@csrf
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Tambah Kategori Fitur</h5>
</div>
<div class="p-4 space-y-4">
<div>
<label class="block text-sm font-medium">Deskripsi</label>
<input type="text" name="deskripsi" class="w-full p-2 border rounded" required>
</div>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" id="closeTambahBtn" class="bg-gray-300 text-black px-3 py-1 rounded">Batal</button>
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan</button>
</div>
</form>
</div>
</div>
<!-- Modal Edit -->
@foreach ($kategoriFitur as $item)
<div id="modalEdit{{ $item->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeEditOverlay" data-id="{{ $item->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<form action="{{ route('admin.kategori_fitur.update', $item->id) }}" method="POST">
@csrf
@method('PUT')
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Edit Kategori Fitur</h5>
</div>
<div class="p-4 space-y-4">
<div>
<label class="block text-sm font-medium">Deskripsi</label>
<input type="text" name="deskripsi" value="{{ $item->deskripsi }}" class="w-full p-2 border rounded" required>
</div>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeEditBtn"
data-id="{{ $item->id }}">Batal</button>
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan Perubahan</button>
</div>
</form>
</div>
</div>
@endforeach
<!-- Modal Hapus -->
@foreach ($kategoriFitur as $item)
<div id="modalDelete{{ $item->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeDeleteOverlay" data-id="{{ $item->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Hapus Kategori Fitur</h5>
</div>
<div class="p-4">
<p>Apakah Anda yakin ingin menghapus kategori <strong>{{ $item->deskripsi }}</strong>?</p>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeDeleteBtn"
data-id="{{ $item->id }}">Batal</button>
<form action="{{ route('admin.kategori_fitur.destroy', $item->id) }}" method="POST" class="inline">
@csrf
@method('DELETE')
<button class="bg-red-600 text-white px-3 py-1 rounded">Hapus</button>
</form>
</div>
</div>
</div>
@endforeach
<script>
// Modal Tambah
const openTambahModal = document.getElementById('openTambahModal');
const modalTambah = document.getElementById('modalTambah');
const closeTambahBtn = document.getElementById('closeTambahBtn');
const closeTambahOverlay = document.getElementById('closeTambahModal');
openTambahModal.addEventListener('click', () => {
modalTambah.classList.remove('hidden');
modalTambah.classList.add('flex');
});
closeTambahBtn.addEventListener('click', () => {
modalTambah.classList.add('hidden');
modalTambah.classList.remove('flex');
});
closeTambahOverlay.addEventListener('click', () => {
modalTambah.classList.add('hidden');
modalTambah.classList.remove('flex');
});
// Modal Edit
const openEditBtns = document.querySelectorAll('.openEditModalBtn');
const closeEditBtns = document.querySelectorAll('.closeEditBtn');
const closeEditOverlays = document.querySelectorAll('.closeEditOverlay');
openEditBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.remove('hidden');
modal.classList.add('flex');
});
});
closeEditBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
closeEditOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalEdit' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
// Modal Delete
const openDeleteBtns = document.querySelectorAll('.openDeleteModalBtn');
const closeDeleteBtns = document.querySelectorAll('.closeDeleteBtn');
const closeDeleteOverlays = document.querySelectorAll('.closeDeleteOverlay');
openDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
modal.classList.remove('hidden');
modal.classList.add('flex');
});
});
closeDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
closeDeleteOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalDelete' + id);
modal.classList.add('hidden');
modal.classList.remove('flex');
});
});
</script>
@endsection

View File

@ -1,224 +0,0 @@
@extends('layouts.app')
@section('title', 'Manajemen Pelanggan')
@section('content')
<div class="container mx-auto py-4">
<!-- Header + Filter -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Manajemen Pelanggan</h3>
<!-- Filter & Search -->
<form method="GET" action="{{ route('admin.pelanggan.index') }}" class="flex space-x-2">
<!-- Filter Kategori -->
<select name="kategori" class="border border-gray-300 rounded px-3 py-2" onchange="this.form.submit()">
<option value="">-- Semua Kategori --</option>
@foreach ($kategoris as $kategori)
<option value="{{ $kategori->nama }}"
{{ request('kategori') == $kategori->nama ? 'selected' : '' }}>
{{ $kategori->nama }}
</option>
@endforeach
</select>
<!-- Search -->
<input type="text" name="search" value="{{ request('search') }}"
placeholder="Cari nama / email / telepon..." class="border border-gray-300 rounded px-3 py-2 w-60">
<button type="submit" class="bg-blue-600 text-white px-4 py-2 rounded">Cari</button>
</form>
</div>
<!-- Flash Message -->
@if (session('success'))
<div id="toast-success" class="mb-4 p-3 rounded bg-green-100 text-green-800 border border-green-300 shadow">
{{ session('success') }}
</div>
<script>
setTimeout(() => document.getElementById('toast-success')?.remove(), 3000);
</script>
@endif
<!-- Tabel Pelanggan -->
<div class="bg-white rounded-lg shadow-sm">
<div class="p-4 overflow-x-auto">
<table class="w-full text-left border-collapse border border-gray-300">
<thead class="bg-gray-100">
<tr>
<th class="p-2 border border-gray-300 text-center w-16">Nomor</th>
<th class="p-2 border border-gray-300 text-center">Nama</th>
<th class="p-2 border border-gray-300 text-center">Template</th>
<th class="p-2 border border-gray-300 text-center">Kategori</th>
<th class="p-2 border border-gray-300 text-center">Email</th>
<th class="p-2 border border-gray-300 text-center">No. Telepon</th>
<th class="p-2 border border-gray-300 text-center">Harga</th>
<th class="p-2 border border-gray-300 text-center">Tanggal Pemesanan</th>
<th class="p-2 border border-gray-300 text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($pelanggans as $key => $pelanggan)
<tr>
<td class="p-2 py-4 border border-gray-300 text-center">
{{ $pelanggans->firstItem() + $key }}
</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->nama_pemesan }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->nama_template }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->kategori ?? '-' }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->email }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $pelanggan->no_tlpn ?? '-' }}</td>
<td class="py-3 px-2 border border-gray-300 text-center">
Rp {{ number_format($pelanggan->harga, 0, ',', '.') }}
</td>
<td class="py-3 px-2 border border-gray-300 text-center">
{{ \Carbon\Carbon::parse($pelanggan->created_at)->format('d M Y') }}
</td>
<td class="p-2 border border-gray-300 text-center">
<div class="flex justify-center space-x-2">
<a href="{{ route('admin.pelanggan.show', $pelanggan->id) }}"
class="text-blue-600 hover:underline flex items-center">
<i class="bi bi-eye mr-1"></i> Detail
</a>
<button class="text-red-600 hover:underline flex items-center openDeleteModalBtn"
data-id="{{ $pelanggan->id }}">
<i class="bi bi-trash mr-1"></i> Hapus
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="9" class="p-2 text-center text-gray-500 border border-gray-300">
Belum ada pelanggan
</td>
</tr>
@endforelse
</tbody>
</table>
{{-- Pagination pindah ke sini --}}
@if ($pelanggans->hasPages())
<div class="p-4 flex justify-center">
<div class="flex space-x-1">
{{-- Tombol Previous --}}
@if ($pelanggans->onFirstPage())
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Prev</span>
@else
<a href="{{ $pelanggans->previousPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Prev</a>
@endif
@php
$total = $pelanggans->lastPage();
$current = $pelanggans->currentPage();
@endphp
{{-- Halaman pertama --}}
@if ($current > 2)
<a href="{{ $pelanggans->url(1) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">1</a>
@if ($current > 3)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
@endif
{{-- Halaman tengah --}}
@for ($i = max(1, $current - 1); $i <= min($total, $current + 1); $i++)
@if ($i == $current)
<span
class="px-3 py-1 rounded-lg bg-blue-600 text-white font-semibold">{{ $i }}</span>
@else
<a href="{{ $pelanggans->url($i) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $i }}</a>
@endif
@endfor
{{-- Halaman terakhir --}}
@if ($current < $total - 1)
@if ($current < $total - 2)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
<a href="{{ $pelanggans->url($total) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $total }}</a>
@endif
{{-- Tombol Next --}}
@if ($pelanggans->hasMorePages())
<a href="{{ $pelanggans->nextPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Next</a>
@else
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Next</span>
@endif
</div>
</div>
@endif
</div>
</div>
<!-- Modal Hapus Pelanggan -->
@foreach ($pelanggans as $pelanggan)
<div id="modalDelete{{ $pelanggan->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeDeleteOverlay" data-id="{{ $pelanggan->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Hapus Pelanggan</h5>
</div>
<div class="p-4">
<p>Apakah Anda yakin ingin menghapus pelanggan <strong>{{ $pelanggan->nama_pemesan }}</strong>?</p>
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeDeleteBtn"
data-id="{{ $pelanggan->id }}">Batal</button>
<form action="{{ route('admin.pelanggan.destroy', $pelanggan->id) }}" method="POST"
class="inline">
@csrf
@method('DELETE')
<button class="bg-red-600 text-white px-3 py-1 rounded">Hapus</button>
</form>
</div>
</div>
</div>
@endforeach
<script>
document.addEventListener('DOMContentLoaded', () => {
const openDeleteBtns = document.querySelectorAll('.openDeleteModalBtn');
const closeDeleteBtns = document.querySelectorAll('.closeDeleteBtn');
const closeDeleteOverlays = document.querySelectorAll('.closeDeleteOverlay');
openDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
});
});
closeDeleteBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
closeDeleteOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalDelete' + id);
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
});
</script>
@endsection

View File

@ -1,152 +0,0 @@
@extends('layouts.app')
@section('title', 'Detail Pelanggan')
@section('content')
<div class="w-full bg-gray-100 p-8">
<div class="bg-white rounded-lg shadow p-8 max-w-5xl mx-auto">
<!-- Tema Undangan -->
<h3 class="text-lg font-semibold mb-4">Tema Undangan</h3>
<div class="grid grid-cols-2 gap-6 mb-8">
<div>
<label class="text-gray-600 text-sm">Nama Template</label>
<div class="border rounded px-3 py-2 min-h-[45px] flex items-center">
{{ $pelanggan->nama_template }}
</div>
</div>
<div>
<label class="text-gray-600 text-sm">Kategori</label>
<div class="border rounded px-3 py-2 min-h-[45px] flex items-center">
{{ $pelanggan->kategori }}
</div>
</div>
<div>
<label class="text-gray-600 text-sm">Price</label>
<div class="border rounded px-3 py-2 min-h-[45px] flex items-center">
Rp {{ number_format($pelanggan->harga, 0, ',', '.') }}
</div>
</div>
<div>
<label class="text-gray-600 text-sm">Tanggal Pemesanan</label>
<div class="border rounded px-3 py-2 min-h-[45px] flex items-center">
{{ $pelanggan->created_at->translatedFormat('d F Y') }}
</div>
</div>
</div>
<!-- Pemesan Undangan -->
<h3 class="text-lg font-semibold mb-4">Pemesan Undangan</h3>
<div class="grid grid-cols-2 gap-6 mb-8">
<div>
<label class="text-gray-600 text-sm">Nama</label>
<div class="border rounded px-3 py-2 min-h-[45px] flex items-center">
{{ $pelanggan->nama_pemesan }}
</div>
</div>
<div>
<label class="text-gray-600 text-sm">No. WhatsApp</label>
<div class="border rounded px-3 py-2 min-h-[45px] flex items-center">
{{ $pelanggan->no_tlpn }}
</div>
</div>
<div class="col-span-2">
<label class="text-gray-600 text-sm">Email</label>
<div class="border rounded px-3 py-2 min-h-[45px] flex items-center">
{{ $pelanggan->email }}
</div>
</div>
</div>
<!-- Detail Form (dinamis dari JSON) -->
@if($pelanggan->details && count($pelanggan->details) > 0)
<h3 class="text-lg font-semibold mb-4">Detail Undangan</h3>
<div class="grid grid-cols-2 gap-6 mb-8">
@foreach($pelanggan->details as $detail)
@foreach($detail->detail_form as $key => $value)
{{-- Skip field galeri/gallery, email, dan template_id --}}
@if(!str_contains(strtolower($key), 'galeri') &&
!str_contains(strtolower($key), 'gallery') &&
$key !== 'email' &&
$key !== 'template_id')
<div>
<label class="text-gray-600 text-sm">{{ ucfirst(str_replace('_',' ',$key)) }}</label>
<div class="border rounded px-3 py-2 min-h-[45px] flex items-center">
@if(is_array($value))
{{ implode(', ', $value) }}
@else
{{ $value }}
@endif
</div>
</div>
@endif
@endforeach
@endforeach
</div>
@endif
<!-- Galeri (dinamis untuk semua field galeri) -->
@foreach($pelanggan->details as $detail)
@foreach($detail->detail_form as $key => $value)
{{-- Cek apakah field mengandung kata 'galeri' atau 'gallery' dan berupa array --}}
@if((str_contains(strtolower($key), 'galeri') || str_contains(strtolower($key), 'gallery')) && is_array($value) && !empty($value))
<div class="mb-6">
<h3 class="text-lg font-semibold mb-4">{{ ucfirst(str_replace('_', ' ', $key)) }}</h3>
<div class="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
@foreach($value as $foto)
<div class="relative group">
<img src="{{ asset('storage/' . $foto) }}"
alt="Foto {{ $key }}"
class="w-full h-24 object-cover rounded border hover:opacity-75 transition-opacity cursor-pointer"
onclick="openModal('{{ asset('storage/' . $foto) }}')">
{{-- Overlay untuk zoom icon --}}
<div class="absolute inset-0 bg-black bg-opacity-0 group-hover:bg-opacity-30 transition-all duration-200 rounded flex items-center justify-center">
<svg class="w-6 h-6 text-white opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7"></path>
</svg>
</div>
</div>
@endforeach
</div>
</div>
@endif
@endforeach
@endforeach
<!-- Modal untuk memperbesar gambar -->
<div id="imageModal" class="fixed inset-0 bg-black bg-opacity-75 hidden z-50 flex items-center justify-center" onclick="closeModal()">
<div class="max-w-4xl max-h-full p-4">
<img id="modalImage" src="" alt="Full Size" class="max-w-full max-h-full object-contain rounded">
<button onclick="closeModal()" class="absolute top-4 right-4 text-white text-2xl hover:text-gray-300">×</button>
</div>
</div>
<!-- Tombol Kembali -->
<div class="mt-8 text-right">
<a href="{{ route('admin.pelanggan.index') }}"
class="bg-gray-500 hover:bg-gray-600 text-white px-5 py-2 rounded">
Kembali
</a>
</div>
</div>
</div>
<script>
function openModal(imageSrc) {
document.getElementById('modalImage').src = imageSrc;
document.getElementById('imageModal').classList.remove('hidden');
}
function closeModal() {
document.getElementById('imageModal').classList.add('hidden');
}
// Close modal with ESC key
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeModal();
}
});
</script>
@endsection

View File

@ -1,119 +0,0 @@
@extends('layouts.app')
@section('title', 'Manajemen Ulasan')
@section('content')
<div class="container mx-auto py-4">
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Manajemen Ulasan</h3>
</div>
<!-- Alert sukses -->
@if (session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4" role="alert">
{{ session('success') }}
</div>
@endif
<!-- Tabel Ulasan -->
<div class="bg-white rounded-lg shadow-sm">
<div class="p-4 overflow-x-auto">
<table class="w-full table-fixed text-left border border-gray-300 border-collapse">
<thead class="bg-gray-100">
<tr>
<th class="p-2 border border-gray-300 w-[50px] text-center">Nomor</th>
<th class="p-2 border border-gray-300 w-[150px] text-center">Nama</th>
<th class="p-2 border border-gray-300 w-[120px] text-center">Kota</th>
<th class="p-2 border border-gray-300 w-[120px] text-center">Penilaian</th>
<th class="p-2 border border-gray-300 w-[350px] text-center">Pesan</th>
<th class="p-2 border border-gray-300 w-[120px] text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse ($reviews as $key => $review)
<tr>
<td class="p-2 py-4 border border-gray-300 text-center truncate">{{ $key + 1 }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $review->name }}</td>
<td class="p-2 py-4 border border-gray-300 truncate">{{ $review->city }}</td>
<td class="p-2 py-4 border border-gray-300 text-center">
@for ($s = 1; $s <= 5; $s++)
<i
class="bi {{ $s <= $review->rating ? 'bi-star-fill text-yellow-500' : 'bi-star text-gray-400' }}"></i>
@endfor
</td>
<td class="p-2 border border-gray-300 truncate" title="{{ $review->message }}">
{{ $review->message }}
</td>
<td class="p-2 border border-gray-300 text-center">
<div class="flex justify-center space-x-2">
<button class="text-red-600 hover:underline flex items-center btn-delete"
data-action="{{ route('admin.reviews.destroy', $review->id) }}"
data-name="{{ $review->name }}">
<i class="bi bi-trash mr-1"></i> Hapus
</button>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="p-2 text-center text-gray-500 border border-gray-300">
Belum ada ulasan
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
<!-- Modal Konfirmasi Hapus Tailwind -->
<div id="modalDelete" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50" id="modalDeleteOverlay"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<form id="deleteForm" method="POST">
@csrf
@method('DELETE')
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Hapus Ulasan</h5>
</div>
<div class="p-4">
Apakah Anda yakin ingin menghapus ulasan dari <strong id="deleteName"></strong>?
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" id="closeDeleteModal"
class="bg-gray-300 text-black px-3 py-1 rounded">Batal</button>
<button type="submit" class="bg-red-600 text-white px-3 py-1 rounded">Ya, Hapus</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const modal = document.getElementById('modalDelete');
const overlay = document.getElementById('modalDeleteOverlay');
const closeBtn = document.getElementById('closeDeleteModal');
const deleteForm = document.getElementById('deleteForm');
const deleteName = document.getElementById('deleteName');
document.querySelectorAll('.btn-delete').forEach(btn => {
btn.addEventListener('click', () => {
deleteForm.action = btn.dataset.action;
deleteName.textContent = btn.dataset.name || 'pengguna ini';
modal.classList.remove('hidden');
modal.classList.add('flex');
});
});
const closeModal = () => {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
closeBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', closeModal);
});
</script>
@endsection

View File

@ -1,611 +0,0 @@
@extends('layouts.app')
@section('title', 'Manajemen Template')
@section('content')
<div class="container mx-auto py-4">
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">
@isset($kategori)
Template Kategori: {{ $kategori->nama }}
@else
Semua Template
@endisset
</h3>
@if (!isset($kategori))
<button id="openTambahModal" class="bg-blue-600 text-white px-3 py-2.5 rounded flex items-center">
<i class="bi bi-plus-lg mr-1"></i> Tambah Template
</button>
@endif
</div>
<!-- Flash & Errors -->
@if (session('success'))
<div id="toast-success" class="mb-4 p-3 rounded bg-green-100 text-green-800 border border-green-300 shadow">
{{ session('success') }}
</div>
<script>
setTimeout(() => document.getElementById('toast-success')?.remove(), 3000);
</script>
@endif
@if ($errors->any())
<div id="toast-error" class="mb-4 p-3 rounded bg-red-100 text-red-800 border border-red-300 shadow">
<ul class="list-disc ml-5">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
<script>
setTimeout(() => document.getElementById('toast-error')?.remove(), 5000);
</script>
@endif
<!-- Tabel Template -->
<div class="bg-white rounded-lg shadow-sm">
<div class="p-4 overflow-x-auto">
<table class="w-full table-fixed text-left border border-gray-300 border-collapse">
<thead class="bg-gray-100">
<tr>
<th class="p-2 border border-gray-300 w-[50px] text-center">Nomor</th>
<th class="p-2 border border-gray-300 w-[200px] text-center">Nama Template</th>
<th class="p-2 border border-gray-300 w-[150px] text-center">Kategori</th>
<th class="p-2 border border-gray-300 w-[200px] text-center">Fitur</th>
<th class="p-2 border border-gray-300 w-[90px] text-center">Foto</th>
<th class="p-2 border border-gray-300 w-[100px] text-center">Harga</th>
<th class="p-2 border border-gray-300 w-[130px] text-center">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($templates as $key => $template)
<tr>
<td class="p-2 border border-gray-300 text-center">
{{ ($templates->currentPage() - 1) * $templates->perPage() + $key + 1 }}
</td>
<td class="p-2 border border-gray-300 truncate">{{ $template->nama_template }}</td>
<td class="p-2 border border-gray-300 truncate">{{ $template->kategori->nama ?? '-' }}</td>
<td class="p-2 border border-gray-300 align-top">
<div class="flex flex-wrap gap-1">
@forelse($template->fiturs as $f)
<span class="inline-block bg-gray-200 px-2 py-0.5 rounded text-xs">
{{ $f->deskripsi }}
</span>
@empty
<span class="text-gray-500">-</span>
@endforelse
</div>
</td>
<td class="p-2 border border-gray-300 text-center">
<div
class="w-12 h-12 overflow-hidden rounded bg-gray-100 flex items-center justify-center mx-auto">
<img src="{{ $template->foto ? asset('storage/' . $template->foto) : asset('default-image.png') }}"
alt="foto" class="max-w-full max-h-full object-contain">
</div>
</td>
<td class="p-2 border border-gray-300 text-left font-semibold">
Rp {{ number_format($template->harga, 0, ',', '.') }}
</td>
<td class="p-2 border border-gray-300 text-center">
<div class="flex justify-center space-x-2">
<button class="text-blue-600 hover:underline flex items-center openEditModalBtn"
data-id="{{ $template->id }}">
<i class="bi bi-pencil mr-1"></i> Edit
</button>
<form action="{{ route('templates.destroy', $template->id) }}" method="POST"
class="inline" onsubmit="return confirm('Hapus template ini?')">
@csrf
@method('DELETE')
<button
class="text-red-600 hover:underline flex items-center btn-delete-template"
data-action="{{ route('templates.destroy', $template->id) }}"
data-name="{{ $template->nama_template }}">
<i class="bi bi-trash mr-1"></i> Hapus
</button>
</form>
</div>
</td>
</tr>
@empty
<tr>
<td colspan="7" class="p-2 text-center text-gray-500 border border-gray-300">Belum ada
template</td>
</tr>
@endforelse
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="p-4 flex justify-center">
<div class="flex space-x-1">
{{-- Tombol Previous --}}
@if ($templates->onFirstPage())
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Prev</span>
@else
<a href="{{ $templates->previousPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Prev</a>
@endif
@php
$total = $templates->lastPage();
$current = $templates->currentPage();
@endphp
{{-- Selalu tampilkan halaman pertama --}}
@if ($current > 2)
<a href="{{ $templates->url(1) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">1</a>
@if ($current > 3)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
@endif
{{-- Halaman tengah --}}
@for ($i = max(1, $current - 1); $i <= min($total, $current + 1); $i++)
@if ($i == $current)
<span
class="px-3 py-1 rounded-lg bg-blue-600 text-white font-semibold">{{ $i }}</span>
@else
<a href="{{ $templates->url($i) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $i }}</a>
@endif
@endfor
{{-- Selalu tampilkan halaman terakhir --}}
@if ($current < $total - 1)
@if ($current < $total - 2)
<span class="px-3 py-1 text-gray-500">...</span>
@endif
<a href="{{ $templates->url($total) }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-blue-100">{{ $total }}</a>
@endif
{{-- Tombol Next --}}
@if ($templates->hasMorePages())
<a href="{{ $templates->nextPageUrl() }}"
class="px-3 py-1 rounded-lg bg-gray-200 text-gray-700 hover:bg-gray-300">Next</a>
@else
<span class="px-3 py-1 rounded-lg bg-gray-200 text-gray-500 cursor-not-allowed">Next</span>
@endif
</div>
</div>
</div>
</div>
<!-- Modal Tambah Template -->
@if (!isset($kategori))
<div id="modalTambah" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50" id="closeTambahModal"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 flex flex-col max-h-[90vh]">
<!-- Header -->
<div class="p-4 border-b sticky top-0 bg-white z-10">
<h5 class="text-lg font-medium">Tambah Template</h5>
</div>
<!-- Scrollable content -->
<div class="p-4 space-y-4 overflow-y-auto flex-1">
<form action="{{ route('templates.store') }}" method="POST" enctype="multipart/form-data"
class="h-full flex flex-col">
@csrf
<div class="flex-1 space-y-4">
<div>
<label class="block text-sm font-medium">Nama Template</label>
<input type="text" name="nama_template" class="w-full p-2 border rounded" required
value="{{ old('nama_template') }}">
</div>
<div>
<label class="block text-sm font-medium">Kategori</label>
<select name="kategori_id" id="kategoriSelectTambah" class="w-full p-2 border rounded">
<option value="">-- Pilih Kategori --</option>
@foreach ($kategoris as $kategoriItem)
<option value="{{ $kategoriItem->id }}" @selected(old('kategori_id') == $kategoriItem->id)>
{{ $kategoriItem->nama }}
</option>
@endforeach
</select>
</div>
<!-- FITUR -->
<div>
<label class="block text-sm font-medium">Fitur</label>
<div id="fiturContainerTambah" class="space-y-3 max-h-64 overflow-auto p-2 border rounded">
@foreach ($kategoriFiturs as $kategoriFitur)
<div>
<p class="font-semibold text-gray-700 mb-1">
{{ $kategoriFitur->deskripsi }}
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
@foreach ($fiturs->where('kategori_fitur_id', $kategoriFitur->id) as $fitur)
<label class="flex items-center space-x-2">
<input type="radio" name="fitur_id[{{ $kategoriFitur->id }}]"
value="{{ $fitur->id }}" data-harga="{{ $fitur->harga }}"
@checked(old("fitur_id.$kategoriFitur->id") == $fitur->id)>
<span class="text-sm">
{{ \Illuminate\Support\Str::limit($fitur->deskripsi, 80) }}
(Rp {{ number_format($fitur->harga, 0, ',', '.') }})
</span>
</label>
@endforeach
</div>
</div>
@endforeach
@if ($fitursTanpaKategori->count())
<div>
<p class="font-semibold text-gray-700 mb-1">Fitur Lainnya</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
@foreach ($fitursTanpaKategori as $fitur)
<label class="flex items-center space-x-2">
<input type="checkbox" name="fitur_id[]"
value="{{ $fitur->id }}" data-harga="{{ $fitur->harga }}"
{{ is_array(old('fitur_id')) && in_array($fitur->id, old('fitur_id')) ? 'checked' : '' }}>
<span class="text-sm">
{{ \Illuminate\Support\Str::limit($fitur->deskripsi, 80) }}
(Rp {{ number_format($fitur->harga, 0, ',', '.') }})
</span>
</label>
@endforeach
</div>
</div>
@endif
</div>
</div>
<div>
<label class="block text-sm font-medium">Harga</label>
<input type="text" name="harga" class="w-full p-2 border rounded" required
min="0" value="{{ old('harga') }}">
</div>
<div>
<label class="block text-sm font-medium">Foto (opsional)</label>
<input type="file" name="foto" class="w-full p-2 border rounded" accept="image/*">
<small class="text-gray-500">Format: JPG, PNG, GIF. Maks 5MB.</small>
</div>
</div>
<!-- Footer -->
<div class="p-4 border-t flex justify-end space-x-2 sticky bottom-0 bg-white z-10">
<button type="button" id="closeTambahBtn"
class="bg-gray-300 text-black px-3 py-1 rounded">Batal</button>
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan</button>
</div>
</form>
</div>
</div>
</div>
@endif
<!-- Modal Edit Template - Fixed Version -->
@foreach ($templates as $template)
<div id="modalEdit{{ $template->id }}" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50 closeEditOverlay" data-id="{{ $template->id }}"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 flex flex-col max-h-[90vh]">
<!-- PINDAHKAN FORM KE LUAR AGAR MENCAKUP SEMUA ELEMEN -->
<form id="editForm{{ $template->id }}" action="{{ route('templates.update', $template->id) }}"
method="POST" enctype="multipart/form-data" class="h-full flex flex-col">
@csrf @method('PUT')
<!-- Header -->
<div class="p-4 border-b sticky top-0 bg-white z-10">
<h5 class="text-lg font-medium">Edit Template</h5>
</div>
<!-- Scrollable content -->
<div class="p-4 space-y-4 overflow-y-auto flex-1">
<div class="flex-1 space-y-4">
<div>
<label class="block text-sm font-medium">Nama Template</label>
<input type="text" name="nama_template"
value="{{ old('nama_template', $template->nama_template) }}"
class="w-full p-2 border rounded" required>
</div>
<div>
<label class="block text-sm font-medium">Kategori</label>
<select name="kategori_id" id="kategoriSelectEdit{{ $template->id }}"
class="w-full p-2 border rounded">
<option value="">-- Pilih Kategori --</option>
@foreach ($kategoris as $kategoriItem)
<option value="{{ $kategoriItem->id }}" @selected($kategoriItem->id == old('kategori_id', $template->kategori_id))>
{{ $kategoriItem->nama }}
</option>
@endforeach
</select>
</div>
<!-- FITUR -->
<div>
<label class="block text-sm font-medium">Fitur</label>
<div id="fiturContainerEdit{{ $template->id }}"
class="space-y-3 max-h-64 overflow-auto p-2 border rounded">
@foreach ($kategoriFiturs as $kategoriFitur)
<div>
<p class="font-semibold text-gray-700 mb-1">
{{ $kategoriFitur->deskripsi }}
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
@foreach ($fiturs->where('kategori_fitur_id', $kategoriFitur->id) as $fiturItem)
<label class="flex items-center space-x-2">
<input type="radio" name="fitur_id[{{ $kategoriFitur->id }}]"
value="{{ $fiturItem->id }}"
data-harga="{{ $fiturItem->harga }}"
{{ old("fitur_id.$kategoriFitur->id") == $fiturItem->id || $template->fiturs->contains($fiturItem->id) ? 'checked' : '' }}>
<span class="text-sm">
{{ \Illuminate\Support\Str::limit($fiturItem->deskripsi, 80) }}
(Rp {{ number_format($fiturItem->harga, 0, ',', '.') }})
</span>
</label>
@endforeach
</div>
</div>
@endforeach
@if ($fitursTanpaKategori->count())
<div>
<p class="font-semibold text-gray-700 mb-1">Fitur Lainnya</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
@foreach ($fitursTanpaKategori as $fitur)
<label class="flex items-center space-x-2">
<input type="checkbox" name="fitur_id[]"
value="{{ $fitur->id }}" data-harga="{{ $fitur->harga }}"
{{ (is_array(old('fitur_id')) && in_array($fitur->id, old('fitur_id'))) || $template->fiturs->contains($fitur->id) ? 'checked' : '' }}>
<span class="text-sm">
{{ \Illuminate\Support\Str::limit($fitur->deskripsi, 80) }}
(Rp {{ number_format($fitur->harga, 0, ',', '.') }})
</span>
</label>
@endforeach
</div>
</div>
@endif
</div>
</div>
<div>
<label class="block text-sm font-medium">Harga</label>
<input type="text" name="harga" value="{{ old('harga', $template->harga) }}"
class="w-full p-2 border rounded" required min="0">
</div>
<div>
<label class="block text-sm font-medium">Foto (opsional)</label>
@if ($template->foto)
<div class="mb-2">
<small class="text-gray-500">Foto saat ini:</small>
<div
class="w-20 h-20 mt-1 overflow-hidden rounded bg-gray-100 flex items-center justify-center border">
<img src="{{ asset('storage/' . $template->foto) }}" alt="foto"
class="max-w-full max-h-full object-contain">
</div>
</div>
@endif
<input type="file" name="foto" class="w-full p-2 border rounded" accept="image/*">
</div>
</div>
</div>
<!-- Footer - SEKARANG DI DALAM FORM -->
<div class="p-4 border-t flex justify-end space-x-2 sticky bottom-0 bg-white z-10">
<button type="button" class="bg-gray-300 text-black px-3 py-1 rounded closeEditBtn"
data-id="{{ $template->id }}">Batal</button>
<!-- TAMBAHKAN type="submit" -->
<button type="submit" class="bg-blue-600 text-white px-3 py-1 rounded">Simpan Perubahan</button>
</div>
</form>
</div>
</div>
@endforeach
<!-- Modal Konfirmasi Hapus Template -->
<div id="modalDeleteTemplate" class="fixed inset-0 hidden items-center justify-center z-50">
<div class="absolute inset-0 bg-black opacity-50" id="modalDeleteTemplateOverlay"></div>
<div class="bg-white rounded-lg shadow-lg w-full max-w-md z-50 overflow-hidden">
<form id="deleteTemplateForm" method="POST">
@csrf
@method('DELETE')
<div class="p-4 border-b">
<h5 class="text-lg font-medium">Hapus Template</h5>
</div>
<div class="p-4">
Apakah Anda yakin ingin menghapus template <strong id="deleteTemplateName"></strong>?
</div>
<div class="p-4 border-t flex justify-end space-x-2">
<button type="button" id="closeDeleteTemplateModal"
class="bg-gray-300 text-black px-3 py-1 rounded">Batal</button>
<button type="submit" class="bg-red-600 text-white px-3 py-1 rounded">
Ya, Hapus
</button>
</div>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// =========================
// Modal Tambah
// =========================
const openTambahModal = document.getElementById('openTambahModal');
const modalTambah = document.getElementById('modalTambah');
const closeTambahBtn = document.getElementById('closeTambahBtn');
const closeTambahOverlay = document.getElementById('closeTambahModal');
if (openTambahModal) {
openTambahModal.addEventListener('click', () => {
modalTambah.classList.remove('hidden');
modalTambah.classList.add('flex');
});
}
if (closeTambahBtn) {
closeTambahBtn.addEventListener('click', () => {
modalTambah.classList.add('hidden');
modalTambah.classList.remove('flex');
});
}
if (closeTambahOverlay) {
closeTambahOverlay.addEventListener('click', () => {
modalTambah.classList.add('hidden');
modalTambah.classList.remove('flex');
});
}
// =========================
// Modal Edit
// =========================
const openEditBtns = document.querySelectorAll('.openEditModalBtn');
const closeEditBtns = document.querySelectorAll('.closeEditBtn');
const closeEditOverlays = document.querySelectorAll('.closeEditOverlay');
openEditBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalEdit' + id);
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
}
});
});
closeEditBtns.forEach(btn => {
btn.addEventListener('click', () => {
const id = btn.dataset.id;
const modal = document.getElementById('modalEdit' + id);
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
closeEditOverlays.forEach(overlay => {
overlay.addEventListener('click', () => {
const id = overlay.dataset.id;
const modal = document.getElementById('modalEdit' + id);
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
});
});
// =========================
// Modal Delete Template
// =========================
const deleteTemplateModal = document.getElementById('modalDeleteTemplate');
const deleteTemplateOverlay = document.getElementById('modalDeleteTemplateOverlay');
const closeDeleteTemplateModal = document.getElementById('closeDeleteTemplateModal');
const deleteTemplateForm = document.getElementById('deleteTemplateForm');
const deleteTemplateName = document.getElementById('deleteTemplateName');
document.querySelectorAll('.btn-delete-template').forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault(); // cegah form langsung submit
deleteTemplateForm.action = btn.dataset.action;
deleteTemplateName.textContent = btn.dataset.name || 'template ini';
deleteTemplateModal.classList.remove('hidden');
deleteTemplateModal.classList.add('flex');
});
});
const closeDeleteModalTemplate = () => {
deleteTemplateModal.classList.add('hidden');
deleteTemplateModal.classList.remove('flex');
};
if (closeDeleteTemplateModal) {
closeDeleteTemplateModal.addEventListener('click', closeDeleteModalTemplate);
}
if (deleteTemplateOverlay) {
deleteTemplateOverlay.addEventListener('click', closeDeleteModalTemplate);
}
// =========================
// Fitur: Hitung Harga Otomatis + Format ribuan
// =========================
function formatRibuan(x) {
if (!x) return '';
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
function hitungHarga(container) {
let total = 0;
// Hitung checkbox (fitur tanpa kategori)
container.querySelectorAll('input[type="checkbox"][name="fitur_id[]"]:checked')
.forEach(cb => {
total += parseInt(cb.dataset.harga) || 0;
});
// Hitung radio (fitur per kategori)
container.querySelectorAll('input[type="radio"]:checked')
.forEach(radio => {
total += parseInt(radio.dataset.harga) || 0;
});
const inputHarga = container.querySelector('input[name="harga"]');
if (inputHarga && !inputHarga.dataset.manual) {
inputHarga.value = formatRibuan(total);
}
}
// Format harga manual + tetap auto ribuan
document.querySelectorAll('input[name="harga"]').forEach(input => {
input.classList.add("harga-input"); // pastikan ada class
input.addEventListener('input', (e) => {
let value = e.target.value.replace(/\D/g, ''); // hanya angka
e.target.value = formatRibuan(value);
e.target.dataset.manual = "true"; // flag manual
});
});
// Modal Tambah
if (modalTambah) {
const inputs = modalTambah.querySelectorAll('input[name="fitur_id[]"], input[type="radio"]');
const inputHarga = modalTambah.querySelector('input[name="harga"]');
inputs.forEach(input => {
input.addEventListener('change', () => hitungHarga(modalTambah));
});
if (inputHarga) {
inputHarga.addEventListener('input', () => {
inputHarga.dataset.manual = "true";
});
}
// Hitung awal kalau ada yang sudah tercentang
hitungHarga(modalTambah);
}
// Modal Edit (banyak)
document.querySelectorAll('[id^="modalEdit"]').forEach(modal => {
const inputs = modal.querySelectorAll('input[name="fitur_id[]"], input[type="radio"]');
const inputHarga = modal.querySelector('input[name="harga"]');
inputs.forEach(input => {
input.addEventListener('change', () => hitungHarga(modal));
});
if (inputHarga) {
inputHarga.addEventListener('input', () => {
inputHarga.dataset.manual = "true";
});
}
// Hitung awal kalau ada yang sudah tercentang
hitungHarga(modal);
});
});
</script>
</div>
@endsection

View File

@ -1,180 +0,0 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>@yield('title', 'Admin Panel')</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css">
<style>
body {
background-color: #f1f5f9;
font-family: 'Poppins', sans-serif;
}
/* Sidebar */
.sidebar {
width: 250px;
height: 100vh;
background: #fff;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.05);
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: space-between;
transition: all 0.3s ease;
}
.submenu {
display: none;
}
.submenu.show {
display: block;
}
</style>
</head>
<body class="flex">
<!-- Sidebar -->
<div class="sidebar flex flex-col">
<!-- LOGO -->
<div class="text-center py-4 border-b">
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="mx-auto mb-2" style="max-height: 80px;">
</div>
<!-- MENU -->
<div class="flex-1 overflow-y-auto">
<p class="text-gray-500 uppercase text-xs font-semibold px-3 mt-4 mb-2">Menu Utama</p>
<ul class="px-2 space-y-1">
<li>
<a href="{{ route('admin.dashboard') }}"
class="flex items-center py-2 px-3 rounded hover:bg-blue-50 {{ request()->is('admin/dashboard') ? 'bg-blue-100 text-blue-600' : 'text-gray-700' }}">
<i class="bi bi-house-door me-2"></i> Dasbor
</a>
</li>
<a href="{{ route('admin.kategori.index') }}"
class="flex items-center py-2 px-3 rounded hover:bg-blue-50 {{ request()->routeIs('admin.kategori.*') ? 'bg-blue-100 text-blue-600' : 'text-gray-700' }}">
<i class="bi bi-diagram-3 me-2"></i> Kategori
</a>
<!-- MANAJEMEN FITUR DROPDOWN -->
<li>
@php
$isFiturPage = request()->is('admin/fitur*') || request()->is('admin/kategori_fitur*');
@endphp
<button
class="w-full flex items-center justify-between py-2 px-3 text-gray-700 rounded hover:bg-blue-50 {{ $isFiturPage ? 'bg-blue-100 text-blue-600' : '' }}"
id="manajemenFiturBtn">
<span><i class="bi bi-grid me-2"></i>Manajemen Fitur</span>
<i class="bi bi-chevron-down transition-transform {{ $isFiturPage ? 'rotate-180' : '' }}"
id="manajemenFiturIcon"></i>
</button>
<ul class="submenu pl-6 space-y-1 mt-2 {{ $isFiturPage ? 'show' : '' }}" id="manajemenFiturSubmenu">
<li>
<a href="{{ route('admin.kategori_fitur.index') }}"
class="block py-2 px-2 rounded {{ request()->is('admin/kategori_fitur*') ? 'bg-blue-100 text-blue-600' : 'text-gray-500 hover:text-blue-600 hover:bg-blue-50' }}">
Kategori Fitur
</a>
</li>
<li>
<a href="{{ route('admin.fitur.index') }}"
class="block py-2 px-2 rounded {{ request()->is('admin/fitur*') ? 'bg-blue-100 text-blue-600' : 'text-gray-500 hover:text-blue-600 hover:bg-blue-50' }}">
Fitur
</a>
</li>
</ul>
</li>
<!-- Dropdown Templat -->
<li>
@php
$isTemplatePage = request()->is('templates*'); // cek apakah sedang di halaman template
@endphp
<button
class="w-full flex items-center justify-between py-2 px-3 text-gray-700 rounded hover:bg-blue-50 {{ $isTemplatePage ? 'bg-blue-100 text-blue-600' : '' }}"
id="templatBtn">
<span><i class="bi bi-card-list me-2"></i>Manajemen Templat</span>
<i class="bi bi-chevron-down transition-transform {{ $isTemplatePage ? 'rotate-180' : '' }}"
id="templatIcon"></i>
</button>
<ul class="submenu pl-6 space-y-1 {{ $isTemplatePage ? 'show' : '' }}" id="templatSubmenu">
<li>
<a href="{{ route('templates.index') }}"
class="block py-2 px-2 rounded {{ request()->is('templates') ? 'bg-blue-100 text-blue-600' : 'text-gray-500 hover:text-blue-600 hover:bg-blue-50' }}">
Semua Template
</a>
</li>
@foreach (\App\Models\Kategori::all() as $kategori)
<li>
<a href="{{ route('templates.byKategori', $kategori->id) }}"
class="block py-2 px-2 rounded {{ request()->is('templates/kategori/' . $kategori->id) ? 'bg-blue-100 text-blue-600' : 'text-gray-500 hover:text-blue-600 hover:bg-blue-50' }}">
{{ $kategori->nama }}
</a>
</li>
@endforeach
</ul>
</li>
<li>
<a href="{{ route('admin.pelanggan.index') }}"
class="flex items-center py-2 px-3 rounded hover:bg-blue-50 {{ request()->is('admin/pelanggan*') ? 'bg-blue-100 text-blue-600' : 'text-gray-700' }}">
<i class="bi bi-people me-2"></i> Pelanggan
</a>
</li>
<li>
<a href="{{ route('admin.reviews.index') }}"
class="flex items-center py-2 px-3 rounded hover:bg-blue-50 {{ request()->is('admin/ulasan') ? 'bg-blue-100 text-blue-600' : 'text-gray-700' }}">
<i class="bi bi-chat-dots me-2"></i> Ulasan
</a>
</li>
</ul>
<p class="text-gray-500 uppercase text-xs font-semibold px-3 mt-4 mb-2">Akun</p>
<ul class="px-2 space-y-1">
<li>
<a href="{{ route('admin.logout') }}"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"
class="flex items-center py-2 px-3 text-red-600 font-semibold hover:bg-red-50 rounded">
<i class="bi bi-box-arrow-right me-2"></i> Keluar
</a>
<form id="logout-form" action="{{ route('admin.logout') }}" method="POST" class="hidden">
@csrf
</form>
</li>
</ul>
</div>
</div>
<!-- MAIN CONTENT -->
<div class="flex-1 ml-[250px] p-6">
@yield('content')
</div>
<script>
// Dropdown Templat
const templatBtn = document.getElementById('templatBtn');
const templatSubmenu = document.getElementById('templatSubmenu');
const templatIcon = document.getElementById('templatIcon');
templatBtn.addEventListener('click', () => {
templatSubmenu.classList.toggle('show');
templatIcon.classList.toggle('rotate-180');
});
// Dropdown Manajemen Fitur
const manajemenFiturBtn = document.getElementById('manajemenFiturBtn');
const manajemenFiturSubmenu = document.getElementById('manajemenFiturSubmenu');
const manajemenFiturIcon = document.getElementById('manajemenFiturIcon');
manajemenFiturBtn.addEventListener('click', () => {
manajemenFiturSubmenu.classList.toggle('show');
manajemenFiturIcon.classList.toggle('rotate-180');
});
</script>
</body>
</html>

View File

@ -2,10 +2,6 @@
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\KategoriApiController;
use App\Http\Controllers\Api\TemplateApiController;
use App\Http\Controllers\Api\FormApiController;
use App\Http\Controllers\Api\ReviewController;
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
@ -21,29 +17,3 @@ use App\Http\Controllers\Api\ReviewController;
Route::middleware('auth:sanctum')->get('/user', function (Request $request) { Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user(); return $request->user();
}); });
// ===============================
// API Kategori (read-only)
// ===============================
Route::get('kategoris', [KategoriApiController::class, 'index']);
Route::get('kategoris/{kategori}', [KategoriApiController::class, 'show']);
// ===============================
// API Reviews
// ===============================
Route::apiResource('reviews', ReviewController::class);
// ===============================
// API Templates
// ===============================
Route::get('templates/random', [TemplateApiController::class, 'random']); // random template
Route::get('templates', [TemplateApiController::class, 'index']);
Route::get('templates/{template}', [TemplateApiController::class, 'show']);
Route::get('templates/category/{id}', [TemplateApiController::class, 'byCategory']);
Route::get('/templates/{id}', [TemplateApiController::class, 'show']); // duplicate tapi ga masalah
// ===============================
// API Form (user submit)
// ===============================
Route::post('form', [FormApiController::class, 'store']); // <<== INI yang ditambah
Route::get('templates/{id}/fiturs', [FormApiController::class, 'getFiturs']);

View File

@ -1,77 +1,18 @@
<?php <?php
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AdminAuthController;
use App\Http\Controllers\KategoriController;
use App\Http\Controllers\FiturController;
use App\Http\Controllers\TemplateController;
use App\Http\Controllers\PelangganController;
use App\Http\Controllers\Api\ReviewController;
use App\Models\Review;
use App\Http\Controllers\DashboardController;
// Redirect ke login admin /*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "web" middleware group. Make something great!
|
*/
Route::get('/', function () { Route::get('/', function () {
return redirect()->route('admin.login'); return view('welcome');
});
// Admin Auth
Route::prefix('admin')->name('admin.')->group(function () {
Route::middleware('guest:admin')->group(function () {
Route::get('/login', [AdminAuthController::class, 'showLogin'])->name('login');
Route::post('/login', [AdminAuthController::class, 'login'])->name('login.post');
});
Route::middleware('auth:admin')->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('logout');
});
});
// Admin Kategori
Route::prefix('admin')->name('admin.')->group(function () {
Route::resource('kategori', KategoriController::class);
});
// Admin Fitur
Route::prefix('admin')->name('admin.')->group(function () {
Route::resource('fitur', FiturController::class);
});
// Admin Template
Route::prefix('admin')->group(function () {
Route::get('templates', [TemplateController::class, 'index'])->name('templates.index');
Route::post('templates', [TemplateController::class, 'store'])->name('templates.store');
Route::put('templates/{template}', [TemplateController::class, 'update'])->name('templates.update');
Route::delete('templates/{template}', [TemplateController::class, 'destroy'])->name('templates.destroy');
Route::get('templates/kategori/{id}', [TemplateController::class, 'byKategori'])->name('templates.byKategori');
});
// Admin Pelanggan
Route::prefix('admin')->name('admin.')->group(function () {
Route::resource('pelanggan', PelangganController::class)->only([
'index',
'show',
'destroy'
]);
});
// Admin Review
Route::prefix('admin')->name('admin.')->middleware('auth:admin')->group(function () {
Route::get('/ulasan', function () {
$reviews = Review::latest()->get();
return view('admin.reviews.index', compact('reviews'));
})->name('reviews.index');
Route::post('/ulasan', [ReviewController::class, 'store'])->name('reviews.store');
Route::put('/ulasan/{review}', [ReviewController::class, 'update'])->name('reviews.update');
Route::delete('/ulasan/{review}', function (Review $review) {
$review->delete();
return redirect()->route('admin.reviews.index')->with('success', 'Ulasan berhasil dihapus');
})->name('reviews.destroy');
});
// Admin Kategori Fitur
Route::prefix('admin')->name('admin.')->group(function () {
Route::resource('kategori_fitur', \App\Http\Controllers\KategoriFiturController::class);
}); });

View File

@ -1,35 +1,5 @@
<template> <template>
<div> <div>
<NuxtLayout>
<NuxtPage /> <NuxtPage />
</NuxtLayout>
</div> </div>
</template> </template>
<!-- <script setup>
const props = defineProps({
category: {
type: String,
required: true,
},
id_category: {
type: Number,
required: true
}
}) -->
// const templates = ref([])
// const fetchTemplates = async () => {
// try {
// templates.value = await $fetch(`http://localhost:8000/api/templates/category/${props.id_category}`)
// } catch (error) {
// console.error('Gagal ambil template:', error)
// }
// }
// onMounted(() => {
// fetchTemplates()
// })
// defineEmits(['back']);

View File

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

View File

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

View File

@ -1,351 +0,0 @@
<template>
<div class="max-w-5xl mx-auto p-8 bg-gradient-to-b from-pink-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-purple-700 drop-shadow-sm">
🎂 Form Pemesanan Undangan Ulang Tahun 🎉
</h1>
<p class="text-gray-600 mt-2">
Isi data berikut dengan lengkap untuk melakukan pemesanan undangan ulang tahun.
</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 -->
<!-- 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 flex items-center gap-2">
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Pemesan Undangan
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="relative">
<input v-model="form.nama_pemesan" type="text" id="nama_pemesan" 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="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 -->
<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> Data Anak
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="relative">
<input v-model="form.nama_lengkap_anak" type="text" id="nama_lengkap_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="nama_lengkap_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 Lengkap Anak
</label>
</div>
<div class="relative">
<input v-model="form.nama_panggilan_anak" type="text" id="nama_panggilan_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="nama_panggilan_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 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 -->
<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> Jadwal Acara
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="relative">
<input v-model="form.hari_tanggal_acara" type="date" id="hari_tanggal_acara" 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="hari_tanggal_acara"
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Hari & Tanggal</label>
</div>
<div class="relative">
<input v-model="form.waktu_acara" type="text" id="waktu_acara" 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="waktu_acara"
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Waktu Acara</label>
</div>
<div class="relative md:col-span-2">
<textarea v-model="form.alamat_acara" id="alamat_acara" rows="3" 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"></textarea>
<label for="alamat_acara"
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Alamat Lengkap</label>
</div>
<div class="relative md:col-span-2">
<input v-model="form.maps_acara" type="text" id="maps_acara" 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-blue-500" />
<label for="maps_acara"
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Link Google Maps (Opsional)</label>
</div>
</div>
</section>
<!-- Informasi Tambahan -->
<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> Informasi Tambahan
</h2>
<div class="relative">
<input v-model="form.link_musik" type="text" id="link_musik" 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-blue-500" />
<label for="link_musik"
class="absolute left-2 top-1 text-gray-500 text-xs peer-focus:text-blue-600">Link Musik (Opsional)</label>
</div>
</section>
<!-- Galeri -->
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
<h2 class="text-lg font-bold text-gray-800 mb-4">Galeri (max 5 gambar)</h2>
<input type="file" multiple accept="image/*" @change="handleFileUpload" class="hidden" id="gallery-upload" />
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
<div v-for="(img, i) in previewImages" :key="i"
class="relative w-full aspect-square rounded-lg overflow-hidden shadow-sm group">
<img :src="img" alt="Preview" class="object-cover w-full h-full" />
<button type="button" @click="removeImage(i)"
class="absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity font-bold"
aria-label="Hapus gambar">
&times;
</button>
</div>
<label v-if="previewImages.length < 5" for="gallery-upload"
class="flex items-center justify-center w-full aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
</label>
</div>
</div>
</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>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
// 1. STRUKTUR DATA DISESUAIKAN DENGAN BACKEND
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_anak: "",
nama_panggilan_anak: "",
bapak_anak: "",
ibu_anak: "",
umur_dirayakan: "",
anak_ke: "",
hari_tanggal_acara: "",
waktu_acara: "",
alamat_acara: "",
maps_acara: "",
link_musik: "",
galeri: [],
});
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);
} 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 (form.value[key] !== null && form.value[key] !== undefined) {
body.append(key, form.value[key]);
}
}
// 2. ENDPOINT API DIPERBAIKI
await $fetch("http://localhost:8000/api/form/ulang-tahun", {
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;
} catch (err) {
console.error(err);
error.value = true;
} finally {
loading.value = false;
}
};
</script>

View File

@ -7,7 +7,7 @@
<!-- Layout gambar + teks --> <!-- Layout gambar + teks -->
<div class="about-layout"> <div class="about-layout">
<div class="about-image"> <div class="about-image">
<img src="/rectangle.png" alt="Tentang Kami - Undangan Digital" /> <img src="/Rectangle.png" alt="Tentang Kami - Undangan Digital" />
</div> </div>
<div class="about-text"> <div class="about-text">
<p> <p>

View File

@ -1,108 +1,175 @@
<script setup>
import { ref, computed } from 'vue'
// id template yang mau ditampilkan
const selectedIds = [1, 2, 3, 5, 6, 8]
// state dropdown
const openDropdownId = ref(null)
const toggleDropdown = (templateId) => {
openDropdownId.value = openDropdownId.value === templateId ? null : templateId
}
// fetch API dari Laravel
const { data: templatesData, error } = await useFetch('http://localhost:8000/api/templates')
// filter hanya id tertentu
const templates = computed(() =>
(templatesData.value || []).filter(t => selectedIds.includes(t.id))
)
</script>
<template> <template>
<section id="template" class="py-16 px-5 text-center"> <section id="template" class="feature-section">
<!-- Header --> <div class="featured-header">
<div class="mb-10"> <h2>Templat Unggulan</h2>
<h2 class="text-[2.9rem] font-bold mb-6">Templat Unggulan</h2> <p>"Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya."</p>
<p class="text-gray-600 text-lg mb-10">
"Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya."
</p>
</div> </div>
<!-- Grid Template --> <!-- Grid Template -->
<div v-if="templates.length" class="grid gap-8 max-w-[1100px] mx-auto grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> <div class="template-grid">
<div <div class="template-card" v-for="i in 6" :key="i">
v-for="t in templates" <div class="template-image">
:key="t.id" <img src="/templat.jpg" alt="Template" />
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300"
>
<!-- Image -->
<img
:src="`http://localhost:8000${t.foto}`"
:alt="t.nama_template"
class="w-full h-48 object-cover"
/>
<!-- Body -->
<div class="p-5 text-center">
<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">
Rp {{ Number(t.harga).toLocaleString('id-ID') }}
</p>
<!-- Dropdown fitur -->
<div v-if="t.fiturs && t.fiturs.length > 0" class="relative mb-4">
<button
@click="toggleDropdown(t.id)"
class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 inline-flex justify-between items-center"
>
<span class="mx-auto text-gray-700 font-semibold">FITUR YANG TERSEDIA</span>
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<div v-if="openDropdownId === t.id">
<ul class="mt-4 space-y-2 text-gray-600 text-left">
<li v-for="f in t.fiturs" :key="f.id" class="flex items-center">
<svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
{{ f.deskripsi }}
</li>
</ul>
</div>
</div> </div>
<!-- Buttons --> <div class="template-body">
<div class="flex items-center gap-3 mt-6"> <h3 class="template-title">Golf Party</h3>
<button <p class="template-price">Rp.89.000</p>
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"
> <select class="template-select">
Preview <option>Fitur Terbaik</option>
</button> <option>Fitur Lengkap</option>
<NuxtLink </select>
:to="`/form/${t.kategori.nama.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" <div class="button-group">
> <button class="btn btn-preview">Preview</button>
Order <button class="btn btn-order">Order</button>
</NuxtLink>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Jika error --> <div class="see-more">
<div v-else class="text-gray-500">Tidak ada template yang bisa ditampilkan</div> <a href="#">Lihat Selengkapnya...</a>
<!-- See more -->
<div class="mt-8 text-right max-w-[1100px] mx-auto">
<NuxtLink
to="/template"
class="text-blue-600 font-medium hover:underline"
>
Lihat Selengkapnya...
</NuxtLink>
</div> </div>
</section> </section>
</template> </template>
<style scoped>
.featured-section {
padding: 60px 20px;
text-align: center;
}
.featured-header h2 {
font-size: 2.9rem;
font-weight: bold;
margin-bottom: 30px;
text-align: center;
}
.featured-header p {
color: #555;
margin-bottom: 40px;
font-size: 17px;
text-align: center;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 24px;
max-width: 1100px;
margin: 0 auto;
}
.template-card {
background: #f2f2f2;
border-radius: 5px;
padding: 16px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
align-items: center;
min-height: 400px;
overflow: hidden;
}
.template-image {
background: #fff;
border-radius: 10px;
width: 100%;
height: 180px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.template-image img {
max-height: 100%;
object-fit: contain;
}
.template-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
margin-top: 12px;
gap: 10px;
}
.template-title {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.template-price {
color: #008000;
font-weight: bold;
margin: 0;
}
.template-select {
width: 100%;
padding: 8px;
border-radius: 6px;
border: 1px solid #ccc;
}
.button-group {
display: flex;
gap: 10px;
width: 100%;
}
.btn {
flex: 1;
padding: 10px 0;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
border: none;
transition: 0.2s;
}
.btn-preview {
background: #e5e5e5;
}
.btn-preview:hover {
background: #d6d6d6;
}
.btn-order {
background: #2563eb;
color: #fff;
}
.btn-order:hover {
background: #1d4ed8;
}
.see-more {
margin-top: 30px;
text-align: right;
max-width: 1100px;
margin-left: auto;
margin-right: auto;
}
.see-more a {
color: #2563eb;
font-weight: 500;
text-decoration: none;
}
.see-more a:hover {
text-decoration: underline;
}
</style>

View File

@ -8,7 +8,7 @@
<div class="footer-column"> <div class="footer-column">
<h4>Layanan</h4> <h4>Layanan</h4>
<ul class="footer-links"> <ul class="footer-links">
<li><a href="/template/Pernikahan">Template Undangan Pernikahan</a></li> <li><a href="#">Template Undangan Pernikahan</a></li>
<li><a href="#">Template Undangan Khitan</a></li> <li><a href="#">Template Undangan Khitan</a></li>
<li><a href="#">Template Undangan Ulang Tahun</a></li> <li><a href="#">Template Undangan Ulang Tahun</a></li>
</ul> </ul>
@ -19,42 +19,19 @@
<ul class="footer-contact-list"> <ul class="footer-contact-list">
<li> <li>
<a href="#" class="social-link"> <a href="#" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v2.385z"/></svg>
<path
d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v2.385z" />
</svg>
<span>ABBAUF TECH</span> <span>ABBAUF TECH</span>
</a> </a>
</li> </li>
<li> <li>
<a href="https://instagram.com/abbauftech" target="_blank" class="inline-flex items-center gap-2 font-medium <a href="#" class="social-link">
bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500 <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.85s-.011 3.584-.069 4.85c-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07s-3.584-.012-4.85-.07c-3.252-.148-4.771-1.691-4.919-4.919-.058-1.265-.069-1.645-.069-4.85s.011-3.584.069-4.85c.149-3.225 1.664-4.771 4.919-4.919 1.266-.057 1.644-.069 4.85-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948s.014 3.667.072 4.947c.2 4.359 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072s3.667-.014 4.947-.072c4.359-.2 6.78-2.618 6.98-6.98.058-1.281.072-1.689.072-4.948s-.014-3.667-.072-4.947c-.2-4.359-2.618-6.78-6.98-6.98-1.281-.058-1.689-.072-4.948-.072zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.162 6.162 6.162 6.162-2.759 6.162-6.162-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4s1.791-4 4-4 4 1.79 4 4-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.441 1.441 1.441 1.441-.645 1.441-1.441-.645-1.44-1.441-1.44z"/></svg>
bg-clip-text text-transparent <span>@abbauf_tech</span>
hover:scale-110 transition-all duration-300">
<!-- Ikon Instagram -->
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24"
fill="url(#igGradient)">
<defs>
<linearGradient id="igGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#8a3ab9" />
<stop offset="50%" style="stop-color:#e95950" />
<stop offset="100%" style="stop-color:#fccc63" />
</linearGradient>
</defs>
<path d="M7.75 2h8.5A5.75 5.75 0 0 1 22 7.75v8.5A5.75 5.75 0 0 1 16.25 22h-8.5A5.75 5.75 0 0 1 2 16.25v-8.5A5.75 5.75 0 0 1 7.75 2zm0 1.5A4.25 4.25 0 0 0 3.5 7.75v8.5A4.25 4.25 0 0 0 7.75 20.5h8.5a4.25 4.25 0 0 0 4.25-4.25v-8.5A4.25 4.25 0 0 0 16.25 3.5h-8.5zm4.25 3a5.75 5.75 0 1 1 0 11.5 5.75 5.75 0 0 1 0-11.5zm0 1.5a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5zm5-2.25a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
</svg>
<!-- Nama IG -->
<span>@abbauftech</span>
</a> </a>
</li> </li>
<li> <li>
<a href="https://www.linkedin.com/posts/abbauf-tech_abbauftech-digitalstrategy-itconsulting-activity-7277021563982340099-8Byg" class="social-link"> <a href="#" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5c0 1.381-1.11 2.5-2.48 2.5s-2.48-1.119-2.48-2.5c0-1.38 1.11-2.5 2.48-2.5s2.48 1.12 2.48 2.5zm.02 4.5h-5v16h5v-16zm7.982 0h-4.968v16h4.969v-8.399c0-4.67 6.029-5.052 6.029 0v8.399h4.988v-10.131c0-7.88-8.922-7.593-11.018-3.714v-2.155z"/></svg>
<path
d="M4.98 3.5c0 1.381-1.11 2.5-2.48 2.5s-2.48-1.119-2.48-2.5c0-1.38 1.11-2.5 2.48-2.5s2.48 1.12 2.48 2.5zm.02 4.5h-5v16h5v-16zm7.982 0h-4.968v16h4.969v-8.399c0-4.67 6.029-5.052 6.029 0v8.399h4.988v-10.131c0-7.88-8.922-7.593-11.018-3.714v-2.155z" />
</svg>
<span>ABBAUF TECH</span> <span>ABBAUF TECH</span>
</a> </a>
</li> </li>
@ -65,41 +42,26 @@
<h4>Kontak Kami</h4> <h4>Kontak Kami</h4>
<ul class="footer-contact-list"> <ul class="footer-contact-list">
<li> <li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3v18h24v-18h-24zm21.518 2l-9.518 7.713-9.518-7.713h19.036zm-19.518 14v-11.817l10 8.104 10-8.104v11.817h-20z"/></svg>
<path
d="M0 3v18h24v-18h-24zm21.518 2l-9.518 7.713-9.518-7.713h19.036zm-19.518 14v-11.817l10 8.104 10-8.104v11.817h-20z" />
</svg>
<a href="mailto:contact@abbauf.com">contact@abbauf.com</a> <a href="mailto:contact@abbauf.com">contact@abbauf.com</a>
</li> </li>
<li> <li>
<a :href="waUrl" target="_blank" <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20 22.621l-3.521-6.795c-.008.004-1.974.97-2.064 1.011-2.24 1.086-6.799-3.473-5.712-5.713.041-.09 1.011-2.064 1.011-2.064l-6.795-3.521-2.918 2.918c-1.603 1.603-1.425 4.933 1.011 7.37 4.301 4.301 9.962 3.593 12.301.954l2.688-2.355-2.356-2.688z"/></svg>
class="inline-flex items-center gap-2 hover:text-green-600 transition-colors"> <a href="tel:02127617679">(021) 2761-7679</a>
<!-- Ikon WhatsApp -->
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path
d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.894 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198 -.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.14-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579 -.487-.5-.669-.51-.173-.008-.371-.01-.57-.01s-.52.074-.792.372 c-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.626.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29 .173-1.414z" />
</svg>
<!-- Nomor WA -->
<span>+62 878-7711-7847</span>
</a>
</li> </li>
<li> <li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.894 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01s-.52.074-.792.372c-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.626.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414z"/></svg>
<path <a href="#">+62 878-7711-7847</a>
d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z" /> </li>
</svg> <li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z"/></svg>
<div> <div>
<strong>Alamat Kantor Pusat</strong> <strong>Alamat Kantor Pusat</strong>
<p>APL Tower Central Park Lantai 19 Unit T7, Jalan Letjen S. Parman, Kavling 28, RT. 012/006, Kel. Tanjung <p>APL Tower Central Park Lantai 19 Unit T7, Jalan Letjen S. Parman, Kavling 28, RT. 012/006, Kel. Tanjung Duren, Kec. Grogol Petamburan, Jakarta Barat, DKI Jakarta 11470, ID</p>
Duren, Kec. Grogol Petamburan, Jakarta Barat, DKI Jakarta 11470, ID</p>
</div> </div>
</li> </li>
<li> <li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z"/></svg>
<path
d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z" />
</svg>
<div> <div>
<strong>Alamat Studio</strong> <strong>Alamat Studio</strong>
<p>Jl. Adhi Karya No. 57 RT 003 RW 015, Kel. Depok, Kec. Pancoran Mas, Depok 16431</p> <p>Jl. Adhi Karya No. 57 RT 003 RW 015, Kel. Depok, Kec. Pancoran Mas, Depok 16431</p>
@ -116,20 +78,15 @@
</template> </template>
<script setup> <script setup>
// Tidak ada script yang dibutuhkan untuk footer statis ini
const adminNumber = "62895602603247";
const defaultMessage = "Halo Admin, saya mau tanya tentang undangan digital 🙏";
const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMessage)}`;
</script> </script>
<style scoped> <style scoped>
.site-footer { .site-footer {
width: 100%; background-color: #f0f2f5; /* Warna abu-abu muda */
background-color: #f0f2f5;
color: #333; color: #333;
padding: 50px 0 20px 0; padding: 50px 0 20px 0;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif; /* Menggunakan font yang terlihat modern */
} }
.container { .container {
@ -140,8 +97,7 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
.footer-content { .footer-content {
display: grid; display: grid;
grid-template-columns: 2fr 1.5fr 1.5fr 2.5fr; grid-template-columns: 2fr 1.5fr 1.5fr 2.5fr; /* Mengatur lebar kolom */
/* Mengatur lebar kolom */
gap: 40px; gap: 40px;
padding-bottom: 30px; padding-bottom: 30px;
border-bottom: 1px solid #d9dce1; border-bottom: 1px solid #d9dce1;
@ -159,8 +115,7 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
color: #111; color: #111;
} }
.footer-links, .footer-links, .footer-contact-list {
.footer-contact-list {
list-style: none; list-style: none;
padding: 0; padding: 0;
margin: 0; margin: 0;
@ -170,17 +125,14 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
margin-bottom: 12px; margin-bottom: 12px;
} }
.footer-links a, .footer-links a, .social-link {
.social-link {
text-decoration: none; text-decoration: none;
color: #555; color: #555;
transition: color 0.3s ease; transition: color 0.3s ease;
} }
.footer-links a:hover, .footer-links a:hover, .social-link:hover {
.social-link:hover { color: #0d6efd; /* Biru Primer */
color: #0d6efd;
/* Biru Primer */
} }
.footer-contact-list li { .footer-contact-list li {
@ -200,7 +152,6 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
text-decoration: none; text-decoration: none;
color: #555; color: #555;
} }
.footer-contact-list a:hover { .footer-contact-list a:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -233,27 +184,21 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
/* Penyesuaian untuk layar kecil (Mobile) */ /* Penyesuaian untuk layar kecil (Mobile) */
@media (max-width: 992px) { @media (max-width: 992px) {
.footer-content { .footer-content {
grid-template-columns: 1fr 1fr; grid-template-columns: 1fr 1fr; /* 2 kolom di tablet */
/* 2 kolom di tablet */
} }
} }
@media (max-width: 768px) { @media (max-width: 768px) {
.footer-content { .footer-content {
grid-template-columns: 1fr; grid-template-columns: 1fr; /* 1 kolom di mobile */
/* 1 kolom di mobile */
text-align: center; text-align: center;
} }
.footer-brand { .footer-brand {
margin-bottom: 20px; margin-bottom: 20px;
} }
.footer-logo { .footer-logo {
margin: 0 auto; margin: 0 auto; /* Logo di tengah */
/* Logo di tengah */
} }
.footer-contact-list li { .footer-contact-list li {
align-items: center; align-items: center;
text-align: left; text-align: left;

View File

@ -2,14 +2,12 @@
<header class="main-header"> <header class="main-header">
<nav class="container"> <nav class="container">
<div class="logo"> <div class="logo">
<NuxtLink to="/" class="logo-link"> <img src="/abbauflogo.png" alt="Abbauf Tech Logo" class="logo-icon">
<img :src="logo" alt="Abbauf Tech Logo" class="logo-icon" />
<span>ABBAUF TECH</span> <span>ABBAUF TECH</span>
</NuxtLink>
</div> </div>
<ul class="nav-links"> <ul class="nav-links">
<li v-for="link in navLinks" :key="link.name"> <li v-for="link in navLinks" :key="link.name">
<NuxtLink :to="link.path">{{ link.name }}</NuxtLink> <a :href="link.path">{{ link.name }}</a>
</li> </li>
</ul> </ul>
</nav> </nav>
@ -18,37 +16,23 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
const logo = '/abbauflogo.png';
const navLinks = ref([ const navLinks = ref([
{ name: 'Beranda', path: '/' }, { name: 'Beranda', path: '#beranda' },
{ name: 'Tentang Kami', path: '/#tentang-kami' }, { name: 'Tentang Kami', path: '#tentang-kami' },
{ name: 'Templat', path: '/template' }, { name: 'Templat', path: '#template' },
{ name: 'Panduan', path: '/#cara' }, { name: 'Panduan', path: '#cara' },
{ name: 'Keistimewaan', path: '/#keistimewaan' }, { name: 'Keistimewaan', path: '#keistimewaan' },
{ name: 'Testimoni', path: '/#testimoni' }, { name: 'Testimoni', path: '#testimoni' },
]); ]);
</script> </script>
<style scoped> <style scoped>
/* ================= NAVBAR ================= */
.main-header { .main-header {
background-color: #eaf2ff; background-color: #eaf2ff;
padding: 15px 0; padding: 15px 0;
border-bottom: 1px solid #d4e3ff; border-bottom: 1px solid #d4e3ff;
width: 100%; width: 100%;
/* FIXED NAVBAR */
position: fixed;
top: 0;
left: 0;
z-index: 1000; /* pastikan di atas konten */
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
}
/* Supaya konten di bawah navbar tidak tertutup */
body {
padding-top: 70px; /* sesuaikan dengan tinggi navbar */
} }
.container { .container {
@ -60,17 +44,26 @@ body {
align-items: center; align-items: center;
} }
.logo a.logo-link { .logo {
display: flex; display: flex;
align-items: center; align-items: center;
font-weight: bold; font-weight: bold;
font-size: 1.5rem; font-size: 1.5rem;
color: #0d6efd; color: #0d6efd;
text-decoration: none;
} }
.main-header {
position: fixed;
top: 0;
left: 0;
}
.logo-icon { .logo-icon {
width: 40px; width: 55px;
margin-right: 10px; margin-right: 10px;
} }

View File

@ -4,7 +4,7 @@
<div class="w-full text-center lg:w-1/2"> <div class="w-full text-center lg:w-1/2">
<h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl pl-15"> <h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl ml-14">
Buat Undangan Digital Praktis Untuk Buat Undangan Digital Praktis Untuk
<div class="h-24 flex items-center justify-center"> <div class="h-24 flex items-center justify-center">
@ -16,42 +16,20 @@
Tanpa Ribet Tanpa Ribet
</h1> </h1>
<p class="mt-4 mb-8 text-lg text-gray-600 pl-15"> <p class="mt-4 mb-8 text-lg text-gray-600 ml-14">
Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu. Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu. Praktis, cepat, dan bisa langsung digunakan.
Praktis, cepat, dan bisa langsung digunakan.
</p> </p>
<div class="flex flex-col justify-center gap-4 sm:flex-row pl-15"> <div class="flex flex-col justify-center gap-4 sm:flex-row ml-14">
<a href="#" class="inline-flex items-center justify-center rounded-lg border-2 border-green-500 bg-white px-8 py-3 font-bold text-gray-800 shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md">
<a :href="waUrl" target="_blank" <svg class="mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
class="inline-flex items-center justify-center rounded-lg border-2 border-green-500 bg-white px-8 py-3 font-bold text-gray-800 shadow-md transition-all duration-300 transform hover:-translate-y-1 hover:shadow-lg hover:bg-green-500 hover:text-white"> <path d="M19.2239 4.7761C17.1659 2.7181 14.6599 1.5 11.9999 1.5...Z" fill="#25D366"/>
<path d="M16.7441 14.968C16.5331 15.538...Z" fill="white"/>
<!-- Ikon WhatsApp -->
<svg class="mr-2 w-6 h-6 transition-colors duration-300" viewBox="0 0 32 32"
xmlns="http://www.w3.org/2000/svg">
<path class="transition-colors duration-300" fill="#25D366"
d="M16 .4a15.6 15.6 0 0 0-13.6 23.8L.4 31.6l7.6-2a15.7 15.7 0 0 0 8 2.2c8.8 0 15.6-7.2 15.6-15.6S24.8.4 16 .4z" />
<path class="transition-colors duration-300" fill="#FFF"
d="M25.2 22.4c-.4 1-2.2 1.8-3 1.8-.8.2-1.8.4-6.2-1.4-5.2-2.2-8.6-7.4-8.8-7.6-.2-.2-2-2.6-2-5s1.2-3.6 1.6-4c.4-.4.8-.6 1.2-.6h.8c.2 0 .6 0 .8.6.4 1 .8 2 .8 2.2.2.4 0 .8-.2 1.2-.2.2-.4.6-.6.8-.2.2-.4.4-.2.8.4.8 1.6 2.6 3.4 4 .2.2 3.6 3 7 .4.4-.2.8-.2 1.2 0 .4.2 2.6 1.2 3 1.4.4.2.6.2.8.4.2.2.2 1 0 2z" />
</svg> </svg>
<span>Hubungi Kami</span> <span>Hubungi Kami</span>
</a> </a>
<a href="#" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-8 py-3 font-bold text-white shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-blue-700">
<a href="/template" class="inline-flex justify-center items-center
rounded-xl
bg-gradient-to-r from-blue-600 to-blue-700
px-10 py-3
font-bold text-white
shadow-lg shadow-blue-500/30
transition-all duration-300
hover:scale-110 hover:shadow-2xl hover:shadow-blue-600/50 hover:from-blue-700 hover:to-blue-800">
Lihat Templat Lihat Templat
</a> </a>
</div> </div>
</div> </div>
@ -85,12 +63,6 @@ onMounted(() => {
onUnmounted(() => { onUnmounted(() => {
clearInterval(intervalId); clearInterval(intervalId);
}); });
const adminNumber = "62895602603247";
const defaultMessage = "Halo Admin, saya mau tanya tentang undangan digital 🙏";
const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMessage)}`;
</script> </script>
<style scoped> <style scoped>

View File

@ -4,264 +4,66 @@
<h2 class="text-4xl font-extrabold text-gray-800 mb-2"> <h2 class="text-4xl font-extrabold text-gray-800 mb-2">
Apa Kata Mereka? Apa Kata Mereka?
</h2> </h2>
<p class="text-lg text-gray-600 mb-10"> <p class="text-lg text-gray-600 mb-16">
Kisah sukses dari para pengguna yang telah mempercayakan momen spesialnya kepada kami. Kisah sukses dari para pengguna yang telah mempercayakan momen spesialnya kepada kami.
</p> </p>
<!-- CSS Marquee Scroll --> <div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
<div class="marquee-container mb-10">
<div class="marquee-content" :style="{ '--total-cards': testimonials?.length || 0 }">
<!-- Render original cards -->
<div <div
v-for="testimonial in testimonials" v-for="testimonial in testimonials"
:key="`original-${testimonial.id}`" :key="testimonial.id"
class="testimonial-card" class="flex flex-col rounded-xl bg-white p-8 text-left shadow-lg transition-transform duration-300 hover:-translate-y-2 hover:shadow-2xl"
@click="previewModal = testimonial"
> >
<!-- Rating -->
<div class="mb-4 flex items-center"> <div class="mb-4 flex items-center">
<svg <svg v-for="n in 5" :key="n" class="h-5 w-5" :class="n <= testimonial.rating ? 'text-yellow-400' : 'text-gray-300'" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
v-for="n in 5"
:key="n"
class="h-5 w-5"
:class="n <= Number(testimonial.rating) ? 'text-yellow-400' : 'text-gray-300'"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07
3.292a1 1 0 00.95.69h3.462c.969 0 1.371
1.24.588 1.81l-2.8 2.034a1 1 0
00-.364 1.118l1.07 3.292c.3.921-.755
1.688-1.54 1.118l-2.8-2.034a1 1
0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1
1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1
1 0 00.951-.69l1.07-3.292z"
/>
</svg>
</div> </div>
<!-- Pesan --> <p class="mb-6 flex-grow text-gray-600 italic">"{{ testimonial.text }}"</p>
<p class="mb-6 flex-grow text-gray-600 italic line-clamp-3 min-h-[72px] break-words">
"{{ testimonial.message }}"
</p>
<!-- User Info --> <div class="flex items-center">
<div> <img class="h-12 w-12 rounded-full object-cover" :src="testimonial.avatar" :alt="testimonial.name">
<div class="ml-4">
<h4 class="font-bold text-gray-800">{{ testimonial.name }}</h4> <h4 class="font-bold text-gray-800">{{ testimonial.name }}</h4>
<p class="text-sm text-gray-500">{{ testimonial.city }}</p> <p class="text-sm text-gray-500">{{ testimonial.role }}</p>
</div>
</div>
<!-- Render clone untuk seamless loop -->
<div
v-for="testimonial in testimonials"
:key="`clone-${testimonial.id}`"
class="testimonial-card"
@click="previewModal = testimonial"
>
<!-- Rating -->
<div class="mb-4 flex items-center">
<svg
v-for="n in 5"
:key="n"
class="h-5 w-5"
:class="n <= Number(testimonial.rating) ? 'text-yellow-400' : 'text-gray-300'"
fill="currentColor"
viewBox="0 0 20 20"
>
<path
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07
3.292a1 1 0 00.95.69h3.462c.969 0 1.371
1.24.588 1.81l-2.8 2.034a1 1 0
00-.364 1.118l1.07 3.292c.3.921-.755
1.688-1.54 1.118l-2.8-2.034a1 1
0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1
1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1
1 0 00.951-.69l1.07-3.292z"
/>
</svg>
</div>
<!-- Pesan -->
<p class="mb-6 flex-grow text-gray-600 italic line-clamp-3 min-h-[72px] break-words">
"{{ testimonial.message }}"
</p>
<!-- User Info -->
<div>
<h4 class="font-bold text-gray-800">{{ testimonial.name }}</h4>
<p class="text-sm text-gray-500">{{ testimonial.city }}</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Tombol Berikan Ulasan -->
<button
@click="openModal = true"
class="px-6 py-3 rounded-lg bg-blue-500 text-white font-semibold shadow hover:bg-blue-700 transition"
>
Berikan Ulasan
</button>
</div>
<!-- Modal Form -->
<div
v-if="openModal"
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/30"
>
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6 relative">
<button
@click="openModal = false"
class="absolute top-3 right-3 text-gray-500 hover:text-gray-800"
>
</button>
<h3 class="text-xl font-bold mb-4 text-gray-800">Tulis Ulasan</h3>
<form @submit.prevent="submitReview">
<div class="mb-4 text-left">
<label class="block text-sm font-medium mb-1">Nama</label>
<input v-model="form.name" type="text" class="w-full rounded border px-3 py-2" required />
</div>
<div class="mb-4 text-left">
<label class="block text-sm font-medium mb-1">Kota</label>
<input v-model="form.city" type="text" class="w-full rounded border px-3 py-2" required />
</div>
<div class="mb-4 text-left">
<label class="block text-sm font-medium mb-1">Rating</label>
<select v-model="form.rating" class="w-full rounded border px-3 py-2" required>
<option value="">Pilih rating</option>
<option v-for="n in 5" :key="n" :value="n">{{ n }} </option>
</select>
</div>
<div class="mb-4 text-left">
<label class="block text-sm font-medium mb-1">Pesan</label>
<textarea v-model="form.message" class="w-full rounded border px-3 py-2" rows="3" required />
</div>
<button
type="submit"
class="w-full bg-blue-600 text-white py-2 rounded-lg font-semibold hover:bg-blue-700 transition"
>
Kirim Ulasan
</button>
</form>
</div>
</div>
<!-- Modal Preview -->
<div
v-if="previewModal"
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/30"
>
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6 relative">
<button
@click="previewModal = null"
class="absolute top-3 right-3 text-gray-500 hover:text-gray-800"
>
</button>
<h3 class="text-xl font-bold mb-4 text-gray-800">Ulasan Lengkap</h3>
<p class="text-gray-600 italic mb-4 whitespace-pre-line break-words">
"{{ previewModal.message }}"
</p>
<h4 class="font-bold text-gray-800">{{ previewModal.name }}</h4>
<p class="text-sm text-gray-500">{{ previewModal.city }}</p>
</div>
</div> </div>
</section> </section>
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { ref } from 'vue';
const { data: testimonials, refresh } = await useFetch('http://localhost:8000/api/reviews') const testimonials = ref([
{
const openModal = ref(false) id: 1,
const previewModal = ref(null) name: 'Rizky & Anisa',
role: 'Pengantin Baru',
const form = ref({ avatar: 'https://i.pravatar.cc/100?u=rizky',
name: '', rating: 5,
city: '', text: 'Desainnya elegan dan modern! Proses pembuatannya juga cepat banget. Semua tamu memuji undangannya. Terima kasih Abbauf Tech!'
rating: '', },
message: '' {
}) id: 2,
name: 'Budi Santoso',
// Submit review role: 'Event Organizer',
const submitReview = async () => { avatar: 'https://i.pravatar.cc/100?u=budi',
try { rating: 5,
await $fetch('http://localhost:8000/api/reviews', { text: 'Sebagai EO, kami butuh platform yang efisien dan hasilnya premium. Abbauf Tech menjawab semua kebutuhan itu. Klien kami sangat puas.'
method: 'POST', },
body: form.value {
}) id: 3,
form.value = { name: '', city: '', rating: '', message: '' } name: 'Citra Lestari',
openModal.value = false role: 'Ulang Tahun Anak',
await refresh() avatar: 'https://i.pravatar.cc/100?u=citra',
} catch (err) { rating: 4,
console.error('Gagal simpan ulasan:', err) text: 'Fitur RSVP dan pengingat sangat membantu. Tema-tema ulang tahunnya juga lucu dan bisa dikustomisasi. Sangat direkomendasikan!'
} },
} ]);
</script> </script>
<style scoped> <style scoped>
/* Marquee Container */ /* Kosong, semua diatur oleh Tailwind */
.marquee-container {
overflow: hidden;
padding: 1rem 0;
}
/* Marquee Content - Contains all cards */
.marquee-content {
display: flex;
gap: 1.5rem;
animation: marquee calc(var(--total-cards) * 8s) linear infinite;
width: max-content;
}
/* Individual testimonial card */
.testimonial-card {
flex-shrink: 0;
width: 24rem; /* 384px = w-96 */
border-radius: 0.75rem;
background: white;
padding: 2rem;
text-align: left;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
transition: transform 300ms, box-shadow 300ms;
cursor: pointer;
}
.testimonial-card:hover {
transform: translateY(-0.5rem);
box-shadow: 0 25px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* Marquee animation */
@keyframes marquee {
0% {
transform: translateX(0);
}
100% {
transform: translateX(-50%);
}
}
/* Pause animation on hover */
.marquee-container:hover .marquee-content {
animation-play-state: paused;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.testimonial-card {
width: 20rem; /* Smaller on mobile */
}
}
</style> </style>

View File

@ -1,222 +0,0 @@
<template>
<div class="flex flex-col min-h-screen">
<!-- Main Content -->
<main class="flex-1">
<div class="max-w-7xl mx-auto px-4 py-8">
<!-- Back button -->
<div class="mb-8">
<NuxtLink
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">
<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>
Kembali ke Beranda
</NuxtLink>
</div>
<!-- Header -->
<h1 class="text-3xl md:text-4xl font-bold text-center text-gray-800">
Pilih Kategori Favoritmu
</h1>
<p class="mt-2 text-center text-gray-500">
Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya.
</p>
<!-- Loading / Error kategori -->
<div v-if="isLoading" class="mt-12 text-center">Memuat kategori...</div>
<div v-else-if="error" class="mt-12 text-center text-red-500">
Gagal memuat kategori.
</div>
<!-- Kategori Grid -->
<div
v-else-if="categories.length > 0"
class="mt-12 flex flex-wrap justify-center gap-6"
>
<div
v-for="category in categories"
:key="category.id + '-' + category.foto"
@click="onCategoryClick(category)"
class="group cursor-pointer relative overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300 w-72"
>
<img
v-if="category.foto"
:src="`http://localhost:8000${category.foto}`"
:alt="category.nama"
class="w-full h-96 object-cover transition-transform duration-300 group-hover:scale-110"
>
<div class="absolute inset-0 bg-gradient-to-t from-black/70 via-black/40 to-transparent"></div>
<div class="absolute inset-0 flex flex-col justify-center items-start px-4 text-white">
<h3 class="text-xl font-semibold mb-2">
{{ category.nama }}
</h3>
<p class="text-lg font-normal leading-snug whitespace-normal break-words max-w-[90%]">
{{ category.deskripsi }}
</p>
</div>
</div>
</div>
<div v-else class="mt-12 text-center text-gray-500">
Belum ada kategori.
</div>
<!-- Header Templates -->
<div class="mt-20 text-center">
<h2 class="text-2xl md:text-3xl font-bold text-gray-800">
Semua Template yang Ada
</h2>
<p class="mt-2 text-gray-500">
Pilih template terbaik sesuai kebutuhan undanganmu.
</p>
</div>
<!-- Templates Grid -->
<div v-if="!isLoadingRandom" class="mt-12">
<!-- Kalau kosong -->
<div v-if="randomTemplates.length === 0" class="text-center text-gray-500 ">
Belum ada template tersedia.
</div>
<!-- Kalau ada -->
<div
v-else
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 -->
<img
:src="t.foto ? `http://localhost:8000${t.foto}` : '/fallback.png'"
:alt="t.nama"
class="w-full h-48 object-cover"
@error="(e) => e.target.src = '/fallback.png'"
/>
<!-- Body -->
<div class="p-5 text-center">
<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">
Rp {{ Number(t.harga).toLocaleString('id-ID') }}
</p>
<!-- Dropdown fitur -->
<div v-if="t.fiturs && t.fiturs.length > 0" class="relative mb-4">
<button
@click="toggleDropdown(t.id)"
class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 inline-flex justify-between items-center"
>
<span class="mx-auto text-gray-700 font-semibold">FITUR YANG TERSEDIA</span>
<svg class="h-5 w-5 text-gray-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<div v-if="openDropdownId === t.id">
<ul class="mt-4 space-y-2 text-gray-600 text-left">
<li v-for="f in t.fiturs" :key="f.id" class="flex items-center">
<svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
{{ f.deskripsi }}
</li>
</ul>
</div>
</div>
<!-- Buttons -->
<div class="flex items-center gap-3 mt-6">
<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"
@click="onTemplateClick(t)"
>
Preview
</button>
<NuxtLink
:to="`/form/${t.kategori?.nama?.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"
>
Order
</NuxtLink>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- Footer -->
<LandingPageFooter class="w-full" />
</div>
</template>
<script setup>
import { ref, onMounted, onActivated } from 'vue'
const emit = defineEmits(['category-selected', 'template-selected'])
const categories = ref([])
const isLoading = ref(true)
const error = ref(null)
const randomTemplates = ref([])
const isLoadingRandom = ref(true)
// dropdown fitur
const openDropdownId = ref(null)
const toggleDropdown = (templateId) => {
openDropdownId.value = openDropdownId.value === templateId ? null : templateId
}
// Fetch kategori
const fetchCategories = async () => {
isLoading.value = true
error.value = null
try {
const res = await $fetch('http://localhost:8000/api/kategoris')
categories.value = res
} catch (err) {
console.error(err)
error.value = 'Gagal memuat kategori.'
} finally {
isLoading.value = false
}
}
// Fetch random template
const fetchRandomTemplates = async () => {
isLoadingRandom.value = true
try {
const res = await $fetch('http://localhost:8000/api/templates/random')
randomTemplates.value = res
} catch (err) {
console.error('Gagal fetch random templates', err)
} finally {
isLoadingRandom.value = false
}
}
onMounted(() => {
fetchCategories()
fetchRandomTemplates()
})
onActivated(() => {
fetchCategories()
fetchRandomTemplates()
})
const onCategoryClick = (category) => {
emit('category-selected', category)
}
const onTemplateClick = (template) => {
emit('template-selected', template)
}
</script>

View File

@ -1,127 +0,0 @@
<template>
<div>
<div class="flex items-center mb-8">
<button @click="$emit('back')"
class="text-blue-600 hover:text-blue-800 font-semibold inline-flex items-center mr-4">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" 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" />
</svg>
Kembali
</button>
<h1 class="text-3xl md:text-4xl font-bold text-gray-800">
Template {{ category }}
</h1>
</div>
<div v-if="isLoading" class="text-center py-10">
<p>Memuat template...</p>
</div>
<div v-else-if="error" class="text-center py-10 text-red-600">
<p>Gagal memuat template.</p>
</div>
<div v-else-if="templates && templates.length > 0"
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8 items-start">
<div v-for="tpl in templates" :key="tpl.id"
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300">
<img :src="`http://localhost:8000${tpl.foto}`" :alt="tpl.nama" class="w-full h-48 object-cover">
<div class="p-5 text-center">
<h4 class="text-xl font-bold text-gray-800 mb-2">{{ tpl.nama }}</h4>
<p class="text-green-600 font-semibold text-xl mb-4">
Rp {{ tpl.harga.toLocaleString('id-ID') }}
</p>
<div v-if="tpl.fitur && tpl.fitur.length > 0" class="relative mb-4">
<button @click="toggleDropdown(tpl.id)"
class="w-full bg-white border border-gray-300 rounded-md shadow-sm px-4 py-2 inline-flex justify-between items-center text-center">
<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">
<path fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
<div v-if="openDropdownId === tpl.id">
<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">
<svg class="h-4 w-4 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
{{ item_fitur.deskripsi }}
</li>
</ul>
</div>
</div>
<div class="mt-6">
<div class="flex items-center gap-3">
<!-- Tombol Preview (masih sama) -->
<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' :
'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'"
target="_blank"
class="w-full bg-white border border-gray-300 text-gray-800 font-semibold py-2 px-4 rounded-lg hover:bg-gray-100 transition-colors text-center block">
Preview
</a>
<!-- Tombol Order langsung ke form Khitan -->
<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 v-else class="text-center py-10 text-gray-500">
<p>Belum ada template untuk kategori ini.</p>
</div>
</div>
</template>
<script setup>
import { ref } 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({
category: { type: String, required: true },
id_category: { type: Number, required: true },
});
defineEmits(['back']);
const { data: templates, pending: isLoading, error } = useFetch(
() => `/api/templates/category/${props.id_category}`,
{
baseURL: 'http://localhost:8000',
key: () => `templates-${props.id_category}`,
transform: (response) => {
if (!response || !Array.isArray(response)) return [];
return response;
}
}
);
</script>

View File

@ -1,348 +0,0 @@
[id].vue
<template>
<div class="max-w-3xl mx-auto p-6">
<h1 class="text-2xl font-bold mb-4">Form Pemesanan</h1>
<!-- Error umum -->
<div v-if="error" class="bg-red-100 text-red-600 p-3 rounded mb-4">
{{ error }}
</div>
<!-- Loading -->
<div v-else-if="loading">Memuat data template...</div>
<!-- Form -->
<div v-else>
<div class="mb-4">
<p class="text-green-600 font-bold text-lg">Rp {{ Number(template.harga).toLocaleString('id-ID') }}</p>
<img v-if="template.thumbnail" :src="template.thumbnail" alt="Preview Template" class="w-64 h-40 object-cover rounded" />
</div>
<!-- Fitur dinamis -->
<div class="mt-4">
<h3 class="font-medium mb-2">Isi Data Fitur:</h3>
<div v-for="fitur in template.fiturs" :key="fitur.id" class="mb-4">
<label class="block font-medium mb-1">{{ fitur.deskripsi }}</label>
<!-- Jika fitur adalah galeri -->
<div v-if="isGallery(fitur.deskripsi)">
<input
type="file"
:multiple="true"
:accept="acceptedImageTypes"
@change="handleGalleryChange($event, fitur.id, parseGalleryMax(fitur.deskripsi))"
class="w-full"
:ref="`fileInput_${fitur.id}`"
/>
<p class="text-sm text-gray-500 mt-1">
Maks. {{ parseGalleryMax(fitur.deskripsi) || defaultGalleryMax }} file.
(Terpilih: {{ (files[`fitur_${fitur.id}`] || []).length }})
</p>
<p v-if="fileErrors[`fitur_${fitur.id}`]" class="text-sm text-red-600 mt-1">
{{ fileErrors[`fitur_${fitur.id}`] }}
</p>
<!-- Preview gambar yang dipilih -->
<div v-if="files[`fitur_${fitur.id}`] && files[`fitur_${fitur.id}`].length > 0" class="mt-2 flex flex-wrap gap-2">
<div v-for="(file, index) in files[`fitur_${fitur.id}`]" :key="index" class="relative">
<img :src="getFilePreview(file)" alt="Preview" class="w-20 h-20 object-cover rounded border" />
<button
@click="removeFile(`fitur_${fitur.id}`, index)"
class="absolute -top-2 -right-2 bg-red-500 text-white rounded-full w-5 h-5 text-xs flex items-center justify-center"
type="button"
>
×
</button>
</div>
</div>
</div>
<!-- Jika fitur adalah tanggal -->
<input
v-else-if="fitur.deskripsi.toLowerCase().includes('tanggal')"
type="date"
v-model="formFields[fieldNameById(fitur.id)]"
class="w-full border rounded px-3 py-2"
/>
<!-- default text -->
<input
v-else
type="text"
v-model="formFields[fieldNameById(fitur.id)]"
placeholder="Isi data..."
class="w-full border rounded px-3 py-2"
/>
</div>
</div>
<!-- Data Pemesan -->
<div class="mt-6">
<label class="block font-medium">Nama Pemesan *</label>
<input v-model="baseForm.nama_pemesan" type="text" class="w-full border rounded px-3 py-2 mb-3" required />
<label class="block font-medium">Email *</label>
<input v-model="baseForm.email" type="email" class="w-full border rounded px-3 py-2 mb-3" required />
<label class="block font-medium">Nomor HP *</label>
<input v-model="baseForm.no_hp" type="text" class="w-full border rounded px-3 py-2 mb-3" required />
</div>
<!-- Submit -->
<button @click="submitForm" class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50" :disabled="isSubmitting">
{{ isSubmitting ? 'Mengirim...' : 'Kirim Pesanan' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const templateId = route.query.template_id
const template = ref({ fiturs: [] })
const loading = ref(true)
const error = ref(null)
const isSubmitting = ref(false)
// base form fields (pemesan)
const baseForm = ref({
nama_pemesan: '',
email: '',
no_hp: '',
catatan: ''
})
// formFields menyimpan nilai text/date untuk tiap fitur keyed by fieldNameById(fitur.id)
const formFields = ref({}) // { "fitur_1": "Budi", "fitur_2": "2025-09-20", ... }
// files menyimpan File[] per fitur.id
const files = ref({}) // { "fitur_3": [File, File], ... }
const fileErrors = ref({}) // error message per gallery field
const defaultGalleryMax = 10
const acceptedImageTypes = 'image/*'
/** ---------- helper ---------- **/
const slugify = (text) =>
text.toString().toLowerCase().trim().replace(/\s+/g, '_').replace(/[^\w\-]/g, '')
// fieldName derived from description (used for backend field name logic)
const fieldName = (deskripsi) => slugify(deskripsi)
// fieldNameById used to link formFields stored by fitur.id
const fieldNameById = (id) => `fitur_${id}`
// cek apakah deskripsi adalah gallery
const isGallery = (deskripsi) => deskripsi.toLowerCase().includes('galeri') || deskripsi.toLowerCase().includes('gallery')
// parse number from "Galeri 3" -> 3. Returns null if not found
const parseGalleryMax = (deskripsi) => {
const m = deskripsi.match(/(\d+)/)
return m ? parseInt(m[1], 10) : null
}
// create preview URL for file
const getFilePreview = (file) => {
return URL.createObjectURL(file)
}
// remove file from selection
const removeFile = (fiturKey, index) => {
if (files.value[fiturKey]) {
files.value[fiturKey].splice(index, 1)
}
}
/** ---------- fetch template ---------- **/
onMounted(async () => {
try {
const res = await fetch(`http://localhost:8000/api/templates/${templateId}`)
if (!res.ok) throw new Error('Gagal memuat data template')
const data = await res.json()
// backend might wrap as {template:..., fiturs:...} or return template object directly.
// normalize: if data.template exists, use it, else assume data is template
const tpl = data.template ?? data
template.value = tpl
// setup empty formFields and files for each fitur
tpl.fiturs?.forEach(f => {
formFields.value[fieldNameById(f.id)] = ''
// gallery init
if (isGallery(f.deskripsi)) {
files.value[fieldNameById(f.id)] = []
fileErrors.value[fieldNameById(f.id)] = ''
}
})
} catch (err) {
console.error(err)
error.value = err.message || 'Gagal memuat template'
} finally {
loading.value = false
}
})
/** ---------- handle gallery selection ---------- **/
function handleGalleryChange(event, fiturId, maxAllowed) {
const selected = Array.from(event.target.files || [])
const allowed = maxAllowed || defaultGalleryMax
const fiturKey = fieldNameById(fiturId)
// reset error
fileErrors.value[fiturKey] = ''
// client-side validation for file count
if (selected.length > allowed) {
fileErrors.value[fiturKey] = `Jumlah file melebihi batas (${allowed}). Pilih maksimal ${allowed} file.`
files.value[fiturKey] = []
event.target.value = '' // reset input
return
}
// optional: validate file types and sizes (e.g., < 10MB)
const tooLarge = selected.find(f => f.size > 10 * 1024 * 1024) // 10MB
if (tooLarge) {
fileErrors.value[fiturKey] = 'Satu atau lebih file melebihi 10MB.'
files.value[fiturKey] = []
event.target.value = ''
return
}
// validate image types
const invalidType = selected.find(f => !f.type.startsWith('image/'))
if (invalidType) {
fileErrors.value[fiturKey] = 'File harus berupa gambar (JPG, PNG, GIF, dll).'
files.value[fiturKey] = []
event.target.value = ''
return
}
// all good
files.value[fiturKey] = selected
}
/** ---------- submit ---------- **/
const submitForm = async () => {
if (isSubmitting.value) return
error.value = null
isSubmitting.value = true
// client basic check: required base fields
if (!baseForm.value.nama_pemesan || !baseForm.value.no_hp || !baseForm.value.email) {
error.value = 'Nama pemesan, No HP, dan Email wajib diisi.'
isSubmitting.value = false
return
}
// validate email format
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(baseForm.value.email)) {
error.value = 'Format email tidak valid.'
isSubmitting.value = false
return
}
try {
// use FormData because there may be files
const fd = new FormData()
fd.append('template_id', templateId)
fd.append('nama_pemesan', baseForm.value.nama_pemesan)
fd.append('email', baseForm.value.email)
fd.append('no_hp', baseForm.value.no_hp)
fd.append('catatan', baseForm.value.catatan || '')
// append feature values (text/date) and files
template.value.fiturs?.forEach(fitur => {
const fiturKey = fieldNameById(fitur.id)
const fieldNameForBackend = fieldName(fitur.deskripsi)
// for gallery fields, append files
if (isGallery(fitur.deskripsi)) {
const fileArray = files.value[fiturKey] || []
console.log(`Appending ${fileArray.length} files for ${fieldNameForBackend}`)
if (fileArray.length > 0) {
// append files with proper array notation for Laravel
fileArray.forEach((file, index) => {
fd.append(`${fieldNameForBackend}[]`, file)
console.log(`Added file ${index}:`, file.name, file.type, file.size)
})
}
} else {
// for text/date fields
const value = formFields.value[fiturKey]
if (value !== undefined && value !== null && String(value).trim() !== '') {
fd.append(fieldNameForBackend, value)
}
}
})
// Debug: log FormData contents
console.log('FormData contents:')
for (let pair of fd.entries()) {
console.log(pair[0] + ':', pair[1])
}
// fetch POST (do NOT set Content-Type browser sets multipart boundary)
const res = await fetch('http://localhost:8000/api/form', {
method: 'POST',
body: fd,
})
const data = await res.json()
console.log('Response:', data)
if (!res.ok) {
if (data.errors) {
// flatten errors object from Laravel
const errorMessages = Object.entries(data.errors).map(([field, messages]) => {
return `${field}: ${Array.isArray(messages) ? messages.join(', ') : messages}`
})
error.value = errorMessages.join(' | ')
} else {
error.value = data.message || 'Gagal mengirim form'
}
return
}
alert('Pesanan berhasil dikirim!')
// reset inputs
resetForm()
} catch (err) {
console.error('Submit error:', err)
error.value = 'Terjadi kesalahan saat mengirim form.'
} finally {
isSubmitting.value = false
}
}
// reset form function
const resetForm = () => {
baseForm.value = { nama_pemesan: '', email: '', no_hp: '', catatan: '' }
// reset feature fields and files
template.value.fiturs?.forEach(f => {
const fiturKey = fieldNameById(f.id)
formFields.value[fiturKey] = ''
if (isGallery(f.deskripsi)) {
files.value[fiturKey] = []
fileErrors.value[fiturKey] = ''
}
})
// reset file inputs
document.querySelectorAll('input[type="file"]').forEach(input => {
input.value = ''
})
}
</script>
<style scoped>
/* Custom styling if needed */
</style>

View File

@ -1,8 +0,0 @@
<template>
<div>
<FormsKhitanForm />
</div>
</template>
<script setup>
</script>

View File

@ -1,9 +0,0 @@
<template>
<div>
<FormsPernikahanForm />
</div>
</template>
<script setup>
</script>

View File

@ -1,9 +0,0 @@
<template>
<div>
<FormsUlangTahunForm />
</div>
</template>
<script setup>
</script>

View File

@ -1,38 +0,0 @@
@ -1,37 +0,0 @@
<template>
<div>
<main class="container mx-auto px-4 py-16">
<TemplatePageCategorySelection
v-if="!selectedCategory"
@category-selected="handleCategorySelect"
/>
<TemplatePageTemplateGrid
v-else
:id_category="selectedCategory.id"
:category="selectedCategory.nama"
@back="goBack"
/>
</main>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 1. Impor komponen anak yang Anda gunakan
import TemplatePageCategorySelection from '~/components/template-page/CategorySelection.vue'; // Pastikan path ini benar
import TemplatePageTemplateGrid from '~/components/template-page/TemplateGrid.vue'; // Pastikan path ini benar
// 2. State untuk menyimpan SELURUH OBJEK kategori yang dipilih
const selectedCategory = ref(null);
// Fungsi ini sekarang akan menerima seluruh objek { id, nama }
const handleCategorySelect = (categoryObject) => {
selectedCategory.value = categoryObject;
};
// Fungsi ini akan menangkap event 'back' dari TemplateGrid
const goBack = () => {
selectedCategory.value = null;
};
</script>

View File

@ -19,9 +19,5 @@ export default defineNuxtConfig({
tailwindcss(), tailwindcss(),
], ],
}, },
runtimeConfig: {
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
}
}
}) })