Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
96b5a07
Add a grid overlay option to V2 block backgrounds
joshbermanssw Jun 18, 2026
d9d7d3d
Show hex codes in the background colour picker and add SSW Dark Gray
joshbermanssw Jun 18, 2026
1b8c0d0
Add v3 consulting blocks (hero, logo carousel, feature steps)
joshbermanssw Jun 18, 2026
e200cfc
Migrate the React consulting page to a v2 page
joshbermanssw Jun 18, 2026
4554a25
fix new bgcolouroptions
joshbermanssw Jun 18, 2026
ee6465a
upd y-height; add raidal bgs
joshbermanssw Jun 18, 2026
c151c4d
reduce radial transparency
joshbermanssw Jun 18, 2026
56e0361
move radials
joshbermanssw Jun 18, 2026
b01c1de
add y line
joshbermanssw Jun 18, 2026
303715e
add pluggable media slot to v3 hero with animated React atom
joshbermanssw Jun 18, 2026
779f2b3
make v3 feature step brow editable and fix rich-text step defaults
joshbermanssw Jun 18, 2026
18e4bba
regenerate tina lock for v3 schema changes
joshbermanssw Jun 18, 2026
9cd9f62
update React consulting page content
joshbermanssw Jun 18, 2026
89926be
add v3 Process block
joshbermanssw Jun 18, 2026
6e133ee
regenerate tina lock for v3 Process schema
joshbermanssw Jun 18, 2026
cdb5840
add Process block to React consulting page
joshbermanssw Jun 18, 2026
68a39da
fix feature steps grid like pattern
joshbermanssw Jun 18, 2026
ecc24ba
fix AST schema
joshbermanssw Jun 18, 2026
f310942
Add stats + testimonial
joshbermanssw Jun 18, 2026
68e911d
l!nt and some ui updates to testimonial sections
joshbermanssw Jun 18, 2026
436ff5f
inc py size
joshbermanssw Jun 18, 2026
f21992f
stackCards + CTA
joshbermanssw Jun 18, 2026
e44347e
add FAQ block
joshbermanssw Jun 18, 2026
0f307a8
add lead capture form v1
joshbermanssw Jun 18, 2026
7c945b4
ui pass over with alex
joshbermanssw Jun 18, 2026
cf400e0
l1nt
joshbermanssw Jun 18, 2026
98f4c7f
l1nt 2
joshbermanssw Jun 18, 2026
026add1
fix build
joshbermanssw Jun 18, 2026
c0e834c
Fix /consulting prerender crash from null React page reference
joshbermanssw Jun 18, 2026
8bd5227
Merge branch 'main' into jb/react-consulting-v2-page-redesign
isaaclombardssw Jun 18, 2026
1ef3711
add SSW adaptive form-field primitives + design tokens
joshbermanssw Jun 19, 2026
231cc27
rebuild lead capture as a multi-step contact form
joshbermanssw Jun 19, 2026
ab40222
add case study link to v3 testimonials
joshbermanssw Jun 19, 2026
93d31ec
update consulting content, logo carousel heading + tina lock
joshbermanssw Jun 19, 2026
29a710d
lint
joshbermanssw Jun 19, 2026
1573c77
upd text
joshbermanssw Jun 19, 2026
a71d1e6
upd text
joshbermanssw Jun 19, 2026
aaf313a
restore #lead-capture-heading anchor target
joshbermanssw Jun 19, 2026
2927c5a
fix(lint): reorder checkbox tailwind classes for prettier
joshbermanssw Jun 19, 2026
04d8b0d
fix(consulting): replace broken testimonial logo with hearing-austral…
joshbermanssw Jun 19, 2026
4badd9a
feat(videoModal): support CMS thumbnail override
joshbermanssw Jun 19, 2026
9f88332
feat(blocks): add v3 Video Highlights and Card Carousel blocks
joshbermanssw Jun 19, 2026
f514df4
feat(consulting): populate React page with new blocks + assets
joshbermanssw Jun 19, 2026
5d6088b
style(cardCarousel): polish v3 card visuals
joshbermanssw Jun 19, 2026
4e44a66
chore(consulting): update card carousel section content
joshbermanssw Jun 19, 2026
58a5ca7
update text
joshbermanssw Jun 19, 2026
8fa957c
fix(videoHighlights): suppress Tina custom-component type error
joshbermanssw Jun 19, 2026
8739470
fix(leadCapture): send location as country + state options for JotForm
joshbermanssw Jun 19, 2026
aac6f9c
fix(consulting): use valid Hearing Australia logo in React testimonial
joshbermanssw Jun 19, 2026
6033729
upd gitignroe
joshbermanssw Jun 19, 2026
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
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ NEXT_PUBLIC_GITHUB_SHA=0000000000000000000000000000000000000000
# Current site URL
SITE_URL=***

