feat: Implement testimonial management with dedicated UI abnd database

This commit is contained in:
Yoga Pangestu 2025-12-20 07:36:03 +07:00
parent cb9e61ee7f
commit 9bd6997359
10 changed files with 231 additions and 0 deletions

View File

@ -0,0 +1,51 @@
<?php
namespace App\Livewire\Datatable\Information;
use App\Enums\IsShow;
use App\Models\Testimonial;
use App\Traits\Datatable\WithConfiguration;
use App\Traits\Datatable\WithPrependColumn;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Str;
use Rappasoft\LaravelLivewireTables\DataTableComponent;
use Rappasoft\LaravelLivewireTables\Views\Column;
class TestimonialsTable extends DataTableComponent
{
use WithConfiguration, WithPrependColumn;
protected $model = Testimonial::class;
public function columns(): array
{
return [
Column::make('Nama', 'user.customer.name')
->searchable(),
Column::make('Konten', 'content')
->searchable()
->format(fn ($value) => Str::limit($value, 50)),
Column::make('Rating', 'rating')
->sortable(),
Column::make('Visibilitas', 'is_show')
->label(fn ($row, $column) => Blade::render('<flux:switch wire:change="toggleShow('.$row->id.')" :checked="$checked" />', ['checked' => $row->is_show === IsShow::SHOW]))
->html(),
];
}
public function toggleShow(Testimonial $testimonial): void
{
$testimonial->update([
'is_show' => $testimonial->is_show === IsShow::SHOW ? IsShow::HIDDEN : IsShow::SHOW,
]);
}
public function builder(): Builder
{
return Testimonial::select('testimonials.id', 'rating', 'content', 'is_show', 'testimonials.created_at')->with(['user', 'user.customer']);
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Studio\Information;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Title('Testimoni')]
class Testimonial extends Component
{
public function render(): View
{
return view('livewire.studio.information.testimonials', [
'pageTitle' => 'Testimoni',
]);
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Enums\IsShow;
use Illuminate\Database\Eloquent\Attributes\Scope;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Testimonial extends Model
{
protected $guarded = ['id'];
protected function casts(): array
{
return [
'is_show' => IsShow::class,
];
}
#[Scope]
public function show(Builder $query): void
{
$query->where('is_show', IsShow::SHOW);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@ -229,6 +229,13 @@ public function boot(): void
'match' => 'studio.information.faq.*', 'match' => 'studio.information.faq.*',
'can' => 'view faq', 'can' => 'view faq',
], ],
[
'label' => 'Testimonial',
'icon' => 'chat-bubble-left-ellipsis',
'route' => 'studio.information.testimonial.index',
'match' => 'studio.information.testimonial.*',
'can' => 'view testimonial',
],
], ],
], ],
[ [

View File

@ -0,0 +1,32 @@
<?php
use App\Enums\IsShow;
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('testimonials', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->text('content');
$table->integer('rating');
$table->enum('is_show', IsShow::values())->default(IsShow::SHOW)->comment(IsShow::comment());
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('testimonials');
}
};

View File

@ -32,6 +32,7 @@ public function run(): void
FaqSeeder::class, FaqSeeder::class,
PerfumeFeatureSeeder::class, PerfumeFeatureSeeder::class,
ChooseUsSeeder::class, ChooseUsSeeder::class,
TestimonialSeeder::class,
]); ]);
} }
} }

View File

@ -137,6 +137,11 @@ public function run(): void
'update faq', 'update faq',
'delete faq', 'delete faq',
'view testimonial',
'create testimonial',
'update testimonial',
'delete testimonial',
'view broadcast', 'view broadcast',
'create broadcast', 'create broadcast',
'delete broadcast', 'delete broadcast',

View File

