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
{
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;
}
$this->aiLoading = true;
try {
$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;
}
}
public function pickSuggestion(string $slug): void
{

View File

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

View File

@ -4,6 +4,7 @@ namespace App\Livewire\Pages\Ai;
use App\Domains\Ai\Services\AiService;
use App\Domains\Link\Models\Link;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View;
use Livewire\Component;
@ -13,9 +14,16 @@ class Anomalies extends Component
public bool $loading = false;
public int $workspaceId = 0;
public function mount(): void
{
$this->workspaceId = require_workspace()->id;
}
public function detect(): void
{
$workspace = require_workspace();
$workspace = Workspace::findOrFail($this->workspaceId);
$this->loading = true;
$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\Link\Models\Link;
use App\Domains\Workspace\Models\Workspace;
use Illuminate\View\View;
use Livewire\Component;
@ -13,9 +14,16 @@ class Insights extends Component
public bool $loading = false;
public int $workspaceId = 0;
public function mount(): void
{
$this->workspaceId = require_workspace()->id;
}
public function generate(): void
{
$workspace = require_workspace();
$workspace = Workspace::findOrFail($this->workspaceId);
$this->loading = true;
$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';
}
public function clearFilters(): void
{
$this->country = '';
$this->device = '';
$this->linkId = '';
}
private function rangeStart(): Carbon
{
return match ($this->range) {

View File

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

View File

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

View File

@ -184,12 +184,13 @@
wire:navigate
data-nav-link="{{ $item['route'] }}"
: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
? 'bg-blue/10 text-blue'
: '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')
<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"/>

View File

@ -7,7 +7,7 @@
}
@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 -->
<div class="flex items-center gap-3 min-w-0">
<!-- Mobile hamburger -->

View File

@ -33,7 +33,7 @@
:root {
--c-bg: #0a0a0f; --c-s1: #111117; --c-s2: #16161e;
--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"] {
--c-bg: #ffffff; --c-s1: #ffffff; --c-s2: #f9fafb;
@ -114,7 +114,7 @@
<!-- Main content area: shifts right based on sidebar width -->
<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'"
>
@persist('topbar')
@ -160,6 +160,12 @@
el.classList.toggle('text-t2', !isActive);
el.classList.toggle('hover:text-t1', !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);

View File

@ -15,7 +15,7 @@
<button wire:click="suggestSlugs" wire:loading.attr="disabled" wire:target="suggestSlugs"
@disabled(!$targetUrl)
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">
<x-heroicon-o-sparkles class="w-3 h-3 inline" />
AI Slug
@ -42,7 +42,7 @@
<div class="flex flex-wrap gap-1.5 mt-2">
@foreach($slugSuggestions as $suggestion)
<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 }}
</button>
@endforeach

View File

@ -47,7 +47,7 @@
</select>
@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">
<x-heroicon-o-x-mark class="w-3 h-3" />
Clear filters

View File

@ -5,7 +5,7 @@
<h1 class="text-t1">Links</h1>
@if($ws)
<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">
<x-heroicon-o-beaker class="w-4 h-4" />
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">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-right font-semibold">Clicks</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>
</thead>
<tbody>
@ -78,11 +77,6 @@
@endif
</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 --}}
<td class="px-4 py-3 text-center">
@if($link->status === 'active')
@ -97,9 +91,11 @@
@endif
</td>
{{-- Actions --}}
<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">
{{-- Actions + Clicks badge (badge always rightmost) --}}
<td class="px-4 py-3">
<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" />
<button
@click="$dispatch('openModal', {component: 'modals.link-variants', arguments: {linkUlid: '{{ $link->ulid }}'}})"
@ -118,11 +114,16 @@
Delete
</button>
</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>
</tr>
@empty
<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">
<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" />