feat(design): refined dark editorial design audit across all views

- CSS: metric-num glow, card-shadow depth, card-accent-top treatment, table-row-hover
  with left accent bar, badge utility, nim-reveal staggered page-load animation,
  nim-skeleton shimmer, bg-grid, polished scrollbar, tighter letter-spacing on headings
- Dashboard: accent-topped metric cards, gradient fill chart, JetBrains Mono tooltips,
  leading-zero rank numbers, improved top-links table
- Links: refined table with tracking-widest thead, badge status pills with dot indicator,
  blue slug hover on A/B action
- QR Codes: type-colored icons, display_type badge, bar-chart empty state SVG
- Bio: bars icon, slug with blue highlight, published dot indicator
- Analytics: grouped range button pill, progress bar distributions per country/device,
  nim-reveal stagger, three metric cards with card-accent-top
- Topbar: sticky + backdrop-blur, accent-gradient avatar

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 15:19:17 +02:00
parent 257d1b94f1
commit 063da474f9
7 changed files with 413 additions and 203 deletions

View File

@ -89,9 +89,116 @@
html { font-size: 14px; }
body { font-size: 0.875rem; line-height: 1.5; }
h1 { font-size: 1.5rem; line-height: 2rem; font-weight: 600; }
h2 { font-size: 1.25rem; line-height: 1.75rem; font-weight: 600; }
h3 { font-size: 1.125rem; line-height: 1.5rem; font-weight: 500; }
h1 { font-size: 1.5rem; line-height: 2rem; font-weight: 700; letter-spacing: -0.02em; }
h2 { font-size: 1.25rem; line-height: 1.75rem; font-weight: 600; letter-spacing: -0.015em; }
h3 { font-size: 1.125rem; line-height: 1.5rem; font-weight: 500; letter-spacing: -0.01em; }
/* Refined scrollbar */
::-webkit-scrollbar { width: 4px; height: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--color-t3); border-radius: 2px; }
::-webkit-scrollbar-thumb:hover { background: var(--color-t2); }
/* Text rendering */
body { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
.font-mono, code, pre { font-variant-numeric: tabular-nums; }
/* Number glow for metrics */
.metric-num {
font-family: var(--font-mono);
font-weight: 700;
letter-spacing: -0.03em;
text-shadow: 0 0 24px color-mix(in srgb, var(--color-accent) 30%, transparent);
}
/* Card depth shadow */
.card-shadow {
box-shadow:
0 1px 0 0 rgba(255,255,255,0.04) inset,
0 -1px 0 0 rgba(0,0,0,0.2) inset,
0 4px 16px rgba(0,0,0,0.3);
}
/* Accent-top card — top border is accent gradient */
.card-accent-top {
position: relative;
}
.card-accent-top::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, var(--color-accent) 0%, var(--color-accent-2) 100%);
border-radius: inherit;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
/* Page-load stagger reveal */
@keyframes nim-fade-up {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.nim-reveal {
opacity: 0;
animation: nim-fade-up 0.35s ease forwards;
}
.nim-reveal-1 { animation-delay: 0.05s; }
.nim-reveal-2 { animation-delay: 0.12s; }
.nim-reveal-3 { animation-delay: 0.19s; }
.nim-reveal-4 { animation-delay: 0.26s; }
.nim-reveal-5 { animation-delay: 0.33s; }
/* Shimmer for loading skeletons */
@keyframes nim-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.nim-skeleton {
background: linear-gradient(90deg, var(--color-s2) 25%, var(--color-s3) 50%, var(--color-s2) 75%);
background-size: 200% 100%;
animation: nim-shimmer 1.4s infinite;
border-radius: 4px;
}
/* Subtle grid background for stat sections */
.bg-grid {
background-image:
linear-gradient(rgba(255,255,255,0.015) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.015) 1px, transparent 1px);
background-size: 24px 24px;
}
/* Table row hover with left accent bar */
.table-row-hover {
position: relative;
transition: background 0.12s ease;
}
.table-row-hover::before {
content: '';
position: absolute;
left: 0; top: 0; bottom: 0;
width: 2px;
background: linear-gradient(180deg, var(--color-accent), var(--color-accent-2));
transform: scaleY(0);
transition: transform 0.15s ease;
border-radius: 0 2px 2px 0;
}
.table-row-hover:hover::before {
transform: scaleY(1);
}
/* Pill badge base */
.badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 9999px;
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: 0.01em;
}
@layer utilities {
.bg-accent-gradient {
@ -106,4 +213,5 @@ h3 { font-size: 1.125rem; line-height: 1.5rem; font-weight: 500; }
.border-accent-gradient {
border-image: linear-gradient(135deg, var(--color-accent), var(--color-accent-2)) 1;
}
.tabular-nums { font-variant-numeric: tabular-nums; }
}

View File

@ -7,7 +7,7 @@
}
@endphp
<header class="h-14 flex-shrink-0 bg-s1 border-b border-white/[.06] flex items-center justify-between px-4 gap-4">
<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">
<!-- Left: Hamburger (mobile) + Title -->
<div class="flex items-center gap-3 min-w-0">
<!-- Mobile hamburger -->
@ -56,23 +56,13 @@
</button>
<!-- Notifications Bell -->
<div class="relative">
<button
class="w-8 h-8 flex items-center justify-center rounded-lg text-t2 hover:text-t1 hover:bg-s2 transition-colors"
aria-label="Notifications"
>
<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 1.5a5 5 0 00-5 5v2.5l-1 1.5h12l-1-1.5V6.5a5 5 0 00-5-5zM6.5 13.5a1.5 1.5 0 003 0"/>
</svg>
</button>
<span class="absolute top-1 right-1 w-3.5 h-3.5 bg-blue rounded-full text-white text-[9px] font-medium flex items-center justify-center leading-none pointer-events-none">3</span>
</div>
<livewire:components.notifications-bell />
<!-- User Dropdown -->
<div class="relative ml-1" x-data="{ userOpen: false }">
<button
@click="userOpen = !userOpen"
class="w-8 h-8 rounded-full bg-blue flex items-center justify-center text-white text-xs font-semibold hover:opacity-90 transition-opacity"
class="w-8 h-8 rounded-full bg-accent-gradient flex items-center justify-center text-white text-xs font-bold hover:opacity-90 transition-opacity"
aria-label="User menu"
>
{{ $initials }}

