Skip to content

Commit

Permalink
First draft of subscriptions
Browse files Browse the repository at this point in the history
  • Loading branch information
jdudley1123 committed Nov 20, 2024
1 parent 5fec0e4 commit e586354
Show file tree
Hide file tree
Showing 18 changed files with 555 additions and 5 deletions.
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ gem "terser"
gem "httparty"
gem "view_component"
gem "seed-fu"
gem 'wicked' # for multi-step forms

group :development do
gem "better_errors"
Expand Down
3 changes: 3 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,8 @@ GEM
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
wicked (2.0.0)
railties (>= 3.0.7)
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.37)
Expand Down Expand Up @@ -498,6 +500,7 @@ DEPENDENCIES
view_component
web-console (>= 3.3.0)
webmock
wicked

RUBY VERSION
ruby 3.3.6p108
Expand Down
70 changes: 70 additions & 0 deletions app/assets/stylesheets/application.tailwind.css.scss
Original file line number Diff line number Diff line change
Expand Up @@ -196,3 +196,73 @@ select {
}
}
}

form {
@apply mt-4;

section {
@apply mb-4;

> div {
@apply mb-2 block;
}
}

label {
@apply block mb-1 font-semibold;
}
input[type="text"],
input[type="search"],
input[type="email"],
input[type="tel"],
input[type="password"],
textarea {
@apply w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500;
}
input[type="checkbox"],
input[type="radio"] {
@apply inline-block mr-2;

+ label {
@apply inline-block;
}
}

.switch {
@apply relative inline-block w-11 h-5;

input[type="checkbox"] {
@apply appearance-none w-11 h-5 bg-airtext-main-light rounded-full checked:bg-airtext-main cursor-pointer transition-colors duration-300;
}
label {
@apply absolute top-0 left-0 w-5 h-5 bg-white rounded-full border border-airtext-main shadow-sm transition-transform duration-300 cursor-pointer;
}
}

.buttons {
@apply flex justify-between gap-2 mt-4;

> * {
@apply w-1/2;
}

a {
@apply inline-block px-4 py-2 font-semibold text-white bg-gray-500 rounded-md hover:bg-blue-600 focus:outline-none focus:ring-1 focus:ring-airtext-main focus:border-airtext-main cursor-pointer text-center;
}
}

button[type="submit"], input[type="submit"] {
@apply inline-block px-4 py-2 font-semibold text-white bg-airtext-main rounded-md hover:bg-blue-600 focus:outline-none focus:ring-1 focus:ring-airtext-main focus:border-airtext-main cursor-pointer;
}
.error-message {
@apply text-red-500 mt-1;
}
}

#search-results {
@apply mt-4;

li {
@apply mb-2 cursor-pointer;
}
}
41 changes: 41 additions & 0 deletions app/controllers/subscriptions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

class SubscriptionsController < ApplicationController
include Wicked::Wizard

steps :contact_details, :contact_verification, :zone_selection, :time_selection, :wrap_up

def show
session[:subscription_form] = nil if params[:reset]
load
render_wizard
end

def update
load
case step
when :contact_verification
# Do nothing
else
@form.assign_attributes(subscription_params)
end
session[:subscription_form] = @form.attributes
render_wizard @form
end

private

def load
@step_number = current_step_index.to_i + 1
@current_step = step
@form = SubscriptionForm.new(session[:subscription_form])
end

def current_step_index
steps.index(step)
end

def subscription_params
params[:subscription_form].permit! #.permit(:email, :sms_number, :voice_number, :zone, :time)
end
end
3 changes: 3 additions & 0 deletions app/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,6 @@ application.register("tab", TabController);

import NavigationController from "./navigation_controller";
application.register("navigation", NavigationController);

import SubscriptionController from "./subscription_controller";
application.register("subscription", SubscriptionController);
110 changes: 110 additions & 0 deletions app/javascript/controllers/subscription_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { Controller } from "@hotwired/stimulus";
import { useDebounce } from "stimulus-use";
import { geocoding } from "@maptiler/client";
import * as zones from "../zone_boundaries/zone-boundaries";
import booleanPointInPolygon from "@turf/boolean-point-in-polygon";

