Compare commits

...

29 Commits

Author SHA1 Message Date
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
71 changed files with 3390 additions and 239 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,48 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Pelanggan;
use App\Models\PelangganDetail;
use Illuminate\Http\Request;
class KhitanApiController extends Controller
{
public function store(Request $request)
{
$data = $request->validate([
'nama_pemesan' => 'required|string|max:255',
'no_hp' => 'required|string|max:20',
'email' => 'required|email',
'nama_anak' => 'required|string|max:255',
'nama_orangtua' => 'required|string|max:255',
'alamat' => 'required|string',
'tanggal_acara' => 'required|date',
'link_musik' => 'nullable|string',
'kata_pengucapan' => 'nullable|string',
'galeri' => 'nullable|string',
'template_id' => 'required|exists:templates,id',
]);
$pelanggan = Pelanggan::create([
'nama_pemesan' => $data['nama_pemesan'],
'nama_template' => 'Template Khitan',
'kategori' => 'khitan',
'email' => $data['email'],
'no_tlpn' => $data['no_hp'],
'harga' => 0,
]);
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,49 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Pelanggan;
use App\Models\PelangganDetail;
use Illuminate\Http\Request;
class PernikahanApiController extends Controller
{
public function store(Request $request)
{
$data = $request->validate([
'nama_pemesan' => 'required|string|max:255',
'no_hp' => 'required|string|max:20',
'email' => 'required|email',
'nama_pria' => 'required|string|max:255',
'nama_wanita' => 'required|string|max:255',
'alamat' => 'required|string',
'tanggal_acara' => 'required|date',
'link_undangan' => 'nullable|string',
'kata_pengucapan' => 'nullable|string',
'galeri' => 'nullable|string',
'link_musik' => 'nullable|string',
'template_id' => 'required|exists:templates,id',
]);
$pelanggan = Pelanggan::create([
'nama_pemesan' => $data['nama_pemesan'],
'nama_template' => 'Template Pernikahan',
'kategori' => 'pernikahan',
'email' => $data['email'],
'no_tlpn' => $data['no_hp'],
'harga' => 0,
]);
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,55 @@
<?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 view('admin.reviews.index', compact('reviews'));
}
// 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,51 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Pelanggan;
use App\Models\PelangganDetail;
use Illuminate\Http\Request;
class UlangTahunApiController extends Controller
{
public function store(Request $request)
{
$data = $request->validate([
'nama_pemesan' => 'required|string|max:255',
'no_hp' => 'required|string|max:20',
'email' => 'required|email',
'nama_panjang' => 'required|string|max:255',
'nama_panggilan' => 'required|string|max:255',
'ulang_tahun_ke' => 'required|integer',
'anak_ke' => 'required|integer',
'nama_orangtua' => 'required|string|max:255',
'alamat' => 'required|string',
'tanggal_acara' => 'required|date',
'link_musik' => 'nullable|string',
'kata_pengucapan' => 'nullable|string',
'galeri' => 'nullable|string',
'template_id' => 'required|exists:templates,id',
]);
$pelanggan = Pelanggan::create([
'nama_pemesan' => $data['nama_pemesan'],
'nama_template' => 'Template Ulang Tahun',
'kategori' => 'ulang_tahun',
'email' => $data['email'],
'no_tlpn' => $data['no_hp'],
'harga' => 0,
]);
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,19 @@
<?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',
'nama_panjang', 'nama_pendek', 'nama_orangtua',
'alamat', 'tanggal_acara', 'link_musik', 'galeri',
'kata_pengucapan'
];
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,18 @@
<?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',
'nama_pria', 'nama_wanita', 'alamat', 'tanggal_acara',
'link_undangan', 'kata_pengucapan', 'galeri', 'link_musik'
];
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,19 @@
<?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',
'nama_panjang', 'nama_panggilan', 'ulang_tahun_ke', 'anak_ke',
'nama_orangtua', 'alamat', 'tanggal_acara', 'link_musik',
'kata_pengucapan', 'galeri'
];
public function template() {
return $this->belongsTo(Template::class);
}
}

View File

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

View File

@ -0,0 +1,23 @@
<?php
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Template>
*/
class TemplateFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
//
];
}
}

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('admins', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('admins');
}
};

View File

@ -0,0 +1,22 @@
<?php
// database/migrations/2025_09_08_000001_create_kategoris_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('kategoris', function (Blueprint $table) {
$table->id();
$table->string('nama');
$table->text('deskripsi')->nullable();
$table->string('foto')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('kategoris');
}
};

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('reviews', function (Blueprint $table) {
$table->id();
$table->tinyInteger('rating'); // nilai bintang 15
$table->text('message');
$table->string('name');
$table->string('city');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('reviews');
}
};

View File

@ -0,0 +1,20 @@
<?php
// database/migrations/2025_09_08_000002_create_fiturs_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('fiturs', function (Blueprint $table) {
$table->id();
$table->text('deskripsi');
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('fiturs');
}
};

View File

@ -0,0 +1,25 @@
<?php
// database/migrations/2025_09_08_000003_create_templates_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('templates', function (Blueprint $table) {
$table->id();
$table->string('nama_template');
$table->foreignId('kategori_id')->constrained()->cascadeOnDelete();
$table->foreignId('fitur_id')->constrained()->cascadeOnDelete();
$table->decimal('harga', 10, 2)->default(0);
$table->string('foto')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('templates');
}
};

View File

@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('pelanggans', function (Blueprint $table) {
$table->id();
$table->string('nama_pemesan');
$table->string('nama_template');
$table->string('kategori');
$table->string('email');
$table->string('no_tlpn');
$table->decimal('harga', 15, 2)->nullable();
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('pelanggans');
}
};

View File

@ -0,0 +1,24 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('pelanggan_details', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('pelanggan_id');
$table->json('detail_form'); // data sesuai kategori
$table->timestamps();
$table->foreign('pelanggan_id')->references('id')->on('pelanggans')->onDelete('cascade');
});
}
public function down(): void
{
Schema::dropIfExists('pelanggan_details');
}
};

View File

@ -0,0 +1,33 @@
<?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');
$table->string('nama_pria');
$table->string('nama_wanita');
$table->string('alamat');
$table->date('tanggal_acara');
$table->string('link_undangan')->nullable();
$table->text('kata_pengucapan')->nullable();
$table->string('galeri')->nullable();
$table->string('link_musik')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('pernikahans');
}
};

