Compare commits
11 Commits
main
...
production
| Author | SHA1 | Date | |
|---|---|---|---|
| d078aab4b0 | |||
| 213a3874d0 | |||
| e20f1fa12f | |||
| ef91b38fa5 | |||
|
|
ddea7e321f | ||
| ddae4df823 | |||
| ff19646ba7 | |||
| 49ac69876c | |||
| c11fdaf58f | |||
| b28ee832d5 | |||
| 720b968c6b |
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
// }
|
||||
// }
|
||||
@ -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);
|
||||
// }
|
||||
// }
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
// }
|
||||
// }
|
||||
@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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!');
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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!');
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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'];
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
];
|
||||
}
|
||||
@ -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); }
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -2,39 +2,94 @@
|
||||
|
||||
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' => [
|
||||
'guard' => 'web',
|
||||
'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' => [
|
||||
// Guard untuk user biasa
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
// Guard untuk admin
|
||||
'admin' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'admins',
|
||||
],
|
||||
],
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| User Providers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| 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' => [
|
||||
// Provider untuk user biasa
|
||||
'users' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => App\Models\User::class,
|
||||
],
|
||||
|
||||
// Provider untuk admin
|
||||
'admins' => [
|
||||
'driver' => 'eloquent',
|
||||
'model' => App\Models\Admin::class,
|
||||
],
|
||||
// 'users' => [
|
||||
// 'driver' => 'database',
|
||||
// 'table' => 'users',
|
||||
// ],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 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' => [
|
||||
'users' => [
|
||||
'provider' => 'users',
|
||||
@ -42,14 +97,19 @@ return [
|
||||
'expire' => 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,
|
||||
|
||||
];
|
||||
|
||||
@ -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 [
|
||||
//
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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 1–5
|
||||
$table->text('message');
|
||||
$table->string('name');
|
||||
$table->string('city');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('reviews');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
@ -2,20 +2,16 @@
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\Admin;
|
||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||
use Illuminate\Database\Seeder;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AdminSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
Admin::updateOrCreate(
|
||||
['email' => 'admin@example.com'],
|
||||
[
|
||||
'name' => 'Super Admin',
|
||||
'password' => Hash::make('password123'), // ganti setelah login
|
||||
]
|
||||
);
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,10 +7,16 @@ use Illuminate\Database\Seeder;
|
||||
|
||||
class DatabaseSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Seed the application's database.
|
||||
*/
|
||||
public function run(): void
|
||||
{
|
||||
$this->call([
|
||||
AdminSeeder::class,
|
||||
]);
|
||||
// \App\Models\User::factory(10)->create();
|
||||
|
||||
// \App\Models\User::factory()->create([
|
||||
// 'name' => 'Test User',
|
||||
// 'email' => 'test@example.com',
|
||||
// ]);
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@ -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>
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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 --}}
|
||||
@ -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 --}}
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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>
|
||||
@ -2,10 +2,6 @@
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
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) {
|
||||
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']);
|
||||
|
||||
@ -1,77 +1,18 @@
|
||||
<?php
|
||||
|
||||
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 () {
|
||||
return redirect()->route('admin.login');
|
||||
});
|
||||
|
||||
// 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);
|
||||
return view('welcome');
|
||||
});
|
||||
|
||||
@ -1,35 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</div>
|
||||
</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']);
|
||||
@ -1,289 +0,0 @@
|
||||
<template>
|
||||
<div class="max-w-5xl mx-auto p-8 bg-gradient-to-b from-green-50 to-blue-50 shadow-lg rounded-xl">
|
||||
<!-- Judul Form -->
|
||||
<div class="text-center mb-10">
|
||||
<h1 class="text-3xl md:text-4xl font-extrabold text-green-700 drop-shadow-sm">
|
||||
🕌 Form Pemesanan Undangan Khitan ✨
|
||||
</h1>
|
||||
<p class="text-gray-600 mt-2">
|
||||
Isi data berikut dengan lengkap untuk pemesanan undangan khitan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitForm" class="space-y-10">
|
||||
|
||||
<!-- Tema Undangan -->
|
||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Tema Undangan
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<input :value="form.nama_template" type="text" placeholder="Nama Template" class="input-readonly" readonly />
|
||||
<input :value="form.kategori" type="text" placeholder="Kategori" class="input-readonly" readonly />
|
||||
<input :value="form.harga" type="text" placeholder="Harga" class="input-readonly" readonly />
|
||||
<input :value="form.tanggal_pemesanan" type="text" placeholder="Tanggal Pemesanan" class="input-readonly"
|
||||
readonly />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pemesan Undangan -->
|
||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Pemesan Undangan
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<input v-model="form.nama_pemesan" type="text" placeholder="Nama Pemesan" class="input" required />
|
||||
<input v-model="form.no_hp" type="text" placeholder="No. WhatsApp" class="input" required />
|
||||
</div>
|
||||
<input v-model="form.email" type="email" placeholder="Email" class="input" required />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Detail Khitan -->
|
||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Detail Khitan</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<input v-model="form.nama_lengkap_anak" type="text" placeholder="Nama Lengkap Anak" class="input" required />
|
||||
<input v-model="form.nama_panggilan_anak" type="text" placeholder="Nama Panggilan Anak" class="input"
|
||||
required />
|
||||
<input v-model="form.bapak_anak" type="text" placeholder="Nama Bapak" class="input" />
|
||||
<input v-model="form.ibu_anak" type="text" placeholder="Nama Ibu" class="input" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Jadwal Acara -->
|
||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Jadwal Acara</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<input v-model="form.hari_tanggal_acara" type="date" class="input" />
|
||||
<input v-model="form.waktu_acara" type="text" placeholder="08.00 WIB" class="input" />
|
||||
<textarea v-model="form.alamat_acara" placeholder="Alamat Acara" rows="3" class="input col-span-2"></textarea>
|
||||
<input v-model="form.maps_acara" type="text" placeholder="Link Google Maps" class="input col-span-2" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rekening, Musik, Galeri -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-6">
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
|
||||
<h2 class="text-lg font-bold text-gray-800">No. Rekening</h2>
|
||||
<input v-model="form.no_rekening1" type="text" placeholder="Rekening 1" class="input" />
|
||||
<input v-model="form.no_rekening2" type="text" placeholder="Rekening 2" class="input" />
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800">Musik</h2>
|
||||
<input v-model="form.link_musik" type="text" placeholder="Link Musik" class="input" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Galeri (max 5 gambar)</h2>
|
||||
<input type="file" multiple accept="image/*" @change="handleFileUpload" class="hidden" id="gallery-upload" />
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="(img, i) in previewImages" :key="i"
|
||||
class="relative w-full aspect-square rounded-lg overflow-hidden shadow-sm group">
|
||||
<img :src="img" alt="Preview" class="object-cover w-full h-full" />
|
||||
<button type="button" @click="removeImage(i)"
|
||||
class="absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity font-bold"
|
||||
aria-label="Hapus gambar">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label v-if="previewImages.length < 5" for="gallery-upload"
|
||||
class="flex items-center justify-center w-full aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<!-- Pilihan Fitur -->
|
||||
<section v-for="kategori in kategoriFiturs" :key="kategori.id"
|
||||
class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4">{{ kategori.nama }}</h2>
|
||||
|
||||
<!-- Radio -->
|
||||
<div v-if="kategori.tipe === 'radio'" class="space-y-2">
|
||||
<label v-for="fitur in kategori.fiturs" :key="fitur.id" class="flex items-center gap-2">
|
||||
<input type="radio" :name="'fitur_' + kategori.id" :value="fitur.id"
|
||||
v-model="form.selectedFiturs[kategori.id]" />
|
||||
{{ fitur.deskripsi }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div v-else class="space-y-2">
|
||||
<label v-for="fitur in kategori.fiturs" :key="fitur.id" class="flex items-center gap-2">
|
||||
<input type="checkbox" :value="fitur.id" v-model="form.selectedFiturs[kategori.id]" />
|
||||
{{ fitur.deskripsi }}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="mt-10 text-center">
|
||||
<button @click="submitForm"
|
||||
class="bg-blue-600 text-white px-10 py-3 rounded-xl font-semibold shadow-md hover:bg-blue-700 hover:shadow-lg transition"
|
||||
:disabled="loading">
|
||||
{{ loading ? "Mengirim..." : "Kirim & Konfirmasi Admin" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alert -->
|
||||
<div v-if="success" class="mt-6 p-4 text-green-800 bg-green-100 rounded-lg text-center font-medium">
|
||||
✅ Form berhasil dikirim!
|
||||
</div>
|
||||
<div v-if="error" class="mt-6 p-4 text-red-800 bg-red-100 rounded-lg text-center font-medium">
|
||||
❌ Gagal mengirim form. Pastikan semua data yang wajib diisi sudah lengkap.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const form = ref({
|
||||
template_id: "",
|
||||
nama_template: "",
|
||||
kategori: "",
|
||||
harga: "",
|
||||
tanggal_pemesanan: new Date().toISOString().split("T")[0],
|
||||
nama_pemesan: "",
|
||||
no_hp: "",
|
||||
email: "",
|
||||
nama_lengkap_anak: "",
|
||||
nama_panggilan_anak: "",
|
||||
bapak_anak: "",
|
||||
ibu_anak: "",
|
||||
hari_tanggal_acara: "",
|
||||
waktu_acara: "",
|
||||
alamat_acara: "",
|
||||
maps_acara: "",
|
||||
no_rekening1: "",
|
||||
no_rekening2: "",
|
||||
link_musik: "",
|
||||
galeri: [],
|
||||
selectedFiturs: {}, // { kategori_id: [fitur_id] }
|
||||
|
||||
});
|
||||
|
||||
const previewImages = ref([]);
|
||||
const loading = ref(false);
|
||||
const success = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.query.template_id) {
|
||||
try {
|
||||
const template = await $fetch(`http://localhost:8000/api/templates/${route.query.template_id}`);
|
||||
form.value.template_id = template.id;
|
||||
form.value.nama_template = template.nama_template;
|
||||
form.value.kategori_id = template.kategori_id;
|
||||
form.value.kategori = template.kategori?.nama || "-";
|
||||
form.value.harga = template.harga;
|
||||
|
||||
// simpan kategori fitur
|
||||
kategoriFiturs.value = template.kategori_fiturs || [];
|
||||
} catch (err) {
|
||||
console.error("Gagal ambil template", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// FUNGSI UNTUK MENAMBAH GAMBAR
|
||||
const handleFileUpload = (event) => {
|
||||
const newFiles = Array.from(event.target.files);
|
||||
const combinedFiles = [...form.value.galeri, ...newFiles];
|
||||
|
||||
// Batasi total file menjadi 5
|
||||
form.value.galeri = combinedFiles.slice(0, 5);
|
||||
|
||||
// Buat ulang array preview berdasarkan data file yang sudah final
|
||||
previewImages.value = [];
|
||||
form.value.galeri.forEach(file => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
previewImages.value.push(e.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
// Reset input agar bisa memilih file yang sama lagi
|
||||
event.target.value = null;
|
||||
};
|
||||
|
||||
// FUNGSI UNTUK MENGHAPUS GAMBAR (SEKARANG DI LUAR)
|
||||
const removeImage = (index) => {
|
||||
form.value.galeri.splice(index, 1);
|
||||
previewImages.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
loading.value = true;
|
||||
success.value = false;
|
||||
error.value = false;
|
||||
|
||||
try {
|
||||
const body = new FormData();
|
||||
for (const key in form.value) {
|
||||
if (key === "galeri") {
|
||||
form.value.galeri.forEach((file) => body.append("galeri[]", file));
|
||||
} else if (key !== "selectedFiturs") {
|
||||
body.append(key, form.value[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// kirim fitur sebagai array fitur_id
|
||||
for (const kategoriId in form.value.selectedFiturs) {
|
||||
const fiturs = Array.isArray(form.value.selectedFiturs[kategoriId])
|
||||
? form.value.selectedFiturs[kategoriId]
|
||||
: [form.value.selectedFiturs[kategoriId]];
|
||||
fiturs.forEach(fiturId => body.append("fiturs[]", fiturId));
|
||||
}
|
||||
|
||||
await $fetch("http://localhost:8000/api/form/khitan", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
success.value = true;
|
||||
|
||||
// WA notif (biar tetap jalan)
|
||||
const adminNumber = "62895602603247";
|
||||
const message = `
|
||||
Halo Admin, ada pemesanan undangan khitan baru 🎉
|
||||
Nama Pemesan: ${form.value.nama_pemesan}
|
||||
No WA: ${form.value.no_hp}
|
||||
Email: ${form.value.email}
|
||||
Template: ${form.value.nama_template} (${form.value.kategori})
|
||||
Harga: ${form.value.harga}
|
||||
Tanggal Pemesanan: ${form.value.tanggal_pemesanan}
|
||||
`;
|
||||
window.location.href = `https://wa.me/${adminNumber}?text=${encodeURIComponent(message)}`;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,355 +0,0 @@
|
||||
<template>
|
||||
<div class="max-w-5xl mx-auto p-8 bg-gradient-to-b from-pink-50 to-red-50 shadow-lg rounded-xl">
|
||||
<!-- Judul Form -->
|
||||
<div class="text-center mb-10">
|
||||
<h1 class="text-3xl md:text-4xl font-extrabold text-red-700 drop-shadow-sm">
|
||||
💍 Form Pemesanan Undangan Pernikahan 💐
|
||||
</h1>
|
||||
<p class="text-gray-600 mt-2">
|
||||
Silakan isi data berikut untuk melakukan pemesanan undangan pernikahan Anda.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submitForm" class="space-y-10">
|
||||
|
||||
<!-- Tema Undangan -->
|
||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Tema Undangan
|
||||
</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<input :value="form.nama_template" type="text" placeholder="Nama Template" class="input-readonly" readonly />
|
||||
<input :value="form.kategori" type="text" placeholder="Kategori" class="input-readonly" readonly />
|
||||
<input :value="form.harga" type="text" placeholder="Harga" class="input-readonly" readonly />
|
||||
<input :value="form.tanggal_pemesanan" type="text" placeholder="Tanggal Pemesanan" class="input-readonly"
|
||||
readonly />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Pemesan Undangan -->
|
||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||
<span class="w-1.5 h-6 bg-blue-600 rounded-full"></span> Pemesan Undangan
|
||||
</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<input v-model="form.nama_pemesan" type="text" placeholder="Nama" class="input" required />
|
||||
<input v-model="form.no_hp" type="text" placeholder="No. WhatsApp" class="input" required />
|
||||
</div>
|
||||
<input v-model="form.email" type="email" placeholder="Email" class="input" required />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Mempelai -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
|
||||
<h2 class="text-lg font-bold text-gray-800">Mempelai Pria</h2>
|
||||
<input v-model="form.nama_lengkap_pria" type="text" placeholder="Nama Lengkap" class="input" required />
|
||||
<input v-model="form.nama_panggilan_pria" type="text" placeholder="Nama Panggilan" class="input" required />
|
||||
<input v-model="form.bapak_pria" type="text" placeholder="Nama Bapak" class="input" />
|
||||
<input v-model="form.ibu_pria" type="text" placeholder="Nama Ibu" class="input" />
|
||||
<input v-model="form.instagram_pria" type="text" placeholder="Instagram" class="input" />
|
||||
<input v-model="form.facebook_pria" type="text" placeholder="Facebook" class="input" />
|
||||
<input v-model="form.twitter_pria" type="text" placeholder="Twitter" class="input" />
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
|
||||
<h2 class="text-lg font-bold text-gray-800">Mempelai Wanita</h2>
|
||||
<input v-model="form.nama_lengkap_wanita" type="text" placeholder="Nama Lengkap" class="input" required />
|
||||
<input v-model="form.nama_panggilan_wanita" type="text" placeholder="Nama Panggilan" class="input" required />
|
||||
<input v-model="form.bapak_wanita" type="text" placeholder="Nama Bapak" class="input" />
|
||||
<input v-model="form.ibu_wanita" type="text" placeholder="Nama Ibu" class="input" />
|
||||
<input v-model="form.instagram_wanita" type="text" placeholder="Instagram" class="input" />
|
||||
<input v-model="form.facebook_wanita" type="text" placeholder="Facebook" class="input" />
|
||||
<input v-model="form.twitter_wanita" type="text" placeholder="Twitter" class="input" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Cerita Kita -->
|
||||
<section class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-10">Cerita Kita</h2>
|
||||
<textarea v-model="form.cerita_kita" placeholder="Tuliskan cerita indah kalian di sini..." rows="1"
|
||||
class="w-full border border-gray-300 rounded-md px-3 py-3 focus:ring-2 focus:ring-blue-500 focus:outline-none transition resize-none"
|
||||
@input="e => { e.target.style.height = 'auto'; e.target.style.height = e.target.scrollHeight + 'px' }" />
|
||||
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Akad & Resepsi -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
|
||||
<h2 class="text-lg font-bold text-gray-800">Akad</h2>
|
||||
<input v-model="form.hari_tanggal_akad" type="date" class="input" />
|
||||
<input v-model="form.waktu_akad" type="text" placeholder="Waktu" class="input" />
|
||||
<input v-model="form.alamat_akad" type="text" placeholder="Alamat" class="input" />
|
||||
<input v-model="form.maps_akad" type="text" placeholder="Link Google Maps" class="input" />
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
|
||||
<h2 class="text-lg font-bold text-gray-800">Resepsi</h2>
|
||||
<input v-model="form.hari_tanggal_resepsi" type="date" class="input" />
|
||||
<input v-model="form.waktu_resepsi" type="text" placeholder="Waktu" class="input" />
|
||||
<input v-model="form.alamat_resepsi" type="text" placeholder="Alamat" class="input" />
|
||||
<input v-model="form.maps_resepsi" type="text" placeholder="Link Google Maps" class="input" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Rekening, Musik, Galeri -->
|
||||
<section class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div class="space-y-6">
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200 space-y-4">
|
||||
<h2 class="text-lg font-bold text-gray-800">No. Rekening</h2>
|
||||
<input v-model="form.no_rekening1" type="text" placeholder="Rekening 1" class="input" />
|
||||
<input v-model="form.no_rekening2" type="text" placeholder="Rekening 2" class="input" />
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800">Musik</h2>
|
||||
<input v-model="form.link_musik" type="text" placeholder="Link Musik" class="input" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4">Galeri (max 5 gambar)</h2>
|
||||
<input type="file" multiple accept="image/*" @change="handleFileUpload" class="hidden" id="gallery-upload" />
|
||||
<div class="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
<div v-for="(img, i) in previewImages" :key="i"
|
||||
class="relative w-full aspect-square rounded-lg overflow-hidden shadow-sm group">
|
||||
<img :src="img" alt="Preview" class="object-cover w-full h-full" />
|
||||
<button type="button" @click="removeImage(i)"
|
||||
class="absolute top-1 right-1 bg-red-600 text-white rounded-full w-6 h-6 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity font-bold"
|
||||
aria-label="Hapus gambar">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<label v-if="previewImages.length < 5" for="gallery-upload"
|
||||
class="flex items-center justify-center w-full aspect-square bg-gray-50 border-2 border-dashed border-gray-300 rounded-lg cursor-pointer hover:bg-gray-100 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-10 w-10 text-gray-400" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<!-- Kategori & Fitur -->
|
||||
<section v-for="kategori in kategoriFiturs" :key="kategori.id"
|
||||
class="p-6 bg-white rounded-xl shadow-sm border border-gray-200">
|
||||
<h2 class="text-lg font-bold text-gray-800 mb-4">
|
||||
{{ kategori.nama }}
|
||||
</h2>
|
||||
|
||||
<!-- Radio -->
|
||||
<div v-if="kategori.tipe === 'radio'">
|
||||
<label v-for="fitur in kategori.fiturs" :key="fitur.id" class="flex items-center gap-2 mb-2">
|
||||
<input type="radio" :name="'kategori_' + kategori.id" :value="fitur.id"
|
||||
v-model="form.selectedFiturs[kategori.id]" />
|
||||
{{ fitur.deskripsi }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Checkbox -->
|
||||
<div v-else>
|
||||
<label v-for="fitur in kategori.fiturs" :key="fitur.id" class="flex items-center gap-2 mb-2">
|
||||
<input type="checkbox" :value="fitur.id" v-model="form.selectedFiturs[kategori.id]" />
|
||||
{{ fitur.deskripsi }}
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="mt-10 text-center">
|
||||
<button @click="submitForm"
|
||||
class="bg-blue-600 text-white px-10 py-3 rounded-xl font-semibold shadow-md hover:bg-blue-700 hover:shadow-lg transition"
|
||||
:disabled="loading">
|
||||
{{ loading ? "Mengirim..." : "Kirim & Konfirmasi Admin" }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Alert -->
|
||||
<div v-if="success" class="mt-6 p-4 text-green-800 bg-green-100 rounded-lg text-center font-medium">✅ Form berhasil
|
||||
dikirim! Silakan tunggu konfirmasi dari admin.</div>
|
||||
<div v-if="error" class="mt-6 p-4 text-red-800 bg-red-100 rounded-lg text-center font-medium">❌ Gagal mengirim form.
|
||||
Pastikan semua data yang wajib diisi sudah lengkap.</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const form = ref({
|
||||
template_id: "",
|
||||
nama_template: "",
|
||||
kategori: "",
|
||||
harga: "",
|
||||
tanggal_pemesanan: new Date().toLocaleDateString("id-ID", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
}),
|
||||
|
||||
nama_pemesan: "",
|
||||
no_hp: "",
|
||||
email: "",
|
||||
|
||||
nama_lengkap_pria: "",
|
||||
nama_panggilan_pria: "",
|
||||
bapak_pria: "",
|
||||
ibu_pria: "",
|
||||
instagram_pria: "",
|
||||
facebook_pria: "",
|
||||
twitter_pria: "",
|
||||
|
||||
nama_lengkap_wanita: "",
|
||||
nama_panggilan_wanita: "",
|
||||
bapak_wanita: "",
|
||||
ibu_wanita: "",
|
||||
instagram_wanita: "",
|
||||
facebook_wanita: "",
|
||||
twitter_wanita: "",
|
||||
|
||||
cerita_kita: "",
|
||||
|
||||
hari_tanggal_akad: "",
|
||||
waktu_akad: "",
|
||||
alamat_akad: "",
|
||||
maps_akad: "",
|
||||
|
||||
hari_tanggal_resepsi: "",
|
||||
waktu_resepsi: "",
|
||||
alamat_resepsi: "",
|
||||
maps_resepsi: "",
|
||||
|
||||
no_rekening1: "",
|
||||
no_rekening2: "",
|
||||
link_musik: "",
|
||||
galeri: [],
|
||||
|
||||
|
||||
selectedFiturs: {}, // { kategori_id: [fitur_id] }
|
||||
});
|
||||
|
||||
const kategoriFiturs = ref([]); // 🆕 simpan kategori fitur
|
||||
const previewImages = ref([]);
|
||||
const loading = ref(false);
|
||||
const success = ref(false);
|
||||
const error = ref(false);
|
||||
|
||||
onMounted(async () => {
|
||||
if (route.query.template_id) {
|
||||
try {
|
||||
const template = await $fetch(
|
||||
`http://localhost:8000/api/templates/${route.query.template_id}`
|
||||
);
|
||||
|
||||
form.value.template_id = template.id;
|
||||
form.value.nama_template = template.nama_template;
|
||||
form.value.kategori = template.kategori?.nama || "-";
|
||||
form.value.harga = new Intl.NumberFormat("id-ID", {
|
||||
style: "currency",
|
||||
currency: "IDR",
|
||||
}).format(template.harga);
|
||||
|
||||
// 🆕 simpan kategori fitur
|
||||
kategoriFiturs.value = template.kategori_fiturs || [];
|
||||
} catch (err) {
|
||||
console.error("Gagal ambil template", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// FUNGSI UNTUK MENAMBAH GAMBAR
|
||||
const handleFileUpload = (event) => {
|
||||
const newFiles = Array.from(event.target.files);
|
||||
const combinedFiles = [...form.value.galeri, ...newFiles];
|
||||
|
||||
form.value.galeri = combinedFiles.slice(0, 5);
|
||||
|
||||
previewImages.value = [];
|
||||
form.value.galeri.forEach((file) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
previewImages.value.push(e.target.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
event.target.value = null;
|
||||
};
|
||||
|
||||
// FUNGSI UNTUK MENGHAPUS GAMBAR
|
||||
const removeImage = (index) => {
|
||||
form.value.galeri.splice(index, 1);
|
||||
previewImages.value.splice(index, 1);
|
||||
};
|
||||
|
||||
const submitForm = async () => {
|
||||
loading.value = true;
|
||||
success.value = false;
|
||||
error.value = false;
|
||||
|
||||
try {
|
||||
const body = new FormData();
|
||||
|
||||
// field umum
|
||||
for (const key in form.value) {
|
||||
if (key === "galeri" || key === "selectedFiturs") continue;
|
||||
if (form.value[key] !== null && form.value[key] !== undefined) {
|
||||
body.append(key, form.value[key]);
|
||||
}
|
||||
}
|
||||
|
||||
// galeri
|
||||
form.value.galeri.forEach((file) => body.append("galeri[]", file));
|
||||
|
||||
// 🆕 fiturs
|
||||
for (const kategoriId in form.value.selectedFiturs) {
|
||||
const fiturs = Array.isArray(form.value.selectedFiturs[kategoriId])
|
||||
? form.value.selectedFiturs[kategoriId]
|
||||
: [form.value.selectedFiturs[kategoriId]];
|
||||
|
||||
fiturs.forEach((fiturId) => body.append("fiturs[]", fiturId));
|
||||
}
|
||||
|
||||
await $fetch("http://localhost:8000/api/form/pernikahan", {
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
|
||||
success.value = true;
|
||||
|
||||
const adminNumber = "62895602603247";
|
||||
const message = `
|
||||
Halo Admin, ada pemesanan undangan pernikahan baru 🎉
|
||||
|
||||
Nama Pemesan: ${form.value.nama_pemesan}
|
||||
No WA: ${form.value.no_hp}
|
||||
Email: ${form.value.email}
|
||||
|
||||
Mempelai Pria: ${form.value.nama_lengkap_pria} (${form.value.nama_panggilan_pria})
|
||||
Mempelai Wanita: ${form.value.nama_lengkap_wanita} (${form.value.nama_panggilan_wanita})
|
||||
|
||||
Akad: ${form.value.hari_tanggal_akad} | ${form.value.waktu_akad}
|
||||
Alamat: ${form.value.alamat_akad}
|
||||
|
||||
Resepsi: ${form.value.hari_tanggal_resepsi} | ${form.value.waktu_resepsi}
|
||||
Alamat: ${form.value.alamat_resepsi}
|
||||
|
||||
Template: ${form.value.nama_template} (${form.value.kategori})
|
||||
Harga: ${form.value.harga}
|
||||
Tanggal Pemesanan: ${form.value.tanggal_pemesanan}
|
||||
`;
|
||||
|
||||
window.location.href = `https://wa.me/${adminNumber}?text=${encodeURIComponent(
|
||||
message
|
||||
)}`;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
error.value = true;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@ -1,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">
|
||||
×
|
||||
</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>
|
||||
@ -7,7 +7,7 @@
|
||||
<!-- Layout gambar + teks -->
|
||||
<div class="about-layout">
|
||||
<div class="about-image">
|
||||
<img src="/rectangle.png" alt="Tentang Kami - Undangan Digital" />
|
||||
<img src="/Rectangle.png" alt="Tentang Kami - Undangan Digital" />
|
||||
</div>
|
||||
<div class="about-text">
|
||||
<p>
|
||||
|
||||
@ -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>
|
||||
<section id="template" class="py-16 px-5 text-center">
|
||||
<!-- Header -->
|
||||
<div class="mb-10">
|
||||
<h2 class="text-[2.9rem] font-bold mb-6">Templat Unggulan</h2>
|
||||
<p class="text-gray-600 text-lg mb-10">
|
||||
"Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya."
|
||||
</p>
|
||||
<section id="template" class="feature-section">
|
||||
<div class="featured-header">
|
||||
<h2>Templat Unggulan</h2>
|
||||
<p>"Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya."</p>
|
||||
</div>
|
||||
|
||||
<!-- Grid Template -->
|
||||
<div v-if="templates.length" class="grid gap-8 max-w-[1100px] mx-auto grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="t in templates"
|
||||
:key="t.id"
|
||||
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 class="template-grid">
|
||||
<div class="template-card" v-for="i in 6" :key="i">
|
||||
<div class="template-image">
|
||||
<img src="/templat.jpg" alt="Template" />
|
||||
</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"
|
||||
>
|
||||
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 class="template-body">
|
||||
<h3 class="template-title">Golf Party</h3>
|
||||
<p class="template-price">Rp.89.000</p>
|
||||
|
||||
<select class="template-select">
|
||||
<option>Fitur Terbaik</option>
|
||||
<option>Fitur Lengkap</option>
|
||||
</select>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="btn btn-preview">Preview</button>
|
||||
<button class="btn btn-order">Order</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Jika error -->
|
||||
<div v-else class="text-gray-500">Tidak ada template yang bisa ditampilkan</div>
|
||||
|
||||
<!-- 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 class="see-more">
|
||||
<a href="#">Lihat Selengkapnya...</a>
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
<div class="footer-column">
|
||||
<h4>Layanan</h4>
|
||||
<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 Ulang Tahun</a></li>
|
||||
</ul>
|
||||
@ -19,42 +19,19 @@
|
||||
<ul class="footer-contact-list">
|
||||
<li>
|
||||
<a href="#" class="social-link">
|
||||
<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>
|
||||
<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>
|
||||
<span>ABBAUF TECH</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://instagram.com/abbauftech" target="_blank" class="inline-flex items-center gap-2 font-medium
|
||||
bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500
|
||||
bg-clip-text text-transparent
|
||||
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 href="#" class="social-link">
|
||||
<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>
|
||||
<span>@abbauf_tech</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li>
|
||||
<a href="https://www.linkedin.com/posts/abbauf-tech_abbauftech-digitalstrategy-itconsulting-activity-7277021563982340099-8Byg" class="social-link">
|
||||
<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>
|
||||
<a href="#" class="social-link">
|
||||
<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>
|
||||
<span>ABBAUF TECH</span>
|
||||
</a>
|
||||
</li>
|
||||
@ -65,41 +42,26 @@
|
||||
<h4>Kontak Kami</h4>
|
||||
<ul class="footer-contact-list">
|
||||
<li>
|
||||
<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>
|
||||
<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>
|
||||
<a href="mailto:contact@abbauf.com">contact@abbauf.com</a>
|
||||
</li>
|
||||
<li>
|
||||
<a :href="waUrl" target="_blank"
|
||||
class="inline-flex items-center gap-2 hover:text-green-600 transition-colors">
|
||||
<!-- 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>
|
||||
<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>
|
||||
<a href="tel:02127617679">(021) 2761-7679</a>
|
||||
</li>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<a href="#">+62 878-7711-7847</a>
|
||||
</li>
|
||||
<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>
|
||||
<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
|
||||
Duren, Kec. Grogol Petamburan, Jakarta Barat, DKI Jakarta 11470, ID</p>
|
||||
<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>
|
||||
</div>
|
||||
</li>
|
||||
<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>
|
||||
<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>
|
||||
<strong>Alamat Studio</strong>
|
||||
<p>Jl. Adhi Karya No. 57 RT 003 RW 015, Kel. Depok, Kec. Pancoran Mas, Depok 16431</p>
|
||||
@ -116,20 +78,15 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
const adminNumber = "62895602603247";
|
||||
const defaultMessage = "Halo Admin, saya mau tanya tentang undangan digital 🙏";
|
||||
const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMessage)}`;
|
||||
|
||||
// Tidak ada script yang dibutuhkan untuk footer statis ini
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.site-footer {
|
||||
width: 100%;
|
||||
background-color: #f0f2f5;
|
||||
background-color: #f0f2f5; /* Warna abu-abu muda */
|
||||
color: #333;
|
||||
padding: 50px 0 20px 0;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: 'Inter', sans-serif; /* Menggunakan font yang terlihat modern */
|
||||
}
|
||||
|
||||
.container {
|
||||
@ -140,8 +97,7 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
||||
|
||||
.footer-content {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1.5fr 1.5fr 2.5fr;
|
||||
/* Mengatur lebar kolom */
|
||||
grid-template-columns: 2fr 1.5fr 1.5fr 2.5fr; /* Mengatur lebar kolom */
|
||||
gap: 40px;
|
||||
padding-bottom: 30px;
|
||||
border-bottom: 1px solid #d9dce1;
|
||||
@ -159,8 +115,7 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
||||
color: #111;
|
||||
}
|
||||
|
||||
.footer-links,
|
||||
.footer-contact-list {
|
||||
.footer-links, .footer-contact-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
@ -170,17 +125,14 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.footer-links a,
|
||||
.social-link {
|
||||
.footer-links a, .social-link {
|
||||
text-decoration: none;
|
||||
color: #555;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.footer-links a:hover,
|
||||
.social-link:hover {
|
||||
color: #0d6efd;
|
||||
/* Biru Primer */
|
||||
.footer-links a:hover, .social-link:hover {
|
||||
color: #0d6efd; /* Biru Primer */
|
||||
}
|
||||
|
||||
.footer-contact-list li {
|
||||
@ -200,7 +152,6 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
||||
text-decoration: none;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.footer-contact-list a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
@ -233,27 +184,21 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
||||
/* Penyesuaian untuk layar kecil (Mobile) */
|
||||
@media (max-width: 992px) {
|
||||
.footer-content {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
/* 2 kolom di tablet */
|
||||
grid-template-columns: 1fr 1fr; /* 2 kolom di tablet */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.footer-content {
|
||||
grid-template-columns: 1fr;
|
||||
/* 1 kolom di mobile */
|
||||
grid-template-columns: 1fr; /* 1 kolom di mobile */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
margin: 0 auto;
|
||||
/* Logo di tengah */
|
||||
margin: 0 auto; /* Logo di tengah */
|
||||
}
|
||||
|
||||
.footer-contact-list li {
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
|
||||
@ -2,14 +2,12 @@
|
||||
<header class="main-header">
|
||||
<nav class="container">
|
||||
<div class="logo">
|
||||
<NuxtLink to="/" class="logo-link">
|
||||
<img :src="logo" alt="Abbauf Tech Logo" class="logo-icon" />
|
||||
<img src="/abbauflogo.png" alt="Abbauf Tech Logo" class="logo-icon">
|
||||
<span>ABBAUF TECH</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
<ul class="nav-links">
|
||||
<li v-for="link in navLinks" :key="link.name">
|
||||
<NuxtLink :to="link.path">{{ link.name }}</NuxtLink>
|
||||
<a :href="link.path">{{ link.name }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
@ -18,37 +16,23 @@
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
const logo = '/abbauflogo.png';
|
||||
|
||||
const navLinks = ref([
|
||||
{ name: 'Beranda', path: '/' },
|
||||
{ name: 'Tentang Kami', path: '/#tentang-kami' },
|
||||
{ name: 'Templat', path: '/template' },
|
||||
{ name: 'Panduan', path: '/#cara' },
|
||||
{ name: 'Keistimewaan', path: '/#keistimewaan' },
|
||||
{ name: 'Testimoni', path: '/#testimoni' },
|
||||
{ name: 'Beranda', path: '#beranda' },
|
||||
{ name: 'Tentang Kami', path: '#tentang-kami' },
|
||||
{ name: 'Templat', path: '#template' },
|
||||
{ name: 'Panduan', path: '#cara' },
|
||||
{ name: 'Keistimewaan', path: '#keistimewaan' },
|
||||
{ name: 'Testimoni', path: '#testimoni' },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* ================= NAVBAR ================= */
|
||||
.main-header {
|
||||
background-color: #eaf2ff;
|
||||
padding: 15px 0;
|
||||
border-bottom: 1px solid #d4e3ff;
|
||||
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 {
|
||||
@ -60,17 +44,26 @@ body {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo a.logo-link {
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
color: #0d6efd;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.main-header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
}
|
||||
|
||||
|
||||
.logo-icon {
|
||||
width: 40px;
|
||||
width: 55px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
<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
|
||||
|
||||
<div class="h-24 flex items-center justify-center">
|
||||
@ -16,42 +16,20 @@
|
||||
Tanpa Ribet
|
||||
</h1>
|
||||
|
||||
<p class="mt-4 mb-8 text-lg text-gray-600 pl-15">
|
||||
Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu.
|
||||
Praktis, cepat, dan bisa langsung digunakan.
|
||||
<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. Praktis, cepat, dan bisa langsung digunakan.
|
||||
</p>
|
||||
<div class="flex flex-col justify-center gap-4 sm:flex-row pl-15">
|
||||
|
||||
<a :href="waUrl" target="_blank"
|
||||
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">
|
||||
|
||||
<!-- 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" />
|
||||
<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">
|
||||
<svg class="mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<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"/>
|
||||
</svg>
|
||||
|
||||
<span>Hubungi Kami</span>
|
||||
</a>
|
||||
|
||||
|
||||
<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">
|
||||
<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">
|
||||
Lihat Templat
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -85,12 +63,6 @@ onMounted(() => {
|
||||
onUnmounted(() => {
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@ -4,264 +4,66 @@
|
||||
<h2 class="text-4xl font-extrabold text-gray-800 mb-2">
|
||||
Apa Kata Mereka?
|
||||
</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.
|
||||
</p>
|
||||
|
||||
<!-- CSS Marquee Scroll -->
|
||||
<div class="marquee-container mb-10">
|
||||
<div class="marquee-content" :style="{ '--total-cards': testimonials?.length || 0 }">
|
||||
<!-- Render original cards -->
|
||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div
|
||||
v-for="testimonial in testimonials"
|
||||
:key="`original-${testimonial.id}`"
|
||||
class="testimonial-card"
|
||||
@click="previewModal = testimonial"
|
||||
:key="testimonial.id"
|
||||
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"
|
||||
>
|
||||
<!-- 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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Pesan -->
|
||||
<p class="mb-6 flex-grow text-gray-600 italic line-clamp-3 min-h-[72px] break-words">
|
||||
"{{ testimonial.message }}"
|
||||
</p>
|
||||
<p class="mb-6 flex-grow text-gray-600 italic">"{{ testimonial.text }}"</p>
|
||||
|
||||
<!-- User Info -->
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<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>
|
||||
<p class="text-sm text-gray-500">{{ testimonial.city }}</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>
|
||||
<p class="text-sm text-gray-500">{{ testimonial.role }}</p>
|
||||
</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>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref } from 'vue';
|
||||
|
||||
const { data: testimonials, refresh } = await useFetch('http://localhost:8000/api/reviews')
|
||||
|
||||
const openModal = ref(false)
|
||||
const previewModal = ref(null)
|
||||
|
||||
const form = ref({
|
||||
name: '',
|
||||
city: '',
|
||||
rating: '',
|
||||
message: ''
|
||||
})
|
||||
|
||||
// Submit review
|
||||
const submitReview = async () => {
|
||||
try {
|
||||
await $fetch('http://localhost:8000/api/reviews', {
|
||||
method: 'POST',
|
||||
body: form.value
|
||||
})
|
||||
form.value = { name: '', city: '', rating: '', message: '' }
|
||||
openModal.value = false
|
||||
await refresh()
|
||||
} catch (err) {
|
||||
console.error('Gagal simpan ulasan:', err)
|
||||
}
|
||||
}
|
||||
const testimonials = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: 'Rizky & Anisa',
|
||||
role: 'Pengantin Baru',
|
||||
avatar: 'https://i.pravatar.cc/100?u=rizky',
|
||||
rating: 5,
|
||||
text: 'Desainnya elegan dan modern! Proses pembuatannya juga cepat banget. Semua tamu memuji undangannya. Terima kasih Abbauf Tech!'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Budi Santoso',
|
||||
role: 'Event Organizer',
|
||||
avatar: 'https://i.pravatar.cc/100?u=budi',
|
||||
rating: 5,
|
||||
text: 'Sebagai EO, kami butuh platform yang efisien dan hasilnya premium. Abbauf Tech menjawab semua kebutuhan itu. Klien kami sangat puas.'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Citra Lestari',
|
||||
role: 'Ulang Tahun Anak',
|
||||
avatar: 'https://i.pravatar.cc/100?u=citra',
|
||||
rating: 4,
|
||||
text: 'Fitur RSVP dan pengingat sangat membantu. Tema-tema ulang tahunnya juga lucu dan bisa dikustomisasi. Sangat direkomendasikan!'
|
||||
},
|
||||
]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Marquee Container */
|
||||
.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 */
|
||||
}
|
||||
}
|
||||
/* Kosong, semua diatur oleh Tailwind */
|
||||
</style>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -1,8 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<FormsKhitanForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<FormsPernikahanForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
@ -1,9 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<FormsUlangTahunForm />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
@ -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>
|
||||
@ -19,9 +19,5 @@ export default defineNuxtConfig({
|
||||
tailwindcss(),
|
||||
],
|
||||
},
|
||||
runtimeConfig: {
|
||||
public: {
|
||||
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user