Add page heroes to VPS and dedicated server pages.
Deploy Ladill Servers / deploy (push) Successful in 1m49s

Surface server counts, status breakdown, and available plans in heroes consistent with other Ladill apps.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-06 14:19:41 +00:00
co-authored by Cursor
parent 55dff218e2
commit b6defa5e47
4 changed files with 82 additions and 12 deletions
@@ -172,6 +172,14 @@ class HostingProductController extends Controller
CustomerHostingOrder::CYCLE_YEARLY => 'Yearly (12 months)', CustomerHostingOrder::CYCLE_YEARLY => 'Yearly (12 months)',
]; ];
$pendingStatuses = ['pending_payment', 'pending_approval', 'approved', 'provisioning'];
$heroStats = [
'total' => $orders->count() + $rcServiceOrders->count(),
'active' => $orders->where('status', 'active')->count() + $rcServiceOrders->where('status', 'active')->count(),
'pending' => $orders->whereIn('status', $pendingStatuses)->count() + $rcServiceOrders->whereIn('status', $pendingStatuses)->count(),
'plans' => $products->count(),
];
return view('hosting.server-type', [ return view('hosting.server-type', [
'title' => $title, 'title' => $title,
'type' => $type, 'type' => $type,
@@ -182,6 +190,7 @@ class HostingProductController extends Controller
'serverOrderPayloads' => $serverOrderPayloads, 'serverOrderPayloads' => $serverOrderPayloads,
'billingCycles' => $billingCycles, 'billingCycles' => $billingCycles,
'userDomains' => $userDomains, 'userDomains' => $userDomains,
'heroStats' => $heroStats,
]); ]);
} }
+10
View File
@@ -69,6 +69,16 @@
} }
} }
.mobile-stats-card {
width: calc(66.666667% - 0.5rem);
}
@media (min-width: 640px) {
.mobile-stats-card {
width: auto;
}
}
/* Ladill app launcher — cap at 5 rows (15 apps), always-visible scroll indicator */ /* Ladill app launcher — cap at 5 rows (15 apps), always-visible scroll indicator */
.ladill-launcher-apps-wrap { .ladill-launcher-apps-wrap {
position: relative; position: relative;
@@ -0,0 +1,47 @@
@props([
'badge',
'title',
'description' => null,
'stats' => [],
])
@php
$statCols = match (count($stats)) {
4 => 'sm:grid-cols-2 lg:grid-cols-4',
2 => 'sm:grid-cols-2',
default => 'sm:grid-cols-3',
};
@endphp
<div {{ $attributes->merge(['class' => 'relative overflow-hidden rounded-2xl border border-slate-200 bg-white']) }}>
<div class="absolute inset-0 bg-gradient-to-br from-indigo-50/80 via-white to-violet-50/60"></div>
<div class="relative px-6 py-8 sm:px-10">
<div class="flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
<div class="max-w-xl">
<div class="inline-flex items-center gap-1.5 rounded-full bg-indigo-100 px-3 py-1 text-xs font-semibold text-indigo-700">
{{ $badge }}
</div>
<h1 class="mt-3 text-2xl font-bold tracking-tight text-slate-900 sm:text-3xl">{{ $title }}</h1>
@if ($description)
<p class="mt-2 text-sm leading-6 text-slate-600">{{ $description }}</p>
@endif
@isset($actions)
<div class="mt-6 flex flex-wrap items-center gap-2">
{{ $actions }}
</div>
@endisset
</div>
@if (count($stats) > 0)
<div class="scrollbar-hide flex snap-x snap-mandatory gap-3 overflow-x-auto pb-2 sm:grid {{ $statCols }} sm:overflow-visible sm:pb-0 lg:gap-4" style="-ms-overflow-style:none;scrollbar-width:none;">
@foreach ($stats as $stat)
<div class="mobile-stats-card shrink-0 snap-start rounded-xl border border-slate-200 bg-white/90 px-4 py-3 text-center backdrop-blur-sm">
<p class="whitespace-nowrap text-xl font-bold text-slate-900 sm:text-2xl">{{ $stat['value'] }}</p>
<p class="mt-0.5 whitespace-nowrap text-[11px] font-medium text-slate-500">{{ $stat['label'] }}</p>
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
+16 -12
View File
@@ -85,20 +85,24 @@
@endphp @endphp
<div class="space-y-6" x-data="{ showOrderModal: @js($shouldOpenOrderModal) }"> <div class="space-y-6" x-data="{ showOrderModal: @js($shouldOpenOrderModal) }">
{{-- Page Header --}} <x-servers.page-hero
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between"> :badge="$type === 'vps' ? 'SSD · Root access · Flexible billing' : 'Bare metal · RAID · Full root'"
<div> :title="$type === 'vps' ? 'VPS' : 'Dedicated servers'"
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">{{ $title }}</h1> :description="$type === 'vps'
<p class="mt-1 text-sm text-gray-500">Manage your {{ strtolower($title) }} orders.</p> ? 'Deploy scalable virtual private servers — pick a plan, region, and image.'
</div> : 'Maximum performance and isolation for production workloads on dedicated hardware.'"
:stats="[
['value' => number_format($heroStats['total']), 'label' => 'Servers'],
['value' => number_format($heroStats['active']), 'label' => 'Active'],
['value' => number_format($heroStats['pending']), 'label' => 'Pending'],
['value' => number_format($heroStats['plans']), 'label' => 'Plans'],
]">
@if ($serverOrderPayloads !== []) @if ($serverOrderPayloads !== [])
<button type="button" @click="showOrderModal = true" <x-slot name="actions">
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"> <x-btn.create type="button" @click="showOrderModal = true">{{ $orderButtonLabel }}</x-btn.create>
<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> </x-slot>
{{ $orderButtonLabel }}
</button>
@endif @endif
</div> </x-servers.page-hero>
@if ($hasAnyProducts) @if ($hasAnyProducts)
<div class="space-y-3"> <div class="space-y-3">