View File

@ -0,0 +1,33 @@
<?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');
$table->string('nama_panjang');
$table->string('nama_panggilan');
$table->integer('ulang_tahun_ke');
$table->integer('anak_ke');
$table->string('nama_orangtua');
$table->string('alamat');
$table->date('tanggal_acara');
$table->string('link_musik')->nullable();
$table->text('kata_pengucapan')->nullable();
$table->string('galeri')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('ulang_tahuns');
}
};

View File

@ -0,0 +1,32 @@
<?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');
$table->string('nama_panjang');
$table->string('nama_pendek');
$table->string('nama_orangtua');
$table->string('alamat');
$table->date('tanggal_acara');
$table->string('link_musik')->nullable();
$table->string('galeri')->nullable();
$table->text('kata_pengucapan')->nullable();
$table->timestamps();
});
}
public function down(): void {
Schema::dropIfExists('khitans');
}
};

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

@ -0,0 +1,109 @@
<!doctype html>
<html lang="id">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Login Admin</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: #ffffff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.login-card {
background: #EFEFEF;
border-radius: 15px;
box-shadow: 0 0 25px rgb(0, 123, 255);
width: 100%;
max-width: 400px; /* batas lebar maksimum */
padding: 30px;
}
.login-logo {
display: block;
margin: 0 auto 15px;
max-width: 130px;
}
.btn-login {
background-color: #3bb9ff;
border: none;
border-radius: 8px;
}
.btn-login:hover {
background-color: #1a8edb;
}
.form-control {
border-radius: 10px;
}
/* Responsive padding */
@media (max-width: 576px) {
.login-card {
padding: 20px;
margin: 10px;
}
.login-logo {
max-width: 100px;
}
h4 {
font-size: 1.2rem;
}
p {
font-size: 0.9rem;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="text-center mb-3">
<!-- Logo -->
<img src="{{ asset('images/logo.png') }}" alt="Logo" class="login-logo">
</div>
<h4 class="text-center mb-2 fw-bold">SELAMAT DATANG</h4>
<p class="text-center text-muted mb-4">Silakan masukkan email dan password anda.</p>
@if ($errors->any())
<div class="alert alert-danger">
{{ $errors->first() }}
</div>
@endif
<form action="{{ route('admin.login.post') }}" method="POST">
@csrf
<div class="mb-3">
<label class="form-label">Nama</label>
<input type="email" name="email" value="{{ old('email') }}" class="form-control"
placeholder="Masukkan nama Anda" required autofocus>
</div>
<div class="mb-3">
<label class="form-label">Kata Sandi</label>
<input type="password" name="password" class="form-control"
placeholder="Masukkan kata sandi Anda" required>
</div>
<div class="d-flex justify-content-between align-items-center mb-3 flex-wrap">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember">
<label class="form-check-label" for="remember">Ingat saya</label>
</div>
<a href="#" class="text-decoration-none mt-2 mt-sm-0">Lupa kata sandi</a>
</div>
<button type="submit" class="btn btn-login text-white w-100">Login</button>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,54 @@
@extends('layouts.app')
@section('title', 'Halaman Dasbor')
@section('content')
<div class="container mx-auto py-4">
<!-- Header -->
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-bold">Halaman Dasbor</h3>
<div class="flex items-center gap-3">
<div class="bg-blue-100 text-blue-600 px-3 py-2 rounded-lg flex items-center gap-2 text-sm">
<i class="bi bi-clock-history"></i>
{{ \Carbon\Carbon::now()->translatedFormat('l, d F Y') }}
</div>
</div>
</div>
<!-- Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white rounded-lg shadow-md p-4 flex justify-between items-center hover:shadow-lg transition-transform duration-300 transform hover:-translate-y-2">
<div>
<h5 class="text-gray-500 text-sm">Kategori</h5>
<h3 class="font-bold text-xl">10</h3>
</div>
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center text-xl">
<i class="bi bi-diagram-3"></i>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-4 flex justify-between items-center hover:shadow-lg transition-transform duration-300 transform hover:-translate-y-2">
<div>
<h5 class="text-gray-500 text-sm">Templat</h5>
<h3 class="font-bold text-xl">20</h3>
</div>
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center text-xl">
<i class="bi bi-card-list"></i>
</div>
</div>
<div class="bg-white rounded-lg shadow-md p-4 flex justify-between items-center hover:shadow-lg transition-transform duration-300 transform hover:-translate-y-2">
<div>
<h5 class="text-gray-500 text-sm">Pelanggan</h5>
<h3 class="font-bold text-xl">24</h3>
</div>
<div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center text-xl">
<i class="bi bi-person"></i>
</div>
</div>
</div>
<!-- Pesan login -->
<div class="bg-green-100 text-green-700 p-4 rounded-lg mt-4">
Berhasil login sebagai <strong>{{ auth('admin')->user()->name }}</strong>
</div>
</div>
@endsection

View File

@ -0,0 +1,164 @@
@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>
<!-- 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-[10%] 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="p-2 border border-gray-300">{{ $key + 1 }}</td>
<td class="p-2 border border-gray-300 truncate whitespace-nowrap">{{ $item->deskripsi }}
</td>
<td class="p-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>
<form action="{{ route('admin.fitur.destroy', $item->id) }}" method="POST"
class="inline" onsubmit="return confirm('Yakin mau hapus fitur ini?')">
@csrf
@method('DELETE')
<button class="text-red-600 flex items-center">
<i class="bi bi-trash mr-1"></i> Hapus
</button>
</form>
</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
<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');
});
});
</script>
@endsection

View File

@ -0,0 +1,231 @@
@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>
<form action="{{ route('admin.kategori.destroy', $item->id) }}" method="POST"
class="inline" onsubmit="return confirm('Yakin mau hapus kategori 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="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
<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');
});
});
</script>
@endsection

View File

