Skip to content
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
5 changes: 5 additions & 0 deletions app/assets/stylesheets/avatars.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,8 @@
}
}
}

.avatar__form {
display: grid;
grid-template-columns: 1fr auto 1fr;
}
7 changes: 7 additions & 0 deletions app/assets/stylesheets/inputs.css
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@
--input-border-size: 0.15rem;
}
}

&:is(.avatar) {
img,
input[type="file"] {
border-radius: 50%;
}
}
}

.input--select {
Expand Down
6 changes: 6 additions & 0 deletions app/assets/stylesheets/utilities.css
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,12 @@
}
}

/* QR Codes */
.qr-code {
aspect-ratio: 1;
block-size: 50dvh;
}

/* Accessibility */
.for-screen-reader {
block-size: 1px;
Expand Down
11 changes: 11 additions & 0 deletions app/controllers/qr_codes_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
class QrCodesController < ApplicationController
allow_unauthenticated_access

def show
qr_code_link = QrCodeLink.from_signed(params[:id])
svg = RQRCode::QRCode.new(qr_code_link.url).as_svg(viewbox: true, fill: :white, color: :black)

expires_in 1.year, public: true
render plain: svg, content_type: "image/svg+xml"
end
end
15 changes: 15 additions & 0 deletions app/controllers/sessions/transfers_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class Sessions::TransfersController < ApplicationController
require_unauthenticated_access

def show
end

def update
if user = User.active.find_by_transfer_id(params[:id])
start_new_session_for user
redirect_to root_path
else
head :bad_request
end
end
end
5 changes: 5 additions & 0 deletions app/controllers/users/avatars_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ def show
end
end

def destroy
@user.avatar.destroy
redirect_to user_path(@user)
end

private
def set_user
@user = Current.account.users.find(params[:user_id])
Expand Down
22 changes: 19 additions & 3 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
class UsersController < ApplicationController
require_unauthenticated_access
require_unauthenticated_access only: %i[ new create ]

before_action :set_account_from_join_code
before_action :set_user, only: %i[ show edit update ]
before_action :set_account_from_join_code, only: %i[ new create ]

def new
@user = @account.users.build
Expand All @@ -13,12 +14,27 @@ def create
redirect_to root_path
end

def show
end

def edit
end

def update
@user.update user_params
redirect_to user_path(@user)
end

private
def set_account_from_join_code
@account = Account.find_by_join_code!(params[:join_code])
end

def set_user
@user = Current.account.users.active.find(params[:id])
end

def user_params
params.expect(user: [ :name, :email_address, :password ])
params.expect(user: [ :name, :email_address, :password, :avatar ])
end
end
8 changes: 8 additions & 0 deletions app/helpers/forms_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module FormsHelper
def auto_submit_form_with(**attributes, &)
data = attributes.delete(:data) || {}
data[:controller] = "auto-submit #{data[:controller]}".strip

form_with **attributes, data: data, &
end
end
6 changes: 6 additions & 0 deletions app/helpers/qr_codes_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module QrCodesHelper
def qr_code_image(url)
qr_code_link = QrCodeLink.new(url)
image_tag qr_code_path(qr_code_link.signed), class: "qr-code center", alt: "QR Code"
end
end
26 changes: 26 additions & 0 deletions app/models/qr_code_link.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class QrCodeLink
attr_reader :url

def initialize(url)
@url = url
end

def signed
self.class.verifier.generate(@url, purpose: :qr_code)
end

def self.from_signed(signed)
new verifier.verify(signed, purpose: :qr_code)
end

private
class << self
def verifier
ActiveSupport::MessageVerifier.new(secret, url_safe: true)
end

def secret
Rails.application.key_generator.generate_key("qr_codes")
end
end
end
2 changes: 2 additions & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class User < ApplicationRecord
include Avatar, Transferable

belongs_to :account

has_many :sessions, dependent: :destroy
Expand Down
17 changes: 17 additions & 0 deletions app/models/user/avatar.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module User::Avatar
extend ActiveSupport::Concern

included do
has_one_attached :avatar
end

class_methods do
def from_avatar_token(sid)
find_signed!(sid, purpose: :avatar)
end
end

def avatar_token
signed_id(purpose: :avatar)
end
end
15 changes: 15 additions & 0 deletions app/models/user/transferable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module User::Transferable
extend ActiveSupport::Concern

TRANSFER_LINK_EXPIRY_DURATION = 4.hours

class_methods do
def find_by_transfer_id(id)
find_signed(id, purpose: :transfer)
end
end

def transfer_id
signed_id(purpose: :transfer, expires_in: TRANSFER_LINK_EXPIRY_DURATION)
end
end
2 changes: 1 addition & 1 deletion app/views/accounts/users/_invite.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
</label>

<div class="flex align-center gap">
<div data-controller="dialog" class="flex-inline">
<div data-controller="dialog" data-dialog-modal-value="true" class="flex-inline">
<%= tag.button class: "btn", data: { action: "dialog#open" } do %>
<%= image_tag "qr-code.svg", aria: { hidden: "true" }, size: 24, class: "colorize--black" %>
<span class="for-screen-reader">Show join link QR code</span>
Expand Down
2 changes: 1 addition & 1 deletion app/views/accounts/users/_user.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<%= avatar_tag user, loading: :lazy, class: "flex-item-no-shrink" %>

<strong class="overflow-ellipsis">
<%= user.name %>
<%= link_to user.name, user_path(user), class: "txt-ink btn btn--plain" %>
</strong>

<hr class="separator--horizontal flex-item-grow" style="--border-color: var(--color-subtle-dark); --border-style: dashed" aria-hidden="true">
Expand Down
5 changes: 0 additions & 5 deletions app/views/accounts/users/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@
<%= image_tag "bolt.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Workflows</span>
<% end %>

<%= button_to session_path, method: :delete, class: "btn" do %>
<%= image_tag "logout.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Sign out</span>
<% end %>
</nav>
<% end %>

Expand Down
2 changes: 1 addition & 1 deletion app/views/comments/show.html.erb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<%= turbo_frame_tag dom_id(@comment) do %>
<div class="comment__content flex-inline flex-column full-width border border-radius">
<div class="comment__author flex align-center gap-half">
<strong><%= @comment.creator.name %></strong>
<strong><%= link_to @comment.creator.name, user_path(@comment.creator), class: "txt-ink btn btn--plain", data: { turbo_frame: "_top" } %></strong>
<%= link_to bucket_bubble_path(@comment.bubble.bucket, @comment.bubble, anchor: "comment_#{@comment.id}"), class: "txt-undecorated" do %>
<%= tag.time @comment.created_at, class: "comment__timestamp" do %>
<%= @comment.created_at.strftime("%b %d") %>
Expand Down
1 change: 1 addition & 0 deletions app/views/sessions/transfers/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<%= auto_submit_form_with method: :put %>
37 changes: 37 additions & 0 deletions app/views/users/_transfer.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<div class="flex flex-column align-center gap txt-medium--responsive txt-medium">
<% url = session_transfer_url(user.transfer_id) %>

<label class="flex flex-column gap full-width">
<div class="flex align-center gap justify-center">
<strong id="session_transfer_label" class="txt-medium">Link to automatically log in on another device</strong>
</div>
<span class="flex align-center gap margin-inline">
<input type="text" class="input fill-white" id="session_transfer_url" value="<%= url %>" aria-labelledby="session_transfer_label" readonly>
</span>
</label>

<div class="flex align-center gap">
<div data-controller="dialog" data-dialog-modal-value="true" class="flex-inline">
<%= tag.button class: "btn", data: { action: "dialog#open" } do %>
<%= image_tag "qr-code.svg", aria: { hidden: "true" }, size: 24, class: "colorize--black" %>
<span class="for-screen-reader">Show auto-login QR code</span>
<% end %>

<dialog class="dialog panel shadow" data-dialog-target="dialog">
<%= qr_code_image(url) %>

<form method="dialog" class="margin-block-start flex justify-center">
<button class="btn">
<%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Close</span>
</button>
</form>
</dialog>
</div>

<%= button_to_copy_to_clipboard(url) do %>
<%= image_tag "copy-paste.svg", aria: { hidden: "true" }, size: 24, class: "colorize--black" %>
<span class="for-screen-reader">Copy auto-login link</span>
<% end %>
</div>
</div>
62 changes: 62 additions & 0 deletions app/views/users/edit.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<% @page_title = "Edit your profile" %>

<% content_for :header do %>
<nav>
<%= link_to user_path(@user), class: "btn flex-item-justify-start" do %>
<%= image_tag "arrow-left.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Back</span>
<% end %>
</nav>
<% end %>

<div class="panel shadow center">
<div class="flex flex-column gap txt-medium">
<%= form_with model: @user, method: :patch, class: "flex flex-column gap", data: { controller: "form upload-preview" } do |form| %>
<div class="align-center center avatar__form gap">
<span class="btn btn--placeholder txt-small"></span>

<label class="btn avatar input--file txt-xx-large center fill-white">
<%= image_tag user_avatar_path(@user), aria: { hidden: "true" }, size: 128, data: { upload_preview_target: "image" } %>
<%= form.file_field :avatar, id: "file", class: "input", accept: "image/*",
data: { upload_preview_target: "input", action: "upload-preview#previewImage" } %>
<span class="for-screen-reader">Profile avatar for <%= @user.name %></span>
</label>

<% if @user.avatar.attached? %>
<%= tag.button type: :submit, form: "avatar-delete-form", class: "btn btn--negative txt-small", data: { turbo_confirm: "Are you sure you want to remove your avatar? This can't be undone." } do %>
<%= image_tag "minus.svg", aria: { hidden: "true" }, size: 20 %>
<span class="for-screen-reader">Delete avatar</span>
<% end %>
<% end %>
</div>

<div class="flex align-center gap">
<%= translation_button :user_name %>
<label class="flex align-center gap input input--actor">
<%= form.text_field :name, class: "input full-width", autocomplete: "name", placeholder: "Name", autofocus: true, required: true, data: { "1p-ignore": true } %>
<%= image_tag "person.svg", aria: { hidden: "true" }, size: 30, class: "colorize--black" %>
</label>
</div>
<div class="flex align-center gap">
<%= translation_button :email_address %>
<label class="flex align-center gap input input--actor">
<%= form.email_field :email_address, class: "input full-width", autocomplete: "username", placeholder: "Email address", required: true %>
<%= image_tag "email.svg", aria: { hidden: "true" }, size: 30, class: "colorize--black" %>
</label>
</div>
<div class="flex align-center gap">
<%= translation_button :password %>
<label class="flex align-center gap input input--actor">
<%= form.password_field :password, class: "input full-width", autocomplete: "new-password", placeholder: "Password", required: false, maxlength: 72 %>
<%= image_tag "password.svg", aria: { hidden: "true" }, size: 30, class: "colorize--black" %>
</label>
</div>
<button type="submit" id="log_in" class="btn btn--reversed center">
<%= image_tag "check.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Sign in</span>
</button>
<% end %>

<%= form_with url: user_avatar_url(@user), method: :delete, id: "avatar-delete-form" %>
</div>
</div>
49 changes: 49 additions & 0 deletions app/views/users/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<% @page_title = @user.name %>

<% content_for :header do %>
<nav>
<%= link_to account_users_path, class: "btn flex-item-justify-start" do %>
<%= image_tag "arrow-left.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Back</span>
<% end %>

<% if Current.user == @user %>
<%= link_to edit_user_path(@user), class: "btn flex-item-justify-end" do %>
<%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Edit</span>
<% end %>
<% end %>
</nav>
<% end %>

<div class="panel shadow center txt-align-center">
<div class="flex flex-column gap">
<div class="avatar txt-xx-large center fill-white">
<%= image_tag user_avatar_path(@user), alt: "Profile avatar for #{@user.name}", class: "avatar", size: 128 %>
</div>

<div class="flex flex-column gap-half margin-block-end">
<h1 class="txt-x-large margin-none"><%= @user.name %></h1>
<div class="txt-medium">
<% if @user.active? %>
<%= mail_to @user.email_address %>
<% else %>
<%= @user.name %> is no longer on this account
<% end %>
</div>
</div>
</div>
</div>

<% if Current.user == @user %>
<div class="panel shadow center margin-block-double">
<%= render "users/transfer", user: @user %>
</div>

<div class="panel shadow center margin-block-double">
<%= button_to session_path, method: :delete, class: "btn center txt-medium" do %>
<%= image_tag "logout.svg", aria: { hidden: true }, size: 24 %>
<span class="for-screen-reader">Sign out</span>
<% end %>
</div>
<% end %>
Loading
Loading