View File

@ -1,16 +1,19 @@
<div>
{{-- Header --}}
<div class="flex flex-wrap items-center justify-between gap-3 mb-6">
<h1 class="text-2xl font-bold text-t1">Analytics</h1>
<div class="flex flex-wrap items-center justify-between gap-3 mb-6 nim-reveal nim-reveal-1">
<div>
<h1 class="text-t1">Analytics</h1>
<p class="text-xs text-t3 mt-0.5">Click-through data for your workspace</p>
</div>
{{-- Range buttons --}}
<div class="flex gap-1.5">
<div class="flex gap-1 bg-s2 border border-white/[.06] rounded-lg p-0.5">
@foreach(['24h' => '24h', '7d' => '7d', '30d' => '30d', '90d' => '90d'] as $key => $label)
<button wire:click="setRange('{{ $key }}')"
class="px-3 py-1.5 text-xs rounded-lg font-medium transition-all
class="px-3 py-1.5 text-xs rounded-md font-semibold transition-all
{{ $range === $key
? 'bg-accent-gradient text-white'
: 'bg-s2 text-t2 border border-white/[.06] hover:text-t1' }}">
? 'bg-accent-gradient text-white shadow-sm'
: 'text-t2 hover:text-t1' }}">
{{ $label }}
</button>
@endforeach
@ -53,51 +56,82 @@
</div>
{{-- Summary metrics --}}
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6">
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<div class="text-xs font-medium text-t2 uppercase tracking-wider">{{ strtoupper($range) }} Clicks</div>
<div class="text-3xl font-bold font-mono text-t1 mt-2">{{ number_format($totalFiltered) }}</div>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3 mb-6">
<div class="bg-s1 border border-white/[.06] rounded-xl p-4 card-shadow card-accent-top nim-reveal nim-reveal-1">
<div class="text-xs font-semibold text-t3 uppercase tracking-widest mb-3">{{ strtoupper($range) }}</div>
<div class="metric-num text-3xl text-t1">{{ number_format($totalFiltered) }}</div>
<div class="text-xs text-t3 mt-1.5">filtered clicks</div>
</div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<div class="text-xs font-medium text-t2 uppercase tracking-wider">All-time Clicks</div>
<div class="text-3xl font-bold font-mono text-t1 mt-2">{{ number_format($allTime) }}</div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-4 card-shadow nim-reveal nim-reveal-2">
<div class="text-xs font-semibold text-t3 uppercase tracking-widest mb-3">All-time</div>
<div class="metric-num text-3xl text-t1">{{ number_format($allTime) }}</div>
<div class="text-xs text-t3 mt-1.5">total clicks</div>
</div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<div class="text-xs font-medium text-t2 uppercase tracking-wider">Today</div>
<div class="text-3xl font-bold font-mono text-t1 mt-2">{{ number_format($totalToday) }}</div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-4 card-shadow nim-reveal nim-reveal-3">
<div class="text-xs font-semibold text-t3 uppercase tracking-widest mb-3">Today</div>
<div class="metric-num text-3xl text-t1">{{ number_format($totalToday) }}</div>
<div class="text-xs text-t3 mt-1.5">since midnight</div>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{{-- Top Countries --}}
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<h3 class="text-sm font-semibold text-t1 mb-4">Top Countries</h3>
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden card-shadow nim-reveal nim-reveal-4">
<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 uppercase tracking-wide">Top Countries</div>
@if($country)
<button wire:click="$set('country', '')" class="text-xs text-t3 hover:text-red transition-colors flex items-center gap-1">
<x-heroicon-o-x-mark class="w-3 h-3" />
Clear
</button>
@endif
</div>
@forelse($byCountry as $row)
<div class="flex items-center justify-between py-2 border-b border-white/[.03] last:border-0">
@php $pct = $totalFiltered > 0 ? round(($row->total / $totalFiltered) * 100) : 0; @endphp
<div class="px-5 py-2.5 border-b border-white/[.03] last:border-0 group">
<div class="flex items-center justify-between mb-1">
<button wire:click="$set('country', '{{ $row->country }}')"
class="text-sm text-t1 hover:text-blue transition-colors text-left {{ $country === $row->country ? 'text-blue font-medium' : '' }}">
class="text-sm text-t1 hover:text-blue transition-colors {{ $country === $row->country ? 'text-blue font-semibold' : '' }}">
{{ $row->country }}
</button>
<span class="text-sm font-mono text-t2">{{ number_format($row->total) }}</span>
<span class="font-mono text-xs text-t2 tabular-nums">{{ number_format($row->total) }}</span>
</div>
<div class="h-0.5 bg-s3 rounded-full overflow-hidden">
<div class="h-full bg-accent-gradient rounded-full transition-all" style="width: {{ $pct }}%"></div>
</div>
</div>
@empty
<p class="text-sm text-t3">No data for this period</p>
<div class="px-5 py-8 text-center text-xs text-t3">No data for this period</div>
@endforelse
</div>
{{-- By Device --}}
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<h3 class="text-sm font-semibold text-t1 mb-4">By Device</h3>
<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="text-xs font-semibold text-t1 uppercase tracking-wide">By Device</div>
@if($device)
<button wire:click="$set('device', '')" class="text-xs text-t3 hover:text-red transition-colors flex items-center gap-1">
<x-heroicon-o-x-mark class="w-3 h-3" />
Clear
</button>
@endif
</div>
@forelse($byDevice as $row)
<div class="flex items-center justify-between py-2 border-b border-white/[.03] last:border-0">
@php $pct = $totalFiltered > 0 ? round(($row->total / $totalFiltered) * 100) : 0; @endphp
<div class="px-5 py-2.5 border-b border-white/[.03] last:border-0">
<div class="flex items-center justify-between mb-1">
<button wire:click="$set('device', '{{ $row->device }}')"
class="text-sm text-t1 capitalize hover:text-blue transition-colors text-left {{ $device === $row->device ? 'text-blue font-medium' : '' }}">
class="text-sm text-t1 capitalize hover:text-blue transition-colors {{ $device === $row->device ? 'text-blue font-semibold' : '' }}">
{{ $row->device ?? 'Unknown' }}
</button>
<span class="text-sm font-mono text-t2">{{ number_format($row->total) }}</span>
<span class="font-mono text-xs text-t2 tabular-nums">{{ number_format($row->total) }}</span>
</div>
<div class="h-0.5 bg-s3 rounded-full overflow-hidden">
<div class="h-full bg-green/60 rounded-full transition-all" style="width: {{ $pct }}%"></div>
</div>
</div>
@empty
<p class="text-sm text-t3">No data for this period</p>
<div class="px-5 py-8 text-center text-xs text-t3">No data for this period</div>
@endforelse
</div>
</div>

