Skip to content

Add /donate Support Saba page with typed recipient registry (#171) - #172

Open
spizeck wants to merge 8 commits into
masterfrom
feat/donate-support-saba-171
Open

spizeck wants to merge 8 commits into
masterfrom
feat/donate-support-saba-171

Conversation

@spizeck

@spizeck spizeck commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Closes #171

Revision (head 9dbc331)

The original implementation covered only the visitor-donation direction. This revision makes /donate the public home of Sea Saba's community giving program, serving both audiences:

  1. Visitors supporting Saba — unchanged: vetted-recipient registry (data/donations.ts, still empty/fail-safe until the owner approves organizations), donations route directly to each organization's own site.
  2. Saba organizations/projects requesting support FROM Sea Saba — new "Request Support from Sea Saba" section: who may ask, what Sea Saba can provide, draft ground rules, the process, and a request form.

Polish pass (head a41a56c)

Owner-review visual/content pass:

  • Ground rules 7 → 6: "Someone responsible we can reach" + "A specific purpose" merged into "A clear project and someone responsible" — both card grids now render a full 2×3 on desktop with no orphan card.
  • Support types 7 → 6: "Financial contribution" + "Sponsorship" merged into "Financial support or sponsorship". The merged option keeps the "financial" slug, so the required-amount gate and any existing drafts are unaffected; the form hint and email serialization use the new label.
  • Cards tightened: py-3 → py-2.5, grid gap-3 → gap-2.5 on both grids.
  • Footer density pass: pt-16 → pt-10, grid gap-12 → gap-8 (sm:gap-10), nav gap-3 → gap-2, heading mt-4 → mt-3, bottom bar mt-12/pt-8 → mt-6/pt-6. Measured ~15% shorter at mobile widths, ~14.5% at 768, ~17% at ≥1024. The pb-[calc(5.5rem+env(safe-area-inset-bottom))] launcher clearance is unchanged and the footer-widgets clearance test still passes at all nine probe widths.
  • Visiting Yachts added to the footer's Explore column (between Courses and About), linking /visiting-yachts.
  • Support Saba moved from Explore to the Plan Your Trip column (after Recommended Partners), grouping it with the visitor-oriented links and balancing Plan/Explore at seven links each.

Revised page structure

  • Hero: Support Saba / "Giving back to the island — and asking us to help"
  • Intro naming both audiences + in-page link to the request section
  • "Your Visit Already Helps" (existing marine-park/chamber fee facts)
  • "Ways to Give Back" (visitor recipient registry — graceful empty state preserved)
  • "Request Support from Sea Saba" — Who can ask → What support can look like → A few ground rules → How it works → no-guarantee note → request form
  • /donate remains the canonical URL (already wired into sitemap, llms.txt, entry points). Title "Support Saba" covers both audiences — recommended over "Donate" or renaming the route.

Standards/policy architecture

All program content lives in data/community-support.ts (typed SupportRequestCategory / SupportType / SupportStandard structures + WHO_CAN_REQUEST, SUPPORT_REQUEST_STEPS, SUPPORT_REQUEST_NO_GUARANTEE). All copy is DRAFT pending owner review — clearly marked in the file header; editing wording never touches JSX. No dollar caps, budgets, deadlines, or funding guarantees anywhere.

The six draft standards encode the anti-abuse controls in positive language: community benefit, a clear project with an identifiable responsible party, visibility into where money goes (proportional — "we may ask" for budget/quote/invoice/proof, not mandatory documents), no unrestricted personal cash gifts, preference for paying suppliers directly / purchasing goods / providing Sea Saba services over cash, and proportional follow-up (receipt/photo/short update).

Request form

components/donations/support-request-form.tsx collects: name*, organization (optional), email*, phone/WhatsApp (optional), category* (Community/Youth/Education/Conservation/Animal welfare/Sports/Culture/Events/Other), support types* (multi-select: financial support or sponsorship, goods, prize/raffle, Sea Saba services, staff time, in-kind), amount (required only when "financial" selected), request*, description*, beneficiaries*, timing*, use-of-support*, "Sea Saba may pay a supplier directly" opt-in checkbox, optional reference URL, and a required accuracy acknowledgement*.

Submission mechanism — why it's safe vs. #104

There is no approved server-side submission path today, and building one from a shared verified sender is exactly what collapsed all visitors into one Respond.io contact (#104). The form therefore uses the same architecture as the contact form: it builds a structured request and opens it in the requester's own email app (mailto:info@seasaba.com) or WhatsApp (wa.me) — the requester's real From address reaches the inbox, so Respond.io attaches it to the correct contact. No new email service, database, SaaS, or file-upload machinery; requesters can attach supporting documents in their own email app. If #104's Custom Channel lands later, lib/support-request.ts is a pure data contract that can feed a server endpoint unchanged.

Privacy & analytics

  • Form data never leaves the browser except inside the visitor's own email/WhatsApp handoff; privacy page updated ("contact and support-request forms… open a pre-filled email in your own email application", request details added to collected-info list, date bumped).
  • Analytics get only generic events — donation_request_started (first field focus, once) and donation_request_submitted (handoff; method = email/whatsapp). A dedicated integration test fills the form with sentinel values and asserts no field content appears in any track() call or dataLayer push; mailto/wa.me URLs are sanitized to bare addresses before entering link_url.

Files changed (this revision)

data/community-support.ts, lib/support-request.ts, components/donations/{community-support-section,support-request-form}.tsx (new); app/(en)/(content)/donate/page.tsx, app/(en)/(content)/privacy/page.tsx, components/footer.tsx, lib/analytics.ts, docs/{ANALYTICS_SEO,OPERATIONS}.md, public/llms.txt, tests/{unit/support-request.test.ts,integration/{donate,internal-links}.test.tsx,e2e/accessibility.spec.ts}.

Tests

  • 14 new unit tests (support-request.test.ts): validation rules, conditional amount, acknowledgement, subject/body contract, optional-field omission, vendor-payment honesty, mailto/wa.me encoding.
  • donate integration suite 8→23 tests: both audiences, standards/categories/types/steps render from data, exactly six standards + six support types (guards the 2×3 grids), merged financial slug preserved, empty-registry fail-safe, metadata, no invented guarantees/amounts/deadlines in policy copy, no em/en dashes in customer-facing copy, validation errors, acknowledgement, conditional amount, mailto handoff contents, direct-payment toggle, WhatsApp handoff, started fires once, PII never reaches analytics.
  • internal-links: footer Explore → /visiting-yachts assertion added.
  • e2e: /donate added to mobile axe scans + desktop axe scan of the form with validation errors shown; smoke/link-integrity/locale-routing already cover the route; footer-widgets launcher clearance and resize-scroll preservation remain green.

Validation

check ✓ · lint ✓ · typecheck ✓ · vitest 392/392 ✓ · build:test ✓ (/donate static) · e2e 124 passed: donate axe (desktop+mobile+error state), smoke, link-integrity, navigation, footer-widgets clearance at 320–1440, resize-scroll. Geometric verification at 320/375/390/768/1024/1280/1440: both card grids render balanced (2×3 at ≥768), equal card heights per row, no horizontal overflow, footer ~15–17% shorter.

Still needs Chad's review

  • All wording in data/community-support.ts is DRAFT — standards, categories, support types, process steps, who-can-ask list.
  • Whether "Support Saba" stays as the public title (recommended) and the /donate URL (kept — canonical, already indexed surfaces).
  • Approved recipient list for data/donations.ts (unchanged dependency).
  • Whether the request form should also appear/link anywhere else, and whether email-or-WhatsApp handoff is the right long-term mechanism vs. Contact form: route inquiries through a respond.io Custom Channel to fix shared-contact identity collapse #104's Custom Channel.
  • Anything requiring a real server endpoint (auto-acknowledgement, uploads) is deferred until that configuration exists.

Generated with Devin

Summary by Sourcery

Launch /donate as Sea Saba’s public community-giving hub for supporting Saba and requesting support from Sea Saba.

New Features:

  • Add the canonical /donate Support Saba page for visitor giving and Sea Saba community-support requests.
  • Provide a typed registry for owner-approved donation recipients with a safe empty state and direct links to recipient websites.
  • Add a structured community-support request form with validation and email or WhatsApp handoff options.

Enhancements:

  • Centralize community-support categories, support types, standards, and process guidance in typed data structures.
  • Protect requester privacy by limiting analytics to generic events and keeping form details in the visitor-controlled handoff.
  • Add Support Saba entry points across relevant pages, navigation, sitemap, and public route coverage.
  • Reduce footer density while balancing the Plan Your Trip and Explore navigation columns.

Documentation:

  • Update privacy, analytics, SEO, and operations documentation for the new support and donation flows.

Tests:

  • Add unit, integration, and end-to-end coverage for the donate page, request form, privacy-safe analytics, links, routing, and accessibility.

Visitors regularly ask how to support Saba beyond their trip; the site had
no canonical answer. Adds a server-rendered /donate page with a typed
DONATION_RECIPIENTS registry (empty pending owner-approved content — a
graceful in-progress state renders instead of fabricated organizations),
the repo-documented marine-park conservation contribution, and a trust
note clarifying that donations go directly to each organization.

Entry points stay out of the primary nav: footer Explore column, the
/partners conservation section, and the /plan-your-trip island section.
Sitemap, llms.txt, and every route inventory updated; new donation_click
analytics event for outbound recipient CTAs.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@sourcery-ai

sourcery-ai Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

This PR adds a statically rendered /donate Support Saba page backed by a typed, intentionally empty recipient registry, with direct-to-first-party donation architecture, restrained internal entry points, SEO integration, analytics, and comprehensive route/content/accessibility test coverage.

Flow diagram for the empty Support Saba recipient state

flowchart TD
    Start[Visitor opens /donate] --> Load[Load DONATION_RECIPIENTS]
    Load --> Empty{Recipients approved?}
    Empty -->|No| InProgress[Show list-in-progress message]
    InProgress --> Contact[Link to /contact to ask Sea Saba]
    Empty -->|Yes| Cards[Render organization cards]
    Cards --> Donation{Direct donation URL exists?}
    Donation -->|Yes| Direct[Donate directly on organization website]
    Donation -->|No| Website[Visit organization website]
Loading

File-Level Changes

Change Details Files
Adds a statically rendered Support Saba page with direct-to-organization donation messaging and a safe empty-state experience.
  • Introduces the /donate page with hero, conservation-fee explanation, marine-park deep link, recipient section, and no-Sea-Saba-processing trust language.
  • Uses the existing operations fee data rather than duplicating conservation figures.
  • Renders a contact prompt when no owner-approved recipients are available.
app/(en)/(content)/donate/page.tsx
components/donations/donations-section.tsx
data/donations.ts
Introduces a typed, data-driven recipient registry and outbound card behavior for future approved organizations.
  • Defines recipient metadata for websites, optional donation URLs, categories, funding descriptions, and local images.
  • Provides donation and website CTAs with new-tab security attributes, descriptive labels, and outbound analytics.
  • Keeps the shipped registry empty until recipient content and first-party URLs receive owner approval.
data/donations.ts
components/donations/donations-section.tsx
lib/analytics.ts
docs/OPERATIONS.md
docs/ANALYTICS_SEO.md
Adds restrained internal discovery paths to the new page without changing primary navigation or homepage content.
  • Adds Support Saba to the footer Explore links.
  • Links to /donate from conservation partners and the island section of the trip-planning page.
components/footer.tsx
components/partners/conservation-partners-section.tsx
app/(en)/(content)/plan-your-trip/page.tsx
Integrates /donate into canonical SEO and route coverage.
  • Adds metadata, sitemap coverage, llms.txt coverage, and shared-layout breadcrumb behavior through the new content route.
  • Extends canonical route and locale-routing checks.
app/(en)/(content)/donate/page.tsx
app/sitemap.ts
public/llms.txt
tests/unit/seo.test.ts
tests/e2e/locale-routing.spec.ts
Extends automated coverage for rendering, content safety, links, accessibility, and public-route smoke tests.
  • Adds integration tests for the empty registry, trust claims, fee link, card semantics, CTA attributes, and recipient content contracts.
  • Adds /donate to smoke, link-integrity, and accessibility page suites.
  • Verifies all new entry-point and page-internal links resolve.
tests/integration/donate.test.tsx
tests/integration/internal-links.test.tsx
tests/e2e/accessibility.spec.ts
tests/e2e/link-integrity.spec.ts
tests/e2e/smoke.spec.ts

Assessment against linked issues

Issue Objective Addressed Explanation
#171 Add a server-rendered /donate page with a restrained Support Saba introduction, exactly one H1, breadcrumb, metadata, canonical URL, conservation context, trust messaging, and a graceful empty state without fabricated recipients or donation claims. ✅
#171 Implement a typed, data-driven donation recipient registry and external-link card system that routes visitors directly to recipient websites, supports optional donation URLs and metadata, uses correct new-tab security/accessibility semantics, and tracks donation CTAs without creating Sea Saba checkout functionality. ✅
#171 Add restrained discovery and navigation entry points and update SEO and route inventories, including the footer, partners page, trip-planning page, sitemap, llms.txt, SEO tests, and relevant smoke, accessibility, link-integrity, and locale-routing tests, while documenting the owner content still required. ✅

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel

vercel Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
seasaba-web Ready Ready Preview Sep 27, 2026 4:47pm UTC

Request Review

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="components/donations/donations-section.tsx" line_range="62" />
<code_context>
+      {recipient.image && (
+        <Image
+          src={recipient.image}
+          alt={recipient.imageAlt ?? ""}
+          width={160}
+          height={48}
</code_context>
<issue_to_address>
**issue (bug_risk):** When a recipient has an `image` but omits `imageAlt`, `DonationCard` renders `alt=""`, causing the organization image to be treated as decorative and hiding potentially meaningful content from screen-reader users. The interface comment says alt text is required, but the optional property does not enforce that contract.

**Triggers:** When a future registry entry supplies `image` without `imageAlt`.

**Suggested fix:** Make `imageAlt` required whenever `image` is present using a discriminated union, or provide a meaningful fallback alt value before rendering the image.

```suggestion
          alt={recipient.imageAlt ?? recipient.name}
```
</issue_to_address>

Sourcery assessment

Approval pending. 1 finding to address first.

Blocking findings: components/donations/donations-section.tsx:62


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

{recipient.image && (
<Image
src={recipient.image}
alt={recipient.imageAlt ?? ""}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): When a recipient has an image but omits imageAlt, DonationCard renders alt="", causing the organization image to be treated as decorative and hiding potentially meaningful content from screen-reader users. The interface comment says alt text is required, but the optional property does not enforce that contract.

Triggers: When a future registry entry supplies image without imageAlt.

Suggested fix: Make imageAlt required whenever image is present using a discriminated union, or provide a meaningful fallback alt value before rendering the image.

Suggested change
alt={recipient.imageAlt ?? ""}
alt={recipient.imageAlt ?? recipient.name}

The page now serves both audiences: visitors looking for vetted island
organizations to support directly, and Saba organizations/projects/events
requesting a donation or sponsorship from Sea Saba — the second half was
missing and is a major purpose of the feature.

- data/community-support.ts: editable DRAFT policy content (who may ask,
  request categories, support types, review standards, process steps,
  no-guarantee note) — wording changes never touch JSX
- lib/support-request.ts: typed request draft, per-field limits, shared
  validation, and mailto/wa.me handoff builders — same visitor-sends-it
  mechanism as the contact form, so Respond.io files requests under the
  requester's real address (no shared-sender collapse, #104) and no server
  endpoint or third-party form service is introduced
- components/donations/: CommunitySupportSection renders the standards
  from data; SupportRequestForm collects name/org/contact, category,
  support types, conditional amount, request/description/beneficiaries/
  timeline/use, direct-supplier-payment option, optional reference URL,
  and a required accuracy acknowledgement
- Privacy page covers support-request details; analytics gain generic
  donation_request_started/submitted events that never carry field values

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@spizeck

spizeck commented Sep 25, 2026

Copy link
Copy Markdown
Owner Author

Heads-up on the failing Critical website tests job: the failure is resize-scroll.spec.ts — homepage landmark preservation › keeps just above footer in view 1280→768 (fromBottom expected > 1500, got 1324). This is deterministic on this branch, not flaky: master's run at 13:31 passed the same test with an identical runner image/container digest, and the same value reproduces locally. The new Support Saba footer link makes the footer ~32px taller at every breakpoint (measured 494px @1280 / 790px @768 vs. the ~462/~758 the spec comments were calibrated on), which shifts which landmark the keeper test tags at max-577 and leaves the viewport closer to the bottom post-reflow. Fixing will likely need a spec adjustment — e.g. re-anchoring the reading position to a stable element instead of a raw px offset, or recalibrating FOOTER_THRESHOLD_HINT — rather than a retry.

The form's FieldError spacer rendered a min-h-5 line under every field,
inflating each row gap to ~42px; collapse it when empty and group the
trailing consent/submit cluster. Rewrite em-dash prose naturally across
the new /donate copy, and recalibrate the resize-scroll 'just above
footer' scenario against the footer's top edge instead of a fixed pixel
threshold so it survives footer geometry changes. Add a page-level
assertion that customer-facing copy contains no em/en dashes.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Fold the responsible-party and specific-purpose ground rules into one
("A clear project and someone responsible") and merge financial +
sponsorship into a single "financial" support type, so both grids render
as full 2x3 on desktop instead of leaving an orphan card. The merged type
keeps the "financial" slug so the required-amount gate and any existing
drafts are unaffected.

Card padding/gap reduced (py-3->py-2.5, gap-3->gap-2.5). Footer gets a
restrained density pass (pt-16->pt-10, grid gap-12->gap-8/10, nav
gap-3->gap-2, heading mt-4->mt-3, bottom bar mt-12/pt-8->mt-6/pt-6): ~15%
shorter on mobile, ~17% on desktop, launcher clearance and safe-area
padding unchanged. Adds Visiting Yachts to the Explore column.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Groups the donation entry point with the visitor-oriented trip-planning
links and balances Plan/Explore at seven links each after the Visiting
Yachts addition — no other footer changes.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview — 8e3977ed Deployed Sep 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a Support Saba donations page (/donate)

1 participant