Skip to content

Commit b1ab38e

Browse files
simonhampclaude
andcommitted
Upgrade dashboard: Flux UI, route restructuring, developer settings
- Change all /customer routes to /dashboard prefix - Add developer settings page with display name and Stripe status - Add plugin status tabs (Pending, Rejected, Approved) - Move rejection reason/resubmit to top of plugin edit screen - Upgrade auth pages to use Flux components with new auth layout - Restructure sidebar: rename Developer Hub to Hub, add Discord link - Show only approved paid plugins on developer hub - Move session messages to top of dashboard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent c2f77f7 commit b1ab38e

27 files changed

Lines changed: 927 additions & 654 deletions

app/Livewire/Customer/Developer/Dashboard.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ public function developerAccount()
3535
#[Computed]
3636
public function plugins(): Collection
3737
{
38-
return auth()->user()->plugins()->withCount('licenses')->get();
38+
return auth()->user()->plugins()
39+
->where('status', \App\Enums\PluginStatus::Approved)
40+
->where('type', \App\Enums\PluginType::Paid)
41+
->withCount('licenses')
42+
->get();
3943
}
4044

4145
#[Computed]

app/Livewire/Customer/Developer/Onboarding.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ public function mount(): void
1717

1818
if ($developerAccount && $developerAccount->hasCompletedOnboarding()) {
1919
$this->redirect(route('customer.developer.dashboard'), navigate: true);
20-
session()->flash('message', 'Your developer account is already set up.');
2120
}
2221
}
2322

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
namespace App\Livewire\Customer\Developer;
4+
5+
use App\Models\DeveloperAccount;
6+
use Livewire\Attributes\Computed;
7+
use Livewire\Attributes\Layout;
8+
use Livewire\Attributes\Title;
9+
use Livewire\Attributes\Validate;
10+
use Livewire\Component;
11+
12+
#[Layout('components.layouts.dashboard')]
13+
#[Title('Developer Settings')]
14+
class Settings extends Component
15+
{
16+
#[Validate('nullable|string|max:255')]
17+
public ?string $displayName = null;
18+
19+
public function mount(): void
20+
{
21+
$this->displayName = auth()->user()->display_name;
22+
}
23+
24+
#[Computed]
25+
public function developerAccount(): ?DeveloperAccount
26+
{
27+
return auth()->user()->developerAccount;
28+
}
29+
30+
public function updateDisplayName(): void
31+
{
32+
$this->validate();
33+
34+
auth()->user()->update([
35+
'display_name' => $this->displayName ?: null,
36+
]);
37+
38+
session()->flash('success', 'Display name updated successfully!');
39+
}
40+
}

app/Livewire/Customer/Plugins/Index.php

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,46 @@
22

33
namespace App\Livewire\Customer\Plugins;
44

5+
use App\Enums\PluginStatus;
56
use App\Models\DeveloperAccount;
6-
use App\Models\Plugin;
7+
78
use Illuminate\Database\Eloquent\Collection;
89
use Livewire\Attributes\Computed;
910
use Livewire\Attributes\Layout;
1011
use Livewire\Attributes\Title;
11-
use Livewire\Attributes\Validate;
12+
use Livewire\Attributes\Url;
1213
use Livewire\Component;
1314

1415
#[Layout('components.layouts.dashboard')]
1516
#[Title('Your Plugins')]
1617
class Index extends Component
1718
{
18-
#[Validate('nullable|string|max:255')]
19-
public ?string $displayName = null;
19+
#[Url]
20+
public string $status = 'pending';
2021

21-
public function mount(): void
22+
#[Computed]
23+
public function plugins(): Collection
2224
{
23-
$this->displayName = auth()->user()->display_name;
25+
return auth()->user()->plugins()
26+
->where('status', $this->status)
27+
->orderBy('created_at', 'desc')
28+
->get();
2429
}
2530

2631
#[Computed]
27-
public function plugins(): Collection
32+
public function pluginCounts(): array
2833
{
29-
return auth()->user()->plugins()->orderBy('created_at', 'desc')->get();
34+
$counts = auth()->user()->plugins()
35+
->selectRaw('status, count(*) as count')
36+
->groupBy('status')
37+
->pluck('count', 'status')
38+
->toArray();
39+
40+
return [
41+
PluginStatus::Approved->value => $counts[PluginStatus::Approved->value] ?? 0,
42+
PluginStatus::Pending->value => $counts[PluginStatus::Pending->value] ?? 0,
43+
PluginStatus::Rejected->value => $counts[PluginStatus::Rejected->value] ?? 0,
44+
];
3045
}
3146

3247
#[Computed]
@@ -35,36 +50,6 @@ public function developerAccount(): ?DeveloperAccount
3550
return auth()->user()->developerAccount;
3651
}
3752

