Compare commits

..

7 Commits

Author SHA1 Message Date
Trevor Vallender 52022ce65e Add sections based on templates 2024-07-04 08:04:10 +01:00
Trevor Vallender 79e3e79b12 Create templates 2024-07-02 18:07:06 +01:00
Trevor Vallender d4b982ea16 Add deserialize method to CharacterSheetSection 2024-06-30 09:49:24 +01:00
Trevor Vallender c870e373bc Add template builder service 2024-06-30 08:46:38 +01:00
Trevor Vallender 032c18d05d Add template model 2024-06-29 09:16:37 +01:00
Trevor Vallender e62578dc0d Don't push footer off page 2024-06-29 09:07:49 +01:00
Trevor Vallender 202f827d57 Logo 2024-06-29 09:03:27 +01:00
25 changed files with 276 additions and 13 deletions

BIN
app/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -5,6 +5,12 @@
--header-color: #15345b; --header-color: #15345b;
--header-text-color: #fff; --header-text-color: #fff;
--header-height: 200px; --header-height: 200px;
--footer-height: 150px;
--shadow-color: #999;
--light-shadow-color: #CCC;
--secondary-text-color: #777;
--shadow-color: #999; --shadow-color: #999;
--light-shadow-color: #CCC; --light-shadow-color: #CCC;

View File

@ -1,3 +1,3 @@
body { body {
font-family: Roboto, sans-serif; font-family: sans-serif;
} }

View File

