From bb4bb1b591bcabc7e99691e9ee3b14e1c29dfdb9 Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Sun, 1 Nov 2026 16:50:41 +1300 Subject: [PATCH] Add offer options for stall seeds with UI updates and tests --- app/assets/stylesheets/_stalls.scss | 71 +++++++++++++++++++ app/controllers/stalls/seeds_controller.rb | 2 +- app/models/stall.rb | 6 +- app/models/stall_seed.rb | 3 + app/views/stalls/_seed_tin.html.haml | 15 +++- app/views/stalls/seeds/new.html.haml | 6 ++ config/locales/en.yml | 10 +++ ...20261101030000_add_offer_to_stall_seeds.rb | 7 ++ db/schema.rb | 3 +- spec/models/stall_spec.rb | 14 ++++ spec/requests/stalls/seeds_controller_spec.rb | 16 +++++ spec/requests/stalls_controller_spec.rb | 4 ++ 12 files changed, 149 insertions(+), 8 deletions(-) create mode 100644 db/migrate/20261101030000_add_offer_to_stall_seeds.rb diff --git a/app/assets/stylesheets/_stalls.scss b/app/assets/stylesheets/_stalls.scss index 638fc23cc..ab14e157c 100644 --- a/app/assets/stylesheets/_stalls.scss +++ b/app/assets/stylesheets/_stalls.scss @@ -484,3 +484,74 @@ $stall-label: #fff8e1; background: $white; cursor: pointer; } + +// "Free" and "swap" are words as well as colours, so neither relies on colour alone. +.stall-offer-tag { + display: flex; + flex-wrap: wrap; + gap: 0 0.5rem; + align-items: baseline; + margin-top: 0.6rem; + padding: 0.25rem 0.6rem; + border-radius: 2px; + color: $white; + line-height: 1.3; + + strong { + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + } +} + +.stall-offer-free { + background: $green; +} + +.stall-offer-swap { + background: $blue; +} + +.stall-legend { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + align-items: center; + margin-bottom: 1.5rem; + + .stall-offer-tag { + margin: 0; + font-size: 0.8rem; + font-weight: 800; + letter-spacing: 0.08em; + text-transform: uppercase; + } +} + +.stall-offer { + margin: 1rem 0; + padding: 0; + border: 0; + + legend { + font-size: 1rem; + font-weight: 600; + } +} + +.stall-offer-choice { + display: inline-flex; + gap: 0.6rem; + align-items: center; + min-height: 44px; + margin-right: 0.75rem; + padding: 0 0.9rem; + border: 1px solid #757575; + border-radius: $card-radius; + background: $white; + cursor: pointer; + + &:has(input:checked) { + border: 2px solid $green; + } +} diff --git a/app/controllers/stalls/seeds_controller.rb b/app/controllers/stalls/seeds_controller.rb index 1a062b5f9..27272772a 100644 --- a/app/controllers/stalls/seeds_controller.rb +++ b/app/controllers/stalls/seeds_controller.rb @@ -23,7 +23,7 @@ module Stalls packets = current_member.seeds.where(id: params[:seed_ids]) return redirect_to new_stall_seed_path, alert: t('.none') if packets.empty? - @stall.put_seeds_on!(packets) + @stall.put_seeds_on!(packets, offer: params[:offer].presence_in(StallSeed.offers.keys) || :free) redirect_to member_stall_path(current_member), notice: t('.added', count: packets.size) end diff --git a/app/models/stall.rb b/app/models/stall.rb index aaacdccf8..6af099376 100644 --- a/app/models/stall.rb +++ b/app/models/stall.rb @@ -26,13 +26,13 @@ class Stall < ApplicationRecord update!(opened_at: nil) end - # Puts packets on the stall. A packet that isn't marked "will trade" is, so + # Puts packets on the stall, as free or as a swap. A packet that isn't marked "will trade" is, so # what friends see on the stall matches what they see in the seed library. - def put_seeds_on!(packets) + def put_seeds_on!(packets, offer: :free) transaction do packets.each do |seed| seed.update!(tradable_to: WILL_TRADE) unless seed.tradable - stall_seeds.find_or_create_by!(seed:) + stall_seeds.find_or_create_by!(seed:) { |stall_seed| stall_seed.offer = offer } end end end diff --git a/app/models/stall_seed.rb b/app/models/stall_seed.rb index 8cde255a5..f43399b0b 100644 --- a/app/models/stall_seed.rb +++ b/app/models/stall_seed.rb @@ -2,6 +2,9 @@ # A packet of seed from the member's library, offered on their stall. class StallSeed < ApplicationRecord + # Given away, or swapped for seeds of the friend's own. + enum :offer, { free: 'free', swap: 'swap' }, default: :free + belongs_to :stall belongs_to :seed diff --git a/app/views/stalls/_seed_tin.html.haml b/app/views/stalls/_seed_tin.html.haml index 10c37ac2c..eb403131f 100644 --- a/app/views/stalls/_seed_tin.html.haml +++ b/app/views/stalls/_seed_tin.html.haml @@ -1,15 +1,24 @@ --# The packets on the stall, as seed packet cards on the table. The owner can add --# more, or take one off. +-# The packets on the stall, as seed packet cards on the table, each saying in +-# words and colour whether it is free or a swap. The owner can add more, or take +-# one off. %section.stall-section{ 'aria-labelledby': 'stall-tin-h' } .stall-section-header %h2#stall-tin-h= t('.heading') %span= t('.packets', count: stall.seeds.size) = link_to t('.add'), new_stall_seed_path, class: 'stall-add' if owner .stall-section-body + %p.stall-legend + %span.stall-offer-tag.stall-offer-free= t('stalls.offers.free') + %span.stall-offer-tag.stall-offer-swap= t('stalls.offers.swap') + = t('.legend') .stall-packets-grid - - stall.seeds.includes(:crop).find_each do |seed| + - stall.stall_seeds.includes(seed: :crop).find_each do |stall_seed| + - seed = stall_seed.seed .stall-tin-item = render 'seeds/card', seed: seed + .stall-offer-tag{ class: "stall-offer-#{stall_seed.offer}" } + %strong= t("stalls.offers.#{stall_seed.offer}") + %span= t("stalls.offers.#{stall_seed.offer}_text") - if owner = button_to stall_seed_path(seed), method: :delete, class: 'stall-take-off', title: t('.take_off') do = icon 'fas', 'xmark' diff --git a/app/views/stalls/seeds/new.html.haml b/app/views/stalls/seeds/new.html.haml index cfbb574c4..28235c118 100644 --- a/app/views/stalls/seeds/new.html.haml +++ b/app/views/stalls/seeds/new.html.haml @@ -27,6 +27,12 @@ - if @others.any? %p.stall-note= t('.will_mark') %p= link_to t('.record_new'), new_seed_path + %fieldset.stall-offer + %legend= t('.offer') + - StallSeed.offers.each_key do |offer| + = label_tag "offer_#{offer}", class: 'stall-offer-choice' do + = radio_button_tag :offer, offer, offer == 'free' + = t(".offers.#{offer}") .d-flex.flex-wrap.gap-3 = submit_tag t('.add'), class: 'btn btn-primary' = link_to t('.cancel'), member_stall_path(current_member), class: 'btn btn-secondary' diff --git a/config/locales/en.yml b/config/locales/en.yml index bec8cf31b..6b87b18eb 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1195,6 +1195,11 @@ en: string: "%{crop} seeds belonging to %{owner}" updated: Seed was successfully updated. stalls: + offers: + free: Free + free_text: Yours for the asking + swap: Swap + swap_text: Bring seeds of your own openings: create: opened: Your garden stall is open. @@ -1213,6 +1218,7 @@ en: seed_tin: add: "+ add" heading: In the seed tin + legend: Each packet says whether it's free or a swap. packets: one: 1 packet other: "%{count} packets" @@ -1237,6 +1243,10 @@ en: breadcrumb: Add seeds cancel: Cancel intro: Pick seeds to put on the stall, from your seed library. + offer: What are you offering them as? + offers: + free: Give away + swap: Swap or trade no_matches: No seeds match "%{query}". none_left: Every packet you have is on the stall already. others: diff --git a/db/migrate/20261101030000_add_offer_to_stall_seeds.rb b/db/migrate/20261101030000_add_offer_to_stall_seeds.rb new file mode 100644 index 000000000..f6777e610 --- /dev/null +++ b/db/migrate/20261101030000_add_offer_to_stall_seeds.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddOfferToStallSeeds < ActiveRecord::Migration[8.1] + def change + add_column :stall_seeds, :offer, :string, default: 'free', null: false + end +end diff --git a/db/schema.rb b/db/schema.rb index fad3cb171..c428dc3b9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_11_01_020000) do +ActiveRecord::Schema[8.1].define(version: 2026_11_01_030000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pg_trgm" @@ -1119,6 +1119,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_11_01_020000) do create_table "stall_seeds", force: :cascade do |t| t.datetime "created_at", null: false + t.string "offer", default: "free", null: false t.bigint "seed_id", null: false t.bigint "stall_id", null: false t.datetime "updated_at", null: false diff --git a/spec/models/stall_spec.rb b/spec/models/stall_spec.rb index 9feddc0a6..67aca162b 100644 --- a/spec/models/stall_spec.rb +++ b/spec/models/stall_spec.rb @@ -47,8 +47,22 @@ describe Stall do expect(shared_seed.reload.tradable_to).to eq('nationally') end + it 'offers them free by default' do + expect(stall.stall_seeds.map(&:offer).uniq).to eq(['free']) + end + it 'does not put a packet on twice' do expect { stall.put_seeds_on!([shared_seed]) }.not_to change(stall.stall_seeds, :count) end end + + describe '#put_seeds_on! as a swap' do + subject(:stall) { create(:stall, :open) } + + before { stall.put_seeds_on!([create(:seed, owner: stall.member)], offer: :swap) } + + it 'offers them as a swap' do + expect(stall.stall_seeds.first).to be_swap + end + end end diff --git a/spec/requests/stalls/seeds_controller_spec.rb b/spec/requests/stalls/seeds_controller_spec.rb index 6b76c5067..2f36501cc 100644 --- a/spec/requests/stalls/seeds_controller_spec.rb +++ b/spec/requests/stalls/seeds_controller_spec.rb @@ -62,6 +62,22 @@ describe Stalls::SeedsController, flipper: :garden_stall do end end + context 'when offering them as a swap' do + before { post stall_seeds_path, params: { seed_ids: [shared.id], offer: 'swap' } } + + it 'offers the packet as a swap' do + expect(stall.stall_seeds.first).to be_swap + end + end + + context 'with an offer that is not one' do + before { post stall_seeds_path, params: { seed_ids: [shared.id], offer: 'sell' } } + + it 'offers the packet free' do + expect(stall.stall_seeds.first).to be_free + end + end + context 'with another member\'s packet' do before { post stall_seeds_path, params: { seed_ids: [create(:seed).id] } } diff --git a/spec/requests/stalls_controller_spec.rb b/spec/requests/stalls_controller_spec.rb index 1fb7e1b8e..19ee9c71c 100644 --- a/spec/requests/stalls_controller_spec.rb +++ b/spec/requests/stalls_controller_spec.rb @@ -62,6 +62,10 @@ describe StallsController, flipper: :garden_stall do expect(Capybara.string(response.body)).to have_css('.stall-section .seed-card', text: 'rocket') end + it 'says the packet is free' do + expect(Capybara.string(response.body)).to have_css('.stall-tin-item .stall-offer-free', text: 'Free') + end + it 'lets the owner take a packet off' do expect(Capybara.string(response.body)).to have_button('Take off the stall') end