Skip to content

Commit d85ab81

Browse files
committed
Add OneAI Support Section and Localization Enhancements
- Introduced a new support section for OneAI, highlighting local engineers and their availability during Indonesian office hours. - Implemented the `getOneaiSupportPoints` function to load localized support proof points for both English and Indonesian. - Created corresponding test cases to validate the functionality of the support points retrieval. - Updated localization files to include support section content, ensuring accurate representation of features in both languages. - Added a support image to enhance the visual appeal of the section. These changes aim to improve user engagement by providing clear and localized support information, emphasizing the benefits of local engineering support.
1 parent 12ed542 commit d85ab81

8 files changed

Lines changed: 265 additions & 0 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import {
4+
getOneaiSupportPoints,
5+
ONEAI_SUPPORT_IMAGE_PATH
6+
} from "./get-oneai-support-points";
7+
8+
describe("ONEAI_SUPPORT_IMAGE_PATH", () => {
9+
it("points to the public support photo", () => {
10+
// Act
11+
const path = ONEAI_SUPPORT_IMAGE_PATH;
12+
13+
// Assert
14+
expect(path).toBe("/images/oneai/support-engineers.jpg");
15+
});
16+
});
17+
18+
describe("getOneaiSupportPoints", () => {
19+
it("returns three English proof points", () => {
20+
// Act
21+
const points = getOneaiSupportPoints({ lang: "en" });
22+
23+
// Assert
24+
expect(points).toHaveLength(3);
25+
expect(points[0]).toEqual({
26+
title: "Same timezone, same workday",
27+
text: "Replies during Indonesian office hours, not after your team has gone home."
28+
});
29+
expect(points[1]?.title).toContain("Bahasa Indonesia");
30+
expect(points[2]?.title).toContain("On-site");
31+
});
32+
33+
it("returns three Indonesian proof points", () => {
34+
// Act
35+
const points = getOneaiSupportPoints({ lang: "id" });
36+
37+
// Assert
38+
expect(points).toHaveLength(3);
39+
expect(points[0]?.title).toContain("Zona waktu");
40+
expect(points[1]?.title).toContain("Bahasa Indonesia");
41+
expect(points[2]?.title).toContain("kantor");
42+
});
43+
44+
it("uses injected point loaders when provided", () => {
45+
// Act
46+
const points = getOneaiSupportPoints({
47+
lang: "en",
48+
pointLoaders: [
49+
{ title: () => "Title A", text: () => "Text A" },
50+
{ title: () => "Title B", text: () => "Text B" }
51+
]
52+
});
53+
54+
// Assert
55+
expect(points).toEqual([
56+
{ title: "Title A", text: "Text A" },
57+
{ title: "Title B", text: "Text B" }
58+
]);
59+
});
60+
61+
it("returns an empty list when no loaders are provided", () => {
62+
// Act
63+
const points = getOneaiSupportPoints({ lang: "en", pointLoaders: [] });
64+
65+
// Assert
66+
expect(points).toEqual([]);
67+
});
68+
});
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { SupportedLanguage } from "@/locales/.generated/types";
2+
import {
3+
oneaiSupportPoints0Text,
4+
oneaiSupportPoints0Title,
5+
oneaiSupportPoints1Text,
6+
oneaiSupportPoints1Title,
7+
oneaiSupportPoints2Text,
8+
oneaiSupportPoints2Title
9+
} from "@/locales/.generated/strings";
10+
11+
/** Public path to the full-bleed support section photo. */
12+
export const ONEAI_SUPPORT_IMAGE_PATH = "/images/oneai/support-engineers.jpg";
13+
14+
export type OneaiSupportPoint = {
15+
title: string;
16+
text: string;
17+
};
18+
19+
type PointLoader = {
20+
title: (lang: SupportedLanguage) => string;
21+
text: (lang: SupportedLanguage) => string;
22+
};
23+
24+
export type GetOneaiSupportPointsOptions = {
25+
lang: SupportedLanguage;
26+
/** Point loaders for tests */
27+
pointLoaders?: PointLoader[];
28+
};
29+
30+
const DEFAULT_POINT_LOADERS: PointLoader[] = [
31+
{ title: oneaiSupportPoints0Title, text: oneaiSupportPoints0Text },
32+
{ title: oneaiSupportPoints1Title, text: oneaiSupportPoints1Text },
33+
{ title: oneaiSupportPoints2Title, text: oneaiSupportPoints2Text }
34+
];
35+
36+
/**
37+
* Loads localized support proof points for the OneAI landing page.
38+
*
39+
* @param options - Locale and optional point loaders for DI
40+
* @returns Ordered title and text pairs shown over the support photo
41+
*/
42+
export function getOneaiSupportPoints({
43+
lang,
44+
pointLoaders = DEFAULT_POINT_LOADERS
45+
}: GetOneaiSupportPointsOptions): OneaiSupportPoint[] {
46+
return pointLoaders.map(({ text, title }) => ({
47+
title: title(lang),
48+
text: text(lang)
49+
}));
50+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import Image from "next/image";
2+
import Link from "next/link";
3+
import { MailIcon } from "lucide-react";
4+
5+
import { Button } from "@/components/ui/button";
6+
import type { SupportedLanguage } from "@/locales/.generated/types";
7+
import {
8+
oneaiSupportCta,
9+
oneaiSupportEyebrow,
10+
oneaiSupportHeading,
11+
oneaiSupportImageAlt,
12+
oneaiSupportLede
13+
} from "@/locales/.generated/strings";
14+
15+
import { SectionReveal } from "../components/motion-wrappers";
16+
import {
17+
getOneaiSupportPoints,
18+
ONEAI_SUPPORT_IMAGE_PATH,
19+
type OneaiSupportPoint
20+
} from "./get-oneai-support-points";
21+
22+
type OneaiSupportSectionProps = {
23+
lang: SupportedLanguage;
24+
mailtoHref: string;
25+
/** Support points loader for tests */
26+
getPoints?: (options: { lang: SupportedLanguage }) => OneaiSupportPoint[];
27+
/** Photo path for tests */
28+
imagePath?: string;
29+
};
30+
31+
/**
32+
* Full-bleed support section contrasting local Hyperjump engineers with overseas vendor queues.
33+
*
34+
* @param props - Locale, mailto CTA, and optional loaders for DI
35+
*/
36+
export function OneaiSupportSection({
37+
lang,
38+
mailtoHref,
39+
getPoints = getOneaiSupportPoints,
40+
imagePath = ONEAI_SUPPORT_IMAGE_PATH
41+
}: OneaiSupportSectionProps) {
42+
const points = getPoints({ lang });
43+
44+
return (
45+
<section
46+
id="support"
47+
data-testid="oneai-support-section"
48+
className="relative min-h-144 w-full overflow-hidden md:min-h-168">
49+
<Image
50+
src={imagePath}
51+
alt={oneaiSupportImageAlt(lang)}
52+
fill
53+
className="object-cover object-[72%_center]"
54+
sizes="100vw"
55+
/>
56+
<div className="absolute inset-0 bg-linear-to-r from-[#020F15]/94 via-[#020F15]/78 to-[#020F15]/40 md:via-[#020F15]/72 md:to-[#020F15]/22" />
57+
58+
<div className="relative z-10 mx-auto flex min-h-144 max-w-6xl items-center px-4 py-16 md:min-h-168 md:px-20 xl:px-0">
59+
<SectionReveal className="max-w-xl">
60+
<p className="mb-4 text-xs font-bold tracking-[0.16em] text-yellow-300 uppercase">
61+
{oneaiSupportEyebrow(lang)}
62+
</p>
63+
<h2
64+
className="mb-5 text-3xl font-semibold tracking-tight text-white md:text-4xl lg:text-[2.75rem] lg:leading-tight"
65+
data-testid="oneai-support-heading">
66+
{oneaiSupportHeading(lang)}
67+
</h2>
68+
<p className="mb-8 text-base leading-relaxed text-white/80 md:text-lg">
69+
{oneaiSupportLede(lang)}
70+
</p>
71+
<ul className="mb-8 space-y-4">
72+
{points.map(({ text, title }) => (
73+
<li key={title} className="border-l-2 border-yellow-300/70 pl-4">
74+
<p className="text-sm font-semibold text-white">{title}</p>
75+
<p className="mt-1 text-sm leading-relaxed text-white/70">
76+
{text}
77+
</p>
78+
</li>
79+
))}
80+
</ul>
81+
<Button
82+
asChild
83+
className="bg-hyperjump-blue hover:bg-hyperjump-blue/90 h-12 rounded-full px-8 text-base font-semibold">
84+
<Link href={mailtoHref} data-testid="oneai-support-cta">
85+
<MailIcon className="mr-2 h-4 w-4" aria-hidden />
86+
{oneaiSupportCta(lang)}
87+
</Link>
88+
</Button>
89+
</SectionReveal>
90+
</div>
91+
</section>
92+
);
93+
}

app/[lang]/(hyperjump)/oneai/page.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ import {
9090
import { OneaiComparisonSection } from "./oneai-comparison-section";
9191
import { OneaiHeroVisual } from "./oneai-hero-visual";
9292
import { OneaiStickyCta } from "./oneai-sticky-cta";
93+
import { OneaiSupportSection } from "./oneai-support-section";
9394

9495
const { url } = data;
9596
const OG_IMAGE = `${url}${ONEAI_OG_IMAGE_PATH}`;
@@ -401,6 +402,8 @@ export default async function OneaiPage({ params }: OneaiPageProps) {
401402

402403
<OneaiComparisonSection lang={lang} />
403404

405+
<OneaiSupportSection lang={lang} mailtoHref={mailtoHref} />
406+
404407
<section
405408
id="faqs"
406409
data-testid="oneai-faq-section"

e2e/oneai.spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
oneaiDocxDownloadCta,
88
oneaiPdfDownloadCta,
99
oneaiHeroHeading,
10+
oneaiSupportHeading,
1011
oneaiMailtoSubject,
1112
oneaiMetaDescription,
1213
oneaiMetaTitle,
@@ -101,6 +102,14 @@ for (const locale of supportedLanguages) {
101102
await expect(docxLink).toHaveText(oneaiDocxDownloadCta(locale));
102103
});
103104

105+
test("support section highlights local engineers", async ({ page }) => {
106+
await expect(page.getByTestId("oneai-support-section")).toBeVisible();
107+
await expect(page.getByTestId("oneai-support-heading")).toHaveText(
108+
oneaiSupportHeading(locale)
109+
);
110+
await expect(page.getByTestId("oneai-support-cta")).toBeVisible();
111+
});
112+
104113
test("faq section is visible", async ({ page }) => {
105114
await expect(page.getByTestId("oneai-faq-section")).toBeVisible();
106115
await expect(page.getByTestId("oneai-faq-accordion")).toBeVisible();

locales/en/oneai.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,27 @@
189189
}
190190
}
191191
},
192+
"support": {
193+
"eyebrow": "Support in Indonesia",
194+
"heading": "If your company is in Indonesia, we are already next door",
195+
"lede": "You get engineers in the same timezone, who speak your language, and can sit with you in the office. Questions get answered during your workday: fast, direct, and easy. Support from companies like OpenAI will never feel like that.",
196+
"points": {
197+
"0": {
198+
"title": "Same timezone, same workday",
199+
"text": "Replies during Indonesian office hours, not after your team has gone home."
200+
},
201+
"1": {
202+
"title": "Bahasa Indonesia, no translation lag",
203+
"text": "Talk to engineers who already understand your stack, your constraints, and your language."
204+
},
205+
"2": {
206+
"title": "On-site when it matters",
207+
"text": "When a rollout needs someone in the room, we can come to you. A global ticket portal cannot."
208+
}
209+
},
210+
"cta": "Talk to the Jakarta team",
211+
"image_alt": "Hyperjump engineers collaborating in a Jakarta office"
212+
},
192213
"pricing": {
193214
"label": "Plan · up to 40 users",
194215
"price": "Rp12.400.000",

locales/id/oneai.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,27 @@
189189
}
190190
}
191191
},
192+
"support": {
193+
"eyebrow": "Support di Indonesia",
194+
"heading": "Cepat dan gampang, karena kami di Indonesia juga",
195+
"lede": "Kalau ada yang macet, Anda tidak antre tiket ke San Francisco. Engineer Hyperjump ada di zona waktu Anda, bicara bahasa Anda, dan bisa datang ke kantor Anda. Support dari perusahaan seperti OpenAI tidak akan seperti itu.",
196+
"points": {
197+
"0": {
198+
"title": "Zona waktu yang sama",
199+
"text": "Dibalas di jam kerja Indonesia, bukan setelah tim Anda pulang."
200+
},
201+
"1": {
202+
"title": "Bahasa Indonesia, tanpa bolak-balik",
203+
"text": "Bicara langsung dengan engineer yang sudah paham stack, kendala, dan bahasa Anda."
204+
},
205+
"2": {
206+
"title": "Datang ke kantor kalau perlu",
207+
"text": "Kalau rollout butuh orang di ruangan, kami bisa datang. Portal tiket global tidak bisa."
208+
}
209+
},
210+
"cta": "Ngobrol dengan tim Jakarta",
211+
"image_alt": "Engineer Hyperjump sedang berkolaborasi di kantor Jakarta"
212+
},
192213
"pricing": {
193214
"label": "Paket harga · hingga 40 pengguna",
194215
"price": "Rp12.400.000",
299 KB
Loading

0 commit comments

Comments
 (0)