Add offer options for stall seeds with UI updates and tests

This commit is contained in:
Brenda Wallace committed 2026-11-02 18:06:42 +13:00
1 parent 275ebd1bab
commit bb4bb1b591
12 files changed
+149 -8

No files matched your search

+71
View File
@@ -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;
}
}
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+3
View File
@@ -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
+12 -3
View File
@@ -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'
+6
View File
@@ -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'
+10
View File
@@ -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:
@@ -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
+2 -1
View File
@@ -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
+14
View File
@@ -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
@@ -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] } }
+4
View File
@@ -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