Module: Bible270::PlanHelper

Defined in:
app/helpers/bible270/plan_helper.rb

Constant Summary collapse

'bible270_scripture'

Instance Method Summary collapse

Instance Method Details

#b270_avatar(reader, size: 72) ⇒ Object



154
155
156
157
158
159
160
161
162
# File 'app/helpers/bible270/plan_helper.rb', line 154

def b270_avatar(reader, size: 72)
  src = b270_avatar_src(reader)
  if src
    image_tag src, class: 'b270-avatar', width: size, height: size, alt: reader.display_name
  else
     :span, reader.initials, class: 'b270-avatar b270-avatar-fallback',
                                        style: "width:#{size}px;height:#{size}px;line-height:#{size}px"
  end
end

#b270_avatar_src(reader) ⇒ Object



148
149
150
151
152
# File 'app/helpers/bible270/plan_helper.rb', line 148

def b270_avatar_src(reader)
  return main_app.rails_blob_path(reader.avatar, only_path: true) if reader.avatar_uploaded?

  reader.avatar_url.presence
end

#b270_bible_versionObject



110
111
112
# File 'app/helpers/bible270/plan_helper.rb', line 110

def b270_bible_version
  current_reader&.effective_bible_version || Translations.resolve(nil)
end

#b270_can_delete?(comment) ⇒ Boolean

Returns:

  • (Boolean)


183
184
185
186
187
# File 'app/helpers/bible270/plan_helper.rb', line 183

def b270_can_delete?(comment)
  return false unless signed_in?

  comment.reader_id == current_reader.id || Bible270.config.admin?(current_reader)
end

#b270_can_edit?(comment) ⇒ Boolean

Only the writer may change their own words. An admin can remove a reflection but not rewrite it: putting words in someone's mouth is worse than taking them away, and taking them away is already the moderator's job.

Returns:

  • (Boolean)


179
180
181
# File 'app/helpers/bible270/plan_helper.rb', line 179

def b270_can_edit?(comment)
  signed_in? && comment.reader_id == current_reader.id
end


49
50
51
52
53
54
# File 'app/helpers/bible270/plan_helper.rb', line 49

def b270_custom_footer
  case Bible270.config.footer_style
  when :partial then render(Bible270.config.footer_partial)
  when :html then (:footer, Bible270.config.footer_html.html_safe, class: 'b270-footer')
  end
end

#b270_date(time) ⇒ Object

Timestamps in the plan's zone rather than UTC. Dates — started_on and the like — need no conversion and are formatted inline.



166
167
168
169
# File 'app/helpers/bible270/plan_helper.rb', line 166

def b270_date(time)
  local = Bible270.local_time(time)
  local&.strftime('%b %-d, %Y')
end

#b270_datetime(time) ⇒ Object



171
172
173
174
# File 'app/helpers/bible270/plan_helper.rb', line 171

def b270_datetime(time)
  local = Bible270.local_time(time)
  local&.strftime('%b %-d, %Y at %-I:%M %p')
end

#b270_day_cell(day, reader: nil, today: nil) ⇒ Object

One cell of the 270-day grid. Four views drew this by hand in four slightly different ways; now they all call this.

The title attribute carries the readings for pointer users; aria-label also includes date, progress, today, and reflection state for assistive technology.



194
195
196
197
198
199
# File 'app/helpers/bible270/plan_helper.rb', line 194

def b270_day_cell(day, reader: nil, today: nil)
  link_to day, day_path(day),
          class: b270_day_cell_classes(day, reader: reader, today: today),
          title: b270_day_readings(day),
          aria: { label: b270_day_cell_label(day, reader: reader, today: today) }
end

#b270_day_cell_classes(day, reader: nil, today: nil) ⇒ Object

Shared with the admin grid, which is a button_to rather than a link because it toggles the day rather than navigating to it.



216
217
218
219
220
221
222
# File 'app/helpers/bible270/plan_helper.rb', line 216

def b270_day_cell_classes(day, reader: nil, today: nil)
  classes = ['b270-cell', b270_day_status_class(reader, day)]
  classes << 'talk' if b270_days_with_reflections.include?(day)
  classes << 'now' if today && day == today

  classes.reject(&:blank?).join(' ')
end

#b270_day_cell_label(day, reader: nil, today: nil) ⇒ Object



