Add page heroes to single, multi, and WordPress hosting pages.
Deploy Ladill Hosting / deploy (push) Successful in 30s
Deploy Ladill Hosting / deploy (push) Successful in 30s
Surface account counts, status breakdown, and available plans in heroes consistent with Ladill Servers. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -107,6 +107,19 @@ class HostingProductController extends Controller
|
||||
$nativeForm = $this->nativeHostingProductForm($type);
|
||||
$userDomains = $this->getUserDomains($user);
|
||||
|
||||
$pendingStatuses = ['pending_payment', 'pending_approval', 'approved', 'provisioning'];
|
||||
$heroStats = [
|
||||
'accounts' => $hostingAccounts->count(),
|
||||
'active' => $hostingAccounts->where('status', 'active')->count()
|
||||
+ $orders->where('status', 'active')->count()
|
||||
+ $rcHostingOrders->where('status', 'active')->count()
|
||||
+ $rcServiceOrders->where('status', 'active')->count(),
|
||||
'pending' => $orders->whereIn('status', $pendingStatuses)->count()
|
||||
+ $rcHostingOrders->whereIn('status', $pendingStatuses)->count()
|
||||
+ $rcServiceOrders->whereIn('status', $pendingStatuses)->count(),
|
||||
'plans' => count($nativeForm['packages'] ?? []),
|
||||
];
|
||||
|
||||
return view('hosting.type', [
|
||||
'title' => $title,
|
||||
'type' => $type,
|
||||
@@ -117,6 +130,7 @@ class HostingProductController extends Controller
|
||||
'marketingCategory' => $marketingCategory,
|
||||
'nativeForm' => $nativeForm,
|
||||
'userDomains' => $userDomains,
|
||||
'heroStats' => $heroStats,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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-launcher-apps-wrap {
|
||||
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>
|
||||
@@ -50,28 +50,48 @@
|
||||
|| $hostingAccounts->isNotEmpty()
|
||||
|| $rcHostingOrders->isNotEmpty()
|
||||
|| $rcServiceOrders->isNotEmpty();
|
||||
|
||||
$heroMeta = match ($type) {
|
||||
\App\Models\HostingProduct::TYPE_SINGLE_DOMAIN => [
|
||||
'badge' => 'One site · SSL · cPanel',
|
||||
'title' => 'Single domain hosting',
|
||||
'description' => 'Reliable hosting for one website — link your domain and go live.',
|
||||
],
|
||||
\App\Models\HostingProduct::TYPE_MULTI_DOMAIN => [
|
||||
'badge' => 'Multiple sites · Shared resources',
|
||||
'title' => 'Multi domain hosting',
|
||||
'description' => 'Host several websites on one account with separate domains.',
|
||||
],
|
||||
\App\Models\HostingProduct::TYPE_WORDPRESS => [
|
||||
'badge' => 'Managed · Optimized · One-click install',
|
||||
'title' => 'WordPress hosting',
|
||||
'description' => 'Fast, managed WordPress with automatic updates and easy staging.',
|
||||
],
|
||||
default => [
|
||||
'badge' => 'Web hosting',
|
||||
'title' => $title,
|
||||
'description' => 'Manage your hosting products and orders.',
|
||||
],
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6" x-data="{ showOrderModal: @js($shouldOpenOrderModal) }">
|
||||
{{-- Page Header --}}
|
||||
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||
<div>
|
||||
<div class="flex items-center gap-3">
|
||||
@if ($type === \App\Models\HostingProduct::TYPE_WORDPRESS)
|
||||
@include('partials.wordpress-icon', ['class' => 'h-9 w-9 object-contain'])
|
||||
@endif
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-gray-900">{{ $title }}</h1>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-500">Manage your {{ strtolower($title) }} products and orders.</p>
|
||||
</div>
|
||||
<x-hosting.page-hero
|
||||
:badge="$heroMeta['badge']"
|
||||
:title="$heroMeta['title']"
|
||||
:description="$heroMeta['description']"
|
||||
:stats="[
|
||||
['value' => number_format($heroStats['accounts']), 'label' => 'Accounts'],
|
||||
['value' => number_format($heroStats['active']), 'label' => 'Active'],
|
||||
['value' => number_format($heroStats['pending']), 'label' => 'Pending'],
|
||||
['value' => number_format($heroStats['plans']), 'label' => 'Plans'],
|
||||
]">
|
||||
@if ($nativeForm && ($nativeForm['has_packages'] ?? false))
|
||||
<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">
|
||||
<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
|
||||
</button>
|
||||
<x-slot name="actions">
|
||||
<x-btn.create type="button" @click="showOrderModal = true">New product</x-btn.create>
|
||||
</x-slot>
|
||||
@endif
|
||||
</div>
|
||||
</x-hosting.page-hero>
|
||||
|
||||
{{-- Products List --}}
|
||||
@if ($hasAnyProducts)
|
||||
|
||||
Reference in New Issue
Block a user