Skip to content

Commit

Permalink
Subscription form
Browse files Browse the repository at this point in the history
  • Loading branch information
jdudley1123 committed Nov 29, 2024
1 parent ae5a0a3 commit 209cb89
Show file tree
Hide file tree
Showing 23 changed files with 938 additions and 6 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 @@ -432,6 +432,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 @@ -494,6 +496,7 @@ DEPENDENCIES
view_component
web-console (>= 3.3.0)
webmock
wicked

RUBY VERSION
ruby 3.3.6p108
Expand Down
52 changes: 52 additions & 0 deletions app/controllers/subscriptions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

class SubscriptionsController < ApplicationController
include Wicked::Wizard

STEPS = %i[contact_details contact_verification zone_selection time_selection wrap_up confirmation]
steps(*STEPS)

def show
session[:subscription_form] = nil if params[:reset]
load_step

# Validate against the previous step
@form.current_step = previous_step
jump_to(previous_step) if @step_number != 1 && @form.nil? && @form.invalid?

# Switch back to the current step
@form.current_step = step

render_wizard
end

def update
load_step
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 finish_wizard_path
forecast_path notice: "Thank you for subscribing!"
end

def load_step
@step_number = steps.index(step).to_i + 1
@current_step = step
@first_step = (step == steps.first)
@final_step = (step == steps.last)
@form = SubscriptionForm.new(session[:subscription_form])
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("navigation", NavigationController);

import ShareController from "./share_controller";
application.register("share", ShareController);

import SubscriptionController from "./subscription_controller";
application.register("subscription", SubscriptionController);
180 changes: 180 additions & 0 deletions app/javascript/controllers/subscription_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
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",
"tags",
];
static debounces = ["search"];

connect() {
useDebounce(this);
}

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

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

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

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

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

async geoCode(location) {
if (!location) {
return;
}

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) {
if (!point) {
return;
}

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 = "";
this.searchResultsTarget.classList.remove("hidden");

if (!results || results.length === 0) {
const li = document.createElement("li");
li.textContent = "No results found within the area covered by airTEXT";
this.searchResultsTarget.appendChild(li);
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;
this.addZoneTag(zoneName);

// Remove from search results
event.target.remove();
}

tagsTargetConnected() {
const checkboxes = document.querySelectorAll(
"input[name='subscription_form[zones][]']"
);
checkboxes.forEach((checkbox) => {
checkbox.addEventListener("change", (event) =>
this.checkboxChanged(event)
);

if (checkbox.checked) {
this.addZoneTag(checkbox.value);
}
});
}

checkboxChanged(event) {
const zoneName = event.target.value;
if (event.target.checked) {
this.addZoneTag(zoneName);
} else {
this.removeZoneTag(zoneName);
}
}

setCheckboxState(zone_name, state) {
const checkbox = document.querySelector(`input[value="${zone_name}"]`);
checkbox.checked = state;
checkbox.dispatchEvent(new Event("change"));
}

tag(zoneName) {
return this.tagsTarget.querySelector(`span[data-zone-name="${zoneName}"]`);
}

tagClicked(event) {
const zoneName = event.target.dataset.zoneName;
this.setCheckboxState(zoneName, false);
}

addZoneTag(zoneName) {
if (this.tag(zoneName)) {
return;
}

const tag = document.createElement("span");
tag.textContent = zoneName;
tag.classList.add("tag");
tag.dataset.zoneName = zoneName;
tag.dataset.action = "click->subscription#tagClicked";
this.tagsTarget.appendChild(tag);

this.setCheckboxState(zoneName, true);
}

removeZoneTag(zoneName) {
this.tag(zoneName).remove();
}
}
Loading

0 comments on commit 209cb89

Please sign in to comment.