Step 3/5: Workspace switcher dropdown + logout button
User: no logout button + clicking workspace switcher in sidebar did nothing.
Sidebar component:
- Workspace switcher now wraps in Alpine x-data={ wsOpen: false }.
Click toggles dropdown, @click.outside + escape-key close it.
- Dropdown lists all workspaces the user can access (owned +
TeamMember memberships), shows active one with accent checkmark.
Fallback: if list is empty, shows just the current workspace so
users never see an empty switcher.
- '+ Neuer Workspace' row at bottom of workspaces list.
- User section: shows auth user's name (header) + email.
- 'Profil' link → route('profile').
- 'Einstellungen' link → route('settings.index').
- 'Abmelden' button: POST form with CSRF to route('logout'),
red-colored row.
- Workspace switch via ?workspace=<slug> query param — middleware
ResolveCurrentWorkspace picks it up, persists in session +
user.current_workspace_id, then page reloads with new scope.
- Sidebar nav title now reads workspace name dynamically.
- Plan-box footer reads real plan + sites/quota + has 'Upgrade →'
link to settings.
CSS additions: clu-ws-dropdown (white 98% bg + glass shadow, z-index
60 to sit above sticky topbar 20 + sidebar 30), clu-ws-section
(uppercase muted header), clu-ws-item (hover bg + active accent
soft), clu-ws-divider (1px hairline). [x-cloak] global rule prevents
FOUC.
Browser-verified: clicking AC avatar + 'Acme Agency PRO' panel
expands dropdown with 'Acme Agency PRO · 87 sites' (current,
checkmark), Neuer Workspace, ADMIN section with email + Profil +
Einstellungen + red Abmelden. 0 console errors. 36/36 Page tests
green.
master
parent
43fb26e1ef
commit
4aa7d544b7
|
|
@ -634,6 +634,48 @@
|
|||
}
|
||||
.clu-top-link a { color: var(--color-accent); font-weight: 600; }
|
||||
|
||||
/* Workspace switcher dropdown */
|
||||
.clu-ws-dropdown {
|
||||
position: absolute;
|
||||
top: 100%; left: 0; right: 0;
|
||||
margin-top: 6px;
|
||||
background: rgba(255,255,255,0.98);
|
||||
border: 1px solid var(--color-glass-border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: 0 18px 50px -8px rgba(40,52,92,0.35), 0 4px 12px -4px rgba(40,52,92,0.15);
|
||||
backdrop-filter: blur(28px) saturate(170%);
|
||||
-webkit-backdrop-filter: blur(28px) saturate(170%);
|
||||
z-index: 60;
|
||||
padding: 6px;
|
||||
max-height: 480px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
[x-cloak] { display: none !important; }
|
||||
.clu-ws-section {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.09em;
|
||||
color: var(--color-muted-2);
|
||||
font-weight: 600;
|
||||
padding: 10px 10px 4px;
|
||||
}
|
||||
.clu-ws-item {
|
||||
display: flex; align-items: center; gap: 9px;
|
||||
padding: 7px 10px;
|
||||
border-radius: var(--radius-xs);
|
||||
color: var(--color-fg);
|
||||
transition: background .12s;
|
||||
text-decoration: none !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
.clu-ws-item:hover { background: rgba(255,255,255,0.7); }
|
||||
.clu-ws-item.active { background: var(--color-accent-soft); }
|
||||
.clu-ws-divider {
|
||||
height: 1px;
|
||||
background: var(--color-hairline);
|
||||
margin: 6px 4px;
|
||||
}
|
||||
|
||||
/* ───── App layout (sidebar fixed + main offset by padding) ───── */
|
||||
.clu-app-grid {
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,33 @@
|
|||
@props(['active' => null])
|
||||
|
||||
@php
|
||||
$ws = auth()->user()?->currentWorkspace;
|
||||
$wsInitials = $ws ? mb_strtoupper(mb_substr(preg_replace('/[^A-Za-z\s]/', '', $ws->name), 0, 2)) : '—';
|
||||
$user = auth()->user();
|
||||
$ws = $user?->currentWorkspace;
|
||||
$wsInitials = $ws ? mb_strtoupper(mb_substr(preg_replace('/[^A-Za-z\s]/', '', $ws->name), 0, 2)) : '—';
|
||||
$userInit = $user ? mb_strtoupper(mb_substr($user->name, 0, 1) . mb_substr(explode(' ', $user->name)[1] ?? '', 0, 1)) : '?';
|
||||
|
||||
// All workspaces the user can access: owned + via TeamMember.
|
||||
$workspaces = collect();
|
||||
if ($user) {
|
||||
$workspaces = $workspaces->merge($user->ownedWorkspaces);
|
||||
if (method_exists($user, 'teamMemberships')) {
|
||||
$workspaces = $workspaces->merge($user->teamMemberships->pluck('workspace'));
|
||||
} else {
|
||||
// Fallback via TeamMember model
|
||||
$memberWsIds = \App\Models\TeamMember::where('user_id', $user->id)->pluck('workspace_id');
|
||||
$workspaces = $workspaces->merge(\App\Models\Workspace::whereIn('id', $memberWsIds)->get());
|
||||
}
|
||||
$workspaces = $workspaces->unique('id')->values();
|
||||
// Fallback: at least the currently-active workspace.
|
||||
if ($workspaces->isEmpty() && $ws) {
|
||||
$workspaces = collect([$ws]);
|
||||
}
|
||||
}
|
||||
|
||||
$sitesUsed = $ws ? \App\Models\Site::count() : 0;
|
||||
$sitesQuota = $ws?->quotas['sites'] ?? 0;
|
||||
$planPct = $sitesQuota ? min(100, round($sitesUsed / $sitesQuota * 100)) : 0;
|
||||
|
||||
$items = [
|
||||
['name' => 'dashboard', 'label' => 'Dashboard', 'route' => 'dashboard',
|
||||
'svg' => '<path d="M3 13h8V3H3v10zm10 8h8V11h-8v10zM3 21h8v-6H3v6zm10-18v6h8V3h-8z"/>'],
|
||||
|
|
@ -31,22 +56,76 @@
|
|||
<div class="clu-brand-name">CluPilot<span>v2.4</span></div>
|
||||
</div>
|
||||
|
||||
<div class="clu-ws-switch" role="button" tabindex="0">
|
||||
<div class="clu-ws-avatar">{{ $wsInitials }}</div>
|
||||
<div class="clu-ws-meta">
|
||||
<div class="clu-ws-name">{{ $ws?->name ?? 'Workspace wählen' }}</div>
|
||||
<div class="clu-ws-plan">{{ strtoupper($ws?->plan ?? 'free') }} · {{ \App\Models\Site::count() }}/{{ $ws?->quotas['sites'] ?? 0 }} sites</div>
|
||||
{{-- Workspace switcher (Alpine dropdown) --}}
|
||||
<div class="relative" x-data="{ wsOpen: false }" @click.outside="wsOpen = false" @keydown.escape.window="wsOpen = false">
|
||||
<button type="button" class="clu-ws-switch w-full" @click="wsOpen = !wsOpen">
|
||||
<div class="clu-ws-avatar">{{ $wsInitials }}</div>
|
||||
<div class="clu-ws-meta">
|
||||
<div class="clu-ws-name">{{ $ws?->name ?? 'Workspace wählen' }}</div>
|
||||
<div class="clu-ws-plan">{{ strtoupper($ws?->plan ?? 'free') }} · {{ $sitesUsed }}/{{ $sitesQuota }} sites</div>
|
||||
</div>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-(--color-muted-2) flex-none" aria-hidden="true">
|
||||
<path d="M7 9l5-5 5 5M7 15l5 5 5-5"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div x-show="wsOpen" x-cloak
|
||||
x-transition:enter="transition ease-out duration-150"
|
||||
x-transition:enter-start="opacity-0 translate-y-[-4px]"
|
||||
x-transition:enter-end="opacity-100 translate-y-0"
|
||||
class="clu-ws-dropdown" style="display:none;">
|
||||
|
||||
<div class="clu-ws-section">WORKSPACES</div>
|
||||
@foreach ($workspaces as $w)
|
||||
<a href="{{ url('/dashboard?workspace=' . $w->slug) }}"
|
||||
class="clu-ws-item @if($ws && $ws->id === $w->id) active @endif">
|
||||
<div class="clu-ws-avatar" style="width:20px;height:20px;font-size:9px;border-radius:5px;">
|
||||
{{ mb_strtoupper(mb_substr(preg_replace('/[^A-Za-z\s]/', '', $w->name), 0, 2)) }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-[12.5px] font-semibold truncate">{{ $w->name }}</div>
|
||||
<div class="text-[10.5px] text-(--color-muted) font-mono">{{ strtoupper($w->plan) }} · {{ $w->sites()->count() }} sites</div>
|
||||
</div>
|
||||
@if ($ws && $ws->id === $w->id)
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" class="text-(--color-accent) flex-none"><path d="M5 12l5 5L20 7"/></svg>
|
||||
@endif
|
||||
</a>
|
||||
@endforeach
|
||||
|
||||
<a href="#" class="clu-ws-item">
|
||||
<div class="clu-ws-avatar" style="width:20px;height:20px;font-size:14px;border-radius:5px;background:rgba(30,35,60,0.08);color:var(--color-muted);">+</div>
|
||||
<span class="text-[12.5px] font-medium">Neuer Workspace</span>
|
||||
</a>
|
||||
|
||||
<div class="clu-ws-divider"></div>
|
||||
<div class="clu-ws-section">{{ $user?->name ?? 'Konto' }}</div>
|
||||
<div class="text-[11px] text-(--color-muted) font-mono px-[10px] pb-[6px] truncate">{{ $user?->email }}</div>
|
||||
|
||||
<a href="{{ route('profile') }}" wire:navigate class="clu-ws-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-(--color-muted) flex-none"><circle cx="12" cy="8" r="4"/><path d="M4 21v-2c0-3 3-5 8-5s8 2 8 5v2"/></svg>
|
||||
<span class="text-[12.5px]">Profil</span>
|
||||
</a>
|
||||
<a href="{{ route('settings.index') }}" wire:navigate class="clu-ws-item">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-(--color-muted) flex-none"><circle cx="12" cy="12" r="3"/><path d="M12 2v3M12 19v3M2 12h3M19 12h3"/></svg>
|
||||
<span class="text-[12.5px]">Einstellungen</span>
|
||||
</a>
|
||||
|
||||
<div class="clu-ws-divider"></div>
|
||||
<form method="POST" action="{{ route('logout') }}" class="block">
|
||||
@csrf
|
||||
<button type="submit" class="clu-ws-item w-full" style="color:var(--color-danger);">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="flex-none"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>
|
||||
<span class="text-[12.5px]">Abmelden</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="text-(--color-muted-2) flex-none" aria-hidden="true">
|
||||
<path d="M7 9l5-5 5 5M7 15l5 5 5-5"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Scrollable nav --}}
|
||||
<div class="clu-sidebar-scroll">
|
||||
<nav class="clu-nav">
|
||||
<div class="clu-nav-title">Cluster</div>
|
||||
<div class="clu-nav-title">{{ $ws?->name ?? 'Cluster' }}</div>
|
||||
@foreach ($items as $item)
|
||||
@php
|
||||
$isActive = ($active === $item['route']) || ($active === $item['name']);
|
||||
|
|
@ -71,13 +150,13 @@
|
|||
<div class="clu-sidebar-foot">
|
||||
<div class="clu-plan-box">
|
||||
<div class="clu-plan-row">
|
||||
<span class="clu-plan-name">PRO</span>
|
||||
<span class="clu-plan-tag">62% USED</span>
|
||||
<span class="clu-plan-name">{{ strtoupper($ws?->plan ?? 'FREE') }}</span>
|
||||
<span class="clu-plan-tag">{{ $planPct }}% USED</span>
|
||||
</div>
|
||||
<div class="clu-plan-meter"><span style="width:62%;"></span></div>
|
||||
<div class="clu-plan-meter"><span style="width:{{ $planPct }}%;"></span></div>
|
||||
<div class="clu-plan-stats">
|
||||
<span>87 / 100 sites</span>
|
||||
<span>€249/mo</span>
|
||||
<span>{{ $sitesUsed }} / {{ $sitesQuota }} sites</span>
|
||||
<a href="{{ route('settings.index') }}" wire:navigate class="text-(--color-accent) hover:underline">Upgrade →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue