Compare commits
46 Commits
production
...
main
Author | SHA1 | Date | |
---|---|---|---|
0eae52f23e | |||
|
1814b00390 | ||
8ff9fff4bf | |||
c8c82d5486 | |||
4c807da4d4 | |||
d550571217 | |||
|
1e9c6440cc | ||
|
72d25ad801 | ||
|
ac2b203f37 | ||
74840a4fcb | |||
ed60c36fa1 | |||
155c1928fd | |||
0d2ab7b957 | |||
55ae830e1d | |||
36dffc322e | |||
779a8c3232 | |||
3c2ca564fb | |||
78357efab7 | |||
0d87a843c2 | |||
|
982aee2a99 | ||
f09d6b8c9c | |||
fb8236daf3 | |||
07aac28e8a | |||
8a7d2eab98 | |||
44dfdec0b0 | |||
|
bfbc8db1fc | ||
|
2b945a5243 | ||
|
68479e8844 | ||
7cba5c3ebd | |||
0252dc8326 | |||
5c59fffc5c | |||
ead36a80de | |||
3b0879934e | |||
f5bab5de8a | |||
657daa7466 | |||
aeb1d56e85 | |||
31a39e9642 | |||
e6565fc33a | |||
bb5315fad5 | |||
43168c4273 | |||
eae9f9fd28 | |||
21d61178ac | |||
c5486575a3 | |||
acc32b08ca | |||
f38f8a286f | |||
6588ee8c46 |
41
backend/app/Http/Controllers/AdminAuthController.php
Normal file
41
backend/app/Http/Controllers/AdminAuthController.php
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
21
backend/app/Http/Controllers/Api/KategoriApiController.php
Normal file
21
backend/app/Http/Controllers/Api/KategoriApiController.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?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()
|
||||||
|
{
|
||||||
|
return response()->json(Kategori::all());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ambil detail satu kategori
|
||||||
|
public function show(Kategori $kategori)
|
||||||
|
{
|
||||||
|
return response()->json($kategori);
|
||||||
|
}
|
||||||
|
}
|
65
backend/app/Http/Controllers/Api/KhitanApiController.php
Normal file
65
backend/app/Http/Controllers/Api/KhitanApiController.php
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
<?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 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|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ✅ 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);
|
||||||
|
}
|
||||||
|
}
|
86
backend/app/Http/Controllers/Api/PernikahanApiController.php
Normal file
86
backend/app/Http/Controllers/Api/PernikahanApiController.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?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 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|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ✅ 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);
|
||||||
|
}
|
||||||
|
}
|
56
backend/app/Http/Controllers/Api/ReviewController.php
Normal file
56
backend/app/Http/Controllers/Api/ReviewController.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
21
backend/app/Http/Controllers/Api/TemplateApiController.php
Normal file
21
backend/app/Http/Controllers/Api/TemplateApiController.php
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?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()
|
||||||
|
{
|
||||||
|
return response()->json(Template::with(['kategori','fitur'])->get());
|
||||||
|
}
|
||||||
|
|
||||||
|
// User bisa lihat detail 1 template
|
||||||
|
public function show(Template $template)
|
||||||
|
{
|
||||||
|
return response()->json($template->load(['kategori','fitur']));
|
||||||
|
}
|
||||||
|
}
|
63
backend/app/Http/Controllers/Api/UlangTahunApiController.php
Normal file
63
backend/app/Http/Controllers/Api/UlangTahunApiController.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?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',
|
||||||
|
'galeri' => 'nullable|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ✅ 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);
|
||||||
|
}
|
||||||
|
}
|
56
backend/app/Http/Controllers/FiturController.php
Normal file
56
backend/app/Http/Controllers/FiturController.php
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Fitur;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class FiturController extends Controller
|
||||||
|
{
|
||||||
|
// Tampilkan semua fitur (halaman admin)
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$fitur = Fitur::all();
|
||||||
|
return view('admin.fitur.index', compact('fitur'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form tambah fitur
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.fitur.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simpan fitur baru
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'deskripsi' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Fitur::create($data);
|
||||||
|
|
||||||
|
return redirect()->route('admin.fitur.index')->with('success', 'Fitur berhasil ditambahkan!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Fitur $fitur)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'deskripsi' => 'required|string',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$fitur->update($data);
|
||||||
|
|
||||||
|
return redirect()->route('admin.fitur.index')->with('success', 'Fitur berhasil diperbarui!');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Hapus fitur
|
||||||
|
public function destroy(Fitur $fitur)
|
||||||
|
{
|
||||||
|
$fitur->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.fitur.index')->with('success', 'Fitur berhasil dihapus!');
|
||||||
|
}
|
||||||
|
}
|
64
backend/app/Http/Controllers/KategoriController.php
Normal file
64
backend/app/Http/Controllers/KategoriController.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
<?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!');
|
||||||
|
}
|
||||||
|
}
|
29
backend/app/Http/Controllers/PelangganController.php
Normal file
29
backend/app/Http/Controllers/PelangganController.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Pelanggan;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class PelangganController extends Controller
|
||||||
|
{
|
||||||
|
// Tampilkan semua pelanggan (admin)
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$pelanggans = Pelanggan::all();
|
||||||
|
return view('admin.pelanggan.index', compact('pelanggans'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detail pelanggan
|
||||||
|
public function show(Pelanggan $pelanggan)
|
||||||
|
{
|
||||||
|
return view('admin.pelanggan.show', compact('pelanggan'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hapus pelanggan
|
||||||
|
public function destroy(Pelanggan $pelanggan)
|
||||||
|
{
|
||||||
|
$pelanggan->delete();
|
||||||
|
return redirect()->route('admin.pelanggan.index')->with('success', 'Pelanggan berhasil dihapus!');
|
||||||
|
}
|
||||||
|
}
|
82
backend/app/Http/Controllers/TemplateController.php
Normal file
82
backend/app/Http/Controllers/TemplateController.php
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Template;
|
||||||
|
use App\Models\Kategori;
|
||||||
|
use App\Models\Fitur;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class TemplateController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$templates = Template::with(['kategori','fitur'])->get();
|
||||||
|
$kategoris = Kategori::all();
|
||||||
|
$fiturs = Fitur::all();
|
||||||
|
|
||||||
|
return view('admin.templates.index', compact('templates', 'kategoris', 'fiturs'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$data = $request->validate([
|
||||||
|
'nama_template' => 'required|string|max:255',
|
||||||
|
'kategori_id' => 'required|exists:kategoris,id',
|
||||||
|
'fitur_id' => 'required|exists:fiturs,id',
|
||||||
|
'foto' => 'nullable|image|mimes:jpg,jpeg,png,gif|max:5120',
|
||||||
|
'harga' => 'required|numeric|min:0'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->hasFile('foto')) {
|
||||||
|
$data['foto'] = $request->file('foto')->store('templates', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
Template::create($data);
|
||||||
|
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|exists:fiturs,id',
|
||||||
|
'foto' => 'nullable|image|mimes:jpg,jpeg,png,gif|max:5120',
|
||||||
|
'harga' => 'required|numeric|min:0'
|
||||||
|
]);
|
||||||
|
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
$template->update($data);
|
||||||
|
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->delete();
|
||||||
|
return redirect()->route('templates.index')->with('success', 'Template berhasil dihapus!');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function byKategori($id)
|
||||||
|
{
|
||||||
|
$kategori = Kategori::findOrFail($id);
|
||||||
|
$templates = Template::with(['kategori','fitur'])
|
||||||
|
->where('kategori_id', $id)
|
||||||
|
->get();
|
||||||
|
$kategoris = Kategori::all();
|
||||||
|
$fiturs = Fitur::all();
|
||||||
|
|
||||||
|
return view('admin.templates.index', compact('templates', 'kategoris', 'fiturs', 'kategori'));
|
||||||
|
}
|
||||||
|
}
|
15
backend/app/Models/Admin.php
Normal file
15
backend/app/Models/Admin.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?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'];
|
||||||
|
}
|
15
backend/app/Models/Fitur.php
Normal file
15
backend/app/Models/Fitur.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
// app/Models/Fitur.php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Fitur extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['deskripsi'];
|
||||||
|
|
||||||
|
public function templates()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Template::class);
|
||||||
|
}
|
||||||
|
}
|
15
backend/app/Models/Kategori.php
Normal file
15
backend/app/Models/Kategori.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
35
backend/app/Models/Khitan.php
Normal file
35
backend/app/Models/Khitan.php
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
25
backend/app/Models/Pelanggan.php
Normal file
25
backend/app/Models/Pelanggan.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?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->hasOne(PelangganDetail::class);
|
||||||
|
}
|
||||||
|
}
|
22
backend/app/Models/PelangganDetail.php
Normal file
22
backend/app/Models/PelangganDetail.php
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
59
backend/app/Models/Pernikahan.php
Normal file
59
backend/app/Models/Pernikahan.php
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
18
backend/app/Models/Review.php
Normal file
18
backend/app/Models/Review.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?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',
|
||||||
|
];
|
||||||
|
}
|
30
backend/app/Models/Template.php
Normal file
30
backend/app/Models/Template.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
// app/Models/Template.php
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Template extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['nama_template', 'kategori_id', 'fitur_id', 'foto', 'harga'];
|
||||||
|
|
||||||
|
public function kategori() {
|
||||||
|
return $this->belongsTo(Kategori::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fitur() {
|
||||||
|
return $this->belongsTo(Fitur::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function pernikahan() {
|
||||||
|
return $this->hasOne(Pernikahan::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ulangTahun() {
|
||||||
|
return $this->hasOne(UlangTahun::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function khitan() {
|
||||||
|
return $this->hasOne(Khitan::class);
|
||||||
|
}
|
||||||
|
}
|
36
backend/app/Models/UlangTahun.php
Normal file
36
backend/app/Models/UlangTahun.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?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,94 +2,39 @@
|
|||||||
|
|
||||||
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,
|
||||||
],
|
],
|
||||||
|
|
||||||
// 'users' => [
|
// Provider untuk admin
|
||||||
// 'driver' => 'database',
|
'admins' => [
|
||||||
// 'table' => 'users',
|
'driver' => 'eloquent',
|
||||||
// ],
|
'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',
|
||||||
@ -97,19 +42,14 @@ 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,
|
||||||
|
|
||||||
];
|
];
|
||||||
|
23
backend/database/factories/TemplateFactory.php
Normal file
23
backend/database/factories/TemplateFactory.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?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 [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,22 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,20 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
<?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 {
|
||||||
|
Schema::create('templates', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('nama_template');
|
||||||
|
$table->foreignId('kategori_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->foreignId('fitur_id')->constrained()->cascadeOnDelete();
|
||||||
|
$table->decimal('harga', 10, 2)->default(0);
|
||||||
|
$table->string('foto')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void {
|
||||||
|
Schema::dropIfExists('templates');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,26 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,65 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
@ -0,0 +1,43 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -2,16 +2,20 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use App\Models\Admin;
|
||||||
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,16 +7,10 @@ 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
|
||||||
{
|
{
|
||||||
// \App\Models\User::factory(10)->create();
|
$this->call([
|
||||||
|
AdminSeeder::class,
|
||||||
// \App\Models\User::factory()->create([
|
]);
|
||||||
// 'name' => 'Test User',
|
}
|
||||||
// 'email' => 'test@example.com',
|
|
||||||
// ]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
BIN
backend/public/images/logo.png
Normal file
BIN
backend/public/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
109
backend/resources/views/admin/auth/login.blade.php
Normal file
109
backend/resources/views/admin/auth/login.blade.php
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<!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>
|
54
backend/resources/views/admin/dashboard.blade.php
Normal file
54
backend/resources/views/admin/dashboard.blade.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
@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>
|
||||||
|
{{ \Carbon\Carbon::now()->translatedFormat('l, d F Y') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cards -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<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">10</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">Templat</h5>
|
||||||
|
<h3 class="font-bold text-xl">20</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">24</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>
|
||||||
|
|
||||||
|
<!-- Pesan login -->
|
||||||
|
<div class="bg-green-100 text-green-700 p-4 rounded-lg mt-4">
|
||||||
|
Berhasil login sebagai <strong>{{ auth('admin')->user()->name }}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
238
backend/resources/views/admin/fitur/index.blade.php
Normal file
238
backend/resources/views/admin/fitur/index.blade.php
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
@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">Manajemen 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-[70%] p-2 border border-gray-300 text-center">Fitur</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">
|
||||||
|
<td class="py-5 px-2 border border-gray-300 text-center">{{ $key + 1 }}</td>
|
||||||
|
<td class="py-5 px-2 border border-gray-300 truncate whitespace-nowrap">{{ $item->deskripsi }}</td>
|
||||||
|
<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="3" class="p-2 text-center text-gray-500">Belum ada fitur</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.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">
|
||||||
|
<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>
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</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
|
285
backend/resources/views/admin/kategori/index.blade.php
Normal file
285
backend/resources/views/admin/kategori/index.blade.php
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
@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
|
141
backend/resources/views/admin/pelanggan/index.blade.php
Normal file
141
backend/resources/views/admin/pelanggan/index.blade.php
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Manajemen Pelanggan')
|
||||||
|
|
||||||
|
@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 Pelanggan</h3>
|
||||||
|
</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">Nama 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">Aksi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse($pelanggans as $key => $pelanggan)
|
||||||
|
<tr>
|
||||||
|
<td class="p-2 py-4 border border-gray-300 text-center">{{ $key + 1 }}</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="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="5" class="p-2 text-center text-gray-500 border border-gray-300">
|
||||||
|
Belum ada pelanggan
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</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 }}</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
|
119
backend/resources/views/admin/reviews/index.blade.php
Normal file
119
backend/resources/views/admin/reviews/index.blade.php
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
@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
|
295
backend/resources/views/admin/templates/index.blade.php
Normal file
295
backend/resources/views/admin/templates/index.blade.php
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
@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 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 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-[150px] 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">{{ $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 truncate">{{ $template->fitur->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="{{ $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">
|
||||||
|
<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>
|
||||||
|
</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 overflow-hidden">
|
||||||
|
<form action="{{ route('templates.store') }}" method="POST" enctype="multipart/form-data">
|
||||||
|
@csrf
|
||||||
|
<div class="p-4 border-b">
|
||||||
|
<h5 class="text-lg font-medium">Tambah Template</h5>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">Kategori</label>
|
||||||
|
<select name="kategori_id" class="w-full p-2 border rounded" required>
|
||||||
|
@foreach ($kategoris as $kategori)
|
||||||
|
<option value="{{ $kategori->id }}">{{ $kategori->nama }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">Fitur</label>
|
||||||
|
<select name="fitur_id" class="w-full p-2 border rounded" required>
|
||||||
|
@foreach ($fiturs as $fitur)
|
||||||
|
<option value="{{ $fitur->id }}">
|
||||||
|
{{ \Illuminate\Support\Str::limit($fitur->deskripsi, 50) }}
|
||||||
|
</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">Harga</label>
|
||||||
|
<input type="number" name="harga" class="w-full p-2 border rounded" required
|
||||||
|
min="0" step="1000">
|
||||||
|
</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 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>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<!-- Modal Edit Template -->
|
||||||
|
@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 overflow-hidden">
|
||||||
|
<form action="{{ route('templates.update', $template->id) }}" method="POST"
|
||||||
|
enctype="multipart/form-data">
|
||||||
|
@csrf @method('PUT')
|
||||||
|
<div class="p-4 border-b">
|
||||||
|
<h5 class="text-lg font-medium">Edit Template</h5>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">Nama Template</label>
|
||||||
|
<input type="text" name="nama_template" value="{{ $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" class="w-full p-2 border rounded" required>
|
||||||
|
@foreach ($kategoris as $kategori)
|
||||||
|
<option value="{{ $kategori->id }}" @selected($kategori->id == $template->kategori_id)>
|
||||||
|
{{ $kategori->nama }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">Fitur</label>
|
||||||
|
<select name="fitur_id" class="w-full p-2 border rounded" required>
|
||||||
|
@foreach ($fiturs as $fitur)
|
||||||
|
<option value="{{ $fitur->id }}" @selected($fitur->id == $template->fitur_id)>
|
||||||
|
{{ $fitur->deskripsi }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium">Harga</label>
|
||||||
|
<input type="number" name="harga" value="{{ $template->harga }}"
|
||||||
|
class="w-full p-2 border rounded" required min="0" step="1000">
|
||||||
|
</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 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="{{ $template->id }}">Batal</button>
|
||||||
|
<button class="bg-blue-600 text-white px-3 py-1 rounded">Simpan Perubahan</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
<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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
@endsection
|
149
backend/resources/views/layouts/app.blade.php
Normal file
149
backend/resources/views/layouts/app.blade.php
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
<!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>
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('admin.kategori.index') }}"
|
||||||
|
class="flex items-center py-2 px-3 rounded hover:bg-blue-50 {{ request()->is('admin/kategori*') ? 'bg-blue-100 text-blue-600' : 'text-gray-700' }}">
|
||||||
|
<i class="bi bi-diagram-3 me-2"></i> Kategori
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="{{ route('admin.fitur.index') }}"
|
||||||
|
class="flex items-center py-2 px-3 rounded hover:bg-blue-50 {{ request()->is('admin/fitur*') ? 'bg-blue-100 text-blue-600' : 'text-gray-700' }}">
|
||||||
|
<i class="bi bi-grid me-2"></i> Fitur
|
||||||
|
</a>
|
||||||
|
</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> 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');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
@ -2,18 +2,26 @@
|
|||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\Api\ReviewController;
|
||||||
|
use App\Http\Controllers\Api\KategoriApiController;
|
||||||
|
use App\Http\Controllers\Api\PernikahanApiController;
|
||||||
|
use App\Http\Controllers\Api\UlangTahunApiController;
|
||||||
|
use App\Http\Controllers\Api\KhitanApiController;
|
||||||
|
use App\Http\Controllers\Api\TemplateApiController;
|
||||||
|
|
||||||
/*
|
// Form API (user)
|
||||||
|--------------------------------------------------------------------------
|
Route::post('form/pernikahan', [PernikahanApiController::class, 'store']);
|
||||||
| API Routes
|
Route::post('form/ulang-tahun', [UlangTahunApiController::class, 'store']);
|
||||||
|--------------------------------------------------------------------------
|
Route::post('form/khitan', [KhitanApiController::class, 'store']);
|
||||||
|
|
|
||||||
| Here is where you can register API routes for your application. These
|
// API Kategori hanya read-only
|
||||||
| routes are loaded by the RouteServiceProvider and all of them will
|
Route::get('kategoris', [KategoriApiController::class, 'index']);
|
||||||
| be assigned to the "api" middleware group. Make something great!
|
Route::get('kategoris/{kategori}', [KategoriApiController::class, 'show']);
|
||||||
|
|
|
||||||
*/
|
// API Reviews
|
||||||
|
Route::apiResource('reviews', ReviewController::class);
|
||||||
|
|
||||||
|
// API Templates
|
||||||
|
Route::get('templates', [TemplateApiController::class, 'index']);
|
||||||
|
Route::get('templates/{template}', [TemplateApiController::class, 'show']);
|
||||||
|
|
||||||
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
|
|
||||||
return $request->user();
|
|
||||||
});
|
|
||||||
|
@ -1,18 +1,83 @@
|
|||||||
<?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;
|
||||||
|
|
||||||
/*
|
//Login
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| 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 view('welcome');
|
return redirect()->route('admin.login');
|
||||||
|
});
|
||||||
|
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', function () {
|
||||||
|
return view('admin.dashboard');
|
||||||
|
})->name('dashboard');
|
||||||
|
|
||||||
|
Route::post('/logout', [AdminAuthController::class, 'logout'])->name('logout');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//Kategori
|
||||||
|
Route::prefix('admin')->name('admin.')->group(function () {
|
||||||
|
Route::resource('kategori', KategoriController::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Route Admin Fitur
|
||||||
|
Route::prefix('admin')->name('admin.')->group(function () {
|
||||||
|
Route::resource('fitur', FiturController::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// Route Admin Pelanggan
|
||||||
|
Route::prefix('admin')->name('admin.')->group(function () {
|
||||||
|
Route::resource('pelanggan', PelangganController::class)->only([
|
||||||
|
'index',
|
||||||
|
'show',
|
||||||
|
'destroy'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\ReviewController;
|
||||||
|
use App\Models\Review;
|
||||||
|
|
||||||
|
Route::prefix('admin')->name('admin.')->middleware('auth:admin')->group(function () {
|
||||||
|
// Halaman daftar ulasan
|
||||||
|
Route::get('/ulasan', function () {
|
||||||
|
$reviews = \App\Models\Review::latest()->get();
|
||||||
|
return view('admin.reviews.index', compact('reviews'));
|
||||||
|
})->name('reviews.index');
|
||||||
|
|
||||||
|
// Tambah ulasan
|
||||||
|
Route::post('/ulasan', [ReviewController::class, 'store'])->name('reviews.store');
|
||||||
|
|
||||||
|
// Update ulasan
|
||||||
|
Route::put('/ulasan/{review}', [ReviewController::class, 'update'])->name('reviews.update');
|
||||||
|
|
||||||
|
// Hapus ulasan
|
||||||
|
Route::delete('/ulasan/{review}', function (Review $review) {
|
||||||
|
$review->delete();
|
||||||
|
return redirect()->route('admin.reviews.index')->with('success', 'Ulasan berhasil dihapus');
|
||||||
|
})->name('reviews.destroy');
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,35 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<NuxtPage />
|
<NuxtLayout>
|
||||||
|
<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']);
|
@ -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>
|
||||||
|
@ -30,7 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="see-more">
|
<div class="see-more">
|
||||||
<a href="#">Lihat Selengkapnya...</a>
|
<NuxtLink to="/template">Lihat Selengkapnya...</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
<header class="main-header">
|
<header class="main-header">
|
||||||
<nav class="container">
|
<nav class="container">
|
||||||
<div class="logo">
|
<div class="logo">
|
||||||
<img src="/abbauflogo.png" alt="Abbauf Tech Logo" class="logo-icon">
|
<NuxtLink to="/" class="logo-link">
|
||||||
<span>ABBAUF TECH</span>
|
<img :src="logo" alt="Abbauf Tech Logo" class="logo-icon" />
|
||||||
|
<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">
|
||||||
<a :href="link.path">{{ link.name }}</a>
|
<NuxtLink :to="link.path">{{ link.name }}</NuxtLink>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@ -16,23 +18,37 @@
|
|||||||
|
|
||||||
<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: '#beranda' },
|
{ name: 'Beranda', path: '/' },
|
||||||
{ 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 {
|
||||||
@ -44,26 +60,17 @@ const navLinks = ref([
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo a.logo-link {
|
||||||
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: 55px;
|
width: 40px;
|
||||||
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 ml-14">
|
<h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl pl-15">
|
||||||
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,10 +16,10 @@
|
|||||||
Tanpa Ribet
|
Tanpa Ribet
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="mt-4 mb-8 text-lg text-gray-600 ml-14">
|
<p class="mt-4 mb-8 text-lg text-gray-600 pl-15">
|
||||||
Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu. Praktis, cepat, dan bisa langsung digunakan.
|
Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu. Praktis, cepat, dan bisa langsung digunakan.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex flex-col justify-center gap-4 sm:flex-row ml-14">
|
<div class="flex flex-col justify-center gap-4 sm:flex-row pl-15">
|
||||||
<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="#" class="inline-flex items-center justify-center rounded-lg border-2 border-green-500 bg-white px-8 py-3 font-bold text-gray-800 shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md">
|
||||||
<svg class="mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg class="mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M19.2239 4.7761C17.1659 2.7181 14.6599 1.5 11.9999 1.5...Z" fill="#25D366"/>
|
<path d="M19.2239 4.7761C17.1659 2.7181 14.6599 1.5 11.9999 1.5...Z" fill="#25D366"/>
|
||||||
@ -27,9 +27,10 @@
|
|||||||
</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 items-center justify-center rounded-lg bg-blue-600 px-8 py-3 font-bold text-white shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-blue-700">
|
||||||
Lihat Templat
|
Lihat Templat
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -4,66 +4,264 @@
|
|||||||
<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-16">
|
<p class="text-lg text-gray-600 mb-10">
|
||||||
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>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
|
<!-- CSS Marquee Scroll -->
|
||||||
<div
|
<div class="marquee-container mb-10">
|
||||||
v-for="testimonial in testimonials"
|
<div class="marquee-content" :style="{ '--total-cards': testimonials?.length || 0 }">
|
||||||
:key="testimonial.id"
|
<!-- Render original cards -->
|
||||||
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"
|
<div
|
||||||
>
|
v-for="testimonial in testimonials"
|
||||||
<div class="mb-4 flex items-center">
|
:key="`original-${testimonial.id}`"
|
||||||
<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>
|
class="testimonial-card"
|
||||||
|
@click="previewModal = testimonial"
|
||||||
|
>
|
||||||
|
<!-- Rating -->
|
||||||
|
<div class="mb-4 flex items-center">
|
||||||
|
<svg
|
||||||
|
v-for="n in 5"
|
||||||
|
:key="n"
|
||||||
|
class="h-5 w-5"
|
||||||
|
:class="n <= Number(testimonial.rating) ? 'text-yellow-400' : 'text-gray-300'"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07
|
||||||
|
3.292a1 1 0 00.95.69h3.462c.969 0 1.371
|
||||||
|
1.24.588 1.81l-2.8 2.034a1 1 0
|
||||||
|
00-.364 1.118l1.07 3.292c.3.921-.755
|
||||||
|
1.688-1.54 1.118l-2.8-2.034a1 1
|
||||||
|
0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1
|
||||||
|
1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1
|
||||||
|
1 0 00.951-.69l1.07-3.292z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pesan -->
|
||||||
|
<p class="mb-6 flex-grow text-gray-600 italic line-clamp-3 min-h-[72px] break-words">
|
||||||
|
"{{ testimonial.message }}"
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- User Info -->
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold text-gray-800">{{ testimonial.name }}</h4>
|
||||||
|
<p class="text-sm text-gray-500">{{ testimonial.city }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mb-6 flex-grow text-gray-600 italic">"{{ testimonial.text }}"</p>
|
<!-- Render clone untuk seamless loop -->
|
||||||
|
<div
|
||||||
|
v-for="testimonial in testimonials"
|
||||||
|
:key="`clone-${testimonial.id}`"
|
||||||
|
class="testimonial-card"
|
||||||
|
@click="previewModal = testimonial"
|
||||||
|
>
|
||||||
|
<!-- Rating -->
|
||||||
|
<div class="mb-4 flex items-center">
|
||||||
|
<svg
|
||||||
|
v-for="n in 5"
|
||||||
|
:key="n"
|
||||||
|
class="h-5 w-5"
|
||||||
|
:class="n <= Number(testimonial.rating) ? 'text-yellow-400' : 'text-gray-300'"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07
|
||||||
|
3.292a1 1 0 00.95.69h3.462c.969 0 1.371
|
||||||
|
1.24.588 1.81l-2.8 2.034a1 1 0
|
||||||
|
00-.364 1.118l1.07 3.292c.3.921-.755
|
||||||
|
1.688-1.54 1.118l-2.8-2.034a1 1
|
||||||
|
0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1
|
||||||
|
1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1
|
||||||
|
1 0 00.951-.69l1.07-3.292z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center">
|
<!-- Pesan -->
|
||||||
<img class="h-12 w-12 rounded-full object-cover" :src="testimonial.avatar" :alt="testimonial.name">
|
<p class="mb-6 flex-grow text-gray-600 italic line-clamp-3 min-h-[72px] break-words">
|
||||||
<div class="ml-4">
|
"{{ testimonial.message }}"
|
||||||
|
</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.role }}</p>
|
<p class="text-sm text-gray-500">{{ testimonial.city }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Tombol Berikan Ulasan -->
|
||||||
|
<button
|
||||||
|
@click="openModal = true"
|
||||||
|
class="px-6 py-3 rounded-lg bg-blue-500 text-white font-semibold shadow hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
Berikan Ulasan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Form -->
|
||||||
|
<div
|
||||||
|
v-if="openModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/30"
|
||||||
|
>
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6 relative">
|
||||||
|
<button
|
||||||
|
@click="openModal = false"
|
||||||
|
class="absolute top-3 right-3 text-gray-500 hover:text-gray-800"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-bold mb-4 text-gray-800">Tulis Ulasan</h3>
|
||||||
|
|
||||||
|
<form @submit.prevent="submitReview">
|
||||||
|
<div class="mb-4 text-left">
|
||||||
|
<label class="block text-sm font-medium mb-1">Nama</label>
|
||||||
|
<input v-model="form.name" type="text" class="w-full rounded border px-3 py-2" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 text-left">
|
||||||
|
<label class="block text-sm font-medium mb-1">Kota</label>
|
||||||
|
<input v-model="form.city" type="text" class="w-full rounded border px-3 py-2" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 text-left">
|
||||||
|
<label class="block text-sm font-medium mb-1">Rating</label>
|
||||||
|
<select v-model="form.rating" class="w-full rounded border px-3 py-2" required>
|
||||||
|
<option value="">Pilih rating</option>
|
||||||
|
<option v-for="n in 5" :key="n" :value="n">{{ n }} ⭐</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-4 text-left">
|
||||||
|
<label class="block text-sm font-medium mb-1">Pesan</label>
|
||||||
|
<textarea v-model="form.message" class="w-full rounded border px-3 py-2" rows="3" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full bg-blue-600 text-white py-2 rounded-lg font-semibold hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
Kirim Ulasan
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal Preview -->
|
||||||
|
<div
|
||||||
|
v-if="previewModal"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-gray-800/30"
|
||||||
|
>
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6 relative">
|
||||||
|
<button
|
||||||
|
@click="previewModal = null"
|
||||||
|
class="absolute top-3 right-3 text-gray-500 hover:text-gray-800"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<h3 class="text-xl font-bold mb-4 text-gray-800">Ulasan Lengkap</h3>
|
||||||
|
<p class="text-gray-600 italic mb-4 whitespace-pre-line break-words">
|
||||||
|
"{{ previewModal.message }}"
|
||||||
|
</p>
|
||||||
|
<h4 class="font-bold text-gray-800">{{ previewModal.name }}</h4>
|
||||||
|
<p class="text-sm text-gray-500">{{ previewModal.city }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const testimonials = ref([
|
const { data: testimonials, refresh } = await useFetch('http://localhost:8000/api/reviews')
|
||||||
{
|
|
||||||
id: 1,
|
const openModal = ref(false)
|
||||||
name: 'Rizky & Anisa',
|
const previewModal = ref(null)
|
||||||
role: 'Pengantin Baru',
|
|
||||||
avatar: 'https://i.pravatar.cc/100?u=rizky',
|
const form = ref({
|
||||||
rating: 5,
|
name: '',
|
||||||
text: 'Desainnya elegan dan modern! Proses pembuatannya juga cepat banget. Semua tamu memuji undangannya. Terima kasih Abbauf Tech!'
|
city: '',
|
||||||
},
|
rating: '',
|
||||||
{
|
message: ''
|
||||||
id: 2,
|
})
|
||||||
name: 'Budi Santoso',
|
|
||||||
role: 'Event Organizer',
|
// Submit review
|
||||||
avatar: 'https://i.pravatar.cc/100?u=budi',
|
const submitReview = async () => {
|
||||||
rating: 5,
|
try {
|
||||||
text: 'Sebagai EO, kami butuh platform yang efisien dan hasilnya premium. Abbauf Tech menjawab semua kebutuhan itu. Klien kami sangat puas.'
|
await $fetch('http://localhost:8000/api/reviews', {
|
||||||
},
|
method: 'POST',
|
||||||
{
|
body: form.value
|
||||||
id: 3,
|
})
|
||||||
name: 'Citra Lestari',
|
form.value = { name: '', city: '', rating: '', message: '' }
|
||||||
role: 'Ulang Tahun Anak',
|
openModal.value = false
|
||||||
avatar: 'https://i.pravatar.cc/100?u=citra',
|
await refresh()
|
||||||
rating: 4,
|
} catch (err) {
|
||||||
text: 'Fitur RSVP dan pengingat sangat membantu. Tema-tema ulang tahunnya juga lucu dan bisa dikustomisasi. Sangat direkomendasikan!'
|
console.error('Gagal simpan ulasan:', err)
|
||||||
},
|
}
|
||||||
]);
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Kosong, semua diatur oleh Tailwind */
|
/* Marquee Container */
|
||||||
|
.marquee-container {
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Marquee Content - Contains all cards */
|
||||||
|
.marquee-content {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
animation: marquee calc(var(--total-cards) * 8s) linear infinite;
|
||||||
|
width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual testimonial card */
|
||||||
|
.testimonial-card {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 24rem; /* 384px = w-96 */
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
background: white;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
transition: transform 300ms, box-shadow 300ms;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-card:hover {
|
||||||
|
transform: translateY(-0.5rem);
|
||||||
|
box-shadow: 0 25px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Marquee animation */
|
||||||
|
@keyframes marquee {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pause animation on hover */
|
||||||
|
.marquee-container:hover .marquee-content {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.testimonial-card {
|
||||||
|
width: 20rem; /* Smaller on mobile */
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<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>
|
||||||
|
<h1 class="text-3xl md:text-4xl font-bold text-center text-gray-800">
|
||||||
|
Pilih Template Favoritmu
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-center text-gray-500">
|
||||||
|
"Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya."
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<div v-else-if="categories && categories.length > 0" class="mt-12 grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
<div
|
||||||
|
v-for="category in categories"
|
||||||
|
:key="category.id"
|
||||||
|
@click="onCategoryClick(category)"
|
||||||
|
class="group cursor-pointer overflow-hidden rounded-lg shadow-lg hover:shadow-2xl transition-all duration-300"
|
||||||
|
>
|
||||||
|
<img :src="category.foto" :alt="category.nama" class="w-full h-48 object-cover group-hover:scale-110 transition-transform duration-300">
|
||||||
|
<div class="p-6 bg-white">
|
||||||
|
<h3 class="text-xl font-semibold text-gray-800">{{ category.nama }}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
const emit = defineEmits(['category-selected']);
|
||||||
|
|
||||||
|
// Gunakan useFetch yang lebih modern dan bersih
|
||||||
|
const { data: categories, pending: isLoading, error } = useFetch('http://localhost:8000/api/kategoris');
|
||||||
|
|
||||||
|
const onCategoryClick = (category) => {
|
||||||
|
// Dan mengirimkan seluruh objek tersebut ke induk
|
||||||
|
emit('category-selected', category);
|
||||||
|
};
|
||||||
|
</script>
|
@ -0,0 +1,94 @@
|
|||||||
|
<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">
|
||||||
|
|
||||||
|
<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="Template">
|
||||||
|
|
||||||
|
|
||||||
|
<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.deskripsi" 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">
|
||||||
|
<li v-for="(feature, index) in tpl.fitur.deskripsi.split(',')" :key="index">
|
||||||
|
{{ feature.trim() }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<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>
|
||||||
|
<button class="w-full bg-blue-600 text-white font-semibold py-2 px-4 rounded-lg hover:bg-blue-700 transition-colors">Order</button>
|
||||||
|
</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>
|
37
proyek-frontend/app/pages/template.vue
Normal file
37
proyek-frontend/app/pages/template.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<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,5 +19,9 @@ 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