bible270 0.19.0 → 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 -0
- 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 +45 -0
- data/app/models/bible270/reader.rb +74 -0
- 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/profiles/edit.html.erb +38 -1
- data/app/views/bible270/readers/_next_steps.html.erb +63 -0
- data/app/views/bible270/readers/index.html.erb +4 -7
- data/app/views/bible270/readers/progress.html.erb +10 -0
- 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/_interaction_status.html.erb +2 -0
- data/app/views/bible270/shared/_interaction_ui.html.erb +69 -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 +115 -9
- 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 +14 -1
|
@@ -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}
|
|
@@ -41,6 +42,29 @@
|
|
|
41
42
|
.b270-field{display:grid;gap:5px;font-size:12.5px;color:var(--muted)}
|
|
42
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%}
|
|
43
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)}
|
|
44
68
|
.b270-emailform input[type=submit]{justify-self:start}
|
|
45
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}
|
|
46
70
|
.b270-or::before,.b270-or::after{content:"";flex:1;height:1px;background:var(--line-soft)}
|
|
@@ -54,6 +78,11 @@
|
|
|
54
78
|
.b270-flash{border-radius:10px;padding:11px 15px;margin-bottom:18px;font-size:14px}
|
|
55
79
|
.b270-flash.alert{background:#f6e4e2;color:#7a2b28;border:1px solid #e6c3bf}
|
|
56
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}
|
|
57
86
|
|
|
58
87
|
.b270-eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);font-weight:600;margin:0 0 10px}
|
|
59
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}
|
|
@@ -68,6 +97,32 @@
|
|
|
68
97
|
.b270-progress-actions{display:flex;flex-direction:column;align-items:flex-start;gap:7px;margin:16px 0 0}
|
|
69
98
|
.b270-todaylink{margin-left:16px;font-size:12px;color:var(--muted);text-decoration:none}
|
|
70
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}
|
|
71
126
|
.b270-signedout{color:var(--muted);font-size:14px;margin:4px 0 0}
|
|
72
127
|
|
|
73
128
|
.b270-startdate{margin-top:18px;padding-top:16px;border-top:1px solid var(--line-soft)}
|
|
@@ -91,16 +146,29 @@
|
|
|
91
146
|
.b270-tm .val{color:var(--faint);font-variant-numeric:tabular-nums;min-width:56px;text-align:right}
|
|
92
147
|
|
|
93
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}
|
|
94
155
|
.b270-daynav{display:flex;align-items:center;gap:10px;margin-bottom:12px}
|
|
95
|
-
.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}
|
|
96
157
|
.b270-arrow:hover{border-color:var(--gold);color:var(--gold)}
|
|
97
|
-
.b270-arrow.disabled{opacity:.
|
|
158
|
+
.b270-arrow.disabled{opacity:.45;cursor:not-allowed}
|
|
98
159
|
.b270-dayhead{flex:1}
|
|
99
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}
|
|
100
161
|
.b270-dayhead .date{display:flex;align-items:center;flex-wrap:wrap;gap:9px;color:var(--faint);font-size:13px;margin-top:4px}
|
|
101
162
|
.b270-dayhead .b270-badge{margin-left:0}
|
|
102
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}
|
|
103
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
|
+
|
|
104
172
|
/* readings */
|
|
105
173
|
.b270-readings{background:var(--card);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow);overflow:hidden}
|
|
106
174
|
.b270-reading{display:flex;gap:15px;align-items:center;padding:16px 20px;border-left:4px solid transparent}
|
|
@@ -186,10 +254,11 @@
|
|
|
186
254
|
.b270-likebtn.liked{filter:none;opacity:1}
|
|
187
255
|
.b270-likecount{font-size:11.5px;color:var(--faint)}
|
|
188
256
|
.b270-liketooltip{display:none;position:absolute;z-index:10;top:calc(100% + 6px);left:0;
|
|
189
|
-
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);
|
|
190
258
|
border-radius:7px;background:var(--ink);color:var(--paper);font-size:12px;
|
|
191
259
|
line-height:1.45;box-shadow:0 3px 10px rgba(0,0,0,.14)}
|
|
192
|
-
.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}
|
|
193
262
|
.b270-liker{display:block;white-space:nowrap}
|
|
194
263
|
.b270-creply{margin-top:6px}
|
|
195
264
|
.b270-cbody{margin:3px 0 0;white-space:pre-wrap}
|
|
@@ -204,7 +273,9 @@
|
|
|
204
273
|
.b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
|
|
205
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}
|
|
206
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)}
|
|
207
277
|
.b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
|
|
278
|
+
.b270-form .row select{max-width:100%}
|
|
208
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}
|
|
209
280
|
.b270-btn:hover{background:#3a352b}
|
|
210
281
|
|
|
@@ -216,7 +287,7 @@
|
|
|
216
287
|
.b270-li .sub{font-size:12.5px;color:var(--faint)}
|
|
217
288
|
.b270-li .stat{margin-left:auto;text-align:right}
|
|
218
289
|
.b270-li .stat .big{font-family:"Spectral",serif;font-size:20px;font-weight:600}
|
|
219
|
-
.b270-
|
|
290
|
+
.b270-community-intro{margin:-8px 0 12px;color:var(--muted);font-size:13px}
|
|
220
291
|
|
|
221
292
|
/* calendar grid */
|
|
222
293
|
.b270-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(30px,1fr));gap:6px;margin-top:16px}
|
|
@@ -234,8 +305,18 @@
|
|
|
234
305
|
|
|
235
306
|
/* On the Reflections page each thread carries the day it belongs to, since
|
|
236
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)}
|
|
237
316
|
.b270-thread{margin-bottom:8px}
|
|
317
|
+
.b270-thread-new{border-left:3px solid var(--gold);padding-left:10px}
|
|
238
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}
|
|
239
320
|
.b270-threadday a{color:var(--gold);font-weight:600;text-decoration:none}
|
|
240
321
|
.b270-threadday a:hover{text-decoration:underline}
|
|
241
322
|
/* Scripture references stay undecorated, hover included, so a line of three of
|
|
@@ -244,6 +325,12 @@
|
|
|
244
325
|
.b270-threadday .b270-reflink:hover{color:var(--muted);font-weight:400;text-decoration:none}
|
|
245
326
|
.b270-threadday .b270-reflink:hover{color:var(--ink)}
|
|
246
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)}
|
|
247
334
|
|
|
248
335
|
.b270-footer{margin-top:20px;padding-top:11px;border-top:1px solid var(--line-soft);color:var(--faint);font-size:13px}
|
|
249
336
|
.b270-footer p{margin:0;line-height:1.5}
|
|
@@ -275,5 +362,24 @@
|
|
|
275
362
|
.b270-navlinks a:hover,.b270-nav .b270-linkbtn:hover{background:var(--paper)}
|
|
276
363
|
.b270-nav form{display:block;width:100%}
|
|
277
364
|
}
|
|
278
|
-
@media (max-width:480px){
|
|
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
|
+
}
|
|
279
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
|
|
@@ -55,6 +55,12 @@ Bible270.configure do |config|
|
|
|
55
55
|
# config.start_date = Date.new(2026, 9, 6)
|
|
56
56
|
config.allow_reader_start_date = true
|
|
57
57
|
|
|
58
|
+
# --- Daily reading reminders -----------------------------------------------
|
|
59
|
+
# Readers choose an opt-in time on their profile only when this is enabled.
|
|
60
|
+
# The host app owns scheduling: run `bin/rails bible270:reminders:send` every 15 minutes.
|
|
61
|
+
config.daily_reminders = false
|
|
62
|
+
# config.mailer_host = "example.com" # required for day/profile links in email
|
|
63
|
+
|
|
58
64
|
# --- Reading links ---------------------------------------------------------
|
|
59
65
|
config.bible_version = "NKJV"
|
|
60
66
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'bible270/daily_reminders'
|
|
4
|
+
|
|
5
|
+
namespace :bible270 do
|
|
6
|
+
namespace :reminders do
|
|
7
|
+
desc 'Send Bible270 reminders now due at each opted-in reader time'
|
|
8
|
+
task send: :environment do
|
|
9
|
+
count = Bible270::DailyReminders.deliver
|
|
10
|
+
puts "Sent #{count} daily reading #{'reminder'.pluralize(count)}."
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: bible270
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrew vonderLuft
|
|
@@ -115,6 +115,8 @@ files:
|
|
|
115
115
|
- app/views/bible270/comments/edit.turbo_stream.erb
|
|
116
116
|
- app/views/bible270/comments/index.html.erb
|
|
117
117
|
- app/views/bible270/comments/update.turbo_stream.erb
|
|
118
|
+
- app/views/bible270/days/_completers.html.erb
|
|
119
|
+
- app/views/bible270/days/_day_progress.html.erb
|
|
118
120
|
- app/views/bible270/days/_reading.html.erb
|
|
119
121
|
- app/views/bible270/days/_start_date.html.erb
|
|
120
122
|
- app/views/bible270/days/index.html.erb
|
|
@@ -123,22 +125,29 @@ files:
|
|
|
123
125
|
- app/views/bible270/likes/toggle.turbo_stream.erb
|
|
124
126
|
- app/views/bible270/notice_mailer/broadcast.html.erb
|
|
125
127
|
- app/views/bible270/notice_mailer/broadcast.text.erb
|
|
128
|
+
- app/views/bible270/notice_mailer/daily_reminder.html.erb
|
|
129
|
+
- app/views/bible270/notice_mailer/daily_reminder.text.erb
|
|
126
130
|
- app/views/bible270/notice_mailer/mentioned.html.erb
|
|
127
131
|
- app/views/bible270/notice_mailer/mentioned.text.erb
|
|
128
132
|
- app/views/bible270/notice_mailer/new_reader.html.erb
|
|
129
133
|
- app/views/bible270/notice_mailer/new_reader.text.erb
|
|
130
134
|
- app/views/bible270/profiles/edit.html.erb
|
|
135
|
+
- app/views/bible270/readers/_next_steps.html.erb
|
|
131
136
|
- app/views/bible270/readers/index.html.erb
|
|
132
137
|
- app/views/bible270/readers/progress.html.erb
|
|
133
138
|
- app/views/bible270/readers/show.html.erb
|
|
134
139
|
- app/views/bible270/sessions/new.html.erb
|
|
140
|
+
- app/views/bible270/shared/_comment_drafts.html.erb
|
|
135
141
|
- app/views/bible270/shared/_day_index.html.erb
|
|
136
142
|
- app/views/bible270/shared/_footer.html.erb
|
|
137
143
|
- app/views/bible270/shared/_grid_legend.html.erb
|
|
138
144
|
- app/views/bible270/shared/_header.html.erb
|
|
145
|
+
- app/views/bible270/shared/_interaction_status.html.erb
|
|
146
|
+
- app/views/bible270/shared/_interaction_ui.html.erb
|
|
139
147
|
- app/views/bible270/shared/_nav_links.html.erb
|
|
140
148
|
- app/views/bible270/shared/_passage_source.html.erb
|
|
141
149
|
- app/views/bible270/shared/_progress.html.erb
|
|
150
|
+
- app/views/bible270/shared/_reading_mode.html.erb
|
|
142
151
|
- app/views/bible270/shared/_reflections.html.erb
|
|
143
152
|
- app/views/bible270/shared/_styles.html.erb
|
|
144
153
|
- app/views/bible270/shared/_track_bars.html.erb
|
|
@@ -161,9 +170,12 @@ files:
|
|
|
161
170
|
- db/migrate/20260101000013_create_bible270_likes.rb
|
|
162
171
|
- db/migrate/20260101000014_add_blue_letter_bible_to_bible270_readers.rb
|
|
163
172
|
- db/migrate/20260101000015_add_passage_source_to_bible270_readers.rb
|
|
173
|
+
- db/migrate/20260101000016_add_daily_reminders_to_bible270_readers.rb
|
|
174
|
+
- db/migrate/20260101000017_add_reflections_seen_at_to_bible270_readers.rb
|
|
164
175
|
- lib/bible270.rb
|
|
165
176
|
- lib/bible270/avatars.rb
|
|
166
177
|
- lib/bible270/configuration.rb
|
|
178
|
+
- lib/bible270/daily_reminders.rb
|
|
167
179
|
- lib/bible270/email_sign_in.rb
|
|
168
180
|
- lib/bible270/engine.rb
|
|
169
181
|
- lib/bible270/favicon.rb
|
|
@@ -178,6 +190,7 @@ files:
|
|
|
178
190
|
- lib/generators/bible270/install/templates/bible270.rb.tt
|
|
179
191
|
- lib/generators/bible270/install/templates/omniauth.rb.tt
|
|
180
192
|
- lib/tasks/bible270_migrations.rake
|
|
193
|
+
- lib/tasks/bible270_reminders.rake
|
|
181
194
|
homepage: https://github.com/avonderluft/bible270
|
|
182
195
|
licenses:
|
|
183
196
|
- MIT
|