bible270 0.16.1 → 0.17.1

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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/bible270/comments_controller.rb +70 -1
  3. data/app/controllers/bible270/days_controller.rb +9 -0
  4. data/app/controllers/bible270/likes_controller.rb +22 -0
  5. data/app/helpers/bible270/plan_helper.rb +83 -0
  6. data/app/models/bible270/comment.rb +50 -2
  7. data/app/models/bible270/like.rb +14 -0
  8. data/app/models/bible270/reader.rb +1 -0
  9. data/app/views/bible270/admin/comments.html.erb +1 -1
  10. data/app/views/bible270/admin/index.html.erb +1 -1
  11. data/app/views/bible270/admin/show.html.erb +2 -1
  12. data/app/views/bible270/comments/_comment.html.erb +34 -10
  13. data/app/views/bible270/comments/_edit_fields.html.erb +25 -0
  14. data/app/views/bible270/comments/_edit_form.html.erb +7 -0
  15. data/app/views/bible270/comments/edit.turbo_stream.erb +3 -0
  16. data/app/views/bible270/comments/index.html.erb +52 -0
  17. data/app/views/bible270/comments/update.turbo_stream.erb +4 -0
  18. data/app/views/bible270/days/index.html.erb +2 -2
  19. data/app/views/bible270/likes/_likes.html.erb +27 -0
  20. data/app/views/bible270/likes/toggle.turbo_stream.erb +3 -0
  21. data/app/views/bible270/notice_mailer/new_reader.html.erb +1 -1
  22. data/app/views/bible270/notice_mailer/new_reader.text.erb +1 -1
  23. data/app/views/bible270/readers/index.html.erb +1 -1
  24. data/app/views/bible270/readers/progress.html.erb +2 -2
  25. data/app/views/bible270/readers/show.html.erb +2 -1
  26. data/app/views/bible270/shared/_day_index.html.erb +2 -2
  27. data/app/views/bible270/shared/_grid_legend.html.erb +16 -0
  28. data/app/views/bible270/shared/_header.html.erb +4 -2
  29. data/app/views/bible270/shared/_reflections.html.erb +1 -1
  30. data/app/views/bible270/shared/_styles.html.erb +40 -2
  31. data/config/routes.rb +4 -0
  32. data/db/migrate/20260101000013_create_bible270_likes.rb +21 -0
  33. data/lib/bible270/configuration.rb +5 -2
  34. data/lib/bible270/version.rb +1 -1
  35. data/lib/bible270.rb +18 -4
  36. metadata +12 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f5518f1eb25a21f7610ea80c8cd46e1d49690716bf98b5644fb3fa443f920a0e
4
- data.tar.gz: 384ec828a091ed3c71abeb71a1d5b5f7d1cb7ff707f997511ca5e6d411cc5095
3
+ metadata.gz: 70091a35da0211de345d0a946f8fe25aea94c7656d7f9fb7b305aa63c6a1b7e6
4
+ data.tar.gz: 98e6db18fb16bdb784b7b2ad095b05860a43a81cb3111279f33b7285345bcbd0
5
5
  SHA512:
6
- metadata.gz: f222369a58be11fb62c8fd76be668aad9010f76f3d88e9d9820d562eefce7becbe7e9b4fd7dd4dfa5e2408c256762fdd91da44942447328fa69782698e387789
7
- data.tar.gz: c1f881c35558530bb27636a2fcd27a0f40ebb9cdc37e9f07cc10fda04aa78ddcfff65f8b210575c10640b46400616727e28ddc3b8594fd9cfa91dd19ac658a3b
6
+ metadata.gz: e03f11e9596c52e59413478a3c5c615ce5bc9f4289ecbed3a037906d51d70e515d56767d18324ba331dded40de502d0d6b7c741c603836d7e351d147375187df
7
+ data.tar.gz: f2496c1342b3be1625927fdbbecd333c1792d96c8c52742359bdfc9cb66e9de7ac0f3617b20b291ee081811e4d937a92e3dbbecc9c5ee29a3c3d0788db9f473a
@@ -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,48 @@ 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)
96
+ .includes(:reader, { likes: :reader }, { replies: [:reader, { likes: :reader }] })
97
+ .index_by(&:id)
98
+ ids.filter_map { |id| threads[id] }
99
+ end
100
+
46
101
  def comment_params
