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();
|
||||
|
||||
// Redirect berdasarkan role user
|
||||
$user = Auth::user();
|
||||
|
||||
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
|
||||
{
|
||||
return [
|
||||
'email' => ['required', 'string', 'email'],
|
||||
'login' => ['required', 'string'],
|
||||
'password' => ['required', 'string'],
|
||||
];
|
||||
}
|
||||
|
@ -41,11 +41,13 @@ class LoginRequest extends FormRequest
|
|||
{
|
||||
$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());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.failed'),
|
||||
'login' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -68,7 +70,7 @@ class LoginRequest extends FormRequest
|
|||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'email' => trans('auth.throttle', [
|
||||
'login' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
|
@ -80,6 +82,6 @@ class LoginRequest extends FormRequest
|
|||
*/
|
||||
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\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable /*implements MustVerifyEmail*/
|
||||
{
|
||||
use HasFactory, Notifiable;
|
||||
use HasFactory, Notifiable, HasRoles;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
@ -18,6 +19,7 @@ class User extends Authenticatable /*implements MustVerifyEmail*/
|
|||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'username',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Providers;
|
|||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
|
@ -11,7 +12,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
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
|
||||
|
||||
use App\Http\Middleware\HandleInertiaRequests;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
|
@ -14,9 +15,14 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||
$middleware->web(append: [
|
||||
\App\Http\Middleware\HandleInertiaRequests::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) {
|
||||
//
|
||||
|
|
|
@ -2,4 +2,5 @@
|
|||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\RepositoryServiceProvider::class,
|
||||
];
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
"laravel/framework": "^11.0",
|
||||
"laravel/sanctum": "^4.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"spatie/laravel-permission": "6.4.0",
|
||||
"tightenco/ziggy": "^2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "86d4a008177912b9459f27068d5f4e1d",
|
||||
"content-hash": "953cea45b540b8469800ed81d3f1fba6",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
@ -3254,6 +3254,88 @@
|
|||
],
|
||||
"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",
|
||||
"version": "v7.1.1",
|
||||
|
@ -8410,5 +8492,5 @@
|
|||
"php": "^8.2"
|
||||
},
|
||||
"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
|
||||
{
|
||||
// User::factory(10)->create();
|
||||
|
||||
User::factory()->create([
|
||||
'name' => 'Test User',
|
||||
'email' => 'test@example.com',
|
||||
$this->call([
|
||||
RolesTableSeeder::class,
|
||||
UserSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
{
|
||||
User::create([
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@gmail.com',
|
||||
'password' => Hash::make('password')
|
||||
$admin = User::create([
|
||||
'name' => 'Admin',
|
||||
'username' => 'admindlh25',
|
||||
'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-avatar": "^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-label": "^2.0.2",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.4",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
"@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-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.6",
|
||||
"@radix-ui/react-tooltip": "^1.1.3",
|
||||
"@reduxjs/toolkit": "^2.5.1",
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"apexcharts": "^4.4.0",
|
||||
"ckeditor4-react": "^4.1.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^3.6.0",
|
||||
|
@ -56,6 +59,7 @@
|
|||
"react-i18next": "^15.4.0",
|
||||
"react-perfect-scrollbar": "^1.5.8",
|
||||
"react-popper": "^2.3.0",
|
||||
"react-quill": "^2.0.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resizable-panels": "^2.0.19",
|
||||
"react-router-dom": "^7.1.4",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import "./bootstrap";
|
||||
import "../css/app.css";
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { createInertiaApp } from "@inertiajs/react";
|
||||
|
@ -6,7 +7,6 @@ import { resolvePageComponent } from "laravel-vite-plugin/inertia-helpers";
|
|||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || "Laravel";
|
||||
|
||||
createInertiaApp({
|
||||
title: (title) => `${title} - ${appName}`,
|
||||
resolve: (name) =>
|
||||
|
@ -14,6 +14,7 @@ createInertiaApp({
|
|||
`./pages/${name}.tsx`,
|
||||
import.meta.glob("./pages/**/*.tsx")
|
||||
),
|
||||
|
||||
setup({ el, App, props }) {
|
||||
const root = createRoot(el);
|
||||
|
||||
|
|
|
@ -3,102 +3,139 @@ import { Button } from "@/components/ui/button";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Link } from "@inertiajs/react";
|
||||
|
||||
const pengumumans = [
|
||||
{
|
||||
id: 1,
|
||||
title_page: "Pengumuman",
|
||||
alt_image: "Pengumuman",
|
||||
date: "16 Januari 2025",
|
||||
title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
|
||||
description:
|
||||
"Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
|
||||
image: "/assets/img1.jpg",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title_page: "Pengumuman",
|
||||
alt_image: "Pengumuman",
|
||||
date: "12 Desember 2024",
|
||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
description:
|
||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
image: "/assets/img1.jpg",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title_page: "Pengumuman",
|
||||
alt_image: "Pengumuman",
|
||||
date: "12 Desember 2024",
|
||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
description:
|
||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
image: "/assets/img1.jpg",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title_page: "Pengumuman",
|
||||
alt_image: "Pengumuman",
|
||||
date: "12 Desember 2024",
|
||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
description:
|
||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
image: "/assets/img1.jpg",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title_page: "Pengumuman",
|
||||
alt_image: "Pengumuman",
|
||||
date: "12 Desember 2024",
|
||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
description:
|
||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
image: "/assets/img1.jpg",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title_page: "Pengumuman",
|
||||
alt_image: "Pengumuman",
|
||||
date: "12 Desember 2024",
|
||||
title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
description:
|
||||
"Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
image: "/assets/img1.jpg",
|
||||
},
|
||||
];
|
||||
// const pengumumans = [
|
||||
// {
|
||||
// id: 1,
|
||||
// title_page: "Pengumuman",
|
||||
// alt_image: "Pengumuman",
|
||||
// date: "16 Januari 2025",
|
||||
// title: "Pelatihan & Sertifikasi Online Bidang Pengendalian Pencemaran Air Dan Udara",
|
||||
// description:
|
||||
// "Kegiatan Pelatihan Dan Uji Sertifikasi Tersebut Akan Diselenggarakan Sebagaimana Jadwal Terlampir, Kegiatan Tersebut Bekerjasama Dengan Lembaga P...",
|
||||
// image: "/assets/img1.jpg",
|
||||
// },
|
||||
// {
|
||||
// id: 2,
|
||||
// title_page: "Pengumuman",
|
||||
// alt_image: "Pengumuman",
|
||||
// date: "12 Desember 2024",
|
||||
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
// description:
|
||||
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
// image: "/assets/img1.jpg",
|
||||
// },
|
||||
// {
|
||||
// id: 3,
|
||||
// title_page: "Pengumuman",
|
||||
// alt_image: "Pengumuman",
|
||||
// date: "12 Desember 2024",
|
||||
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
// description:
|
||||
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
// image: "/assets/img1.jpg",
|
||||
// },
|
||||
// {
|
||||
// id: 4,
|
||||
// title_page: "Pengumuman",
|
||||
// alt_image: "Pengumuman",
|
||||
// date: "12 Desember 2024",
|
||||
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
// description:
|
||||
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
// image: "/assets/img1.jpg",
|
||||
// },
|
||||
// {
|
||||
// id: 5,
|
||||
// title_page: "Pengumuman",
|
||||
// alt_image: "Pengumuman",
|
||||
// date: "12 Desember 2024",
|
||||
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
// description:
|
||||
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
// image: "/assets/img1.jpg",
|
||||
// },
|
||||
// {
|
||||
// id: 6,
|
||||
// title_page: "Pengumuman",
|
||||
// alt_image: "Pengumuman",
|
||||
// date: "12 Desember 2024",
|
||||
// title: "Pembinaan Pelaporan secara Online Pengelolaan Lingkungan melalui situs Status Ketaatan Lingkungan (SKL)",
|
||||
// description:
|
||||
// "Sukolompok Pengawasan Lingkungan Bidang Pengawasan dan Penataan Hukum Dinas Lingkungan Hidup Prov. DKI Jakarta...",
|
||||
// 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 (
|
||||
<section className="container max-w-7xl py-8 px-6">
|
||||
{/* List of Announcements */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{pengumumans.map((item) => (
|
||||
<Card key={item.id} className="md:p-4">
|
||||
{posts.map((post) => (
|
||||
<Card key={post.PostId} className="md:p-4">
|
||||
<img
|
||||
src={item.image}
|
||||
alt={item.alt_image}
|
||||
src={`/storage/${post.ImagePost}`}
|
||||
alt={post.JudulPost}
|
||||
className="rounded-md"
|
||||
/>
|
||||
<CardContent className="p-4">
|
||||
<Badge className="bg-red-600 text-white">
|
||||
{item.title_page}
|
||||
{post.kategori?.NamaKategori} |{" "}
|
||||
{post.subkategori?.NamaSubKategori}
|
||||
</Badge>
|
||||
<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>
|
||||
<h3 className="text-md font-semibold mt-2 text-gray-900">
|
||||
{item.title}
|
||||
{post.JudulPost}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 mt-2">
|
||||
{item.description}
|
||||
{post.DescPost.replace(/<[^>]*>/g, "")}
|
||||
</p>
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-red-600 mt-2 pl-0"
|
||||
>
|
||||
Baca Selengkapnya{" "}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
<Link href={`/post/${post.SlugPost}`}>
|
||||
<Button
|
||||
variant="link"
|
||||
className="text-red-600 mt-2 pl-0"
|
||||
>
|
||||
Baca Selengkapnya{" "}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
} from "@/components/ui/drawer";
|
||||
import RunningText from "./RunningText";
|
||||
import { useTheme } from "next-themes";
|
||||
import SearchDialog from "./SearchDialog";
|
||||
|
||||
interface NavItemsProps {
|
||||
mobile?: boolean;
|
||||
|
@ -26,7 +27,7 @@ interface NavItemsProps {
|
|||
const Navbar = () => {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isDrawerOpen, setIsDrawerOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
// const [searchQuery, setSearchQuery] = useState("");
|
||||
const { theme, setTheme } = useTheme();
|
||||
|
||||
// Handle scroll effect
|
||||
|
@ -44,14 +45,6 @@ const Navbar = () => {
|
|||
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 }) => (
|
||||
<>
|
||||
<NavigationMenuItem>
|
||||
|
@ -82,7 +75,7 @@ const Navbar = () => {
|
|||
</Link>
|
||||
</NavigationMenuItem>
|
||||
))}
|
||||
<NavigationMenuItem>
|
||||
{/* <NavigationMenuItem>
|
||||
<form onSubmit={handleSearch} className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
|
@ -101,6 +94,9 @@ const Navbar = () => {
|
|||
<Search className="h-4 w-4 text-white" />
|
||||
</Button>
|
||||
</form>
|
||||
</NavigationMenuItem> */}
|
||||
<NavigationMenuItem>
|
||||
<SearchDialog />
|
||||
</NavigationMenuItem>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// PopupModal.js
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { X, ArrowLeft, ArrowRight } from "lucide-react";
|
||||
|
@ -15,7 +14,7 @@ const PopupModal = ({ onClose }: { onClose: () => void }) => {
|
|||
title: "Workshop Peningkatan Kapasitas Pengendalian Pencemaran Udara",
|
||||
image: "/assets/popup-2.jpeg",
|
||||
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",
|
||||
|
@ -36,30 +35,39 @@ const PopupModal = ({ onClose }: { onClose: () => void }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-50">
|
||||
<div className="bg-white p-6 rounded-lg max-w-lg w-full relative">
|
||||
<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 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}>
|
||||
<X className="w-6 h-6 text-gray-700" />
|
||||
</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}
|
||||
</h2>
|
||||
<img
|
||||
src={slides[currentSlide].image}
|
||||
alt="Popup Slide Image"
|
||||
className="w-full h-auto mb-4"
|
||||
/>
|
||||
<p className="text-sm text-gray-600 text-center mb-4">
|
||||
{slides[currentSlide].description}
|
||||
</p>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<button onClick={prevSlide} className="p-2">
|
||||
<div className="relative">
|
||||
<img
|
||||
src={slides[currentSlide].image}
|
||||
alt="Popup Slide Image"
|
||||
className="w-full h-full md:h-96 object-contain mb-4"
|
||||
/>
|
||||
<button
|
||||
onClick={prevSlide}
|
||||
className="absolute left-2 top-1/2 transform -translate-y-1/2 p-2 bg-white bg-opacity-50 rounded-full"
|
||||
>
|
||||
<ArrowLeft className="w-6 h-6 text-gray-700" />
|
||||
</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" />
|
||||
</button>
|
||||
</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">
|
||||
<Button className="bg-green-800 text-white px-6 py-2 rounded-lg">
|
||||
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 {
|
||||
Command,
|
||||
FolderArchive,
|
||||
Frame,
|
||||
Home,
|
||||
LifeBuoy,
|
||||
|
@ -38,20 +39,28 @@ const data = {
|
|||
icon: Home,
|
||||
},
|
||||
{
|
||||
title: "Projects",
|
||||
title: "Data Master",
|
||||
url: "#",
|
||||
icon: SquareTerminal,
|
||||
icon: FolderArchive,
|
||||
isActive: true,
|
||||
items: [
|
||||
{
|
||||
title: "History",
|
||||
url: "#",
|
||||
title: "Kategori Post",
|
||||
url: "/admin/kategori",
|
||||
},
|
||||
{
|
||||
title: "Sub Kategori Post",
|
||||
url: "/admin/subkategori",
|
||||
},
|
||||
// {
|
||||
// title: "Menu",
|
||||
// url: "/menus",
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Design Engineering",
|
||||
url: "#",
|
||||
title: "Post",
|
||||
url: "/admin/post",
|
||||
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" />
|
||||
<Navbar />
|
||||
<HeroSecond title="Pengumuman" />
|
||||
<CardPengumuman />
|
||||
<CardPengumuman posts={[]} />
|
||||
<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;
|
||||
}) {
|
||||
const { data, setData, post, processing, errors, reset } = useForm({
|
||||
email: "",
|
||||
login: "",
|
||||
password: "",
|
||||
remember: false,
|
||||
});
|
||||
|
@ -73,19 +73,19 @@ export default function Login({
|
|||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="m@example.com"
|
||||
value={data.email}
|
||||
type="text"
|
||||
placeholder="m@example.com/username"
|
||||
value={data.login}
|
||||
onChange={(e) =>
|
||||
setData(
|
||||
"email",
|
||||
"login",
|
||||
e.target.value
|
||||
)
|
||||
}
|
||||
required
|
||||
/>
|
||||
<InputError
|
||||
message={errors.email}
|
||||
message={errors.login}
|
||||
/>
|
||||
</div>
|
||||
<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
|
||||
|
||||
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\RefCategoryController;
|
||||
use App\Http\Controllers\RefSubCategoryController;
|
||||
use App\Http\Controllers\SubCategoryController;
|
||||
use App\Http\Controllers\SubKategoriController;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Inertia\Inertia;
|
||||
|
@ -39,6 +47,8 @@ Route::get('/peraturan', function () {
|
|||
return Inertia::render('Peraturan');
|
||||
});
|
||||
|
||||
// Dashboard Route
|
||||
|
||||
Route::get('/dashboard', function () {
|
||||
return Inertia::render('dashboard');
|
||||
})->middleware(['auth'])->name('dashboard');
|
||||
|
@ -49,4 +59,30 @@ Route::middleware(['auth'])->group(function () {
|
|||
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';
|
||||
|
|
Loading…
Reference in New Issue