diff --git a/app/assets/stylesheets/_stalls.scss b/app/assets/stylesheets/_stalls.scss new file mode 100644 index 000000000..30b33a408 --- /dev/null +++ b/app/assets/stylesheets/_stalls.scss @@ -0,0 +1,269 @@ +// A member's garden stall: awning, hanging sign and a table, boarded up behind +// a shutter while it is closed. When it has just been opened the pieces animate +// in one after another (.stall-opening); without motion they are simply there. +$stall-wood: #6d4c41; +$stall-outline: #3e2a1c; +$stall-label: #fff8e1; + +.stall-page { + max-width: 760px; + margin: 0 auto; +} + +.stall { + position: relative; + overflow: clip; + border-radius: $card-radius; + margin-bottom: 1.75rem; +} + +.stall-awning { + display: flex; + position: relative; + z-index: 2; + padding: 0 3px; + transform-origin: top center; + + span { + flex: 1 1 0; + min-width: 0; + height: 34px; + margin-left: -3px; + border: 3px solid $stall-outline; + border-top-width: 0; + border-bottom-left-radius: 50% 34px; + border-bottom-right-radius: 50% 34px; + } + + .stall-stripe-green { + background: $green; + } + + .stall-stripe-white { + background: $white; + } +} + +.stall-body { + display: flex; + flex-direction: column; + align-items: center; + position: relative; + z-index: 1; +} + +.stall-sign { + box-sizing: border-box; + width: min(100%, 560px); + margin-top: 22px; + padding: 14px 20px; + border: 3px solid $stall-outline; + border-radius: $card-radius; + background: $stall-wood; + color: $white; + text-align: center; + transform-origin: top center; + + p { + margin: 0; + } +} + +.stall-sign-name { + font-size: clamp(1.25rem, 4.2vw, 1.9rem); + font-weight: 600; + letter-spacing: 0.06em; + line-height: 1.15; + text-transform: uppercase; +} + +.stall-sign-text { + margin-top: 4px; + font-size: 1rem; +} + +.stall-table { + width: 100%; + margin-top: 22px; +} + +.stall-spots { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 16px; + padding: 0 12px 16px; +} + +.stall-spot { + display: flex; + align-items: center; + justify-content: center; + height: 120px; + border: 3px dashed $stall-wood; + border-radius: $card-radius; + color: $brown; + font-size: 1.4rem; + font-weight: 600; +} + +.stall-tabletop { + height: 26px; + border-top: 3px solid $stall-outline; + background: $stall-wood; + box-shadow: inset 0 6px 0 rgba(255, 255, 255, 0.12); +} + +// Rolled up and out of sight whenever the stall is open. +.stall-shutter { + display: none; + position: absolute; + inset: 0; + z-index: 5; + align-items: center; + justify-content: center; + border: 3px solid $stall-outline; + border-radius: $card-radius; + background: repeating-linear-gradient(180deg, $stall-wood 0 26px, #5a3d33 26px 30px); +} + +.stall-closed .stall-shutter, +.stall-opening .stall-shutter { + display: flex; +} + +.stall-shutter-sign { + padding: 6px 26px; + border: 3px solid $stall-outline; + border-radius: 4px 16px 16px 4px; + background: $stall-label; + font-size: 2rem; + font-weight: 600; + transform: rotate(-3deg); +} + +.stall-closed { + .stall-awning { + transform: scaleY(0); + } + + .stall-sign, + .stall-table { + opacity: 0; + } + + // Keeps the shutter from collapsing to nothing with the contents hidden. + .stall-body { + min-height: 300px; + } +} + +.stall-panel { + box-sizing: border-box; + padding: 1.6rem 1.75rem; + border-radius: $card-radius; + background: $white; + box-shadow: 0 0 4px rgba(0, 0, 0, 0.14), 0 3px 4px rgba(0, 0, 0, 0.12), 0 1px 5px rgba(0, 0, 0, 0.2); + + h1 { + font-size: 150%; + font-weight: 500; + } +} + +.stall-choices { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 1.25rem; +} + +.stall-choice { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.6rem; + padding: 1.4rem; + border: 2px dashed rgba($brown, 0.35); + border-radius: $card-radius; + background: $white; + color: inherit; + text-align: left; + + strong { + font-size: 150%; + } +} + +.stall-choice-soon { + color: $ink-muted; + font-weight: 700; + letter-spacing: 0.02em; + text-transform: uppercase; +} + +.stall-card { + padding: 1rem 1.25rem; + border: 2px solid $stall-outline; + border-radius: 4px 16px 16px 4px; + background: $stall-label; +} + +@media (prefers-reduced-motion: no-preference) { + .stall-opening { + .stall-shutter { + animation: stall-shutter-up 800ms cubic-bezier(0.65, 0, 0.35, 1) forwards; + } + + .stall-awning { + animation: stall-awning-drop 700ms cubic-bezier(0.3, 1.5, 0.5, 1) 450ms backwards; + } + + .stall-sign { + animation: stall-sign-swing 800ms cubic-bezier(0.3, 1.6, 0.5, 1) 1000ms backwards; + } + + .stall-table { + animation: stall-table-in 600ms cubic-bezier(0.3, 1.3, 0.5, 1) 1400ms backwards; + } + + .stall-spot { + animation: stall-spot-in 500ms cubic-bezier(0.3, 1.6, 0.5, 1) 1800ms backwards; + } + } + + .stall-reveal-delayed { + animation: stall-spot-in 500ms cubic-bezier(0.3, 1.4, 0.5, 1) 2100ms backwards; + } +} + +@keyframes stall-shutter-up { + to { + transform: translateY(-108%); + } +} + +@keyframes stall-awning-drop { + from { + transform: scaleY(0); + } +} + +@keyframes stall-sign-swing { + from { + opacity: 0; + transform: translateY(-60px) rotate(-10deg); + } +} + +@keyframes stall-table-in { + from { + opacity: 0; + transform: scale(0.6); + } +} + +@keyframes stall-spot-in { + from { + opacity: 0; + transform: translateY(24px) scale(0.7); + } +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 75928b440..a733c11cd 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -53,6 +53,7 @@ @import "photos"; @import "posts"; @import "seeds"; +@import "stalls"; @import "predictions"; @import "homepage"; diff --git a/app/controllers/concerns/feature_gated.rb b/app/controllers/concerns/feature_gated.rb new file mode 100644 index 000000000..5873a3d80 --- /dev/null +++ b/app/controllers/concerns/feature_gated.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Controllers for something still being rolled out: not found until an admin +# switches the feature on. +module FeatureGated + extend ActiveSupport::Concern + + class_methods do + def require_feature(name) + before_action { raise ActiveRecord::RecordNotFound unless Feature.enabled?(name) } + end + end +end diff --git a/app/controllers/stalls/openings_controller.rb b/app/controllers/stalls/openings_controller.rb new file mode 100644 index 000000000..558f36475 --- /dev/null +++ b/app/controllers/stalls/openings_controller.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# A member opening their garden stall, or closing it again. +module Stalls + class OpeningsController < ApplicationController + include FeatureGated + + require_feature :garden_stall + before_action :authenticate_member! + + def create + current_member.stall!.open! + flash[:stall_opened] = true + redirect_to member_stall_path(current_member) + end + + def destroy + current_member.stall!.close! + redirect_to member_stall_path(current_member), notice: t('.closed') + end + end +end diff --git a/app/controllers/stalls_controller.rb b/app/controllers/stalls_controller.rb new file mode 100644 index 000000000..4915fedd9 --- /dev/null +++ b/app/controllers/stalls_controller.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# A member's garden stall, shared by link. Only the owner sees it while it is +# closed. +class StallsController < ApplicationController + include FeatureGated + + require_feature :garden_stall + + def show + @member = Member.confirmed.find_by!(slug: params[:member_slug]) + @owner = @member == current_member + @stall = @member.stall || @member.build_stall + raise ActiveRecord::RecordNotFound unless @stall.open? || @owner + + # The shutter rolls up the first time the owner sees it open. + @opening = flash[:stall_opened].present? + end +end diff --git a/app/models/feature.rb b/app/models/feature.rb index 840391999..7e7267004 100644 --- a/app/models/feature.rb +++ b/app/models/feature.rb @@ -3,7 +3,7 @@ # The features admins can switch on and off, kept in Flipper. Each is off until # an admin switches it on. class Feature - NAMES = %i(job_ideas enable_planting_reminders enable_harvest_reminders planting_weather).freeze + NAMES = %i(job_ideas enable_planting_reminders enable_harvest_reminders planting_weather garden_stall).freeze attr_reader :name diff --git a/app/models/member.rb b/app/models/member.rb index 69a6efee5..9c0927c63 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -17,6 +17,7 @@ class Member < ApplicationRecord has_many :comments, foreign_key: 'author_id', dependent: :destroy, inverse_of: :author has_many :forums, foreign_key: 'owner_id', dependent: :nullify, inverse_of: :owner has_many :gardens, foreign_key: 'owner_id', dependent: :destroy, inverse_of: :owner + has_one :stall, dependent: :destroy # Where they raise seedlings before planting them out. has_one :nursery, -> { nurseries }, class_name: 'Garden', foreign_key: 'owner_id', dependent: nil, inverse_of: :owner has_many :plantings, foreign_key: 'owner_id', dependent: :destroy, inverse_of: :owner @@ -188,6 +189,11 @@ class Member < ApplicationRecord # which can happen sometimes especially with FactoryBot associations after_create { |member| Garden.create(name: "Garden", owner_id: member.id) } + # Their stall, made the first time they open it. + def stall! + stall || create_stall! + end + # Their nursery, made the first time it's wanted, as most members never need # one. The member may already have a garden called "Nursery", and garden names # are unique per member, so ours is numbered until it's free. diff --git a/app/models/stall.rb b/app/models/stall.rb new file mode 100644 index 000000000..2466dfad2 --- /dev/null +++ b/app/models/stall.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# A member's garden stall: one page where they offer spare seedlings and seeds +# to friends. A member has at most one. It is open when it has an opened_at, and +# closing it keeps the row so opening it again is just a timestamp. +class Stall < ApplicationRecord + belongs_to :member + + validates :member_id, uniqueness: true + + def open? + opened_at.present? + end + + def open! + update!(opened_at: Time.current) + end + + def close! + update!(opened_at: nil) + end +end diff --git a/app/views/gardens/show.html.haml b/app/views/gardens/show.html.haml index c0d54e835..d1b35900b 100644 --- a/app/views/gardens/show.html.haml +++ b/app/views/gardens/show.html.haml @@ -23,6 +23,7 @@ .mb-3 = render 'jobs/ideas_banner', href: suggestions_path, open: false, text: t('.suggested_plantings', count: @suggestion_count), go: t('.suggested_plantings_go') + = render 'stalls/card', garden: @garden = render 'gardens/facts', garden: @garden, growing_now: @current_plantings.size = render 'gardens/description', garden: @garden = render 'gardens/join_prompt', garden: @garden unless member_signed_in? diff --git a/app/views/stalls/_card.html.haml b/app/views/stalls/_card.html.haml new file mode 100644 index 000000000..5fd12c489 --- /dev/null +++ b/app/views/stalls/_card.html.haml @@ -0,0 +1,11 @@ +-# On the nursery garden: where the owner opens their garden stall, or goes to it. +- if garden.nursery? && can?(:edit, garden) && Feature.enabled?(:garden_stall) + - stall = garden.owner.stall + .stall-card.mb-3 + %h2.h5= t('.heading') + - if stall&.open? + %p= t('.open_text') + = link_to t('.go'), member_stall_path(garden.owner), class: 'btn btn-outline-success' + - else + %p= t('.text') + = link_to t('.open'), member_stall_path(garden.owner), class: 'btn btn-success' diff --git a/app/views/stalls/_stall.html.haml b/app/views/stalls/_stall.html.haml new file mode 100644 index 000000000..bd0cdcc51 --- /dev/null +++ b/app/views/stalls/_stall.html.haml @@ -0,0 +1,18 @@ +-# The stall itself: awning, sign and table. Closed, it is boarded up behind a +-# shutter. Just opened, the pieces animate in; see _stalls.scss. +- name = t('.sign', name: member) +%section.stall{ 'aria-label': name, class: [('stall-closed' unless open), ('stall-opening' if opening)] } + .stall-awning{ 'aria-hidden': 'true' } + - 12.times do |i| + %span{ class: (i.even? ? 'stall-stripe-green' : 'stall-stripe-white') } + .stall-body + .stall-sign + %p.stall-sign-name= name + %p.stall-sign-text= t('.tagline') + .stall-table + .stall-spots{ 'aria-hidden': 'true' } + - %i(seedlings seeds more).each do |spot| + .stall-spot= t(".spots.#{spot}") + .stall-tabletop + .stall-shutter{ 'aria-hidden': 'true' } + %span.stall-shutter-sign= t('.closed') diff --git a/app/views/stalls/show.html.haml b/app/views/stalls/show.html.haml new file mode 100644 index 000000000..27157b16b --- /dev/null +++ b/app/views/stalls/show.html.haml @@ -0,0 +1,41 @@ +- content_for :title, t('.title', name: @member) + +- content_for :breadcrumbs do + %li.breadcrumb-item= link_to t('layouts.menu.gardens'), gardens_path + %li.breadcrumb-item.active= t('.breadcrumb') + +.stall-page + .record-identity.mb-4 + = render 'shared/record_owner', owner: @member + .record-identity-text + %p.garden-grower + = t('.garden_of') + %strong.record-grower-name= link_to @member, @member + + = render 'stalls/stall', member: @member, open: @stall.open?, opening: @opening + + - if @owner + - if @stall.open? + %section.stall-panel{ 'aria-labelledby': 'stall-first-h', class: ('stall-reveal-delayed' if @opening) } + %h1#stall-first-h= t('.first_heading') + %p.text-muted= t('.first_intro') + .stall-choices + %button.stall-choice{ type: 'button', disabled: true } + %strong= t('.seedlings') + %span= t('.seedlings_text') + %span.stall-choice-soon= t('.coming_soon') + %button.stall-choice{ type: 'button', disabled: true } + %strong= t('.seeds') + %span= t('.seeds_text') + %span.stall-choice-soon= t('.coming_soon') + .d-flex.flex-wrap.gap-3.align-items-center.mt-3 + = button_to t('.close'), stall_opening_path, method: :delete, class: 'btn btn-secondary' + %span.text-muted= t('.link_is', url: member_stall_url(@member)) + - else + %section.stall-panel{ 'aria-labelledby': 'stall-open-h' } + %h1#stall-open-h= t('.open_heading') + %p= t('.open_intro') + %ul + - %i(choose_what choose_who choose_where expiry privacy).each do |point| + %li= t(".points.#{point}") + = button_to t('.open'), stall_opening_path, class: 'btn btn-primary btn-lg' diff --git a/config/locales/en.yml b/config/locales/en.yml index 00823a953..a9d8e90e9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -97,6 +97,7 @@ en: descriptions: enable_harvest_reminders: Sends the weekly harvest reminder emails to members who want them. enable_planting_reminders: Sends the weekly planting reminder emails to members who want them. + garden_stall: A page where members offer spare seedlings and seeds to friends. Switch on once all the parts are deployed. job_ideas: Suggested jobs for members, from their past jobs and plantings, crop templates and what gardeners nearby are doing. planting_weather: Temperature and rainfall charts on each planting, from daily weather for the garden's area. index: @@ -111,6 +112,7 @@ en: names: enable_harvest_reminders: Harvest reminders enable_planting_reminders: Planting reminders + garden_stall: Garden stall job_ideas: Job ideas planting_weather: Planting weather update: @@ -1192,6 +1194,46 @@ en: heading: "%{crop} seeds" string: "%{crop} seeds belonging to %{owner}" updated: Seed was successfully updated. + stalls: + openings: + destroy: + closed: Your garden stall is closed. + card: + go: Go to your stall + heading: Garden stall + open: Open a garden stall + open_text: Your garden stall is open for friends. + text: Share spare seedlings and seeds with friends. You choose what and who, offers close after 4 weeks, and no addresses are kept. + show: + breadcrumb: Garden stall + close: Close the stall + coming_soon: Coming soon + first_heading: Your stall is open. What goes on it first? + first_intro: You can add more any time. Nothing shows to friends until something is on the table. + garden_of: Garden of + link_is: Your link is %{url} + open: Open my stall + open_heading: Open a garden stall + open_intro: Got spare seedlings or seeds? Put them on a stall and share the link with your friends. Give them away or swap. + points: + choose_what: You choose what goes on the table. + choose_where: 'You choose where friends can be: your region, your country or anywhere.' + choose_who: 'You choose who can bagsy: anyone signed in, or only people you follow.' + expiry: Offers close after 4 weeks, so the stall never goes stale. + privacy: Growstuff keeps no addresses. Friends message you to arrange pick-up. + seedlings: Seedlings + seedlings_text: Plants you've raised that need a home. + seeds: Seeds + seeds_text: Packets you've saved and will trade. + title: "%{name}'s garden stall" + stall: + closed: Closed for now + sign: "%{name}'s garden stall" + spots: + more: anything else + seedlings: seedlings here + seeds: seeds here + tagline: Seedlings and seeds for friends. Free, or swap. suggestions: index: lede: What gardeners at %{band} are planting this month, most planted first. diff --git a/config/routes.rb b/config/routes.rb index c041b8eed..4d8db61eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -51,6 +51,11 @@ Rails.application.routes.draw do end # Before the gardens resource, so "nursery" isn't taken for a garden's slug. + # Opening and closing your own garden stall. + resource :stall, only: [] do + resource :opening, only: %i(create destroy), module: :stalls + end + resource :nursery, only: %i(show create) do get :panel resource :decline, only: :create, module: :nurseries @@ -152,6 +157,7 @@ Rails.application.routes.draw do get '/timeline', to: redirect('/my/neighbours') resources :members, param: :slug do + resource :stall, only: :show # Before the gardens resource, so "inactive" isn't taken for a garden's slug. get 'gardens/inactive' => 'gardens#inactive', as: :inactive_gardens # param: :slug to match the top-level gardens resource, so the controller's diff --git a/db/migrate/20261101010000_create_stalls.rb b/db/migrate/20261101010000_create_stalls.rb new file mode 100644 index 000000000..15259f52e --- /dev/null +++ b/db/migrate/20261101010000_create_stalls.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class CreateStalls < ActiveRecord::Migration[8.1] + def change + create_table :stalls do |t| + t.references :member, null: false, foreign_key: true, index: { unique: true } + t.datetime :opened_at + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 401030747..8851c9ce6 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_000000) do +ActiveRecord::Schema[8.1].define(version: 2026_11_01_010000) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "pg_trgm" @@ -1117,6 +1117,14 @@ ActiveRecord::Schema[8.1].define(version: 2026_11_01_000000) do t.index ["source"], name: "index_seeds_on_source" end + create_table "stalls", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "member_id", null: false + t.datetime "opened_at" + t.datetime "updated_at", null: false + t.index ["member_id"], name: "index_stalls_on_member_id", unique: true + end + create_table "versions", force: :cascade do |t| t.datetime "created_at" t.string "event", null: false @@ -1188,4 +1196,5 @@ ActiveRecord::Schema[8.1].define(version: 2026_11_01_000000) do add_foreign_key "posts", "photos", column: "card_photo_id", on_delete: :nullify add_foreign_key "reminder_events", "members", on_delete: :cascade add_foreign_key "seeds", "plantings", column: "parent_planting_id", name: "parent_planting", on_delete: :nullify + add_foreign_key "stalls", "members" end diff --git a/spec/factories/stalls.rb b/spec/factories/stalls.rb new file mode 100644 index 000000000..4bc66805e --- /dev/null +++ b/spec/factories/stalls.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :stall do + member + opened_at { nil } + + trait :open do + opened_at { Time.zone.local(2026, 11, 1, 12) } + end + end +end diff --git a/spec/models/stall_spec.rb b/spec/models/stall_spec.rb new file mode 100644 index 000000000..8dc21124f --- /dev/null +++ b/spec/models/stall_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Stall do + subject(:stall) { create(:stall) } + + it { is_expected.not_to be_open } + + describe '#open!' do + before { stall.open! } + + it { is_expected.to be_open } + end + + describe '#close!' do + subject(:stall) { create(:stall, :open) } + + before { stall.close! } + + it { is_expected.not_to be_open } + end + + describe 'with a second stall for the same member' do + subject(:second) { build(:stall, member: stall.member) } + + it { is_expected.not_to be_valid } + end +end diff --git a/spec/requests/gardens_stall_card_spec.rb b/spec/requests/gardens_stall_card_spec.rb new file mode 100644 index 000000000..ed959093f --- /dev/null +++ b/spec/requests/gardens_stall_card_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe GardensController, 'garden stall card', flipper: :garden_stall do + let(:owner) { create(:member) } + let(:nursery) { create(:garden, owner:, nursery: true) } + + context 'when the owner looks at their nursery' do + before do + sign_in owner + get garden_path(nursery) + end + + it 'offers to open a stall' do + expect(Capybara.string(response.body)).to have_link('Open a garden stall', href: member_stall_path(owner)) + end + end + + context 'when a visitor looks at the nursery' do + before { get garden_path(nursery) } + + it 'has no stall card' do + expect(Capybara.string(response.body)).to have_no_css('.stall-card') + end + end +end diff --git a/spec/requests/stalls/openings_controller_spec.rb b/spec/requests/stalls/openings_controller_spec.rb new file mode 100644 index 000000000..f0bc855de --- /dev/null +++ b/spec/requests/stalls/openings_controller_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Stalls::OpeningsController, flipper: :garden_stall do + let(:owner) { create(:member) } + let(:visitor) { create(:member) } + + describe 'POST /stall/opening' do + context 'when signed in as the owner' do + before do + sign_in owner + post stall_opening_path + end + + it 'opens the stall' do + expect(owner.reload.stall).to be_open + end + + it 'goes to the stall' do + expect(response).to redirect_to(member_stall_path(owner)) + end + end + + context 'when signed in as someone else' do + before do + sign_in visitor + post stall_opening_path + end + + it 'leaves the stall alone' do + expect(owner.reload.stall).to be_nil + end + end + + context 'when signed out' do + before { post stall_opening_path } + + it 'asks them to sign in' do + expect(response).to redirect_to(new_member_session_path) + end + end + end + + describe 'DELETE /stall/opening' do + let!(:stall) { create(:stall, :open, member: owner) } + + context 'when signed in as the owner' do + before do + sign_in owner + delete stall_opening_path + end + + it 'closes the stall' do + expect(stall.reload).not_to be_open + end + end + + context 'when signed in as someone else' do + before do + sign_in visitor + delete stall_opening_path + end + + it 'leaves the stall open' do + expect(stall.reload).to be_open + end + end + end +end diff --git a/spec/requests/stalls_controller_spec.rb b/spec/requests/stalls_controller_spec.rb new file mode 100644 index 000000000..12d2579eb --- /dev/null +++ b/spec/requests/stalls_controller_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe StallsController, flipper: :garden_stall do + let(:owner) { create(:member) } + let(:visitor) { create(:member) } + + describe 'GET /members/:slug/stall' do + context 'when signed in as the owner without a stall' do + before do + sign_in owner + get member_stall_path(owner) + end + + it 'invites them to open one' do + expect(Capybara.string(response.body)).to have_button('Open my stall') + end + + it 'shows the shutter' do + expect(response.body).to include('stall-shutter') + end + end + + context 'when signed in as the owner with an open stall' do + before do + create(:stall, :open, member: owner) + sign_in owner + get member_stall_path(owner) + end + + it 'asks what goes on first' do + expect(Capybara.string(response.body)).to have_text('What goes on it first?') + end + + it 'offers to close it' do + expect(Capybara.string(response.body)).to have_button('Close the stall') + end + end + + context 'when a visitor looks at an open stall' do + before do + create(:stall, :open, member: owner) + get member_stall_path(owner) + end + + it 'shows the sign' do + expect(response.body).to include("#{owner}'s garden stall") + end + + it 'has no button to close it' do + expect(Capybara.string(response.body)).to have_no_button('Close the stall') + end + end + + context 'when a visitor looks at a closed stall' do + before do + create(:stall, member: owner) + get member_stall_path(owner) + end + + it 'is not found' do + expect(response).to have_http_status(:not_found) + end + end + + context 'when the feature is off', flipper: [] do + before do + Flipper.disable(:garden_stall) + create(:stall, :open, member: owner) + get member_stall_path(owner) + end + + it 'is not found' do + expect(response).to have_http_status(:not_found) + end + end + end +end