bible270 1.9.1 → 1.10.1
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 +4 -4
- data/README.md +15 -7
- data/app/controllers/bible270/admin_controller.rb +19 -3
- data/app/controllers/bible270/profiles_controller.rb +53 -32
- data/app/controllers/bible270/readers_controller.rb +2 -4
- data/app/helpers/bible270/plan_helper.rb +9 -4
- data/app/models/bible270/comment.rb +8 -5
- data/app/models/bible270/reader.rb +4 -2
- data/app/views/bible270/admin/index.html.erb +1 -1
- data/app/views/bible270/admin/show.html.erb +63 -34
- data/app/views/bible270/comments/_edit_fields.html.erb +1 -1
- data/app/views/bible270/comments/_edit_form.html.erb +1 -1
- data/app/views/bible270/comments/_form.html.erb +2 -1
- data/app/views/bible270/profiles/edit.html.erb +36 -16
- data/app/views/bible270/readers/show.html.erb +1 -1
- data/app/views/bible270/shared/_comment_drafts.html.erb +11 -5
- data/app/views/bible270/shared/_header.html.erb +26 -6
- data/app/views/bible270/shared/_styles.html.erb +30 -0
- data/config/routes.rb +1 -0
- data/lib/bible270/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b91b3cab89c11a366f5530e56caa8342c99352854eff8245447d96ce3a2727de
|
|
4
|
+
data.tar.gz: 29cf150387757a35d5b46d7ef80a0dd01080162ac57d7847efdba52174cc0e13
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4e7cf3647cecdde9ca4f5d692ea52bab1d0d711611ff05fcb0a93f5ddca161a0f9f1e3f4485b3cc94bd444181cbb5b2c04d1da71c8f53d57b28c230a71249180
|
|
7
|
+
data.tar.gz: d65529ce89abd4b8415e661468d898f06dfa7421fae5821038d96466326ac7018c1f6cc4bb9a60c0542ee1e8e1d64eeb3787473935eb99fec2a9f64582691ff5
|
data/README.md
CHANGED
|
@@ -272,6 +272,8 @@ Admins get an extra "Admin" link in the header. The broadcast form can send an i
|
|
|
272
272
|
message either to every reader with an email address or only to readers recognized as administrators.
|
|
273
273
|
From a reader's page you can:
|
|
274
274
|
|
|
275
|
+
- **set their theme** — Parchment, Earth, Forest, Midnight, or System, independently of your own theme;
|
|
276
|
+
|
|
275
277
|
- **move them to a day** — "put them on day 42 as of today" back-dates the start date; check-offs are
|
|
276
278
|
keyed to day numbers, so nothing in their history moves;
|
|
277
279
|
- **set their progress** — mark complete through day N (this also clears anything after, so it sets
|
|
@@ -284,7 +286,8 @@ From a reader's page you can:
|
|
|
284
286
|
The Reflections page lists conversations by their latest approved reply, supports day and author
|
|
285
287
|
filters, and paginates older threads. Signed-in readers see which conversations became active since
|
|
286
288
|
their previous visit. Reflection and reply drafts are retained for up to 30 days in that browser and
|
|
287
|
-
cleared after a successful post.
|
|
289
|
+
cleared after a successful post. If browser storage is blocked or full, the composer warns that the draft
|
|
290
|
+
could not be saved; posting still works. On a shared device, clearing browser storage also removes saved drafts.
|
|
288
291
|
Typing `@` in the composer offers up to five unambiguous reader handles; manual mentions continue to
|
|
289
292
|
work without JavaScript. A quiet **Formatting** disclosure opens a toolbar for bold, italic,
|
|
290
293
|
quotations, bulleted and numbered lists, links, and YouTube videos without requiring readers to know Markdown syntax.
|
|
@@ -368,13 +371,17 @@ Psalm 35: []
|
|
|
368
371
|
Psalm 78: [20, 39, 55]
|
|
369
372
|
```
|
|
370
373
|
|
|
371
|
-
##
|
|
374
|
+
## Theme
|
|
372
375
|
|
|
373
|
-
Readers can choose **
|
|
374
|
-
|
|
375
|
-
|
|
376
|
+
Readers can choose **Parchment** (light), **Earth** (dark brown), **Forest** (dark green),
|
|
377
|
+
**Midnight** (dark blue), or **System** under **Profile → Theme**, then save. The preference is stored
|
|
378
|
+
with their account and applies throughout Bible270 on every device. System is the default for existing
|
|
379
|
+
and new readers, follows the device's current appearance using Parchment for light or Earth for dark,
|
|
376
380
|
and responds to changes without reloading. Signed-out visitors also use System.
|
|
377
|
-
The completion dove retains its bright colors in
|
|
381
|
+
The completion dove retains its bright colors in every theme.
|
|
382
|
+
|
|
383
|
+
Existing preferences are preserved under the new names. Forest and Midnight require no additional
|
|
384
|
+
migration once display preferences are installed.
|
|
378
385
|
|
|
379
386
|
Upgrading hosts must install and run migration `20260101000023`:
|
|
380
387
|
|
|
@@ -420,7 +427,8 @@ Daily reminders are optional at both levels: the host enables the feature, then
|
|
|
420
427
|
whether to receive it and at what local time from their profile. The feature and reader opt-in both
|
|
421
428
|
default to off; each reader's time defaults to `08:00`. Profile times use quarter-hour choices so they
|
|
422
429
|
match the recommended 15-minute scheduler interval. An existing off-quarter preference remains
|
|
423
|
-
available until that reader chooses a new time.
|
|
430
|
+
available until that reader chooses a new time. The profile shows the plan's effective time zone beside
|
|
431
|
+
the reminder settings; this is not automatically the reader's device time zone.
|
|
424
432
|
|
|
425
433
|
```ruby
|
|
426
434
|
# config/initializers/bible270.rb
|
|
@@ -22,7 +22,7 @@ module Bible270
|
|
|
22
22
|
|
|
23
23
|
before_action :require_admin!
|
|
24
24
|
before_action :load_reader,
|
|
25
|
-
only: %i[show destroy update_start update_profile update_notifications update_completion_animation
|
|
25
|
+
only: %i[show destroy update_start update_profile update_theme update_notifications update_completion_animation
|
|
26
26
|
remove_avatar complete_through toggle_day]
|
|
27
27
|
before_action :load_comment, only: %i[hide_comment unhide_comment destroy_comment]
|
|
28
28
|
|
|
@@ -67,7 +67,7 @@ module Bible270
|
|
|
67
67
|
@days_completed = Reader.completed_days_by_id
|
|
68
68
|
@reader_sort_options = READER_SORT_OPTIONS
|
|
69
69
|
@reader_sort = READER_SORT_OPTIONS.key?(params[:sort]) ? params[:sort] : 'first_name'
|
|
70
|
-
readers = Reader.
|
|
70
|
+
readers = Reader.with_avatar.load
|
|
71
71
|
@recent_reader_activities = Checkoff.recent_activity_by_reader(readers.map(&:id))
|
|
72
72
|
@reader_report = ReaderProgressReport.new(
|
|
73
73
|
readers: readers,
|
|
@@ -105,7 +105,7 @@ module Bible270
|
|
|
105
105
|
# Everything is visible by default; this lists it all so anything unsuitable
|
|
106
106
|
# can be taken down.
|
|
107
107
|
def comments
|
|
108
|
-
scope = Comment.includes(
|
|
108
|
+
scope = Comment.with_reader_avatars.includes(parent: :reader).order(created_at: :desc)
|
|
109
109
|
@filter = params[:filter].to_s
|
|
110
110
|
scope = scope.hidden if @filter == 'hidden'
|
|
111
111
|
scope = scope.approved if @filter == 'visible'
|
|
@@ -248,6 +248,22 @@ module Bible270
|
|
|
248
248
|
end
|
|
249
249
|
end
|
|
250
250
|
|
|
251
|
+
def update_theme
|
|
252
|
+
unless Reader.display_mode_column?
|
|
253
|
+
return redirect_to admin_reader_path(@reader),
|
|
254
|
+
alert: 'Run the pending Bible270 database migration before changing the theme.'
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
mode = params[:display_mode].to_s
|
|
258
|
+
unless Reader::DISPLAY_MODES.include?(mode)
|
|
259
|
+
return redirect_to admin_reader_path(@reader), alert: 'Choose a theme from the list.'
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
@reader.reload unless @reader.has_attribute?(:display_mode)
|
|
263
|
+
@reader.update!(display_mode: mode)
|
|
264
|
+
redirect_to admin_reader_path(@reader), notice: 'Theme updated.'
|
|
265
|
+
end
|
|
266
|
+
|
|
251
267
|
def update_notifications
|
|
252
268
|
unless Bible270.config.mention_notifications
|
|
253
269
|
return redirect_to admin_reader_path(@reader), alert: 'Reflection emails are disabled for this installation.'
|
|
@@ -17,39 +17,30 @@ module Bible270
|
|
|
17
17
|
@comment_notification_level = requested_comment_notification_level
|
|
18
18
|
@daily_reminders_available = daily_reminders_available?
|
|
19
19
|
@display_mode = params.fetch(:display_mode, current_reader.effective_display_mode).to_s
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
@names = { first_name: params[:first_name], last_name: params[:last_name] }
|
|
21
|
+
@daily_reminders = params[:daily_reminders] == '1'
|
|
22
|
+
@daily_reminder_time = daily_reminder_time_param
|
|
23
|
+
names = Names.normalize(params[:first_name], params[:last_name])
|
|
24
|
+
source = (params[:passage_source].presence || Reader::DEFAULT_PASSAGE_SOURCE).to_s
|
|
25
|
+
problems = [comment_notification_problem, display_mode_problem, avatar_problem].compact
|
|
26
|
+
problems << 'both a first and last name' unless names
|
|
27
|
+
if params[:bible_version].present? && !Translations.valid?(params[:bible_version])
|
|
24
28
|
problems << 'a translation from the list'
|
|
25
29
|
end
|
|
26
|
-
source
|
|
27
|
-
|
|
28
|
-
if params[:avatar].present? && !current_reader.attach_avatar(params[:avatar])
|
|
29
|
-
problems << (current_reader.errors[:avatar].first || 'a valid image')
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
requested_daily_reminders = params[:daily_reminders] == '1'
|
|
33
|
-
requested_daily_reminder_time = daily_reminder_time_param
|
|
34
|
-
if @daily_reminders_available && !Reader.valid_daily_reminder_time?(requested_daily_reminder_time)
|
|
30
|
+
problems << 'a reading-link source from the list' unless Reader::PASSAGE_SOURCES.include?(source)
|
|
31
|
+
if @daily_reminders_available && !Reader.valid_daily_reminder_time?(@daily_reminder_time)
|
|
35
32
|
problems << 'a reminder time in 24-hour HH:MM format'
|
|
36
33
|
end
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@names = { first_name: params[:first_name], last_name: params[:last_name] }
|
|
48
|
-
@daily_reminders = requested_daily_reminders
|
|
49
|
-
@daily_reminder_time = requested_daily_reminder_time
|
|
50
|
-
flash.now[:alert] = "Please give #{problems.to_sentence}."
|
|
51
|
-
render :edit, status: :unprocessable_entity
|
|
52
|
-
end
|
|
34
|
+
return render_profile_error("Please give #{problems.to_sentence}.") if problems.any?
|
|
35
|
+
|
|
36
|
+
save_profile(names.merge(passage_source: source))
|
|
37
|
+
redirect_to profile_path, notice: 'Your profile has been updated.'
|
|
38
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
39
|
+
message = e.record ? e.record.errors.full_messages.to_sentence : nil
|
|
40
|
+
# Reload does not discard Active Storage's deferred attachment changes.
|
|
41
|
+
current_reader.attachment_changes.clear if Reader.avatar_uploads?
|
|
42
|
+
current_reader.reload
|
|
43
|
+
render_profile_error("Your profile could not be saved. #{message.presence || 'Please try again.'}")
|
|
53
44
|
end
|
|
54
45
|
|
|
55
46
|
def remove_avatar
|
|
@@ -63,11 +54,41 @@ module Bible270
|
|
|
63
54
|
return unless Reader.display_mode_column?
|
|
64
55
|
return if Reader::DISPLAY_MODES.include?(@display_mode)
|
|
65
56
|
|
|
66
|
-
'a
|
|
57
|
+
'a theme from the list'
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def avatar_problem
|
|
61
|
+
upload = params[:avatar]
|
|
62
|
+
return if upload.blank?
|
|
63
|
+
return 'a valid image' unless Reader.avatar_uploads?
|
|
64
|
+
|
|
65
|
+
Avatars.problem_with(content_type: upload.content_type, byte_size: upload.size)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def save_profile(attributes)
|
|
69
|
+
attributes[:bible_version] = Translations.normalize(params[:bible_version]) if params[:bible_version].present?
|
|
70
|
+
attributes[:display_mode] = @display_mode if Reader.display_mode_column?
|
|
71
|
+
if @comment_notifications_available
|
|
72
|
+
attributes[:notify_on_mention] = @comment_notification_level != 'none'
|
|
73
|
+
attributes[:notify_on_all_comments] = @comment_notification_level == 'all'
|
|
74
|
+
end
|
|
75
|
+
if @daily_reminders_available
|
|
76
|
+
attributes[:daily_reminders] = @daily_reminders
|
|
77
|
+
attributes[:daily_reminder_time] = @daily_reminder_time
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
Reader.transaction do
|
|
81
|
+
current_reader.assign_attributes(attributes)
|
|
82
|
+
# Unlike attach, assignment never saves early. Active Storage persists the
|
|
83
|
+
# attachment with the reader and uploads only after a successful commit.
|
|
84
|
+
current_reader.avatar = params[:avatar] if params[:avatar].present?
|
|
85
|
+
current_reader.save!
|
|
86
|
+
end
|
|
67
87
|
end
|
|
68
88
|
|
|
69
|
-
def
|
|
70
|
-
|
|
89
|
+
def render_profile_error(message)
|
|
90
|
+
flash.now[:alert] = message
|
|
91
|
+
render :edit, status: :unprocessable_entity
|
|
71
92
|
end
|
|
72
93
|
|
|
73
94
|
def load_comment_notification_state
|
|
@@ -5,7 +5,7 @@ module Bible270
|
|
|
5
5
|
def index
|
|
6
6
|
# Community is a directory of fellow readers, not a leaderboard. Keep the
|
|
7
7
|
# order predictable without implying that progress determines standing.
|
|
8
|
-
@readers = Reader.
|
|
8
|
+
@readers = Reader.with_avatar.to_a.sort_by(&:sort_name)
|
|
9
9
|
@today_day = current_reader&.today_day
|
|
10
10
|
@show_admin_progress = Bible270.config.admin?(current_reader)
|
|
11
11
|
@days_completed = Reader.completed_days_by_id if @show_admin_progress
|
|
@@ -47,9 +47,7 @@ module Bible270
|
|
|
47
47
|
private
|
|
48
48
|
|
|
49
49
|
def recent_reflections(reader, limit:)
|
|
50
|
-
comments = reader.comments.recent
|
|
51
|
-
.includes(:reader, { likes: :reader }, { replies: [:reader, { likes: :reader }] })
|
|
52
|
-
.limit(limit).to_a
|
|
50
|
+
comments = reader.comments.recent.with_thread_details.limit(limit).to_a
|
|
53
51
|
ids = comments.map(&:id)
|
|
54
52
|
# A reader's own replies already appear beneath any listed parent reflection.
|
|
55
53
|
comments.reject { |comment| ids.include?(comment.parent_id) }
|
|
@@ -159,10 +159,11 @@ module Bible270
|
|
|
159
159
|
reader.avatar_url.presence
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
def b270_avatar(reader, size: 72)
|
|
162
|
+
def b270_avatar(reader, size: 72, loading: 'lazy')
|
|
163
163
|
src = b270_avatar_src(reader)
|
|
164
164
|
if src
|
|
165
|
-
image_tag src, class: 'b270-avatar', width: size, height: size, alt: reader.display_name
|
|
165
|
+
image_tag src, class: 'b270-avatar', width: size, height: size, alt: reader.display_name,
|
|
166
|
+
loading: loading, decoding: 'async'
|
|
166
167
|
else
|
|
167
168
|
content_tag :span, reader.initials, class: 'b270-avatar b270-avatar-fallback',
|
|
168
169
|
style: "width:#{size}px;height:#{size}px;line-height:#{size}px"
|
|
@@ -181,12 +182,16 @@ module Bible270
|
|
|
181
182
|
local&.strftime('%b %-d, %Y at %-I:%M %p')
|
|
182
183
|
end
|
|
183
184
|
|
|
185
|
+
def b270_activity_datetime(time)
|
|
186
|
+
local = Bible270.local_time(time)
|
|
187
|
+
local&.strftime('%b %-d, %Y %H:%M')
|
|
188
|
+
end
|
|
189
|
+
|
|
184
190
|
def b270_reader_activity_description(activity)
|
|
185
191
|
day_link = link_to "Day #{activity.day}", day_path(activity.day), title: b270_day_readings(activity.day)
|
|
186
192
|
|
|
187
193
|
if activity.day_completed?
|
|
188
|
-
|
|
189
|
-
safe_join(['completed ', day_link, " (#{readings})"])
|
|
194
|
+
safe_join(['completed ', day_link])
|
|
190
195
|
else
|
|
191
196
|
reference = Plan.parts_for(activity.day, activity.track).fetch(activity.part)
|
|
192
197
|
safe_join(["checked off “#{reference}” for ", day_link])
|
|
@@ -52,9 +52,14 @@ module Bible270
|
|
|
52
52
|
# Only the reflections themselves: replies are rendered under their parent, so
|
|
53
53
|
# listing them again at the top level would show each twice.
|
|
54
54
|
scope :for_day, ->(d) { approved.where(day: d, parent_id: nil).order(created_at: :asc) }
|
|
55
|
-
scope :
|
|
56
|
-
|
|
55
|
+
scope :with_reader_avatars, -> {
|
|
56
|
+
Reader.avatar_uploads? ? includes(reader: { avatar_attachment: :blob }) : includes(:reader)
|
|
57
57
|
}
|
|
58
|
+
scope :with_thread_details, -> {
|
|
59
|
+
reply_reader = Reader.avatar_uploads? ? { reader: { avatar_attachment: :blob } } : :reader
|
|
60
|
+
with_reader_avatars.includes({ likes: :reader }, { replies: [reply_reader, { likes: :reader }] })
|
|
61
|
+
}
|
|
62
|
+
scope :threads_for_day, ->(d) { for_day(d).with_thread_details }
|
|
58
63
|
scope :recent, -> { approved.order(created_at: :desc) }
|
|
59
64
|
|
|
60
65
|
def self.body_format_column?
|
|
@@ -95,9 +100,7 @@ module Bible270
|
|
|
95
100
|
page = page.to_i.clamp(1, pages)
|
|
96
101
|
page_rows = ordered.slice((page - 1) * per_page, per_page) || []
|
|
97
102
|
ids = page_rows.map(&:first)
|
|
98
|
-
loaded = approved.where(id: ids)
|
|
99
|
-
.includes(:reader, { likes: :reader }, { replies: [:reader, { likes: :reader }] })
|
|
100
|
-
.index_by(&:id)
|
|
103
|
+
loaded = approved.where(id: ids).with_thread_details.index_by(&:id)
|
|
101
104
|
|
|
102
105
|
ThreadPage.new(
|
|
103
106
|
threads: ids.filter_map { |id| loaded[id] },
|
|
@@ -19,6 +19,8 @@ module Bible270
|
|
|
19
19
|
|
|
20
20
|
def self.avatar_uploads? = AVATAR_UPLOADS
|
|
21
21
|
|
|
22
|
+
scope :with_avatar, -> { avatar_uploads? ? includes(avatar_attachment: :blob) : all }
|
|
23
|
+
|
|
22
24
|
# Fires for every way a reader comes into existence — email sign-in, OmniAuth,
|
|
23
25
|
# or a bridged host user — rather than in each controller, so no path can
|
|
24
26
|
# quietly skip it. after_create_commit, so nothing is sent for a reader whose
|
|
@@ -27,7 +29,7 @@ module Bible270
|
|
|
27
29
|
|
|
28
30
|
PASSAGE_SOURCES = %w[bible_com bible_gateway blue_letter_bible].freeze
|
|
29
31
|
DEFAULT_PASSAGE_SOURCE = 'bible_com'
|
|
30
|
-
DISPLAY_MODES = %w[light dark system].freeze
|
|
32
|
+
DISPLAY_MODES = %w[light dark dark_green dark_blue system].freeze
|
|
31
33
|
DEFAULT_DISPLAY_MODE = 'system'
|
|
32
34
|
DAILY_REMINDER_TIME_FORMAT = %r{\A(?:[01]\d|2[0-3]):[0-5]\d\z}
|
|
33
35
|
DAILY_REMINDER_MINUTES = %w[00 15 30 45].freeze
|
|
@@ -87,7 +89,7 @@ module Bible270
|
|
|
87
89
|
.having('COUNT(*) >= ?', Plan.total_parts(day))
|
|
88
90
|
.order(Checkoff.arel_table[:created_at].maximum.asc, :reader_id)
|
|
89
91
|
.pluck(:reader_id)
|
|
90
|
-
readers_by_id = where(id: reader_ids).index_by(&:id)
|
|
92
|
+
readers_by_id = with_avatar.where(id: reader_ids).index_by(&:id)
|
|
91
93
|
reader_ids.filter_map { |reader_id| readers_by_id[reader_id] }
|
|
92
94
|
end
|
|
93
95
|
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
</div>
|
|
97
97
|
<div class="sub b270-reader-activity">
|
|
98
98
|
<% if (activity = @recent_reader_activities[reader.id]) %>
|
|
99
|
-
<%=
|
|
99
|
+
<%= b270_activity_datetime(activity.occurred_at) %>: <%= b270_reader_activity_description(activity) %>
|
|
100
100
|
<% else %>
|
|
101
101
|
No reading activity yet
|
|
102
102
|
<% end %>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<p class="b270-eyebrow"><%= link_to 'Admin', admin_path %></p>
|
|
4
4
|
<h1><%= @reader.display_name %></h1>
|
|
5
5
|
<p class="lede">
|
|
6
|
-
<%= @reader.email.
|
|
6
|
+
<%= @reader.email.present? ? mail_to(@reader.email) : @reader.provider %> ·
|
|
7
7
|
<%= @days_completed %> of <%= Bible270::Plan::DAYS %> days ·
|
|
8
8
|
<%= @reader.completion_percent %>%
|
|
9
9
|
<% if @reader.today_day %>· day <%= @reader.today_day %> today
|
|
@@ -16,15 +16,21 @@
|
|
|
16
16
|
<%= form_with url: admin_reader_profile_path(@reader), method: :patch, class: "b270-emailform",
|
|
17
17
|
html: { multipart: true } do %>
|
|
18
18
|
<div class="b270-avatarrow">
|
|
19
|
-
<%= b270_avatar(@reader, size: 72) %>
|
|
19
|
+
<%= b270_avatar(@reader, size: 72, loading: 'eager') %>
|
|
20
20
|
<% if Bible270::Reader.avatar_uploads? %>
|
|
21
|
-
<
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
<div class="b270-avatarcontrols" style="flex:1;min-width:0">
|
|
22
|
+
<label class="b270-field">
|
|
23
|
+
<span>Picture</span>
|
|
24
|
+
<%= file_field_tag :avatar, accept: Bible270::Avatars.content_types.join(',') %>
|
|
25
|
+
<span class="b270-hint" style="margin:0">
|
|
26
|
+
PNG, JPEG, GIF or WebP, up to <%= Bible270::Avatars.human_size(Bible270::Avatars.max_bytes) %>.
|
|
27
|
+
</span>
|
|
28
|
+
</label>
|
|
29
|
+
<% if @reader.avatar_uploaded? %>
|
|
30
|
+
<%= button_tag "Remove picture", type: "submit", class: "b270-linkbtn",
|
|
31
|
+
form: "b270-remove-picture" %>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
28
34
|
<% end %>
|
|
29
35
|
</div>
|
|
30
36
|
|
|
@@ -40,9 +46,54 @@
|
|
|
40
46
|
<% end %>
|
|
41
47
|
|
|
42
48
|
<% if @reader.avatar_uploaded? %>
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
<%# Keep picture removal separate from unsaved name and upload changes. %>
|
|
50
|
+
<%= form_with url: admin_reader_avatar_path(@reader), method: :delete,
|
|
51
|
+
html: { id: "b270-remove-picture", hidden: true },
|
|
52
|
+
data: { turbo_confirm: "Remove #{@reader.display_name}'s picture?" } do %>
|
|
53
|
+
<% end %>
|
|
54
|
+
<% end %>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="b270-panel">
|
|
58
|
+
<h2 class="b270-subhead">Which translation they read</h2>
|
|
59
|
+
<p class="b270-hint" style="margin-top:0">
|
|
60
|
+
Reading links open in this translation.
|
|
61
|
+
<% if @reader.bible_version.blank? %>
|
|
62
|
+
They have expressed no preference, so they follow the site default
|
|
63
|
+
(<%= Bible270.config.bible_version %>).
|
|
64
|
+
<% end %>
|
|
65
|
+
</p>
|
|
66
|
+
|
|
67
|
+
<%= form_with url: admin_reader_version_path(@reader), method: :patch, class: "b270-form" do %>
|
|
68
|
+
<div class="row">
|
|
69
|
+
<%= select_tag :bible_version,
|
|
70
|
+
options_for_select([["Site default (#{Bible270.config.bible_version})", ""]] +
|
|
71
|
+
Bible270::Translations.options,
|
|
72
|
+
@reader.bible_version.to_s),
|
|
73
|
+
class: "b270-select", style: "width:auto" %>
|
|
74
|
+
</div>
|
|
75
|
+
<%= render "bible270/shared/passage_source", reader: @reader %>
|
|
76
|
+
<%= submit_tag "Save", class: "b270-btn" %>
|
|
77
|
+
<% end %>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="b270-panel">
|
|
81
|
+
<h2 class="b270-subhead" id="b270-theme-heading">Theme</h2>
|
|
82
|
+
<% if Bible270::Reader.display_mode_column? %>
|
|
83
|
+
<%= form_with url: admin_reader_theme_path(@reader), method: :patch, class: "b270-emailform" do %>
|
|
84
|
+
<%= select_tag :display_mode,
|
|
85
|
+
options_for_select([["Parchment", "light"], ["Earth", "dark"], ["Forest", "dark_green"],
|
|
86
|
+
["Midnight", "dark_blue"], ["System", "system"]], @reader.effective_display_mode),
|
|
87
|
+
class: "b270-select", aria: { labelledby: "b270-theme-heading", describedby: "b270-theme-hint" } %>
|
|
88
|
+
<p id="b270-theme-hint" class="b270-hint" style="margin:0">
|
|
89
|
+
System follows the reader's device setting: Parchment for light, Earth for dark.
|
|
90
|
+
</p>
|
|
91
|
+
<%= submit_tag "Save", class: "b270-btn" %>
|
|
92
|
+
<% end %>
|
|
93
|
+
<% else %>
|
|
94
|
+
<p class="b270-flash alert" style="margin:0">
|
|
95
|
+
Run the pending Bible270 database migration before changing the theme.
|
|
96
|
+
</p>
|
|
46
97
|
<% end %>
|
|
47
98
|
</div>
|
|
48
99
|
|
|
@@ -119,28 +170,6 @@
|
|
|
119
170
|
<%= render "bible270/shared/reflections", comments: @comments, moderate: true, editable: true, actions: true %>
|
|
120
171
|
</div>
|
|
121
172
|
|
|
122
|
-
<div class="b270-panel">
|
|
123
|
-
<h2 class="b270-subhead">Which translation they read</h2>
|
|
124
|
-
<p class="b270-hint" style="margin-top:0">
|
|
125
|
-
Reading links open in this translation.
|
|
126
|
-
<% if @reader.bible_version.blank? %>
|
|
127
|
-
They have expressed no preference, so they follow the site default
|
|
128
|
-
(<%= Bible270.config.bible_version %>).
|
|
129
|
-
<% end %>
|
|
130
|
-
</p>
|
|
131
|
-
|
|
132
|
-
<%= form_with url: admin_reader_version_path(@reader), method: :patch, class: "b270-form" do %>
|
|
133
|
-
<div class="row">
|
|
134
|
-
<%= select_tag :bible_version,
|
|
135
|
-
options_for_select([["Site default (#{Bible270.config.bible_version})", ""]] +
|
|
136
|
-
Bible270::Translations.options,
|
|
137
|
-
@reader.bible_version.to_s),
|
|
138
|
-
class: "b270-select", style: "width:auto" %>
|
|
139
|
-
</div>
|
|
140
|
-
<%= render "bible270/shared/passage_source", reader: @reader %>
|
|
141
|
-
<%= submit_tag "Save", class: "b270-btn" %>
|
|
142
|
-
<% end %>
|
|
143
|
-
</div>
|
|
144
173
|
|
|
145
174
|
<div class="b270-panel">
|
|
146
175
|
<h2 class="b270-subhead">Completions</h2>
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
<%= f.select :track,
|
|
21
21
|
options_for_select([["The whole day", ""]] + Bible270::Plan::TRACKS.map { |k, v| [v[:label], k] },
|
|
22
22
|
comment.track.to_s),
|
|
23
|
-
{}, class: "b270-select", style: "width:auto" %>
|
|
23
|
+
{}, class: "b270-select", style: "width:auto", aria: { label: "Reading this reflection relates to" } %>
|
|
24
24
|
<% end %>
|
|
25
25
|
<%= f.submit "Save", class: "b270-btn" %>
|
|
26
26
|
<%= link_to "Cancel", day_path(comment.day, anchor: "comment-#{comment.id}"), class: "b270-linkbtn" %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<%# locals: (comment:) %>
|
|
2
2
|
<div class="b270-comment b270-editing" id="comment-<%= comment.id %>">
|
|
3
|
-
<%= b270_avatar(comment.reader) %>
|
|
3
|
+
<%= b270_avatar(comment.reader, loading: 'eager') %>
|
|
4
4
|
<div style="flex:1">
|
|
5
5
|
<%= render partial: "bible270/comments/edit_fields", locals: { comment: comment } %>
|
|
6
6
|
</div>
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
<div class="row">
|
|
26
26
|
<%= f.select :track,
|
|
27
27
|
options_for_select([["The whole day", ""]] + Bible270::Plan::TRACKS.map { |k, v| [v[:label], k] }),
|
|
28
|
-
{},
|
|
28
|
+
{}, aria: { label: "Reading this reflection relates to" },
|
|
29
|
+
style: "font:inherit;padding:8px;border:1px solid var(--line);border-radius:8px;background:#fff" %>
|
|
29
30
|
<%= f.submit "Post reflection", class: "b270-btn" %>
|
|
30
31
|
</div>
|
|
31
32
|
<% end %>
|
|
@@ -8,15 +8,21 @@
|
|
|
8
8
|
<%= form_with url: profile_path, method: :patch, class: "b270-emailform",
|
|
9
9
|
html: { multipart: true } do %>
|
|
10
10
|
<div class="b270-avatarrow">
|
|
11
|
-
<%= b270_avatar(current_reader, size: 72) %>
|
|
11
|
+
<%= b270_avatar(current_reader, size: 72, loading: 'eager') %>
|
|
12
12
|
<% if Bible270::Reader.avatar_uploads? %>
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
<div class="b270-avatarcontrols" style="flex:1;min-width:0">
|
|
14
|
+
<label class="b270-field">
|
|
15
|
+
<span>Picture</span>
|
|
16
|
+
<%= file_field_tag :avatar, accept: Bible270::Avatars.content_types.join(',') %>
|
|
17
|
+
<span class="b270-hint" style="margin:0">
|
|
18
|
+
PNG, JPEG, GIF or WebP, up to <%= Bible270::Avatars.human_size(Bible270::Avatars.max_bytes) %>.
|
|
19
|
+
</span>
|
|
20
|
+
</label>
|
|
21
|
+
<% if current_reader.avatar_uploaded? %>
|
|
22
|
+
<%= button_tag "Remove picture", type: "submit", class: "b270-linkbtn",
|
|
23
|
+
form: "b270-remove-picture" %>
|
|
24
|
+
<% end %>
|
|
25
|
+
</div>
|
|
20
26
|
<% end %>
|
|
21
27
|
</div>
|
|
22
28
|
|
|
@@ -40,15 +46,16 @@
|
|
|
40
46
|
|
|
41
47
|
<% if Bible270::Reader.display_mode_column? %>
|
|
42
48
|
<label class="b270-field">
|
|
43
|
-
<span>
|
|
49
|
+
<span>Theme</span>
|
|
44
50
|
<%= select_tag :display_mode,
|
|
45
|
-
options_for_select([["
|
|
51
|
+
options_for_select([["Parchment", "light"], ["Earth", "dark"], ["Forest", "dark_green"],
|
|
52
|
+
["Midnight", "dark_blue"], ["System", "system"]], @display_mode),
|
|
46
53
|
class: "b270-select", aria: { describedby: "b270-display-hint" } %>
|
|
47
|
-
<span id="b270-display-hint" class="b270-hint" style="margin:0">System follows your device's light
|
|
54
|
+
<span id="b270-display-hint" class="b270-hint" style="margin:0">System follows your device's setting: Parchment for light, Earth for dark.</span>
|
|
48
55
|
</label>
|
|
49
56
|
<% else %>
|
|
50
57
|
<p class="b270-flash alert" style="margin:0">
|
|
51
|
-
|
|
58
|
+
Theme settings are temporarily unavailable while this installation finishes a database update.
|
|
52
59
|
</p>
|
|
53
60
|
<% end %>
|
|
54
61
|
|
|
@@ -63,6 +70,13 @@
|
|
|
63
70
|
<% end %>
|
|
64
71
|
|
|
65
72
|
<% if @daily_reminders_available %>
|
|
73
|
+
<% reminder_local_time = Bible270.local_time(Time.now) %>
|
|
74
|
+
<% reminder_zone = if reminder_local_time.respond_to?(:time_zone)
|
|
75
|
+
reminder_local_time.time_zone.tzinfo.name
|
|
76
|
+
else
|
|
77
|
+
["Server time zone", reminder_local_time.zone.presence].compact.join(" — ")
|
|
78
|
+
end %>
|
|
79
|
+
<% reminder_tooltip = "Uses the plan's time zone: #{reminder_zone}, not your device's time zone. You can turn reminders off here at any time." %>
|
|
66
80
|
<% reminder_hour, reminder_minute = @daily_reminder_time.to_s.split(":", 2) %>
|
|
67
81
|
<% reminder_minutes = (Bible270::Reader::DAILY_REMINDER_MINUTES + [reminder_minute]).compact.uniq.sort %>
|
|
68
82
|
<div class="b270-field">
|
|
@@ -73,7 +87,7 @@
|
|
|
73
87
|
aria: { describedby: "daily-reminder-optin-hint" } %>
|
|
74
88
|
<span>Email me each day's readings</span>
|
|
75
89
|
<span class="b270-reminder-tooltip" id="daily-reminder-optin-hint" role="tooltip">
|
|
76
|
-
|
|
90
|
+
<%= reminder_tooltip %>
|
|
77
91
|
</span>
|
|
78
92
|
</label>
|
|
79
93
|
<div class="b270-reminder-time">
|
|
@@ -88,11 +102,14 @@
|
|
|
88
102
|
options_for_select(reminder_minutes, reminder_minute),
|
|
89
103
|
class: "b270-time-select", required: true, aria: { label: "Minute" } %>
|
|
90
104
|
<span class="b270-reminder-tooltip" id="daily-reminder-time-hint" role="tooltip">
|
|
91
|
-
|
|
105
|
+
<%= reminder_tooltip %>
|
|
92
106
|
</span>
|
|
93
107
|
</span>
|
|
94
108
|
</div>
|
|
95
109
|
</div>
|
|
110
|
+
<span id="daily-reminder-zone" class="b270-hint" style="margin:0;overflow-wrap:anywhere">
|
|
111
|
+
Plan time zone: <%= reminder_zone %>
|
|
112
|
+
</span>
|
|
96
113
|
</div>
|
|
97
114
|
<% elsif Bible270.config.daily_reminders? %>
|
|
98
115
|
<p class="b270-flash alert" style="margin:0">
|
|
@@ -104,8 +121,11 @@
|
|
|
104
121
|
<% end %>
|
|
105
122
|
|
|
106
123
|
<% if current_reader.avatar_uploaded? %>
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
<%# The picture button targets this separate form so it never submits profile edits. %>
|
|
125
|
+
<%= form_with url: profile_avatar_path, method: :delete,
|
|
126
|
+
html: { id: "b270-remove-picture", hidden: true },
|
|
127
|
+
data: { turbo_confirm: "Remove your picture?" } do %>
|
|
128
|
+
<% end %>
|
|
109
129
|
<% end %>
|
|
110
130
|
|
|
111
131
|
<% unless Bible270::Reader.avatar_uploads? %>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<% content_for :title, "#{@reader.display_name} — #{Bible270.config.app_name}" %>
|
|
2
2
|
<% content_for :hide_day_index, true %><%# this page has its own grid %>
|
|
3
3
|
<div style="display:flex;align-items:center;gap:16px;margin-bottom:8px">
|
|
4
|
-
<%= b270_avatar(@reader, size: 72) %>
|
|
4
|
+
<%= b270_avatar(@reader, size: 72, loading: 'eager') %>
|
|
5
5
|
<div>
|
|
6
6
|
<h1 style="margin:0"><%= @reader.display_name %></h1>
|
|
7
7
|
<% if signed_in? && current_reader.id == @reader.id %>
|
|
@@ -20,8 +20,13 @@
|
|
|
20
20
|
try { return area.getItem(key); } catch (_error) { return null; }
|
|
21
21
|
},
|
|
22
22
|
set(area, key, value) {
|
|
23
|
-
if (!area) return;
|
|
24
|
-
try {
|
|
23
|
+
if (!area) return false;
|
|
24
|
+
try {
|
|
25
|
+
area.setItem(key, value);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (_error) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
25
30
|
},
|
|
26
31
|
remove(area, key) {
|
|
27
32
|
if (!area) return;
|
|
@@ -49,7 +54,7 @@
|
|
|
49
54
|
|
|
50
55
|
function setStatus(form, message) {
|
|
51
56
|
const status = form.querySelector("[data-b270-draft-status]");
|
|
52
|
-
if (status) status.textContent = message;
|
|
57
|
+
if (status && status.textContent !== message) status.textContent = message;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
function save(form) {
|
|
@@ -81,8 +86,9 @@
|
|
|
81
86
|
savedAt: Date.now(),
|
|
82
87
|
revision: `${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`
|
|
83
88
|
};
|
|
84
|
-
storage.set(local, key, JSON.stringify(draft));
|
|
85
|
-
setStatus(form, "Draft saved in this browser."
|
|
89
|
+
const saved = storage.set(local, key, JSON.stringify(draft));
|
|
90
|
+
setStatus(form, saved ? "Draft saved in this browser." :
|
|
91
|
+
"Draft couldn’t be saved. Keep this page open until posting.");
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
function settlePending(key = null) {
|
|
@@ -29,18 +29,38 @@
|
|
|
29
29
|
(() => {
|
|
30
30
|
if (window.Bible270NavigationMenu) return;
|
|
31
31
|
|
|
32
|
+
function setExpanded(menu, expanded) {
|
|
33
|
+
const toggle = menu && menu.querySelector("button.b270-navtoggle");
|
|
34
|
+
const links = menu && menu.querySelector(".b270-navlinks");
|
|
35
|
+
if (!toggle || !links) return;
|
|
36
|
+
|
|
37
|
+
toggle.setAttribute("aria-expanded", String(expanded));
|
|
38
|
+
links.hidden = !expanded;
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
document.addEventListener("click", (event) => {
|
|
33
42
|
const target = event.target;
|
|
43
|
+
document.querySelectorAll(".b270-navmenu").forEach((menu) => {
|
|
44
|
+
if (!menu.contains(target)) setExpanded(menu, false);
|
|
45
|
+
});
|
|
46
|
+
|
|
34
47
|
const toggle = target.closest && target.closest("button.b270-navtoggle");
|
|
35
48
|
if (!toggle) return;
|
|
36
49
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
50
|
+
setExpanded(toggle.closest(".b270-navmenu"), toggle.getAttribute("aria-expanded") !== "true");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
document.addEventListener("keydown", (event) => {
|
|
54
|
+
if (event.key !== "Escape" || event.defaultPrevented) return;
|
|
55
|
+
|
|
56
|
+
const menu = event.target.closest && event.target.closest(".b270-navmenu");
|
|
57
|
+
const toggle = menu && menu.querySelector("button.b270-navtoggle");
|
|
58
|
+
if (!toggle || toggle.getAttribute("aria-expanded") !== "true") return;
|
|
40
59
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
const restoreFocus = menu.contains(document.activeElement);
|
|
61
|
+
setExpanded(menu, false);
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
if (restoreFocus) toggle.focus();
|
|
44
64
|
});
|
|
45
65
|
|
|
46
66
|
window.Bible270NavigationMenu = true;
|
|
@@ -32,6 +32,36 @@
|
|
|
32
32
|
--alert-surface:#42282a; --alert-ink:#ffc0b8; --alert-line:#a66d68;
|
|
33
33
|
--notice-surface:#25382a; --notice-ink:#b6dfb6; --notice-line:#6d956e;
|
|
34
34
|
}
|
|
35
|
+
.b270[data-display-mode="dark_green"]{
|
|
36
|
+
color-scheme:dark;
|
|
37
|
+
background-image:none;
|
|
38
|
+
--control-line:#7c9280;
|
|
39
|
+
--paper:#1c3027; --card:#17291f; --ink:#d9decb; --muted:#bdc9b5; --faint:#a6b69e;
|
|
40
|
+
--line:#3c5045; --line-soft:#304638; --teal:#83c4b3; --oxblood:#df9aa6; --gold:#d8c184;
|
|
41
|
+
--gold-lite:#62501e; --shadow:0 1px 0 rgba(0,0,0,.1);
|
|
42
|
+
--control:#101c17; --task-surface:rgba(255,255,255,.025); --button-hover:#1e3829;
|
|
43
|
+
--on-accent:#e3e7d5; --complete-ink:#e3e7d5; --complete-dot:#e3e7d5;
|
|
44
|
+
--button-surface:#102219; --button-ink:#e3e7d5;
|
|
45
|
+
--teal-fill:#28554b; --oxblood-fill:#6a2c3c; --gold-fill:#62501e;
|
|
46
|
+
--teal-mark:#4d9d89; --oxblood-mark:#c46c7f; --gold-mark:#b79b45;
|
|
47
|
+
--alert-surface:#3e282c; --alert-ink:#ffc1c2; --alert-line:#a87479;
|
|
48
|
+
--notice-surface:#243b2b; --notice-ink:#c1e2b9; --notice-line:#779c70;
|
|
49
|
+
}
|
|
50
|
+
.b270[data-display-mode="dark_blue"]{
|
|
51
|
+
color-scheme:dark;
|
|
52
|
+
background-image:none;
|
|
53
|
+
--control-line:#7c8da7;
|
|
54
|
+
--paper:#182537; --card:#121d2e; --ink:#d1dbe8; --muted:#b6c3d5; --faint:#9dadc3;
|
|
55
|
+
--line:#36465d; --line-soft:#29394e; --teal:#81bfca; --oxblood:#df9caf; --gold:#d5bd82;
|
|
56
|
+
--gold-lite:#5d4c25; --shadow:0 1px 0 rgba(0,0,0,.1);
|
|
57
|
+
--control:#0b1422; --task-surface:rgba(255,255,255,.025); --button-hover:#20334d;
|
|
58
|
+
--on-accent:#e0e7ef; --complete-ink:#e0e7ef; --complete-dot:#e0e7ef;
|
|
59
|
+
--button-surface:#0c1829; --button-ink:#e0e7ef;
|
|
60
|
+
--teal-fill:#245363; --oxblood-fill:#622d49; --gold-fill:#5d4c25;
|
|
61
|
+
--teal-mark:#4c9eae; --oxblood-mark:#b86382; --gold-mark:#b59a54;
|
|
62
|
+
--alert-surface:#3b2432; --alert-ink:#ffc0cd; --alert-line:#a66c80;
|
|
63
|
+
--notice-surface:#1e3430; --notice-ink:#b7decc; --notice-line:#6b9787;
|
|
64
|
+
}
|
|
35
65
|
@media (prefers-color-scheme:dark){
|
|
36
66
|
.b270[data-display-mode="system"]{
|
|
37
67
|
color-scheme:dark;
|
data/config/routes.rb
CHANGED
|
@@ -23,6 +23,7 @@ Bible270::Engine.routes.draw do
|
|
|
23
23
|
get 'admin/readers/:id', to: 'admin#show', as: :admin_reader
|
|
24
24
|
delete 'admin/readers/:id', to: 'admin#destroy'
|
|
25
25
|
patch 'admin/readers/:id/profile', to: 'admin#update_profile', as: :admin_reader_profile
|
|
26
|
+
patch 'admin/readers/:id/theme', to: 'admin#update_theme', as: :admin_reader_theme
|
|
26
27
|
patch 'admin/readers/:id/notifications', to: 'admin#update_notifications', as: :admin_reader_notifications
|
|
27
28
|
patch 'admin/readers/:id/completion-animation', to: 'admin#update_completion_animation',
|
|
28
29
|
as: :admin_reader_completion_animation
|
data/lib/bible270/version.rb
CHANGED