From 46d24ac62858fc62555189b37e182dd377bfb73d Mon Sep 17 00:00:00 2001 From: Mattias Bodlund Date: Tue, 9 Jun 2026 15:23:21 +0200 Subject: [PATCH] na --- Gemfile.lock | 16 +-- app/assets/images/ico-coins.svg | 1 + app/assets/images/ico-fuel.svg | 1 + app/assets/images/ico-thumb.svg | 1 + app/assets/stylesheets/application.css | 187 ++++++++++++++++++++++++- app/controllers/game_controller.rb | 8 +- app/helpers/game_helper.rb | 63 +++++++++ app/models/node.rb | 5 +- app/views/game/done.html.erb | 2 +- app/views/game/result.html.erb | 1 - app/views/game/results.html.erb | 84 +++++++++++ config/locales/en.yml | 55 +++++++- config/routes.rb | 2 +- 13 files changed, 407 insertions(+), 19 deletions(-) create mode 100644 app/assets/images/ico-coins.svg create mode 100644 app/assets/images/ico-fuel.svg create mode 100644 app/assets/images/ico-thumb.svg delete mode 100644 app/views/game/result.html.erb create mode 100644 app/views/game/results.html.erb 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 @@
<%= link_to tag.span(t("game.drumroll_see_the_result")), - { action: "result" }, + { action: "results" }, class: (@result_node.parent.chance? and @result_node.bad_answer?) ? "cta last_save" : "cta" %>
diff --git a/app/views/game/result.html.erb b/app/views/game/result.html.erb deleted file mode 100644 index 15ba721..0000000 --- a/app/views/game/result.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%- content_for :title, node_title(@node) %> diff --git a/app/views/game/results.html.erb b/app/views/game/results.html.erb new file mode 100644 index 0000000..a4479b9 --- /dev/null +++ b/app/views/game/results.html.erb @@ -0,0 +1,84 @@ +<%- content_for :title, node_title(@node) %> + +
+

<%= result_headline(current_player) %>

+
+
+
+
<%= result_description(current_player) %>
+
+ <%= svg "ico-wave" %> +
+ + +
+
<%= svg "ico-last-save" %>
+

<%= impact_label(:food_waste) %>

+

<%= impact_message(current_player, :food_waste) %>

+ +
+ +

<%= t("game.results.what_many_dont_know") %>

+ +
+ <% impact_learn_more(:food_waste).each do |paragraph| %> +

<%= paragraph %>

+ <% end %> +
+
+ +
+
<%= svg "ico-fuel" %>
+

<%= impact_label(:emissions) %>

+

<%= impact_message(current_player, :emissions) %>

+ +
+ +

<%= t("game.results.what_many_dont_know") %>

+ +
+ <% impact_learn_more(:emissions).each do |paragraph| %> +

<%= paragraph %>

+ <% end %> +
+
+ +
+
<%= svg "ico-coins" %>
+

<%= impact_label(:income) %>

+

<%= impact_message(current_player, :income) %>

+ +
+ +

<%= t("game.results.what_many_dont_know") %>

+ +
+ <% impact_learn_more(:income).each do |paragraph| %> +

<%= paragraph %>

