fix: bugs 1,4,5,7,8,9 — sidebar icon, theme button, copy-link, UTM computed, workspace context, theme sync

Bug 1: sidebar toggle — single rotating SVG → two x-show SVGs (chevron-left / chevron-right)
Bug 4: theme button — template x-if → svg x-show x-cloak; apply() now resolves 'system'
  to dark/light before setting data-theme (was setting data-theme="system" → CSS ignored it)
Bug 5: x-ui.copy-button added to links, bio, qr-codes rows and api-tokens new-token display
Bug 7: UtmBuilder::getFullUrlProperty() (Livewire v2 magic) → #[Computed] fullUrl()
  (Livewire v3 dropped getXProperty magic — $fullUrl was undefined in blade)
Bug 8: require_workspace() moved from render() to mount() in Overview, Links/Index, Settings/Index
  (Livewire update requests hit /livewire/update — ResolveWorkspace middleware never ran → threw)
Bug 9: Preferences::save() dispatches theme-changed-server → layout listener calls
  Alpine.store('theme').apply(theme) so DOM updates immediately without reload

Bugs 2, 3, 6 already correct in codebase (layout had transition/ml-60 binding,
mobile sidebar used $store.sidebar.open, routes used ULID via model route key).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 17:30:44 +02:00
parent 43ff6d6407
commit 28fbcba227
14 changed files with 81 additions and 40 deletions

View File

