fix(FIX-12): 10 bugs — AI workspace context, Alpine sidebar/theme, analytics clearFilters, link badge layout, slug suggest

BUG 1: Insights/Anomalies called require_workspace() in action methods — add mount() cache
BUG 2: Analytics clear-filter used multiple $set() calls — replace with clearFilters() action
BUG 3: Static md:ml-60 on main div blocked Alpine :class md:ml-16 override — remove static
BUG 4: updateActiveNav missed icon <span> colors on @persist sidebar — update icon span too
BUG 5: Topbar <header> lacked x-data — Alpine x-show on theme SVGs not reactive
BUG 6: --color-t2 #8888a0 too dark in dark mode — raise to #b8b8d0
BUG 7: UtmBuilder::createLink() used current_workspace() in AJAX — pass workspaceUlid via mount
BUG 8: Clicks badge in separate column, not rightmost — merge into actions cell
BUG 9: Sidebar nav icons w-4 h-4 — increase to w-5 h-5, justify-center when collapsed
BUG 10: suggestSlugs() no try-catch; text-purple undefined — add guard + use text-accent

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 18:02:23 +02:00
parent 28fbcba227
commit e211770312
13 changed files with 98 additions and 47 deletions

View File

@ -74,13 +74,20 @@ class CreateLink extends ModalComponent
public function suggestSlugs(): void public function suggestSlugs(): void
{ {
if (! $this->targetUrl) { if (! filter_var($this->targetUrl, FILTER_VALIDATE_URL)) {
$this->dispatch('toast', message: 'Bitte zuerst eine gültige URL eingeben', type: 'warning');
return; return;
} }
$this->aiLoading = true; $this->aiLoading = true;
try {
$this->slugSuggestions = app(SlugSuggester::class)->suggest($this->targetUrl); $this->slugSuggestions = app(SlugSuggester::class)->suggest($this->targetUrl);
} catch (\Exception $e) {
$this->dispatch('toast', message: 'AI-Fehler: '.$e->getMessage(), type: 'error');
} finally {
$this->aiLoading = false; $this->aiLoading = false;
} }
}
public function pickSuggestion(string $slug): void public function pickSuggestion(string $slug): void
{ {

View File

@ -6,8 +6,11 @@ use Illuminate\View\View;
use Livewire\Attributes\Computed; 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 $workspaceUlid = '';
public string $baseUrl = ''; public string $baseUrl = '';
public string $utmSource = ''; public string $utmSource = '';
@ -22,6 +25,11 @@ class UtmBuilder extends ModalComponent
public bool $copied = false; public bool $copied = false;
public function mount(string $workspaceUlid = ''): void
{
$this->workspaceUlid = $workspaceUlid;
}
#[Computed] #[Computed]
public function fullUrl(): string public function fullUrl(): string
{ {
@ -55,11 +63,15 @@ class UtmBuilder extends ModalComponent
public function createLink(): void public function createLink(): void
{ {
$url = $this->fullUrl; $url = $this->fullUrl;
$this->closeModal(); $wsUlid = $this->workspaceUlid;
if (! $wsUlid) {
$ws = current_workspace(); $ws = current_workspace();
if ($ws) { $wsUlid = $ws !== null ? $ws->ulid : '';
}
$this->closeModal();
if ($wsUlid) {
$this->dispatch('openModal', component: 'modals.create-link', arguments: [ $this->dispatch('openModal', component: 'modals.create-link', arguments: [
'workspaceUlid' => $ws->ulid, 'workspaceUlid' => $wsUlid,
'prefillUrl' => $url, 'prefillUrl' => $url,
]); ]);
} }

View File

@ -4,6 +4,7 @@ namespace App\Livewire\Pages\Ai;
use App\Domains\Ai\Services\AiService; use App\Domains\Ai\Services\AiService;
use App\Domains\Link\Models\Link; use App\Domains\Link\Models\Link;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Component; use Livewire\Component;
@ -13,9 +14,16 @@ class Anomalies extends Component
public bool $loading = false; public bool $loading = false;
public int $workspaceId = 0;
public function mount(): void
{
$this->workspaceId = require_workspace()->id;
}
public function detect(): void public function detect(): void
{ {
$workspace = require_workspace(); $workspace = Workspace::findOrFail($this->workspaceId);
$this->loading = true; $this->loading = true;
$links = Link::where('workspace_id', $workspace->id) $links = Link::where('workspace_id', $workspace->id)

View File

@ -4,6 +4,7 @@ namespace App\Livewire\Pages\Ai;
use App\Domains\Ai\Services\AiService; use App\Domains\Ai\Services\AiService;
use App\Domains\Link\Models\Link; use App\Domains\Link\Models\Link;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Component; use Livewire\Component;
@ -13,9 +14,16 @@ class Insights extends Component
public bool $loading = false; public bool $loading = false;
public int $workspaceId = 0;
public function mount(): void
{
$this->workspaceId = require_workspace()->id;
}
public function generate(): void public function generate(): void
{ {
$workspace = require_workspace(); $workspace = Workspace::findOrFail($this->workspaceId);
$this->loading = true; $this->loading = true;
$topLinks = Link::where('workspace_id', $workspace->id) $topLinks = Link::where('workspace_id', $workspace->id)

View File

@ -46,6 +46,13 @@ class Index extends Component
$this->range = in_array($range, ['24h', '7d', '30d', '90d']) ? $range : '7d'; $this->range = in_array($range, ['24h', '7d', '30d', '90d']) ? $range : '7d';
} }
public function clearFilters(): void
{
$this->country = '';
$this->device = '';
$this->linkId = '';
}
private function rangeStart(): Carbon private function rangeStart(): Carbon
{ {
return match ($this->range) { return match ($this->range) {

View File

@ -2,6 +2,7 @@
namespace App\Livewire\Pages\Settings; namespace App\Livewire\Pages\Settings;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View; use Illuminate\View\View;
use Livewire\Component; use Livewire\Component;
@ -28,7 +29,7 @@ class Index extends Component
'workspaceSlug' => 'nullable|string|max:50|alpha_dash', 'workspaceSlug' => 'nullable|string|max:50|alpha_dash',
]); ]);
$ws = \App\Domains\Workspace\Models\Workspace::findOrFail($this->workspaceId); $ws = Workspace::findOrFail($this->workspaceId);
$ws->update([ $ws->update([
'name' => $this->workspaceName, 'name' => $this->workspaceName,
'slug' => $this->workspaceSlug ?: $ws->slug, 'slug' => $this->workspaceSlug ?: $ws->slug,

View File

@ -46,7 +46,7 @@
--color-b1: rgb(255 255 255 / 8%); --color-b1: rgb(255 255 255 / 8%);
--color-b2: rgb(255 255 255 / 14%); --color-b2: rgb(255 255 255 / 14%);
--color-t1: #f0f0f5; --color-t1: #f0f0f5;
--color-t2: #8888a0; --color-t2: #b8b8d0;
--color-t3: #44445a; --color-t3: #44445a;
/* Accent — purple-pink gradient, consistent across both themes */ /* Accent — purple-pink gradient, consistent across both themes */

View File

@ -184,12 +184,13 @@
wire:navigate wire:navigate
data-nav-link="{{ $item['route'] }}" data-nav-link="{{ $item['route'] }}"
:title="$store.sidebar.collapsed ? '{{ $item['label'] }}' : ''" :title="$store.sidebar.collapsed ? '{{ $item['label'] }}' : ''"
class="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors group :class="$store.sidebar.collapsed ? 'justify-center px-2 gap-0' : 'gap-3 px-3'"
class="flex items-center py-2 rounded-lg text-sm transition-colors group
{{ $isActive {{ $isActive
? 'bg-blue/10 text-blue' ? 'bg-blue/10 text-blue'
: 'text-t2 hover:text-t1 hover:bg-s2' }}" : 'text-t2 hover:text-t1 hover:bg-s2' }}"
> >
<span class="w-4 h-4 flex-shrink-0 {{ $isActive ? 'text-blue' : 'text-t3 group-hover:text-t2' }}"> <span class="w-5 h-5 flex-shrink-0 {{ $isActive ? 'text-blue' : 'text-t3 group-hover:text-t2' }}">
@if($item['icon'] === 'dashboard') @if($item['icon'] === 'dashboard')
<svg fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5"> <svg fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M2 2h5v5H2zM9 2h5v5H9zM2 9h5v5H2zM9 9h5v5H9z"/> <path stroke-linecap="round" stroke-linejoin="round" d="M2 2h5v5H2zM9 2h5v5H9zM2 9h5v5H2zM9 9h5v5H9z"/>

View File

@ -7,7 +7,7 @@
} }
@endphp @endphp
<header class="h-14 flex-shrink-0 bg-s1/80 backdrop-blur-sm border-b border-white/[.06] flex items-center justify-between px-4 gap-4 sticky top-0 z-30"> <header x-data class="h-14 flex-shrink-0 bg-s1/80 backdrop-blur-sm border-b border-white/[.06] flex items-center justify-between px-4 gap-4 sticky top-0 z-30">
<!-- Left: Hamburger (mobile) + Title --> <!-- Left: Hamburger (mobile) + Title -->
<div class="flex items-center gap-3 min-w-0"> <div class="flex items-center gap-3 min-w-0">
<!-- Mobile hamburger --> <!-- Mobile hamburger -->

View File

@ -33,7 +33,7 @@
:root { :root {
--c-bg: #0a0a0f; --c-s1: #111117; --c-s2: #16161e; --c-bg: #0a0a0f; --c-s1: #111117; --c-s2: #16161e;
--c-b1: rgba(255,255,255,0.08); --c-accent: #7c3aed; --c-b1: rgba(255,255,255,0.08); --c-accent: #7c3aed;
--c-t1: #f0f0f5; --c-t2: #8888a0; --c-t1: #f0f0f5; --c-t2: #b8b8d0;
} }
[data-theme="light"] { [data-theme="light"] {
--c-bg: #ffffff; --c-s1: #ffffff; --c-s2: #f9fafb; --c-bg: #ffffff; --c-s1: #ffffff; --c-s2: #f9fafb;
@ -114,7 +114,7 @@
<!-- Main content area: shifts right based on sidebar width --> <!-- Main content area: shifts right based on sidebar width -->
<div <div
class="flex flex-col min-h-screen transition-[margin-left] duration-200 ease-in-out md:ml-60" class="flex flex-col min-h-screen transition-[margin-left] duration-200 ease-in-out"
:class="$store.sidebar?.collapsed ? 'md:ml-16' : 'md:ml-60'" :class="$store.sidebar?.collapsed ? 'md:ml-16' : 'md:ml-60'"
> >
@persist('topbar') @persist('topbar')
@ -160,6 +160,12 @@
el.classList.toggle('text-t2', !isActive); el.classList.toggle('text-t2', !isActive);
el.classList.toggle('hover:text-t1', !isActive); el.classList.toggle('hover:text-t1', !isActive);
el.classList.toggle('hover:bg-s2', !isActive); el.classList.toggle('hover:bg-s2', !isActive);
// Also update icon span — sidebar is persisted so server classes stay stale
var icon = el.querySelector('span.flex-shrink-0');
if (icon) {
icon.classList.toggle('text-blue', isActive);
icon.classList.toggle('text-t3', !isActive);
}
}); });
} }
document.addEventListener('DOMContentLoaded', updateActiveNav); document.addEventListener('DOMContentLoaded', updateActiveNav);

View File

@ -15,7 +15,7 @@
<button wire:click="suggestSlugs" wire:loading.attr="disabled" wire:target="suggestSlugs" <button wire:click="suggestSlugs" wire:loading.attr="disabled" wire:target="suggestSlugs"
@disabled(!$targetUrl) @disabled(!$targetUrl)
type="button" type="button"
class="flex items-center gap-1 text-xs text-purple hover:opacity-80 transition-opacity disabled:opacity-40"> class="flex items-center gap-1 text-xs text-accent hover:opacity-80 transition-opacity disabled:opacity-40">
<span wire:loading.remove wire:target="suggestSlugs"> <span wire:loading.remove wire:target="suggestSlugs">
<x-heroicon-o-sparkles class="w-3 h-3 inline" /> <x-heroicon-o-sparkles class="w-3 h-3 inline" />
AI Slug AI Slug
@ -42,7 +42,7 @@
<div class="flex flex-wrap gap-1.5 mt-2"> <div class="flex flex-wrap gap-1.5 mt-2">
@foreach($slugSuggestions as $suggestion) @foreach($slugSuggestions as $suggestion)
<button wire:click="pickSuggestion('{{ $suggestion }}')" type="button" <button wire:click="pickSuggestion('{{ $suggestion }}')" type="button"
class="px-2 py-1 bg-purple/10 border border-purple/20 text-purple text-xs rounded font-mono hover:bg-purple/20 transition-colors"> class="px-2 py-1 bg-accent/10 border border-accent/30 text-accent text-xs rounded font-mono hover:bg-accent/20 transition-colors">
{{ $suggestion }} {{ $suggestion }}
</button> </button>
@endforeach @endforeach

View File

@ -47,7 +47,7 @@
</select> </select>
@if($country || $device || $linkId) @if($country || $device || $linkId)
<button wire:click="$set('country', ''); $set('device', ''); $set('linkId', '')" <button wire:click="clearFilters"
class="px-3 py-2 text-xs text-red hover:opacity-80 transition-opacity flex items-center gap-1"> class="px-3 py-2 text-xs text-red hover:opacity-80 transition-opacity flex items-center gap-1">
<x-heroicon-o-x-mark class="w-3 h-3" /> <x-heroicon-o-x-mark class="w-3 h-3" />
Clear filters Clear filters

View File

@ -5,7 +5,7 @@
<h1 class="text-t1">Links</h1> <h1 class="text-t1">Links</h1>
@if($ws) @if($ws)
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<button @click="$dispatch('openModal', {component: 'modals.utm-builder'})" <button @click="$dispatch('openModal', {component: 'modals.utm-builder', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-3 py-2 bg-s2 border border-white/[.06] text-t1 rounded-lg text-sm font-medium hover:bg-s3 transition-colors flex items-center gap-1.5"> class="px-3 py-2 bg-s2 border border-white/[.06] text-t1 rounded-lg text-sm font-medium hover:bg-s3 transition-colors flex items-center gap-1.5">
<x-heroicon-o-beaker class="w-4 h-4" /> <x-heroicon-o-beaker class="w-4 h-4" />
UTM Builder UTM Builder
@ -41,9 +41,8 @@
<th class="px-4 py-3 text-left font-semibold">Link</th> <th class="px-4 py-3 text-left font-semibold">Link</th>
<th class="px-4 py-3 text-left font-semibold">Target</th> <th class="px-4 py-3 text-left font-semibold">Target</th>
<th class="px-4 py-3 text-left font-semibold hidden md:table-cell">Tags</th> <th class="px-4 py-3 text-left font-semibold hidden md:table-cell">Tags</th>
<th class="px-4 py-3 text-right font-semibold">Clicks</th>
<th class="px-4 py-3 text-center font-semibold">Status</th> <th class="px-4 py-3 text-center font-semibold">Status</th>
<th class="px-4 py-3 w-32"></th> <th class="px-4 py-3 w-48"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -78,11 +77,6 @@
@endif @endif
</td> </td>
{{-- Clicks --}}
<td class="px-4 py-3 text-right font-mono text-t1 text-sm font-bold tabular-nums">
{{ number_format($link->clicks_count ?? 0) }}
</td>
{{-- Status Badge --}} {{-- Status Badge --}}
<td class="px-4 py-3 text-center"> <td class="px-4 py-3 text-center">
@if($link->status === 'active') @if($link->status === 'active')
@ -97,9 +91,11 @@
@endif @endif
</td> </td>
{{-- Actions --}} {{-- Actions + Clicks badge (badge always rightmost) --}}
<td class="px-4 py-3 text-right"> <td class="px-4 py-3">
<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">
{{-- Hover-revealed actions --}}
<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/'.$link->slug" label="Copy" /> <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 }}'}})"
@ -118,11 +114,16 @@
Delete Delete
</button> </button>
</div> </div>
{{-- Clicks badge always visible, rightmost --}}
<span class="shrink-0 font-mono text-xs text-t2 px-2 py-1 bg-s3 rounded">
{{ number_format($link->clicks_count ?? 0) }}
</span>
</div>
</td> </td>
</tr> </tr>
@empty @empty
<tr> <tr>
<td colspan="6" class="px-4 py-16 text-center"> <td colspan="5" class="px-4 py-16 text-center">
<div class="flex flex-col items-center gap-3"> <div class="flex flex-col items-center gap-3">
<svg class="w-10 h-10 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5"> <svg class="w-10 h-10 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" /> <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />