bible270 1.0.0 → 1.1.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -9
  3. data/README.md +7 -10
  4. data/app/controllers/bible270/admin_controller.rb +2 -3
  5. data/app/controllers/bible270/days_controller.rb +1 -36
  6. data/app/controllers/bible270/profiles_controller.rb +20 -1
  7. data/app/controllers/bible270/readers_controller.rb +1 -66
  8. data/app/mailers/bible270/notice_mailer.rb +32 -8
  9. data/app/models/bible270/comment.rb +33 -9
  10. data/app/models/bible270/reader.rb +8 -40
  11. data/app/views/bible270/comments/create.turbo_stream.erb +0 -2
  12. data/app/views/bible270/comments/index.html.erb +0 -24
  13. data/app/views/bible270/days/_completers.html.erb +0 -2
  14. data/app/views/bible270/days/_day_progress.html.erb +3 -3
  15. data/app/views/bible270/days/index.html.erb +1 -27
  16. data/app/views/bible270/days/show.html.erb +4 -16
  17. data/app/views/bible270/notice_mailer/{mentioned.html.erb → comment_notice.html.erb} +5 -2
  18. data/app/views/bible270/notice_mailer/{mentioned.text.erb → comment_notice.text.erb} +5 -2
  19. data/app/views/bible270/profiles/edit.html.erb +11 -0
  20. data/app/views/bible270/readers/_next_steps.html.erb +24 -60
  21. data/app/views/bible270/readers/index.html.erb +2 -3
  22. data/app/views/bible270/readers/progress.html.erb +6 -30
  23. data/app/views/bible270/readers/show.html.erb +1 -6
  24. data/app/views/bible270/shared/_day_index.html.erb +6 -4
  25. data/app/views/bible270/shared/_header.html.erb +21 -4
  26. data/app/views/bible270/shared/_progress.html.erb +3 -14
  27. data/app/views/bible270/shared/_styles.html.erb +3 -32
  28. data/config/routes.rb +0 -4
  29. data/db/migrate/20260101000011_add_mention_notices_to_bible270_readers.rb +3 -3
  30. data/lib/bible270/configuration.rb +8 -5
  31. data/lib/bible270/mentions.rb +0 -9
  32. data/lib/bible270/version.rb +1 -1
  33. data/lib/generators/bible270/install/install_generator.rb +2 -0
  34. data/lib/generators/bible270/install/templates/bible270.rb.tt +2 -0
  35. metadata +3 -5
  36. data/app/views/bible270/days/_start_date.html.erb +0 -43
  37. data/app/views/bible270/shared/_reading_mode.html.erb +0 -62
@@ -14,11 +14,11 @@
14
14
  "aria-label": "Previous day unavailable" %>
15
15
  <% end %>
16
16
  <div class="b270-dayhead">
17
- <div class="k">Day <%= day %><% if complete %><span class="b270-badge" data-b270-compact-hide>Complete</span><% end %></div>
17
+ <div class="k">Day <%= day %><% if complete %><span class="b270-badge">Complete</span><% end %></div>
18
18
  <% if reader&.dated? %>
19
19
  <div class="date">
20
20
  <%= reader.date_for_day(day).strftime("%A, %B %-d, %Y") %>
21
- <% if reader.today?(day) %><span class="b270-badge" data-b270-compact-hide>Today</span><% end %>
21
+ <% if reader.today?(day) %><span class="b270-badge">Today</span><% end %>
22
22
  </div>
23
23
  <% end %>
24
24
  </div>
@@ -33,7 +33,7 @@
33
33
 
34
34
  <% if complete %>
35
35
  <div class="b270-day-complete" role="status" aria-live="polite">
36
- <div data-b270-compact-hide>
36
+ <div>
37
37
  <strong>Day <%= day %> complete</strong>
38
38
  <span>All <%= total_parts %> portions are checked off.</span>
39
39
  </div>
@@ -5,30 +5,4 @@
5
5
 
6
6
  <%= render "bible270/shared/progress" %>
7
7
 
8
- <% if @readers.any? %>
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>
14
- <div class="b270-list">
15
- <% @readers.first(5).each do |r| %>
16
- <div class="b270-li">
17
- <%= b270_avatar(r) %>
18
- <div>
19
- <div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
20
- <% day_word = @days_completed[r.id] == 1 ? "day" : "days" %>
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>
29
- </div>
30
- </div>
31
- <% end %>
32
- </div>
33
- <p style="margin:12px 0 0"><%= link_to "Meet the community →", community_path %></p>
34
- <% end %>
8
+ <p style="margin:12px 0 0"><%= link_to "Meet your fellow readers →", community_path %></p>
@@ -1,13 +1,8 @@
1
1
  <% content_for :title, "Day #{@day} — #{Bible270.config.app_name}" %>