@ -3,9 +3,9 @@
namespace App\Livewire\Modals; namespace App\Livewire\Modals;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Attributes\Computed;
use LivewireUI\Modal\ModalComponent; use LivewireUI\Modal\ModalComponent;
/** @property-read string $fullUrl */
class UtmBuilder extends ModalComponent class UtmBuilder extends ModalComponent
{ {
public string $baseUrl = ''; public string $baseUrl = '';
@ -22,7 +22,8 @@ class UtmBuilder extends ModalComponent
public bool $copied = false; public bool $copied = false;
public function getFullUrlProperty(): string #[Computed]
public function fullUrl(): string
{ {
if (! $this->baseUrl) { if (! $this->baseUrl) {
return ''; return '';

View File

@ -9,27 +9,38 @@ use Livewire\Component;
class Overview extends Component class Overview extends Component
{ {
public function render(): View public int $workspaceId = 0;
public string $workspaceUlid = '';
public function mount(): void
{ {
$workspace = require_workspace(); $workspace = require_workspace();
$this->workspaceId = $workspace->id;
$this->workspaceUlid = $workspace->ulid;
}
public function render(): View
{
$wsId = $this->workspaceId;
// Hero metrics // Hero metrics
$today = Click::where('workspace_id', $workspace->id) $today = Click::where('workspace_id', $wsId)
->whereDate('clicked_at', today()) ->whereDate('clicked_at', today())
->count(); ->count();
$last7 = Click::where('workspace_id', $workspace->id) $last7 = Click::where('workspace_id', $wsId)
->where('clicked_at', '>=', now()->subDays(7)) ->where('clicked_at', '>=', now()->subDays(7))
->count(); ->count();
$last30 = Click::where('workspace_id', $workspace->id) $last30 = Click::where('workspace_id', $wsId)
->where('clicked_at', '>=', now()->subDays(30)) ->where('clicked_at', '>=', now()->subDays(30))
->count(); ->count();
$totalLinks = Link::where('workspace_id', $workspace->id)->count(); $totalLinks = Link::where('workspace_id', $wsId)->count();
// Chart data: daily clicks for last 30 days // Chart data: daily clicks for last 30 days
$chartData = Click::where('workspace_id', $workspace->id) $chartData = Click::where('workspace_id', $wsId)
->where('clicked_at', '>=', now()->subDays(29)->startOfDay()) ->where('clicked_at', '>=', now()->subDays(29)->startOfDay())
->selectRaw('DATE(clicked_at) as date, COUNT(*) as total') ->selectRaw('DATE(clicked_at) as date, COUNT(*) as total')
->groupBy('date') ->groupBy('date')
@ -45,7 +56,7 @@ class Overview extends Component
} }
// Top links by clicks // Top links by clicks
$topLinks = Link::where('workspace_id', $workspace->id) $topLinks = Link::where('workspace_id', $wsId)
->withCount(['clicks as total_clicks']) ->withCount(['clicks as total_clicks'])
->orderByDesc('total_clicks') ->orderByDesc('total_clicks')
->limit(5) ->limit(5)

View File

@ -16,6 +16,13 @@ class Index extends Component
public string $statusFilter = ''; public string $statusFilter = '';
public int $workspaceId = 0;
public function mount(): void
{
$this->workspaceId = require_workspace()->id;
}
public function updatingSearch(): void public function updatingSearch(): void
{ {
$this->resetPage(); $this->resetPage();
@ -36,9 +43,7 @@ class Index extends Component
public function render(): View public function render(): View
{ {
$workspace = require_workspace(); $links = Link::where('workspace_id', $this->workspaceId)
$links = Link::where('workspace_id', $workspace->id)
->when($this->search, fn ($q) => $q->where(function ($q) { ->when($this->search, fn ($q) => $q->where(function ($q) {
$q->where('title', 'like', "%{$this->search}%") $q->where('title', 'like', "%{$this->search}%")
->orWhere('slug', 'like', "%{$this->search}%") ->orWhere('slug', 'like', "%{$this->search}%")

View File

@ -29,6 +29,7 @@ class Preferences extends Component
'theme' => $this->theme, 'theme' => $this->theme,
]); ]);
$this->dispatch('theme-changed-server', theme: $this->theme);
session()->flash('status', 'Preferences saved.'); session()->flash('status', 'Preferences saved.');
} }

View File

@ -11,9 +11,12 @@ class Index extends Component
public string $workspaceSlug = ''; public string $workspaceSlug = '';
public int $workspaceId = 0;
public function mount(): void public function mount(): void
{ {
$ws = current_workspace(); $ws = require_workspace();
$this->workspaceId = $ws->id;
$this->workspaceName = $ws->name; $this->workspaceName = $ws->name;
$this->workspaceSlug = $ws->slug; $this->workspaceSlug = $ws->slug;
} }
@ -25,9 +28,10 @@ class Index extends Component
'workspaceSlug' => 'nullable|string|max:50|alpha_dash', 'workspaceSlug' => 'nullable|string|max:50|alpha_dash',
]); ]);
require_workspace()->update([ $ws = \App\Domains\Workspace\Models\Workspace::findOrFail($this->workspaceId);
$ws->update([
'name' => $this->workspaceName, 'name' => $this->workspaceName,
'slug' => $this->workspaceSlug ?: require_workspace()->slug, 'slug' => $this->workspaceSlug ?: $ws->slug,
]); ]);
session()->flash('status', 'Workspace settings saved.'); session()->flash('status', 'Workspace settings saved.');

View File

@ -46,9 +46,12 @@ document.addEventListener('alpine:init', () => {
init() { this.apply(this.current); }, init() { this.apply(this.current); },
apply(theme) { apply(theme) {
this.current = theme; this.current = theme;
document.documentElement.dataset.theme = theme; const actual = theme === 'system'
? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
: theme;
document.documentElement.dataset.theme = actual;
localStorage.setItem('nimuli_theme', theme); localStorage.setItem('nimuli_theme', theme);
document.cookie = 'nimuli_theme=' + theme + '; path=/; max-age=31536000; SameSite=Lax'; document.cookie = 'nimuli_theme=' + actual + '; path=/; max-age=31536000; SameSite=Lax';
}, },
cycle() { cycle() {
const order = ['dark', 'light', 'system']; const order = ['dark', 'light', 'system'];

View File

@ -244,10 +244,13 @@
:title="$store.sidebar.collapsed ? 'Expand sidebar' : 'Collapse sidebar'" :title="$store.sidebar.collapsed ? 'Expand sidebar' : 'Collapse sidebar'"
class="flex items-center gap-3 w-full px-3 py-2 rounded-lg text-t3 hover:text-t1 hover:bg-s2 transition-colors" class="flex items-center gap-3 w-full px-3 py-2 rounded-lg text-t3 hover:text-t1 hover:bg-s2 transition-colors"
> >
<span class="w-4 h-4 flex-shrink-0 transition-transform duration-200" :class="$store.sidebar.collapsed ? 'rotate-180' : ''"> <span class="w-4 h-4 flex-shrink-0">
<svg fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5"> <svg x-show="!$store.sidebar.collapsed" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 3L5 8l5 5"/> <path stroke-linecap="round" stroke-linejoin="round" d="M10 3L5 8l5 5"/>
</svg> </svg>
<svg x-show="$store.sidebar.collapsed" x-cloak fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 3l5 5-5 5"/>
</svg>
</span> </span>
<span x-show="!$store.sidebar.collapsed" class="text-xs">Collapse</span> <span x-show="!$store.sidebar.collapsed" class="text-xs">Collapse</span>
</button> </button>

View File

@ -36,23 +36,17 @@
:title="'Theme: ' + $store.theme.current" :title="'Theme: ' + $store.theme.current"
aria-label="Toggle theme" aria-label="Toggle theme"
> >
<template x-if="$store.theme.current === 'dark'"> <svg x-show="$store.theme.current === 'dark'" class="w-4 h-4" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<svg class="w-4 h-4" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M3.5 3.5l1 1M11.5 11.5l1 1M11.5 4.5l1-1M3.5 12.5l1-1"/> <path stroke-linecap="round" stroke-linejoin="round" d="M8 1v1.5M8 13.5V15M1 8h1.5M13.5 8H15M3.5 3.5l1 1M11.5 11.5l1 1M11.5 4.5l1-1M3.5 12.5l1-1"/>
<circle cx="8" cy="8" r="3"/> <circle cx="8" cy="8" r="3"/>
</svg> </svg>
</template> <svg x-show="$store.theme.current === 'light'" x-cloak class="w-4 h-4" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<template x-if="$store.theme.current === 'light'">
<svg class="w-4 h-4" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M8 3a5 5 0 100 10A5 5 0 008 3z" clip-rule="evenodd"/> <path stroke-linecap="round" stroke-linejoin="round" d="M8 3a5 5 0 100 10A5 5 0 008 3z" clip-rule="evenodd"/>
<path stroke-linecap="round" stroke-linejoin="round" d="M11.5 4.5a5 5 0 01-7 7A5 5 0 0111.5 4.5z"/> <path stroke-linecap="round" stroke-linejoin="round" d="M11.5 4.5a5 5 0 01-7 7A5 5 0 0111.5 4.5z"/>
</svg> </svg>
</template> <svg x-show="$store.theme.current === 'system'" x-cloak class="w-4 h-4" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<template x-if="$store.theme.current === 'system'">
<svg class="w-4 h-4" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<rect x="2" y="2" width="12" height="9" rx="1.5"/><path stroke-linecap="round" stroke-linejoin="round" d="M5.5 14.5h5M8 11.5v3"/> <rect x="2" y="2" width="12" height="9" rx="1.5"/><path stroke-linecap="round" stroke-linejoin="round" d="M5.5 14.5h5M8 11.5v3"/>
</svg> </svg>
</template>
</button> </button>
<!-- Notifications Bell --> <!-- Notifications Bell -->

View File

@ -165,6 +165,16 @@
document.addEventListener('DOMContentLoaded', updateActiveNav); document.addEventListener('DOMContentLoaded', updateActiveNav);
document.addEventListener('livewire:navigated', updateActiveNav); document.addEventListener('livewire:navigated', updateActiveNav);
// Sync server-saved theme to Alpine store + DOM
document.addEventListener('livewire:init', function () {
Livewire.on('theme-changed-server', function (event) {
var theme = event.theme ?? (Array.isArray(event) ? event[0]?.theme : null);
if (theme && window.Alpine && Alpine.store('theme')) {
Alpine.store('theme').apply(theme);
}
});
});
// copy-to-clipboard Livewire event // copy-to-clipboard Livewire event
document.addEventListener('livewire:init', function () { document.addEventListener('livewire:init', function () {
Livewire.on('copy-to-clipboard', function (event) { Livewire.on('copy-to-clipboard', function (event) {

View File

@ -36,6 +36,7 @@
</span> </span>
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150"> <div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150">
<x-ui.copy-button :value="'https://nimu.li/b/'.$page->slug" label="Copy" />
<button <button
@click="$dispatch('openModal', {component: 'modals.edit-bio-page', arguments: {bioUlid: '{{ $page->ulid }}'}})" @click="$dispatch('openModal', {component: 'modals.edit-bio-page', arguments: {bioUlid: '{{ $page->ulid }}'}})"
class="text-t2 text-xs hover:text-t1 transition-colors">Edit</button> class="text-t2 text-xs hover:text-t1 transition-colors">Edit</button>

View File

@ -6,18 +6,18 @@
<p class="text-xs text-t3 mt-0.5">{{ now()->format('l, F j') }}</p> <p class="text-xs text-t3 mt-0.5">{{ now()->format('l, F j') }}</p>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a href="{{ route('w.analytics.index', request()->route('workspace')) }}" <a href="{{ route('w.analytics.index', $workspaceUlid) }}"
wire:navigate wire:navigate
class="px-3 py-2 text-xs text-t2 hover:text-t1 transition-colors flex items-center gap-1.5"> class="px-3 py-2 text-xs text-t2 hover:text-t1 transition-colors flex items-center gap-1.5">
<x-heroicon-o-arrow-trending-up class="w-3.5 h-3.5" /> <x-heroicon-o-arrow-trending-up class="w-3.5 h-3.5" />
Analytics Analytics
</a> </a>
<a href="{{ route('w.qr.index', request()->route('workspace')) }}" <a href="{{ route('w.qr.index', $workspaceUlid) }}"
wire:navigate wire:navigate
class="px-3 py-2 bg-s2 border border-white/[.06] text-t1 rounded-lg text-xs font-medium hover:bg-s3 transition-colors"> class="px-3 py-2 bg-s2 border border-white/[.06] text-t1 rounded-lg text-xs font-medium hover:bg-s3 transition-colors">
+ QR Code + QR Code
</a> </a>
<a href="{{ route('w.links.index', request()->route('workspace')) }}" <a href="{{ route('w.links.index', $workspaceUlid) }}"
wire:navigate wire:navigate
class="px-3 py-2 bg-accent-gradient text-white rounded-lg text-xs font-semibold hover:opacity-90 transition-opacity"> class="px-3 py-2 bg-accent-gradient text-white rounded-lg text-xs font-semibold hover:opacity-90 transition-opacity">
+ New Link + New Link
@ -129,7 +129,7 @@
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden card-shadow nim-reveal nim-reveal-5"> <div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden card-shadow nim-reveal nim-reveal-5">
<div class="px-5 py-3.5 border-b border-white/[.06] flex items-center justify-between"> <div class="px-5 py-3.5 border-b border-white/[.06] flex items-center justify-between">
<div class="text-xs font-semibold text-t1 tracking-wide uppercase">Top Links</div> <div class="text-xs font-semibold text-t1 tracking-wide uppercase">Top Links</div>
<a href="{{ route('w.links.index', request()->route('workspace')) }}" wire:navigate <a href="{{ route('w.links.index', $workspaceUlid) }}" wire:navigate
class="text-xs text-t3 hover:text-t1 transition-colors">View all </a> class="text-xs text-t3 hover:text-t1 transition-colors">View all </a>
</div> </div>
<table class="w-full"> <table class="w-full">

View File

@ -100,6 +100,7 @@
{{-- Actions --}} {{-- Actions --}}
<td class="px-4 py-3 text-right"> <td class="px-4 py-3 text-right">
<div class="flex items-center justify-end gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150"> <div class="flex items-center justify-end gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150">
<x-ui.copy-button :value="'https://nimu.li/'.$link->slug" label="Copy" />
<button <button
@click="$dispatch('openModal', {component: 'modals.link-variants', arguments: {linkUlid: '{{ $link->ulid }}'}})" @click="$dispatch('openModal', {component: 'modals.link-variants', arguments: {linkUlid: '{{ $link->ulid }}'}})"
class="text-t3 text-xs hover:text-blue transition-colors flex items-center gap-1"> class="text-t3 text-xs hover:text-blue transition-colors flex items-center gap-1">

View File

@ -48,6 +48,10 @@
</div> </div>
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150 flex-shrink-0"> <div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150 flex-shrink-0">
@php $qrUrl = $qr->payload['data'] ?? ($qr->payload['url'] ?? null); @endphp
@if($qrUrl)
<x-ui.copy-button :value="$qrUrl" label="Copy URL" />
@endif
<button <button
@click="$dispatch('openModal', {component: 'modals.edit-qr-code', arguments: {qrUlid: '{{ $qr->ulid }}'}})" @click="$dispatch('openModal', {component: 'modals.edit-qr-code', arguments: {qrUlid: '{{ $qr->ulid }}'}})"
class="text-t2 text-xs hover:text-t1 transition-colors">Edit</button> class="text-t2 text-xs hover:text-t1 transition-colors">Edit</button>

View File

@ -18,6 +18,9 @@
<div class="flex-1"> <div class="flex-1">
<p class="text-sm font-medium text-green mb-2">Token created copy it now, it won't be shown again</p> <p class="text-sm font-medium text-green mb-2">Token created copy it now, it won't be shown again</p>
<code class="block font-mono text-xs text-t1 bg-s3 px-3 py-2 rounded-lg break-all">{{ $newToken }}</code> <code class="block font-mono text-xs text-t1 bg-s3 px-3 py-2 rounded-lg break-all">{{ $newToken }}</code>
<div class="mt-2">
<x-ui.copy-button :value="$newToken" label="Copy Token" />
</div>
</div> </div>
<button wire:click="dismissNewToken" class="text-t3 hover:text-t1 flex-shrink-0"><x-heroicon-o-x-mark class="w-4 h-4" /></button> <button wire:click="dismissNewToken" class="text-t3 hover:text-t1 flex-shrink-0"><x-heroicon-o-x-mark class="w-4 h-4" /></button>
</div> </div>