From dc80fda693b6a2cff8b1e12cd9b0a60c105b4116 Mon Sep 17 00:00:00 2001 From: nicnocquee <{{GITHUB_EMAIL}}> Date: Tue, 27 Oct 2026 16:55:11 +0700 Subject: [PATCH] Update the public product lineup. Feature OneAI, SearchPoint, and MediaPulse on the homepage, and drop Avenu, StartGPT, and TypeTable from the products page. --- app/[lang]/(hyperjump)/page.tsx | 6 +- app/[lang]/(hyperjump)/products/data.ts | 17 +++- .../products/get-featured-products.test.ts | 15 ++++ ...-products-page-commercial-products.test.ts | 90 +++++++++++++++++++ .../get-products-page-commercial-products.ts | 38 ++++++++ app/[lang]/(hyperjump)/products/page.tsx | 5 +- e2e/homepage.spec.ts | 19 +++- locales/en/products.json | 4 + locales/id/products.json | 4 + 9 files changed, 191 insertions(+), 7 deletions(-) create mode 100644 app/[lang]/(hyperjump)/products/get-products-page-commercial-products.test.ts create mode 100644 app/[lang]/(hyperjump)/products/get-products-page-commercial-products.ts diff --git a/app/[lang]/(hyperjump)/page.tsx b/app/[lang]/(hyperjump)/page.tsx index c05c8e3a..596f1016 100644 --- a/app/[lang]/(hyperjump)/page.tsx +++ b/app/[lang]/(hyperjump)/page.tsx @@ -17,6 +17,7 @@ import { } from "@/components/ui/accordion"; import data from "@/data.json"; import { dynamicOpengraph } from "@/lib/default-metadata"; +import { isExternalUrl } from "@/lib/is-external-url"; import { supportedLanguages, type SupportedLanguage @@ -344,8 +345,9 @@ function FeaturedProducts({ lang }: HomeParams) { {product.image && (
diff --git a/app/[lang]/(hyperjump)/products/data.ts b/app/[lang]/(hyperjump)/products/data.ts index 8e8f3525..2cd60c87 100644 --- a/app/[lang]/(hyperjump)/products/data.ts +++ b/app/[lang]/(hyperjump)/products/data.ts @@ -22,7 +22,9 @@ import { productsCommercialData8Text, productsCommercialData8Title, productsCommercialData9Text, - productsCommercialData9Title + productsCommercialData9Title, + productsCommercialData10Text, + productsCommercialData10Title } from "@/locales/.generated/strings"; import type { SupportedLanguage } from "@/locales/.generated/types"; @@ -30,7 +32,11 @@ import type { SupportedLanguage } from "@/locales/.generated/types"; * Product slugs shown on the homepage, in display order. * Edit this list to change which commercial products are featured. */ -export const FEATURED_PRODUCT_SLUGS = ["typetable", "hydra8", "avenu"] as const; +export const FEATURED_PRODUCT_SLUGS = [ + "oneai", + "searchpoint", + "media-pulse" +] as const; export type FeaturedProductSlug = (typeof FEATURED_PRODUCT_SLUGS)[number]; @@ -58,6 +64,13 @@ export function getCommercialProduct( image: "/images/products/oneai.svg", urlLearnMore: `/${lang}/oneai` }, + { + slug: "searchpoint", + title: productsCommercialData10Title(lang), + description: productsCommercialData10Text(lang), + image: "/images/searchpoint/feature-sp-1-trimmed.webp", + urlLearnMore: "https://searchpoint.sinarmas.hyperjump.tech/landing" + }, { slug: "typetable", title: productsCommercialData6Title(lang), diff --git a/app/[lang]/(hyperjump)/products/get-featured-products.test.ts b/app/[lang]/(hyperjump)/products/get-featured-products.test.ts index f7293467..1305519c 100644 --- a/app/[lang]/(hyperjump)/products/get-featured-products.test.ts +++ b/app/[lang]/(hyperjump)/products/get-featured-products.test.ts @@ -71,6 +71,21 @@ describe("getFeaturedProducts", () => { expect(result).toEqual([]); }); + it("features OneAI, SearchPoint, and MediaPulse from the production catalog", () => { + // Act + const result = getFeaturedProducts(lang); + + // Assert + expect(result.map((product) => product.slug)).toEqual([ + "oneai", + "searchpoint", + "media-pulse" + ]); + expect( + result.find((product) => product.slug === "searchpoint")?.urlLearnMore + ).toBe("https://searchpoint.sinarmas.hyperjump.tech/landing"); + }); + it("returns an empty list when catalog has no matches", () => { // Act const result = getFeaturedProducts(lang, { diff --git a/app/[lang]/(hyperjump)/products/get-products-page-commercial-products.test.ts b/app/[lang]/(hyperjump)/products/get-products-page-commercial-products.test.ts new file mode 100644 index 00000000..9be5c355 --- /dev/null +++ b/app/[lang]/(hyperjump)/products/get-products-page-commercial-products.test.ts @@ -0,0 +1,90 @@ +import { describe, expect, it } from "vitest"; + +import type { SupportedLanguage } from "@/locales/.generated/types"; + +import type { CommercialProduct } from "./data"; +import { + getProductsPageCommercialProducts, + PRODUCTS_PAGE_EXCLUDED_SLUGS +} from "./get-products-page-commercial-products"; + +const lang: SupportedLanguage = "en"; + +/** + * Builds a minimal commercial product for tests. + * + * @param slug - Product slug + * @param title - Display title + */ +function fakeProduct(slug: string, title: string): CommercialProduct { + return { + slug, + title, + description: `${title} description`, + image: `/images/products/${slug}.svg`, + urlLearnMore: `https://example.com/${slug}` + }; +} + +describe("getProductsPageCommercialProducts", () => { + it("omits Avenu, StartGPT, and TypeTable and keeps the remaining products in order", () => { + // Setup + const catalog = [ + fakeProduct("oneai", "OneAI"), + fakeProduct("avenu", "Avenu"), + fakeProduct("typetable", "TypeTable"), + fakeProduct("startgpt", "StartGPT"), + fakeProduct("hydra8", "Hydra8") + ]; + + // Act + const result = getProductsPageCommercialProducts(lang, { + getProducts: () => catalog + }); + + // Assert + expect(result.map((product) => product.slug)).toEqual(["oneai", "hydra8"]); + }); + + it("returns every product when the exclusion list is empty", () => { + // Setup + const catalog = [ + fakeProduct("avenu", "Avenu"), + fakeProduct("startgpt", "StartGPT") + ]; + + // Act + const result = getProductsPageCommercialProducts(lang, { + getProducts: () => catalog, + excludedSlugs: [] + }); + + // Assert + expect(result).toEqual(catalog); + }); + + it("returns an empty list when the catalog is empty", () => { + // Act + const result = getProductsPageCommercialProducts(lang, { + getProducts: () => [], + excludedSlugs: PRODUCTS_PAGE_EXCLUDED_SLUGS + }); + + // Assert + expect(result).toEqual([]); + }); + + it("omits Avenu, StartGPT, and TypeTable from the production catalog", () => { + // Act + const result = getProductsPageCommercialProducts(lang); + + // Assert + const slugs = result.map((product) => product.slug); + expect(slugs).not.toContain("avenu"); + expect(slugs).not.toContain("startgpt"); + expect(slugs).not.toContain("typetable"); + expect(slugs).toContain("oneai"); + expect(slugs).toContain("searchpoint"); + expect(slugs).toContain("hydra8"); + }); +}); diff --git a/app/[lang]/(hyperjump)/products/get-products-page-commercial-products.ts b/app/[lang]/(hyperjump)/products/get-products-page-commercial-products.ts new file mode 100644 index 00000000..d4dce68a --- /dev/null +++ b/app/[lang]/(hyperjump)/products/get-products-page-commercial-products.ts @@ -0,0 +1,38 @@ +import type { SupportedLanguage } from "@/locales/.generated/types"; + +import { getCommercialProduct, type CommercialProduct } from "./data"; + +/** Product slugs omitted from the products page. */ +export const PRODUCTS_PAGE_EXCLUDED_SLUGS = [ + "avenu", + "startgpt", + "typetable" +] as const; + +type GetProducts = (lang: SupportedLanguage) => CommercialProduct[]; + +type GetProductsPageCommercialProductsOptions = { + /** Catalog loader; defaults to production commercial products. */ + getProducts?: GetProducts; + /** Slugs to omit; defaults to PRODUCTS_PAGE_EXCLUDED_SLUGS. */ + excludedSlugs?: readonly string[]; +}; + +/** + * Returns commercial products for the products page, omitting excluded slugs. + * + * @param lang - Active locale passed to the product catalog loader + * @param options - Optional DI overrides for the catalog and excluded slugs + * @returns Commercial products in catalog order, without excluded slugs + */ +export function getProductsPageCommercialProducts( + lang: SupportedLanguage, + { + getProducts = getCommercialProduct, + excludedSlugs = PRODUCTS_PAGE_EXCLUDED_SLUGS + }: GetProductsPageCommercialProductsOptions = {} +): CommercialProduct[] { + const excluded = new Set(excludedSlugs); + + return getProducts(lang).filter((product) => !excluded.has(product.slug)); +} diff --git a/app/[lang]/(hyperjump)/products/page.tsx b/app/[lang]/(hyperjump)/products/page.tsx index cbaed660..0826f328 100644 --- a/app/[lang]/(hyperjump)/products/page.tsx +++ b/app/[lang]/(hyperjump)/products/page.tsx @@ -29,7 +29,8 @@ import { } from "../components/motion-wrappers"; import type { CommercialProduct, OpenSourceProduct } from "./data"; -import { getCommercialProduct, openSourceProducts } from "./data"; +import { openSourceProducts } from "./data"; +import { getProductsPageCommercialProducts } from "./get-products-page-commercial-products"; import { OSSSection } from "./oss-section"; const { url } = dataJson; @@ -65,7 +66,7 @@ export async function generateMetadata({ export default async function ProductsPage({ params }: ProductsProps) { const { lang } = await params; - const commercial = getCommercialProduct(lang); + const commercial = getProductsPageCommercialProducts(lang); const oss = openSourceProducts(lang); const [featured, ...restCommercial] = commercial; const allProducts: (CommercialProduct | OpenSourceProduct)[] = [ diff --git a/e2e/homepage.spec.ts b/e2e/homepage.spec.ts index 9188a33f..341c2330 100644 --- a/e2e/homepage.spec.ts +++ b/e2e/homepage.spec.ts @@ -141,10 +141,27 @@ test.describe("Homepage", () => { }); await expect(productsHeading).toBeVisible(); - for (const title of ["TypeTable", "Hydra8", "Avenu"]) { + for (const title of ["OneAI", "SearchPoint", "MediaPulse"]) { await expect(products.getByText(title).first()).toBeVisible(); } + await expect(products.getByText("TypeTable")).toHaveCount(0); + + const oneAi = products.getByRole("link", { name: /OneAI/i }); + await expect(oneAi).toHaveAttribute("href", /\/oneai$/); + + const searchPoint = products.getByRole("link", { name: /SearchPoint/i }); + await expect(searchPoint).toHaveAttribute( + "href", + "https://searchpoint.sinarmas.hyperjump.tech/landing" + ); + + const mediaPulse = products.getByRole("link", { name: /MediaPulse/i }); + await expect(mediaPulse).toHaveAttribute( + "href", + /mediapulse\.hyperjump\.tech/i + ); + const frontierNews = products.locator("#frontier-news"); await expect(frontierNews).toBeVisible(); await expect( diff --git a/locales/en/products.json b/locales/en/products.json index f613fd9d..a92599dc 100644 --- a/locales/en/products.json +++ b/locales/en/products.json @@ -44,6 +44,10 @@ "9": { "title": "Frontier News", "text": "Daily newsletter with curated summaries of YouTube videos about AI, available in multiple languages." + }, + "10": { + "title": "SearchPoint", + "text": "Search local files, OneDrive, and SharePoint in one place. On-premise, with source permissions respected and raw files kept in your environment." } } }, diff --git a/locales/id/products.json b/locales/id/products.json index b5623024..34ebf233 100644 --- a/locales/id/products.json +++ b/locales/id/products.json @@ -44,6 +44,10 @@ "9": { "title": "Frontier News", "text": "Newsletter harian berisi ringkasan terkurasi dari video YouTube tentang AI, tersedia dalam berbagai bahasa." + }, + "10": { + "title": "SearchPoint", + "text": "Cari file lokal, OneDrive, dan SharePoint di satu tempat. On-premise, dengan izin sumber yang tetap dihormati dan file asli yang tetap di lingkungan Anda." } } },