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
9 changes: 8 additions & 1 deletion app/(pages)/pay/cancelled/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { BrandMark } from "@/components/brand-mark";

// Landing page customers reach if they back out of Stripe's hosted payment
// page (Checkout cancel_url). Static and data-free by design.
Expand All @@ -15,7 +16,13 @@ export default function PayCancelledPage() {
return (
<main id="main-content" tabIndex={-1} className="mx-auto max-w-200 px-6 pb-20 md:pb-30">
<div className="rounded-lg border border-stone bg-paper p-8 text-center">
<h1 className="text-2xl font-bold tracking-tight">Payment canceled</h1>
<BrandMark
tone="black"
size={56}
decorative
className="mx-auto opacity-80"
/>
<h1 className="mt-4 text-2xl font-bold tracking-tight">Payment canceled</h1>
<p className="mt-3 text-muted-foreground">
No charge was made. If you meant to complete the payment, ask the
brewery for the payment link again — it may need to be re-sent.
Expand Down
11 changes: 9 additions & 2 deletions app/(pages)/pay/complete/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Metadata } from "next";
import { BrandMark } from "@/components/brand-mark";

// Landing page customers reach after paying on Stripe's hosted page
// (Checkout success_url). Static and data-free by design — payment state is
Expand All @@ -15,8 +16,14 @@ export const metadata: Metadata = {
export default function PayCompletePage() {
return (
<main id="main-content" tabIndex={-1} className="mx-auto max-w-200 px-6 pb-20 md:pb-30">
<div className="rounded-lg border border-stone bg-paper p-8 text-center">
<h1 className="text-2xl font-bold tracking-tight">Payment received</h1>
<div className="rounded-lg border border-moss/30 bg-moss/5 p-8 text-center">
<BrandMark
tone="black"
size={56}
decorative
className="mx-auto opacity-80"
/>
<h1 className="mt-4 text-2xl font-bold tracking-tight">Payment received</h1>
<p className="mt-3 text-muted-foreground">
Thank you — your payment to Deep Dive Brewing Co was received. If
you asked for a receipt, it is on its way to your inbox.
Expand Down
9 changes: 8 additions & 1 deletion app/error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
import Link from "next/link";
import { SiteNav } from "@/components/site-nav";
import { BrandMark } from "@/components/brand-mark";
import { Button } from "@/components/ui/button";

// Root error boundary: an unhandled render error in any page lands here with
Expand Down Expand Up @@ -39,7 +40,13 @@ export default function Error({
tabIndex={-1}
className="mx-auto max-w-300 px-6 pb-20 pt-[calc(5.75rem+env(safe-area-inset-top))] text-center md:pb-30"
>
<h1 className="text-3xl font-bold tracking-tight md:text-4xl">
<BrandMark
tone="black"
size={72}
decorative
className="mx-auto opacity-80"
/>
<h1 className="mt-6 text-3xl font-bold tracking-tight md:text-4xl">
Something went wrong
</h1>
<p className="mx-auto mt-3 max-w-180 text-muted-foreground">
Expand Down
23 changes: 18 additions & 5 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import Link from "next/link";
import { SiteNav } from "@/components/site-nav";
import { BrandMark } from "@/components/brand-mark";
import { Button } from "@/components/ui/button";

export const metadata: Metadata = {
Expand All @@ -20,18 +21,30 @@ export default function NotFound() {
tabIndex={-1}
className="mx-auto max-w-300 px-6 pb-20 pt-[calc(5.75rem+env(safe-area-inset-top))] text-center md:pb-30"
>
<p className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">
<BrandMark
tone="black"
size={72}
decorative
className="mx-auto opacity-80"
/>
<p className="mt-6 text-sm font-semibold uppercase tracking-wider text-muted-foreground">
404
</p>
<h1 className="mt-2 text-3xl font-bold tracking-tight md:text-4xl">
This page could not be found
</h1>
<p className="mx-auto mt-3 max-w-180 text-muted-foreground">
The link may be outdated or the page may have moved.
The link may be outdated or the page may have moved. The beers,
however, are right where we left them.
</p>
<Button asChild className="mt-8 h-11 min-h-[44px] px-6">
<Link href="/">Back to homepage</Link>
</Button>
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
<Button asChild className="h-11 min-h-[44px] px-6">
<Link href="/">Back to homepage</Link>
</Button>
<Button asChild variant="outline" className="h-11 min-h-[44px] px-6">
<Link href="/beers">Browse our beers</Link>
</Button>
</div>
</main>
</>
);
Expand Down
38 changes: 38 additions & 0 deletions components/beers-filter-grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,44 @@ export function BeersFilterGrid({ beers }: BeersFilterGridProps) {
<BeerCard key={beer.slug} beer={beer} priority={index === 0} />
))}
</div>

{filteredBeers.length === 0 && activeFilter === "all" && (
<section className="rounded-lg border border-stone bg-stone/20 p-5">
<p className="text-muted-foreground">
No beers on the list right now — availability changes with the
season.
</p>
</section>
)}
{filteredBeers.length === 0 && activeFilter !== "all" && (
<section className="rounded-lg border border-stone bg-stone/20 p-5">
<p className="text-muted-foreground">
No{" "}
{filterOptions
.find((option) => option.value === activeFilter)
?.label.toLowerCase()}{" "}
beers on the list right now — availability changes with the
season. The full lineup is still on tap.
</p>
<button
type="button"
onClick={() => {
setActiveFilter("all");
trackEvent("beer_filter", {
event_category: "engagement",
filter: "all",
cta_location: "beers_page",
});
}}
className={cn(
"mt-3 inline-flex min-h-[44px] items-center justify-center rounded-md border border-stone bg-paper px-4 py-2 text-sm font-medium text-ink hover:opacity-85 focus-visible:ring-2 focus-visible:ring-ocean/50",
pressableClasses
)}
>
Show all beers
</button>
</section>
)}
</>
);
}
1 change: 1 addition & 0 deletions components/tour-inquiry-cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export function TourInquiryCta({
min={1}
step={1}
inputMode="numeric"
autoComplete="off"
placeholder="Number of people"
value={partySize}
onChange={(event) => setPartySize(event.target.value)}
Expand Down
58 changes: 45 additions & 13 deletions components/trade-inquiry-form.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
"use client";

import Link from "next/link";
import { useEffect, useRef, useState } from "react";
import { Button } from "@/components/ui/button";
import { BrandMark } from "@/components/brand-mark";
import { trackEvent } from "@/lib/analytics";
import { TRADE_LEAD_ISLANDS, TRADE_VENUE_TYPES } from "@/lib/trade-leads-common";

Expand Down Expand Up @@ -49,6 +51,9 @@ export function TradeInquiryForm() {

async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
// Enter-to-submit can re-fire while a request is in flight even though
// the button is disabled — guard so a second press never double-posts.
if (status === "submitting") return;
setStatus("submitting");
setErrorMessage("");

Expand Down Expand Up @@ -102,7 +107,13 @@ export function TradeInquiryForm() {
aria-live="polite"
className="rounded-lg border border-moss/30 bg-moss/5 p-8 text-center outline-none focus-visible:ring-2 focus-visible:ring-moss/50"
>
<p className="font-semibold text-ink">Thanks for reaching out.</p>
<BrandMark
tone="black"
size={48}
decorative
className="mx-auto opacity-80"
/>
<p className="mt-4 font-semibold text-ink">Thanks for reaching out.</p>
<p className="mt-2 text-sm text-muted-foreground">
Your inquiry is in our inbox. The Deep Dive team will follow up.
</p>
Expand All @@ -111,7 +122,11 @@ export function TradeInquiryForm() {
}

return (
<form onSubmit={handleSubmit} className="space-y-6">
<form
onSubmit={handleSubmit}
aria-busy={status === "submitting"}
className="space-y-6"
>
{/* Honeypot: hidden from humans and assistive technology */}
<div className="sr-only" aria-hidden="true">
<label htmlFor="website">Website</label>
Expand Down Expand Up @@ -219,19 +234,36 @@ export function TradeInquiryForm() {
/>
</div>

<div
ref={statusRef}
tabIndex={-1}
role="status"
aria-live="polite"
className="outline-none focus-visible:ring-2 focus-visible:ring-ember/50"
>
{status === "error" && (
<p className="text-sm text-ember">
{/* Rendered only when there's an error — an always-mounted empty live
region is an invisible dead element for sighted users. role=status
announces it the moment it appears; focus moves here so keyboard
users land on the message (entered values are preserved). */}
{status === "error" && (
<div
ref={statusRef}
tabIndex={-1}
role="status"
className="rounded-lg border border-ember/40 bg-ember/5 px-4 py-3 outline-none focus-visible:ring-2 focus-visible:ring-ember/50"
>
<p className="text-sm font-medium text-ember">
We couldn&rsquo;t send your inquiry
</p>
<p className="mt-1 text-sm text-ember/90">
{errorMessage || "Something went wrong. Please try again."}
</p>
)}
</div>
<p className="mt-1 text-sm text-muted-foreground">
Your details are still here — try sending again, or reach us via
the{" "}
<Link
href="/contact"
className="font-medium text-ocean underline underline-offset-2 transition-opacity duration-200 hover:opacity-85"
>
contact page
</Link>{" "}
if it keeps happening.
</p>
</div>
)}

<Button type="submit" disabled={status === "submitting"} className="h-11 min-h-[44px] px-6">
{status === "submitting" ? "Sending..." : "Send inquiry"}
Expand Down
2 changes: 1 addition & 1 deletion smoke-tests/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,7 @@ test("trade form announces submission errors in a status region", async ({

test("beer filter buttons expose pressed state", async ({ page }) => {
await page.goto("/beers");
const all = page.getByRole("button", { name: "All" });
const all = page.getByRole("button", { name: "All", exact: true });
const core = page.getByRole("button", { name: "Core" });
await expect(all).toHaveAttribute("aria-pressed", "true");
await core.press("Enter");
Expand Down
4 changes: 3 additions & 1 deletion smoke-tests/smoke.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ test("beers listing renders and exposes beer filtering", async ({ page }) => {
page.getByRole("heading", { name: "Flagship Styles" })
).toBeVisible();
// Filter controls render regardless of whether beer data loaded.
await expect(page.getByRole("button", { name: "All" })).toBeVisible();
await expect(
page.getByRole("button", { name: "All", exact: true })
).toBeVisible();
await expect(page.getByRole("button", { name: "Core" })).toBeVisible();
});

Expand Down
Loading