Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ This codebase (Rails 8.1)
| Directory | Purpose |
|---|---|
| `app/frontend/entrypoints/` | Vite entry points (application.js, application.css) |
| `app/frontend/javascript/controllers/` | Stimulus controllers (73) |
| `app/frontend/javascript/controllers/` | Stimulus controllers (74) |
| `app/frontend/javascript/rhino/` | Rich text editor customizations (mentions, grid) |
| `app/frontend/stylesheets/` | Tailwind CSS and component styles |

Expand Down Expand Up @@ -280,6 +280,7 @@ end
- `confirm_email` β€” Email confirmation UI
- `dirty_form` β€” Unsaved changes detection
- `dismiss` β€” Dismissable elements
- `download_all` β€” Fires a batch of file downloads from one "Download all" button (callout forms/handouts pages); new-tab items open instead of downloading
- `dropdown` β€” Dropdown menus with keyboard/click-outside handling
- `event_staff_bio` β€” Loads a selected person's read-only profile bio (with edit link) alongside the editable event-specific bio on the staff form
- `file_preview` β€” File upload preview
Expand Down
26 changes: 20 additions & 6 deletions app/controllers/events/callouts_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,26 @@ def ce
# Forms page: callout-card links to the W-9 (when requested), invoice (when
# requested), and the letter to supervisors resource β€” each opens in a new tab.
def forms
@form_cards = build_form_cards
letter = Resource.find_by(title: "Letter to Supervisors")
@form_cards = build_form_cards(letter)
@download_all_items = build_form_download_items(letter)
end

# Handouts page: callout-card links to the training worksheet/handout
# resources, in display order, each opening its own registrant resource page
# (PDF preview + download, with a back-to-ticket eyebrow).
def handouts
by_title = Resource.where(title: HANDOUT_RESOURCE_TITLES).index_by(&:title)
@handout_cards = HANDOUT_RESOURCE_TITLES.filter_map do |title|
resource = by_title[title]
next unless resource
resources = HANDOUT_RESOURCE_TITLES.filter_map { |title| by_title[title] }
@handout_cards = resources.map do |resource|
resource_card(icon: "fa-solid fa-file-pdf", title: resource.title,
subtitle: "Open this training resource",
href: registration_resource_path(@event_registration.slug, resource), target: nil)
end
@download_all_items = resources.filter_map do |resource|
next unless resource.downloadable_asset&.file&.attached?
{ href: resource_download_path(resource) }
end
end

# Registrant-facing page for a single Resource, shown in the shared callout
Expand Down Expand Up @@ -104,7 +109,7 @@ def set_event

# Builds the callout-card links shown on the forms page. The W-9 and invoice
# are always available; the letter to supervisors follows when seeded.
def build_form_cards
def build_form_cards(letter)
cards = [
resource_card(icon: "fa-solid fa-file-pdf", title: "Download W-9",
subtitle: "AWBW's W-9 tax form for your records",
Expand All @@ -113,7 +118,6 @@ def build_form_cards
subtitle: "Itemized invoice for this registration",
href: registration_invoice_path(@event_registration.slug))
]
letter = Resource.find_by(title: "Letter to Supervisors")
if letter
cards << resource_card(icon: "fa-solid fa-file-arrow-down", title: "Letter to supervisors",
subtitle: "Share to request release time",
Expand All @@ -122,6 +126,16 @@ def build_form_cards
cards
end

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: The invoice has no stored PDF (it is rendered HTML printed via window.print()), so it can't be bundled as a file download β€” newTab: true opens its print-to-PDF page instead. Everything else is a real file.

# Items for the forms "Download all" button: the W-9 and (when attached) the
# letter download directly; the invoice has no stored PDF, so it opens its
# print-to-PDF page in a new tab.
def build_form_download_items(letter)
items = [ { href: "/documents/awbw-w9.pdf" } ]
items << { href: resource_download_path(letter) } if letter&.downloadable_asset&.file&.attached?
items << { href: registration_invoice_path(@event_registration.slug), newTab: true }
items
end

# A blue callout card linking to a document. External/static links open in a
# new tab (target: "_blank"); registrant resource pages stay in-tab so the
# back-to-ticket eyebrow works (pass target: nil).
Expand Down
34 changes: 34 additions & 0 deletions app/frontend/javascript/controllers/download_all_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Controller } from "@hotwired/stimulus"