View File

@ -1,54 +1,62 @@
<div>
@php $ws = current_workspace(); @endphp
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-semibold text-t1">Link-in-Bio</h1>
<div class="flex items-center justify-between mb-6 nim-reveal nim-reveal-1">
<div>
<h1 class="text-t1">Link-in-Bio</h1>
<p class="text-xs text-t3 mt-0.5">{{ $bioPages->total() }} pages</p>
</div>
@if($ws)
<button
@click="$dispatch('openModal', {component: 'modals.create-bio-page', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
+ New Bio Page
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-semibold hover:opacity-90 transition-opacity flex items-center gap-1.5">
<x-heroicon-o-plus class="w-4 h-4" />
New Bio Page
</button>
@endif
</div>
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden card-shadow nim-reveal nim-reveal-2">
@forelse($bioPages as $page)
<div class="flex items-center gap-4 px-5 py-4 border-b border-white/[.04] group hover:bg-white/[.02] transition-colors">
<div class="w-10 h-10 bg-s2 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h7.5"/>
</svg>
@php $title = is_array($page->title) ? ($page->title['en'] ?? $page->slug) : ($page->title ?? $page->slug); @endphp
<div class="flex items-center gap-4 px-5 py-3.5 border-b border-white/[.03] last:border-0 table-row-hover hover:bg-white/[.015] group relative">
<div class="w-8 h-8 bg-s2 border border-white/[.06] rounded-lg flex items-center justify-center flex-shrink-0">
<x-heroicon-o-bars-3-bottom-left class="w-4 h-4 text-t3" />
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-t1">{{ is_array($page->title) ? ($page->title['en'] ?? $page->slug) : ($page->title ?? $page->slug) }}</div>
<div class="text-xs text-t2 font-mono">nimu.li/{{ $page->slug }}</div>
<div class="text-sm font-semibold text-t1">{{ $title }}</div>
<div class="text-xs text-t3 font-mono mt-0.5">nimu.li/<span class="text-blue">{{ $page->slug }}</span></div>
</div>
<span class="text-xs px-2 py-0.5 rounded-full {{ $page->is_published ? 'bg-green/10 text-green' : 'bg-white/[.06] text-t3' }}">
<span class="badge {{ $page->is_published ? 'bg-green/10 text-green' : 'bg-white/[.06] text-t3' }}">
@if($page->is_published)
<span class="w-1 h-1 rounded-full bg-green inline-block"></span>
@endif
{{ $page->is_published ? 'Published' : 'Draft' }}
</span>
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150">
<button
@click="$dispatch('openModal', {component: 'modals.edit-bio-page', arguments: {bioUlid: '{{ $page->ulid }}'}})"
class="text-t2 text-xs hover:text-t1">Edit</button>
class="text-t2 text-xs hover:text-t1 transition-colors">Edit</button>
<button
@click="$dispatch('openModal', {component: 'modals.delete-bio-page', arguments: {bioUlid: '{{ $page->ulid }}'}})"
class="text-red text-xs hover:opacity-80">Delete</button>
class="text-t3 text-xs hover:text-red transition-colors">Delete</button>
</div>
</div>
@empty
<div class="p-12 text-center">
<div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5M3.75 17.25h7.5"/>
<div class="p-16 text-center">
<svg class="w-10 h-10 mx-auto text-t3 mb-4" fill="none" viewBox="0 0 40 40" stroke="currentColor" stroke-width="1">
<rect x="6" y="4" width="28" height="32" rx="3"/>
<path stroke-linecap="round" d="M12 12h16M12 18h16M12 24h10"/>
</svg>
</div>
<h3 class="text-lg font-medium text-t1 mb-2">No bio pages yet</h3>
<p class="text-sm text-t2 mb-6">Create a beautiful link-in-bio page for Instagram, TikTok and more.</p>
<p class="text-sm text-t2 mb-1.5">No bio pages yet</p>
<p class="text-xs text-t3 mb-6">Create a link-in-bio page for Instagram, TikTok and more.</p>
@if($ws)
<button
@click="$dispatch('openModal', {component: 'modals.create-bio-page', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2.5 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
Create your first Bio Page
class="px-4 py-2.5 bg-accent-gradient text-white rounded-lg text-sm font-semibold hover:opacity-90 transition-opacity">
Create first Bio Page
</button>
@endif
</div>

