From 8b5c1f38910b92e9bc25cbd0c3c27ec599e16f3f Mon Sep 17 00:00:00 2001 From: Tajim Date: Fri, 11 Sep 2026 19:54:36 +0600 Subject: [PATCH] feat(profile): allow clearing and removing avatar photo --- app/Http/Controllers/ProfileController.php | 8 +- .../Requests/Profile/UpdateProfileRequest.php | 1 + resources/js/pages/Profile.vue | 73 ++++++++++++++++--- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index b060df94..40702487 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -21,7 +21,13 @@ public function update(UpdateProfileRequest $request) $user = $request->user(); $validated = $request->validated(); - if ($request->hasFile('file')) { + if ($request->boolean('clear_image')) { + if ($user->image_path) { + Storage::delete($user->image_path); + } + + $validated['image_path'] = null; + } elseif ($request->hasFile('file')) { $path = $request->file('file')->store('users/profile-images'); if ($user->image_path) { diff --git a/app/Http/Requests/Profile/UpdateProfileRequest.php b/app/Http/Requests/Profile/UpdateProfileRequest.php index a20a2239..fdaa37ee 100644 --- a/app/Http/Requests/Profile/UpdateProfileRequest.php +++ b/app/Http/Requests/Profile/UpdateProfileRequest.php @@ -36,6 +36,7 @@ public function rules(): array new CleanText, ], 'file' => ['sometimes', 'nullable', 'image', 'mimes:jpg,jpeg,png,webp', 'max:5120'], + 'clear_image' => ['sometimes', 'boolean'], 'about' => ['sometimes', 'nullable', 'string', 'max:1000', new CleanText], 'institution' => ['sometimes', 'nullable', 'string', 'max:255', new CleanText], 'activity_privacy' => ['sometimes', 'string', Rule::in(['public', 'appreciators_only', 'private'])], diff --git a/resources/js/pages/Profile.vue b/resources/js/pages/Profile.vue index f0b94326..dcc62398 100644 --- a/resources/js/pages/Profile.vue +++ b/resources/js/pages/Profile.vue @@ -33,12 +33,15 @@ const isUnverified = computed(() => { const showAdvancedSettings = ref(false); const showConfirmModal = ref(false); const isCompressingAvatar = ref(false); +const avatarPreview = ref(null); +const fileInputRef = ref(null); const form = useForm({ _method: 'PUT', name: user.value?.name || '', username: user.value?.username || '', file: null as File | null, + clear_image: false, about: user.value?.about || '', institution: user.value?.institution || '', activity_privacy: user.value?.activity_privacy || 'public', @@ -85,12 +88,37 @@ const handleAvatarSelect = async (event: Event) => { } form.file = resultFile; + form.clear_image = false; + + if (avatarPreview.value) { + URL.revokeObjectURL(avatarPreview.value); + } + + avatarPreview.value = URL.createObjectURL(resultFile); } finally { isCompressingAvatar.value = false; } } }; +const handleRemoveAvatar = () => { + form.file = null; + form.errors.file = ''; + + if (avatarPreview.value) { + URL.revokeObjectURL(avatarPreview.value); + avatarPreview.value = null; + } + + if (fileInputRef.value) { + fileInputRef.value.value = ''; + } + + if (user.value?.image_url) { + form.clear_image = true; + } +}; + const handleEmailToggle = () => { if (form.receive_emails) { showConfirmModal.value = true; @@ -376,8 +404,12 @@ const submitForm = () => {
@@ -398,13 +430,34 @@ const submitForm = () => {
- +
+ + +
{ {{ isCompressingAvatar ? 'Optimizing avatar...' - : 'Supports PNG, JPG, or WEBP up to 5MB (auto-optimized).' + : form.clear_image + ? 'Photo will be removed upon saving.' + : 'Supports PNG, JPG, or WEBP up to 5MB (auto-optimized).' }}