From 52d52824bcacd28bcce84d9d2cf3a9246a7a8e71 Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 16 May 2026 11:07:31 +0200 Subject: [PATCH] feat: add profile, team, settings subpages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- app/Livewire/Pages/Profile/PersonalInfo.php | 39 +++++++++++ app/Livewire/Pages/Profile/Preferences.php | 39 +++++++++++ app/Livewire/Pages/Profile/Security.php | 37 +++++++++++ app/Livewire/Pages/Settings/ApiTokens.php | 40 ++++++++++++ app/Livewire/Pages/Settings/Index.php | 25 +++++++ app/Livewire/Pages/Settings/Webhooks.php | 15 +++++ app/Livewire/Pages/Team/Members.php | 22 +++++++ resources/views/components/sidebar.blade.php | 11 ++++ resources/views/components/topbar.blade.php | 2 +- .../pages/profile/personal-info.blade.php | 36 ++++++++++ .../pages/profile/preferences.blade.php | 46 +++++++++++++ .../livewire/pages/profile/security.blade.php | 41 ++++++++++++ .../pages/settings/api-tokens.blade.php | 65 +++++++++++++++++++ .../livewire/pages/settings/index.blade.php | 65 ++++++++++++------- .../pages/settings/webhooks.blade.php | 30 +++++++++ .../livewire/pages/team/members.blade.php | 44 +++++++++++++ routes/web.php | 13 +++- 17 files changed, 543 insertions(+), 27 deletions(-) create mode 100644 app/Livewire/Pages/Profile/PersonalInfo.php create mode 100644 app/Livewire/Pages/Profile/Preferences.php create mode 100644 app/Livewire/Pages/Profile/Security.php create mode 100644 app/Livewire/Pages/Settings/ApiTokens.php create mode 100644 app/Livewire/Pages/Settings/Webhooks.php create mode 100644 app/Livewire/Pages/Team/Members.php create mode 100644 resources/views/livewire/pages/profile/personal-info.blade.php create mode 100644 resources/views/livewire/pages/profile/preferences.blade.php create mode 100644 resources/views/livewire/pages/profile/security.blade.php create mode 100644 resources/views/livewire/pages/settings/api-tokens.blade.php create mode 100644 resources/views/livewire/pages/settings/webhooks.blade.php create mode 100644 resources/views/livewire/pages/team/members.blade.php diff --git a/app/Livewire/Pages/Profile/PersonalInfo.php b/app/Livewire/Pages/Profile/PersonalInfo.php new file mode 100644 index 0000000..4be3670 --- /dev/null +++ b/app/Livewire/Pages/Profile/PersonalInfo.php @@ -0,0 +1,39 @@ +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']); + } +} diff --git a/app/Livewire/Pages/Profile/Preferences.php b/app/Livewire/Pages/Profile/Preferences.php new file mode 100644 index 0000000..1c0cfdb --- /dev/null +++ b/app/Livewire/Pages/Profile/Preferences.php @@ -0,0 +1,39 @@ +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']); + } +} diff --git a/app/Livewire/Pages/Profile/Security.php b/app/Livewire/Pages/Profile/Security.php new file mode 100644 index 0000000..1ebfa26 --- /dev/null +++ b/app/Livewire/Pages/Profile/Security.php @@ -0,0 +1,37 @@ +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']); + } +} diff --git a/app/Livewire/Pages/Settings/ApiTokens.php b/app/Livewire/Pages/Settings/ApiTokens.php new file mode 100644 index 0000000..bf8c287 --- /dev/null +++ b/app/Livewire/Pages/Settings/ApiTokens.php @@ -0,0 +1,40 @@ +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']); + } +} diff --git a/app/Livewire/Pages/Settings/Index.php b/app/Livewire/Pages/Settings/Index.php index d0b300f..b517699 100644 --- a/app/Livewire/Pages/Settings/Index.php +++ b/app/Livewire/Pages/Settings/Index.php @@ -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') diff --git a/app/Livewire/Pages/Settings/Webhooks.php b/app/Livewire/Pages/Settings/Webhooks.php new file mode 100644 index 0000000..23ba279 --- /dev/null +++ b/app/Livewire/Pages/Settings/Webhooks.php @@ -0,0 +1,15 @@ +layout('layouts.nimuli-app', ['title' => 'Webhooks']); + } +} diff --git a/app/Livewire/Pages/Team/Members.php b/app/Livewire/Pages/Team/Members.php new file mode 100644 index 0000000..a34fa50 --- /dev/null +++ b/app/Livewire/Pages/Team/Members.php @@ -0,0 +1,22 @@ +where('workspace_id', $workspace->id) + ->get(); + + return view('livewire.pages.team.members', compact('members')) + ->layout('layouts.nimuli-app', ['title' => 'Team']); + } +} diff --git a/resources/views/components/sidebar.blade.php b/resources/views/components/sidebar.blade.php index a0b516d..7cb42a3 100644 --- a/resources/views/components/sidebar.blade.php +++ b/resources/views/components/sidebar.blade.php @@ -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 @@ + @elseif($item['icon'] === 'team') + + + @endif {{ $item['label'] }} diff --git a/resources/views/components/topbar.blade.php b/resources/views/components/topbar.blade.php index 5779d5d..e54233e 100644 --- a/resources/views/components/topbar.blade.php +++ b/resources/views/components/topbar.blade.php @@ -99,7 +99,7 @@
+ + + @if(session('status')) +
{{ session('status') }}
+ @endif + +
+

