Skip to content

profile photo for users #635

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
# wont be uploaded to the repo.


# database setup:
# database setup
DATABASE_USER=postgres
DATABASE_NAME=timeoverflow_development

Expand All @@ -19,3 +19,9 @@ MAIL_LINK_PROTO=http

# a list of emails for superadmin users
ADMINS="admin@timeoverflow.org"

# AWS settings
AWS_ACCESS_KEY_ID=XXXXXXXX
AWS_SECRET_ACCESS_KEY=XXXXXXXX
AWS_BUCKET=timeoverflow_development
AWS_REGION=us-east-1
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ tags
.sass-cache
capybara-*.html
/vendor/bundle
/storage/
/coverage/
/spec/tmp/*
.byebug_history
Expand Down
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ gem 'pg_search', '~> 2.3.5'
gem 'skylight', '~> 5.0'
gem 'sidekiq', '~> 6.1.2'
gem 'sidekiq-cron', '~> 1.2.0'
gem 'aws-sdk-s3', '~> 1.94', require: false
gem 'image_processing', '~> 1.2'

# Assets
gem 'jquery-rails', '~> 4.3.5'
Expand Down
25 changes: 25 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,22 @@ GEM
ast (2.4.2)
autoprefixer-rails (10.2.4.0)
execjs
aws-eventstream (1.1.1)
aws-partitions (1.451.0)
aws-sdk-core (3.114.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.43.0)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.94.1)
aws-sdk-core (~> 3, >= 3.112.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.2.3)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt (3.1.16)
bindex (0.8.1)
bootsnap (1.7.4)
Expand Down Expand Up @@ -154,11 +170,15 @@ GEM
http_accept_language (2.1.1)
i18n (1.8.10)
concurrent-ruby (~> 1.0)
image_processing (1.12.1)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
inherited_resources (1.12.0)
actionpack (>= 5.2, < 6.2)
has_scope (~> 0.6)
railties (>= 5.2, < 6.2)
responders (>= 2, < 4)
jmespath (1.4.0)
jquery-rails (4.3.5)
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
Expand Down Expand Up @@ -201,6 +221,7 @@ GEM
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2021.0225)
mini_magick (4.11.0)
mini_mime (1.0.3)
mini_portile2 (2.5.1)
minitest (5.14.4)
Expand Down Expand Up @@ -324,6 +345,8 @@ GEM
rack (>= 1.1)
rubocop (>= 0.90.0, < 2.0)
ruby-progressbar (1.11.0)
ruby-vips (2.1.0)
ffi (~> 1.12)
ruby2_keywords (0.0.4)
rubyzip (2.3.0)
sassc (2.4.0)
Expand Down Expand Up @@ -405,6 +428,7 @@ PLATFORMS

DEPENDENCIES
activeadmin (~> 2.9.0)
aws-sdk-s3 (~> 1.94)
bootsnap (~> 1.7.3)
bootstrap-sass (~> 3.4)
byebug (~> 11.0)
Expand All @@ -419,6 +443,7 @@ DEPENDENCIES
faker (~> 2.15)
has_scope (~> 0.7.2)
http_accept_language (~> 2.1.1)
image_processing (~> 1.2)
jquery-rails (~> 4.3.5)
json_translate (~> 4.0.0)
kaminari (~> 1.2.1)
Expand Down
110 changes: 110 additions & 0 deletions app/assets/javascripts/application/avatar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
$(function () {
$('#avatar-js').on("change", () => {
$('#dialog').modal({
show: true
});

preview_image_modal();

const panel = $('#crop_panel');
let m_pos;
let x_axis = true;

//add a listener to the document depending on where you tap in the box
panel.on("pointerdown", function(e) {
const BORDER_SIZE = 20;

if (e.offsetY >= (parseInt(panel.css('height')) - BORDER_SIZE)) {
m_pos = e.y;
x_axis = false;
document.addEventListener("pointermove", resize, false);
} else if (e.offsetX >= (parseInt(panel.css('width')) - BORDER_SIZE)) {
m_pos = e.x;
x_axis = true;
document.addEventListener("pointermove", resize, false);
} else {
pos3 = e.clientX;
pos4 = e.clientY;
document.addEventListener("pointermove", drag, false);
}
});

// remove listeners from the document when you stop pressing
document.addEventListener("pointerup", function () {
document.removeEventListener("pointermove", resize, false);
document.removeEventListener("pointermove", drag, false);
document.removeEventListener("pointermove", resize, false);
}, false);

// on submit take the parameters of the box to crop the avatar
$('#form_photo').on("submit", () => {
let total_width = parseInt(getComputedStyle(document.getElementById("containerCrop")).width);
let photo_width = parseInt(getComputedStyle(document.getElementById("foto")).width);
let left_displacement = total_width - photo_width;

$('#height_offset').val(parseInt(panel.css('top')) - 15);
$('#width_offset').val(parseInt(panel.css('left')) - 15 - (left_displacement/2));
$('#height_width').val(parseInt(panel.css('width')));
$('#original_width').val($('#foto').width());
});

function resize(e) {
e = e || window.event;
e.preventDefault();

let mov = x_axis ? e.x : e.y;
const dx = m_pos - mov;
m_pos = mov;

if (can_change(panel, dx, false)) {
panel.width(panel.width() - dx);
panel.height(panel.width() - dx);
}
}

function drag(e) {
e = e || window.event;
e.preventDefault();

pos1 = pos3 - e.x;
pos2 = pos4 - e.y;
pos3 = e.x;
pos4 = e.y;

if (can_change(panel, pos1 + pos2, true)) {
panel.offset({
top: (panel.offset().top - pos2),
left: (panel.offset().left - pos1)
});
}
}

function can_change(el, mov, dragging) {
let canChange = true;
let pos = dragging ? [el.css('top'), el.css('left'), el.css('bottom'), el.css('right')] : [el.css('bottom'), el.css('right')];

pos.forEach((el, ix) => {
let next = dragging && (ix == 0 || ix == 1) ? parseInt(el) - mov : parseInt(el) + mov;

if (next < 14) canChange = false;
});

return canChange;
}

function preview_image_modal() {
var preview = document.querySelector('#foto');
var file = document.querySelector('#avatar-js').files[0];
var reader = new FileReader();

reader.onloadend = function () {
preview.src = reader.result;
}

if (file)
reader.readAsDataURL(file);
else
preview.src = "";
}
});
});
1 change: 1 addition & 0 deletions app/assets/stylesheets/application.scss
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
@import "bootstrap-sprockets";
@import "application/bootstrap-custom";
@import "application/member-card";
@import "application/avatar";
@import "application/footer";

html {
Expand Down
36 changes: 36 additions & 0 deletions app/assets/stylesheets/application/avatar.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#form_photo {
border: 0px;
padding: 0px;
margin-bottom: 0px;
}

#crop_panel {
position: absolute;
width: 140px;
height: 140px;
border-left: 4px dashed black;
cursor: move;
touch-action: none;
border-top: 4px dashed black;
}

#crop_panel::before {
content: "";
border-right: 4px dashed black;
position: absolute;
right: 0;
width: 16px;
height: 100%;
cursor: w-resize;
}

#crop_panel::after {
content: "";
border-bottom: 4px dashed black;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
height: 16px;
cursor: n-resize;
}
38 changes: 38 additions & 0 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@ def update
end
end

def update_avatar
avatar = params[:avatar]

if avatar && content_type_permitted(avatar.content_type)
current_user.avatar.purge if current_user.avatar.attached?

crop_image_and_save(current_user, avatar)
else
flash[:error] = t 'users.show.invalid_format'
end

redirect_to current_user
end

private

def search_and_load_members(members_scope, default_search_params)
Expand Down Expand Up @@ -116,4 +130,28 @@ def redirect_to_after_create
name: @user.username)
end
end

def crop_image_and_save(user, avatar)
orig_width = params[:original_width].to_i
width = params[:height_width].to_i
left = params[:width_offset].to_i
top = params[:height_offset].to_i

image_processed = ImageProcessing::MiniMagick.
source(avatar.tempfile).
resize_to_fit(orig_width, nil).
crop("#{width}x#{width}+#{left}+#{top}!").
convert("png").
call

user.avatar.attach(
io: image_processed,
filename: user.username,
content_type: avatar.content_type
)
end

def content_type_permitted(avatar_content_type)
%w[image/jpeg image/pjpeg image/png image/x-png].include? avatar_content_type
end
end
4 changes: 0 additions & 4 deletions app/decorators/member_decorator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,6 @@ def mail_to
view.mail_to(email) if email && !email.end_with?('example.com')
end

def avatar_img(size=32)
view.image_tag(view.avatar_url(user, size), width: size, height: size)
end

def account_balance
view.seconds_to_hm(object.account.try(:balance))
end
Expand Down
7 changes: 6 additions & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,13 @@ def page_title
current_organization || 'TimeOverflow'
end

# from gravatar
def avatar_url(user, size = 32)
user.avatar.attached? ?
user.avatar.variant(resize: "#{size}x#{size}") :
gravatar_url(user, size)
end

def gravatar_url(user, size = 32)
gravatar_id = Digest::MD5::hexdigest(user.email).downcase
gravatar_options = {
set: "set1",
Expand Down
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ class User < ApplicationRecord

attr_accessor :empty_email

has_one_attached :avatar
has_many :members, dependent: :destroy
has_many :organizations, through: :members
has_many :accounts, through: :members
Expand Down
50 changes: 50 additions & 0 deletions app/views/users/_avatar.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<div class="col-sm-3 col-xs-5 text-center">
<% if @user == current_user %>
<%= image_tag avatar_url(@user, 140) %>
<%= form_with url: update_avatar_path, id: "form_photo", method: :put do |f| %>
<%= label_tag "avatar-js", "avatar" do %>
<a class="btn btn-link">
<%= glyph :pencil %>
<%= t ".change_your_image" %>
</a>
<% end %>
<%= f.file_field :avatar,
id: "avatar-js",
class: "hidden",
accept: "image/*" %>
<div class="modal fade" id="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">
<%= t ".crop_the_image" %>
</h4>
</div>
<div class="modal-body">
<div id="containerCrop" class="col-12">
<div id="crop_panel"></div>
<img id="foto" alt="" />
</div>
</div>
<div class="modal-footer">
<div class="form-actions">
<button type="button" class="btn btn-default" data-dismiss="modal">
<%= t 'users.new.cancel' %>
</button>
<%= f.hidden_field :height_width, value: 140 %>
<%= f.hidden_field :width_offset, value: 1 %>
<%= f.hidden_field :height_offset, value: 1 %>
<%= f.hidden_field :original_width, value: 1 %>
<%= f.button :submit, class: "btn btn-primary" do %>
<%= t("global.save") %>
<% end %>
</div>
</div>
</div>
</div>
</div>
<% end %>
<% else %>
<%= image_tag avatar_url(@user, 140) %>
<% end %>
</div>
Loading