47
102
  params.require(:comment).permit(:body, :track, :parent_id)
48
103
  end
104
+
105
+ # Scoped to the reader, so someone else's reflection is simply not found —
106
+ # it reveals nothing about what exists.
107
+ def own_comment
108
+ current_reader.comments.find_by(id: params[:id])
109
+ end
110
+
111
+ # An admin may remove any reflection; everyone else only their own. Editing
112
+ # stays with the writer either way.
113
+ def deletable_comment
114
+ return own_comment unless Bible270.config.admin?(current_reader)
115
+
116
+ Comment.find_by(id: params[:id])
117
+ end
49
118
  end
50
119
  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
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bible270
4
+ # A heart on someone's reflection. One route, because liking and unliking are
5
+ # the same gesture — tapping the heart again takes it back.
6
+ class LikesController < ApplicationController
7
+ def toggle
8
+ return unless require_reader!
9
+
10
+ @comment = Comment.approved.find_by(id: params[:id])
11
+ head :not_found and return unless @comment
12
+
13
+ @comment.toggle_like!(current_reader)
14
+ @comment.reload
15
+
16
+ respond_to do |format|
17
+ format.turbo_stream
18
+ format.html { redirect_to day_path(@comment.day, anchor: "comment-#{@comment.id}") }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -137,6 +137,89 @@ module Bible270
137
137
  end
138
138
  end
139
139
 
140
+ # Timestamps in the plan's zone rather than UTC. Dates — started_on and the
141
+ # like — need no conversion and are formatted inline.
142
+ def b270_date(time)
143
+ local = Bible270.local_time(time)
144
+ local&.strftime('%b %-d, %Y')
145
+ end
146
+
147
+ def b270_datetime(time)
148
+ local = Bible270.local_time(time)
149
+ local&.strftime('%b %-d, %Y at %-I:%M %p')
150
+ end
151
+
152
+ # Only the writer may change their own words. An admin can remove a reflection
153
+ # but not rewrite it: putting words in someone's mouth is worse than taking
154
+ # them away, and taking them away is already the moderator's job.
155
+ def b270_can_edit?(comment)
156
+ signed_in? && comment.reader_id == current_reader.id
157
+ end
158
+
159
+ def b270_can_delete?(comment)
160
+ return false unless signed_in?
161
+
162
+ comment.reader_id == current_reader.id || Bible270.config.admin?(current_reader)
163
+ end
164
+
165
+ # One cell of the 270-day grid. Four views drew this by hand in four slightly
166
+ # different ways; now they all call this.
167
+ #
168
+ # The title attribute carries the readings, so hovering a square says what is
169
+ # on that day — no JavaScript, and it works for keyboard focus too.
170
+ def b270_day_cell(day, reader: nil, today: nil)
171
+ link_to day, day_path(day),
172
+ class: b270_day_cell_classes(day, reader: reader, today: today),
173
+ title: b270_day_readings(day)
174
+ end
175
+
176
+ # Shared with the admin grid, which is a button_to rather than a link because
177
+ # it toggles the day rather than navigating to it.
178
+ def b270_day_cell_classes(day, reader: nil, today: nil)
179
+ classes = ['b270-cell', b270_day_status_class(reader, day)]
180
+ classes << 'talk' if b270_days_with_reflections.include?(day)
181
+ classes << 'now' if today && day == today
182
+
183
+ classes.reject(&:blank?).join(' ')
184
+ end
185
+
186
+ # "Genesis 1-3 · Matthew 1 · Psalm 1" as plain text, for a title attribute.
187
+ def b270_day_readings(day)
188
+ readings = Bible270::Plan.readings_for(day)
189
+ Bible270::Plan::TRACKS.keys.filter_map { |track| readings[track] }.join(' · ')
190
+ end
191
+
192
+ # The same references as links to the passage, in the reader's own translation
193
+ # when they have chosen one — b270_passage_url falls back to the site default
194
+ # for a visitor.
195
+ def b270_day_reading_links(day)
196
+ readings = Bible270::Plan.readings_for(day)
197
+
198
+ links = Bible270::Plan::TRACKS.keys.filter_map do |track|
199
+ reference = readings[track]
200
+ next if reference.blank?
201
+
202
+ link_to reference, b270_passage_url(reference),
203
+ class: 'b270-reflink', target: '_blank', rel: 'noopener'
204
+ end
205
+
206
+ safe_join(links, ' · ')
207
+ end
208
+
209
+ # The days that have at least one visible reflection, fetched once per request.
210
+ # Called from the layout's grid, which no controller sets up, so it cannot be an
211
+ # instance variable assigned in an action.
212
+ def b270_days_with_reflections
213
+ @b270_days_with_reflections ||=
214
+ if defined?(Bible270::Comment)
215
+ Bible270::Comment.approved.distinct.pluck(:day).to_set
216
+ else
217
+ Set.new
218
+ end
219
+ rescue StandardError
220
+ Set.new
221
+ end
222
+
140
223
  def b270_day_status_class(reader, day)
141
224
  return '' unless reader
142
225
 
@@ -3,6 +3,15 @@
3
3
  module Bible270
4
4
  # A publicly visible reflection attached to a plan day (optionally a track).
5
5
  class Comment < ApplicationRecord
6
+ # One heart for both states. An unliked one is the same emoji shown grey by
7
+ # CSS, so it cannot drift in size from a real like, and hovering it previews
8
+ # exactly what clicking will produce.
9
+ #
10
+ # The white heart this replaced was legible on white but nearly invisible on
11
+ # the paper background; an outline glyph like U+2661 is drawn by the font
12
+ # rather than the emoji set and needs hand-tuning to match.
13
+ HEART = '❤️'
14
+
6
15
  self.table_name = 'bible270_comments'
7
16
 
8
17
  belongs_to :reader, class_name: 'Bible270::Reader'
@@ -12,6 +21,7 @@ module Bible270
12
21
  belongs_to :parent, class_name: 'Bible270::Comment', optional: true
13
22
  has_many :replies, class_name: 'Bible270::Comment', foreign_key: :parent_id,
14
23
  dependent: :destroy, inverse_of: :parent
24
+ has_many :likes, class_name: 'Bible270::Like', dependent: :destroy, inverse_of: :comment
15
25
 
16
26
  validates :day, inclusion: { in: 1..Plan::DAYS }
17
27
  validates :body, presence: true, length: { maximum: 4000 }
@@ -28,13 +38,18 @@ module Bible270
28
38
  # Fires once the reflection is really saved, so nothing is sent for one whose
29
39
  # transaction rolls back.
30
40
  after_create_commit :notify_mentioned_readers
41
+ # Only the people newly named: editing a reflection five times must not mail
42
+ # the same reader five times.
43
+ after_update_commit :notify_newly_mentioned_readers
31
44
 
32
45
  scope :approved, -> { where(approved: true) }
33
46
  scope :hidden, -> { where(approved: false) }
34
47
  # Only the reflections themselves: replies are rendered under their parent, so
35
48
  # listing them again at the top level would show each twice.
36
49
  scope :for_day, ->(d) { approved.where(day: d, parent_id: nil).order(created_at: :asc) }
37
- scope :threads_for_day, ->(d) { for_day(d).includes(:reader, replies: :reader) }
50
+ scope :threads_for_day, ->(d) {
51
+ for_day(d).includes(:reader, { likes: :reader }, { replies: [:reader, { likes: :reader }] })
52
+ }
38
53
  scope :recent, -> { approved.order(created_at: :desc) }
39
54
 
40
55
  # Every reflection is visible when written; these are the moderation actions.
@@ -43,6 +58,31 @@ module Bible270
43
58
  def hidden? = !approved
44
59
  def reply? = parent_id.present?
45
60
 
61
+ def liked_by?(reader)
62
+ return false if reader.nil?
63
+
64
+ likes.any? { |like| like.reader_id == reader.id }
65
+ end
66
+
67
+ # Toggling returns the new state, so a caller can say what happened without
68
+ # asking again.
69
+ def toggle_like!(reader)
70
+ existing = likes.find { |like| like.reader_id == reader.id }
71
+ if existing
72
+ existing.destroy
73
+ likes.reload
74
+ false
75
+ else
76
+ likes.create(reader: reader)
77
+ likes.reload
78
+ true
79
+ end
80
+ end
81
+
82
+ # A second's grace: created_at and updated_at differ by microseconds on
83
+ # insert, which would mark every reflection as edited.
84
+ def edited? = updated_at - created_at > 1
85
+
46
86
  # Visible replies, oldest first: a conversation reads forwards.
