Mattias Bodlund 12 hours ago
parent
commit
691826db60
26 changed files with 275 additions and 253 deletions
  1. +2
    -2
      Gemfile.lock
  2. +42
    -0
      app/assets/stylesheets/application.css
  3. +2
    -0
      app/assets/stylesheets/forms.css
  4. +8
    -16
      app/controllers/admin/nodes_controller.rb
  5. +2
    -37
      app/controllers/concerns/quiz_helper_methods.rb
  6. +0
    -1
      app/controllers/direct_uploads_controller.rb
  7. +14
    -0
      app/controllers/players_controller.rb
  8. +36
    -0
      app/controllers/stages_controller.rb
  9. +39
    -46
      app/helpers/admin/nodes_helper.rb
  10. +2
    -0
      app/helpers/stages_helper.rb
  11. +17
    -70
      app/models/node.rb
  12. +26
    -0
      app/models/player.rb
  13. +0
    -20
      app/views/languages/_intro.html.erb
  14. +12
    -1
      app/views/languages/index.html.erb
  15. +1
    -1
      app/views/layouts/application.html.erb
  16. +0
    -11
      app/views/site/tmpl_article.html.erb
  17. +0
    -0
      app/views/site/tmpl_chance.html.erb
  18. +0
    -0
      app/views/site/tmpl_choise.html.erb
  19. +19
    -0
      app/views/stages/show.html.erb
  20. +5
    -18
      config/locales/en.yml
  21. +16
    -25
      config/routes.rb
  22. +2
    -4
      db/migrate/20240424093536_create_attachments.rb
  23. +10
    -0
      db/migrate/20250527114853_create_players.rb
  24. +9
    -0
      db/migrate/20260323110134_add_missing_columns_to_players.rb
  25. +4
    -1
      db/schema.rb
  26. +7
    -0
      test/controllers/stages_controller_test.rb

+ 2
- 2
Gemfile.lock View File

@ -154,7 +154,7 @@ GEM
kaminari-core (= 1.2.2)
kaminari-core (1.2.2)
language_server-protocol (3.17.0.5)
lexxy (0.8.5.beta)
lexxy (0.9.0.beta)
rails (>= 8.0.2)
lint_roller (1.1.0)
logger (1.7.0)
@ -467,7 +467,7 @@ CHECKSUMS
kaminari-activerecord (1.2.2) sha256=0dd3a67bab356a356f36b3b7236bcb81cef313095365befe8e98057dd2472430
kaminari-core (1.2.2) sha256=3bd26fec7370645af40ca73b9426a448d09b8a8ba7afa9ba3c3e0d39cdbb83ff
language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
lexxy (0.8.5.beta) sha256=336b95507f80cb3ddad533c27edca8851c7773831a767495cdabfc984c4831ec
lexxy (0.9.0.beta) sha256=d64aa2beb09ff64400e45813a4893a7cbe875273198680d95d172fe11ebe829d
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04


+ 42
- 0
app/assets/stylesheets/application.css View File

@ -200,3 +200,45 @@ html[lang="sv"] [data-locale='sv'],
html[lang="uk"] [data-locale='uk'] {
display: revert;
}
main {
flex-grow: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0.75rem;
}
article {
background-color: var(--clr-white);
padding: 2rem 1.5rem;
max-width: 48ch;
& > *:first-child {
margin-block-start: 0;
}
}
h2 {
line-height: 1.2;
margin-block-end: 1em;
}
p {
margin-block-end: 1em;
}
.action-container {
margin-block-start: 2em;
}
button {
appearance: none;
border: none;
background-color: var(--clr-black);
color: var(--clr-white);
padding: 0.6em 1em;
cursor: pointer;
border-radius: 0.4em;
}

+ 2
- 0
app/assets/stylesheets/forms.css View File

