From 974e04aeae0c8683f1481a873e9676f4db09f91d Mon Sep 17 00:00:00 2001 From: Pan YANG Date: Fri, 31 Jul 2026 09:19:21 +0800 Subject: [PATCH 1/4] feat(catalog): order deprecated products last --- src/components/product/ComparisonTable.tsx | 22 +++++++---- src/lib/deprecated.ts | 15 ++++++++ tests/deprecated-order.test.ts | 44 ++++++++++++++++++++++ 3 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 src/lib/deprecated.ts create mode 100644 tests/deprecated-order.test.ts diff --git a/src/components/product/ComparisonTable.tsx b/src/components/product/ComparisonTable.tsx index e0f6c195..185ec842 100644 --- a/src/components/product/ComparisonTable.tsx +++ b/src/components/product/ComparisonTable.tsx @@ -1,7 +1,9 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' +import { DeprecatedBadge } from '@/components/controls/DeprecatedBadge' import { Link } from '@/i18n/navigation' +import { sortDeprecatedLast } from '@/lib/deprecated' export interface ComparisonColumn { key: string @@ -41,6 +43,7 @@ export default function ComparisonTable({ const [scrollLeft, setScrollLeft] = useState(0) const [calculatedOffset, setCalculatedOffset] = useState(0) const columnWidthsMeasured = useRef(false) + const sortedItems = useMemo(() => sortDeprecatedLast(items), [items]) useEffect(() => { // Calculate sticky offset based on header and breadcrumb heights @@ -231,7 +234,7 @@ export default function ComparisonTable({ - {items.map((item, index) => ( + {sortedItems.map((item, index) => ( - - {item[itemNameKey] as string} - +
+ + {item[itemNameKey] as string} + + {item.deprecated === true && } +
{columns.map(column => ( (items: readonly T[]): T[] { + return items + .map((item, index) => ({ item, index })) + .sort((a, b) => { + const deprecatedOrder = + Number(a.item.deprecated === true) - Number(b.item.deprecated === true) + + return deprecatedOrder || a.index - b.index + }) + .map(({ item }) => item) +} diff --git a/tests/deprecated-order.test.ts b/tests/deprecated-order.test.ts new file mode 100644 index 00000000..a3f5a5a7 --- /dev/null +++ b/tests/deprecated-order.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from 'vitest' +import { sortDeprecatedLast } from '@/lib/deprecated' + +describe('deprecated product ordering', () => { + it('pins deprecated products after active products', () => { + const items = [ + { id: 'deprecated-a', deprecated: true }, + { id: 'active-a' }, + { id: 'deprecated-b', deprecated: true }, + { id: 'active-b', deprecated: false }, + ] + + expect(sortDeprecatedLast(items).map(item => item.id)).toEqual([ + 'active-a', + 'active-b', + 'deprecated-a', + 'deprecated-b', + ]) + }) + + it('preserves the existing order within each lifecycle group', () => { + const items = [ + { id: 'active-b' }, + { id: 'active-a' }, + { id: 'deprecated-b', deprecated: true }, + { id: 'deprecated-a', deprecated: true }, + ] + + expect(sortDeprecatedLast(items).map(item => item.id)).toEqual([ + 'active-b', + 'active-a', + 'deprecated-b', + 'deprecated-a', + ]) + }) + + it('does not mutate the input array', () => { + const items = [{ id: 'deprecated', deprecated: true }, { id: 'active' }] + + sortDeprecatedLast(items) + + expect(items.map(item => item.id)).toEqual(['deprecated', 'active']) + }) +}) From c2ffdbfb447cf76fa480c0f79ba1cfc90f6d897f Mon Sep 17 00:00:00 2001 From: Pan YANG Date: Fri, 31 Jul 2026 09:21:04 +0800 Subject: [PATCH 2/4] feat(seo): add localized comparison guides --- .../[locale]/clis/comparison/page.client.tsx | 38 ++- .../extensions/comparison/page.client.tsx | 35 ++- .../[locale]/ides/comparison/page.client.tsx | 35 ++- src/components/product/ComparisonGuide.tsx | 139 ++++++++++ translations/de/pages/comparison.json | 258 ++++++++++++++++- translations/en/pages/comparison.json | 260 +++++++++++++++++- translations/es/pages/comparison.json | 258 ++++++++++++++++- translations/fr/pages/comparison.json | 258 ++++++++++++++++- translations/id/pages/comparison.json | 258 ++++++++++++++++- translations/ja/pages/comparison.json | 258 ++++++++++++++++- translations/ko/pages/comparison.json | 258 ++++++++++++++++- translations/pt/pages/comparison.json | 258 ++++++++++++++++- translations/ru/pages/comparison.json | 258 ++++++++++++++++- translations/tr/pages/comparison.json | 258 ++++++++++++++++- translations/zh-Hans/pages/comparison.json | 260 +++++++++++++++++- translations/zh-Hant/pages/comparison.json | 258 ++++++++++++++++- 16 files changed, 3243 insertions(+), 104 deletions(-) create mode 100644 src/components/product/ComparisonGuide.tsx diff --git a/src/app/[locale]/clis/comparison/page.client.tsx b/src/app/[locale]/clis/comparison/page.client.tsx index 0a9525ea..f1da09d7 100644 --- a/src/app/[locale]/clis/comparison/page.client.tsx +++ b/src/app/[locale]/clis/comparison/page.client.tsx @@ -6,9 +6,13 @@ import { AppleIcon, LinuxIcon, WindowsIcon } from '@/components/controls/Platfor import Footer from '@/components/Footer' import Header from '@/components/Header' import { Breadcrumb } from '@/components/navigation/Breadcrumb' +import ComparisonGuide, { + type ComparisonGuideContent, + type ComparisonGuideItem, + type ComparisonProfileItem, +} from '@/components/product/ComparisonGuide' import ComparisonTable, { type ComparisonColumn } from '@/components/product/ComparisonTable' import { PricingSummaryValue } from '@/components/product/ProductPricing' -import { Link } from '@/i18n/navigation' import { withVendorCommunityUrlsForCatalog } from '@/lib/community-urls' import { clisData, vendorsData } from '@/lib/generated' import { getGithubStars } from '@/lib/generated/github-stars' @@ -28,6 +32,19 @@ type Props = { export default function CLIComparisonPageClient({ locale: _locale }: Props) { const tPage = useTranslations('pages.comparison') const tShared = useTranslations('shared') + const guideContent: ComparisonGuideContent = { + title: tPage('clis.guide.title'), + intro: tPage('clis.guide.intro', { count: clis.length }), + criteria: tPage.raw('clis.guide.criteria') as ComparisonGuideItem[], + shortlistTitle: tPage('clis.guide.shortlistTitle'), + shortlistIntro: tPage('clis.guide.shortlistIntro'), + steps: tPage.raw('clis.guide.steps') as ComparisonGuideItem[], + profilesTitle: tPage('clis.guide.profilesTitle'), + profilesIntro: tPage('clis.guide.profilesIntro'), + profiles: tPage.raw('clis.guide.profiles') as ComparisonProfileItem[], + faqTitle: tPage('clis.guide.faqTitle'), + faqs: tPage.raw('clis.guide.faqs') as ComparisonGuideItem[], + } const columns: ComparisonColumn[] = [ { @@ -279,7 +296,7 @@ export default function CLIComparisonPageClient({ locale: _locale }: Props) { /> {/* Page Header */} -
+

{tPage('clis.title')} @@ -291,7 +308,10 @@ export default function CLIComparisonPageClient({ locale: _locale }: Props) {

{/* Comparison Table */} -
+
[]} @@ -302,17 +322,7 @@ export default function CLIComparisonPageClient({ locale: _locale }: Props) {
- {/* Back Navigation */} -
-
- - ← {tPage('clis.backTo')} - -
-
+