2
2
 
3
- <div class="b270-day-page" data-b270-day-page data-b270-reading-mode="full">
3
+ <div class="b270-day-page">
4
4
  <%= render "bible270/days/day_progress", day: @day, reader: current_reader %>
5
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>
9
- </div>
10
-
11
6
  <%= render "bible270/shared/interaction_status" %>
12
7
 
13
8
  <div id="readings" class="b270-readings">
@@ -16,19 +11,15 @@
16
11
  </div>
17
12
 
18
13
  <% 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>
14
+ <p class="b270-signin-hint" style="margin-top:12px"><%= link_to "Sign in", sign_in_path(origin: request.fullpath) %> to track your reading and join the conversation.</p>
20
15
  <% end %>
21
16
 
22
- <div class="b270-day-community" data-b270-compact-hide>
17
+ <div class="b270-day-community">
23
18
  <%= render "bible270/days/completers", day: @day, completers: @completers %>
24
19
 
25
20
  <h2 class="b270-section-h">Reflections</h2>
26
21
  <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 %>
22
+ <%= render partial: "bible270/comments/comment", collection: @comments, as: :comment %>
32
23
  </div>
33
24
 
34
25
  <% if signed_in? %>
@@ -36,11 +27,8 @@
36
27
  <%= render "bible270/comments/form", day: @day, comment: @new_comment %>
37
28
  </div>
38
29
  <%= 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
30
  <% end %>
42
31
  </div>
43
32
  </div>
44
33
 
45
- <%= render "bible270/shared/reading_mode" %>
46
34
  <%= render "bible270/shared/interaction_ui" %>
@@ -3,7 +3,7 @@
3
3
  <body style="margin:0;padding:24px;background:#ece6da;font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#231f18">
4
4
  <div style="max-width:520px;margin:0 auto;background:#f4efe6;border:1px solid #d7cfbf;border-radius:14px;padding:28px">
5
5
  <h1 style="margin:0 0 6px;font-family:Georgia,serif;font-size:20px;font-weight:600">
6
- <%= @author.display_name %> mentioned you
6
+ <%= @heading %>
7
7
  </h1>
8
8
  <p style="margin:0 0 18px;font-size:14px;color:#6d6558">
9
9
  Day <%= @comment.day %> · <%= @app_name %>
@@ -20,7 +20,10 @@
20
20
  <% end %>
21
21
 
22
22
  <p style="margin:22px 0 0;font-size:12px;color:#9a9082">
23
- You are getting this because someone wrote your name in a reflection.
23
+ <%= @reason %>
24
+ <% if @profile_url %>
25
+ <a href="<%= @profile_url %>" style="color:#6d6558">Manage email preferences</a>.
26
+ <% end %>
24
27
  </p>
25
28
  </div>
26
29
  </body>
@@ -1,4 +1,4 @@
1
- <%= @author.display_name %> mentioned you in a reflection on day <%= @comment.day %>.
1
+ <%= @introduction %>
2
2
 
3
3
  <%= @comment.body %>
4
4
 
@@ -8,4 +8,7 @@ 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.
11
+ <%= @reason %>
12
+ <% if @profile_url %>
13
+ Manage email preferences: <%= @profile_url %>
14
+ <% end %>
@@ -37,6 +37,17 @@
37
37
  </label>
38
38
  <%= render "bible270/shared/passage_source", reader: current_reader %>
39
39
 
40
+ <% if @comment_notifications_available %>
41
+ <div class="b270-field">
42
+ <span>Email notifications</span>
43
+ <label class="b270-reminder-optin">
44
+ <%= hidden_field_tag :notify_on_mention, "0", id: nil %>
45
+ <%= check_box_tag :notify_on_mention, "1", @notify_on_mention %>
46
+ <span>Email me about replies and mentions</span>
47
+ </label>
48
+ </div>
49
+ <% end %>
50
+
40
51
  <% if @daily_reminders_available %>
41
52
  <% reminder_hour, reminder_minute = @daily_reminder_time.to_s.split(":", 2) %>