@ -17,7 +17,7 @@ main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 1em 1em 4em 1em; padding: 1em 1em 4em 1em;
min-height: calc(100vh - var(--header-height)); min-height: calc(100vh - var(--header-height) - var(--footer-height));
} }
@media(min-width: 800px) { @media(min-width: 800px) {
@ -58,6 +58,11 @@ header {
header h1 { header h1 {
margin: 0; margin: 0;
padding: 1em; padding: 1em;
img {
width: 100%;
max-width: 600px;
min-width: 400px;
}
} }
header h1 a:link, header h1 a:visited { header h1 a:link, header h1 a:visited {
@ -166,6 +171,7 @@ footer {
padding: 1em; padding: 1em;
margin: 0 auto; margin: 0 auto;
font-size: .8em; font-size: .8em;
height: var(--footer-height);
a:link, a:visited { a:link, a:visited {
color: var(--header-text-color); color: var(--header-text-color);
text-decoration: none; text-decoration: none;
@ -175,3 +181,7 @@ footer {
margin: 0 auto; margin: 0 auto;
} }
} }
.subsections {
margin: .5em;
}

View File

@ -15,6 +15,7 @@ class CharacterSheetSectionsController < ApplicationController
end end
def new def new
@templates = Template.game_system(@character.game_system)
if params[:parent_section_id].present? if params[:parent_section_id].present?
@parent_section = @character.character_sheet_sections.find_by(id: params[:parent_section_id]) @parent_section = @character.character_sheet_sections.find_by(id: params[:parent_section_id])
end end
@ -22,7 +23,16 @@ class CharacterSheetSectionsController < ApplicationController
end end
def create def create
@templates = Template.game_system(@character.game_system)
if params[:template_id].present?
@section = CharacterSheetSection.new_from_template(
template: Template.find(params[:template_id]),
params: character_sheet_section_params,
character: @character,
)
else
@section = @character.character_sheet_sections.new(character_sheet_section_params) @section = @character.character_sheet_sections.new(character_sheet_section_params)
end
@editable = true @editable = true
unless @section.save unless @section.save
@parent_section = @section.parent_section @parent_section = @section.parent_section

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
class TemplatesController < ApplicationController
before_action :set_character_sheet_section, only: [ :new ]
before_action :set_template, only: [ :show ]
def new
@template = Template.new
end
def create
@section = CharacterSheetSection.find(template_params[:character_sheet_section_id])
@template = TemplateBuilder.create!(
name: template_params[:name],
from: @section,
game_system: GameSystem.find(template_params[:game_system_id]),
)
if @template.persisted?
redirect_to @template
else
flash.now[:alert] = t(".error")
render :new, status: :unprocessable_entity
end
end
def show
end
private
def set_template
@template = Template.find(params[:id])
end
def set_character_sheet_section
@section = CharacterSheetSection.find(params[:character_sheet_section_id])
end
def template_params
params.require(:template).permit(
:name,
:character_sheet_section_id,
:game_system_id,
:klass,
)
end
end

View File

@ -35,7 +35,7 @@ class CharacterSheetFeature < ApplicationRecord
def set_order_index def set_order_index
return if order_index.present? return if order_index.present?
if character_sheet_section.character_sheet_features.any? if character_sheet_section.character_sheet_features.count > 1
self.order_index = character_sheet_section.character_sheet_features.order(:order_index).last.order_index + 1 self.order_index = character_sheet_section.character_sheet_features.order(:order_index).last.order_index + 1
else else
self.order_index = 1 self.order_index = 1

View File

@ -18,6 +18,13 @@ class CharacterSheetSection < ApplicationRecord
scope :top_level, -> { where.missing(:parent_section) } scope :top_level, -> { where.missing(:parent_section) }
def self.new_from_template(template:, character:, params:)
section = CharacterSheetSection.deserialize(JSON.parse(template.content))
section.assign_attributes(params)
section.character = character
section
end
def lowest_order_index def lowest_order_index
character_sheet_features.minimum(:order_index) || 1 character_sheet_features.minimum(:order_index) || 1
end end
@ -25,4 +32,22 @@ class CharacterSheetSection < ApplicationRecord
def highest_order_index def highest_order_index
character_sheet_features.maximum(:order_index) || 1 character_sheet_features.maximum(:order_index) || 1
end end
def self.deserialize(h)
section = new
h["character_sheet_subsections"].each do |sub|
section.character_sheet_subsections.deserialize(sub)
end
h["stats"].each do |stat|
section.stats.build(stat)
end
h["text_fields"].each do |text_field|
section.text_fields.build(text_field)
end
section
end
end end

14
app/models/template.rb Normal file
View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class Template < ApplicationRecord
belongs_to :game_system
validates :name, presence: true,
uniqueness: { scope: :game_system_id },
length: { maximum: 200 }
validates :content, presence: true
validates :klass, presence: true,
length: { maximum: 200 }
scope :game_system, ->(game_system) { where(game_system:) }
end

View File

@ -0,0 +1,40 @@
# frozen_string_literaL: true
class TemplateBuilder
class << self
def create!(name:, from:, game_system:)
Template.create(
name:,
klass: from.class.name,
game_system:,
content: serialize_character_sheet_section(from),
)
end
private
def serialize_character_sheet_section(section)
always_except = [ :id, :created_at, :updated_at ]
subsection_except = [ :character_id, :parent_section_id ]
always_include = {
stats: { except: always_except + [ :value ] },
text_fields: { except: always_except },
}
section.to_json(
except: always_except + subsection_except,
include: {
character_sheet_subsections: {
except: always_except + subsection_except,
include: always_include.merge(
character_sheet_subsections: {
except: always_except + subsection_except,
include: always_include,
},
),
},
}.merge(always_include),
)
end
end
end

View File

@ -14,4 +14,8 @@
</div> </div>
<% end %> <% end %>
</div> </div>
<div id="<%= dom_id(character_sheet_section) %>_subsections" class="subsections">
<%= render character_sheet_section.character_sheet_subsections %>
</div>
</div> </div>

View File

@ -7,6 +7,8 @@
new_character_sheet_section_stat_path(section), data: { turbo_stream: true }, class: "add-stat" %> new_character_sheet_section_stat_path(section), data: { turbo_stream: true }, class: "add-stat" %>
<%= link_to t(".add_text_field"), <%= link_to t(".add_text_field"),
new_character_sheet_section_text_field_path(section), data: { turbo_stream: true }, class: "add-text-field" %> new_character_sheet_section_text_field_path(section), data: { turbo_stream: true }, class: "add-text-field" %>
<%= link_to t(".save_as_template"),
new_character_sheet_section_template_path(section), data: { turbo_frame: "_top" } %>
<% end %> <% end %>
<% unless id == "character_sheet_add_section" %> <% unless id == "character_sheet_add_section" %>
<%= link_to t(".delete_section", name: section.name), character_sheet_section_path(section), <%= link_to t(".delete_section", name: section.name), character_sheet_section_path(section),

View File

@ -6,6 +6,10 @@
<%= f.text_field :name %> <%= f.text_field :name %>
<%= display_form_errors(@section, :name) %> <%= display_form_errors(@section, :name) %>
<%= label_tag :template_id, t(".template") %>
<%= collection_select nil, :template_id, @templates, :id, :name, include_blank: " " %>
<%= display_form_errors(@section, :template) %>
<%= f.submit button_text %> <%= f.submit button_text %>
<% end %> <% end %>
</section> </section>

View File

@ -1,4 +1,4 @@
<% content_target = @section.parent_section.present? ? "#{dom_id(@section.parent_section)}_sections" <% content_target = @section.parent_section.present? ? "#{dom_id(@section.parent_section)}_subsections"
: "character_sheet" %> : "character_sheet" %>
<%= turbo_stream.append(content_target) do %> <%= turbo_stream.append(content_target) do %>
<%= render @section %> <%= render @section %>

View File

@ -1,5 +1,5 @@
<div id=<%= dom_id(character) %> class="character"> <div id=<%= dom_id(character) %> class="character">
<h5><%= link_to character.name, character %></h5> <h5><%= link_to character.name, character, data: { turbo_frame: "_top" } %></h5>
<ul> <ul>
<li><%= character.game_system.name %></li> <li><%= character.game_system.name %></li>
<li><%= character.user.username %></li> <li><%= character.user.username %></li>

View File

@ -16,7 +16,9 @@
<%= turbo_frame_tag :modal %> <%= turbo_frame_tag :modal %>
<header> <header>
<%= yield(:header_content) if content_for?(:header_content) %> <%= yield(:header_content) if content_for?(:header_content) %>
<h1><%= link_to t("site_name"), root_path %></h1> <h1><%= link_to root_path do %>
<%= image_tag "logo.png", width: "600px" %>
<% end %></h1>
<nav> <nav>
<ul> <ul>
<% if logged_in? %> <% if logged_in? %>

View File

@ -0,0 +1,13 @@
<section class="inset">
<%= form_with model: @template, url: character_sheet_section_templates_path(@section) do |f| %>
<%= f.hidden_field :character_sheet_section_id, value: @section.id %>
<%= f.hidden_field :game_system_id, value: @section.character.game_system.id %>
<%= f.hidden_field :klass, value: "CharacterSheetSection" %>
<%= f.label :name %>
<%= f.text_field :name %>
<%= display_form_errors(@section, :name) %>
<%= f.submit t(".save") %>
<% end %>
</section>

View File

@ -0,0 +1,6 @@
<% content_for :title, t(".create_template") %>
<h1><%= t(".create_template") %></h1>
<%= render partial: "templates/form",
locals: { template: @template, button_text: t(".create_template") } %>

View File

@ -0,0 +1,10 @@
<% content_for :title, @template.name %>
<h1><%= @template.name %></h1>
<dl>
<dt><%= t(".game_system") %>:</dt>
<dd><%= @template.game_system.name %></dd>
<dt><%= t(".content") %>:</dt>
<dd><%= @template.content %></dd>
</dl>

View File

@ -66,6 +66,7 @@ en:
confirm_delete: Are you sure you want to delete this section? confirm_delete: Are you sure you want to delete this section?
add_stat: Add stat add_stat: Add stat
add_text_field: Add text field add_text_field: Add text field
save_as_template: Save as template
index: index:
edit: Edit character sheet edit: Edit character sheet
stop_editing: Stop editing stop_editing: Stop editing
@ -74,6 +75,8 @@ en:
add_section: Add section add_section: Add section
new: new:
create_section: Create section create_section: Create section
form:
template: From template (leave blank for none)
characters: characters:
index: index:
my_characters: My characters my_characters: My characters
@ -141,7 +144,7 @@ en:
stats: stats:
show: show:
roll: Roll! roll: Roll!
roll_type_html: Roll %{name}! <br> <small>%{command}</small> roll_type_html: Roll %{name}! (%{command})
min_allowed: Min min_allowed: Min
max_allowed: Max max_allowed: Max
stat: stat:
@ -220,6 +223,17 @@ en:
destroy: destroy:
success: Deleted table “%{name}”. success: Deleted table “%{name}”.
error: Failed to delete table. error: Failed to delete table.
templates:
new:
create_template: Create template
create:
success: The template “%{name}” has been created
error: Failed to create template
form:
save: Create template
show:
game_system: Game system
content: Content
text_fields: text_fields:
text_field: text_field:
delete: Delete text field delete: Delete text field

View File

@ -23,6 +23,7 @@ Rails.application.routes.draw do
end end
resources :character_sheet_sections, only: [ :destroy ] do resources :character_sheet_sections, only: [ :destroy ] do
resources :stats, only: [ :new, :create ] resources :stats, only: [ :new, :create ]
resources :templates, only: [ :new, :create ]
resources :text_fields, only: [ :new, :create ] resources :text_fields, only: [ :new, :create ]
end end
resources :characters do resources :characters do
@ -38,6 +39,7 @@ Rails.application.routes.draw do
resources :events, only: [ :index ] resources :events, only: [ :index ]
resources :table_invites, only: [ :new, :create ] resources :table_invites, only: [ :new, :create ]
end end
resources :templates, only: [ :show ]
resources :text_fields, only: [ :show, :update, :destroy ] resources :text_fields, only: [ :show, :update, :destroy ]
resources :admin, only: [ :index ] resources :admin, only: [ :index ]

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
class CreateTemplates < ActiveRecord::Migration[7.1]
def change
create_table :templates do |t|
t.belongs_to :game_system, null: false, foreign_key: true
t.string :name, null: false
t.jsonb :content, null: false, default: {}
t.string :klass, null: false
t.timestamps
end
add_check_constraint :templates, "length(name) <= 200", name: "chk_template_name_max_length"
add_check_constraint :templates, "length(klass) <= 200", name: "chk_template_klass_max_length"
add_index :templates, [ :name, :game_system_id ], unique: true
end
end

16
db/schema.rb generated
View File

@ -10,7 +10,7 @@
# #
# 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_06_21_141219) do ActiveRecord::Schema[7.1].define(version: 2024_06_29_080930) 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"
@ -283,6 +283,19 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_21_141219) do
t.check_constraint "length(slug::text) <= 100", name: "chk_table_slug_max_length" t.check_constraint "length(slug::text) <= 100", name: "chk_table_slug_max_length"
end end
create_table "templates", force: :cascade do |t|
t.bigint "game_system_id", null: false
t.string "name", null: false
t.jsonb "content", default: {}, null: false
t.string "klass", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["game_system_id"], name: "index_templates_on_game_system_id"
t.index ["name", "game_system_id"], name: "index_templates_on_name_and_game_system_id", unique: true
t.check_constraint "length(klass::text) <= 200", name: "chk_template_klass_max_length"
t.check_constraint "length(name::text) <= 200", name: "chk_template_name_max_length"
end
create_table "text_fields", force: :cascade do |t| create_table "text_fields", 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
@ -328,4 +341,5 @@ ActiveRecord::Schema[7.1].define(version: 2024_06_21_141219) do
add_foreign_key "table_invites", "tables" add_foreign_key "table_invites", "tables"
add_foreign_key "tables", "game_systems" add_foreign_key "tables", "game_systems"
add_foreign_key "tables", "users", column: "owner_id" add_foreign_key "tables", "users", column: "owner_id"
add_foreign_key "templates", "game_systems"
end end

View File

@ -0,0 +1,18 @@
# frozen_string_literal: true
require "test_helper"
class TemplateBuilderTest < ActiveSupport::TestCase
test "creates template from a CharacterSheetSection" do
section = character_sheet_sections(:stats)
template = TemplateBuilder.create!(
name: "Test template",
game_system: GameSystem.last,
from: section,
)
template_hash = JSON.parse(template.content)
assert_equal 1, template_hash["character_sheet_subsections"].count
assert_equal 3, template_hash["stats"].count
assert_equal "Stats", template_hash["name"]
end
end

12
todo.md
View File

@ -1,8 +1,12 @@
- Edit dice roll command, types - Templates
- request invite - Rename to SectionTemplate
- Weapons/spells: skills? - Template accessibility level
- Do this have_many :stats? - Users can add their own, "official" ones are always available
- Unofficial ones can be added to a local list of favourites
- Add from template
- Edit/delete templates
- Lists - Lists
- request invite
- improve dice roll parsing to allow e.g. choose highest - improve dice roll parsing to allow e.g. choose highest
- Easy add amount to stat - Easy add amount to stat
- icons on features - icons on features