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
5 changes: 5 additions & 0 deletions components/blocks-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,10 @@ const V3CardCarousel = dynamic(() =>
)
);

const V3Pills = dynamic(() =>
import("./blocks/v3/pills/pills").then((mod) => mod.V3Pills)
);

const componentMap = {
AboutUs,
Carousel,
Expand Down Expand Up @@ -307,6 +311,7 @@ const componentMap = {
V3Cta,
V3Testimonials,
V3StackCards,
V3Pills,
V3Faq,
V3LeadCapture,
V3VideoHighlights,
Expand Down
2 changes: 2 additions & 0 deletions components/blocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ 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 { V3PillsSchema } from "./v3/pills/pills.schema";
import { V3FaqSchema } from "./v3/faq/faq.schema";
import { V3LeadCaptureSchema } from "./v3/leadCapture/leadCapture.template";
import { V3VideoHighlightsSchema } from "./v3/videoHighlights/videoHighlights.schema";
Expand All @@ -95,6 +96,7 @@ export const pageBlocks: Template[] = [
V3CtaSchema,
V3TestimonialsSchema,
V3StackCardsSchema,
V3PillsSchema,
V3FaqSchema,
V3LeadCaptureSchema,
V3VideoHighlightsSchema,
Expand Down
4 changes: 4 additions & 0 deletions components/blocks/v3/hero/hero.schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ export const V3HeroSchema: Template = {
value: "reactConsultingSvg",
label: "React Consulting Atom (animated)",
},
{
value: "aiConsultingSvg",
label: "AI Consulting Neural Network (animated)",
},
],
},
{
Expand Down
111 changes: 111 additions & 0 deletions components/blocks/v3/hero/media/AiConsultingHeroMedia.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"use client";

import { useReducedMotion } from "framer-motion";

/**
* Neural-network composition for the AI Consulting v3 hero RHS.
*
* Layers of nodes are fully connected; a handful of the resulting edges carry a
* pulse that travels along the real path (via <animateMotion><mpath/>), so the
* signal always tracks the line it belongs to.
*/

const LAYERS = [
{ x: 46, ys: [78, 150, 222] },
{ x: 169, ys: [50, 117, 183, 250] },
{ x: 292, ys: [110, 190] },
];

const EDGES = LAYERS.slice(0, -1).flatMap((layer, layerIndex) =>
layer.ys.flatMap((y1) =>
LAYERS[layerIndex + 1].ys.map((y2) => ({
x1: layer.x,
y1,
x2: LAYERS[layerIndex + 1].x,
y2,
}))
)
);

// Every third edge carries a pulse β€” enough movement to read as a live network
// without turning the whole graph into noise.
const PULSE_EDGES = EDGES.map((_, i) => i).filter((i) => i % 3 === 0);

const NODE_COLOUR = "#CC4141";
const PULSE_COLOUR = "#DA7373";

const glow = (colour: string) => ({ filter: `drop-shadow(0 0 8px ${colour})` });

export default function AiConsultingHeroMedia() {
const reduceMotion = useReducedMotion();

return (
<svg
viewBox="0 0 338 300"
fill="none"
xmlns="http://www.w3.org/2000/svg"
role="img"
aria-label="Animated neural network illustrating custom AI models"
className="h-auto w-full max-w-media overflow-visible"
>
{EDGES.map((edge, i) => (
<path
key={i}
id={`hero-ai-edge-${i}`}
d={`M${edge.x1} ${edge.y1}L${edge.x2} ${edge.y2}`}
stroke={NODE_COLOUR}
strokeOpacity={0.35}
strokeWidth={1.5}
/>
))}

{PULSE_EDGES.map((edgeIndex, i) =>
reduceMotion ? null : (
<circle
key={edgeIndex}
r={4}
fill={PULSE_COLOUR}
style={glow(PULSE_COLOUR)}
>
<animateMotion
dur="2.8s"
begin={`-${i * 0.45}s`}
repeatCount="indefinite"
>
<mpath href={`#hero-ai-edge-${edgeIndex}`} />
</animateMotion>
</circle>
)
)}

{LAYERS.map((layer, layerIndex) =>
layer.ys.map((y, i) => {
const radius = layerIndex === LAYERS.length - 1 ? 16 : 9;
return (
<circle
key={`${layerIndex}-${i}`}
cx={layer.x}
cy={y}
r={radius}
fill={NODE_COLOUR}
style={glow(NODE_COLOUR)}
>
{!reduceMotion && (
<animate
attributeName="r"
values={`${radius};${radius + 2.5};${radius}`}
dur="3s"
begin={`-${(layerIndex + i) * 0.4}s`}
repeatCount="indefinite"
calcMode="spline"
keyTimes="0;0.5;1"
keySplines="0.4 0 0.6 1;0.4 0 0.6 1"
/>
)}
</circle>
);
})
)}
</svg>
);
}
1 change: 1 addition & 0 deletions components/blocks/v3/hero/media/registry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { ComponentType } from "react";
*/
export const heroMediaRegistry: Record<string, ComponentType> = {
reactConsultingSvg: dynamic(() => import("./ReactConsultingHeroMedia")),
aiConsultingSvg: dynamic(() => import("./AiConsultingHeroMedia")),
};

export type HeroMediaKey = keyof typeof heroMediaRegistry;
76 changes: 76 additions & 0 deletions components/blocks/v3/pills/pills.schema.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type { Template } from "tinacms";
import alternatingHeadingSchema from "../../../blocksSubtemplates/alternatingHeading.schema";
import { backgroundSchema } from "../../../layout/v2ComponentWrapper.schema";

export const V3PillsSchema: Template = {
name: "v3Pills",
label: "<V3> Pills",
ui: {
defaultItem: {
background: { backgroundColour: 7 },
brow: "TECH WE USE",
heading: "AI Models We Work With",
pills: [{ label: "Claude" }, { label: "ChatGPT" }, { label: "Gemini" }],
},
},
fields: [
//@ts-expect-error – custom component typing won't be pinned down
backgroundSchema,
{
type: "string",
label: "Brow",
name: "brow",
description: "Small eyebrow text above the title.",
},
alternatingHeadingSchema,
{
type: "string",
label: "Subtitle",
name: "subtitle",
description: "Optional short line beneath the title.",
ui: { component: "textarea" },
},
{
type: "object",
label: "Pills",
name: "pills",
list: true,
description: "Labels shown as a wrapping row of pills.",
ui: {
itemProps: (item) => ({ label: item?.label ?? "Pill" }),
defaultItem: { label: "Lorem" },
},
fields: [
{
type: "string",
label: "Label",
name: "label",
},
{
type: "image",
label: "Image",
name: "image",
description: "Optional. Shown to the left of the label.",
},
{
type: "string",
label: "Image Alt Text",
name: "imageAlt",
description: "Defaults to the label.",
},
{
type: "string",
label: "Link",
name: "link",
description:
"Optional. If set, the pill becomes clickable. Use a relative path (e.g. /consulting) for internal links or a full URL (https://…) for external ones.",
},
{
type: "boolean",
label: "Open in New Tab",
name: "newTab",
},
],
},
],
};
92 changes: 92 additions & 0 deletions components/blocks/v3/pills/pills.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import AlternatingText from "@/components/alternating-text";
import V2ComponentWrapper from "@/components/layout/v2ComponentWrapper";
import { Container } from "@/components/util/container";
import Image from "next/image";
import Link from "next/link";
import { tinaField } from "tinacms/dist/react";

export function V3Pills({ data }) {
const pills = data?.pills ?? [];

return (
<V2ComponentWrapper data={data}>
<Container
size="custom"
padding="px-4 sm:px-8"
className="py-16 md:py-24"
>
{data?.brow && (
<span
data-tina-field={tinaField(data, "brow")}
className="flex items-center gap-2 font-mono text-xs uppercase tracking-wider text-sswRed"
>
{data.brow}
</span>
)}
{data?.heading && (
<h2
data-tina-field={tinaField(data, "heading")}
className="my-4 text-3xl text-white lg:text-4xl"
>
<AlternatingText text={data.heading} />
</h2>
)}
{data?.subtitle && (
<p
data-tina-field={tinaField(data, "subtitle")}
className="max-w-2xl text-base font-light text-gray-400"
>
{data.subtitle}
</p>
)}

{pills.length > 0 && (
<div className="mt-10 flex flex-wrap gap-3">
{pills.map((pill, index) => {
const inner = (
<>
{pill?.image && (
<span className="relative size-5 shrink-0">
<Image
src={pill.image}
alt={pill?.imageAlt ?? pill?.label ?? ""}
fill
sizes="20px"
className="object-contain"
/>
</span>
)}
<span>{pill?.label}</span>
</>
);

const className =
"flex h-12 items-center gap-2.5 rounded-xl border-0.75 border-sswBorder bg-sswCard px-5 font-semibold text-white";

return pill?.link ? (
<Link
key={`v3-pill-${index}`}
href={pill.link}
target={pill?.newTab ? "_blank" : undefined}
rel={pill?.newTab ? "noopener noreferrer" : undefined}
data-tina-field={tinaField(pill, "label")}
className={`${className} !no-underline transition hover:border-sswRed`}
>
{inner}
</Link>
) : (
<div
key={`v3-pill-${index}`}
data-tina-field={tinaField(pill, "label")}
className={className}
>
{inner}
</div>
);
})}
</div>
)}
</Container>
</V2ComponentWrapper>
);
}
8 changes: 7 additions & 1 deletion components/blocks/v3/testimonials/testimonials.schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,14 @@ export const V3TestimonialsSchema: Template = {
type: "string",
label: "Case Study URL",
name: "caseStudyUrl",
description: "If set, a link is shown below the quote.",
},
{
type: "string",
label: "Case Study Link Text",
name: "caseStudyLabel",
description:
"If set, a 'SEE CASE STUDY' link is shown below the quote.",
"Text for the case study link (always shown in uppercase). Defaults to 'See Case Study'.",
},
{
type: "string",
Expand Down
4 changes: 3 additions & 1 deletion components/blocks/v3/testimonials/testimonials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,9 @@ export function V3Testimonials({ data }) {
}}
className="group mt-6 inline-flex items-center gap-1 text-sm font-semibold uppercase tracking-wide text-foreground transition hover:text-sswRed"
>
See Case Study
<span data-tina-field={tinaField(current, "caseStudyLabel")}>
{current.caseStudyLabel || "See Case Study"}
</span>
<TiArrowRight className="size-5 transition group-hover:translate-x-1" />
</motion.a>
)}
Expand Down
3 changes: 2 additions & 1 deletion components/layout/v2ComponentWrapper.schema.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ export const backgroundSchema = {
type: "boolean",
label: "Bleed",
name: "bleed",
description: "If true, the background will bleed into lower blocks.",
description:
"If true, the background image and red glow extend past this block into the ones above and below.",
},
{
type: "boolean",
Expand Down
14 changes: 12 additions & 2 deletions components/layout/v2ComponentWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,21 @@ const V2ComponentWrapper = ({
<>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-10 bg-red-glow-tl"
className={classNames(
"pointer-events-none absolute inset-x-0 z-10",
data.background?.bleed
? "-inset-y-40 bg-red-glow-tl-bleed"
: "inset-y-0 bg-red-glow-tl"
)}
/>
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 z-10 bg-red-glow-r"
className={classNames(
"pointer-events-none absolute inset-x-0 z-10",
data.background?.bleed
? "-inset-y-40 bg-red-glow-r-bleed"
: "inset-y-0 bg-red-glow-r"
)}
/>
</>
)}
Expand Down
Loading
Loading