Skip to content
Merged
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
7 changes: 7 additions & 0 deletions app/models/registration_ticket_callout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ def page_content?
description.present? || resources.any?
end

# The Payment built-in's visibility is driven entirely by live balance status,
# so the manual visibility controls (drip date, payment gate) don't apply — the
# editor hides its Visibility section.
def payment?
magic_key == "payment"
end

# Whether the callout's page content is drip-scheduled to reveal only from a
# future date. The card still shows on the ticket; the page withholds its
# content until then (see the callout show page).
Expand Down
178 changes: 109 additions & 69 deletions app/views/events/_registration_ticket_callout_fields.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,41 @@
<div class="pt-2 shrink-0 text-gray-200" title="Save the event to enable drag-reordering"><i class="fa-solid fa-grip-vertical"></i></div>
<% end %>

<div class="flex-1 min-w-0">
<%= f.check_box :published, class: "peer rounded border-gray-300 text-purple-600" %>

<div class="flex-1 min-w-0 relative">
<%#
Always-visible summary row: sits inline next to the checkbox so you
know which callout you are toggling even when it is hidden.
Published toggle, pinned to the card's top-right corner. The checkbox is
kept as a bare sibling *before* the content div below so the
`peer-checked:block` reveal still works — only its visual position is moved
(via absolute), not its DOM order. For the same reason the label is a
separate `for`-linked element rather than wrapping the input (wrapping it
would stop it being a sibling of the content div and break the reveal).
%>
<%= f.label :published, "Published", class: "absolute top-0 right-6 text-sm font-medium text-gray-600 cursor-pointer select-none" %>
<%= f.check_box :published, class: "peer rounded border-gray-300 text-purple-600 absolute top-0.5 right-0" %>

<div class="inline-flex items-center gap-x-3 gap-y-1 ml-2 text-sm align-middle"><span class="text-gray-800 font-medium truncate max-w-xs" title="<%= f.object.title %>"><%= f.object.title %></span></div>
<%#
Always-visible header row: identifies the callout even when its editor is
collapsed. Holds the built-in badge, title, and default-status text on the
left; `pr-24` reserves the top-right gutter so nothing runs under the
Published toggle pinned there.
%>

<%# Editable content — hidden when the Published checkbox is unchecked. %>
<div class="hidden peer-checked:block space-y-3 mt-3">
<div class="flex items-center gap-x-3 pr-24 text-sm">
<% if f.object.magic? %>
<div class="flex items-center gap-2">
<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700">Built in</span>

<% if f.object.persisted? && DefaultTicketCallouts.customized?(f.object) %>
<label class="flex items-center gap-2 text-xs text-gray-500"><%= f.check_box :reset_to_default, class: "rounded border-gray-300 text-purple-600" %> Restore to default</label>
<% else %>
<span class="text-xs text-gray-300">Matches default</span>
<% end %>
</div>
<span class="inline-flex items-center rounded-full bg-purple-100 px-2 py-0.5 text-xs font-medium text-purple-700 shrink-0">Built in</span>
<% end %>

<span class="text-gray-800 font-medium truncate max-w-xs" title="<%= f.object.title %>"><%= f.object.title %></span>
</div>

<%# Editable content — hidden when the Published checkbox is unchecked.
`mt-5` keeps the preview card clear of the Published toggle above it. %>
<div class="hidden peer-checked:block space-y-3 mt-5">
<%#
Live preview of how this callout appears on the ticket — the same card
registrants see (icon, title, subtitle, themed colour). Seeded from the
registrants see (icon, title, subtitle, themed color). Seeded from the
record and repainted by the callout-preview controller as the admin edits
the title, subtitle, type, colour, or icon. The badge and live status the
the title, subtitle, type, color, or icon. The badge and live status the
app supplies at render time aren't shown here.
%>
<% preview_theme = f.object.theme %>
Expand Down Expand Up @@ -92,64 +98,88 @@
</div>
</div>

