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
102 changes: 102 additions & 0 deletions app/(en)/(content)/donate/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import Link from "next/link";
import { createMetadata } from "@/lib/metadata";
import { PageHero } from "@/components/page-hero";
import { DonationsSection } from "@/components/donations/donations-section";
import { CommunitySupportSection } from "@/components/donations/community-support-section";
import { DONATION_RECIPIENTS } from "@/data/donations";
import { OPERATIONS } from "@/data/operations";
import { divingAnchors } from "@/lib/anchors";

export const metadata = createMetadata({
title: "Support Saba",
description:
"Ways to support Saba beyond your visit, and how island organizations, projects, and events can request a donation or sponsorship from Sea Saba.",
path: "/donate",
});

export default function DonatePage() {
return (
<>
<PageHero
src="/images/optimized/windwardside-village-saba.webp"
alt="Colorful red-roofed cottages of Windwardside village on the green hillside of Saba"
title="Support Saba"
subtitle="Giving back to the island and asking us to help"
/>

<p className="text-base leading-relaxed text-muted-foreground">
Visitors fall for Saba quickly: the reefs, the trails, the quiet
villages, and the people who keep it all running. Many guests ask how
they can give something back once they&apos;re home. And on the island
itself, community groups, teams, and projects regularly need a hand.
This page is for both.
</p>
<p className="mt-3 text-base leading-relaxed text-muted-foreground">
If you&apos;re a visitor, the most meaningful support goes straight to
the island organizations doing the work. If you&apos;re on Saba and
looking for a donation or sponsorship from Sea Saba, scroll down to{" "}
<Link
href="/donate#request-support"
className="font-medium text-primary underline-offset-4 hover:underline"
>
Request Support from Sea Saba
</Link>
.
</p>

{/* Your Visit Already Helps */}
<section className="mt-14">
<h2 className="text-xl font-semibold text-foreground">
Your Visit Already Helps
</h2>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
Everything we dive sits inside the Saba National Marine Park, and
every dive carries a conservation contribution of{" "}
${OPERATIONS.conservationFees.marineParkPerDiveUsd + OPERATIONS.conservationFees.chamberContributionPerDiveUsd}{" "}
per diver: ${OPERATIONS.conservationFees.marineParkPerDiveUsd} to the
Marine Park and ${OPERATIONS.conservationFees.chamberContributionPerDiveUsd}{" "}
to the island&apos;s hyperbaric chamber fund. Snorkel trips contribute{" "}
${OPERATIONS.conservationFees.snorkelParkPerPersonUsd} per person.
These fees are set by the park and the chamber, not Sea Saba, and they
fund the moorings, patrols, and emergency coverage that keep the
reefs healthy.
</p>
<p className="mt-3 text-sm text-muted-foreground">
<Link
href={`/diving#${divingAnchors.marinePark}`}
className="font-medium text-primary underline-offset-4 hover:underline"
>
How the Saba Marine Park works
</Link>
</p>
</section>

{/* Ways to Give Back */}
<section className="mt-14">
<h2 className="text-xl font-semibold text-foreground">
Ways to Give Back
</h2>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
When you&apos;re ready to do more, a number of island organizations
welcome direct support. Every card below links out to the
organization&apos;s own website. Your donation goes to them, not
through us.
</p>

<DonationsSection recipients={DONATION_RECIPIENTS} />

<p className="mt-6 text-xs leading-relaxed text-muted-foreground">
Any donation link on this page opens the organization&apos;s own
website in a new tab. Sea Saba never collects, processes, or retains
donated funds. Each organization manages its own gifts and receipts.
</p>
</section>

{/* Request Support from Sea Saba — the other half of the page: local
organizations and project leads asking Sea Saba for help. All policy
content renders from data/community-support.ts (DRAFT pending owner
review). */}
<CommunitySupportSection />
</>
);
}
6 changes: 6 additions & 0 deletions app/(en)/(content)/plan-your-trip/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -939,6 +939,12 @@ export default function PlanYourTripPage() {
</ul>
</div>
</div>
<p className="mt-8 text-sm text-muted-foreground">
Many visitors leave asking how they can give something back to the island.{" "}
<Link href="/donate" className="font-medium text-primary underline-offset-4 hover:underline">
See ways to support Saba
</Link>.
</p>
</section>

{/* FAQ */}
Expand Down
9 changes: 5 additions & 4 deletions app/(en)/(content)/privacy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function PrivacyPage() {
</p>

<p className="mt-2 text-sm text-muted-foreground">
Last updated: August 2026
Last updated: September 2026
</p>

<div className="mt-10 space-y-10">
Expand All @@ -38,6 +38,7 @@ export default function PrivacyPage() {
<li>Travel dates and booking details</li>
<li>Certification level and dive experience</li>
<li>Information submitted through reservation forms</li>
<li>Donation, sponsorship, or community-support request details you send us</li>
<li>Information submitted through liability waivers or medical questionnaires, when applicable</li>
<li>Messages and inquiries</li>
<li>Payment and transaction information necessary to process reservations</li>
Expand Down Expand Up @@ -131,9 +132,9 @@ export default function PrivacyPage() {
If you contact Sea Saba through email, WhatsApp, website chat, or website forms, we may retain those communications to provide customer service and assist with future inquiries.
</p>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
Our website contact form does not transmit your message to us directly — it opens a
pre-filled email in your own email application, and the message is sent from your email
account when you choose to send it.
Our website contact and support-request forms do not transmit your message to us
directly. They open a pre-filled email in your own email application, and the message
is sent from your email account when you choose to send it.
</p>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
The website chat widget is provided by Respond.io. When you open the chat and submit the
Expand Down
1 change: 1 addition & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const SITEMAP_ROUTES: {
{ path: "/about", changeFrequency: "monthly", priority: 0.7 },
{ path: "/contact", changeFrequency: "monthly", priority: 0.6 },
{ path: "/partners", changeFrequency: "monthly", priority: 0.6 },
{ path: "/donate", changeFrequency: "monthly", priority: 0.6 },
{ path: "/terms", changeFrequency: "yearly", priority: 0.3 },
{ path: "/privacy", changeFrequency: "yearly", priority: 0.3 },
{ path: "/cookie-policy", changeFrequency: "yearly", priority: 0.3 },
Expand Down
109 changes: 109 additions & 0 deletions components/donations/community-support-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import {
SUPPORT_REQUEST_CATEGORIES,
SUPPORT_TYPES,
WHO_CAN_REQUEST,
SUPPORT_STANDARDS,
SUPPORT_REQUEST_STEPS,
SUPPORT_REQUEST_NO_GUARANTEE,
} from "@/data/community-support";
import { SupportRequestForm } from "./support-request-form";

/**
* The request side of /donate (#171): who may ask Sea Saba for support, what
* kinds of help are possible, the ground rules, the process, and the request
* form. All policy content renders from data/community-support.ts — edit the
* wording there, not here. DRAFT copy pending owner review.
*/
export function CommunitySupportSection() {
return (
<section className="mt-14" aria-labelledby="request-support">
<h2 id="request-support" className="text-xl font-semibold text-foreground">
Request Support from Sea Saba
</h2>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
Sea Saba has been part of this island since 1985, and giving back is part
of how we operate: a raffle prize for a school fundraiser, air fills for
a beach cleanup, sponsorship for a youth team. If you&apos;re working on
something worthwhile on Saba, we&apos;d like to hear about it.
</p>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
We&apos;re a small local business, so we keep a few clear ground rules,
not to make asking harder, but so we can keep saying yes to good
projects and know our support lands where it&apos;s meant to.
</p>

{/* Who can ask */}
<h3 className="mt-8 text-base font-semibold text-foreground">Who can ask</h3>
<ul className="mt-3 list-disc space-y-1.5 pl-5 text-sm leading-relaxed text-muted-foreground">
{WHO_CAN_REQUEST.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<p className="mt-3 text-sm text-muted-foreground">
Requests we see most often fall into a few areas (
{SUPPORT_REQUEST_CATEGORIES.map((c) => c.label).join(", ").replace(/, ([^,]*)$/, " or $1")}
), but anything that benefits the island is worth asking about.
</p>

{/* What support can look like */}
<h3 className="mt-8 text-base font-semibold text-foreground">
What support can look like
</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
It doesn&apos;t have to be money. Depending on the request, Sea Saba can help with:
</p>
<ul role="list" className="mt-3 grid list-none gap-2.5 pl-0 sm:grid-cols-2">
{SUPPORT_TYPES.map((type) => (
<li
key={type.value}
className="rounded-lg border border-border/50 bg-muted/20 px-4 py-2.5"
>
<p className="text-sm font-medium text-foreground">{type.label}</p>
<p className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
{type.description}
</p>
</li>
))}
</ul>

{/* Ground rules */}
<h3 className="mt-8 text-base font-semibold text-foreground">
A few ground rules
</h3>
<ul role="list" className="mt-3 grid list-none gap-2.5 pl-0 sm:grid-cols-2">
{SUPPORT_STANDARDS.map((standard) => (
<li
key={standard.title}
className="rounded-lg border border-border/50 px-4 py-2.5"
>
<p className="text-sm font-medium text-foreground">{standard.title}</p>
<p className="mt-0.5 text-xs leading-relaxed text-muted-foreground">
{standard.description}
</p>
</li>
))}
</ul>

{/* How it works */}
<h3 className="mt-8 text-base font-semibold text-foreground">How it works</h3>
<ol className="mt-3 list-decimal space-y-1.5 pl-5 text-sm leading-relaxed text-muted-foreground">
{SUPPORT_REQUEST_STEPS.map((step) => (
<li key={step}>{step}</li>
))}
</ol>
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">
{SUPPORT_REQUEST_NO_GUARANTEE}
</p>

{/* The form */}
<h3 className="mt-8 text-base font-semibold text-foreground">Send a request</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
This gives us what we need to evaluate your request fairly. It&apos;s
not a grant application. Short, honest answers are perfect.
</p>
<div className="mt-4 rounded-lg border border-border/60 bg-card p-6">
<SupportRequestForm />
</div>
</section>
);
}
123 changes: 123 additions & 0 deletions components/donations/donations-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use client";

import Image from "next/image";
import Link from "next/link";
import { ExternalLink } from "lucide-react";
import { TrackedOutboundLink } from "@/components/tracked-outbound-link";
import { cn } from "@/lib/utils";
import type { DonationRecipient } from "@/data/donations";

const CATEGORY_LABELS: Record<NonNullable<DonationRecipient["category"]>, string> = {
conservation: "Conservation",
community: "Community",
animals: "Animals",
youth: "Youth",
culture: "Culture",
"marine-conservation": "Marine conservation & coral restoration",
"science-education": "Science, education, youth & culture",
};

interface DonationsSectionProps {
recipients: DonationRecipient[];
}

/**
* Recipient grid for /donate (#171). While DONATION_RECIPIENTS is empty the
* section renders a graceful in-progress note — no fabricated organizations
* may appear publicly. Cards always send visitors to the recipient's own
* site; Sea Saba never processes donations.
*/
export function DonationsSection({ recipients }: DonationsSectionProps) {
if (recipients.length === 0) {
return (
<div className="mt-6 rounded-xl border border-dashed border-border/60 bg-muted/20 p-6">
<p className="text-sm leading-relaxed text-muted-foreground">
We&apos;re assembling a short list of Saba organizations we can
wholeheartedly recommend. Until it&apos;s ready, the most reliable
route is also the simplest:{" "}
<Link
href="/contact"
className="font-medium text-primary underline-offset-4 hover:underline"
>
ask us
</Link>
. We live here, and we&apos;re happy to point you at a cause that fits.
</p>
</div>
);
}

// Three columns only once the registry outgrows two entries — with one or
// two approved recipients a wider card fills the row and keeps the longer
// focus labels on one line instead of leaving an empty third column.
return (
<div
className={cn(
"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-2",
recipients.length > 2 && "lg:grid-cols-3"
)}
>
{recipients.map((recipient) => (
<DonationCard key={recipient.name} recipient={recipient} />
))}
</div>
);
}

function DonationCard({ recipient }: { recipient: DonationRecipient }) {
return (
<article className="group flex flex-col rounded-xl border border-border/50 bg-background p-5 transition-all duration-200 hover:-translate-y-0.5 hover:border-primary/30 hover:shadow-sm">
{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}

width={160}
height={48}
className="mb-3 h-14 w-auto self-start object-contain object-left"
/>
)}
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1.5">
<h3 className="text-base font-semibold text-foreground">{recipient.name}</h3>
{recipient.category && (
<span className="shrink-0 rounded-full bg-primary/10 px-2.5 py-0.5 text-xs font-medium text-primary">
{CATEGORY_LABELS[recipient.category]}
</span>
)}
</div>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{recipient.description}
</p>
{recipient.funds && (
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
<span className="font-medium text-foreground">Helps fund:</span>{" "}
{recipient.funds}
</p>
)}

<div className="mt-auto flex flex-wrap items-center gap-x-4 gap-y-2 pt-5">
{recipient.donationUrl && (
<TrackedOutboundLink
href={recipient.donationUrl}
eventName="donation_click"
buttonText={`Donate to ${recipient.name}`}
ariaLabel={`Donate directly to ${recipient.name} on their own website, opens in a new tab`}
className="inline-flex items-center gap-1.5 rounded-md bg-primary px-3.5 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
>
Donate directly
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</TrackedOutboundLink>
)}
<TrackedOutboundLink
href={recipient.website}
eventName="social_click"
buttonText={`Visit ${recipient.name}`}
ariaLabel={`Visit ${recipient.name}'s website, opens in a new tab`}
className="inline-flex items-center gap-1.5 text-sm font-medium text-primary transition-colors hover:text-primary/80"
>
{recipient.donationUrl ? "Visit website" : "Visit their website"}
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</TrackedOutboundLink>
</div>
</article>
);
}
Loading
Loading