47
87
  def visible_replies = replies.approved.order(created_at: :asc)
48
88
  def moderated? = moderated_at.present?
@@ -70,11 +110,19 @@ module Bible270
70
110
 
71
111
  # A mention emails the person named. Failures are logged rather than raised:
72
112
  # nobody should lose a reflection because the mail server is down.
73
- def notify_mentioned_readers
113
+ def notify_newly_mentioned_readers
114
+ return unless saved_change_to_body?
115
+
116
+ already = Reader.mentioned_in(body_before_last_save).map(&:id)
117
+ notify_mentioned_readers(except: already)
118
+ end
119
+
120
+ def notify_mentioned_readers(except: [])
74
121
  return unless Bible270.config.mention_notifications
75
122
 
76
123
  Reader.mentioned_in(body).each do |mentioned|
77
124
  next if mentioned.id == reader_id
125
+ next if except.include?(mentioned.id)
78
126
  next unless mentioned.wants_mention_notices?
79
127
 
80
128
  notice = NoticeMailer.mentioned(comment_id: id, reader_id: mentioned.id)
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bible270
4
+ # A reader's heart on a reflection. There is nothing to a like but who gave it
5
+ # and when — the count is the number of rows, and the hover text is the name.
6
+ class Like < ApplicationRecord
7
+ self.table_name = 'bible270_likes'
8
+
9
+ belongs_to :reader, class_name: 'Bible270::Reader'
10
+ belongs_to :comment, class_name: 'Bible270::Comment'
11
+
12
+ validates :reader_id, uniqueness: { scope: :comment_id }
13
+ end
14
+ end
@@ -8,6 +8,7 @@ module Bible270
8
8
 
9
9
  has_many :checkoffs, class_name: 'Bible270::Checkoff', dependent: :destroy
10
10
  has_many :comments, class_name: 'Bible270::Comment', dependent: :destroy
11
+ has_many :likes, class_name: 'Bible270::Like', dependent: :destroy, inverse_of: :reader
11
12
  belongs_to :owner, polymorphic: true, optional: true
12
13
 
13
14
  # Active Storage is optional: an app may have it disabled, and the engine has
@@ -18,7 +18,7 @@
18
18
  <div class="b270-cmeta">
19
19
  <strong><%= comment.reader.display_name %></strong>
20
20
  · <%= link_to "day #{comment.day}", day_path(comment.day) %>
21
- · <%= comment.created_at.strftime('%b %-d, %Y') %>
21
+ · <%= b270_date(comment.created_at) %>
22
22
  <% if comment.reply? %>· reply to <strong><%= comment.parent&.reader&.display_name || 'a deleted reflection' %></strong><% end %>
23
23
  <% if comment.hidden? %><span class="b270-badge">Hidden</span><% end %>
24
24
  </div>
@@ -9,7 +9,7 @@
9
9
  <% if @enrollment_closed %>
10
10
  <% if @enrollment_closed_at %>
11
11
  <p class="b270-startline">Enrollment closed
12
- on <%= @enrollment_closed_at.strftime('%B %-d, %Y') %><% end %>.
12
+ on <%= b270_date(@enrollment_closed_at) %><% end %>.
13
13
  The <%= @readers.size %> reader<%= 's' unless @readers.size == 1 %> already taking part can
14
14
  still sign in and carry on.</p>
15
15
  <%= button_to 'Open to new readers', admin_enrollment_path, method: :patch,
@@ -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,28 +1,52 @@
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">
5
12
  <div class="b270-cmeta">
6
13
  <strong><%= link_to comment.reader.display_name, reader_path(comment.reader), style: "text-decoration:none" %></strong>
7
- · <%= comment.created_at.strftime("%b %-d, %Y") %>
14
+ · <%= b270_date(comment.created_at) %>
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="<%= b270_datetime(comment.updated_at) %>">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. %>
19
- <% unless reply %>
20
- <% if signed_in? && comment.reader_id != current_reader.id %>
21
- <div class="b270-creply">
22
- <%= link_to "Reply", day_path(comment.day, reply_to: comment.id, anchor: "new_comment_form"),
23
- class: "b270-linkbtn" %>
24
- </div>
40
+ <div class="b270-cactions">
41
+ <%= render partial: "bible270/likes/likes", locals: { comment: comment } %>
42
+ <%# A reply may be liked but not replied to: one level deep. %>
43
+ <% if !reply && signed_in? && comment.reader_id != current_reader.id %>
44
+ <%= link_to "Reply", day_path(comment.day, reply_to: comment.id, anchor: "new_comment_form"),
45
+ class: "b270-linkbtn" %>
25
46
  <% end %>
