Skip to content

Commit dfd62b4

Browse files
Add avatar refresh button for GitHub-connected users (#1386)
* Add avatar refresh button for GitHub-connected users - Add refresh button next to avatar on user profile pages - Button only visible when viewing your own profile - Button only appears if GitHub account is connected - Implement per-user rate limiting (1 request per minute) - Add ProfileController::refresh() method with proper validation - Update avatar component to support showRefresh prop - Fix method name from hasIdenticon() to hasGithubIdenticon() for consistency - Add route for avatar refresh functionality This feature allows users to manually refresh their GitHub avatar without waiting for automatic updates. The rate limiting prevents abuse while providing a better user experience. * Fix code styling * Apply suggested changes: rename method, fix route, and update comment * wip * wip * wip * wip * Refactor avatar refresh to Livewire component - Replace form-based refresh with a dedicated Livewire component - Fix refresh icon position on avatar - Remove old refresh route and controller method * Fix code styling * wip * wip --------- Co-authored-by: Dries Vints <dries@vints.be>
1 parent 7dbcc7a commit dfd62b4

File tree

5 files changed

+122
-22
lines changed

5 files changed

+122
-22
lines changed

app/Livewire/RefreshAvatar.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
namespace App\Livewire;
4+
5+
use App\Concerns\SendsAlerts;
6+
use App\Jobs\UpdateUserIdenticonStatus;
7+
use App\Models\User;
8+
use Illuminate\Support\Facades\RateLimiter;
9+
use Illuminate\View\View;
10+
use Livewire\Component;
11+
12+
final class RefreshAvatar extends Component
13+
{
14+
use SendsAlerts;
15+
16+
public User $user;
17+
18+
public function mount(User $user): void
19+
{
20+
$this->user = $user;
21+
}
22+
23+
public function refresh(): void
24+
{
25+
if (! $this->user->hasConnectedGitHubAccount()) {
26+
$this->error('You need to connect your GitHub account to refresh your avatar.');
27+
28+
$this->redirectRoute('settings.profile');
29+
30+
return;
31+
}
32+
33+
// Rate limiting: 1 request per 1 minute per user.
34+
$key = 'avatar-refresh:'.$this->user->id();
35+
36+
if (RateLimiter::tooManyAttempts($key, 1)) {
37+
$this->error('Please wait 1 minute before refreshing your avatar again.');
38+
39+
$this->redirectRoute('settings.profile');
40+
41+
return;
42+
}
43+
44+
// Record this attempt for 1 minute.
45+
RateLimiter::hit($key, 60);
46+
47+
UpdateUserIdenticonStatus::dispatchSync($this->user);
48+
49+
$this->success('Avatar refreshed successfully!');
50+
51+
$this->redirectRoute('settings.profile');
52+
}
53+
54+
public function render(): View
55+
{
56+
return view('livewire.refresh-avatar');
57+
}
58+
}

app/Models/User.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ public function hasConnectedGitHubAccount(): bool
123123
return ! is_null($this->githubId());
124124
}
125125

126-
public function hasIdenticon(): bool
126+
public function hasGitHubIdenticon(): bool
127127
{
128128
return (bool) $this->github_has_identicon;
129129
}
Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
@props([
22
'user',
33
'unlinked' => false,
4+
'showRefresh' => false,
45
])
56

67
<?php
7-
$src = $user->githubId() && ! $user->hasIdenticon()
8+
$src = $user->githubId() && ! $user->hasGitHubIdenticon()
89
? sprintf('https://avatars.githubusercontent.com/u/%s', $user->githubId())
910
: asset('https://laravel.io/images/laravelio-icon-gray.svg');
1011
?>
1112

12-
@unless ($unlinked)
13-
<a href="{{ route('profile', $user->username()) }}">
14-
@endunless
13+
<div class="relative inline-block">
14+
@unless ($unlinked)
15+
<a href="{{ route('profile', $user->username()) }}">
16+
@endunless
1517

