front end fixes

This commit is contained in:
Mark Gregory
2026-07-27 07:37:11 +01:00
parent 0be7be48e7
commit 108f95dbfa
21 changed files with 687 additions and 423 deletions
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('products', function (Blueprint $table): void {
$table->string('product_type')->default('product')->after('name');
$table->text('description')->nullable()->after('product_type');
$table->decimal('price', 12, 2)->nullable()->after('description');
$table->string('currency', 3)->default('GBP')->after('price');
$table->string('billing_period')->nullable()->after('currency');
$table->json('features')->nullable()->after('billing_period');
$table->string('cta_label')->nullable()->after('features');
$table->string('cta_url')->nullable()->after('cta_label');
$table->boolean('is_featured')->default(false)->after('cta_url');
$table->unsignedInteger('sort_order')->default(0)->after('is_featured');
});
}
public function down(): void
{
Schema::table('products', function (Blueprint $table): void {
$table->dropColumn([
'product_type', 'description', 'price', 'currency', 'billing_period',
'features', 'cta_label', 'cta_url', 'is_featured', 'sort_order',
]);
});
}
};
@@ -11,8 +11,35 @@ final class StoreProductRequest extends FormRequest
return $this->user() !== null;
}
protected function prepareForValidation(): void
{
$this->merge([
'is_active' => $this->boolean('is_active'),
'is_featured' => $this->boolean('is_featured'),
'features' => collect(preg_split('/\r\n|\r|\n/', (string) $this->input('features')))
->map(fn (string $feature) => trim($feature))
->filter()
->values()
->all(),
]);
}
public function rules(): array
{
return ['name' => ['required', 'string', 'max:255'], 'is_active' => ['sometimes', 'boolean']];
return [
'name' => ['required', 'string', 'max:255'],
'product_type' => ['required', 'in:product,plan'],
'description' => ['nullable', 'string'],
'price' => ['nullable', 'numeric', 'min:0'],
'currency' => ['required', 'string', 'size:3'],
'billing_period' => ['nullable', 'string', 'max:40'],
'features' => ['array'],
'features.*' => ['string', 'max:255'],
'cta_label' => ['nullable', 'string', 'max:80'],
'cta_url' => ['nullable', 'string', 'max:2048'],
'is_featured' => ['boolean'],
'sort_order' => ['integer', 'min:0'],
'is_active' => ['boolean'],
];
}
}
+7 -1
View File
@@ -10,6 +10,12 @@ final class Product extends Model
protected function casts(): array
{
return ['is_active' => 'boolean'];
return [
'price' => 'decimal:2',
'features' => 'array',
'is_active' => 'boolean',
'is_featured' => 'boolean',
'sort_order' => 'integer',
];
}
}
@@ -1 +1,25 @@
<form method="POST">@csrf<flux:input name="name" :label="__('Name')" required /><flux:button type="submit" variant="primary">{{ __('Save Product') }}</flux:button></form>
<form method="POST" class="space-y-5">
@csrf
<div class="grid gap-5 md:grid-cols-2">
<flux:input name="name" :label="__('Name')" required />
<flux:select name="product_type" :label="__('Type')">
<option value="product">Product</option>
<option value="plan">Pricing plan</option>
</flux:select>
<flux:input name="price" type="number" step="0.01" min="0" :label="__('Price')" />
<flux:input name="currency" value="GBP" maxlength="3" :label="__('Currency')" />
<flux:input name="billing_period" value="month" :label="__('Billing period')" />
<flux:input name="sort_order" type="number" min="0" value="0" :label="__('Sort order')" />
</div>
<flux:textarea name="description" :label="__('Description')" rows="3" />
<flux:textarea name="features" :label="__('Features — one per line')" rows="6" />
<div class="grid gap-5 md:grid-cols-2">
<flux:input name="cta_label" :label="__('Button label')" />
<flux:input name="cta_url" :label="__('Button URL')" />
</div>
<div class="flex flex-wrap gap-6">
<flux:checkbox name="is_active" value="1" checked :label="__('Active')" />
<flux:checkbox name="is_featured" value="1" :label="__('Featured / most popular')" />
</div>
<flux:button type="submit" variant="primary">{{ __('Save Product') }}</flux:button>
</form>
@@ -1,3 +1,26 @@
<x-layouts.app :title="__('Product')">
<div class="space-y-6"><h1 class="text-2xl font-semibold">Product</h1>@foreach ($records as $record)<div>{{ $record->name }}</div>@endforeach{{ $records->links() }}</div>
<x-layouts.app :title="__('Products & Pricing')">
<div class="space-y-6">
<div>
<h1 class="text-2xl font-semibold">Products & Pricing</h1>
<p class="mt-1 text-sm text-zinc-500">Manage catalogue products and public-facing pricing plans from one place.</p>
</div>
<section class="rounded-2xl border border-zinc-200/70 bg-white/80 p-6 shadow-sm backdrop-blur dark:border-white/10 dark:bg-zinc-950/70">
@include('products::admin.products.form')
</section>
<div class="grid gap-4 lg:grid-cols-3">
@forelse ($records as $record)
<article class="rounded-2xl border border-zinc-200/70 bg-white/80 p-5 shadow-sm backdrop-blur dark:border-white/10 dark:bg-zinc-950/70">
<div class="flex items-start justify-between gap-4">
<div><h2 class="font-semibold">{{ $record->name }}</h2><p class="text-sm text-zinc-500">{{ ucfirst($record->product_type ?? 'product') }}</p></div>
@if($record->is_featured)<span class="rounded-full bg-orange-500/10 px-2.5 py-1 text-xs font-semibold text-orange-600">Featured</span>@endif
</div>
@if($record->price !== null)<p class="mt-4 text-2xl font-bold">{{ $record->currency }} {{ $record->price }} <small class="text-sm font-normal">/{{ $record->billing_period }}</small></p>@else<p class="mt-4 text-2xl font-bold">Custom</p>@endif
<p class="mt-2 text-sm text-zinc-500">{{ $record->description }}</p>
</article>
@empty
<p class="text-sm text-zinc-500">No products or plans have been created.</p>
@endforelse
</div>
{{ $records->links() }}
</div>
</x-layouts.app>