nimuli/resources/views/components/sidebar.blade.php

241 lines
13 KiB
PHP

@php
$workspace = current_workspace();
$workspaces = auth()->check() ? auth()->user()->workspaces()->orderBy('name')->get() : collect();
$wsUlid = $workspace?->ulid;
$wsNav = $workspace ? [
['label' => 'Dashboard', 'route' => 'w.dashboard', 'params' => [$wsUlid], 'match' => 'w.dashboard', 'icon' => 'dashboard', 'exact' => true],
['label' => 'Links', 'route' => 'w.links.index', 'params' => [$wsUlid], 'match' => 'w.links.*', 'icon' => 'links'],
['label' => 'QR Codes', 'route' => 'w.qr.index', 'params' => [$wsUlid], 'match' => 'w.qr.*', 'icon' => 'qr'],
['label' => 'Link-in-Bio','route' => 'w.bio.index', 'params' => [$wsUlid], 'match' => 'w.bio.*', 'icon' => 'bio'],
['label' => 'Analytics', 'route' => 'w.analytics.index', 'params' => [$wsUlid], 'match' => 'w.analytics.*', 'icon' => 'analytics'],
['label' => 'Domains', 'route' => 'w.domains.index', 'params' => [$wsUlid], 'match' => 'w.domains.*', 'icon' => 'domains'],
] : [
['label' => 'Dashboard', 'route' => 'dashboard', 'params' => [], 'match' => 'dashboard', 'icon' => 'dashboard', 'exact' => true],
];
$accountNav = $workspace ? [
['label' => 'Settings', 'route' => 'w.settings.index', 'params' => [$wsUlid], 'match' => 'w.settings.*', 'icon' => 'settings'],
['label' => 'Billing', 'route' => 'w.billing.index', 'params' => [$wsUlid], 'match' => 'w.billing.*', 'icon' => 'billing'],
['label' => 'Team', 'route' => 'w.team.index', 'params' => [$wsUlid], 'match' => 'w.team.*', 'icon' => 'team'],
['label' => 'AI', 'route' => 'w.ai.insights', 'params' => [$wsUlid], 'match' => 'w.ai.*', 'icon' => 'ai'],
] : [];
$plan = $workspace?->plan;
$linksUsed = $workspace ? $workspace->links()->count() : 0;
$linkLimit = $plan?->link_limit ?? 50;
$usagePct = $linkLimit > 0 ? min(100, round(($linksUsed / $linkLimit) * 100)) : 0;
$userName = auth()->user()?->name ?? '';
$initials = collect(explode(' ', trim($userName)))->map(fn($w) => strtoupper(substr($w, 0, 1)))->take(2)->implode('');
@endphp
<aside
x-data="{ wsOpen: false }"
class="nim-sidebar fixed inset-y-0 left-0 h-screen flex flex-col z-40 -translate-x-full md:translate-x-0"
:style="$store.sidebar?.mobileOpen ? 'translate: 0' : null"
>
{{-- ═══ Header ═══ --}}
<header class="flex-shrink-0 px-3 py-3 border-b border-white/[.06]" @click.outside="wsOpen = false">
<button
@click="$store.sidebar?.isCollapsed && !$store.sidebar?.isMobile ? $store.sidebar.toggle() : (wsOpen = !wsOpen)"
:aria-expanded="wsOpen.toString()"
class="ws-trigger w-full flex items-center gap-3 px-2.5 py-2 rounded-xl hover:bg-s2 transition-colors duration-150"
>
<img src="/brand/logo-mark.svg" alt="Nimuli" class="ws-logo w-8 h-8 flex-shrink-0 rounded-lg">
<span class="ws-text flex-1 min-w-0 text-left grid leading-snug">
<span class="ws-brand text-accent-gradient font-bold text-sm tracking-tight">nimuli</span>
<span class="ws-name text-xs text-t2 truncate">{{ $workspace?->name ?? 'Select workspace' }}</span>
</span>
<svg class="ws-chevron w-4 h-4 text-t3 flex-shrink-0 transition-transform duration-200"
:class="wsOpen && 'rotate-180'"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="6 9 12 15 18 9"/>
</svg>
</button>
{{-- Mobile close --}}
<button
@click="$store.sidebar.closeMobile()"
class="md:hidden absolute right-3 top-3.5 p-2 rounded-lg text-t3 hover:text-t1 hover:bg-s2 transition-colors"
>
<svg class="w-5 h-5" fill="none" viewBox="0 0 20 20" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6l8 8M6 14L14 6"/>
</svg>
</button>
{{-- Workspace dropdown --}}
<div
x-show="wsOpen"
x-transition:enter="transition ease-out duration-150"
x-transition:enter-start="opacity-0 -translate-y-1"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-100"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 -translate-y-1"
class="mt-2 bg-s2 border border-white/[.06] rounded-xl overflow-hidden"
x-cloak
>
@forelse($workspaces as $ws)
<a
href="{{ route('w.links.index', $ws->ulid) }}"
wire:navigate
@click="wsOpen = false"
class="flex items-center gap-2.5 px-3.5 py-2.5 text-sm border-b border-white/[.06] transition-colors hover:bg-s3
{{ $workspace?->id === $ws->id ? 'text-t1 font-medium' : 'text-t2' }}"
>
<span class="w-6 h-6 rounded-md flex items-center justify-center text-white font-bold text-xs flex-shrink-0 bg-accent-gradient">
{{ strtoupper(substr($ws->name, 0, 1)) }}
</span>
<span class="flex-1 truncate">{{ $ws->name }}</span>
@if($workspace?->id === $ws->id)
<svg class="w-3.5 h-3.5 flex-shrink-0 text-accent" fill="none" viewBox="0 0 24 24"
stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>
@endif
</a>
@empty
<div class="px-3.5 py-2.5 text-xs text-t3">No workspaces yet</div>
@endforelse
<button
@click="$dispatch('openModal', {component: 'modals.create-workspace'}); wsOpen = false"
class="w-full flex items-center gap-2.5 px-3.5 py-2.5 text-sm text-t2 hover:text-t1 hover:bg-s3 transition-colors text-left"
>
<svg class="w-4 h-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
<span>New Workspace</span>
</button>
</div>
</header>
{{-- ═══ Nav ═══ --}}
<nav class="nim-sb-nav flex-1 overflow-y-auto overflow-x-hidden px-2.5 py-2.5" aria-label="Main">
@if($workspace)
<div class="nav-label px-3 pt-1 pb-1.5 text-[11px] uppercase tracking-widest font-medium text-t3">
Workspace
</div>
@endif
@foreach($wsNav as $item)
@php
$isActive = request()->routeIs($item['match']);
$url = Route::has($item['route']) ? route($item['route'], $item['params']) : '#';
@endphp
<a
href="{{ $url }}"
wire:navigate
data-nav-link="{{ $item['route'] }}"
@isset($item['exact']) data-nav-exact="1" @endisset
data-label="{{ $item['label'] }}"
class="nav-item relative flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium w-full {{ $isActive ? 'nim-nav-active' : '' }}"
>
<span class="w-[18px] h-[18px] flex-shrink-0">
@include('partials.sidebar-icon', ['icon' => $item['icon']])
</span>
<span class="nav-text truncate">{{ $item['label'] }}</span>
</a>
@endforeach
@if($workspace && count($accountNav))
<div class="nav-label px-3 pt-4 pb-1.5 text-[11px] uppercase tracking-widest font-medium text-t3">
Account
</div>
@endif
@foreach($accountNav as $item)
@php
$isActive = request()->routeIs($item['match']);
$url = Route::has($item['route']) ? route($item['route'], $item['params']) : '#';
@endphp
<a
href="{{ $url }}"
wire:navigate
data-nav-link="{{ $item['route'] }}"
data-label="{{ $item['label'] }}"
class="nav-item relative flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium w-full {{ $isActive ? 'nim-nav-active' : '' }}"
>
<span class="w-[18px] h-[18px] flex-shrink-0">
@include('partials.sidebar-icon', ['icon' => $item['icon']])
</span>
<span class="nav-text truncate">{{ $item['label'] }}</span>
@if($item['icon'] === 'ai')
<span class="nav-badge ml-auto text-[10px] px-1.5 py-0.5 rounded-full font-semibold bg-accent/10 text-accent">New</span>
@endif
</a>
@endforeach
{{-- Plan usage --}}
@if($workspace && $plan)
<div class="nav-plan-usage px-1 pt-4 pb-1">
<div class="p-3 rounded-xl bg-s2 border border-white/[.06]">
<div class="flex items-center justify-between mb-2">
<span class="text-xs font-medium text-t2">{{ ucfirst($plan->name) }} Plan</span>
@if(str_contains(strtolower($plan->name), 'free'))
<a href="{{ Route::has('w.billing.index') ? route('w.billing.index', $wsUlid) : '#' }}"
wire:navigate class="text-xs text-accent hover:underline">Upgrade</a>
@endif
</div>
<div class="flex justify-between mb-1.5">
<span class="text-xs text-t3">Links</span>
<span class="text-xs text-t2">{{ number_format($linksUsed) }} / {{ number_format($linkLimit) }}</span>
</div>
<div class="w-full h-1 rounded-full bg-s3">
<div class="h-1 rounded-full bg-accent transition-all" style="width: {{ $usagePct }}%"></div>
</div>
</div>
</div>
@endif
</nav>
{{-- ═══ Footer ═══ --}}
<footer class="flex-shrink-0 border-t border-white/[.06]">
{{-- Collapse toggle (desktop only) --}}
<button
@click="$store.sidebar.toggle()"
class="collapse-row max-md:hidden w-full flex items-center gap-3 px-[18px] py-2.5 text-sm text-t3 hover:text-t1 border-b border-white/[.06] transition-colors duration-150"
>
<svg class="w-4 h-4 flex-shrink-0 transition-transform duration-200"
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 18 9 12 15 6"/>
</svg>
<span class="collapse-text">Collapse</span>
</button>
{{-- User row --}}
@auth
<div class="user-row flex items-center gap-3 px-3.5 py-3 min-w-0">
<div class="flex-shrink-0 w-8 h-8 rounded-full bg-accent flex items-center justify-center text-white text-xs font-semibold">
{{ $initials ?: strtoupper(substr(auth()->user()->email, 0, 2)) }}
</div>
<div class="user-info flex-1 min-w-0">
<div class="text-sm font-medium text-t1 truncate">{{ auth()->user()->name }}</div>
<div class="text-xs text-t3 truncate">{{ auth()->user()->email }}</div>
</div>
<form class="user-logout-btn flex-shrink-0" method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" title="Sign out" aria-label="Sign out"
class="p-2 rounded-lg text-t3 hover:text-red-400 hover:bg-red-400/10 transition-colors">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
</button>
</form>
</div>
@endauth
</footer>
</aside>