Skip to content

Commit

Permalink
dm-5091 add avatar management to user profile edit page (#1110)
Browse files Browse the repository at this point in the history
* add file for user profile js

contains logic for uploading user profile photos

* update .avatar-profile-photo css class

* update user profile partial

adjusts logic to enable photo upload and removal for users with granted_public_bio: true

elements dealing with photo editing commented out, leaving behind in case the functionality ever needs to be re-implemented

* update user_profile_utilities js

cleans up functions by removing unused vars

* add feature test for user profile pic management
  • Loading branch information
PhilipDeFraties authored Nov 6, 2024
1 parent ea1a553 commit 5043394
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 36 deletions.
270 changes: 270 additions & 0 deletions app/assets/javascripts/_user_profile_utilities.es6
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
(($) => {
const $document = $(document);
let $deleteBtn;
let $imgsContainer;
let $placeholderImg;
// let $editBtn;
// let $saveEditBtn;
// let $cancelEditBtn;

function _toggleDeleteBtn({ visible, target }) {
let imgDeleteBtn = $(target).closest('.dm-cropper-boundary').find($deleteBtn);
let hideDeleteBtn = $(target).closest('.dm-cropper-boundary').find($imgsContainer).hasClass('dm-resource-image');

if (visible && !hideDeleteBtn) {
imgDeleteBtn.removeClass('hidden');
} else {
imgDeleteBtn.addClass('hidden');
}
}

function _clearUpload({ target }) {
let $imgImgsContainer = $(target).closest('.dm-cropper-boundary').find($imgsContainer)
let area = $(target).closest('.dm-cropper-boundary').data('area')

$imgImgsContainer.empty()
$(target)
.closest('.dm-cropper-boundary').find(".usa-file-input")
.replaceWith(`
<input class="dm-cropper-upload-image usa-hint usa-file-input ${area}-image-attachment" type="file" accept=".jpg,.jpeg,.png" />
`)
$('.dm-cropper-upload-image').on('change', (event) => {
_attachAvatarImg({ uploadedImg: event.target.files[0], target: event.target });
})
$placeholderImg.removeClass('display-none')
}

function _attachAvatarImg({ uploadedImg, target }) {
let imgSizeMb = uploadedImg.size * 0.000001; // convert bytes to MB
let $errorText = $('.dm-image-error-text');
let $imgImgsContainer = $(target).closest('.dm-cropper-boundary').find($imgsContainer);

if (imgSizeMb <= 32) {
let reader = new FileReader();

reader.onload = function(event) {
let imgOrgElement = `<img src="${event.target.result}" class="avatar-profile-photo" alt=""/>`;
$imgImgsContainer.empty();
$imgImgsContainer.append(imgOrgElement);
$errorText.addClass('hidden');
_successfulImageLoad({ target });

$(target).closest('.dm-cropper-boundary').find('.dm-cropper-delete-image input[type="checkbox"]').prop('checked', false);
};

reader.readAsDataURL(uploadedImg);
} else {
$imgImgsContainer.empty();
$errorText.removeClass('hidden');
$errorText.find('p').text('Sorry, you cannot upload an image larger than 32MB.');
_failedImageLoad({ target });
}
}

function _failedImageLoad({ target }) {
_toggleDeleteBtn({ visible: false, target });
_clearUpload({ target });
// _toggleEditBtn({ visible: false, target })
// _setCropBoxValues({ isCrop: false, target });
// _toggleCropperBtnView({ visible: false, target });
}

function _successfulImageLoad({ target }) {
// _toggleEditBtn({ visible: true, target })
_toggleDeleteBtn({ visible: true, target });
// _setCropBoxValues({ isCrop: false, target });
// _toggleCropperBtnView({ visible: false, target });
}

function _attachUploadEventListener() {
$('.dm-cropper-upload-image').on('change', (event) => {
_attachAvatarImg({ uploadedImg: event.target.files[0], target: event.target });
$placeholderImg.addClass('display-none');
})
}

function _attachDeleteEventListener() {
$deleteBtn.click((event) => {
_clearUpload({ target: event.target })
_toggleDeleteBtn({ visible: false, target: event.target });
// _toggleEditBtn({ visible: false, target: event.target });
// _toggleCropperBtnView({ visible: false, target: event.target });
// _setCropBoxValues({ isCrop: false, target: event.target });
});
}

// Photo editing logic, taken from overview_image_editor file, needs adjusting to get working properly:

// function _attachEditEventListener() {
// $editBtn.click((event) => {
// event.preventDefault();
// _toggleCropper({ visible: true, target: event.target });
// // _toggleDeleteBtn({ visible: false, target: event.target });
// _toggleImageView({ isCrop: true, target: event.target });
// // _toggleCropperBtnView({ visible: true, target: event.target });
// // _toggleEditBtn({ visible: false, target: event.target });
// });
// }

// function _attachSaveEditEventListener() {
// $saveEditBtn.click((event) => {
// _setCropBoxValues({ isCrop: true, target: event.target });
// });
// }

// function _attachCancelEditEventListener() {
// $cancelEditBtn.click((event) => {
// _toggleImageView({ isCrop: false, target: event.target })
// _toggleDeleteBtn({ visible: true, target: event.target });
// // _toggleCropper({ visible: false, target: event.target });
// // _toggleCropperBtnView({ visible: false, target: event.target });
// // _toggleEditBtn({ visible: true, target: event.target });
// // _setCropBoxValues({ isCrop: false, target: event.target });
// });
// }

// function _toggleCropperBtnView({ visible, target}) {
// let $imgSaveEditBtn = $(target).closest('.dm-cropper-boundary').find($saveEditBtn)
// let $imgCancelEditBtn = $(target).closest('.dm-cropper-boundary').find($cancelEditBtn)

// if (visible) {
// $imgSaveEditBtn.removeClass('hidden');
// $imgCancelEditBtn.removeClass('hidden');
// } else {
// $imgSaveEditBtn.addClass('hidden');
// $imgCancelEditBtn.addClass('hidden');
// }
// }

// function _createModifiedImage({ target }) {
// let $image = $(target).closest('.dm-cropper-boundary').find('.dm-cropper-thumbnail-original');
// let setAsCanvas = $(target).closest('.dm-cropper-boundary').find($imgsContainer).hasClass('dm-resource-image');
// if ($image && $image.data('cropper')) {
// // Generate the cropped image as a canvas
// $(target).closest('.dm-cropper-boundary').find('.dm-cropper-thumbnail-original').addClass('hidden')
// let croppedCanvas = $image.data('cropper').getCroppedCanvas({ width: 310 })
// if (setAsCanvas) {
// croppedCanvas.classList.add('dm-cropper-thumbnail-modified')
// $(target).closest('.dm-cropper-boundary').find($imgsContainer).append(croppedCanvas)
// } else {
// let url = croppedCanvas.toDataURL();
// let image = new Image();
// image.src = url;
// image.classList.add('dm-cropper-thumbnail-modified')
// $(target).closest('.dm-cropper-boundary').find($imgsContainer).append(image)
// }

// // Optionally, toggle other UI elements (e.g., hide Save/Cancel, show Edit)
// _toggleCropperBtnView({ visible: false, target });
// _toggleEditBtn({ visible: true, target });
// _toggleDeleteBtn({ visible: true, target });
// }
// }


// function _toggleImageView({ isCrop, target }) {
// let $originalImage = $(target).closest('.dm-cropper-boundary').find('.dm-cropper-thumbnail-original');
// let $modifiedCanvas = $(target).closest('.dm-cropper-boundary').find('.dm-cropper-thumbnail-modified');

// if (isCrop && $originalImage) {
// $originalImage.removeClass('hidden');
// $modifiedCanvas.addClass('hidden')
// } else {
// if ($modifiedCanvas.length > 0) {
// $originalImage.addClass('hidden');
// $modifiedCanvas.removeClass('hidden')
// } else {
// $originalImage.removeClass('hidden');
// }
// }
// }

// function _toggleCropper({ visible, target }) {
// let $image = $(target).closest('.dm-cropper-boundary').find('.dm-cropper-thumbnail-original');

// if (visible) {
// let cropOptions = {
// checkCrossOrigin: false,
// checkOrientation: true,
// viewMode: 2,
// minContainerWidth: 100,
// aspectRatio: 1
// }

// // create Cropper instance
// $image.cropper(cropOptions);
// } else {
// if ($image.data('cropper')) {
// $image.data('cropper').destroy();
// }
// }
// }

// function _toggleEditBtn({ visible, target }) {
// let $imgEditBtn = $(target).closest('.dm-cropper-boundary').find($editBtn)

// if (visible) {
// $imgEditBtn.removeClass('hidden');
// } else {
// $imgEditBtn.addClass('hidden');
// }
// }

// function _setCropBoxValues({ isCrop, target }) {
// let $image = $(target).closest('.dm-cropper-boundary').find('.dm-cropper-thumbnail-original');

// if (isCrop && $image.data('cropper')) {
// let cropValues = $image.data('cropper').getData(true);
// $(target).closest('.dm-cropper-boundary').find(".crop_x").val(cropValues.x);
// $(target).closest('.dm-cropper-boundary').find(".crop_y").val(cropValues.y);
// $(target).closest('.dm-cropper-boundary').find(".crop_w").val(cropValues.width);
// $(target).closest('.dm-cropper-boundary').find(".crop_h").val(cropValues.height);

// _createModifiedImage({ target })
// _toggleCropper({ visible: false, target });
// _toggleCropperBtnView({ visible: false, target });
// _toggleEditBtn({ visible: true, target });
// _toggleDeleteBtn({ visible: true, target });
// _toggleImageView({ isCrop: false, target });
// } else {
// $(target).closest('.dm-cropper-boundary').find(".crop_x").val(null);
// $(target).closest('.dm-cropper-boundary').find(".crop_y").val(null);
// $(target).closest('.dm-cropper-boundary').find(".crop_w").val(null);
// $(target).closest('.dm-cropper-boundary').find(".crop_h").val(null);
// }
// }

function attachImgActionsEventListeners() {
_attachUploadEventListener();
_attachDeleteEventListener();
// _attachEditEventListener();
// _attachSaveEditEventListener();
// _attachCancelEditEventListener();
}

function setImageVars() {
$deleteBtn = $('.dm-cropper-delete-image');
$imgsContainer = $('.dm-cropper-images-container');
$placeholderImg = $('.cropper-image-placeholder');
// $editBtn = $('.dm-cropper-edit-mode');
// $cancelEditBtn = $('.dm-cropper-cancel-edit');
// $saveEditBtn = $('.dm-cropper-save-edit');
}

function attachNewFieldEventListeners() {
$document.arrive('.dm-cropper-boundary', (newElem) => {
setImageVars();
_attachUploadEventListener();
// _attachSaveEditEventListener();
// _attachCancelEditEventListener();
})
}

function loadCropperFunctions() {
setImageVars();
attachImgActionsEventListeners();
attachNewFieldEventListeners();
}

$document.on('turbolinks:load', loadCropperFunctions);
})(window.jQuery);
2 changes: 2 additions & 0 deletions app/assets/stylesheets/dm/components/_profile.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
.avatar-profile-photo {
min-width: 100%;
height: 14rem !important;
aspect-ratio: 1 / 1;
object-fit: cover;
}

.avatar-profile-photo-container {
Expand Down
82 changes: 46 additions & 36 deletions app/views/users/edit_profile.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<%= javascript_tag 'data-turbolinks-track': 'reload' do %>
<%= render partial: 'edit_profile', formats: [:js] %>
<% end %>
<%= javascript_include_tag '_user_profile_utilities', 'data-turbolinks-track': 'reload' %>
<% end %>

<div class="margin-top-0">
Expand Down Expand Up @@ -48,53 +49,62 @@
</div>
</div>
<% end %>
<!-- Profile Image and Actions -->
<% avatar_exists = @user.avatar.present? && @user.avatar.exists? %>
<div class="grid-container padding-0">
<div class="grid-row margin-bottom-3 dm-cropper-boundary">
<div class="grid-col-4 margin-right-1 profile-image-left-col-setter">
<div class="dm-cropper-images-container avatar-profile-photo-container" data-type="user">
<% if @user.avatar.present? && @user.avatar.exists? %>
<%= image_tag @user.avatar_s3_presigned_url(:thumb), alt: "user avatar for #{@user.full_name}", class: 'dm-cropper-thumbnail-modified avatar-profile-photo'%>
<% if avatar_exists %>
<%= image_tag @user.avatar_s3_presigned_url(:thumb), alt: "user avatar for #{@user.full_name}", class: 'avatar-profile-photo' %>
<% end %>
</div>
<div class="display-none <%= @user.avatar.present? && @user.avatar.exists? ? 'profile-avatar-container bg-base-lightest radius-md cropper-image-placeholder hidden' : 'profile-avatar-container bg-base-lightest radius-md cropper-image-placeholder' %>">
<i class="fas fa-user empty-user-avatar text-base-lighter"></i>
</div>
</div>
<div class="grid-col-fill profile-image-right-col-setter display-none">
<p>Photo</p>
<div class="text-base margin-y-1">
<p class="line-height-sans-505">
Upload a photo that clearly shows your face. You can upload a .jpg, .jpeg, or .png file and the size limit is 1GB.
</p>
</div>
<div class="margin-bottom-2 dm-image-editor-text hidden">
<p>
Please click "Save edits" and then "Save changes" to save and exit editor.
</p>
<div class="profile-avatar-container bg-base-lightest radius-md cropper-image-placeholder <%= 'display-none' if avatar_exists %>">
<i class="fas fa-user empty-user-avatar text-base-lighter"></i>
</div>
</div>

<div class="display-none grid-row flex-align-start display-none">
<% for attribute in [:crop_x, :crop_y, :crop_w, :crop_h] %>
<%= f.hidden_field attribute, :id => attribute, :value => nil %>
<% end %>
<div class="grid-col-fill">
<a class="usa-button dm-cropper-save-edit hidden" aria-controls='photo-save-edit' aria-expanded='false'>Save edits</a>
<a class="usa-button usa-button--outline dm-cropper-cancel-edit hidden" aria-controls='photo-cancel-edit' aria-expanded='false'>Cancel edits</a>
<a class="<%= @user.avatar.present? && @user.avatar.exists? ? "usa-button usa-button--outline dm-cropper-edit-mode" : "usa-button usa-button--outline dm-cropper-edit-mode hidden" %>" aria-controls='photo-crop' aria-expanded='false'>Edit photo</a>
<div>
<%= f.label :avatar,
@user.avatar.present? && @user.avatar.exists? ? 'Upload new photo' : 'Upload photo',
class: @user.avatar.present? && @user.avatar.exists? ? 'dm-cropper-upload-image-label user-avatar-upload-label' : 'dm-cropper-upload-image-label usa-button usa-button--outline'
%>
<%= f.file_field :avatar, class: "hidden-upload cropper-upload-image", accept: 'image/*' %>
<% if @user.granted_public_bio %>
<div class="grid-col-fill profile-image-right-col-setter">
<p>Photo</p>
<div class="text-base margin-y-1">
<p class="line-height-sans-505">
Upload a photo that clearly shows your face. You can upload a .jpg, .jpeg, or .png file and the size limit is 1GB.
</p>
</div>
<div class="dm-cropper-delete-image">
<%= f.label :delete_avatar, 'Remove photo', class: @user.avatar.present? && @user.avatar.exists? ? 'display-inline-block text-secondary dm-cropper-delete-image-label' : 'display-inline-block text-secondary dm-cropper-delete-image-label hidden'%>
<%= f.check_box :delete_avatar, { class: @user.avatar.present? && @user.avatar.exists? ? 'usa-checkbox__input dm-cropper-delete-image' : 'usa-checkbox__input dm-cropper-delete-image hidden'}, 'true', 'false' %>

<div class="margin-bottom-2 dm-image-editor-text hidden">
<p>Please click "Save edits" and then "Save changes" to save and exit editor.</p>
</div>

<!-- Image Editing Buttons -->
<div class="grid-row flex-align-start">
<!--
<% for attribute in [:crop_x, :crop_y, :crop_w, :crop_h] %>
<%= f.hidden_field attribute, id: attribute, value: nil %>
<% end %>
-->
<div class="grid-col-fill">
<!--
<a class="usa-button dm-cropper-save-edit display-none" aria-expanded="false">Save edits</a>
<a class="usa-button usa-button--outline dm-cropper-cancel-edit display-none" aria-expanded="false">Cancel edits</a>
<a class="<%= avatar_exists ? 'usa-button usa-button--outline dm-cropper-edit-mode' : 'usa-button usa-button--outline dm-cropper-edit-mode hidden' %>" aria-controls="photo-crop" aria-expanded="false">Edit photo</a>
-->
<!-- Upload Photo -->
<div>
<%= f.label :avatar, avatar_exists ? 'Upload new photo' : 'Upload photo', class: 'dm-cropper-upload-image-label usa-button usa-button--outline' %>
<%= f.file_field :avatar, class: "dm-cropper-upload-image hidden-upload cropper-upload-image", accept: 'image/*' %>
</div>

<!-- Delete Photo -->
<div class="dm-cropper-delete-image <%= 'hidden' unless avatar_exists %>">
<%= f.label :delete_avatar, 'Remove photo', class: "display-inline-block text-secondary dm-cropper-delete-image-label" %>
<%= f.check_box :delete_avatar, { class: avatar_exists ? 'usa-checkbox__input dm-cropper-delete-image' : 'usa-checkbox__input dm-cropper-delete-image hidden' }, 'true', 'false' %>
</div>
</div>
</div>
</div>
</div>
</div>
<% end %>
</div>
</div>
</div>
Expand Down
Loading

0 comments on commit 5043394

Please sign in to comment.