diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4bace6f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# IKEA Foundation - Week 2026 Project + +This project is a Ruby on Rails application developed for the IKEA Foundation. It appears to be a multi-lingual, interactive game or educational tool ("Week 2026") where players progress through various stages, making choices or facing "chance" events. + +## Key Features + +- **Multi-lingual Support:** Uses the `mobility` gem for translating content into numerous languages (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). +- **Content Management:** A hierarchical "Node" system (using `ancestry`) for managing pages, stages, and interactive elements. +- **Player Progression:** Tracks player progress, scores, and decisions across different stages. +- **Interactive Stages:** Includes mechanics for "flipping" between "chance" and "choice" outcomes, sampling random results, and advancing through a sequence of stages. +- **Admin Interface:** A backend for managing nodes, assets (using Active Storage), users, and translations. +- **Background Jobs:** Uses `sidekiq` and `redis` for asynchronous processing. +- **Search:** Integrates `pg_search` for full-text search capabilities within the node/content system. + +## Technical Stack + +- **Framework:** Ruby on Rails 8.1.2 +- **Language:** Ruby 3.4.9 +- **Database:** PostgreSQL +- **Asset Pipeline:** Propshaft with Importmap-rails and Stimulus/Turbo. +- **Task Management:** Sidekiq with Redis. +- **Styling/UI:** Custom CSS/JS (Stimulus controllers for interactivity). + +## Core Models + +- `Node`: The central model for content, representing pages, stages, and interactive components in a tree structure. +- `Player`: Represents a session/user playing the game, tracking their `progress`, `current_stage`, and `score`. +- `Asset` & `Attachment`: Manage files and their association with nodes. +- `User`: Handles admin authentication and roles. +- `VerificationCode`: Likely used for secure admin login or two-step authentication. + +## Project Structure + +- `app/controllers/admin/`: Admin backend logic. +- `app/controllers/stages_controller.rb`: Main logic for the interactive game stages. +- `app/controllers/site_controller.rb`: Logic for rendering content nodes as pages. +- `config/locales/`: Translation files for all supported languages. +- `db/migrate/`: Database schema evolutions. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..d028c9b --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,9 @@ +# Gemini CLI Project Configuration + +This project contains documentation for AI agents in the [AGENTS.md](./AGENTS.md) file. + +## Core Mandates + +- Adhere to the project structure and architectural patterns described in [AGENTS.md](./AGENTS.md). +- Ensure all new features or bug fixes are verified through automated tests. +- Maintain multi-lingual support by updating the necessary translation files in `config/locales/`. diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 84f8be7..0049354 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -95,6 +95,15 @@ font: 16px/1.3 var(--ff-ikea); } +#debug { + position: fixed; + bottom: 0; + left: 0; + font: 0.75rem ui-monospace; + padding: 1em; + max-width: 40ch; +} + .turbo-progress-bar { height: 3px; background-color: #0058a3; @@ -231,6 +240,10 @@ p { .action-container { margin-block-start: 2em; + display: flex; + flex-direction: column; + gap: 0.4em; + } button { diff --git a/app/controllers/players_controller.rb b/app/controllers/players_controller.rb index ce9b0d3..2a8526c 100644 --- a/app/controllers/players_controller.rb +++ b/app/controllers/players_controller.rb @@ -4,7 +4,7 @@ class PlayersController < ApplicationController skip_before_action :require_player! def create - reset_session + # reset_session @player = Player.create(locale: I18n.locale) session[:player_id] = @player.id diff --git a/app/controllers/stages_controller.rb b/app/controllers/stages_controller.rb index cf18223..db01513 100644 --- a/app/controllers/stages_controller.rb +++ b/app/controllers/stages_controller.rb @@ -3,34 +3,81 @@ class StagesController < ApplicationController before_action :set_locale before_action :require_player! - before_action :set_node, only: [ :show, :flip, :choose ] + before_action :set_stage + before_action :set_outcome, only: [ :reveal, :pick, :result ] + + + # GET def show - @progress = current_player.progress[params[:id].to_s] || {} end + + # POST def flip outcome = rand < 0.5 ? "chance" : "choice" current_player.record_flip(params[:id], outcome) - redirect_to stage_path(id: params[:id]) + + redirect_to url_for(action: "reveal") + end + + + # GET Chance or Choice + def reveal + @node = @stage.children.viewable.find_by(template: @outcome) + redirect_to root_path and return if @outcome.blank? or @node.blank? + + render action: @outcome + end + + + # POST + def pick + redirect_to root_path and return if @outcome.blank? + + @node = @stage.children.viewable.find_by(template: @outcome) + possible_answers = @node.children.viewable + + + if @outcome == "chance" + result = possible_answers.pluck(:template).sample + elsif @outcome == "choice" + result = possible_answers.find_by(id: params[:option])&.template + end + current_player.record_pick_result(params[:id], result) unless result.blank? + + redirect_to action: "result" 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) + # GET + def result + redirect_to root_path and return if @outcome.blank? + + @result = current_player.progress[params[:id].to_s]["result"] + + question_node = @stage.children.viewable.find_by(template: @outcome) + @node = question_node.children.viewable.find_by(template: @result) + + + logger.info(current_player.inspect) end + private - def set_node - @node = Node.at_depth(1).viewable.ordered[params[:id].to_i - 1] - if @node.nil? + def set_stage + @stage = Node.at_depth(1).viewable.ordered[params[:id].to_i - 1] + + if @stage.nil? redirect_to root_path end end + + def set_outcome + @outcome ||= current_player.progress[params[:id].to_s]["flip"] + end + + end diff --git a/app/models/node.rb b/app/models/node.rb index abee89b..e4cee29 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -26,12 +26,38 @@ class Node < ApplicationRecord enum :status, { status_published: 0, status_draft: 1, status_archived: 2 } enum :template, { - tmpl_stage: 0, - tmpl_chance: 1, - tmpl_choice: 2, - tmpl_bonus: 3 + start: 0, + stage: 1, + choice: 2, + chance: 3, + bonus: 4, + best: 5, + bad: 6, + good: 7, + game_over: 8 } + def available_templates + return [ :start ] if root? + + case depth + when 1 + [ :stage ] + when 2 + [ :choice, :chance, :bonus ] + when 3 + if parent&.chance? + [ :good, :bad, :game_over ] + elsif parent&.choice? + [ :best, :good, :bad ] + else + [] + end + else + [] + end + end + include PgSearch::Model pg_search_scope :pg_search, against: { title: "A", url: "B", page_title: "A", page_description: "B", href: "B", slug: "B" }, diff --git a/app/models/player.rb b/app/models/player.rb index 7e79444..ebeaefb 100644 --- a/app/models/player.rb +++ b/app/models/player.rb @@ -19,6 +19,14 @@ class Player < ApplicationRecord save end + # stage_index: 1, 2, 3... + # result: 'good', 'bad', 'game_over' + def record_pick_result(stage_index, result) + self.progress[stage_index.to_s] ||= {} + self.progress[stage_index.to_s]["result"] = result + save + end + def advance_to_next_stage! self.current_stage += 1 save diff --git a/app/views/admin/nodes/_form.html.erb b/app/views/admin/nodes/_form.html.erb index bca8334..ed64ca5 100644 --- a/app/views/admin/nodes/_form.html.erb +++ b/app/views/admin/nodes/_form.html.erb @@ -69,8 +69,8 @@ f: form, attr: :template, include_blank: false, - choices: Node.templates.map { |k,v| [t(k, scope: :'nodes.templates'), k] }.sort - } %> + choices: @node.available_templates.map { |k| [t(k, scope: :'nodes.templates'), k] }.sort + } %> <%= render partial: 'material/tom_select_field', locals: { diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 94e8307..ba1216c 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -32,5 +32,7 @@ + + <%= tag.div content_for(:debug), id: "debug" if Rails.env.development? %> diff --git a/app/views/stages/chance.html.erb b/app/views/stages/chance.html.erb new file mode 100644 index 0000000..03e6fb0 --- /dev/null +++ b/app/views/stages/chance.html.erb @@ -0,0 +1,19 @@ +<%- + content_for :title, @node.page_title.blank? ? @node.title : @node.page_title + content_for :meta_description, @node.page_description + + content_for :debug, current_player.inspect +%> + + +
+ + <% @node.attachments.each do |attachment| %> + <%= attachment.body.html_safe %> + <% end %> + +
+ <%= button_to t("flip_the_card"), url_for(action: 'pick'), method: :post %> +
+ +
\ No newline at end of file diff --git a/app/views/stages/choice.html.erb b/app/views/stages/choice.html.erb new file mode 100644 index 0000000..667fe89 --- /dev/null +++ b/app/views/stages/choice.html.erb @@ -0,0 +1,20 @@ +<%- + content_for :title, @node.page_title.blank? ? @node.title : @node.page_title + content_for :meta_description, @node.page_description + + content_for :debug, current_player.inspect +%> + + +
+ + <% @node.attachments.each do |attachment| %> + <%= attachment.body.html_safe %> + <% end %> + +
+ <% @node.children.viewable.ordered.each do |answer_option| %> + <%= button_to answer_option.title, url_for(action: 'pick', option: answer_option.id), method: :post %> + <% end %> +
+
\ No newline at end of file diff --git a/app/views/stages/result.html.erb b/app/views/stages/result.html.erb new file mode 100644 index 0000000..8efef99 --- /dev/null +++ b/app/views/stages/result.html.erb @@ -0,0 +1,15 @@ +<%- + content_for :title, @node.page_title.blank? ? @node.title : @node.page_title + content_for :meta_description, @node.page_description + + content_for :debug, current_player.inspect +%> + + +
+ + <% @node.attachments.each do |attachment| %> + <%= attachment.body.html_safe %> + <% end %> + +
\ No newline at end of file diff --git a/app/views/stages/show.html.erb b/app/views/stages/show.html.erb index 133694e..818e537 100644 --- a/app/views/stages/show.html.erb +++ b/app/views/stages/show.html.erb @@ -1,12 +1,14 @@ <%- - content_for :title, @node.page_title.blank? ? @node.title : @node.page_title - content_for :meta_description, @node.page_description + content_for :title, @stage.page_title.blank? ? @stage.title : @stage.page_title + content_for :meta_description, @stage.page_description + + content_for :debug, current_player.inspect %>
- <% @node.attachments.each do |attachment| %> + <% @stage.attachments.each do |attachment| %> <%= attachment.body.html_safe %> <% end %> @@ -14,6 +16,4 @@ <%= button_to t("flip"), url_for(action: 'flip'), method: :post %> - <%= current_player.inspect %> -
\ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index a682e55..35c13df 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -196,11 +196,16 @@ en: da: Danish en: English de: German - templates: - tmpl_stage: Stage - tmpl_chance: Chance - tmpl_choice: Choice - tmpl_bonus: Bonus chance + templates: + start: Start + stage: Stage + choice: Choice + chance: Chance + best: Best + bad: Bad + game_over: Game Over + good: Good + categories: box: Box folder: Folder diff --git a/config/routes.rb b/config/routes.rb index b84faf1..5f271f5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -52,9 +52,15 @@ Rails.application.routes.draw 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 post "start", to: "players#create", as: "start" - 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 + scope "stages/:id", constraints: { id: /\d+/ } do + get "", to: "stages#show", as: "stage" + post "flip", to: "stages#flip" + + get "reveal", to: "stages#reveal" + post "pick(/:option)", to: "stages#pick" + + get "result", to: "stages#result" + end get "", to: "languages#show" # get "*url", to: "site#page", constraints: lambda { |req| req.path.exclude?("storage") }