bible270 1.3.2 → 1.4.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/app/controllers/bible270/checkoffs_controller.rb +29 -10
- data/app/controllers/bible270/days_controller.rb +1 -0
- data/app/views/bible270/checkoffs/toggle.turbo_stream.erb +2 -1
- data/app/views/bible270/days/_day_progress.html.erb +5 -2
- data/app/views/bible270/days/show.html.erb +3 -1
- data/app/views/bible270/shared/_day_completion_dove.html.erb +17 -0
- data/app/views/bible270/shared/_day_completion_effect.html.erb +67 -0
- data/app/views/bible270/shared/_interaction_ui.html.erb +5 -1
- data/app/views/bible270/shared/_styles.html.erb +14 -1
- data/lib/bible270/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8975632aa3e90165634356f25fc0c8cff2909e84b3177a58e192b16ee325b745
|
|
4
|
+
data.tar.gz: 543aa029054018c26f87d2d1836302ab671de51b70caa7a531ed0011a405f5e6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c76e854eb6449addef5b1deb2066d70cc6c51e8650817efa7c4fef8efa0d847d0b1e85c75d9faa91dde497495a01c980c674342148326f8c43f146a3321175d4
|
|
7
|
+
data.tar.gz: 80761d784e504c167954827dbe3e3e8193e571e91eee67b8bd9aa12b7990b9845020f20399c3367f63b41d441107fa15748e6e4199d327ce2588d1976a6227ca
|
data/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,15 @@ All notable changes to bible270. Format follows [Keep a Changelog](https://keepa
|
|
|
4
4
|
|
|
5
5
|
## Unreleased Changes
|
|
6
6
|
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- mark a newly completed day with a brief, translucent dove animation that begins immediately on the final checkoff in JavaScript-capable browsers and remains available after Turbo or HTML-fallback responses; it respects reduced-motion preferences, replaces the final generic success message, and never blocks interaction
|
|
10
|
+
|
|
7
11
|
### Fixed
|
|
8
12
|
|
|
9
13
|
- correct Day 27's New Testament reading to include Matthew 26:75
|
|
10
14
|
- silently recover all Turbo interactions from stale sessions, retry once with a fresh CSRF token, and reload rather than submit with a known-stale token if refreshing fails
|
|
15
|
+
- keep repeated or concurrent explicit checkoffs idempotent instead of raising a duplicate-validation Rails 422 error
|
|
11
16
|
|
|
12
17
|
**[Full Changelog](https://github.com/avonderluft/bible270/compare/v1.3.0...main)**
|
|
13
18
|
|
|
@@ -18,9 +18,10 @@ 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
|
-
marked_read = apply_checkoff(requested_state)
|
|
21
|
+
marked_read, checkoff_changed = apply_checkoff(requested_state)
|
|
22
22
|
|
|
23
23
|
@reader = current_reader.reload.reload_progress
|
|
24
|
+
@day_just_completed = marked_read && checkoff_changed && @reader.day_complete?(@day)
|
|
24
25
|
@reader_tracks = @reader.read_tracks_for(@day)
|
|
25
26
|
@readings = Plan.readings_for(@day)
|
|
26
27
|
|
|
@@ -34,8 +35,12 @@ module Bible270
|
|
|
34
35
|
respond_to do |format|
|
|
35
36
|
format.turbo_stream
|
|
36
37
|
format.html do
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if @day_just_completed
|
|
39
|
+
redirect_to day_path(@day), flash: { b270_day_just_completed: @day }
|
|
40
|
+
else
|
|
41
|
+
message = marked_read ? 'Reading marked read.' : 'Reading marked unread.'
|
|
42
|
+
redirect_to day_path(@day), flash: { b270_interaction_status: message }
|
|
43
|
+
end
|
|
39
44
|
end
|
|
40
45
|
end
|
|
41
46
|
end
|
|
@@ -43,19 +48,33 @@ module Bible270
|
|
|
43
48
|
private
|
|
44
49
|
|
|
45
50
|
def apply_checkoff(requested_state)
|
|
46
|
-
existing =
|
|
51
|
+
existing = find_checkoff
|
|
47
52
|
if requested_state == '1'
|
|
48
53
|
current_reader.ensure_started!
|
|
49
|
-
|
|
50
|
-
true
|
|
54
|
+
[true, existing.nil? && create_checkoff]
|
|
51
55
|
elsif requested_state == '0' || existing
|
|
52
|
-
existing
|
|
53
|
-
false
|
|
56
|
+
[false, existing ? existing.destroy.present? : false]
|
|
54
57
|
else
|
|
55
58
|
current_reader.ensure_started!
|
|
56
|
-
|
|
57
|
-
true
|
|
59
|
+
[true, create_checkoff]
|
|
58
60
|
end
|
|
59
61
|
end
|
|
62
|
+
|
|
63
|
+
def find_checkoff
|
|
64
|
+
Checkoff.uncached do
|
|
65
|
+
Checkoff.find_by(reader_id: current_reader.id, day: @day, track: @track, part: @part)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def create_checkoff
|
|
70
|
+
Checkoff.create!(reader: current_reader, day: @day, track: @track, part: @part)
|
|
71
|
+
true
|
|
72
|
+
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotUnique
|
|
73
|
+
# A repeated or concurrent desired-state request is already successful. Do
|
|
74
|
+
# not hide any other validation failure under the same recovery.
|
|
75
|
+
raise unless find_checkoff
|
|
76
|
+
|
|
77
|
+
false
|
|
78
|
+
end
|
|
60
79
|
end
|
|
61
80
|
end
|
|
@@ -20,6 +20,7 @@ module Bible270
|
|
|
20
20
|
@editing_comment_id = editing_comment_id
|
|
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
|
+
@day_just_completed = flash[:b270_day_just_completed].to_i == @day
|
|
23
24
|
|
|
24
25
|
# Every box on the day, not every track: an Old Testament reading of three
|
|
25
26
|
# chapters is three rows, so counting tracks would call someone finished
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
<% end %>
|
|
5
5
|
|
|
6
6
|
<%= turbo_stream.replace "day_progress_#{@day}" do %>
|
|
7
|
-
<%= render "bible270/days/day_progress", day: @day, reader: @reader
|
|
7
|
+
<%= render "bible270/days/day_progress", day: @day, reader: @reader,
|
|
8
|
+
just_completed: @day_just_completed %>
|
|
8
9
|
<% end %>
|
|
9
10
|
|
|
10
11
|
<%= turbo_stream.replace "completers_#{@day}" do %>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
<%# locals: (day:, reader:) %>
|
|
1
|
+
<%# locals: (day:, reader:, just_completed: false) %>
|
|
2
2
|
<%
|
|
3
3
|
previous_day = day > 1 ? day - 1 : nil
|
|
4
4
|
next_day = day < Bible270::Plan::DAYS ? day + 1 : nil
|
|
5
5
|
total_parts = Bible270::Plan.total_parts(day)
|
|
6
6
|
complete = reader&.day_complete?(day)
|
|
7
|
+
completion_data = just_completed ? { b270_day_just_completed: true, b270_day: day } : {}
|
|
7
8
|
%>
|
|
8
9
|
<div id="day_progress_<%= day %>">
|
|
9
10
|
<div class="b270-daynav">
|
|
@@ -32,7 +33,9 @@
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
<% if complete %>
|
|
35
|
-
|
|
36
|
+
<%= render "bible270/shared/day_completion_dove", day: day if just_completed %>
|
|
37
|
+
<div class="b270-day-complete" role="status" aria-live="polite"
|
|
38
|
+
<%= tag.attributes(data: completion_data) %>>
|
|
36
39
|
<div>
|
|
37
40
|
<strong>Day <%= day %> complete</strong>
|
|
38
41
|
<span>All <%= total_parts %> portions are checked off.</span>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
<% content_for :title, "Day #{@day} — #{Bible270.config.app_name}" %>
|
|
2
2
|
|
|
3
3
|
<div class="b270-day-page">
|
|
4
|
-
<%= render "bible270/days/day_progress", day: @day, reader: current_reader
|
|
4
|
+
<%= render "bible270/days/day_progress", day: @day, reader: current_reader,
|
|
5
|
+
just_completed: @day_just_completed %>
|
|
5
6
|
|
|
6
7
|
<%= render "bible270/shared/interaction_status" %>
|
|
7
8
|
|
|
@@ -33,3 +34,4 @@
|
|
|
33
34
|
</div>
|
|
34
35
|
|
|
35
36
|
<%= render "bible270/shared/interaction_ui" %>
|
|
37
|
+
<%= render "bible270/shared/day_completion_effect", day: @day %>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<%# locals: (day:) %>
|
|
2
|
+
<svg class="b270-completion-dove" data-b270-completion-dove data-b270-day="<%= day %>"
|
|
3
|
+
viewBox="0 0 260 200" aria-hidden="true" focusable="false">
|
|
4
|
+
<defs>
|
|
5
|
+
<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" />
|
|
9
|
+
</radialGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
<g class="b270-dove-silhouette">
|
|
12
|
+
<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
|
+
<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
|
+
<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 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 3 .2 6 .8 8 1.5-2.5 1.3-5.5 2.1-8 2.5-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
|
+
</g>
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<%# locals: (day:) %>
|
|
2
|
+
<template data-b270-completion-dove-template data-b270-day="<%= day %>">
|
|
3
|
+
<%= render "bible270/shared/day_completion_dove", day: day %>
|
|
4
|
+
</template>
|
|
5
|
+
<script nonce="<%= content_security_policy_nonce %>">
|
|
6
|
+
(() => {
|
|
7
|
+
if (window.Bible270DayCompletionEffect) return;
|
|
8
|
+
|
|
9
|
+
function willCompleteDay(form) {
|
|
10
|
+
const desiredState = form.querySelector("input[name='checked']");
|
|
11
|
+
if (!desiredState || desiredState.value !== "1") return false;
|
|
12
|
+
|
|
13
|
+
const unfinished = Array.from(
|
|
14
|
+
document.querySelectorAll("form[data-b270-checkoff='true'] input[name='checked'][value='1']")
|
|
15
|
+
);
|
|
16
|
+
return unfinished.length === 1 && unfinished[0].form === form;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function startDove(day) {
|
|
20
|
+
if (document.querySelector("[data-b270-completion-dove][data-b270-immediate]")) return;
|
|
21
|
+
|
|
22
|
+
const template = document.querySelector("[data-b270-completion-dove-template]");
|
|
23
|
+
if (!template || template.dataset.b270Day !== day) return;
|
|
24
|
+
|
|
25
|
+
const dove = template.content.querySelector("[data-b270-completion-dove]").cloneNode(true);
|
|
26
|
+
dove.dataset.b270Immediate = "true";
|
|
27
|
+
document.body.append(dove);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function removeServerDuplicate() {
|
|
31
|
+
const immediate = document.querySelector("[data-b270-completion-dove][data-b270-immediate]");
|
|
32
|
+
if (!immediate) return;
|
|
33
|
+
|
|
34
|
+
document.querySelectorAll("[data-b270-completion-dove]").forEach((dove) => {
|
|
35
|
+
if (dove !== immediate) dove.remove();
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
document.addEventListener("submit", (event) => {
|
|
40
|
+
const form = event.target.closest("form[data-b270-checkoff='true']");
|
|
41
|
+
const check = form && form.querySelector(".b270-check");
|
|
42
|
+
if (!check) return;
|
|
43
|
+
|
|
44
|
+
const bounds = check.getBoundingClientRect();
|
|
45
|
+
document.documentElement.style.setProperty(
|
|
46
|
+
"--b270-dove-origin-x",
|
|
47
|
+
`${bounds.left + bounds.width / 2}px`
|
|
48
|
+
);
|
|
49
|
+
document.documentElement.style.setProperty(
|
|
50
|
+
"--b270-dove-origin-y",
|
|
51
|
+
`${bounds.top + bounds.height / 2}px`
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (willCompleteDay(form)) startDove(form.action.match(/\/day\/(\d+)/)?.[1] || "");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
document.addEventListener("animationend", (event) => {
|
|
58
|
+
if (event.target.matches("[data-b270-completion-dove]")) event.target.remove();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
new MutationObserver(removeServerDuplicate).observe(document.documentElement, {
|
|
62
|
+
childList: true,
|
|
63
|
+
subtree: true
|
|
64
|
+
});
|
|
65
|
+
window.Bible270DayCompletionEffect = true;
|
|
66
|
+
})();
|
|
67
|
+
</script>
|
|
@@ -177,7 +177,11 @@
|
|
|
177
177
|
staleSessionResponse(event.detail.fetchResponse)) return;
|
|
178
178
|
|
|
179
179
|
delete form.dataset.b270RetriedAfterStaleSession;
|
|
180
|
-
|
|
180
|
+
const completedDay = form.dataset.b270Checkoff === "true" &&
|
|
181
|
+
document.querySelector("[data-b270-day-just-completed='true']");
|
|
182
|
+
if (event.detail.success && completedDay) {
|
|
183
|
+
announce("");
|
|
184
|
+
} else if (event.detail.success) {
|
|
181
185
|
announce(form.dataset.b270Success || "Saved.", "success");
|
|
182
186
|
} else {
|
|
183
187
|
announce(form.dataset.b270Failure || "Couldn’t save that. Try again.", "error");
|
|
@@ -147,6 +147,18 @@
|
|
|
147
147
|
.b270-day-complete strong{font-family:"Spectral",serif;font-size:16px}
|
|
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
|
+
.b270-completion-dove{position:fixed;z-index:1000;top:var(--b270-dove-origin-y,72vh);left:var(--b270-dove-origin-x,50vw);
|
|
151
|
+
width:min(75vw,97.5vh);height:auto;overflow:visible;pointer-events:none;opacity:0;
|
|
152
|
+
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;animation:b270-dove-flight 1.6s cubic-bezier(.18,.72,.28,1) both}
|
|
154
|
+
.b270-dove-silhouette{fill:url(#b270-dove-light);stroke:none}
|
|
155
|
+
@keyframes b270-dove-flight{
|
|
156
|
+
0%{top:var(--b270-dove-origin-y,72vh);left:var(--b270-dove-origin-x,50vw);opacity:.22;visibility:visible;transform:translate(-50%,-50%) scale(.02) rotate(-8deg)}
|
|
157
|
+
8%{top:calc(var(--b270-dove-origin-y,72vh) - 10px);left:var(--b270-dove-origin-x,50vw);opacity:.78;transform:translate(-50%,-50%) scale(.08) rotate(-5deg)}
|
|
158
|
+
64%{top:38vh;left:52vw;opacity:.92;transform:translate(-50%,-50%) scale(1) rotate(4deg)}
|
|
159
|
+
99%{visibility:visible}
|
|
160
|
+
100%{top:-25vh;left:115vw;opacity:0;visibility:hidden;transform:translate(-50%,-50%) scale(1.55) rotate(12deg)}
|
|
161
|
+
}
|
|
150
162
|
|
|
151
163
|
/* readings */
|
|
152
164
|
.b270-readings{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
|
|
@@ -156,7 +168,7 @@
|
|
|
156
168
|
.b270-reading.rest{opacity:.65}
|
|
157
169
|
.b270-toggle{border:none;background:none;padding:0;cursor:pointer}
|
|
158
170
|
.b270-toggle form{margin:0}
|
|
159
|
-
.b270-check{width:26px;height:26px;border-radius:8px;border:2px solid var(--line);flex:none;display:grid;place-items:center;background:#fff;transition:
|
|
171
|
+
.b270-check{width:26px;height:26px;border-radius:8px;border:2px solid var(--line);flex:none;display:grid;place-items:center;background:#fff;transition:background-color .18s,border-color .18s,opacity .18s}
|
|
160
172
|
.b270-check svg{width:15px;height:15px;stroke:#fff;stroke-width:3;fill:none;stroke-linecap:round;stroke-linejoin:round;opacity:0}
|
|
161
173
|
.b270-reading.done.ot .b270-check{background:var(--teal);border-color:var(--teal)}
|
|
162
174
|
.b270-reading.done.nt .b270-check{background:var(--oxblood);border-color:var(--oxblood)}
|
|
@@ -366,6 +378,7 @@
|
|
|
366
378
|
.b270 .b270-day-complete .b270-btn{margin:3px 0 0}
|
|
367
379
|
}
|
|
368
380
|
@media (prefers-reduced-motion:reduce){
|
|
381
|
+
.b270-completion-dove{display:none!important}
|
|
369
382
|
.b270 *,.b270 *::before,.b270 *::after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}
|
|
370
383
|
}
|
|
371
384
|
</style>
|
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
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew vonderLuft
|
|
@@ -138,6 +138,8 @@ files:
|
|
|
138
138
|
- app/views/bible270/sessions/new.html.erb
|
|
139
139
|
- app/views/bible270/shared/_comment_drafts.html.erb
|
|
140
140
|
- app/views/bible270/shared/_comment_notification_choices.html.erb
|
|
141
|
+
- app/views/bible270/shared/_day_completion_dove.html.erb
|
|
142
|
+
- app/views/bible270/shared/_day_completion_effect.html.erb
|
|
141
143
|
- app/views/bible270/shared/_day_index.html.erb
|
|
142
144
|
- app/views/bible270/shared/_footer.html.erb
|
|
143
145
|
- app/views/bible270/shared/_grid_legend.html.erb
|