bible270 0.18.3 → 1.0.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 +16 -0
- data/README.md +67 -4
- data/Rakefile +178 -4
- data/app/controllers/bible270/checkoffs_controller.rb +34 -9
- data/app/controllers/bible270/comments_controller.rb +63 -28
- data/app/controllers/bible270/days_controller.rb +17 -6
- data/app/controllers/bible270/likes_controller.rb +15 -4
- data/app/controllers/bible270/profiles_controller.rb +37 -4
- data/app/controllers/bible270/readers_controller.rb +48 -3
- data/app/helpers/bible270/plan_helper.rb +17 -3
- data/app/mailers/bible270/notice_mailer.rb +22 -1
- data/app/models/bible270/comment.rb +48 -3
- data/app/models/bible270/reader.rb +74 -7
- data/app/views/bible270/checkoffs/toggle.turbo_stream.erb +8 -0
- data/app/views/bible270/comments/_edit_fields.html.erb +3 -1
- data/app/views/bible270/comments/_form.html.erb +10 -1
- data/app/views/bible270/comments/create.turbo_stream.erb +1 -1
- data/app/views/bible270/comments/index.html.erb +61 -10
- data/app/views/bible270/days/_completers.html.erb +12 -0
- data/app/views/bible270/days/_day_progress.html.erb +47 -0
- data/app/views/bible270/days/_reading.html.erb +10 -2
- data/app/views/bible270/days/_start_date.html.erb +1 -1
- data/app/views/bible270/days/index.html.erb +16 -8
- data/app/views/bible270/days/show.html.erb +38 -42
- data/app/views/bible270/likes/_likes.html.erb +5 -1
- data/app/views/bible270/notice_mailer/daily_reminder.html.erb +30 -0
- data/app/views/bible270/notice_mailer/daily_reminder.text.erb +14 -0
- data/app/views/bible270/notice_mailer/mentioned.html.erb +0 -1
- data/app/views/bible270/notice_mailer/mentioned.text.erb +1 -2
- data/app/views/bible270/profiles/edit.html.erb +41 -8
- data/app/views/bible270/readers/_next_steps.html.erb +63 -0
- data/app/views/bible270/readers/index.html.erb +6 -9
- data/app/views/bible270/readers/progress.html.erb +13 -3
- data/app/views/bible270/shared/_comment_drafts.html.erb +169 -0
- data/app/views/bible270/shared/_day_index.html.erb +1 -1
- data/app/views/bible270/shared/_header.html.erb +14 -17
- data/app/views/bible270/shared/_interaction_status.html.erb +2 -0
- data/app/views/bible270/shared/_interaction_ui.html.erb +69 -0
- data/app/views/bible270/shared/_nav_links.html.erb +15 -0
- data/app/views/bible270/shared/_progress.html.erb +24 -6
- data/app/views/bible270/shared/_reading_mode.html.erb +62 -0
- data/app/views/bible270/shared/_styles.html.erb +147 -10
- data/config/routes.rb +4 -3
- data/db/migrate/20260101000016_add_daily_reminders_to_bible270_readers.rb +9 -0
- data/db/migrate/20260101000017_add_reflections_seen_at_to_bible270_readers.rb +7 -0
- data/lib/bible270/configuration.rb +10 -4
- data/lib/bible270/daily_reminders.rb +57 -0
- data/lib/bible270/migration_reconciler.rb +13 -0
- data/lib/bible270/version.rb +1 -1
- data/lib/bible270.rb +1 -0
- data/lib/generators/bible270/install/install_generator.rb +6 -0
- data/lib/generators/bible270/install/templates/bible270.rb.tt +6 -0
- data/lib/tasks/bible270_reminders.rake +13 -0
- metadata +15 -1
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script nonce="<%= content_security_policy_nonce %>">
|
|
2
|
+
(() => {
|
|
3
|
+
if (window.Bible270InteractionUI) {
|
|
4
|
+
window.Bible270InteractionUI.reset();
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function statusRegion() {
|
|
9
|
+
return document.getElementById("b270-interaction-status");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function announce(message, state = "") {
|
|
13
|
+
const region = statusRegion();
|
|
14
|
+
if (!region) return;
|
|
15
|
+
|
|
16
|
+
region.textContent = message || "";
|
|
17
|
+
region.dataset.state = state;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function resetForm(form) {
|
|
21
|
+
form.removeAttribute("aria-busy");
|
|
22
|
+
form.dataset.b270Submitting = "false";
|
|
23
|
+
form.querySelectorAll("[data-b270-disabled-by-submit]").forEach((control) => {
|
|
24
|
+
control.disabled = false;
|
|
25
|
+
delete control.dataset.b270DisabledBySubmit;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function reset() {
|
|
30
|
+
document.querySelectorAll("form[data-b270-submit='true']").forEach(resetForm);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
document.addEventListener("submit", (event) => {
|
|
34
|
+
const form = event.target.closest("form[data-b270-submit='true']");
|
|
35
|
+
if (!form) return;
|
|
36
|
+
if (form.dataset.b270Submitting === "true") {
|
|
37
|
+
event.preventDefault();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
form.dataset.b270Submitting = "true";
|
|
42
|
+
form.setAttribute("aria-busy", "true");
|
|
43
|
+
const submitter = event.submitter || form.querySelector("button[type='submit'],input[type='submit']");
|
|
44
|
+
if (submitter) {
|
|
45
|
+
submitter.disabled = true;
|
|
46
|
+
submitter.dataset.b270DisabledBySubmit = "true";
|
|
47
|
+
}
|
|
48
|
+
announce(form.dataset.b270Pending || "Saving…", "pending");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
document.addEventListener("turbo:submit-end", (event) => {
|
|
52
|
+
const form = event.target;
|
|
53
|
+
if (!form.matches("form[data-b270-submit='true']")) return;
|
|
54
|
+
|
|
55
|
+
resetForm(form);
|
|
56
|
+
if (event.detail.success) {
|
|
57
|
+
announce(form.dataset.b270Success || "Saved.", "success");
|
|
58
|
+
} else {
|
|
59
|
+
announce(form.dataset.b270Failure || "Couldn’t save that. Try again.", "error");
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
window.addEventListener("pageshow", reset);
|
|
64
|
+
document.addEventListener("turbo:load", reset);
|
|
65
|
+
|
|
66
|
+
window.Bible270InteractionUI = { reset };
|
|
67
|
+
reset();
|
|
68
|
+
})();
|
|
69
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%= link_to "Community", community_path %>
|
|
2
|
+
<%# Public, like the community page: anyone may read reflections. %>
|
|
3
|
+
<%= link_to "Reflections", reflections_path %>
|
|
4
|
+
<% if signed_in? %>
|
|
5
|
+
<%= link_to "My Progress", progress_path %>
|
|
6
|
+
<%= link_to "Profile", profile_path %>
|
|
7
|
+
<% if Bible270.config.admin?(current_reader) %><%= link_to "Admin", admin_path %><% end %>
|
|
8
|
+
<%= button_to "Sign out", sign_out_path, method: :delete, class: "b270-linkbtn",
|
|
9
|
+
form: { style: "display:inline" } %>
|
|
10
|
+
<% elsif Bible270.config.any_sign_in_method? %>
|
|
11
|
+
<%# Always route to the sign-in page: it offers email as well as any
|
|
12
|
+
social providers, so nobody is shut out. OmniAuth's own request phase
|
|
13
|
+
rejects GET, so its buttons live on that page as POST forms. %>
|
|
14
|
+
<%= link_to "Sign in", sign_in_path(origin: request.fullpath), class: "b270-signin" %>
|
|
15
|
+
<% end %>
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
panel_today_day = today_day || panel_reader&.today_day
|
|
24
24
|
panel_community_date = community_start_date == :unset ? Bible270.config.start_date : community_start_date
|
|
25
25
|
day_verb = panel_start_day == 1 ? 'Start' : 'Continue'
|
|
26
|
+
panel_behind = panel_today_day.present? && panel_start_day < panel_today_day
|
|
26
27
|
%>
|
|
27
28
|
<div class="b270-panel">
|
|
28
29
|
<% if panel_reader %>
|
|
@@ -34,12 +35,29 @@
|
|
|
34
35
|
|
|
35
36
|
<%= render "bible270/shared/track_bars", reader: panel_reader %>
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<%=
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
<% if panel_behind %>
|
|
39
|
+
<div class="b270-recovery">
|
|
40
|
+
<p class="b270-recovery-heading">Choose the reading that works for you today</p>
|
|
41
|
+
<p class="b270-recovery-copy">Your next unfinished reading is Day <%= panel_start_day %>, while the community is on Day <%= panel_today_day %>. Whichever you choose, your completed readings stay saved.</p>
|
|
42
|
+
<div class="b270-progress-actions b270-recovery-actions">
|
|
43
|
+
<%= link_to "Catch up from Day #{panel_start_day} →", day_path(panel_start_day), class: "b270-btn" %>
|
|
44
|
+
<%= link_to "Read with the community — Day #{panel_today_day}", day_path(panel_today_day), class: "b270-todaylink" %>
|
|
45
|
+
<% if Bible270.config.allow_reader_start_date %>
|
|
46
|
+
<%= button_to "Make Day #{panel_start_day} my new today", restart_schedule_path,
|
|
47
|
+
method: :patch, class: "b270-linkbtn",
|
|
48
|
+
form: { class: "b270-restart-form",
|
|
49
|
+
data: { turbo_confirm: "Make Day #{panel_start_day} today? Your existing check-offs and reflections will not change." } } %>
|
|
50
|
+
<% end %>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
<% else %>
|
|
54
|
+
<div class="b270-progress-actions">
|
|
55
|
+
<%= link_to "#{day_verb} reading — Day #{panel_start_day} →", day_path(panel_start_day), class: "b270-btn" %>
|
|
56
|
+
<% if panel_today_day && panel_today_day != panel_start_day %>
|
|
57
|
+
<%= link_to "Go to today (Day #{panel_today_day})", day_path(panel_today_day), class: "b270-todaylink" %>
|
|
58
|
+
<% end %>
|
|
59
|
+
</div>
|
|
60
|
+
<% end %>
|
|
43
61
|
|
|
44
62
|
<%= render "bible270/days/start_date", reader: panel_reader %>
|
|
45
63
|
<% else %>
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
<script nonce="<%= content_security_policy_nonce %>">
|
|
2
|
+
(() => {
|
|
3
|
+
if (window.Bible270ReadingMode) {
|
|
4
|
+
window.Bible270ReadingMode.scan();
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const storageKey = "bible270:reading-mode";
|
|
9
|
+
|
|
10
|
+
function browserStorage() {
|
|
11
|
+
try { return window.localStorage; } catch (_error) { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const storage = browserStorage();
|
|
15
|
+
let mode = "full";
|
|
16
|
+
if (storage) {
|
|
17
|
+
try { mode = storage.getItem(storageKey) === "compact" ? "compact" : "full"; } catch (_error) { /* Use full mode. */ }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function remember(nextMode) {
|
|
21
|
+
mode = nextMode;
|
|
22
|
+
if (!storage) return;
|
|
23
|
+
try { storage.setItem(storageKey, nextMode); } catch (_error) { /* Keep this page's mode. */ }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function apply(page) {
|
|
27
|
+
const compact = mode === "compact";
|
|
28
|
+
page.dataset.b270ReadingMode = mode;
|
|
29
|
+
page.querySelectorAll("[data-b270-compact-toggle]").forEach((button) => {
|
|
30
|
+
button.setAttribute("aria-pressed", String(compact));
|
|
31
|
+
});
|
|
32
|
+
page.querySelectorAll("[data-b270-compact-hide]").forEach((element) => {
|
|
33
|
+
element.hidden = compact;
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const options = page.querySelector("[data-b270-reading-options]");
|
|
37
|
+
if (options) options.hidden = false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function scan() {
|
|
41
|
+
document.querySelectorAll("[data-b270-day-page]").forEach(apply);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
document.addEventListener("click", (event) => {
|
|
45
|
+
const button = event.target.closest?.("[data-b270-compact-toggle]");
|
|
46
|
+
if (!button) return;
|
|
47
|
+
|
|
48
|
+
const page = button.closest("[data-b270-day-page]");
|
|
49
|
+
if (!page) return;
|
|
50
|
+
|
|
51
|
+
remember(page.dataset.b270ReadingMode === "compact" ? "full" : "compact");
|
|
52
|
+
apply(page);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
document.addEventListener("turbo:load", scan);
|
|
56
|
+
window.addEventListener("pageshow", scan);
|
|
57
|
+
new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true });
|
|
58
|
+
|
|
59
|
+
window.Bible270ReadingMode = { scan };
|
|
60
|
+
scan();
|
|
61
|
+
})();
|
|
62
|
+
</script>
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<style>
|
|
2
2
|
.b270{
|
|
3
|
-
--paper:#ece6da; --card:#f4efe6; --ink:#231f18; --muted:#
|
|
4
|
-
--line:#d7cfbf; --line-soft:#e2dbcd; --teal:#2f6a67; --oxblood:#8a2f3b; --gold:#
|
|
5
|
-
--gold-lite:#
|
|
3
|
+
--paper:#ece6da; --card:#f4efe6; --ink:#231f18; --muted:#5d564c; --faint:#6e665b;
|
|
4
|
+
--line:#d7cfbf; --line-soft:#e2dbcd; --teal:#2f6a67; --oxblood:#8a2f3b; --gold:#80631d;
|
|
5
|
+
--gold-lite:#80631d; --shadow:0 1px 0 rgba(35,31,24,.04),0 10px 30px -18px rgba(35,31,24,.35);
|
|
6
6
|
margin:0; background:radial-gradient(120% 80% at 100% 0%,rgba(165,129,44,.06),transparent 55%),var(--paper);
|
|
7
7
|
color:var(--ink); font-family:"Inter",system-ui,sans-serif; line-height:1.2; -webkit-font-smoothing:antialiased;
|
|
8
8
|
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
colouring an <a> (.b270-btn, .b270-cell, .b270-arrow ...) must be
|
|
12
12
|
qualified with .b270 to outrank it, or the link inherits body colour. */
|
|
13
13
|
.b270 a{color:inherit}
|
|
14
|
+
.b270 :where(a,button,input,select,textarea,summary):focus-visible{outline:2px solid var(--gold);outline-offset:2px}
|
|
14
15
|
.b270-wrap{max-width:820px;margin:0 auto;padding:clamp(14px,3.5vw,36px) clamp(14px,4vw,40px) 64px}
|
|
15
16
|
|
|
16
17
|
.b270-topbar{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding-bottom:20px;border-bottom:1px solid var(--line-soft);margin-bottom:26px}
|
|
@@ -22,7 +23,9 @@
|
|
|
22
23
|
.b270-mark svg,.b270-mark img{display:block;border-radius:8px}
|
|
23
24
|
.b270-title{font-family:"Spectral",Georgia,serif;font-weight:600;font-size:22px;text-decoration:none}
|
|
24
25
|
.b270-tagline{font-size:12px;color:var(--faint);letter-spacing:.04em}
|
|
25
|
-
.b270-nav{display:flex;align-items:center;
|
|
26
|
+
.b270-nav{display:flex;align-items:center;font-size:14px}
|
|
27
|
+
.b270-desktopnav{display:flex;align-items:center;gap:16px}
|
|
28
|
+
.b270-navmenu{display:none}
|
|
26
29
|
.b270-nav a{color:var(--muted);text-decoration:none}
|
|
27
30
|
.b270-nav a:hover{color:var(--gold)}
|
|
28
31
|
/* button_to renders a form; make it a flex item so the button sits on the same
|
|
@@ -39,6 +42,29 @@
|
|
|
39
42
|
.b270-field{display:grid;gap:5px;font-size:12.5px;color:var(--muted)}
|
|
40
43
|
.b270-field input{font:inherit;font-size:15px;padding:10px 12px;border:1px solid var(--line);border-radius:9px;background:#fff;color:var(--ink);width:100%}
|
|
41
44
|
.b270-field input:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 3px rgba(165,129,44,.13)}
|
|
45
|
+
.b270-reminder-row{display:flex;align-items:center;gap:12px;width:100%;flex-wrap:wrap}
|
|
46
|
+
.b270-reminder-optin,.b270-reminder-time{display:inline-flex;align-items:center;gap:7px;color:var(--muted);white-space:nowrap}
|
|
47
|
+
.b270-reminder-optin{position:relative;min-width:0}
|
|
48
|
+
.b270-reminder-time{margin-left:auto;gap:3px;font-size:12px}
|
|
49
|
+
.b270-time-control{position:relative;display:inline-grid}
|
|
50
|
+
.b270-reminder-tooltip{visibility:hidden;opacity:0;position:absolute;z-index:20;right:0;bottom:calc(100% + 7px);
|
|
51
|
+
width:240px;max-width:calc(100vw - 28px);padding:7px 9px;border:1px solid var(--line-soft);border-radius:7px;
|
|
52
|
+
background:var(--ink);color:var(--paper);font-size:12px;line-height:1.4;
|
|
53
|
+
white-space:normal;box-shadow:0 3px 10px rgba(0,0,0,.14);pointer-events:none;
|
|
54
|
+
transition:opacity .12s}
|
|
55
|
+
.b270-time-control:hover .b270-reminder-tooltip,
|
|
56
|
+
.b270-time-control:focus-within .b270-reminder-tooltip,
|
|
57
|
+
.b270-reminder-optin:hover .b270-reminder-tooltip,
|
|
58
|
+
.b270-reminder-optin:focus-within .b270-reminder-tooltip,
|
|
59
|
+
.b270-reminder-optin:active .b270-reminder-tooltip,
|
|
60
|
+
.b270-time-control:active .b270-reminder-tooltip{visibility:visible;opacity:1}
|
|
61
|
+
.b270-field .b270-reminder-optin input[type=checkbox]{width:auto;margin:0;flex:none}
|
|
62
|
+
.b270-time-control{grid-template-columns:48px 5px 48px;align-items:center;gap:2px}
|
|
63
|
+
.b270-field .b270-time-select{width:48px;height:30px;margin:0;padding:4px 2px;border:1px solid var(--line);
|
|
64
|
+
border-radius:7px;background:#fff;color:var(--ink);font:12px/16px system-ui,sans-serif;
|
|
65
|
+
font-variant-numeric:tabular-nums;text-align:center}
|
|
66
|
+
.b270-time-select:focus{outline:none;border-color:var(--gold);box-shadow:0 0 0 3px rgba(165,129,44,.13)}
|
|
67
|
+
.b270-time-separator{display:grid;place-items:center;height:30px;font:12px/30px system-ui,sans-serif;color:var(--muted)}
|
|
42
68
|
.b270-emailform input[type=submit]{justify-self:start}
|
|
43
69
|
.b270-or{display:flex;align-items:center;gap:12px;margin:22px 0 18px;color:var(--faint);font-size:12px;letter-spacing:.1em;text-transform:uppercase}
|
|
44
70
|
.b270-or::before,.b270-or::after{content:"";flex:1;height:1px;background:var(--line-soft)}
|
|
@@ -52,6 +78,11 @@
|
|
|
52
78
|
.b270-flash{border-radius:10px;padding:11px 15px;margin-bottom:18px;font-size:14px}
|
|
53
79
|
.b270-flash.alert{background:#f6e4e2;color:#7a2b28;border:1px solid #e6c3bf}
|
|
54
80
|
.b270-flash.notice{background:#e7efe6;color:#2f5d34;border:1px solid #c6dcc6}
|
|
81
|
+
.b270-interaction-status{margin:8px 0 12px;padding:8px 11px;border-radius:8px;background:rgba(47,106,103,.08);color:var(--teal);font-size:12.5px}
|
|
82
|
+
.b270-interaction-status:empty{display:none}
|
|
83
|
+
.b270-interaction-status[data-state="pending"]{color:var(--muted)}
|
|
84
|
+
.b270-interaction-status[data-state="error"]{background:#f6e4e2;color:#7a2b28}
|
|
85
|
+
.b270 form[aria-busy="true"] [data-b270-disabled-by-submit]{cursor:wait;opacity:.55}
|
|
55
86
|
|
|
56
87
|
.b270-eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);font-weight:600;margin:0 0 10px}
|
|
57
88
|
.b270 h1{font-family:"Spectral",serif;font-weight:600;font-size:clamp(22px,6vw,33px);line-height:1.03;letter-spacing:-.01em;margin:0 0 6px}
|
|
@@ -63,6 +94,35 @@
|
|
|
63
94
|
.b270-bignum .n{font-family:"Spectral",serif;font-size:38px;font-weight:600;line-height:1}
|
|
64
95
|
.b270-bignum .of{color:var(--muted);font-size:15px}
|
|
65
96
|
.b270-bignum .pct{margin-left:auto;color:var(--gold);font-weight:600;font-variant-numeric:tabular-nums}
|
|
97
|
+
.b270-progress-actions{display:flex;flex-direction:column;align-items:flex-start;gap:7px;margin:16px 0 0}
|
|
98
|
+
.b270-todaylink{margin-left:16px;font-size:12px;color:var(--muted);text-decoration:none}
|
|
99
|
+
.b270-todaylink:hover{color:var(--gold);text-decoration:underline}
|
|
100
|
+
.b270-recovery{margin-top:16px;padding:14px 16px;border:1px solid var(--line-soft);border-radius:10px;background:rgba(165,129,44,.06)}
|
|
101
|
+
.b270-recovery-heading{margin:0;font-family:"Spectral",serif;font-size:17px;font-weight:600}
|
|
102
|
+
.b270-recovery-copy{margin:4px 0 0;color:var(--muted);font-size:13px;line-height:1.45}
|
|
103
|
+
.b270-recovery-actions{margin-top:12px}
|
|
104
|
+
.b270-recovery-actions .b270-todaylink{margin-left:0}
|
|
105
|
+
.b270-restart-form{margin:0}
|
|
106
|
+
.b270-recovery-actions .b270-linkbtn{margin:2px 0 0;color:var(--muted)}
|
|
107
|
+
.b270-next-steps>.b270-subhead{margin-bottom:2px}
|
|
108
|
+
.b270-next-section{padding-top:15px;margin-top:15px;border-top:1px solid var(--line-soft)}
|
|
109
|
+
.b270-next-section h3{margin:0 0 9px;font-size:13px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
|
110
|
+
.b270-task-list{list-style:none;margin:0;padding:0;display:grid;gap:8px}
|
|
111
|
+
.b270-task{display:flex;align-items:center;justify-content:space-between;gap:14px;padding:10px 12px;border:1px solid var(--line-soft);border-radius:9px;background:rgba(255,255,255,.35)}
|
|
112
|
+
.b270-task-body{display:grid;gap:3px;min-width:0}
|
|
113
|
+
.b270-task-body strong{font-size:14px}
|
|
114
|
+
.b270-task-body span{color:var(--muted);font-size:12px;line-height:1.4}
|
|
115
|
+
.b270-task-body span a{color:var(--muted);text-decoration:underline;text-decoration-color:var(--line)}
|
|
116
|
+
.b270-task-body span a:hover{color:var(--gold);text-decoration-color:var(--gold)}
|
|
117
|
+
.b270 .b270-task-link{flex:none;color:var(--gold);font-size:12.5px;font-weight:600;text-decoration:none;white-space:nowrap}
|
|
118
|
+
.b270-task-link:hover{text-decoration:underline}
|
|
119
|
+
.b270-next-note{margin:0;color:var(--faint);font-size:12.5px;line-height:1.45}
|
|
120
|
+
.b270-task-list+.b270-next-note{margin-top:8px}
|
|
121
|
+
.b270-week-count{margin:0 0 7px;color:var(--muted);font-size:13px}
|
|
122
|
+
.b270-week-count strong{color:var(--ink)}
|
|
123
|
+
.b270-week-progress{display:block;width:100%;height:8px;accent-color:var(--teal)}
|
|
124
|
+
.b270-milestone{margin:0 0 3px;color:var(--muted);font-size:13px}
|
|
125
|
+
.b270-milestone strong{color:var(--ink);font-family:"Spectral",serif;font-size:17px}
|
|
66
126
|
.b270-signedout{color:var(--muted);font-size:14px;margin:4px 0 0}
|
|
67
127
|
|
|
68
128
|
.b270-startdate{margin-top:18px;padding-top:16px;border-top:1px solid var(--line-soft)}
|
|
@@ -86,16 +146,29 @@
|
|
|
86
146
|
.b270-tm .val{color:var(--faint);font-variant-numeric:tabular-nums;min-width:56px;text-align:right}
|
|
87
147
|
|
|
88
148
|
/* day nav */
|
|
149
|
+
.b270-day-page [hidden]{display:none!important}
|
|
150
|
+
.b270-reading-options{display:flex;justify-content:flex-end;margin:-4px 0 10px}
|
|
151
|
+
.b270-reading-options .b270-linkbtn{margin:0;color:var(--muted)}
|
|
152
|
+
.b270-reading-mode-toggle[aria-pressed="true"]{color:var(--gold);font-weight:600}
|
|
153
|
+
.b270-day-page[data-b270-reading-mode="compact"] [data-b270-compact-hide]{display:none!important}
|
|
154
|
+
.b270-day-page[data-b270-reading-mode="compact"] .b270-day-complete{justify-content:flex-end;padding:0;border:0;background:transparent;box-shadow:none}
|
|
89
155
|
.b270-daynav{display:flex;align-items:center;gap:10px;margin-bottom:12px}
|
|
90
|
-
.b270 .b270-arrow{width:38px;height:38px;border-radius:10px;border:1px solid var(--line);background:var(--card);cursor:pointer;font-size:17px;color:var(--muted);display:grid;place-items:center;text-decoration:none}
|
|
156
|
+
.b270 .b270-arrow{width:38px;height:38px;padding:0;border-radius:10px;border:1px solid var(--line);background:var(--card);cursor:pointer;font:inherit;font-size:17px;line-height:1;color:var(--muted);display:grid;place-items:center;text-decoration:none}
|
|
91
157
|
.b270-arrow:hover{border-color:var(--gold);color:var(--gold)}
|
|
92
|
-
.b270-arrow.disabled{opacity:.
|
|
158
|
+
.b270-arrow.disabled{opacity:.45;cursor:not-allowed}
|
|
93
159
|
.b270-dayhead{flex:1}
|
|
94
160
|
.b270-dayhead .k{display:flex;align-items:center;flex-wrap:wrap;gap:9px;font-family:"Spectral",serif;font-size:24px;font-weight:600;line-height:1}
|
|
95
161
|
.b270-dayhead .date{display:flex;align-items:center;flex-wrap:wrap;gap:9px;color:var(--faint);font-size:13px;margin-top:4px}
|
|
96
162
|
.b270-dayhead .b270-badge{margin-left:0}
|
|
97
163
|
.b270-badge{display:inline-block;vertical-align:middle;line-height:1;font-size:10.5px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:#fff;background:var(--gold-lite);padding:5px 8px 4px;border-radius:20px;margin-left:9px}
|
|
98
164
|
|
|
165
|
+
.b270-day-complete{display:flex;align-items:center;gap:14px;margin:0 0 14px;padding:11px 13px;
|
|
166
|
+
border:1px solid var(--line);border-radius:10px;background:var(--card);box-shadow:var(--shadow)}
|
|
167
|
+
.b270-day-complete div{display:flex;flex-direction:column;gap:2px;min-width:0}
|
|
168
|
+
.b270-day-complete strong{font-family:"Spectral",serif;font-size:16px}
|
|
169
|
+
.b270-day-complete span{font-size:12px;color:var(--muted)}
|
|
170
|
+
.b270 .b270-day-complete .b270-btn{flex:none;margin:0 0 0 auto}
|
|
171
|
+
|
|
99
172
|
/* readings */
|
|
100
173
|
.b270-readings{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
|
|
101
174
|
.b270-reading{display:flex;gap:15px;align-items:center;padding:16px 20px;border-left:4px solid transparent}
|
|
@@ -181,10 +254,11 @@
|
|
|
181
254
|
.b270-likebtn.liked{filter:none;opacity:1}
|
|
182
255
|
.b270-likecount{font-size:11.5px;color:var(--faint)}
|
|
183
256
|
.b270-liketooltip{display:none;position:absolute;z-index:10;top:calc(100% + 6px);left:0;
|
|
184
|
-
min-width:max-content;padding:6px 9px;border:1px solid var(--line-soft);
|
|
257
|
+
min-width:max-content;max-width:calc(100vw - 28px);padding:6px 9px;border:1px solid var(--line-soft);
|
|
185
258
|
border-radius:7px;background:var(--ink);color:var(--paper);font-size:12px;
|
|
186
259
|
line-height:1.45;box-shadow:0 3px 10px rgba(0,0,0,.14)}
|
|
187
|
-
.b270-likes:hover .b270-liketooltip,.b270-likes:focus-within .b270-liketooltip
|
|
260
|
+
.b270-likes:hover .b270-liketooltip,.b270-likes:focus-within .b270-liketooltip,
|
|
261
|
+
.b270-likes:active .b270-liketooltip{display:block}
|
|
188
262
|
.b270-liker{display:block;white-space:nowrap}
|
|
189
263
|
.b270-creply{margin-top:6px}
|
|
190
264
|
.b270-cbody{margin:3px 0 0;white-space:pre-wrap}
|
|
@@ -199,7 +273,9 @@
|
|
|
199
273
|
.b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
|
|
200
274
|
.b270-input{width:100%;padding:9px 11px;border:1px solid var(--line);border-radius:9px;background:#fff;font:inherit;font-size:14px;margin-bottom:8px}
|
|
201
275
|
.b270-form textarea{width:100%;font:inherit;font-size:15px;border:1px solid var(--line);border-radius:10px;padding:11px 13px;background:#fff;resize:vertical;min-height:76px}
|
|
276
|
+
.b270-draft-status{min-height:16px;margin:4px 0 -4px;font-size:11.5px;color:var(--faint)}
|
|
202
277
|
.b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
|
|
278
|
+
.b270-form .row select{max-width:100%}
|
|
203
279
|
.b270 .b270-btn{border:none;border-radius:10px;padding:8px 16px;margin-top:4px;font:inherit;font-weight:600;font-size:14px;cursor:pointer;background:var(--ink);color:var(--paper);text-decoration:none;display:inline-block}
|
|
204
280
|
.b270-btn:hover{background:#3a352b}
|
|
205
281
|
|
|
@@ -211,7 +287,7 @@
|
|
|
211
287
|
.b270-li .sub{font-size:12.5px;color:var(--faint)}
|
|
212
288
|
.b270-li .stat{margin-left:auto;text-align:right}
|
|
213
289
|
.b270-li .stat .big{font-family:"Spectral",serif;font-size:20px;font-weight:600}
|
|
214
|
-
.b270-
|
|
290
|
+
.b270-community-intro{margin:-8px 0 12px;color:var(--muted);font-size:13px}
|
|
215
291
|
|
|
216
292
|
/* calendar grid */
|
|
217
293
|
.b270-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(30px,1fr));gap:6px;margin-top:16px}
|
|
@@ -229,8 +305,18 @@
|
|
|
229
305
|
|
|
230
306
|
/* On the Reflections page each thread carries the day it belongs to, since
|
|
231
307
|
there is no day heading above it. */
|
|
308
|
+
.b270-reflection-tools{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin:30px 0 14px}
|
|
309
|
+
.b270-reflection-tools .b270-section-h{margin:0}
|
|
310
|
+
.b270-reflection-count{margin:3px 0 0;font-size:12px;color:var(--faint)}
|
|
311
|
+
.b270-reflection-filters{display:flex;align-items:flex-end;justify-content:flex-end;gap:8px;flex-wrap:wrap}
|
|
312
|
+
.b270-reflection-filters label{display:grid;gap:3px;font-size:11.5px;color:var(--muted)}
|
|
313
|
+
.b270-reflection-filters select{min-width:112px;max-width:180px;padding:7px 25px 7px 9px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink);font:inherit;font-size:12.5px}
|
|
314
|
+
.b270 .b270-reflection-filters .b270-btn{margin:0;padding:8px 13px;font-size:12.5px}
|
|
315
|
+
.b270 .b270-filter-clear{align-self:center;font-size:12px;color:var(--muted)}
|
|
232
316
|
.b270-thread{margin-bottom:8px}
|
|
317
|
+
.b270-thread-new{border-left:3px solid var(--gold);padding-left:10px}
|
|
233
318
|
.b270-threadday{margin:0;font-size:12px;letter-spacing:.04em;color:var(--faint)}
|
|
319
|
+
.b270-new-reflection{display:inline-block;margin-left:7px;padding:3px 6px 2px;border-radius:10px;background:rgba(128,99,29,.12);color:var(--gold);font-size:10px;font-weight:600;letter-spacing:.04em;text-transform:uppercase}
|
|
234
320
|
.b270-threadday a{color:var(--gold);font-weight:600;text-decoration:none}
|
|
235
321
|
.b270-threadday a:hover{text-decoration:underline}
|
|
236
322
|
/* Scripture references stay undecorated, hover included, so a line of three of
|
|
@@ -239,10 +325,61 @@
|
|
|
239
325
|
.b270-threadday .b270-reflink:hover{color:var(--muted);font-weight:400;text-decoration:none}
|
|
240
326
|
.b270-threadday .b270-reflink:hover{color:var(--ink)}
|
|
241
327
|
.b270-gridnote{margin:10px 0 0;font-size:11.5px;color:var(--faint)}
|
|
328
|
+
.b270-pagination{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:12px;margin:20px 0;color:var(--faint);font-size:12.5px}
|
|
329
|
+
.b270-pagination a{color:var(--gold);font-weight:600;text-decoration:none}
|
|
330
|
+
.b270-pagination a:last-child{justify-self:end}
|
|
331
|
+
.b270-pagination span{grid-column:2;text-align:center}
|
|
332
|
+
.b270-filter-empty{margin-top:12px}
|
|
333
|
+
.b270-filter-empty p{margin:0 0 8px;color:var(--muted)}
|
|
242
334
|
|
|
243
335
|
.b270-footer{margin-top:20px;padding-top:11px;border-top:1px solid var(--line-soft);color:var(--faint);font-size:13px}
|
|
244
336
|
.b270-footer p{margin:0;line-height:1.5}
|
|
245
337
|
details.b270-index summary{cursor:pointer;font-family:"Spectral",serif;font-size:18px;font-weight:600;list-style:none;margin-top:30px}
|
|
246
338
|
details.b270-index summary::-webkit-details-marker{display:none}
|
|
247
|
-
@media (max-width:
|
|
339
|
+
@media (max-width:620px){
|
|
340
|
+
.b270-topbar{flex-wrap:nowrap;align-items:center}
|
|
341
|
+
.b270-brand{min-width:0}
|
|
342
|
+
.b270-brandtext{min-width:0}
|
|
343
|
+
.b270-tagline{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
344
|
+
.b270-nav{position:relative;flex:none}
|
|
345
|
+
.b270-desktopnav{display:none}
|
|
346
|
+
.b270-navmenu{display:block}
|
|
347
|
+
.b270-navtoggle{display:flex;align-items:center;justify-content:center;width:40px;height:38px;
|
|
348
|
+
list-style:none;cursor:pointer;padding:0;border:1px solid var(--line);
|
|
349
|
+
border-radius:8px;background:var(--card)}
|
|
350
|
+
.b270-navtoggle::-webkit-details-marker{display:none}
|
|
351
|
+
.b270-navtoggle:focus-visible{outline:2px solid var(--gold);outline-offset:2px}
|
|
352
|
+
.b270-menuicon{display:flex;flex-direction:column;gap:4px}
|
|
353
|
+
.b270-menuicon span{display:block;width:18px;height:2px;border-radius:2px;background:var(--ink)}
|
|
354
|
+
.b270-navmenu[open] .b270-menuicon span{background:var(--gold)}
|
|
355
|
+
.b270-navmenu:not([open])>.b270-navlinks{display:none}
|
|
356
|
+
.b270-navlinks{position:absolute;z-index:20;right:0;top:calc(100% + 7px);min-width:180px;
|
|
357
|
+
flex-direction:column;align-items:stretch;gap:0;padding:6px;
|
|
358
|
+
border:1px solid var(--line);border-radius:10px;background:var(--card);
|
|
359
|
+
box-shadow:var(--shadow)}
|
|
360
|
+
.b270-navlinks a,.b270-nav .b270-linkbtn{display:block;width:100%;padding:9px 10px;
|
|
361
|
+
border-radius:6px;text-align:left;white-space:nowrap}
|
|
362
|
+
.b270-navlinks a:hover,.b270-nav .b270-linkbtn:hover{background:var(--paper)}
|
|
363
|
+
.b270-nav form{display:block;width:100%}
|
|
364
|
+
}
|
|
365
|
+
@media (max-width:480px){
|
|
366
|
+
.b270-tm{grid-template-columns:104px 1fr auto}
|
|
367
|
+
.b270-avatarrow{align-items:flex-start;flex-wrap:wrap}
|
|
368
|
+
.b270-form .row{align-items:stretch;flex-direction:column}
|
|
369
|
+
.b270-form .row select{width:100%}
|
|
370
|
+
.b270-form .row .b270-btn{align-self:flex-start}
|
|
371
|
+
.b270-reading{padding:14px 15px;gap:12px}
|
|
372
|
+
.b270-reading.multi .b270-cat,.b270-reading.multi .b270-parts-meta{margin-left:38px}
|
|
373
|
+
.b270-part{gap:12px}
|
|
374
|
+
.b270-task{align-items:flex-start;flex-direction:column;gap:7px}
|
|
375
|
+
.b270-day-complete{align-items:flex-start;flex-direction:column}
|
|
376
|
+
.b270-reflection-tools{align-items:stretch;flex-direction:column;gap:10px}
|
|
377
|
+
.b270-reflection-filters{align-items:stretch;justify-content:flex-start}
|
|
378
|
+
.b270-reflection-filters label{flex:1;min-width:120px}
|
|
379
|
+
.b270-reflection-filters select{width:100%;max-width:none}
|
|
380
|
+
.b270 .b270-day-complete .b270-btn{margin:3px 0 0}
|
|
381
|
+
}
|
|
382
|
+
@media (prefers-reduced-motion:reduce){
|
|
383
|
+
.b270 *,.b270 *::before,.b270 *::after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}
|
|
384
|
+
}
|
|
248
385
|
</style>
|
data/config/routes.rb
CHANGED
|
@@ -45,12 +45,13 @@ Bible270::Engine.routes.draw do
|
|
|
45
45
|
get 'community', to: 'readers#index', as: :community
|
|
46
46
|
get 'readers/:id', to: 'readers#show', as: :reader
|
|
47
47
|
|
|
48
|
-
patch 'start-date',
|
|
49
|
-
delete 'start-date',
|
|
48
|
+
patch 'start-date', to: 'readers#update_start_date', as: :start_date
|
|
49
|
+
delete 'start-date', to: 'readers#clear_start_date', as: :clear_start_date
|
|
50
|
+
patch 'schedule/restart', to: 'readers#restart_schedule', as: :restart_schedule
|
|
50
51
|
|
|
51
52
|
# Built-in OmniAuth sign-in. The request phase (POST <mount>/auth/:provider)
|
|
52
53
|
# is handled by the OmniAuth middleware in the host app, not by these routes.
|
|
53
|
-
get 'sign_in', to: 'sessions#new',
|
|
54
|
+
get 'sign_in', to: 'sessions#new', as: :sign_in
|
|
54
55
|
|
|
55
56
|
# Email (magic link) sign-in, for readers without a social account.
|
|
56
57
|
post 'sign_in/email', to: 'sessions#email_link', as: :email_sign_in_link
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddDailyRemindersToBible270Readers < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
add_column :bible270_readers, :daily_reminders, :boolean, default: false, null: false
|
|
6
|
+
add_column :bible270_readers, :daily_reminder_time, :string, default: '08:00', null: false
|
|
7
|
+
add_column :bible270_readers, :last_daily_reminder_sent_on, :date
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -234,6 +234,7 @@ module Bible270
|
|
|
234
234
|
attr_accessor :email_sign_in_deliver_later
|
|
235
235
|
|
|
236
236
|
def email_sign_in? = !!@email_sign_in
|
|
237
|
+
def daily_reminders? = !!@daily_reminders
|
|
237
238
|
|
|
238
239
|
# Reserved domains from RFC 2606 — a From: address at one of these will fail
|
|
239
240
|
# SPF for your real domain, and receiving servers routinely drop it without
|
|
@@ -241,9 +242,9 @@ module Bible270
|
|
|
241
242
|
# changed, but nothing was checking that it had been.
|
|
242
243
|
PLACEHOLDER_DOMAINS = %w[example.com example.org example.net example.edu invalid localhost].freeze
|
|
243
244
|
|
|
244
|
-
# Why
|
|
245
|
+
# Why reader-facing mail is likely to vanish, or nil if it looks fine.
|
|
245
246
|
def mailer_from_problem
|
|
246
|
-
return nil unless email_sign_in?
|
|
247
|
+
return nil unless email_sign_in? || daily_reminders?
|
|
247
248
|
|
|
248
249
|
address = mailer_from.to_s.strip
|
|
249
250
|
return 'config.mailer_from is blank' if address.empty?
|
|
@@ -292,6 +293,10 @@ module Bible270
|
|
|
292
293
|
# from, so without this the notice names the reader but cannot link to them.
|
|
293
294
|
attr_accessor :mailer_host
|
|
294
295
|
|
|
296
|
+
# Allow readers to opt in to a daily email with that date's readings. The host
|
|
297
|
+
# application owns scheduling the bible270:reminders:send task.
|
|
298
|
+
attr_accessor :daily_reminders
|
|
299
|
+
|
|
295
300
|
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, :header_mark, :remember_for, :require_sign_in_to_participate, :mention_notifications
|
|
296
301
|
|
|
297
302
|
# Public labels.
|
|
@@ -331,8 +336,8 @@ module Bible270
|
|
|
331
336
|
|
|
332
337
|
# Email a reader when someone writes "@them" in a reflection. Readers can opt
|
|
333
338
|
# out individually on their profile; this switches the whole feature off.
|
|
334
|
-
# How many
|
|
335
|
-
#
|
|
339
|
+
# How many threads each Reflections page shows. A thread resurfaces when someone
|
|
340
|
+
# replies to it, so this is a count of conversations, not of messages.
|
|
336
341
|
attr_accessor :reflections_page_size
|
|
337
342
|
|
|
338
343
|
def remember_signed_in?
|
|
@@ -381,6 +386,7 @@ module Bible270
|
|
|
381
386
|
@registration_notice_emails = []
|
|
382
387
|
@registration_notice_deliver_later = false
|
|
383
388
|
@mailer_host = nil
|
|
389
|
+
@daily_reminders = false
|
|
384
390
|
@admin_emails = []
|
|
385
391
|
@admin_resolver = nil
|
|
386
392
|
@email_sign_in_deliver_later = false
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'bible270/plan'
|
|
5
|
+
|
|
6
|
+
module Bible270
|
|
7
|
+
# Delivers one reminder for each reader whose dated plan has an unfinished
|
|
8
|
+
# reading and whose chosen local reminder time has arrived. Scheduling belongs
|
|
9
|
+
# to the host application; repeated runs on the same local date are safe.
|
|
10
|
+
module DailyReminders
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def deliver(at: Time.now)
|
|
14
|
+
return 0 unless Bible270.config.daily_reminders?
|
|
15
|
+
|
|
16
|
+
unless Reader.daily_reminder_columns?
|
|
17
|
+
Rails.logger.error(
|
|
18
|
+
'[bible270] daily reminders unavailable: run bible270:install:migrations and db:migrate'
|
|
19
|
+
)
|
|
20
|
+
return 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
local_time = local_time_for(at)
|
|
24
|
+
return 0 unless local_time.respond_to?(:to_date)
|
|
25
|
+
|
|
26
|
+
date = local_time.to_date
|
|
27
|
+
sent = 0
|
|
28
|
+
Reader.daily_reminder_recipients(on: date).find_each do |reader|
|
|
29
|
+
next unless reader.daily_reminder_due_at?(local_time)
|
|
30
|
+
|
|
31
|
+
day = Plan.day_for(date, reader.effective_start_date, clamp: false)
|
|
32
|
+
next unless Plan.valid_day?(day)
|
|
33
|
+
next if reader.day_complete?(day)
|
|
34
|
+
|
|
35
|
+
delivery = NoticeMailer.daily_reminder(reader_id: reader.id, day: day, on: date)
|
|
36
|
+
next unless delivery.message.perform_deliveries
|
|
37
|
+
|
|
38
|
+
delivery.deliver_now
|
|
39
|
+
reader.update_column(:last_daily_reminder_sent_on, date)
|
|
40
|
+
sent += 1
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
Rails.logger.error(
|
|
43
|
+
"[bible270] daily reminder for reader #{reader.id} failed: #{e.class}: #{e.message}"
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
sent
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def local_time_for(at)
|
|
50
|
+
return nil if at.nil?
|
|
51
|
+
|
|
52
|
+
Bible270.local_time(at)
|
|
53
|
+
rescue StandardError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -25,6 +25,8 @@ module Bible270
|
|
|
25
25
|
'CreateBible270Likes' => :check_likes,
|
|
26
26
|
'AddBlueLetterBibleToBible270Readers' => :check_blue_letter_bible,
|
|
27
27
|
'AddPassageSourceToBible270Readers' => :check_passage_source,
|
|
28
|
+
'AddDailyRemindersToBible270Readers' => :check_daily_reminders,
|
|
29
|
+
'AddReflectionsSeenAtToBible270Readers' => :check_reflections_seen_at,
|
|
28
30
|
'CreateActiveStorageTables' => :check_active_storage
|
|
29
31
|
}.freeze
|
|
30
32
|
|
|
@@ -177,6 +179,17 @@ module Bible270
|
|
|
177
179
|
table_missing(:bible270_readers, columns: %i[passage_source])
|
|
178
180
|
end
|
|
179
181
|
|
|
182
|
+
def check_daily_reminders
|
|
183
|
+
table_missing(
|
|
184
|
+
:bible270_readers,
|
|
185
|
+
columns: %i[daily_reminders daily_reminder_time last_daily_reminder_sent_on]
|
|
186
|
+
)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def check_reflections_seen_at
|
|
190
|
+
table_missing(:bible270_readers, columns: %i[reflections_seen_at])
|
|
191
|
+
end
|
|
192
|
+
|
|
180
193
|
# The Bible270 installer can install Active Storage for reader avatars. A
|
|
181
194
|
# copied database can therefore have the same timestamp mismatch for Rails'
|
|
182
195
|
# generated Active Storage migration as it has for the engine migrations.
|
data/lib/bible270/version.rb
CHANGED
data/lib/bible270.rb
CHANGED
|
@@ -512,6 +512,12 @@ module Bible270
|
|
|
512
512
|
# config.start_date = Date.new(#{Date.today.year}, 1, 1)
|
|
513
513
|
config.allow_reader_start_date = true
|
|
514
514
|
|
|
515
|
+
# --- Daily reading reminders -----------------------------------------------
|
|
516
|
+
# Readers choose an opt-in time only when enabled. This application owns
|
|
517
|
+
# scheduling: run `bin/rails bible270:reminders:send` every 15 minutes.
|
|
518
|
+
config.daily_reminders = false
|
|
519
|
+
# config.mailer_host = 'example.com' # links back to the day and profile
|
|
520
|
+
|
|
515
521
|
# --- Reading links --------------------------------------------------------
|
|
516
522
|
config.bible_version = 'NKJV'
|
|
517
523
|
end
|