38-
public function updateDisplayName(): void
39-
{
40-
$this->validate();
41-
42-
auth()->user()->update([
43-
'display_name' => $this->displayName ?: null,
44-
]);
45-
46-
session()->flash('success', 'Display name updated successfully!');
47-
}
48-
49-
public function resubmitPlugin(int $pluginId): void
50-
{
51-
$plugin = Plugin::findOrFail($pluginId);
52-
53-
if ($plugin->user_id !== auth()->id()) {
54-
abort(403);
55-
}
56-
57-
if (! $plugin->isRejected()) {
58-
session()->flash('error', 'Only rejected plugins can be resubmitted.');
59-
60-
return;
61-
}
62-
63-
$plugin->resubmit();
64-
65-
session()->flash('success', 'Your plugin has been resubmitted for review!');
66-
}
67-
6853
public function render()
6954
{
7055
return view('livewire.customer.plugins.index');
Lines changed: 23 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,28 @@
1-
<x-layout title="Reset Password">
2-
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
3-
<div class="max-w-md w-full space-y-8">
4-
<div>
5-
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
6-
Reset your password
7-
</h2>
8-
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
9-
Enter your email address and we'll send you a link to reset your password.
10-
</p>
11-
</div>
1+
<x-layouts.auth title="Reset Password">
2+
<flux:heading size="xl" class="text-center">Reset your password</flux:heading>
3+
<flux:text class="mt-2 text-center">
4+
Enter your email address and we'll send you a link to reset your password.
5+
</flux:text>
126

13-
@if (session('status'))
14-
<div class="bg-green-100 dark:bg-green-800 border border-green-400 text-green-700 dark:text-green-100 px-4 py-3 rounded">
15-
{{ session('status') }}
16-
</div>
17-
@endif
7+
@if (session('status'))
8+
<flux:callout variant="success" icon="check-circle" class="mt-6">
9+
<flux:callout.text>{{ session('status') }}</flux:callout.text>
10+
</flux:callout>
11+
@endif
1812

19-
<form class="mt-8 space-y-6" action="{{ route('password.email') }}" method="POST">
20-
@csrf
21-
22-
<div>
23-
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
24-
Email address
25-
</label>
26-
<input
27-
id="email"
28-
name="email"
29-
type="email"
30-
autocomplete="email"
31-
required
32-
value="{{ old('email') }}"
33-
class="mt-1 appearance-none relative block w-full px-3 py-2 border @error('email') border-red-300 @else border-gray-300 @enderror dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
34-
placeholder="Enter your email address"
35-
>
36-
@error('email')
37-
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
38-
@enderror
39-
</div>
13+
<form action="{{ route('password.email') }}" method="POST" class="mt-8 space-y-6">
14+
@csrf
4015

41-
<div>
42-
<button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-900">
43-
Send Password Reset Link
44-
</button>
45-
</div>
16+
<flux:field>
17+
<flux:label>Email address</flux:label>
18+
<flux:input name="email" type="email" autocomplete="email" required value="{{ old('email') }}" placeholder="Enter your email address" :invalid="$errors->has('email')" />
19+
<flux:error name="email" />
20+
</flux:field>
4621

47-
<div class="text-center">
48-
<a href="{{ route('customer.login') }}" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
49-
Back to login
50-
</a>
51-
</div>
52-
</form>
22+
<flux:button type="submit" variant="primary" class="w-full">Send Password Reset Link</flux:button>
23+
24+
<div class="text-center">
25+
<flux:link href="{{ route('customer.login') }}">Back to login</flux:link>
5326
</div>
54-
</div>
55-
</x-layout>
27+
</form>
28+
</x-layouts.auth>
Lines changed: 49 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,53 @@
1-
<x-layout title="Customer Login">
2-
<div class="min-h-screen flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
3-
<div class="max-w-md w-full space-y-8">
4-
<div>
5-
<h2 class="mt-6 text-center text-3xl font-extrabold text-gray-900 dark:text-white">
6-
Sign in to your account
7-
</h2>
8-
<p class="mt-2 text-center text-sm text-gray-600 dark:text-gray-400">
9-
Or
10-
<a href="{{ route('customer.register') }}" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
11-
create a new account
12-
</a>
13-
</p>
1+
<x-layouts.auth title="Sign In">
2+
<flux:heading size="xl" class="text-center">Sign in to your account</flux:heading>
3+
<flux:text class="mt-2 text-center">
4+
Or <flux:link href="{{ route('customer.register') }}">create a new account</flux:link>
5+
</flux:text>
6+
7+
@if (session('status'))
8+
<flux:callout variant="success" icon="check-circle" class="mt-6">
9+
<flux:callout.text>{{ session('status') }}</flux:callout.text>
10+
</flux:callout>
11+
@endif
12+
13+
@if (session('message'))
14+
<flux:callout variant="secondary" icon="information-circle" class="mt-6">
15+
<flux:callout.text>{{ session('message') }}</flux:callout.text>
16+
</flux:callout>
17+
@endif
18+
19+
<form action="{{ route('customer.login') }}" method="POST" class="mt-8 space-y-6">
20+
@csrf
21+
22+
<div class="space-y-4">
23+
<flux:field>
24+
<flux:label>Email address</flux:label>
25+
<flux:input name="email" type="email" autocomplete="email" required value="{{ old('email') }}" placeholder="Enter your email address" :invalid="$errors->has('email')" />
26+
<flux:error name="email" />
27+
</flux:field>
28+
29+
<flux:field>
30+
<flux:label>Password</flux:label>
31+
<flux:input name="password" type="password" autocomplete="current-password" required viewable placeholder="Enter your password" :invalid="$errors->has('password')" />
32+
<flux:error name="password" />
33+
</flux:field>
34+
35+
<div class="flex items-center justify-between">
36+
<flux:checkbox name="remember" label="Remember me" />
37+
38+
<flux:link href="{{ route('password.request') }}" class="text-sm">Forgot your password?</flux:link>
1439
</div>
40+
</div>
1541

16-
@if (session('status'))
17-
<div class="bg-green-100 dark:bg-green-800 border border-green-400 text-green-700 dark:text-green-100 px-4 py-3 rounded">
18-
{{ session('status') }}
19-
</div>
20-
@endif
21-
22-
<form class="mt-8 space-y-6" action="{{ route('customer.login') }}" method="POST">
23-
@csrf
24-
25-
<div class="space-y-4">
26-
<div>
27-
<label for="email" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
28-
Email address
29-
</label>
30-
<input
31-
id="email"
32-
name="email"
33-
type="email"
34-
autocomplete="email"
35-
required
36-
value="{{ old('email') }}"
37-
class="mt-1 appearance-none relative block w-full px-3 py-2 border @error('email') border-red-300 @else border-gray-300 @enderror dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
38-
placeholder="Enter your email address"
39-
>
40-
@error('email')
41-
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
42-
@enderror
43-
</div>
44-
45-
<div>
46-
<label for="password" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
47-
Password
48-
</label>
49-
<input
50-
id="password"
51-
name="password"
52-
type="password"
53-
autocomplete="current-password"
54-
required
55-
class="mt-1 appearance-none relative block w-full px-3 py-2 border @error('password') border-red-300 @else border-gray-300 @enderror dark:border-gray-600 placeholder-gray-500 dark:placeholder-gray-400 text-gray-900 dark:text-white dark:bg-gray-800 rounded-md focus:outline-none focus:ring-blue-500 focus:border-blue-500 focus:z-10 sm:text-sm"
56-
placeholder="Enter your password"
57-
>
58-
@error('password')
59-
<p class="mt-1 text-sm text-red-600 dark:text-red-400">{{ $message }}</p>
60-
@enderror
61-
</div>
62-
63-
<div class="flex items-center justify-between">
64-
<div class="flex items-center">
65-
<input
66-
id="remember"
67-
name="remember"
68-
type="checkbox"
69-
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
70-
>
71-
<label for="remember" class="ml-2 block text-sm text-gray-700 dark:text-gray-300">
72-
Remember me
73-
</label>
74-
</div>
75-
76-
<div class="text-sm">
77-
<a href="{{ route('password.request') }}" class="font-medium text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300">
78-
Forgot your password?
79-
</a>
80-
</div>
81-
</div>
82-
</div>
83-
84-
<div>
85-
<button type="submit" class="group relative w-full flex justify-center py-2 px-4 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-900">
86-
Sign in
87-
</button>
88-
</div>
89-
</form>
42+
<flux:button type="submit" variant="primary" class="w-full">Sign in</flux:button>
43+
</form>
9044

91-
<div class="mt-6">
92-
<div class="relative">
93-
<div class="absolute inset-0 flex items-center">
94-
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
95-
</div>
96-
<div class="relative flex justify-center text-sm">
97-
<span class="px-2 bg-white dark:bg-gray-900 text-gray-500 dark:text-gray-400">Or continue with</span>
98-
</div>
99-
</div>
45+
<flux:separator text="Or" class="my-6" />
10046

101-
<div class="mt-6">
102-
<a href="{{ route('login.github') }}" class="w-full inline-flex justify-center py-2 px-4 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 dark:focus:ring-offset-gray-900">
103-
<svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
104-
<path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd" />
105-
</svg>
106-
Continue with GitHub
107-
</a>
108-
</div>
109-
</div>
110-
</div>
111-
</div>
112-
</x-layout>
47+
<flux:button href="{{ route('login.github') }}" variant="ghost" class="w-full">
48+
<svg class="size-5" fill="currentColor" viewBox="0 0 20 20" aria-hidden="true">
49+
<path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd" />
50+
</svg>
51+
Sign in with GitHub
52+
</flux:button>
53+
</x-layouts.auth>

0 commit comments

Comments
 (0)