42
53
  <div class="b270-field">
@@ -1,63 +1,27 @@
1
- <%# locals: (reader:, partial_day_tasks:, remaining_partial_days:, week_days:, week_completed:, next_milestone:, days_to_milestone:) %>
1
+ <%# locals: (partial_day_tasks:, remaining_partial_days:) %>
2
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>
3
+ <h2 class="b270-subhead">Finish what you started</h2>
4
+ <ul class="b270-task-list">
5
+ <% partial_day_tasks.each do |task| %>
6
+ <% day = task[:day] %>
7
+ <% first_track = task[:remaining].first&.fetch(:track) %>
8
+ <li class="b270-task">
9
+ <div class="b270-task-body">
10
+ <strong><%= link_to "Day #{day}", day_path(day, anchor: "reading_#{day}_#{first_track}") %></strong>
11
+ <span>
12
+ Still to read:
13
+ <%= safe_join(task[:remaining].map { |part|
14
+ link_to(part[:reference], day_path(day, anchor: "reading_#{day}_#{part[:track]}"))
15
+ }, " · ") %>
16
+ </span>
17
+ </div>
18
+ <%= link_to "Continue →", day_path(day, anchor: "reading_#{day}_#{first_track}"),
19
+ class: "b270-task-link" %>
20
+ </li>
33
21
  <% 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>
22
+ </ul>
23
+ <% if remaining_partial_days.positive? %>
24
+ <p class="b270-next-note">View <%= remaining_partial_days %> more partially read
25
+ <%= "day".pluralize(remaining_partial_days) %> under “View all 270 days”.</p>
26
+ <% end %>
63
27
  </div>
@@ -7,11 +7,10 @@
7
7
  <div class="b270-list">
8
8
  <% @readers.each do |r| %>
9
9
  <div class="b270-li">
10
- <%= b270_avatar(r, size: 72) %>
10
+ <%= link_to b270_avatar(r, size: 72), reader_path(r), "aria-label": r.display_name %>
11
11
  <div>
12
12
  <div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
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>
13
+ <div class="sub">joined <%= Bible270.local_time(r.created_at).strftime("%b %-d %Y") %></div>
15
14
  </div>
16
15
  </div>
17
16
  <% end %>
@@ -1,43 +1,19 @@
1
1
  <% content_for :title, "My Progress — #{Bible270.config.app_name}" %>
2
- <% content_for :hide_day_index, true %><%# the panel is followed by its own grid below %>
3
2
 
4
3
  <p class="b270-eyebrow">My Progress</p>
5
4
  <h1>Your reading journey</h1>
6
5
 
7
6
  <%= render "bible270/shared/progress" %>
8
7
 
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 %>
8
+ <% if @reader && @partial_day_tasks.any? %>
9
+ <%= render "bible270/readers/next_steps", partial_day_tasks: @partial_day_tasks,
10
+ remaining_partial_days: @remaining_partial_days %>
17
11
  <% end %>
18
12
 
19
- <div class="b270-panel">
20
- <%= render "bible270/shared/reflections", comments: @recent_comments,
21
- title: "Your recent reflections",
22
- empty_text: "You have not written any reflections yet." %>
23
- </div>
24
-
25
13
  <% if @reader %>
26
14
  <div class="b270-panel">
27
- <h2 class="b270-subhead">Every day at a glance</h2>
28
- <div class="b270-grid">
29
- <% today = @reader.today_day %>
30
- <% (1..Bible270::Plan::DAYS).each do |d| %>
31
- <%= b270_day_cell(d, reader: @reader, today: today) %>
32
- <% end %>
33
- </div>
34
- <p class="b270-gridnote">Dot indicates days with reflections.</p>
35
- <p class="b270-hint">Filled squares are days you have finished; a lighter square means you have
36
- read some of that day but not all of it.</p>
15
+ <%= render "bible270/shared/reflections", comments: @recent_comments,
16
+ title: "Your recent reflections",
17
+ empty_text: "You have not written any reflections yet." %>
37
18
  </div>
38
-
39
- <p style="margin-top:18px">
40
- <%= link_to "How others are doing →", community_path %>
41
- · <%= link_to "Your profile", profile_path %>
42
- </p>
43
19
  <% end %>
@@ -20,9 +20,4 @@
20
20
  title: "Their reflections", empty_text: "No reflections yet." %>
21
21
  </div>
22
22
 
