diff --git a/Gemfile b/Gemfile
index d279a87b8..53b8bded4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -69,6 +69,7 @@ gem 'turbo-rails'
# Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem 'stimulus-rails'
gem 'public_activity'
+gem 'view_component'
group :development do
gem 'better_errors'
diff --git a/Gemfile.lock b/Gemfile.lock
index c50679c92..e7c6dc5f5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -584,6 +584,10 @@ GEM
uri (1.1.1)
useragent (0.16.11)
version_gem (1.1.3)
+ view_component (4.5.0)
+ actionview (>= 7.1.0)
+ activesupport (>= 7.1.0)
+ concurrent-ruby (~> 1)
web-console (4.2.1)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
@@ -694,6 +698,7 @@ DEPENDENCIES
timecop (~> 0.9.10)
turbo-rails
tzinfo-data
+ view_component
web-console (>= 4.1.0)
webmock
diff --git a/app/components/chapters_sidebar_component.html.erb b/app/components/chapters_sidebar_component.html.erb
new file mode 100644
index 000000000..df5116ca4
--- /dev/null
+++ b/app/components/chapters_sidebar_component.html.erb
@@ -0,0 +1,12 @@
+<% cache "chapters-sidebar" do %>
+ <% if title %>
+
<%= title %>
+ <% end %>
+
+ <% chapters.each do |chapter| %>
+ -
+ <%= link_to chapter.name, chapter_path(chapter.slug) %>
+
+ <% end %>
+
+<% end %>
diff --git a/app/components/chapters_sidebar_component.rb b/app/components/chapters_sidebar_component.rb
new file mode 100644
index 000000000..c4e2b8faa
--- /dev/null
+++ b/app/components/chapters_sidebar_component.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ChaptersSidebarComponent < ViewComponent::Base
+ # ViewComponent::Base does not define initialize, so super is not needed
+ def initialize(chapters:, title: nil) # rubocop:disable Lint/MissingSuper
+ @chapters = chapters
+ @title = title
+ end
+
+ private
+
+ attr_reader :chapters, :title
+end
diff --git a/app/models/chapter.rb b/app/models/chapter.rb
index fa98ae7b3..ae0f9d9bd 100644
--- a/app/models/chapter.rb
+++ b/app/models/chapter.rb
@@ -16,6 +16,9 @@ class Chapter < ApplicationRecord
has_many :feedbacks, through: :workshops
before_save :set_slug
+ after_update_commit :expire_chapters_sidebar_cache
+ after_create_commit :expire_chapters_sidebar_cache
+ after_destroy_commit :expire_chapters_sidebar_cache
scope :active, -> { where(active: true) }
@@ -47,6 +50,10 @@ def coaches
private
+ def expire_chapters_sidebar_cache
+ Rails.cache.delete('chapters-sidebar')
+ end
+
def time_zone_exists
return unless time_zone && ActiveSupport::TimeZone[time_zone].nil?
diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/show.html.haml
index a4c19c2a1..53c43ba8a 100644
--- a/app/views/dashboard/show.html.haml
+++ b/app/views/dashboard/show.html.haml
@@ -77,12 +77,7 @@
= link_to 'Explore all events →', upcoming_events_path, class: 'btn btn-outline-primary mt-3'
.col-lg-4.pl-lg-5
- %h3
- = t('homepage.chapters.title')
- %ul.list-unstyled.ms-0
- - @chapters.each do |chapter|
- %li
- = link_to chapter.name, chapter_path(chapter.slug)
+ = render ChaptersSidebarComponent.new(chapters: @chapters, title: t('homepage.chapters.title'))
- if @testimonials.any?
.py-4.py-lg-5.bg-light
diff --git a/spec/components/chapters_sidebar_component_spec.rb b/spec/components/chapters_sidebar_component_spec.rb
new file mode 100644
index 000000000..ee4483b99
--- /dev/null
+++ b/spec/components/chapters_sidebar_component_spec.rb
@@ -0,0 +1,24 @@
+require 'rails_helper'
+require 'view_component/test_helpers'
+
+RSpec.describe ChaptersSidebarComponent do
+ include ViewComponent::TestHelpers
+ include Rails.application.routes.url_helpers
+
+ let(:chapters) { Fabricate.times(3, :chapter) }
+
+ it 'renders chapter names as links' do
+ render_inline ChaptersSidebarComponent.new(chapters: chapters)
+
+ chapters.each do |chapter|
+ expect(page).to have_link(chapter.name, href: chapter_path(chapter.slug))
+ end
+ end
+
+ it 'renders nothing when no chapters' do
+ render_inline ChaptersSidebarComponent.new(chapters: [])
+
+ expect(page).to have_css('ul.list-unstyled.ms-0', visible: true)
+ expect(page).to have_no_css('li')
+ end
+end
diff --git a/spec/models/chapter_spec.rb b/spec/models/chapter_spec.rb
index 55db73d56..575685425 100644
--- a/spec/models/chapter_spec.rb
+++ b/spec/models/chapter_spec.rb
@@ -41,4 +41,28 @@
end
end
end
+
+ context 'cache expiration' do
+ let(:cache_key) { 'chapters-sidebar' }
+
+ it 'expires cache when chapter is created' do
+ Rails.cache.write(cache_key, 'cached content')
+ Fabricate(:chapter)
+ expect(Rails.cache.read(cache_key)).to be_nil
+ end
+
+ it 'expires cache when chapter is updated' do
+ Rails.cache.write(cache_key, 'cached content')
+ chapter = Fabricate(:chapter)
+ chapter.update!(name: 'Updated Name')
+ expect(Rails.cache.read(cache_key)).to be_nil
+ end
+
+ it 'expires cache when chapter is destroyed' do
+ Rails.cache.write(cache_key, 'cached content')
+ chapter = Fabricate(:chapter)
+ chapter.destroy
+ expect(Rails.cache.read(cache_key)).to be_nil
+ end
+ end
end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
new file mode 100644
index 000000000..069b8d2d4
--- /dev/null
+++ b/spec/rails_helper.rb
@@ -0,0 +1,45 @@
+# This file is copied to spec/ when you run 'rails generate rspec:install'
+ENV['RAILS_ENV'] ||= 'test'
+require File.expand_path('../config/environment', __dir__)
+# Prevent database truncation if the environment is production
+abort('The Rails environment is running in production mode!') if Rails.env.production?
+require 'rspec/rails'
+# Add additional requires below this line. Rails is not loaded until this point!
+
+# Requires supporting ruby files with custom matchers and macros, etc, in
+# spec/support/ and its subdirectories. Files matching `spec/**/*_helper.rb` can
+# be required explicitly.
+Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f }
+
+# Checks for pending migration and applies them before tests are run.
+# If you are not using ActiveRecord, you can remove these lines.
+begin
+ ActiveRecord::Migration.check_all_pending!
+rescue ActiveRecord::PendingMigrationError => e
+ puts e.to_s.strip
+ exit 1
+end
+RSpec.configure do |config|
+ # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
+ config.fixture_paths = ["#{::Rails.root}/spec/fixtures"]
+
+ # If you're not using ActiveRecord, or you'd prefer not to use each of your
+ # test frameworks (like Capybara or Selenium) for test
+ # see https://relishapp.com/rspec/rspec-rails/docs/configuration
+ config.use_transactional_fixtures = true
+
+ # RSpec Rails can automatically mix in different behaviours based on the
+ # file location of the spec. This line needs to be present for ViewComponent
+ # to work with controller specs
+ config.infer_spec_type_from_file_location!
+
+ # Connect ViewComponent to RSpec, adding RSpec metadata type: :component.
+ # This extends Rails' own built-in (e.g. "type: :controller") metadata system.
+ RSpec::Rails::DIRECTORY_MAPPINGS[:component] = %w[spec/components]
+
+ # Filter lines from Rails gems in backtraces.
+ config.filter_rails_from_backtrace!
+ # arbitrary gems may also be filtered from backtraces
+ # this line is optional and should be present if your application depends
+ # on external gems, but this is not needed for ViewComponent tests
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 3ee48ccc4..3ca44fcb8 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -71,19 +71,6 @@ def self.branch_coverage?
# See https://github.com/DatabaseCleaner/database_cleaner#rspec-with-capybara-example
config.before(:suite) do
- if config.use_transactional_fixtures?
- raise(<<-MSG)
- Delete line `config.use_transactional_fixtures = true` from spec_helper.rb
- (or set it to false) to prevent uncommitted transactions being used in
- JavaScript-dependent specs.
-
- During testing, the app-under-test that the browser driver connects to
- uses a different database connection to the database connection used by
- the spec. The app's database connection would not be able to access
- uncommitted transaction data setup over the spec's database connection.
- MSG
- end
-
DatabaseCleaner.clean_with(:truncation)
DatabaseCleaner.strategy = :deletion
end
diff --git a/spec/views/dashboard/show.html.haml_spec.rb b/spec/views/dashboard/show.html.haml_spec.rb
new file mode 100644
index 000000000..64d02fa40
--- /dev/null
+++ b/spec/views/dashboard/show.html.haml_spec.rb
@@ -0,0 +1,22 @@
+require 'rails_helper'
+
+RSpec.describe 'dashboard/show.html.haml', type: :view do
+ let(:chapters) { Fabricate.times(2, :chapter) }
+ let(:upcoming_workshops) { {} }
+ let(:testimonials) { [] }
+
+ before do
+ assign(:chapters, chapters)
+ assign(:upcoming_workshops, upcoming_workshops)
+ assign(:has_more_events, false)
+ assign(:testimonials, testimonials)
+ render
+ end
+
+ it 'renders the chapters sidebar component' do
+ expect(rendered).to have_selector('.col-lg-4.pl-lg-5')
+ chapters.each do |chapter|
+ expect(rendered).to have_link(chapter.name, href: chapter_path(chapter.slug))
+ end
+ end
+end