diff --git a/Gemfile.lock b/Gemfile.lock index adad63e..6d2c4de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -281,7 +281,7 @@ GEM zeitwerk (~> 2.6) rainbow (3.1.1) rake (13.4.2) - rbs (4.1.3) + rbs (4.2.0) logger prism (>= 1.6.0) tsort @@ -299,8 +299,8 @@ GEM io-console (~> 0.5) request_store (1.7.0) rack (>= 1.4) - rubocop (1.89.0) - json (~> 2.3) + rubocop (1.90.0) + json (>= 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) parallel (>= 1.10) @@ -342,11 +342,11 @@ GEM stimulus-rails (1.3.4) railties (>= 6.0.0) thor (1.5.0) - thruster (0.1.25) - thruster (0.1.25-aarch64-linux) - thruster (0.1.25-arm64-darwin) - thruster (0.1.25-x86_64-darwin) - thruster (0.1.25-x86_64-linux) + thruster (0.1.26) + thruster (0.1.26-aarch64-linux) + thruster (0.1.26-arm64-darwin) + thruster (0.1.26-x86_64-darwin) + thruster (0.1.26-x86_64-linux) timeout (0.6.1) tsort (0.2.0) turbo-rails (2.0.23) @@ -525,14 +525,14 @@ CHECKSUMS railties (8.1.3.1) sha256=2388a232579a00cefea4487de66c8553c3408c1300abdc6cf1799d86ffb04487 rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 - rbs (4.1.3) sha256=0c4474a9751cdc14364bfad0b3e53678323bbdc2c31683b0445932867dbab8c4 + rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674 rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469 redis (6.0.0) sha256=de71c10edd106986b759ec7ecdd08b63b9c0ee7414a0d0c1da73d31ba2bccda6 redis-client (0.30.1) sha256=5151bc5c7bbfe48623732cdae3b900d8a22dc691cc7cdfacfb351ac55116522d regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d request_store (1.7.0) sha256=e1b75d5346a315f452242a68c937ef8e48b215b9453a77a6c0acdca2934c88cb - rubocop (1.89.0) sha256=4dee8e3ee9c45e474834efd9e8d6fd031e8331c8dacdff0de4ad65ae0a6faae7 + rubocop (1.90.0) sha256=9eb4c065b5c5154e4ef554c547972f3905a9eb6b53e657e580b6796b54bf8242 rubocop-ast (1.50.0) sha256=b9ca88300da0803ee222ad20cdb30494c0a784eed06fdc35d254b06d662788db rubocop-performance (1.27.0) sha256=eeeb1374d062a368ee1c787b70eb0b0cc4b184cb1f8565f424760946146d61ce rubocop-rails (2.37.0) sha256=6e1645add5060e0328f8ddda0d820f55697c591394398bf14bb9dccb62f14b7e @@ -544,11 +544,11 @@ CHECKSUMS ssrf_filter (1.5.0) sha256=e03dcdb9d1730d7f6710532a606b3543df2a448a0293ce04a2d995523c5a97f6 stimulus-rails (1.3.4) sha256=765676ffa1f33af64ce026d26b48e8ffb2e0b94e0f50e9119e11d6107d67cb06 thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 - thruster (0.1.25) sha256=d97d704e91fa37d5858bc238dea6186f7310b633abfeea9f2ba814f252203716 - thruster (0.1.25-aarch64-linux) sha256=495265b11fc3c3596ae0d70ed902b60db82dafb2a5b47a2c527d3176ee3cf311 - thruster (0.1.25-arm64-darwin) sha256=20338360cce4035e787135ae154a10663f3d3d61b15bfa72df347fe9d982de57 - thruster (0.1.25-x86_64-darwin) sha256=633d337baee05e8eacbc18e3986ea6bbf8ea237abd67f5e4351b8aa710623c5e - thruster (0.1.25-x86_64-linux) sha256=e5586003b34dc2f4200f8412101e906621e276bb294d41d1b63a3c41e348503d + thruster (0.1.26) sha256=6e45e807086b29d51404841bd1ad493b67cd95892fd65dc5afcdd32e82e94ce8 + thruster (0.1.26-aarch64-linux) sha256=2171cb34928c0250830008f535c4ab2ee57846cc3f5d3e96c3475f7b3de7a541 + thruster (0.1.26-arm64-darwin) sha256=40676164c433abf31313422305d9e9e9210cf941b1befad88e2428b3b8dcc36c + thruster (0.1.26-x86_64-darwin) sha256=c7f7f0cbefd8030ea03bf88f6a988f595a0618a35a1a0d358e729c6898eb5b6a + thruster (0.1.26-x86_64-linux) sha256=3117a6ee430663f845a0457699fe9a05232dcc6c396e2cc83504de5a223c60e8 timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f turbo-rails (2.0.23) sha256=ee0d90733aafff056cf51ff11e803d65e43cae258cc55f6492020ec1f9f9315f diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index a9022d0..987ab9b 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -109,6 +109,12 @@ h2@property --glow-deg { background-color: #0058a3; } +/* The whole game lives in a single turbo-frame so the URL never changes. + `contents` keeps the frame out of the layout:
stays a flex child of . */ +turbo-frame#game { + display: contents; +} + body { margin: 0; display: flex; diff --git a/app/controllers/game_controller.rb b/app/controllers/game_controller.rb index a430a02..2c41656 100644 --- a/app/controllers/game_controller.rb +++ b/app/controllers/game_controller.rb @@ -2,6 +2,12 @@ class GameController < ApplicationController include QuizHelperMethods skip_before_action :require_player!, only: [ :index, :start ] + # The whole game lives in one turbo-frame declared in the application layout, + # so the URL never changes. Frame requests must render that layout too: + # turbo-rails otherwise swaps in its bare "turbo_rails/frame" layout, and the + # response comes back without the Turbo is looking for. + layout "application" + before_action :set_node, only: [ :stage, :answer, :stage_result ] helper_method :root_node, diff --git a/app/helpers/game_helper.rb b/app/helpers/game_helper.rb index ce516c4..ee29acd 100644 --- a/app/helpers/game_helper.rb +++ b/app/helpers/game_helper.rb @@ -19,6 +19,15 @@ module GameHelper end + # Answers are shuffled so the good one isn't always in the same slot, but a + # "chance" answer is pinned to the bottom — it reads as the wildcard option + # and the stage-1 confirm dialog is anchored to it. + def shuffled_answers(node) + chance, rest = node.children.ordered.partition(&:chance?) + rest.shuffle + chance + end + + def render_responsive_picture(asset = nil, alt: "", lazy: false, fetchpriority: nil) return unless asset diff --git a/app/javascript/application.js b/app/javascript/application.js index e9674f0..66f6764 100644 --- a/app/javascript/application.js +++ b/app/javascript/application.js @@ -17,6 +17,13 @@ application.register("chance", ChanceController) application.register("share", ShareController) application.register("modal", ModalController) +// The whole game runs inside one turbo-frame so the URL never changes. Frame +// navigations don't reset scroll the way a full page visit does, so each new +// screen would otherwise open at the previous screen's scroll offset. +document.addEventListener("turbo:frame-render", (event) => { + if (event.target.id === "game") window.scrollTo(0, 0) +}) + // Configure Stimulus development experience application.debug = false window.Stimulus = application diff --git a/app/javascript/chance_controller.js b/app/javascript/chance_controller.js index 8bccc3e..6081a0c 100644 --- a/app/javascript/chance_controller.js +++ b/app/javascript/chance_controller.js @@ -4,6 +4,9 @@ export default class extends Controller { static targets = ["dialog"] confirm(event) { + // The second pass — after the player confirmed — falls through to Turbo. + if (this.confirmed) return + event.preventDefault() this.pendingForm = event.target this.dialogTarget.showModal() @@ -11,7 +14,10 @@ export default class extends Controller { ok() { this.dialogTarget.close() - this.pendingForm?.submit() + // requestSubmit(), not submit(): the native call skips the submit event, so + // Turbo would miss it and navigate the whole page out of the game frame. + this.confirmed = true + this.pendingForm?.requestSubmit() } cancel() { @@ -19,8 +25,7 @@ export default class extends Controller { this.pendingForm = null } - backdropClick(event) { - console.info(event.target === this.dialogTarget) + backdropClick(event) { if (event.target === this.dialogTarget) this.cancel() } } diff --git a/app/views/game/_facts_content.html.erb b/app/views/game/_facts_content.html.erb index 252849e..6b84916 100644 --- a/app/views/game/_facts_content.html.erb +++ b/app/views/game/_facts_content.html.erb @@ -27,7 +27,6 @@
- <%= link_to tag.span(t("game.got_it_lets_get_started")), {action: "intro"}, class: "cta", - data: { turbo_frame: "_top" } %> + <%= link_to tag.span(t("game.got_it_lets_get_started")), {action: "intro"}, class: "cta" %>
diff --git a/app/views/game/_language-menu.html.erb b/app/views/game/_language-menu.html.erb index 36ad280..61094c1 100644 --- a/app/views/game/_language-menu.html.erb +++ b/app/views/game/_language-menu.html.erb @@ -33,7 +33,8 @@
  • <%= link_to v, url_for(locale: k), class: ("current" if current), - "aria-current": ("page" if current) %>
  • + "aria-current": ("page" if current), + data: { turbo_frame: "_top" } %> <% end%> diff --git a/app/views/game/last_save.html.erb b/app/views/game/last_save.html.erb index 78bfd65..e625de1 100644 --- a/app/views/game/last_save.html.erb +++ b/app/views/game/last_save.html.erb @@ -20,7 +20,7 @@
    - <% @node.children.ordered.each do |node| %> + <% shuffled_answers(@node).each do |node| %> <%= button_to node.title, url_for(action: "answer_last_save", value: node.id) %> <% end %> diff --git a/app/views/game/stage.html.erb b/app/views/game/stage.html.erb index f005799..1a9b064 100644 --- a/app/views/game/stage.html.erb +++ b/app/views/game/stage.html.erb @@ -17,7 +17,7 @@
    - <% @node.children.ordered.each do |node| %> + <% shuffled_answers(@node).each do |node| %> <%= button_to node.title, url_for(action: "answer", value: node.id), class: ("chance" if node.chance?), form: ((stage_index == 1 and node.chance?) ? { data: { action: "submit->chance#confirm" } } : {}) %> diff --git a/app/views/game/start.turbo_stream.erb b/app/views/game/start.turbo_stream.erb index 0dec844..1411237 100644 --- a/app/views/game/start.turbo_stream.erb +++ b/app/views/game/start.turbo_stream.erb @@ -1,4 +1,4 @@ -<%= turbo_stream.append "page" do %> +<%= turbo_stream.append "game" do %> - <%= content_for :header %> + <%= turbo_frame_tag "game" do %> + <%= content_for :header %> - <% if respond_to?(:current_player) && current_player %> - - <% end %> + <% if respond_to?(:current_player) && current_player %> + + <% end %> - <%= tag.main class: [@result_node, @node].compact.map { |n| n.template }.first, - data: { - controller: content_for(:main_controller) - } do %> - <%= yield %> + <%= tag.main class: [@result_node, @node].compact.map { |n| n.template }.first, + data: { + controller: content_for(:main_controller) + } do %> + <%= yield %> + <% end %> <% end %>