@ -0,0 +1,133 @@
@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">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">Aksi</th>
</tr>
</thead>
<tbody>
@forelse($pelanggans as $key => $pelanggan)
<tr>
<td class="p-2 border border-gray-300 text-center">{{ $key + 1 }}</td>
<td class="p-2 border border-gray-300 truncate">{{ $pelanggan->nama_pemesan }}</td>
<td class="p-2 border border-gray-300 truncate">{{ $pelanggan->email }}</td>
<td class="p-2 border border-gray-300 truncate">{{ $pelanggan->no_tlpn ?? '-' }}</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 border border-gray-300 text-center truncate">{{ $key + 1 }}</td>
<td class="p-2 border border-gray-300 truncate">{{ $review->name }}</td>
<td class="p-2 border border-gray-300 truncate">{{ $review->city }}</td>
<td class="p-2 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,279 @@
@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-right 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 }}">{{ $fitur->deskripsi }}</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 text-gray-700">
<i class="bi bi-people me-2"></i> Pelanggan
</a>
</li>
<li>
<a href="{{ route('admin.reviews.index') }}"
class="flex items-center py-2 px-3 rounded hover:bg-blue-50 {{ request()->is('admin/ulasan') ? 'bg-blue-100 text-blue-600' : 'text-gray-700' }}">
<i class="bi bi-chat-dots me-2"></i> Ulasan
</a>
</li>
</ul>
<p class="text-gray-500 uppercase text-xs font-semibold px-3 mt-4 mb-2">Akun</p>
<ul class="px-2 space-y-1">
<li>
<a href="{{ route('admin.logout') }}"
onclick="event.preventDefault(); document.getElementById('logout-form').submit();"
class="flex items-center py-2 px-3 text-red-600 font-semibold hover:bg-red-50 rounded">
<i class="bi bi-box-arrow-right me-2"></i> Keluar
</a>
<form id="logout-form" action="{{ route('admin.logout') }}" method="POST" class="hidden">
@csrf
</form>
</li>
</ul>
</div>
</div>
<!-- MAIN CONTENT -->
<div class="flex-1 ml-[250px] p-6">
@yield('content')
</div>
<script>
// Dropdown Templat
const templatBtn = document.getElementById('templatBtn');
const templatSubmenu = document.getElementById('templatSubmenu');
const templatIcon = document.getElementById('templatIcon');
templatBtn.addEventListener('click', () => {
templatSubmenu.classList.toggle('show');
templatIcon.classList.toggle('rotate-180');
});
</script>
</body>
</html>

View File

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

View File

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

View File

@ -1 +1,4 @@
@import "tailwindcss";
@import "tailwindcss";
html {
scroll-behavior: smooth;
}

View File

@ -0,0 +1,133 @@
<template>
<section id="tentang-kami" class="about-section">
<div class="container">
<!-- Judul -->
<h2 class="section-title">Tentang Kami</h2>
<!-- Layout gambar + teks -->
<div class="about-layout">
<div class="about-image">
<img src="/logo1.png" alt="Tentang Kami - Undangan Digital" />
</div>
<div class="about-text">
<p>
"Abbauf Tech memudahkan pembuatan undangan digital yang praktis dan elegan, siap digunakan untuk berbagai acara spesial Anda."
</p>
</div>
</div>
<!-- Info Box -->
<div class="about-info">
<div class="info-card" v-for="(item, index) in infoList" :key="index">
<div class="icon">
<component :is="item.icon" />
</div>
<h3>{{ item.title }}</h3>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { h } from "vue";
import { User, Headphones, Layout } from "lucide-vue-next";
// Data untuk kotak informasi
const infoList = [
{
title: "300+ Template",
icon: () => h(Layout, { size: 36, color: "#0d6efd" }),
},
{
title: "24/7 Support",
icon: () => h(Headphones, { size: 36, color: "#0d6efd" }),
},
{
title: "100+ Client",
icon: () => h(User, { size: 36, color: "#0d6efd" }),
},
];
</script>
<style scoped>
.about-section {
padding: 80px 0;
background-color: #ffffff;
text-align: center;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.section-title {
font-size: 2.9rem;
font-weight: 700;
margin-bottom: 50px;
color: #000;
}
.about-layout {
display: flex;
align-items: center;
justify-content: center;
gap: 40px;
flex-wrap: wrap;
margin-bottom: 50px;
}
.about-image img {
max-width: 400px;
height: auto;
margin-bottom: 20px;
margin-right: 20px;
}
.about-text {
max-width: 500px;
font-size: 1.4rem;
color: #000;
line-height: 1.6;
font-family: 'Roboto', sans-serif;
margin-bottom: 20px;
}
.about-info {
display: flex;
justify-content: center;
gap: 30px;
flex-wrap: wrap;
}
.info-card {
flex: 1;
min-width: 200px;
max-width: 250px;
border: 3px solid #0d6efd;
border-radius: 8px;
padding: 20px;
text-align: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.info-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.1);
}
.info-card h3 {
margin-top: 15px;
font-size: 1.1rem;
font-weight: 600;
color: #333;
}
.icon {
display: flex;
justify-content: center;
margin-bottom: 10px;
}
</style>

View File

