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 [
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default authentication "guard" and password
|
||||||
|
| reset options for your application. You may change these defaults
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
'defaults' => [
|
'defaults' => [
|
||||||
'guard' => 'web',
|
'guard' => 'web',
|
||||||
'passwords' => 'users',
|
'passwords' => 'users',
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Guards
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Next, you may define every authentication guard for your application.
|
||||||
|
| Of course, a great default configuration has been defined for you
|
||||||
|
| here which uses session storage and the Eloquent user provider.
|
||||||
|
|
|
||||||
|
| All authentication drivers have a user provider. This defines how the
|
||||||
|
| users are actually retrieved out of your database or other storage
|
||||||
|
| mechanisms used by this application to persist your user's data.
|
||||||
|
|
|
||||||
|
| Supported: "session"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
'guards' => [
|
'guards' => [
|
||||||
// Guard untuk user biasa
|
|
||||||
'web' => [
|
'web' => [
|
||||||
'driver' => 'session',
|
'driver' => 'session',
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
],
|
],
|
||||||
|
|
||||||
// Guard untuk admin
|
|
||||||
'admin' => [
|
|
||||||
'driver' => 'session',
|
|
||||||
'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' => [
|
'providers' => [
|
||||||
// Provider untuk user biasa
|
|
||||||
'users' => [
|
'users' => [
|
||||||
'driver' => 'eloquent',
|
'driver' => 'eloquent',
|
||||||
'model' => App\Models\User::class,
|
'model' => App\Models\User::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
// Provider untuk admin
|
// 'users' => [
|
||||||
'admins' => [
|
// 'driver' => 'database',
|
||||||
'driver' => 'eloquent',
|
// 'table' => 'users',
|
||||||
'model' => App\Models\Admin::class,
|
// ],
|
||||||
],
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Resetting Passwords
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| You may specify multiple password reset configurations if you have more
|
||||||
|
| than one user table or model in the application and you want to have
|
||||||
|
| separate password reset settings based on the specific user types.
|
||||||
|
|
|
||||||
|
| The expire time is the number of minutes that each reset token will be
|
||||||
|
| considered valid. This security feature keeps tokens short-lived so
|
||||||
|
| they have less time to be guessed. You may change this as needed.
|
||||||
|
|
|
||||||
|
| The throttle setting is the number of seconds a user must wait before
|
||||||
|
| generating more password reset tokens. This prevents the user from
|
||||||
|
| quickly generating a very large amount of password reset tokens.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
'passwords' => [
|
'passwords' => [
|
||||||
'users' => [
|
'users' => [
|
||||||
'provider' => 'users',
|
'provider' => 'users',
|
||||||
@ -42,14 +97,19 @@ return [
|
|||||||
'expire' => 60,
|
'expire' => 60,
|
||||||
'throttle' => 60,
|
'throttle' => 60,
|
||||||
],
|
],
|
||||||
'admins' => [
|
|
||||||
'provider' => 'admins',
|
|
||||||
'table' => 'password_reset_tokens',
|
|
||||||
'expire' => 60,
|
|
||||||
'throttle' => 60,
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Password Confirmation Timeout
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may define the amount of seconds before a password confirmation
|
||||||
|
| times out and the user is prompted to re-enter their password via the
|
||||||
|
| confirmation screen. By default, the timeout lasts for three hours.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
'password_timeout' => 10800,
|
'password_timeout' => 10800,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|||||||
@ -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;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use App\Models\Admin;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
use Illuminate\Support\Facades\Hash;
|
|
||||||
|
|
||||||
class AdminSeeder extends Seeder
|
class AdminSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
Admin::updateOrCreate(
|
//
|
||||||
['email' => 'admin@example.com'],
|
|
||||||
[
|
|
||||||
'name' => 'Super Admin',
|
|
||||||
'password' => Hash::make('password123'), // ganti setelah login
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,10 +7,16 @@ use Illuminate\Database\Seeder;
|
|||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Seed the application's database.
|
||||||
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->call([
|
// \App\Models\User::factory(10)->create();
|
||||||
AdminSeeder::class,
|
|
||||||
]);
|
// \App\Models\User::factory()->create([
|
||||||
}
|
// 'name' => 'Test User',
|
||||||
|
// 'email' => 'test@example.com',
|
||||||
|
// ]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB |
@ -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\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\Api\KategoriApiController;
|
|
||||||
use App\Http\Controllers\Api\TemplateApiController;
|
|
||||||
use App\Http\Controllers\Api\FormApiController;
|
|
||||||
use App\Http\Controllers\Api\ReviewController;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
@ -21,29 +17,3 @@ use App\Http\Controllers\Api\ReviewController;
|
|||||||
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
|
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
|
||||||
return $request->user();
|
return $request->user();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===============================
|
|
||||||
// API Kategori (read-only)
|
|
||||||
// ===============================
|
|
||||||
Route::get('kategoris', [KategoriApiController::class, 'index']);
|
|
||||||
Route::get('kategoris/{kategori}', [KategoriApiController::class, 'show']);
|
|
||||||
|
|
||||||
// ===============================
|
|
||||||
// API Reviews
|
|
||||||
// ===============================
|
|
||||||
Route::apiResource('reviews', ReviewController::class);
|
|
||||||
|
|
||||||
// ===============================
|
|
||||||
// API Templates
|
|
||||||
// ===============================
|
|
||||||
Route::get('templates/random', [TemplateApiController::class, 'random']); // random template
|
|
||||||
Route::get('templates', [TemplateApiController::class, 'index']);
|
|
||||||
Route::get('templates/{template}', [TemplateApiController::class, 'show']);
|
|
||||||
Route::get('templates/category/{id}', [TemplateApiController::class, 'byCategory']);
|
|
||||||
Route::get('/templates/{id}', [TemplateApiController::class, 'show']); // duplicate tapi ga masalah
|
|
||||||
|
|
||||||
// ===============================
|
|
||||||
// API Form (user submit)
|
|
||||||
// ===============================
|
|
||||||
Route::post('form', [FormApiController::class, 'store']); // <<== INI yang ditambah
|
|
||||||
Route::get('templates/{id}/fiturs', [FormApiController::class, 'getFiturs']);
|
|
||||||
|
|||||||
@ -1,77 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\AdminAuthController;
|
|
||||||
use App\Http\Controllers\KategoriController;
|
|
||||||
use App\Http\Controllers\FiturController;
|
|
||||||
use App\Http\Controllers\TemplateController;
|
|
||||||
use App\Http\Controllers\PelangganController;
|
|
||||||
use App\Http\Controllers\Api\ReviewController;
|
|
||||||
use App\Models\Review;
|
|
||||||
use App\Http\Controllers\DashboardController;
|
|
||||||
|
|
||||||
// Redirect ke login admin
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Web Routes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here is where you can register web routes for your application. These
|
||||||
|
| routes are loaded by the RouteServiceProvider and all of them will
|
||||||
|
| be assigned to the "web" middleware group. Make something great!
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
Route::get('/', function () {
|
Route::get('/', function () {
|
||||||
return redirect()->route('admin.login');
|
return view('welcome');
|
||||||
});
|
|
||||||
|
|
||||||
// Admin Auth
|
|
||||||
Route::prefix('admin')->name('admin.')->group(function () {
|
|
||||||
Route::middleware('guest:admin')->group(function () {
|
|
||||||
Route::get('/login', [AdminAuthController::class, 'showLogin'])->name('login');
|
|
||||||
Route::post('/login', [AdminAuthController::class, 'login'])->name('login.post');
|
|
||||||
});
|
|
||||||
|
|
||||||
Route::middleware('auth:admin')->group(function () {
|
|
||||||
Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
|
||||||
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('logout');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin Kategori
|
|
||||||
Route::prefix('admin')->name('admin.')->group(function () {
|
|
||||||
Route::resource('kategori', KategoriController::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin Fitur
|
|
||||||
Route::prefix('admin')->name('admin.')->group(function () {
|
|
||||||
Route::resource('fitur', FiturController::class);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin Template
|
|
||||||
Route::prefix('admin')->group(function () {
|
|
||||||
Route::get('templates', [TemplateController::class, 'index'])->name('templates.index');
|
|
||||||
Route::post('templates', [TemplateController::class, 'store'])->name('templates.store');
|
|
||||||
Route::put('templates/{template}', [TemplateController::class, 'update'])->name('templates.update');
|
|
||||||
Route::delete('templates/{template}', [TemplateController::class, 'destroy'])->name('templates.destroy');
|
|
||||||
Route::get('templates/kategori/{id}', [TemplateController::class, 'byKategori'])->name('templates.byKategori');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin Pelanggan
|
|
||||||
Route::prefix('admin')->name('admin.')->group(function () {
|
|
||||||
Route::resource('pelanggan', PelangganController::class)->only([
|
|
||||||
'index',
|
|
||||||
'show',
|
|
||||||
'destroy'
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin Review
|
|
||||||
Route::prefix('admin')->name('admin.')->middleware('auth:admin')->group(function () {
|
|
||||||
Route::get('/ulasan', function () {
|
|
||||||
$reviews = Review::latest()->get();
|
|
||||||
return view('admin.reviews.index', compact('reviews'));
|
|
||||||
})->name('reviews.index');
|
|
||||||
|
|
||||||
Route::post('/ulasan', [ReviewController::class, 'store'])->name('reviews.store');
|
|
||||||
Route::put('/ulasan/{review}', [ReviewController::class, 'update'])->name('reviews.update');
|
|
||||||
Route::delete('/ulasan/{review}', function (Review $review) {
|
|
||||||
$review->delete();
|
|
||||||
return redirect()->route('admin.reviews.index')->with('success', 'Ulasan berhasil dihapus');
|
|
||||||
})->name('reviews.destroy');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Admin Kategori Fitur
|
|
||||||
Route::prefix('admin')->name('admin.')->group(function () {
|
|
||||||
Route::resource('kategori_fitur', \App\Http\Controllers\KategoriFiturController::class);
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,35 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NuxtLayout>
|
<NuxtPage />
|
||||||
<NuxtPage />
|
|
||||||
</NuxtLayout>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- <script setup>
|
|
||||||
const props = defineProps({
|
|
||||||
category: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
id_category: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
}) -->
|
|
||||||
|
|
||||||
// const templates = ref([])
|
|
||||||
|
|
||||||
// const fetchTemplates = async () => {
|
|
||||||
// try {
|
|
||||||
// templates.value = await $fetch(`http://localhost:8000/api/templates/category/${props.id_category}`)
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Gagal ambil template:', error)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// onMounted(() => {
|
|
||||||
// fetchTemplates()
|
|
||||||
// })
|
|
||||||
|
|
||||||
// defineEmits(['back']);
|
|
||||||
@ -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 -->
|
<!-- Layout gambar + teks -->
|
||||||
<div class="about-layout">
|
<div class="about-layout">
|
||||||
<div class="about-image">
|
<div class="about-image">
|
||||||
<img src="/rectangle.png" alt="Tentang Kami - Undangan Digital" />
|
<img src="/Rectangle.png" alt="Tentang Kami - Undangan Digital" />
|
||||||
</div>
|
</div>
|
||||||
<div class="about-text">
|
<div class="about-text">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@ -1,108 +1,175 @@
|
|||||||
<script setup>
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
|
|
||||||
// id template yang mau ditampilkan
|
|
||||||
const selectedIds = [1, 2, 3, 5, 6, 8]
|
|
||||||
|
|
||||||
// state dropdown
|
|
||||||
const openDropdownId = ref(null)
|
|
||||||
const toggleDropdown = (templateId) => {
|
|
||||||
openDropdownId.value = openDropdownId.value === templateId ? null : templateId
|
|
||||||
}
|
|
||||||
|
|
||||||
// fetch API dari Laravel
|
|
||||||
const { data: templatesData, error } = await useFetch('http://localhost:8000/api/templates')
|
|
||||||
|
|
||||||
// filter hanya id tertentu
|
|
||||||
const templates = computed(() =>
|
|
||||||
(templatesData.value || []).filter(t => selectedIds.includes(t.id))
|
|
||||||
)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section id="template" class="py-16 px-5 text-center">
|
<section id="template" class="feature-section">
|
||||||
<!-- Header -->
|
<div class="featured-header">
|
||||||
<div class="mb-10">
|
<h2>Templat Unggulan</h2>
|
||||||
<h2 class="text-[2.9rem] font-bold mb-6">Templat Unggulan</h2>
|
<p>"Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya."</p>
|
||||||
<p class="text-gray-600 text-lg mb-10">
|
|
||||||
"Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya."
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid Template -->
|
<!-- Grid Template -->
|
||||||
<div v-if="templates.length" class="grid gap-8 max-w-[1100px] mx-auto grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
|
<div class="template-grid">
|
||||||
<div
|
<div class="template-card" v-for="i in 6" :key="i">
|
||||||
v-for="t in templates"
|
<div class="template-image">
|
||||||
:key="t.id"
|
<img src="/templat.jpg" alt="Template" />
|
||||||
class="bg-white border rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300"
|
</div>
|
||||||
>
|
|
||||||
<!-- Image -->
|
|
||||||
<img
|
|
||||||
:src="`http://localhost:8000${t.foto}`"
|
|
||||||
:alt="t.nama_template"
|
|
||||||
class="w-full h-48 object-cover"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Body -->
|
<div class="template-body">
|
||||||
<div class="p-5 text-center">
|
<h3 class="template-title">Golf Party</h3>
|
||||||
<h4 class="text-xl font-bold text-gray-800 mb-2">{{ t.nama }}</h4>
|
<p class="template-price">Rp.89.000</p>
|
||||||
<p class="text-green-600 font-semibold text-xl mb-4">
|
|
||||||
Rp {{ Number(t.harga).toLocaleString('id-ID') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- Dropdown fitur -->
|
<select class="template-select">
|
||||||
<div v-if="t.fiturs && t.fiturs.length > 0" class="relative mb-4">
|
<option>Fitur Terbaik</option>
|
||||||
<button
|
<option>Fitur Lengkap</option>
|
||||||
@click="toggleDropdown(t.id)"
|
</select>
|
||||||
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">
|
<div class="button-group">
|
||||||
<ul class="mt-4 space-y-2 text-gray-600 text-left">
|
<button class="btn btn-preview">Preview</button>
|
||||||
<li v-for="f in t.fiturs" :key="f.id" class="flex items-center">
|
<button class="btn btn-order">Order</button>
|
||||||
<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"
|
|
||||||
>
|
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Jika error -->
|
<div class="see-more">
|
||||||
<div v-else class="text-gray-500">Tidak ada template yang bisa ditampilkan</div>
|
<a href="#">Lihat Selengkapnya...</a>
|
||||||
|
|
||||||
<!-- See more -->
|
|
||||||
<div class="mt-8 text-right max-w-[1100px] mx-auto">
|
|
||||||
<NuxtLink
|
|
||||||
to="/template"
|
|
||||||
class="text-blue-600 font-medium hover:underline"
|
|
||||||
>
|
|
||||||
Lihat Selengkapnya...
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.featured-section {
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-header h2 {
|
||||||
|
font-size: 2.9rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.featured-header p {
|
||||||
|
color: #555;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
font-size: 17px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card {
|
||||||
|
background: #f2f2f2;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 16px;
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-image {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-image img {
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-body {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 12px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-price {
|
||||||
|
color: #008000;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-preview {
|
||||||
|
background: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-preview:hover {
|
||||||
|
background: #d6d6d6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-order {
|
||||||
|
background: #2563eb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-order:hover {
|
||||||
|
background: #1d4ed8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.see-more {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: right;
|
||||||
|
max-width: 1100px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.see-more a {
|
||||||
|
color: #2563eb;
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.see-more a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<div class="footer-column">
|
<div class="footer-column">
|
||||||
<h4>Layanan</h4>
|
<h4>Layanan</h4>
|
||||||
<ul class="footer-links">
|
<ul class="footer-links">
|
||||||
<li><a href="/template/Pernikahan">Template Undangan Pernikahan</a></li>
|
<li><a href="#">Template Undangan Pernikahan</a></li>
|
||||||
<li><a href="#">Template Undangan Khitan</a></li>
|
<li><a href="#">Template Undangan Khitan</a></li>
|
||||||
<li><a href="#">Template Undangan Ulang Tahun</a></li>
|
<li><a href="#">Template Undangan Ulang Tahun</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -19,42 +19,19 @@
|
|||||||
<ul class="footer-contact-list">
|
<ul class="footer-contact-list">
|
||||||
<li>
|
<li>
|
||||||
<a href="#" class="social-link">
|
<a href="#" class="social-link">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v2.385z"/></svg>
|
||||||
<path
|
|
||||||
d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v2.385z" />
|
|
||||||
</svg>
|
|
||||||
<span>ABBAUF TECH</span>
|
<span>ABBAUF TECH</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="https://instagram.com/abbauftech" target="_blank" class="inline-flex items-center gap-2 font-medium
|
<a href="#" class="social-link">
|
||||||
bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.85s-.011 3.584-.069 4.85c-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07s-3.584-.012-4.85-.07c-3.252-.148-4.771-1.691-4.919-4.919-.058-1.265-.069-1.645-.069-4.85s.011-3.584.069-4.85c.149-3.225 1.664-4.771 4.919-4.919 1.266-.057 1.644-.069 4.85-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948s.014 3.667.072 4.947c.2 4.359 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072s3.667-.014 4.947-.072c4.359-.2 6.78-2.618 6.98-6.98.058-1.281.072-1.689.072-4.948s-.014-3.667-.072-4.947c-.2-4.359-2.618-6.78-6.98-6.98-1.281-.058-1.689-.072-4.948-.072zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.162 6.162 6.162 6.162-2.759 6.162-6.162-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4s1.791-4 4-4 4 1.79 4 4-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.441 1.441 1.441 1.441-.645 1.441-1.441-.645-1.44-1.441-1.44z"/></svg>
|
||||||
bg-clip-text text-transparent
|
<span>@abbauf_tech</span>
|
||||||
hover:scale-110 transition-all duration-300">
|
|
||||||
<!-- Ikon Instagram -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24"
|
|
||||||
fill="url(#igGradient)">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="igGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
||||||
<stop offset="0%" style="stop-color:#8a3ab9" />
|
|
||||||
<stop offset="50%" style="stop-color:#e95950" />
|
|
||||||
<stop offset="100%" style="stop-color:#fccc63" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<path d="M7.75 2h8.5A5.75 5.75 0 0 1 22 7.75v8.5A5.75 5.75 0 0 1 16.25 22h-8.5A5.75 5.75 0 0 1 2 16.25v-8.5A5.75 5.75 0 0 1 7.75 2zm0 1.5A4.25 4.25 0 0 0 3.5 7.75v8.5A4.25 4.25 0 0 0 7.75 20.5h8.5a4.25 4.25 0 0 0 4.25-4.25v-8.5A4.25 4.25 0 0 0 16.25 3.5h-8.5zm4.25 3a5.75 5.75 0 1 1 0 11.5 5.75 5.75 0 0 1 0-11.5zm0 1.5a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5zm5-2.25a1 1 0 1 1 0 2 1 1 0 0 1 0-2z" />
|
|
||||||
</svg>
|
|
||||||
<!-- Nama IG -->
|
|
||||||
<span>@abbauftech</span>
|
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a href="https://www.linkedin.com/posts/abbauf-tech_abbauftech-digitalstrategy-itconsulting-activity-7277021563982340099-8Byg" class="social-link">
|
<a href="#" class="social-link">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5c0 1.381-1.11 2.5-2.48 2.5s-2.48-1.119-2.48-2.5c0-1.38 1.11-2.5 2.48-2.5s2.48 1.12 2.48 2.5zm.02 4.5h-5v16h5v-16zm7.982 0h-4.968v16h4.969v-8.399c0-4.67 6.029-5.052 6.029 0v8.399h4.988v-10.131c0-7.88-8.922-7.593-11.018-3.714v-2.155z"/></svg>
|
||||||
<path
|
|
||||||
d="M4.98 3.5c0 1.381-1.11 2.5-2.48 2.5s-2.48-1.119-2.48-2.5c0-1.38 1.11-2.5 2.48-2.5s2.48 1.12 2.48 2.5zm.02 4.5h-5v16h5v-16zm7.982 0h-4.968v16h4.969v-8.399c0-4.67 6.029-5.052 6.029 0v8.399h4.988v-10.131c0-7.88-8.922-7.593-11.018-3.714v-2.155z" />
|
|
||||||
</svg>
|
|
||||||
<span>ABBAUF TECH</span>
|
<span>ABBAUF TECH</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -65,41 +42,26 @@
|
|||||||
<h4>Kontak Kami</h4>
|
<h4>Kontak Kami</h4>
|
||||||
<ul class="footer-contact-list">
|
<ul class="footer-contact-list">
|
||||||
<li>
|
<li>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3v18h24v-18h-24zm21.518 2l-9.518 7.713-9.518-7.713h19.036zm-19.518 14v-11.817l10 8.104 10-8.104v11.817h-20z"/></svg>
|
||||||
<path
|
|
||||||
d="M0 3v18h24v-18h-24zm21.518 2l-9.518 7.713-9.518-7.713h19.036zm-19.518 14v-11.817l10 8.104 10-8.104v11.817h-20z" />
|
|
||||||
</svg>
|
|
||||||
<a href="mailto:contact@abbauf.com">contact@abbauf.com</a>
|
<a href="mailto:contact@abbauf.com">contact@abbauf.com</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a :href="waUrl" target="_blank"
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20 22.621l-3.521-6.795c-.008.004-1.974.97-2.064 1.011-2.24 1.086-6.799-3.473-5.712-5.713.041-.09 1.011-2.064 1.011-2.064l-6.795-3.521-2.918 2.918c-1.603 1.603-1.425 4.933 1.011 7.37 4.301 4.301 9.962 3.593 12.301.954l2.688-2.355-2.356-2.688z"/></svg>
|
||||||
class="inline-flex items-center gap-2 hover:text-green-600 transition-colors">
|
<a href="tel:02127617679">(021) 2761-7679</a>
|
||||||
<!-- Ikon WhatsApp -->
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path
|
|
||||||
d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.894 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198 -.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.14-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579 -.487-.5-.669-.51-.173-.008-.371-.01-.57-.01s-.52.074-.792.372 c-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.626.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29 .173-1.414z" />
|
|
||||||
</svg>
|
|
||||||
<!-- Nomor WA -->
|
|
||||||
<span>+62 878-7711-7847</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.894 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01s-.52.074-.792.372c-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.626.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414z"/></svg>
|
||||||
<path
|
<a href="#">+62 878-7711-7847</a>
|
||||||
d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z" />
|
</li>
|
||||||
</svg>
|
<li>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z"/></svg>
|
||||||
<div>
|
<div>
|
||||||
<strong>Alamat Kantor Pusat</strong>
|
<strong>Alamat Kantor Pusat</strong>
|
||||||
<p>APL Tower Central Park Lantai 19 Unit T7, Jalan Letjen S. Parman, Kavling 28, RT. 012/006, Kel. Tanjung
|
<p>APL Tower Central Park Lantai 19 Unit T7, Jalan Letjen S. Parman, Kavling 28, RT. 012/006, Kel. Tanjung Duren, Kec. Grogol Petamburan, Jakarta Barat, DKI Jakarta 11470, ID</p>
|
||||||
Duren, Kec. Grogol Petamburan, Jakarta Barat, DKI Jakarta 11470, ID</p>
|
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z"/></svg>
|
||||||
<path
|
|
||||||
d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z" />
|
|
||||||
</svg>
|
|
||||||
<div>
|
<div>
|
||||||
<strong>Alamat Studio</strong>
|
<strong>Alamat Studio</strong>
|
||||||
<p>Jl. Adhi Karya No. 57 RT 003 RW 015, Kel. Depok, Kec. Pancoran Mas, Depok 16431</p>
|
<p>Jl. Adhi Karya No. 57 RT 003 RW 015, Kel. Depok, Kec. Pancoran Mas, Depok 16431</p>
|
||||||
@ -116,20 +78,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
// Tidak ada script yang dibutuhkan untuk footer statis ini
|
||||||
const adminNumber = "62895602603247";
|
|
||||||
const defaultMessage = "Halo Admin, saya mau tanya tentang undangan digital 🙏";
|
|
||||||
const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMessage)}`;
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.site-footer {
|
.site-footer {
|
||||||
width: 100%;
|
background-color: #f0f2f5; /* Warna abu-abu muda */
|
||||||
background-color: #f0f2f5;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
padding: 50px 0 20px 0;
|
padding: 50px 0 20px 0;
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif; /* Menggunakan font yang terlihat modern */
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -140,8 +97,7 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
|||||||
|
|
||||||
.footer-content {
|
.footer-content {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1.5fr 1.5fr 2.5fr;
|
grid-template-columns: 2fr 1.5fr 1.5fr 2.5fr; /* Mengatur lebar kolom */
|
||||||
/* Mengatur lebar kolom */
|
|
||||||
gap: 40px;
|
gap: 40px;
|
||||||
padding-bottom: 30px;
|
padding-bottom: 30px;
|
||||||
border-bottom: 1px solid #d9dce1;
|
border-bottom: 1px solid #d9dce1;
|
||||||
@ -159,8 +115,7 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
|||||||
color: #111;
|
color: #111;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-links,
|
.footer-links, .footer-contact-list {
|
||||||
.footer-contact-list {
|
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -170,17 +125,14 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
|||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-links a,
|
.footer-links a, .social-link {
|
||||||
.social-link {
|
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #555;
|
color: #555;
|
||||||
transition: color 0.3s ease;
|
transition: color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-links a:hover,
|
.footer-links a:hover, .social-link:hover {
|
||||||
.social-link:hover {
|
color: #0d6efd; /* Biru Primer */
|
||||||
color: #0d6efd;
|
|
||||||
/* Biru Primer */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-contact-list li {
|
.footer-contact-list li {
|
||||||
@ -200,7 +152,6 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-contact-list a:hover {
|
.footer-contact-list a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
@ -233,27 +184,21 @@ const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMes
|
|||||||
/* Penyesuaian untuk layar kecil (Mobile) */
|
/* Penyesuaian untuk layar kecil (Mobile) */
|
||||||
@media (max-width: 992px) {
|
@media (max-width: 992px) {
|
||||||
.footer-content {
|
.footer-content {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr; /* 2 kolom di tablet */
|
||||||
/* 2 kolom di tablet */
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.footer-content {
|
.footer-content {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr; /* 1 kolom di mobile */
|
||||||
/* 1 kolom di mobile */
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-brand {
|
.footer-brand {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-logo {
|
.footer-logo {
|
||||||
margin: 0 auto;
|
margin: 0 auto; /* Logo di tengah */
|
||||||
/* Logo di tengah */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-contact-list li {
|
.footer-contact-list li {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@ -2,14 +2,12 @@
|
|||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<nav class="container">
|
<nav class="container">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<NuxtLink to="/" class="logo-link">
|
<img src="/abbauflogo.png" alt="Abbauf Tech Logo" class="logo-icon">
|
||||||
<img :src="logo" alt="Abbauf Tech Logo" class="logo-icon" />
|
<span>ABBAUF TECH</span>
|
||||||
<span>ABBAUF TECH</span>
|
|
||||||
</NuxtLink>
|
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav-links">
|
<ul class="nav-links">
|
||||||
<li v-for="link in navLinks" :key="link.name">
|
<li v-for="link in navLinks" :key="link.name">
|
||||||
<NuxtLink :to="link.path">{{ link.name }}</NuxtLink>
|
<a :href="link.path">{{ link.name }}</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@ -18,37 +16,23 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
const logo = '/abbauflogo.png';
|
|
||||||
|
|
||||||
const navLinks = ref([
|
const navLinks = ref([
|
||||||
{ name: 'Beranda', path: '/' },
|
{ name: 'Beranda', path: '#beranda' },
|
||||||
{ name: 'Tentang Kami', path: '/#tentang-kami' },
|
{ name: 'Tentang Kami', path: '#tentang-kami' },
|
||||||
{ name: 'Templat', path: '/template' },
|
{ name: 'Templat', path: '#template' },
|
||||||
{ name: 'Panduan', path: '/#cara' },
|
{ name: 'Panduan', path: '#cara' },
|
||||||
{ name: 'Keistimewaan', path: '/#keistimewaan' },
|
{ name: 'Keistimewaan', path: '#keistimewaan' },
|
||||||
{ name: 'Testimoni', path: '/#testimoni' },
|
{ name: 'Testimoni', path: '#testimoni' },
|
||||||
]);
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* ================= NAVBAR ================= */
|
|
||||||
.main-header {
|
.main-header {
|
||||||
background-color: #eaf2ff;
|
background-color: #eaf2ff;
|
||||||
padding: 15px 0;
|
padding: 15px 0;
|
||||||
border-bottom: 1px solid #d4e3ff;
|
border-bottom: 1px solid #d4e3ff;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
/* FIXED NAVBAR */
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1000; /* pastikan di atas konten */
|
|
||||||
box-shadow: 0 2px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Supaya konten di bawah navbar tidak tertutup */
|
|
||||||
body {
|
|
||||||
padding-top: 70px; /* sesuaikan dengan tinggi navbar */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@ -60,17 +44,26 @@ body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo a.logo-link {
|
.logo {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
color: #0d6efd;
|
color: #0d6efd;
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.main-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.logo-icon {
|
.logo-icon {
|
||||||
width: 40px;
|
width: 55px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<div class="w-full text-center lg:w-1/2">
|
<div class="w-full text-center lg:w-1/2">
|
||||||
|
|
||||||
<h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl pl-15">
|
<h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl ml-14">
|
||||||
Buat Undangan Digital Praktis Untuk
|
Buat Undangan Digital Praktis Untuk
|
||||||
|
|
||||||
<div class="h-24 flex items-center justify-center">
|
<div class="h-24 flex items-center justify-center">
|
||||||
@ -16,42 +16,20 @@
|
|||||||
Tanpa Ribet
|
Tanpa Ribet
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="mt-4 mb-8 text-lg text-gray-600 pl-15">
|
<p class="mt-4 mb-8 text-lg text-gray-600 ml-14">
|
||||||
Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu.
|
Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu. Praktis, cepat, dan bisa langsung digunakan.
|
||||||
Praktis, cepat, dan bisa langsung digunakan.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col justify-center gap-4 sm:flex-row pl-15">
|
<div class="flex flex-col justify-center gap-4 sm:flex-row ml-14">
|
||||||
|
<a href="#" class="inline-flex items-center justify-center rounded-lg border-2 border-green-500 bg-white px-8 py-3 font-bold text-gray-800 shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md">
|
||||||
<a :href="waUrl" target="_blank"
|
<svg class="mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
class="inline-flex items-center justify-center rounded-lg border-2 border-green-500 bg-white px-8 py-3 font-bold text-gray-800 shadow-md transition-all duration-300 transform hover:-translate-y-1 hover:shadow-lg hover:bg-green-500 hover:text-white">
|
<path d="M19.2239 4.7761C17.1659 2.7181 14.6599 1.5 11.9999 1.5...Z" fill="#25D366"/>
|
||||||
|
<path d="M16.7441 14.968C16.5331 15.538...Z" fill="white"/>
|
||||||
<!-- Ikon WhatsApp -->
|
|
||||||
<svg class="mr-2 w-6 h-6 transition-colors duration-300" viewBox="0 0 32 32"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path class="transition-colors duration-300" fill="#25D366"
|
|
||||||
d="M16 .4a15.6 15.6 0 0 0-13.6 23.8L.4 31.6l7.6-2a15.7 15.7 0 0 0 8 2.2c8.8 0 15.6-7.2 15.6-15.6S24.8.4 16 .4z" />
|
|
||||||
<path class="transition-colors duration-300" fill="#FFF"
|
|
||||||
d="M25.2 22.4c-.4 1-2.2 1.8-3 1.8-.8.2-1.8.4-6.2-1.4-5.2-2.2-8.6-7.4-8.8-7.6-.2-.2-2-2.6-2-5s1.2-3.6 1.6-4c.4-.4.8-.6 1.2-.6h.8c.2 0 .6 0 .8.6.4 1 .8 2 .8 2.2.2.4 0 .8-.2 1.2-.2.2-.4.6-.6.8-.2.2-.4.4-.2.8.4.8 1.6 2.6 3.4 4 .2.2 3.6 3 7 .4.4-.2.8-.2 1.2 0 .4.2 2.6 1.2 3 1.4.4.2.6.2.8.4.2.2.2 1 0 2z" />
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<span>Hubungi Kami</span>
|
<span>Hubungi Kami</span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-8 py-3 font-bold text-white shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-blue-700">
|
||||||
|
|
||||||
<a href="/template" class="inline-flex justify-center items-center
|
|
||||||
rounded-xl
|
|
||||||
bg-gradient-to-r from-blue-600 to-blue-700
|
|
||||||
px-10 py-3
|
|
||||||
font-bold text-white
|
|
||||||
shadow-lg shadow-blue-500/30
|
|
||||||
transition-all duration-300
|
|
||||||
hover:scale-110 hover:shadow-2xl hover:shadow-blue-600/50 hover:from-blue-700 hover:to-blue-800">
|
|
||||||
Lihat Templat
|
Lihat Templat
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -85,12 +63,6 @@ onMounted(() => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
});
|
});
|
||||||
|
|
||||||
const adminNumber = "62895602603247";
|
|
||||||
const defaultMessage = "Halo Admin, saya mau tanya tentang undangan digital 🙏";
|
|
||||||
const waUrl = `https://wa.me/${adminNumber}?text=${encodeURIComponent(defaultMessage)}`;
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -4,264 +4,66 @@
|
|||||||
<h2 class="text-4xl font-extrabold text-gray-800 mb-2">
|
<h2 class="text-4xl font-extrabold text-gray-800 mb-2">
|
||||||
Apa Kata Mereka?
|
Apa Kata Mereka?
|
||||||
</h2>
|
</h2>
|
||||||
<p class="text-lg text-gray-600 mb-10">
|
<p class="text-lg text-gray-600 mb-16">
|
||||||
Kisah sukses dari para pengguna yang telah mempercayakan momen spesialnya kepada kami.
|
Kisah sukses dari para pengguna yang telah mempercayakan momen spesialnya kepada kami.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- CSS Marquee Scroll -->
|
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<div class="marquee-container mb-10">
|
<div
|
||||||
<div class="marquee-content" :style="{ '--total-cards': testimonials?.length || 0 }">
|
v-for="testimonial in testimonials"
|
||||||
<!-- Render original cards -->
|
:key="testimonial.id"
|
||||||
<div
|
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"
|
||||||
v-for="testimonial in testimonials"
|
>
|
||||||
:key="`original-${testimonial.id}`"
|
<div class="mb-4 flex items-center">
|
||||||
class="testimonial-card"
|
<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>
|
||||||
@click="previewModal = testimonial"
|
|
||||||
>
|
|
||||||
<!-- Rating -->
|
|
||||||
<div class="mb-4 flex items-center">
|
|
||||||
<svg
|
|
||||||
v-for="n in 5"
|
|
||||||
:key="n"
|
|
||||||
class="h-5 w-5"
|
|
||||||
:class="n <= Number(testimonial.rating) ? 'text-yellow-400' : 'text-gray-300'"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07
|
|
||||||
3.292a1 1 0 00.95.69h3.462c.969 0 1.371
|
|
||||||
1.24.588 1.81l-2.8 2.034a1 1 0
|
|
||||||
00-.364 1.118l1.07 3.292c.3.921-.755
|
|
||||||
1.688-1.54 1.118l-2.8-2.034a1 1
|
|
||||||
0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1
|
|
||||||
1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1
|
|
||||||
1 0 00.951-.69l1.07-3.292z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pesan -->
|
|
||||||
<p class="mb-6 flex-grow text-gray-600 italic line-clamp-3 min-h-[72px] break-words">
|
|
||||||
"{{ testimonial.message }}"
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- User Info -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-bold text-gray-800">{{ testimonial.name }}</h4>
|
|
||||||
<p class="text-sm text-gray-500">{{ testimonial.city }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Render clone untuk seamless loop -->
|
<p class="mb-6 flex-grow text-gray-600 italic">"{{ testimonial.text }}"</p>
|
||||||
<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 -->
|
<div class="flex items-center">
|
||||||
<p class="mb-6 flex-grow text-gray-600 italic line-clamp-3 min-h-[72px] break-words">
|
<img class="h-12 w-12 rounded-full object-cover" :src="testimonial.avatar" :alt="testimonial.name">
|
||||||
"{{ testimonial.message }}"
|
<div class="ml-4">
|
||||||
</p>
|
|
||||||
|
|
||||||
<!-- User Info -->
|
|
||||||
<div>
|
|
||||||
<h4 class="font-bold text-gray-800">{{ testimonial.name }}</h4>
|
<h4 class="font-bold text-gray-800">{{ testimonial.name }}</h4>
|
||||||
<p class="text-sm text-gray-500">{{ testimonial.city }}</p>
|
<p class="text-sm text-gray-500">{{ testimonial.role }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tombol Berikan Ulasan -->
|
|
||||||
<button
|
|
||||||
@click="openModal = true"
|
|
||||||
class="px-6 py-3 rounded-lg bg-blue-500 text-white font-semibold shadow hover:bg-blue-700 transition"
|
|
||||||
>
|
|
||||||
Berikan Ulasan
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Form -->
|
|
||||||
<div
|
|
||||||
v-if="openModal"
|
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/30"
|
|
||||||
>
|
|
||||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6 relative">
|
|
||||||
<button
|
|
||||||
@click="openModal = false"
|
|
||||||
class="absolute top-3 right-3 text-gray-500 hover:text-gray-800"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<h3 class="text-xl font-bold mb-4 text-gray-800">Tulis Ulasan</h3>
|
|
||||||
|
|
||||||
<form @submit.prevent="submitReview">
|
|
||||||
<div class="mb-4 text-left">
|
|
||||||
<label class="block text-sm font-medium mb-1">Nama</label>
|
|
||||||
<input v-model="form.name" type="text" class="w-full rounded border px-3 py-2" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4 text-left">
|
|
||||||
<label class="block text-sm font-medium mb-1">Kota</label>
|
|
||||||
<input v-model="form.city" type="text" class="w-full rounded border px-3 py-2" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4 text-left">
|
|
||||||
<label class="block text-sm font-medium mb-1">Rating</label>
|
|
||||||
<select v-model="form.rating" class="w-full rounded border px-3 py-2" required>
|
|
||||||
<option value="">Pilih rating</option>
|
|
||||||
<option v-for="n in 5" :key="n" :value="n">{{ n }} ⭐</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-4 text-left">
|
|
||||||
<label class="block text-sm font-medium mb-1">Pesan</label>
|
|
||||||
<textarea v-model="form.message" class="w-full rounded border px-3 py-2" rows="3" required />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="w-full bg-blue-600 text-white py-2 rounded-lg font-semibold hover:bg-blue-700 transition"
|
|
||||||
>
|
|
||||||
Kirim Ulasan
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal Preview -->
|
|
||||||
<div
|
|
||||||
v-if="previewModal"
|
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/30"
|
|
||||||
>
|
|
||||||
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6 relative">
|
|
||||||
<button
|
|
||||||
@click="previewModal = null"
|
|
||||||
class="absolute top-3 right-3 text-gray-500 hover:text-gray-800"
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<h3 class="text-xl font-bold mb-4 text-gray-800">Ulasan Lengkap</h3>
|
|
||||||
<p class="text-gray-600 italic mb-4 whitespace-pre-line break-words">
|
|
||||||
"{{ previewModal.message }}"
|
|
||||||
</p>
|
|
||||||
<h4 class="font-bold text-gray-800">{{ previewModal.name }}</h4>
|
|
||||||
<p class="text-sm text-gray-500">{{ previewModal.city }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue';
|
||||||
|
|
||||||
const { data: testimonials, refresh } = await useFetch('http://localhost:8000/api/reviews')
|
const testimonials = ref([
|
||||||
|
{
|
||||||
const openModal = ref(false)
|
id: 1,
|
||||||
const previewModal = ref(null)
|
name: 'Rizky & Anisa',
|
||||||
|
role: 'Pengantin Baru',
|
||||||
const form = ref({
|
avatar: 'https://i.pravatar.cc/100?u=rizky',
|
||||||
name: '',
|
rating: 5,
|
||||||
city: '',
|
text: 'Desainnya elegan dan modern! Proses pembuatannya juga cepat banget. Semua tamu memuji undangannya. Terima kasih Abbauf Tech!'
|
||||||
rating: '',
|
},
|
||||||
message: ''
|
{
|
||||||
})
|
id: 2,
|
||||||
|
name: 'Budi Santoso',
|
||||||
// Submit review
|
role: 'Event Organizer',
|
||||||
const submitReview = async () => {
|
avatar: 'https://i.pravatar.cc/100?u=budi',
|
||||||
try {
|
rating: 5,
|
||||||
await $fetch('http://localhost:8000/api/reviews', {
|
text: 'Sebagai EO, kami butuh platform yang efisien dan hasilnya premium. Abbauf Tech menjawab semua kebutuhan itu. Klien kami sangat puas.'
|
||||||
method: 'POST',
|
},
|
||||||
body: form.value
|
{
|
||||||
})
|
id: 3,
|
||||||
form.value = { name: '', city: '', rating: '', message: '' }
|
name: 'Citra Lestari',
|
||||||
openModal.value = false
|
role: 'Ulang Tahun Anak',
|
||||||
await refresh()
|
avatar: 'https://i.pravatar.cc/100?u=citra',
|
||||||
} catch (err) {
|
rating: 4,
|
||||||
console.error('Gagal simpan ulasan:', err)
|
text: 'Fitur RSVP dan pengingat sangat membantu. Tema-tema ulang tahunnya juga lucu dan bisa dikustomisasi. Sangat direkomendasikan!'
|
||||||
}
|
},
|
||||||
}
|
]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Marquee Container */
|
/* Kosong, semua diatur oleh Tailwind */
|
||||||
.marquee-container {
|
|
||||||
overflow: hidden;
|
|
||||||
padding: 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Marquee Content - Contains all cards */
|
|
||||||
.marquee-content {
|
|
||||||
display: flex;
|
|
||||||
gap: 1.5rem;
|
|
||||||
animation: marquee calc(var(--total-cards) * 8s) linear infinite;
|
|
||||||
width: max-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Individual testimonial card */
|
|
||||||
.testimonial-card {
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: 24rem; /* 384px = w-96 */
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
background: white;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: left;
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
|
||||||
transition: transform 300ms, box-shadow 300ms;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-card:hover {
|
|
||||||
transform: translateY(-0.5rem);
|
|
||||||
box-shadow: 0 25px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Marquee animation */
|
|
||||||
@keyframes marquee {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pause animation on hover */
|
|
||||||
.marquee-container:hover .marquee-content {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.testimonial-card {
|
|
||||||
width: 20rem; /* Smaller on mobile */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
@ -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(),
|
tailwindcss(),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
runtimeConfig: {
|
|
||||||
public: {
|
|
||||||
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
Loading…
Reference in New Issue
Block a user