mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-10-31 11:21:06 -04:00
200 lines
7.2 KiB
Ruby
200 lines
7.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'nokogiri'
|
|
module ApplicationHelper
|
|
# Renders an empty element for app/javascript/react_islands.jsx to mount the
|
|
# named React component into, passing it props. The page also needs the
|
|
# bundle: `= javascript_include_tag 'react_islands', defer: true`.
|
|
# A block is the page's own markup for the island, shown until it mounts and
|
|
# to anyone without JavaScript; the island replaces it.
|
|
def react_component(name, props = {}, html_options = {}, &block)
|
|
content_tag(:div, block ? capture(&block) : '', html_options.merge(data: { react_component: name, props: props.to_json }))
|
|
end
|
|
|
|
# What the PhotoDialog island needs: a fallback name for the dialog's heading,
|
|
# and the icon in its header. The links it opens from carry the rest.
|
|
def photo_dialog_props(label = 'this record')
|
|
{ label:, icon_url: image_path('icons/photo.svg') }
|
|
end
|
|
|
|
# What the AuthDialog island needs: where each dialog's form posts (and the
|
|
# pages they stand in for), the captcha's site key, and the site's name.
|
|
def auth_dialog_props
|
|
{
|
|
urls: {
|
|
sign_in_page: new_member_session_path, sign_up_page: new_member_registration_path,
|
|
sign_in: member_session_path, sign_up: member_registration_path,
|
|
forgot_password: new_member_password_path
|
|
},
|
|
recaptcha_site_key: Recaptcha.configuration.site_key.presence,
|
|
site_name: ENV.fetch('GROWSTUFF_SITE_NAME', 'Growstuff')
|
|
}
|
|
end
|
|
|
|
# Whether the page being shown is one of these paths or something under one,
|
|
# for marking which part of the site the top menu's link belongs to. Any
|
|
# anchor or query on a path is ignored.
|
|
def current_section?(*paths)
|
|
paths.any? do |path|
|
|
base = path.to_s.split(/[#?]/).first
|
|
request.path == base || request.path.start_with?("#{base}/")
|
|
end
|
|
end
|
|
|
|
def parse_date(str)
|
|
str ||= '' # Date.parse barfs on nil
|
|
str == '' ? nil : Date.parse(str)
|
|
end
|
|
|
|
def build_alert_classes(alert_type = :info)
|
|
classes = 'alert alert-dismissible fade show '
|
|
case alert_type.to_sym
|
|
when :alert, :danger, :error, :validation_errors
|
|
classes += 'alert-danger'
|
|
when :warning, :todo
|
|
classes += 'alert-warning'
|
|
when :notice, :success
|
|
classes += 'alert-success'
|
|
when :info
|
|
classes += 'alert-info'
|
|
end
|
|
classes
|
|
end
|
|
|
|
# Similar to Rails' time_ago_in_words, but gives a more standard
|
|
# output like "in 3 days" or "5 months ago".
|
|
# Also handles the case where from_time is a Date and to_time is a Date
|
|
# (in which case it just says "today" if they're the same date).
|
|
#
|
|
# NOTE: This is similar to distance_of_time_in_words but different enough
|
|
# that I think it's worth having a separate helper for it.
|
|
#
|
|
# from_time - the starting time (Time or Date)
|
|
# to_time - the ending time (Time or Date). Default: now (Time.zone.now)
|
|
# include_seconds - whether to include seconds in the calculation
|
|
#
|
|
# Returns a string like "in 3 days" or "5 months ago"
|
|
def standard_time_distance(from_time, to_time = 0, include_seconds = false)
|
|
return 'today' if from_time.is_a?(Date) && (from_time == to_time)
|
|
|
|
return 'now' if from_time == to_time
|
|
return "#{distance_of_time_in_words(from_time, to_time, include_seconds:)} ago" if from_time < to_time
|
|
|
|
"in #{distance_of_time_in_words(from_time, to_time, include_seconds:)}"
|
|
end
|
|
|
|
# Produces a cache key for uniquely identifying cached fragments.
|
|
def cache_key_for(klass, identifier = "all")
|
|
count = klass.count
|
|
max_updated_at = klass.maximum(:updated_at).try(:utc).try(:to_fs, :number)
|
|
"#{klass.name.downcase.pluralize}/#{identifier}-#{count}-#{max_updated_at}"
|
|
end
|
|
|
|
# A helper to replace the complex template compilation mess
|
|
# of HAML, Tilt, and dynamic compilation with interpolated ruby.
|
|
#
|
|
# Photos placed in a post are only drawn with `figures: true`, which is for
|
|
# the post itself, and must be given text that has already had its tags
|
|
# stripped, as the figures are HTML.
|
|
def markdownify(text, figures: false)
|
|
translator = Haml::Filters::GrowstuffMarkdown.new
|
|
text = text.to_s.dup
|
|
translator.expand_photos!(text, figures:)
|
|
translator.expand_crops!(text)
|
|
translator.expand_plantings!(text)
|
|
translator.expand_members!(text)
|
|
text
|
|
end
|
|
|
|
# A 1x1 transparent GIF, standing in for a real avatar when
|
|
# config.x.blank_avatars is set, which browser specs do: otherwise they fetch
|
|
# every avatar from gravatar.com for real, and one arriving mid-test moves
|
|
# whatever sits under it, so clicks land on the wrong thing.
|
|
BLANK_AVATAR = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
|
|
|
|
#
|
|
# Returns an image uri for a given member.
|
|
#
|
|
# Falls back to Gravatar
|
|
#
|
|
def avatar_uri(member, size = 150)
|
|
return unless member
|
|
# Compared against true on purpose: an unset config.x key returns an empty
|
|
# OrderedOptions, which is truthy, so a plain `if` here blanks avatars in
|
|
# every environment.
|
|
return BLANK_AVATAR if Rails.configuration.x.blank_avatars == true
|
|
|
|
if member.preferred_avatar_uri.present?
|
|
# Some avatars support different sizes
|
|
# http://graph.facebook.com/12345678/picture?width=150&height=150
|
|
uri = URI.parse(member.preferred_avatar_uri)
|
|
|
|
uri.query = "&width=#{size}&height=#{size}" if uri.host == 'graph.facebook.com'
|
|
|
|
# TODO: Assess flickr - https://www.flickr.com/services/api/misc.buddyicons.html
|
|
|
|
return uri.to_s
|
|
end
|
|
|
|
Gravatar.new(member.email).image_url(size:,
|
|
default: :identicon,
|
|
ssl: true)
|
|
end
|
|
|
|
# Returns a string with the quantity and the right pluralization for a
|
|
# given collection and model.
|
|
def localize_plural(collection, model)
|
|
pluralize(collection.size, model.model_name.to_s.downcase)
|
|
end
|
|
|
|
def show_inactive_tickbox_path(type, owner: nil, crop: nil, show_all: false)
|
|
all = show_all ? '' : 1
|
|
|
|
if owner.present?
|
|
public_send("member_#{type}_path", owner, all:)
|
|
elsif crop.present?
|
|
public_send("crop_#{type}_path", crop, all:)
|
|
else
|
|
public_send("#{type}_path", all:)
|
|
end
|
|
end
|
|
|
|
def title(type, owner, crop, planting)
|
|
if owner
|
|
t(".title.owner_#{type}", owner: owner.login_name)
|
|
elsif crop
|
|
t(".title.crop_#{type}", crop: crop.name)
|
|
elsif planting
|
|
t(".title.planting_#{type}", planting: planting.to_s)
|
|
else
|
|
t(".title.default")
|
|
end
|
|
end
|
|
|
|
def og_description(description)
|
|
strip_tags(description).split(' ')[0..20].join(' ')
|
|
end
|
|
|
|
def github_releases
|
|
return [] if Rails.env.test?
|
|
|
|
feed_url = 'https://github.com/Growstuff/growstuff/releases.atom'
|
|
# A failed fetch returns nil, which skip_nil keeps out of the cache.
|
|
Rails.cache.fetch(feed_url, expires_in: 1.day, skip_nil: true) do
|
|
response = Faraday.get(feed_url)
|
|
next unless response.success?
|
|
|
|
Nokogiri::XML(response.body).xpath('//xmlns:entry').first(2).map do |entry|
|
|
{
|
|
title: entry.xpath('xmlns:title').text,
|
|
content: entry.xpath('xmlns:content').text,
|
|
link: entry.xpath('xmlns:link/@href').text,
|
|
updated: entry.xpath('xmlns:updated').text
|
|
}
|
|
end
|
|
end || []
|
|
rescue Faraday::Error, Nokogiri::XML::XPath::SyntaxError
|
|
[]
|
|
end
|
|
end
|