Skip to content

Commit 3ab42af

Browse files
authored
Adds for later list items for members (#2020)
# What it does This adds a saved for later page as well as buttons for adding/removing items from a member's saved for later list. I think this is a reasonable starting point for this feature and I'm assuming it'll change a fair bit as we gather feedback before merging it in (assuming we merge this in and don't just make a new PR, depending on the feedback). # Why it is important I know some idea of this has been mentioned in stakeholder meetings because some users will use holds to remind themselves of items they need but don't otherwise have a way to save. So they make the hold and then pass on the hold until they're ready to borrow it. This gives them a place to save those items without messing with the loaning/holding flow. # UI Change Screenshot I should note that users that are not signed in won't see any of these buttons in the interface. The for later page: <img width="1004" height="966" alt="Screenshot 2025-11-01 at 5 16 14 PM" src="https://github.com/user-attachments/assets/ac092e7f-1537-49cc-af50-762f1bef5138" /> The empty for later page: <img width="988" height="259" alt="Screenshot 2025-11-01 at 5 15 15 PM" src="https://github.com/user-attachments/assets/addd752f-7b88-4c71-8728-6e89d7fd08a8" /> The navigation: <img width="256" height="272" alt="Screenshot 2025-11-01 at 5 22 59 PM" src="https://github.com/user-attachments/assets/09fe5415-a281-436c-8298-4352e8323c15" /> Item details page when not wish listed: <img width="1001" height="544" alt="Screenshot 2025-11-01 at 5 15 41 PM" src="https://github.com/user-attachments/assets/8111d323-d6d6-41dc-8806-c2143e849327" /> Item details page when wish listed: <img width="995" height="505" alt="Screenshot 2025-11-01 at 5 15 47 PM" src="https://github.com/user-attachments/assets/47bd357c-55cf-4eab-af1f-f5ff830233d1" /> Items index page with a mix of wish listed and not wish listed items: <img width="1033" height="806" alt="Screenshot 2025-11-01 at 5 15 29 PM" src="https://github.com/user-attachments/assets/af15cfb6-7892-4e6d-8ab4-6d3951ae8ad4" /> # Implementation notes ~~I called it a "Wish List" but I don't think it would be so hard to rename the concept. The public library refers to this feature as your "For Later Shelf" but I wasn't sure that was right for this context. I also thought about calling it "favorites" but to me that implied that it would only be for tools you'd want to borrow over and over.~~ On the technical side, I implemented this with Turbo Streams so the interface just automatically updates. If someone is browsing the site without javascript turned on things should still work, they'll just be redirected when they add or remove items. In terms of styles I just use small buttons for adding/removing items. The actual wish list page rips off a lot of markup from the index page. I think a case could be made to make it a partial but imagine these could end up diverging so I didn't want to start there. **Update:** I've changed the model from `WishListItem` to `ForLaterListItem`. Right now there isn't a `ForLaterList` model but I think that would be simple enough to add later on (and convert the current model into more of a join table). There are a lot of little UI thinks to manage re multiple lists (when you click save is it a dropdown of lists? a dialog? etc) so I think it's best to wait a bit on diving into that.
1 parent 4c164f0 commit 3ab42af

20 files changed

+448
-1
lines changed

app/assets/stylesheets/application_styles.scss

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ $photo-width: 187px;
184184
justify-content: space-between;
185185
}
186186

187+
.for-later-list-button-container {
188+
margin-block-start: 4px;
189+
}
190+
187191
.item-info {
188192
flex: 1;
189193
}
@@ -453,6 +457,21 @@ $min-width: variables.$size-md + 1;
453457
h1 {
454458
margin-bottom: 0.1em;
455459
}
460+
h2 {
461+
align-items: center;
462+
display: flex;
463+
gap: 4px;
464+
}
465+
466+
#for_later_list_item_show {
467+
flex: 1;
468+
}
469+
470+
#for_later_list_item_show form {
471+
align-items: center;
472+
display: flex;
473+
justify-content: flex-end;
474+
}
456475
}
457476

