From 4aa7d544b729c5b3409298a1d47289879923e6cb Mon Sep 17 00:00:00 2001 From: Boban Blaskovic Date: Mon, 1 Jun 2026 01:26:10 +0200 Subject: [PATCH] Step 3/5: Workspace switcher dropdown + logout button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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= 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. --- app/resources/css/app.css | 42 +++++++ .../views/components/clu/sidebar.blade.php | 111 +++++++++++++++--- 2 files changed, 137 insertions(+), 16 deletions(-) diff --git a/app/resources/css/app.css b/app/resources/css/app.css index bd05ae6..1cdcfdc 100644 --- a/app/resources/css/app.css +++ b/app/resources/css/app.css @@ -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; diff --git a/app/resources/views/components/clu/sidebar.blade.php b/app/resources/views/components/clu/sidebar.blade.php index dcdede9..51b52df 100644 --- a/app/resources/views/components/clu/sidebar.blade.php +++ b/app/resources/views/components/clu/sidebar.blade.php @@ -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' => ''], @@ -31,22 +56,76 @@
CluPilotv2.4
-
-
{{ $wsInitials }}
-
-
{{ $ws?->name ?? 'Workspace wählen' }}
-
{{ strtoupper($ws?->plan ?? 'free') }} · {{ \App\Models\Site::count() }}/{{ $ws?->quotas['sites'] ?? 0 }} sites
+ {{-- Workspace switcher (Alpine dropdown) --}} +
+ + + -
{{-- Scrollable nav --}}