47
+ </div>
48
+
49
+ <% unless reply %>
26
50
 
27
51
  <div class="b270-replies" id="replies-<%= comment.id %>">
28
52
  <%= render partial: "bible270/comments/comment",
@@ -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>Shared gleanings</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>
@@ -0,0 +1,27 @@
1
+ <%# locals: (comment:) %>
2
+ <%#
3
+ One heart per like, each carrying its giver's name — so a row of hearts is both
4
+ the count and, on hover, the list of who.
5
+
6
+ Your own like is not drawn in the row: the button is your heart, filled, and
7
+ saying so. Drawing both gave a reader two hearts for one like.
8
+ %>
9
+ <%
10
+ liked = signed_in? && comment.liked_by?(current_reader)
11
+ others = signed_in? ? comment.likes.reject { |like| like.reader_id == current_reader.id } : comment.likes
12
+ %>
13
+ <span class="b270-likes" id="likes-<%= comment.id %>">
14
+ <% others.each do |like| %><span class="b270-heart"
15
+ title="<%= like.reader.display_name %>"><%= Bible270::Comment::HEART %></span><% end %>
16
+
17
+ <% if signed_in? %>
18
+ <%= button_to like_comment_path(comment), method: :post, class: "b270-likebtn#{' liked' if liked}",
19
+ form: { style: "display:inline", data: { turbo_stream: true } },
20
+ aria: { label: liked ? "Take back your like" : "Like this reflection" },
21
+ title: liked ? "You liked this — tap to take it back" : "Like this reflection" do %>
22
+ <%= Bible270::Comment::HEART %>
23
+ <% end %>
24
+ <% elsif comment.likes.any? %>
25
+ <span class="b270-likecount"><%= comment.likes.size %></span>
26
+ <% end %>
27
+ </span>
@@ -0,0 +1,3 @@
1
+ <%= turbo_stream.replace "likes-#{@comment.id}" do %>
2
+ <%= render partial: "bible270/likes/likes", locals: { comment: @comment } %>
3
+ <% end %>
@@ -11,7 +11,7 @@
11
11
  <tr><td style="padding:3px 12px 3px 0;color:#6d6558">Name</td><td><%= @reader.display_name %></td></tr>
12
12
  <tr><td style="padding:3px 12px 3px 0;color:#6d6558">Email</td><td><%= @reader.email.presence || '(none given)' %></td></tr>
13
13
  <tr><td style="padding:3px 12px 3px 0;color:#6d6558">Signed in with</td><td><%= @reader.provider.presence || 'the host application' %></td></tr>
14
- <tr><td style="padding:3px 12px 3px 0;color:#6d6558">Joined</td><td><%= @reader.created_at.strftime('%B %-d, %Y at %-I:%M %p') %></td></tr>
14
+ <tr><td style="padding:3px 12px 3px 0;color:#6d6558">Joined</td><td><%= Bible270.local_time(@reader.created_at).strftime('%B %-d, %Y at %-I:%M %p') %></td></tr>
15
15
  </table>
16
16
 
17
17
  <p style="margin:18px 0 0;font-size:14px;color:#6d6558">
@@ -3,7 +3,7 @@
3
3
  Name: <%= @reader.display_name %>
4
4
  Email: <%= @reader.email.presence || '(none given)' %>
5
5
  Signed in with: <%= @reader.provider.presence || 'the host application' %>
6
- Joined: <%= @reader.created_at.strftime('%B %-d, %Y at %-I:%M %p') %>
6
+ Joined: <%= Bible270.local_time(@reader.created_at).strftime('%B %-d, %Y at %-I:%M %p') %>
7
7
 
8
8
  That makes <%= @total %> reader<%= 's' unless @total == 1 %> taking part.
9
9
  <% if @admin_url %>
@@ -11,7 +11,7 @@
11
11
  <%= b270_avatar(r, size: 40) %>
12
12
  <div>
13
13
  <div class="name"><%= link_to r.display_name, reader_path(r), style: "text-decoration:none" %></div>
14
- <div class="sub"><%= @days_completed[r.id] %> of <%= Bible270::Plan::DAYS %> days · joined <%= r.created_at.strftime("%b %Y") %></div>
14
+ <div class="sub"><%= @days_completed[r.id] %> of <%= Bible270::Plan::DAYS %> days · joined <%= Bible270.local_time(r.created_at).strftime("%b %Y") %></div>
15
15
  </div>
16
16
  <div class="stat">
17
17
  <span class="big"><%= (@days_completed[r.id].to_f / Bible270::Plan::DAYS * 100).round %>%</span>
@@ -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,12 +9,14 @@
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 "Posts", reflections_path %>
14
16
  <% if signed_in? %>
15
- <% if Bible270.config.admin?(current_reader) %><%= link_to "Admin", admin_path %><% end %>
16
17
  <%= link_to "Progress", progress_path %>
17
18
  <%= link_to "Profile", profile_path %>
19
+ <% if Bible270.config.admin?(current_reader) %><%= link_to "Admin", admin_path %><% end %>
18
20
  <%= button_to "Sign out", sign_out_path, method: :delete, class: "b270-linkbtn",
19
21
  form: { style: "display:inline" } %>
20
22
  <% elsif Bible270.config.any_sign_in_method? %>
@@ -19,7 +19,7 @@
19
19
  <div style="flex:1">
20
20
  <div class="b270-cmeta">
21
21
  <%= link_to "Day #{comment.day}", day_path(comment.day) %>
22
- · <%= comment.created_at.strftime("%b %-d, %Y") %>
22
+ · <%= b270_date(comment.created_at) %>
23
23
  <% if moderate && comment.hidden? %><span class="b270-badge">Hidden</span><% end %>
24
24
  </div>
25
25
 
@@ -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}
@@ -161,8 +161,27 @@
161
161
  .b270-replying strong{color:var(--ink)}
162
162
  .b270-mention{color:var(--teal);font-weight:600;text-decoration:none}
163
163
  .b270-mention:hover{text-decoration:underline}
164
+ .b270-cactions{display:flex;align-items:center;gap:12px;margin-top:6px;flex-wrap:wrap}
165
+ .b270-likes{display:inline-flex;align-items:center;gap:2px}
166
+ .b270-heart{font-size:13px;line-height:1;cursor:default}
167
+ /* Same size as a heart in the row: the button is one of the hearts, not a
168
+ control sitting beside them. Unliked, it is the same emoji desaturated —
169
+ visible against the paper, and hovering shows what clicking will give. */
170
+ .b270-likebtn{background:none;border:0;padding:0 2px;font-size:13px;line-height:1;cursor:pointer;
171
+ filter:grayscale(1);opacity:.5;transition:filter .12s,opacity .12s}
172
+ .b270-likebtn:hover{filter:none;opacity:1}
173
+ .b270-likebtn.liked{filter:none;opacity:1}
174
+ .b270-likecount{font-size:11.5px;color:var(--faint);margin-left:2px}
164
175
  .b270-creply{margin-top:6px}
165
176
  .b270-cbody{margin:3px 0 0;white-space:pre-wrap}
177
+ .b270-editform{margin-top:6px}
178
+ .b270-editform textarea{min-height:64px}
179
+ .b270-editerror{margin:0 0 8px;font-size:13px;color:var(--oxblood)}
180
+ .b270-cedit{font-size:12px;color:var(--muted);margin-left:8px}
181
+ .b270-cedit:hover{color:var(--gold)}
182
+ .b270-edited{font-style:italic;color:var(--faint)}
183
+ .b270-comment.b270-editing{background:rgba(165,129,44,.05);border-radius:10px;padding:12px}
184
+ .b270-editing textarea{min-height:64px}
166
185
  .b270-cdel{font-size:12px;color:var(--oxblood);background:none;border:none;cursor:pointer;padding:0;margin-left:8px}
167
186
  .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
187
  .b270-form .row{display:flex;align-items:center;gap:10px;margin-top:10px}