16-
<flux:avatar
17-
circle
18-
loading="lazy"
19-
src="{{ $src }}"
20-
alt="{{ $user->name() }}"
21-
{{ $attributes->merge(['class' => 'bg-gray-50']) }}
22-
/>
18+
<flux:avatar
19+
circle
20+
loading="lazy"
21+
src="{{ $src }}"
22+
alt="{{ $user->name() }}"
23+
{{ $attributes->merge(['class' => 'bg-gray-50']) }} />
2324

24-
@unless ($unlinked)
25-
</a>
26-
@endunless
25+
@unless ($unlinked)
26+
</a>
27+
@endunless
28+
29+
@if ($showRefresh && $user->hasConnectedGitHubAccount())
30+
<div class="absolute bottom-0 right-0 transform translate-x-1 translate-y-1">
31+
<livewire:refresh-avatar :user="$user" />
32+
</div>
33+
@endif
34+
</div>
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<div>
2+
<button
3+
wire:click="refresh"
4+
wire:loading.attr="disabled"
5+
type="button"
6+
class="flex items-center justify-center w-10 h-10 bg-white border-2 border-gray-300 rounded-full shadow-sm hover:bg-gray-50 hover:border-lio-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-lio-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
7+
title="Refresh avatar from GitHub">
8+
<svg
9+
wire:loading.remove
10+
xmlns="http://www.w3.org/2000/svg"
11+
class="h-5 w-5 text-gray-600"
12+
fill="none"
13+
viewBox="0 0 24 24"
14+
stroke="currentColor">
15+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
16+
</svg>
17+
<svg
18+
wire:loading
19+
class="animate-spin h-5 w-5 text-gray-600"
20+
xmlns="http://www.w3.org/2000/svg"
21+
fill="none"
22+
viewBox="0 0 24 24">
23+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
24+
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
25+
</svg>
26+
</button>
27+
</div>

resources/views/users/settings/profile.blade.php

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@
1313
</p>
1414
</div>
1515

16-
<div class="flex flex-col space-y-6 lg:flex-row lg:space-y-0 lg:space-x-6">
17-
<div class="grow space-y-6">
18-
<div class="space-y-1">
16+
<div class="grid grid-cols-12 gap-6">
17+
<div class="col-span-12 sm:col-span-6 space-y-6">
18+
<div>
1919
<x-forms.label for="name" />
2020

2121
<x-forms.inputs.input name="name" :value="Auth::user()->name()" required />
2222
</div>
2323

24-
<div class="space-y-1">
24+
<div>
2525
<x-forms.label for="bio" />
2626

2727
<x-forms.inputs.textarea name="bio" maxlength="160">
@@ -34,17 +34,24 @@
3434
</div>
3535
</div>
3636

37-
<div class="grow space-y-1 lg:grow-0 lg:shrink-0">
37+
<div class="col-span-12 sm:col-span-6">
3838
<p class="block text-sm leading-5 font-medium text-gray-700" aria-hidden="true">
3939
Profile Image
4040
</p>
4141

4242
<div class="flex items-center mt-2">
4343
<div class="shrink-0 inline-block overflow-hidden" aria-hidden="true">
44-
<x-avatar :user="Auth::user()" class="h-32 w-32 mt-4" unlinked />
44+
<div class="flex justify-center lg:justify-start">
45+
<x-avatar
46+
:user="Auth::user()"
47+
class="h-32 w-32 mt-4"
48+
unlinked
49+
:show-refresh="Auth::user()->isLoggedInUser()"
50+
/>
51+
</div>
4552

4653
<span class="mt-4 inline-block text-sm text-gray-500">
47-
Change your avatar for
54+
Change or refresh your avatar for<br>
4855

4956
<a href="https://github.com/{{ Auth::user()->githubUsername() }}" class="text-lio-700">
5057
your GitHub profile

0 commit comments

Comments
 (0)