diff --git a/app/helpers/better_together/events_helper.rb b/app/helpers/better_together/events_helper.rb
index 88070da7e..19bee5223 100644
--- a/app/helpers/better_together/events_helper.rb
+++ b/app/helpers/better_together/events_helper.rb
@@ -3,6 +3,37 @@
module BetterTogether
# View helpers for events
module EventsHelper
+ # Returns a formatted time range for an event
+ # If only a start time is present, it is formatted by itself.
+ # If the event ends on the same day, the end time is shown without the date.
+ # Otherwise, both start and end are fully formatted.
+ def event_time_range(event, format: :event)
+ return unless event&.starts_at
+
+ start_time = l(event.starts_at, format: format)
+ return start_time unless event.ends_at
+
+ end_time = if event.starts_at.to_date == event.ends_at.to_date
+ l(event.ends_at, format: '%-I:%M %p')
+ else
+ l(event.ends_at, format: format)
+ end
+
+ "#{start_time} - #{end_time}"
+ end
+
+ # Builds a location string from the event's location and its associated address
+ def event_location(event)
+ location = event&.location
+ return unless location
+
+ parts = []
+ parts << location.name if location.respond_to?(:name) && location.name.present?
+ parts << location.location.to_s if location.respond_to?(:location) && location.location.present?
+
+ parts.compact.join(', ').presence
+ end
+
# Return hosts for an event that the current user is authorized to view.
# Keeps view markup small and centralizes the policy logic for testing.
def visible_event_hosts(event)
diff --git a/app/views/better_together/events/_event.html.erb b/app/views/better_together/events/_event.html.erb
index 2b78b4e9e..6a4cbb1f4 100644
--- a/app/views/better_together/events/_event.html.erb
+++ b/app/views/better_together/events/_event.html.erb
@@ -4,16 +4,16 @@
<%= cache event.cache_key_with_version do %>
<% if policy(event).show? %>
<%= render 'better_together/shared/card', entity: event do %>
- <% if event.location&.name&.present? %>
-
<%= @resource.name %>
- <% if @event.location&.name&.present? %>
+ <% if (location = event_location(@event)) %>
- <%= @event.location %>
+ <%= location %>
<% end %>
- <% if @event.starts_at.present? %>
+ <% if (time_range = event_time_range(@event)) %>
- <%= l(@event.starts_at, format: :event) %>
+ <%= time_range %>
<% end %>
@@ -92,16 +92,16 @@
<%= @event.privacy.humanize %>
- <% if @event.location&.name&.present? %>
+ <% if (location = event_location(@event)) %>
- <%= @event.location %>
+ <%= location %>
<% end %>
- <% if @event.starts_at.present? %>
+ <% if (time_range = event_time_range(@event)) %>
- <%= l(@event.starts_at, format: :event) %>
+ <%= time_range %>
- <% end %>
+ <% end %
<% if @event.registration_url.present? %>
<%= link_to t('better_together.events.register'), @event.registration_url, target: '_blank', class: 'text-decoration-none' %>
diff --git a/spec/helpers/better_together/events_helper_spec.rb b/spec/helpers/better_together/events_helper_spec.rb
new file mode 100644
index 000000000..fec0da5ef
--- /dev/null
+++ b/spec/helpers/better_together/events_helper_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+module BetterTogether
+ RSpec.describe EventsHelper, type: :helper do
+ describe '#event_time_range' do
+ let(:start_time) { Time.zone.parse('2024-03-10 15:00') }
+ let(:event) { instance_double(Event, starts_at: start_time, ends_at: end_time) }
+
+ context 'when end time is on same day' do
+ let(:end_time) { Time.zone.parse('2024-03-10 17:00') }
+
+ it 'formats with end time only' do
+ expected = "#{I18n.l(start_time, format: :event)} - #{I18n.l(end_time, format: '%-I:%M %p')}"
+ expect(helper.event_time_range(event)).to eq(expected)
+ end
+ end
+
+ context 'when end time is on a different day' do
+ let(:end_time) { Time.zone.parse('2024-03-11 17:00') }
+
+ it 'formats both start and end times fully' do
+ expected = "#{I18n.l(start_time, format: :event)} - #{I18n.l(end_time, format: :event)}"
+ expect(helper.event_time_range(event)).to eq(expected)
+ end
+ end
+
+ context 'without an end time' do
+ let(:end_time) { nil }
+
+ it 'returns only the start time' do
+ expect(helper.event_time_range(event)).to eq(I18n.l(start_time, format: :event))
+ end
+ end
+ end
+
+ describe '#event_location' do
+ it 'combines name and location' do
+ locatable = instance_double(
+ Geography::LocatableLocation,
+ name: 'Town Hall',
+ location: 'Springfield'
+ )
+ event = instance_double(Event, location: locatable)
+
+ expect(helper.event_location(event)).to eq('Town Hall, Springfield')
+ end
+
+ it 'returns nil when no location is present' do
+ expect(helper.event_location(instance_double(Event, location: nil))).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/views/better_together/events/_event.html.erb_spec.rb b/spec/views/better_together/events/_event.html.erb_spec.rb
new file mode 100644
index 000000000..e92b914c4
--- /dev/null
+++ b/spec/views/better_together/events/_event.html.erb_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe 'better_together/events/_event', type: :view do
+ it 'renders formatted location and time range' do
+ start_time = Time.zone.parse('2024-03-10 15:00')
+ end_time = Time.zone.parse('2024-03-10 17:00')
+ locatable = instance_double(
+ BetterTogether::Geography::LocatableLocation,
+ name: 'Town Hall',
+ location: 'Springfield'
+ )
+ event = instance_double(
+ BetterTogether::Event,
+ cache_key_with_version: 'events/1-20240310',
+ starts_at: start_time,
+ ends_at: end_time,
+ location: locatable
+ )
+
+ view.define_singleton_method(:categories_badge) { |*_args| '' }
+ allow(view).to receive(:render).with('better_together/shared/card', entity: event).and_yield
+ allow(view).to receive(:event_time_range).and_call_original
+ allow(view).to receive(:event_location).and_call_original
+
+ render partial: 'better_together/events/event', locals: { event: event }
+
+ expect(view).to have_received(:event_time_range).with(event, format: :short)
+ expect(view).to have_received(:event_location).with(event)
+
+ expect(rendered).to include('Town Hall, Springfield')
+ expected_time = "#{I18n.l(start_time, format: :event)} - #{I18n.l(end_time, format: '%-I:%M %p')}"
+ expect(rendered).to include(expected_time)
+ end
+end