253 lines
15 KiB
PHP
253 lines
15 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="transition: width .28s cubic-bezier(.4,0,.2,1), transform .28s cubic-bezier(.4,0,.2,1);"
|
|
:style="$store.sidebar?.mobileOpen ? 'transform: translateX(0)' : null"
|
|
>
|
|
|
|
{{-- ═══ Header ═══ --}}
|
|
<header class="nim-sb-header flex-shrink-0 px-3.5 py-3 border-b" style="border-color:var(--nim-border); background:linear-gradient(180deg,rgba(255,255,255,0.015),transparent);" @click.outside="wsOpen = false">
|
|
<button
|
|
@click="wsOpen = !wsOpen"
|
|
:aria-expanded="wsOpen.toString()"
|
|
class="ws-trigger w-full flex items-center gap-3 px-2.5 py-2 rounded-[10px] transition-colors duration-150"
|
|
style="hover:background:var(--nim-surface-3)"
|
|
onmouseenter="this.style.background='var(--nim-surface-3)'"
|
|
onmouseleave="this.style.background=''"
|
|
>
|
|
<span class="ws-logo flex-shrink-0 w-9 h-9 rounded-[9px] flex items-center justify-center text-white font-bold text-[18px] relative overflow-hidden"
|
|
style="background:var(--nim-brand-grad);box-shadow:0 6px 16px -6px rgba(168,85,247,.55),inset 0 1px 0 rgba(255,255,255,.25)">
|
|
<span style="position:relative;z-index:1">N</span>
|
|
<span style="position:absolute;inset:0;background:radial-gradient(circle at 30% 20%,rgba(255,255,255,.35),transparent 50%);pointer-events:none"></span>
|
|
</span>
|
|
<span class="ws-text flex-1 min-w-0 text-left" style="display:grid;line-height:1.2">
|
|
<span class="ws-brand font-bold text-[15px] tracking-tight"
|
|
style="background:var(--nim-brand-grad);-webkit-background-clip:text;background-clip:text;color:transparent">nimuli</span>
|
|
<span class="ws-name text-[12.5px] truncate" style="color:var(--nim-text-muted)">
|
|
{{ $workspace?->name ?? 'Select workspace' }}
|
|
</span>
|
|
</span>
|
|
<svg class="ws-chevron flex-shrink-0 w-4 h-4 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-4 p-2 rounded-lg transition-colors"
|
|
style="color:var(--nim-text-muted)"
|
|
onmouseenter="this.style.background='rgba(255,255,255,0.05)'"
|
|
onmouseleave="this.style.background=''">
|
|
<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 rounded-[10px] overflow-hidden border"
|
|
style="background:var(--nim-surface-2);border-color:var(--nim-border)"
|
|
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-3 text-[13.5px] transition-colors border-b"
|
|
style="border-color:var(--nim-border);{{ $workspace?->id === $ws->id ? 'color:var(--nim-text);font-weight:500' : 'color:var(--nim-text-muted)' }}"
|
|
onmouseenter="this.style.background='rgba(255,255,255,0.03)'"
|
|
onmouseleave="this.style.background=''"
|
|
>
|
|
<span class="w-[22px] h-[22px] rounded-[6px] flex items-center justify-center text-white font-bold text-[11px] flex-shrink-0"
|
|
style="background:var(--nim-brand-grad)">{{ strtoupper(substr($ws->name, 0, 1)) }}</span>
|
|
<span class="flex-1 truncate">{{ $ws->name }}</span>
|
|
@if($workspace?->id === $ws->id)
|
|
<svg class="w-4 h-4 flex-shrink-0" style="color:var(--nim-accent-2)"
|
|
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-3 text-xs" style="color:var(--nim-text-dim)">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-3 text-[13.5px] transition-colors"
|
|
style="color:var(--nim-text-muted)"
|
|
onmouseenter="this.style.background='rgba(255,255,255,0.03)';this.style.color='var(--nim-text)'"
|
|
onmouseleave="this.style.background='';this.style.color='var(--nim-text-muted)'"
|
|
>
|
|
<svg class="w-[18px] h-[18px] 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-2 pb-1.5 text-[11px] uppercase tracking-[.08em] font-medium" style="color:var(--nim-text-dim)">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-[9px] 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 flex-1 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-[.08em] font-medium" style="color:var(--nim-text-dim)">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-[9px] 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 flex-1 truncate">{{ $item['label'] }}</span>
|
|
@if($item['icon'] === 'ai')
|
|
<span class="nav-badge text-[10.5px] px-1.5 py-0.5 rounded-full font-semibold tracking-wide"
|
|
style="background:var(--nim-accent-bg-strong);color:var(--nim-accent-2)">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 border" style="background:var(--nim-surface-2);border-color:var(--nim-border)">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="text-xs font-medium" style="color:var(--nim-text-muted)">{{ 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 hover:underline" style="color:var(--nim-accent-2)">Upgrade</a>
|
|
@endif
|
|
</div>
|
|
<div class="flex justify-between mb-1.5">
|
|
<span class="text-xs" style="color:var(--nim-text-dim)">Links</span>
|
|
<span class="text-xs" style="color:var(--nim-text-muted)">{{ number_format($linksUsed) }} / {{ number_format($linkLimit) }}</span>
|
|
</div>
|
|
<div class="w-full h-1 rounded-full" style="background:var(--nim-surface-3)">
|
|
<div class="h-1 rounded-full transition-all" style="width:{{ $usagePct }}%;background:var(--nim-accent)"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
</nav>
|
|
|
|
{{-- ═══ Footer ═══ --}}
|
|
<footer class="nim-sb-footer flex-shrink-0 border-t" style="border-color:var(--nim-border);background:linear-gradient(0deg,rgba(255,255,255,.012),transparent)">
|
|
|
|
{{-- Collapse toggle (desktop only) --}}
|
|
<button
|
|
@click="$store.sidebar.toggle()"
|
|
class="collapse-row max-md:hidden w-full flex items-center gap-2.5 px-[18px] py-2.5 text-[13px] border-b transition-colors duration-150"
|
|
style="color:var(--nim-text-dim);border-color:var(--nim-border)"
|
|
onmouseenter="this.style.color='var(--nim-text)'"
|
|
onmouseleave="this.style.color='var(--nim-text-dim)'"
|
|
>
|
|
<svg class="w-4 h-4 flex-shrink-0 transition-transform duration-250" 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.5 min-w-0">
|
|
<div class="nim-avatar flex-shrink-0 w-9 h-9 rounded-full flex items-center justify-center text-white font-semibold text-[13px]"
|
|
style="background:linear-gradient(135deg,#8b5cf6,#6366f1);box-shadow:inset 0 1px 0 rgba(255,255,255,.18)">
|
|
{{ $initials ?: strtoupper(substr(auth()->user()->email, 0, 2)) }}
|
|
</div>
|
|
<div class="user-info flex-1 min-w-0" style="line-height:1.2">
|
|
<div class="text-[13.5px] font-semibold truncate" style="color:var(--nim-text)">{{ auth()->user()->name }}</div>
|
|
<div class="text-[12px] truncate" style="color:var(--nim-text-dim)">{{ 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 transition-colors duration-150"
|
|
style="color:var(--nim-text-dim)">
|
|
<svg class="w-[18px] h-[18px]" 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>
|