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.
- checksums.yaml +4 -4
- data/app/controllers/bible270/comments_controller.rb +70 -1
- data/app/controllers/bible270/days_controller.rb +9 -0
- data/app/controllers/bible270/likes_controller.rb +22 -0
- data/app/helpers/bible270/plan_helper.rb +83 -0
- data/app/models/bible270/comment.rb +50 -2
- data/app/models/bible270/like.rb +14 -0
- data/app/models/bible270/reader.rb +1 -0
- data/app/views/bible270/admin/comments.html.erb +1 -1
- data/app/views/bible270/admin/index.html.erb +1 -1
- data/app/views/bible270/admin/show.html.erb +2 -1
- data/app/views/bible270/comments/_comment.html.erb +34 -10
- data/app/views/bible270/comments/_edit_fields.html.erb +25 -0
- data/app/views/bible270/comments/_edit_form.html.erb +7 -0
- data/app/views/bible270/comments/edit.turbo_stream.erb +3 -0
- data/app/views/bible270/comments/index.html.erb +52 -0
- data/app/views/bible270/comments/update.turbo_stream.erb +4 -0
- data/app/views/bible270/days/index.html.erb +2 -2
- data/app/views/bible270/likes/_likes.html.erb +27 -0
- data/app/views/bible270/likes/toggle.turbo_stream.erb +3 -0
- data/app/views/bible270/notice_mailer/new_reader.html.erb +1 -1
- data/app/views/bible270/notice_mailer/new_reader.text.erb +1 -1
- data/app/views/bible270/readers/index.html.erb +1 -1
- data/app/views/bible270/readers/progress.html.erb +2 -2
- data/app/views/bible270/readers/show.html.erb +2 -1
- data/app/views/bible270/shared/_day_index.html.erb +2 -2
- data/app/views/bible270/shared/_grid_legend.html.erb +16 -0
- data/app/views/bible270/shared/_header.html.erb +4 -2
- data/app/views/bible270/shared/_reflections.html.erb +1 -1
- data/app/views/bible270/shared/_styles.html.erb +40 -2
- data/config/routes.rb +4 -0
- data/db/migrate/20260101000013_create_bible270_likes.rb +21 -0
- data/lib/bible270/configuration.rb +5 -2
- data/lib/bible270/version.rb +1 -1
- data/lib/bible270.rb +18 -4
- metadata +12 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70091a35da0211de345d0a946f8fe25aea94c7656d7f9fb7b305aa63c6a1b7e6
|
|
4
|
+
data.tar.gz: 98e6db18fb16bdb784b7b2ad095b05860a43a81cb3111279f33b7285345bcbd0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 =
|
|
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) {
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
14
|
+
· <%= b270_date(comment.created_at) %>
|
|
8
15
|
<% if comment.track.present? %>· <%= b270_track(comment.track)[:label] %><% end %>
|
|
9
|
-
<% if
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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,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 %>
|
|
@@ -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
|
-
<%
|
|
19
|
-
<div class="sub"><%= @days_completed[r.id] %> <%=
|
|
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>
|
|
@@ -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
|
-
<%=
|
|
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
|
-
<%=
|
|
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
|
-
<%=
|
|
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
|
-
|
|
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
|
|
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(
|
|
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:
|
|
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
|
-
|
|
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
|
data/lib/bible270/version.rb
CHANGED
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
|
-
|
|
26
|
-
|
|
25
|
+
local_time(Time.now).to_date
|
|
26
|
+
end
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
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.
|
|
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
|