diff --git a/Gemfile.lock b/Gemfile.lock index 62376a9..db50a3b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index f9ac840..84f8be7 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -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; +} \ No newline at end of file diff --git a/app/assets/stylesheets/forms.css b/app/assets/stylesheets/forms.css index 0dbd150..054f254 100644 --- a/app/assets/stylesheets/forms.css +++ b/app/assets/stylesheets/forms.css @@ -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 { diff --git a/app/controllers/admin/nodes_controller.rb b/app/controllers/admin/nodes_controller.rb index 723b346..b3b6234 100644 --- a/app/controllers/admin/nodes_controller.rb +++ b/app/controllers/admin/nodes_controller.rb @@ -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 diff --git a/app/controllers/concerns/quiz_helper_methods.rb b/app/controllers/concerns/quiz_helper_methods.rb index ba72499..dbf51ec 100644 --- a/app/controllers/concerns/quiz_helper_methods.rb +++ b/app/controllers/concerns/quiz_helper_methods.rb @@ -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 diff --git a/app/controllers/direct_uploads_controller.rb b/app/controllers/direct_uploads_controller.rb index db6cae0..c8c5bdd 100644 --- a/app/controllers/direct_uploads_controller.rb +++ b/app/controllers/direct_uploads_controller.rb @@ -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 diff --git a/app/controllers/players_controller.rb b/app/controllers/players_controller.rb new file mode 100644 index 0000000..ce9b0d3 --- /dev/null +++ b/app/controllers/players_controller.rb @@ -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 diff --git a/app/controllers/stages_controller.rb b/app/controllers/stages_controller.rb new file mode 100644 index 0000000..cf18223 --- /dev/null +++ b/app/controllers/stages_controller.rb @@ -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 diff --git a/app/helpers/admin/nodes_helper.rb b/app/helpers/admin/nodes_helper.rb index 731e9df..13eb912 100644 --- a/app/helpers/admin/nodes_helper.rb +++ b/app/helpers/admin/nodes_helper.rb @@ -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 diff --git a/app/helpers/stages_helper.rb b/app/helpers/stages_helper.rb new file mode 100644 index 0000000..93ca31e --- /dev/null +++ b/app/helpers/stages_helper.rb @@ -0,0 +1,2 @@ +module StagesHelper +end diff --git a/app/models/node.rb b/app/models/node.rb index fd52de1..abee89b 100644 --- a/app/models/node.rb +++ b/app/models/node.rb @@ -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 diff --git a/app/models/player.rb b/app/models/player.rb new file mode 100644 index 0000000..7e79444 --- /dev/null +++ b/app/models/player.rb @@ -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 diff --git a/app/views/languages/_intro.html.erb b/app/views/languages/_intro.html.erb index 675d80c..e69de29 100644 --- a/app/views/languages/_intro.html.erb +++ b/app/views/languages/_intro.html.erb @@ -1,20 +0,0 @@ - -<% @node.attachments.limit(2).each_with_index do |attachment, i| %> -