Add server product catalog so VPS and dedicated pages can take orders.
Deploy Ladill Servers / deploy (push) Successful in 25s

Seed Contabo-backed VPS and dedicated plans, open the order modal from dashboard links, and show clearer order CTAs when the catalog is empty.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-06-06 20:01:05 +00:00
co-authored by Cursor
parent 1217bdac41
commit 608e6947bf
5 changed files with 167 additions and 22 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ Push to `main`, then:
```bash ```bash
cd /var/www/ladill-servers/current cd /var/www/ladill-servers/current
php artisan migrate --force php artisan migrate --force
php artisan db:seed --class=HostingProductSeeder --force php artisan db:seed --class=ServerProductSeeder --force
php artisan config:cache route:cache view:cache php artisan config:cache route:cache view:cache
``` ```
+1 -12
View File
@@ -2,28 +2,17 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
use Database\Seeders\HostingProductSeeder;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
{ {
use WithoutModelEvents; use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void public function run(): void
{ {
$this->call([ $this->call([
HostingProductSeeder::class, ServerProductSeeder::class,
]);
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
]); ]);
} }
} }
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace Database\Seeders;
use App\Models\HostingProduct;
use Illuminate\Database\Seeder;
class ServerProductSeeder extends Seeder
{
public function run(): void
{
$products = [
[
'name' => 'VPS S',
'slug' => 'vps-s',
'description' => 'Entry-level VPS with 4 vCPU cores, 8 GB RAM, 75 GB NVMe SSD, and unlimited bandwidth. Perfect for small projects and development.',
'category' => 'vps',
'type' => 'vps',
'price_monthly' => 0,
'price_quarterly' => 0,
'price_yearly' => 0,
'currency' => 'GHS',
'disk_gb' => 75,
'bandwidth_gb' => null,
'cpu_cores' => 3,
'ram_mb' => 8192,
'ssl_included' => true,
'contabo_product_id' => 'V91',
'contabo_region' => 'EU',
'is_active' => true,
'is_featured' => false,
'is_visible' => true,
'sort_order' => 10,
],
[
'name' => 'VPS M',
'slug' => 'vps-m',
'description' => 'Mid-range VPS with 6 vCPU cores, 12 GB RAM, 100 GB NVMe SSD, and unlimited bandwidth. Ideal for growing applications and medium traffic sites.',
'category' => 'vps',
'type' => 'vps',
'price_monthly' => 0,
'price_quarterly' => 0,
'price_yearly' => 0,
'currency' => 'GHS',
'disk_gb' => 100,
'bandwidth_gb' => null,
'cpu_cores' => 6,
'ram_mb' => 12288,
'ssl_included' => true,
'contabo_product_id' => 'V94',
'contabo_region' => 'EU',
'is_active' => true,
'is_featured' => true,
'is_visible' => true,
'sort_order' => 11,
],
[
'name' => 'VPS L',
'slug' => 'vps-l',
'description' => 'High-performance VPS with 8 vCPU cores, 24 GB RAM, 200 GB NVMe SSD, and unlimited bandwidth. Built for demanding workloads and high-traffic applications.',
'category' => 'vps',
'type' => 'vps',
'price_monthly' => 0,
'price_quarterly' => 0,
'price_yearly' => 0,
'currency' => 'GHS',
'disk_gb' => 200,
'bandwidth_gb' => null,
'cpu_cores' => 8,
'ram_mb' => 24576,
'ssl_included' => true,
'contabo_product_id' => 'V97',
'contabo_region' => 'EU',
'is_active' => true,
'is_featured' => false,
'is_visible' => true,
'sort_order' => 12,
],
[
'name' => 'VPS XL',
'slug' => 'vps-xl',
'description' => 'Enterprise VPS with 12 vCPU cores, 48 GB RAM, 250 GB NVMe SSD, and unlimited bandwidth. Maximum performance for resource-intensive applications.',
'category' => 'vps',
'type' => 'vps',
'price_monthly' => 0,
'price_quarterly' => 0,
'price_yearly' => 0,
'currency' => 'GHS',
'disk_gb' => 250,
'bandwidth_gb' => null,
'cpu_cores' => 12,
'ram_mb' => 49152,
'ssl_included' => true,
'contabo_product_id' => 'V100',
'contabo_region' => 'EU',
'is_active' => true,
'is_featured' => false,
'is_visible' => true,
'sort_order' => 13,
],
[
'name' => 'Dedicated Entry',
'slug' => 'dedicated-entry',
'description' => 'Entry-level dedicated server with 12 CPU cores (AMD Ryzen 9 7900), 64 GB RAM, 1 TB NVMe SSD, and unlimited bandwidth. Full root access with no virtualization overhead.',
'category' => 'dedicated',
'type' => 'dedicated',
'price_monthly' => 0,
'price_yearly' => 0,
'currency' => 'GHS',
'disk_gb' => 1000,
'bandwidth_gb' => null,
'cpu_cores' => 12,
'ram_mb' => 65536,
'ssl_included' => true,
'contabo_product_id' => 'amd-ryzen-12-cores',
'is_active' => true,
'is_featured' => false,
'is_visible' => true,
'sort_order' => 20,
],
[
'name' => 'Dedicated Pro',
'slug' => 'dedicated-pro',
'description' => 'Professional dedicated server with 24 CPU cores (AMD EPYC 9224), 128 GB ECC RAM, 2 TB SSD storage, and unlimited bandwidth. Maximum power for enterprise workloads.',
'category' => 'dedicated',
'type' => 'dedicated',
'price_monthly' => 0,
'price_yearly' => 0,
'currency' => 'GHS',
'disk_gb' => 2000,
'bandwidth_gb' => null,
'cpu_cores' => 24,
'ram_mb' => 131072,
'ssl_included' => true,
'contabo_product_id' => 'amd-genoa-24-cores',
'is_active' => true,
'is_featured' => true,
'is_visible' => true,
'sort_order' => 21,
],
];
foreach ($products as $product) {
HostingProduct::updateOrCreate(
['slug' => $product['slug']],
$product
);
}
}
}
@@ -20,7 +20,11 @@
'server_password_confirmation', 'server_password_confirmation',
]; ];
$hasOrderFormErrors = collect($orderFormErrorFields)->contains(fn ($field) => $errors->has($field)); $hasOrderFormErrors = collect($orderFormErrorFields)->contains(fn ($field) => $errors->has($field));
$shouldOpenOrderModal = (bool) session('open_product_order_modal', false) || $hasOrderFormErrors; $shouldOpenOrderModal = (bool) session('open_product_order_modal', false)
|| $hasOrderFormErrors
|| request()->boolean('order');
$orderButtonLabel = $type === 'vps' ? 'Order VPS' : 'Order dedicated server';
$initialServerStep = old('product_id') $initialServerStep = old('product_id')
? (($errors->has('server_password') || $errors->has('server_password_confirmation')) ? 3 : 2) ? (($errors->has('server_password') || $errors->has('server_password_confirmation')) ? 3 : 2)
@@ -91,7 +95,7 @@
<button type="button" @click="showOrderModal = true" <button type="button" @click="showOrderModal = true"
class="inline-flex items-center gap-1.5 rounded-lg bg-gray-900 px-3.5 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-gray-800"> class="inline-flex items-center gap-1.5 rounded-lg bg-gray-900 px-3.5 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-gray-800">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg> <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
New Product {{ $orderButtonLabel }}
</button> </button>
@endif @endif
</div> </div>
@@ -209,9 +213,11 @@
<button type="button" @click="showOrderModal = true" <button type="button" @click="showOrderModal = true"
class="inline-flex items-center gap-1.5 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition hover:bg-gray-800"> class="inline-flex items-center gap-1.5 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white shadow-sm transition hover:bg-gray-800">
<svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg> <svg class="h-4 w-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/></svg>
Get Started {{ $orderButtonLabel }}
</button> </button>
</div> </div>
@else
<p class="mt-4 text-sm text-amber-700">Server plans are not available right now. Please contact support or try again later.</p>
@endif @endif
</div> </div>
@endif @endif
+6 -6
View File
@@ -8,11 +8,11 @@
<p class="mt-0.5 text-sm text-slate-500">Your VPS and dedicated servers at a glance.</p> <p class="mt-0.5 text-sm text-slate-500">Your VPS and dedicated servers at a glance.</p>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
<a href="{{ route('servers.vps') }}" class="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-indigo-700"> <a href="{{ route('servers.vps', ['order' => 1]) }}" class="inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-indigo-700">
New VPS Order VPS
</a> </a>
<a href="{{ route('servers.dedicated') }}" class="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-50"> <a href="{{ route('servers.dedicated', ['order' => 1]) }}" class="inline-flex items-center justify-center rounded-lg border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 transition hover:bg-slate-50">
New dedicated Order dedicated
</a> </a>
</div> </div>
</div> </div>
@@ -40,8 +40,8 @@
<p class="text-sm font-semibold text-slate-700">No servers yet</p> <p class="text-sm font-semibold text-slate-700">No servers yet</p>
<p class="mx-auto mt-1 max-w-sm text-xs text-slate-400">Deploy a VPS or dedicated server configure region, image, and billing in minutes.</p> <p class="mx-auto mt-1 max-w-sm text-xs text-slate-400">Deploy a VPS or dedicated server configure region, image, and billing in minutes.</p>
<div class="mt-5 flex flex-wrap justify-center gap-2"> <div class="mt-5 flex flex-wrap justify-center gap-2">
<a href="{{ route('servers.vps') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Browse VPS</a> <a href="{{ route('servers.vps', ['order' => 1]) }}" class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700">Order VPS</a>
<a href="{{ route('servers.dedicated') }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Browse dedicated</a> <a href="{{ route('servers.dedicated', ['order' => 1]) }}" class="rounded-lg border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Order dedicated</a>
</div> </div>
</div> </div>
@else @else