23
- <div class="b270-grid">
24
- <% (1..Bible270::Plan::DAYS).each do |d| %>
25
- <%= b270_day_cell(d, reader: @reader) %>
26
- <% end %>
27
- </div>
28
- <p class="b270-gridnote">Dot indicates days with reflections.</p>
23
+ <%= render "bible270/shared/day_index", reader: @reader %>
@@ -1,12 +1,14 @@
1
+ <%# locals: (reader: nil) %>
1
2
  <% if Bible270::Plan::DAYS.positive? %>
3
+ <% grid_reader = reader || current_reader %>
4
+ <% today = signed_in? && grid_reader == current_reader ? current_reader.today_day : nil %>
2
5
  <details class="b270-index">
3
- <summary>All <%= Bible270::Plan::DAYS %> days</summary>
6
+ <summary>View all <%= Bible270::Plan::DAYS %> days</summary>
4
7
  <div class="b270-grid">
5
- <% today = signed_in? ? current_reader.today_day : nil %>
6
8
  <% (1..Bible270::Plan::DAYS).each do |d| %>
7
- <%= b270_day_cell(d, reader: current_reader, today: today) %>
9
+ <%= b270_day_cell(d, reader: grid_reader, today: today) %>
8
10
  <% end %>
9
11
  </div>
10
- <p class="b270-gridnote">Dot indicates days with reflections.</p>
12
+ <p class="b270-gridnote">Dot indicates days with reflections.</p>
11
13
  </details>
12
14
  <% end %>
@@ -2,10 +2,7 @@
2
2
  <div class="b270-brand">
3
3
  <%= link_to root_path, class: "b270-brandlink" do %>
4
4
  <%= b270_mark %>
5
- <span class="b270-brandtext">
6
- <span class="b270-title"><%= Bible270.config.app_name %></span>
7
- <span class="b270-tagline"><%= Bible270.config.tagline %></span>
8
- </span>
5
+ <span class="b270-title"><%= Bible270.config.app_name %></span>
9
6
  <% end %>
10
7
  </div>
11
8
  <nav class="b270-nav" aria-label="Primary navigation">
@@ -24,3 +21,23 @@
24
21
  </details>
25
22
  </nav>
26
23
  </header>
24
+
25
+ <script nonce="<%= content_security_policy_nonce %>">
26
+ (() => {
27
+ if (window.Bible270NavigationMenu) return;
28
+
29
+ document.addEventListener("click", (event) => {
30
+ const target = event.target;
31
+ const toggle = target.closest && target.closest("summary.b270-navtoggle");
32
+ if (!toggle) return;
33
+
34
+ const menu = toggle.closest("details.b270-navmenu");
35
+ if (!menu) return;
36
+
37
+ event.preventDefault();
38
+ menu.open = !menu.open;
39
+ });
40
+
41
+ window.Bible270NavigationMenu = true;
42
+ })();
43
+ </script>
@@ -36,19 +36,9 @@
36
36
  <%= render "bible270/shared/track_bars", reader: panel_reader %>
37
37
 
38
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>
39
+ <div class="b270-progress-actions">
40
+ <%= link_to "Catch up from Day #{panel_start_day} →", day_path(panel_start_day), class: "b270-btn" %>
41
+ <%= link_to "Read with the community — Day #{panel_today_day}", day_path(panel_today_day), class: "b270-todaylink" %>
52
42
  </div>
53
43
  <% else %>
54
44
  <div class="b270-progress-actions">
@@ -59,7 +49,6 @@
59
49
  </div>
60
50
  <% end %>
61
51
 
62
- <%= render "bible270/days/start_date", reader: panel_reader %>
63
52
  <% else %>
64
53
  <p class="b270-signedout"><%= link_to "Sign in", sign_in_path %> to track your progress and join
65
54
  the conversation. You can browse the whole plan and read others' reflections without an account.</p>
@@ -18,11 +18,9 @@
18
18
  .b270-brand{display:flex;margin-right:auto}
19
19
  /* The whole brand is one link, so the mark and the words highlight together. */
20
20
  .b270-brandlink{display:flex;align-items:center;gap:12px;text-decoration:none}
21
- .b270-brandtext{display:flex;flex-direction:column}
22
21
  .b270-mark{display:flex;align-items:center;flex:none;line-height:0}
23
22
  .b270-mark svg,.b270-mark img{display:block;border-radius:8px}
24
23
  .b270-title{font-family:"Spectral",Georgia,serif;font-weight:600;font-size:22px;text-decoration:none}
25
- .b270-tagline{font-size:12px;color:var(--faint);letter-spacing:.04em}
26
24
  .b270-nav{display:flex;align-items:center;font-size:14px}
27
25
  .b270-desktopnav{display:flex;align-items:center;gap:16px}
28
26
  .b270-navmenu{display:none}
@@ -97,16 +95,8 @@
97
95
  .b270-progress-actions{display:flex;flex-direction:column;align-items:flex-start;gap:7px;margin:16px 0 0}
98
96
  .b270-todaylink{margin-left:16px;font-size:12px;color:var(--muted);text-decoration:none}
99
97
  .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)}
98
+
99
+
110
100
  .b270-task-list{list-style:none;margin:0;padding:0;display:grid;gap:8px}
111
101
  .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
102
  .b270-task-body{display:grid;gap:3px;min-width:0}
@@ -118,21 +108,10 @@
118
108
  .b270-task-link:hover{text-decoration:underline}
119
109
  .b270-next-note{margin:0;color:var(--faint);font-size:12.5px;line-height:1.45}
120
110
  .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}
126
111
  .b270-signedout{color:var(--muted);font-size:14px;margin:4px 0 0}
127
112
 
128
- .b270-startdate{margin-top:18px;padding-top:16px;border-top:1px solid var(--line-soft)}
129
113
  .b270-startline{margin:0;font-size:13.5px;color:var(--muted)}
130
114
  .b270-startline strong{color:var(--ink)}
131
- .b270-startline em{font-style:normal;color:var(--gold);font-weight:600}
132
- .b270-startedit{margin-top:10px}
133
- .b270-startedit summary{cursor:pointer;font-size:13px;color:var(--muted);list-style:none}
134
- .b270-startedit summary::-webkit-details-marker{display:none}
135
- .b270-startedit summary:hover{color:var(--gold)}
136
115
  .b270-startform{display:flex;align-items:center;gap:10px;margin-top:10px;flex-wrap:wrap}
137
116
  .b270-startform input[type=date]{font:inherit;font-size:14px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:#fff;color:var(--ink)}
138
117
  .b270-linkbtn{background:none;border:none;padding:0;margin-top:8px;font:inherit;font-size:12.5px;color:var(--oxblood);cursor:pointer;text-decoration:underline}
@@ -146,12 +125,7 @@
146
125
  .b270-tm .val{color:var(--faint);font-variant-numeric:tabular-nums;min-width:56px;text-align:right}
147
126
 
148
127
  /* 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}
128
+
155
129
  .b270-daynav{display:flex;align-items:center;gap:10px;margin-bottom:12px}
156
130
  .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}
157
131
  .b270-arrow:hover{border-color:var(--gold);color:var(--gold)}
@@ -287,7 +261,6 @@
287
261
  .b270-li .sub{font-size:12.5px;color:var(--faint)}
288
262
  .b270-li .stat{margin-left:auto;text-align:right}
289
263
  .b270-li .stat .big{font-family:"Spectral",serif;font-size:20px;font-weight:600}
290
- .b270-community-intro{margin:-8px 0 12px;color:var(--muted);font-size:13px}
291
264
 
292
265
  /* calendar grid */
293
266
  .b270-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(30px,1fr));gap:6px;margin-top:16px}
@@ -339,8 +312,6 @@
339
312
  @media (max-width:620px){
340
313
  .b270-topbar{flex-wrap:nowrap;align-items:center}
341
314
  .b270-brand{min-width:0}
342
- .b270-brandtext{min-width:0}
343
- .b270-tagline{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
344
315
  .b270-nav{position:relative;flex:none}
345
316
  .b270-desktopnav{display:none}
346
317
  .b270-navmenu{display:block}
data/config/routes.rb CHANGED
@@ -45,10 +45,6 @@ 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', 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
51
-
52
48
  # Built-in OmniAuth sign-in. The request phase (POST <mount>/auth/:provider)
53
49
  # is handled by the OmniAuth middleware in the host app, not by these routes.
54
50
  get 'sign_in', to: 'sessions#new', as: :sign_in
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class AddMentionNoticesToBible270Readers < ActiveRecord::Migration[7.0]
4
- # Being emailed because someone typed your name is the sort of thing people
5
- # want to switch off, so it is a per-reader setting rather than a site-wide one.
6
- # Default true: a mention nobody hears about is not much use.
4
+ # Replies and explicit mentions share one preference: both are direct responses
5
+ # from another reader, and separate switches would add needless complexity.
6
+ # Default true so existing conversations remain connected.
7
7
  def change
8
8
  add_column :bible270_readers, :notify_on_mention, :boolean, null: false, default: true
9
9
  end
@@ -297,7 +297,7 @@ module Bible270
297
297
  # application owns scheduling the bible270:reminders:send task.
298
298
  attr_accessor :daily_reminders
299
299
 
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
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
301
301
 
302
302
  # Public labels.
303
303
  attr_accessor :app_name
@@ -311,8 +311,9 @@ module Bible270
311
311
  # (defaulting to the day they first check something off).
312
312
  attr_reader :start_date
313
313
 
314
- # Whether an individual reader may set or change their own start date. When
315
- # false, everyone is pinned to config.start_date.
314
+ # Whether individual readers may follow personal start dates. When false,
315
+ # everyone is pinned to config.start_date; administrators may still store
316
+ # individual dates for use if personal calendars are enabled later.
316
317
  attr_accessor :allow_reader_start_date
317
318
 
318
319
  # Only signed-in readers may check off and comment (viewing is always open).
@@ -334,8 +335,10 @@ module Bible270
334
335
  # config.time_zone = 'America/Los_Angeles' # a named zone
335
336
  attr_accessor :time_zone
336
337
 
337
- # Email a reader when someone writes "@them" in a reflection. Readers can opt
338
- # out individually on their profile; this switches the whole feature off.
338
+ # Email readers about direct replies and explicit @mentions. Readers can opt
339
+ # out with one profile preference; this switches the whole feature off.
340
+ attr_accessor :mention_notifications
341
+
339
342
  # How many threads each Reflections page shows. A thread resurfaces when someone
340
343
  # replies to it, so this is a count of conversations, not of messages.
341
344
  attr_accessor :reflections_page_size
@@ -49,15 +49,6 @@ module Bible270
49
49
  handles.uniq
50
50
  end
51
51
 
52
- # The form to write when there is more than one reader with the same first
53
- # name: the longer handle is unambiguous.
54
- def preferred_handle(first_name, last_name, ambiguous: false)
55
- handles = handles_for(first_name, last_name)
56
- return nil if handles.empty?
57
-
58
- ambiguous ? handles.last : handles.first
59
- end
60
-
61
52
  # Wraps each mention so a view can style it, leaving the text otherwise
62
53
  # untouched. The block receives the handle and returns the replacement.
63
54
  def highlight(text)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -510,12 +510,14 @@ module Bible270
510
510
  # --- Start date -----------------------------------------------------------
511
511
  # A community-wide start date, or leave nil for an undated plan.
512
512
  # config.start_date = Date.new(#{Date.today.year}, 1, 1)
513
+ # Allow personal calendars, stamped when an undated reader first checks off.
513
514
  config.allow_reader_start_date = true
514
515
 
515
516
  # --- Daily reading reminders -----------------------------------------------
516
517
  # Readers choose an opt-in time only when enabled. This application owns
517
518
  # scheduling: run `bin/rails bible270:reminders:send` every 15 minutes.
518
519
  config.daily_reminders = false
520
+ config.mention_notifications = true # direct replies and @mentions; readers may opt out
519
521
  # config.mailer_host = 'example.com' # links back to the day and profile
520
522
 
521
523
  # --- Reading links --------------------------------------------------------
@@ -53,12 +53,14 @@ Bible270.configure do |config|
53
53
  # --- Start date ------------------------------------------------------------
54
54
  # Community-wide start date (Date, Time, or "YYYY-MM-DD"), or nil for undated.
55
55
  # config.start_date = Date.new(2026, 9, 6)
56
+ # Allow personal calendars, stamped when an undated reader first checks off.
56
57
  config.allow_reader_start_date = true
57
58
 
58
59
  # --- Daily reading reminders -----------------------------------------------
59
60
  # Readers choose an opt-in time on their profile only when this is enabled.
60
61
  # The host app owns scheduling: run `bin/rails bible270:reminders:send` every 15 minutes.
61
62
  config.daily_reminders = false
63
+ config.mention_notifications = true # direct replies and @mentions; readers may opt out
62
64
  # config.mailer_host = "example.com" # required for day/profile links in email
63
65
 
64
66
  # --- Reading links ---------------------------------------------------------