Reorganize Woo Manager mobile header and store switcher placement.
Deploy Ladill Woo Manager / deploy (push) Successful in 1m7s

Desktop store switcher now sits after search; mobile shows route-based page titles, bottom nav, and store switching inside the profile sheet.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-07 02:45:14 +00:00
co-authored by Cursor
parent 26b1d5b433
commit 93bfc3e2d2
18 changed files with 165 additions and 20 deletions
+6 -1
View File
@@ -4,6 +4,7 @@ namespace App\Providers;
use App\Services\Woo\SubscriptionService;
use App\Support\CurrentWooStore;
use App\Support\MobileTopbar;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;
@@ -16,7 +17,7 @@ class AppServiceProvider extends ServiceProvider
public function boot(): void
{
View::composer(['partials.sidebar', 'partials.topbar'], function ($view) {
View::composer(['partials.sidebar', 'partials.topbar', 'partials.mobile-bottom-nav', 'partials.store-switcher-menu'], function ($view) {
$account = ladill_account() ?? auth()->user();
$svc = app(SubscriptionService::class);
$stores = app(CurrentWooStore::class);
@@ -27,5 +28,9 @@ class AppServiceProvider extends ServiceProvider
$view->with('wooActiveStores', $account ? $stores->activeStores($account) : collect());
$view->with('currentWooStore', $account ? $stores->resolve($account) : null);
});
View::composer(['partials.topbar'], function ($view) {
$view->with(MobileTopbar::resolve());
});
}
}
+32 -6
View File
@@ -2,18 +2,44 @@
namespace App\Support;
use Illuminate\Http\Request;
class MobileTopbar
{
public static function resolve(): array
/** @var array<string, string> */
private const ROUTE_TITLES = [
'woo.dashboard' => 'Overview',
'woo.orders.*' => 'Orders',
'woo.products.create' => 'New product',
'woo.products.edit' => 'Edit product',
'woo.products.*' => 'Products',
'woo.categories.create' => 'New category',
'woo.categories.edit' => 'Edit category',
'woo.categories.*' => 'Categories',
'woo.stores.*' => 'Stores',
'woo.settings' => 'Settings',
'woo.pro.*' => 'Plans',
'notifications.*' => 'Notifications',
];
public static function resolve(?Request $request = null): array
{
$request ??= request();
if ($request) {
foreach (self::ROUTE_TITLES as $pattern => $title) {
if ($request->routeIs($pattern)) {
return ['mobileTopbarTitle' => $title];
}
}
}
$appName = config('mobile-topbar.app_name', 'Ladill');
$title = $appName === 'Ladill'
? 'Ladill'
: "Ladill {$appName}";
return [
'mobileTopbarTitle' => $title,
'mobileTopbarTitle' => $appName === 'Ladill'
? 'Ladill'
: "Ladill {$appName}",
];
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
<?php
return [
'app_name' => 'Mini',
'app_name' => 'Woo Manager',
];
@@ -2,7 +2,7 @@
<div {{ $attributes->merge(['class' => 'mx-auto max-w-3xl space-y-6']) }}>
<div>
<h1 class="text-2xl font-semibold text-slate-900">{{ $title }}</h1>
<h1 class="hidden text-2xl font-semibold text-slate-900 lg:block">{{ $title }}</h1>
@if ($description)
<p class="mt-1 text-sm text-slate-500">{{ $description }}</p>
@endif
+25
View File
@@ -27,6 +27,31 @@
</div>
</div>
@auth
@php
$navUser = auth()->user();
$navInitials = collect(explode(' ', trim((string) $navUser?->name)))
->filter()->take(2)
->map(fn ($part) => strtoupper(substr($part, 0, 1)))
->implode('');
$navAvatarUrl = $navUser && method_exists($navUser, 'avatarUrl')
? $navUser->avatarUrl()
: ($navUser?->avatar_url ?? null);
@endphp
@include('partials.mobile-bottom-nav', [
'homeUrl' => route('woo.dashboard'),
'homeActive' => request()->routeIs('woo.dashboard'),
'searchUrl' => route('woo.orders.index'),
'searchActive' => request()->routeIs('woo.orders.*'),
'notificationsUrl' => route('notifications.index'),
'notificationsActive' => request()->routeIs('notifications.*'),
'unreadUrl' => route('notifications.unread'),
'profileActive' => false,
'profileName' => $navUser?->name ?? '',
'profileSubtitle' => $navUser?->email ?? '',
'profileMenuItems' => \App\Support\UserProfileMenu::items($navUser),
'avatarUrl' => $navAvatarUrl,
'initials' => $navInitials !== '' ? $navInitials : 'U',
])
@include('partials.sso-keepalive')
@include('partials.afia')
@endauth
@@ -122,6 +122,8 @@
</div>
@endif
@include('partials.store-switcher-menu', ['onNavigate' => 'profileOpen = false'])
@include('partials.user-profile-menu', [
'items' => $profileMenuItems,
'variant' => 'sheet',
@@ -0,0 +1,27 @@
@if(($wooActiveStores ?? collect())->count() > 0)
<div class="border-b border-slate-100 px-2 py-2">
<p class="px-2 py-1.5 text-xs font-semibold uppercase tracking-wide text-slate-400">Switch store</p>
@foreach($wooActiveStores as $store)
<form method="post" action="{{ route('woo.stores.switch') }}">
@csrf
<input type="hidden" name="store" value="{{ $store->id }}">
<button type="submit"
@if (! empty($onNavigate)) @click="{{ $onNavigate }}" @endif
class="flex w-full items-center gap-2 rounded-xl px-3 py-2.5 text-left text-sm font-medium transition hover:bg-slate-100 {{ $currentWooStore?->id === $store->id ? 'bg-indigo-50 text-indigo-700' : 'text-slate-700' }}">
<svg class="h-4 w-4 shrink-0 {{ $currentWooStore?->id === $store->id ? 'text-indigo-500' : 'text-slate-400' }}" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 21v-7.5a.75.75 0 0 1 .75-.75h3a.75.75 0 0 1 .75.75V21m-4.5 0H2.36m11.14 0H18m0 0h3.64m-1.39 0V9.349M3.75 21V9.349m0 0a3.001 3.001 0 0 0 3.75-.615A2.993 2.993 0 0 0 9.75 9.75c.896 0 1.7-.393 2.25-1.016a2.993 2.993 0 0 0 2.25 1.016c.896 0 1.7-.393 2.25-1.016a3.001 3.001 0 0 0 3.75.614m-16.5 0a3.004 3.004 0 0 1-.621-4.72L4.318 3.44A1.5 1.5 0 0 1 5.378 3h13.243a1.5 1.5 0 0 1 1.06.44l1.19 1.189a3 3 0 0 1-.621 4.72M6.75 18h3.75a.75.75 0 0 0 .75-.75V13.5a.75.75 0 0 0-.75-.75H6.75a.75.75 0 0 0-.75.75v3.75c0 .414.336.75.75.75Z" />
</svg>
<span class="min-w-0 flex-1 truncate">{{ $store->site_name ?? parse_url($store->site_url, PHP_URL_HOST) }}</span>
@if($currentWooStore?->id === $store->id)
<span class="shrink-0 text-xs font-medium text-indigo-600">Active</span>
@endif
</button>
</form>
@endforeach
<a href="{{ route('woo.stores.index') }}"
@if (! empty($onNavigate)) @click="{{ $onNavigate }}" @endif
class="mt-1 block rounded-xl px-3 py-2 text-xs font-medium text-indigo-600 hover:bg-indigo-50 hover:text-indigo-800">
Manage stores
</a>
</div>
@endif
@@ -1,5 +1,5 @@
@if(($wooActiveStores ?? collect())->count() > 0)
<div class="relative hidden lg:block" x-data="{ open: false }" @click.outside="open = false">
<div class="relative hidden shrink-0 lg:block" x-data="{ open: false }" @click.outside="open = false">
<button type="button" @click="open = !open"
class="flex max-w-[14rem] items-center gap-2 rounded-xl border border-slate-200 bg-slate-50 px-3 py-2 text-left text-sm text-slate-700 hover:bg-white">
<svg class="h-4 w-4 shrink-0 text-slate-400" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
+4 -2
View File
@@ -9,9 +9,9 @@
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
</button>
@include('partials.store-switcher')
@include('partials.mobile-topbar-title')
{{-- Desktop: search (customers) --}}
{{-- Desktop: search (orders) --}}
<form method="GET" action="{{ route('woo.orders.index') }}"
class="relative hidden w-full max-w-md lg:block"
x-data
@@ -22,6 +22,8 @@
class="w-full rounded-xl border border-slate-200 bg-slate-50 py-2 pl-9 pr-10 text-sm text-slate-700 placeholder-slate-400 transition focus:border-indigo-300 focus:bg-white focus:ring-2 focus:ring-indigo-100 focus:outline-none">
<kbd class="pointer-events-none absolute right-3 top-1/2 hidden -translate-y-1/2 rounded-md border border-slate-200 bg-white px-1.5 py-0.5 text-[10px] font-medium text-slate-400 sm:inline-block">/</kbd>
</form>
@include('partials.store-switcher')
</div>
<div class="flex items-center gap-3">
@@ -2,7 +2,7 @@
<x-slot name="title">{{ $category->exists ? 'Edit category' : 'New category' }}</x-slot>
<div class="mx-auto max-w-2xl space-y-6">
<div>
<h1 class="text-xl font-semibold text-slate-900">{{ $category->exists ? 'Edit category' : 'New category' }}</h1>
<h1 class="hidden text-xl font-semibold text-slate-900 lg:block">{{ $category->exists ? 'Edit category' : 'New category' }}</h1>
<p class="mt-1 text-sm text-slate-500">Changes sync to WooCommerce when the Ladill plugin is connected.</p>
</div>
@@ -3,7 +3,7 @@
<div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-slate-900">Categories</h1>
<h1 class="hidden text-xl font-semibold text-slate-900 lg:block">Categories</h1>
<p class="mt-1 text-sm text-slate-500">
@if($store)
Categories for {{ $store->site_name ?? $store->site_url }}.
+1 -1
View File
@@ -2,7 +2,7 @@
<x-slot name="title">Overview</x-slot>
<div class="space-y-6">
<div>
<h1 class="text-xl font-semibold text-slate-900">Woo Manager</h1>
<h1 class="hidden text-xl font-semibold text-slate-900 lg:block">Woo Manager</h1>
<p class="mt-1 text-sm text-slate-500">
@if($currentStore)
Managing <strong>{{ $currentStore->site_name ?? $currentStore->site_url }}</strong> switch stores from the top bar.
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-slate-900">Orders</h1>
<h1 class="hidden text-xl font-semibold text-slate-900 lg:block">Orders</h1>
<p class="mt-1 text-sm text-slate-500">
@if($store)
Orders for {{ $store->site_name ?? $store->site_url }}.
+1 -1
View File
@@ -12,7 +12,7 @@
@endif
<div class="rounded-2xl border border-slate-200 bg-white px-6 py-5">
<h1 class="text-2xl font-bold text-slate-900">Choose your Woo Manager plan</h1>
<h1 class="hidden text-2xl font-bold text-slate-900 lg:block">Choose your Woo Manager plan</h1>
<p class="mt-1 text-sm text-slate-600">Pay monthly from your Ladill wallet, or prepay 6/12/24 months via Paystack.</p>
@if ($gatingActive && ! ($subscription && $subscription->entitled()))
<div class="mt-4 flex flex-wrap gap-2">
+1 -1
View File
@@ -2,7 +2,7 @@
<x-slot name="title">{{ $product->exists ? 'Edit product' : 'New product' }}</x-slot>
<div class="mx-auto max-w-5xl space-y-6">
<div>
<h1 class="text-xl font-semibold text-slate-900">{{ $product->exists ? 'Edit product' : 'New product' }}</h1>
<h1 class="hidden text-xl font-semibold text-slate-900 lg:block">{{ $product->exists ? 'Edit product' : 'New product' }}</h1>
<p class="mt-1 text-sm text-slate-500">Changes sync to WooCommerce when the Ladill plugin is connected.</p>
</div>
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-slate-900">Products</h1>
<h1 class="hidden text-xl font-semibold text-slate-900 lg:block">Products</h1>
<p class="mt-1 text-sm text-slate-500">
@if($currentStore)
Products for {{ $currentStore->site_name ?? $currentStore->site_url }}.
+1 -1
View File
@@ -3,7 +3,7 @@
<div class="space-y-6">
<div class="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 class="text-xl font-semibold text-slate-900">Stores</h1>
<h1 class="hidden text-xl font-semibold text-slate-900 lg:block">Stores</h1>
<p class="mt-1 text-sm text-slate-500">WooCommerce sites connected via the Ladill plugin.</p>
@if(! $hasPaidPlan)
<p class="mt-1 text-xs text-slate-500">{{ $storeCount }}/{{ min($storeLimit, 1) }} store(s) on free plan</p>
+58
View File
@@ -0,0 +1,58 @@
<?php
namespace Tests\Feature;
use App\Models\User;
use App\Models\WooStore;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class WooMobileNavTest extends TestCase
{
use RefreshDatabase;
public function test_orders_page_shows_mobile_header_title_and_profile_store_menu(): void
{
$user = User::factory()->create();
WooStore::create([
'user_id' => $user->id,
'site_url' => 'https://shop.example.com',
'site_name' => 'Example Shop',
'status' => WooStore::STATUS_ACTIVE,
'connected_at' => now(),
]);
$this->actingAs($user)
->get(route('woo.orders.index'))
->assertOk()
->assertSee('min-w-0 flex-1 lg:hidden', false)
->assertSee('>Orders</h1>', false)
->assertSee('hidden text-xl font-semibold text-slate-900 lg:block', false)
->assertSee('Switch store', false)
->assertSee('>Profile</span>', false);
}
public function test_desktop_store_switcher_renders_after_search_form(): void
{
$user = User::factory()->create();
WooStore::create([
'user_id' => $user->id,
'site_url' => 'https://shop.example.com',
'site_name' => 'Example Shop',
'status' => WooStore::STATUS_ACTIVE,
'connected_at' => now(),
]);
$html = $this->actingAs($user)
->get(route('woo.dashboard'))
->assertOk()
->getContent();
$searchPos = strpos($html, 'Search orders…');
$switcherPos = strpos($html, 'Example Shop');
$this->assertNotFalse($searchPos);
$this->assertNotFalse($switcherPos);
$this->assertLessThan($switcherPos, $searchPos);
}
}