Personal Information

+
+
+ + + @error('name')

{{ $message }}

@enderror +
+
+ + + @error('email')

{{ $message }}

@enderror +
+ +
+
+
diff --git a/resources/views/livewire/pages/profile/preferences.blade.php b/resources/views/livewire/pages/profile/preferences.blade.php new file mode 100644 index 0000000..055edc3 --- /dev/null +++ b/resources/views/livewire/pages/profile/preferences.blade.php @@ -0,0 +1,46 @@ +
+
+

Profile

+ +
+ + @if(session('status')) +
{{ session('status') }}
+ @endif + +
+

Preferences

+
+
+ + +
+
+ +
+ @foreach(['dark' => 'Dark', 'light' => 'Light', 'system' => 'System'] as $val => $label) + + @endforeach +
+
+ +
+
+
diff --git a/resources/views/livewire/pages/profile/security.blade.php b/resources/views/livewire/pages/profile/security.blade.php new file mode 100644 index 0000000..0d2f4bb --- /dev/null +++ b/resources/views/livewire/pages/profile/security.blade.php @@ -0,0 +1,41 @@ +
+
+

Profile

+ +
+ + @if(session('status')) +
{{ session('status') }}
+ @endif + +
+

Change Password

+
+
+ + + @error('currentPassword')

{{ $message }}

@enderror +
+
+ + + @error('newPassword')

{{ $message }}

@enderror +
+
+ + +
+ +
+
+
diff --git a/resources/views/livewire/pages/settings/api-tokens.blade.php b/resources/views/livewire/pages/settings/api-tokens.blade.php new file mode 100644 index 0000000..928868a --- /dev/null +++ b/resources/views/livewire/pages/settings/api-tokens.blade.php @@ -0,0 +1,65 @@ +
+
+

Settings

+ +
+ + {{-- New token one-time display --}} + @if($newToken) +
+
+
+

Token created — copy it now, it won't be shown again

+ {{ $newToken }} +
+ +
+
+ @endif + + {{-- Create token --}} +
+

Create New Token

+
+ + +
+ @error('tokenName')

{{ $message }}

@enderror +
+ + {{-- Token list --}} +
+
+ Active Tokens +
+
+ @forelse($tokens as $token) +
+
+
{{ $token->name }}
+
+ Created {{ $token->created_at->diffForHumans() }} + @if($token->last_used_at) · Last used {{ $token->last_used_at->diffForHumans() }} @endif +
+
+ +
+ @empty +
No API tokens yet
+ @endforelse +
+
+
diff --git a/resources/views/livewire/pages/settings/index.blade.php b/resources/views/livewire/pages/settings/index.blade.php index a9eacef..f13d373 100644 --- a/resources/views/livewire/pages/settings/index.blade.php +++ b/resources/views/livewire/pages/settings/index.blade.php @@ -1,32 +1,51 @@ -
+

Settings

-

Manage your workspace settings

+
-
- {{-- Workspace Name --}} -
-

Workspace

-
-
- - -
- -
-
+ @if(session('status')) +
{{ session('status') }}
+ @endif - {{-- Danger Zone --}} -
-

Danger Zone

-

Once you delete a workspace, there is no going back.

-
+ + {{-- Danger Zone --}} +
+

Danger Zone

+

Permanently delete this workspace and all its data. This action cannot be undone.

+ +
diff --git a/resources/views/livewire/pages/settings/webhooks.blade.php b/resources/views/livewire/pages/settings/webhooks.blade.php new file mode 100644 index 0000000..23b0c61 --- /dev/null +++ b/resources/views/livewire/pages/settings/webhooks.blade.php @@ -0,0 +1,30 @@ +
+
+

Settings

+ +
+ +
+

Receive HTTP POST notifications for workspace events

+ +
+ +
+
+ + + +
+

No webhooks configured

+

Add a webhook URL to receive real-time events.

+
+
diff --git a/resources/views/livewire/pages/team/members.blade.php b/resources/views/livewire/pages/team/members.blade.php new file mode 100644 index 0000000..4b94523 --- /dev/null +++ b/resources/views/livewire/pages/team/members.blade.php @@ -0,0 +1,44 @@ +
+
+
+

Team

+

Manage workspace members and permissions

+
+ +
+ +
+
+ {{ $members->count() }} {{ Str::plural('member', $members->count()) }} +
+ +
+ @forelse($members as $member) +
+ {{-- Avatar --}} +
+ {{ strtoupper(substr($member->user->name ?? $member->user->email, 0, 1)) }} +
+ {{-- Info --}} +
+
{{ $member->user->name ?? '—' }}
+
{{ $member->user->email }}
+
+ {{-- Role --}} + + {{ ucfirst($member->role) }} + + {{-- Actions --}} + @if($member->role !== 'owner') + + @endif +
+ @empty +
No team members yet
+ @endforelse +
+
+
diff --git a/routes/web.php b/routes/web.php index 4add290..895017d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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