201
202
203
204
205
206
207
208
209
210
211
212
# File 'app/helpers/bible270/plan_helper.rb', line 201

def b270_day_cell_label(day, reader: nil, today: nil)
  parts = ["Day #{day}"]
  parts << reader.date_for_day(day).strftime('%A, %B %-d, %Y') if reader&.dated?
  if reader
    status = { complete: 'complete', partial: 'partially read', none: 'not read' }.fetch(reader.day_status(day))
    parts << status
  end
  parts << 'today' if today == day
  parts << 'has reflections' if b270_days_with_reflections.include?(day)
  parts << b270_day_readings(day)
  parts.join('. ')
end

The same references as links to the passage, in the reader's own translation when they have chosen one — b270_passage_url falls back to the site default for a visitor.



233
234
235
236
237
238
239
240
241
242
243
244
# File 'app/helpers/bible270/plan_helper.rb', line 233

def b270_day_reading_links(day)
  readings = Bible270::Plan.readings_for(day)

  links = Bible270::Plan::TRACKS.keys.filter_map do |track|
    reference = readings[track]
    next if reference.blank?

    b270_passage_link reference, class: 'b270-reflink'
  end

  safe_join(links, ' · ')
end

#b270_day_readings(day) ⇒ Object

"Genesis 1-3 · Matthew 1 · Psalm 1" as plain text, for a title attribute.



225
226
227
228
# File 'app/helpers/bible270/plan_helper.rb', line 225

def b270_day_readings(day)
  readings = Bible270::Plan.readings_for(day)
  Bible270::Plan::TRACKS.keys.filter_map { |track| readings[track] }.join(' · ')
end

#b270_day_status_class(reader, day) ⇒ Object



260
261
262
263
264
265
266
267
268
# File 'app/helpers/bible270/plan_helper.rb', line 260

def b270_day_status_class(reader, day)
  return '' unless reader

  case reader.day_status(day)
  when :complete then 'complete'
  when :partial  then 'partial'
  else ''
  end
end

#b270_days_with_reflectionsObject

The days that have at least one visible reflection, fetched once per request. Called from the layout's grid, which no controller sets up, so it cannot be an instance variable assigned in an action.



249
250
251
252
253
254
255
256
257
258
# File 'app/helpers/bible270/plan_helper.rb', line 249

def b270_days_with_reflections
  @b270_days_with_reflections ||=
    if defined?(Bible270::Comment)
      Bible270::Comment.approved.distinct.pluck(:day).to_set
    else
      Set.new
    end
rescue StandardError
  Set.new
end

#b270_favicon_tagObject



101
102
103
104
105
106
107
108
# File 'app/helpers/bible270/plan_helper.rb', line 101

def b270_favicon_tag
  favicon = Bible270.config.favicon
  return if favicon == false

  href = favicon.presence || Favicon.data_uri
  type = href.start_with?('data:image/svg+xml', 'http') || href.end_with?('.svg') ? 'image/svg+xml' : nil
  tag.link(rel: 'icon', type: type, href: href)
end

The signed-in reader's translation, falling back to the site default for visitors who aren't signed in. Applies to every page the engine's layout renders, i.e. everything under the mount point. Pages rendered inside a host layout need this in that layout's instead. The footer, from whichever source the host configured. A partial is looked up in the host's view paths as well as the engine's, so 'shared/footer' finds app/views/shared/_footer.html.erb. The footer, from whichever source the host configured. A partial is looked up in the host's view paths as well as the engine's, so 'shared/footer' finds app/views/shared/_footer.html.erb. config.footer_placement decides whether a custom footer replaces the engine's or sits alongside it.



35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/helpers/bible270/plan_helper.rb', line 35

def b270_footer
  config = Bible270.config
  return if config.footer_style == :none
  return render('bible270/shared/footer') if config.footer_style == :default

  parts = [b270_custom_footer]
  if config.keep_default_footer?
    default = render('bible270/shared/footer')
    config.resolved_footer_placement == :after ? parts.unshift(default) : parts.push(default)
  end

  safe_join(parts)
end

#b270_mark(size: 38) ⇒ Object



90
91
92
93
94
95
96
97
98
99
# File 'app/helpers/bible270/plan_helper.rb', line 90