458477
.landing-page {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
module Account
2+
class ForLaterListItemsController < BaseController
3+
include Pagy::Backend
4+
5+
def index
6+
scope = current_member.for_later_list_items.includes(item: [:borrow_policy, :active_holds, :checked_out_exclusive_loan, :categories]).strict_loading
7+
@pagy, @for_later_list_items = pagy(scope, items: 20)
8+
end
9+
10+
def create
11+
for_later_list_item = ForLaterListItem.new(for_later_list_item_params)
12+
for_later_list_item.member = current_member
13+
14+
for_later_list_item.save!
15+
16+
item = for_later_list_item.item
17+
18+
respond_to do |format|
19+
format.html { redirect_to item_path(for_later_list_item.item) }
20+
format.turbo_stream do
21+
render turbo_stream: [
22+
turbo_stream.replace("for_later_list_item_show", partial: "items/for_later_list_item_show", locals: {item:, for_later_list_item:}),
23+
turbo_stream.replace("#{helpers.dom_id(item)}_for_later_list_items_index", partial: "items/for_later_list_items_index", locals: {item:, for_later_list_item:})
24+
]
25+
end
26+
end
27+
end
28+
29+
def destroy
30+
for_later_list_item = current_member.for_later_list_items.find(params.expect(:id))
31+
32+
for_later_list_item.destroy!
33+
34+
item = for_later_list_item.item
35+
36+
respond_to do |format|
37+
format.html { redirect_to account_for_later_list_items_path }
38+
format.turbo_stream do
39+
render turbo_stream: [
40+
turbo_stream.remove(helpers.dom_id(for_later_list_item)),
41+
turbo_stream.replace("for_later_list_item_show", partial: "items/for_later_list_item_show", locals: {item:, for_later_list_item: nil}),
42+
turbo_stream.replace("#{helpers.dom_id(item)}_for_later_list_items_index", partial: "items/for_later_list_items_index", locals: {item:, for_later_list_item: nil})
43+
]
44+
end
45+
end
46+
end
47+
48+
private
49+
50+
def for_later_list_item_params
51+
params.expect(for_later_list_item: [:item_id])
52+
end
53+
end
54+
end

app/controllers/items_controller.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ def index
1818

1919
@categories = CategoryNode.with_items
2020
@pagy, @items = pagy(item_scope)
21+
@for_later_list_items_by_item_id = {}
22+
if user_signed_in?
23+
@for_later_list_items_by_item_id = current_member.for_later_list_items.where(item_id: @items.map(&:id)).group_by(&:item_id) || {}
24+
end
2125

2226
# Track that a search was performed if we are on the first page
2327
if @query && @pagy.page == 1

app/lib/feature_flags.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,8 @@ def self.new_appointments_page_enabled?(override = nil)
1414
def self.group_lending_enabled?
1515
ENV["FEATURE_GROUP_LENDING"] == "on"
1616
end
17+
18+
def self.for_later_lists_enabled?
19+
ENV["FOR_LATER_LISTS"] == "on"
20+
end
1721
end

app/models/for_later_list_item.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class ForLaterListItem < ApplicationRecord
2+
belongs_to :item
3+
belongs_to :member
4+
5+
validates :item, uniqueness: {scope: :member}
6+
end

app/models/item.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def next_hold
4747
has_many :tickets, dependent: :destroy
4848
has_one :last_active_ticket, -> { active.newest_first }, class_name: "Ticket"
4949

50+
has_many :for_later_list_items, -> { order(created_at: :desc) }, dependent: :destroy
51+
has_many :for_later_listed_members, through: :for_later_list_items, source: :member
52+
5053
has_one_attached :image
5154

5255
audited except: :plain_text_description

app/models/member.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ class Member < ApplicationRecord
2222

2323
belongs_to :user, optional: true
2424
has_many :notes, as: :notable
25+
has_many :for_later_list_items, -> { order(created_at: :desc) }, dependent: :destroy
26+
has_many :for_later_listed_items, through: :for_later_list_items, source: :item
2527

2628
PRONOUNS = ["he/him", "she/her", "they/them"]
2729
enum :id_kind, {drivers_license: 0, state_id: 1, city_key: 2, student_id: 3, employee_id: 4, other_id_kind: 5}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<h1 class="title">Saved for later</h1>
2+
3+
<% if @for_later_list_items.any? %>
4+
<ul class="items-list">
5+
<% @for_later_list_items.each do |for_later_list_item| %>
6+
<% item = for_later_list_item.item %>
7+
<li id="<%= dom_id(for_later_list_item) %>" class="item-container">
8+
<% if item.image.attached? %>
9+
<%= link_to item_path(item) do %>
10+
<%= image_tag item_image_url(item.image, resize_to_limit: [200, 140]), alt: "" %>
11+
<% end %>
12+
<% else %>
13+
<div class="item-image-placeholder"></div>
14+
<% end %>
15+
16+
<div class="item-content">
17+
<%= tag.div class: "item-info" do %>
18+
<strong><%= link_to item.name, item_path(item), class: "item-name", id: "item-name-#{item.id}" %></strong>
19+
<%= item_status_label(item) %>
20+
<% if item.borrow_policy.requires_approval? %>
21+
<span class="label label-secondary item-borrow-policy"><%= item.borrow_policy.code %>-Tool</span>
22+
<% end %>
23+
<% if item.active_holds.any? %>
24+
<span class="text-small"><%= pluralize item.active_holds.size, "hold" %></span>
25+
<% end %>
26+
<br>
27+
<strong><%= full_item_number(item) %></strong>
28+
<% if item.size.present? %>
29+
<span class="label"><%= item.size %></span>
30+
<% end %>
31+
<% if item.strength.present? %>
32+
<span class="label"><%= item.strength %></span>
33+
<% end %>
34+
<br>
35+
36+
<%= tag.div [item.brand, item.model].select(&:present?).join(", ") %>
37+
<%= button_to "Don't need it", account_for_later_list_item_path(for_later_list_item),
38+
method: :delete, class: "btn" %>
39+
<% end %>
40+
41+
<%= tag.div class: "item-categories" do %>
42+
<% item.categories.each do |category| %>
43+
<%= link_to category.name, {category: category.id} %>
44+
<% end %>
45+
<% end %>
46+
</div>
47+
</li>
48+
<% end %>
49+
</ul>
50+
51+
<%== pagy_bootstrap_nav(@pagy) %>
52+
<% else %>
53+
<p>Your saved for later items will appear here. You can save items by <%= link_to "checking out our inventory", items_path %>.</p>
54+
<% end %>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<% if FeatureFlags.for_later_lists_enabled? %>
2+
<span id="for_later_list_item_show">
3+
<% if for_later_list_item.present? %>
4+
<%= button_to "Don't need it", account_for_later_list_item_path(for_later_list_item),
5+
method: :delete, class: "btn" %>
6+
<% else %>
7+
<%= button_to "Save for later", account_for_later_list_items_path,
8+
method: :post, class: "btn", params: {for_later_list_item: {item_id: item.id}} %>
9+
<% end %>
10+
</span>
11+
<% end %>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<% if FeatureFlags.for_later_lists_enabled? %>
2+
<div id="<%= "#{dom_id(item)}_for_later_list_items_index" %>" class="for-later-list-button-container">
3+
<% if for_later_list_item.present? %>
4+
<%= button_to "Don't need it", account_for_later_list_item_path(for_later_list_item),
5+
method: :delete, class: "btn btn-sm" %>
6+
<% else %>
7+
<%= button_to "Save for later", account_for_later_list_items_path,
8+
method: :post, class: "btn btn-sm", params: {for_later_list_item: {item_id: item.id}} %>
9+
<% end %>
10+
</div>
11+
<% end %>

0 commit comments

Comments
 (0)