Compare commits

...

3 Commits

Author SHA1 Message Date
Trevor Vallender 01c0ab9cc0 Add missing email previews 2024-05-30 14:39:41 +01:00
Trevor Vallender 46d8a4bbca Add ActionText and user profile text 2024-05-30 09:07:54 +01:00
Trevor Vallender b58c0a7c1a Add Action Text 2024-05-30 08:40:36 +01:00
24 changed files with 323 additions and 19 deletions

View File

@ -15,7 +15,7 @@ gem "jbuilder"
gem "bcrypt", "~> 3.1.7" gem "bcrypt", "~> 3.1.7"
gem "tzinfo-data", platforms: %i[ windows jruby ] gem "tzinfo-data", platforms: %i[ windows jruby ]
gem "bootsnap", require: false gem "bootsnap", require: false
# gem "image_processing", "~> 1.2" gem "image_processing", "~> 1.2"
gem "solid_queue" gem "solid_queue"
gem "mission_control-jobs" gem "mission_control-jobs"

View File

@ -105,6 +105,7 @@ GEM
erubi (1.12.0) erubi (1.12.0)
et-orbi (1.2.11) et-orbi (1.2.11)
tzinfo tzinfo
ffi (1.16.3)
fugit (1.10.1) fugit (1.10.1)
et-orbi (~> 1, >= 1.2.7) et-orbi (~> 1, >= 1.2.7)
raabro (~> 1.4) raabro (~> 1.4)
@ -112,6 +113,9 @@ GEM
activesupport (>= 6.1) activesupport (>= 6.1)
i18n (1.14.5) i18n (1.14.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
importmap-rails (2.0.1) importmap-rails (2.0.1)
actionpack (>= 6.0.0) actionpack (>= 6.0.0)
activesupport (>= 6.0.0) activesupport (>= 6.0.0)
@ -135,6 +139,7 @@ GEM
net-smtp net-smtp
marcel (1.0.4) marcel (1.0.4)
matrix (0.4.2) matrix (0.4.2)
mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (5.23.1) minitest (5.23.1)
mission_control-jobs (0.2.1) mission_control-jobs (0.2.1)
@ -259,6 +264,8 @@ GEM
rubocop-performance rubocop-performance
rubocop-rails rubocop-rails
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
ruby-vips (2.2.1)
ffi (~> 1.12)
rubyzip (2.3.2) rubyzip (2.3.2)
selenium-webdriver (4.21.1) selenium-webdriver (4.21.1)
base64 (~> 0.2) base64 (~> 0.2)
@ -311,6 +318,7 @@ DEPENDENCIES
bootsnap bootsnap
capybara capybara
debug debug
image_processing (~> 1.2)
importmap-rails importmap-rails
jbuilder jbuilder
mission_control-jobs mission_control-jobs

View File

@ -0,0 +1,31 @@
/*
* Provides a drop-in pointer for the default Trix stylesheet that will format the toolbar and
* the trix-editor content (whether displayed or under editing). Feel free to incorporate this
* inclusion directly in any other asset bundle and remove this file.
*
*= require trix
*/
/*
* We need to override trix.csss image gallery styles to accommodate the
* <action-text-attachment> element we wrap around attachments. Otherwise,
* images in galleries will be squished by the max-width: 33%; rule.
*/
.trix-content .attachment-gallery > action-text-attachment,
.trix-content .attachment-gallery > .attachment {
flex: 1 0 33%;
padding: 0 0.5em;
max-width: 33%;
}
.trix-content .attachment-gallery.attachment-gallery--2 > action-text-attachment,
.trix-content .attachment-gallery.attachment-gallery--2 > .attachment, .trix-content .attachment-gallery.attachment-gallery--4 > action-text-attachment,
.trix-content .attachment-gallery.attachment-gallery--4 > .attachment {
flex-basis: 50%;
max-width: 50%;
}
.trix-content action-text-attachment .attachment {
padding: 0 !important;
max-width: 100% !important;
}

View File

@ -12,4 +12,5 @@
--notice-text-color: #fff; --notice-text-color: #fff;
--invalid-alert: #d00; --invalid-alert: #d00;
--input-background: #fff;
} }

View File