// Fires a batch of downloads from a single "Download all" button. Each item is
// { href, newTab } β€” file URLs (W-9, handout/letter PDFs) download in place via a
// temporary anchor; newTab items (the print-to-PDF invoice page, which has no
// stored file) open in a new tab. Downloads are staggered so browsers don't
// suppress the later ones in the burst.
//
// <button data-controller="download-all"
// data-download-all-items-value="[{&quot;href&quot;:&quot;/a.pdf&quot;}]"
// data-action="download-all#start">Download all</button>
export default class extends Controller {
static values = { items: Array, delay: { type: Number, default: 400 } }

start() {
this.itemsValue.forEach((item, index) => {
setTimeout(() => this.trigger(item), index * this.delayValue)
})
}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ€– From Claude: Staggered by delay ms β€” browsers suppress multiple downloads fired synchronously in one tick, so the burst is spread out.


trigger(item) {
if (item.newTab) {
window.open(item.href, "_blank", "noopener")
return
}
const link = document.createElement("a")
link.href = item.href
link.download = ""
link.rel = "noopener"
document.body.appendChild(link)
link.click()
link.remove()
}
}
3 changes: 3 additions & 0 deletions app/frontend/javascript/controllers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ application.register("dirty-form", DirtyFormController)
import DismissController from "./dismiss_controller"
application.register("dismiss", DismissController)

import DownloadAllController from "./download_all_controller"
application.register("download-all", DownloadAllController)

import FlipCardController from "./flip_card_controller"
application.register("flip-card", FlipCardController)

Expand Down
3 changes: 3 additions & 0 deletions app/views/events/callouts/_callout_page.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
<h1 class="text-xl font-semibold tracking-wide leading-tight"><%= title %></h1>
<p class="text-sm text-blue-200"><%= @event.title %></p>
</div>
<% if content_for?(:header_action) %>
<div class="shrink-0"><%= yield :header_action %></div>
<% end %>
</div>
<div class="absolute inset-x-0 bottom-0 h-1 bg-gradient-to-r from-accent via-amber-400 to-accent"></div>
</div>
Expand Down
11 changes: 11 additions & 0 deletions app/views/events/callouts/_download_all_button.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<%# "Download all" header action for callout pages that bundle several files.
items: array of { href:, newTab: } hashes (see CalloutsController). Fires each
download in turn via the download-all Stimulus controller. %>
<%= button_tag type: "button", "aria-label": "Download all",
class: "inline-flex items-center gap-2 rounded-lg bg-white/15 px-3 py-1.5 text-sm font-medium text-white ring-1 ring-inset ring-white/30 hover:bg-white/25 transition-colors",
data: { controller: "download-all",
download_all_items_value: items,
action: "download-all#start" } do %>
<i class="fa-solid fa-download"></i>
<span class="hidden sm:inline">Download all</span>
<% end %>
5 changes: 5 additions & 0 deletions app/views/events/callouts/forms.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<% content_for(:page_bg_class, "public") %>
<% content_for(:page_title, "Forms β€” #{@event.title}") %>
<% if @download_all_items.any? %>
<% content_for(:header_action) do %>
<%= render "events/callouts/download_all_button", items: @download_all_items %>
<% end %>
<% end %>
<%= render layout: "events/callouts/callout_page", locals: { title: "Forms" } do %>
<% if @form_cards.any? %>
<div class="space-y-3">
Expand Down
5 changes: 5 additions & 0 deletions app/views/events/callouts/handouts.html.erb
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<% content_for(:page_bg_class, "public") %>
<% content_for(:page_title, "Handouts β€” #{@event.title}") %>
<% if @download_all_items.any? %>
<% content_for(:header_action) do %>
<%= render "events/callouts/download_all_button", items: @download_all_items %>
<% end %>
<% end %>
<%= render layout: "events/callouts/callout_page", locals: { title: "Handouts" } do %>
<% if @handout_cards.any? %>
<div class="space-y-3">
Expand Down
48 changes: 48 additions & 0 deletions spec/requests/events/callouts_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require "rails_helper"

RSpec.describe "Events::Callouts", type: :request do
let(:event) { create(:event) }
let(:registration) { create(:event_registration, event: event) }

describe "GET /registration/:slug/forms" do
it "renders a Download all button bundling the W-9 and the invoice" do
get registration_forms_path(registration.slug)

expect(response).to have_http_status(:success)
expect(response.body).to include("download-all#start")
expect(response.body).to include("/documents/awbw-w9.pdf")
expect(response.body).to include(registration_invoice_path(registration.slug))
end

it "includes the letter download when the resource has an attached file" do
letter = create(:resource, title: "Letter to Supervisors")
create(:downloadable_asset, owner: letter)

get registration_forms_path(registration.slug)

expect(response.body).to include(resource_download_path(letter))
end
end

describe "GET /registration/:slug/handouts" do
let(:title) { Events::CalloutsController::HANDOUT_RESOURCE_TITLES.first }

it "renders a Download all button with each attached handout" do
handout = create(:resource, title: title)
create(:downloadable_asset, owner: handout)

get registration_handouts_path(registration.slug)

expect(response).to have_http_status(:success)
expect(response.body).to include("download-all#start")
expect(response.body).to include(resource_download_path(handout))
end

it "omits the Download all button when no handout has an attached file" do
get registration_handouts_path(registration.slug)

expect(response).to have_http_status(:success)
expect(response.body).not_to include("download-all#start")
end
end
end