From e59ea14084ac9ea1d150800f18b242ad96142142 Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Fri, 25 Oct 2024 14:09:31 +0100 Subject: [PATCH 01/12] Add air quality alert styling colours to Tailwind MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As these class names will be dynamically generated in the template, we need to explicitly include all of them in our Tailwind config file ‘safelist’, or else they will not be included in the build: https://tailwindcss.com/docs/content-configuration#safelisting-classes --- tailwind.config.js | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tailwind.config.js b/tailwind.config.js index 4e478437..c46cc364 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,5 +5,39 @@ module.exports = { './app/assets/stylesheets/**/*.css', './app/javascript/**/*.js', './app/components/**/*.html.erb' - ] + ], + theme: { + extend: { + colors: { + 'moderate-alert-guidance-panel': '#FBDA3033', + 'high-alert-guidance-panel': '#EBDBDE', + 'very-high-alert-guidance-panel': '#EBDBDE', + }, + } + }, + safelist: [ + 'bg-moderate-alert-guidance-panel', + 'bg-high-alert-guidance-panel', + 'bg-very-high-alert-guidance-panel', + "bg-lime-400", + "bg-green-400", + "bg-lime-600", + "bg-yellow-300", + "bg-amber-200", + "bg-yellow-500", + "bg-orange-500", + "bg-red-500", + "bg-red-800", + "bg-stone-700", + "text-lime-400", + "text-green-400", + "text-lime-600", + "text-yellow-300", + "text-amber-200", + "text-yellow-500", + "text-orange-500", + "text-red-500", + "text-red-800", + "text-stone-700" + ], } From 2c1b8067a50a3953a782e82504316a65b91aba8f Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Fri, 1 Nov 2024 09:18:30 +0000 Subject: [PATCH 02/12] Refactor turbo stream implementation for updating forecasts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This approach, of returning via a turbo stream template, gives us more flexibility to apply changes to multple distinct elements within the page. In our case, we want to re render the pollen, UV and temperature predictions, but we’ll also want to re render the alert guidance component in the next commit. --- .../styled_forecasts_controller.rb | 10 ++-- app/views/styled_forecasts/_day_tab.html.erb | 4 +- .../styled_forecasts/update.turbo_stream.erb | 1 + .../styled_forecasts_controller_spec.rb | 54 ++----------------- 4 files changed, 11 insertions(+), 58 deletions(-) create mode 100644 app/views/styled_forecasts/update.turbo_stream.erb diff --git a/app/controllers/styled_forecasts_controller.rb b/app/controllers/styled_forecasts_controller.rb index 8c69ff56..6bec411b 100644 --- a/app/controllers/styled_forecasts_controller.rb +++ b/app/controllers/styled_forecasts_controller.rb @@ -9,13 +9,11 @@ def update @maptiler_api_key = ENV.fetch("MAPTILER_API_KEY") forecasts = CercForecastService.latest_forecasts_for(zone).data - day_forecast = forecast_for_day(params.fetch("day"), forecasts) + @day_forecast = forecast_for_day(params.fetch("day"), forecasts) - render turbo_stream: turbo_stream.replace( - "day_predictions", - partial: "predictions", - locals: {forecast: day_forecast} - ) + respond_to do |format| + format.turbo_stream + end end private diff --git a/app/views/styled_forecasts/_day_tab.html.erb b/app/views/styled_forecasts/_day_tab.html.erb index bafe73b2..47560957 100644 --- a/app/views/styled_forecasts/_day_tab.html.erb +++ b/app/views/styled_forecasts/_day_tab.html.erb @@ -6,10 +6,8 @@ "tab-target": "day" } do %> -<%= - link_to update_styled_forecast_path(day: day), +<%= link_to update_styled_forecast_path(day: day, format: :turbo_stream), data: { - turbo_frame: "day_predictions", action: "click->tab#switch_tab" } do %>
diff --git a/app/views/styled_forecasts/update.turbo_stream.erb b/app/views/styled_forecasts/update.turbo_stream.erb new file mode 100644 index 00000000..67d47e2e --- /dev/null +++ b/app/views/styled_forecasts/update.turbo_stream.erb @@ -0,0 +1 @@ +<%= turbo_stream.replace("day_predictions", partial: "predictions", locals: {forecast: @day_forecast}) %> diff --git a/spec/controllers/styled_forecasts_controller_spec.rb b/spec/controllers/styled_forecasts_controller_spec.rb index 39b0079e..347ce173 100644 --- a/spec/controllers/styled_forecasts_controller_spec.rb +++ b/spec/controllers/styled_forecasts_controller_spec.rb @@ -49,58 +49,14 @@ end describe "GET :update" do - let(:tag_builder) do - instance_double(Turbo::Streams::TagBuilder, replace: true) - end - - before do - allow(Turbo::Streams::TagBuilder).to receive(:new).and_return(tag_builder) - end - context "when a recognised _day_ parameter is received" do - describe "when the day is _today_" do - it "passes the first forecast to the view" do - allow(CercForecastService).to receive(:latest_forecasts_for).and_return(forecasts) - - get :update, params: {day: :today} - - expect(CercForecastService).to have_received(:latest_forecasts_for).with(southwark) - expect(tag_builder).to have_received(:replace).with( - "day_predictions", - partial: "predictions", - locals: {forecast: forecasts.data.first} - ) - end - end - - describe "when the day is _tomorrow_" do - it "passes the second forecast to the view" do - allow(CercForecastService).to receive(:latest_forecasts_for).and_return(forecasts) - - get :update, params: {day: :tomorrow} - - expect(CercForecastService).to have_received(:latest_forecasts_for).with(southwark) - expect(tag_builder).to have_received(:replace).with( - "day_predictions", - partial: "predictions", - locals: {forecast: forecasts.data.second} - ) - end - end - - describe "when the day is _day_after_tomorrow_" do - it "passes the third forecast to the view" do - allow(CercForecastService).to receive(:latest_forecasts_for).and_return(forecasts) + it "renders the turbo update template" do + allow(CercForecastService).to receive(:latest_forecasts_for).and_return(forecasts) - get :update, params: {day: :day_after_tomorrow} + get :update, params: {day: :today}, format: :turbo_stream - expect(CercForecastService).to have_received(:latest_forecasts_for).with(southwark) - expect(tag_builder).to have_received(:replace).with( - "day_predictions", - partial: "predictions", - locals: {forecast: forecasts.data.third} - ) - end + expect(CercForecastService).to have_received(:latest_forecasts_for).with(southwark) + expect(response).to render_template("styled_forecasts/update") end end From f351951381d323940aab34b43af465ffd822b206 Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Tue, 5 Nov 2024 09:56:22 +0000 Subject: [PATCH 03/12] Create partial for rendering alert guidance This will be triggered by the turbo stream in the previous commit, and will be rendered when there is an air quality alert. I have also removed the bold tags from the guidance text as they are not part of the designs. --- .../styled_forecasts/_alert_guidance.html.erb | 12 +++++++ .../styled_forecasts/_forecast_tabs.html.erb | 1 + .../styled_forecasts/update.turbo_stream.erb | 1 + config/locales/en.yml | 32 +++++++++---------- 4 files changed, 30 insertions(+), 16 deletions(-) create mode 100644 app/views/styled_forecasts/_alert_guidance.html.erb diff --git a/app/views/styled_forecasts/_alert_guidance.html.erb b/app/views/styled_forecasts/_alert_guidance.html.erb new file mode 100644 index 00000000..f2c01ea4 --- /dev/null +++ b/app/views/styled_forecasts/_alert_guidance.html.erb @@ -0,0 +1,12 @@ +<%= turbo_frame_tag "alert_guidance" do %> + <% if air_pollution_prediction.air_quality_alert %> +
+

