feat(sidebar+profile+buttons): sidebar active state, 3-tab profile, consistent button variants
- Sidebar JS: DOMContentLoaded + livewire:navigated updateActiveNav, data-nav-link on links - Profile: exactly 3 tabs (Personal Info, Preferences, Security) with wire:navigate - Buttons: all primary actions use bg-accent-gradient text-white - UTM Builder + copy-to-clipboard Livewire event handler in app layout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
278bc42b37
commit
1ff4114674
|
|
@ -3,11 +3,14 @@
|
||||||
namespace App\Http\Middleware;
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
use App\Domains\Workspace\Models\Workspace;
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Domains\Workspace\Services\CurrentWorkspace;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ResolveWorkspace
|
class ResolveWorkspace
|
||||||
{
|
{
|
||||||
|
public function __construct(private CurrentWorkspace $current) {}
|
||||||
|
|
||||||
public function handle(Request $request, Closure $next): mixed
|
public function handle(Request $request, Closure $next): mixed
|
||||||
{
|
{
|
||||||
$ulid = $request->route('workspace');
|
$ulid = $request->route('workspace');
|
||||||
|
|
@ -16,7 +19,7 @@ class ResolveWorkspace
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
||||||
$workspace = Workspace::where('ulid', $ulid)->firstOrFail();
|
$workspace = Workspace::where('ulid', is_string($ulid) ? $ulid : $ulid->ulid)->firstOrFail();
|
||||||
|
|
||||||
// 404 instead of 403 — don't leak existence across workspace boundaries
|
// 404 instead of 403 — don't leak existence across workspace boundaries
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
@ -26,8 +29,8 @@ class ResolveWorkspace
|
||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$request->merge(['current_workspace' => $workspace]);
|
$this->current->set($workspace);
|
||||||
app()->instance('current_workspace', $workspace);
|
$request->route()->setParameter('workspace', $workspace);
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ class Overview extends Component
|
||||||
{
|
{
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$workspace = require_workspace();
|
||||||
|
|
||||||
// Hero metrics
|
// Hero metrics
|
||||||
$today = Click::where('workspace_id', $workspace->id)
|
$today = Click::where('workspace_id', $workspace->id)
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class Index extends Component
|
||||||
|
|
||||||
public function mount(): void
|
public function mount(): void
|
||||||
{
|
{
|
||||||
$ws = app('current_workspace');
|
$ws = current_workspace();
|
||||||
$this->workspaceName = $ws->name;
|
$this->workspaceName = $ws->name;
|
||||||
$this->workspaceSlug = $ws->slug;
|
$this->workspaceSlug = $ws->slug;
|
||||||
}
|
}
|
||||||
|
|
@ -25,9 +25,9 @@ class Index extends Component
|
||||||
'workspaceSlug' => 'nullable|string|max:50|alpha_dash',
|
'workspaceSlug' => 'nullable|string|max:50|alpha_dash',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
app('current_workspace')->update([
|
require_workspace()->update([
|
||||||
'name' => $this->workspaceName,
|
'name' => $this->workspaceName,
|
||||||
'slug' => $this->workspaceSlug ?: app('current_workspace')->slug,
|
'slug' => $this->workspaceSlug ?: require_workspace()->slug,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
session()->flash('status', 'Workspace settings saved.');
|
session()->flash('status', 'Workspace settings saved.');
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ class Members extends Component
|
||||||
{
|
{
|
||||||
public function render(): View
|
public function render(): View
|
||||||
{
|
{
|
||||||
$workspace = app('current_workspace');
|
$workspace = require_workspace();
|
||||||
|
|
||||||
$members = WorkspaceMember::with('user')
|
$members = WorkspaceMember::with('user')
|
||||||
->where('workspace_id', $workspace->id)
|
->where('workspace_id', $workspace->id)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ namespace App\Providers;
|
||||||
use App\Domains\Translation\Providers\DatabaseTranslationLoader;
|
use App\Domains\Translation\Providers\DatabaseTranslationLoader;
|
||||||
use App\Domains\Workspace\Models\Workspace;
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
use App\Domains\Workspace\Policies\WorkspacePolicy;
|
use App\Domains\Workspace\Policies\WorkspacePolicy;
|
||||||
|
use App\Domains\Workspace\Services\CurrentWorkspace;
|
||||||
use Illuminate\Support\Facades\Gate;
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Laravel\Cashier\Cashier;
|
use Laravel\Cashier\Cashier;
|
||||||
|
|
@ -16,6 +17,8 @@ class AppServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function register(): void
|
public function register(): void
|
||||||
{
|
{
|
||||||
|
$this->app->singleton(CurrentWorkspace::class);
|
||||||
|
|
||||||
// TranslationServiceProvider is deferred and re-binds translation.loader when it
|
// TranslationServiceProvider is deferred and re-binds translation.loader when it
|
||||||
// resolves. We extend 'translator' to swap in our DB loader after the instance is
|
// resolves. We extend 'translator' to swap in our DB loader after the instance is
|
||||||
// created, then freeze it in the container so app('translation.loader') is consistent.
|
// created, then freeze it in the container so app('translation.loader') is consistent.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Domains\Workspace\Models\Workspace;
|
||||||
|
use App\Domains\Workspace\Services\CurrentWorkspace;
|
||||||
|
|
||||||
|
if (! function_exists('current_workspace')) {
|
||||||
|
function current_workspace(): ?Workspace
|
||||||
|
{
|
||||||
|
$ws = app(CurrentWorkspace::class)->get();
|
||||||
|
if ($ws) {
|
||||||
|
return $ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward-compat fallback: tests and legacy code may bind via app()->instance()
|
||||||
|
if (app()->bound('current_workspace')) {
|
||||||
|
/** @var Workspace $ws */
|
||||||
|
$ws = app('current_workspace');
|
||||||
|
app(CurrentWorkspace::class)->set($ws);
|
||||||
|
|
||||||
|
return $ws;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! function_exists('require_workspace')) {
|
||||||
|
function require_workspace(): Workspace
|
||||||
|
{
|
||||||
|
$ws = current_workspace();
|
||||||
|
if (! $ws) {
|
||||||
|
throw new \RuntimeException('No current workspace set. Route missing workspace.resolve middleware?');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ws;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Icons Sets
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| With this config option you can define a couple of
|
||||||
|
| default icon sets. Provide a key name for your icon
|
||||||
|
| set and a combination from the options below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'sets' => [
|
||||||
|
|
||||||
|
// 'default' => [
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Icons Path
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | Provide the relative path from your app root to your SVG icons
|
||||||
|
// | directory. Icons are loaded recursively so there's no need to
|
||||||
|
// | list every sub-directory.
|
||||||
|
// |
|
||||||
|
// | Relative to the disk root when the disk option is set.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'path' => 'resources/svg',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Filesystem Disk
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | Optionally, provide a specific filesystem disk to read
|
||||||
|
// | icons from. When defining a disk, the "path" option
|
||||||
|
// | starts relatively from the disk root.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'disk' => '',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Default Prefix
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | This config option allows you to define a default prefix for
|
||||||
|
// | your icons. The dash separator will be applied automatically
|
||||||
|
// | to every icon name. It's required and needs to be unique.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'prefix' => 'icon',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Fallback Icon
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | This config option allows you to define a fallback
|
||||||
|
// | icon when an icon in this set cannot be found.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'fallback' => '',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Default Set Classes
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | This config option allows you to define some classes which
|
||||||
|
// | will be applied by default to all icons within this set.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'class' => '',
|
||||||
|
//
|
||||||
|
// /*
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// | Default Set Attributes
|
||||||
|
// |-----------------------------------------------------------------
|
||||||
|
// |
|
||||||
|
// | This config option allows you to define some attributes which
|
||||||
|
// | will be applied by default to all icons within this set.
|
||||||
|
// |
|
||||||
|
// */
|
||||||
|
//
|
||||||
|
// 'attributes' => [
|
||||||
|
// // 'width' => 50,
|
||||||
|
// // 'height' => 50,
|
||||||
|
// ],
|
||||||
|
//
|
||||||
|
// ],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global Default Classes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to define some classes which
|
||||||
|
| will be applied by default to all icons.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'class' => '',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global Default Attributes
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to define some attributes which
|
||||||
|
| will be applied by default to all icons.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'attributes' => [
|
||||||
|
// 'width' => 50,
|
||||||
|
// 'height' => 50,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Global Fallback Icon
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to define a global fallback
|
||||||
|
| icon when an icon in any set cannot be found. It can
|
||||||
|
| reference any icon from any configured set.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'fallback' => '',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Components
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These config options allow you to define some
|
||||||
|
| settings related to Blade Components.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'components' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|----------------------------------------------------------------------
|
||||||
|
| Disable Components
|
||||||
|
|----------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to disable Blade components
|
||||||
|
| completely. It's useful to avoid performance problems
|
||||||
|
| when working with large icon libraries.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'disabled' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|----------------------------------------------------------------------
|
||||||
|
| Default Icon Component Name
|
||||||
|
|----------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This config option allows you to define the name
|
||||||
|
| for the default Icon class component.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 'icon',
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
|
|
@ -73,7 +73,7 @@ return [
|
||||||
RequestReceived::class => [
|
RequestReceived::class => [
|
||||||
...Octane::prepareApplicationForNextOperation(),
|
...Octane::prepareApplicationForNextOperation(),
|
||||||
...Octane::prepareApplicationForNextRequest(),
|
...Octane::prepareApplicationForNextRequest(),
|
||||||
//
|
\App\Listeners\ResetCurrentWorkspace::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
RequestHandled::class => [
|
RequestHandled::class => [
|
||||||
|
|
@ -82,6 +82,7 @@ return [
|
||||||
|
|
||||||
RequestTerminated::class => [
|
RequestTerminated::class => [
|
||||||
// FlushUploadedFiles::class,
|
// FlushUploadedFiles::class,
|
||||||
|
\App\Listeners\ResetCurrentWorkspace::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
TaskReceived::class => [
|
TaskReceived::class => [
|
||||||
|
|
|
||||||
|
|
@ -53,5 +53,13 @@ document.addEventListener('alpine:init', () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
Alpine.store('sidebar', { open: false });
|
Alpine.store('sidebar', {
|
||||||
|
open: false,
|
||||||
|
collapsed: localStorage.getItem('sidebarCollapsed') === 'true',
|
||||||
|
toggle() {
|
||||||
|
this.collapsed = ! this.collapsed;
|
||||||
|
localStorage.setItem('sidebarCollapsed', this.collapsed);
|
||||||
|
window.dispatchEvent(new CustomEvent('sidebar-toggled', { detail: { collapsed: this.collapsed } }));
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
@php
|
@php
|
||||||
$workspace = app()->bound('current_workspace') ? app('current_workspace') : null;
|
$workspace = current_workspace();
|
||||||
$workspaces = auth()->check() ? auth()->user()->workspaces()->orderBy('name')->get() : collect();
|
$workspaces = auth()->check() ? auth()->user()->workspaces()->orderBy('name')->get() : collect();
|
||||||
|
|
||||||
$wsUlid = $workspace?->ulid;
|
$wsUlid = $workspace?->ulid;
|
||||||
|
|
@ -68,6 +68,13 @@
|
||||||
'match' => 'w.team.*',
|
'match' => 'w.team.*',
|
||||||
'icon' => 'team',
|
'icon' => 'team',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'label' => 'AI',
|
||||||
|
'route' => 'w.ai.insights',
|
||||||
|
'params' => [$wsUlid],
|
||||||
|
'match' => 'w.ai.*',
|
||||||
|
'icon' => 'ai',
|
||||||
|
],
|
||||||
] : [
|
] : [
|
||||||
[
|
[
|
||||||
'label' => 'Dashboard',
|
'label' => 'Dashboard',
|
||||||
|
|
@ -90,67 +97,78 @@
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<aside
|
<aside
|
||||||
x-show="$store.sidebar.open || $el.closest('[data-desktop-sidebar]') !== null"
|
x-data="{ wsOpen: false }"
|
||||||
class="w-60 flex-shrink-0 bg-s1 border-r border-white/[.06] flex flex-col h-full"
|
:class="$store.sidebar.collapsed ? 'w-16' : 'w-60'"
|
||||||
data-desktop-sidebar
|
class="fixed inset-y-0 left-0 h-screen flex flex-col bg-s1 border-r border-white/[.06] z-40 transition-[width] duration-200 ease-in-out
|
||||||
|
-translate-x-full md:translate-x-0"
|
||||||
|
:style="$store.sidebar?.open ? 'transform: translateX(0)' : ''"
|
||||||
>
|
>
|
||||||
<!-- Header: Logo + Workspace Switcher -->
|
<!-- Header: Logo + Workspace Switcher -->
|
||||||
<div class="flex-shrink-0" x-data="{ wsOpen: false }">
|
<div class="flex-shrink-0 relative" @click.outside="wsOpen = false">
|
||||||
<button
|
<!-- Collapsed state: centered logo acts as expand toggle -->
|
||||||
@click="wsOpen = !wsOpen"
|
<div x-show="$store.sidebar.collapsed" class="flex items-center justify-center py-4 border-b border-white/[.06]">
|
||||||
class="w-full flex items-center gap-3 px-4 py-4 hover:bg-s2 transition-colors border-b border-white/[.06]"
|
<button @click="$store.sidebar.toggle()" title="Expand sidebar" class="flex items-center justify-center w-7 h-7 hover:opacity-75 transition-opacity">
|
||||||
>
|
<img src="/brand/logo-mark.svg" alt="Nimuli" class="w-7 h-7">
|
||||||
<img src="/brand/logo-mark.svg" alt="Nimuli" class="w-7 h-7 flex-shrink-0">
|
</button>
|
||||||
<div class="flex-1 text-left min-w-0">
|
</div>
|
||||||
<div class="text-sm font-bold tracking-tight text-accent-gradient">nimuli</div>
|
|
||||||
@if($workspace)
|
|
||||||
<div class="text-xs text-t2 truncate mt-0.5">{{ $workspace->name }}</div>
|
|
||||||
@else
|
|
||||||
<div class="text-xs text-t2">Select workspace</div>
|
|
||||||
@endif
|
|
||||||
</div>
|
|
||||||
<svg class="w-4 h-4 text-t3 flex-shrink-0 transition-transform" :class="wsOpen && 'rotate-180'" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6l4 4 4-4"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- Workspace Switcher Dropdown -->
|
<!-- Expanded state: full header with workspace switcher -->
|
||||||
<div
|
<div x-show="!$store.sidebar.collapsed">
|
||||||
x-show="wsOpen"
|
<button
|
||||||
x-transition:enter="transition ease-out duration-100"
|
@click="wsOpen = !wsOpen"
|
||||||
x-transition:enter-start="opacity-0 -translate-y-1"
|
class="w-full flex items-center gap-3 px-4 py-4 hover:bg-s2 transition-colors border-b border-white/[.06]"
|
||||||
x-transition:enter-end="opacity-100 translate-y-0"
|
>
|
||||||
x-transition:leave="transition ease-in duration-75"
|
<img src="/brand/logo-mark.svg" alt="Nimuli" class="w-7 h-7 flex-shrink-0">
|
||||||
x-transition:leave-start="opacity-100 translate-y-0"
|
<div class="flex-1 text-left min-w-0">
|
||||||
x-transition:leave-end="opacity-0 -translate-y-1"
|
<div class="text-sm font-bold tracking-tight text-accent-gradient">nimuli</div>
|
||||||
class="bg-s2 border-b border-white/[.06] py-1"
|
@if($workspace)
|
||||||
@click.outside="wsOpen = false"
|
<div class="text-xs text-t2 truncate mt-0.5">{{ $workspace->name }}</div>
|
||||||
>
|
@else
|
||||||
@forelse($workspaces as $ws)
|
<div class="text-xs text-t2">Select workspace</div>
|
||||||
<a
|
|
||||||
href="{{ route('w.links.index', $ws->ulid) }}"
|
|
||||||
wire:navigate
|
|
||||||
class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-s3 transition-colors {{ $workspace?->id === $ws->id ? 'text-t1' : 'text-t2' }}"
|
|
||||||
>
|
|
||||||
<span class="flex-1 truncate">{{ $ws->name }}</span>
|
|
||||||
@if($workspace?->id === $ws->id)
|
|
||||||
<svg class="w-3.5 h-3.5 text-blue flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
|
|
||||||
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
|
||||||
</svg>
|
|
||||||
@endif
|
@endif
|
||||||
</a>
|
</div>
|
||||||
@empty
|
<svg class="w-4 h-4 text-t3 flex-shrink-0 transition-transform" :class="wsOpen && 'rotate-180'" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||||||
<div class="px-4 py-2 text-xs text-t3">No workspaces yet</div>
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4 6l4 4 4-4"/>
|
||||||
@endforelse
|
</svg>
|
||||||
<div class="border-t border-white/[.06] mt-1 pt-1">
|
</button>
|
||||||
<button
|
|
||||||
wire:click="$dispatch('openModal', {component: 'modals.create-workspace'})"
|
<!-- Workspace Switcher Dropdown -->
|
||||||
class="flex items-center gap-2 px-4 py-2 text-sm text-t2 hover:text-t1 hover:bg-s3 transition-colors w-full text-left">
|
<div
|
||||||
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
x-show="wsOpen"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M8 3v10M3 8h10"/>
|
x-transition:enter="transition ease-out duration-100"
|
||||||
</svg>
|
x-transition:enter-start="opacity-0 -translate-y-1"
|
||||||
New Workspace
|
x-transition:enter-end="opacity-100 translate-y-0"
|
||||||
</button>
|
x-transition:leave="transition ease-in duration-75"
|
||||||
|
x-transition:leave-start="opacity-100 translate-y-0"
|
||||||
|
x-transition:leave-end="opacity-0 -translate-y-1"
|
||||||
|
class="bg-s2 border-b border-white/[.06] py-1"
|
||||||
|
>
|
||||||
|
@forelse($workspaces as $ws)
|
||||||
|
<a
|
||||||
|
href="{{ route('w.links.index', $ws->ulid) }}"
|
||||||
|
wire:navigate
|
||||||
|
class="flex items-center gap-2 px-4 py-2 text-sm hover:bg-s3 transition-colors {{ $workspace?->id === $ws->id ? 'text-t1' : 'text-t2' }}"
|
||||||
|
>
|
||||||
|
<span class="flex-1 truncate">{{ $ws->name }}</span>
|
||||||
|
@if($workspace?->id === $ws->id)
|
||||||
|
<svg class="w-3.5 h-3.5 text-blue flex-shrink-0" fill="currentColor" viewBox="0 0 16 16">
|
||||||
|
<path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
|
||||||
|
</svg>
|
||||||
|
@endif
|
||||||
|
</a>
|
||||||
|
@empty
|
||||||
|
<div class="px-4 py-2 text-xs text-t3">No workspaces yet</div>
|
||||||
|
@endforelse
|
||||||
|
<div class="border-t border-white/[.06] mt-1 pt-1">
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.create-workspace'})"
|
||||||
|
class="flex items-center gap-2 px-4 py-2 text-sm text-t2 hover:text-t1 hover:bg-s3 transition-colors w-full text-left">
|
||||||
|
<svg class="w-3.5 h-3.5" fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 3v10M3 8h10"/>
|
||||||
|
</svg>
|
||||||
|
New Workspace
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -165,6 +183,8 @@
|
||||||
<a
|
<a
|
||||||
href="{{ $url }}"
|
href="{{ $url }}"
|
||||||
wire:navigate
|
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="flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors group
|
||||||
{{ $isActive
|
{{ $isActive
|
||||||
? 'bg-blue/10 text-blue'
|
? 'bg-blue/10 text-blue'
|
||||||
|
|
@ -207,16 +227,36 @@
|
||||||
<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">
|
||||||
<circle cx="6" cy="5" r="2"/><path stroke-linecap="round" stroke-linejoin="round" d="M1.5 14c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4"/><circle cx="11.5" cy="5" r="1.5"/><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 13c0-1.8-1-3-2.5-3.5"/>
|
<circle cx="6" cy="5" r="2"/><path stroke-linecap="round" stroke-linejoin="round" d="M1.5 14c0-2.5 2-4 4.5-4s4.5 1.5 4.5 4"/><circle cx="11.5" cy="5" r="1.5"/><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 13c0-1.8-1-3-2.5-3.5"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
@elseif($item['icon'] === 'ai')
|
||||||
|
<svg fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8 1.5l1.5 3 3.5.5-2.5 2.5.5 3.5L8 9.5 5 11l.5-3.5L3 5l3.5-.5L8 1.5z"/>
|
||||||
|
</svg>
|
||||||
@endif
|
@endif
|
||||||
</span>
|
</span>
|
||||||
{{ $item['label'] }}
|
<span x-show="!$store.sidebar.collapsed" class="truncate">{{ $item['label'] }}</span>
|
||||||
</a>
|
</a>
|
||||||
@endforeach
|
@endforeach
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Collapse Toggle -->
|
||||||
|
<div class="flex-shrink-0 border-t border-white/[.06] px-2 py-2">
|
||||||
|
<button
|
||||||
|
@click="$store.sidebar.toggle()"
|
||||||
|
: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"
|
||||||
|
>
|
||||||
|
<span class="w-4 h-4 flex-shrink-0 transition-transform duration-200" :class="$store.sidebar.collapsed ? 'rotate-180' : ''">
|
||||||
|
<svg 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"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
|
<span x-show="!$store.sidebar.collapsed" class="text-xs">Collapse</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Plan Usage Box -->
|
<!-- Plan Usage Box -->
|
||||||
@if($workspace && $plan)
|
@if($workspace && $plan)
|
||||||
<div class="mx-3 mb-3 p-3 bg-s2 border border-white/[.06] rounded-lg">
|
<div x-show="!$store.sidebar.collapsed" class="mx-3 mb-3 p-3 bg-s2 border border-white/[.06] rounded-lg">
|
||||||
<div class="flex items-center justify-between mb-1.5">
|
<div class="flex items-center justify-between mb-1.5">
|
||||||
<span class="text-xs font-medium text-t2">{{ ucfirst($plan->name) }} Plan</span>
|
<span class="text-xs font-medium text-t2">{{ ucfirst($plan->name) }} Plan</span>
|
||||||
@if(str_contains(strtolower($plan->name), 'free'))
|
@if(str_contains(strtolower($plan->name), 'free'))
|
||||||
|
|
@ -235,15 +275,15 @@
|
||||||
|
|
||||||
<!-- User Footer -->
|
<!-- User Footer -->
|
||||||
@auth
|
@auth
|
||||||
<div class="flex-shrink-0 border-t border-white/[.06] p-3 flex items-center gap-3">
|
<div class="flex-shrink-0 border-t border-white/[.06] p-3 flex items-center gap-3 min-w-0">
|
||||||
<div class="w-8 h-8 rounded-full bg-blue flex items-center justify-center text-white text-xs font-semibold flex-shrink-0">
|
<div class="w-8 h-8 rounded-full bg-blue flex items-center justify-center text-white text-xs font-semibold flex-shrink-0">
|
||||||
{{ $initials ?: strtoupper(substr(auth()->user()->email, 0, 2)) }}
|
{{ $initials ?: strtoupper(substr(auth()->user()->email, 0, 2)) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div x-show="!$store.sidebar.collapsed" class="flex-1 min-w-0">
|
||||||
<div class="text-xs font-medium text-t1 truncate">{{ auth()->user()->name }}</div>
|
<div class="text-xs font-medium text-t1 truncate">{{ auth()->user()->name }}</div>
|
||||||
<div class="text-xs text-t3 truncate">{{ auth()->user()->email }}</div>
|
<div class="text-xs text-t3 truncate">{{ auth()->user()->email }}</div>
|
||||||
</div>
|
</div>
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form x-show="!$store.sidebar.collapsed" method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
{{ $cancelLabel }}
|
{{ $cancelLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button wire:click="confirm"
|
<button wire:click="confirm"
|
||||||
class="px-4 py-2 text-sm font-medium rounded-lg transition-opacity hover:opacity-90 {{ $danger ? 'bg-red text-white' : 'bg-blue text-white' }}">
|
class="px-4 py-2 text-sm font-medium rounded-lg transition-opacity hover:opacity-90 {{ $danger ? 'bg-red text-white' : 'bg-accent-gradient text-white' }}">
|
||||||
{{ $confirmLabel }}
|
{{ $confirmLabel }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
<div class="flex justify-end mt-6">
|
<div class="flex justify-end mt-6">
|
||||||
<button wire:click="closeModal"
|
<button wire:click="closeModal"
|
||||||
class="px-4 py-2 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
class="px-4 py-2 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
Done
|
Done
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<div class="flex gap-3 mt-6">
|
<div class="flex gap-3 mt-6">
|
||||||
<button wire:click="create"
|
<button wire:click="create"
|
||||||
class="flex-1 px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
class="flex-1 px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
Create Token
|
Create Token
|
||||||
</button>
|
</button>
|
||||||
<button wire:click="closeModal"
|
<button wire:click="closeModal"
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
|
|
||||||
<div class="flex gap-3 mt-6">
|
<div class="flex gap-3 mt-6">
|
||||||
<button wire:click="save"
|
<button wire:click="save"
|
||||||
class="flex-1 px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
class="flex-1 px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
Create Webhook
|
Create Webhook
|
||||||
</button>
|
</button>
|
||||||
<button wire:click="closeModal"
|
<button wire:click="closeModal"
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
|
|
||||||
<div class="flex gap-3 mt-6">
|
<div class="flex gap-3 mt-6">
|
||||||
<button wire:click="create"
|
<button wire:click="create"
|
||||||
class="flex-1 px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
class="flex-1 px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
Create Workspace
|
Create Workspace
|
||||||
</button>
|
</button>
|
||||||
<button wire:click="closeModal"
|
<button wire:click="closeModal"
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
|
|
||||||
<div class="flex gap-3 mt-6">
|
<div class="flex gap-3 mt-6">
|
||||||
<button wire:click="save"
|
<button wire:click="save"
|
||||||
class="flex-1 px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
class="flex-1 px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
<button wire:click="closeModal"
|
<button wire:click="closeModal"
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button wire:click="save"
|
<button wire:click="save"
|
||||||
class="px-4 py-2 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
class="px-4 py-2 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
<div class="flex gap-3 mt-6">
|
<div class="flex gap-3 mt-6">
|
||||||
<button wire:click="invite"
|
<button wire:click="invite"
|
||||||
class="flex-1 px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
class="flex-1 px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
Send Invite
|
Send Invite
|
||||||
</button>
|
</button>
|
||||||
<button wire:click="closeModal"
|
<button wire:click="closeModal"
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex items-center justify-between mb-5">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold text-t1">A/B Test Variants</h3>
|
||||||
|
<p class="text-xs text-t3 mt-0.5 font-mono truncate max-w-xs">{{ $linkTarget }}</p>
|
||||||
|
</div>
|
||||||
|
<button wire:click="addVariant" type="button"
|
||||||
|
class="px-3 py-1.5 bg-s2 border border-white/[.06] text-xs font-medium text-t1 rounded-lg hover:bg-s3 transition-colors flex items-center gap-1.5 {{ count($variants) >= 5 ? 'opacity-40 cursor-not-allowed' : '' }}"
|
||||||
|
@disabled(count($variants) >= 5)>
|
||||||
|
<x-heroicon-o-plus class="w-3.5 h-3.5" />
|
||||||
|
Add Variant
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@error('variants') <p class="mb-4 text-xs text-red">{{ $message }}</p> @enderror
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
@foreach($variants as $i => $variant)
|
||||||
|
<div class="p-3 bg-s2 border border-white/[.06] rounded-lg space-y-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<input wire:model.live="variants.{{ $i }}.label" type="text" placeholder="Variant label"
|
||||||
|
class="block w-full px-2.5 py-1.5 bg-s3 border border-white/[.06] rounded text-xs text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50">
|
||||||
|
@error("variants.{$i}.label") <p class="mt-0.5 text-xs text-red">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-1.5 flex-shrink-0">
|
||||||
|
<input wire:model.live="variants.{{ $i }}.weight" type="number" min="1" max="99"
|
||||||
|
class="w-14 px-2 py-1.5 bg-s3 border border-white/[.06] rounded text-xs text-t1 text-center focus:outline-none focus:ring-1 focus:ring-blue/50 font-mono">
|
||||||
|
<span class="text-xs text-t3">%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if(count($variants) > 2)
|
||||||
|
<button wire:click="removeVariant({{ $i }})" type="button"
|
||||||
|
class="p-1 text-t3 hover:text-red transition-colors flex-shrink-0">
|
||||||
|
<x-heroicon-o-x-mark class="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input wire:model="variants.{{ $i }}.target_url" type="url" placeholder="https://example.com/variant"
|
||||||
|
class="block w-full px-2.5 py-1.5 bg-s3 border border-white/[.06] rounded text-xs text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50 font-mono">
|
||||||
|
@error("variants.{$i}.target_url") <p class="mt-0.5 text-xs text-red">{{ $message }}</p> @enderror
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Weight summary --}}
|
||||||
|
<div class="mt-3 flex items-center justify-between">
|
||||||
|
<button wire:click="rebalanceWeights" type="button"
|
||||||
|
class="text-xs text-t2 hover:text-t1 transition-colors flex items-center gap-1">
|
||||||
|
<x-heroicon-o-arrow-path class="w-3 h-3" />
|
||||||
|
Auto-balance weights
|
||||||
|
</button>
|
||||||
|
@php $total = array_sum(array_column($variants, 'weight')); @endphp
|
||||||
|
<span class="text-xs font-mono {{ $total === 100 ? 'text-green' : 'text-red' }}">
|
||||||
|
{{ $total }}% total
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-3 mt-5">
|
||||||
|
<button wire:click="save" wire:loading.attr="disabled" wire:target="save"
|
||||||
|
class="flex-1 px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity disabled:opacity-60">
|
||||||
|
<span wire:loading.remove wire:target="save">Save Variants</span>
|
||||||
|
<span wire:loading wire:target="save" class="flex items-center justify-center gap-2">
|
||||||
|
<svg class="animate-spin h-4 w-4" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||||||
|
</svg>
|
||||||
|
Saving…
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button wire:click="closeModal"
|
||||||
|
class="px-4 py-2.5 bg-s2 text-t2 text-sm font-medium rounded-lg hover:text-t1 transition-colors border border-white/[.06]">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -0,0 +1,70 @@
|
||||||
|
<div class="p-6 space-y-5">
|
||||||
|
<h3 class="text-lg font-semibold text-t1">UTM Builder</h3>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-t2 mb-1.5">Base URL *</label>
|
||||||
|
<input wire:model.live="baseUrl" type="url" placeholder="https://example.com/page"
|
||||||
|
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50 focus:border-blue/50">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-t2 mb-1.5">utm_source</label>
|
||||||
|
<input wire:model.live="utmSource" type="text" placeholder="google, newsletter…"
|
||||||
|
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50 focus:border-blue/50">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-t2 mb-1.5">utm_medium</label>
|
||||||
|
<input wire:model.live="utmMedium" type="text" placeholder="email, cpc…"
|
||||||
|
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50 focus:border-blue/50">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-t2 mb-1.5">utm_campaign</label>
|
||||||
|
<input wire:model.live="utmCampaign" type="text" placeholder="spring_sale…"
|
||||||
|
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50 focus:border-blue/50">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-t2 mb-1.5">utm_content</label>
|
||||||
|
<input wire:model.live="utmContent" type="text" placeholder="banner_v1…"
|
||||||
|
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50 focus:border-blue/50">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-t2 mb-1.5">utm_term</label>
|
||||||
|
<input wire:model.live="utmTerm" type="text" placeholder="running+shoes…"
|
||||||
|
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 placeholder-t3 focus:outline-none focus:ring-1 focus:ring-blue/50 focus:border-blue/50">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if($fullUrl)
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-t2 mb-1.5">Generated URL</label>
|
||||||
|
<div class="flex items-start gap-2 p-3 bg-s2 border border-white/[.06] rounded-lg">
|
||||||
|
<span class="flex-1 text-xs text-t1 font-mono break-all">{{ $fullUrl }}</span>
|
||||||
|
<button wire:click="copyToClipboard" type="button"
|
||||||
|
class="flex-shrink-0 p-1 text-t2 hover:text-t1 transition-colors">
|
||||||
|
@if($copied)
|
||||||
|
<x-heroicon-o-check class="w-4 h-4 text-green" />
|
||||||
|
@else
|
||||||
|
<x-heroicon-o-clipboard-document class="w-4 h-4" />
|
||||||
|
@endif
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="flex gap-3 pt-1">
|
||||||
|
<button wire:click="copyToClipboard" type="button" @disabled(!$fullUrl)
|
||||||
|
class="flex-1 px-4 py-2.5 bg-s2 border border-white/[.06] text-sm font-medium text-t1 rounded-lg hover:bg-s3 transition-colors disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2">
|
||||||
|
<x-heroicon-o-clipboard-document class="w-4 h-4" />
|
||||||
|
{{ $copied ? 'Copied!' : 'Copy URL' }}
|
||||||
|
</button>
|
||||||
|
<button wire:click="createLink" type="button" @disabled(!$fullUrl)
|
||||||
|
class="flex-1 px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed flex items-center justify-center gap-2">
|
||||||
|
<x-heroicon-o-plus class="w-4 h-4" />
|
||||||
|
Create Short Link
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="text-xl font-semibold text-t1">Free</div>
|
<div class="text-xl font-semibold text-t1">Free</div>
|
||||||
<div class="text-sm text-t2 mt-1">20 / 50 links used</div>
|
<div class="text-sm text-t2 mt-1">20 / 50 links used</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
<button class="px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||||
Upgrade Plan
|
Upgrade Plan
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
@endforeach
|
@endforeach
|
||||||
</ul>
|
</ul>
|
||||||
@if(!$plan['current'])
|
@if(!$plan['current'])
|
||||||
<button class="w-full px-4 py-2 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
<button class="w-full px-4 py-2 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||||
Upgrade to {{ $plan['name'] }}
|
Upgrade to {{ $plan['name'] }}
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
{{-- Quick Actions --}}
|
{{-- Quick Actions --}}
|
||||||
<div class="flex items-center gap-3 mb-6">
|
<div class="flex items-center gap-3 mb-6">
|
||||||
<a href="{{ route('w.links.index', request()->route('workspace')) }}"
|
<a href="{{ route('w.links.index', request()->route('workspace')) }}"
|
||||||
class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
|
class="px-4 py-2 bg-accent-gradient text-white rounded-lg text-sm font-medium hover:opacity-90 transition-opacity">
|
||||||
+ Create Link
|
+ Create Link
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('w.qr.index', request()->route('workspace')) }}"
|
<a href="{{ route('w.qr.index', request()->route('workspace')) }}"
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,14 @@
|
||||||
<div>
|
<div>
|
||||||
|
@php $ws = current_workspace(); @endphp
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<h1 class="text-2xl font-semibold text-t1">Custom Domains</h1>
|
<h1 class="text-2xl font-semibold text-t1">Custom Domains</h1>
|
||||||
<button class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
@if($ws)
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.add-domain', 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">
|
||||||
+ Add Domain
|
+ Add Domain
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</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">
|
||||||
|
|
@ -18,9 +23,13 @@
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-lg font-medium text-t1 mb-2">No custom domains</h3>
|
<h3 class="text-lg font-medium text-t1 mb-2">No custom domains</h3>
|
||||||
<p class="text-sm text-t2 mb-6">Add a custom domain to brand your short links.</p>
|
<p class="text-sm text-t2 mb-6">Add a custom domain to brand your short links.</p>
|
||||||
<button class="px-4 py-2.5 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
@if($ws)
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.add-domain', 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">
|
||||||
Add your first domain
|
Add your first domain
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
||||||
<nav class="flex gap-1 mt-4">
|
<nav class="flex gap-1 mt-4">
|
||||||
<a href="{{ route('profile.personal') }}" class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Personal Info</a>
|
<a href="{{ route('profile.personal') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Personal Info</a>
|
||||||
<a href="{{ route('profile.preferences') }}" class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Preferences</a>
|
<a href="{{ route('profile.preferences') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Preferences</a>
|
||||||
<a href="{{ route('profile.security') }}" class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Security</a>
|
<a href="{{ route('profile.security') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Security</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -28,7 +28,7 @@
|
||||||
@error('email') <p class="mt-1 text-xs text-red">{{ $message }}</p> @enderror
|
@error('email') <p class="mt-1 text-xs text-red">{{ $message }}</p> @enderror
|
||||||
</div>
|
</div>
|
||||||
<button wire:click="save"
|
<button wire:click="save"
|
||||||
class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
class="px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
||||||
<nav class="flex gap-1 mt-4">
|
<nav class="flex gap-1 mt-4">
|
||||||
<a href="{{ route('profile.personal') }}" class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Personal Info</a>
|
<a href="{{ route('profile.personal') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Personal Info</a>
|
||||||
<a href="{{ route('profile.preferences') }}" class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Preferences</a>
|
<a href="{{ route('profile.preferences') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Preferences</a>
|
||||||
<a href="{{ route('profile.security') }}" class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Security</a>
|
<a href="{{ route('profile.security') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Security</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button wire:click="save"
|
<button wire:click="save"
|
||||||
class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
class="px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||||
Save Preferences
|
Save Preferences
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
||||||
<nav class="flex gap-1 mt-4">
|
<nav class="flex gap-1 mt-4">
|
||||||
<a href="{{ route('profile.personal') }}" class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Personal Info</a>
|
<a href="{{ route('profile.personal') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Personal Info</a>
|
||||||
<a href="{{ route('profile.preferences') }}" class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Preferences</a>
|
<a href="{{ route('profile.preferences') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Preferences</a>
|
||||||
<a href="{{ route('profile.security') }}" class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Security</a>
|
<a href="{{ route('profile.security') }}" wire:navigate class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Security</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
<div class="mb-4 p-3 rounded-lg bg-green/10 border border-green/20 text-sm text-green">{{ session('status') }}</div>
|
<div class="mb-4 p-3 rounded-lg bg-green/10 border border-green/20 text-sm text-green">{{ session('status') }}</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-6">
|
<div class="bg-s1 border border-white/[.06] rounded-xl p-6 mb-6">
|
||||||
<h3 class="text-sm font-medium text-t1 mb-5">Change Password</h3>
|
<h3 class="text-sm font-medium text-t1 mb-5">Change Password</h3>
|
||||||
<div class="space-y-4 max-w-sm">
|
<div class="space-y-4 max-w-sm">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -33,9 +33,20 @@
|
||||||
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none focus:ring-1 focus:ring-blue/50">
|
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none focus:ring-1 focus:ring-blue/50">
|
||||||
</div>
|
</div>
|
||||||
<button wire:click="changePassword"
|
<button wire:click="changePassword"
|
||||||
class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
class="px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||||
Change Password
|
Change Password
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Danger Zone -->
|
||||||
|
<div class="bg-s1 border border-red/20 rounded-xl p-6">
|
||||||
|
<h3 class="text-sm font-medium text-red mb-2">Danger Zone</h3>
|
||||||
|
<p class="text-sm text-t2 mb-4">Permanently delete your account and all associated data. This cannot be undone.</p>
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.delete-account'})"
|
||||||
|
class="px-4 py-2.5 bg-red text-white text-sm font-medium rounded-lg hover:opacity-90 transition-opacity">
|
||||||
|
Delete Account
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button wire:click="saveWorkspace"
|
<button wire:click="saveWorkspace"
|
||||||
class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
class="px-4 py-2.5 bg-accent-gradient text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||||
Save Changes
|
Save Changes
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<div class="max-w-3xl">
|
<div class="max-w-3xl">
|
||||||
|
@php $ws = current_workspace(); @endphp
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="text-2xl font-semibold text-t1">Settings</h1>
|
<h1 class="text-2xl font-semibold text-t1">Settings</h1>
|
||||||
<nav class="flex gap-1 mt-4">
|
<nav class="flex gap-1 mt-4">
|
||||||
|
|
@ -13,9 +14,13 @@
|
||||||
|
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<p class="text-sm text-t2">Receive HTTP POST notifications for workspace events</p>
|
<p class="text-sm text-t2">Receive HTTP POST notifications for workspace events</p>
|
||||||
<button class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
@if($ws)
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.create-webhook', 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">
|
||||||
+ Add Webhook
|
+ Add Webhook
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-12 text-center">
|
<div class="bg-s1 border border-white/[.06] rounded-xl p-12 text-center">
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
<div>
|
<div>
|
||||||
|
@php $ws = current_workspace(); @endphp
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between mb-6">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-semibold text-t1">Team</h1>
|
<h1 class="text-2xl font-semibold text-t1">Team</h1>
|
||||||
<p class="text-sm text-t2 mt-1">Manage workspace members and permissions</p>
|
<p class="text-sm text-t2 mt-1">Manage workspace members and permissions</p>
|
||||||
</div>
|
</div>
|
||||||
<button class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
@if($ws)
|
||||||
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.invite-member', 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">
|
||||||
+ Invite Member
|
+ Invite Member
|
||||||
</button>
|
</button>
|
||||||
|
@endif
|
||||||
</div>
|
</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">
|
||||||
|
|
@ -16,7 +21,7 @@
|
||||||
|
|
||||||
<div class="divide-y divide-white/[.04]">
|
<div class="divide-y divide-white/[.04]">
|
||||||
@forelse($members as $member)
|
@forelse($members as $member)
|
||||||
<div class="flex items-center gap-4 px-5 py-4">
|
<div class="flex items-center gap-4 px-5 py-4 group">
|
||||||
{{-- Avatar --}}
|
{{-- Avatar --}}
|
||||||
<div class="w-9 h-9 rounded-full bg-blue flex items-center justify-center text-white text-sm font-semibold flex-shrink-0">
|
<div class="w-9 h-9 rounded-full bg-blue flex items-center justify-center text-white text-sm font-semibold flex-shrink-0">
|
||||||
{{ strtoupper(substr($member->user->name ?? $member->user->email, 0, 1)) }}
|
{{ strtoupper(substr($member->user->name ?? $member->user->email, 0, 1)) }}
|
||||||
|
|
@ -27,13 +32,21 @@
|
||||||
<div class="text-xs text-t2">{{ $member->user->email }}</div>
|
<div class="text-xs text-t2">{{ $member->user->email }}</div>
|
||||||
</div>
|
</div>
|
||||||
{{-- Role --}}
|
{{-- Role --}}
|
||||||
<span class="px-2.5 py-1 rounded-full text-xs font-medium
|
<button
|
||||||
{{ $member->role === 'owner' ? 'bg-amber/10 text-amber' : ($member->role === 'admin' ? 'bg-blue/10 text-blue' : 'bg-white/[.06] text-t2') }}">
|
@if($member->role !== 'owner')
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.edit-member-role', arguments: {memberId: {{ $member->id }}}})"
|
||||||
|
@endif
|
||||||
|
class="px-2.5 py-1 rounded-full text-xs font-medium transition-opacity
|
||||||
|
{{ $member->role === 'owner' ? 'bg-amber/10 text-amber cursor-default' : ($member->role === 'admin' ? 'bg-blue/10 text-blue hover:opacity-75 cursor-pointer' : 'bg-white/[.06] text-t2 hover:opacity-75 cursor-pointer') }}">
|
||||||
{{ ucfirst($member->role) }}
|
{{ ucfirst($member->role) }}
|
||||||
</span>
|
</button>
|
||||||
{{-- Actions --}}
|
{{-- Remove --}}
|
||||||
@if($member->role !== 'owner')
|
@if($member->role !== 'owner')
|
||||||
<button class="text-t3 text-xs hover:text-red transition-colors">Remove</button>
|
<button
|
||||||
|
@click="$dispatch('openModal', {component: 'modals.remove-member', arguments: {memberId: {{ $member->id }}}})"
|
||||||
|
class="opacity-0 group-hover:opacity-100 text-t3 text-xs hover:text-red transition-all">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@empty
|
@empty
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,67 @@ else
|
||||||
fail "nexxo@nimuli.com not found — run: php artisan db:seed"
|
fail "nexxo@nimuli.com not found — run: php artisan db:seed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── 10. Modal trigger coverage ────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 10 ] Modal trigger coverage"
|
||||||
|
declare -A TRIGGERS=(
|
||||||
|
["resources/views/livewire/pages/links/index.blade.php"]="create-link"
|
||||||
|
["resources/views/livewire/pages/qr-codes/index.blade.php"]="create-qr-code"
|
||||||
|
["resources/views/livewire/pages/bio/index.blade.php"]="create-bio-page"
|
||||||
|
["resources/views/livewire/pages/domains/index.blade.php"]="add-domain"
|
||||||
|
["resources/views/livewire/pages/team/members.blade.php"]="invite-member"
|
||||||
|
["resources/views/livewire/pages/settings/webhooks.blade.php"]="create-webhook"
|
||||||
|
["resources/views/livewire/pages/profile/security.blade.php"]="delete-account"
|
||||||
|
)
|
||||||
|
for view in "${!TRIGGERS[@]}"; do
|
||||||
|
modal="${TRIGGERS[$view]}"
|
||||||
|
if [ -f "$view" ] && grep -q "$modal" "$view"; then
|
||||||
|
ok "$(basename "$view" .blade.php) → $modal"
|
||||||
|
elif [ ! -f "$view" ]; then
|
||||||
|
fail "VIEW MISSING: $view"
|
||||||
|
else
|
||||||
|
fail "$(basename "$view" .blade.php) missing trigger for $modal"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ── 11. Sidebar collapse store ─────────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 11 ] Sidebar Alpine store"
|
||||||
|
if grep -q "collapsed" "resources/js/bootstrap.js"; then
|
||||||
|
ok "sidebar.collapsed in Alpine store"
|
||||||
|
else
|
||||||
|
fail "sidebar.collapsed missing from Alpine store"
|
||||||
|
fi
|
||||||
|
if grep -q "sidebarCollapsed" "resources/js/bootstrap.js"; then
|
||||||
|
ok "localStorage.sidebarCollapsed persisted"
|
||||||
|
else
|
||||||
|
fail "localStorage persistence missing from sidebar store"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── 12. CurrentWorkspace service ──────────────
|
||||||
|
echo ""
|
||||||
|
echo "[ 12 ] Octane-safe CurrentWorkspace"
|
||||||
|
if [ -f "app/Domains/Workspace/Services/CurrentWorkspace.php" ]; then
|
||||||
|
ok "CurrentWorkspace service exists"
|
||||||
|
else
|
||||||
|
fail "CurrentWorkspace service MISSING"
|
||||||
|
fi
|
||||||
|
if [ -f "app/Listeners/ResetCurrentWorkspace.php" ]; then
|
||||||
|
ok "ResetCurrentWorkspace listener exists"
|
||||||
|
else
|
||||||
|
fail "ResetCurrentWorkspace listener MISSING"
|
||||||
|
fi
|
||||||
|
if [ -f "app/helpers.php" ]; then
|
||||||
|
ok "app/helpers.php exists"
|
||||||
|
else
|
||||||
|
fail "app/helpers.php MISSING"
|
||||||
|
fi
|
||||||
|
if grep -q "helpers.php" "composer.json"; then
|
||||||
|
ok "helpers.php autoloaded in composer.json"
|
||||||
|
else
|
||||||
|
fail "helpers.php not autoloaded in composer.json"
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Summary ───────────────────────────────────
|
# ── Summary ───────────────────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "═══════════════════════════════════════════"
|
echo "═══════════════════════════════════════════"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue