bible270 0.16.1 → 0.17.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5518f1eb25a21f7610ea80c8cd46e1d49690716bf98b5644fb3fa443f920a0e
4
- data.tar.gz: 384ec828a091ed3c71abeb71a1d5b5f7d1cb7ff707f997511ca5e6d411cc5095
3
+ metadata.gz: f2e0821b84cb0bc7ece339c39775d9b7b814b4f274e721f112cc393aabfc5093
4
+ data.tar.gz: 53f7e5d9386bfd4f9ae14ed6ac56daedab5df0139c0043e4bac6576bde257ed2
5
5
  SHA512:
6
- metadata.gz: f222369a58be11fb62c8fd76be668aad9010f76f3d88e9d9820d562eefce7becbe7e9b4fd7dd4dfa5e2408c256762fdd91da44942447328fa69782698e387789
7
- data.tar.gz: c1f881c35558530bb27636a2fcd27a0f40ebb9cdc37e9f07cc10fda04aa78ddcfff65f8b210575c10640b46400616727e28ddc3b8594fd9cfa91dd19ac658a3b
6
+ metadata.gz: ef8060ffe3f6a7c99e35978886609c580e091a0bb0b6f86b809ef7bf8afa8bdda919f7e94c97f04ca5bf69e30b5b70b79b37333fb0850073864d40ed3bf8b909
7
+ data.tar.gz: c17db24c8ca3ab97e950f2fa516f5366b9d98dcbaa0422a4130acab6031bdeeb1a7faefed1d28e6f4ea44f77404ea3a50a28c041b8bba48edab4936bf158313c
@@ -2,6 +2,11 @@
2
2
 
3
3
  module Bible270
4
4
  class CommentsController < ApplicationController
5
+ def index
6
+ @days_with_reflections = Comment.approved.distinct.pluck(:day).sort
7
+ @threads = recent_threads
8
+ end
9
+
5
10
  def create
6
11
  return unless require_reader!
7
12
 
@@ -27,10 +32,34 @@ module Bible270
27
32
  end
28
33
  end
29
34
 
35
+ def update
36
+ return unless require_reader!
37
+
38
+ @comment = own_comment
39
+ head :not_found and return unless @comment
40
+
41
+ @day = @comment.day
42
+ if @comment.update(comment_params.except(:parent_id))
43
+ respond_to do |format|
44
+ format.turbo_stream
45
+ format.html { redirect_to day_path(@day, anchor: "comment-#{@comment.id}") }
46
+ end
47
+ else
48
+ respond_to do |format|
49
+ format.turbo_stream do
50
+ render turbo_stream: turbo_stream.replace("comment-#{@comment.id}",
51
+ partial: 'bible270/comments/edit_form',
52
+ locals: { comment: @comment })
53
+ end
54
+ format.html { redirect_to day_path(@day), alert: @comment.errors.full_messages.to_sentence }
55
+ end
56
+ end
57
+ end
58
+
30
59
  def destroy
31
60
  return unless require_reader!
32
61
 
33
- @comment = current_reader.comments.find_by(id: params[:id])
62
+ @comment = deletable_comment
34
63
  head :not_found and return unless @comment
35
64
 
36
65
  @day = @comment.day
@@ -43,8 +72,46 @@ module Bible270
43
72
 
44
73
  private
45
74
 
75
+ # The most recently active conversations, newest first. A reply counts as
76
+ # activity, so answering an old reflection brings the whole thread back up —
77
+ # otherwise a lively discussion on day 3 would stay buried while quieter, newer
78
+ # reflections sat above it.
79
+ def recent_threads
80
+ wanted = Bible270.config.reflections_page_size.to_i
81
+ wanted = 10 unless wanted.positive?
82
+
83
+ # Look at rather more messages than threads, since several may belong to one.
84
+ recent = Comment.approved.order(created_at: :desc).limit(wanted * 5)
85
+ .pluck(:id, :parent_id, :created_at)
86
+
87
+ roots = {}
88
+ recent.each do |id, parent_id, created_at|
89
+ root = parent_id || id
90
+ roots[root] ||= created_at
91
+ roots[root] = created_at if created_at > roots[root]
92
+ end
93
+
94
+ ids = roots.sort_by { |_root, at| -at.to_i }.first(wanted).map(&:first)
95
+ threads = Comment.approved.where(id: ids).includes(:reader, replies: :reader).index_by(&:id)
96
+ ids.filter_map { |id| threads[id] }
97
+ end
98
+
46
99
  def comment_params
47
100
  params.require(:comment).permit(:body, :track, :parent_id)
48
101
  end
102
+
103
+ # Scoped to the reader, so someone else's reflection is simply not found —
104
+ # it reveals nothing about what exists.
105
+ def own_comment
106
+ current_reader.comments.find_by(id: params[:id])
107
+ end
108
+
109
+ # An admin may remove any reflection; everyone else only their own. Editing
110
+ # stays with the writer either way.
111
+ def deletable_comment
112
+ return own_comment unless Bible270.config.admin?(current_reader)
113
+
114
+ Comment.find_by(id: params[:id])
115
+ end
49
116
  end
50
117
  end
@@ -27,6 +27,7 @@ module Bible270
27
27
  @comments = Comment.threads_for_day(@day)
28
28
  # Replying prefills the form with the handle rather than needing JavaScript,
29
29
  # so it works with Turbo and without an asset pipeline.
30
+ @editing_comment_id = editing_comment_id
30
31
  parent = reply_parent
31
32
  @new_comment = Comment.new(day: @day, parent_id: parent&.id, body: reply_prefix(parent))
32
33
  @reader_tracks = current_reader ? current_reader.read_tracks_for(@day) : []
@@ -50,6 +51,14 @@ module Bible270
50
51
  # "@handle " for the reflection being replied to, or nothing. Private, but
51
52
  # declared with the keyword rather than trailing the file, so it is obvious
52
53
  # this is not an action.
54
+ # The reflection the reader has asked to edit: their own, on this day. Anything
55
+ # else is ignored rather than refused — the page still renders.
56
+ def editing_comment_id
57
+ return nil if params[:edit].blank? || current_reader.nil?
58
+
59
+ current_reader.comments.where(day: @day, id: params[:edit]).pick(:id)
60
+ end
61
+
53
62
  # The reflection being replied to, if the link carried one and it is a
54
63
  # top-level reflection: a reply to a reply is not allowed.
55
64
  def reply_parent
@@ -137,6 +137,77 @@ module Bible270
137
137
  end
138
138
  end
139
139
 
140
+ # Only the writer may change their own words. An admin can remove a reflection
141
+ # but not rewrite it: putting words in someone's mouth is worse than taking
142
+ # them away, and taking them away is already the moderator's job.
143
+ def b270_can_edit?(comment)
144
+ signed_in? && comment.reader_id == current_reader.id
145
+ end
146
+
147
+ def b270_can_delete?(comment)
148
+ return false unless signed_in?
149
+
150
+ comment.reader_id == current_reader.id || Bible270.config.admin?(current_reader)
151
+ end
152
+
153
+ # One cell of the 270-day grid. Four views drew this by hand in four slightly
154
+ # different ways; now they all call this.
155
+ #
156
+ # The title attribute carries the readings, so hovering a square says what is
157
+ # on that day — no JavaScript, and it works for keyboard focus too.
158
+ def b270_day_cell(day, reader: nil, today: nil)
159
+ link_to day, day_path(day),
160
+ class: b270_day_cell_classes(day, reader: reader, today: today),
161
+ title: b270_day_readings(day)
162
+ end
163
+
164
+ # Shared with the admin grid, which is a button_to rather than a link because
165
+ # it toggles the day rather than navigating to it.
166
+ def b270_day_cell_classes(day, reader: nil, today: nil)
167
+ classes = ['b270-cell', b270_day_status_class(reader, day)]
168
+ classes << 'talk' if b270_days_with_reflections.include?(day)
169
+ classes << 'now' if today && day == today
170
+
171
+ classes.reject(&:blank?).join(' ')
172
+ end
173
+
174
+ # "Genesis 1-3 · Matthew 1 · Psalm 1" as plain text, for a title attribute.
175
+ def b270_day_readings(day)
176
+ readings = Bible270::Plan.readings_for(day)
177
+ Bible270::Plan::TRACKS.keys.filter_map { |track| readings[track] }.join(' · ')
178
+ end
179
+
180
+ # The same references as links to the passage, in the reader's own translation
181
+ # when they have chosen one — b270_passage_url falls back to the site default
182
+ # for a visitor.
183
+ def b270_day_reading_links(day)
184
+ readings = Bible270::Plan.readings_for(day)
185
+
186
+ links = Bible270::Plan::TRACKS.keys.filter_map do |track|
187
+ reference = readings[track]
188
+ next if reference.blank?
189
+
190
+ link_to reference, b270_passage_url(reference),
191
+ class: 'b270-reflink', target: '_blank', rel: 'noopener'
192
+ end
193
+
194
+ safe_join(links, ' · ')
195
+ end
196
+
197
+ # The days that have at least one visible reflection, fetched once per request.
198
+ # Called from the layout's grid, which no controller sets up, so it cannot be an
199
+ # instance variable assigned in an action.
200
+ def b270_days_with_reflections
201
+ @b270_days_with_reflections ||=
202
+ if defined?(Bible270::Comment)
203
+ Bible270::Comment.approved.distinct.pluck(:day).to_set
204
+ else
205
+ Set.new
206
+ end
207
+ rescue StandardError
208
+ Set.new
209
+ end
210
+
140
211
  def b270_day_status_class(reader, day)
141
212
  return '' unless reader
142
213
 
@@ -28,6 +28,9 @@ module Bible270
28
28
  # Fires once the reflection is really saved, so nothing is sent for one whose
29
29
  # transaction rolls back.
30
30
  after_create_commit :notify_mentioned_readers
31
+ # Only the people newly named: editing a reflection five times must not mail
32
+ # the same reader five times.
33
+ after_update_commit :notify_newly_mentioned_readers
31
34
 
32
35
  scope :approved, -> { where(approved: true) }
33
36
  scope :hidden, -> { where(approved: false) }
@@ -43,6 +46,10 @@ module Bible270
43
46
  def hidden? = !approved
44
47
  def reply? = parent_id.present?
45
48
 
49
+ # A second's grace: created_at and updated_at differ by microseconds on
50
+ # insert, which would mark every reflection as edited.
51
+ def edited? = updated_at - created_at > 1
52
+
46
53
  # Visible replies, oldest first: a conversation reads forwards.
47
54
  def visible_replies = replies.approved.order(created_at: :asc)
48
55
  def moderated? = moderated_at.present?
@@ -70,11 +77,19 @@ module Bible270
70
77
 
71
78
  # A mention emails the person named. Failures are logged rather than raised:
72
79
  # nobody should lose a reflection because the mail server is down.
73
- def notify_mentioned_readers
80
+ def notify_newly_mentioned_readers
81
+ return unless saved_change_to_body?
82
+
83
+ already = Reader.mentioned_in(body_before_last_save).map(&:id)
84
+ notify_mentioned_readers(except: already)
85
+ end
86
+
87
+ def notify_mentioned_readers(except: [])
74
88
  return unless Bible270.config.mention_notifications
75
89
 
76
90
  Reader.mentioned_in(body).each do |mentioned|
77
91
  next if mentioned.id == reader_id
92
+ next if except.include?(mentioned.id)
78
93
  next unless mentioned.wants_mention_notices?
79
94
 
80
95
  notice = NoticeMailer.mentioned(comment_id: id, reader_id: mentioned.id)
@@ -80,7 +80,8 @@
80
80
  <div class="b270-grid" style="margin-top:16px">
81
81
  <% (1..Bible270::Plan::DAYS).each do |d| %>
82
82
  <%= button_to d, admin_reader_day_path(@reader, d), method: :post,
83
- class: "b270-cell #{b270_day_status_class(@reader, d)}",
83
+ class: b270_day_cell_classes(d, reader: @reader),
84
+ title: b270_day_readings(d),
84
85
  form: { style: 'display:contents', id: "day-#{d}" } %>
85
86
  <% end %>
86
87
  </div>
@@ -1,4 +1,11 @@
1
1
  <%# locals: (comment:, reply: false) %>
2
+ <%#
3
+ Whether this reflection is being edited comes from the controller rather than a
4
+ local, so it works for a reply as well as a top-level reflection without the day
5
+ view having to thread a flag through the collection. It is nil everywhere else
6
+ the partial is rendered.
7
+ %>
8
+ <% editing = @editing_comment_id.present? && @editing_comment_id == comment.id %>
2
9
  <div class="b270-comment <%= 'b270-reply' if reply %>" id="comment-<%= comment.id %>">
3
10
  <%= b270_avatar(comment.reader, size: reply ? 28 : 34) %>
4
11
  <div style="flex:1">
@@ -6,13 +13,27 @@
6
13
  <strong><%= link_to comment.reader.display_name, reader_path(comment.reader), style: "text-decoration:none" %></strong>
7
14
  · <%= comment.created_at.strftime("%b %-d, %Y") %>
8
15
  <% if comment.track.present? %>· <%= b270_track(comment.track)[:label] %><% end %>
9
- <% if signed_in? && comment.reader_id == current_reader.id %>
16
+ <% if comment.edited? %>· <span class="b270-edited" title="<%= comment.updated_at.strftime('%b %-d, %Y at %-I:%M %p') %>">edited</span><% end %>
17
+ <% if b270_can_edit?(comment) %>
18
+ <%= link_to "edit", day_path(comment.day, edit: comment.id, anchor: "comment-#{comment.id}"),
19
+ class: "b270-cedit" %>
20
+ <% end %>
21
+ <% if b270_can_delete?(comment) %>
22
+ <%# The confirm goes on the form, since button_to renders one and that is
23
+ where Turbo looks. Only when removing someone else's: confirming your
24
+ own deletion every time would be nagging. %>
10
25
  <%= button_to "delete", comment_path(comment), method: :delete, class: "b270-cdel",
11
- form: { style: "display:inline", data: { turbo_stream: true } } %>
26
+ form: { style: "display:inline",
27
+ data: { turbo_stream: true,
28
+ turbo_confirm: (b270_can_edit?(comment) ? nil : "Delete #{comment.reader.display_name}'s reflection?") } } %>
12
29
  <% end %>
13
30
  </div>
14
31
 
15
- <p class="b270-cbody"><%= b270_with_mentions(comment.body) %></p>
32
+ <% if editing %>
33
+ <%= render partial: "bible270/comments/edit_fields", locals: { comment: comment } %>
34
+ <% else %>
35
+ <p class="b270-cbody"><%= b270_with_mentions(comment.body) %></p>
36
+ <% end %>
16
37
 
17
38
  <%# Only a top-level reflection can be replied to: one level deep, so a reply
18
39
  offers no Reply of its own. %>
