app/helpers/application_helper.rb
# typed: strict
# frozen_string_literal: true
# Methods added to this helper will be available to all templates in the application.
module ApplicationHelper
extend T::Sig
# For sorbet
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TextHelper
include ActionView::Helpers::FormHelper
include Kernel
sig { params(quiet: T::Boolean).returns(String) }
def pa_link_classes(quiet:)
c = "text-fuchsia hover:text-fuchsia-darker focus:outline-none focus:bg-sun-yellow"
if quiet
"#{c} hover:underline"
else
"#{c} font-bold underline"
end
end
# TODO: Generalise to support all the variants
sig { params(body: T.untyped, url: T.untyped, extra_classes: T.nilable(String), title: T.nilable(String), quiet: T::Boolean).returns(String) }
def pa_link_to(body, url, extra_classes: nil, title: nil, quiet: false)
# These extra classes can't override the default styling because they're at the end
link_to(body, url, class: "#{pa_link_classes(quiet:)} #{extra_classes}", title:)
end
# TODO: Generalise to support all the variants
sig { params(body: T.untyped, url: T.untyped, extra_classes: T.nilable(String), title: T.nilable(String), quiet: T::Boolean).returns(String) }
def pa_link_to_unless_current(body, url, extra_classes: nil, title: nil, quiet: false)
# These extra classes can't override the default styling because they're at the end
link_to_unless_current(body, url, class: "#{pa_link_classes(quiet:)} #{extra_classes}", title:)
end
# TODO: Generalise to support all the variants
# TODO: The signature is not consistent with pa_link_to. This is currently used by kamanari pager.
sig { params(condition: T::Boolean, body: T.untyped, url: T.untyped, html_options: T::Hash[Symbol, T.untyped], quiet: T::Boolean).returns(String) }
def pa_link_to_unless(condition, body, url, html_options = {}, quiet: false)
# These extra classes can't override the default styling because they're at the end
html_options[:class] = "#{pa_link_classes(quiet:)} #{html_options[:class]}"
link_to_unless(condition, body, url, html_options)
end
sig { params(path: String, extra_classes: T::Array[Symbol], block: T.untyped).returns(T.untyped) }
def menu_item(path, extra_classes: [], &block)
li_selected(current_page?(path), extra_classes:) do
link_to(capture(&block), path)
end
end
sig { params(selected: T::Boolean, extra_classes: T::Array[Symbol], block: T.untyped).returns(T.untyped) }
def li_selected(selected, extra_classes: [], &block)
content_tag(:li, capture(&block), class: extra_classes + (selected ? [:selected] : []))
end
sig { params(url: String, block: T.untyped).returns(T.untyped) }
def nav_item(url, &block)
active = current_page?(url)
body = capture(&block)
body += content_tag(:span, "(current)", class: "sr-only") if active
content_tag(:li, link_to(body, url, class: "nav-link"),
class: ["nav-item", ("active" if active)])
end
sig { params(meters: T.any(Float, Integer)).returns(String) }
def meters_in_words(meters)
if meters < 1000
value = meters.to_f
units = "m"
else
value = meters / 1000.0
units = "km"
end
"#{significant_figure_remove_trailing_zero(value, 2)} #{units}"
end
sig { params(value: Float, sig_figs: Integer).returns(T.untyped) }
def significant_figure_remove_trailing_zero(value, sig_figs)
text = significant_figure(value, sig_figs).to_s
if text[-2..] == ".0"
text[0..-3]
else
text
end
end
# Round the number a to s significant figures
sig { params(value: Float, sig_figs: Integer).returns(Float) }
def significant_figure(value, sig_figs)
if value.positive?
a = Math.log10(value).ceil - sig_figs
if a.negative?
m = T.cast(10**-a, Integer)
(value.to_f * m).round.to_f / m
else
m = T.cast(10**a, Integer)
(value.to_f / m).round.to_f * m
end
elsif value.negative?
-significant_figure(-value, sig_figs)
else
0.0
end
end
sig { params(value_in_km: Float).returns(String) }
def km_in_words(value_in_km)
meters_in_words(value_in_km * 1000)
end
# For some particular number of days return a human readable version
sig { params(days: Integer).returns(String) }
def days_in_words(days)
case days
when 365 / 4
"3 months"
when 365 / 2
"6 months"
when 365
"year"
when 365 * 2
"2 years"
when 365 * 5
"5 years"
when 365 * 10
"10 years"
else
raise "Unexpected number of days"
end
end
sig { returns(T::Array[T::Hash[Symbol, String]]) }
def contributors
JSON.parse(File.read("CONTRIBUTORS.json"), symbolize_names: true)
end
sig { params(contributor: T::Hash[Symbol, String]).returns(String) }
def contributor_profile_url(contributor)
if contributor[:github].blank?
params = {
q: "fullname:\"#{contributor[:name]}\"",
type: "Users"
}
"https://github.com/search?#{params.to_query}"
else
"https://github.com/#{contributor[:github]}"
end
end
sig { params(html: String).returns(String) }
def our_sanitize(html)
# Using sanitize gem here because it also adds rel="nofollow" to links automatically
# which reduces "SEO" spam
cleaned = Sanitize.clean(html, Sanitize::Config::BASIC)
# We're trusting that the sanitize library does the right thing here. We
# kind of have to. It's returning some allowed html. So, we have to mark
# it as safe
# rubocop:disable Rails/OutputSafety
cleaned.html_safe
# rubocop:enable Rails/OutputSafety
end
sig { params(params: T::Hash[Symbol, String]).returns(String) }
def facebook_share_url(params)
"https://www.facebook.com/sharer/sharer.php?#{params.to_query}"
end
sig { params(params: T::Hash[Symbol, String]).returns(String) }
def twitter_share_url(params)
"https://twitter.com/intent/tweet?#{params.to_query}"
end
sig { returns(String) }
def donate_url
"https://donate.planningalerts.org.au/"
end
sig { params(name: String, path: T.untyped, options: T::Hash[Symbol, String], block: T.nilable(T.proc.void)).returns(String) }
def pa_button_to(name, path = nil, options = {}, &block)
if block_given?
path = name
name = capture(&block)
end
form_with(
url: url_for(path),
class: options.delete(:form_class),
method: options.delete(:method),
data: options.delete(:data)
) do |f|
f.button name, options
end
end
# Go to the contact page and pre-fill that you want to talk about API stuff
sig { returns(String) }
def api_contact_path
Rails.application.routes.url_helpers.documentation_contact_path(reason: "I want API access or commercial use")
end
end