View File

@ -1,54 +1,60 @@
<div>
{{-- Quick Actions --}}
<div class="flex items-center gap-3 mb-6">
<a href="{{ route('w.links.index', request()->route('workspace')) }}"
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
+ Create Link
{{-- Page header --}}
<div class="flex items-center justify-between mb-7 nim-reveal nim-reveal-1">
<div>
<h1 class="text-t1">Overview</h1>
<p class="text-xs text-t3 mt-0.5">{{ now()->format('l, F j') }}</p>
</div>
<div class="flex items-center gap-2">
<a href="{{ route('w.analytics.index', request()->route('workspace')) }}"
wire:navigate
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" />
Analytics
</a>
<a href="{{ route('w.qr.index', request()->route('workspace')) }}"
class="px-4 py-2 bg-s2 text-t1 border border-white/[.06] rounded-lg text-sm font-medium hover:bg-s3 transition-colors">
+ Create QR Code
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">
+ QR Code
</a>
<a href="{{ route('w.analytics.index', request()->route('workspace')) }}"
class="px-4 py-2 text-t2 text-sm font-medium hover:text-t1 transition-colors">
View Analytics
<a href="{{ route('w.links.index', request()->route('workspace')) }}"
wire:navigate
class="px-3 py-2 bg-accent-gradient text-white rounded-lg text-xs font-semibold hover:opacity-90 transition-opacity">
+ New Link
</a>
</div>
{{-- Hero Metrics --}}
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{{-- Clicks Today --}}
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<div class="text-xs font-medium text-t2 uppercase tracking-wider">Clicks Today</div>
<div class="text-3xl font-semibold font-mono text-t1 mt-2">{{ number_format($today) }}</div>
<div class="text-xs text-t3 mt-1">since midnight</div>
</div>
{{-- Last 7 Days --}}
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<div class="text-xs font-medium text-t2 uppercase tracking-wider">Last 7 Days</div>
<div class="text-3xl font-semibold font-mono text-t1 mt-2">{{ number_format($last7) }}</div>
<div class="text-xs text-t3 mt-1">rolling 7-day window</div>
{{-- Metric cards --}}
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-5">
<div class="bg-s1 border border-white/[.06] rounded-xl p-4 card-shadow card-accent-top nim-reveal nim-reveal-1">
<div class="text-xs font-medium text-t3 uppercase tracking-widest mb-3">Today</div>
<div class="metric-num text-3xl text-t1">{{ number_format($today) }}</div>
<div class="text-xs text-t3 mt-1.5">clicks since midnight</div>
</div>
{{-- Last 30 Days --}}
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<div class="text-xs font-medium text-t2 uppercase tracking-wider">Last 30 Days</div>
<div class="text-3xl font-semibold font-mono text-t1 mt-2">{{ number_format($last30) }}</div>
<div class="text-xs text-t3 mt-1">rolling 30-day window</div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-4 card-shadow nim-reveal nim-reveal-2">
<div class="text-xs font-medium text-t3 uppercase tracking-widest mb-3">7 Days</div>
<div class="metric-num text-3xl text-t1">{{ number_format($last7) }}</div>
<div class="text-xs text-t3 mt-1.5">rolling window</div>
</div>
{{-- Total Links --}}
<div class="bg-s1 border border-white/[.06] rounded-xl p-5">
<div class="text-xs font-medium text-t2 uppercase tracking-wider">Total Links</div>
<div class="text-3xl font-semibold font-mono text-t1 mt-2">{{ number_format($totalLinks) }}</div>
<div class="text-xs text-t3 mt-1">active links</div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-4 card-shadow nim-reveal nim-reveal-3">
<div class="text-xs font-medium text-t3 uppercase tracking-widest mb-3">30 Days</div>
<div class="metric-num text-3xl text-t1">{{ number_format($last30) }}</div>
<div class="text-xs text-t3 mt-1.5">rolling window</div>
</div>
<div class="bg-s1 border border-white/[.06] rounded-xl p-4 card-shadow nim-reveal nim-reveal-4">
<div class="text-xs font-medium text-t3 uppercase tracking-widest mb-3">Links</div>
<div class="metric-num text-3xl text-t1">{{ number_format($totalLinks) }}</div>
<div class="text-xs text-t3 mt-1.5">active links</div>
</div>
</div>
{{-- 30-Day Chart --}}
{{-- Chart --}}
<div
class="bg-s1 border border-white/[.06] rounded-xl p-5 mt-4"
class="bg-s1 border border-white/[.06] rounded-xl p-5 mb-4 card-shadow nim-reveal nim-reveal-5"
x-data
x-init="
const ctx = document.getElementById('clicksChart');
@ -58,26 +64,51 @@
labels: {{ Js::from($labels) }},
datasets: [{
data: {{ Js::from($data) }},
borderColor: '#4f8ef7',
backgroundColor: 'rgba(79,142,247,0.08)',
borderColor: '#7c3aed',
backgroundColor: (ctx) => {
const g = ctx.chart.ctx.createLinearGradient(0, 0, 0, 160);
g.addColorStop(0, 'rgba(124,58,237,0.18)');
g.addColorStop(1, 'rgba(124,58,237,0.00)');
return g;
},
fill: true,
tension: 0.4,
pointRadius: 0,
borderWidth: 2,
pointHoverRadius: 4,
pointHoverBackgroundColor: '#7c3aed',
borderWidth: 1.5,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
interaction: { mode: 'index', intersect: false },
plugins: {
legend: { display: false },
tooltip: {
backgroundColor: '#111117',
borderColor: 'rgba(255,255,255,0.08)',
borderWidth: 1,
titleColor: '#8888a0',
bodyColor: '#f0f0f5',
titleFont: { family: 'JetBrains Mono', size: 10 },
bodyFont: { family: 'JetBrains Mono', size: 13, weight: '700' },
padding: 10,
displayColors: false,
callbacks: { label: ctx => ctx.parsed.y.toLocaleString() + ' clicks' }
}
},
scales: {
x: {
grid: { color: 'rgba(255,255,255,0.04)' },
ticks: { color: '#8888a0', font: { size: 11 } }
grid: { color: 'rgba(255,255,255,0.03)', drawTicks: false },
border: { display: false },
ticks: { color: '#44445a', font: { family: 'JetBrains Mono', size: 10 }, maxTicksLimit: 8, maxRotation: 0 }
},
y: {
grid: { color: 'rgba(255,255,255,0.04)' },
ticks: { color: '#8888a0', font: { size: 11 } },
position: 'right',
grid: { color: 'rgba(255,255,255,0.03)', drawTicks: false },
border: { display: false },
ticks: { color: '#44445a', font: { family: 'JetBrains Mono', size: 10 }, maxTicksLimit: 4 },
beginAtZero: true
}
}
@ -85,43 +116,54 @@
});
"
>
<h3 class="text-sm font-medium text-t1 mb-4">Clicks Last 30 Days</h3>
<div class="h-48">
<div class="flex items-center justify-between mb-4">
<div class="text-xs font-semibold text-t1 tracking-wide">CLICKS LAST 30 DAYS</div>
<div class="w-2 h-2 rounded-full bg-accent-gradient"></div>
</div>
<div class="h-44">
<canvas id="clicksChart"></canvas>
</div>
</div>
{{-- Top 5 Links --}}
<div class="bg-s1 border border-white/[.06] rounded-xl mt-4 overflow-hidden">
<div class="px-5 py-4 border-b border-white/[.06]">
<h3 class="text-sm font-medium text-t1">Top Links</h3>
{{-- Top Links --}}
<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="text-xs font-semibold text-t1 tracking-wide uppercase">Top Links</div>
<a href="{{ route('w.links.index', request()->route('workspace')) }}" wire:navigate
class="text-xs text-t3 hover:text-t1 transition-colors">View all </a>
</div>
<table class="w-full text-sm">
<table class="w-full">
<thead>
<tr class="text-xs font-medium text-t2 uppercase tracking-wider border-b border-white/[.06]">
<th class="px-5 py-3 text-left w-10">#</th>
<th class="px-5 py-3 text-left">Slug</th>
<th class="px-5 py-3 text-left">Title</th>
<th class="px-5 py-3 text-right">Clicks</th>
<tr class="text-t3 text-xs uppercase tracking-widest border-b border-white/[.04]">
<th class="px-5 py-2.5 text-left font-medium w-8">#</th>
<th class="px-5 py-2.5 text-left font-medium">Slug</th>
<th class="px-5 py-2.5 text-left font-medium hidden md:table-cell">Title</th>
<th class="px-5 py-2.5 text-right font-medium">Clicks</th>
</tr>
</thead>
<tbody class="divide-y divide-white/[.04]">
<tbody>
@forelse($topLinks as $index => $link)
<tr class="hover:bg-s2/50 transition-colors">
<td class="px-5 py-3 text-t3 font-mono text-xs">{{ $index + 1 }}</td>
<tr class="border-b border-white/[.03] last:border-0 table-row-hover hover:bg-white/[.015]">
<td class="px-5 py-3 text-t3 font-mono text-xs tabular-nums">{{ str_pad($index + 1, 2, '0', STR_PAD_LEFT) }}</td>
<td class="px-5 py-3">
<span class="font-mono text-blue text-sm">{{ $link->slug }}</span>
</td>
<td class="px-5 py-3 text-t2 text-sm truncate max-w-xs">
{{ $link->title ?: '—' }}
</td>
<td class="px-5 py-3 text-right font-mono text-t1 font-semibold">
{{ number_format($link->total_clicks) }}
<div class="flex items-baseline gap-1">
<span class="text-t3 text-xs">nimu.li/</span><span class="font-mono text-blue text-sm font-medium">{{ $link->slug }}</span>
</div>
</td>
<td class="px-5 py-3 text-t3 text-xs truncate max-w-xs hidden md:table-cell">{{ $link->title ?: '—' }}</td>
<td class="px-5 py-3 text-right font-mono text-t1 font-bold text-sm tabular-nums">{{ number_format($link->total_clicks) }}</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-5 py-10 text-center text-t3">No links yet.</td>
<td colspan="4" class="px-5 py-12 text-center">
<div class="flex flex-col items-center gap-3">
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 32 32" stroke="currentColor" stroke-width="1">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11l-8 8M11 11h8v8"/>
<circle cx="16" cy="16" r="13" stroke-dasharray="4 3"/>
</svg>
<p class="text-xs text-t3">No links yet create your first one</p>
</div>
</td>
</tr>
@endforelse
</tbody>

