bible270 1.4.1 → 1.4.2
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/CHANGELOG.md +9 -0
- data/app/controllers/bible270/admin_controller.rb +19 -2
- data/app/controllers/bible270/checkoffs_controller.rb +20 -16
- data/app/controllers/bible270/days_controller.rb +2 -9
- data/app/models/bible270/reader.rb +29 -0
- data/app/views/bible270/admin/show.html.erb +23 -0
- data/app/views/bible270/checkoffs/toggle.turbo_stream.erb +1 -1
- data/app/views/bible270/days/_completers.html.erb +2 -3
- data/app/views/bible270/days/_day_progress.html.erb +4 -2
- data/app/views/bible270/days/show.html.erb +3 -3
- data/app/views/bible270/shared/_day_completion_dove.html.erb +7 -5
- data/app/views/bible270/shared/_interaction_ui.html.erb +62 -1
- data/app/views/bible270/shared/_styles.html.erb +10 -6
- data/config/routes.rb +3 -0
- data/db/migrate/20260101000020_add_completion_dove_disabled_to_bible270_readers.rb +7 -0
- data/lib/bible270/migration_reconciler.rb +5 -0
- data/lib/bible270/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8792007d09956f4b53cbe9842007baea209a5eb0010758ef35e7d6f1977928f0
|
|
4
|
+
data.tar.gz: 0c5d16299136af62a6e1466210e962ef6e8d48065d759d9b5b57450215471c31
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89cbba3ff6adfc107efacddb9b2d80903452488384983b488ecf733aa6e16d55451a0137626cda5a446591b2c28585201f49b612927a6ba80ad0b54a5889c377
|
|
7
|
+
data.tar.gz: d2d501c3f9935405bf0b850124f2329b3c5355ccfdf5d7403688329a65f1bfdc4a779fc3d65d957eba242363393e6a057519ef509b337986f6426bb2a3c6ffdc
|
data/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,18 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
|
|
|
4
4
|
|
|
5
5
|
## Unreleased Changes
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- let administrators disable the day-completion dove animation for individual readers
|
|
10
|
+
|
|
7
11
|
### Changed
|
|
8
12
|
|
|
9
13
|
- simplify day completion to one continuous server-confirmed dove flight from the lower center, eliminating the stagger between a pre-submit animation and the Turbo or HTML response
|
|
14
|
+
- keep the dove bright in dark mode, grow it to an 80%-of-viewport fit, and finish with a quick fade instead of veering offscreen
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- show the dove only once for each persisted day-completion event, even when Turbo replays a response or retries a stale-session request
|
|
10
19
|
|
|
11
20
|
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.4.0...main)**
|
|
12
21
|
|
|
@@ -13,8 +13,8 @@ module Bible270
|
|
|
13
13
|
|
|
14
14
|
before_action :require_admin!
|
|
15
15
|
before_action :load_reader,
|
|
16
|
-
only: %i[show destroy update_start update_profile update_notifications
|
|
17
|
-
toggle_day]
|
|
16
|
+
only: %i[show destroy update_start update_profile update_notifications update_completion_animation
|
|
17
|
+
remove_avatar complete_through toggle_day]
|
|
18
18
|
before_action :load_comment, only: %i[hide_comment unhide_comment destroy_comment]
|
|
19
19
|
|
|
20
20
|
# ---- enrolment --------------------------------------------------------
|
|
@@ -74,6 +74,7 @@ module Bible270
|
|
|
74
74
|
|
|
75
75
|
def show
|
|
76
76
|
@comment_notifications_available = comment_notifications_available_for?(@reader)
|
|
77
|
+
@completion_dove_preference_available = completion_dove_available_for(@reader)
|
|
77
78
|
@days_completed = @reader.days_completed
|
|
78
79
|
@comments = @reader.comments.order(created_at: :desc).limit(50)
|
|
79
80
|
end
|
|
@@ -236,6 +237,16 @@ module Bible270
|
|
|
236
237
|
redirect_to admin_reader_path(@reader), notice: 'Reflection email setting updated.'
|
|
237
238
|
end
|
|
238
239
|
|
|
240
|
+
def update_completion_animation
|
|
241
|
+
unless completion_dove_available_for(@reader)
|
|
242
|
+
alert = 'Run the pending Bible270 database migration before changing the completion animation.'
|
|
243
|
+
return redirect_to(admin_reader_path(@reader), alert: alert)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
@reader.update!(completion_dove_disabled: params[:completion_dove_disabled] == '1')
|
|
247
|
+
redirect_to admin_reader_path(@reader), notice: 'Completion animation setting updated.'
|
|
248
|
+
end
|
|
249
|
+
|
|
239
250
|
def remove_avatar
|
|
240
251
|
@reader.remove_avatar!
|
|
241
252
|
redirect_to admin_reader_path(@reader), notice: "Removed #{@reader.display_name}'s picture."
|
|
@@ -298,6 +309,12 @@ module Bible270
|
|
|
298
309
|
true
|
|
299
310
|
end
|
|
300
311
|
|
|
312
|
+
def completion_dove_available_for(reader)
|
|
313
|
+
available = Reader.completion_dove_column?
|
|
314
|
+
reader.reload if available && !reader.has_attribute?('completion_dove_disabled')
|
|
315
|
+
available
|
|
316
|
+
end
|
|
317
|
+
|
|
301
318
|
def require_admin!
|
|
302
319
|
return if Bible270.config.admin_configured? && Bible270.config.admin?(current_reader)
|
|
303
320
|
|
|
@@ -18,25 +18,27 @@ module Bible270
|
|
|
18
18
|
requested_state = params[:checked] if params.key?(:checked)
|
|
19
19
|
head :bad_request and return if params.key?(:checked) && !%w[0 1].include?(requested_state)
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
# Serialize this reader's transitions so two different final portions
|
|
22
|
+
# submitted concurrently cannot both announce the same day completion.
|
|
23
|
+
marked_read = nil
|
|
24
|
+
current_reader.with_lock do
|
|
25
|
+
marked_read, @completion_event_id = apply_checkoff(requested_state)
|
|
26
|
+
@reader = current_reader.reload.reload_progress
|
|
27
|
+
@day_just_completed = marked_read && @completion_event_id && @reader.day_complete?(@day)
|
|
28
|
+
end
|
|
25
29
|
@reader_tracks = @reader.read_tracks_for(@day)
|
|
26
30
|
@readings = Plan.readings_for(@day)
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
completer_ids = Checkoff.where(day: @day)
|
|
30
|
-
.group(:reader_id)
|
|
31
|
-
.having('COUNT(*) >= ?', required)
|
|
32
|
-
.pluck(:reader_id)
|
|
33
|
-
@completers = Reader.where(id: completer_ids).order(:display_name)
|
|
32
|
+
@completers = Reader.completers_for(@day)
|
|
34
33
|
|
|
35
34
|
respond_to do |format|
|
|
36
35
|
format.turbo_stream
|
|
37
36
|
format.html do
|
|
38
37
|
if @day_just_completed
|
|
39
|
-
redirect_to day_path(@day), flash: {
|
|
38
|
+
redirect_to day_path(@day), flash: {
|
|
39
|
+
b270_day_just_completed: @day,
|
|
40
|
+
b270_completion_event_id: @completion_event_id
|
|
41
|
+
}
|
|
40
42
|
else
|
|
41
43
|
message = marked_read ? 'Reading marked read.' : 'Reading marked unread.'
|
|
42
44
|
redirect_to day_path(@day), flash: { b270_interaction_status: message }
|
|
@@ -51,9 +53,10 @@ module Bible270
|
|
|
51
53
|
existing = find_checkoff
|
|
52
54
|
if requested_state == '1'
|
|
53
55
|
current_reader.ensure_started!
|
|
54
|
-
[true, existing.nil?
|
|
56
|
+
[true, existing.nil? ? create_checkoff : nil]
|
|
55
57
|
elsif requested_state == '0' || existing
|
|
56
|
-
|
|
58
|
+
existing&.destroy
|
|
59
|
+
[false, nil]
|
|
57
60
|
else
|
|
58
61
|
current_reader.ensure_started!
|
|
59
62
|
[true, create_checkoff]
|
|
@@ -66,15 +69,16 @@ module Bible270
|
|
|
66
69
|
end
|
|
67
70
|
end
|
|
68
71
|
|
|
72
|
+
# The created id is stable if Turbo replays this response, while a later
|
|
73
|
+
# re-completion creates a new id and is correctly treated as a new event.
|
|
69
74
|
def create_checkoff
|
|
70
|
-
Checkoff.create!(reader: current_reader, day: @day, track: @track, part: @part)
|
|
71
|
-
true
|
|
75
|
+
Checkoff.create!(reader: current_reader, day: @day, track: @track, part: @part).id
|
|
72
76
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
73
77
|
# A repeated or concurrent desired-state request is already successful. Do
|
|
74
78
|
# not hide any other validation failure under the same recovery.
|
|
75
79
|
raise unless find_checkoff
|
|
76
80
|
|
|
77
|
-
|
|
81
|
+
nil
|
|
78
82
|
end
|
|
79
83
|
end
|
|
80
84
|
end
|
|
@@ -21,16 +21,9 @@ module Bible270
|
|
|
21
21
|
@new_comment = Comment.new(day: @day, parent_id: reply_parent&.id)
|
|
22
22
|
@reader_tracks = current_reader ? current_reader.read_tracks_for(@day) : []
|
|
23
23
|
@day_just_completed = flash[:b270_day_just_completed].to_i == @day
|
|
24
|
+
@completion_event_id = flash[:b270_completion_event_id] if @day_just_completed
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
# chapters is three rows, so counting tracks would call someone finished
|
|
27
|
-
# who had only read the Old Testament.
|
|
28
|
-
required = Plan.total_parts(@day)
|
|
29
|
-
completer_ids = Checkoff.where(day: @day)
|
|
30
|
-
.group(:reader_id)
|
|
31
|
-
.having('COUNT(*) >= ?', required)
|
|
32
|
-
.pluck(:reader_id)
|
|
33
|
-
@completers = Reader.where(id: completer_ids).order(:display_name)
|
|
26
|
+
@completers = Reader.completers_for(@day)
|
|
34
27
|
end
|
|
35
28
|
|
|
36
29
|
private
|
|
@@ -33,6 +33,7 @@ module Bible270
|
|
|
33
33
|
REFLECTIONS_SEEN_COLUMN = 'reflections_seen_at'
|
|
34
34
|
COMMENT_NOTIFICATION_LEVELS = %w[all personal none].freeze
|
|
35
35
|
COMMENT_NOTIFICATION_COLUMNS = %w[notify_on_mention notify_on_all_comments].freeze
|
|
36
|
+
COMPLETION_DOVE_COLUMN = 'completion_dove_disabled'
|
|
36
37
|
MENTION_SUGGESTION_LIMIT = 5
|
|
37
38
|
|
|
38
39
|
validates :display_name, presence: true
|
|
@@ -64,6 +65,18 @@ module Bible270
|
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
|
|
68
|
+
# A day is completed when its final required checkoff is created, so MAX(created_at)
|
|
69
|
+
# is the completion moment. Keep reader_id as a deterministic tie-breaker.
|
|
70
|
+
def self.completers_for(day)
|
|
71
|
+
reader_ids = Checkoff.where(day: day)
|
|
72
|
+
.group(:reader_id)
|
|
73
|
+
.having('COUNT(*) >= ?', Plan.total_parts(day))
|
|
74
|
+
.order(Checkoff.arel_table[:created_at].maximum.asc, :reader_id)
|
|
75
|
+
.pluck(:reader_id)
|
|
76
|
+
readers_by_id = where(id: reader_ids).index_by(&:id)
|
|
77
|
+
reader_ids.filter_map { |reader_id| readers_by_id[reader_id] }
|
|
78
|
+
end
|
|
79
|
+
|
|
67
80
|
def self.reflections_seen_column?
|
|
68
81
|
column_names.include?(REFLECTIONS_SEEN_COLUMN)
|
|
69
82
|
rescue ActiveRecord::StatementInvalid
|
|
@@ -289,6 +302,22 @@ module Bible270
|
|
|
289
302
|
where(notify_on_all_comments: true).where.not(email: [nil, ''])
|
|
290
303
|
end
|
|
291
304
|
|
|
305
|
+
def self.completion_dove_column?
|
|
306
|
+
return true if column_names.include?(COMPLETION_DOVE_COLUMN)
|
|
307
|
+
|
|
308
|
+
reset_column_information
|
|
309
|
+
column_names.include?(COMPLETION_DOVE_COLUMN)
|
|
310
|
+
rescue ActiveRecord::StatementInvalid
|
|
311
|
+
false
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Keep day completion usable while an upgraded host is waiting to run the
|
|
315
|
+
# preference migration. Historically the dove was enabled, so a missing
|
|
316
|
+
# column preserves that behavior.
|
|
317
|
+
def completion_dove_disabled?
|
|
318
|
+
has_attribute?(COMPLETION_DOVE_COLUMN) && self[COMPLETION_DOVE_COLUMN] == true
|
|
319
|
+
end
|
|
320
|
+
|
|
292
321
|
# Small, deterministic suggestions for the reflection composer. Full-handle
|
|
293
322
|
# collisions are omitted because the server must never guess which reader a
|
|
294
323
|
# mention intended.
|
|
@@ -70,6 +70,29 @@
|
|
|
70
70
|
<% end %>
|
|
71
71
|
</div>
|
|
72
72
|
|
|
73
|
+
<div class="b270-panel">
|
|
74
|
+
<h2 class="b270-subhead">Completion animation</h2>
|
|
75
|
+
<% unless @completion_dove_preference_available %>
|
|
76
|
+
<p class="b270-flash alert" style="margin:0">
|
|
77
|
+
Run the pending Bible270 database migration before changing the completion animation.
|
|
78
|
+
</p>
|
|
79
|
+
<% else %>
|
|
80
|
+
<%= form_with url: admin_reader_completion_animation_path(@reader), method: :patch,
|
|
81
|
+
class: "b270-emailform" do %>
|
|
82
|
+
<div class="b270-field">
|
|
83
|
+
<label class="b270-reminder-optin">
|
|
84
|
+
<%= check_box_tag :completion_dove_disabled, "1", @reader.completion_dove_disabled? %>
|
|
85
|
+
<span>Turn off the dove animation for this reader</span>
|
|
86
|
+
</label>
|
|
87
|
+
</div>
|
|
88
|
+
<p class="b270-hint" style="margin:0">
|
|
89
|
+
Day completion and progress tracking still work normally when the animation is off.
|
|
90
|
+
</p>
|
|
91
|
+
<%= submit_tag 'Save', class: 'b270-btn' %>
|
|
92
|
+
<% end %>
|
|
93
|
+
<% end %>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
73
96
|
<div class="b270-panel">
|
|
74
97
|
<h2 class="b270-subhead">Where they are in the plan</h2>
|
|
75
98
|
<%= form_with url: admin_reader_start_path(@reader), method: :patch, class: "b270-startform" do %>
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
<%= turbo_stream.replace "day_progress_#{@day}" do %>
|
|
7
7
|
<%= render "bible270/days/day_progress", day: @day, reader: @reader,
|
|
8
|
-
just_completed: @day_just_completed %>
|
|
8
|
+
just_completed: @day_just_completed, completion_event_id: @completion_event_id %>
|
|
9
9
|
<% end %>
|
|
10
10
|
|
|
11
11
|
<%= turbo_stream.replace "completers_#{@day}" do %>
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
<div class="b270-completers" id="completers_<%= day %>">
|
|
3
3
|
<% if completers.any? %>
|
|
4
4
|
<span>Finished this day:</span>
|
|
5
|
-
<% completers.
|
|
6
|
-
<%= link_to b270_avatar(reader), reader_path(reader), title: reader.display_name %>
|
|
5
|
+
<% completers.each do |reader| %>
|
|
6
|
+
<%= link_to b270_avatar(reader, size: 56), reader_path(reader), title: reader.display_name %>
|
|
7
7
|
<% end %>
|
|
8
|
-
<% if completers.size > 12 %><span><%= completers.size - 12 %> more</span><% end %>
|
|
9
8
|
<% end %>
|
|
10
9
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<%# locals: (day:, reader:, just_completed: false) %>
|
|
1
|
+
<%# locals: (day:, reader:, just_completed: false, completion_event_id: nil) %>
|
|
2
2
|
<%
|
|
3
3
|
previous_day = day > 1 ? day - 1 : nil
|
|
4
4
|
next_day = day < Bible270::Plan::DAYS ? day + 1 : nil
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
<% if complete %>
|
|
36
|
-
|
|
36
|
+
<% if just_completed && completion_event_id && !reader&.completion_dove_disabled? %>
|
|
37
|
+
<%= render "bible270/shared/day_completion_dove", day: day, completion_event_id: completion_event_id %>
|
|
38
|
+
<% end %>
|
|
37
39
|
<div class="b270-day-complete" role="status" aria-live="polite"
|
|
38
40
|
<%= tag.attributes(data: completion_data) %>>
|
|
39
41
|
<div>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<div class="b270-day-page">
|
|
4
4
|
<%= render "bible270/days/day_progress", day: @day, reader: current_reader,
|
|
5
|
-
just_completed: @day_just_completed %>
|
|
5
|
+
just_completed: @day_just_completed, completion_event_id: @completion_event_id %>
|
|
6
6
|
|
|
7
7
|
<%= render "bible270/shared/interaction_status" %>
|
|
8
8
|
|
|
@@ -16,8 +16,6 @@
|
|
|
16
16
|
<% end %>
|
|
17
17
|
|
|
18
18
|
<div class="b270-day-community">
|
|
19
|
-
<%= render "bible270/days/completers", day: @day, completers: @completers %>
|
|
20
|
-
|
|
21
19
|
<h2 class="b270-section-h">Reflections</h2>
|
|
22
20
|
<div id="comments">
|
|
23
21
|
<%= render partial: "bible270/comments/comment", collection: @comments, as: :comment %>
|
|
@@ -30,6 +28,8 @@
|
|
|
30
28
|
<%= render "bible270/shared/comment_drafts" %>
|
|
31
29
|
<%= render "bible270/shared/mention_typeahead" %>
|
|
32
30
|
<% end %>
|
|
31
|
+
|
|
32
|
+
<%= render "bible270/days/completers", day: @day, completers: @completers %>
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
35
35
|
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
<%# locals: (day:) %>
|
|
1
|
+
<%# locals: (day:, completion_event_id:) %>
|
|
2
2
|
<svg class="b270-completion-dove" data-b270-completion-dove data-b270-day="<%= day %>"
|
|
3
|
+
data-b270-completion-event="<%= completion_event_id %>" data-turbo-temporary
|
|
3
4
|
viewBox="0 0 260 200" aria-hidden="true" focusable="false">
|
|
4
5
|
<defs>
|
|
5
6
|
<radialGradient id="b270-dove-light" cx="50%" cy="43%" r="68%">
|
|
6
|
-
<stop offset="0%" stop-color="#fff" />
|
|
7
|
-
<stop offset="58%" stop-color="#fffef8" />
|
|
8
|
-
<stop offset="100%" stop-color="#fff9e2" />
|
|
7
|
+
<stop class="b270-dove-light-center" offset="0%" stop-color="#fff" />
|
|
8
|
+
<stop class="b270-dove-light-middle" offset="58%" stop-color="#fffef8" />
|
|
9
|
+
<stop class="b270-dove-light-edge" offset="100%" stop-color="#fff9e2" />
|
|
9
10
|
</radialGradient>
|
|
10
11
|
</defs>
|
|
11
12
|
<g class="b270-dove-silhouette">
|
|
12
13
|
<path d="M122 94C103 82 87 69 72 53L39 17l11 28-23-23 15 32-23-20 19 30-24-16 22 26-25-12 27 23-24-7 30 19-22-3 31 17c20 11 42 16 59 8 5-9 8-18 10-25Z" />
|
|
13
14
|
<path d="M138 94c19-12 35-25 50-41l33-36-11 28 23-23-15 32 23-20-19 30 24-16-22 26 25-12-27 23 24-7-30 19 22-3-31 17c-20 11-42 16-59 8-5-9-8-18-10-25Z" />
|
|
14
15
|
<path d="M148 140c6 7 16 14 24 19 2 2 1 5-2 6-5 2-10-1-14-5 4 8 7 17 4 22-2 3-5 1-7-2l-6-11c3 10 2 19-3 24-2 3-5 1-6-3l-4-15c0 11-3 21-8 24-3 2-5-1-6-5l-1-19c-3 10-6 17-11 20-3 2-5-1-5-5l2-19c-4 8-9 13-14 14-3 1-5-2-4-6l6-18c-5 5-11 7-16 4-3-1-3-4 0-7 7-7 17-13 26-19Z" />
|
|
15
|
-
<path
|
|
16
|
+
<path class="b270-dove-beak" d="M140 80C144 80 148 80.8 151 82 152 82.4 152 83.2 151 83.8 148 85.1 144 85.8 140 85 138.5 83.5 138.5 81.2 140 80Z" />
|
|
17
|
+
<path d="M108 116c5-10 9-17 14-17-1-11-2-23 4-29 5-4 12-3 15 1 2 3 2 6 1 9 1 1 1 3 0 4-1 5-3 10-5 15 6 3 11 9 15 17-1 15-6 28-16 39-3 4-7 7-10 9-4-2-8-5-11-9-9-11-14-25-11-37 1-1 2-2 4-2Z" />
|
|
16
18
|
</g>
|
|
17
19
|
</svg>
|
|
@@ -18,6 +18,53 @@
|
|
|
18
18
|
region.dataset.state = state;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
const shownCompletionDoves = new Set();
|
|
22
|
+
|
|
23
|
+
function claimCompletionDove(dove) {
|
|
24
|
+
const eventId = dove.dataset.b270CompletionEvent;
|
|
25
|
+
if (!eventId) return false;
|
|
26
|
+
|
|
27
|
+
const key = `b270-completion-dove:${eventId}`;
|
|
28
|
+
if (shownCompletionDoves.has(key)) return false;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
if (window.sessionStorage.getItem(key) === "shown") return false;
|
|
32
|
+
window.sessionStorage.setItem(key, "shown");
|
|
33
|
+
} catch (_error) {
|
|
34
|
+
// Some privacy modes disable session storage. The in-memory set still
|
|
35
|
+
// prevents duplicate Turbo rendering within the current page.
|
|
36
|
+
}
|
|
37
|
+
shownCompletionDoves.add(key);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function initializeCompletionDove(dove) {
|
|
42
|
+
if (dove.dataset.b270DoveInitialized === "true") return;
|
|
43
|
+
dove.dataset.b270DoveInitialized = "true";
|
|
44
|
+
|
|
45
|
+
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
46
|
+
dove.remove();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!claimCompletionDove(dove)) {
|
|
51
|
+
dove.remove();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Keep an active flight outside the Turbo-replaced progress panel so a
|
|
56
|
+
// replayed response cannot restart or cut short the same completion event.
|
|
57
|
+
if (dove.parentElement !== document.body) document.body.appendChild(dove);
|
|
58
|
+
requestAnimationFrame(() => {
|
|
59
|
+
if (dove.isConnected) dove.classList.add("b270-completion-dove-active");
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function initializeCompletionDoves(root = document) {
|
|
64
|
+
if (root.matches && root.matches("[data-b270-completion-dove]")) initializeCompletionDove(root);
|
|
65
|
+
root.querySelectorAll?.("[data-b270-completion-dove]").forEach(initializeCompletionDove);
|
|
66
|
+
}
|
|
67
|
+
|
|
21
68
|
function resetForm(form) {
|
|
22
69
|
form.removeAttribute("aria-busy");
|
|
23
70
|
form.dataset.b270Submitting = "false";
|
|
@@ -29,8 +76,18 @@
|
|
|
29
76
|
|
|
30
77
|
function reset() {
|
|
31
78
|
document.querySelectorAll("form[data-b270-submit='true']").forEach(resetForm);
|
|
79
|
+
initializeCompletionDoves();
|
|
32
80
|
}
|
|
33
81
|
|
|
82
|
+
const completionDoveObserver = new MutationObserver((mutations) => {
|
|
83
|
+
mutations.forEach((mutation) => {
|
|
84
|
+
mutation.addedNodes.forEach((node) => {
|
|
85
|
+
if (node.nodeType === Node.ELEMENT_NODE) initializeCompletionDoves(node);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
completionDoveObserver.observe(document.documentElement, { childList: true, subtree: true });
|
|
90
|
+
|
|
34
91
|
const SESSION_REFRESH_AFTER = 5 * 60 * 1000;
|
|
35
92
|
let sessionRefreshedAt = Date.now();
|
|
36
93
|
let sessionRefresh = null;
|
|
@@ -201,8 +258,12 @@
|
|
|
201
258
|
}
|
|
202
259
|
});
|
|
203
260
|
document.addEventListener("turbo:load", reset);
|
|
261
|
+
document.addEventListener("turbo:before-cache", () => {
|
|
262
|
+
document.querySelectorAll("[data-b270-completion-dove]").forEach((dove) => dove.remove());
|
|
263
|
+
});
|
|
204
264
|
document.addEventListener("animationend", (event) => {
|
|
205
|
-
if (event.
|
|
265
|
+
if (event.animationName === "b270-dove-flight" &&
|
|
266
|
+
event.target.matches("[data-b270-completion-dove]")) event.target.remove();
|
|
206
267
|
});
|
|
207
268
|
|
|
208
269
|
window.Bible270InteractionUI = { reset };
|
|
@@ -148,16 +148,20 @@
|
|
|
148
148
|
.b270-day-complete span{font-size:12px;color:var(--muted)}
|
|
149
149
|
.b270 .b270-day-complete .b270-btn{flex:none;margin:0 0 0 auto}
|
|
150
150
|
.b270-completion-dove{position:fixed;z-index:1000;top:72vh;left:50vw;
|
|
151
|
-
width:min(
|
|
151
|
+
width:min(80vw,104vh);height:auto;overflow:visible;pointer-events:none;opacity:0;
|
|
152
|
+
color-scheme:only light;forced-color-adjust:none;
|
|
152
153
|
filter:drop-shadow(0 0 8px rgba(255,255,255,1)) drop-shadow(0 0 24px rgba(255,252,220,1)) drop-shadow(0 0 52px rgba(225,191,92,.82));
|
|
153
|
-
will-change:top,left,transform,opacity
|
|
154
|
-
.b270-dove-
|
|
154
|
+
will-change:top,left,transform,opacity}
|
|
155
|
+
.b270-completion-dove-active{animation:b270-dove-flight 1.7s cubic-bezier(.18,.72,.28,1) both}
|
|
156
|
+
.b270-dove-silhouette{fill:url(#b270-dove-light)!important;stroke:none}
|
|
157
|
+
.b270-dove-light-center{stop-color:#fff!important}
|
|
158
|
+
.b270-dove-light-middle{stop-color:#fffef8!important}
|
|
159
|
+
.b270-dove-light-edge{stop-color:#fff9e2!important}
|
|
155
160
|
@keyframes b270-dove-flight{
|
|
156
161
|
0%{top:72vh;left:50vw;opacity:.22;visibility:visible;transform:translate(-50%,-50%) scale(.02) rotate(-8deg)}
|
|
157
162
|
8%{top:calc(72vh - 10px);left:50vw;opacity:.78;transform:translate(-50%,-50%) scale(.08) rotate(-5deg)}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
100%{top:-25vh;left:115vw;opacity:0;visibility:hidden;transform:translate(-50%,-50%) scale(1.55) rotate(12deg)}
|
|
163
|
+
33%{top:50vh;left:50vw;opacity:1;visibility:visible;transform:translate(-50%,-50%) scale(1) rotate(4deg)}
|
|
164
|
+
100%{top:50vh;left:50vw;opacity:0;visibility:hidden;transform:translate(-50%,-50%) scale(1) rotate(4deg)}
|
|
161
165
|
}
|
|
162
166
|
|
|
163
167
|
/* readings */
|
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# Rails routes are intentionally kept in one cohesive DSL block.
|
|
3
4
|
Bible270::Engine.routes.draw do
|
|
4
5
|
root to: 'days#index'
|
|
5
6
|
|
|
@@ -22,6 +23,8 @@ Bible270::Engine.routes.draw do
|
|
|
22
23
|
delete 'admin/readers/:id', to: 'admin#destroy'
|
|
23
24
|
patch 'admin/readers/:id/profile', to: 'admin#update_profile', as: :admin_reader_profile
|
|
24
25
|
patch 'admin/readers/:id/notifications', to: 'admin#update_notifications', as: :admin_reader_notifications
|
|
26
|
+
patch 'admin/readers/:id/completion-animation', to: 'admin#update_completion_animation',
|
|
27
|
+
as: :admin_reader_completion_animation
|
|
25
28
|
delete 'admin/readers/:id/avatar', to: 'admin#remove_avatar', as: :admin_reader_avatar
|
|
26
29
|
patch 'admin/readers/:id/version', to: 'admin#update_bible_version', as: :admin_reader_version
|
|
27
30
|
patch 'admin/readers/:id/start', to: 'admin#update_start', as: :admin_reader_start
|
|
@@ -29,6 +29,7 @@ module Bible270
|
|
|
29
29
|
'AddReflectionsSeenAtToBible270Readers' => :check_reflections_seen_at,
|
|
30
30
|
'AddAllCommentNoticesToBible270Readers' => :check_all_comment_notices,
|
|
31
31
|
'ChangePassageSourceDefaultToBibleCom' => :check_passage_source_default,
|
|
32
|
+
'AddCompletionDoveDisabledToBible270Readers' => :check_completion_dove_disabled,
|
|
32
33
|
'CreateActiveStorageTables' => :check_active_storage
|
|
33
34
|
}.freeze
|
|
34
35
|
|
|
@@ -204,6 +205,10 @@ module Bible270
|
|
|
204
205
|
column&.default == 'bible_com' ? [] : ['column bible270_readers.passage_source default bible_com']
|
|
205
206
|
end
|
|
206
207
|
|
|
208
|
+
def check_completion_dove_disabled
|
|
209
|
+
table_missing(:bible270_readers, columns: %i[completion_dove_disabled])
|
|
210
|
+
end
|
|
211
|
+
|
|
207
212
|
# The Bible270 installer can install Active Storage for reader avatars. A
|
|
208
213
|
# copied database can therefore have the same timestamp mismatch for Rails'
|
|
209
214
|
# generated Active Storage migration as it has for the engine migrations.
|
data/lib/bible270/version.rb
CHANGED
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: 1.4.
|
|
4
|
+
version: 1.4.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew vonderLuft
|
|
@@ -175,6 +175,7 @@ files:
|
|
|
175
175
|
- db/migrate/20260101000017_add_reflections_seen_at_to_bible270_readers.rb
|
|
176
176
|
- db/migrate/20260101000018_add_all_comment_notices_to_bible270_readers.rb
|
|
177
177
|
- db/migrate/20260101000019_change_passage_source_default_to_bible_com.rb
|
|
178
|
+
- db/migrate/20260101000020_add_completion_dove_disabled_to_bible270_readers.rb
|
|
178
179
|
- lib/bible270.rb
|
|
179
180
|
- lib/bible270/avatars.rb
|
|
180
181
|
- lib/bible270/configuration.rb
|