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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@
<div class="w-full text-center lg:w-1/2"> <div class="w-full text-center lg:w-1/2">
<h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl ml-14"> <h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl pl-15">
Buat Undangan Digital Praktis Untuk Buat Undangan Digital Praktis Untuk
<div class="h-24 flex items-center justify-center"> <div class="h-24 flex items-center justify-center">
@ -16,10 +16,10 @@
Tanpa Ribet Tanpa Ribet
</h1> </h1>
<p class="mt-4 mb-8 text-lg text-gray-600 ml-14"> <p class="mt-4 mb-8 text-lg text-gray-600 pl-15">
Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu. Praktis, cepat, dan bisa langsung digunakan. Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu. Praktis, cepat, dan bisa langsung digunakan.
</p> </p>
<div class="flex flex-col justify-center gap-4 sm:flex-row ml-14"> <div class="flex flex-col justify-center gap-4 sm:flex-row pl-15">
<a href="#" class="inline-flex items-center justify-center rounded-lg border-2 border-green-500 bg-white px-8 py-3 font-bold text-gray-800 shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md"> <a href="#" class="inline-flex items-center justify-center rounded-lg border-2 border-green-500 bg-white px-8 py-3 font-bold text-gray-800 shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md">
<svg class="mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg class="mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.2239 4.7761C17.1659 2.7181 14.6599 1.5 11.9999 1.5...Z" fill="#25D366"/> <path d="M19.2239 4.7761C17.1659 2.7181 14.6599 1.5 11.9999 1.5...Z" fill="#25D366"/>
@ -27,9 +27,10 @@
</svg> </svg>
<span>Hubungi Kami</span> <span>Hubungi Kami</span>
</a> </a>
<a href="#" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-8 py-3 font-bold text-white shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-blue-700"> <a href="/template" class="inline-flex items-center justify-center rounded-lg bg-blue-600 px-8 py-3 font-bold text-white shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md hover:bg-blue-700">
Lihat Templat Lihat Templat
</a> </a>
</div> </div>
</div> </div>

View File

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

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(), tailwindcss(),
], ],
}, },
runtimeConfig: {
public: {
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL || 'http://localhost:8000'
}
}
}) })