View File

@ -1,8 +1,8 @@
<div>
@php $ws = current_workspace(); @endphp
{{-- Header --}}
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-bold text-t1">Links</h1>
<div class="flex items-center justify-between mb-6 nim-reveal nim-reveal-1">
<h1 class="text-t1">Links</h1>
@if($ws)
<div class="flex items-center gap-2">
<button @click="$dispatch('openModal', {component: 'modals.utm-builder'})"
@ -34,26 +34,26 @@
</div>
{{-- Table --}}
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden card-shadow">
<table class="w-full text-sm">
<thead class="border-b border-white/[.06]">
<tr class="text-t3 text-xs uppercase tracking-wider">
<th class="px-4 py-3 text-left font-medium">Link</th>
<th class="px-4 py-3 text-left font-medium">Target</th>
<th class="px-4 py-3 text-left font-medium hidden md:table-cell">Tags</th>
<th class="px-4 py-3 text-right font-medium">Clicks</th>
<th class="px-4 py-3 text-center font-medium">Status</th>
<th class="px-4 py-3"></th>
<thead class="border-b border-white/[.06] bg-s2/40">
<tr class="text-t3 text-xs uppercase tracking-widest">
<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>
</tr>
</thead>
<tbody class="divide-y divide-white/[.04]">
<tbody>
@forelse($links as $link)
<tr class="hover:bg-white/[.02] transition-colors group">
<tr class="border-b border-white/[.03] last:border-0 table-row-hover hover:bg-white/[.015] group relative">
{{-- Slug + title --}}
<td class="px-4 py-3">
<div class="flex items-baseline gap-1">
<span class="text-t3 text-xs">nimu.li/</span>
<span class="font-mono text-blue text-sm">{{ $link->slug }}</span>
<span class="font-mono text-blue text-sm font-semibold">{{ $link->slug }}</span>
</div>
@if($link->title)
<div class="text-t3 text-xs mt-0.5 truncate max-w-xs">{{ $link->title }}</div>
@ -61,7 +61,7 @@
</td>
{{-- Target URL --}}
<td class="px-4 py-3 text-t2 text-xs max-w-xs">
<td class="px-4 py-3 text-t3 text-xs max-w-xs">
<span class="block truncate" title="{{ $link->target_url }}">{{ $link->target_url }}</span>
</td>
@ -70,7 +70,7 @@
@if(!empty($link->tags))
<div class="flex flex-wrap gap-1">
@foreach($link->tags as $tag)
<span class="bg-s2 text-t3 text-xs px-2 py-0.5 rounded-full">{{ $tag }}</span>
<span class="badge bg-s3 text-t3">{{ $tag }}</span>
@endforeach
</div>
@else
@ -79,27 +79,30 @@
</td>
{{-- Clicks --}}
<td class="px-4 py-3 text-right font-mono text-t1 text-sm">
<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')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green/10 text-green">active</span>
<span class="badge bg-green/10 text-green">
<span class="w-1 h-1 rounded-full bg-green inline-block"></span>
active
</span>
@elseif($link->status === 'expired')
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber/10 text-amber">expired</span>
<span class="badge bg-amber/10 text-amber">expired</span>
@else
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-white/[.06] text-t3">{{ $link->status }}</span>
<span class="badge bg-white/[.06] text-t3">{{ $link->status }}</span>
@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-opacity">
<div class="flex items-center justify-end gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150">
<button
@click="$dispatch('openModal', {component: 'modals.link-variants', arguments: {linkUlid: '{{ $link->ulid }}'}})"
class="text-t2 text-xs hover:text-purple transition-colors flex items-center gap-1">
class="text-t3 text-xs hover:text-blue transition-colors flex items-center gap-1">
<x-heroicon-o-chart-bar class="w-3 h-3" />
A/B
</button>
@ -110,7 +113,7 @@
</button>
<button
@click="$dispatch('openModal', {component: 'modals.delete-link', arguments: {linkUlid: '{{ $link->ulid }}'}})"
class="text-red text-xs hover:opacity-80 transition-opacity">
class="text-t3 text-xs hover:text-red transition-colors">
Delete
</button>
</div>