@@ -0,0 +1,25 @@
1
+ <%# locals: (comment:) %>
2
+ <%#
3
+ The edit form for one reflection. Reached by ?edit=<id> on the day page, which
4
+ needs no JavaScript — the same approach as ?reply_to=. Turbo will upgrade the
5
+ save to a stream when it is driving the page, and fall back to a redirect when
6
+ it is not.
7
+ %>
8
+ <%= form_with model: comment, url: comment_path(comment), method: :patch,
9
+ class: "b270-form b270-editform", data: { turbo_stream: true } do |f| %>
10
+ <% if comment.errors.any? %>
11
+ <p class="b270-editerror"><%= comment.errors.full_messages.to_sentence %></p>
12
+ <% end %>
13
+
14
+ <%= f.text_area :body %>
15
+ <div class="row">
16
+ <% unless comment.reply? %>
17
+ <%= f.select :track,
18
+ options_for_select([["The whole day", ""]] + Bible270::Plan::TRACKS.map { |k, v| [v[:label], k] },
19
+ comment.track.to_s),
20
+ {}, class: "b270-select", style: "width:auto" %>
21
+ <% end %>
22
+ <%= f.submit "Save", class: "b270-btn" %>
23
+ <%= link_to "Cancel", day_path(comment.day, anchor: "comment-#{comment.id}"), class: "b270-linkbtn" %>
24
+ </div>
25
+ <% end %>
@@ -0,0 +1,7 @@
1
+ <%# locals: (comment:) %>
2
+ <div class="b270-comment b270-editing" id="comment-<%= comment.id %>">
3
+ <%= b270_avatar(comment.reader) %>
4
+ <div style="flex:1">
5
+ <%= render partial: "bible270/comments/edit_fields", locals: { comment: comment } %>
6
+ </div>
7
+ </div>
@@ -0,0 +1,3 @@
1
+ <%= turbo_stream.replace "comment-#{@comment.id}" do %>
2
+ <%= render partial: "bible270/comments/edit_form", locals: { comment: @comment } %>
3
+ <% end %>
@@ -0,0 +1,52 @@
1
+ <% content_for :title, "Reflections — #{Bible270.config.app_name}" %>
2
+ <% content_for :hide_day_index, true %><%# this page has its own, filtered grid %>
3
+
4
+ <p class="b270-eyebrow">Reflections</p>
5
+ <h1>What people are saying</h1>
6
+
7
+ <% if @days_with_reflections.any? %>
8
+ <div class="b270-panel">
9
+ <h2 class="b270-subhead">Days with reflections</h2>
10
+ <p class="b270-hint" style="margin-top:0">
11
+ <%= @days_with_reflections.size %>
12
+ of <%= Bible270::Plan::DAYS %> days have been written about.
13
+ </p>
14
+
15
+ <%# Only the days that have something on them, so the grid is a map of the
16
+ conversation rather than of the plan. Squares keep their usual meaning:
17
+ the fill is reading progress, and hovering shows that day's readings. %>
18
+ <div class="b270-grid">
19
+ <% @days_with_reflections.each do |day| %>
20
+ <%= b270_day_cell(day, reader: current_reader) %>
21
+ <% end %>
22
+ </div>
23
+ </div>
24
+
25
+ <h2 class="b270-section-h">Most recent</h2>
26
+ <div id="comments">
27
+ <% @threads.each do |thread| %>
28
+ <div class="b270-thread">
29
+ <p class="b270-threadday">
30
+ <%= link_to "Day #{thread.day}", day_path(thread.day) %>
31
+ · <%= b270_day_reading_links(thread.day) %>
32
+ </p>
33
+ <%= render partial: "bible270/comments/comment", locals: { comment: thread, reply: false } %>
34
+ </div>
35
+ <% end %>
36
+ </div>
37
+
38
+ <p style="margin-top:18px">
39
+ <%= link_to "Browse the whole plan →", root_path %>
40
+ </p>
41
+ <% else %>
42
+ <div class="b270-panel">
43
+ <p class="b270-signedout" style="margin:0">
44
+ No one has written a reflection yet.
45
+ <% if signed_in? %>
46
+ <%= link_to "Be the first", day_path(current_reader.current_day) %>.
47
+ <% else %>
48
+ <%= link_to "Sign in", sign_in_path %> to add one.
49
+ <% end %>
50
+ </p>
51
+ </div>
52
+ <% end %>
@@ -0,0 +1,4 @@
1
+ <%= turbo_stream.replace "comment-#{@comment.id}" do %>
2
+ <%= render partial: "bible270/comments/comment",
3
+ locals: { comment: @comment, reply: @comment.reply? } %>
4
+ <% end %>
@@ -15,8 +15,8 @@
15
15
  <%= b270_avatar(r) %>
16
16
  <div>
17
17
  <div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
18
- <% @day_number = @days_completed[r.id] == 1 ? "day" : "days" %>
19
- <div class="sub"><%= @days_completed[r.id] %> <%= @day_number %> read</div>
18
+ <% day_word = @days_completed[r.id] == 1 ? "day" : "days" %>
19
+ <div class="sub"><%= @days_completed[r.id] %> <%= day_word %> read</div>
20
20
  </div>
21
21
  <div class="stat"><span class="big"><%= (@days_completed[r.id].to_f / Bible270::Plan::DAYS * 100).round %>%</span></div>
22
22
  </div>
@@ -18,10 +18,10 @@
18
18
  <div class="b270-grid">
19
19
  <% today = @reader.today_day %>
20
20
  <% (1..Bible270::Plan::DAYS).each do |d| %>
21
- <%= link_to d, day_path(d),
22
- class: "b270-cell #{b270_day_status_class(@reader, d)} #{'now' if d == today}" %>
21
+ <%= b270_day_cell(d, reader: @reader, today: today) %>
23
22
  <% end %>
24
23
  </div>
24
+ <p class="b270-gridnote">Dot indicates days with reflections.</p>
25
25
  <p class="b270-hint">Filled squares are days you have finished; a lighter square means you have
26
26
  read some of that day but not all of it.</p>
27
27
  </div>
@@ -22,7 +22,8 @@
22
22
 
23
23
  <div class="b270-grid">
24
24
  <% (1..Bible270::Plan::DAYS).each do |d| %>
25
- <%= link_to d, day_path(d), class: "b270-cell #{b270_day_status_class(@reader, d)}" %>
25
+ <%= b270_day_cell(d, reader: @reader) %>
26
26
  <% end %>
27
27
  </div>
28
+ <p class="b270-gridnote">Dot indicates days with reflections.</p>
28
29
 
@@ -4,9 +4,9 @@
4
4
  <div class="b270-grid">
5
5
  <% today = signed_in? ? current_reader.current_day : nil %>
6
6
  <% (1..Bible270::Plan::DAYS).each do |d| %>
7
- <%= link_to d, day_path(d),
8
- class: "b270-cell #{b270_day_status_class(current_reader, d)} #{'now' if d == today}" %>
7
+ <%= b270_day_cell(d, reader: current_reader, today: today) %>
9
8
  <% end %>
10
9
  </div>
10
+ <p class="b270-gridnote">Dot indicates days with reflections.</p>
11
11
  </details>
12
12
  <% end %>
@@ -0,0 +1,16 @@
1
+ <%# locals: (reflections: true) %>
2
+ <%#
3
+ Six states need saying out loud: three degrees of progress, each of which may
4
+ also have reflections. Hovering a square shows that day's readings.
5
+
6
+ reflections: false on the Reflections page, where every square has them by
7
+ definition and the entry would explain nothing.
8
+ %>
9
+ <p class="b270-legend">
10
+ <span><i></i> not yet read</span>
11
+ <span><i class="partial"></i> partly read</span>
12
+ <span><i class="complete"></i> finished</span>
13
+ <% if reflections %>
14
+ <span><i class="talk"></i> has reflections</span>
15
+ <% end %>
16
+ </p>
@@ -9,8 +9,10 @@
9
9
  <% end %>
10
10
  </div>
11
11
  <nav class="b270-nav">
12
- <%= link_to "Plan", root_path %>
12
+ <!--<%= link_to "Plan", root_path %>-->
13
13
  <%= link_to "People", community_path %>
14
+ <%# Public, like the community page: anyone may read reflections. %>
15
+ <%= link_to "Reflections", reflections_path %>
14
16
  <% if signed_in? %>
15
17
  <% if Bible270.config.admin?(current_reader) %><%= link_to "Admin", admin_path %><% end %>
16
18
  <%= link_to "Progress", progress_path %>
@@ -54,7 +54,7 @@
54
54
  .b270-flash.notice{background:#e7efe6;color:#2f5d34;border:1px solid #c6dcc6}
55
55
 
56
56
  .b270-eyebrow{font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--gold);font-weight:600;margin:0 0 10px}
57
- .b270 h1{font-family:"Spectral",serif;font-weight:600;font-size:clamp(28px,6vw,42px);line-height:1.03;letter-spacing:-.01em;margin:0 0 6px}
57
+ .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}
58
58
  .b270 h1 em{font-style:italic;color:var(--muted);font-weight:400}
59
59
  .b270 .lede{color:var(--muted);max-width:77ch;margin:0 0 24px;font-size:15px}
60
60
 
@@ -76,7 +76,7 @@
76
76
  .b270-startform{display:flex;align-items:center;gap:10px;margin-top:10px;flex-wrap:wrap}
77
77
  .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)}
78
78
  .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}
79
- .b270-hint{margin:8px 0 0;font-size:12px;color:var(--faint);max-width:52ch}
79
+ .b270-hint{margin:8px 0 0;font-size:12px;color:var(--faint);max-width:111ch}
80
80
  .b270-tracks{margin-top:16px;display:grid;gap:11px}
81
81
  .b270-tm{display:grid;grid-template-columns:130px 1fr auto;align-items:center;gap:12px;font-size:12.5px}
82
82
  .b270-tm .lab{color:var(--muted);display:flex;align-items:center;gap:8px}
@@ -163,6 +163,14 @@
163
163
  .b270-mention:hover{text-decoration:underline}
164
164
  .b270-creply{margin-top:6px}
165
165
  .b270-cbody{margin:3px 0 0;white-space:pre-wrap}
166
+ .b270-editform{margin-top:6px}
167
+ .b270-editform textarea{min-height:64px}
168
+ .b270-editerror{margin:0 0 8px;font-size:13px;color:var(--oxblood)}
169
+ .b270-cedit{font-size:12px;color:var(--muted);margin-left:8px}
170
+ .b270-cedit:hover{color:var(--gold)}
171
+ .b270-edited{font-style:italic;color:var(--faint)}
172
+ .b270-comment.b270-editing{background:rgba(165,129,44,.05);border-radius:10px;padding:12px}
173
+ .b270-editing textarea{min-height:64px}
166
174
  .b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
167
175
  .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}
168
176
  .b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
@@ -185,8 +193,27 @@
185
193
  .b270-cell:hover{border-color:var(--gold);color:var(--gold)}
186
194
  .b270-cell.partial{background:linear-gradient(135deg,var(--card) 50%,rgba(165,129,44,.35) 50%);color:var(--muted)}
