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
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
· <em>the 270 days have elapsed</em>
|
|
14
14
|
<% elsif (off = reader.days_off_pace) %>
|
|
15
15
|
<% if off > 0 %>
|
|
16
|
-
· <em
|
|
16
|
+
· <em>reading at your own pace</em>
|
|
17
17
|
<% elsif off < 0 %>
|
|
18
18
|
· <em><%= -off %> day<%= "s" unless off == -1 %> ahead</em>
|
|
19
19
|
<% else %>
|
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
<% content_for :title, Bible270.config.app_name %>
|
|
2
2
|
<p class="b270-eyebrow"><%= Bible270.config.tagline %></p>
|
|
3
3
|
<h1><%= Bible270.config.app_name %> <em>— read God's whole story</em></h1>
|
|
4
|
-
<p class="lede">Daily passages from the Old Testament, New Testament, and Psalms or Proverbs — portions sized to take about the same time to read. Check off what you read; your place is saved for you, and you can
|
|
4
|
+
<p class="lede">Daily passages from the Old Testament, New Testament, and Psalms or Proverbs — portions sized to take about the same time to read. Check off what you read; your place is saved for you, and you can share reflections and encourage one another along the way.</p>
|
|
5
5
|
|
|
6
6
|
<%= render "bible270/shared/progress" %>
|
|
7
7
|
|
|
8
8
|
<% if @readers.any? %>
|
|
9
|
-
|
|
10
|
-
<
|
|
9
|
+
<h2 class="b270-section-h">Reading together</h2>
|
|
10
|
+
<p class="b270-community-intro">
|
|
11
|
+
Showing <%= [@readers.size, 5].min %> of <%= pluralize(@readers.size, "reader") %>, ordered by recent
|
|
12
|
+
checkoffs and reflections; new readers by join date.
|
|
13
|
+
</p>
|
|
11
14
|
<div class="b270-list">
|
|
12
|
-
<% @readers.first(
|
|
15
|
+
<% @readers.first(5).each do |r| %>
|
|
13
16
|
<div class="b270-li">
|
|
14
|
-
<span class="rank"><%= i + 1 %></span>
|
|
15
17
|
<%= b270_avatar(r) %>
|
|
16
18
|
<div>
|
|
17
19
|
<div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
|
|
18
20
|
<% day_word = @days_completed[r.id] == 1 ? "day" : "days" %>
|
|
19
|
-
|
|
21
|
+
<% activity = @reader_activity.fetch(r.id) %>
|
|
22
|
+
<% activity_verb = { reading: "read", reflection: "reflected", joined: "joined" }.fetch(activity[:kind]) %>
|
|
23
|
+
<div class="sub">
|
|
24
|
+
<%= @days_completed[r.id] %> <%= day_word %> read ·
|
|
25
|
+
<span class="b270-reader-activity"><%= activity_verb %>
|
|
26
|
+
<time datetime="<%= activity[:at].iso8601 %>" title="<%= b270_datetime(activity[:at]) %>"><%= time_ago_in_words(activity[:at]) %> ago</time>
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
20
29
|
</div>
|
|
21
|
-
<div class="stat"><span class="big"><%= (@days_completed[r.id].to_f / Bible270::Plan::DAYS * 100).round %>%</span></div>
|
|
22
30
|
</div>
|
|
23
31
|
<% end %>
|
|
24
32
|
</div>
|
|
25
|
-
<p style="margin:12px 0 0"><%= link_to "
|
|
33
|
+
<p style="margin:12px 0 0"><%= link_to "Meet the community →", community_path %></p>
|
|
26
34
|
<% end %>
|
|
@@ -1,50 +1,46 @@
|
|
|
1
1
|
<% content_for :title, "Day #{@day} — #{Bible270.config.app_name}" %>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<% if current_reader.today?(@day) %><span class="b270-badge">Today</span><% end %>
|
|
10
|
-
</div>
|
|
11
|
-
<% end %>
|
|
2
|
+
|
|
3
|
+
<div class="b270-day-page" data-b270-day-page data-b270-reading-mode="full">
|
|
4
|
+
<%= render "bible270/days/day_progress", day: @day, reader: current_reader %>
|
|
5
|
+
|
|
6
|
+
<div class="b270-reading-options" data-b270-reading-options hidden>
|
|
7
|
+
<button type="button" class="b270-linkbtn b270-reading-mode-toggle"
|
|
8
|
+
data-b270-compact-toggle aria-pressed="false">Compact reading</button>
|
|
12
9
|
</div>
|
|
13
|
-
<%= link_to "›", (@next_day ? day_path(@next_day) : "#"), class: "b270-arrow #{'disabled' unless @next_day}", "aria-label": "Next day" %>
|
|
14
|
-
</div>
|
|
15
10
|
|
|
16
|
-
|
|
17
|
-
<%= render partial: "bible270/days/reading", collection: %w[ot nt pp],
|
|
18
|
-
as: :track, locals: { day: @day, readings: @readings, reader_tracks: @reader_tracks } %>
|
|
19
|
-
</div>
|
|
11
|
+
<%= render "bible270/shared/interaction_status" %>
|
|
20
12
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
<div class="b270-completers" id="completers_<%= @day %>">
|
|
26
|
-
<% if @completers.any? %>
|
|
27
|
-
<span>Finished this day:</span>
|
|
28
|
-
<% @completers.first(12).each { |r| %><%= link_to b270_avatar(r), reader_path(r), title: r.display_name %><% } %>
|
|
29
|
-
<% if @completers.size > 12 %><span><%= @completers.size - 12 %> more</span><% end %>
|
|
30
|
-
<% else %>
|
|
31
|
-
<span>No one has finished this day yet — be the first.</span>
|
|
32
|
-
<% end %>
|
|
33
|
-
</div>
|
|
13
|
+
<div id="readings" class="b270-readings">
|
|
14
|
+
<%= render partial: "bible270/days/reading", collection: %w[ot nt pp],
|
|
15
|
+
as: :track, locals: { day: @day, readings: @readings, reader_tracks: @reader_tracks } %>
|
|
16
|
+
</div>
|
|
34
17
|
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
<% if @comments.any? %>
|
|
38
|
-
<%= render partial: "bible270/comments/comment", collection: @comments, as: :comment %>
|
|
39
|
-
<% else %>
|
|
40
|
-
<p style="color:var(--faint)" id="no_comments">No reflections yet. Share the first.</p>
|
|
18
|
+
<% unless signed_in? %>
|
|
19
|
+
<p class="b270-signin-hint" style="margin-top:12px"><%= link_to "Sign in", sign_in_path(origin: request.fullpath) %> to check off these readings.</p>
|
|
41
20
|
<% end %>
|
|
42
|
-
</div>
|
|
43
21
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
22
|
+
<div class="b270-day-community" data-b270-compact-hide>
|
|
23
|
+
<%= render "bible270/days/completers", day: @day, completers: @completers %>
|
|
24
|
+
|
|
25
|
+
<h2 class="b270-section-h">Reflections</h2>
|
|
26
|
+
<div id="comments">
|
|
27
|
+
<% if @comments.any? %>
|
|
28
|
+
<%= render partial: "bible270/comments/comment", collection: @comments, as: :comment %>
|
|
29
|
+
<% else %>
|
|
30
|
+
<p style="color:var(--faint)" id="no_comments">No reflections yet. Share the first.</p>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<% if signed_in? %>
|
|
35
|
+
<div style="margin-top:18px">
|
|
36
|
+
<%= render "bible270/comments/form", day: @day, comment: @new_comment %>
|
|
37
|
+
</div>
|
|
38
|
+
<%= render "bible270/shared/comment_drafts" %>
|
|
39
|
+
<% else %>
|
|
40
|
+
<p class="b270-signin-hint" style="margin-top:14px"><%= link_to "Sign in", sign_in_path(origin: request.fullpath) %> to leave a reflection.</p>
|
|
41
|
+
<% end %>
|
|
47
42
|
</div>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<%= render "bible270/shared/reading_mode" %>
|
|
46
|
+
<%= render "bible270/shared/interaction_ui" %>
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
<% if signed_in? %>
|
|
17
17
|
<%= button_to like_comment_path(comment), method: :post, class: "b270-likebtn#{' liked' if likes.any?}",
|
|
18
|
-
|
|
18
|
+
params: { liked: liked ? "0" : "1" },
|
|
19
|
+
form: { style: "display:inline",
|
|
20
|
+
data: { turbo_stream: true, b270_submit: true, b270_pending: "Saving like…",
|
|
21
|
+
b270_success: (liked ? "Like removed." : "Reflection liked."),
|
|
22
|
+
b270_failure: "Couldn’t update that like. Try again." } },
|
|
19
23
|
aria: {
|
|
20
24
|
label: liked ? "Take back your like" : "Like this reflection",
|
|
21
25
|
describedby: (tooltip_id if likes.any?)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<body style="margin:0;padding:24px;background:#ece6da;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#231f18">
|
|
4
|
+
<div style="max-width:520px;margin:0 auto;background:#f4efe6;border:1px solid #d7cfbf;border-radius:14px;padding:28px">
|
|
5
|
+
<h1 style="margin:0 0 6px;font-family:Georgia,serif;font-size:20px;font-weight:600">
|
|
6
|
+
Day <%= @day %> is ready, <%= @reader.first_name.presence || @reader.display_name %>
|
|
7
|
+
</h1>
|
|
8
|
+
<p style="margin:0 0 18px;font-size:14px;color:#6d6558">
|
|
9
|
+
<%= @on.strftime("%A, %B %-d, %Y") %> · <%= @app_name %>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<ul style="margin:0;padding-left:22px;font-size:15px;line-height:1.7">
|
|
13
|
+
<% @readings.each do |track, reference| %>
|
|
14
|
+
<li><strong><%= Bible270::Plan::TRACKS.fetch(track).fetch(:label) %>:</strong> <%= reference %></li>
|
|
15
|
+
<% end %>
|
|
16
|
+
</ul>
|
|
17
|
+
|
|
18
|
+
<% if @day_url %>
|
|
19
|
+
<p style="margin:20px 0 0">
|
|
20
|
+
<a href="<%= @day_url %>" style="display:inline-block;background:#231f18;color:#ece6da;text-decoration:none;padding:10px 18px;border-radius:10px;font-weight:600;font-size:14px">Open day <%= @day %></a>
|
|
21
|
+
</p>
|
|
22
|
+
<% end %>
|
|
23
|
+
|
|
24
|
+
<p style="margin:22px 0 0;font-size:12px;color:#9a9082">
|
|
25
|
+
You opted in to daily reading reminders.
|
|
26
|
+
<% if @profile_url %><a href="<%= @profile_url %>" style="color:#6d6558">Change this in your profile</a>.<% end %>
|
|
27
|
+
</p>
|
|
28
|
+
</div>
|
|
29
|
+
</body>
|
|
30
|
+
</html>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Day <%= @day %> is ready, <%= @reader.first_name.presence || @reader.display_name %>.
|
|
2
|
+
<%= @on.strftime("%A, %B %-d, %Y") %> · <%= @app_name %>
|
|
3
|
+
|
|
4
|
+
<% @readings.each do |track, reference| %>
|
|
5
|
+
<%= Bible270::Plan::TRACKS.fetch(track).fetch(:label) %>: <%= reference %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% if @day_url %>
|
|
8
|
+
Open day <%= @day %>: <%= @day_url %>
|
|
9
|
+
<% end %>
|
|
10
|
+
--
|
|
11
|
+
You opted in to daily reading reminders.
|
|
12
|
+
<% if @profile_url %>
|
|
13
|
+
Change this in your profile: <%= @profile_url %>
|
|
14
|
+
<% end %>
|
|
@@ -8,5 +8,4 @@ Read it and reply: <%= @day_url %>
|
|
|
8
8
|
You can find it on day <%= @comment.day %> of <%= @app_name %>.
|
|
9
9
|
<% end %>
|
|
10
10
|
--
|
|
11
|
-
You are getting this because someone wrote your name in a reflection.
|
|
12
|
-
turn these off on your profile page.
|
|
11
|
+
You are getting this because someone wrote your name in a reflection.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<% content_for :title, "
|
|
2
|
-
<p class="b270-eyebrow">
|
|
3
|
-
<h1>Your
|
|
1
|
+
<% content_for :title, "Profile — #{Bible270.config.app_name}" %>
|
|
2
|
+
<p class="b270-eyebrow">Profile</p>
|
|
3
|
+
<h1>Your details</h1>
|
|
4
4
|
<p class="lede">This is how you appear beside your reflections and on the community page.</p>
|
|
5
5
|
|
|
6
6
|
<div class="b270-panel">
|
|
@@ -19,10 +19,6 @@
|
|
|
19
19
|
<% end %>
|
|
20
20
|
</div>
|
|
21
21
|
|
|
22
|
-
<label class="b270-field" style="flex-direction:row;align-items:center;gap:10px">
|
|
23
|
-
<%= check_box_tag :notify_on_mention, "1", current_reader.notify_on_mention %>
|
|
24
|
-
<span style="margin:0">Email me when someone mentions me in a reflection</span>
|
|
25
|
-
</label>
|
|
26
22
|
|
|
27
23
|
<label class="b270-field">
|
|
28
24
|
<span>First name</span>
|
|
@@ -41,6 +37,43 @@
|
|
|
41
37
|
</label>
|
|
42
38
|
<%= render "bible270/shared/passage_source", reader: current_reader %>
|
|
43
39
|
|
|
40
|
+
<% if @daily_reminders_available %>
|
|
41
|
+
<% reminder_hour, reminder_minute = @daily_reminder_time.to_s.split(":", 2) %>
|
|
42
|
+
<div class="b270-field">
|
|
43
|
+
<span>Daily reading reminder</span>
|
|
44
|
+
<div class="b270-reminder-row">
|
|
45
|
+
<label class="b270-reminder-optin">
|
|
46
|
+
<%= check_box_tag :daily_reminders, "1", @daily_reminders,
|
|
47
|
+
aria: { describedby: "daily-reminder-optin-hint" } %>
|
|
48
|
+
<span>Email me each day's readings</span>
|
|
49
|
+
<span class="b270-reminder-tooltip" id="daily-reminder-optin-hint" role="tooltip">
|
|
50
|
+
Uses the plan's configured time zone. You can turn reminders off here at any time.
|
|
51
|
+
</span>
|
|
52
|
+
</label>
|
|
53
|
+
<div class="b270-reminder-time">
|
|
54
|
+
<span>at</span>
|
|
55
|
+
<span class="b270-time-control" role="group" aria-label="Reminder time"
|
|
56
|
+
aria-describedby="daily-reminder-time-hint">
|
|
57
|
+
<%= select_tag :daily_reminder_hour,
|
|
58
|
+
options_for_select((0..23).map { |hour| format("%02d", hour) }, reminder_hour),
|
|
59
|
+
class: "b270-time-select", required: true, aria: { label: "Hour" } %>
|
|
60
|
+
<span class="b270-time-separator" aria-hidden="true">:</span>
|
|
61
|
+
<%= select_tag :daily_reminder_minute,
|
|
62
|
+
options_for_select((0..59).map { |minute| format("%02d", minute) }, reminder_minute),
|
|
63
|
+
class: "b270-time-select", required: true, aria: { label: "Minute" } %>
|
|
64
|
+
<span class="b270-reminder-tooltip" id="daily-reminder-time-hint" role="tooltip">
|
|
65
|
+
Uses the plan's configured time zone. You can turn reminders off here at any time.
|
|
66
|
+
</span>
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
<% elsif Bible270.config.daily_reminders? %>
|
|
72
|
+
<p class="b270-flash alert" style="margin:0">
|
|
73
|
+
Daily reminders are temporarily unavailable while this installation finishes a database update.
|
|
74
|
+
</p>
|
|
75
|
+
<% end %>
|
|
76
|
+
|
|
44
77
|
<%= submit_tag "Save", class: "b270-btn" %>
|
|
45
78
|
<% end %>
|
|
46
79
|
|
|
@@ -55,4 +88,4 @@
|
|
|
55
88
|
<% end %>
|
|
56
89
|
</div>
|
|
57
90
|
|
|
58
|
-
<p style="margin-top:18px"><%= link_to "← Back to your progress",
|
|
91
|
+
<p style="margin-top:18px"><%= link_to "← Back to your progress", progress_path %></p>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<%# locals: (reader:, partial_day_tasks:, remaining_partial_days:, week_days:, week_completed:, next_milestone:, days_to_milestone:) %>
|
|
2
|
+
<div class="b270-panel b270-next-steps">
|
|
3
|
+
<h2 class="b270-subhead">Your next steps</h2>
|
|
4
|
+
|
|
5
|
+
<section class="b270-next-section" aria-labelledby="finish-started-heading">
|
|
6
|
+
<h3 id="finish-started-heading">Finish what you started</h3>
|
|
7
|
+
<% if partial_day_tasks.any? %>
|
|
8
|
+
<ul class="b270-task-list">
|
|
9
|
+
<% partial_day_tasks.each do |task| %>
|
|
10
|
+
<% day = task[:day] %>
|
|
11
|
+
<% first_track = task[:remaining].first&.fetch(:track) %>
|
|
12
|
+
<li class="b270-task">
|
|
13
|
+
<div class="b270-task-body">
|
|
14
|
+
<strong><%= link_to "Day #{day}", day_path(day, anchor: "reading_#{day}_#{first_track}") %></strong>
|
|
15
|
+
<span>
|
|
16
|
+
Still to read:
|
|
17
|
+
<%= safe_join(task[:remaining].map { |part|
|
|
18
|
+
link_to(part[:reference], day_path(day, anchor: "reading_#{day}_#{part[:track]}"))
|
|
19
|
+
}, " · ") %>
|
|
20
|
+
</span>
|
|
21
|
+
</div>
|
|
22
|
+
<%= link_to "Continue →", day_path(day, anchor: "reading_#{day}_#{first_track}"),
|
|
23
|
+
class: "b270-task-link" %>
|
|
24
|
+
</li>
|
|
25
|
+
<% end %>
|
|
26
|
+
</ul>
|
|
27
|
+
<% if remaining_partial_days.positive? %>
|
|
28
|
+
<p class="b270-next-note"><%= remaining_partial_days %> more partially read
|
|
29
|
+
<%= "day".pluralize(remaining_partial_days) %> are highlighted in the grid below.</p>
|
|
30
|
+
<% end %>
|
|
31
|
+
<% else %>
|
|
32
|
+
<p class="b270-next-note">No partially read days. Your next reading is ready above.</p>
|
|
33
|
+
<% end %>
|
|
34
|
+
</section>
|
|
35
|
+
|
|
36
|
+
<section class="b270-next-section" aria-labelledby="this-week-heading">
|
|
37
|
+
<h3 id="this-week-heading">This week</h3>
|
|
38
|
+
<% if week_days.any? %>
|
|
39
|
+
<p class="b270-week-count"><strong><%= week_completed %> of <%= week_days.size %></strong>
|
|
40
|
+
scheduled <%= "day".pluralize(week_days.size) %> complete so far this week</p>
|
|
41
|
+
<progress class="b270-week-progress" value="<%= week_completed %>" max="<%= week_days.size %>"
|
|
42
|
+
aria-label="<%= week_completed %> of <%= week_days.size %> scheduled days complete this week"></progress>
|
|
43
|
+
<% elsif reader.dated? %>
|
|
44
|
+
<p class="b270-next-note">No plan days are scheduled so far this week.</p>
|
|
45
|
+
<% elsif Bible270.config.allow_reader_start_date %>
|
|
46
|
+
<p class="b270-next-note">Set a start date to see your weekly progress.</p>
|
|
47
|
+
<% else %>
|
|
48
|
+
<p class="b270-next-note">This plan is not tied to calendar dates.</p>
|
|
49
|
+
<% end %>
|
|
50
|
+
</section>
|
|
51
|
+
|
|
52
|
+
<section class="b270-next-section" aria-labelledby="milestone-heading">
|
|
53
|
+
<h3 id="milestone-heading">Next milestone</h3>
|
|
54
|
+
<% if next_milestone %>
|
|
55
|
+
<% milestone_day, milestone_label = next_milestone %>
|
|
56
|
+
<p class="b270-milestone"><strong><%= milestone_day %> days</strong> · <%= milestone_label %></p>
|
|
57
|
+
<p class="b270-next-note"><%= days_to_milestone %>
|
|
58
|
+
<%= "day".pluralize(days_to_milestone) %> to go.</p>
|
|
59
|
+
<% else %>
|
|
60
|
+
<p class="b270-milestone"><strong>All <%= Bible270::Plan::DAYS %> days complete.</strong></p>
|
|
61
|
+
<% end %>
|
|
62
|
+
</section>
|
|
63
|
+
</div>
|
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
<% content_for :title, "Community — #{Bible270.config.app_name}" %>
|
|
2
|
-
<p class="b270-eyebrow">
|
|
3
|
-
<h1>
|
|
4
|
-
<p class="lede">
|
|
2
|
+
<p class="b270-eyebrow">Community</p>
|
|
3
|
+
<h1>Reading together</h1>
|
|
4
|
+
<p class="lede">A community walking through the Word, sharing reflections and encouraging one another.</p>
|
|
5
5
|
|
|
6
6
|
<% if @readers.any? %>
|
|
7
7
|
<div class="b270-list">
|
|
8
|
-
<% @readers.
|
|
8
|
+
<% @readers.each do |r| %>
|
|
9
9
|
<div class="b270-li">
|
|
10
|
-
<span class="rank"><%= i + 1 %></span>
|
|
11
10
|
<%= b270_avatar(r, size: 72) %>
|
|
12
11
|
<div>
|
|
13
12
|
<div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
<div class="stat">
|
|
17
|
-
<span class="big"><%= (@days_completed[r.id].to_f / Bible270::Plan::DAYS * 100).round %>%</span>
|
|
13
|
+
<% day_word = @days_completed[r.id] == 1 ? "day" : "days" %>
|
|
14
|
+
<div class="sub"><%= @days_completed[r.id] %> <%= day_word %> read · joined <%= Bible270.local_time(r.created_at).strftime("%b %Y") %></div>
|
|
18
15
|
</div>
|
|
19
16
|
</div>
|
|
20
17
|
<% end %>
|
|
@@ -1,11 +1,21 @@
|
|
|
1
|
-
<% content_for :title, "
|
|
1
|
+
<% content_for :title, "My Progress — #{Bible270.config.app_name}" %>
|
|
2
2
|
<% content_for :hide_day_index, true %><%# the panel is followed by its own grid below %>
|
|
3
3
|
|
|
4
|
-
<p class="b270-eyebrow">
|
|
5
|
-
<h1
|
|
4
|
+
<p class="b270-eyebrow">My Progress</p>
|
|
5
|
+
<h1>Your reading journey</h1>
|
|
6
6
|
|
|
7
7
|
<%= render "bible270/shared/progress" %>
|
|
8
8
|
|
|
9
|
+
<% if @reader %>
|
|
10
|
+
<%= render "bible270/readers/next_steps", reader: @reader,
|
|
11
|
+
partial_day_tasks: @partial_day_tasks,
|
|
12
|
+
remaining_partial_days: @remaining_partial_days,
|
|
13
|
+
week_days: @week_days,
|
|
14
|
+
week_completed: @week_completed,
|
|
15
|
+
next_milestone: @next_milestone,
|
|
16
|
+
days_to_milestone: @days_to_milestone %>
|
|
17
|
+
<% end %>
|
|
18
|
+
|
|
9
19
|
<div class="b270-panel">
|
|
10
20
|
<%= render "bible270/shared/reflections", comments: @recent_comments,
|
|
11
21
|
title: "Your recent reflections",
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
<script nonce="<%= content_security_policy_nonce %>">
|
|
2
|
+
(() => {
|
|
3
|
+
if (window.Bible270Drafts) {
|
|
4
|
+
window.Bible270Drafts.scan();
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const maxAge = 30 * 24 * 60 * 60 * 1000;
|
|
9
|
+
const pendingKey = "bible270:draft:pending";
|
|
10
|
+
|
|
11
|
+
function browserStorage(name) {
|
|
12
|
+
try { return window[name]; } catch (_error) { return null; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const local = browserStorage("localStorage");
|
|
16
|
+
const session = browserStorage("sessionStorage");
|
|
17
|
+
const storage = {
|
|
18
|
+
get(area, key) {
|
|
19
|
+
if (!area) return null;
|
|
20
|
+
try { return area.getItem(key); } catch (_error) { return null; }
|
|
21
|
+
},
|
|
22
|
+
set(area, key, value) {
|
|
23
|
+
if (!area) return;
|
|
24
|
+
try { area.setItem(key, value); } catch (_error) { /* Drafts degrade safely. */ }
|
|
25
|
+
},
|
|
26
|
+
remove(area, key) {
|
|
27
|
+
if (!area) return;
|
|
28
|
+
try { area.removeItem(key); } catch (_error) { /* Drafts degrade safely. */ }
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function readDraft(key) {
|
|
33
|
+
const raw = storage.get(local, key);
|
|
34
|
+
if (!raw) return null;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const draft = JSON.parse(raw);
|
|
38
|
+
if (typeof draft.body !== "string" || typeof draft.savedAt !== "number" ||
|
|
39
|
+
Date.now() - draft.savedAt > maxAge) {
|
|
40
|
+
storage.remove(local, key);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return draft;
|
|
44
|
+
} catch (_error) {
|
|
45
|
+
storage.remove(local, key);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function setStatus(form, message) {
|
|
51
|
+
const status = form.querySelector("[data-b270-draft-status]");
|
|
52
|
+
if (status) status.textContent = message;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function save(form) {
|
|
56
|
+
const key = form.dataset.b270DraftKey;
|
|
57
|
+
const body = form.querySelector("textarea[name='comment[body]']");
|
|
58
|
+
const track = form.querySelector("select[name='comment[track]']");
|
|
59
|
+
if (!key || !body) return;
|
|
60
|
+
|
|
61
|
+
if (!body.value.trim()) {
|
|
62
|
+
storage.remove(local, key);
|
|
63
|
+
setStatus(form, "");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const trackValue = track ? track.value : "";
|
|
68
|
+
const current = readDraft(key);
|
|
69
|
+
if (current && current.body === body.value && current.track === trackValue) {
|
|
70
|
+
setStatus(form, "Draft saved in this browser.");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const draft = {
|
|
75
|
+
body: body.value,
|
|
76
|
+
track: trackValue,
|
|
77
|
+
savedAt: Date.now(),
|
|
78
|
+
revision: `${Date.now().toString(36)}:${Math.random().toString(36).slice(2)}`
|
|
79
|
+
};
|
|
80
|
+
storage.set(local, key, JSON.stringify(draft));
|
|
81
|
+
setStatus(form, "Draft saved in this browser.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function settlePending(key = null) {
|
|
85
|
+
const raw = storage.get(session, pendingKey);
|
|
86
|
+
if (!raw) return;
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const pending = JSON.parse(raw);
|
|
90
|
+
if (key && pending.key !== key) return;
|
|
91
|
+
const current = readDraft(pending.key);
|
|
92
|
+
if (current && current.revision === pending.revision) {
|
|
93
|
+
storage.remove(local, pending.key);
|
|
94
|
+
}
|
|
95
|
+
} catch (_error) {
|
|
96
|
+
// A malformed pending marker should never prevent the form from working.
|
|
97
|
+
}
|
|
98
|
+
storage.remove(session, pendingKey);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rememberSubmission(form) {
|
|
102
|
+
save(form);
|
|
103
|
+
const key = form.dataset.b270DraftKey;
|
|
104
|
+
const draft = readDraft(key);
|
|
105
|
+
if (draft) {
|
|
106
|
+
storage.set(session, pendingKey, JSON.stringify({ key, revision: draft.revision }));
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function restore(form) {
|
|
111
|
+
if (form.dataset.b270DraftErrors === "true") return;
|
|
112
|
+
|
|
113
|
+
const draft = readDraft(form.dataset.b270DraftKey);
|
|
114
|
+
if (!draft) return;
|
|
115
|
+
|
|
116
|
+
const body = form.querySelector("textarea[name='comment[body]']");
|
|
117
|
+
const track = form.querySelector("select[name='comment[track]']");
|
|
118
|
+
if (body) body.value = draft.body;
|
|
119
|
+
if (track && Array.from(track.options).some((option) => option.value === draft.track)) {
|
|
120
|
+
track.value = draft.track;
|
|
121
|
+
}
|
|
122
|
+
setStatus(form, "Draft restored from this browser.");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function initialize(form) {
|
|
126
|
+
if (form.dataset.b270DraftReady === "true") return;
|
|
127
|
+
form.dataset.b270DraftReady = "true";
|
|
128
|
+
|
|
129
|
+
if (form.dataset.b270DraftPosted === "true") {
|
|
130
|
+
settlePending(form.dataset.b270DraftKey);
|
|
131
|
+
} else {
|
|
132
|
+
restore(form);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
form.addEventListener("input", () => save(form));
|
|
136
|
+
form.addEventListener("change", () => save(form));
|
|
137
|
+
form.addEventListener("submit", () => rememberSubmission(form));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function scan() {
|
|
141
|
+
document.querySelectorAll("form[data-b270-draft='true']").forEach(initialize);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (/^#comment-\d+$/.test(window.location.hash)) {
|
|
145
|
+
settlePending();
|
|
146
|
+
} else {
|
|
147
|
+
storage.remove(session, pendingKey);
|
|
148
|
+
}
|
|
149
|
+
scan();
|
|
150
|
+
|
|
151
|
+
new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true });
|
|
152
|
+
window.addEventListener("pageshow", scan);
|
|
153
|
+
window.addEventListener("pagehide", () => {
|
|
154
|
+
document.querySelectorAll("form[data-b270-draft='true']").forEach(save);
|
|
155
|
+
});
|
|
156
|
+
document.addEventListener("turbo:submit-end", (event) => {
|
|
157
|
+
const form = event.target;
|
|
158
|
+
if (!form.matches("form[data-b270-draft='true']")) return;
|
|
159
|
+
|
|
160
|
+
if (event.detail.success) {
|
|
161
|
+
settlePending(form.dataset.b270DraftKey);
|
|
162
|
+
} else {
|
|
163
|
+
storage.remove(session, pendingKey);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
window.Bible270Drafts = { scan };
|
|
168
|
+
})();
|
|
169
|
+
</script>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<details class="b270-index">
|
|
3
3
|
<summary>All <%= Bible270::Plan::DAYS %> days</summary>
|
|
4
4
|
<div class="b270-grid">
|
|
5
|
-
<% today = signed_in? ? current_reader.
|
|
5
|
+
<% today = signed_in? ? current_reader.today_day : nil %>
|
|
6
6
|
<% (1..Bible270::Plan::DAYS).each do |d| %>
|
|
7
7
|
<%= b270_day_cell(d, reader: current_reader, today: today) %>
|
|
8
8
|
<% end %>
|
|
@@ -8,22 +8,19 @@
|
|
|
8
8
|
</span>
|
|
9
9
|
<% end %>
|
|
10
10
|
</div>
|
|
11
|
-
<nav class="b270-nav">
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
rejects GET, so its buttons live on that page as POST forms. %>
|
|
26
|
-
<%= link_to "Sign in", sign_in_path(origin: request.fullpath), class: "b270-signin" %>
|
|
27
|
-
<% end %>
|
|
11
|
+
<nav class="b270-nav" aria-label="Primary navigation">
|
|
12
|
+
<div class="b270-desktopnav">
|
|
13
|
+
<%= render "bible270/shared/nav_links" %>
|
|
14
|
+
</div>
|
|
15
|
+
<details class="b270-navmenu">
|
|
16
|
+
<summary class="b270-navtoggle" aria-label="Menu">
|
|
17
|
+
<span class="b270-menuicon" aria-hidden="true">
|
|
18
|
+
<span></span><span></span><span></span>
|
|
19
|
+
</span>
|
|
20
|
+
</summary>
|
|
21
|
+
<div class="b270-navlinks">
|
|
22
|
+
<%= render "bible270/shared/nav_links" %>
|
|
23
|
+
</div>
|
|
24
|
+
</details>
|
|
28
25
|
</nav>
|
|
29
26
|
</header>
|