+ <% end %> +
+
+ +
+ + <%= tag.h3 t("game.did_you_like_this_game") %> + +
+ + +
+ +
+ +
+ <%= tag.h3 t("game.join_our_newsletter") %> + <%= tag.p t("game.join_our_newsletter_desc") %> + + <%= link_to "#" do %> + <%= tag.span svg("ico-arrow-right") %> + <%= tag.span t("game.sounds_good") %> + <% end %> +
+ +<%= button_to tag.span(t("game.save_another_tomato")), start_path, class: "cta" %> + diff --git a/config/locales/en.yml b/config/locales/en.yml index 5d93a3e..48a8c83 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -15,11 +15,11 @@ en: play_time: Play time 2-5 minutes go_to_slide: Go to slide - got_it_lets_get_started: Got it, let’s get started + got_it_lets_get_started: Got it, let’s get started stage_i_of_n: Stage %{i} of %{n} take_a_chance: Take a chance - its_risky: | + its_risky: | It’s risky
but it can boost your score. ok: Ok @@ -27,6 +27,56 @@ en: next_stage: Next stage let_me_try: Let me try drumroll_see_the_result: Drumroll… see your result + did_you_like_this_game: Did you like this game? + + join_our_newsletter: Join our newsletter + join_our_newsletter_desc: We'd love to keep you in the loop and share stories about our partners and the important work they're doing. + sounds_good: Sounds good! + save_another_tomato: Save another tomato + + results: + best: + headline: Well done! You saved the tomato! + description: "That was just excellent. Let’s take a closer look at some of your moves:" + balanced: + headline: Good job! You saved the tomato! + description: "Overall, you did well. Let’s take a closer look at some of your moves:" + worst: + headline: Phew! You saved the tomato! + description: "But it was a close call. Let’s take a closer look at some of your moves:" + compost: + headline: Good save! Your tomato ended up as compost! + description: "Let’s take a closer look at some of your moves:" + landfill: + headline: Ouch! Your tomato ended up in a landfill! + description: "Let’s take a closer look at some of your moves:" + + what_many_dont_know: What many don’t know + food_waste: + label: Food Waste + positive: Your choices helped prevent food waste and kept the tomato useful for longer. + neutral: Some choices helped reduce food waste, while others still created risks. + negative: Some choices increased the risk of the tomato going to waste. + learn_more: + - Fresh produce, like tomatoes, can go to waste while it grows. But the risk of food waste is also big after harvest, for example during storage and transport. + - Here, even simple solutions like stackable crates can help reduce food waste, because you avoid the food getting squashed and damaged on its way to the market. Solar-powered cooling solutions can also help, as they can prevent food from spoiling in the heat. + emissions: + label: Emissions + positive: Your choices helped reduce greenhouse gas emissions. + neutral: Some choices lowered emissions, while others still added climate impact. + negative: Some choices increased greenhouse gas emissions. + learn_more: + - Food waste itself can cause greenhouse gases when it rots in a landfill. That’s why it’s key to avoid food waste in the first place. When food spoils or isn’t safe to eat, composting it is a more climate-friendly option. Maybe you’re already doing this at home or in your local community! + - Greenhouse gases can also come from using things like chemical fertiliser, as well as pumps, vans, and trucks that run on diesel and gasoline. So, ensuring farmers have access to renewable energy, electric transport and training in eco-friendly farming can also help lower emissions a lot. + income: + label: Income + positive: Your choices helped farmers earn more from their crops. + neutral: Some choices improved income opportunities, but not all decisions supported farmers. + negative: Some choices made it harder for farmers to earn income from the tomato. + early_exit: You didn’t get to sell your tomato. + learn_more: + - Finding a place to sell the things you grow isn’t always easy for small farmers. Setting up produce hubs can help, because then farmers can get together, bulk their crops and sell at a better price. Helping farmers connect with local shops and supermarkets is also a good solution. That means shorter travel distances and a lower risk of food waste and pollution. + - Another thing to keep in mind is that small farmers’ incomes are affected by a lot of things. Money for fertiliser and water is not always available, which can make it harder to grow crops and earn money. That’s why access to a small business loan can help. countries: au: Australia @@ -290,6 +340,7 @@ en: bad_answer: Bad answer chance: Chance last_save: Last save + results: Score categories: box: Box diff --git a/config/routes.rb b/config/routes.rb index 4122440..f335780 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -74,7 +74,7 @@ Rails.application.routes.draw do post "last_save/answer", to: "game#answer_last_save" get "done", to: "game#done" - get "result", to: "game#result" + get "results", to: "game#results" get "", to: "game#index", as: :locale_root end