@ -1,4 +1,4 @@
form { form, fieldset {
display: grid; display: grid;
gap: 1rem; gap: 1rem;
grid-template-columns: 1fr 2fr; grid-template-columns: 1fr 2fr;
@ -31,4 +31,12 @@ form {
color: var(--invalid-alert); color: var(--invalid-alert);
font-size: .8em; font-size: .8em;
} }
fieldset, p, trix-editor {
grid-column: 1/3;
}
trix-editor {
background-color: var(--input-background);
}
} }

View File

@ -2,14 +2,15 @@
class UsersController < ApplicationController class UsersController < ApplicationController
skip_before_action :authenticate, only: [ :new, :create ] skip_before_action :authenticate, only: [ :new, :create ]
before_action :set_user, only: [ :show ] before_action :set_user, only: [ :show, :edit, :update ]
before_action :ensure_self, only: [ :edit, :update ]
def new def new
@user = User.new @user = User.new
end end
def create def create
@user = User.new(user_params) @user = User.new(new_user_params)
if @user.save if @user.save
token = @user.generate_token_for(:email_verification) token = @user.generate_token_for(:email_verification)
UserMailer.with(user: @user, token: token).email_verification.deliver_later UserMailer.with(user: @user, token: token).email_verification.deliver_later
@ -27,9 +28,21 @@ class UsersController < ApplicationController
end end
end end
def edit
end
def update
if @user.update(existing_user_params)
redirect_to @user, notice: t(".success")
else
flash.now[:alert] = t(".error")
render :edit, status: :unprocessable_entity
end
end
private private
def user_params def new_user_params
params.require(:user).permit( params.require(:user).permit(
:username, :username,
:password, :password,
@ -40,7 +53,22 @@ class UsersController < ApplicationController
) )
end end
def existing_user_params
params.require(:user).permit(
:password,
:password_confirmation,
:password_challenge,
:first_name,
:last_name,
:profile,
)
end
def set_user def set_user
@user = User.find_by(username: params[:id]) @user = User.find_by(username: params[:id])
end end
def ensure_self
head :forbidden unless @user == Current.user || Current.user.admin?
end
end end

View File

@ -1,3 +1,6 @@
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails // Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
import "@hotwired/turbo-rails" import "@hotwired/turbo-rails"
import "controllers" import "controllers"
import "trix"
import "@rails/actiontext"

View File

@ -5,6 +5,7 @@ class User < ApplicationRecord
has_many :owned_tables, foreign_key: :owner_id, class_name: "Table" has_many :owned_tables, foreign_key: :owner_id, class_name: "Table"
has_many :players, dependent: :destroy has_many :players, dependent: :destroy
has_many :tables, through: :players has_many :tables, through: :players
has_rich_text :profile
has_secure_password has_secure_password
generates_token_for :password_reset, expires_in: 4.hours do generates_token_for :password_reset, expires_in: 4.hours do

View File

@ -0,0 +1,14 @@
<figure class="attachment attachment--<%= blob.representable? ? "preview" : "file" %> attachment--<%= blob.filename.extension %>">
<% if blob.representable? %>
<%= image_tag blob.representation(resize_to_limit: local_assigns[:in_gallery] ? [ 800, 600 ] : [ 1024, 768 ]) %>
<% end %>
<figcaption class="attachment__caption">
<% if caption = blob.try(:caption) %>
<%= caption %>
<% else %>
<span class="attachment__name"><%= blob.filename %></span>
<span class="attachment__size"><%= number_to_human_size blob.byte_size %></span>
<% end %>
</figcaption>
</figure>

View File

@ -0,0 +1,3 @@
<div class="trix-content">
<%= yield -%>
</div>

View File

@ -19,6 +19,7 @@
<ul> <ul>
<% if logged_in? %> <% if logged_in? %>
<li><%= link_to t(".tables"), tables_path %></li> <li><%= link_to t(".tables"), tables_path %></li>
<li><%= link_to t(".profile"), user_path(Current.user) %></li>
<li><%= link_to t("log_out"), logout_path, data: {turbo_method: :delete} %></li> <li><%= link_to t("log_out"), logout_path, data: {turbo_method: :delete} %></li>
<% if Current.user.admin? %> <% if Current.user.admin? %>
<li><%= link_to t("administration"), admin_index_path %></li> <li><%= link_to t("administration"), admin_index_path %></li>

View File