<%# Both collapsible sections share one row so their toggles line up. %>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
<details class="group">
<summary class="flex items-center gap-1.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden text-xs font-medium text-gray-600">
<i class="fa-solid fa-chevron-right text-[10px] text-gray-400 transition-transform group-open:rotate-90"></i>
Type, colour, icon, and display date
</summary>

<%#
Two rows of two: type + colour, then icon + the drip date that gates
when the callout first shows on the ticket.
%>
<%#
Two columns, top-aligned. The left column stacks Type/color/icon and
Visibility so Visibility always sits directly beneath Type/color/icon,
independent of how tall the Linked resources column on the right grows.
%>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 items-start">
<div class="space-y-4">
<details class="group">
<summary class="flex items-center gap-1.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden text-xs font-medium text-gray-600">
<i class="fa-solid fa-chevron-right text-[10px] text-gray-400 transition-transform group-open:rotate-90"></i>
Type, color, and icon
</summary>

<%# One row of three: type, color, icon. %>
<div class="mt-2 grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<%= f.label :callout_type, "Type", class: "block text-xs font-medium text-gray-600 mb-0.5" %>

<%= f.select :callout_type,
RegistrationTicketCallout::CALLOUT_TYPES.map { |t| [ t.humanize, t ] },
{},
class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm",
data: { callout_preview_target: "typeSelect", action: "callout-preview#update" } %>
</div>

<div class="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<%= f.label :callout_type, "Type", class: "block text-xs font-medium text-gray-600 mb-0.5" %>
<div>
<%= f.label :color_class, "Color", class: "block text-xs font-medium text-gray-600 mb-0.5" %>

<%= f.select :callout_type,
RegistrationTicketCallout::CALLOUT_TYPES.map { |t| [ t.humanize, t ] },
{},
class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm",
data: { callout_preview_target: "typeSelect", action: "callout-preview#update" } %>
</div>
<%= f.select :color_class,
DomainTheme::SWATCH_COLORS.map { |c| [ c.to_s.humanize, c ] },
{ include_blank: "Default (by type)" },
class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm",
data: { callout_preview_target: "colorSelect", action: "callout-preview#update" } %>