+ <%= t("air_quality_alert.#{air_pollution_prediction.air_quality_alert.daqi_level}.guidance.title") %> +

+
+ <%= t("air_quality_alert.#{air_pollution_prediction.air_quality_alert.daqi_level}.guidance.detail_html") %> +
+
+ <% end %> +<% end %> diff --git a/app/views/styled_forecasts/_forecast_tabs.html.erb b/app/views/styled_forecasts/_forecast_tabs.html.erb index b15ff8ea..f553af72 100644 --- a/app/views/styled_forecasts/_forecast_tabs.html.erb +++ b/app/views/styled_forecasts/_forecast_tabs.html.erb @@ -10,6 +10,7 @@ <%= render "day_tab", forecast: @forecasts.second, day: :tomorrow %> <%= render "day_tab", forecast: @forecasts.third, day: :day_after_tomorrow %>
+ <%= render partial: "alert_guidance", locals: {air_pollution_prediction: @forecasts.first} %> <%= render partial: "map_selector" %>
diff --git a/app/views/styled_forecasts/update.turbo_stream.erb b/app/views/styled_forecasts/update.turbo_stream.erb index 67d47e2e..8fdf4101 100644 --- a/app/views/styled_forecasts/update.turbo_stream.erb +++ b/app/views/styled_forecasts/update.turbo_stream.erb @@ -1 +1,2 @@ <%= turbo_stream.replace("day_predictions", partial: "predictions", locals: {forecast: @day_forecast}) %> +<%= turbo_stream.replace("alert_guidance", partial: "alert_guidance", locals: {air_pollution_prediction: @day_forecast}) %> diff --git a/config/locales/en.yml b/config/locales/en.yml index 0030e4e7..4cb6ad28 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -54,28 +54,28 @@ en: title: Action may be required detail_html: Adults and children with lung problems, and adults with heart - problems, who experience symptoms, should think about reducing + problems, who experience symptoms, should think about reducing strenuous physical activity, particularly outdoors. high: guidance: title: Action required detail_html: - Think about reducing activity, particularly outdoors, if you - have discomfort such as sore eyes, a cough or sore - throat.

Older people, adults and children with lung problems, - and adults with heart problems should reduce strenuous physical - activity, particularly outdoors, and especially if they experience - symptoms.

People with asthma may need to use their reliever - inhaler more often, but remember to never exceed the stated dose or - take more than your doctor has advised + Think about reducing activity, particularly outdoors, if you have + discomfort such as sore eyes, a cough or sore throat.

Older + people, adults and children with lung problems, and adults with heart + problems should reduce strenuous physical activity, particularly + outdoors, and especially if they experience symptoms.

People + with asthma may need to use their reliever inhaler more often, but + remember to never exceed the stated dose or take more than your doctor + has advised. very_high: guidance: title: Action required detail_html: - Everyone should reduce strenuous physical activity, - particularly outdoors and especially if you experience symptoms such - as a cough or sore throat.

Older people, adults and children - with lung problems, and adults with heart problems should avoid - strenuous physical activity.

People with asthma may need to - use their reliever inhaler more often, but remember to never exceed - the stated dose or take more than your doctor has advised. + Everyone should reduce strenuous physical activity, particularly + outdoors and especially if you experience symptoms such as a cough or + sore throat.

Older people, adults and children with lung + problems, and adults with heart problems should avoid strenuous + physical activity.