@ -1096,9 +1096,11 @@ ul {
align-self: center;
}
.focus .ts-control,
.ts-control {
border: none;
padding: 8px 0;
box-shadow: none;
}
.ts-dropdown, .ts-control, .ts-control input {


+ 8
- 16
app/controllers/admin/nodes_controller.rb View File

@ -1,5 +1,4 @@
class Admin::NodesController < Admin::AdminController
before_action :set_node, only: %i[ edit update destroy children sort toggle ]
helper_method :current_node_id, :open_node_ids
@ -33,15 +32,14 @@ class Admin::NodesController < Admin::AdminController
# POST /nodes or /nodes.json
def create
@node = Node.new(node_params)
@node.status = :status_draft
base_title = t('ui.untitled')
base_title = t("ui.untitled")
title = base_title
count = 0
while @node.siblings.exists?(title: {I18n.locale => title})
while @node.siblings.exists?(title: { I18n.locale => title })
count += 1
title = "#{base_title} #{count}"
end
@ -54,7 +52,7 @@ class Admin::NodesController < Admin::AdminController
session[:open_node_ids] << @node.id unless session[:open_node_ids].include?(@node.id)
format.turbo_stream
format.html { redirect_to edit_admin_node_url(@node), notice: t('ui.category_created', category: t(@node.template, scope: 'nodes.templates')) }
format.html { redirect_to edit_admin_node_url(@node), notice: t("ui.category_created", category: t(@node.template, scope: "nodes.templates")) }
else
format.html { render :new, status: :unprocessable_entity }
end
@ -74,15 +72,14 @@ class Admin::NodesController < Admin::AdminController
# PATCH/PUT /nodes/1
def update
params[:node][:expires_at] ||= nil
respond_to do |format|
if @node.update(node_params)
format.turbo_stream {
flash.now[:notice] = t('ui.category_updated', category: t(@node.template, scope: 'nodes.templates'))
flash.now[:notice] = t("ui.category_updated", category: t(@node.template, scope: "nodes.templates"))
}
format.html { redirect_to edit_node_url(@node), notice: t('ui.category_updated', category: t(@node.category, scope: 'nodes.categories')) }
format.html { redirect_to edit_node_url(@node), notice: t("ui.category_updated", category: t(@node.category, scope: "nodes.categories")) }
else
format.html { render :edit, status: :unprocessable_entity }
end
@ -90,7 +87,7 @@ class Admin::NodesController < Admin::AdminController
end
# DELETE /nodes/1
# DELETE /nodes/1
def destroy
if @node.destroy!
@destroyed_node = @node
@ -98,9 +95,9 @@ class Admin::NodesController < Admin::AdminController
end
respond_to do |format|
format.turbo_stream {
flash.now[:notice] = t('ui.category_destroyed', category: t(@destroyed_node.category, scope: 'nodes.categories'))
flash.now[:notice] = t("ui.category_destroyed", category: t(@destroyed_node.category, scope: "nodes.categories"))
}
format.html { redirect_to nodes_url, notice: t(:'nodes.destroyed') }
format.html { redirect_to nodes_url, notice: t(:"nodes.destroyed") }
end
end
@ -124,8 +121,6 @@ class Admin::NodesController < Admin::AdminController
end
private
@ -188,7 +183,4 @@ private
session[:open_node_ids] << @node.id unless session[:open_node_ids].include?(@node.id)
end
end
end

+ 2
- 37
app/controllers/concerns/quiz_helper_methods.rb View File

@ -5,51 +5,16 @@ module QuizHelperMethods
before_action :require_player!
before_action :set_locale
helper_method :current_player,
:questions,
:question,
:questions_size,
:question_index,
:player_question_answer,
:result_node
helper_method :current_player
end
private
def questions
@questions ||= Node.at_depth(1).tmpl_article.viewable.ordered.to_a
end
def result_node
@result_node ||= Node.at_depth(1).tmpl_list.viewable.first
end
def question
@question ||= questions[params[:id].to_i-1]
end
def player_question_answer
@player_question_answer ||= Answer.find_by(player_id: current_player.id, node_id: question.id)&.value
end
def questions_size
@questions_size ||= questions.size
end
def question_index
@question_index ||= (params[:id].to_i)
end
def require_player!
unless player_present?
redirect_to url_for(controller: "players", action: "new")
redirect_to url_for(controller: "languages", action: "index")
end
end


+ 0
- 1
app/controllers/direct_uploads_controller.rb View File

@ -6,5 +6,4 @@ class DirectUploadsController < ActiveStorage::DirectUploadsController
def authenticate!
head :unauthorized unless User.enabled.admin_role.find_by(id: session[:user_id]).present?
end
end

+ 14
- 0
app/controllers/players_controller.rb View File

@ -0,0 +1,14 @@
class PlayersController < ApplicationController
include QuizHelperMethods
skip_before_action :require_player!
def create
reset_session
@player = Player.create(locale: I18n.locale)
session[:player_id] = @player.id
redirect_to stage_path(id: 1)
end
end

+ 36
- 0
app/controllers/stages_controller.rb View File

@ -0,0 +1,36 @@
class StagesController < ApplicationController
include QuizHelperMethods
before_action :set_locale
before_action :require_player!
before_action :set_node, only: [ :show, :flip, :choose ]
def show
@progress = current_player.progress[params[:id].to_s] || {}
end
def flip
outcome = rand < 0.5 ? "chance" : "choice"
current_player.record_flip(params[:id], outcome)
redirect_to stage_path(id: params[:id])
end
def choose
option_index = params[:option].to_i
current_player.record_option(params[:id], option_index)
# Advance to next stage
current_player.advance_to_next_stage!
redirect_to stage_path(id: current_player.current_stage)
end
private
def set_node
@node = Node.at_depth(1).viewable.ordered[params[:id].to_i - 1]
if @node.nil?
redirect_to root_path
end
end
end

+ 39
- 46
app/helpers/admin/nodes_helper.rb View File

@ -1,13 +1,11 @@
module Admin::NodesHelper
def node_structure_for_select(parent, node)
result = []
result << [parent.title, parent.id] if parent.root?
result << [ parent.title, parent.id ] if parent.root?
parent.children.ordered.each do |child|
result << ["#{"-" * (child.depth)} #{child.title}".html_safe, child.id, { disabled: (child == node or child.ancestor_ids.include?(node.id)) }]
result << [ "#{"-" * (child.depth)} #{child.title}".html_safe, child.id, { disabled: (child == node or child.ancestor_ids.include?(node.id)) } ]
result = result + node_structure_for_select(child, node)
end
@ -17,7 +15,7 @@ module Admin::NodesHelper
def spacer_node(node)
return if node.root?
tag.div class: 'spacer' do
tag.div class: "spacer" do
node.depth.times do
concat tag.div nil
end
@ -26,29 +24,27 @@ module Admin::NodesHelper
def toggle_node(node)
return tag.div nil, class: 'child' if node.document?
link_to url_for(controller: 'nodes', action: 'children', id: node.id),
class: 'child parent',
return tag.div nil, class: "child" if node.document?
link_to url_for(controller: "nodes", action: "children", id: node.id),
class: "child parent",
data: {
turbo_stream: true,
controller: "nodes",
action: "click->nodes#toggle_children"
} do
concat tag.span('expand_more')
concat tag.span('keyboard_arrow_right')
concat tag.span("expand_more")
concat tag.span("keyboard_arrow_right")
end
end
def tree_title(node)
# result = [link_to(ENV['PROJECT_NAME'],
# result = [link_to(ENV["PROJECT_NAME"],
# url_for(url_base_options.merge(id: nil)),
# class: "list-title-link#{' has-popup-menu' if node.blank?}",
# class: "list-title-link#{" has-popup-menu" if node.blank?}",
# data: {
# icon: 'museum',
# action: node.blank? ? 'click->popup#toggle' : 'click->nodes#set_current',
# icon: "museum",
# action: node.blank? ? "click->popup#toggle" : "click->nodes#set_current",
# turbo_stream: node.blank? ? nil : true,
# })]
result = []
@ -57,31 +53,31 @@ module Admin::NodesHelper
result << tree_node_title_link(n, node)
end
result[-1] = "#{tag.div "#{result[-1]}#{node_popup_menu(node)}".html_safe, data: {controller: 'popup'} }"
result[-1] = "#{tag.div "#{result[-1]}#{node_popup_menu(node)}".html_safe, data: { controller: "popup" } }"
result.join(tag.span('>')).html_safe
result.join(tag.span(">")).html_safe
end
def node_popup_menu(node=nil)
tag.div class: 'popup-menu' do
def node_popup_menu(node = nil)
tag.div class: "popup-menu" do
tag.ul do
concat tag.li(link_to t('ui.edit'), edit_admin_node_path(node), data: {icon: 'edit', turbo_action: 'advance'}) if node
concat tag.li(link_to t("ui.edit"), edit_admin_node_path(node), data: {icon: "edit", turbo_action: "advance"}) if node
Node.categories.each do |node_category|
concat tag.li button_to(t(node_category, scope: 'nodes.new_categories'),
url_for(controller: 'nodes', action: 'create'),
params: {node: {parent_id: node&.id}},
concat tag.li button_to(t(node_category, scope: "nodes.new_categories"),
url_for(controller: "nodes", action: "create"),
params: { node: { parent_id: node&.id } },
data: {
action: 'click->popup#close_open',
icon: t(node_category, scope: 'nodes.icons')
action: "click->popup#close_open",
icon: t(node_category, scope: "nodes.icons")
})
end
# concat tag.li button_to(t('ui.reindex'),
# url_for(controller: 'nodes', action: 'reindex', id: node.id),
# concat tag.li button_to(t("ui.reindex"),
# url_for(controller: "nodes", action: "reindex", id: node.id),
# method: :patch,
# data: {
# action: 'click->popup#close_open',
# icon: 'refresh'
# action: "click->popup#close_open",
# icon: "refresh"
# }) if node
end
end
@ -89,7 +85,6 @@ module Admin::NodesHelper
def node_link_title(node)
# return node.title if !node.document? or node.attachments.blank? or !node.attachments.first.asset.representable?
# asset = node.attachments.first.asset
@ -97,49 +92,47 @@ module Admin::NodesHelper
node_icon = nil
node_title = tag.span(node.title)
(node_icon || '').concat(node_title).html_safe
(node_icon || "").concat(node_title).html_safe
end
def node_flags(node)
return if node.copyright_with_inheritance.blank? and node.tags_with_inheritance.blank?
tag.div class: 'node__flags' do
concat tag.span('copyright') if node.copyright_with_inheritance.present?
concat tag.span('sell') if node.tags_with_inheritance.any?
tag.div class: "node__flags" do
concat tag.span("copyright") if node.copyright_with_inheritance.present?
concat tag.span("sell") if node.tags_with_inheritance.any?
end
end
def drawer_node_link(node)
link_to tag.span(node.title),
url_for(controller: 'nodes', action: node.document? ? 'edit' : 'tree', id: node.id),
url_for(controller: "nodes", action: node.document? ? "edit" : "tree", id: node.id),
title: node.title,
class: 'node-title',
id: dom_id(node, 'drawer-link'),
class: "node-title",
id: dom_id(node, "drawer-link"),
data: {
nodes_id_param: node.id,
turbo_stream: true,
action: 'click->nodes#set_current',
action: "click->nodes#set_current",
icon: t("nodes.icons.#{node.category}")
}
end
def tree_node_title_link(node, current_node=nil)
def tree_node_title_link(node, current_node = nil)
current_node ||= node
url_base_options = {controller: 'nodes', action: 'tree'}
url_base_options = { controller: "nodes", action: "tree" }
link_to(node.title,
url_for(url_base_options.merge(id: node.id)),
class: current_node == node ? 'list-title-link has-popup-menu' : 'list-title-link',
id: dom_id(node, 'list-title-link'),
class: current_node == node ? "list-title-link has-popup-menu" : "list-title-link",
id: dom_id(node, "list-title-link"),
data: {
action: current_node == node ? 'click->popup#toggle' : 'click->nodes#set_current',
action: current_node == node ? "click->popup#toggle" : "click->nodes#set_current",
turbo_stream: current_node != node ? true : nil,
nodes_id_param: node.id,
icon: t("nodes.icons.#{node.category}")
})
end
end

+ 2
- 0
app/helpers/stages_helper.rb View File

@ -0,0 +1,2 @@
module StagesHelper
end

+ 17
- 70
app/models/node.rb View File

@ -1,6 +1,5 @@
class Node < ApplicationRecord
MENUS = %i""
MENUS = %i[]
COOKIE_POLICY = :cookie_policy
SETTINGS = MENUS << COOKIE_POLICY
@ -14,7 +13,7 @@ class Node < ApplicationRecord
translates :slug,
:tags,
locale_accessors: I18n.available_locales,
fallbacks: {zh: :en, hr: :en, cs: :en, da: :en, nl: :en, fi: :en, fr: :en, fr_ca: :en, de: :en, hu: :en, it: :en, ja: :en, ko: :en, nb: :en, pl: :en, pt: :en, ro: :en, sr: :en, sk: :en, sl: :en, es: :en, sv: :en, uk: :en}
fallbacks: { zh: :en, hr: :en, cs: :en, da: :en, nl: :en, fi: :en, fr: :en, fr_ca: :en, de: :en, hu: :en, it: :en, ja: :en, ko: :en, nb: :en, pl: :en, pt: :en, ro: :en, sr: :en, sk: :en, sl: :en, es: :en, sv: :en, uk: :en }
translates :title,
:url,
@ -22,14 +21,12 @@ class Node < ApplicationRecord
:page_title,
:page_description,
locale_accessors: I18n.available_locales,
fallbacks: {zh: :en, hr: :en, cs: :en, da: :en, nl: :en, fi: :en, fr: :en, fr_ca: :en, de: :en, hu: :en, it: :en, ja: :en, ko: :en, nb: :en, pl: :en, pt: :en, ro: :en, sr: :en, sk: :en, sl: :en, es: :en, sv: :en, uk: :en}
fallbacks: { zh: :en, hr: :en, cs: :en, da: :en, nl: :en, fi: :en, fr: :en, fr_ca: :en, de: :en, hu: :en, it: :en, ja: :en, ko: :en, nb: :en, pl: :en, pt: :en, ro: :en, sr: :en, sk: :en, sl: :en, es: :en, sv: :en, uk: :en }
enum :status, { status_published: 0, status_draft: 1, status_archived: 2 }
enum :template, {
tmpl_article: 0,
tmpl_stage: 0,
tmpl_chance: 1,
tmpl_choice: 2,
tmpl_bonus: 3
@ -37,9 +34,9 @@ class Node < ApplicationRecord
include PgSearch::Model
pg_search_scope :pg_search,
against: {title: 'A', url: 'B', page_title: 'A', page_description: 'B', href: 'B', slug: 'B' },
against: { title: "A", url: "B", page_title: "A", page_description: "B", href: "B", slug: "B" },
associated_against: {
attachments: [:body, :url]
attachments: [ :body, :url ]
}
before_validation :remove_empty_tags
@ -51,7 +48,7 @@ class Node < ApplicationRecord
allow_nil: true
scope :ordered, -> { order(position: :asc) }
scope :by_title, ->(rev) { order(Arel.sql(rev ? "title->>'#{I18n.locale.to_s}' DESC, id DESC": "title ->>'#{I18n.locale.to_s}' ASC, id ASC")) }
scope :by_title, ->(rev) { order(Arel.sql(rev ? "title->>'#{I18n.locale}' DESC, id DESC": "title ->>'#{I18n.locale}' ASC, id ASC")) }
scope :by_status, ->(rev) { order(status: rev ? :desc : :asc, id: rev ? :desc : :asc) }
scope :by_slug, ->(rev) { order(ancestry: rev ? :desc : :asc, position: rev ? :desc : :asc, id: rev ? :desc : :asc) }
scope :by_last_modified, ->(rev) { order(updated_at: rev ? :asc : :desc, id: rev ? :desc : :asc) }
@ -62,10 +59,10 @@ class Node < ApplicationRecord
where("(cardinality(excluded_locales) = 0 AND is_allowlist = false) OR
(is_allowlist = true AND ? = ANY(excluded_locales)) OR
(is_allowlist = false AND NOT(? = ANY(excluded_locales)))",
I18n.locale.to_s, I18n.locale.to_s )
I18n.locale.to_s, I18n.locale.to_s)
}
scope :viewable, -> { status_published.for_current_locale.where('published_at <= ? AND (expires_at IS NULL OR expires_at > ?)', Time.current, Time.current) }
scope :viewable, -> { status_published.for_current_locale.where("published_at <= ? AND (expires_at IS NULL OR expires_at > ?)", Time.current, Time.current) }
scope :of_template, ->(tmpl) { where(template: Node.templates[tmpl.to_s]) }
scope :with_setting, ->(setting) { setting.kind_of?(Array) ? where("settings && ?", "{#{setting.join(',')}}") : where("settings @> ?", "{#{setting}}") }
@ -77,25 +74,25 @@ class Node < ApplicationRecord
def self.tags
result = {}
I18n.available_locales.each do |locale|
key = locale.to_s.downcase.sub('-','_')
result[key] = pluck(Arel.sql("tags->'#{locale}'")).flatten.compact.uniq.sort.reject{|v| v.blank?}
key = locale.to_s.downcase.sub("-", "_")
result[key] = pluck(Arel.sql("tags->'#{locale}'")).flatten.compact.uniq.sort.reject { |v| v.blank? }
end
result
end
def self.categories
['document']
[ "document" ]
end
def category
self.children.any? ? 'folder' : 'document'
self.children.any? ? "folder" : "document"
end
def document?
self.category == 'document'
self.category == "document"
end
@ -126,66 +123,16 @@ class Node < ApplicationRecord
def node_type
return "site" if self.site?
'tile'
"tile"
end
def answer_percentages
# Get all valid answers (non-nil values)
valid_answers = answers.where.not(value: nil)
# Count answers by value
value_counts = valid_answers.group(:value).count
# Calculate total number of answers
total_answers = valid_answers.count
# Initialize with 0 for both expected values
percentages = {0 => 0, 1 => 0}
# Return initial percentages if no answers
return percentages if total_answers.zero?
# Calculate exact percentages first (not rounded)
exact_percentages = {}
value_counts.each do |value, count|
# Only consider values 0 and 1
next unless [0, 1].include?(value)
exact_percentages[value] = count.to_f / total_answers * 100
end
# First round down all percentages
[0, 1].each do |value|
percentages[value] = (exact_percentages[value] || 0).floor
end
# Calculate how much we're off by due to rounding
remaining = 100 - percentages.values.sum
# Distribute the remaining percentage points to values with the largest fractional parts
if remaining > 0
# Filter exact percentages to only include 0 and 1
sorted_by_fraction = exact_percentages
.select { |k, _| [0, 1].include?(k) }
.sort_by { |_, p| p - p.floor }
.reverse
# Add 1 to the values with the largest fractional parts
sorted_by_fraction.take(remaining).each do |value, _|
percentages[value] += 1
end
end
# Return the percentages hash with keys 0 and 1
percentages
end
private
def remove_empty_tags
%w"settings excluded_locales tags_en tags_zh tags_hr tags_cs tags_da tags_nl tags_fi tags_fr tags_fr_ca tags_de tags_hu tags_it tags_ja tags_ko tags_nb tags_pl tags_pt tags_ro tags_sr tags_sk tags_sl tags_es tags_sv tags_uk ".map do |k|
%w[settings excluded_locales tags_en tags_zh tags_hr tags_cs tags_da tags_nl tags_fi tags_fr tags_fr_ca tags_de tags_hu tags_it tags_ja tags_ko tags_nb tags_pl tags_pt tags_ro tags_sr tags_sk tags_sl tags_es tags_sv tags_uk].map do |k|
self.send "#{k}=", self.send(k).reject { |v| v.blank? } unless self.send(k).blank?
end
end
end

+ 26
- 0
app/models/player.rb View File

@ -0,0 +1,26 @@
class Player < ApplicationRecord
attribute :progress, :json, default: {}
attribute :current_stage, :integer, default: 1
attribute :score, :integer, default: 0
# stage_index: 1, 2, 3...
# outcome: 'chance' or 'choice'
def record_flip(stage_index, outcome)
self.progress[stage_index.to_s] ||= {}
self.progress[stage_index.to_s]["flip"] = outcome
save
end
# stage_index: 1, 2, 3...
# option_index: 1, 2, or 3
def record_option(stage_index, option_index)
self.progress[stage_index.to_s] ||= {}
self.progress[stage_index.to_s]["option"] = option_index
save
end
def advance_to_next_stage!
self.current_stage += 1
save
end
end

+ 0
- 20
app/views/languages/_intro.html.erb View File

@ -1,20 +0,0 @@
<% @node.attachments.limit(2).each_with_index do |attachment, i| %>
<div class="animation-element">
<% if i == 0 %>
<div class="intro-content-header">
<%= attachment.body %>
</div>
<% else %>
<div class="intro-content-body">
<%= attachment.body.html_safe %>
</div>
<% end %>
</div>
<% end %>
<div class="animation-element">
<%= link_to tag.span(t('get_started')),
url_for(controller: 'players', action: 'new', locale: I18n.locale),
class: 'button__base' %>
</div>

+ 12
- 1
app/views/languages/index.html.erb View File

@ -1,3 +1,14 @@
<%- content_for :title, t('project_name') %>
<h1>Intro</h1>
<article>
<% @node.attachments.each do |attachment| %>
<%= attachment.body.html_safe %>
<% end %>
<div class="action-container">
<%= button_to t("let_me_try"),
start_path(locale: I18n.locale),
method: :post %>
</div>
</article>

+ 1
- 1
app/views/layouts/application.html.erb View File

@ -16,7 +16,7 @@
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" sizes="16x16 32x32">
<link rel="icon" sizes="192x192" href="/ikea-favicon-300x300.png">
<%= stylesheet_link_tag "application" %>
<%= stylesheet_link_tag "reset", "application" %>
</head>


+ 0
- 11
app/views/site/tmpl_article.html.erb View File

@ -1,11 +0,0 @@
<%-
attachment = @node.attachments.limit(1)[0]
article_parts = parse_article_html(attachment&.body)
content_for :title, @node.page_title.blank? ? article_parts[:title] : @node.page_title
content_for :meta_description, @node.page_description.blank? ? article_parts[:description] : @node.page_description
%>
<%= session[:player_id] %>

app/views/site/tmpl_list.html.erb → app/views/site/tmpl_chance.html.erb View File


app/views/site/tmpl_index.html.erb → app/views/site/tmpl_choise.html.erb View File


+ 19
- 0
app/views/stages/show.html.erb View File

@ -0,0 +1,19 @@
<%-
content_for :title, @node.page_title.blank? ? @node.title : @node.page_title
content_for :meta_description, @node.page_description
%>
<article>
<% @node.attachments.each do |attachment| %>
<%= attachment.body.html_safe %>
<% end %>
<div class="action-container">
<%= button_to t("flip"), url_for(action: 'flip'), method: :post %>
</div>
<%= current_player.inspect %>
</article>

+ 5
- 18
config/locales/en.yml View File

@ -1,23 +1,10 @@
en:
domain: completesentences.ikeafoundation.org
project_name: IKEA Foundation Week 2025
project_name: IKEA Foundation Week 2026
client_name: IKEA Foundation
get_started: Let’s go!
what_is_your_name: What’s your name?
write_your_name: Write your name.
submit: Submit
next_question: Next question
of_people_worldwide_think_just_lik_you: of people worldwide think like you.
see_results: See results
results: Results
read_more: Read more
share_on_story: Share
read_more_link: https://ikeafoundation.org/25
please_type_your_name_to_continue: Please type your name to continue
share_title: IKEA Foundation Week quiz result
share_text: Quiz result description
let_me_try: Let me try!
languages:
en: English
@ -210,8 +197,8 @@ en:
en: English
de: German
templates:
tmpl_article: Side
tmpl_chance: Chance
tmpl_stage: Stage
tmpl_chance: Chance
tmpl_choice: Choice
tmpl_bonus: Bonus chance
categories:


+ 16
- 25
config/routes.rb View File

@ -5,11 +5,10 @@ Rails.application.routes.draw do
# Can be used by load balancers and uptime monitors to verify that the app is live.
# get "up" => "rails/health#show", as: :rails_health_check
get 'admin', to: redirect('/admin/en')
get "admin", to: redirect("/admin/en")
namespace :admin do
scope ':locale', constraints: { locale: /en|zh|hr|cs|da|nl|fi|fr|fr-CA|de|hu|it|ja|ko|nb|pl|pt|ro|sr|sk|sl|es|sv|uk/ } do
scope ":locale", constraints: { locale: /en|zh|hr|cs|da|nl|fi|fr|fr-CA|de|hu|it|ja|ko|nb|pl|pt|ro|sr|sk|sl|es|sv|uk/ } do
# Assets
resources :assets do
collection do
@ -36,40 +35,32 @@ Rails.application.routes.draw do
resources :users
# Root
root to: 'nodes#index'
root to: "nodes#index"
end
# Cache
delete 'cache/clear', to: "admin#clear_cache"
delete "cache/clear", to: "admin#clear_cache"
resources :sessions, path: 'login', only: [:index, :create, :destroy]
get "logout", to: 'sessions#destroy', as: "logout"
resources :sessions, path: "login", only: [ :index, :create, :destroy ]
get "logout", to: "sessions#destroy", as: "logout"
# 2 step auth
post 'login/verify', to: 'sessions#verify'
get 'login/verification', to: 'sessions#verification'
post "login/verify", to: "sessions#verify"
get "login/verification", to: "sessions#verification"
end
scope ':locale', constraints: { locale: /en|zh|hr|cs|da|nl|fi|fr|fr-CA|de|hu|it|ja|ko|nb|pl|pt|ro|sr|sk|sl|es|sv|uk/ } do
get 'player', to: 'players#new'
post 'player', to: 'players#create'
scope ":locale", constraints: { locale: /en|zh|hr|cs|da|nl|fi|fr|fr-CA|de|hu|it|ja|ko|nb|pl|pt|ro|sr|sk|sl|es|sv|uk/ } do
post "start", to: "players#create", as: "start"
post 'q/:id/answer', to: 'answers#create'
patch 'q/:id/answer', to: 'answers#update'
get 'q/:id/answer', to: 'questions#answer'
get 'q/:id', to: 'questions#show'
get "stages/:id", to: "stages#show", constraints: { id: /\d+/ }, as: :stage
post "stages/:id/flip", to: "stages#flip", constraints: { id: /\d+/ }, as: :flip_stage
post "stages/:id/choose", to: "stages#choose", constraints: { id: /\d+/ }, as: :choose_stage
post 'score', to: 'questions#score'
get 'result/:id', to: 'questions#shared_result'
get 'result', to: 'questions#result'
get '', to: 'languages#show'
# get '*url', to: 'site#page', constraints: lambda { |req| req.path.exclude?('storage') }
get "", to: "languages#show"
# get "*url", to: "site#page", constraints: lambda { |req| req.path.exclude?("storage") }
end
put 'update_locale', to: 'languages#update'
put "update_locale", to: "languages#update"
# Defines the root path route ("/")
root "languages#index"


+ 2
- 4
db/migrate/20240424093536_create_attachments.rb View File

@ -1,7 +1,6 @@
class CreateAttachments < ActiveRecord::Migration[7.1]
def change
create_table :attachments do |t|
t.references 'asset'
t.integer "attachable_for_id"
@ -14,9 +13,8 @@ class CreateAttachments < ActiveRecord::Migration[7.1]
t.integer "position"
t.timestamps
t.index ["attachable_for_id", "attachable_for_type"], name: "attachable_for"
t.index ["is_favorite"], name: "index_attachments_on_is_favorite"
t.index [ "attachable_for_id", "attachable_for_type" ], name: "attachable_for"
t.index [ "is_favorite" ], name: "index_attachments_on_is_favorite"
end
end
end

+ 10
- 0
db/migrate/20250527114853_create_players.rb View File

@ -0,0 +1,10 @@
class CreatePlayers < ActiveRecord::Migration[8.1]
def change
create_table :players do |t|
t.text :locale
t.integer :current_stage, index: true
t.jsonb :progress, default: {}
t.timestamps
end
end
end

+ 9
- 0
db/migrate/20260323110134_add_missing_columns_to_players.rb View File

@ -0,0 +1,9 @@
class AddMissingColumnsToPlayers < ActiveRecord::Migration[8.1]
def change
add_column :players, :name, :text
add_column :players, :score, :integer
add_column :players, :answer_cache, :integer, array: true, default: []
add_index :players, :answer_cache, using: :gin
add_index :players, :score
end
end

+ 4
- 1
db/schema.rb View File

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.1].define(version: 2025_05_27_114853) do
ActiveRecord::Schema[8.1].define(version: 2026_03_23_110134) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@ -117,11 +117,14 @@ ActiveRecord::Schema[8.1].define(version: 2025_05_27_114853) do
create_table "players", force: :cascade do |t|
t.integer "answer_cache", default: [], array: true
t.datetime "created_at", null: false
t.integer "current_stage"
t.text "locale"
t.text "name"
t.jsonb "progress", default: {}
t.integer "score"
t.datetime "updated_at", null: false
t.index ["answer_cache"], name: "index_players_on_answer_cache", using: :gin
t.index ["current_stage"], name: "index_players_on_current_stage"
t.index ["score"], name: "index_players_on_score"
end


+ 7
- 0
test/controllers/stages_controller_test.rb View File

@ -0,0 +1,7 @@
require "test_helper"
class StagesControllerTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end

Loading…
Cancel
Save