Compare commits

..

46 Commits

Author SHA1 Message Date
0eae52f23e fix desain 2025-09-11 16:02:17 +07:00
Farhaan4
1814b00390 fix template & kategori 2025-09-11 15:04:49 +07:00
8ff9fff4bf fix bug logo 2025-09-11 14:58:28 +07:00
c8c82d5486 fix 2025-09-11 14:42:34 +07:00
4c807da4d4 Merge branch 'main' of https://git.abbauf.com/Magang-2025/Undangan 2025-09-11 14:26:46 +07:00
d550571217 add testimoni 2025-09-11 14:26:39 +07:00
Farhaan4
1e9c6440cc tes api kategori & template 2025-09-11 14:12:05 +07:00
Farhaan4
72d25ad801 Merge branch 'main' of https://git.abbauf.com/Magang-2025/Undangan 2025-09-11 11:26:46 +07:00
Farhaan4
ac2b203f37 template 2025-09-11 11:26:04 +07:00
74840a4fcb add notif 2025-09-11 11:21:02 +07:00
ed60c36fa1 add modal delete 2025-09-11 11:10:13 +07:00
155c1928fd Merge branch 'main' of https://git.abbauf.com/Magang-2025/Undangan 2025-09-11 10:29:04 +07:00
0d2ab7b957 fix desain 2025-09-11 10:28:52 +07:00
55ae830e1d Merge branch 'main' of https://git.abbauf.com/Magang-2025/Undangan 2025-09-11 10:28:30 +07:00
36dffc322e [Ubah Controller Pernikahan, HBD, Khitan] 2025-09-11 10:27:46 +07:00
779a8c3232 fix desain 2025-09-11 10:24:21 +07:00
3c2ca564fb [Edit From] 2025-09-11 09:50:26 +07:00
78357efab7 Merge branch 'main' of https://git.abbauf.com/Magang-2025/Undangan 2025-09-10 20:46:56 +07:00
0d87a843c2 fix desain 2025-09-10 20:28:35 +07:00
Farhaan4
982aee2a99 tes api kategori 2025-09-10 20:05:46 +07:00
f09d6b8c9c fix desain 2025-09-09 14:53:44 +07:00
fb8236daf3 fix harga 2025-09-09 13:53:48 +07:00
07aac28e8a [Contoller Templat Harga] 2025-09-09 13:21:03 +07:00
8a7d2eab98 add template dll. 2025-09-09 11:40:57 +07:00
44dfdec0b0 [Ubah All] 2025-09-08 15:55:58 +07:00
Alfa Ramadhan
bfbc8db1fc API template 2025-09-08 14:23:18 +07:00
Alfa Ramadhan
2b945a5243 Merge branch 'main' of https://git.abbauf.com/Magang-2025/Undangan 2025-09-04 13:34:43 +07:00
Alfa Ramadhan
68479e8844 Template 2025-09-04 13:34:29 +07:00
7cba5c3ebd delete account 2025-09-04 13:32:27 +07:00
0252dc8326 fix review 2025-09-04 10:09:32 +07:00
5c59fffc5c FIx desain 2025-09-04 10:03:10 +07:00
ead36a80de [Edit Kategori] 2025-09-04 09:35:32 +07:00
3b0879934e fix image size 2025-09-03 16:39:16 +07:00
f5bab5de8a fix image size 2025-09-03 16:38:06 +07:00
657daa7466 tailwind css 2025-09-03 16:28:35 +07:00
aeb1d56e85 [Update Kategori] 2025-09-03 10:17:26 +07:00
31a39e9642 fix sidebar 2025-09-02 13:47:29 +07:00
e6565fc33a [Cek Fitur] 2025-09-02 11:49:55 +07:00
bb5315fad5 add fitur view 2025-09-02 11:38:04 +07:00
43168c4273 [Fitur] 2025-09-02 11:23:32 +07:00
eae9f9fd28 fix kategori 2025-09-02 10:45:15 +07:00
21d61178ac add kategori & ulasan view 2025-09-01 16:11:43 +07:00
c5486575a3 [Kategori & Api Ulasan]
done
2025-09-01 14:29:01 +07:00
acc32b08ca [Add Kategori]
Belum View
2025-09-01 12:02:36 +07:00
f38f8a286f [Login Admin]
Done
2025-09-01 09:45:58 +07:00
6588ee8c46 [Admin] 2025-09-01 08:59:34 +07:00
56 changed files with 3206 additions and 194 deletions