@ -3,7 +3,7 @@
<section class="inset"> <section class="inset">
<%= form_with model: user do |f| %> <%= form_with model: user do |f| %>
<%= f.label :username %> <%= f.label :username %>
<%= f.text_field :username, required: true %> <%= f.text_field :username, required: true, disabled: user.persisted? %>
<%= display_form_errors(user, :username) %> <%= display_form_errors(user, :username) %>
<%= f.label :first_name %> <%= f.label :first_name %>
@ -15,16 +15,34 @@
<%= display_form_errors(user, :last_name) %> <%= display_form_errors(user, :last_name) %>
<%= f.label :email %> <%= f.label :email %>
<%= f.text_field :email, required: true %> <%= f.text_field :email, required: true, disabled: user.persisted? %>
<%= display_form_errors(user, :email) %> <%= display_form_errors(user, :email) %>
<%= f.label :password %> <fieldset>
<%= f.password_field :password, required: true %> <legend><%= t(".password") %></legend>
<%= display_form_errors(user, :password) %>
<%= f.label :password_confirmation %> <% if user.persisted? %>
<%= f.password_field :password_confirmation, required: true %> <%= f.label :password_challenge, t(".current_password") %>
<%= display_form_errors(user, :password_confirmation) %> <%= f.password_field :password_challenge, required: user.new_record? %>
<%= display_form_errors(user, :password_challenge) %>
<p><%= t(".password_hint") %></p>
<% end %>
<%= f.label :password %>
<%= f.password_field :password, required: user.new_record? %>
<%= display_form_errors(user, :password) %>
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation, required: user.new_record? %>
<%= display_form_errors(user, :password_confirmation) %>
</fieldset>
<% if user.persisted? %>
<%= f.label :profile %>
<%= f.rich_text_area :profile %>
<%= display_form_errors(user, :profile) %>
<% end %>
<%= f.submit button_text %> <%= f.submit button_text %>
<% end %> <% end %>

View File

@ -0,0 +1,6 @@
<% content_for :title, t(".edit_profile") %>
<h2><%= t(".edit_profile") %></h2>
<%= render partial: "users/form",
locals: { user: @user, button_text: t(".update_profile") } %>

View File

@ -2,6 +2,13 @@
<h2><%= @user.username %></h2> <h2><%= @user.username %></h2>
<% if @user == Current.user && @table_invites.any? %> <aside>
<%= link_to t(".your_invites"), table_invites_path %> <% if @user == Current.user %>
<% end %> <%= link_to t(".edit_profile"), edit_user_path(@user) %>
<% if @table_invites.any? %>
<%= link_to t(".your_invites"), table_invites_path %>
<% end %>
<% end %>
</aside>
<%= @user.profile %>

View File

@ -7,3 +7,5 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js" pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js" pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"
pin_all_from "app/javascript/controllers", under: "controllers" pin_all_from "app/javascript/controllers", under: "controllers"
pin "trix"
pin "@rails/actiontext", to: "actiontext.esm.js"

View File

@ -15,6 +15,7 @@ en:
game_systems: Game Systems game_systems: Game Systems
jobs: Jobs jobs: Jobs
application: application:
profile: Profile
tables: Tables tables: Tables
mailer: mailer:
greeting: "Hi %{name}," greeting: "Hi %{name},"
@ -130,6 +131,17 @@ en:
success: "Thanks for joining Tabletop Companion, %{name}! Please check your email to verify your address." success: "Thanks for joining Tabletop Companion, %{name}! Please check your email to verify your address."
show: show:
your_invites: Your invites your_invites: Your invites
edit_profile: Edit profile
edit:
edit_profile: Edit profile
update_profile: Update profile
form:
password: Password
password_hint: To keep your existing password, leave the below fields blank
current_password: Current password
update:
success: Your profile has been updated
error: Failed to update profile
user_mailer: user_mailer:
email_verified: email_verified:
content: |- content: |-

View File

