Add profile picture removal
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user