def b270_mark(size: 38)
  configured = Bible270.config.header_mark
  return nil if configured == false

  if configured.is_a?(String) && configured.present?
    return image_tag(configured, width: size, height: size, alt: '', class: 'b270-mark')
  end

   :span, Favicon.inline_svg(size: size).html_safe, class: 'b270-mark'
end

#b270_omniauth_path(provider) ⇒ Object

Path that begins the OmniAuth request phase. Must be POSTed to: OmniAuth 2.0+ refuses GET on its own routes (CVE-2015-9284), so every sign-in control in these views is a button_to, not a link_to.



14
15
16
17
# File 'app/helpers/bible270/plan_helper.rb', line 14

def b270_omniauth_path(provider)
  prefix = Bible270.config.omniauth_path_prefix || "#{request.script_name}/auth"
  "#{prefix}/#{provider}"
end

The named target and noopener are the secure no-JavaScript fallback. Interaction UI retains one opener-backed tab handle on desktop. iOS Safari cannot foreground a reused background tab, so it closes and replaces that script-opened tab on each click. The two fixed providers are intentionally trusted for this enhanced path.



130
131
132
133
134
# File 'app/helpers/bible270/plan_helper.rb', line 130

def b270_passage_link(reference, **options)
  data = options.fetch(:data, {}).merge(b270_passage_link: true)
  options.merge!(data: data, target: PASSAGE_LINK_TARGET, rel: 'noopener')
  link_to reference, b270_passage_url(reference), options
end

#b270_passage_url(reference, version = nil) ⇒ Object



114
115
116
117
118
119
120
121
122
123
# File 'app/helpers/bible270/plan_helper.rb', line 114

def b270_passage_url(reference, version = nil)
  return '#' if reference.blank?

  selected_version = version || b270_bible_version
  if selected_version != Translations::ORIGINAL_LANGUAGES && current_reader&.blue_letter_bible?
    return Translations.passage_url(reference, selected_version, blue_letter: true)
  end

  Translations.passage_url(reference, selected_version)
end

#b270_provider_label(provider) ⇒ Object



19
20
21
# File 'app/helpers/bible270/plan_helper.rb', line 19

def b270_provider_label(provider)
  Bible270.config.label_for_provider(provider)
end

#b270_track(track) ⇒ Object



7
8
9
# File 'app/helpers/bible270/plan_helper.rb', line 7

def b270_track(track)
  Plan::TRACKS.fetch(track.to_s)
end

#b270_version_tag(version = nil, track: nil) ⇒ Object

Small grey "(NKJV)" after a reading, so it's clear which translation the link opens in. The original-language preference names the actual language used for each track rather than repeating the combined preference code.



139
140
141
142
143
144
145
146
# File 'app/helpers/bible270/plan_helper.rb', line 139

def b270_version_tag(version = nil, track: nil)
  label = version || b270_bible_version
  if label == Translations::ORIGINAL_LANGUAGES && track
    label = track.to_s == 'nt' ? 'Greek' : 'Hebrew'
  end

   :span, "(#{label})", class: 'b270-version'
end

#b270_with_mentions(body) ⇒ Object

The mark beside the title in the header. Larger than the favicon and inlined as SVG, so it stays crisp at any size and needs no asset pipeline. Renders "@handle" as a link to that reader. An unresolved or ambiguous handle is left as plain text, which is how a writer discovers it found nobody.

Built by walking the matches and escaping everything between them, rather than escaping afterwards: the body is reader-supplied, so the only safe construction is one where every non-link fragment is escaped by hand.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'app/helpers/bible270/plan_helper.rb', line 64

def b270_with_mentions(body)
  text = body.to_s
  readers = Bible270::Reader.mentioned_in(text)

  pieces = []
  cursor = 0

  text.to_enum(:scan, Bible270::Mentions::PATTERN).each do
    match = Regexp.last_match
    pieces << ERB::Util.html_escape(text[cursor...match.begin(0)])

    handle = Bible270::Mentions.normalize(match[1])
    reader = readers.find { |candidate| candidate.answers_to?(handle) }
    pieces << if reader
                link_to(match[0], reader_path(reader), class: 'b270-mention')
              else
                ERB::Util.html_escape(match[0])
              end

    cursor = match.end(0)
  end

  pieces << ERB::Util.html_escape(text[cursor..].to_s)
  safe_join(pieces)
end