#JOTFORM API KEY
JOTFORM_API_KEY=***

# Recaptcha Keys
GOOGLE_RECAPTCHA_KEY=***

Expand Down
66 changes: 66 additions & 0 deletions app/api/lead-capture/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { NextRequest } from "next/server";

/**
* Receives lead-capture answers from the V3LeadCapture block and forwards them
* to JotForm's submissions API, keeping the API key server-side.
*
* Body: { jotFormId: string, fields: Record<qid, value> }
* Each `fields` key is a JotForm question id (qid); JotForm expects them encoded
* as `submission[{qid}]=value`.
*/
export async function POST(request: NextRequest) {
try {
const apiKey = process.env.JOTFORM_API_KEY;
if (!apiKey) {
return Response.json(
{ message: "JotForm is not configured." },
{ status: 500 }
);
}

const { jotFormId, fields } = await request.json();

if (!jotFormId || !fields || typeof fields !== "object") {
return Response.json(
{ message: "Missing jotFormId or fields." },
{ status: 400 }
);
}

const body = new URLSearchParams();
for (const [qid, value] of Object.entries(fields)) {
if (value != null && value !== "") {
body.append(`submission[${qid}]`, String(value));
}
}

const res = await fetch(
`https://api.jotform.com/form/${encodeURIComponent(
jotFormId
)}/submissions?apiKey=${encodeURIComponent(apiKey)}`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: body.toString(),
}
);

if (!res.ok) {
const detail = await res.text();
console.error("JotForm submission failed:", res.status, detail);
return Response.json(
{ message: "Failed to submit lead." },
{ status: 502 }
);
}

return Response.json({ ok: true }, { status: 200 });
} catch (error) {
console.error("lead-capture error:", error);
return Response.json({ message: "Unexpected error." }, { status: 500 });
}
}