@ -0,0 +1,175 @@
<template>
<section id="template" class="feature-section">
<div class="featured-header">
<h2>Templat Unggulan</h2>
<p>"Tersedia berbagai desain undangan pernikahan, khitan, ulang tahun, dan lainnya."</p>
</div>
<!-- Grid Template -->
<div class="template-grid">
<div class="template-card" v-for="i in 6" :key="i">
<div class="template-image">
<img src="/templat.jpg" alt="Template" />
</div>
<div class="template-body">
<h3 class="template-title">Golf Party</h3>
<p class="template-price">Rp.89.000</p>
<select class="template-select">
<option>Fitur Terbaik</option>
<option>Fitur Lengkap</option>
</select>
<div class="button-group">
<button class="btn btn-preview">Preview</button>
<button class="btn btn-order">Order</button>
</div>
</div>
</div>
</div>
<div class="see-more">
<NuxtLink to="/template">Lihat Selengkapnya...</NuxtLink>
</div>
</section>
</template>
<style scoped>
.featured-section {
padding: 60px 20px;
text-align: center;
}
.featured-header h2 {
font-size: 2.9rem;
font-weight: bold;
margin-bottom: 30px;
text-align: center;
}
.featured-header p {
color: #555;
margin-bottom: 40px;
font-size: 17px;
text-align: center;
}
.template-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 24px;
max-width: 1100px;
margin: 0 auto;
}
.template-card {
background: #f2f2f2;
border-radius: 5px;
padding: 16px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
display: flex;
flex-direction: column;
align-items: center;
min-height: 400px;
overflow: hidden;
}
.template-image {
background: #fff;
border-radius: 10px;
width: 100%;
height: 180px;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
}
.template-image img {
max-height: 100%;
object-fit: contain;
}
.template-body {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
margin-top: 12px;
gap: 10px;
}
.template-title {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.template-price {
color: #008000;
font-weight: bold;
margin: 0;
}
.template-select {
width: 100%;
padding: 8px;
border-radius: 6px;
border: 1px solid #ccc;
}
.button-group {
display: flex;
gap: 10px;
width: 100%;
}
.btn {
flex: 1;
padding: 10px 0;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
border: none;
transition: 0.2s;
}
.btn-preview {
background: #e5e5e5;
}
.btn-preview:hover {
background: #d6d6d6;
}
.btn-order {
background: #2563eb;
color: #fff;
}
.btn-order:hover {
background: #1d4ed8;
}
.see-more {
margin-top: 30px;
text-align: right;
max-width: 1100px;
margin-left: auto;
margin-right: auto;
}
.see-more a {
color: #2563eb;
font-weight: 500;
text-decoration: none;
}
.see-more a:hover {
text-decoration: underline;
}
</style>

View File

@ -0,0 +1,207 @@
<template>
<footer class="site-footer">
<div class="container footer-content">
<div class="footer-brand">
<img src="/ABBAUF.png" alt="Abbauf Tech Logo" class="footer-logo">
</div>
<div class="footer-column">
<h4>Layanan</h4>
<ul class="footer-links">
<li><a href="#">Template Undangan Pernikahan</a></li>
<li><a href="#">Template Undangan Khitan</a></li>
<li><a href="#">Template Undangan Ulang Tahun</a></li>
</ul>
</div>
<div class="footer-column">
<h4>Ikuti Kami</h4>
<ul class="footer-contact-list">
<li>
<a href="#" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M9 8h-3v4h3v12h5v-12h3.642l.358-4h-4v-1.667c0-.955.192-1.333 1.115-1.333h2.885v-5h-3.808c-3.596 0-5.192 1.583-5.192 4.615v2.385z"/></svg>
<span>ABBAUF TECH</span>
</a>
</li>
<li>
<a href="#" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.85s-.011 3.584-.069 4.85c-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07s-3.584-.012-4.85-.07c-3.252-.148-4.771-1.691-4.919-4.919-.058-1.265-.069-1.645-.069-4.85s.011-3.584.069-4.85c.149-3.225 1.664-4.771 4.919-4.919 1.266-.057 1.644-.069 4.85-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948s.014 3.667.072 4.947c.2 4.359 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072s3.667-.014 4.947-.072c4.359-.2 6.78-2.618 6.98-6.98.058-1.281.072-1.689.072-4.948s-.014-3.667-.072-4.947c-.2-4.359-2.618-6.78-6.98-6.98-1.281-.058-1.689-.072-4.948-.072zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.162 6.162 6.162 6.162-2.759 6.162-6.162-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4s1.791-4 4-4 4 1.79 4 4-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.441 1.441 1.441 1.441-.645 1.441-1.441-.645-1.44-1.441-1.44z"/></svg>
<span>@abbauf_tech</span>
</a>
</li>
<li>
<a href="#" class="social-link">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M4.98 3.5c0 1.381-1.11 2.5-2.48 2.5s-2.48-1.119-2.48-2.5c0-1.38 1.11-2.5 2.48-2.5s2.48 1.12 2.48 2.5zm.02 4.5h-5v16h5v-16zm7.982 0h-4.968v16h4.969v-8.399c0-4.67 6.029-5.052 6.029 0v8.399h4.988v-10.131c0-7.88-8.922-7.593-11.018-3.714v-2.155z"/></svg>
<span>ABBAUF TECH</span>
</a>
</li>
</ul>
</div>
<div class="footer-column footer-contact">
<h4>Kontak Kami</h4>
<ul class="footer-contact-list">
<li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M0 3v18h24v-18h-24zm21.518 2l-9.518 7.713-9.518-7.713h19.036zm-19.518 14v-11.817l10 8.104 10-8.104v11.817h-20z"/></svg>
<a href="mailto:contact@abbauf.com">contact@abbauf.com</a>
</li>
<li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20 22.621l-3.521-6.795c-.008.004-1.974.97-2.064 1.011-2.24 1.086-6.799-3.473-5.712-5.713.041-.09 1.011-2.064 1.011-2.064l-6.795-3.521-2.918 2.918c-1.603 1.603-1.425 4.933 1.011 7.37 4.301 4.301 9.962 3.593 12.301.954l2.688-2.355-2.356-2.688z"/></svg>
<a href="tel:02127617679">(021) 2761-7679</a>
</li>
<li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M.057 24l1.687-6.163c-1.041-1.804-1.588-3.849-1.587-5.946.003-6.556 5.338-11.891 11.893-11.891 3.181.001 6.167 1.24 8.413 3.488 2.245 2.248 3.481 5.236 3.48 8.414-.003 6.557-5.338 11.892-11.894 11.892-1.99-.001-3.951-.5-5.688-1.448l-6.305 1.654zm6.597-3.807c1.676.995 3.276 1.591 5.392 1.592 5.448 0 9.886-4.434 9.889-9.885.002-5.462-4.415-9.89-9.881-9.892-5.452 0-9.887 4.434-9.889 9.884-.001 2.225.651 3.891 1.746 5.634l-.999 3.648 3.742-.981zm11.387-5.464c-.074-.124-.272-.198-.57-.347-.297-.149-1.758-.868-2.031-.967-.272-.099-.47-.149-.669.149-.198.297-.768.967-.941 1.165-.173.198-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01s-.52.074-.792.372c-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.626.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414z"/></svg>
<a href="#">+62 878-7711-7847</a>
</li>
<li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z"/></svg>
<div>
<strong>Alamat Kantor Pusat</strong>
<p>APL Tower Central Park Lantai 19 Unit T7, Jalan Letjen S. Parman, Kavling 28, RT. 012/006, Kel. Tanjung Duren, Kec. Grogol Petamburan, Jakarta Barat, DKI Jakarta 11470, ID</p>
</div>
</li>
<li>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-4.198 0-8 3.403-8 7.602 0 4.198 3.469 9.21 8 16.398 4.531-7.188 8-12.2 8-16.398 0-4.199-3.801-7.602-8-7.602zm0 11c-1.657 0-3-1.343-3-3s1.343-3 3-3 3 1.343 3 3-1.343 3-3 3z"/></svg>
<div>
<strong>Alamat Studio</strong>
<p>Jl. Adhi Karya No. 57 RT 003 RW 015, Kel. Depok, Kec. Pancoran Mas, Depok 16431</p>
</div>
</li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2025 ABBAUF TECH - Hak cipta dilindungi undang-undang</p>
</div>
</footer>
</template>
<script setup>
// Tidak ada script yang dibutuhkan untuk footer statis ini
</script>
<style scoped>
.site-footer {
background-color: #f0f2f5; /* Warna abu-abu muda */
color: #333;
padding: 50px 0 20px 0;
font-family: 'Inter', sans-serif; /* Menggunakan font yang terlihat modern */
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.footer-content {
display: grid;
grid-template-columns: 2fr 1.5fr 1.5fr 2.5fr; /* Mengatur lebar kolom */
gap: 40px;
padding-bottom: 30px;
border-bottom: 1px solid #d9dce1;
}
.footer-logo {
max-width: 150px;
}
.footer-column h4 {
font-size: 1.2rem;
font-weight: 600;
margin-top: 0;
margin-bottom: 20px;
color: #111;
}
.footer-links, .footer-contact-list {
list-style: none;
padding: 0;
margin: 0;
}
.footer-links li {
margin-bottom: 12px;
}
.footer-links a, .social-link {
text-decoration: none;
color: #555;
transition: color 0.3s ease;
}
.footer-links a:hover, .social-link:hover {
color: #0d6efd; /* Biru Primer */
}
.footer-contact-list li {
display: flex;
align-items: flex-start;
margin-bottom: 18px;
gap: 15px;
color: #555;
}
.footer-contact-list svg {
flex-shrink: 0;
margin-top: 4px;
}
.footer-contact-list a {
text-decoration: none;
color: #555;
}
.footer-contact-list a:hover {
text-decoration: underline;
}
.footer-contact-list strong {
display: block;
color: #111;
font-weight: 600;
margin-bottom: 4px;
}
.footer-contact-list p {
margin: 0;
line-height: 1.6;
}
.social-link {
display: flex;
align-items: center;
gap: 10px;
}
.footer-bottom {
text-align: center;
padding-top: 20px;
font-size: 0.9rem;
color: #666;
}
/* Penyesuaian untuk layar kecil (Mobile) */
@media (max-width: 992px) {
.footer-content {
grid-template-columns: 1fr 1fr; /* 2 kolom di tablet */
}
}
@media (max-width: 768px) {
.footer-content {
grid-template-columns: 1fr; /* 1 kolom di mobile */
text-align: center;
}
.footer-brand {
margin-bottom: 20px;
}
.footer-logo {
margin: 0 auto; /* Logo di tengah */
}
.footer-contact-list li {
align-items: center;
text-align: left;
}
}
</style>

View File

@ -2,12 +2,13 @@
<header class="main-header">
<nav class="container">
<div class="logo">
<img src="/logo1.png" alt="Abbauf Tech Logo" class="logo-icon">
<span>ABBAUF TECH</span>
<NuxtLink to="/" class="logo-link"> <img src="/ABBAUF.png" alt="Abbauf Tech Logo" class="logo-icon">
<span>ABBAUF TECH</span>
</NuxtLink>
</div>
<ul class="nav-links">
<li v-for="link in navLinks" :key="link.name">
<a :href="link.path">{{ link.name }}</a>
<NuxtLink :to="link.path">{{ link.name }}</NuxtLink>
</li>
</ul>
</nav>
@ -18,18 +19,21 @@
import { ref } from 'vue';
const navLinks = ref([
{ name: 'Beranda', path: '#' },
{ name: 'Tentang Kami', path: '#' },
{ name: 'Templat', path: '#' },
{ name: 'Panduan', path: '#' },
{ name: 'Keistimewaan', path: '#' },
{ name: 'Testimoni', path: '#' },
// Path untuk Beranda diubah menjadi / agar selalu kembali ke halaman utama
{ name: 'Beranda', path: '/' },
// Path ini agar berfungsi dari halaman lain (seperti /template)
{ name: 'Tentang Kami', path: '/#tentang-kami' },
// INI BAGIAN YANG PALING PENTING: Path diubah ke /template
{ name: 'Templat', path: '/template' },
{ name: 'Panduan', path: '/#cara' },
{ name: 'Keistimewaan', path: '/#keistimewaan' },
{ name: 'Testimoni', path: '/#testi' },
]);
</script>
<style scoped>
.main-header {
background-color: #eaf2ff; /* Warna biru muda */
background-color: #eaf2ff;
padding: 15px 0;
border-bottom: 1px solid #d4e3ff;
width: 100%;
@ -44,23 +48,24 @@ const navLinks = ref([
align-items: center;
}
.logo {
.logo a.logo-link { /* Style untuk logo link */
display: flex;
align-items: center;
font-weight: bold;
font-size: 1.5rem;
color: #0d6efd; /* Biru Primer */
color: #0d6efd;
text-decoration: none; /* Hilangkan garis bawah default */
}
.logo-icon {
width: 40px; /* Sesuaikan ukuran logo */
width: 40px;
margin-right: 10px;
}
.nav-links {
list-style: none;
display: flex;
gap: 30px; /* Jarak antar menu */
gap: 30px;
margin: 0;
padding: 0;
}
@ -73,6 +78,6 @@ const navLinks = ref([
}
.nav-links a:hover {
color: #0d6efd; /* Warna biru saat di-hover */
color: #0d6efd;
}
</style>

View File

@ -1,30 +1,41 @@
<template>
<section class="hero-section">
<div class="container hero-layout">
<div class="hero-text">
<h1>
Buat Undangan Digital Praktis
<span class="highlight">Untuk Semua</span>
<section id="beranda" class="py-20 px-5">
<div class="container mx-auto flex flex-col items-center gap-12 lg:flex-row">
<div class="w-full text-center lg:w-1/2">
<h1 class="text-5xl font-extrabold leading-tight text-gray-800 lg:text-6xl">
Buat Undangan Digital Praktis Untuk
<div class="h-24 flex items-center justify-center">
<Transition name="fade" mode="out-in">
<span class="text-blue-600" :key="currentWord">{{ currentWord }}</span>
</Transition>
</div>
Tanpa Ribet
</h1>
<p>
<p class="mt-4 mb-8 text-lg text-gray-600">
Coba undangan digital PRAKTIS untuk berbagai acara. Pilih template praktis atau premium sesuai kebutuhanmu. Praktis, cepat, dan bisa langsung digunakan.
</p>
<div class="cta-buttons">
<a href="#" class="btn btn-whatsapp">
<svg 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.5C5.9249 1.5 0.9999 6.425 0.9999 12.5C0.9999 14.717 1.6219 16.814 2.7669 18.57L1.4179 22.582L5.5949 21.274C7.2729 22.3 9.2279 22.9 11.2999 22.9H11.9999C18.0749 22.9 22.9999 17.975 22.9999 11.9C22.9999 9.24 21.7819 6.734 19.7239 4.676L19.2239 4.7761Z" fill="#25D366"/>
<path d="M16.7441 14.968C16.5331 15.538 15.4211 16.039 14.8811 16.191C14.3411 16.342 13.6261 16.355 13.0611 16.151C12.4961 15.947 11.7581 15.688 10.1581 14.18C8.2541 12.378 7.0981 10.217 6.8481 9.689C6.5991 9.16 5.8611 7.78 5.8611 6.94C5.8611 6.103 6.3051 5.689 6.5541 5.452C6.8031 5.215 7.2021 5.122 7.5121 5.122C7.6561 5.122 7.7951 5.132 7.9251 5.143C8.2851 5.179 8.4711 5.196 8.6511 5.568C8.8611 6.004 9.4211 7.42 9.4911 7.572C9.5611 7.724 9.6111 7.928 9.5111 8.132C9.4111 8.336 9.3521 8.428 9.2111 8.58C9.0711 8.732 8.9411 8.855 8.7911 9.006C8.6711 9.129 8.5511 9.253 8.6811 9.48C8.8111 9.707 9.3011 10.512 10.1211 11.332C11.1611 12.372 11.9611 12.75 12.3011 12.911C12.6411 13.072 12.9861 13.03 13.2361 12.78C13.5611 12.455 13.8861 12.01 14.3061 11.59C14.5411 11.355 14.8361 11.29 15.1361 11.39C15.4361 11.49 16.6361 12.09 16.9961 12.26C17.3561 12.43 17.5761 12.53 17.6561 12.65C17.7361 12.77 17.7411 13.06 17.7111 13.16L17.7161 14.008C17.7161 14.008 17.7111 14.003 17.6811 14.038C17.5911 14.128 17.3011 14.24 16.9511 14.39L16.7441 14.968Z" fill="white"/>
<div class="flex flex-col justify-center gap-4 sm:flex-row">
<a href="#" class="inline-flex items-center justify-center rounded-lg border-2 border-green-500 bg-white px-8 py-3 font-bold text-gray-800 shadow-sm transition-transform duration-300 hover:-translate-y-1 hover:shadow-md">
<svg class="mr-2" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.2239 4.7761C17.1659 2.7181 14.6599 1.5 11.9999 1.5...Z" fill="#25D366"/>
<path d="M16.7441 14.968C16.5331 15.538...Z" fill="white"/>
</svg>
<span>Hubungi Kami</span>
</a>
<a href="#" class="btn btn-primary">Lihat Templat</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">
Lihat Templat
</a>
</div>
</div>
<div class="hero-image-wrapper">
<img src="/logo2.png" alt="Tampilan Undangan Digital di Ponsel">
<div class="testimonial">
<div class="w-full lg:w-1/2">
<img src="/logo2.png" alt="Tampilan Undangan Digital di Ponsel" class="mx-auto max-w-full">
<div class="mt-6 text-center italic text-gray-500">
<p>"Abadikan momen indahmu dengan undangan digital yang elegan."</p>
<p>"Satu aplikasi untuk semua momen spesialmu."</p>
</div>
@ -34,97 +45,35 @@
</template>
<script setup>
// Script bisa dikosongkan jika tidak ada logika dinamis
import { ref, onMounted, onUnmounted } from 'vue';
// Bagian script ini tidak perlu diubah
const words = ['Semua', 'Pernikahan', 'Khitan', 'Ulang Tahun'];
const currentWordIndex = ref(0);
const currentWord = ref(words[0]);
let intervalId = null;
onMounted(() => {
intervalId = setInterval(() => {
currentWordIndex.value = (currentWordIndex.value + 1) % words.length;
currentWord.value = words[currentWordIndex.value];
}, 1000);
});
onUnmounted(() => {
clearInterval(intervalId);
});
</script>
<style scoped>
.hero-section {
padding: 60px 0;
/* Bagian style ini tidak perlu diubah */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.hero-layout {
display: flex;
align-items: center;
gap: 50px;
}
.hero-text {
flex: 1; /* Mengambil ruang yang tersedia */
}
.hero-text h1 {
font-size: 3.5rem; /* Ukuran font besar */
font-weight: 800;
line-height: 1.2;
margin-bottom: 20px;
}
.hero-text h1 .highlight {
color: #0d6efd; /* Warna biru untuk "Untuk Semua" */
}
.hero-text p {
font-size: 1.1rem;
color: #555;
margin-bottom: 30px;
}
.cta-buttons {
display: flex;
gap: 15px;
}
.btn {
padding: 15px 30px;
border-radius: 8px;
text-decoration: none;
font-weight: bold;
font-size: 1rem;
display: inline-flex;
align-items: center;
justify-content: center;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn:hover {
transform: translateY(-3px);
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.btn-whatsapp {
background-color: #fff;
border: 2px solid #25D366; /* Warna hijau WhatsApp */
color: #222;
}
.btn-whatsapp svg {
margin-right: 8px;
}
.btn-primary {
background-color: #0d6efd; /* Warna biru */
color: #fff;
border: 2px solid #0d6efd;
}
.hero-image-wrapper {
flex: 1;
text-align: center;
}
.hero-image-wrapper img {
max-width: 100%;
height: auto;
}
.testimonial {
margin-top: 20px;
font-style: italic;
color: #444;
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,112 @@
<template>
<section id="cara" class="howto-section">
<div class="howto-header">
<h2>Cara Membuat Undangan Digital</h2>
<p>"Buat undanganmu dengan simple, mudah dan praktis."</p>
</div>
<div class="howto-grid">
<div class="howto-card" v-for="(item, index) in steps" :key="index">
<div class="card-icon">
<img :src="`/event-list${index}.png`" :alt="item.title" />
</div>
<h3>{{ item.title }}</h3>
<p>{{ item.desc }}</p>
</div>
</div>
</section>
</template>
<script setup>
const steps = [
{
title: "Mulai dari Kategori",
desc: "Pilih Kategory sesuai kebutuhan."
},
{
title: "Tentukan Template",
desc: "Sesuaikan desain sesuai selera."
},
{
title: "Isi Data",
desc: "Nama, tanggal, lokasi, dan detail acara, dll."
},
{
title: "Bagikan Undangan",
desc: "Lewat WhatsApp, link, atau media sosial."
},
{
title: "Lihat Hasil",
desc: "Pratinjau undangan sebelum simpan."
},
{
title: "Tambahkan Media",
desc: "Foto, musik, dan peta lokasi."
}
];
</script>
<style scoped>
.howto-section {
padding: 60px 20px;
text-align: center;
}
.howto-header h2 {
font-size: 2rem;
font-weight: bold;
margin-bottom: 10px;
}
.howto-header p {
color: #555;
font-size: 15px;
margin-bottom: 40px;
}
.howto-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
max-width: 1000px;
margin: 0 auto;
}
.howto-card {
background: #f8f9fc;
border-radius: 16px;
padding: 20px;
box-shadow: 0 4px 10px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.2s ease;
}
.howto-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
}
.card-icon {
margin-bottom: 12px;
display: flex;
justify-content: center;
align-items: center;
}
.card-icon img {
width: 60px;
height: 60px;
object-fit: contain;
}
.howto-card h3 {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
}
.howto-card p {
font-size: 14px;
color: #555;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<section class="bg-white py-20 px-5 text-center">
<div class="container mx-auto">
<h2 class="text-4xl font-extrabold text-gray-800 mb-2">
Keistimewaan Kami
</h2>
<p class="text-lg text-gray-600 mb-16">
"Membuat undangan digitalmu terlihat lebih hidup dan istimewa."
</p>
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-4">
<div class="flex flex-col items-center rounded-xl border border-gray-200 bg-white p-8 text-center shadow-md transition-transform duration-300 hover:-translate-y-2 hover:shadow-xl">
<div class="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600"><path d="M3 18v-6a9 9 0 0 1 18 0v6"></path><path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"></path></svg>
</div>
<h3 class="mb-4 text-xl font-bold text-gray-800">
Layanan Customer Care Setiap Hari
</h3>
<p class="text-base text-gray-600">
Tim admin siap membantu dari jam 07:00 hingga 20:00 WIB tanpa hari libur.
</p>
</div>
<div class="flex flex-col items-center rounded-xl border border-gray-200 bg-white p-8 text-center shadow-md transition-transform duration-300 hover:-translate-y-2 hover:shadow-xl">
<div class="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-2.6-2.6a1 1 0 0 0-1.4 0l-1.6 1.6a1 1 0 0 0 0 1.4l3.77 3.77a6 6 0 0 1-7.94 7.94l-2.6-2.6a1 1 0 0 0-1.4 0L.3 21.7a1 1 0 0 0 0 1.4l.7.7a1 1 0 0 0 1.4 0l2.6-2.6a6 6 0 0 1 7.94-7.94l3.77-3.77z"></path><line x1="12" y1="12" x2="16" y2="16"></line></svg>
</div>
<h3 class="mb-4 text-xl font-bold text-gray-800">
Desain Personal Sesuai Gaya Anda
</h3>
<p class="text-base text-gray-600">
Pilih dari ratusan tema menarik dan ubah warna, font, serta tata letaknya dengan mudah.
</p>
</div>
<div class="flex flex-col items-center rounded-xl border border-gray-200 bg-white p-8 text-center shadow-md transition-transform duration-300 hover:-translate-y-2 hover:shadow-xl">
<div class="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M23 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>
</div>
<h3 class="mb-4 text-xl font-bold text-gray-800">
Undangan Fleksibel
</h3>
<p class="text-base text-gray-600">
Nikmati kemudahan membagikan undangan dengan jumlah tamu sesuai kebutuhan Anda.
</p>
</div>
<div class="flex flex-col items-center rounded-xl border border-gray-200 bg-white p-8 text-center shadow-md transition-transform duration-300 hover:-translate-y-2 hover:shadow-xl">
<div class="mb-6 flex h-16 w-16 items-center justify-center rounded-full bg-blue-100">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="text-blue-600"><circle cx="12" cy="12" r="10"></circle><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"></polygon></svg>
</div>
<h3 class="mb-4 text-xl font-bold text-gray-800">
Tema Eksklusif & Bisa Dikustomisasi
</h3>
<p class="text-base text-gray-600">
Pilih ratusan desain menarik, ubah warna, font, hingga tata letak. Buat undangan yang benar-benar mencerminkan gayamu.
</p>
</div>
</div>
</div>
</section>
</template>
<script setup>
// Tidak ada script yang dibutuhkan untuk bagian ini
</script>
<style scoped>
/* Kosong! Semua styling sudah diatur oleh Tailwind di dalam template. */
</style>

View File

@ -0,0 +1,69 @@
<template>
<section id="testimoni" class="bg-slate-50 py-20 px-5">
<div class="container mx-auto text-center">
<h2 class="text-4xl font-extrabold text-gray-800 mb-2">
Apa Kata Mereka?
</h2>
<p class="text-lg text-gray-600 mb-16">
Kisah sukses dari para pengguna yang telah mempercayakan momen spesialnya kepada kami.
</p>
<div class="grid grid-cols-1 gap-8 md:grid-cols-2 lg:grid-cols-3">
<div
v-for="testimonial in testimonials"
:key="testimonial.id"
class="flex flex-col rounded-xl bg-white p-8 text-left shadow-lg transition-transform duration-300 hover:-translate-y-2 hover:shadow-2xl"
>
<div class="mb-4 flex items-center">
<svg v-for="n in 5" :key="n" class="h-5 w-5" :class="n <= testimonial.rating ? 'text-yellow-400' : 'text-gray-300'" fill="currentColor" viewBox="0 0 20 20"><path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z"></path></svg>
</div>
<p class="mb-6 flex-grow text-gray-600 italic">"{{ testimonial.text }}"</p>
<div class="flex items-center">
<img class="h-12 w-12 rounded-full object-cover" :src="testimonial.avatar" :alt="testimonial.name">
<div class="ml-4">
<h4 class="font-bold text-gray-800">{{ testimonial.name }}</h4>
<p class="text-sm text-gray-500">{{ testimonial.role }}</p>
</div>
</div>
</div>
</div>
</div>
</section>
</template>
<script setup>
import { ref } from 'vue';
const testimonials = ref([
{
id: 1,
name: 'Rizky & Anisa',
role: 'Pengantin Baru',
avatar: 'https://i.pravatar.cc/100?u=rizky',
rating: 5,
text: 'Desainnya elegan dan modern! Proses pembuatannya juga cepat banget. Semua tamu memuji undangannya. Terima kasih Abbauf Tech!'
},
{
id: 2,
name: 'Budi Santoso',
role: 'Event Organizer',
avatar: 'https://i.pravatar.cc/100?u=budi',
rating: 5,
text: 'Sebagai EO, kami butuh platform yang efisien dan hasilnya premium. Abbauf Tech menjawab semua kebutuhan itu. Klien kami sangat puas.'
},
{
id: 3,
name: 'Citra Lestari',
role: 'Ulang Tahun Anak',
avatar: 'https://i.pravatar.cc/100?u=citra',
rating: 4,
text: 'Fitur RSVP dan pengingat sangat membantu. Tema-tema ulang tahunnya juga lucu dan bisa dikustomisasi. Sangat direkomendasikan!'
},
]);
</script>
<style scoped>
/* Kosong, semua diatur oleh Tailwind */
</style>

View File

@ -0,0 +1,58 @@
<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 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.nama)"
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']);
const categories = ref([])
// Ambil data kategori dari Laravel API pakai $fetch
const fetchCategories = async () => {
try {
categories.value = await $fetch('http://localhost:8000/api/kategoris')
} catch (error) {
console.error('Gagal ambil kategori:', error)
}
}
const onCategoryClick = (categoryName) => {
emit('category-selected', categoryName);
};
onMounted(() => {
fetchCategories()
})
</script>

View File

@ -0,0 +1,49 @@
<template>
<div>
<div class="flex items-center mb-8">
<button @click="$emit('back')" class="text-blue-600 hover:text-blue-800 font-semibold mr-4">
&larr; Kembali ke Kategori
</button>
<h1 class="text-3xl md:text-4xl font-bold text-gray-800">
Pilih Template {{ category }} Favoritmu
</h1>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<div v-for="tpl in templates" :key="tpl.id" class="border rounded-lg p-4 text-center">
<img :src="tpl.foto" :alt="tpl.nama" class="bg-gray-200 h-40 mb-4 rounded-md object-cover w-full" />
<h4 class="font-semibold">{{ tpl.nama }}</h4>
<p class="text-gray-500">Rp {{ tpl.harga }}</p>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
category: {
type: String,
required: true,
},
id_category: {
type: Number,
required: true
}
})
const templates = ref([])
const fetchTemplates = async () => {
try {
templates.value = await $fetch(`http://localhost:8000/api/templates/category/${props.id_category}`)
} catch (error) {
console.error('Gagal ambil template:', error)
}
}
onMounted(() => {
fetchTemplates()
})
defineEmits(['back']);
</script>

View File

@ -2,10 +2,39 @@
<div>
<LandingPageHeader />
<LandingPageHero />
<section id="tentang-kami">
<AboutSection />
</section>
<section id="templat">
<FeaturedTemplates />
</section>
<section id="panduan">
<HowToSection />
</section>
<section id="keistimewaan">
<PrivilegeSection />
</section>
<section id="testimoni">
<TestimoniSection />
</section>
<Footer />
</div>
</template>
<script setup>
// Bagian script Anda sudah benar, tidak perlu diubah.
import LandingPageHeader from '~/components/landing-page/header.vue';
import LandingPageHero from '~/components/landing-page/hero.vue';
import AboutSection from '~/components/landing-page/aboutsection.vue';
import FeaturedTemplates from '~/components/landing-page/featuredtemplates.vue';
import HowToSection from '~/components/landing-page/howtosection.vue';
import PrivilegeSection from '~/components/landing-page/privilegesection.vue';
import Footer from '~/components/landing-page/footer.vue';
import TestimoniSection from '~/components/landing-page/testimonisection.vue';
</script>

View File

@ -0,0 +1,31 @@
<template>
<div class="container mx-auto px-4 py-16">
<TemplatePageCategorySelection
v-if="!selectedCategory"
@category-selected="handleCategorySelect"
/>
<TemplatePageTemplateGrid
v-else
:category="selectedCategory"
@back="goBack"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
// State untuk menyimpan kategori yang sedang dipilih
const selectedCategory = ref(null);
// Fungsi ini akan menangkap event dari CategorySelection
const handleCategorySelect = (categoryName) => {
selectedCategory.value = categoryName;
};
// Fungsi ini akan menangkap event 'back' dari TemplateGrid
const goBack = () => {
selectedCategory.value = null;
};
</script>

View File

@ -13,6 +13,7 @@
"@nuxt/image": "^1.11.0",
"@tailwindcss/vite": "^4.1.12",
"eslint": "^9.34.0",
"lucide-vue-next": "^0.542.0",
"nuxt": "^4.0.3",
"tailwindcss": "^4.1.12",
"vue": "^3.5.20",
@ -10426,6 +10427,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-vue-next": {
"version": "0.542.0",
"resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.542.0.tgz",
"integrity": "sha512-cJfyhFoneDgYTouHwUJEutXaCW5EQuRrBsvfELudWnMiwfqvcEtpZTFZLdZ5Nrqow+znzn+Iyhu3KeYIfa3mEg==",
"license": "ISC",
"peerDependencies": {
"vue": ">=3.0.1"
}
},
"node_modules/luxon": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz",

View File

@ -16,6 +16,7 @@
"@nuxt/image": "^1.11.0",
"@tailwindcss/vite": "^4.1.12",
"eslint": "^9.34.0",
"lucide-vue-next": "^0.542.0",
"nuxt": "^4.0.3",
"tailwindcss": "^4.1.12",
"vue": "^3.5.20",

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 MiB

After

Width:  |  Height:  |  Size: 3.0 MiB

View File

Before

Width:  |  Height:  |  Size: 456 KiB

After

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB