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
6 changes: 4 additions & 2 deletions app/[lang]/(hyperjump)/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -344,8 +345,9 @@ function FeaturedProducts({ lang }: HomeParams) {
<StaggerItem key={product.slug}>
<a
href={product.urlLearnMore}
target="_blank"
rel="noopener noreferrer"
{...(product.urlLearnMore && isExternalUrl(product.urlLearnMore)
? { target: "_blank", rel: "noopener noreferrer" }
: {})}
className="group flex h-full flex-col rounded-2xl border border-black/6 bg-white p-7 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl hover:shadow-black/6">
{product.image && (
<div className="bg-hyperjump-surface -mx-7 -mt-7 mb-5 flex h-48 items-center justify-center overflow-hidden rounded-t-2xl">
Expand Down
17 changes: 15 additions & 2 deletions app/[lang]/(hyperjump)/products/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,21 @@ import {
productsCommercialData8Text,
productsCommercialData8Title,
productsCommercialData9Text,
productsCommercialData9Title
productsCommercialData9Title,
productsCommercialData10Text,
productsCommercialData10Title
} from "@/locales/.generated/strings";
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];

Expand Down Expand Up @@ -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),
Expand Down
15 changes: 15 additions & 0 deletions app/[lang]/(hyperjump)/products/get-featured-products.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
});
});
Original file line number Diff line number Diff line change
@@ -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<string>(excludedSlugs);

return getProducts(lang).filter((product) => !excluded.has(product.slug));
}
5 changes: 3 additions & 2 deletions app/[lang]/(hyperjump)/products/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)[] = [
Expand Down
19 changes: 18 additions & 1 deletion e2e/homepage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions locales/en/products.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions locales/id/products.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
}
},
Expand Down
Loading