feat: add profile, team, settings subpages
- Profile pages: PersonalInfo, Preferences, Security - Team: Members list with role management stub - Settings: ApiTokens (Sanctum, one-time display), Webhooks stub - Settings/Index: workspace name/slug update - Sidebar: Team nav item + SVG icon - Topbar: profile link → profile.personal - Routes: profile.personal, profile.preferences, profile.security, w.team.index, w.settings.api-tokens, w.settings.webhooks Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
b86bdd772a
commit
52d52824bc
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Pages\Profile;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class PersonalInfo extends Component
|
||||
{
|
||||
public string $name = '';
|
||||
public string $email = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->name = auth()->user()->name;
|
||||
$this->email = auth()->user()->email;
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate([
|
||||
'name' => 'required|string|max:100',
|
||||
'email' => 'required|email|max:200|unique:users,email,' . auth()->id(),
|
||||
]);
|
||||
|
||||
auth()->user()->update([
|
||||
'name' => $this->name,
|
||||
'email' => $this->email,
|
||||
]);
|
||||
|
||||
session()->flash('status', 'Profile updated.');
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.pages.profile.personal-info')
|
||||
->layout('layouts.nimuli-app', ['title' => 'Profile']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Pages\Profile;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Preferences extends Component
|
||||
{
|
||||
public string $locale = 'de';
|
||||
public string $theme = 'dark';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->locale = auth()->user()->locale ?? 'de';
|
||||
$this->theme = auth()->user()->theme ?? 'dark';
|
||||
}
|
||||
|
||||
public function save(): void
|
||||
{
|
||||
$this->validate([
|
||||
'locale' => 'required|in:de,en',
|
||||
'theme' => 'required|in:dark,light,system',
|
||||
]);
|
||||
|
||||
auth()->user()->update([
|
||||
'locale' => $this->locale,
|
||||
'theme' => $this->theme,
|
||||
]);
|
||||
|
||||
session()->flash('status', 'Preferences saved.');
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.pages.profile.preferences')
|
||||
->layout('layouts.nimuli-app', ['title' => 'Preferences']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Pages\Profile;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class Security extends Component
|
||||
{
|
||||
public string $currentPassword = '';
|
||||
public string $newPassword = '';
|
||||
public string $newPasswordConfirmation = '';
|
||||
|
||||
public function changePassword(): void
|
||||
{
|
||||
$this->validate([
|
||||
'currentPassword' => 'required',
|
||||
'newPassword' => 'required|min:8|confirmed',
|
||||
]);
|
||||
|
||||
if (!Hash::check($this->currentPassword, auth()->user()->password)) {
|
||||
$this->addError('currentPassword', 'Current password is incorrect.');
|
||||
return;
|
||||
}
|
||||
|
||||
auth()->user()->update(['password' => Hash::make($this->newPassword)]);
|
||||
$this->reset(['currentPassword', 'newPassword', 'newPasswordConfirmation']);
|
||||
session()->flash('status', 'Password changed successfully.');
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.pages.profile.security')
|
||||
->layout('layouts.nimuli-app', ['title' => 'Security']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Pages\Settings;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class ApiTokens extends Component
|
||||
{
|
||||
public string $tokenName = '';
|
||||
public ?string $newToken = null;
|
||||
|
||||
public function createToken(): void
|
||||
{
|
||||
$this->validate(['tokenName' => 'required|string|max:100']);
|
||||
|
||||
$token = auth()->user()->createToken($this->tokenName);
|
||||
$this->newToken = $token->plainTextToken;
|
||||
$this->tokenName = '';
|
||||
}
|
||||
|
||||
public function revokeToken(int $id): void
|
||||
{
|
||||
auth()->user()->tokens()->where('id', $id)->delete();
|
||||
}
|
||||
|
||||
public function dismissNewToken(): void
|
||||
{
|
||||
$this->newToken = null;
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$tokens = auth()->user()->tokens()->latest()->get();
|
||||
|
||||
return view('livewire.pages.settings.api-tokens', compact('tokens'))
|
||||
->layout('layouts.nimuli-app', ['title' => 'API Tokens']);
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,31 @@ use Livewire\Component;
|
|||
|
||||
class Index extends Component
|
||||
{
|
||||
public string $workspaceName = '';
|
||||
public string $workspaceSlug = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$ws = app('current_workspace');
|
||||
$this->workspaceName = $ws->name;
|
||||
$this->workspaceSlug = $ws->slug;
|
||||
}
|
||||
|
||||
public function saveWorkspace(): void
|
||||
{
|
||||
$this->validate([
|
||||
'workspaceName' => 'required|string|max:100',
|
||||
'workspaceSlug' => 'nullable|string|max:50|alpha_dash',
|
||||
]);
|
||||
|
||||
app('current_workspace')->update([
|
||||
'name' => $this->workspaceName,
|
||||
'slug' => $this->workspaceSlug ?: app('current_workspace')->slug,
|
||||
]);
|
||||
|
||||
session()->flash('status', 'Workspace settings saved.');
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.pages.settings.index')
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Pages\Settings;
|
||||
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Webhooks extends Component
|
||||
{
|
||||
public function render(): View
|
||||
{
|
||||
return view('livewire.pages.settings.webhooks')
|
||||
->layout('layouts.nimuli-app', ['title' => 'Webhooks']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Livewire\Pages\Team;
|
||||
|
||||
use App\Domains\Workspace\Models\WorkspaceMember;
|
||||
use Illuminate\View\View;
|
||||
use Livewire\Component;
|
||||
|
||||
class Members extends Component
|
||||
{
|
||||
public function render(): View
|
||||
{
|
||||
$workspace = app('current_workspace');
|
||||
|
||||
$members = WorkspaceMember::with('user')
|
||||
->where('workspace_id', $workspace->id)
|
||||
->get();
|
||||
|
||||
return view('livewire.pages.team.members', compact('members'))
|
||||
->layout('layouts.nimuli-app', ['title' => 'Team']);
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,13 @@
|
|||
'match' => 'w.billing.*',
|
||||
'icon' => 'billing',
|
||||
],
|
||||
[
|
||||
'label' => 'Team',
|
||||
'route' => 'w.team.index',
|
||||
'params' => [$wsUlid],
|
||||
'match' => 'w.team.*',
|
||||
'icon' => 'team',
|
||||
],
|
||||
] : [
|
||||
[
|
||||
'label' => 'Dashboard',
|
||||
|
|
@ -193,6 +200,10 @@
|
|||
<svg fill="none" viewBox="0 0 16 16" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="1.5" y="3.5" width="13" height="9" rx="1.5"/><path stroke-linecap="round" d="M1.5 6.5h13"/>
|
||||
</svg>
|
||||
@elseif($item['icon'] === 'team')
|
||||
<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"/>
|
||||
</svg>
|
||||
@endif
|
||||
</span>
|
||||
{{ $item['label'] }}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@
|
|||
<!-- Links -->
|
||||
<div class="py-1">
|
||||
<a
|
||||
href="{{ route('profile') }}"
|
||||
href="{{ route('profile.personal') }}"
|
||||
wire:navigate
|
||||
@click="userOpen = false"
|
||||
class="flex items-center gap-2.5 px-4 py-2 text-sm text-t2 hover:text-t1 hover:bg-s3 transition-colors"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
<div class="max-w-2xl">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
||||
<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.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.security') }}" class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Security</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@if(session('status'))
|
||||
<div class="mb-4 p-3 rounded-lg bg-green/10 border border-green/20 text-sm text-green">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-6">
|
||||
<h3 class="text-sm font-medium text-t1 mb-5">Personal Information</h3>
|
||||
<div class="space-y-4 max-w-sm">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">Name</label>
|
||||
<input wire:model="name" type="text"
|
||||
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">
|
||||
@error('name') <p class="mt-1 text-xs text-red">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">Email</label>
|
||||
<input wire:model="email" type="email"
|
||||
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">
|
||||
@error('email') <p class="mt-1 text-xs text-red">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<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">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<div class="max-w-2xl">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
||||
<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.preferences') }}" 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>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@if(session('status'))
|
||||
<div class="mb-4 p-3 rounded-lg bg-green/10 border border-green/20 text-sm text-green">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-6">
|
||||
<h3 class="text-sm font-medium text-t1 mb-5">Preferences</h3>
|
||||
<div class="space-y-5 max-w-sm">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">Language</label>
|
||||
<select wire:model="locale"
|
||||
class="block w-full px-3 py-2.5 bg-s2 border border-white/[.06] rounded-lg text-sm text-t1 focus:outline-none">
|
||||
<option value="de">Deutsch</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-2">Theme</label>
|
||||
<div class="flex gap-2">
|
||||
@foreach(['dark' => 'Dark', 'light' => 'Light', 'system' => 'System'] as $val => $label)
|
||||
<label class="flex-1 cursor-pointer">
|
||||
<input wire:model="theme" type="radio" value="{{ $val }}" class="sr-only">
|
||||
<div class="px-3 py-2 text-sm text-center rounded-lg border transition-colors
|
||||
{{ $theme === $val ? 'bg-blue/10 border-blue/40 text-blue' : 'bg-s2 border-white/[.06] text-t2 hover:text-t1' }}">
|
||||
{{ $label }}
|
||||
</div>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
<button wire:click="save"
|
||||
class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||
Save Preferences
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
<div class="max-w-2xl">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-t1">Profile</h1>
|
||||
<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.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.security') }}" class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Security</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@if(session('status'))
|
||||
<div class="mb-4 p-3 rounded-lg bg-green/10 border border-green/20 text-sm text-green">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-6">
|
||||
<h3 class="text-sm font-medium text-t1 mb-5">Change Password</h3>
|
||||
<div class="space-y-4 max-w-sm">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">Current Password</label>
|
||||
<input wire:model="currentPassword" type="password"
|
||||
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">
|
||||
@error('currentPassword') <p class="mt-1 text-xs text-red">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">New Password</label>
|
||||
<input wire:model="newPassword" type="password"
|
||||
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">
|
||||
@error('newPassword') <p class="mt-1 text-xs text-red">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">Confirm New Password</label>
|
||||
<input wire:model="newPasswordConfirmation" type="password"
|
||||
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>
|
||||
<button wire:click="changePassword"
|
||||
class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||
Change Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<div class="max-w-3xl">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-t1">Settings</h1>
|
||||
<nav class="flex gap-1 mt-4">
|
||||
<a href="{{ route('w.settings.index', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Workspace</a>
|
||||
<a href="{{ route('w.settings.api-tokens', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">API Tokens</a>
|
||||
<a href="{{ route('w.settings.webhooks', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Webhooks</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{{-- New token one-time display --}}
|
||||
@if($newToken)
|
||||
<div class="mb-4 p-4 rounded-xl bg-green/10 border border-green/20">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium text-green mb-2">Token created — copy it now, it won't be shown again</p>
|
||||
<code class="block font-mono text-xs text-t1 bg-s3 px-3 py-2 rounded-lg break-all">{{ $newToken }}</code>
|
||||
</div>
|
||||
<button wire:click="dismissNewToken" class="text-t3 hover:text-t1 flex-shrink-0 text-sm">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Create token --}}
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-6 mb-4">
|
||||
<h3 class="text-sm font-medium text-t1 mb-4">Create New Token</h3>
|
||||
<div class="flex gap-3 max-w-sm">
|
||||
<input wire:model="tokenName" type="text" placeholder="Token name"
|
||||
class="flex-1 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">
|
||||
<button wire:click="createToken"
|
||||
class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90 flex-shrink-0">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
@error('tokenName') <p class="mt-2 text-xs text-red">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Token list --}}
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-white/[.06]">
|
||||
<span class="text-xs font-medium text-t2 uppercase tracking-wider">Active Tokens</span>
|
||||
</div>
|
||||
<div class="divide-y divide-white/[.04]">
|
||||
@forelse($tokens as $token)
|
||||
<div class="flex items-center gap-4 px-5 py-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-t1">{{ $token->name }}</div>
|
||||
<div class="text-xs text-t2 mt-0.5">
|
||||
Created {{ $token->created_at->diffForHumans() }}
|
||||
@if($token->last_used_at) · Last used {{ $token->last_used_at->diffForHumans() }} @endif
|
||||
</div>
|
||||
</div>
|
||||
<button wire:click="revokeToken({{ $token->id }})"
|
||||
wire:confirm="Revoke this token?"
|
||||
class="text-red text-xs hover:opacity-80">Revoke</button>
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-5 py-10 text-center text-t3 text-sm">No API tokens yet</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1,32 +1,51 @@
|
|||
<div>
|
||||
<div class="max-w-3xl">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-t1">Settings</h1>
|
||||
<p class="text-sm text-t2 mt-1">Manage your workspace settings</p>
|
||||
<nav class="flex gap-1 mt-4">
|
||||
<a href="{{ route('w.settings.index', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Workspace</a>
|
||||
<a href="{{ route('w.settings.api-tokens', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">API Tokens</a>
|
||||
<a href="{{ route('w.settings.webhooks', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Webhooks</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
{{-- Workspace Name --}}
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-6">
|
||||
<h3 class="text-sm font-medium text-t1 mb-4">Workspace</h3>
|
||||
<div class="max-w-sm space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">Name</label>
|
||||
<input type="text" value="{{ app('current_workspace')?->name }}"
|
||||
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>
|
||||
<button class="px-4 py-2 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if(session('status'))
|
||||
<div class="mb-4 p-3 rounded-lg bg-green/10 border border-green/20 text-sm text-green">{{ session('status') }}</div>
|
||||
@endif
|
||||
|
||||
{{-- 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">Once you delete a workspace, there is no going back.</p>
|
||||
<button class="px-4 py-2 bg-red/10 text-red text-sm font-medium rounded-lg hover:bg-red/20 border border-red/20">
|
||||
Delete Workspace
|
||||
{{-- Workspace Info --}}
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-6 mb-4">
|
||||
<h3 class="text-sm font-medium text-t1 mb-5">Workspace Settings</h3>
|
||||
<div class="space-y-4 max-w-sm">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">Workspace Name</label>
|
||||
<input wire:model="workspaceName" type="text"
|
||||
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">
|
||||
@error('workspaceName') <p class="mt-1 text-xs text-red">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-t2 mb-1.5">Workspace Slug</label>
|
||||
<div class="flex items-center bg-s2 border border-white/[.06] rounded-lg overflow-hidden">
|
||||
<span class="px-3 py-2.5 text-sm text-t3 border-r border-white/[.06]">nimu.li/~</span>
|
||||
<input wire:model="workspaceSlug" type="text"
|
||||
class="flex-1 px-3 py-2.5 bg-transparent text-sm text-t1 font-mono focus:outline-none">
|
||||
</div>
|
||||
</div>
|
||||
<button wire:click="saveWorkspace"
|
||||
class="px-4 py-2.5 bg-blue text-white text-sm font-medium rounded-lg hover:opacity-90">
|
||||
Save Changes
|
||||
</button>
|
||||
</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 this workspace and all its data. This action cannot be undone.</p>
|
||||
<button class="px-4 py-2 bg-red/10 text-red text-sm font-medium rounded-lg hover:bg-red/20 border border-red/20">
|
||||
Delete Workspace
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,30 @@
|
|||
<div class="max-w-3xl">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-t1">Settings</h1>
|
||||
<nav class="flex gap-1 mt-4">
|
||||
<a href="{{ route('w.settings.index', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">Workspace</a>
|
||||
<a href="{{ route('w.settings.api-tokens', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg text-t2 hover:text-t1 hover:bg-s2">API Tokens</a>
|
||||
<a href="{{ route('w.settings.webhooks', request()->route('workspace')) }}"
|
||||
class="px-3 py-1.5 text-sm rounded-lg bg-blue/10 text-blue font-medium">Webhooks</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<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">
|
||||
+ Add Webhook
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl p-12 text-center">
|
||||
<div class="w-14 h-14 bg-s2 rounded-2xl flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="w-7 h-7 text-t3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-base font-medium text-t1 mb-2">No webhooks configured</h3>
|
||||
<p class="text-sm text-t2">Add a webhook URL to receive real-time events.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<div>
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold text-t1">Team</h1>
|
||||
<p class="text-sm text-t2 mt-1">Manage workspace members and permissions</p>
|
||||
</div>
|
||||
<button class="px-4 py-2 bg-blue text-white rounded-lg text-sm font-medium hover:opacity-90">
|
||||
+ Invite Member
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="bg-s1 border border-white/[.06] rounded-xl overflow-hidden">
|
||||
<div class="px-5 py-3 border-b border-white/[.06] flex items-center justify-between">
|
||||
<span class="text-xs font-medium text-t2 uppercase tracking-wider">{{ $members->count() }} {{ Str::plural('member', $members->count()) }}</span>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-white/[.04]">
|
||||
@forelse($members as $member)
|
||||
<div class="flex items-center gap-4 px-5 py-4">
|
||||
{{-- 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">
|
||||
{{ strtoupper(substr($member->user->name ?? $member->user->email, 0, 1)) }}
|
||||
</div>
|
||||
{{-- Info --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="text-sm font-medium text-t1">{{ $member->user->name ?? '—' }}</div>
|
||||
<div class="text-xs text-t2">{{ $member->user->email }}</div>
|
||||
</div>
|
||||
{{-- Role --}}
|
||||
<span class="px-2.5 py-1 rounded-full text-xs font-medium
|
||||
{{ $member->role === 'owner' ? 'bg-amber/10 text-amber' : ($member->role === 'admin' ? 'bg-blue/10 text-blue' : 'bg-white/[.06] text-t2') }}">
|
||||
{{ ucfirst($member->role) }}
|
||||
</span>
|
||||
{{-- Actions --}}
|
||||
@if($member->role !== 'owner')
|
||||
<button class="text-t3 text-xs hover:text-red transition-colors">Remove</button>
|
||||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-5 py-12 text-center text-t3 text-sm">No team members yet</div>
|
||||
@endforelse
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -22,9 +22,13 @@ Route::get('dashboard', function () {
|
|||
return view('dashboard');
|
||||
})->middleware(['auth', 'verified'])->name('dashboard');
|
||||
|
||||
Route::view('profile', 'profile')
|
||||
->middleware(['auth'])
|
||||
->name('profile');
|
||||
Route::get('profile', fn() => redirect()->route('profile.personal'))->middleware(['auth'])->name('profile');
|
||||
|
||||
Route::middleware(['auth'])->group(function () {
|
||||
Route::get('/profile/personal', App\Livewire\Pages\Profile\PersonalInfo::class)->name('profile.personal');
|
||||
Route::get('/profile/preferences', App\Livewire\Pages\Profile\Preferences::class)->name('profile.preferences');
|
||||
Route::get('/profile/security', App\Livewire\Pages\Profile\Security::class)->name('profile.security');
|
||||
});
|
||||
|
||||
// Workspace-scoped routes — placed before slug catch-all
|
||||
Route::middleware(['auth', 'verified', ResolveWorkspace::class])
|
||||
|
|
@ -38,7 +42,10 @@ Route::middleware(['auth', 'verified', ResolveWorkspace::class])
|
|||
Route::get('/analytics', App\Livewire\Pages\Analytics\Index::class)->name('analytics.index');
|
||||
Route::get('/domains', App\Livewire\Pages\Domains\Index::class)->name('domains.index');
|
||||
Route::get('/settings', App\Livewire\Pages\Settings\Index::class)->name('settings.index');
|
||||
Route::get('/settings/api-tokens', App\Livewire\Pages\Settings\ApiTokens::class)->name('settings.api-tokens');
|
||||
Route::get('/settings/webhooks', App\Livewire\Pages\Settings\Webhooks::class)->name('settings.webhooks');
|
||||
Route::get('/billing', App\Livewire\Pages\Billing\Index::class)->name('billing.index');
|
||||
Route::get('/team', App\Livewire\Pages\Team\Members::class)->name('team.index');
|
||||
});
|
||||
|
||||
// Bio page route — must be before the slug catch-all
|
||||
|
|
|
|||
Loading…
Reference in New Issue