View 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');
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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']));
}
}

View 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);
}
}

View 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!');
}
}

View 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!');
}
}

View 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!');
}
}

View 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'));
}
}

View 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'];
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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);
}
}

View 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',
];
}

View 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);
}
}

View 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);
}
}

View File

@ -2,94 +2,39 @@
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option controls the default authentication "guard" and password
| reset options for your application. You may change these defaults
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| here which uses session storage and the Eloquent user provider.
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| Supported: "session"
|
*/
'guards' => [
// Guard untuk user biasa
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// Guard untuk admin
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication drivers have a user provider. This defines how the
| users are actually retrieved out of your database or other storage
| mechanisms used by this application to persist your user's data.
|
| If you have multiple user tables or models you may configure multiple
| sources which represent each model / table. These sources may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
// Provider untuk user biasa
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
// Provider untuk admin
'admins' => [
'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' => [
'users' => [
'provider' => 'users',
@ -97,19 +42,14 @@ return [
'expire' => 60,
'throttle' => 60,
],
'admins' => [
'provider' => 'admins',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| times out and the user is prompted to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => 10800,
];

View 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 [
//
];
}
}

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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 15
$table->text('message');
$table->string('name');
$table->string('city');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('reviews');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

@ -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');
}
};

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View 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>

View 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

View 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

View 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

View 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

View 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

View 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

View 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>

View File

@ -2,18 +2,26 @@
use Illuminate\Http\Request;
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;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
// Form API (user)
Route::post('form/pernikahan', [PernikahanApiController::class, 'store']);
Route::post('form/ulang-tahun', [UlangTahunApiController::class, 'store']);
Route::post('form/khitan', [KhitanApiController::class, 'store']);
// API Kategori hanya read-only
Route::get('kategoris', [KategoriApiController::class, 'index']);
Route::get('kategoris/{kategori}', [KategoriApiController::class, 'show']);
// API Reviews
Route::apiResource('reviews', ReviewController::class);
// API Templates
Route::get('templates', [TemplateApiController::class, 'index']);
Route::get('templates/{template}', [TemplateApiController::class, 'show']);
Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});

View File

@ -1,18 +1,83 @@
<?php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AdminAuthController;
use App\Http\Controllers\KategoriController;
use App\Http\Controllers\FiturController;
use App\Http\Controllers\TemplateController;
use App\Http\Controllers\PelangganController;
/*
|--------------------------------------------------------------------------
| 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!
|
*/
//Login
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');
});

View File

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

View File

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

View File

@ -30,7 +30,7 @@
</div>
<div class="see-more">
<a href="#">Lihat Selengkapnya...</a>
<NuxtLink to="/template">Lihat Selengkapnya...</NuxtLink>
</div>
</section>
</template>

View File

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

View File

@ -4,7 +4,7 @@
<div class="w-full text-center lg:w-1/2">
<h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl ml-14">
<h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl pl-15">
Buat Undangan Digital Praktis Untuk
<div class="h-24 flex items-center justify-center">
@ -16,10 +16,10 @@
Tanpa Ribet
</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.
</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">
<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"/>
@ -27,9 +27,10 @@
</svg>
<span>Hubungi Kami</span>
</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
</a>
</div>
</div>

View File

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

View File

@ -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>

View File

@ -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>

View 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>

View File

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