Add profile picture removal
This commit is contained in:
@@ -66,6 +66,13 @@ class ProfileController extends Controller
|
|||||||
return to_route('profile.edit');
|
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.
|
* Delete the user's profile.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<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 { computed, onBeforeUnmount, ref } from 'vue';
|
||||||
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
|
||||||
import DeleteUser from '@/components/DeleteUser.vue';
|
import DeleteUser from '@/components/DeleteUser.vue';
|
||||||
@@ -28,28 +29,41 @@ const breadcrumbItems: BreadcrumbItem[] = [
|
|||||||
const page = usePage();
|
const page = usePage();
|
||||||
const user = computed(() => page.props.auth.user);
|
const user = computed(() => page.props.auth.user);
|
||||||
const avatarPreview = ref<string | null>(null);
|
const avatarPreview = ref<string | null>(null);
|
||||||
|
const avatarDeleteForm = useForm({});
|
||||||
const displayedAvatar = computed(
|
const displayedAvatar = computed(
|
||||||
() => avatarPreview.value ?? user.value.avatar,
|
() => 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 handleAvatarChange = (event: Event) => {
|
||||||
const input = event.target as HTMLInputElement;
|
const input = event.target as HTMLInputElement;
|
||||||
const file = input.files?.[0];
|
const file = input.files?.[0];
|
||||||
|
|
||||||
if (avatarPreview.value) {
|
clearAvatarPreview();
|
||||||
URL.revokeObjectURL(avatarPreview.value);
|
|
||||||
avatarPreview.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
avatarPreview.value = URL.createObjectURL(file);
|
avatarPreview.value = URL.createObjectURL(file);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const removeAvatar = (): void => {
|
||||||
|
avatarDeleteForm.delete(ProfileController.destroyAvatar.url(), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: clearAvatarPreview,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (avatarPreview.value) {
|
clearAvatarPreview();
|
||||||
URL.revokeObjectURL(avatarPreview.value);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -93,13 +107,26 @@ onBeforeUnmount(() => {
|
|||||||
</AvatarFallback>
|
</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div class="grid flex-1 gap-2">
|
<div class="grid flex-1 gap-2">
|
||||||
<Input
|
<div class="flex flex-col gap-2 sm:flex-row">
|
||||||
id="avatar"
|
<Input
|
||||||
name="avatar"
|
id="avatar"
|
||||||
type="file"
|
name="avatar"
|
||||||
accept="image/png,image/jpeg,image/webp"
|
type="file"
|
||||||
@change="handleAvatarChange"
|
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">
|
<p class="text-muted-foreground text-sm">
|
||||||
PNG, JPG, or WebP up to 2 MB.
|
PNG, JPG, or WebP up to 2 MB.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ Route::middleware('auth')
|
|||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::get('/', 'edit')->name('profile.edit');
|
Route::get('/', 'edit')->name('profile.edit');
|
||||||
Route::patch('/', 'update')->name('profile.update');
|
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);
|
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()
|
public function test_name_change_regenerates_the_default_profile_picture()
|
||||||
{
|
{
|
||||||
Storage::fake('public');
|
Storage::fake('public');
|
||||||
|
|||||||
Reference in New Issue
Block a user