From dcf59ff4e7ab85fbd937c37170b7729d3059b29f Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Mon, 21 Sep 2026 08:27:21 +1200 Subject: [PATCH] Add a gallery page of a member's past gardens /members/:slug/gardens/past shows their inactive gardens as photo cards: when each was in use, how many plantings, and the crops grown in it. The owner (or a collaborator) can mark a garden active again from its card. Co-Authored-By: Claude Sonnet 5 --- app/assets/stylesheets/_past_gardens.scss | 38 ++++++++++++ app/assets/stylesheets/application.scss | 1 + app/controllers/gardens_controller.rb | 15 +++++ app/helpers/gardens_helper.rb | 9 +++ app/views/gardens/_past_card.html.haml | 23 ++++++++ app/views/gardens/past.html.haml | 31 ++++++++++ config/routes.rb | 2 + spec/features/gardens/past_gardens_spec.rb | 67 ++++++++++++++++++++++ 8 files changed, 186 insertions(+) create mode 100644 app/assets/stylesheets/_past_gardens.scss create mode 100644 app/views/gardens/_past_card.html.haml create mode 100644 app/views/gardens/past.html.haml create mode 100644 spec/features/gardens/past_gardens_spec.rb diff --git a/app/assets/stylesheets/_past_gardens.scss b/app/assets/stylesheets/_past_gardens.scss new file mode 100644 index 000000000..b3475507c --- /dev/null +++ b/app/assets/stylesheets/_past_gardens.scss @@ -0,0 +1,38 @@ +// A member's past gardens: a gallery of cards, a photo and what was grown. + +.past-garden-card { + overflow: hidden; + + .card-img-top { + display: block; + width: 100%; + aspect-ratio: 3 / 2; + object-fit: cover; + } + + .card-title { + font-weight: 500; + } + + .past-garden-meta { + color: #666; + font-size: 0.9rem; + } + + .past-garden-crops { + display: flex; + flex-wrap: wrap; + margin: 0; + padding: 0; + + li { + margin: 0 0.5rem 0.5rem 0; + } + } + + .past-garden-more { + align-self: center; + color: #666; + font-size: 0.9rem; + } +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index a3f90302c..b8d93b791 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -24,6 +24,7 @@ @import "notifications"; @import "plantings"; @import "garden_cards"; +@import "past_gardens"; @import "photos"; @import "posts"; @import "seeds"; diff --git a/app/controllers/gardens_controller.rb b/app/controllers/gardens_controller.rb index 43acc9893..eb99c06c7 100644 --- a/app/controllers/gardens_controller.rb +++ b/app/controllers/gardens_controller.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true class GardensController < DataController + # Anyone can look at a member's past gardens, as they can their current ones. + skip_before_action :authenticate_member!, only: :past + skip_load_and_authorize_resource only: :past + def index @owner = Member.find_by!(slug: params[:member_slug]) if params[:member_slug].present? @show_all = params[:all] == '1' @@ -17,6 +21,17 @@ class GardensController < DataController respond_with(@gardens) end + # A member's gardens that are no longer active, as a gallery of what they grew. + def past + @owner = Member.confirmed.find_by!(slug: params[:member_slug]) + owned = Garden.inactive.where(owner: @owner) + collaborating = Garden.inactive.where(id: GardenCollaborator.where(member: @owner).select(:garden_id)) + @gardens = owned.or(collaborating) + .includes(:owner, plantings: { crop: { parent: :parent } }) + .order(updated_at: :desc) + .paginate(page: params[:page], per_page: 12) + end + def show @current_plantings = @garden.plantings.current.where.not(failed: true).includes(:crop, :owner).order(planted_at: :desc) @current_activities = @garden.activities.current.includes(:owner).order(created_at: :desc) diff --git a/app/helpers/gardens_helper.rb b/app/helpers/gardens_helper.rb index e70a7c4ed..11c6b6629 100644 --- a/app/helpers/gardens_helper.rb +++ b/app/helpers/gardens_helper.rb @@ -11,6 +11,15 @@ module GardensHelper } end + # "2022–2024 · 12 plantings": when a past garden was in use, by its plantings. + def garden_history_summary(garden) + plantings = garden.plantings.to_a + years = plantings.filter_map { |planting| planting.planted_at&.year } + years = [garden.created_at.year] if years.empty? + range = years.min == years.max ? years.min.to_s : "#{years.min}–#{years.max}" + "#{range} · #{pluralize(plantings.size, 'planting')}" + end + def display_garden_description(garden) if garden.description.nil? "no description provided." diff --git a/app/views/gardens/_past_card.html.haml b/app/views/gardens/_past_card.html.haml new file mode 100644 index 000000000..477cdeab8 --- /dev/null +++ b/app/views/gardens/_past_card.html.haml @@ -0,0 +1,23 @@ +- crops = garden.plantings.map(&:crop).uniq +- shown = 8 +.card.h-100.past-garden-card + = link_to garden_path(garden) do + = image_tag garden_image_path(garden), alt: "Image of #{garden.name}", class: 'card-img-top' + .card-body + %h2.h5.card-title= link_to garden.name, garden_path(garden) + %p.past-garden-meta= garden_history_summary(garden) + - if garden.description.present? + %p.card-text= truncate(garden.description, length: 130, separator: ' ', omission: '...') + - if crops.any? + %ul.past-garden-crops.list-unstyled{'aria-label': 'Grown here'} + - crops.first(shown).each do |crop| + %li + = link_to crop_path(crop) do + .chip.crop-chip + = crop_icon(crop) + = crop.name + - if crops.size > shown + %li.past-garden-more= "and #{crops.size - shown} more" + - if can?(:edit, garden) + .card-footer + = garden_mark_active_button(garden, classes: 'btn btn-sm btn-outline-success') diff --git a/app/views/gardens/past.html.haml b/app/views/gardens/past.html.haml new file mode 100644 index 000000000..6093e97b7 --- /dev/null +++ b/app/views/gardens/past.html.haml @@ -0,0 +1,31 @@ +- content_for :title, "#{@owner}'s past gardens" + +- content_for :breadcrumbs do + %li.breadcrumb-item= link_to 'Gardens', gardens_path + %li.breadcrumb-item= link_to "#{@owner}'s gardens", member_gardens_path(@owner) + %li.breadcrumb-item.active= link_to 'Past gardens', member_past_gardens_path(@owner) + +%h1 #{@owner}'s past gardens + +.row + .col-md-2 + %small + %a{href: "#content"} + Skip to main content + = render 'layouts/nav', model: Garden + = link_to "Current gardens", member_gardens_path(@owner), class: 'nav-link' + %hr/ + = render @owner + + .col-md-10#content + - if @gardens.empty? + %p + There are no past gardens to show. + - if @owner == current_member + A garden you mark as inactive will appear here. + - else + %p.lead Gardens that are no longer in use, and what was grown in them. + .row.row-cols-1.row-cols-sm-2.row-cols-lg-3.g-4 + - @gardens.each do |garden| + .col= render 'gardens/past_card', garden: garden + .mt-3= will_paginate @gardens diff --git a/config/routes.rb b/config/routes.rb index 2ee16c606..59e6f94c8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -115,6 +115,8 @@ Rails.application.routes.draw do resources :timeline resources :members, param: :slug do + # Before the gardens resource, so "past" isn't taken for a garden's id. + get 'gardens/past' => 'gardens#past', as: :past_gardens resources :gardens resources :seeds resources :plantings diff --git a/spec/features/gardens/past_gardens_spec.rb b/spec/features/gardens/past_gardens_spec.rb new file mode 100644 index 000000000..747100231 --- /dev/null +++ b/spec/features/gardens/past_gardens_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe "A member's past gardens" do + let(:owner) { create(:member, login_name: 'gardener') } + let(:tomato) { create(:tomato) } + let!(:retired) { create(:inactive_garden, owner: owner, name: 'Old plot', description: 'Where it all began') } + let!(:current) { create(:garden, owner: owner, name: 'Current plot') } + + before do + create(:planting, owner: owner, garden: retired, crop: tomato, planted_at: Date.new(2021, 10, 1)) + create(:planting, owner: owner, garden: retired, crop: create(:maize), planted_at: Date.new(2023, 2, 1)) + end + + context 'when nobody is signed in' do + before { visit member_past_gardens_path(owner) } + + it 'shows only the inactive gardens, with when they were used and what was grown' do + expect(page).to have_link 'Old plot', href: garden_path(retired) + expect(page).to have_no_text 'Current plot' + expect(page).to have_text '2021–2023 · 2 plantings' + expect(page).to have_text 'Where it all began' + expect(page).to have_link tomato.name, href: crop_path(tomato) + end + + it 'does not offer to reactivate' do + expect(page).to have_no_link 'Mark as active' + end + + it 'links back to the current gardens' do + expect(page).to have_link 'Current gardens', href: member_gardens_path(owner) + end + end + + context 'when the owner is signed in' do + include_context 'signed in member' + let(:owner) { member } + + before { visit member_past_gardens_path(owner) } + + it 'lets them reactivate a garden' do + click_link 'Mark as active' + + expect(retired.reload).to be_active + end + end + + context "when someone else's past gardens" do + include_context 'signed in member' + + before { visit member_past_gardens_path(owner) } + + it 'does not let a signed in visitor reactivate them' do + expect(page).to have_link 'Old plot' + expect(page).to have_no_link 'Mark as active' + end + end + + context 'with no past gardens' do + before { visit member_past_gardens_path(create(:member)) } + + it 'says so' do + expect(page).to have_text 'There are no past gardens to show.' + end + end +end