export default class SubscriptionController extends Controller {
static targets = ["email", "sms", "voice", "searchField", "searchResults"];
static debounces = ["search"];

connect() {
useDebounce(this);
}

toggleEmail() {
this.emailTarget.classList.toggle("hidden");
}

toggleSms() {
this.smsTarget.classList.toggle("hidden");
}

toggleVoice() {
this.voiceTarget.classList.toggle("hidden");
}

searchFieldTargetConnected() {
this.maptilerApiKey = this.searchFieldTarget.dataset.maptilerApiKey;
}

async search() {
console.log("searching...");
const userInput = this.searchFieldTarget.value;
const coordinates = await this.geoCode(userInput);
const zones = this.findZones(coordinates);
this.displaySearchResults(zones);
}

async geoCode(location) {
console.log("geocoding...");
const result = await geocoding.forward(location, {
apiKey: this.maptilerApiKey,
country: ["GB"],
proximity: [-0.116773, 51.510357],
types: [
"region",
"subregion",
"county",
"joint_municipality",
"joint_submunicipality",
"municipality",
"municipal_district",
"locality",
"neighbourhood",
"place",
"postal_code",
"address",
"road",
"poi",
],
});

return result?.features[0]?.geometry?.coordinates;
}

findZones(point) {
console.log(point);
if (!point) {
console.log("No coordinates found");
return;
}

console.log("finding zone...");
const matches = [];
// Point has to be in the format [longitude, latitude]
for (const [, zone] of Object.entries(zones)) {
if (booleanPointInPolygon(point, zone)) {
matches.push(zone);
}
}

// Sort by zone level so more specific zones are returned first
matches.sort((a, b) => b.properties.level - a.properties.level);

return matches;
}

displaySearchResults(results) {
this.searchResultsTarget.innerHTML = "";

if (!results) {
this.searchResultsTarget.innerHTML = "No results found";
return;
}

results.forEach((result) => {
const li = document.createElement("li");
li.textContent = result.properties.name;
li.dataset.action = "click->subscription#selectSearchResult";
this.searchResultsTarget.appendChild(li);
});
}

selectSearchResult(event) {
const zoneName = event.target.textContent;
const checkbox = document.querySelector(`input[value="${zoneName}"]`);
console.log(checkbox)
checkbox.checked = true;
}
}
120 changes: 120 additions & 0 deletions app/models/subscription_form.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
class SubscriptionForm
include ActiveModel::Model
include ActiveModel::Attributes

attribute :current_step

attribute :receive_email
attribute :receive_sms
attribute :receive_voice
attribute :email
attribute :sms_number
attribute :voice_number

attribute :zone_search
attribute :zones

attribute :time

attribute :reason
attribute :source
attribute :research
attribute :terms
attribute :privacy

validate :one_contact_method_present?, if: -> { current_step == "contact_details" }
validates :email, presence: true, if: -> { receive_email && current_step == "contact_details" }
validates :sms_number, presence: true, if: -> { receive_sms && current_step == "contact_details" }
validates :voice_number, presence: true, if: -> { receive_voice && current_step == "contact_details" }

validate :no_more_than_5_zones?, if: -> { current_step == "zone_selection" }
validates :zones, presence: true, if: -> { current_step == "zone_selection" }

validates :time, presence: true, if: -> { current_step == "time_selection" }

validates :terms, acceptance: true, if: -> { current_step == "wrap_up" }
validates :privacy, acceptance: true, if: -> { current_step == "wrap_up" }

TIMES = [
['AM', 'Morning – 6am'],
['PM', 'Evening – 6pm'],
]

SOURCES = [
[:airtext_application_form, 'airTEXT application form'],
[:poster_on_bus, 'Poster on bus'],
[:poster_on_tube, 'Poster on tube'],
[:mayor_of_london, 'Mayor of London'],
[:tfl_air_quality_alerts, 'TfL air quality alerts'],
[:council_website, 'Council website'],
[:council_magazine, 'Council magazine'],
[:breathe_easy_group, 'Breathe Easy group'],
[:barts_health_nhs_trust, 'Barts Health NHS trust'],
[:islington_primary_schools, 'Islington primary schools'],
[:shine, 'SHINE (Seasonal Health Interventions Network)'],
[:merton_cac, "Merton's Clean Air Campaign"],
[:be_air_aware_campaign, 'Be Air Aware Campaign'],
[:my_doctor, 'My doctor'],
[:local_pharmacist, 'Local pharmacist'],
[:local_council_employee, 'Local council employee'],
[:national_newspaper, 'National newspaper'],
[:local_newspaper, 'Local newspaper'],
[:air_quality_workshop, 'Air Quality Workshop'],
[:a_friend, 'A friend'],
[:tv_radio, 'TV/radio'],
[:web_or_search_engine, 'Web or Search Engine'],
[:twitter, 'Twitter'],
[:facebook, 'Facebook'],
[:other, 'Other'],
]

def one_contact_method_present?
errors.add(:base, 'You must select at least one contact method') if email.blank? && sms_number.blank? && voice_number.blank?
end

def no_more_than_5_zones?
return false unless zones.present?

errors.add(:zones, 'You can select no more than 5 zones') if zones.size > 5
end

def save
if current_step == "wrap_up" && valid?
create_subscriptions
else
valid?
end
end

def modes
[
("email" if receive_email),
("sms" if receive_sms),
("voicemail" if receive_voice),
].compact
end

def create_subscriptions
zones.compact.each do |zone|
modes.each do |mode|
create_subscription(zone, mode)
end
end
end

def create_subscription(zone, mode)
CercSubscriberApiClient.create_subscription(
# subscriber_id: subscriber_id,
zone: zone,
mode: mode,
phone: (sms_number if mode == "sms") || (voice_number if mode == "voice"),
email: (email if mode == "email"),
ampm: time,
subscriber_details: {
"whySignup" => reason,
"howHeard" => source,
"allowContact" => research,
}
)
end
end
4 changes: 1 addition & 3 deletions app/views/forecasts/_subscribe.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,5 @@
<header>
Subscribe to receive air quality alerts for your area
</header>
<a href="#">
Sign up for alerts
</a>
<%= link_to("Sign up for alerts", subscriptions_path, class: "button") %>
</div>
Loading

0 comments on commit e586354

Please sign in to comment.