People with asthma may need to use their + reliever inhaler more often, but remember to never exceed the stated + dose or take more than your doctor has advised. From b38b200512e56bde539f3ef1995ecd7a5b31d8be Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Fri, 1 Nov 2024 09:43:53 +0000 Subject: [PATCH 04/12] Add styles for incorporating alerts into air pollution tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rules that govern which styles should apply are quite complex, so I have ended up with a slightly verbose implementation that hopefully is at least fairly clear. The order in which the classes are listed is also important - the ‘inactive’ class needs to be below ‘active’ in order to override it. There are two versions of each of the DAQI alert classes. This is unfortunate but allows us to simplify the Javascript code that toggles the selected classes. --- .../stylesheets/application.tailwind.css.scss | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/application.tailwind.css.scss b/app/assets/stylesheets/application.tailwind.css.scss index 9947c61a..293caf61 100644 --- a/app/assets/stylesheets/application.tailwind.css.scss +++ b/app/assets/stylesheets/application.tailwind.css.scss @@ -5,5 +5,54 @@ @tailwind utilities; .active { - @apply border-[3px] border-black border-solid border-b-0 text-black -mb-[3px] bg-white; + @apply border-[3px] border-black border-solid border-b-0 -mb-[3px] bg-white; +} + +.after-today { + @apply text-black bg-white; +} + +.daqi-level-1-today, +.daqi-level-2-today, +.daqi-level-3-today { + @apply text-black bg-white; +} + +.inactive { + @apply border-b-0 border-l-2 border-r-2 border-t-2 border-gray-400 border-dashed text-gray-400; +} + +.daqi-level-4-today, +.daqi-alert-after-today-selected-level-4 { + @apply text-black bg-yellow-300; +} + +.daqi-level-5-today, +.daqi-alert-after-today-selected-level-5 { + @apply text-black bg-amber-200; +} + +.daqi-level-6-today, +.daqi-alert-after-today-selected-level-6 { + @apply text-black bg-yellow-500; +} + +.daqi-level-7-today, +.daqi-alert-after-today-selected-level-7 { + @apply text-white bg-orange-500; +} + +.daqi-level-8-today, +.daqi-alert-after-today-selected-level-8 { + @apply text-white bg-red-500; +} + +.daqi-level-9-today, +.daqi-alert-after-today-selected-level-9 { + @apply text-white bg-red-800; +} + +.daqi-level-10-today, +.daqi-alert-after-today-selected-level-10 { + @apply text-white bg-stone-700; } From d684306d0f20e65e810783827b2f7a81ac40f767 Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Fri, 1 Nov 2024 09:20:31 +0000 Subject: [PATCH 05/12] Add exclamation triangle icon We will use this in our air pollution tabs when there is an alert. --- app/views/shared/icons/_exclamation_triangle.html.erb | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 app/views/shared/icons/_exclamation_triangle.html.erb diff --git a/app/views/shared/icons/_exclamation_triangle.html.erb b/app/views/shared/icons/_exclamation_triangle.html.erb new file mode 100644 index 00000000..6fd278bb --- /dev/null +++ b/app/views/shared/icons/_exclamation_triangle.html.erb @@ -0,0 +1,3 @@ + 6 ? "white" : "black"%> class="size-8 -ml-[2.125rem]"> + + From 01a7aa4cb9f1d29bd493548ad66db2dd2961a614 Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Tue, 5 Nov 2024 08:59:24 +0000 Subject: [PATCH 06/12] Create new DayTab view component This component will allow us to store the logic required to calculate the styling required, which vary according to each DAQI level. Add background colour class util to air pollution prediction We will use this to generate the colour used in the forecast tabs. --- app/components/day_tab_component.html.erb | 33 ++++++ app/components/day_tab_component.rb | 23 ++++ spec/components/day_tab_component_spec.rb | 107 +++++++++++++++++++ spec/factories/air_pollutions_predictions.rb | 90 ++++++++++++++++ spec/factories/forecasts.rb | 40 +++++++ 5 files changed, 293 insertions(+) create mode 100644 app/components/day_tab_component.html.erb create mode 100644 app/components/day_tab_component.rb create mode 100644 spec/components/day_tab_component_spec.rb diff --git a/app/components/day_tab_component.html.erb b/app/components/day_tab_component.html.erb new file mode 100644 index 00000000..55ec9e14 --- /dev/null +++ b/app/components/day_tab_component.html.erb @@ -0,0 +1,33 @@ +<%= tag.div class: "tab py-4 flex-1 #{@day} mx-2 px-1 text-center #{@day == :today ? "active daqi-level-#{@forecast.air_pollution.value}-today" : 'inactive after-today'}", + data: { + date: @forecast.date.to_s, + controller: "tab", + "tab-active-class": "active", + "tab-inactive-class": "inactive", + "tab-target": "day" + } do + %> +<%= link_to update_styled_forecast_path(day: @day, format: :turbo_stream), + data: { + action: "click->tab#switch_tab" + } do %> +
+ <%= @forecast.date == Date.today ? 'Today' : @forecast.date.strftime('%A') %> +
+
+ <%= @forecast.date.strftime("%d %B") %> +
+
+
+ <% if @forecast.air_pollution.value > 6 %> + <%= render partial: "shared/icons/exclamation_triangle" %> + <% end %> +
+
+ <%= @forecast.air_pollution.label.capitalize %> +
+
+ Index <%= @forecast.air_pollution.value %>/10 +
+<% end %> +<% end %> diff --git a/app/components/day_tab_component.rb b/app/components/day_tab_component.rb new file mode 100644 index 00000000..462ee9d6 --- /dev/null +++ b/app/components/day_tab_component.rb @@ -0,0 +1,23 @@ +class DayTabComponent < ViewComponent::Base + def initialize(forecast:, day:) + @forecast = forecast + @day = day + end + + TAG_COLOURS = { + 1 => "bg-lime-400", + 2 => "bg-green-400", + 3 => "bg-lime-600", + 4 => "bg-yellow-300", + 5 => "bg-amber-200", + 6 => "bg-yellow-500", + 7 => "bg-orange-500", + 8 => "bg-red-500", + 9 => "bg-red-800", + 10 => "bg-stone-700" + } + + def daqi_indicator_colour_class + TAG_COLOURS.fetch(@forecast.air_pollution.value) + end +end diff --git a/spec/components/day_tab_component_spec.rb b/spec/components/day_tab_component_spec.rb new file mode 100644 index 00000000..4a1dbfe8 --- /dev/null +++ b/spec/components/day_tab_component_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe DayTabComponent, type: :component do + describe "daqi_indicator_colour_class" do + context "when the air pollution level is 1" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_1), day: :today) + } + + it "returns _bg-lime-400_" do + expect(component.daqi_indicator_colour_class).to eq("bg-lime-400") + end + end + + context "when the air pollution level is 2" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_2), day: :today) + } + + it "returns _bg-green-400_" do + expect(component.daqi_indicator_colour_class).to eq("bg-green-400") + end + end + + context "when the air pollution level is 3" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_3), day: :today) + } + + it "returns _bg-lime-600_" do + expect(component.daqi_indicator_colour_class).to eq("bg-lime-600") + end + end + + context "when the air pollution level is 4" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_4), day: :today) + } + + it "returns _bg-yellow-300_" do + expect(component.daqi_indicator_colour_class).to eq("bg-yellow-300") + end + end + + context "when the air pollution level is 5" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_5), day: :today) + } + + it "returns _bg-amber-200_" do + expect(component.daqi_indicator_colour_class).to eq("bg-amber-200") + end + end + + context "when the air pollution level is 6" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_6), day: :today) + } + + it "returns _bg-yellow-500_" do + expect(component.daqi_indicator_colour_class).to eq("bg-yellow-500") + end + end + + context "when the air pollution level is 7" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_7), day: :today) + } + + it "returns _bg-orange-500_" do + expect(component.daqi_indicator_colour_class).to eq("bg-orange-500") + end + end + + context "when the air pollution level is 8" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_8), day: :today) + } + + it "returns _bg-red-500_" do + expect(component.daqi_indicator_colour_class).to eq("bg-red-500") + end + end + + context "when the air pollution level is 9" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_9), day: :today) + } + + it "returns _bg-red-800_" do + expect(component.daqi_indicator_colour_class).to eq("bg-red-800") + end + end + + context "when the air pollution level is 10" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_10), day: :today) + } + + it "returns _bg-stone-700_" do + expect(component.daqi_indicator_colour_class).to eq("bg-stone-700") + end + end + end +end diff --git a/spec/factories/air_pollutions_predictions.rb b/spec/factories/air_pollutions_predictions.rb index d9c49edb..2e4e2063 100644 --- a/spec/factories/air_pollutions_predictions.rb +++ b/spec/factories/air_pollutions_predictions.rb @@ -53,6 +53,96 @@ ozone { 10 } end + trait 1 do + value { 1 } + label { "LOW" } + nitrogen_dioxide { 1 } + particulate_matter_10 { 1 } + particulate_matter_2_5 { 1 } + ozone { 1 } + end + + trait 2 do + value { 2 } + label { "LOW" } + nitrogen_dioxide { 2 } + particulate_matter_10 { 2 } + particulate_matter_2_5 { 2 } + ozone { 2 } + end + + trait 3 do + value { 3 } + label { "LOW" } + nitrogen_dioxide { 3 } + particulate_matter_10 { 3 } + particulate_matter_2_5 { 3 } + ozone { 3 } + end + + trait 4 do + value { 4 } + label { "MODERATE" } + nitrogen_dioxide { 4 } + particulate_matter_10 { 4 } + particulate_matter_2_5 { 4 } + ozone { 4 } + end + + trait 5 do + value { 5 } + label { "MODERATE" } + nitrogen_dioxide { 5 } + particulate_matter_10 { 5 } + particulate_matter_2_5 { 5 } + ozone { 5 } + end + + trait 6 do + value { 6 } + label { "MODERATE" } + nitrogen_dioxide { 6 } + particulate_matter_10 { 6 } + particulate_matter_2_5 { 6 } + ozone { 6 } + end + + trait 7 do + value { 7 } + label { "HIGH" } + nitrogen_dioxide { 7 } + particulate_matter_10 { 7 } + particulate_matter_2_5 { 7 } + ozone { 7 } + end + + trait 8 do + value { 8 } + label { "HIGH" } + nitrogen_dioxide { 8 } + particulate_matter_10 { 8 } + particulate_matter_2_5 { 8 } + ozone { 8 } + end + + trait 9 do + value { 9 } + label { "HIGH" } + nitrogen_dioxide { 9 } + particulate_matter_10 { 9 } + particulate_matter_2_5 { 9 } + ozone { 9 } + end + + trait 10 do + value { 10 } + label { "VERY HIGH" } + nitrogen_dioxide { 10 } + particulate_matter_10 { 10 } + particulate_matter_2_5 { 10 } + ozone { 10 } + end + initialize_with { new( forecasted_at: forecasted_at, diff --git a/spec/factories/forecasts.rb b/spec/factories/forecasts.rb index f5e94ece..20099aa1 100644 --- a/spec/factories/forecasts.rb +++ b/spec/factories/forecasts.rb @@ -29,6 +29,46 @@ pollen { FactoryBot.build(:pollen_prediction) } temperature { FactoryBot.build(:temperature_prediction) } + trait :air_pollution_level_1 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 1) } + end + + trait :air_pollution_level_2 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 2) } + end + + trait :air_pollution_level_3 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 3) } + end + + trait :air_pollution_level_4 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 4) } + end + + trait :air_pollution_level_5 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 5) } + end + + trait :air_pollution_level_6 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 6) } + end + + trait :air_pollution_level_7 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 7) } + end + + trait :air_pollution_level_8 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 8) } + end + + trait :air_pollution_level_9 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 9) } + end + + trait :air_pollution_level_10 do + air_pollution { FactoryBot.build(:air_pollution_prediction, 10) } + end + initialize_with { new( obtained_at: obtained_at, From 9820a5130a7be6ef8b7edb975679f77ef8967395 Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Fri, 1 Nov 2024 09:22:59 +0000 Subject: [PATCH 07/12] Replace day_tab partial with new view component --- app/components/day_tab_component.html.erb | 2 +- app/components/day_tab_component.rb | 8 +++++ app/views/styled_forecasts/_day_tab.html.erb | 29 ------------------- .../styled_forecasts/_forecast_tabs.html.erb | 6 ++-- spec/components/day_tab_component_spec.rb | 22 ++++++++++++++ 5 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 app/views/styled_forecasts/_day_tab.html.erb diff --git a/app/components/day_tab_component.html.erb b/app/components/day_tab_component.html.erb index 55ec9e14..a3121624 100644 --- a/app/components/day_tab_component.html.erb +++ b/app/components/day_tab_component.html.erb @@ -1,4 +1,4 @@ -<%= tag.div class: "tab py-4 flex-1 #{@day} mx-2 px-1 text-center #{@day == :today ? "active daqi-level-#{@forecast.air_pollution.value}-today" : 'inactive after-today'}", +<%= tag.div class: "tab py-4 flex-1 #{@day} mx-2 px-1 text-center #{classes}", data: { date: @forecast.date.to_s, controller: "tab", diff --git a/app/components/day_tab_component.rb b/app/components/day_tab_component.rb index 462ee9d6..99d7d929 100644 --- a/app/components/day_tab_component.rb +++ b/app/components/day_tab_component.rb @@ -20,4 +20,12 @@ def initialize(forecast:, day:) def daqi_indicator_colour_class TAG_COLOURS.fetch(@forecast.air_pollution.value) end + + def classes + if @day == :today + "active daqi-level-#{@forecast.air_pollution.value}-today" + else + "inactive after-today" + end + end end diff --git a/app/views/styled_forecasts/_day_tab.html.erb b/app/views/styled_forecasts/_day_tab.html.erb deleted file mode 100644 index 47560957..00000000 --- a/app/views/styled_forecasts/_day_tab.html.erb +++ /dev/null @@ -1,29 +0,0 @@ -<%= tag.div class: "tab py-4 flex-1 #{day} mx-2 px-1 text-center text-gray-400 daqi-low border-b-0 border-l-2 border-r-2 border-t-2 border-gray-400 border-dashed #{day == :today ? "active" : ''}", - data: { - date: forecast.date.to_s, - controller: "tab", - "tab-active-class": "active", - "tab-target": "day" - } do - %> -<%= link_to update_styled_forecast_path(day: day, format: :turbo_stream), - data: { - action: "click->tab#switch_tab" - } do %> -
- <%= forecast.date == Date.today ? 'Today' : forecast.date.strftime('%A') %> -
-
- <%= forecast.date.strftime("%d %B") %> -
-
- ● -
-
- <%= forecast.air_pollution.label.capitalize %> -
-
- Index <%= forecast.air_pollution.value %>/10 -
-<% end %> -<% end %> diff --git a/app/views/styled_forecasts/_forecast_tabs.html.erb b/app/views/styled_forecasts/_forecast_tabs.html.erb index f553af72..9860d1b4 100644 --- a/app/views/styled_forecasts/_forecast_tabs.html.erb +++ b/app/views/styled_forecasts/_forecast_tabs.html.erb @@ -6,9 +6,9 @@ class="tabs flex flex-row items-stretch mt-4 m-1 text-xs font-bold border-b-[3px] border-black" data-turbo-prefetch="false" > - <%= render "day_tab", forecast: @forecasts.first, day: :today %> - <%= render "day_tab", forecast: @forecasts.second, day: :tomorrow %> - <%= render "day_tab", forecast: @forecasts.third, day: :day_after_tomorrow %> + <%= render(DayTabComponent.new(forecast: @forecasts.first, day: :today)) %> + <%= render(DayTabComponent.new(forecast: @forecasts.second, day: :tomorrow)) %> + <%= render(DayTabComponent.new(forecast: @forecasts.third, day: :day_after_tomorrow)) %> <%= render partial: "alert_guidance", locals: {air_pollution_prediction: @forecasts.first} %> <%= render partial: "map_selector" %> diff --git a/spec/components/day_tab_component_spec.rb b/spec/components/day_tab_component_spec.rb index 4a1dbfe8..278d2ba6 100644 --- a/spec/components/day_tab_component_spec.rb +++ b/spec/components/day_tab_component_spec.rb @@ -104,4 +104,26 @@ end end end + + describe "classes" do + context "when the day is today" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_1), day: :today) + } + + it "returns the classes for today's tab" do + expect(component.classes).to eq("active daqi-level-1-today") + end + end + + context "when the day is not today" do + let(:component) { + DayTabComponent.new(forecast: FactoryBot.build(:forecast, :air_pollution_level_1), day: :tomorrow) + } + + it "returns the classes for the after today tabs" do + expect(component.classes).to eq("inactive after-today") + end + end + end end From 8109bce22d9f6cb02dfd7ec9feac41528a03716c Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Fri, 1 Nov 2024 09:20:47 +0000 Subject: [PATCH 08/12] Update JS active/inactive class toggle We now need to toggle an inactive class as well as an active class, because of the interaction of the active class with the daqi-level classes. In cases of alerts, we want to override the active class with the relevant daqi-level class, but where that tab is not active, we want to override that again with our inactive styling. --- app/javascript/controllers/tab_controller.js | 10 ++++++---- spec/feature_steps/forecast_steps.rb | 9 +++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/app/javascript/controllers/tab_controller.js b/app/javascript/controllers/tab_controller.js index 4ebf3ae8..793b2e45 100644 --- a/app/javascript/controllers/tab_controller.js +++ b/app/javascript/controllers/tab_controller.js @@ -2,7 +2,7 @@ import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="tab" export default class extends Controller { - static classes = ["active"]; + static classes = ["active", "inactive"]; static targets = ["day"]; connect() {} @@ -10,11 +10,13 @@ export default class extends Controller { switch_tab() { this.makeAllTabsInactive(); this.dayTarget.classList.toggle(this.activeClass); + this.dayTarget.classList.toggle(this.inactiveClass); } makeAllTabsInactive() { - document - .querySelectorAll(".tabs .tab") - .forEach((el) => el.classList.remove(this.activeClass)); + document.querySelectorAll(".tabs .tab").forEach((el) => { + el.classList.remove(this.activeClass); + el.classList.add(this.inactiveClass); + }); } } diff --git a/spec/feature_steps/forecast_steps.rb b/spec/feature_steps/forecast_steps.rb index f61113dc..af9a4b6f 100644 --- a/spec/feature_steps/forecast_steps.rb +++ b/spec/feature_steps/forecast_steps.rb @@ -114,6 +114,9 @@ def and_i_see_predicted_uv_level_v2 def then_i_see_that_the_tomorrow_tab_is_active expect(page).to have_css(".tab.tomorrow.active") + expect(page).to have_css(".tab.today.inactive") + expect(page).to have_css(".tab.day_after_tomorrow.inactive") + expect(page).not_to have_css(".tab.today.active") expect(page).not_to have_css(".tab.day_after_tomorrow.active") end @@ -121,6 +124,9 @@ def then_i_see_that_the_tomorrow_tab_is_active def and_i_see_that_the_today_tab_is_active expect(page).to have_css(".tab.today.active") + expect(page).to have_css(".tab.tomorrow.inactive") + expect(page).to have_css(".tab.day_after_tomorrow.inactive") + expect(page).not_to have_css(".tab.tomorrow.active") expect(page).not_to have_css(".tab.day_after_tomorrow.active") end @@ -128,6 +134,9 @@ def and_i_see_that_the_today_tab_is_active def then_i_see_that_the_day_after_tomorrow_tab_is_active expect(page).to have_css(".tab.day_after_tomorrow.active") + expect(page).to have_css(".tab.today.inactive") + expect(page).to have_css(".tab.tomorrow.inactive") + expect(page).not_to have_css(".tab.today.active") expect(page).not_to have_css(".tab.tomorrow.active") end From 2d0c48ab3c0a8a0b55edc18f5081375eee311675 Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Fri, 1 Nov 2024 09:15:28 +0000 Subject: [PATCH 09/12] Add integration tests for air quality alerts --- spec/feature_steps/air_quality_steps.rb | 62 ++++++++++++++++++- .../view_styled_air_quality_alerts_spec.rb | 46 ++++++++++++++ 2 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 spec/features/visitors/view_styled_air_quality_alerts_spec.rb diff --git a/spec/feature_steps/air_quality_steps.rb b/spec/feature_steps/air_quality_steps.rb index ad9d4283..2b4dc7a5 100644 --- a/spec/feature_steps/air_quality_steps.rb +++ b/spec/feature_steps/air_quality_steps.rb @@ -28,6 +28,11 @@ def when_i_look_at_the_forecasts click_link("View forecasts") end + def when_i_look_at_the_forecasts_v2 + visit root_path + click_link("View new style forecasts") + end + def expect_to_see_alert_date_for(day) date = case day when :today @@ -41,6 +46,19 @@ def expect_to_see_alert_date_for(day) expect(page).to have_content("air quality alert for #{date}") end + def expect_to_see_alert_date_for_v2(day) + date = case day + when :today + Date.today.strftime("%d %b %Y") + when :tomorrow + Date.tomorrow.strftime("%d %b %Y") + when :day_after_tomorrow + (Date.tomorrow + 1.day).strftime("%d %b %Y") + end + + expect(page).to have_content("air quality alert for #{date}") + end + def expect_to_see_alert_level(level) label = case level when :moderate @@ -54,12 +72,23 @@ def expect_to_see_alert_level(level) expect(page).to have_css(".alert-level", text: label) end + def expect_to_see_alert_level_v2(level) + label = case level + when :moderate + "Moderate" + when :high + "High" + when :very_high + "Very high" + end + + expect(page).to have_css(".daqi-label", text: label) + end + def expect_to_see_guidance_for(level) expect(page).to have_content(I18n.t("air_quality_alert.#{level}.guidance.title")) expect(page).to have_content( - ActionController::Base.helpers.strip_tags( - I18n.t("air_quality_alert.#{level}.guidance.detail_html") - ) + I18n.t("air_quality_alert.#{level}.guidance.detail_html").truncate(20, omission: "") ) end @@ -71,6 +100,33 @@ def then_i_see_an_air_quality_alert_of_high_for_today end end + def then_i_see_an_air_quality_alert_of_high_for_today_v2 + within(".today[data-date='#{Date.today}']") do + expect_to_see_alert_level_v2(:high) + end + within(".alert-guidance") do + expect_to_see_guidance_for(:high) + end + end + + def then_i_see_an_air_quality_alert_of_moderate_for_tomorrow_v2 + within(".tomorrow[data-date='#{Date.tomorrow}']") do + expect_to_see_alert_level_v2(:moderate) + end + within(".alert-guidance") do + expect_to_see_guidance_for(:moderate) + end + end + + def then_i_see_an_air_quality_alert_of_v_high_for_day_after_tomorrow_v2 + within(".day_after_tomorrow[data-date='#{Date.tomorrow + 1.day}']") do + expect_to_see_alert_level_v2(:very_high) + end + within(".alert-guidance") do + expect_to_see_guidance_for(:very_high) + end + end + def and_i_see_an_air_quality_alert_of_moderate_for_tomorrow within(".alert-level-moderate[data-alert-date='#{Date.tomorrow}']") do expect_to_see_alert_date_for(:tomorrow) diff --git a/spec/features/visitors/view_styled_air_quality_alerts_spec.rb b/spec/features/visitors/view_styled_air_quality_alerts_spec.rb new file mode 100644 index 00000000..f6e9ef8f --- /dev/null +++ b/spec/features/visitors/view_styled_air_quality_alerts_spec.rb @@ -0,0 +1,46 @@ +# Feature: View air quality alerts +# - So that I can take appropriate protective action +# - As a visitor +# - I want to see Air quality alerts for air pollution predictions with warning +# statuses above "low" +RSpec.feature "Air quality alerts", feature: true do + around do |example| + env_vars = { + CERC_API_HOST_URL: "https://cerc.example.com", + CERC_API_KEY: "SECRET-API-KEY", + CERC_API_CACHE_LIMIT_MINS: "60", + MAPTILER_API_KEY: "TOPSECRET" + } + ClimateControl.modify(env_vars) { example.run } + end + + include AirQualitySteps, ForecastSteps + + before do + given_an_air_pollution_prediction_for_today_w_high_warning_status + and_an_air_pollution_prediction_for_tomorrow_w_moderate_warning_status + and_an_air_pollution_prediction_for_day_after_tomorrow_w_v_high_warning_status + and_the_response_from_cercs_api_is_stubbed_accordingly + end + + scenario "View air quality alert for today" do + when_i_look_at_the_forecasts_v2 + then_i_see_an_air_quality_alert_of_high_for_today_v2 + end + + scenario "View air quality alert for tomorrow", js: true do + visit root_path + when_i_select_view_forecasts_v2 + and_i_switch_to_the_tab_for_tomorrow + + then_i_see_an_air_quality_alert_of_moderate_for_tomorrow_v2 + end + + scenario "View air quality alert for the day after tomorrow", js: true do + visit root_path + when_i_select_view_forecasts_v2 + and_i_switch_to_the_tab_for_day_after_tomorrow + + then_i_see_an_air_quality_alert_of_v_high_for_day_after_tomorrow_v2 + end +end From 16567ec465d27bf59b5ef185dc81c27805f2e8b5 Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Fri, 1 Nov 2024 12:17:05 +0000 Subject: [PATCH 10/12] Fix Cuprite error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The integration tests in CI were complaining about overlapping elements on a button because of he icon on the new air pollution tab. It doesn’t matter that it overlaps, so I’ve introduced this alternative syntax to fix the issue. --- spec/feature_steps/forecast_steps.rb | 4 ++-- spec/features/visitors/view_styled_forecasts_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/feature_steps/forecast_steps.rb b/spec/feature_steps/forecast_steps.rb index af9a4b6f..30727d82 100644 --- a/spec/feature_steps/forecast_steps.rb +++ b/spec/feature_steps/forecast_steps.rb @@ -57,9 +57,9 @@ def and_i_switch_to_the_tab_for_day_after_tomorrow def switch_to_tab_for(day) case day when :tomorrow - find(".tab.tomorrow a").click + find(".tab.tomorrow a").trigger("click") when :day_after_tomorrow - find(".tab.day_after_tomorrow a").click + find(".tab.day_after_tomorrow a").trigger("click") else raise "day: #{day} not expected" end diff --git a/spec/features/visitors/view_styled_forecasts_spec.rb b/spec/features/visitors/view_styled_forecasts_spec.rb index fc6c0fb6..3d0fdee8 100644 --- a/spec/features/visitors/view_styled_forecasts_spec.rb +++ b/spec/features/visitors/view_styled_forecasts_spec.rb @@ -58,8 +58,8 @@ visit root_path when_i_select_view_forecasts_v2 - and_i_switch_to_the_tab_for_day_after_tomorrow + and_i_switch_to_the_tab_for_day_after_tomorrow then_i_see_that_the_day_after_tomorrow_tab_is_active and_i_see_predicted_uv_level_for_day_after_tomorrow and_i_see_predicted_pollen_level_for_day_after_tomorrow From e060cec133b0fad5523c5238d3a714e31a22426b Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Tue, 5 Nov 2024 15:59:44 +0000 Subject: [PATCH 11/12] Update alert stylings for air pollution tabs If today has an alert, we always want to display the alert stylings. For tomorrow and the day after tomorrow, if they have alerts, we only want to display the alert stylings when the tab is selected. I have opted for a slightly verbose Javascript implementation that extends the Stimulus code that was already in place, rather than bringing the tabs into the Turbo stream that is already in place for this action. However, if any part of this design becomes any more complex, this would probably be a good candidate to refactor along those lines. --- app/components/day_tab_component.html.erb | 10 ++--- app/javascript/controllers/tab_controller.js | 43 ++++++++++++++++++-- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/app/components/day_tab_component.html.erb b/app/components/day_tab_component.html.erb index a3121624..4512e15f 100644 --- a/app/components/day_tab_component.html.erb +++ b/app/components/day_tab_component.html.erb @@ -4,14 +4,14 @@ controller: "tab", "tab-active-class": "active", "tab-inactive-class": "inactive", - "tab-target": "day" + "tab-target": "dayPrediction" } do %> <%= link_to update_styled_forecast_path(day: @day, format: :turbo_stream), data: { action: "click->tab#switch_tab" } do %> -
+
<%= @forecast.date == Date.today ? 'Today' : @forecast.date.strftime('%A') %>
@@ -19,14 +19,14 @@
- <% if @forecast.air_pollution.value > 6 %> - <%= render partial: "shared/icons/exclamation_triangle" %> + <% if @forecast.air_pollution.value > 3 %> + <%= render partial: "shared/icons/exclamation_triangle", locals: {forecast: @forecast} %> <% end %>
<%= @forecast.air_pollution.label.capitalize %>
-
+
Index <%= @forecast.air_pollution.value %>/10
<% end %> diff --git a/app/javascript/controllers/tab_controller.js b/app/javascript/controllers/tab_controller.js index 793b2e45..5d5a4fde 100644 --- a/app/javascript/controllers/tab_controller.js +++ b/app/javascript/controllers/tab_controller.js @@ -3,14 +3,17 @@ import { Controller } from "@hotwired/stimulus"; // Connects to data-controller="tab" export default class extends Controller { static classes = ["active", "inactive"]; - static targets = ["day"]; + static targets = ["dayPrediction", "day", "daqiValue"]; connect() {} switch_tab() { this.makeAllTabsInactive(); - this.dayTarget.classList.toggle(this.activeClass); - this.dayTarget.classList.toggle(this.inactiveClass); + this.removeDaqiAlertClasses(); + + this.addAlertClassIfNotTodayAndHasAirQualityAlert(); + this.dayPredictionTarget.classList.toggle(this.activeClass); + this.dayPredictionTarget.classList.toggle(this.inactiveClass); } makeAllTabsInactive() { @@ -19,4 +22,38 @@ export default class extends Controller { el.classList.add(this.inactiveClass); }); } + + addAlertClassIfNotTodayAndHasAirQualityAlert() { + const daqiValue = this.getDaqiValue(); + if (this.dayTarget.innerText !== "Today" && daqiValue > 3) { + this.addDaqiAlertClass(); + } + } + + addDaqiAlertClass() { + const daqiValue = this.getDaqiValue(); + this.dayPredictionTarget.classList.add( + `daqi-alert-after-today-selected-level-${daqiValue}` + ); + } + + removeDaqiAlertClasses() { + document.querySelectorAll(".tabs .tab").forEach((el) => { + const daqiAlertClassRegex = new RegExp( + "daqi-alert-after-today-selected-level-(\\d{1,2})" + ); + + const fullClassName = Array.from(el.classList).find((className) => + className.match(daqiAlertClassRegex) + ); + + if (fullClassName) { + el.classList.remove(fullClassName); + } + }); + } + + getDaqiValue() { + return this.daqiValueTarget.innerText.split("/")[0].split(" ")[1]; + } } From 6eca99ab38d15c46836e1f2faab65d95ae386e52 Mon Sep 17 00:00:00 2001 From: Patrick Fleming Date: Wed, 6 Nov 2024 09:28:37 +0000 Subject: [PATCH 12/12] Update integration tests to check for alert classes We need to update our integration tests to allow us to explicitly set DAQI values, so that we can check that the correct class for that value has been set on the active tab where there is an alert. --- spec/feature_steps/air_quality_steps.rb | 10 ++++++++-- spec/fixtures/api/forecasts.rb | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/spec/feature_steps/air_quality_steps.rb b/spec/feature_steps/air_quality_steps.rb index 2b4dc7a5..26ca4930 100644 --- a/spec/feature_steps/air_quality_steps.rb +++ b/spec/feature_steps/air_quality_steps.rb @@ -4,11 +4,11 @@ def given_an_air_pollution_prediction_for_today_w_high_warning_status end def and_an_air_pollution_prediction_for_tomorrow_w_moderate_warning_status - forecasts << Fixtures::API.build_forecast(day: :tomorrow, air_pollution_status: :moderate) + forecasts << Fixtures::API.build_forecast(day: :tomorrow, air_pollution_status: :moderate, daqi_value: 4) end def and_an_air_pollution_prediction_for_day_after_tomorrow_w_v_high_warning_status - forecasts << Fixtures::API.build_forecast(day: :day_after_tomorrow, air_pollution_status: :very_high) + forecasts << Fixtures::API.build_forecast(day: :day_after_tomorrow, air_pollution_status: :very_high, daqi_value: 10) end def given_an_air_pollution_prediction_for_today_w_low_status @@ -116,6 +116,9 @@ def then_i_see_an_air_quality_alert_of_moderate_for_tomorrow_v2 within(".alert-guidance") do expect_to_see_guidance_for(:moderate) end + + expect(page).to have_css(".tab.tomorrow.daqi-alert-after-today-selected-level-4") + expect(page).not_to have_css(".tab.day_after_tomorrow.daqi-alert-after-today-selected-level-10") end def then_i_see_an_air_quality_alert_of_v_high_for_day_after_tomorrow_v2 @@ -125,6 +128,9 @@ def then_i_see_an_air_quality_alert_of_v_high_for_day_after_tomorrow_v2 within(".alert-guidance") do expect_to_see_guidance_for(:very_high) end + + expect(page).to have_css(".tab.day_after_tomorrow.daqi-alert-after-today-selected-level-10") + expect(page).not_to have_css(".tab.tomorrow.daqi-alert-after-today-selected-level-4") end def and_i_see_an_air_quality_alert_of_moderate_for_tomorrow diff --git a/spec/fixtures/api/forecasts.rb b/spec/fixtures/api/forecasts.rb index b6ba4eb3..69fd2a57 100644 --- a/spec/fixtures/api/forecasts.rb +++ b/spec/fixtures/api/forecasts.rb @@ -1,7 +1,7 @@ module Fixtures module API class << self - def build_forecast(day:, air_pollution_status:, pollen: :moderate, temperature: :normal, uv: :moderate) + def build_forecast(day:, air_pollution_status:, pollen: :moderate, temperature: :normal, uv: :moderate, daqi_value: nil) <<~JSON { "NO2": 1, @@ -16,7 +16,7 @@ def build_forecast(day:, air_pollution_status:, pollen: :moderate, temperature: "rain_pm": 3.01, "temp_max": #{max_temp_for(temperature)}, "temp_min": #{min_temp_for(temperature)}, - "total": #{daqi_value_for_level(air_pollution_status)}, + "total": #{daqi_value || daqi_value_for_level(air_pollution_status)}, "total_status": "#{total_status_for(air_pollution_status)}", "uv": #{daqi_value_for_level(uv)}, "wind_am": 5.3,