bible270 0.11.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52e08240916fa78ed55408939d5ab6ecd297113f96b4f1cee646d471a9b289dd
4
- data.tar.gz: 5e078496dab3ca466eb2c1ef702a1ef4578cfa16566aa8194eb04022025ac05b
3
+ metadata.gz: 7d1cc4a6173e82beae097bc870026ba3ae1ae0869a32f77efab2ea12fa1dbe2b
4
+ data.tar.gz: be16d337547c10478a9160bc9a7b229d1994f37305c0d68801564549a2c1bb78
5
5
  SHA512:
6
- metadata.gz: 07ab2d3720035d36f7dd9226455d2239db65fe0c623a682aea50ceec94cd74d4a0d13dd5c3cac0b06c8d66b2a08234d57f4cdb432ac647d905ff0f9116b5a2dd
7
- data.tar.gz: eb5082979de6f974634b2a6abdafab906f7bbd62d68b465f14b13d5a9ec73d461c82f203c79e1c48084e4e7fdc00c16ea7bf3390713f3c11447267ed980ab1c9
6
+ metadata.gz: 16dafcc090a16f119b5ca9db84c362116c44375ef5e729ca8a915cbe0b6421fb0772847372392624cdaef22d45a39d52e1f33426b1d4ca082dd3d7cc16ad547f
7
+ data.tar.gz: c24039c41a16d88e2cfc6f400f6054ab552c8a2fb3669260e3dee1eaddf2fbcbb0745f8acd4693a7875f6c2c4b8a7b42d87978c5869cde0fb5e40ee16bdeae8f
data/CHANGELOG.md CHANGED
@@ -1,16 +1,31 @@
1
1
  # Changelog
2
2
 
3
- All notable changes to bible270. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
4
- this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html), treating pre-1.0 minor bumps as the place breaking changes may land.
3
+ All notable changes to bible270. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html), treating pre-1.0 minor bumps as the place breaking changes may land.
5
4
 
6
5
  ## Unreleased Changes
7
6
 
8
7
  See the changes since the last release:
9
8
 
10
- **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.11.0...main)**
9
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.12.0...main)**
10
+
11
+ ## [0.12.0] — 2026-07-29
12
+
13
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.11.0...v0.12.0)**
14
+
15
+ ### Added
16
+
17
+ - Readers can select their preferred Bible translation in profile edit view
18
+ - Scripture links show bible version in parens
19
+
20
+ ### Changed
21
+
22
+ - Top menu shows 'Progress' in place of user display name
23
+ - Dependabot gem updates to minitest and rubocop
11
24
 
12
25
  ## [0.11.0] — 2026-07-28
13
-
26
+
27
+ **[Full Changelog](https://github.com/avonderluft/bible270/compare/v0.10.0...v0.11.0)**
28
+
14
29
  ### Added
15
30
 
16
31
  - **Readers can edit their own name and picture** at `<mount>/profile`, linked from the nav. An admin
@@ -234,7 +249,8 @@ This is the initial public release.
234
249
 
235
250
  * Email sign-in requires working Action Mailer delivery in the host application. Set `config.mailer_from`; delivery is inline by default, or set `email_sign_in_deliver_later` when a queue backend is available.
236
251
 
237
- [0.11.0]: https://github.com/avonderluft/bible270/releases/tag/v0.10.0
252
+ [0.12.0]: https://github.com/avonderluft/bible270/releases/tag/v0.12.0
253
+ [0.11.0]: https://github.com/avonderluft/bible270/releases/tag/v0.11.0
238
254
  [0.10.0]: https://github.com/avonderluft/bible270/releases/tag/v0.10.0
239
255
  [0.9.0]: https://github.com/avonderluft/bible270/releases/tag/v0.9.0
240
256
  [0.6.2]: https://github.com/avonderluft/bible270/releases/tag/v0.6.2
data/README.md CHANGED
@@ -297,8 +297,8 @@ Bible270.configure do |config|
297
297
  config.tagline = "A 270-day journey through Scripture"
298
298
  config.admin_emails = %w[andrew@example.org]
299
299
 
300
- # config.start_date = nil # community start date, or nil for undated
301
- config.start_date = Date.new(2026, 9, 6)
300
+ config.start_date = nil # community start date, or nil for undated
301
+ # config.start_date = Date.new(2026, 9, 6)
302
302
  config.allow_reader_start_date = true
303
303
 
304
304
  config.parent_controller = "ActionController::Base" # or "::ApplicationController"
@@ -322,6 +322,42 @@ Bible270.configure do |config|
322
322
  }
323
323
  end
