<% if current_user %>
+ <%= link_to "Dashboard", dashboard_path, class: "text-blue block py-2 font-medium" %>
<%= link_to "Schedule", schedule_path, class: "text-blue block py-2 font-medium" %>
<%= link_to "My Plan", plan_path, class: "text-blue block py-2 font-medium" %>
<% if admin? %>
From b1499db6d63b7dcb7b7d5aecc87692c65278e1d6 Mon Sep 17 00:00:00 2001
From: Katya Sarmiento <5871075+Kitkatnik@users.noreply.github.com>
Date: Sun, 26 Apr 2026 22:11:28 -0400
Subject: [PATCH 02/13] Add day picker to Host-an-Activity form
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
When the form is opened from /dashboard's "Host an Activity" card,
no day is implied by the entry point, so the user couldn't pick
which day their activity belonged to — everything defaulted to
Saturday.
Render a Day select dropdown in that case (driven by
ScheduleItem::DAY_META). When the form is opened from /plan's
"+ Add custom block" link the day is already implied by the
section, so keep using a hidden field there. The distinguisher
is whether the caller passes show_day_picker: true.
---
app/views/schedule_items/_form.html.erb | 22 +++++++++++++++++-----
app/views/schedule_items/edit.html.erb | 2 +-
app/views/schedule_items/new.html.erb | 2 +-
3 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/app/views/schedule_items/_form.html.erb b/app/views/schedule_items/_form.html.erb
index 4d83ab8..a36419b 100644
--- a/app/views/schedule_items/_form.html.erb
+++ b/app/views/schedule_items/_form.html.erb
@@ -10,11 +10,23 @@
<%= form_with model: schedule_item, class: "space-y-4" do |f| %>
- <%# Day is implied by the plan section this form was opened from —
- ScheduleItemsController#new seeds @schedule_item.day from params[:day].
- We render it as a hidden field so admins-browsing-as-attendees still
- submit the intended day without an extra dropdown. %>
- <%= f.hidden_field :day %>
+ <%# Day is set one of two ways:
+ - When the form is opened from a /plan day section ("+ Add custom block"),
+ ScheduleItemsController#new seeds @schedule_item.day from params[:day]
+ and we keep the field hidden — the user already chose the day implicitly.
+ - When opened from /dashboard ("Host an Activity"), no day is implied,
+ so the caller passes show_day_picker: true and we render a select. %>
+ <% if local_assigns.fetch(:show_day_picker, false) %>
+
+ <%= f.label :day, "Day", class: "label" %>
+ <%= f.select :day,
+ ScheduleItem::DAY_META.map { |key, meta| [ "#{meta[:label]} · #{meta[:date]}", key ] },
+ {},
+ class: "input" %>
+
+ <% else %>
+ <%= f.hidden_field :day %>
+ <% end %>
diff --git a/app/views/schedule_items/edit.html.erb b/app/views/schedule_items/edit.html.erb
index 74c5bd8..b296d98 100644
--- a/app/views/schedule_items/edit.html.erb
+++ b/app/views/schedule_items/edit.html.erb
@@ -6,6 +6,6 @@
Edit your activity
- <%= render "form", schedule_item: @schedule_item %>
+ <%= render "form", schedule_item: @schedule_item, show_day_picker: false %>
diff --git a/app/views/schedule_items/new.html.erb b/app/views/schedule_items/new.html.erb
index 5aa4920..c91a77d 100644
--- a/app/views/schedule_items/new.html.erb
+++ b/app/views/schedule_items/new.html.erb
@@ -14,7 +14,7 @@
on /plan this fragment is extracted into that frame (form
expands inline). On direct visit, the surrounding page renders. %>
<%= turbo_frame_tag "new_schedule_item_#{@schedule_item.day}" do %>
- <%= render "form", schedule_item: @schedule_item %>
+ <%= render "form", schedule_item: @schedule_item, show_day_picker: params[:day].blank? %>
<% end %>
From 7328348f6bf206eba05c8b462e1db6bdede4fe55 Mon Sep 17 00:00:00 2001
From: Katya Sarmiento <5871075+Kitkatnik@users.noreply.github.com>
Date: Sun, 26 Apr 2026 22:11:38 -0400
Subject: [PATCH 03/13] Add zap emoji to Lightning Talks badge
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Single source of truth — the override flows through
kind_display_label and propagates to the kind filter chips,
public schedule cards, and admin index automatically.
---
app/helpers/schedule_helper.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/helpers/schedule_helper.rb b/app/helpers/schedule_helper.rb
index 3317b59..7f40864 100644
--- a/app/helpers/schedule_helper.rb
+++ b/app/helpers/schedule_helper.rb
@@ -1,5 +1,5 @@
module ScheduleHelper
- KIND_DISPLAY_OVERRIDES = { "lightning" => "Lightning Talks" }.freeze
+ KIND_DISPLAY_OVERRIDES = { "lightning" => "⚡ Lightning Talks" }.freeze
def kind_display_label(kind)
return nil if kind.blank?
From 7f38e741b17be9d26fd1f609f1fdcfab575a8eef Mon Sep 17 00:00:00 2001
From: Katya Sarmiento <5871075+Kitkatnik@users.noreply.github.com>
Date: Sun, 26 Apr 2026 22:11:51 -0400
Subject: [PATCH 04/13] Fix stale PDF page count on application confirmation
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The confirmation page advertised "2 pages" but the rendered PDF
is actually 3 pages — page-one's instructions overflow into a
second sheet before the notary addendum starts on the third.
---
app/views/embassy_applications/show.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/embassy_applications/show.html.erb b/app/views/embassy_applications/show.html.erb
index f62ab71..5f2ee45 100644
--- a/app/views/embassy_applications/show.html.erb
+++ b/app/views/embassy_applications/show.html.erb
@@ -27,7 +27,7 @@
embassy_application_path(@application, format: :pdf),
class: "btn btn-red btn-lg" %>
- 2 pages · standard letter size · includes Notary Certification
+ 3 pages · standard letter size · includes Notary Certification
From d7c85c0d2e73efa6f7dfa7f1a8e94d48e21c535e Mon Sep 17 00:00:00 2001
From: Katya Sarmiento <5871075+Kitkatnik@users.noreply.github.com>
Date: Sun, 26 Apr 2026 22:20:50 -0400
Subject: [PATCH 05/13] Redesign admin embassy application view for interview
readability
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The show page was a single dense card of
/- /
- styled with
undefined CSS classes, so the questions and answers blurred together
and the attaché had no way to scan during the stamping interview.
Restructure as:
- Header row with applicant + appointment + submitted timestamp
- Two-up "interview cheat sheet" panel: notary assignment (with
follow-up prompt) and Section 3 random pool draw
- Pill-style jump-nav across the five sections
- Each section gets a strong header, instructions, and (for section 3)
a "random pool" badge so the attaché knows the questions vary
- Each Q&A renders as its own card: a "1.3" tag, the external_id, the
question label in muted text, and the answer in a left-accented
callout box that's the visual focus
Answer rendering by type:
- short/long/select/date: prose in the callout, simple_format-aware
- checkbox_group: pill chips instead of bulleted list (faster to scan)
- checkbox: green "✓ Affirmed" / red "✗ Not affirmed"
- blank: muted italic "— Blank —"
Extracts a _question_row partial to keep the case-by-field-type logic
out of the show template.
---
app/assets/stylesheets/application.css | 226 ++++++++++++++++++
.../_question_row.html.erb | 37 +++
.../admin/embassy_applications/show.html.erb | 96 ++++----
3 files changed, 313 insertions(+), 46 deletions(-)
create mode 100644 app/views/admin/embassy_applications/_question_row.html.erb
diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css
index 4a39ffc..bf01765 100644
--- a/app/assets/stylesheets/application.css
+++ b/app/assets/stylesheets/application.css
@@ -1508,6 +1508,232 @@ hr {
margin: 0;
}
+/* Admin: embassy application — interview view ------------------------- */
+/* The attaché reads each Q&A aloud during the stamping ceremony, so the
+ answer is the visual focus and the question is a small label above it. */
+
+.qa-meta-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 1rem;
+}
+@media (min-width: 720px) {
+ .qa-meta-grid { grid-template-columns: 1fr 1fr; }
+}
+
+.qa-meta-card {
+ background: #fffaf0;
+ border: 1px solid #e6d3a3;
+ border-left: 4px solid #c89b3c;
+ border-radius: 0.375rem;
+ padding: 1rem 1.25rem;
+}
+
+.qa-meta-card__label {
+ font-size: 0.6875rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ color: #8a6d2a;
+ margin: 0 0 0.375rem;
+}
+
+.qa-meta-card__value {
+ font-size: 1rem;
+ color: #0C2866;
+ margin: 0;
+ font-weight: 500;
+ line-height: 1.4;
+}
+
+.qa-meta-card__sub {
+ font-size: 0.8125rem;
+ color: #6b7280;
+ margin: 0.5rem 0 0;
+ line-height: 1.4;
+}
+
+/* Section jump-nav at top of Q&A */
+.qa-jump-nav {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.5rem;
+ padding: 0.625rem;
+ background: #f3f4f6;
+ border-radius: 0.5rem;
+}
+
+.qa-jump-nav__link {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.875rem;
+ background: #ffffff;
+ border: 1px solid #d9dee1;
+ border-radius: 999px;
+ font-size: 0.8125rem;
+ text-decoration: none;
+ color: #0C2866;
+ transition: background-color 0.15s ease, color 0.15s ease, border-color 0.15s ease;
+}
+
+.qa-jump-nav__link:hover {
+ background: #0C2866;
+ color: #ffffff;
+ border-color: #0C2866;
+}
+
+.qa-jump-nav__num {
+ font-weight: 700;
+ font-size: 0.75rem;
+ color: #c11e2a;
+}
+
+.qa-jump-nav__link:hover .qa-jump-nav__num { color: #ffffff; }
+
+/* One section block (header + question rows) */
+.qa-section {
+ margin-bottom: 2.5rem;
+ scroll-margin-top: 1rem;
+}
+
+.qa-section__header {
+ margin-bottom: 1.25rem;
+}
+
+.qa-section__header .embassy-section-header {
+ display: flex;
+ align-items: baseline;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+}
+
+.qa-section__instructions {
+ font-size: 0.875rem;
+ color: #6b7280;
+ font-style: italic;
+ margin: 0.5rem 0 0;
+ max-width: 65ch;
+ line-height: 1.5;
+}
+
+.qa-randompool-badge {
+ display: inline-block;
+ padding: 0.1875rem 0.625rem;
+ background: #fef3c7;
+ color: #92400e;
+ border-radius: 999px;
+ font-size: 0.6875rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: none;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+}
+
+/* One question + answer */
+.qa-question {
+ background: #ffffff;
+ border: 1px solid #e5e7eb;
+ border-radius: 0.5rem;
+ padding: 1.25rem 1.5rem;
+ margin-bottom: 0.875rem;
+ box-shadow: 0 1px 2px 0 rgba(0,0,0,0.04);
+}
+
+.qa-question__meta {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ margin-bottom: 0.5rem;
+}
+
+.qa-question__num {
+ display: inline-block;
+ padding: 0.1875rem 0.5rem;
+ background: #0C2866;
+ color: #ffffff;
+ border-radius: 0.25rem;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+}
+
+.qa-question__id {
+ font-size: 0.75rem;
+ color: #9ca3af;
+}
+
+.qa-question__label {
+ font-size: 0.9375rem;
+ font-weight: 500;
+ color: #4b5563;
+ margin: 0 0 0.875rem;
+ line-height: 1.45;
+}
+
+/* The answer is the focal element. Left accent bar draws the eye. */
+.qa-question__answer {
+ background: #f9fafb;
+ border-left: 4px solid #0C2866;
+ border-radius: 0 0.375rem 0.375rem 0;
+ padding: 0.875rem 1.125rem;
+}
+
+.qa-question__answer p {
+ margin: 0 0 0.5rem;
+ font-size: 1.0625rem;
+ line-height: 1.55;
+ color: #111827;
+}
+
+.qa-question__answer p:last-child { margin-bottom: 0; }
+
+.qa-question__answer .qa-answer-text {
+ font-size: 1.0625rem;
+ line-height: 1.55;
+ color: #111827;
+}
+
+.qa-question__answer .qa-answer-blank {
+ font-style: italic;
+ color: #9ca3af;
+ font-size: 0.9375rem;
+}
+
+.qa-pills {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.4375rem;
+}
+
+.qa-pill {
+ display: inline-block;
+ padding: 0.3125rem 0.75rem;
+ background: #e0e7ff;
+ color: #1e3a8a;
+ border-radius: 999px;
+ font-size: 0.9375rem;
+ font-weight: 500;
+}
+
+.qa-question__answer .qa-affirm {
+ font-size: 1.0625rem;
+ color: #047857;
+ font-weight: 600;
+ margin: 0;
+}
+
+.qa-question__answer .qa-decline {
+ font-size: 1.0625rem;
+ color: #b91c1c;
+ font-weight: 600;
+ margin: 0;
+}
+
+/* Long-form answers get a touch more breathing room. */
+.qa-question--long .qa-question__answer { padding: 1rem 1.25rem; }
+
/* Booking confirm actions --------------------------------------------- */
/* button_to wraps its button in a