Initial Rapih
parent
a9edde2fbd
commit
f00612339c
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Http\Requests\RoleRequest;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class RoleController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$roles = Role::with('permissions')
|
||||||
|
->latest()
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
// Kembalikan data ke komponen Inertia 'Admin/Roles/Index'
|
||||||
|
return inertia('Admin/Roles/Index', compact('roles'));
|
||||||
|
}
|
||||||
|
}
|
|
@ -33,6 +33,9 @@ class AuthenticatedSessionController extends Controller
|
||||||
|
|
||||||
$request->session()->regenerate();
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
// Redirect berdasarkan role user
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
return redirect()->intended(route('dashboard', absolute: false));
|
return redirect()->intended(route('dashboard', absolute: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\KategoriRequest;
|
||||||
|
use App\Models\Kategori;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class KategoriController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$kategori = Kategori::latest()->get();
|
||||||
|
return Inertia::render('admin/kategori/index_kategori', ['kategori' => $kategori]);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error fetching Kategori: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(KategoriRequest $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$kategori = Kategori::withTrashed()
|
||||||
|
->where('NamaKategori', $request->NamaKategori)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($kategori) {
|
||||||
|
$kategori->restore();
|
||||||
|
return redirect()->route('admin.kategori.index')->with('success', 'Kategori berhasil dikembalikan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Kategori::create($request->validated());
|
||||||
|
|
||||||
|
return redirect()->route('admin.kategori.index')->with('success', 'Kategori berhasil dibuat.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error creating Kategori: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public function update(KategoriRequest $request, Kategori $kategori)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$kategori->update($request->validated());
|
||||||
|
return redirect()->route('admin.kategori.index')->with('success', 'Kategori berhasil diperbarui.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error updating Kategori: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Kategori $kategori)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$kategori->delete();
|
||||||
|
return redirect()->route('admin.kategori.index')->with('success', 'Kategori berhasil dihapus.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error deleting Kategori: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\PostRequest;
|
||||||
|
use App\Models\Kategori;
|
||||||
|
use App\Models\Post;
|
||||||
|
use App\Models\SubKategori;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class PostController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$posts = Post::with(['kategori', 'subkategori'])->latest()->get();
|
||||||
|
$kategori = Kategori::all();
|
||||||
|
$subkategori = SubKategori::all();
|
||||||
|
|
||||||
|
return Inertia::render('admin/post/index_post', [
|
||||||
|
'posts' => $posts,
|
||||||
|
'kategori' => $kategori,
|
||||||
|
'subkategori' => $subkategori
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
$kategori = Kategori::all();
|
||||||
|
$subkategori = SubKategori::all();
|
||||||
|
|
||||||
|
return Inertia::render('admin/post/add_post', [
|
||||||
|
'kategori' => $kategori,
|
||||||
|
'subkategori' => $subkategori
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(PostRequest $request)
|
||||||
|
{
|
||||||
|
|
||||||
|
try {
|
||||||
|
$data = $request->validated();
|
||||||
|
$data['IsPublish'] = $request->has('IsPublish');
|
||||||
|
|
||||||
|
if ($request->hasFile('ImagePost')) {
|
||||||
|
$data['ImagePost'] = $request->file('ImagePost')->store('images/posts');
|
||||||
|
}
|
||||||
|
|
||||||
|
Post::create($data);
|
||||||
|
|
||||||
|
return redirect()->route('admin.post.index')->with('success', 'Post berhasil ditambahkan');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return redirect()->route('admin.post.index')->with('error', 'Post gagal ditambahkan');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Post $post)
|
||||||
|
{
|
||||||
|
|
||||||
|
return Inertia::render('admin/post/edit_post', [
|
||||||
|
'post' => $post,
|
||||||
|
'kategori' => Kategori::all(),
|
||||||
|
'subkategori' => SubKategori::where('KategoriId', $post->KategoriId)->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(PostRequest $request, Post $post)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$data = $request->validated();
|
||||||
|
|
||||||
|
if ($request->hasFile('ImagePost')) {
|
||||||
|
Storage::disk('public')->delete($post->ImagePost);
|
||||||
|
$data['ImagePost'] = $request->file('ImagePost')->store('images/posts', 'public');
|
||||||
|
}
|
||||||
|
|
||||||
|
$post->update($data);
|
||||||
|
|
||||||
|
return redirect()->route('admin.post.index')->with('success', 'Post berhasil diperbarui.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error updating Post: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Post $post)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
Storage::disk('public')->delete($post->ImagePost);
|
||||||
|
$post->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.post.index')->with('success', 'Post berhasil dihapus.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error deleting Post: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\SubKategoriRequest;
|
||||||
|
use App\Models\Kategori;
|
||||||
|
use App\Models\SubKategori;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Inertia\Inertia;
|
||||||
|
|
||||||
|
class SubKategoriController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$subkategori = SubKategori::with('kategori')->latest()->get();
|
||||||
|
$kategori = Kategori::all();
|
||||||
|
|
||||||
|
return Inertia::render('admin/subkategori/index_subkategori', [
|
||||||
|
'subkategori' => $subkategori,
|
||||||
|
'kategori' => $kategori
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(SubKategoriRequest $request)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
SubKategori::create($request->validated());
|
||||||
|
return redirect()->route('admin.subkategori.index')->with('success', 'SubKategori berhasil dibuat.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error creating SubKategori: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(SubKategoriRequest $request, SubKategori $subkategori)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$subkategori->update($request->validated());
|
||||||
|
return redirect()->route('admin.subkategori.index')->with('success', 'SubKategori berhasil diperbarui.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error updating SubKategori: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(SubKategori $subkategori)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$subkategori->delete();
|
||||||
|
return redirect()->route('admin.subkategori.index')->with('success', 'SubKategori berhasil dihapus.');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('Error deleting SubKategori: ' . $e->getMessage());
|
||||||
|
return back()->with('error', 'Something went wrong.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,7 +27,7 @@ class LoginRequest extends FormRequest
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'email' => ['required', 'string', 'email'],
|
'login' => ['required', 'string'],
|
||||||
'password' => ['required', 'string'],
|
'password' => ['required', 'string'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -41,11 +41,13 @@ class LoginRequest extends FormRequest
|
||||||
{
|
{
|
||||||
$this->ensureIsNotRateLimited();
|
$this->ensureIsNotRateLimited();
|
||||||
|
|
||||||
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
|
$loginField = filter_var($this->input('login'), FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
|
||||||
|
|
||||||
|
if (! Auth::attempt([$loginField => $this->input('login'), 'password' => $this->input('password')], $this->boolean('remember'))) {
|
||||||
RateLimiter::hit($this->throttleKey());
|
RateLimiter::hit($this->throttleKey());
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'email' => trans('auth.failed'),
|
'login' => trans('auth.failed'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +70,7 @@ class LoginRequest extends FormRequest
|
||||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||||
|
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'email' => trans('auth.throttle', [
|
'login' => trans('auth.throttle', [
|
||||||
'seconds' => $seconds,
|
'seconds' => $seconds,
|
||||||
'minutes' => ceil($seconds / 60),
|
'minutes' => ceil($seconds / 60),
|
||||||
]),
|
]),
|
||||||
|
@ -80,6 +82,6 @@ class LoginRequest extends FormRequest
|
||||||
*/
|
*/
|
||||||
public function throttleKey(): string
|
public function throttleKey(): string
|
||||||
{
|
{
|
||||||
return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
|
return Str::transliterate(Str::lower($this->string('login')).'|'.$this->ip());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class KategoriRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'NamaKategori' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('Kategori', 'NamaKategori')->whereNull('deleted_at') // Abaikan data yang dihapus (soft delete)
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class PostRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'KategoriId' => 'required|exists:Kategori,KategoriId',
|
||||||
|
'SubKategoriId' => [
|
||||||
|
'required',
|
||||||
|
Rule::exists('SubKategori', 'SubKategoriId')->where('KategoriId', $this->KategoriId),
|
||||||
|
],
|
||||||
|
'JudulPost' => ['required', 'string', 'max:255'],
|
||||||
|
'DescPost' => ['required', 'string'],
|
||||||
|
'ImagePost' => ['nullable', 'image', 'mimes:jpg,png,webp', 'max:2048'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class RoleRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'permissions' => 'required|array',
|
||||||
|
'permissions.*' => 'exists:permissions,id'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class SubKategoriRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'KategoriId' => ['required', 'integer', 'exists:Kategori,KategoriId'],
|
||||||
|
'NamaSubKategori' => ['required', 'string', 'max:255'],
|
||||||
|
Rule::unique('SubKategori', 'NamaSubKategori')->where('KategoriId', $this->KategoriId)->whereNull('deleted_at')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class Kategori extends Model
|
||||||
|
{
|
||||||
|
use HasFactory; use SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'Kategori';
|
||||||
|
|
||||||
|
protected $primaryKey = 'KategoriId';
|
||||||
|
|
||||||
|
protected $fillable = ['NamaKategori'];
|
||||||
|
|
||||||
|
protected $dates = ['deleted_at'];
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class Post extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'Post';
|
||||||
|
protected $primaryKey = 'PostId';
|
||||||
|
protected $fillable = [
|
||||||
|
'KategoriId',
|
||||||
|
'SubKategoriId',
|
||||||
|
'JudulPost',
|
||||||
|
'SlugPost',
|
||||||
|
'DescPost',
|
||||||
|
'ImagePost',
|
||||||
|
'IsPublish',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function kategori()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Kategori::class, 'KategoriId', 'KategoriId');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function subkategori()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(SubKategori::class, 'SubKategoriId', 'SubKategoriId');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFormattedPublishDateAttribute()
|
||||||
|
{
|
||||||
|
return \Carbon\Carbon::parse($this->created_at)->translatedFormat('d F Y');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function boot()
|
||||||
|
{
|
||||||
|
parent::boot();
|
||||||
|
|
||||||
|
static::creating(function ($post) {
|
||||||
|
$post->Slug = Str::slug($post->JudulPost);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class SubKategori extends Model
|
||||||
|
{
|
||||||
|
use HasFactory; use SoftDeletes;
|
||||||
|
|
||||||
|
protected $table = 'SubKategori';
|
||||||
|
|
||||||
|
protected $primaryKey = 'SubKategoriId';
|
||||||
|
|
||||||
|
protected $fillable = ['KategoriId', 'NamaSubKategori'];
|
||||||
|
|
||||||
|
public function kategori()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Kategori::class, 'KategoriId', 'KategoriId');
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,10 +6,11 @@ namespace App\Models;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
|
||||||
class User extends Authenticatable /*implements MustVerifyEmail*/
|
class User extends Authenticatable /*implements MustVerifyEmail*/
|
||||||
{
|
{
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable, HasRoles;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The attributes that are mass assignable.
|
* The attributes that are mass assignable.
|
||||||
|
@ -18,6 +19,7 @@ class User extends Authenticatable /*implements MustVerifyEmail*/
|
||||||
*/
|
*/
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name',
|
'name',
|
||||||
|
'username',
|
||||||
'email',
|
'email',
|
||||||
'password',
|
'password',
|
||||||
];
|
];
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
@ -11,7 +12,7 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
|
||||||
|
class RepositoryServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\HandleInertiaRequests;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
use Illuminate\Foundation\Configuration\Middleware;
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
@ -14,9 +15,14 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
$middleware->web(append: [
|
$middleware->web(append: [
|
||||||
\App\Http\Middleware\HandleInertiaRequests::class,
|
\App\Http\Middleware\HandleInertiaRequests::class,
|
||||||
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
\Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class,
|
||||||
|
HandleInertiaRequests::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
//
|
$middleware->alias([
|
||||||
|
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
|
||||||
|
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
|
||||||
|
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
|
||||||
|
]);
|
||||||
})
|
})
|
||||||
->withExceptions(function (Exceptions $exceptions) {
|
->withExceptions(function (Exceptions $exceptions) {
|
||||||
//
|
//
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
|
|
||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\RepositoryServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
"laravel/framework": "^11.0",
|
"laravel/framework": "^11.0",
|
||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9",
|
||||||
|
"spatie/laravel-permission": "6.4.0",
|
||||||
"tightenco/ziggy": "^2.0"
|
"tightenco/ziggy": "^2.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "86d4a008177912b9459f27068d5f4e1d",
|
"content-hash": "953cea45b540b8469800ed81d3f1fba6",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "brick/math",
|
"name": "brick/math",
|
||||||
|
@ -3254,6 +3254,88 @@
|
||||||
],
|
],
|
||||||
"time": "2024-04-27T21:32:50+00:00"
|
"time": "2024-04-27T21:32:50+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "spatie/laravel-permission",
|
||||||
|
"version": "6.4.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/spatie/laravel-permission.git",
|
||||||
|
"reference": "05cce017fe3ac78f60a3fce78c07fe6e8e6e6e52"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/spatie/laravel-permission/zipball/05cce017fe3ac78f60a3fce78c07fe6e8e6e6e52",
|
||||||
|
"reference": "05cce017fe3ac78f60a3fce78c07fe6e8e6e6e52",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"illuminate/auth": "^8.12|^9.0|^10.0|^11.0",
|
||||||
|
"illuminate/container": "^8.12|^9.0|^10.0|^11.0",
|
||||||
|
"illuminate/contracts": "^8.12|^9.0|^10.0|^11.0",
|
||||||
|
"illuminate/database": "^8.12|^9.0|^10.0|^11.0",
|
||||||
|
"php": "^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"laravel/passport": "^11.0|^12.0",
|
||||||
|
"orchestra/testbench": "^6.23|^7.0|^8.0|^9.0",
|
||||||
|
"phpunit/phpunit": "^9.4|^10.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"providers": [
|
||||||
|
"Spatie\\Permission\\PermissionServiceProvider"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "6.x-dev",
|
||||||
|
"dev-master": "6.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"files": [
|
||||||
|
"src/helpers.php"
|
||||||
|
],
|
||||||
|
"psr-4": {
|
||||||
|
"Spatie\\Permission\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Freek Van der Herten",
|
||||||
|
"email": "freek@spatie.be",
|
||||||
|
"homepage": "https://spatie.be",
|
||||||
|
"role": "Developer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Permission handling for Laravel 8.0 and up",
|
||||||
|
"homepage": "https://github.com/spatie/laravel-permission",
|
||||||
|
"keywords": [
|
||||||
|
"acl",
|
||||||
|
"laravel",
|
||||||
|
"permission",
|
||||||
|
"permissions",
|
||||||
|
"rbac",
|
||||||
|
"roles",
|
||||||
|
"security",
|
||||||
|
"spatie"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/spatie/laravel-permission/issues",
|
||||||
|
"source": "https://github.com/spatie/laravel-permission/tree/6.4.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/spatie",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-02-28T08:11:20+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/clock",
|
"name": "symfony/clock",
|
||||||
"version": "v7.1.1",
|
"version": "v7.1.1",
|
||||||
|
@ -8410,5 +8492,5 @@
|
||||||
"php": "^8.2"
|
"php": "^8.2"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.3.0"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,186 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'models' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your permissions. Of course, it
|
||||||
|
* is often just the "Permission" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Permission model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Permission` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permission' => Spatie\Permission\Models\Permission::class,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* Eloquent model should be used to retrieve your roles. Of course, it
|
||||||
|
* is often just the "Role" model but you may use whatever you like.
|
||||||
|
*
|
||||||
|
* The model you want to use as a Role model needs to implement the
|
||||||
|
* `Spatie\Permission\Contracts\Role` contract.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role' => Spatie\Permission\Models\Role::class,
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
'table_names' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'roles' => 'roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your permissions. We have chosen a basic
|
||||||
|
* default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'permissions' => 'permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasPermissions" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_permissions' => 'model_has_permissions',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your models roles. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_has_roles' => 'model_has_roles',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When using the "HasRoles" trait from this package, we need to know which
|
||||||
|
* table should be used to retrieve your roles permissions. We have chosen a
|
||||||
|
* basic default value but you may easily change it to any table you like.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role_has_permissions' => 'role_has_permissions',
|
||||||
|
],
|
||||||
|
|
||||||
|
'column_names' => [
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related pivots other than defaults
|
||||||
|
*/
|
||||||
|
'role_pivot_key' => null, //default 'role_id',
|
||||||
|
'permission_pivot_key' => null, //default 'permission_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to name the related model primary key other than
|
||||||
|
* `model_id`.
|
||||||
|
*
|
||||||
|
* For example, this would be nice if your primary keys are all UUIDs. In
|
||||||
|
* that case, name this `model_uuid`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'model_morph_key' => 'model_id',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Change this if you want to use the teams feature and your related model's
|
||||||
|
* foreign key is other than `team_id`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'team_foreign_key' => 'team_id',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the method for checking permissions will be registered on the gate.
|
||||||
|
* Set this to false if you want to implement custom logic for checking permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'register_permission_check_method' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
|
||||||
|
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
|
||||||
|
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
|
||||||
|
*/
|
||||||
|
'register_octane_reset_listener' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Teams Feature.
|
||||||
|
* When set to true the package implements teams using the 'team_foreign_key'.
|
||||||
|
* If you want the migrations to register the 'team_foreign_key', you must
|
||||||
|
* set this to true before doing the migration.
|
||||||
|
* If you already did the migration then you must make a new migration to also
|
||||||
|
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
|
||||||
|
* (view the latest version of this package's migration file)
|
||||||
|
*/
|
||||||
|
|
||||||
|
'teams' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Passport Client Credentials Grant
|
||||||
|
* When set to true the package will use Passports Client to check permissions
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use_passport_client_credentials' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required permission names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_permission_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When set to true, the required role names are added to exception messages.
|
||||||
|
* This could be considered an information leak in some contexts, so the default
|
||||||
|
* setting is false here for optimum safety.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'display_role_in_exception' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default wildcard permission lookups are disabled.
|
||||||
|
* See documentation to understand supported syntax.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enable_wildcard_permission' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The class to use for interpreting wildcard permissions.
|
||||||
|
* If you need to modify delimiters, override the class and specify its name here.
|
||||||
|
*/
|
||||||
|
// 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class,
|
||||||
|
|
||||||
|
/* Cache-specific settings */
|
||||||
|
|
||||||
|
'cache' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* By default all permissions are cached for 24 hours to speed up performance.
|
||||||
|
* When permissions or roles are updated the cache is flushed automatically.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The cache key used to store all permissions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'key' => 'spatie.permission.cache',
|
||||||
|
|
||||||
|
/*
|
||||||
|
* You may optionally indicate a specific cache driver to use for permission and
|
||||||
|
* role caching using any of the `store` drivers listed in the cache.php config
|
||||||
|
* file. Using 'default' here means to use the `default` set in cache.php.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'store' => 'default',
|
||||||
|
],
|
||||||
|
];
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('Kategori', function (Blueprint $table) {
|
||||||
|
$table->id('KategoriId');
|
||||||
|
$table->string('NamaKategori')->unique();
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('Kategori');
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,138 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$teams = config('permission.teams');
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
$columnNames = config('permission.column_names');
|
||||||
|
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||||
|
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||||
|
|
||||||
|
if (empty($tableNames)) {
|
||||||
|
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
}
|
||||||
|
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
|
||||||
|
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::create($tableNames['permissions'], function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id'); // permission id
|
||||||
|
$table->string('name'); // For MySQL 8.0 use string('name', 125);
|
||||||
|
$table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125);
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
|
||||||
|
$table->bigIncrements('id'); // role id
|
||||||
|
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
|
||||||
|
}
|
||||||
|
$table->string('name'); // For MySQL 8.0 use string('name', 125);
|
||||||
|
$table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125);
|
||||||
|
$table->timestamps();
|
||||||
|
if ($teams || config('permission.testing')) {
|
||||||
|
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
|
||||||
|
} else {
|
||||||
|
$table->unique(['name', 'guard_name']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
|
||||||
|
$table->string('model_type');
|
||||||
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_permissions_permission_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_permissions_permission_model_type_primary');
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->string('model_type');
|
||||||
|
$table->unsignedBigInteger($columnNames['model_morph_key']);
|
||||||
|
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
if ($teams) {
|
||||||
|
$table->unsignedBigInteger($columnNames['team_foreign_key']);
|
||||||
|
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
|
||||||
|
|
||||||
|
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
} else {
|
||||||
|
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
|
||||||
|
'model_has_roles_role_model_type_primary');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
|
||||||
|
$table->unsignedBigInteger($pivotPermission);
|
||||||
|
$table->unsignedBigInteger($pivotRole);
|
||||||
|
|
||||||
|
$table->foreign($pivotPermission)
|
||||||
|
->references('id') // permission id
|
||||||
|
->on($tableNames['permissions'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->foreign($pivotRole)
|
||||||
|
->references('id') // role id
|
||||||
|
->on($tableNames['roles'])
|
||||||
|
->onDelete('cascade');
|
||||||
|
|
||||||
|
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
|
||||||
|
});
|
||||||
|
|
||||||
|
app('cache')
|
||||||
|
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
|
||||||
|
->forget(config('permission.cache.key'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
|
||||||
|
if (empty($tableNames)) {
|
||||||
|
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
|
||||||
|
}
|
||||||
|
|
||||||
|
Schema::drop($tableNames['role_has_permissions']);
|
||||||
|
Schema::drop($tableNames['model_has_roles']);
|
||||||
|
Schema::drop($tableNames['model_has_permissions']);
|
||||||
|
Schema::drop($tableNames['roles']);
|
||||||
|
Schema::drop($tableNames['permissions']);
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('username')->unique()->after('name');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('username');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('SubKategori', function (Blueprint $table) {
|
||||||
|
$table->id('SubKategoriId');
|
||||||
|
$table->unsignedBigInteger('KategoriId');
|
||||||
|
$table->string('NamaSubKategori');
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// Foreign key ke tabel Kategori
|
||||||
|
$table->foreign('KategoriId')->references('KategoriId')->on('Kategori')->onDelete('cascade');
|
||||||
|
|
||||||
|
// UNIQUE constraint untuk memastikan kategori & subkategori tidak duplikat
|
||||||
|
$table->unique(['KategoriId', 'NamaSubKategori']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('SubKategori');
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('Post', function (Blueprint $table) {
|
||||||
|
$table->id('PostId');
|
||||||
|
$table->unsignedBigInteger('KategoriId');
|
||||||
|
$table->unsignedBigInteger('SubKategoriId');
|
||||||
|
$table->string('JudulPost');
|
||||||
|
$table->string('SlugPost');
|
||||||
|
$table->text('DescPost');
|
||||||
|
$table->string('ImagePost')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
|
||||||
|
$table->foreign('KategoriId')->references('KategoriId')->on('Kategori')->onDelete('cascade');
|
||||||
|
$table->foreign('SubKategoriId')->references('SubKategoriId')->on('SubKategori')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('Post');
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('Post', function (Blueprint $table) {
|
||||||
|
$table->boolean('IsPublish')->default(0);
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('Post', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('IsPublish');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -13,11 +13,9 @@ class DatabaseSeeder extends Seeder
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// User::factory(10)->create();
|
$this->call([
|
||||||
|
RolesTableSeeder::class,
|
||||||
User::factory()->create([
|
UserSeeder::class,
|
||||||
'name' => 'Test User',
|
|
||||||
'email' => 'test@example.com',
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
|
||||||
|
class PermissionsTableSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
$resources = [
|
||||||
|
'users' => ['index', 'create', 'edit', 'delete'],
|
||||||
|
'roles' => ['index', 'create', 'edit', 'delete'],
|
||||||
|
'kategori' => ['index', 'create', 'edit', 'delete'],
|
||||||
|
'subkategori' => ['index', 'create', 'edit', 'delete'],
|
||||||
|
];
|
||||||
|
foreach ($resources as $resource => $actions) {
|
||||||
|
foreach ($actions as $action) {
|
||||||
|
$permissionName = "{$resource}.{$action}";
|
||||||
|
|
||||||
|
Permission::firstOrCreate(['name' => $permissionName, 'guard_name' => 'web']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class PostSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Ambil kategori dan subkategori dari database
|
||||||
|
$kategori = DB::table('Kategori')->pluck('KategoriId')->toArray();
|
||||||
|
$subkategori = DB::table('SubKategori')->pluck('SubKategoriId')->toArray();
|
||||||
|
|
||||||
|
if (empty($kategori) || empty($subkategori)) {
|
||||||
|
$this->command->info('Tidak ada data kategori atau subkategori. Harap jalankan seeder kategori dan subkategori terlebih dahulu.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buat 10 post dummy
|
||||||
|
for ($i = 1; $i <= 10; $i++) {
|
||||||
|
$judul = "Judul Post " . $i;
|
||||||
|
$slug = Str::slug($judul);
|
||||||
|
|
||||||
|
DB::table('Post')->insert([
|
||||||
|
'KategoriId' => $kategori[array_rand($kategori)],
|
||||||
|
'SubKategoriId' => $subkategori[array_rand($subkategori)],
|
||||||
|
'JudulPost' => $judul,
|
||||||
|
'SlugPost' => $slug,
|
||||||
|
'DescPost' => "<p>Deskripsi konten untuk $judul</p>",
|
||||||
|
'ImagePost' => 'images/posts/default.png', // Pastikan ada file default.png di folder public/images/posts/
|
||||||
|
'IsPublish' => rand(0, 1),
|
||||||
|
'created_at' => Carbon::now()->subDays(rand(1, 30))->format('Y-m-d H:i:s'),
|
||||||
|
'updated_at' => Carbon::now()->format('Y-m-d H:i:s'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->command->info('Seeder post berhasil dijalankan.');
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class RolesTableSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$admin = Role::firstOrCreate(['name' => 'admin', 'guard_name' => 'web']);
|
||||||
|
$user = Role::firstOrCreate(['name' => 'user', 'guard_name' => 'web']);
|
||||||
|
|
||||||
|
// Assign permissions ke role
|
||||||
|
$admin->syncPermissions(Permission::all()); // Admin mendapatkan semua akses
|
||||||
|
|
||||||
|
// Assign role `admin` ke user
|
||||||
|
$user->assignRole($admin);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,10 +14,22 @@ class UserSeeder extends Seeder
|
||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
User::create([
|
$admin = User::create([
|
||||||
'name' => 'Admin User',
|
'name' => 'Admin',
|
||||||
'email' => 'admin@gmail.com',
|
'username' => 'admindlh25',
|
||||||
'password' => Hash::make('password')
|
'email' => 'dlh@gmail.com',
|
||||||
|
'password' => Hash::make('admindlh25'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$admin->assignRole('admin');
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => 'Uji Coba',
|
||||||
|
'username' => 'ujicoba25',
|
||||||
|
'email' => 'ujicoba@gmail.com',
|
||||||
|
'password' => Hash::make('ujicoba25'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->assignRole('user');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -29,19 +29,22 @@
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-avatar": "^1.1.1",
|
"@radix-ui/react-avatar": "^1.1.1",
|
||||||
"@radix-ui/react-collapsible": "^1.1.1",
|
"@radix-ui/react-collapsible": "^1.1.1",
|
||||||
"@radix-ui/react-dialog": "^1.1.2",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-navigation-menu": "^1.2.4",
|
"@radix-ui/react-navigation-menu": "^1.2.4",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
|
"@radix-ui/react-select": "^2.1.6",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
|
"@radix-ui/react-toast": "^1.2.6",
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@reduxjs/toolkit": "^2.5.1",
|
"@reduxjs/toolkit": "^2.5.1",
|
||||||
"@types/react-redux": "^7.1.34",
|
"@types/react-redux": "^7.1.34",
|
||||||
"apexcharts": "^4.4.0",
|
"apexcharts": "^4.4.0",
|
||||||
|
"ckeditor4-react": "^4.1.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
@ -56,6 +59,7 @@
|
||||||
"react-i18next": "^15.4.0",
|
"react-i18next": "^15.4.0",
|
||||||
"react-perfect-scrollbar": "^1.5.8",
|
"react-perfect-scrollbar": "^1.5.8",
|
||||||
"react-popper": "^2.3.0",
|
"react-popper": "^2.3.0",
|
||||||
|
"react-quill": "^2.0.0",
|
||||||
"react-redux": "^9.2.0",
|
"react-redux": "^9.2.0",
|
||||||
"react-resizable-panels": "^2.0.19",
|
"react-resizable-panels": "^2.0.19",
|
||||||
"react-router-dom": "^7.1.4",
|
"react-router-dom": "^7.1.4",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import "./bootstrap";
|
import "./bootstrap";
|
||||||
|
import "../css/app.css";
|
||||||
|
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
import { createInertiaApp } from "@inertiajs/react";
|
import { createInertiaApp } from "@inertiajs/react";
|
||||||
|
@ -6,7 +7,6 @@ import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
|
||||||
import { ThemeProvider } from "@/components/theme-provider";
|
import { ThemeProvider } from "@/components/theme-provider";
|
||||||
|
|
||||||
const appName = import.meta.env.VITE_APP_NAME || "Laravel";
|
const appName = import.meta.env.VITE_APP_NAME || "Laravel";
|
||||||
|
|
||||||
createInertiaApp({
|
createInertiaApp({
|
||||||
title: (title) => `${title} - ${appName}`,
|
title: (title) => `${title} - ${appName}`,
|
||||||
resolve: (name) =>
|
resolve: (name) =>
|
||||||
|
@ -14,6 +14,7 @@ createInertiaApp({
|
||||||
`./pages/${name}.tsx`,
|
`./pages/${name}.tsx`,
|
||||||
import.meta.glob("./pages/**/*.tsx")
|
import.meta.glob("./pages/**/*.tsx")
|
||||||
),
|
),
|
||||||
|
|
||||||
setup({ el, App, props }) {
|
setup({ el, App, props }) {
|
||||||
const root = createRoot(el);
|
const root = createRoot(el);
|
||||||
|
|
||||||
|
|
|
@ -3,102 +3,139 @@ import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import { Link } from "@inertiajs/react";
|
||||||
|
|
||||||
const pengumumans = [
|
// const pengumumans = [
|
||||||
{
|
// {
|
||||||
id: 1,
|
// id: 1,
|
||||||
title_page: "Pengumuman",
|
// title_page: "Pengumuman",
|
||||||
alt_image: "Pengumuman",
|
// alt_image: "Pengumuman",
|
||||||
date: "16 Januari 2025",
|
// date: "16 Januari 2025",
|
||||||
title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
|
// title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
|
||||||
description:
|
// description:
|
||||||
"Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
|
// "Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
|
||||||
image: "/assets/img1.jpg",
|
// image: "/assets/img1.jpg",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 2,
|
// id: 2,
|
||||||
title_page: "Pengumuman",
|
// title_page: "Pengumuman",
|
||||||
alt_image: "Pengumuman",
|
// alt_image: "Pengumuman",
|
||||||
date: "12 Desember 2024",
|
// date: "12 Desember 2024",
|
||||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||||
description:
|
// description:
|
||||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||||
image: "/assets/img1.jpg",
|
// image: "/assets/img1.jpg",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 3,
|
// id: 3,
|
||||||
title_page: "Pengumuman",
|
// title_page: "Pengumuman",
|
||||||
alt_image: "Pengumuman",
|
// alt_image: "Pengumuman",
|
||||||
date: "12 Desember 2024",
|
// date: "12 Desember 2024",
|
||||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||||
description:
|
// description:
|
||||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||||
image: "/assets/img1.jpg",
|
// image: "/assets/img1.jpg",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 4,
|
// id: 4,
|
||||||
title_page: "Pengumuman",
|
// title_page: "Pengumuman",
|
||||||
alt_image: "Pengumuman",
|
// alt_image: "Pengumuman",
|
||||||
date: "12 Desember 2024",
|
// date: "12 Desember 2024",
|
||||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||||
description:
|
// description:
|
||||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||||
image: "/assets/img1.jpg",
|
// image: "/assets/img1.jpg",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 5,
|
// id: 5,
|
||||||
title_page: "Pengumuman",
|
// title_page: "Pengumuman",
|
||||||
alt_image: "Pengumuman",
|
// alt_image: "Pengumuman",
|
||||||
date: "12 Desember 2024",
|
// date: "12 Desember 2024",
|
||||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||||
description:
|
// description:
|
||||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||||
image: "/assets/img1.jpg",
|
// image: "/assets/img1.jpg",
|
||||||
},
|
// },
|
||||||
{
|
// {
|
||||||
id: 6,
|
// id: 6,
|
||||||
title_page: "Pengumuman",
|
// title_page: "Pengumuman",
|
||||||
alt_image: "Pengumuman",
|
// alt_image: "Pengumuman",
|
||||||
date: "12 Desember 2024",
|
// date: "12 Desember 2024",
|
||||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||||
description:
|
// description:
|
||||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||||
image: "/assets/img1.jpg",
|
// image: "/assets/img1.jpg",
|
||||||
},
|
// },
|
||||||
];
|
// ];
|
||||||
|
|
||||||
const CardPengumuman = () => {
|
interface SubKategori {
|
||||||
|
SubKategoriId: number;
|
||||||
|
NamaSubKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Kategori {
|
||||||
|
KategoriId: number;
|
||||||
|
NamaKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Post {
|
||||||
|
PostId: number;
|
||||||
|
JudulPost: string;
|
||||||
|
DescPost: string;
|
||||||
|
SlugPost: string;
|
||||||
|
ImagePost: string;
|
||||||
|
IsPublish: boolean;
|
||||||
|
created_at: string;
|
||||||
|
kategori?: Kategori;
|
||||||
|
subkategori?: SubKategori;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CardPengumumanProps {
|
||||||
|
posts: Post[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const CardPengumuman = ({ posts }: CardPengumumanProps) => {
|
||||||
return (
|
return (
|
||||||
<section className="container max-w-7xl py-8 px-6">
|
<section className="container max-w-7xl py-8 px-6">
|
||||||
{/* List of Announcements */}
|
{/* List of Announcements */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{pengumumans.map((item) => (
|
{posts.map((post) => (
|
||||||
<Card key={item.id} className="md:p-4">
|
<Card key={post.PostId} className="md:p-4">
|
||||||
<img
|
<img
|
||||||
src={item.image}
|
src={`/storage/${post.ImagePost}`}
|
||||||
alt={item.alt_image}
|
alt={post.JudulPost}
|
||||||
className="rounded-md"
|
className="rounded-md"
|
||||||
/>
|
/>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<Badge className="bg-red-600 text-white">
|
<Badge className="bg-red-600 text-white">
|
||||||
{item.title_page}
|
{post.kategori?.NamaKategori} |{" "}
|
||||||
|
{post.subkategori?.NamaSubKategori}
|
||||||
</Badge>
|
</Badge>
|
||||||
<p className="text-gray-500 text-sm mt-2">
|
<p className="text-gray-500 text-sm mt-2">
|
||||||
{item.date}
|
{new Date(post.created_at).toLocaleDateString(
|
||||||
|
"id-ID",
|
||||||
|
{
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<h3 className="text-md font-semibold mt-2 text-gray-900">
|
<h3 className="text-md font-semibold mt-2 text-gray-900">
|
||||||
{item.title}
|
{post.JudulPost}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-600 mt-2">
|
<p className="text-sm text-gray-600 mt-2">
|
||||||
{item.description}
|
{post.DescPost.replace(/<[^>]*>/g, "")}
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Link href={`/post/${post.SlugPost}`}>
|
||||||
variant="link"
|
<Button
|
||||||
className="text-red-600 mt-2 pl-0"
|
variant="link"
|
||||||
>
|
className="text-red-600 mt-2 pl-0"
|
||||||
Baca Selengkapnya{" "}
|
>
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
Baca Selengkapnya{" "}
|
||||||
</Button>
|
<ArrowRight className="ml-2 w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
import RunningText from "./RunningText";
|
import RunningText from "./RunningText";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
import SearchDialog from "./SearchDialog";
|
||||||
|
|
||||||
interface NavItemsProps {
|
interface NavItemsProps {
|
||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
|
@ -26,7 +27,7 @@ interface NavItemsProps {
|
||||||
const Navbar = () => {
|
const Navbar = () => {
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||||
const [searchQuery, setSearchQuery] = useState("");
|
// const [searchQuery, setSearchQuery] = useState("");
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
// Handle scroll effect
|
// Handle scroll effect
|
||||||
|
@ -44,14 +45,6 @@ const Navbar = () => {
|
||||||
setTheme(theme === "light" ? "dark" : "light");
|
setTheme(theme === "light" ? "dark" : "light");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSearch = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (searchQuery) {
|
|
||||||
router.push(`/search/${searchQuery}`);
|
|
||||||
setSearchQuery(""); // Clear input setelah search
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const NavItems: React.FC<NavItemsProps> = ({ mobile = false, onClose }) => (
|
const NavItems: React.FC<NavItemsProps> = ({ mobile = false, onClose }) => (
|
||||||
<>
|
<>
|
||||||
<NavigationMenuItem>
|
<NavigationMenuItem>
|
||||||
|
@ -82,7 +75,7 @@ const Navbar = () => {
|
||||||
</Link>
|
</Link>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
))}
|
))}
|
||||||
<NavigationMenuItem>
|
{/* <NavigationMenuItem>
|
||||||
<form onSubmit={handleSearch} className="flex items-center">
|
<form onSubmit={handleSearch} className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -101,6 +94,9 @@ const Navbar = () => {
|
||||||
<Search className="h-4 w-4 text-white" />
|
<Search className="h-4 w-4 text-white" />
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
</NavigationMenuItem> */}
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<SearchDialog />
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
// PopupModal.js
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { X, ArrowLeft, ArrowRight } from "lucide-react";
|
import { X, ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
|
@ -15,7 +14,7 @@ const PopupModal = ({ onClose }: { onClose: () => void }) => {
|
||||||
title: "Workshop Peningkatan Kapasitas Pengendalian Pencemaran Udara",
|
title: "Workshop Peningkatan Kapasitas Pengendalian Pencemaran Udara",
|
||||||
image: "/assets/popup-2.jpeg",
|
image: "/assets/popup-2.jpeg",
|
||||||
description:
|
description:
|
||||||
"Workshop ini bertujuan untuk meningkatkan pengetahuan dan keterampilan di bidang lingkungan hidup.",
|
"Workshop ini bertujuan untuk meningkatkan pengetahuan dan keterampilan di bidang lingkungan hidup. Workshop ini bertujuan untuk meningkatkan pengetahuan dan keterampilan di bidang lingkungan hidup.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Sosialisasi Program Ramah Lingkungan",
|
title: "Sosialisasi Program Ramah Lingkungan",
|
||||||
|
@ -36,30 +35,39 @@ const PopupModal = ({ onClose }: { onClose: () => void }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50 container md:max-w-none">
|
||||||
<div className="bg-white p-6 rounded-lg max-w-lg w-full relative">
|
<div className="bg-white p-6 rounded-lg relative md:max-w-2xl mx-auto w-full h-50 md:h-5/6 flex flex-col justify-between md:justify-around">
|
||||||
<button className="absolute top-2 right-2" onClick={onClose}>
|
<button className="absolute top-2 right-2" onClick={onClose}>
|
||||||
<X className="w-6 h-6 text-gray-700" />
|
<X className="w-6 h-6 text-gray-700" />
|
||||||
</button>
|
</button>
|
||||||
<h2 className="text-xl font-bold text-center mb-4">
|
<h2 className="md:text-xl text-md font-bold text-center mb-4">
|
||||||
{slides[currentSlide].title}
|
{slides[currentSlide].title}
|
||||||
</h2>
|
</h2>
|
||||||
<img
|
<div className="relative">
|
||||||
src={slides[currentSlide].image}
|
<img
|
||||||
alt="Popup Slide Image"
|
src={slides[currentSlide].image}
|
||||||
className="w-full h-auto mb-4"
|
alt="Popup Slide Image"
|
||||||
/>
|
className="w-full h-full md:h-96 object-contain mb-4"
|
||||||
<p className="text-sm text-gray-600 text-center mb-4">
|
/>
|
||||||
{slides[currentSlide].description}
|
<button
|
||||||
</p>
|
onClick={prevSlide}
|
||||||
<div className="flex justify-between items-center mb-4">
|
className="absolute left-2 top-1/2 transform -translate-y-1/2 p-2 bg-white bg-opacity-50 rounded-full"
|
||||||
<button onClick={prevSlide} className="p-2">
|
>
|
||||||
<ArrowLeft className="w-6 h-6 text-gray-700" />
|
<ArrowLeft className="w-6 h-6 text-gray-700" />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={nextSlide} className="p-2">
|
<button
|
||||||
|
onClick={nextSlide}
|
||||||
|
className="absolute right-2 top-1/2 transform -translate-y-1/2 p-2 bg-white bg-opacity-50 rounded-full"
|
||||||
|
>
|
||||||
<ArrowRight className="w-6 h-6 text-gray-700" />
|
<ArrowRight className="w-6 h-6 text-gray-700" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* <h2 className="text-xl font-bold text-center mb-4">
|
||||||
|
{slides[currentSlide].title}
|
||||||
|
</h2> */}
|
||||||
|
<p className="text-sm text-gray-600 text-center mb-4 overflow-hidden text-ellipsis whitespace-nowrap">
|
||||||
|
{slides[currentSlide].description}
|
||||||
|
</p>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Button className="bg-green-800 text-white px-6 py-2 rounded-lg">
|
<Button className="bg-green-800 text-white px-6 py-2 rounded-lg">
|
||||||
Selengkapnya
|
Selengkapnya
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { Search } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogClose,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { router } from "@inertiajs/react";
|
||||||
|
|
||||||
|
const SearchDialog: React.FC = () => {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
|
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (searchQuery.trim()) {
|
||||||
|
router.visit(`/search/${encodeURIComponent(searchQuery)}`);
|
||||||
|
setSearchQuery(""); // Clear input setelah search
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="bg-green-800/90 hover:bg-green-700 text-white p-2 rounded-full"
|
||||||
|
>
|
||||||
|
<Search className="h-6 w-6" />
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="bg-white text-black p-6 rounded-lg w-[90%] max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-lg font-bold text-center">
|
||||||
|
Search
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSearch}
|
||||||
|
className="flex items-center gap-2 w-full"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
className="bg-gray-100 px-3 py-2 rounded-lg focus:outline-none w-full"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="bg-green-700 hover:bg-green-600 text-white p-2 rounded-lg"
|
||||||
|
>
|
||||||
|
<Search className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute top-4 right-4 text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchDialog;
|
|
@ -3,6 +3,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
|
FolderArchive,
|
||||||
Frame,
|
Frame,
|
||||||
Home,
|
Home,
|
||||||
LifeBuoy,
|
LifeBuoy,
|
||||||
|
@ -38,20 +39,28 @@ const data = {
|
||||||
icon: Home,
|
icon: Home,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Projects",
|
title: "Data Master",
|
||||||
url: "#",
|
url: "#",
|
||||||
icon: SquareTerminal,
|
icon: FolderArchive,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
title: "History",
|
title: "Kategori Post",
|
||||||
url: "#",
|
url: "/admin/kategori",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Sub Kategori Post",
|
||||||
|
url: "/admin/subkategori",
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// title: "Menu",
|
||||||
|
// url: "/menus",
|
||||||
|
// },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Design Engineering",
|
title: "Post",
|
||||||
url: "#",
|
url: "/admin/post",
|
||||||
icon: Frame,
|
icon: Frame,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||||
|
import { X } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Dialog = DialogPrimitive.Root
|
||||||
|
|
||||||
|
const DialogTrigger = DialogPrimitive.Trigger
|
||||||
|
|
||||||
|
const DialogPortal = DialogPrimitive.Portal
|
||||||
|
|
||||||
|
const DialogClose = DialogPrimitive.Close
|
||||||
|
|
||||||
|
const DialogOverlay = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Overlay
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||||
|
|
||||||
|
const DialogContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay />
|
||||||
|
<DialogPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
</DialogPrimitive.Close>
|
||||||
|
</DialogPrimitive.Content>
|
||||||
|
</DialogPortal>
|
||||||
|
))
|
||||||
|
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = "DialogHeader"
|
||||||
|
|
||||||
|
const DialogFooter = ({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
DialogFooter.displayName = "DialogFooter"
|
||||||
|
|
||||||
|
const DialogTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-lg font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||||
|
|
||||||
|
const DialogDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<DialogPrimitive.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Dialog,
|
||||||
|
DialogPortal,
|
||||||
|
DialogOverlay,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogFooter,
|
||||||
|
DialogTitle,
|
||||||
|
DialogDescription,
|
||||||
|
}
|
|
@ -0,0 +1,157 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown, ChevronUp } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectScrollUpButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUp className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
))
|
||||||
|
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
||||||
|
|
||||||
|
const SelectScrollDownButton = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDown className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
))
|
||||||
|
SelectScrollDownButton.displayName =
|
||||||
|
SelectPrimitive.ScrollDownButton.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Textarea = React.forwardRef<
|
||||||
|
HTMLTextAreaElement,
|
||||||
|
React.ComponentProps<"textarea">
|
||||||
|
>(({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<textarea
|
||||||
|
className={cn(
|
||||||
|
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
Textarea.displayName = "Textarea"
|
||||||
|
|
||||||
|
export { Textarea }
|
|
@ -0,0 +1,127 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const ToastProvider = ToastPrimitives.Provider;
|
||||||
|
|
||||||
|
const ToastViewport = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Viewport
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||||
|
|
||||||
|
const toastVariants = cva(
|
||||||
|
"group pointer-events-auto relative flex w-full items-center justify-between space-x-2 overflow-hidden rounded-md border p-4 pr-6 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "border bg-background text-foreground",
|
||||||
|
destructive:
|
||||||
|
"destructive group border-destructive bg-destructive text-destructive-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Toast = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
|
||||||
|
VariantProps<typeof toastVariants>
|
||||||
|
>(({ className, variant, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<ToastPrimitives.Root
|
||||||
|
ref={ref}
|
||||||
|
className={cn(toastVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||||
|
|
||||||
|
const ToastAction = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Action
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium transition-colors hover:bg-secondary focus:outline-none focus:ring-1 focus:ring-ring disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||||
|
|
||||||
|
const ToastClose = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Close
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"absolute right-1 top-1 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-1 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
toast-close=""
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</ToastPrimitives.Close>
|
||||||
|
));
|
||||||
|
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||||
|
|
||||||
|
const ToastTitle = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Title
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm font-semibold [&+div]:text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||||
|
|
||||||
|
const ToastDescription = React.forwardRef<
|
||||||
|
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<ToastPrimitives.Description
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm opacity-90", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||||
|
|
||||||
|
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||||
|
|
||||||
|
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||||
|
|
||||||
|
export {
|
||||||
|
type ToastProps,
|
||||||
|
type ToastActionElement,
|
||||||
|
ToastProvider,
|
||||||
|
ToastViewport,
|
||||||
|
Toast,
|
||||||
|
ToastTitle,
|
||||||
|
ToastDescription,
|
||||||
|
ToastClose,
|
||||||
|
ToastAction,
|
||||||
|
};
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import {
|
||||||
|
Toast,
|
||||||
|
ToastClose,
|
||||||
|
ToastDescription,
|
||||||
|
ToastProvider,
|
||||||
|
ToastTitle,
|
||||||
|
ToastViewport,
|
||||||
|
} from "@/components/ui/toast";
|
||||||
|
|
||||||
|
export function Toaster() {
|
||||||
|
const { toasts } = useToast();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ToastProvider duration={2000} swipeDirection="right">
|
||||||
|
{toasts.map(function ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
action,
|
||||||
|
...props
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Toast key={id} {...props}>
|
||||||
|
<div className="grid gap-1">
|
||||||
|
{title && <ToastTitle>{title}</ToastTitle>}
|
||||||
|
{description && (
|
||||||
|
<ToastDescription>
|
||||||
|
{description}
|
||||||
|
</ToastDescription>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{action}
|
||||||
|
<ToastClose />
|
||||||
|
</Toast>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<ToastViewport />
|
||||||
|
</ToastProvider>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,194 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
// Inspired by react-hot-toast library
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ToastActionElement,
|
||||||
|
ToastProps,
|
||||||
|
} from "@/components/ui/toast"
|
||||||
|
|
||||||
|
const TOAST_LIMIT = 1
|
||||||
|
const TOAST_REMOVE_DELAY = 1000000
|
||||||
|
|
||||||
|
type ToasterToast = ToastProps & {
|
||||||
|
id: string
|
||||||
|
title?: React.ReactNode
|
||||||
|
description?: React.ReactNode
|
||||||
|
action?: ToastActionElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionTypes = {
|
||||||
|
ADD_TOAST: "ADD_TOAST",
|
||||||
|
UPDATE_TOAST: "UPDATE_TOAST",
|
||||||
|
DISMISS_TOAST: "DISMISS_TOAST",
|
||||||
|
REMOVE_TOAST: "REMOVE_TOAST",
|
||||||
|
} as const
|
||||||
|
|
||||||
|
let count = 0
|
||||||
|
|
||||||
|
function genId() {
|
||||||
|
count = (count + 1) % Number.MAX_SAFE_INTEGER
|
||||||
|
return count.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType = typeof actionTypes
|
||||||
|
|
||||||
|
type Action =
|
||||||
|
| {
|
||||||
|
type: ActionType["ADD_TOAST"]
|
||||||
|
toast: ToasterToast
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["UPDATE_TOAST"]
|
||||||
|
toast: Partial<ToasterToast>
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["DISMISS_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: ActionType["REMOVE_TOAST"]
|
||||||
|
toastId?: ToasterToast["id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
toasts: ToasterToast[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
const addToRemoveQueue = (toastId: string) => {
|
||||||
|
if (toastTimeouts.has(toastId)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
toastTimeouts.delete(toastId)
|
||||||
|
dispatch({
|
||||||
|
type: "REMOVE_TOAST",
|
||||||
|
toastId: toastId,
|
||||||
|
})
|
||||||
|
}, TOAST_REMOVE_DELAY)
|
||||||
|
|
||||||
|
toastTimeouts.set(toastId, timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const reducer = (state: State, action: Action): State => {
|
||||||
|
switch (action.type) {
|
||||||
|
case "ADD_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "UPDATE_TOAST":
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === action.toast.id ? { ...t, ...action.toast } : t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
case "DISMISS_TOAST": {
|
||||||
|
const { toastId } = action
|
||||||
|
|
||||||
|
// ! Side effects ! - This could be extracted into a dismissToast() action,
|
||||||
|
// but I'll keep it here for simplicity
|
||||||
|
if (toastId) {
|
||||||
|
addToRemoveQueue(toastId)
|
||||||
|
} else {
|
||||||
|
state.toasts.forEach((toast) => {
|
||||||
|
addToRemoveQueue(toast.id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.map((t) =>
|
||||||
|
t.id === toastId || toastId === undefined
|
||||||
|
? {
|
||||||
|
...t,
|
||||||
|
open: false,
|
||||||
|
}
|
||||||
|
: t
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "REMOVE_TOAST":
|
||||||
|
if (action.toastId === undefined) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toasts: state.toasts.filter((t) => t.id !== action.toastId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const listeners: Array<(state: State) => void> = []
|
||||||
|
|
||||||
|
let memoryState: State = { toasts: [] }
|
||||||
|
|
||||||
|
function dispatch(action: Action) {
|
||||||
|
memoryState = reducer(memoryState, action)
|
||||||
|
listeners.forEach((listener) => {
|
||||||
|
listener(memoryState)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type Toast = Omit<ToasterToast, "id">
|
||||||
|
|
||||||
|
function toast({ ...props }: Toast) {
|
||||||
|
const id = genId()
|
||||||
|
|
||||||
|
const update = (props: ToasterToast) =>
|
||||||
|
dispatch({
|
||||||
|
type: "UPDATE_TOAST",
|
||||||
|
toast: { ...props, id },
|
||||||
|
})
|
||||||
|
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: "ADD_TOAST",
|
||||||
|
toast: {
|
||||||
|
...props,
|
||||||
|
id,
|
||||||
|
open: true,
|
||||||
|
onOpenChange: (open) => {
|
||||||
|
if (!open) dismiss()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: id,
|
||||||
|
dismiss,
|
||||||
|
update,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useToast() {
|
||||||
|
const [state, setState] = React.useState<State>(memoryState)
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
listeners.push(setState)
|
||||||
|
return () => {
|
||||||
|
const index = listeners.indexOf(setState)
|
||||||
|
if (index > -1) {
|
||||||
|
listeners.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [state])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
toast,
|
||||||
|
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { useToast, toast }
|
|
@ -11,7 +11,7 @@ export default function Pengumuman() {
|
||||||
<Head title="Pengumuman" />
|
<Head title="Pengumuman" />
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<HeroSecond title="Pengumuman" />
|
<HeroSecond title="Pengumuman" />
|
||||||
<CardPengumuman />
|
<CardPengumuman posts={[]} />
|
||||||
<Footer />
|
<Footer />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,461 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "@inertiajs/react";
|
||||||
|
import { PageProps } from "@/types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
// import { toast } from "react-toastify";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import AuthenticatedLayout from "@/layouts/authenticated-layout";
|
||||||
|
import { Head } from "@inertiajs/react";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
interface Kategori {
|
||||||
|
KategoriId: number | null;
|
||||||
|
NamaKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 5;
|
||||||
|
|
||||||
|
export default function KategoriIndex({
|
||||||
|
kategori,
|
||||||
|
}: PageProps<{ kategori: Kategori[] }>) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
post,
|
||||||
|
put,
|
||||||
|
delete: destroy,
|
||||||
|
reset,
|
||||||
|
} = useForm<Kategori>({
|
||||||
|
KategoriId: null,
|
||||||
|
NamaKategori: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<Kategori | null>(null);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const [filteredKategori, setFilteredKategori] = useState(kategori);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = kategori;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
filtered = filtered.filter((kategori) =>
|
||||||
|
kategori.NamaKategori.toLowerCase().includes(
|
||||||
|
search.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredKategori(filtered);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [kategori, search]);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredKategori.length / ITEMS_PER_PAGE);
|
||||||
|
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||||
|
const endIndex = startIndex + ITEMS_PER_PAGE;
|
||||||
|
const currentItems = filteredKategori.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
put(`/admin/kategori/${data.KategoriId}`, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Berhasil",
|
||||||
|
description: "Kategori berhasil diperbarui",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setIsModalOpen(false);
|
||||||
|
reset();
|
||||||
|
setEditing(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Gagal",
|
||||||
|
description:
|
||||||
|
"Terjadi kesalahan saat memperbarui kategori",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
post("/admin/kategori", {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Berhasil",
|
||||||
|
description: "Kategori berhasil dibuat",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setIsModalOpen(false);
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Gagal",
|
||||||
|
description: "Terjadi kesalahan saat membuat kategori",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (kategori: Kategori) => {
|
||||||
|
setData({ ...kategori });
|
||||||
|
setEditing(true);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (deleteConfirm) {
|
||||||
|
destroy(`/admin/kategori/${deleteConfirm.KategoriId}`, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Berhasil",
|
||||||
|
description: "Kategori berhasil dihapus",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Gagal",
|
||||||
|
description:
|
||||||
|
"Terjadi kesalahan saat menghapus kategori",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<AuthenticatedLayout header={"Kategori Post"}>
|
||||||
|
<Head title="Kategori Post" />
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
|
<div className="relative w-full md:w-96">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-4 w-4" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Cari Kategori..."
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="pl-10 pr-4 w-full border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Dialog
|
||||||
|
open={isModalOpen}
|
||||||
|
onOpenChange={setIsModalOpen}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(false);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 rounded-lg flex items-center gap-2 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Buat Kategori
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-semibold">
|
||||||
|
{editing
|
||||||
|
? "Ubah Kategori"
|
||||||
|
: "Buat Kategori"}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-col gap-4 mt-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Name
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan nama kategori"
|
||||||
|
value={data.NamaKategori}
|
||||||
|
onChange={(e) =>
|
||||||
|
setData(
|
||||||
|
"NamaKategori",
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
{/* <div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Publish
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
data.is_publish
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
}
|
||||||
|
onValueChange={(
|
||||||
|
value
|
||||||
|
) =>
|
||||||
|
setData(
|
||||||
|
"is_publish",
|
||||||
|
value === "true"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="true">
|
||||||
|
Published
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="false">
|
||||||
|
Unpublished
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="mt-6">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{editing ? "Ubah" : "Buat"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="rounded-lg border border-gray-200 overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-gray-50 dark:bg-black/50">
|
||||||
|
{/* <TableHead className="font-semibold">
|
||||||
|
ID
|
||||||
|
</TableHead> */}
|
||||||
|
<TableHead className="font-semibold">
|
||||||
|
No
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="font-semibold">
|
||||||
|
Name
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="font-semibold">
|
||||||
|
Actions
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{currentItems.map((kategori, index) => (
|
||||||
|
<TableRow
|
||||||
|
key={kategori.KategoriId || "new"}
|
||||||
|
className="hover:bg-gray-50 transition-colors duration-150"
|
||||||
|
>
|
||||||
|
{/* <TableCell>{category.id}</TableCell> */}
|
||||||
|
<TableCell>
|
||||||
|
{startIndex + index + 1}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{kategori.NamaKategori}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
handleEdit(kategori)
|
||||||
|
}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
setDeleteConfirm(
|
||||||
|
kategori
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="destructive"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{currentItems.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-center py-4 text-gray-500"
|
||||||
|
>
|
||||||
|
Tidak ada data
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
{/* <div className="flex flex-row mt-3">
|
||||||
|
<div className="text-sm w-1/2">
|
||||||
|
1 : Pengumuman <br /> 2 : Undangan
|
||||||
|
</div>
|
||||||
|
<div className="text-sm w-1/2">
|
||||||
|
3 : Peraturan <br /> 4 : Popup Home
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Showing {startIndex + 1} to{" "}
|
||||||
|
{Math.min(endIndex, filteredKategori.length)} of{" "}
|
||||||
|
{filteredKategori.length} entries
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(currentPage - 1)
|
||||||
|
}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{Array.from(
|
||||||
|
{ length: totalPages },
|
||||||
|
(_, i) => i + 1
|
||||||
|
).map((page) => (
|
||||||
|
<Button
|
||||||
|
key={page}
|
||||||
|
variant={
|
||||||
|
currentPage === page
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(page)}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(currentPage + 1)
|
||||||
|
}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deleteConfirm && (
|
||||||
|
<Dialog open={true} onOpenChange={() => setDeleteConfirm(null)}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-semibold">
|
||||||
|
Konfirmasi Penghapusan
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Apakah Anda yakin ingin menghapus "
|
||||||
|
<span className="font-medium">
|
||||||
|
{deleteConfirm.NamaKategori}
|
||||||
|
</span>
|
||||||
|
"?
|
||||||
|
<br />
|
||||||
|
<span className="text-red-500">
|
||||||
|
Tindakan ini tidak dapat dibatalkan.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => setDeleteConfirm(null)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDelete}
|
||||||
|
variant="destructive"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Ya, hapus
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
<Toaster />
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,273 @@
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { useForm } from "@inertiajs/react";
|
||||||
|
import AuthenticatedLayout from "@/layouts/authenticated-layout";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Head } from "@inertiajs/react";
|
||||||
|
import { CKEditor } from "ckeditor4-react"; // ✅ Import CKEditor langsung
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
const slugify = (text: string) => text.toLowerCase().replace(/\s+/g, "-");
|
||||||
|
|
||||||
|
interface Kategori {
|
||||||
|
KategoriId: number;
|
||||||
|
NamaKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubKategori {
|
||||||
|
SubKategoriId: number;
|
||||||
|
KategoriId: number;
|
||||||
|
NamaSubKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddPostProps {
|
||||||
|
kategori: Kategori[];
|
||||||
|
subkategori: SubKategori[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AddPost({ kategori, subkategori }: AddPostProps) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { data, setData, post, reset } = useForm({
|
||||||
|
KategoriId: "",
|
||||||
|
SubKategoriId: "",
|
||||||
|
JudulPost: "",
|
||||||
|
SlugPost: "",
|
||||||
|
DescPost: "",
|
||||||
|
ImagePost: null as File | null,
|
||||||
|
IsPublish: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setData("SlugPost", slugify(data.JudulPost));
|
||||||
|
}, [data.JudulPost]);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("KategoriId", data.KategoriId);
|
||||||
|
formData.append("SubKategoriId", data.SubKategoriId);
|
||||||
|
formData.append("JudulPost", data.JudulPost);
|
||||||
|
formData.append("SlugPost", data.SlugPost);
|
||||||
|
formData.append("DescPost", data.DescPost);
|
||||||
|
formData.append("IsPublish", data.IsPublish.toString());
|
||||||
|
|
||||||
|
if (data.ImagePost) {
|
||||||
|
formData.append("ImagePost", data.ImagePost);
|
||||||
|
}
|
||||||
|
|
||||||
|
post("/admin/post", {
|
||||||
|
data: formData,
|
||||||
|
headers: { "Content-Type": "multipart/form-data" },
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Berhasil",
|
||||||
|
description: "Post berhasil dibuat",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Gagal",
|
||||||
|
description: "Terjadi kesalahan saat membuat Post",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticatedLayout header="Tambah Post">
|
||||||
|
<Head title="Tambah Post" />
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Judul Post"
|
||||||
|
value={data.JudulPost}
|
||||||
|
onChange={(e) => setData("JudulPost", e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Slug Post"
|
||||||
|
value={data.SlugPost}
|
||||||
|
readOnly
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setData("KategoriId", Number(value).toString())
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Pilih Kategori" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{kategori.map((kat) => (
|
||||||
|
<SelectItem
|
||||||
|
key={kat.KategoriId}
|
||||||
|
value={kat.KategoriId.toString()}
|
||||||
|
>
|
||||||
|
{kat.NamaKategori}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{data.KategoriId && (
|
||||||
|
<Select
|
||||||
|
onValueChange={(value) =>
|
||||||
|
setData(
|
||||||
|
"SubKategoriId",
|
||||||
|
Number(value).toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Pilih SubKategori" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{subkategori
|
||||||
|
.filter(
|
||||||
|
(sub) =>
|
||||||
|
sub.KategoriId ===
|
||||||
|
Number(data.KategoriId)
|
||||||
|
)
|
||||||
|
.map((sub) => (
|
||||||
|
<SelectItem
|
||||||
|
key={sub.SubKategoriId}
|
||||||
|
value={sub.SubKategoriId.toString()}
|
||||||
|
>
|
||||||
|
{sub.NamaSubKategori}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="relative min-h-[300px]">
|
||||||
|
<CKEditor
|
||||||
|
initData={data.DescPost}
|
||||||
|
onChange={(evt) => {
|
||||||
|
const newData = evt.editor.getData();
|
||||||
|
setData("DescPost", newData);
|
||||||
|
}}
|
||||||
|
config={{
|
||||||
|
height: 300,
|
||||||
|
removePlugins:
|
||||||
|
"easyimage,cloudservices,exportpdf",
|
||||||
|
toolbar: [
|
||||||
|
{ name: "document", items: ["Source"] },
|
||||||
|
{
|
||||||
|
name: "clipboard",
|
||||||
|
items: [
|
||||||
|
"Cut",
|
||||||
|
"Copy",
|
||||||
|
"Paste",
|
||||||
|
"PasteText",
|
||||||
|
"PasteFromWord",
|
||||||
|
"-",
|
||||||
|
"Undo",
|
||||||
|
"Redo",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "editing",
|
||||||
|
items: [
|
||||||
|
"Find",
|
||||||
|
"Replace",
|
||||||
|
"-",
|
||||||
|
"SelectAll",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "basicstyles",
|
||||||
|
items: [
|
||||||
|
"Bold",
|
||||||
|
"Italic",
|
||||||
|
"Underline",
|
||||||
|
"Strike",
|
||||||
|
"Subscript",
|
||||||
|
"Superscript",
|
||||||
|
"-",
|
||||||
|
"RemoveFormat",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "paragraph",
|
||||||
|
items: [
|
||||||
|
"NumberedList",
|
||||||
|
"BulletedList",
|
||||||
|
"-",
|
||||||
|
"Outdent",
|
||||||
|
"Indent",
|
||||||
|
"-",
|
||||||
|
"Blockquote",
|
||||||
|
"-",
|
||||||
|
"JustifyLeft",
|
||||||
|
"JustifyCenter",
|
||||||
|
"JustifyRight",
|
||||||
|
"JustifyBlock",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "links",
|
||||||
|
items: ["Link", "Unlink"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "insert",
|
||||||
|
items: [
|
||||||
|
"Image",
|
||||||
|
"Table",
|
||||||
|
"HorizontalRule",
|
||||||
|
"SpecialChar",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "styles",
|
||||||
|
items: [
|
||||||
|
"Styles",
|
||||||
|
"Format",
|
||||||
|
"Font",
|
||||||
|
"FontSize",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "colors",
|
||||||
|
items: ["TextColor", "BGColor"],
|
||||||
|
},
|
||||||
|
{ name: "tools", items: ["Maximize"] },
|
||||||
|
],
|
||||||
|
filebrowserUploadUrl: "/images/posts",
|
||||||
|
filebrowserUploadMethod: "form",
|
||||||
|
removeButtons: "",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="file"
|
||||||
|
accept="image/png, image/jpg, image/webp"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (e.target.files && e.target.files[0]) {
|
||||||
|
setData("ImagePost", e.target.files[0]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" className="bg-blue-600 text-white">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export default function PostEdit() {
|
||||||
|
return <div>PostEdit</div>;
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Link, useForm } from "@inertiajs/react";
|
||||||
|
import { PageProps } from "@/types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import AuthenticatedLayout from "@/layouts/authenticated-layout";
|
||||||
|
import { Head } from "@inertiajs/react";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
||||||
|
interface SubKategori {
|
||||||
|
SubKategoriId: number;
|
||||||
|
NamaSubKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Kategori {
|
||||||
|
KategoriId: number;
|
||||||
|
NamaKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Posting {
|
||||||
|
PostId: number | null;
|
||||||
|
JudulPost: string;
|
||||||
|
SubKategoriId: number | null;
|
||||||
|
IsPublish: boolean;
|
||||||
|
kategori?: Kategori;
|
||||||
|
subkategori?: SubKategori;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 5;
|
||||||
|
|
||||||
|
export default function PostIndex({
|
||||||
|
posts = [],
|
||||||
|
}: PageProps<{ posts: Posting[] }>) {
|
||||||
|
const { toast } = useToast();
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
const { delete: destroy } = useForm({});
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
const [filteredPosting, setFilteredPosting] = useState(posts);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = posts;
|
||||||
|
if (search) {
|
||||||
|
filtered = filtered.filter((posting) =>
|
||||||
|
posting.JudulPost.toLowerCase().includes(search.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
setFilteredPosting(filtered);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [posts, search]);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(filteredPosting.length / ITEMS_PER_PAGE);
|
||||||
|
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||||
|
const endIndex = startIndex + ITEMS_PER_PAGE;
|
||||||
|
const currentItems = filteredPosting.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = (PostId: number) => {
|
||||||
|
if (confirm("Apakah Anda yakin ingin menghapus postingan ini?")) {
|
||||||
|
destroy(`/admin/post/${PostId}`, {
|
||||||
|
onSuccess: () =>
|
||||||
|
toast({
|
||||||
|
title: "Berhasil",
|
||||||
|
description: "Post berhasil dihapus",
|
||||||
|
variant: "default",
|
||||||
|
}),
|
||||||
|
onError: () =>
|
||||||
|
toast({
|
||||||
|
title: "Gagal",
|
||||||
|
description: "Terjadi kesalahan saat menghapus Post",
|
||||||
|
variant: "destructive",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticatedLayout header="Daftar Postingan">
|
||||||
|
<Head title="Daftar Postingan" />
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader className="flex flex-row justify-between items-center">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Cari Post..."
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="w-96 border-gray-200 rounded-lg"
|
||||||
|
/>
|
||||||
|
<Link href="/admin/post/add">
|
||||||
|
<Button className="bg-blue-600 text-white flex items-center gap-2">
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Tambah Post
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-gray-50">
|
||||||
|
<TableHead>No</TableHead>
|
||||||
|
<TableHead>Judul</TableHead>
|
||||||
|
<TableHead>Kategori</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead>Aksi</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{currentItems.length > 0 ? (
|
||||||
|
currentItems.map((posting, index) => (
|
||||||
|
<TableRow key={posting.PostId}>
|
||||||
|
<TableCell>
|
||||||
|
{startIndex + index + 1}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{posting.JudulPost}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{posting.kategori?.NamaKategori}{" "}
|
||||||
|
|{" "}
|
||||||
|
{
|
||||||
|
posting.subkategori
|
||||||
|
?.NamaSubKategori
|
||||||
|
}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{posting.IsPublish
|
||||||
|
? "Published"
|
||||||
|
: "Draft"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex gap-2">
|
||||||
|
<Link
|
||||||
|
href={`/admin/post/edit/${posting.PostId}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
handleDelete(
|
||||||
|
posting.PostId!
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="destructive"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-center py-4 text-gray-500"
|
||||||
|
>
|
||||||
|
Tidak ada data
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Showing {startIndex + 1} to{" "}
|
||||||
|
{Math.min(endIndex, filteredPosting.length)} of{" "}
|
||||||
|
{filteredPosting.length} entries
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(currentPage - 1)
|
||||||
|
}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{Array.from(
|
||||||
|
{ length: totalPages },
|
||||||
|
(_, i) => i + 1
|
||||||
|
).map((page) => (
|
||||||
|
<Button
|
||||||
|
key={page}
|
||||||
|
variant={
|
||||||
|
currentPage === page
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(page)}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(currentPage + 1)
|
||||||
|
}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<Toaster />
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,552 @@
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { useForm } from "@inertiajs/react";
|
||||||
|
import { PageProps } from "@/types";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
// import { toast } from "react-toastify";
|
||||||
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import {
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
Pencil,
|
||||||
|
Trash2,
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import AuthenticatedLayout from "@/layouts/authenticated-layout";
|
||||||
|
import { Head } from "@inertiajs/react";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
|
||||||
|
interface Kategori {
|
||||||
|
KategoriId: number;
|
||||||
|
NamaKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SubKategori {
|
||||||
|
SubKategoriId: number | null;
|
||||||
|
KategoriId: number;
|
||||||
|
NamaSubKategori: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ITEMS_PER_PAGE = 5;
|
||||||
|
|
||||||
|
export default function SubKategoriIndex({
|
||||||
|
subkategori = [],
|
||||||
|
kategori = [],
|
||||||
|
}: PageProps<{ subkategori: SubKategori[]; kategori: Kategori[] }>) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
post,
|
||||||
|
put,
|
||||||
|
delete: destroy,
|
||||||
|
reset,
|
||||||
|
} = useForm<SubKategori>({
|
||||||
|
SubKategoriId: null,
|
||||||
|
KategoriId: kategori.length > 0 ? kategori[0].KategoriId : 0,
|
||||||
|
NamaSubKategori: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [deleteConfirm, setDeleteConfirm] = useState<SubKategori | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const [filteredKategori, setFilteredKategori] = useState(subkategori);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let filtered = subkategori;
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
filtered = filtered.filter((subkategori) =>
|
||||||
|
subkategori.NamaSubKategori.toLowerCase().includes(
|
||||||
|
search.toLowerCase()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFilteredKategori(filtered);
|
||||||
|
setCurrentPage(1);
|
||||||
|
}, [kategori, search]);
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(
|
||||||
|
(filteredKategori?.length || 0) / ITEMS_PER_PAGE
|
||||||
|
);
|
||||||
|
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
|
||||||
|
const endIndex = startIndex + ITEMS_PER_PAGE;
|
||||||
|
const currentItems = filteredKategori.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setSearch(e.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (editing) {
|
||||||
|
put(`/admin/subkategori/${data.SubKategoriId}`, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Berhasil",
|
||||||
|
description: "Sub Kategori berhasil diperbarui",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setIsModalOpen(false);
|
||||||
|
reset();
|
||||||
|
setEditing(false);
|
||||||
|
setData({
|
||||||
|
SubKategoriId: null,
|
||||||
|
NamaSubKategori: "",
|
||||||
|
KategoriId: kategori[0]?.KategoriId || 0,
|
||||||
|
});
|
||||||
|
setEditing(false);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Gagal",
|
||||||
|
description:
|
||||||
|
"Terjadi kesalahan saat memperbarui Sub Kategori",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
post("/admin/subkategori", {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Berhasil",
|
||||||
|
description: "Sub Kategori berhasil dibuat",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setIsModalOpen(false);
|
||||||
|
setData({
|
||||||
|
SubKategoriId: null,
|
||||||
|
NamaSubKategori: "",
|
||||||
|
KategoriId: kategori[0]?.KategoriId || 0,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Gagal",
|
||||||
|
description:
|
||||||
|
"Terjadi kesalahan saat membuat Sub Kategori",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEdit = (subkategori: SubKategori) => {
|
||||||
|
setData({ ...subkategori });
|
||||||
|
setEditing(true);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (deleteConfirm) {
|
||||||
|
destroy(`/admin/subkategori/${deleteConfirm.SubKategoriId}`, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast({
|
||||||
|
title: "Berhasil",
|
||||||
|
description: "Sub Kategori berhasil dihapus",
|
||||||
|
variant: "default",
|
||||||
|
});
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
toast({
|
||||||
|
title: "Gagal",
|
||||||
|
description:
|
||||||
|
"Terjadi kesalahan saat menghapus Sub Kategori",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
setDeleteConfirm(null);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<AuthenticatedLayout header={"Sub Kategori Post"}>
|
||||||
|
<Head title="Sub Kategori Post" />
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<Card className="shadow-lg">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||||
|
<div className="relative w-full md:w-96">
|
||||||
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 h-4 w-4" />
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Cari Sub Kategori..."
|
||||||
|
value={search}
|
||||||
|
onChange={handleSearch}
|
||||||
|
className="pl-10 pr-4 w-full border-gray-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Dialog
|
||||||
|
open={isModalOpen}
|
||||||
|
onOpenChange={setIsModalOpen}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(false);
|
||||||
|
reset();
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}}
|
||||||
|
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-4 py-2 rounded-lg flex items-center gap-2 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
Buat Sub Kategori
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-semibold">
|
||||||
|
{editing
|
||||||
|
? "Ubah Sub Kategori"
|
||||||
|
: "Buat Sub Kategori"}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex flex-col gap-4 mt-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Kategori
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={data.KategoriId.toString()}
|
||||||
|
onValueChange={(
|
||||||
|
value
|
||||||
|
) =>
|
||||||
|
setData(
|
||||||
|
"KategoriId",
|
||||||
|
parseInt(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Pilih Kategori" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{kategori.length >
|
||||||
|
0 ? (
|
||||||
|
kategori.map(
|
||||||
|
(
|
||||||
|
kategori
|
||||||
|
) => (
|
||||||
|
<SelectItem
|
||||||
|
key={
|
||||||
|
kategori.KategoriId
|
||||||
|
}
|
||||||
|
value={kategori.KategoriId.toString()}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
kategori.NamaKategori
|
||||||
|
}
|
||||||
|
</SelectItem>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="p-2 text-gray-500">
|
||||||
|
Tidak ada
|
||||||
|
kategori
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Sub Kategori
|
||||||
|
</label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Masukkan Nama Sub Kategori"
|
||||||
|
value={data.NamaSubKategori}
|
||||||
|
onChange={(e) =>
|
||||||
|
setData(
|
||||||
|
"NamaSubKategori",
|
||||||
|
e.target.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
{/* <div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium text-gray-700">
|
||||||
|
Publish
|
||||||
|
</label>
|
||||||
|
<Select
|
||||||
|
value={
|
||||||
|
data.is_active
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
}
|
||||||
|
onValueChange={(
|
||||||
|
value
|
||||||
|
) =>
|
||||||
|
setData(
|
||||||
|
"is_active",
|
||||||
|
value === "true"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select status" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="true">
|
||||||
|
Published
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="false">
|
||||||
|
Unpublished
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="mt-6">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
|
>
|
||||||
|
{editing
|
||||||
|
? "Perbarui"
|
||||||
|
: "Buat"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="rounded-lg border border-gray-200 overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-gray-50 dark:bg-black/50">
|
||||||
|
{/* <TableHead className="font-semibold">
|
||||||
|
ID
|
||||||
|
</TableHead> */}
|
||||||
|
<TableHead className="font-semibold">
|
||||||
|
No
|
||||||
|
</TableHead>
|
||||||
|
<TableHead>Kategori</TableHead>
|
||||||
|
<TableHead className="font-semibold">
|
||||||
|
Sub Kategori
|
||||||
|
</TableHead>
|
||||||
|
{/* <TableHead className="font-semibold">
|
||||||
|
Status
|
||||||
|
</TableHead> */}
|
||||||
|
<TableHead className="font-semibold">
|
||||||
|
Actions
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{currentItems.map((subkategori, index) => (
|
||||||
|
<TableRow
|
||||||
|
key={
|
||||||
|
subkategori.SubKategoriId ||
|
||||||
|
"new"
|
||||||
|
}
|
||||||
|
className="hover:bg-gray-50 transition-colors duration-150"
|
||||||
|
>
|
||||||
|
{/* <TableCell>{subkategori.id}</TableCell> */}
|
||||||
|
<TableCell>
|
||||||
|
{startIndex + index + 1}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{kategori.find(
|
||||||
|
(c) =>
|
||||||
|
c.KategoriId ===
|
||||||
|
subkategori.KategoriId
|
||||||
|
)?.NamaKategori || "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{subkategori.NamaSubKategori}
|
||||||
|
</TableCell>
|
||||||
|
{/* <TableCell>
|
||||||
|
{subkategori.is_active
|
||||||
|
? "Published"
|
||||||
|
: "Unpublished"}
|
||||||
|
</TableCell> */}
|
||||||
|
<TableCell className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
handleEdit(subkategori)
|
||||||
|
}
|
||||||
|
variant="outline"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
setDeleteConfirm(
|
||||||
|
subkategori
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="destructive"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
{currentItems.length === 0 && (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell
|
||||||
|
colSpan={4}
|
||||||
|
className="text-center py-4 text-gray-500"
|
||||||
|
>
|
||||||
|
Tidak ada data
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
{/* <div className="flex flex-row mt-3">
|
||||||
|
<div className="text-sm w-1/2">
|
||||||
|
1 : Pengumuman <br /> 2 : Undangan
|
||||||
|
</div>
|
||||||
|
<div className="text-sm w-1/2">
|
||||||
|
3 : Peraturan <br /> 4 : Popup Home
|
||||||
|
</div>
|
||||||
|
</div> */}
|
||||||
|
<div className="flex items-center justify-between mt-4">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
Showing {startIndex + 1} to{" "}
|
||||||
|
{Math.min(endIndex, filteredKategori.length)} of{" "}
|
||||||
|
{filteredKategori.length} entries
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(currentPage - 1)
|
||||||
|
}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{Array.from(
|
||||||
|
{ length: totalPages },
|
||||||
|
(_, i) => i + 1
|
||||||
|
).map((page) => (
|
||||||
|
<Button
|
||||||
|
key={page}
|
||||||
|
variant={
|
||||||
|
currentPage === page
|
||||||
|
? "default"
|
||||||
|
: "outline"
|
||||||
|
}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(page)}
|
||||||
|
>
|
||||||
|
{page}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
handlePageChange(currentPage + 1)
|
||||||
|
}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{deleteConfirm && (
|
||||||
|
<Dialog open={true} onOpenChange={() => setDeleteConfirm(null)}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-xl font-semibold">
|
||||||
|
Konfirmasi Penghapusan
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="py-4">
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Apakah Anda yakin ingin menghapus "
|
||||||
|
<span className="font-medium">
|
||||||
|
{deleteConfirm.NamaSubKategori}
|
||||||
|
</span>
|
||||||
|
"?
|
||||||
|
<br />
|
||||||
|
<span className="text-red-500">
|
||||||
|
Tindakan ini tidak dapat dibatalkan.
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => setDeleteConfirm(null)}
|
||||||
|
variant="outline"
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleDelete}
|
||||||
|
variant="destructive"
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
Ya, hapus
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
<Toaster />
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -14,7 +14,7 @@ export default function Login({
|
||||||
canResetPassword: boolean;
|
canResetPassword: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { data, setData, post, processing, errors, reset } = useForm({
|
const { data, setData, post, processing, errors, reset } = useForm({
|
||||||
email: "",
|
login: "",
|
||||||
password: "",
|
password: "",
|
||||||
remember: false,
|
remember: false,
|
||||||
});
|
});
|
||||||
|
@ -73,19 +73,19 @@ export default function Login({
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="text"
|
||||||
placeholder="m@example.com"
|
placeholder="m@example.com/username"
|
||||||
value={data.email}
|
value={data.login}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setData(
|
setData(
|
||||||
"email",
|
"login",
|
||||||
e.target.value
|
e.target.value
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
<InputError
|
<InputError
|
||||||
message={errors.email}
|
message={errors.login}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import { useForm } from "@inertiajs/react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
TableHead,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
} from "@/components/ui/table";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTrigger,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogFooter,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Plus, Pencil, Trash2 } from "lucide-react";
|
||||||
|
import AuthenticatedLayout from "@/layouts/authenticated-layout";
|
||||||
|
import { Head } from "@inertiajs/react";
|
||||||
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
|
||||||
|
interface Menu {
|
||||||
|
id: number | null;
|
||||||
|
name: string;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Index({ menus }: { menus: Menu[] }) {
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
setData,
|
||||||
|
post,
|
||||||
|
put,
|
||||||
|
delete: destroy,
|
||||||
|
reset,
|
||||||
|
} = useForm<Menu>({
|
||||||
|
id: null,
|
||||||
|
name: "",
|
||||||
|
is_active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthenticatedLayout header="Manajemen Menu">
|
||||||
|
<Head title="Manajemen Menu" />
|
||||||
|
<div className="container mx-auto p-4">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(false);
|
||||||
|
reset();
|
||||||
|
setIsModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus className="h-4 w-4" /> Tambah Menu
|
||||||
|
</Button>
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Nama Menu</TableHead>
|
||||||
|
<TableHead>Aksi</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{menus.map((menu) => (
|
||||||
|
<TableRow key={menu.id}>
|
||||||
|
<TableCell>{menu.name}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setEditing(true);
|
||||||
|
setIsModalOpen(true);
|
||||||
|
setData(menu);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pencil className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
destroy(`/menus/${menu.id}`)
|
||||||
|
}
|
||||||
|
variant="destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<Toaster />
|
||||||
|
</AuthenticatedLayout>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,14 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\CategoryController;
|
||||||
|
use App\Http\Controllers\KategoriController;
|
||||||
|
use App\Http\Controllers\MenuController;
|
||||||
|
use App\Http\Controllers\PostController;
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
|
use App\Http\Controllers\RefCategoryController;
|
||||||
|
use App\Http\Controllers\RefSubCategoryController;
|
||||||
|
use App\Http\Controllers\SubCategoryController;
|
||||||
|
use App\Http\Controllers\SubKategoriController;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
@ -39,6 +47,8 @@ Route::get('/peraturan', function () {
|
||||||
return Inertia::render('Peraturan');
|
return Inertia::render('Peraturan');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dashboard Route
|
||||||
|
|
||||||
Route::get('/dashboard', function () {
|
Route::get('/dashboard', function () {
|
||||||
return Inertia::render('dashboard');
|
return Inertia::render('dashboard');
|
||||||
})->middleware(['auth'])->name('dashboard');
|
})->middleware(['auth'])->name('dashboard');
|
||||||
|
@ -49,4 +59,30 @@ Route::middleware(['auth'])->group(function () {
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::middleware(['auth'])->prefix('admin')->group(function () {
|
||||||
|
Route::get('/kategori', [KategoriController::class, 'index'])->name('admin.kategori.index');
|
||||||
|
Route::post('/kategori', [KategoriController::class, 'store'])->name('admin.kategori.store');
|
||||||
|
Route::put('/kategori/{kategori}', [KategoriController::class, 'update'])->name('admin.kategori.update');
|
||||||
|
Route::delete('/kategori/{kategori}', [KategoriController::class, 'destroy'])->name('admin.kategori.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware(['auth'])->prefix('admin')->group(function () {
|
||||||
|
Route::get('/subkategori', [SubKategoriController::class, 'index'])->name('admin.subkategori.index');
|
||||||
|
Route::post('/subkategori', [SubKategoriController::class, 'store'])->name('admin.subkategori.store');
|
||||||
|
Route::put('/subkategori/{subkategori}', [SubKategoriController::class, 'update'])->name('admin.subkategori.update');
|
||||||
|
Route::delete('/subkategori/{subkategori}', [SubKategoriController::class, 'destroy'])->name('admin.subkategori.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
Route::middleware(['auth'])->prefix('admin')->group(function () {
|
||||||
|
Route::get('/post', [PostController::class, 'index'])->name('admin.post.index');
|
||||||
|
Route::get('/post/add', [PostController::class, 'create'])->name('admin.post.create');
|
||||||
|
Route::post('/post', [PostController::class, 'store'])->name('admin.post.store');
|
||||||
|
Route::get('/post/edit/{post}', [PostController::class, 'edit'])->name('admin.post.edit');
|
||||||
|
Route::put('/post/{post}', [PostController::class, 'update'])->name('admin.post.update');
|
||||||
|
Route::delete('/post/{post}', [PostController::class, 'destroy'])->name('admin.post.destroy');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|
Loading…
Reference in New Issue