@@ -185,8 +204,27 @@
185
204
  .b270-cell:hover{border-color:var(--gold);color:var(--gold)}
186
205
  .b270-cell.partial{background:linear-gradient(135deg,var(--card) 50%,rgba(165,129,44,.35) 50%);color:var(--muted)}
187
206
  .b270-cell.complete{background:var(--teal);border-color:var(--teal);color:#eef3f2;font-weight:600}
207
+ /* Days that have reflections carry a dot and nothing else: the fill stays
208
+ free to mean reading progress, so the two facts never compete. On a finished
209
+ square the dot goes pale, since oxblood on teal is hard to pick out. */
210
+ .b270-cell.talk{position:relative}
211
+ .b270-cell.talk::after{content:"";position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:50%;background:var(--oxblood)}
212
+ .b270-cell.talk.complete::after{background:#f4efe6}
188
213
  .b270-cell.now{box-shadow:0 0 0 2px var(--gold)}
189
214
 
215
+ /* On the Reflections page each thread carries the day it belongs to, since
216
+ there is no day heading above it. */
217
+ .b270-thread{margin-bottom:8px}
218
+ .b270-threadday{margin:0;font-size:12px;letter-spacing:.04em;color:var(--faint)}
219
+ .b270-threadday a{color:var(--gold);font-weight:600;text-decoration:none}
220
+ .b270-threadday a:hover{text-decoration:underline}
221
+ /* Scripture references stay undecorated, hover included, so a line of three of
222
+ them does not read as a row of buttons. */
223
+ .b270-threadday .b270-reflink,
224
+ .b270-threadday .b270-reflink:hover{color:var(--muted);font-weight:400;text-decoration:none}
225
+ .b270-threadday .b270-reflink:hover{color:var(--ink)}
226
+ .b270-gridnote{margin:10px 0 0;font-size:11.5px;color:var(--faint)}
227
+
190
228
  .b270-footer{margin-top:20px;padding-top:11px;border-top:1px solid var(--line-soft);color:var(--faint);font-size:13px}
191
229
  .b270-footer p{margin:0;line-height:1.5}
192
230
  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,8 @@ 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
+ post 'comments/:id/like', to: 'likes#toggle', as: :like_comment
16
+ patch 'comments/:id', to: 'comments#update'
15
17
  delete 'comments/:id', to: 'comments#destroy', as: :comment
16
18
 
17
19
  # Admin panel — 404s unless config.admin_emails / admin_resolver allows you.
@@ -34,6 +36,8 @@ Bible270::Engine.routes.draw do
34
36
  patch 'profile', to: 'profiles#update'
35
37
  delete 'profile/avatar', to: 'profiles#remove_avatar', as: :profile_avatar
36
38
 
39
+ get 'reflections', to: 'comments#index', as: :reflections
40
+
37
41
  get 'progress', to: 'readers#progress', as: :progress
38
42
 
39
43
  get 'community', to: 'readers#index', as: :community
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateBible270Likes < ActiveRecord::Migration[7.0]
4
+ def change
5
+ create_table :bible270_likes do |t|
6
+ t.references :reader, null: false, index: false,
7
+ foreign_key: { to_table: :bible270_readers }
8
+ t.references :comment, null: false, index: false,
9
+ foreign_key: { to_table: :bible270_comments }
10
+ t.timestamps
11
+ end
12
+
13
+ # One like per reader per reflection, enforced by the database as well as the
14
+ # model: a double-tap should not become two hearts.
15
+ add_index :bible270_likes, %i[reader_id comment_id], unique: true,
16
+ name: 'idx_b270_likes_unique'
17
+ # Reading a thread asks for a comment's likes, so that is the index that earns
18
+ # its keep.
19
+ add_index :bible270_likes, :comment_id, name: 'idx_b270_likes_comment'
20
+ end
21
+ end
@@ -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.1'
5
5
  end
data/lib/bible270.rb CHANGED
@@ -22,10 +22,24 @@ module_function
22
22
  # it; Date.today follows the machine. A self-hosted reading plan wants the
23
23
  # latter, so that is the default, with config.time_zone to override.
24
24
  def today
25
- zone = config.time_zone
26
- return Date.today if zone.nil? || zone.to_s.empty?
25
+ local_time(Time.now).to_date
26
+ end
27
27
 
28
- found = Time.respond_to?(:find_zone) ? Time.find_zone(zone) : nil
29
- found ? found.today : Date.today
28
+ # A stored timestamp in the plan's own zone.
29
+ #
30
+ # created_at and friends come back in Rails' Time.zone, which is UTC unless the
31
+ # host app sets it — so a reflection written at 5pm in Los Angeles was being
32
+ # shown as the next day. This is the same rule Bible270.today follows: the
33
+ # machine's zone, or config.time_zone when one is given.
34
+ def local_time(time)
35
+ return nil if time.nil?
36
+
37
+ zone = config.time_zone
38
+ if zone.to_s.empty?
39
+ time.getlocal
40
+ else
41
+ found = Time.respond_to?(:find_zone) ? Time.find_zone(zone) : nil
42
+ found ? time.in_time_zone(found) : time.getlocal
43
+ end
30
44
  end
31
45
  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.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew vonderLuft
@@ -85,6 +85,7 @@ files:
85
85
  - app/controllers/bible270/checkoffs_controller.rb
86
86
  - app/controllers/bible270/comments_controller.rb
87
87
  - app/controllers/bible270/days_controller.rb
88
+ - app/controllers/bible270/likes_controller.rb
88
89
  - app/controllers/bible270/profiles_controller.rb
89
90
  - app/controllers/bible270/readers_controller.rb
90
91
  - app/controllers/bible270/sessions_controller.rb
@@ -95,6 +96,7 @@ files:
95
96
  - app/models/bible270/application_record.rb
96
97
  - app/models/bible270/checkoff.rb
97
98
  - app/models/bible270/comment.rb
99
+ - app/models/bible270/like.rb
98
100
  - app/models/bible270/reader.rb
99
101
  - app/models/bible270/setting.rb
100
102
  - app/models/bible270/sign_in_token.rb
@@ -103,13 +105,20 @@ files:
103
105
  - app/views/bible270/admin/show.html.erb
104
106
  - app/views/bible270/checkoffs/toggle.turbo_stream.erb
105
107
  - app/views/bible270/comments/_comment.html.erb
108
+ - app/views/bible270/comments/_edit_fields.html.erb
109
+ - app/views/bible270/comments/_edit_form.html.erb
106
110
  - app/views/bible270/comments/_form.html.erb
107
111
  - app/views/bible270/comments/create.turbo_stream.erb
108
112
  - app/views/bible270/comments/destroy.turbo_stream.erb
113
+ - app/views/bible270/comments/edit.turbo_stream.erb
114
+ - app/views/bible270/comments/index.html.erb
115
+ - app/views/bible270/comments/update.turbo_stream.erb
109
116
  - app/views/bible270/days/_reading.html.erb
110
117
  - app/views/bible270/days/_start_date.html.erb
111
118
  - app/views/bible270/days/index.html.erb
112
119
  - app/views/bible270/days/show.html.erb
120
+ - app/views/bible270/likes/_likes.html.erb
121
+ - app/views/bible270/likes/toggle.turbo_stream.erb
113
122
  - app/views/bible270/notice_mailer/mentioned.html.erb
114
123
  - app/views/bible270/notice_mailer/mentioned.text.erb
115
124
  - app/views/bible270/notice_mailer/new_reader.html.erb
@@ -121,6 +130,7 @@ files:
121
130
  - app/views/bible270/sessions/new.html.erb
122
131
  - app/views/bible270/shared/_day_index.html.erb
123
132
  - app/views/bible270/shared/_footer.html.erb
133
+ - app/views/bible270/shared/_grid_legend.html.erb
124
134
  - app/views/bible270/shared/_header.html.erb
125
135
  - app/views/bible270/shared/_progress.html.erb
126
136
  - app/views/bible270/shared/_reflections.html.erb
@@ -142,6 +152,7 @@ files:
142
152
  - db/migrate/20260101000010_add_remember_token_to_bible270_readers.rb
143
153
  - db/migrate/20260101000011_add_mention_notices_to_bible270_readers.rb
144
154
  - db/migrate/20260101000012_add_parent_to_bible270_comments.rb
155
+ - db/migrate/20260101000013_create_bible270_likes.rb
145
156
  - lib/bible270.rb
146
157
  - lib/bible270/avatars.rb
147
158
  - lib/bible270/configuration.rb