feat(article): implementasi crud
-membuat permission di seeder -membuat enum untuk status
This commit is contained in:
parent
7dca0c9231
commit
8b8ce3077b
33
app/Enums/ArticleStatus.php
Normal file
33
app/Enums/ArticleStatus.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use App\Traits\WithCommentEnum;
|
||||
use App\Traits\WithValueEnum;
|
||||
|
||||
enum ArticleStatus: int
|
||||
{
|
||||
use WithCommentEnum, WithValueEnum;
|
||||
|
||||
case PUBLISHED = 1;
|
||||
case DRAFT = 2;
|
||||
case ARCHIVED = 3;
|
||||
|
||||
public function label()
|
||||
{
|
||||
return match ($this) {
|
||||
self::PUBLISHED => 'Publish',
|
||||
self::DRAFT => 'Draft',
|
||||
self::ARCHIVED => 'Arsip',
|
||||
};
|
||||
}
|
||||
|
||||
public function color()
|
||||
{
|
||||
return match ($this) {
|
||||
self::PUBLISHED => 'emerald',
|
||||
self::DRAFT => 'yellow',
|
||||
self::ARCHIVED => 'rose',
|
||||
};
|
||||
}
|
||||
}
|
||||
61
app/Livewire/Datatable/Studio/Manage/ArticlesTable.php
Normal file
61
app/Livewire/Datatable/Studio/Manage/ArticlesTable.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Datatable\Studio\Manage;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Traits\Datatable\WithConfiguration;
|
||||
use App\Traits\Datatable\WithPrependColumn;
|
||||
use App\Traits\WithMediaHandler;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Rappasoft\LaravelLivewireTables\DataTableComponent;
|
||||
use Rappasoft\LaravelLivewireTables\Views\Column;
|
||||
|
||||
class ArticlesTable extends DataTableComponent
|
||||
{
|
||||
use WithConfiguration, WithMediaHandler, WithPrependColumn;
|
||||
|
||||
protected $model = Article::class;
|
||||
|
||||
public function columns(): array
|
||||
{
|
||||
return [
|
||||
Column::make('Penulis', 'author.employee.full_name')->searchable(),
|
||||
|
||||
Column::make('Judul', 'title')->searchable(),
|
||||
|
||||
Column::make('Status', 'status')
|
||||
->format(
|
||||
fn($value) => Blade::render('<flux:badge color="' . $value->color() . '">' . $value->label() . '</flux:badge>')
|
||||
)
|
||||
->html(),
|
||||
|
||||
Column::make('Aksi')
|
||||
->label(function ($row) {
|
||||
$actions = '';
|
||||
|
||||
if (auth()->user()->can('update article')) {
|
||||
$actions .= view('components.datatables.edit', [
|
||||
'id' => $row->hash,
|
||||
'editRoute' => route('studio.manage.article.edit', $row->hash),
|
||||
])->render();
|
||||
}
|
||||
|
||||
if (auth()->user()->can('delete article')) {
|
||||
$actions .= view('components.datatables.delete', [
|
||||
'id' => $row->hash,
|
||||
'deleteRoute' => route('studio.manage.article.delete', $row->hash),
|
||||
])->render();
|
||||
}
|
||||
|
||||
return $actions;
|
||||
})
|
||||
->html(),
|
||||
];
|
||||
}
|
||||
|
||||
public function builder(): Builder
|
||||
{
|
||||
return Article::select('articles.id', 'title', 'articles.status')->with(['author', 'author.employee']);
|
||||
}
|
||||
}
|
||||
98
app/Livewire/Forms/Studio/Manage/ArticleForm.php
Normal file
98
app/Livewire/Forms/Studio/Manage/ArticleForm.php
Normal file
@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms\Studio\Manage;
|
||||
|
||||
use App\Enums\ArticleStatus;
|
||||
use App\Models\Article;
|
||||
use App\Traits\WithMediaHandler;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Livewire\Form;
|
||||
|
||||
class ArticleForm extends Form
|
||||
{
|
||||
use WithMediaHandler;
|
||||
|
||||
public ?Article $article = null;
|
||||
|
||||
public string $title = '';
|
||||
|
||||
public string $excerpt = '';
|
||||
|
||||
public string $content = '';
|
||||
|
||||
public string $status = '';
|
||||
|
||||
public array $thumbnail = [];
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'title' => ['required', 'string', 'max:200'],
|
||||
'excerpt' => ['required', 'string'],
|
||||
'content' => ['required', 'string'],
|
||||
'status' => ['required', Rule::in(ArticleStatus::cases())],
|
||||
'thumbnail' => ['required', 'array'],
|
||||
];
|
||||
}
|
||||
|
||||
public function validationAttributes(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'judul',
|
||||
'excerpt' => 'cuplikan',
|
||||
'content' => 'konten',
|
||||
];
|
||||
}
|
||||
|
||||
public function setArticle(Article $article)
|
||||
{
|
||||
$this->article = $article;
|
||||
|
||||
$this->title = $article->title;
|
||||
$this->excerpt = $article->excerpt;
|
||||
$this->content = $article->content;
|
||||
$this->status = $article->status->value;
|
||||
$this->thumbnail = $this->mapMediaCollection($article->getMedia('thumbnail'));
|
||||
}
|
||||
|
||||
public function store()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function () {
|
||||
$article = Article::create([
|
||||
'author_id' => auth()->id(),
|
||||
'title' => $this->title,
|
||||
'excerpt' => $this->excerpt,
|
||||
'content' => $this->content,
|
||||
'status' => $this->status,
|
||||
'published_at' => $this->status == ArticleStatus::PUBLISHED->value ? now() : null,
|
||||
]);
|
||||
|
||||
$this->uploadMedia($this->thumbnail, $article, 'thumbnail');
|
||||
});
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
$this->validate();
|
||||
|
||||
DB::transaction(function () {
|
||||
$this->article->update(array_merge(
|
||||
[
|
||||
'title' => $this->title,
|
||||
'excerpt' => $this->excerpt,
|
||||
'content' => $this->content,
|
||||
'status' => $this->status,
|
||||
],
|
||||
($this->status == ArticleStatus::PUBLISHED->value && ! $this->article->published_at)
|
||||
? ['published_at' => now()]
|
||||
: []
|
||||
));
|
||||
|
||||
$this->syncMedia($this->thumbnail, $this->article, 'thumbnail');
|
||||
$this->uploadMedia($this->thumbnail, $this->article, 'thumbnail');
|
||||
});
|
||||
}
|
||||
}
|
||||
38
app/Livewire/Studio/Manage/Article/Create.php
Normal file
38
app/Livewire/Studio/Manage/Article/Create.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Manage\Article;
|
||||
|
||||
use App\Livewire\Forms\Studio\Manage\ArticleForm;
|
||||
use App\Traits\WithAuthorization;
|
||||
use App\Traits\WithToast;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Tambah Artikel')]
|
||||
class Create extends Component
|
||||
{
|
||||
use WithAuthorization, WithToast, WithUpdatedData;
|
||||
|
||||
public ArticleForm $form;
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->canOrAbort('create article');
|
||||
|
||||
$this->form->store();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
$this->toast('Artikel berhasil ditambahkan.');
|
||||
|
||||
$this->redirectRoute('studio.manage.article.index', navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.studio.manage.article.form', [
|
||||
'pageTitle' => 'Tambah Artikel',
|
||||
]);
|
||||
}
|
||||
}
|
||||
44
app/Livewire/Studio/Manage/Article/Edit.php
Normal file
44
app/Livewire/Studio/Manage/Article/Edit.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Manage\Article;
|
||||
|
||||
use App\Livewire\Forms\Studio\Manage\ArticleForm;
|
||||
use App\Models\Article;
|
||||
use App\Traits\WithAuthorization;
|
||||
use App\Traits\WithToast;
|
||||
use App\Traits\WithUpdatedData;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Ubah Artikel')]
|
||||
class Edit extends Component
|
||||
{
|
||||
use WithToast, WithUpdatedData, WithAuthorization;
|
||||
|
||||
public ArticleForm $form;
|
||||
|
||||
public function mount(Article $article)
|
||||
{
|
||||
$this->form->setArticle($article);
|
||||
}
|
||||
|
||||
public function save()
|
||||
{
|
||||
$this->canOrAbort('update article');
|
||||
|
||||
$this->form->update();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
$this->toast('Artikel berhasil diperbarui.');
|
||||
|
||||
$this->redirectRoute('studio.manage.article.index', navigate: true);
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.studio.manage.article.form', [
|
||||
'pageTitle' => 'Ubah Artikel',
|
||||
]);
|
||||
}
|
||||
}
|
||||
34
app/Livewire/Studio/Manage/Article/Index.php
Normal file
34
app/Livewire/Studio/Manage/Article/Index.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Studio\Manage\Article;
|
||||
|
||||
use App\Models\Article;
|
||||
use App\Traits\WithConfirmation;
|
||||
use App\Traits\WithToast;
|
||||
use Flux\Flux;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Title('Artikel')]
|
||||
class Index extends Component
|
||||
{
|
||||
use WithConfirmation, WithToast;
|
||||
|
||||
public function delete(Article $article)
|
||||
{
|
||||
$article->delete();
|
||||
|
||||
$this->dispatch('refreshDatatable');
|
||||
|
||||
$this->toast('Artikel berhasil dihapus.');
|
||||
|
||||
Flux::modals()->close();
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.studio.manage.article.index', [
|
||||
'pageTitle' => 'Artikel',
|
||||
]);
|
||||
}
|
||||
}
|
||||
42
app/Models/Article.php
Normal file
42
app/Models/Article.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ArticleStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\MediaLibrary\HasMedia;
|
||||
use Spatie\MediaLibrary\InteractsWithMedia;
|
||||
use Spatie\Sluggable\HasSlug;
|
||||
use Spatie\Sluggable\SlugOptions;
|
||||
use Veelasky\LaravelHashId\Eloquent\HashableId;
|
||||
|
||||
class Article extends Model implements HasMedia
|
||||
{
|
||||
use HasFactory, HasSlug, InteractsWithMedia, SoftDeletes, HashableId;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => ArticleStatus::class,
|
||||
'published_at' => 'datetime',
|
||||
'viwes' => 'int',
|
||||
];
|
||||
}
|
||||
|
||||
public function getSlugOptions(): SlugOptions
|
||||
{
|
||||
return SlugOptions::create()
|
||||
->generateSlugsFrom('name')
|
||||
->saveSlugsTo('slug');
|
||||
}
|
||||
|
||||
public function author(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'author_id');
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,7 @@
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
@ -62,4 +63,9 @@ public function outlets(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Outlet::class);
|
||||
}
|
||||
|
||||
public function articles(): HasMany
|
||||
{
|
||||
return $this->hasMany(Article::class);
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,6 +120,18 @@ public function boot(): void
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'heading' => 'Kelola',
|
||||
'items' => [
|
||||
[
|
||||
'label' => 'Artikel',
|
||||
'icon' => 'newspaper',
|
||||
'route' => 'studio.manage.article.index',
|
||||
'match' => 'studio.manage.article.*',
|
||||
'can' => 'view article',
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$view->with('sidebar', $sidebar);
|
||||
|
||||
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use App\Enums\ArticleStatus;
|
||||
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('articles', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('author_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('title', 200);
|
||||
$table->string('slug')->unique();
|
||||
$table->text('excerpt');
|
||||
$table->text('content');
|
||||
$table->enum('status', [ArticleStatus::values()])->default(ArticleStatus::PUBLISH)->comment(ArticleStatus::comment());
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->unsignedInteger('views')->default(0);
|
||||
$table->timestamp('created_at')->useCurrent();
|
||||
$table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('articles');
|
||||
}
|
||||
};
|
||||
@ -83,6 +83,11 @@ public function run(): void
|
||||
'create expense',
|
||||
'update expense',
|
||||
'delete expense',
|
||||
|
||||
'view article',
|
||||
'create article',
|
||||
'update article',
|
||||
'delete article',
|
||||
];
|
||||
|
||||
foreach ($permissions as $permission) {
|
||||
@ -163,6 +168,11 @@ public function run(): void
|
||||
'create bottle',
|
||||
'update bottle',
|
||||
'delete bottle',
|
||||
|
||||
'view article',
|
||||
'create article',
|
||||
'update article',
|
||||
'delete article',
|
||||
]));
|
||||
|
||||
$partner->syncPermissions([
|
||||
|
||||
@ -0,0 +1,88 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
<div>
|
||||
<flux:button href="{{ route('studio.manage.article.index') }}" wire:navigate.hover class="text-sm">
|
||||
Kembali
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col lg:flex-row gap-4">
|
||||
<div class="w-full lg:w-3/4 space-y-4">
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<flux:field>
|
||||
<flux:label>Judul <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:input placeholder="Masukkan judul" wire:model.live.debounce.500ms="form.title"
|
||||
autofocus autocomplete="off" />
|
||||
<flux:error name="form.title" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Cuplikasn <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:editor wire:model="form.excerpt" placeholder="Masukkan cuplikan" auotocomplete="off"
|
||||
class="**:data-[slot=content]:min-h-[100px]!" />
|
||||
<flux:error name="form.excerpt" />
|
||||
</flux:field>
|
||||
|
||||
<flux:field>
|
||||
<flux:label>Konten <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:editor wire:model="form.content" placeholder="Masukkan konten" auotocomplete="off"
|
||||
class="**:data-[slot=content]:min-h-[100px]!" />
|
||||
<flux:error name="form.content" />
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
</div>
|
||||
|
||||
<div class="w-full lg:w-1/3 space-y-4">
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<flux:field>
|
||||
<flux:label>Status <span class="text-red-500 ms-1">*</span></flux:label>
|
||||
<flux:radio.group wire:model.live="form.status" variant="buttons" class="w-full *:flex-1">
|
||||
@foreach (\App\Enums\ArticleStatus::cases() as $status)
|
||||
<flux:radio value="{{ $status->value }}">
|
||||
{{ $status->label() }}
|
||||
</flux:radio>
|
||||
@endforeach
|
||||
</flux:radio.group>
|
||||
<flux:error name="form.status" />
|
||||
</flux:field>
|
||||
</flux:card>
|
||||
|
||||
<flux:card class="space-y-6 p-6">
|
||||
<div class="space-y-3">
|
||||
<h3 class="text-sm font-medium">Thumbnail <span class="text-red-500 ms-1">*</span></h3>
|
||||
<div class="dropzone-wrapper">
|
||||
<livewire:dropzone wire:model="form.thumbnail" :rules="['image', 'mimes:png,jpeg', 'max:10420']" :max-files="1"
|
||||
:key="'thumbnail'" :files="$form->thumbnail" />
|
||||
@error('form.thumbnail')
|
||||
<div role="alert" aria-live="polite" aria-atomic="true"
|
||||
class="mt-3 text-sm font-medium text-red-500 dark:text-red-400">
|
||||
<svg class="shrink-0 [:where(&)]:size-5 inline" data-flux-icon=""
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
|
||||
aria-hidden="true" data-slot="icon">
|
||||
<path fill-rule="evenodd"
|
||||
d="M8.485 2.495c.673-1.167 2.357-1.167 3.03 0l6.28 10.875c.673 1.167-.17 2.625-1.516 2.625H3.72c-1.347 0-2.189-1.458-1.515-2.625L8.485 2.495ZM10 5a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 10 5Zm0 9a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||
clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
{{ $message }}
|
||||
</div>
|
||||
@enderror
|
||||
</div>
|
||||
</div>
|
||||
</flux:card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-start">
|
||||
<flux:button variant="primary" class="sm:w-auto cursor-pointer" wire:click="save">
|
||||
Simpan
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</flux:main>
|
||||
@ -0,0 +1,21 @@
|
||||
<flux:main>
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
|
||||
</div>
|
||||
@if (auth()->user()->can('create article'))
|
||||
<div>
|
||||
<flux:button href="{{ route('studio.manage.article.create') }}" variant="primary" wire:navigate.hover
|
||||
class="text-sm">
|
||||
Tambah
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<livewire:datatable.studio.manage.articles-table />
|
||||
</div>
|
||||
|
||||
@include('components.confirmation.delete')
|
||||
</flux:main>
|
||||
@ -18,6 +18,9 @@
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Create as VoucherCreate;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Edit as VoucherEdit;
|
||||
use App\Livewire\Studio\Loyalty\Voucher\Index as VoucherIndex;
|
||||
use App\Livewire\Studio\Manage\Article\Create as ArticleCreate;
|
||||
use App\Livewire\Studio\Manage\Article\Edit as ArticleEdit;
|
||||
use App\Livewire\Studio\Manage\Article\Index as ArticleIndex;
|
||||
use App\Livewire\Studio\Master\Outlet\Create as OutletCreate;
|
||||
use App\Livewire\Studio\Master\Outlet\Edit as OutletEdit;
|
||||
use App\Livewire\Studio\Master\Outlet\Index as OutletIndex;
|
||||
@ -114,4 +117,13 @@
|
||||
Route::delete('/{expense}/delete', ExpenseComponent::class)->name('delete')->middleware();
|
||||
});
|
||||
});
|
||||
|
||||
Route::prefix('manage')->name('studio.manage.')->group(function () {
|
||||
Route::prefix('articles')->name('article.')->group(function () {
|
||||
Route::get('/', ArticleIndex::class)->name('index')->middleware('can:view article');
|
||||
Route::get('/create', ArticleCreate::class)->name('create')->middleware('can:create article');
|
||||
Route::get('/{article}/edit', ArticleEdit::class)->name('edit')->middleware('can:update article');
|
||||
Route::delete('/{article}/delete', ArticleCreate::class)->name('delete')->middleware('can:delete article');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user