diff --git a/Gemfile.lock b/Gemfile.lock index 3016c7e..c3d334b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -152,7 +152,7 @@ GEM kaminari-core (= 1.2.2) kaminari-core (1.2.2) language_server-protocol (3.17.0.5) - lexxy (0.9.15.alpha.4) + lexxy (0.9.17) rails (>= 8.0.2) lint_roller (1.1.0) logger (1.7.0) @@ -175,7 +175,7 @@ GEM mobility (1.3.2) i18n (>= 0.6.10, < 2) request_store (~> 1.0) - msgpack (1.8.1) + msgpack (1.8.2) net-imap (0.6.4) date net-protocol @@ -311,7 +311,7 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.47.1, < 2.0) - rubocop-rails (2.35.3) + rubocop-rails (2.35.4) activesupport (>= 4.2.0) lint_roller (~> 1.1) rack (>= 1.1) @@ -358,7 +358,7 @@ GEM actionview (>= 8.0.0) bindex (>= 0.4.0) railties (>= 8.0.0) - websocket-driver (0.8.0) + websocket-driver (0.8.1) base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) @@ -462,7 +462,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.9.15.alpha.4) sha256=7a0ee226537eca2e17a5466073dd4f6215a00339fcde78e54425e8150b55c125 + lexxy (0.9.17) sha256=1a265ec2ecdb0cce621900b0886a44890f195c84e4c6b187c9812a25a60d16cf lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 loofah (2.25.1) sha256=d436c73dbd0c1147b16c4a41db097942d217303e1f7728704b37e4df9f6d2e04 @@ -472,7 +472,7 @@ CHECKSUMS mini_mime (1.1.5) sha256=8681b7e2e4215f2a159f9400b5816d85e9d8c6c6b491e96a12797e798f8bccef minitest (6.0.6) sha256=153ea36d1d987a62942382b61075745042a2b3123b1cd48f4c3675af9cc7d6f1 mobility (1.3.2) sha256=32fbbb0e53118ef42de20daa6ac94dbb758c628874092eba311b968a1e1d757b - msgpack (1.8.1) sha256=3fef787cd3965fd119c08a22724a56a93ca25008c3421fc15039f603a8b7c86c + msgpack (1.8.2) sha256=e440d11c99d6dfe8b2fbc4feb74c3518c1ba024357c70bbd734d9bb1a44d0d25 net-imap (0.6.4) sha256=9a5598c67a3022c284d98430ef1d4948e7dbdb62596f61081ea8ca933270a02b net-pop (0.1.2) sha256=848b4e982013c15b2f0382792268763b748cce91c9e91e36b0f27ed26420dff3 net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8 @@ -526,7 +526,7 @@ CHECKSUMS rubocop (1.87.0) sha256=b9d9ddf55116a513f8ef2c7ae660662d8b49301f118d3f0df61865b33a5c188d rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 rubocop-performance (1.26.1) sha256=cd19b936ff196df85829d264b522fd4f98b6c89ad271fa52744a8c11b8f71834 - rubocop-rails (2.35.3) sha256=6edd45410866912b9b2e90ae3aeafd31d576df2bb2a9c9408f1667a50c32c7de + rubocop-rails (2.35.4) sha256=3aeaa325439c89950e8327565682ea794065d08e2ecbbfe95032bfa295a35df5 rubocop-rails-omakase (1.1.0) sha256=2af73ac8ee5852de2919abbd2618af9c15c19b512c4cfc1f9a5d3b6ef009109d ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 ruby-vips (2.3.0) sha256=e685ec02c13969912debbd98019e50492e12989282da5f37d05f5471442f5374 @@ -550,7 +550,7 @@ CHECKSUMS uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6 useragent (0.16.11) sha256=700e6413ad4bb954bb63547fa098dddf7b0ebe75b40cc6f93b8d54255b173844 web-console (4.3.0) sha256=e13b71301cdfc2093f155b5aa3a622db80b4672d1f2f713119cc7ec7ac6a6da4 - websocket-driver (0.8.0) sha256=ed0dba4b943c22f17f9a734817e808bc84cdce6a7e22045f5315aa57676d4962 + websocket-driver (0.8.1) sha256=5ab238238ce230e5d4b262d2be39624c867914eab99171dc4952b58b577c2d96 websocket-extensions (0.1.5) sha256=1c6ba63092cda343eb53fc657110c71c754c56484aad42578495227d717a8241 zeitwerk (2.8.2) sha256=7212a61311083c604184b1ea2574b9aa05cd14f855a0841c06985cabe9181d12 diff --git a/app/assets/images/ico-coins.svg b/app/assets/images/ico-coins.svg new file mode 100644 index 0000000..3cdc85b --- /dev/null +++ b/app/assets/images/ico-coins.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/ico-fuel.svg b/app/assets/images/ico-fuel.svg new file mode 100644 index 0000000..56f07e2 --- /dev/null +++ b/app/assets/images/ico-fuel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/ico-thumb.svg b/app/assets/images/ico-thumb.svg new file mode 100644 index 0000000..a0831bd --- /dev/null +++ b/app/assets/images/ico-thumb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 6c4801b..325a674 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -123,6 +123,10 @@ body { touch-action: manipulation; background-color: var(--clr-white); + + &:has(.results) { + background-color: var(--clr-sand-light); + } } .debug-score { @@ -132,6 +136,7 @@ body { top: 0; padding: 1rem; background-color: var(--clr-white); + z-index: 100; & dl { display: grid; @@ -307,6 +312,12 @@ main { } } +.results { + --clr-action: var(--clr-green); + margin-block-end: 1rem; +} + + .good_answer { & .hero-container { background-color: var(--clr-green); @@ -659,4 +670,178 @@ dialog::backdrop { text-align: center; flex: 1; } -} \ No newline at end of file +} + +.results-header { + background-color: var(--clr-green); + padding: 2rem 1rem 3rem 1rem; + + strong { + display: block; + } +} + +.results-tagline { + display: grid; + grid-template-columns: 1fr max-content; + + & svg { + fill: var(--clr-green); + rotate: 180deg; + } + + &>div:first-child { + align-self: center; + font: var(--td-base); + padding: 1rem 0 1rem 1rem; + } +} + +.impact { + display: flex; + flex-direction: column; + margin: 1.5rem 1rem; + font: var(--td-base); + + & + .impact { + margin-block-start: 0; + } + + & h3 { + font: var(--td-base); + font-weight: 700; + margin: 0; + } + + & .icon { + width: 40px; + aspect-ratio: 1; + display: inline-flex; + justify-content: center; + align-items: center; + background-color: var(--clr-green); + border-radius: 50%; + margin-block-end: 0.25rem; + } + + & details { + border-width: 1px 0; + border-color: var(--clr-black); + border-style: solid; + margin: 0.5rem 0; + padding: 0.5rem 0; + + & summary { + list-style: none; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5rem; + } + + h4 { + font: var(--td-base); + font-weight: 700; + margin: 0; + } + + & p { + margin: 0.5rem 0; + } + + & summary::-webkit-details-marker { display: none; } + + & .details-icon { + width: 23px; + aspect-ratio: 1; + border-radius: 50%; + border: 1px solid var(--clr-black); + box-sizing: border-box; + position: relative; + &::before { + content: ""; + height: 13px; + width: 1px; + left: 10px; + top: 4px; + position: absolute; + background-color: var(--clr-black); + } + &::after { + content: ""; + width: 13px; + height: 1px; + left: 4px; + top: 10px; + position: absolute; + background-color: var(--clr-black); + } + } + + &[open] .details-icon::before { + display: none; + } + } + +} + +.like-container { + margin: 0 1rem 1.5rem 1rem; + + & h3 { + font: var(--td-base); + font-weight: 700; + margin: 0 0 0.5rem 0; + } + + & > div { + display: flex; + gap: 0.5rem; + } + + button { + appearance: none; + border-radius: 50%; + width: 40px; + aspect-ratio: 1; + border: 1px solid var(--clr-black); + background-color: transparent; + display: flex; + justify-content: center; + align-items: center; + + &.thumbs-down { + & svg { + rotate: 180deg; + } + } + } +} + +.newsletter-container { + margin: 0 1rem 2rem 1rem; + + & p { + margin: 0 0 1rem 0; + } + + a { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--clr-black); + text-decoration: none; + + & span:has(svg) { + background-color: var(--clr-green); + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 50%; + } + } +} + diff --git a/app/controllers/game_controller.rb b/app/controllers/game_controller.rb index 0dd68c9..e373c11 100644 --- a/app/controllers/game_controller.rb +++ b/app/controllers/game_controller.rb @@ -65,7 +65,6 @@ class GameController < ApplicationController def last_save @node = last_save_node - end @@ -83,6 +82,8 @@ class GameController < ApplicationController def done + current_player.update(is_done: true) unless current_player.is_done? + if current_player.last_save_answer_id @node = last_save_node @result_node = @node.descendants.find_by(id: current_player.last_save_answer_id) @@ -93,7 +94,8 @@ class GameController < ApplicationController end - def result + def results + @node = root_node.children.results.first end @@ -110,7 +112,7 @@ private def stages - @stages ||= root_node.children.stage + @stages ||= root_node.children.ordered.stage end diff --git a/app/helpers/game_helper.rb b/app/helpers/game_helper.rb index f602bc2..2cc2c87 100644 --- a/app/helpers/game_helper.rb +++ b/app/helpers/game_helper.rb @@ -42,4 +42,67 @@ module GameHelper "#{rails_storage_proxy_path(asset.file.variant(resize_to_limit: [ w, nil ], format: "webp"))} #{w}w" }.join(", ") end + + + RESULT_BANDS = [ [ 8, :best ], [ 5, :balanced ], [ 0, :worst ] ].freeze + + def result_state(player) + if (answer_id = player.last_save_answer_id) + return last_save_result_state(answer_id) + end + + overall = player.score.to_i + RESULT_BANDS.find { |threshold, _| overall >= threshold }.last + end + + + def result_headline(player) + t("game.results.#{result_state(player)}.headline").html_safe + end + + + def result_description(player) + t("game.results.#{result_state(player)}.description").html_safe + end + + + IMPACT_TONE_BANDS = { + default: [ [ 3, :positive ], [ 1, :neutral ], [ -Float::INFINITY, :negative ] ], + income: [ [ 2, :positive ], [ 0, :neutral ], [ -Float::INFINITY, :negative ] ] + }.freeze + + def impact_tone(player, category) + score = player.score_for(category) + bands = IMPACT_TONE_BANDS[category] || IMPACT_TONE_BANDS[:default] + bands.find { |threshold, _| score >= threshold }.last + end + + + def impact_label(category) + t("game.results.#{category}.label") + end + + + def impact_message(player, category) + if category == :income && player.last_save_answer_id.present? + return t("game.results.income.early_exit") + end + + t("game.results.#{category}.#{impact_tone(player, category)}") + end + + + def impact_learn_more(category) + t("game.results.#{category}.learn_more") + end + + + private + + def last_save_result_state(answer_id) + answer = Node.find_by(id: answer_id) + config = JSON.parse(Rails.root.join("config", "question_scores.json").read) + type = config.dig("last_save", "answers", answer.position - 1, "result_type") if answer + type&.to_sym || :compost + end end diff --git a/app/models/node.rb b/app/models/node.rb index 2f55b83..ffbae05 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -31,7 +31,8 @@ class Node < ApplicationRecord good_answer: 4, bad_answer: 5, chance: 6, - last_save: 7 + last_save: 7, + results: 8 } def available_templates @@ -39,7 +40,7 @@ class Node < ApplicationRecord case depth when 1 - [ :facts, :intro, :stage, :last_save ] + [ :facts, :intro, :stage, :last_save, :results ] when 2 [ :good_answer, :bad_answer, :chance ] when 3 diff --git a/app/views/game/done.html.erb b/app/views/game/done.html.erb index 0010d3e..a25ae31 100644 --- a/app/views/game/done.html.erb +++ b/app/views/game/done.html.erb @@ -16,7 +16,7 @@
<%= impact_message(current_player, :food_waste) %>
+ +<%= paragraph %>
+ <% end %> +<%= impact_message(current_player, :emissions) %>
+ +<%= paragraph %>
+ <% end %> +<%= impact_message(current_player, :income) %>
+ +<%= paragraph %>
+ <% end %> +