@ -8,7 +8,7 @@ Rails.application.routes.draw do
get "login" => "sessions#new", as: :login get "login" => "sessions#new", as: :login
delete "logout" => "sessions#destroy", as: :logout delete "logout" => "sessions#destroy", as: :logout
resources :users, only: [ :new, :create, :show ] resources :users, only: [ :new, :create, :show, :edit, :update ]
resources :account_verifications, only: [ :show ] resources :account_verifications, only: [ :show ]
resources :sessions, only: [ :new, :create, :destroy ] resources :sessions, only: [ :new, :create, :destroy ]

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20170806125915)
class CreateActiveStorageTables < ActiveRecord::Migration[7.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :active_storage_blobs, id: primary_key_type do |t|
t.string :key, null: false
t.string :filename, null: false
t.string :content_type
t.text :metadata
t.string :service_name, null: false
t.bigint :byte_size, null: false
t.string :checksum
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :key ], unique: true
end
create_table :active_storage_attachments, id: primary_key_type do |t|
t.string :name, null: false
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.references :blob, null: false, type: foreign_key_type
if connection.supports_datetime_with_precision?
t.datetime :created_at, precision: 6, null: false
else
t.datetime :created_at, null: false
end
t.index [ :record_type, :record_id, :name, :blob_id ], name: :index_active_storage_attachments_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
create_table :active_storage_variant_records, id: primary_key_type do |t|
t.belongs_to :blob, null: false, index: false, type: foreign_key_type
t.string :variation_digest, null: false
t.index [ :blob_id, :variation_digest ], name: :index_active_storage_variant_records_uniqueness, unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
# This migration comes from action_text (originally 20180528164100)
class CreateActionTextTables < ActiveRecord::Migration[6.0]
def change
# Use Active Record's configured type for primary and foreign keys
primary_key_type, foreign_key_type = primary_and_foreign_key_types
create_table :action_text_rich_texts, id: primary_key_type do |t|
t.string :name, null: false
t.text :body, size: :long
t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type
t.timestamps
t.index [ :record_type, :record_id, :name ], name: "index_action_text_rich_texts_uniqueness", unique: true
end
end
private
def primary_and_foreign_key_types
config = Rails.configuration.generators
setting = config.options[config.orm][:primary_key_type]
primary_key_type = setting || :primary_key
foreign_key_type = setting || :bigint
[ primary_key_type, foreign_key_type ]
end
end

42
db/schema.rb generated
View File

@ -10,10 +10,48 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_05_30_071707) do ActiveRecord::Schema[7.1].define(version: 2024_05_30_073852) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
t.text "body"
t.string "record_type", null: false
t.bigint "record_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.string "service_name", null: false
t.bigint "byte_size", null: false
t.string "checksum"
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "game_systems", force: :cascade do |t| create_table "game_systems", force: :cascade do |t|
t.string "name", null: false t.string "name", null: false
t.datetime "created_at", null: false t.datetime "created_at", null: false
@ -191,6 +229,8 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_30_071707) do
t.check_constraint "length(username::text) >= 3", name: "chk_username_min_length" t.check_constraint "length(username::text) >= 3", name: "chk_username_min_length"
end end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "players", "tables" add_foreign_key "players", "tables"
add_foreign_key "players", "users" add_foreign_key "players", "users"
add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade add_foreign_key "solid_queue_blocked_executions", "solid_queue_jobs", column: "job_id", on_delete: :cascade

View File

@ -34,6 +34,24 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_response :unprocessable_entity assert_response :unprocessable_entity
end end
test "should get edit" do
sign_in users(:trevor)
get edit_user_url(users(:trevor))
assert_response :success
end
test "can only edit own user" do
sign_in users(:trevor)
get edit_user_url(users(:gimli))
assert_response :forbidden
end
test "should update user" do
sign_in users(:trevor)
patch user_url(users(:trevor)), params: { user: { profile: "All about me" } }
assert_redirected_to user_path(users(:trevor))
end
private private
def user_params def user_params

View File

@ -0,0 +1,4 @@
one:
record: trevor (User)
name: profile
body: <p>I am just <strong>so awesome</strong></p>

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
# Preview all emails at http://localhost:3000/rails/mailers/table_invite_mailer
class TableInviteMailerPreview < ActionMailer::Preview
def invite_user
TableInviteMailer.with(table_invite: TableInvite.first).invite_user
end
def invite_new_user
TableInviteMailer.with(table_invite: TableInvite.first).invite_new_user
end
end

View File

@ -1,4 +1,4 @@
- user page, avatar, profile description - avatars
- list invites on user page - list invites on user page
- notifications - notifications
- Add characters to users/tables - Add characters to users/tables