324
324
  ```
325
+ ### Footer
326
+
327
+ The engine's pages end with a short note about how the plan works. Replace it with your own:
328
+
329
+ ```ruby
330
+ config.footer_partial = 'shared/footer' # a partial in your app
331
+ config.footer_html = '<p>&copy; 2026 …</p>'
332
+ config.footer = false # no footer at all
333
+ ```
334
+
335
+ By default yours replaces the engine's. To keep both:
336
+
337
+ ```ruby
338
+ config.footer_placement = :after # engine's note, then yours
339
+ config.footer_placement = :before # yours, then the engine's note
340
+ config.footer_placement = :replace # default
341
+ ```
342
+
343
+ A partial is looked up in your app's view paths as well as the engine's, so `'shared/footer'` finds
344
+ `app/views/shared/_footer.html.erb`. Keep it reasonably self-contained: engine controllers include
345
+ the engine's helpers, not your app's, so a partial calling your own helpers needs
346
+ `helper ::ApplicationHelper` adding to the engine's controller — or just inline the markup.
347
+
348
+ ### Favicon
349
+
350
+ Pages under the mount point carry their own favicon — a rustic loaf, inlined as an SVG data URI, so
351
+ there's nothing to add to your asset pipeline.
352
+
353
+ ```ruby
354
+ config.favicon = nil # built-in loaf (default)
355
+ config.favicon = '/icons/my-icon.svg' # your own
356
+ config.favicon = false # none; the host's favicon applies
357
+ ```
358
+
359
+ It's emitted by the engine's layout, so if you point `config.layout` at your own layout, add
360
+ `<%= b270_favicon_tag %>` to its `<head>`.
325
361
 
326
362
  References link to Bible Gateway by default — point `passage_url_builder` at your another reader if you wish, or your own reader if you host one.
327
363
 
@@ -10,7 +10,7 @@ module Bible270
10
10
  class AdminController < ApplicationController
11
11
  before_action :require_admin!
12
12
  before_action :load_reader,
13
- only: %i[show destroy update_start update_name complete_through toggle_day]
13
+ only: %i[show destroy update_start update_profile remove_avatar complete_through toggle_day]
14
14
  before_action :load_comment, only: %i[hide_comment unhide_comment destroy_comment]
15
15
 
16
16
  # ---- enrolment --------------------------------------------------------
@@ -95,14 +95,30 @@ module Bible270
95
95
  end
96
96
  end
97
97
 
98
- def update_name
99
- if @reader.update_names(params[:first_name], params[:last_name])
100
- redirect_to admin_reader_path(@reader), notice: 'Name updated.'
98
+ # Name and picture together, mirroring what a reader can change about
99
+ # themselves on their own profile.
100
+ def update_profile
101
+ problems = []
102
+ if (params[:first_name].present? || params[:last_name].present?) && !@reader.update_names(params[:first_name],
103
+ params[:last_name])
104
+ problems << 'both a first and last name'
105
+ end
106
+ if params[:avatar].present? && !@reader.attach_avatar(params[:avatar])
107
+ problems << (@reader.errors[:avatar].first || 'a valid image')
108
+ end
109
+
110
+ if problems.empty?
111
+ redirect_to admin_reader_path(@reader), notice: 'Updated.'
101
112
  else
102
- redirect_to admin_reader_path(@reader), alert: 'Both a first and last name are needed.'
113
+ redirect_to admin_reader_path(@reader), alert: "Needs #{problems.to_sentence}."
103
114
  end
104
115
  end
105
116
 
117
+ def remove_avatar
118
+ @reader.remove_avatar!
119
+ redirect_to admin_reader_path(@reader), notice: "Removed #{@reader.display_name}'s picture."
120
+ end
121
+
106
122
  def complete_through
107
123
  day = params[:day].to_i
108
124
  if @reader.mark_through!(day)
@@ -14,6 +14,9 @@ module Bible270
14
14
  problems = []
15
15
  problems << 'both a first and last name' unless current_reader.update_names(params[:first_name],
16
16
  params[:last_name])
17
+ if params[:bible_version].present? && !current_reader.update_bible_version(params[:bible_version])
18
+ problems << 'a translation from the list'
19
+ end
17
20
  if params[:avatar].present? && !current_reader.attach_avatar(params[:avatar])
18
21
  problems << (current_reader.errors[:avatar].first || 'a valid image')
19
22
  end
@@ -18,14 +18,64 @@ module Bible270
18
18
  Bible270.config.label_for_provider(provider)
19
19
  end
20
20
 
21
- def b270_passage_url(reference)
21
+ # The signed-in reader's translation, falling back to the site default for
22
+ # visitors who aren't signed in.
23
+ # Applies to every page the engine's layout renders, i.e. everything under
24
+ # the mount point. Pages rendered inside a host layout need this in that
25
+ # layout's <head> instead.
26
+ # The footer, from whichever source the host configured. A partial is looked
27
+ # up in the host's view paths as well as the engine's, so 'shared/footer'
28
+ # finds app/views/shared/_footer.html.erb.
29
+ # The footer, from whichever source the host configured. A partial is looked
30
+ # up in the host's view paths as well as the engine's, so 'shared/footer'
31
+ # finds app/views/shared/_footer.html.erb. config.footer_placement decides
32
+ # whether a custom footer replaces the engine's or sits alongside it.
33
+ def b270_footer
34
+ config = Bible270.config
35
+ return if config.footer_style == :none
36
+ return render('bible270/shared/footer') if config.footer_style == :default
37
+
38
+ parts = [b270_custom_footer]
39
+ if config.keep_default_footer?
40
+ default = render('bible270/shared/footer')
41
+ config.resolved_footer_placement == :after ? parts.unshift(default) : parts.push(default)
42
+ end
43
+
44
+ safe_join(parts)
45
+ end
46
+
47
+ def b270_custom_footer
48
+ case Bible270.config.footer_style
49
+ when :partial then render(Bible270.config.footer_partial)
50
+ when :html then content_tag(:footer, Bible270.config.footer_html.html_safe, class: 'b270-footer')
51
+ end
52
+ end
53
+
54
+ def b270_favicon_tag
55
+ favicon = Bible270.config.favicon
56
+ return if favicon == false
57
+
58
+ href = favicon.presence || Favicon.data_uri
59
+ type = href.start_with?('data:image/svg+xml', 'http') || href.end_with?('.svg') ? 'image/svg+xml' : nil
60
+ tag.link(rel: 'icon', type: type, href: href)
61
+ end
62
+
63
+ def b270_bible_version
64
+ current_reader&.effective_bible_version || Translations.resolve(nil)
65
+ end
66
+
67
+ def b270_passage_url(reference, version = nil)
22
68
  return '#' if reference.blank?
23
69
 
24
- Bible270.config.passage_url_builder.call(reference, Bible270.config.bible_version)
70
+ Bible270.config.passage_url_builder.call(reference, version || b270_bible_version)
71
+ end
72
+
73
+ # Small grey "(NKJV)" after a reading, so it's clear which translation the
74
+ # link opens in.
75
+ def b270_version_tag(version = nil)
76
+ content_tag :span, "(#{version || b270_bible_version})", class: 'b270-version'
25
77
  end
26
78
 
27
- # An uploaded avatar wins over whatever the sign-in provider gave us.
28
- # Active Storage routes live in the host app, hence main_app.
29
79
  def b270_avatar_src(reader)
30
80
  return main_app.rails_blob_path(reader.avatar, only_path: true) if reader.avatar_uploaded?
31
81
 
@@ -114,6 +114,22 @@ module Bible270
114
114
  Names.first_with_last_initial(first_name, last_name).presence || display_name
115
115
  end
116
116
 
117
+ # The translation this reader reads in. Null means "whatever the site default
118
+ # is", so changing config.bible_version moves everyone who hasn't chosen.
119
+ def effective_bible_version
120
+ Translations.resolve(bible_version)
121
+ end
122
+
123
+ def bible_version_label
124
+ Translations.label(effective_bible_version)
125
+ end
126
+
127
+ def update_bible_version(code)
128
+ return false unless Translations.valid?(code)
129
+
130
+ update(bible_version: Translations.normalize(code))
131
+ end
132
+
117
133
  def sort_name
118
134
  [last_name, first_name].map { |n| n.to_s.strip.downcase }.join(' ').strip.presence || display_name.to_s.downcase
119
135
  end
@@ -209,7 +225,7 @@ module Bible270
209
225
  def suggested_names
210
226
  return { first_name: first_name, last_name: last_name } if full_name
211
227
 
212
- Names.split(display_name) || { first_name: display_name, last_name: nil }
228
+ Names.split_display_name(display_name) || { first_name: display_name, last_name: nil }
213
229
  end
214
230
 
215
231
  # ---- administrative adjustments --------------------------------------
@@ -1,4 +1,5 @@
1
1
  <% content_for :title, "#{@reader.display_name} — Admin" %>
2
+ <% content_for :hide_day_index, true %><%# this page has its own grid %>
2
3
  <p class="b270-eyebrow"><%= link_to 'Admin', admin_path %></p>
3
4
  <h1><%= @reader.display_name %></h1>
4
5
  <p class="lede">
@@ -9,17 +10,37 @@
9
10
  </p>
10
11
 
11
12
  <div class="b270-panel">
12
- <h2 class="b270-subhead">Name</h2>
13
- <%= form_with url: admin_reader_name_path(@reader), method: :patch, class: "b270-emailform" do %>
13
+ <h2 class="b270-subhead">Name and picture</h2>
14
+ <%= form_with url: admin_reader_profile_path(@reader), method: :patch, class: "b270-emailform",
15
+ html: { multipart: true } do %>
16
+ <div class="b270-avatarrow">
17
+ <%= b270_avatar(@reader, size: 64) %>
18
+ <% if Bible270::Reader.avatar_uploads? %>
19
+ <label class="b270-field" style="flex:1">
20
+ <span>Picture</span>
21
+ <%= file_field_tag :avatar, accept: Bible270::Avatars.content_types.join(',') %>
22
+ <span class="b270-hint" style="margin:0">
23
+ PNG, JPEG, GIF or WebP, up to <%= Bible270::Avatars.human_size(Bible270::Avatars.max_bytes) %>.
24
+ </span>
25
+ </label>
26
+ <% end %>
27
+ </div>
28
+
14
29
  <label class="b270-field">
15
30
  <span>First name</span>
16
- <%= text_field_tag :first_name, @reader.suggested_names[:first_name], required: true %>
31
+ <%= text_field_tag :first_name, @reader.suggested_names[:first_name] %>
17
32
  </label>
18
33
  <label class="b270-field">
19
34
  <span>Last name</span>
20
- <%= text_field_tag :last_name, @reader.suggested_names[:last_name], required: true %>
35
+ <%= text_field_tag :last_name, @reader.suggested_names[:last_name] %>
21
36
  </label>
22
- <%= submit_tag 'Save name', class: 'b270-btn' %>
37
+ <%= submit_tag 'Save', class: 'b270-btn' %>
38
+ <% end %>
39
+
40
+ <% if @reader.avatar_uploaded? %>
41
+ <%= button_to "Remove picture", admin_reader_avatar_path(@reader), method: :delete,
42
+ class: 'b270-linkbtn',
43
+ form: { data: { turbo_confirm: "Remove #{@reader.display_name}'s picture?" } } %>
23
44
  <% end %>
24
45
  </div>
25
46
 
@@ -11,7 +11,7 @@
11
11
  <% end %>
12
12
  <div class="b270-rbody">
13
13
  <div class="b270-cat"><%= t[:label] %></div>
14
- <div class="b270-ref"><%= link_to ref, b270_passage_url(ref), target: "_blank", rel: "noopener" %></div>
14
+ <div class="b270-ref"><%= link_to ref, b270_passage_url(ref), target: "_blank", rel: "noopener" %> <%= b270_version_tag %></div>
15
15
  </div>
16
16
  </div>
17
17
  <% else %>
@@ -23,7 +23,8 @@
23
23
  <% end %>
24
24
  </div>
25
25
  <p style="margin:16px 0 0">
26
- <%= link_to "Continue reading Day #{@start_day} ", day_path(@start_day), class: "b270-btn" %>
26
+ <% @day_verb = @start_day == 1 ? "Start" : "Continue" %>
27
+ <%= link_to "#{@day_verb} reading — Day #{@start_day} →", day_path(@start_day), class: "b270-btn" %>
27
28
  <% if @today_day && @today_day != @start_day %>
28
29
  <%= link_to "Go to today (Day #{@today_day})", day_path(@today_day), style: "margin-left:12px" %>
29
30
  <% end %>
@@ -41,9 +42,10 @@
41
42
  </div>
42
43
 
43
44
  <% if @readers.any? %>
44
- <h2 class="b270-section-h">The community</h2>
45
+ <% rankings= @readers.count < 5 ? @readers.count : 5 %>
46
+ <h2 class="b270-section-h">Top <%= rankings %> reader leaders ...</h2>
45
47
  <div class="b270-list">
46
- <% @readers.first(5).each_with_index do |r, i| %>
48
+ <% @readers.first(rankings).each_with_index do |r, i| %>
47
49
  <div class="b270-li">
48
50
  <span class="rank"><%= i + 1 %></span>
49
51
  <%= b270_avatar(r) %>
@@ -27,6 +27,14 @@
27
27
  <span>Last name</span>
28
28
  <%= text_field_tag :last_name, @names[:last_name], required: true, autocomplete: "family-name" %>
29
29
  </label>
30
+ <label class="b270-field">
31
+ <span>Bible translation</span>
32
+ <%= select_tag :bible_version,
33
+ options_for_select(Bible270::Translations.options, current_reader.effective_bible_version),
34
+ class: "b270-select" %>
35
+ <span class="b270-hint" style="margin:0">Reading links open in this translation.</span>
36
+ </label>
37
+
30
38
  <%= submit_tag "Save", class: "b270-btn" %>
31
39
  <% end %>
32
40
 
@@ -1,8 +1,12 @@
1
1
  <% content_for :title, "#{@reader.display_name} — #{Bible270.config.app_name}" %>
2
+ <% content_for :hide_day_index, true %><%# this page has its own grid %>
2
3
  <div style="display:flex;align-items:center;gap:16px;margin-bottom:8px">
3
4
  <%= b270_avatar(@reader, size: 56) %>
4
5
  <div>
5
6
  <h1 style="margin:0"><%= @reader.display_name %></h1>
7
+ <% if signed_in? && current_reader.id == @reader.id %>
8
+ <p style="margin:2px 0 0"><%= link_to 'Edit your name', profile_path, class: 'b270-linkbtn' %></p>
9
+ <% end %>
6
10
  <p style="margin:2px 0 0;color:var(--faint);font-size:14px"><%= @days_completed %> of <%= Bible270::Plan::DAYS %> days complete · <%= @reader.completion_percent %>%</p>
7
11
  </div>
8
12
  </div>
@@ -0,0 +1,3 @@
1
+ <footer class="b270-footer">
2
+ The Old and New Testaments and are read once across the 270 days, with Psalms and Proverbs running alongside. Long chapters are divided so that daily readings are roughly equivalent in length.
3
+ </footer>
@@ -107,6 +107,8 @@
107
107
  .b270-rbody{flex:1;min-width:0}
108
108
  .b270-cat{font-size:11px;letter-spacing:.13em;text-transform:uppercase;color:var(--faint);font-weight:600}
109
109
  .b270-ref{font-family:"Spectral",serif;font-size:19px;font-weight:500;margin-top:1px}
110
+ .b270-version{font-size:11.5px;color:var(--faint);font-weight:400;letter-spacing:.02em;white-space:nowrap}
111
+ .b270-select{font:inherit;font-size:14px;padding:10px 30px 10px 12px;border:1px solid var(--line);border-radius:9px;background:#fff;color:var(--ink);width:100%;max-width:100%;text-overflow:ellipsis}
110
112
  .b270-ref a{text-decoration:none;border-bottom:1px solid transparent}
111
113
  .b270-ref a:hover{border-bottom-color:var(--gold)}
112
114
  .b270-reading.done .b270-ref{color:var(--muted)}
@@ -149,7 +151,7 @@
149
151
  .b270-cell.complete{background:var(--teal);border-color:var(--teal);color:#eef3f2;font-weight:600}
150
152
  .b270-cell.now{box-shadow:0 0 0 2px var(--gold)}
151
153
 
152
- .b270-footer{margin-top:40px;padding-top:22px;border-top:1px solid var(--line-soft);color:var(--faint);font-size:13px}
154
+ .b270-footer{margin-top:20px;padding-top:11px;border-top:1px solid var(--line-soft);color:var(--faint);font-size:13px}
153
155
  .b270-footer p{margin:0;line-height:1.5}
154
156
  details.b270-index summary{cursor:pointer;font-family:"Spectral",serif;font-size:18px;font-weight:600;list-style:none;margin-top:30px}
155
157
  details.b270-index summary::-webkit-details-marker{display:none}
@@ -9,6 +9,7 @@
9
9
  <link rel="preconnect" href="https://fonts.googleapis.com">
10
10
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
11
  <link href="https://fonts.googleapis.com/css2?family=Spectral:ital,wght@0,400;0,500;0,600;1,400&family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
12
+ <%= b270_favicon_tag %>
12
13
  <%= render "bible270/shared/styles" %>
13
14
  <%= yield :head %>
14
15
  </head>
@@ -18,10 +19,9 @@
18
19
  <% if flash[:alert] %><div class="b270-flash alert"><%= flash[:alert] %></div><% end %>
19
20
  <% if flash[:notice] %><div class="b270-flash notice"><%= flash[:notice] %></div><% end %>
20
21
  <%= yield %>
21
- <%= render 'bible270/shared/day_index' %>
22
- <footer class="b270-footer">
23
- <p>The Old Testament, New Testament and Psalms/Proverbs are each read once across the 270 days. Long chapters are divided so the readings fill each day evenly, e.g. Psalm 119 comes in eleven sections.</p>
24
- </footer>
22
+ <%# Pages with their own day grid opt out with content_for :hide_day_index %>
23
+ <%= render 'bible270/shared/day_index' unless content_for?(:hide_day_index) %>
24
+ <%= b270_footer %>
25
25
  </div>
26
26
  </body>
27
27
  </html>
data/config/routes.rb CHANGED
@@ -15,7 +15,8 @@ Bible270::Engine.routes.draw do
15
15
  get 'admin', to: 'admin#index', as: :admin
16
16
  get 'admin/readers/:id', to: 'admin#show', as: :admin_reader
17
17
  delete 'admin/readers/:id', to: 'admin#destroy'
18
- patch 'admin/readers/:id/name', to: 'admin#update_name', as: :admin_reader_name
18
+ patch 'admin/readers/:id/profile', to: 'admin#update_profile', as: :admin_reader_profile
19
+ delete 'admin/readers/:id/avatar', to: 'admin#remove_avatar', as: :admin_reader_avatar
19
20
  patch 'admin/readers/:id/start', to: 'admin#update_start', as: :admin_reader_start
20
21
  patch 'admin/readers/:id/through', to: 'admin#complete_through', as: :admin_reader_through
21
22
  post 'admin/readers/:id/days/:day', to: 'admin#toggle_day', as: :admin_reader_day
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddBibleVersionToBible270Readers < ActiveRecord::Migration[7.0]
4
+ def change
5
+ # Null means "use whatever the site default is", so changing
6
+ # config.bible_version moves everyone who hasn't chosen for themselves.
7
+ add_column :bible270_readers, :bible_version, :string
8
+ end
9
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bible270/version'
4
+ require 'bible270/plan'
5
+ require 'bible270/email_sign_in'
6
+ require 'bible270/names'
7
+ require 'bible270/avatars'
8
+ require 'bible270/translations'
9
+ require 'bible270/favicon'
10
+ require 'bible270/configuration'
11
+ require 'bible270/engine' if defined?(Rails::Engine)
12
+
13
+ module Bible270
14
+ end
@@ -6,7 +6,10 @@
6
6
  # URI.encode_www_form_component produces byte-identical output to CGI.escape.
7
7
  require 'uri'
8
8
  require 'bible270/plan'
9
+ require 'bible270/email_sign_in'
9
10
  require 'bible270/avatars'
11
+ require 'bible270/translations'
12
+ require 'bible270/favicon'
10
13
 
11
14
  module Bible270
12
15
  class Configuration
@@ -140,6 +143,59 @@ module Bible270
140
143
  # from the panel at runtime; setting this to false launches closed.
141
144
  attr_accessor :enrollment_open
142
145
 
146
+ # Favicon for the engine's pages. nil uses the built-in loaf, false renders
147
+ # no link tag at all (so the host's own favicon applies), and a string is
148
+ # used as the href — a path, a URL, or your own data URI.
149
+ # Footer for the engine's pages. Three ways to set it, and they're checked in
150
+ # this order:
151
+ #
152
+ # config.footer_partial = 'shared/footer' # a partial in your app
153
+ # config.footer_html = '<p>…</p>' # raw HTML, marked safe
154
+ # config.footer = false # no footer at all
155
+ #
156
+ # With none of them set you get the engine's own note about how the plan works.
157
+ # Rails adds the leading underscore when looking a partial up, so
158
+ # 'layouts/_bible270_footer' would search for '__bible270_footer' and raise
159
+ # MissingTemplate. Accept either spelling and normalise.
160
+ attr_reader :footer_partial
161
+
162
+ def footer_partial=(path)
163
+ @footer_partial =
164
+ if path.nil?
165
+ nil
166
+ else
167
+ segments = path.to_s.strip.split('/')
168
+ segments[-1] = segments[-1].sub(%r{\A_}, '') unless segments.empty?
169
+ segments.join('/')
170
+ end
171
+ end
172
+
173
+ # Where your footer goes relative to the engine's own: :replace (default),
174
+ # :after, or :before. Anything else is treated as :replace.
175
+ attr_accessor :footer_placement
176
+
177
+ PLACEMENTS = %i[replace after before].freeze
178
+
179
+ def resolved_footer_placement
180
+ placement = footer_placement.to_s.downcase.to_sym
181
+ PLACEMENTS.include?(placement) ? placement : :replace
182
+ end
183
+
184
+ # Whether the engine's own footer is shown alongside yours.
185
+ def keep_default_footer?
186
+ footer_style != :none && %i[after before].include?(resolved_footer_placement)
187
+ end
188
+
189
+ # :none, :partial, :html or :default.
190
+ def footer_style
191
+ # Plain Ruby, not present?: this file has to load without ActiveSupport.
192
+ return :none if footer == false
193
+ return :partial unless footer_partial.to_s.strip.empty?
194
+ return :html unless footer_html.to_s.strip.empty?
195
+
196
+ :default
197
+ end
198
+
143
199
  # Optional YAML file holding the same thing, re-read whenever it changes so
144
200
  # breaks can be tuned without restarting:
145
201
  #
@@ -147,7 +203,6 @@ module Bible270
147
203
  # Psalm 35: []
148
204
  # Psalm 78: [20, 39, 55]
149
205
  attr_accessor :chapter_breaks_path
150
- attr_accessor :after_sign_out_path, :email_sign_in_max_per_window, :passage_url_builder, :tagline, :avatar_max_bytes, :chapter_breaks, :admin_emails, :admin_resolver
151
206
 
152
207
  def admin?(reader)
153
208
  return false if reader.nil?
@@ -202,7 +257,11 @@ module Bible270
202
257
  end
203
258
 
204
259
  # Default Bible translation and a builder for external passage links.
205
- attr_accessor :bible_version
260
+ # Which translations readers may choose from. Defaults to all of
261
+ # Bible270::Translations::VERSIONS; narrow it to offer fewer.
262
+ attr_accessor :bible_versions
263
+
264
+ attr_accessor :after_sign_out_path, :email_sign_in_max_per_window, :passage_url_builder, :tagline, :footer_html, :footer, :favicon, :avatar_max_bytes, :chapter_breaks, :admin_emails, :admin_resolver, :bible_version
206
265
 
207
266
  # Public labels.
208
267
  attr_accessor :app_name
@@ -230,9 +289,11 @@ module Bible270
230
289
  @after_sign_in_path = nil
231
290
  @after_sign_out_path = nil
232
291
  @bible_version = 'NKJV'
292
+ @bible_versions = Translations::VERSIONS.keys
233
293
  @passage_url_builder = ->(reference, version) do
234
294
  search = URI.encode_www_form_component(reference)
235
- version = URI.encode_www_form_component(version)
295
+ # Translations.gateway_code maps 'NASB95' to Bible Gateway's 'NASB1995'.
296
+ version = URI.encode_www_form_component(Translations.gateway_code(version))
236
297
  "https://www.biblegateway.com/passage/?search=#{search}&version=#{version}"
237
298
  end
238
299
  @app_name = 'Daily Bread'
@@ -252,6 +313,11 @@ module Bible270
252
313
  @email_sign_in_require_name = true
253
314
  @email_sign_in_log_link = nil
254
315
  @enrollment_open = true
316
+ @footer_partial = nil
317
+ @footer_placement = :replace
318
+ @footer_html = nil
319
+ @footer = nil
320
+ @favicon = nil
255
321
  @avatar_max_bytes = Avatars::DEFAULT_MAX_BYTES
256
322
  @chapter_breaks = {}
257
323
  @chapter_breaks_path = nil
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bible270
4
+ # A favicon for the engine's own pages, inlined as a data URI.
5
+ #
6
+ # Inlined rather than served as an asset because the engine deliberately has no
7
+ # asset-pipeline dependency — its CSS is inlined the same way — so this works
8
+ # whether the host uses Propshaft, Sprockets or neither, and costs no request.
9
+ module Favicon
10
+ # A rustic loaf: a domed round on an ink board, with three slashed scores
11
+ # across the crust and a paler crumb showing at the base.
12
+ # Colours match the engine's palette (--ink, --gold, --paper).
13
+ SVG = <<~SVG
14
+ <svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'>
15
+ <rect width='32' height='32' rx='7' fill='%23231f18'/>
16
+ <path d='M4.5 21c0-6.2 5.1-10.5 11.5-10.5S27.5 14.8 27.5 21c0 1.4-1 2.4-2.4 2.4H6.9C5.5 23.4 4.5 22.4 4.5 21z'
17
+ fill='%23c6a24a'/>
18
+ <path d='M6.9 23.4h18.2c1.1 0 2-.6 2.3-1.5H4.6c.3.9 1.2 1.5 2.3 1.5z' fill='%23ece6da'/>
19
+ <g stroke='%23231f18' stroke-width='1.6' stroke-linecap='round' opacity='.75'>
20
+ <path d='M10.6 17.6l2.6-3'/>
21
+ <path d='M15 16.8l2.6-3'/>
22
+ <path d='M19.4 17.6l2.6-3'/>
23
+ </g>
24
+ </svg>
25
+ SVG
26
+
27
+ module_function
28
+
29
+ # Percent-encoding matters here: a raw '#' would truncate the URI at the
30
+ # fragment, and raw spaces are invalid in one.
31
+ def data_uri
32
+ compact = SVG.gsub(%r{\s+}, ' ').strip
33
+ "data:image/svg+xml,#{compact.gsub(' ', '%20')}"
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bible270
4
+ # The translations a reader may choose between, and how each maps onto the
5
+ # external reader's own code. Kept free of Rails so it can be unit tested.
6
+ #
7
+ # The distinction matters: readers know the NASB 1995 as "NASB95", but Bible
8
+ # Gateway's parameter for it is "NASB1995". Sending the display code straight
9
+ # through would land every link on a search page instead of the passage.
10
+ module Translations
11
+ # `label` is the full name, for prose. `short` is what goes in the select,
12
+ # where "New American Standard Bible 1995" overflows the box.
13
+ VERSIONS = {
14
+ 'NKJV' => { label: 'New King James Version', short: 'New King James Version', gateway: 'NKJV' },
15
+ 'NASB95' => { label: 'New American Standard Bible 1995', short: 'New American Standard 1995', gateway: 'NASB1995' },
16
+ 'LSB' => { label: 'Legacy Standard Bible', short: 'Legacy Standard Version', gateway: 'LSB' },
17
+ 'ESV' => { label: 'English Standard Version', short: 'English Standard Version', gateway: 'ESV' },
18
+ 'KJV' => { label: 'King James Version', short: 'King James Version', gateway: 'KJV' }
19
+ }.freeze
20
+
21
+ DEFAULT = 'NKJV'
22
+
23
+ module_function
24
+
25
+ # The codes offered to readers, narrowed by config.bible_versions if the host
26
+ # only wants some of them.
27
+ def codes
28
+ allowed = Bible270.config.bible_versions if Bible270.config.respond_to?(:bible_versions)
29
+ requested = Array(allowed).map { |code| code.to_s.upcase }.select { |code| VERSIONS.key?(code) }
30
+ requested.any? ? requested : VERSIONS.keys
31
+ end
32
+
33
+ def valid?(code)
34
+ codes.include?(normalize(code))
35
+ end
36
+
37
+ def normalize(code)
38
+ code.to_s.strip.upcase
39
+ end
40
+
41
+ def label(code)
42
+ VERSIONS.dig(normalize(code), :label)
43
+ end
44
+
45
+ def short_label(code)
46
+ VERSIONS.dig(normalize(code), :short) || label(code)
47
+ end
48
+
49
+ # The code the external reader expects, which is not always the code readers
50
+ # recognise.
51
+ def gateway_code(code)
52
+ VERSIONS.dig(normalize(code), :gateway) || normalize(code)
53
+ end
54
+
55
+ # [[label, code], ...] for a select field.
56
+ def options
57
+ codes.map { |code| ["#{code} — #{short_label(code)}", code] }
58
+ end
59
+
60
+ # Falls back to the configured default, then to ESV, so a link is never built
61
+ # with a blank version.
62
+ def resolve(code)
63
+ return normalize(code) if valid?(code)
64
+
65
+ configured = Bible270.config.bible_version if Bible270.config.respond_to?(:bible_version)
66
+ return normalize(configured) if valid?(configured)
67
+
68
+ codes.include?(DEFAULT) ? DEFAULT : codes.first
69
+ end
70
+ end
71
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '0.11.2'
4
+ VERSION = '0.13.0'
5
5
  end
data/lib/bible270.rb CHANGED
@@ -5,6 +5,8 @@ require 'bible270/plan'
5
5
  require 'bible270/email_sign_in'
6
6
  require 'bible270/names'
7
7
  require 'bible270/avatars'
8
+ require 'bible270/translations'
9
+ require 'bible270/favicon'
8
10
  require 'bible270/configuration'
9
11
  require 'bible270/engine' if defined?(Rails::Engine)
10
12
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bible270
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.2
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft
@@ -100,6 +100,7 @@ files:
100
100
  - app/views/bible270/readers/show.html.erb
101
101
  - app/views/bible270/sessions/new.html.erb
102
102
  - app/views/bible270/shared/_day_index.html.erb
103
+ - app/views/bible270/shared/_footer.html.erb
103
104
  - app/views/bible270/shared/_header.html.erb
104
105
  - app/views/bible270/shared/_styles.html.erb
105
106
  - app/views/bible270/sign_in_mailer/magic_link.html.erb
@@ -113,13 +114,17 @@ files:
113
114
  - db/migrate/20260101000005_add_names_to_bible270_readers.rb
114
115
  - db/migrate/20260101000006_add_approval_to_bible270_comments.rb
115
116
  - db/migrate/20260101000007_create_bible270_settings.rb
117
+ - db/migrate/20260101000008_add_bible_version_to_bible270_readers.rb
116
118
  - lib/bible270.rb
117
119
  - lib/bible270/avatars.rb
120
+ - lib/bible270/bible270.rb
118
121
  - lib/bible270/configuration.rb
119
122
  - lib/bible270/email_sign_in.rb
120
123
  - lib/bible270/engine.rb
124
+ - lib/bible270/favicon.rb
121
125
  - lib/bible270/names.rb
122
126
  - lib/bible270/plan.rb
127
+ - lib/bible270/translations.rb
123
128
  - lib/bible270/versification.rb
124
129
  - lib/bible270/version.rb
125
130
  - lib/generators/bible270/install/install_generator.rb