export async function GET() {
return Response.json({ message: "Unsupported method" }, { status: 405 });
}
2 changes: 1 addition & 1 deletion app/consulting/[filename]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export async function generateMetadata(

const seo =
newPage?.props?.data?.consultingv2?.seo ||
oldPage.props?.data?.consulting?.seo;
oldPage?.props?.data?.consulting?.seo;

const headerUrl = newPage?.props?.header?.url || oldPage?.props?.header?.url;
if (seo && !seo.canonical) {
Expand Down
54 changes: 54 additions & 0 deletions components/blocks-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,50 @@ const Spacer = dynamic(() =>
import("./blocks/spacer/spacer").then((mod) => mod.Spacer)
);

const V3Hero = dynamic(() =>
import("./blocks/v3/hero/hero").then((mod) => mod.V3Hero)
);

const V3LogoCarousel = dynamic(() =>
import("./blocks/v3/logoCarousel/logoCarousel").then(
(mod) => mod.V3LogoCarousel
)
);

const V3FeatureSteps = dynamic(() =>
import("./blocks/v3/featureSteps/featureSteps").then(
(mod) => mod.V3FeatureSteps
)
);

const V3Process = dynamic(() =>
import("./blocks/v3/process/process").then((mod) => mod.V3Process)
);

const V3Statistics = dynamic(() => import("./blocks/v3/statistics/statistics"));

const V3Cta = dynamic(() =>
import("./blocks/v3/cta/cta").then((mod) => mod.V3Cta)
);

const V3Testimonials = dynamic(() =>
import("./blocks/v3/testimonials/testimonials").then(
(mod) => mod.V3Testimonials
)
);

const V3StackCards = dynamic(() =>
import("./blocks/v3/stackCards/stackCards").then((mod) => mod.V3StackCards)
);

const V3Faq = dynamic(() =>
import("./blocks/v3/faq/faq").then((mod) => mod.V3Faq)
);

const V3LeadCapture = dynamic(() =>
import("./blocks/v3/leadCapture/leadCapture").then((mod) => mod.V3LeadCapture)
);

const componentMap = {
AboutUs,
Carousel,
Expand Down Expand Up @@ -198,6 +242,16 @@ const componentMap = {
TechnologyCardCarousel,
Spacer,
UtilityButton,
V3Hero,
V3LogoCarousel,
V3FeatureSteps,
V3Process,
V3Statistics,
V3Cta,
V3Testimonials,
V3StackCards,
V3Faq,
V3LeadCapture,
};

export const Blocks = ({ prefix, blocks }) => {
Expand Down
20 changes: 20 additions & 0 deletions components/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,32 @@ import { SpacerSchema } from "./spacer/spacer.schema";
import { tableBlockSchema } from "./tableLayout.schema";
import { testimonialsListSchema } from "./testimonialsList";
import { upcomingEventsBlockSchema } from "./upcomingEvents";
import { V3FeatureStepsSchema } from "./v3/featureSteps/featureSteps.schema";
import { V3HeroSchema } from "./v3/hero/hero.schema";
import { V3ProcessSchema } from "./v3/process/process.schema";
import { V3StatisticsTemplate } from "./v3/statistics/statistics.schema";
import { V3CtaSchema } from "./v3/cta/cta.schema";
import { V3LogoCarouselSchema } from "./v3/logoCarousel/logoCarousel.schema";
import { V3TestimonialsSchema } from "./v3/testimonials/testimonials.schema";
import { V3StackCardsSchema } from "./v3/stackCards/stackCards.schema";
import { V3FaqSchema } from "./v3/faq/faq.schema";
import { V3LeadCaptureSchema } from "./v3/leadCapture/leadCapture.template";
import { verticalImageLayoutBlockSchema } from "./verticalImageLayout";
import { verticalListItemSchema } from "./verticalListItem";
import { videoEmbedBlockSchema } from "./videoEmbed.schema";

//NOTE: this is the order that blocks will appear in the Tina Editor
export const pageBlocks: Template[] = [
V3HeroSchema,
V3LogoCarouselSchema,
V3FeatureStepsSchema,
V3ProcessSchema,
V3StatisticsTemplate,
V3CtaSchema,
V3TestimonialsSchema,
V3StackCardsSchema,
V3FaqSchema,
V3LeadCaptureSchema,
BreadcrumbSchema,
ImageTextBlockSchema,
LogoCarouselSchema,
Expand Down
48 changes: 48 additions & 0 deletions components/blocks/v3/cta/cta.schema.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Template } from "tinacms";
import alternatingHeadingSchema from "../../../blocksSubtemplates/alternatingHeading.schema";
import { buttonSchema } from "../../../button/templateButton.schema";
import { backgroundSchema } from "../../../layout/v2ComponentWrapper.schema";

export const V3CtaSchema: Template = {
name: "v3Cta",
label: "<V3> CTA",
ui: {
defaultItem: {
background: {
backgroundColour: 7,
redGlow: true,
gridOverlay: true,
},
heading: "Let's build something **you're proud of.**",
description:
"Get in touch and we'll set up an initial meeting to show you where we'd start.",
buttons: [{ buttonText: "Book your initial meeting", colour: 0 }],
},
},
fields: [
//@ts-expect-error – custom component typing won't be pinned down
backgroundSchema,
alternatingHeadingSchema,
{
type: "rich-text",
label: "Description",
name: "description",
description: "Supporting text shown beneath the heading.",
toolbarOverride: ["bold", "italic", "link"],
},
{
type: "object",
label: "Buttons",
name: "buttons",
list: true,
description: "A row of buttons. Max 2.",
ui: {
defaultItem: { buttonText: "Book your initial meeting" },
max: 2,
itemProps: (item) => ({ label: item?.buttonText ?? "Button" }),
},
//@ts-expect-error – fields are not being recognized
fields: buttonSchema,
},
],
};
46 changes: 46 additions & 0 deletions components/blocks/v3/cta/cta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import AlternatingText from "@/components/alternating-text";
import ButtonRow from "@/components/blocksSubtemplates/buttonRow";
import V2ComponentWrapper from "@/components/layout/v2ComponentWrapper";
import { Container } from "@/components/util/container";
import { tinaField } from "tinacms/dist/react";
import { TinaMarkdown } from "tinacms/dist/rich-text";

export function V3Cta({ data }) {
return (
<V2ComponentWrapper data={data}>
<Container
size="custom"
padding="px-4 sm:px-8"
className="flex flex-col items-center py-24 text-center md:py-32"
>
{data?.heading && (
<h2
data-tina-field={tinaField(data, "heading")}
className="max-w-3xl text-4xl text-white lg:text-6xl"
>
<AlternatingText text={data.heading} />
</h2>
)}
{data?.description && (
<div
data-tina-field={tinaField(data, "description")}
className="mt-6 max-w-sm lg:max-w-2xl"
>
<TinaMarkdown
content={data.description}
components={{
p: (props) => (
<p
{...props}
className="text-base font-light text-gray-300"
/>
),
}}
/>
</div>
)}
<ButtonRow data={data} className="mt-8 items-center justify-center" />
</Container>
</V2ComponentWrapper>
);
}
68 changes: 68 additions & 0 deletions components/blocks/v3/faq/faq.schema.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { Template } from "tinacms";
import alternatingHeadingSchema from "../../../blocksSubtemplates/alternatingHeading.schema";
import { backgroundSchema } from "../../../layout/v2ComponentWrapper.schema";

// Rich-text nested inside a list isn't coerced from a string, so its default
// must be a rich-text AST object (matching accordionSchema's accordionItems).
const answerDefault = (text: string) => ({
type: "root",
children: [{ type: "p", children: [{ type: "text", text }] }],
});

export const V3FaqSchema: Template = {
name: "v3Faq",
label: "<V3> FAQ",
ui: {
defaultItem: {
heading: "Frequently asked questions",
faqs: [
{
question: "How much does it cost?",
answer: answerDefault(
"Every engagement is scoped to your goals, so there's no single price. The Health Check and first session are free, and you'll get a clear, costed plan before you commit to anything."
),
},
{
question: "Do you work with our existing team?",
answer: answerDefault("Yes — we embed alongside your team."),
},
{
question: "Can you rescue a project that's already in trouble?",
answer: answerDefault("Absolutely. We start with a Health Check."),
},
],
},
},
fields: [
//@ts-expect-error – custom component typing won't be pinned down
backgroundSchema,
alternatingHeadingSchema,
{
type: "object",
label: "Questions",
name: "faqs",
list: true,
description: "The list of question / answer pairs.",
ui: {
itemProps: (item) => ({ label: item?.question ?? "Question" }),
defaultItem: {
question: "New question?",
answer: answerDefault("Answer goes here."),
},
},
fields: [
{
type: "string",
label: "Question",
name: "question",
},
{
type: "rich-text",
label: "Answer",
name: "answer",
toolbarOverride: ["bold", "italic", "link"],
},
],
},
],
};
Loading
Loading