Add profile picture removal
linter / quality (pull_request) Successful in 1m36s
tests / ci (8.4) (pull_request) Successful in 1m54s

This commit is contained in:
2026-06-24 15:30:11 +02:00
parent 62e4a8487c
commit 0d26113ba7
4 changed files with 81 additions and 15 deletions
@@ -66,6 +66,13 @@ class ProfileController extends Controller
return to_route('profile.edit');
}
public function destroyAvatar(Request $request): RedirectResponse
{
$this->avatars->generate($request->user());
return to_route('profile.edit');
}
/**
* Delete the user's profile.
*/
+42 -15
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { Form, Head, Link, usePage } from '@inertiajs/vue3';
import { Form, Head, Link, useForm, usePage } from '@inertiajs/vue3';
import { Trash2 } from 'lucide-vue-next';
import { computed, onBeforeUnmount, ref } from 'vue';
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
import DeleteUser from '@/components/DeleteUser.vue';
@@ -28,28 +29,41 @@ const breadcrumbItems: BreadcrumbItem[] = [
const page = usePage();
const user = computed(() => page.props.auth.user);
const avatarPreview = ref<string | null>(null);
const avatarDeleteForm = useForm({});
const displayedAvatar = computed(
() => avatarPreview.value ?? user.value.avatar,
);
const canRemoveAvatar = computed(
() => user.value.avatar !== null && !user.value.avatar_is_generated,
);
const clearAvatarPreview = (): void => {
if (avatarPreview.value) {
URL.revokeObjectURL(avatarPreview.value);
avatarPreview.value = null;
}
};
const handleAvatarChange = (event: Event) => {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (avatarPreview.value) {
URL.revokeObjectURL(avatarPreview.value);
avatarPreview.value = null;
}
clearAvatarPreview();
if (file) {
avatarPreview.value = URL.createObjectURL(file);
}
};
const removeAvatar = (): void => {
avatarDeleteForm.delete(ProfileController.destroyAvatar.url(), {
preserveScroll: true,
onSuccess: clearAvatarPreview,
});
};
onBeforeUnmount(() => {
if (avatarPreview.value) {
URL.revokeObjectURL(avatarPreview.value);
}
clearAvatarPreview();
});
</script>
@@ -93,13 +107,26 @@ onBeforeUnmount(() => {
</AvatarFallback>
</Avatar>
<div class="grid flex-1 gap-2">
<Input
id="avatar"
name="avatar"
type="file"
accept="image/png,image/jpeg,image/webp"
@change="handleAvatarChange"
/>
<div class="flex flex-col gap-2 sm:flex-row">
<Input
id="avatar"
name="avatar"
type="file"
accept="image/png,image/jpeg,image/webp"
@change="handleAvatarChange"
/>
<Button
v-if="canRemoveAvatar"
type="button"
variant="outline"
:disabled="avatarDeleteForm.processing"
data-test="delete-profile-picture-button"
@click="removeAvatar"
>
<Trash2 class="size-4" />
Remove
</Button>
</div>
<p class="text-muted-foreground text-sm">
PNG, JPG, or WebP up to 2 MB.
</p>
+1
View File
@@ -16,6 +16,7 @@ Route::middleware('auth')
->group(function (): void {
Route::get('/', 'edit')->name('profile.edit');
Route::patch('/', 'update')->name('profile.update');
Route::delete('avatar', 'destroyAvatar')->name('profile.avatar.destroy');
});
});
@@ -104,6 +104,37 @@ class ProfileUpdateTest extends TestCase
Storage::disk('public')->assertExists($user->avatar_path);
}
public function test_user_can_delete_their_uploaded_profile_picture()
{
Storage::fake('public');
$user = User::factory()->create([
'name' => 'Upload User',
'avatar_path' => 'avatars/uploads/current.png',
'avatar_color' => '#06B6D4',
'avatar_is_generated' => false,
]);
Storage::disk('public')->put($user->avatar_path, 'uploaded image');
$response = $this
->actingAs($user)
->delete(route('profile.avatar.destroy'));
$response
->assertSessionHasNoErrors()
->assertRedirect(route('profile.edit'));
$user->refresh();
$this->assertTrue($user->avatar_is_generated);
$this->assertSame('#06B6D4', $user->avatar_color);
$this->assertNotSame('avatars/uploads/current.png', $user->avatar_path);
$this->assertStringStartsWith('avatars/generated/', $user->avatar_path);
Storage::disk('public')->assertMissing('avatars/uploads/current.png');
Storage::disk('public')->assertExists($user->avatar_path);
$this->assertStringContainsString('UU', Storage::disk('public')->get($user->avatar_path));
}
public function test_name_change_regenerates_the_default_profile_picture()
{
Storage::fake('public');