<div>
<%= f.label :color_class, "Colour", class: "block text-xs font-medium text-gray-600 mb-0.5" %>
<% if f.object.app_colored? %>
<p class="text-xs text-gray-400 mt-0.5">The app overrides this with a live-status color when the registrant has action to take (e.g. amber while something's outstanding, orange while a balance is due).</p>
<% end %>
</div>

<%= f.select :color_class,
DomainTheme::SWATCH_COLORS.map { |c| [ c.to_s.humanize, c ] },
{ include_blank: "Default (by type)" },
class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm",
data: { callout_preview_target: "colorSelect", action: "callout-preview#update" } %>
<div>
<%= f.label :icon_class, "Icon", class: "block text-xs font-medium text-gray-600 mb-0.5" %>

<% if f.object.app_colored? %>
<p class="text-xs text-gray-400 mt-0.5">The app overrides this with a live-status colour when the registrant has action to take (e.g. amber while something's outstanding, orange while a balance is due).</p>
<% end %>
<%= f.text_field :icon_class,
class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm font-mono",
data: { callout_preview_target: "iconInput", action: "callout-preview#update" } %>
</div>
</div>
</details>

<div>
<%= f.label :icon_class, "Icon", class: "block text-xs font-medium text-gray-600 mb-0.5" %>
<%#
Visibility: the callout's show/hide rules — a drip date and the
payment gate. Opened on load when either is set. Hidden for the Payment
built-in, which the app shows/hides from live balance status.
%>
<% unless f.object.payment? %>
<details class="group" <%= "open" if f.object.display_from.present? || f.object.payment_access_gated? %>>
<summary class="flex items-center gap-1.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden text-xs font-medium text-gray-600">
<i class="fa-solid fa-chevron-right text-[10px] text-gray-400 transition-transform group-open:rotate-90"></i>
Visibility
</summary>

<%= f.text_field :icon_class,
class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm font-mono",
data: { callout_preview_target: "iconInput", action: "callout-preview#update" } %>
</div>
<div class="mt-2 grid grid-cols-1 sm:grid-cols-2 gap-4 items-start">
<div>
<%= f.label :display_from, "Display date", class: "block text-xs font-medium text-gray-600 mb-0.5" %>

<div>
<%= f.label :display_from, "Display date", class: "block text-xs font-medium text-gray-600 mb-0.5" %>
<%= f.date_field :display_from,
class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm" %>

<%= f.date_field :display_from,
class: "w-full rounded border-gray-300 bg-white shadow-sm px-2 py-1 text-sm" %>
<p class="text-xs text-gray-500 mt-0.5">Drips onto the ticket on and after this date. Leave blank to show right away.</p>
</div>

<p class="text-xs text-gray-500 mt-0.5">Drips onto the ticket on and after this date. Leave blank to show right away.</p>
</div>
</div>
</details>
<div>
<%= f.label :payment_access_gated, "Payment-gated", class: "block text-xs font-medium text-gray-600 mb-0.5" %>

<label class="flex items-center gap-2 text-sm text-gray-600"><%= f.check_box :payment_access_gated, class: "rounded border-gray-300 text-purple-600" %> Only show if paid or intends to pay</label>
</div>
</div>
</details>
<% end %>
</div>

<%# Opened on load only when resources are saved. %>
<%# RIGHT COLUMN: linked resources. Opened on load only when resources are saved. %>
<details class="group" <%= "open" if f.object.registration_ticket_callout_resources.any? %>>
<summary class="flex items-center gap-1.5 cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden text-xs font-medium text-gray-600">
<i class="fa-solid fa-chevron-right text-[10px] text-gray-400 transition-transform group-open:rotate-90"></i>
Expand Down Expand Up @@ -187,10 +217,20 @@
</details>
</div>

<div class="flex flex-wrap items-center gap-x-6 gap-y-2">
<% unless f.object.magic? %>
<label class="flex items-center gap-2 text-sm text-gray-600"><%= f.check_box :payment_access_gated, class: "rounded border-gray-300 text-purple-600" %> Only show once payment is made or committed</label>
<%= link_to_remove_association "Remove", f,
<%#
Bottom-right action: custom callouts can be deleted; built-ins can't, so
they show their default status (or a restore checkbox once customized) in
the same spot instead.
%>
<div class="flex items-center justify-end gap-x-6 text-sm">
<% if f.object.magic? %>
<% if f.object.persisted? && DefaultTicketCallouts.customized?(f.object) %>
<label class="flex items-center gap-2 text-xs text-gray-500"><%= f.check_box :reset_to_default, class: "rounded border-gray-300 text-purple-600" %> Restore to default</label>
<% else %>
<span class="text-xs text-gray-300">Matches default</span>
<% end %>
<% else %>
<%= link_to_remove_association "Delete callout", f,
class: "text-sm text-gray-400 hover:text-red-600 underline" %>
<% end %>
</div>
Expand Down
11 changes: 11 additions & 0 deletions spec/models/registration_ticket_callout_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,17 @@

expect(event.registration_ticket_callouts.visible).to eq([ shown ])
end

it "#payment? is true only for the payment built-in" do
event = create(:event)
payment = create(:registration_ticket_callout, event:, magic_key: "payment")
other = create(:registration_ticket_callout, event:, magic_key: "faq")
custom = create(:registration_ticket_callout, event:, magic_key: nil)

expect(payment.payment?).to be(true)
expect(other.payment?).to be(false)
expect(custom.payment?).to be(false)
end
end

describe "#published (inverse of hidden)" do
Expand Down