Compare commits

..

1 Commits

Author SHA1 Message Date
google-labs-jules[bot]
564f5f0dca feat: Convert navbar to Bootstrap 5.1 offcanvas menu
This commit changes the navbar to use the Bootstrap 5.1 offcanvas component.

The following changes were made:
- Modified `app/views/layouts/_header.html.haml` to update the navbar-toggler to trigger the offcanvas menu.
- Modified `app/views/layouts/_menu.haml` to wrap the menu in an offcanvas component.
2025-09-01 07:56:38 +00:00
37 changed files with 248 additions and 633 deletions

View File

@@ -33,29 +33,29 @@ GEM
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
actioncable (7.2.2.2)
actionpack (= 7.2.2.2)
activesupport (= 7.2.2.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
actionmailbox (7.2.2.2)
actionpack (= 7.2.2.2)
activejob (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
mail (>= 2.8.0)
actionmailer (7.2.2.1)
actionpack (= 7.2.2.1)
actionview (= 7.2.2.1)
activejob (= 7.2.2.1)
activesupport (= 7.2.2.1)
actionmailer (7.2.2.2)
actionpack (= 7.2.2.2)
actionview (= 7.2.2.2)
activejob (= 7.2.2.2)
activesupport (= 7.2.2.2)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.2.1)
actionview (= 7.2.2.1)
activesupport (= 7.2.2.1)
actionpack (7.2.2.2)
actionview (= 7.2.2.2)
activesupport (= 7.2.2.2)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
@@ -64,15 +64,15 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.2.1)
actionpack (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
actiontext (7.2.2.2)
actionpack (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.2.1)
activesupport (= 7.2.2.1)
actionview (7.2.2.2)
activesupport (= 7.2.2.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@@ -87,22 +87,22 @@ GEM
active_utils (3.5.0)
activesupport (>= 4.2)
i18n
activejob (7.2.2.1)
activesupport (= 7.2.2.1)
activejob (7.2.2.2)
activesupport (= 7.2.2.2)
globalid (>= 0.3.6)
activemodel (7.2.2.1)
activesupport (= 7.2.2.1)
activerecord (7.2.2.1)
activemodel (= 7.2.2.1)
activesupport (= 7.2.2.1)
activemodel (7.2.2.2)
activesupport (= 7.2.2.2)
activerecord (7.2.2.2)
activemodel (= 7.2.2.2)
activesupport (= 7.2.2.2)
timeout (>= 0.4.0)
activestorage (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
activerecord (= 7.2.2.1)
activesupport (= 7.2.2.1)
activestorage (7.2.2.2)
actionpack (= 7.2.2.2)
activejob (= 7.2.2.2)
activerecord (= 7.2.2.2)
activesupport (= 7.2.2.2)
marcel (~> 1.0)
activesupport (7.2.2.1)
activesupport (7.2.2.2)
base64
benchmark (>= 0.3)
bigdecimal
@@ -251,7 +251,7 @@ GEM
elasticsearch-transport (7.0.0)
faraday
multi_json
erb (5.0.1)
erb (5.0.2)
erubi (1.13.1)
erubis (2.7.0)
excon (1.2.5)
@@ -351,7 +351,7 @@ GEM
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.0)
io-console (0.8.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
@@ -423,14 +423,14 @@ GEM
bigdecimal (~> 3.1)
net-http (0.6.0)
uri
net-imap (0.4.20)
net-imap (0.5.9)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.0)
net-smtp (0.5.1)
net-protocol
netrc (0.11.0)
nio4r (2.7.4)
@@ -499,20 +499,20 @@ GEM
rackup (1.0.1)
rack (< 3)
webrick
rails (7.2.2.1)
actioncable (= 7.2.2.1)
actionmailbox (= 7.2.2.1)
actionmailer (= 7.2.2.1)
actionpack (= 7.2.2.1)
actiontext (= 7.2.2.1)
actionview (= 7.2.2.1)
activejob (= 7.2.2.1)
activemodel (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
rails (7.2.2.2)
actioncable (= 7.2.2.2)
actionmailbox (= 7.2.2.2)
actionmailer (= 7.2.2.2)
actionpack (= 7.2.2.2)
actiontext (= 7.2.2.2)
actionview (= 7.2.2.2)
activejob (= 7.2.2.2)
activemodel (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
bundler (>= 1.15.0)
railties (= 7.2.2.1)
railties (= 7.2.2.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -532,9 +532,9 @@ GEM
rails_stdout_logging
rails_serve_static_assets (0.0.5)
rails_stdout_logging (0.0.5)
railties (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
railties (7.2.2.2)
actionpack (= 7.2.2.2)
activesupport (= 7.2.2.2)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -553,8 +553,8 @@ GEM
recaptcha (5.20.1)
redis-client (0.23.2)
connection_pool
regexp_parser (2.11.1)
reline (0.6.1)
regexp_parser (2.11.2)
reline (0.6.2)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
@@ -582,7 +582,7 @@ GEM
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (8.0.1)
rspec-rails (8.0.2)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
@@ -628,7 +628,7 @@ GEM
rubocop-factory_bot (2.27.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.32.0)
rubocop-rails (2.33.3)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@@ -648,7 +648,7 @@ GEM
ruby-units (4.1.0)
ruby-vips (2.2.1)
ffi (~> 1.12)
rubyzip (2.4.1)
rubyzip (3.0.1)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
@@ -668,11 +668,11 @@ GEM
activemodel (>= 6.1)
hashie
securerandom (0.4.1)
selenium-webdriver (4.34.0)
selenium-webdriver (4.35.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
rubyzip (>= 1.2.2, < 4.0)
websocket (~> 1.0)
sidekiq (7.3.9)
base64
@@ -730,7 +730,8 @@ GEM
rack-test (>= 0.5.3)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.7.6)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
will_paginate (4.0.1)

View File

@@ -78,6 +78,7 @@ class ApplicationController < ActionController::Base
:tos_agreement,
# profile stuff
:bio, :location, :latitude, :longitude,
:website_url, :instagram_handle, :facebook_handle, :bluesky_handle, :other_url,
# email settings
:show_email, :newsletter, :send_notification_email, :send_planting_reminder,
# update password

View File

@@ -75,7 +75,6 @@ class CropsController < ApplicationController
end
def show
@problems = Problem.joins(plantings: :crop).where(crops: { id: @crop.id }).distinct
respond_to do |format|
format.html do
@posts = @crop.posts.order(created_at: :desc).paginate(page: params[:page])

View File

@@ -107,7 +107,7 @@ class PlantingsController < DataController
:crop_id, :description, :garden_id, :planted_at,
:parent_seed_id,
:quantity, :sunniness, :planted_from, :finished,
:finished_at, :failed, problem_ids: []
:finished_at
)
end

View File

@@ -1,76 +0,0 @@
# frozen_string_literal: true
class ProblemsController < ApplicationController
before_action :authenticate_member!, except: %i(index show)
load_and_authorize_resource id_param: :slug, class: Problem
respond_to :html, :json
def index
@problems = Problem.approved.popular.paginate(page: params[:page])
@num_requested_problems = requested_problems.size if current_member
respond_with @problems
end
def requested
@requested = requested_problems.paginate(page: params[:page])
respond_with @requested
end
def show
@problem = Problem.friendly.find(params[:id])
@plantings = @problem.plantings.paginate(page: params[:page])
respond_with @problem
end
def new
@problem = Problem.new
respond_with @problem
end
def edit; end
def create
@problem = Problem.new(problem_params)
if current_member.role? :problem_wrangler
@problem.creator = current_member
else
@problem.requester = current_member
@problem.approval_status = "pending"
end
@problem.save
respond_with @problem
end
def update
if can?(:wrangle, @problem)
@problem.approval_status = 'rejected' if params.fetch("reject", false)
@problem.approval_status = 'approved' if params.fetch("approve", false)
end
@problem.update(problem_params)
respond_with @problem
end
def wrangle
@approval_status = params[:approval_status]
@problems = case @approval_status
when "pending"
Problem.pending_approval
when "rejected"
Problem.rejected
else
Problem.recent
end.paginate(page: params[:page])
@problem_wranglers = Role.problem_wranglers
respond_with @problems
end
private
def problem_params
params.require(:problem).permit(:name, :reason_for_rejection, :rejection_notes)
end
def requested_problems
current_member.requested_problems.pending_approval
end
end

View File

@@ -32,7 +32,7 @@ module ApplicationHelper
# of HAML, Tilt, and dynamic compilation with interpolated ruby.
def markdownify(text)
translator = Haml::Filters::GrowstuffMarkdown.new
translator.expand_members!(translator.expand_problems!(translator.expand_crops!(text.to_s)))
translator.expand_members!(translator.expand_crops!(text.to_s))
end
#

View File

@@ -34,8 +34,6 @@ class Ability
# are wranglers or admins
cannot :read, Crop
can :read, Crop, approval_status: "approved"
cannot :read, Problem
can :read, Problem, approval_status: "approved"
# scientific names should only be viewable if associated crop is approved
cannot :read, ScientificName
can :read, ScientificName do |sn|
@@ -58,8 +56,6 @@ class Ability
# members can see even rejected or pending crops if they requested it
can :read, Crop, requester_id: member.id
can :requested, Crop # see list of crops they've requested
can :read, Problem, requester_id: member.id
can :requested, Problem
# managing your own user settings
can :update, Member, id: member.id
@@ -86,14 +82,8 @@ class Ability
can :gbif, Crop
end
if member.role? :problem_wrangler
can :wrangle, Problem
can :manage, Problem
end
# any member can create a crop provisionally
can :create, Crop
can :create, Problem
# can create & destroy their own authentications against other sites.
can :create, Authentication

View File

@@ -17,7 +17,6 @@ class Crop < ApplicationRecord
has_many :scientific_names, dependent: :delete_all
has_many :alternate_names, dependent: :delete_all
has_many :plantings, dependent: :destroy
has_many :problems, through: :plantings
has_many :seeds, dependent: :destroy
has_many :harvests, dependent: :destroy
has_many :photo_associations, dependent: :delete_all, inverse_of: :crop

View File

@@ -42,10 +42,6 @@ class Member < ApplicationRecord
inverse_of: :requester
has_many :created_crops, class_name: 'Crop', foreign_key: 'creator_id', dependent: :nullify,
inverse_of: :creator
has_many :requested_problems, class_name: 'Problem', foreign_key: 'requester_id', dependent: :nullify,
inverse_of: :requester
has_many :created_problems, class_name: 'Problem', foreign_key: 'creator_id', dependent: :nullify,
inverse_of: :creator
has_many :created_alternate_names, class_name: 'AlternateName', foreign_key: 'creator_id', inverse_of: :creator
has_many :created_scientific_names, class_name: 'ScientificName', foreign_key: 'creator_id', inverse_of: :creator
@@ -95,6 +91,9 @@ class Member < ApplicationRecord
uniqueness: {
case_sensitive: false
}
validates :website_url, format: { with: /\Ahttps?:\/\//, message: "must start with http:// or https://" }, allow_blank: true
validates :other_url, format: { with: /\Ahttps?:\/\//, message: "must start with http:// or https://" }, allow_blank: true
validates :instagram_handle, :facebook_handle, :bluesky_handle, format: { without: %r{\Ahttps?:\/\/|\/}, message: "should be a handle, not a URL" }, allow_blank: true
#
# Triggers

View File

@@ -24,8 +24,6 @@ class Planting < ApplicationRecord
belongs_to :crop, counter_cache: true
has_many :harvests, dependent: :destroy
has_many :activities, dependent: :destroy
has_many :planting_problems, dependent: :destroy
has_many :problems, through: :planting_problems
#
# Ancestry of food

View File

@@ -1,8 +0,0 @@
# frozen_string_literal: true
class PlantingProblem < ApplicationRecord
include PhotoCapable
belongs_to :planting
belongs_to :problem
end

View File

@@ -13,14 +13,11 @@ class Post < ApplicationRecord
has_many :comments, dependent: :destroy
has_many :crop_posts, dependent: :delete_all
has_many :crops, through: :crop_posts
has_many :problem_posts, dependent: :delete_all
has_many :problems, through: :problem_posts
after_create :send_notification
#
# Triggers
after_save :update_crop_posts_association
after_save :update_problem_posts_association
default_scope { joins(:author).merge(Member.kept) } # Ensures the owner still exists
@@ -78,17 +75,6 @@ class Post < ApplicationRecord
end
end
def update_problem_posts_association
problems.clear
# look for problems mentioned in the post. eg. [aphids](problem)
body.scan(Haml::Filters::GrowstuffMarkdown::PROBLEM_REGEX) do |_m|
problem_name = Regexp.last_match(1)
problem = Problem.case_insensitive_name(problem_name).first
# create association
problems << problem if problem && problems.exclude?(problem)
end
end
def send_notification
recipients = []
sender = author.id

View File

@@ -1,88 +0,0 @@
# frozen_string_literal: true
class Problem < ApplicationRecord
extend FriendlyId
include PhotoCapable
include SearchCrops # Note: This might need to be adapted to SearchProblems
friendly_id :name, use: %i(slugged finders)
##
## Relationships
belongs_to :creator, class_name: 'Member', optional: true, inverse_of: :created_problems
belongs_to :requester, class_name: 'Member', optional: true, inverse_of: :requested_problems
has_many :planting_problems, dependent: :delete_all
has_many :plantings, through: :planting_problems
has_many :problem_posts, dependent: :delete_all
has_many :posts, through: :problem_posts, dependent: :delete_all
##
## Scopes
scope :recent, -> { approved.order(created_at: :desc) }
scope :popular, -> { approved.order(Arel.sql("plantings_count desc, lower(name) asc")) }
scope :pending_approval, -> { where(approval_status: "pending") }
scope :approved, -> { where(approval_status: "approved") }
scope :rejected, -> { where(approval_status: "rejected") }
scope :interesting, -> { approved.has_photos }
scope :has_photos, -> { includes(:photos).where.not(photos: { id: nil }) }
##
## Validations
validates :reason_for_rejection, presence: true, if: :rejected?
validate :must_be_rejected_if_rejected_reasons_present
validate :must_have_meaningful_reason_for_rejection
validates :name, uniqueness: { scope: :approval_status }, if: :pending?
def to_s
name
end
def to_param
slug
end
def pending?
approval_status == "pending"
end
def approved?
approval_status == "approved"
end
def rejected?
approval_status == "rejected"
end
def approval_statuses
%w(rejected pending approved)
end
def reasons_for_rejection
["already in database", "not a pest or disease", "not enough information", "other"]
end
def rejection_explanation
return rejection_notes if reason_for_rejection == "other"
reason_for_rejection
end
def self.case_insensitive_name(name)
where(["lower(problems.name) = :value", { value: name.downcase }])
end
private
def must_be_rejected_if_rejected_reasons_present
return if rejected?
return unless reason_for_rejection.present? || rejection_notes.present?
errors.add(:approval_status, "must be rejected if a reason for rejection is present")
end
def must_have_meaningful_reason_for_rejection
return unless reason_for_rejection == "other" && rejection_notes.blank?
errors.add(:rejection_notes, "must be added if the reason for rejection is \"other\"")
end
end

View File

@@ -1,6 +0,0 @@
# frozen_string_literal: true
class ProblemPost < ApplicationRecord
belongs_to :problem
belongs_to :post
end

View File

@@ -8,7 +8,7 @@ class Role < ApplicationRecord
has_and_belongs_to_many :members
class << self
%i(crop_wranglers admins problem_wranglers).each do |method|
%i(crop_wranglers admins).each do |method|
define_method method do
slug = method.to_s.singularize.dasherize
Role.where(slug:).try(:first).try(:members)

View File

@@ -3,3 +3,4 @@
There are
= link_to "https://openfarm.cc/en/crops/#{CGI.escape @crop.name.gsub(' ', '-').downcase}" do
#{crop.guides_count} growing guides on Open Farm

View File

@@ -30,15 +30,6 @@
- @crop.companions.each do |companion|
= render 'crops/tiny', crop: companion
- if @problems.any?
%section.problems
%h2 Problems
- @problems.group_by(&:name).each do |problem_name, problems|
- problem = problems.first
= link_to problem_path(problem) do
= problem_name
%span.badge.badge-secondary= problems.size
%section.photos
= cute_icon
= render 'crops/photos', crop: @crop

View File

@@ -13,7 +13,32 @@
.form-group
= f.label :bio, class: 'control-label col-md-2'
.col-md-8
= f.text_area :bio, rows: 6, class: 'form-control'
= f.text_area :bio, rows: 6, class: 'form-control', placeholder: "I'm am XYZ gardener interested in A, B, C"
.form-group
= f.label :website_url, 'Website', class: 'control-label col-md-2'
.col-md-8
= f.url_field :website_url, class: 'form-control', placeholder: "https://you.example.com/"
.form-group
= f.label :instagram_handle, 'Instagram', class: 'control-label col-md-2'
.col-md-8
= f.text_field :instagram_handle, class: 'form-control', placeholder: 'your_handle'
.form-group
= f.label :facebook_handle, 'Facebook', class: 'control-label col-md-2'
.col-md-8
= f.text_field :facebook_handle, class: 'form-control', placeholder: 'your_handle'
.form-group
= f.label :bluesky_handle, 'Bluesky', class: 'control-label col-md-2'
.col-md-8
= f.text_field :bluesky_handle, class: 'form-control', placeholder: 'your_handle'
.form-group
= f.label :other_url, 'Other URL', class: 'control-label col-md-2'
.col-md-8
= f.url_field :other_url, class: 'form-control', placeholder: "https://you.example.com/"
.form-group
%label.control-label.col-md-2

View File

@@ -5,3 +5,5 @@
number_crops: link_to(t('.number_crops_linktext', count: Crop.count.to_i), crops_path),
number_plantings: link_to(t('.number_plantings_linktext', count: Planting.count.to_i), plantings_path),
number_gardens: link_to(t('.number_gardens_linktext', count: Garden.count.to_i), gardens_path))

View File

@@ -6,6 +6,6 @@
%span.site-name Growstuff
.nav= render 'crops/search_bar'
.nav
%button.navbar-toggler{ "aria-controls" => "navbarSupportedContent", "aria-expanded" => "false", "aria-label" => "Toggle navigation", "data-bs-target" => "#navbarSupportedContent", "data-bs-toggle" => "collapse", type: "button" }
%button.navbar-toggler{ "aria-controls" => "offcanvasNavbar", "aria-label" => "Toggle navigation", "data-bs-target" => "#offcanvasNavbar", "data-bs-toggle" => "offcanvas", type: "button" }
%i.fas.fa-ellipsis-v.navbar-toggler-icon
= render 'layouts/menu'

View File

@@ -1,98 +1,102 @@
#navbarSupportedContent.collapse.navbar-collapse
%ul.navbar-nav.mr-auto
- if signed_in?
%li.nav-item
= link_to timeline_index_path, method: :get, class: 'nav-link text-white' do
= image_tag 'icons/notification.svg', class: 'img img-icon', alt: "Notifications"
%li.nav-item
= link_to member_gardens_path(current_member), class: 'nav-link text-white', title: "My gardens" do
= image_icon 'gardens'
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}
= image_tag "icons/gardener.svg", class: 'img img-icon', alt: t('.record'), aria: { hidden: "true" }
= t('.record')
.dropdown-menu
= link_to new_planting_path, class: 'dropdown-item' do
= image_icon('planting-add')
= t('buttons.new_planting')
= link_to new_harvest_path, class: 'dropdown-item' do
= image_icon('harvest-add')
= t('buttons.new_harvest')
= link_to new_activity_path, class: 'dropdown-item' do
= image_icon('activity-add')
= t('buttons.new_activity')
= link_to new_seed_path, class: 'dropdown-item' do
= image_icon('seed-add')
= t('buttons.new_seeds')
= link_to new_post_path, class: 'dropdown-item' do
= post_icon
= t('buttons.new_post')
- cache("everyone-menu", expires_in: 1.week) do
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.crops')
.dropdown-menu
= link_to crops_path, class: 'dropdown-item' do
= t('.browse_crops')
= link_to seeds_path, class: 'dropdown-item' do
= seed_icon
= t('.seeds')
= link_to plantings_path, class: 'dropdown-item' do
= planting_icon
= t('.plantings')
= link_to harvests_path, class: 'dropdown-item' do
= harvest_icon
= t('.harvests')
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.community')
.dropdown-menu{"aria-labelledby" => "navbarDropdown"}
= link_to t('.community_map'), places_path, class: 'dropdown-item'
= link_to t('.browse_members'), members_path, class: 'dropdown-item'
= link_to t('.posts'), posts_path, class: 'dropdown-item'
= link_to t('.forums'), forums_path, class: 'dropdown-item'
- if member_signed_in?
- if current_member.role?(:crop_wrangler) || current_member.role?(:admin)
.offcanvas.offcanvas-start#offcanvasNavbar{ 'aria-labelledby' => 'offcanvasNavbarLabel', 'tabindex' => '-1' }
.offcanvas-header
%h5.offcanvas-title#offcanvasNavbarLabel Menu
%button.btn-close.text-reset{ 'data-bs-dismiss' => 'offcanvas', 'aria-label' => 'Close' }
.offcanvas-body
%ul.navbar-nav.mr-auto
- if signed_in?
%li.nav-item
= link_to timeline_index_path, method: :get, class: 'nav-link' do
= image_tag 'icons/notification.svg', class: 'img img-icon', alt: "Notifications"
%li.nav-item
= link_to member_gardens_path(current_member), class: 'nav-link', title: "My gardens" do
= image_icon 'gardens'
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.admin')
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}
= image_tag "icons/gardener.svg", class: 'img img-icon', alt: t('.record'), aria: { hidden: "true" }
= t('.record')
.dropdown-menu
= link_to new_planting_path, class: 'dropdown-item' do
= image_icon('planting-add')
= t('buttons.new_planting')
= link_to new_harvest_path, class: 'dropdown-item' do
= image_icon('harvest-add')
= t('buttons.new_harvest')
= link_to new_activity_path, class: 'dropdown-item' do
= image_icon('activity-add')
= t('buttons.new_activity')
= link_to new_seed_path, class: 'dropdown-item' do
= image_icon('seed-add')
= t('buttons.new_seeds')
= link_to new_post_path, class: 'dropdown-item' do
= post_icon
= t('buttons.new_post')
- cache("everyone-menu", expires_in: 1.week) do
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.crops')
.dropdown-menu
= link_to crops_path, class: 'dropdown-item' do
= t('.browse_crops')
= link_to seeds_path, class: 'dropdown-item' do
= seed_icon
= t('.seeds')
= link_to plantings_path, class: 'dropdown-item' do
= planting_icon
= t('.plantings')
= link_to harvests_path, class: 'dropdown-item' do
= harvest_icon
= t('.harvests')
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.community')
.dropdown-menu{"aria-labelledby" => "navbarDropdown"}
- if current_member.role?(:crop_wrangler)
= link_to t('.crop_wrangling'), wrangle_crops_path, class: 'dropdown-item'
- if current_member.role?(:admin)
= link_to t('.admin'), admin_path, class: 'dropdown-item'
= link_to t('.community_map'), places_path, class: 'dropdown-item'
= link_to t('.browse_members'), members_path, class: 'dropdown-item'
= link_to t('.posts'), posts_path, class: 'dropdown-item'
= link_to t('.forums'), forums_path, class: 'dropdown-item'
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}
= image_tag(avatar_uri(current_member, 50), alt: 'Avatar of current member', height: 25, width: 25, aria: { hidden: "true" })
= current_member.login_name
- if current_member.unread_count.positive?
%span.badge.badge-info= current_member.unread_count
.dropdown-menu{"aria-labelledby" => "navbarDropdown"}
= link_to member_path(current_member), class: 'dropdown-item' do
= t('.profile')
= link_to member_activities_path(current_member), class: 'dropdown-item' do
= t('.activities')
= link_to member_gardens_path(current_member), class: 'dropdown-item' do
= t('.gardens')
= link_to member_plantings_path(current_member), class: 'dropdown-item' do
= t('.plantings')
= link_to member_harvests_path(current_member), class: 'dropdown-item' do
= t('.harvest')
= link_to member_seeds_path(current_member), class: 'dropdown-item' do
= t('.seeds')
= link_to t('.posts'), member_posts_path(current_member), class: 'dropdown-item'
- if member_signed_in?
- if current_member.role?(:crop_wrangler) || current_member.role?(:admin)
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}= t('.admin')
.dropdown-menu{"aria-labelledby" => "navbarDropdown"}
- if current_member.role?(:crop_wrangler)
= link_to t('.crop_wrangling'), wrangle_crops_path, class: 'dropdown-item'
- if current_member.role?(:admin)
= link_to t('.admin'), admin_path, class: 'dropdown-item'
- if current_member.unread_count.positive?
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", href: "#", role: "button"}
= image_tag(avatar_uri(current_member, 50), alt: 'Avatar of current member', height: 25, width: 25, aria: { hidden: "true" })
= current_member.login_name
- if current_member.unread_count.positive?
%span.badge.badge-info= current_member.unread_count
.dropdown-menu{"aria-labelledby" => "navbarDropdown"}
= link_to member_path(current_member), class: 'dropdown-item' do
= t('.profile')
= link_to member_activities_path(current_member), class: 'dropdown-item' do
= t('.activities')
= link_to member_gardens_path(current_member), class: 'dropdown-item' do
= t('.gardens')
= link_to member_plantings_path(current_member), class: 'dropdown-item' do
= t('.plantings')
= link_to member_harvests_path(current_member), class: 'dropdown-item' do
= t('.harvest')
= link_to member_seeds_path(current_member), class: 'dropdown-item' do
= t('.seeds')
= link_to t('.posts'), member_posts_path(current_member), class: 'dropdown-item'
- if current_member.unread_count.positive?
.dropdown-divider
%strong
= link_to(conversations_path, class: 'dropdown-item') do
= t('.inbox')
%span.badge.badge-info= current_member.unread_count
- else
= link_to t('.inbox'), conversations_path, class: 'dropdown-item'
.dropdown-divider
%strong
= link_to(conversations_path, class: 'dropdown-item') do
= t('.inbox')
%span.badge.badge-info= current_member.unread_count
- else
= link_to t('.inbox'), conversations_path, class: 'dropdown-item'
.dropdown-divider
= link_to t('.sign_out'), destroy_member_session_path, method: :delete, class: 'dropdown-item'
= link_to t('.sign_out'), destroy_member_session_path, method: :delete, class: 'dropdown-item'
- else
%li.nav-item= link_to t('.sign_in'), new_member_session_path, id: 'navbar-signin', class: 'btn btn-signin'
%li.nav-item= link_to t('.sign_up'), new_member_registration_path, id: 'navbar-signup', class: 'btn btn-signup'
- else
%li.nav-item= link_to t('.sign_in'), new_member_session_path, id: 'navbar-signin', class: 'btn btn-signin'
%li.nav-item= link_to t('.sign_up'), new_member_registration_path, id: 'navbar-signup', class: 'btn btn-signup'

View File

@@ -1,17 +1,42 @@
- if twitter_auth || flickr_auth || member.show_email
- if member.website_url.present? || member.instagram_handle.present? || member.facebook_handle.present? || member.bluesky_handle.present? || member.other_url.present? || twitter_auth || flickr_auth || member.show_email
%h4 Contact
- if member.website_url.present?
%p
= icon 'fas', 'globe', class: 'fa-fw'
= link_to "Website", member.website_url, target: '_blank', rel: 'noopener noreferrer'
- if member.instagram_handle.present?
%p
= icon 'fab', 'instagram', class: 'fa-fw'
= link_to member.instagram_handle, "https://instagram.com/#{member.instagram_handle}", target: '_blank', rel: 'noopener noreferrer'
- if member.facebook_handle.present?
%p
= icon 'fab', 'facebook', class: 'fa-fw'
= link_to member.facebook_handle, "https://facebook.com/#{member.facebook_handle}", target: '_blank', rel: 'noopener noreferrer'
- if member.bluesky_handle.present?
%p
= icon 'fas', 'comment-dots', class: 'fa-fw'
= link_to member.bluesky_handle, "https://bsky.app/profile/#{member.bluesky_handle}", target: '_blank', rel: 'noopener noreferrer'
- if member.other_url.present?
%p
= icon 'fas', 'link', class: 'fa-fw'
= link_to "More...", member.other_url, target: '_blank', rel: 'noopener noreferrer'
- if twitter_auth
%p
= image_tag "twitter_32.png", size: "32x32", alt: 'Twitter logo'
= link_to twitter_auth.name, "https://twitter.com/#{twitter_auth.name}"
= link_to twitter_auth.name, "https://twitter.com/#{twitter_auth.name}", target: '_blank', rel: 'noopener noreferrer'
- if flickr_auth
%p
= image_tag "flickr_32.png", size: "32x32", alt: 'Flickr logo'
= link_to flickr_auth.name, "https://flickr.com/photos/#{flickr_auth.uid}"
= link_to flickr_auth.name, "https://flickr.com/photos/#{flickr_auth.uid}", target: '_blank', rel: 'noopener noreferrer'
- if member.show_email
%p
Email:
= icon 'fas', 'envelope', class: 'fa-fw'
= mail_to member.email

View File

@@ -47,11 +47,6 @@
= f.number_field :quantity, label: 'How many?', min: 1
= f.text_area :description, rows: 6, label: 'Tell us more about it'
= f.collection_check_boxes :problem_ids, Problem.approved.order(:name), :id, :name do |b|
.form-check
= b.check_box
= b.label
.row
.col-md-6
= f.check_box :finished, label: 'Mark as finished'

View File

@@ -51,23 +51,6 @@
.col-md-8.col-xs-12
%section= render 'facts', planting: @planting
- if @planting.problems.any?
%section.problems
%h2 Problems
- @planting.planting_problems.each do |planting_problem|
.card
.card-header
%h3= planting_problem.problem.name
.card-body
- if planting_problem.photos.any?
.row
- planting_problem.photos.each do |photo|
.col-md-4
= image_tag photo.thumbnail_url, class: 'img-fluid'
- else
%p No photos of this problem for this planting yet.
- if @planting.description.present?
= cute_icon
.card

View File

@@ -9,3 +9,4 @@
- else
%h2 There are no comments yet

View File

@@ -1,54 +0,0 @@
= bootstrap_form_for(@problem) do |f|
- if @problem.errors.any?
#error_explanation.alert.alert-warning{role: "alert"}
%h3
= pluralize(@problem.errors.size, "error")
prohibited this problem from being saved:
%ul
- @problem.errors.full_messages.each do |msg|
%li= msg
.card.col-12.col-md-8.mx-auto.float-none.white
.card-header
- if content_for? :title
%h1.h2-responsive.text-center
%strong=yield :title
.card-body
- if can? :wrangle, @problem
%p
%span.help-block
As a problem wrangler, you can approve or reject problem suggestions.
%h2 Basic information
.form-group#new_problem
= f.text_field :name, required: true
%span.help-block
The common name for the problem, in English (required).
- if can? :wrangle, @problem
Wranglers: please ensure this is singular, and capitalize
proper nouns only.
- if (can?(:wrangle, @problem) && @problem.requester) || (cannot?(:wrangle, @problem) && @problem.new_record?)
%h2 Problem request notes
= f.text_area :request_notes, rows: 3, id: 'request_notes', label: 'Comments'
- unless can? :wrangle, @problem
%p
When you submit this form, your suggestion will be sent to our team of
volunteer problem wranglers for review. We'll let you know the outcome as soon as we can.
- if can?(:wrangle, @problem) && @problem.requester
= f.select(:reason_for_rejection, @problem.reasons_for_rejection, include_blank: true)
= f.text_area :rejection_notes, rows: 3
%span.help-block
Please provide additional notes why this problem request was rejected if the above reasons do not apply.
.card-footer
.text-right
- if @problem.approved?
= f.submit 'Save'
- else
= f.submit 'Reject', class: 'btn btn-danger', name: 'reject'
= f.submit 'Approve and save', class: 'btn btn-success', name: 'approve'

View File

@@ -1,10 +0,0 @@
- cache problem do
.card.problem-thumbnail
= link_to problem_path(id: problem.slug) do
= image_tag(problem.thumbnail_url.presence || placeholder_image,
alt: "Image of #{problem.name}",
class: 'img img-card')
.text
%h3.problem-name= link_to problem.name, problem_path(id: problem.slug)

View File

@@ -1,30 +0,0 @@
- content_for :title, "Edit problem: #{@problem.name}"
- if @problem.approval_status == "approved"
- if @problem.requester
%p
Requested by #{link_to @problem.requester, @problem.requester}
#{distance_of_time_in_words(@problem.created_at, Time.zone.now)} ago.
%p
Approved by #{link_to @problem.creator, @problem.creator}.
- else
%p
Added by
= link_to @problem.creator, @problem.creator
#{distance_of_time_in_words(@problem.created_at, Time.zone.now)} ago.
- elsif @problem.approval_status == "pending"
.alert.alert-danger
%p
Requested by #{link_to @problem.requester, @problem.requester}
#{distance_of_time_in_words(@problem.created_at, Time.zone.now)} ago.
%p
Status: #{@problem.approval_status}.
- elsif @problem.approval_status == "rejected"
.alert.alert-danger
%p
Requested by #{link_to @problem.requester, @problem.requester}
#{distance_of_time_in_words(@problem.created_at, Time.zone.now)} ago.
%p
Status: #{@problem.approval_status} by #{link_to @problem.creator, @problem.creator}.
= render 'form'

View File

@@ -1,13 +0,0 @@
- content_for :title, t('.title')
- content_for :breadcrumbs do
%li.breadcrumb-item.active= link_to 'Problems', problems_path
%section.problems
%h2= t('.title')
= will_paginate @problems
.index-cards
- @problems.each do |p|
= render 'problems/thumbnail', problem: p
= will_paginate @problems

View File

@@ -1,15 +0,0 @@
- content_for :title, (can?(:wrangle, @problem) ? "New problem" : "Suggest a problem")
- unless can? :wrangle, @problem
%p
Thanks for taking the time to suggest a problem! Our problem database is
managed by volunteers, and we appreciate your help. Here are some
things to consider when suggesting a new problem:
%ul
%li
First, you might want to search our problems
to make sure we don't have it already, perhaps under an alternate name.
%li
The Growstuff database only contains problems related to growing edible plants.
= render 'form'

View File

@@ -1,26 +0,0 @@
- content_for :title, @problem.name
- content_for :breadcrumbs do
%li.breadcrumb-item= link_to 'Problems', problems_path
%li.breadcrumb-item.active= link_to @problem.name.capitalize, @problem
.jumbotron
%h1= @problem.name
.row
.col-md-9
%section.plantings
%h2 Plantings with this problem
= will_paginate @plantings
.index-cards
- @plantings.each do |p|
= render 'plantings/thumbnail', planting: p
= will_paginate @plantings
.col-md-3
.card
.card-body
%h4.card-title= @problem.name
%p.card-text
This problem has been reported on
= pluralize(@problem.plantings.count, 'planting')
so far.

View File

@@ -1,51 +0,0 @@
- content_for :title, "Problem Wrangling"
%h1 Problem Wrangling
%nav.nav
= link_to "Add Problem", new_problem_path, class: 'btn'
%section.problem_wranglers
%h2 Problem Wranglers
- @problem_wranglers.each do |problem_wrangler|
= render 'members/tiny', member: problem_wrangler
%hr/
%section
%h2 Problems
%ul#myTab.nav.nav-tabs{role: "tablist"}
%li.nav-item
%a#home-tab.nav-link{ href: wrangle_problems_path, role: "tab", class: @approval_status.blank? ? 'active' : ''}
Recently added
%li.nav-item
%a#profile-tab.nav-link{ href: wrangle_problems_path(approval_status: "pending"), role: "tab", class: @approval_status == "pending" ? 'active' : ''}
Pending approval
%li.nav-item
%a#contact-tab.nav-link{ href: wrangle_problems_path(approval_status: "rejected"), role: "tab", class: @approval_status == "rejected" ? 'active' : ''} Rejected
%table.table.table-striped.table-bordered.table-sm{id: @approval_status.blank? ? 'recently-added-problems' : "#{@approval_status}-problems" }
%tr
%th Name
%th Requested by
- if @approval_status == "rejected"
%th Rejected by
- if @approval_status != "rejected" && @approval_status != "pending"
%th Added by
%th When
- @problems.each do |p|
%tr
%td
= link_to edit_problem_path(p) do
= icon 'fas', 'bug'
= p.name
%td= p.requester.present? ? (link_to p.requester, p.requester) : "N/A"
- unless @approval_status == "pending"
%td= p.creator.present? ? (link_to p.creator, p.creator) : "N/A"
%td
= distance_of_time_in_words(p.created_at, Time.zone.now)
ago.
= page_entries_info @problems
= will_paginate @problems

View File

@@ -89,13 +89,6 @@ Rails.application.routes.draw do
end
end
resources :problems, param: :slug do
collection do
get 'requested'
get 'wrangle'
end
end
resources :comments
resources :forums

View File

@@ -0,0 +1,9 @@
class AddSocialMediaToMembers < ActiveRecord::Migration[6.0]
def change
add_column :members, :website_url, :string
add_column :members, :instagram_handle, :string
add_column :members, :facebook_handle, :string
add_column :members, :bluesky_handle, :string
add_column :members, :other_handle, :string
end
end

View File

@@ -0,0 +1,5 @@
class RenameOtherHandleToOtherUrlInMembers < ActiveRecord::Migration[6.0]
def change
rename_column :members, :other_handle, :other_url
end
end

View File

@@ -446,6 +446,11 @@ ActiveRecord::Schema[7.2].define(version: 2024_09_29_041435) do
t.integer "photos_count"
t.integer "forums_count"
t.integer "activities_count"
t.string "website_url"
t.string "instagram_handle"
t.string "facebook_handle"
t.string "bluesky_handle"
t.string "other_url"
t.index ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true
t.index ["discarded_at"], name: "index_members_on_discarded_at"
t.index ["email"], name: "index_members_on_email", unique: true

View File

@@ -4,7 +4,6 @@
class Haml::Filters
class GrowstuffMarkdown
CROP_REGEX = /(?<!\\)\[([^\[\]]+?)\]\(crop\)/
PROBLEM_REGEX = /(?<!\\)\[([^\[\]]+?)\]\(problem\)/
MEMBER_REGEX = /(?<!\\)\[([^\[\]]+?)\]\(member\)/
MEMBER_AT_REGEX = /(?<!\\)(@\w+)/
MEMBER_ESCAPE_AT_REGEX = /(?<!\\)\\(?=@\w+)/
@@ -43,25 +42,6 @@ class Haml::Filters
end
end
def expand_problems!(text)
# turn [aphids](problem) into [aphids](http://growstuff.org/problems/aphids)
text.gsub(PROBLEM_REGEX) do
problem_str = Regexp.last_match(1)
# find problem case-insensitively
problem = Problem.where('lower(name) = ?', problem_str.downcase).first
problem_link problem, problem_str
end
end
def problem_link(problem, link_text)
if problem
url = Rails.application.routes.url_helpers.problem_url(problem, only_path: true)
"[#{link_text}](#{url})"
else
link_text
end
end
def crop_link(crop, link_text)
if crop
url = Rails.application.routes.url_helpers.crop_url(crop, only_path: true)