@ -0,0 +1,69 @@
<?php
namespace Database\Seeders;
use App\Enums\IsShow;
use App\Models\Testimonial;
use App\Models\User;
use Illuminate\Database\Seeder;
class TestimonialSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$testimonials = [
[
'content' => 'Parfumnya wangi banget, tahan lama seharian! Recommended banget pokoknya.',
'rating' => 5,
'is_show' => IsShow::SHOW,
],
[
'content' => 'Pengiriman cepat, packing aman. Wanginya juga sesuai ekspektasi. Terima kasih!',
'rating' => 5,
'is_show' => IsShow::SHOW,
],
[
'content' => 'Wanginya enak, tapi sayang kurang tahan lama di luar ruangan yg panas. Tapi worth it lah harga segini.',
'rating' => 4,
'is_show' => IsShow::SHOW,
],
[
'content' => 'Suka banget sama varian vanilla-nya. Manis tapi nggak bikin pusing. Bakal repeat order sih ini.',
'rating' => 5,
'is_show' => IsShow::SHOW,
],
[
'content' => 'Botolnya pecah dikit pas nyampe, mungkin dari ekspedisinya. Tapi parfumnya masih oke.',
'rating' => 3,
'is_show' => IsShow::HIDDEN,
],
[
'content' => 'Pelayanan ramah, admin fast respon. Parfumnya juga original. Top marketop!',
'rating' => 5,
'is_show' => IsShow::SHOW,
],
[
'content' => 'Aromanya mewah, cocok buat kondangan. Temen-temen pada nanyain pake parfum apa.',
'rating' => 5,
'is_show' => IsShow::SHOW,
],
[
'content' => 'Lumayan lah buat sehari-hari. Gak terlalu menyengat.',
'rating' => 4,
'is_show' => IsShow::SHOW,
],
];
foreach ($testimonials as $testimonial) {
Testimonial::create([
'user_id' => User::whereHas('customer')->inRandomOrder()->first()->id,
'content' => $testimonial['content'],
'rating' => $testimonial['rating'],
'is_show' => $testimonial['is_show'],
]);
}
}
}

View File

@ -0,0 +1,11 @@
<flux:main>
<div class="flex justify-between items-center">
<div>
<flux:heading size="xl">{{ $pageTitle }}</flux:heading>
</div>
</div>
<div class="mt-6">
<livewire:datatable.information.testimonials-table />
</div>
</flux:main>

View File

@ -24,6 +24,7 @@
use App\Livewire\Studio\Information\Broadcast as BroadcastComponent; use App\Livewire\Studio\Information\Broadcast as BroadcastComponent;
use App\Livewire\Studio\Information\Faq as FaqComponent; use App\Livewire\Studio\Information\Faq as FaqComponent;
use App\Livewire\Studio\Information\PriceRequest as PriceRequestComponent; use App\Livewire\Studio\Information\PriceRequest as PriceRequestComponent;
use App\Livewire\Studio\Information\Testimonial as TestimonialComponent;
use App\Livewire\Studio\Loyalty\Customer as CustomerComponent; use App\Livewire\Studio\Loyalty\Customer as CustomerComponent;
use App\Livewire\Studio\Loyalty\RedemptionVerification; use App\Livewire\Studio\Loyalty\RedemptionVerification;
use App\Livewire\Studio\Loyalty\Reward as RewardComponent; use App\Livewire\Studio\Loyalty\Reward as RewardComponent;
@ -184,6 +185,10 @@
Route::prefix('faqs')->name('faq.')->group(function () { Route::prefix('faqs')->name('faq.')->group(function () {
Route::get('/', FaqComponent::class)->name('index')->middleware('can:view faq'); Route::get('/', FaqComponent::class)->name('index')->middleware('can:view faq');
}); });
Route::prefix('testimonials')->name('testimonial.')->group(function () {
Route::get('/', TestimonialComponent::class)->name('index')->middleware('can:view testimonial');
});
}); });
Route::prefix('feature')->name('studio.feature.')->group(function () { Route::prefix('feature')->name('studio.feature.')->group(function () {