View File

@ -1,54 +1,79 @@
<div>
@php $ws = current_workspace(); @endphp
<div class="flex items-center justify-between mb-6">
<h1 class="text-2xl font-semibold text-t1">QR Codes</h1>
<div class="flex items-center justify-between mb-6 nim-reveal nim-reveal-1">
<div>
<h1 class="text-t1">QR Codes</h1>
<p class="text-xs text-t3 mt-0.5">{{ $qrCodes->total() }} total</p>
</div>
@if($ws)
<button
@click="$dispatch('openModal', {component: 'modals.create-qr-code', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
+ Create QR Code
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-semibold hover:opacity-90 transition-opacity flex items-center gap-1.5">
<x-heroicon-o-plus class="w-4 h-4" />
New QR Code
</button>
@endif
</div>
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
@if($search ?? false)
<div class="mb-4 nim-reveal nim-reveal-1">
<input wire:model.live.debounce.300ms="search" type="text" placeholder="Search QR codes…"
class="w-full max-w-sm px-3 py-2 bg-s1 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50">
</div>
@endif
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden card-shadow nim-reveal nim-reveal-2">
@forelse($qrCodes as $qr)
<div class="flex items-center gap-4 px-5 py-4 border-b border-white/[.04] group hover:bg-white/[.02] transition-colors">
<div class="w-10 h-10 bg-s2 rounded-lg flex items-center justify-center flex-shrink-0">
<svg class="w-5 h-5 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/>
</svg>
<div class="flex items-center gap-4 px-5 py-3.5 border-b border-white/[.03] last:border-0 table-row-hover hover:bg-white/[.015] group relative">
{{-- QR type icon --}}
<div class="w-8 h-8 bg-s2 border border-white/[.06] rounded-lg flex items-center justify-center flex-shrink-0">
@php
$displayType = $qr->payload['display_type'] ?? $qr->type;
$typeColors = ['url' => 'text-blue', 'vcard' => 'text-green', 'wifi' => 'text-amber', 'email' => 'text-purple', 'default' => 'text-t3'];
$typeColor = $typeColors[$displayType] ?? $typeColors['default'];
@endphp
<x-heroicon-o-qr-code class="w-4 h-4 {{ $typeColor }}" />
</div>
<div class="flex-1 min-w-0">
<div class="text-sm font-medium text-t1">{{ $qr->label ?: $qr->ulid }}</div>
<div class="text-xs text-t2 truncate">{{ $qr->type }} · {{ $qr->payload['url'] ?? '' }}</div>
<div class="text-sm font-semibold text-t1">{{ $qr->payload['label'] ?? ($qr->label ?? $qr->ulid) }}</div>
<div class="text-xs text-t3 truncate mt-0.5 font-mono">{{ $qr->payload['data'] ?? ($qr->payload['url'] ?? '') }}</div>
</div>
<span class="text-xs px-2 py-0.5 rounded-full {{ $qr->is_dynamic ? 'bg-blue/10 text-blue' : 'bg-white/[.06] text-t3' }}">
<div class="flex items-center gap-2 flex-shrink-0">
<span class="badge bg-s2 text-t3 border border-white/[.04] uppercase tracking-wide">{{ strtoupper($qr->payload['display_type'] ?? $qr->type) }}</span>
<span class="badge {{ $qr->is_dynamic ? 'bg-blue/10 text-blue' : 'bg-white/[.06] text-t3' }}">
{{ $qr->is_dynamic ? 'Dynamic' : 'Static' }}
</span>
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-opacity">
</div>
<div class="flex items-center gap-3 opacity-0 group-hover:opacity-100 transition-all duration-150 flex-shrink-0">
<button
@click="$dispatch('openModal', {component: 'modals.edit-qr-code', arguments: {qrUlid: '{{ $qr->ulid }}'}})"
class="text-t2 text-xs hover:text-t1">Edit</button>
class="text-t2 text-xs hover:text-t1 transition-colors">Edit</button>
<button
@click="$dispatch('openModal', {component: 'modals.delete-qr-code', arguments: {qrUlid: '{{ $qr->ulid }}'}})"
class="text-red text-xs hover:opacity-80">Delete</button>
class="text-t3 text-xs hover:text-red transition-colors">Delete</button>
</div>
</div>
@empty
<div class="p-12 text-center">
<div class="w-16 h-16 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 013.75 9.375v-4.5zM3.75 14.625c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5a1.125 1.125 0 01-1.125-1.125v-4.5zM13.5 4.875c0-.621.504-1.125 1.125-1.125h4.5c.621 0 1.125.504 1.125 1.125v4.5c0 .621-.504 1.125-1.125 1.125h-4.5A1.125 1.125 0 0113.5 9.375v-4.5z"/>
<div class="p-16 text-center">
<svg class="w-12 h-12 mx-auto text-t3 mb-4" fill="none" viewBox="0 0 48 48" stroke="currentColor" stroke-width="1">
<rect x="4" y="4" width="16" height="16" rx="2"/>
<rect x="28" y="4" width="16" height="16" rx="2"/>
<rect x="4" y="28" width="16" height="16" rx="2"/>
<rect x="8" y="8" width="8" height="8"/>
<rect x="32" y="8" width="8" height="8"/>
<rect x="8" y="32" width="8" height="8"/>
<path stroke-linecap="round" d="M28 28h4M36 28h4M28 32v4M32 32h4M36 32v4M28 40h4M32 36h4M36 36v4M40 40h4"/>
</svg>
</div>
<h3 class="text-lg font-medium text-t1 mb-2">No QR Codes yet</h3>
<p class="text-sm text-t2 mb-6">Create QR codes for URLs, vCards, WiFi and more.</p>
<p class="text-sm text-t2 mb-1.5">No QR codes yet</p>
<p class="text-xs text-t3 mb-6">Create QR codes for URLs, vCards, WiFi and more.</p>
@if($ws)
<button
@click="$dispatch('openModal', {component: 'modals.create-qr-code', arguments: {workspaceUlid: '{{ $ws->ulid }}'}})"
class="px-4 py-2.5 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
Create your first QR Code
class="px-4 py-2.5 bg-accent-gradient text-white rounded-lg text-sm font-semibold hover:opacity-90 transition-opacity">
Create first QR Code
</button>
@endif
</div>