187
195
  .b270-cell.complete{background:var(--teal);border-color:var(--teal);color:#eef3f2;font-weight:600}
196
+ /* Days that have reflections carry a dot and nothing else: the fill stays
197
+ free to mean reading progress, so the two facts never compete. On a finished
198
+ square the dot goes pale, since oxblood on teal is hard to pick out. */
199
+ .b270-cell.talk{position:relative}
200
+ .b270-cell.talk::after{content:"";position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:50%;background:var(--oxblood)}
201
+ .b270-cell.talk.complete::after{background:#f4efe6}
188
202
  .b270-cell.now{box-shadow:0 0 0 2px var(--gold)}
189
203
 
204
+ /* On the Reflections page each thread carries the day it belongs to, since
205
+ there is no day heading above it. */
206
+ .b270-thread{margin-bottom:8px}
207
+ .b270-threadday{margin:0;font-size:12px;letter-spacing:.04em;color:var(--faint)}
208
+ .b270-threadday a{color:var(--gold);font-weight:600;text-decoration:none}
209
+ .b270-threadday a:hover{text-decoration:underline}
210
+ /* Scripture references stay undecorated, hover included, so a line of three of
211
+ them does not read as a row of buttons. */
212
+ .b270-threadday .b270-reflink,
213
+ .b270-threadday .b270-reflink:hover{color:var(--muted);font-weight:400;text-decoration:none}
214
+ .b270-threadday .b270-reflink:hover{color:var(--ink)}
215
+ .b270-gridnote{margin:10px 0 0;font-size:11.5px;color:var(--faint)}
216
+
190
217
  .b270-footer{margin-top:20px;padding-top:11px;border-top:1px solid var(--line-soft);color:var(--faint);font-size:13px}
191
218
  .b270-footer p{margin:0;line-height:1.5}
192
219
  details.b270-index summary{cursor:pointer;font-family:"Spectral",serif;font-size:18px;font-weight:600;list-style:none;margin-top:30px}
data/config/routes.rb CHANGED
@@ -12,6 +12,7 @@ Bible270::Engine.routes.draw do
12
12
  constraints: { day: %r{\d+} }
13
13
  post 'day/:day/comments', to: 'comments#create', as: :day_comments,
14
14
  constraints: { day: %r{\d+} }
15
+ patch 'comments/:id', to: 'comments#update'
15
16
  delete 'comments/:id', to: 'comments#destroy', as: :comment
16
17
 
17
18
  # Admin panel — 404s unless config.admin_emails / admin_resolver allows you.
@@ -34,6 +35,8 @@ Bible270::Engine.routes.draw do
34
35
  patch 'profile', to: 'profiles#update'
35
36
  delete 'profile/avatar', to: 'profiles#remove_avatar', as: :profile_avatar
36
37
 
38
+ get 'reflections', to: 'comments#index', as: :reflections
39
+
37
40
  get 'progress', to: 'readers#progress', as: :progress
38
41
 
39
42
  get 'community', to: 'readers#index', as: :community
@@ -292,7 +292,7 @@ module Bible270
292
292
  # from, so without this the notice names the reader but cannot link to them.
293
293
  attr_accessor :mailer_host
294
294
 
295
- 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
295
+ 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
296
 
297
297
  # Public labels.
298
298
  attr_accessor :app_name
@@ -331,7 +331,9 @@ module Bible270
331
331
 
332
332
  # Email a reader when someone writes "@them" in a reflection. Readers can opt
333
333
  # out individually on their profile; this switches the whole feature off.
334
- attr_accessor :mention_notifications
334
+ # How many recent threads the Reflections page shows. A thread resurfaces when
335
+ # someone replies to it, so this is a count of conversations, not of messages.
336
+ attr_accessor :reflections_page_size
335
337
 
336
338
  def remember_signed_in?
337
339
  remember_for.to_i.positive?
@@ -354,6 +356,7 @@ module Bible270
354
356
  @app_name = 'Daily Bread'
355
357
  @tagline = 'Journeying through Scripture in 9 months'
356
358
  @time_zone = nil
359
+ @reflections_page_size = 10
357
360
  @mention_notifications = true
358
361
  @header_mark = nil
359
362
  @remember_for = 365 * 24 * 60 * 60
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bible270
4
- VERSION = '0.16.1'
4
+ VERSION = '0.17.0'
5
5
  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.16.1
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft
@@ -103,9 +103,14 @@ files:
103
103
  - app/views/bible270/admin/show.html.erb
104
104
  - app/views/bible270/checkoffs/toggle.turbo_stream.erb
105
105
  - app/views/bible270/comments/_comment.html.erb
106
+ - app/views/bible270/comments/_edit_fields.html.erb
107
+ - app/views/bible270/comments/_edit_form.html.erb
106
108
  - app/views/bible270/comments/_form.html.erb
107
109
  - app/views/bible270/comments/create.turbo_stream.erb
108
110
  - app/views/bible270/comments/destroy.turbo_stream.erb
111
+ - app/views/bible270/comments/edit.turbo_stream.erb
112
+ - app/views/bible270/comments/index.html.erb
113
+ - app/views/bible270/comments/update.turbo_stream.erb
109
114
  - app/views/bible270/days/_reading.html.erb
110
115
  - app/views/bible270/days/_start_date.html.erb
111
116
  - app/views/bible270/days/index.html.erb
@@ -121,6 +126,7 @@ files:
121
126
  - app/views/bible270/sessions/new.html.erb
122
127
  - app/views/bible270/shared/_day_index.html.erb
123
128
  - app/views/bible270/shared/_footer.html.erb
129
+ - app/views/bible270/shared/_grid_legend.html.erb
124
130
  - app/views/bible270/shared/_header.html.erb
125
131
  - app/views/bible270/shared/_progress.html.erb
126
132
  - app/views/bible270/shared/_reflections.html.erb