From fa33d7b911cbf53b2ddd5a4e80d1bdd5655d4b9f Mon Sep 17 00:00:00 2001 From: Brenda Wallace Date: Mon, 19 Oct 2026 17:45:18 +1300 Subject: [PATCH] Add seasonal features to neighbours: implement season markers and headings, enhance activity filtering for 'me' scope, and update styles for better visibility. --- app/assets/stylesheets/_neighbours.scss | 12 ++++++++ app/helpers/neighbours_helper.rb | 31 +++++++++++++++++++ app/services/neighbours.rb | 22 ++++++++++++-- app/views/neighbours/_days.html.haml | 8 ++++- config/locales/en.yml | 9 ++++++ spec/features/neighbours_spec.rb | 27 +++++++++++++++++ spec/helpers/neighbours_helper_spec.rb | 35 ++++++++++++++++++++++ spec/services/neighbours_spec.rb | 40 +++++++++++++++++++++++++ 8 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 spec/helpers/neighbours_helper_spec.rb diff --git a/app/assets/stylesheets/_neighbours.scss b/app/assets/stylesheets/_neighbours.scss index 7f4d7d258..2c0a25afd 100644 --- a/app/assets/stylesheets/_neighbours.scss +++ b/app/assets/stylesheets/_neighbours.scss @@ -21,6 +21,18 @@ .neighbours-side { flex: 1 1 300px; min-width: 0; } +.neighbours-season { + margin: 28px 0 4px; + padding: 6px 12px; + font-size: 0.95rem; + font-weight: 700; + color: #fff; + background: #3f5e2b; + border-radius: 4px; + + &:first-child { margin-top: 0; } +} + .neighbours-day { display: flex; gap: 10px; diff --git a/app/helpers/neighbours_helper.rb b/app/helpers/neighbours_helper.rb index 947882919..d6a48b0b8 100644 --- a/app/helpers/neighbours_helper.rb +++ b/app/helpers/neighbours_helper.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module NeighboursHelper + include SeasonsHelper + PICTURES_SHOWN = 8 LAUNCHED_ON = Date.new(2026, 10, 14) NEW_FOR = 3.months @@ -25,6 +27,31 @@ module NeighboursHelper day == today - 1 ? t('neighbours.day.yesterday', date:) : date end + SEASON_STARTS = [3, 6, 9, 12].freeze + + # The first day of the season a date is in. Both hemispheres change season + # on the same days, so this tells us where a marker goes whichever half of + # the world someone gardens in. + def season_start(date) + month = SEASON_STARTS.select { |start| start <= date.month }.max + month ? Date.new(date.year, month, 1) : Date.new(date.year - 1, 12, 1) + end + + # "Spring 2026", or "Spring 2026 · Autumn in the northern hemisphere" when + # the feed has people in both halves of the world. Their own hemisphere is + # not named, unless they have not said where they live. + def season_heading(start, neighbours) + hemispheres = neighbours.hemispheres + return if hemispheres.empty? + + mine = hemisphere(neighbours.member.latitude) + names = hemispheres.map do |half| + season = t("seasons.#{season_in(half, start)}") + hemispheres.one? || half == mine ? season : t('neighbours.season.in_hemisphere', season:, hemisphere: t("neighbours.season.#{half}")) + end + "#{names.first} #{start.year}#{names.drop(1).map { |name| " · #{name}" }.join}" + end + # Where someone is and when they did it: "Upper Hutt · 2 hours ago", or # "from 7:40am" when they did several things in a day. def entry_when(entry) @@ -61,6 +88,10 @@ module NeighboursHelper end end + def season_in(hemisphere, start) + SEASON_MONTHS.fetch(hemisphere).find { |_season, months| months.include?(start.month) }.first + end + def subject_label(subject) case subject when Garden then t('neighbours.subject.garden', name: subject.name) diff --git a/app/services/neighbours.rb b/app/services/neighbours.rb index 352f66835..65f8b15d1 100644 --- a/app/services/neighbours.rb +++ b/app/services/neighbours.rb @@ -5,7 +5,9 @@ # Nearby widens in rings, close by first and then further out, until there are # enough people to show. class Neighbours - SCOPES = %w(auto following nearby anywhere).freeze + include SeasonsHelper + + SCOPES = %w(auto following nearby anywhere me).freeze RADII_KM = [25, 150].freeze WORLD_KM = 20_000 MIN_NEARBY = 3 @@ -66,7 +68,7 @@ class Neighbours end def shows_nearby? - scope != 'following' + %w(following me).exclude?(scope) end def located? @@ -117,7 +119,7 @@ class Neighbours # Who the page is about, for its headings: people they follow and people # near them, or only one, or nearby once it has widened to somewhere. def audience - return :following if scope == 'following' + return scope.to_sym if %w(following me).include?(scope) return (widened? ? :all : :following_and_near) if followed.any? && shows_following? reach || :near @@ -125,6 +127,8 @@ class Neighbours # What to say under the title: what they follow and how nearby went. def lede + return :me if scope == 'me' + if followed.empty? then nobody_close? ? :widened : :none elsif few_followed? then :few else :following @@ -145,6 +149,16 @@ class Neighbours @feed end + # The hemispheres the feed spans, their own first, so each season marker can + # name the season in each. Empty when nobody in it has said where they live. + def hemispheres + @hemispheres ||= begin + latitudes = Member.where(id: feed_owner_ids).where.not(latitude: nil).distinct.pluck(Arel.sql('latitude < 0')) + theirs = latitudes.map { |south| south ? :southern : :northern } + ([hemisphere(member.latitude)] + theirs).compact.uniq + end + end + def more_feed? build_feed unless defined?(@feed) @more_feed @@ -185,6 +199,8 @@ class Neighbours end def feed_owner_ids + return [member.id] if scope == 'me' + @feed_owner_ids ||= (shows_following? ? followed_ids : []) + nearby_members.map(&:id) end diff --git a/app/views/neighbours/_days.html.haml b/app/views/neighbours/_days.html.haml index d5b5c591b..84197260c 100644 --- a/app/views/neighbours/_days.html.haml +++ b/app/views/neighbours/_days.html.haml @@ -1,6 +1,12 @@ -# Days of the feed. `after` is the last day already on the page, so a day that --# carries on from it does not get a second heading. +-# carries on from it does not get a second heading, and neither does a season +-# marker when it is still the same season. +- previous_season = after && season_start(after) - neighbours.feed.each_with_index do |day, index| + - season = season_start(day.day) + - if season != previous_season && (heading = season_heading(season, neighbours)) + %h2.neighbours-season= heading + - previous_season = season - unless index.zero? && day.day == after %h3.neighbours-day= day_heading(day.day) = render partial: 'neighbours/entry', collection: day.updates, as: :entry diff --git a/config/locales/en.yml b/config/locales/en.yml index 83854bf66..b665d9c7b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -497,6 +497,9 @@ en: following_and_near: feed: People you follow and gardeners near you posts: From people you follow and near you + me: + feed: Just you + posts: From you near: feed: Gardeners near you posts: From gardeners near you @@ -552,6 +555,10 @@ en: region: Within %{km} km widened: We widened the search until we found some. you: you + season: + in_hemisphere: "%{season} in the %{hemisphere} hemisphere" + northern: northern + southern: southern show: add_location_html: "%{link} to see gardeners near you." add_location_link: Add where you live @@ -559,12 +566,14 @@ en: lede: few: You follow a few people. Showing them first, then those close to you. following: What the gardeners you follow, and the ones near you, are up to. + me: Just what you have been up to. none: You are not following anyone yet, so this shows gardeners near you. widened: Nobody you follow, and nobody close by yet, so we looked further out. scope: anywhere: Anywhere auto: Auto following: People I follow + me: Me nearby: Nearby title: My friends & neighbours welcome: diff --git a/spec/features/neighbours_spec.rb b/spec/features/neighbours_spec.rb index 38eee2180..bc73cefd0 100644 --- a/spec/features/neighbours_spec.rb +++ b/spec/features/neighbours_spec.rb @@ -68,6 +68,33 @@ describe 'My friends and neighbours' do expect(page).to have_css('article', text: /friendly.*Planned an activity, Weed the beans/m) end + it 'shows only their own activity under Me' do + friend = create(:member, login_name: 'friendly') + member.follows.create!(followed: friend) + create(:planting, owner: friend, garden: friend.gardens.first, planted_at: Time.zone.today) + create(:harvest, owner: member, harvested_at: Time.zone.today) + + visit neighbours_path(scope: 'me') + expect(page).to have_css('article', text: /Harvested/).and have_no_css('article', text: /friendly/) + end + + it 'marks where the season changes' do + create(:harvest, owner: member, harvested_at: Date.new(2026, 10, 20)) + create(:harvest, owner: member, harvested_at: Date.new(2026, 8, 20)) + + visit neighbours_path(scope: 'me') + expect(page).to have_css('h2.neighbours-season', text: 'Spring 2026').and have_css('h2.neighbours-season', text: 'Winter 2026') + end + + it 'names the season in the other hemisphere too' do + friend = create_located_member('Oslo', 59.9, 10.7, login_name: 'norwegian') + member.follows.create!(followed: friend) + create(:harvest, owner: friend, harvested_at: Date.new(2026, 10, 20)) + + visit neighbours_path + expect(page).to have_css('h2.neighbours-season', text: 'Spring 2026 · Autumn in the northern hemisphere') + end + it 'stops suggesting someone who is dismissed' do crop = create(:crop) create(:planting, owner: member, garden: member.gardens.first, crop:) diff --git a/spec/helpers/neighbours_helper_spec.rb b/spec/helpers/neighbours_helper_spec.rb new file mode 100644 index 000000000..26cdf99c4 --- /dev/null +++ b/spec/helpers/neighbours_helper_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe NeighboursHelper do + describe '#season_start' do + it 'is the first of the month a season begins in' do + expect(helper.season_start(Date.new(2026, 10, 20))).to eq Date.new(2026, 9, 1) + end + + it 'belongs to the year before in January' do + expect(helper.season_start(Date.new(2027, 1, 15))).to eq Date.new(2026, 12, 1) + end + end + + describe '#season_heading' do + let(:start) { Date.new(2026, 12, 1) } + let(:member) { build(:member, latitude: -41.29) } + + it 'names the season in the southern hemisphere' do + neighbours = instance_double(Neighbours, member:, hemispheres: [:southern]) + expect(helper.season_heading(start, neighbours)).to eq 'Summer 2026' + end + + it 'names both when the feed spans the world' do + neighbours = instance_double(Neighbours, member:, hemispheres: %i(southern northern)) + expect(helper.season_heading(start, neighbours)).to eq 'Summer 2026 · Winter in the northern hemisphere' + end + + it 'is nothing when nobody has said where they live' do + neighbours = instance_double(Neighbours, member:, hemispheres: []) + expect(helper.season_heading(start, neighbours)).to be_nil + end + end +end diff --git a/spec/services/neighbours_spec.rb b/spec/services/neighbours_spec.rb index 9f088ce14..ff77c8284 100644 --- a/spec/services/neighbours_spec.rb +++ b/spec/services/neighbours_spec.rb @@ -161,6 +161,46 @@ describe Neighbours do end end + describe 'with the me scope' do + subject(:neighbours) { described_class.new(member, scope: 'me') } + + let(:friend) { create(:member) } + + before { member.follows.create!(followed: friend) } + + it 'has what the member did' do + planting = create(:planting, owner: member, garden: member.gardens.first, planted_at: Date.new(2026, 10, 1)) + + expect(neighbours.feed.flat_map(&:updates).flat_map(&:items).map(&:record)).to eq [planting] + end + + it 'leaves out the people they follow' do + create(:planting, owner: friend, garden: friend.gardens.first, planted_at: Date.new(2026, 10, 1)) + + expect(neighbours.feed).to be_empty + end + + it 'does not look for people nearby' do + expect(neighbours.shows_nearby?).to be false + end + end + + describe '#hemispheres' do + it 'has their own first, then the other' do + friend = create_located_member('Oslo', 59.9, 10.7) + member.follows.create!(followed: friend) + + expect(described_class.new(member).hemispheres).to eq %i(southern northern) + end + + it 'has one when everyone is in the same half' do + friend = create_located_member('Auckland', -36.85, 174.76) + member.follows.create!(followed: friend) + + expect(described_class.new(member).hemispheres).to eq [:southern] + end + end + describe '#feed photos and layouts' do let(:friend) { create(:member) } let(:planting) { create(:planting, owner: friend, garden: friend.gardens.first, planted_at: Date.new(2026, 10, 1)) }