From e3b72eb0542773c7bbb07f3678493a9e319e170c Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 2 Jul 2026 19:51:48 -0700 Subject: [PATCH 1/6] feat(comparison): add Sim vs Competitor comparison pages - New /comparison hub + /comparison/[provider] detail pages for Sim vs 16 competitors (n8n, Zapier, Make, Gumloop, Workato, Retool, Pipedream, OpenAI AgentKit, Tines, StackAI, Power Automate, Vellum, Claude Cowork, Langflow, Flowise), each with ~60 sourced, dated facts across platform, AI capabilities, integrations, pricing, security, observability, and support - Data layer at lib/compare/data (types, per-competitor profiles) is UI-free and independently auditable - BreadcrumbList, ItemList, and FAQPage JSON-LD per page; sitemap picks up all pages automatically via ALL_COMPETITORS - Two new fact categories (parallel execution, Agent2Agent protocol) researched and sourced against every competitor's own docs --- .../(landing)/comparison/[provider]/page.tsx | 330 +++++ .../comparison/comparison-sections.ts | 154 +++ .../brand-icon-tile/brand-icon-tile.tsx | 83 ++ .../components/brand-icon-tile/index.ts | 2 + .../comparison-cards/comparison-cards.tsx | 47 + .../components/comparison-cards/index.ts | 2 + .../comparison-table/comparison-table.tsx | 151 ++ .../components/comparison-table/index.ts | 2 + .../components/fact-value/fact-value.tsx | 67 + .../comparison/components/fact-value/index.ts | 2 + .../components/key-differences/index.ts | 2 + .../key-differences/key-differences.tsx | 66 + .../components/source-info/index.ts | 2 + .../components/source-info/source-info.tsx | 40 + .../app/(landing)/comparison/fact-status.ts | 30 + .../app/(landing)/comparison/not-found.tsx | 26 + apps/sim/app/(landing)/comparison/page.tsx | 194 +++ apps/sim/app/(landing)/comparison/utils.ts | 208 +++ .../(landing)/components/footer/footer.tsx | 1 + apps/sim/app/sitemap.ts | 15 + apps/sim/components/icons.tsx | 145 ++ .../compare/data/competitors/claude-cowork.ts | 1064 ++++++++++++++ .../lib/compare/data/competitors/flowise.ts | 842 +++++++++++ .../lib/compare/data/competitors/gumloop.ts | 1093 +++++++++++++++ .../lib/compare/data/competitors/langflow.ts | 883 ++++++++++++ apps/sim/lib/compare/data/competitors/make.ts | 1232 +++++++++++++++++ apps/sim/lib/compare/data/competitors/n8n.ts | 1218 ++++++++++++++++ .../data/competitors/openai-agentkit.ts | 1163 ++++++++++++++++ .../lib/compare/data/competitors/pipedream.ts | 1069 ++++++++++++++ .../data/competitors/power-automate.ts | 1184 ++++++++++++++++ .../lib/compare/data/competitors/retool.ts | 1079 +++++++++++++++ .../lib/compare/data/competitors/stackai.ts | 940 +++++++++++++ .../sim/lib/compare/data/competitors/tines.ts | 1096 +++++++++++++++ .../lib/compare/data/competitors/vellum.ts | 991 +++++++++++++ .../lib/compare/data/competitors/workato.ts | 1220 ++++++++++++++++ .../lib/compare/data/competitors/zapier.ts | 1159 ++++++++++++++++ apps/sim/lib/compare/data/feature-catalog.ts | 925 +++++++++++++ apps/sim/lib/compare/data/index.ts | 27 + apps/sim/lib/compare/data/sim.ts | 1070 ++++++++++++++ apps/sim/lib/compare/data/types.ts | 290 ++++ apps/sim/package.json | 1 + bun.lock | 11 + 42 files changed, 20126 insertions(+) create mode 100644 apps/sim/app/(landing)/comparison/[provider]/page.tsx create mode 100644 apps/sim/app/(landing)/comparison/comparison-sections.ts create mode 100644 apps/sim/app/(landing)/comparison/components/brand-icon-tile/brand-icon-tile.tsx create mode 100644 apps/sim/app/(landing)/comparison/components/brand-icon-tile/index.ts create mode 100644 apps/sim/app/(landing)/comparison/components/comparison-cards/comparison-cards.tsx create mode 100644 apps/sim/app/(landing)/comparison/components/comparison-cards/index.ts create mode 100644 apps/sim/app/(landing)/comparison/components/comparison-table/comparison-table.tsx create mode 100644 apps/sim/app/(landing)/comparison/components/comparison-table/index.ts create mode 100644 apps/sim/app/(landing)/comparison/components/fact-value/fact-value.tsx create mode 100644 apps/sim/app/(landing)/comparison/components/fact-value/index.ts create mode 100644 apps/sim/app/(landing)/comparison/components/key-differences/index.ts create mode 100644 apps/sim/app/(landing)/comparison/components/key-differences/key-differences.tsx create mode 100644 apps/sim/app/(landing)/comparison/components/source-info/index.ts create mode 100644 apps/sim/app/(landing)/comparison/components/source-info/source-info.tsx create mode 100644 apps/sim/app/(landing)/comparison/fact-status.ts create mode 100644 apps/sim/app/(landing)/comparison/not-found.tsx create mode 100644 apps/sim/app/(landing)/comparison/page.tsx create mode 100644 apps/sim/app/(landing)/comparison/utils.ts create mode 100644 apps/sim/lib/compare/data/competitors/claude-cowork.ts create mode 100644 apps/sim/lib/compare/data/competitors/flowise.ts create mode 100644 apps/sim/lib/compare/data/competitors/gumloop.ts create mode 100644 apps/sim/lib/compare/data/competitors/langflow.ts create mode 100644 apps/sim/lib/compare/data/competitors/make.ts create mode 100644 apps/sim/lib/compare/data/competitors/n8n.ts create mode 100644 apps/sim/lib/compare/data/competitors/openai-agentkit.ts create mode 100644 apps/sim/lib/compare/data/competitors/pipedream.ts create mode 100644 apps/sim/lib/compare/data/competitors/power-automate.ts create mode 100644 apps/sim/lib/compare/data/competitors/retool.ts create mode 100644 apps/sim/lib/compare/data/competitors/stackai.ts create mode 100644 apps/sim/lib/compare/data/competitors/tines.ts create mode 100644 apps/sim/lib/compare/data/competitors/vellum.ts create mode 100644 apps/sim/lib/compare/data/competitors/workato.ts create mode 100644 apps/sim/lib/compare/data/competitors/zapier.ts create mode 100644 apps/sim/lib/compare/data/feature-catalog.ts create mode 100644 apps/sim/lib/compare/data/index.ts create mode 100644 apps/sim/lib/compare/data/sim.ts create mode 100644 apps/sim/lib/compare/data/types.ts diff --git a/apps/sim/app/(landing)/comparison/[provider]/page.tsx b/apps/sim/app/(landing)/comparison/[provider]/page.tsx new file mode 100644 index 00000000000..40b541349dc --- /dev/null +++ b/apps/sim/app/(landing)/comparison/[provider]/page.tsx @@ -0,0 +1,330 @@ +import type { Metadata } from 'next' +import { notFound } from 'next/navigation' +import type { CompetitorProfile } from '@/lib/compare/data' +import { simProfile } from '@/lib/compare/data' +import { SITE_URL } from '@/lib/core/utils/urls' +import { buildLandingMetadata } from '@/lib/landing/seo' +import { COMPARISON_SECTIONS, getFactGroup } from '@/app/(landing)/comparison/comparison-sections' +import { BrandIconTile, SimIconTile } from '@/app/(landing)/comparison/components/brand-icon-tile' +import { ComparisonCards } from '@/app/(landing)/comparison/components/comparison-cards' +import { ComparisonTable } from '@/app/(landing)/comparison/components/comparison-table' +import { KeyDifferences } from '@/app/(landing)/comparison/components/key-differences' +import { + ALL_COMPETITORS, + buildBottomLine, + buildComparisonFaqs, + getCompetitorBySlug, + getLatestVerifiedDate, + SIM_LATEST_VERIFIED, +} from '@/app/(landing)/comparison/utils' +import { BackLink } from '@/app/(landing)/components' +import { Cta } from '@/app/(landing)/components/cta/cta' +import { JsonLd } from '@/app/(landing)/components/json-ld' +import { LandingFAQ } from '@/app/(landing)/components/landing-faq' + +const baseUrl = SITE_URL + +export const revalidate = 3600 +export const dynamicParams = false + +export async function generateStaticParams() { + return ALL_COMPETITORS.map((competitor) => ({ provider: competitor.id })) +} + +/** Flattens a profile's facts into JSON-LD `additionalProperty` entries, in {@link COMPARISON_SECTIONS} order. */ +function factsToProperties(profile: CompetitorProfile) { + return COMPARISON_SECTIONS.flatMap((section) => { + const group = getFactGroup(profile, section.group) + return section.rows.map((row) => ({ + '@type': 'PropertyValue', + name: row.label, + value: group[row.key]?.value ?? 'Unknown', + })) + }) +} + +export async function generateMetadata({ + params, +}: { + params: Promise<{ provider: string }> +}): Promise { + const { provider: providerSlug } = await params + const competitor = getCompetitorBySlug(providerSlug) + + if (!competitor) { + return {} + } + + return buildLandingMetadata({ + title: `Sim vs ${competitor.name}: AI Workspace Comparison`, + description: `Compare Sim, the open-source AI workspace, to ${competitor.name} on platform, AI, integrations, pricing, security, and support. Sourced and dated facts.`, + path: `/comparison/${competitor.id}`, + keywords: [ + `Sim vs ${competitor.name}`, + `${competitor.name} alternative`, + `${competitor.name} vs Sim`, + `open source ${competitor.name} alternative`, + `${competitor.name} comparison`, + 'AI agent workspace', + 'AI workflow automation comparison', + ].join(', '), + }) +} + +export default async function ComparisonProviderPage({ + params, +}: { + params: Promise<{ provider: string }> +}) { + const { provider: providerSlug } = await params + const competitor = getCompetitorBySlug(providerSlug) + + if (!competitor) { + notFound() + } + + const faqs = buildComparisonFaqs(competitor) + const verdict = buildBottomLine(competitor) + const CompetitorIcon = competitor.brand?.icon + + const breadcrumbJsonLd = { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: [ + { '@type': 'ListItem', position: 1, name: 'Home', item: baseUrl }, + { '@type': 'ListItem', position: 2, name: 'Comparison', item: `${baseUrl}/comparison` }, + { + '@type': 'ListItem', + position: 3, + name: `Sim vs ${competitor.name}`, + item: `${baseUrl}/comparison/${competitor.id}`, + }, + ], + } + + const latestVerified = new Date( + Math.max(SIM_LATEST_VERIFIED.getTime(), getLatestVerifiedDate(competitor).getTime()) + ) + + const productComparisonJsonLd = { + '@context': 'https://schema.org', + '@type': 'ItemList', + name: `Sim vs ${competitor.name}`, + description: `Feature and pricing comparison between Sim and ${competitor.name}.`, + url: `${baseUrl}/comparison/${competitor.id}`, + dateModified: latestVerified.toISOString().slice(0, 10), + numberOfItems: 2, + itemListElement: [ + { + '@type': 'ListItem', + position: 1, + item: { + '@type': 'SoftwareApplication', + name: 'Sim', + applicationCategory: 'BusinessApplication', + url: SITE_URL, + description: simProfile.oneLiner, + additionalProperty: factsToProperties(simProfile), + }, + }, + { + '@type': 'ListItem', + position: 2, + item: { + '@type': 'SoftwareApplication', + name: competitor.name, + applicationCategory: 'BusinessApplication', + url: competitor.website, + description: competitor.oneLiner, + additionalProperty: factsToProperties(competitor), + }, + }, + ], + } + + const faqJsonLd = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: faqs.map((faq) => ({ + '@type': 'Question', + name: faq.question, + acceptedAnswer: { + '@type': 'Answer', + text: faq.answer, + }, + })), + } + + return ( + <> + + + + +
+
+
+ +
+ +
+

+ Sim vs {competitor.name} +

+

+ Sim is the open-source AI workspace where teams build, deploy, and manage AI agents + visually, conversationally, or with code. Here is how Sim compares to{' '} + {competitor.name} on platform architecture, AI capabilities, integrations, pricing, + security, and support. Every fact below is sourced and dated. +

+

+ Sim is an open-source AI workspace for building, deploying, and managing AI agents. + This page compares Sim to {competitor.name} across platform architecture, AI + capabilities, integrations, pricing, security and compliance, observability, and + support, using sourced, dated facts for buyers evaluating both platforms. +

+
+
+ +
+ +
+
+
+
+

+ + What is Sim? +

+

+ {simProfile.oneLiner} +

+
+
+

+ {CompetitorIcon ? ( + + ) : null} + What is {competitor.name}? +

+

+ {competitor.oneLiner} +

+
+
+ +
+ +
+

+ Key differences at a glance +

+ +
+ +
+ +
+

+ Sim vs {competitor.name}: feature-by-feature comparison +

+ +
+ +
+ +
+
+
+

+ Sim standout features +

+
+ +
+
+
+

+ Documented {competitor.name} limitations +

+
+ +
+
+ +
+ +
+

+ Bottom line +

+
+

+ {verdict.chooseSim} +

+

+ {verdict.chooseCompetitor} +

+
+
+ +
+ +
+

+ Frequently asked questions +

+
+ +
+
+
+
+ +
+
+ +
+ +
+ + ) +} diff --git a/apps/sim/app/(landing)/comparison/comparison-sections.ts b/apps/sim/app/(landing)/comparison/comparison-sections.ts new file mode 100644 index 00000000000..609955be3db --- /dev/null +++ b/apps/sim/app/(landing)/comparison/comparison-sections.ts @@ -0,0 +1,154 @@ +import type { ComparisonFacts, CompetitorProfile, Fact } from '@/lib/compare/data' + +/** + * The one place a {@link ComparisonFacts} group is read back out of a profile + * by group key. Every render-side consumer (the table, key-differences strip, + * JSON-LD builder) needs this same lookup; centralizing it here means there is + * exactly one cast to reason about instead of one per call site. + */ +export function getFactGroup( + profile: CompetitorProfile, + group: G +): Record { + return profile.facts[group] as Record +} + +/** + * One row in a comparison table section. Maps a human label to a fact key + * within a {@link ComparisonFacts} group. `key` is intentionally `string` + * (rather than a per-group `keyof` union) so a single array can hold rows + * for every group without TypeScript collapsing the distributed generic to + * `never`; correctness is enforced once, by construction, in + * {@link COMPARISON_SECTIONS} below, and the renderer reads through it. + */ +export interface ComparisonRowDef { + key: string + label: string +} + +/** One section of the comparison table, mirroring a {@link ComparisonFacts} group. */ +export interface ComparisonSectionDef { + group: keyof ComparisonFacts + title: string + rows: ComparisonRowDef[] +} + +/** + * Type-checks a section's rows against its own group's actual fact keys + * (via the per-call generic `G`), then widens to the plain `ComparisonRowDef` + * shape used by {@link COMPARISON_SECTIONS}. This is where row-key + * correctness is actually enforced. A typo here fails the build. + */ +function defineSection(section: { + group: G + title: string + rows: Array<{ key: keyof ComparisonFacts[G]; label: string }> +}): ComparisonSectionDef { + return section as ComparisonSectionDef +} + +/** + * Canonical section/row order for rendering a {@link ComparisonFacts} profile + * pair as a table. Single source of truth for row labels. Add a field here + * once and every comparison page picks it up. + */ +export const COMPARISON_SECTIONS: ComparisonSectionDef[] = [ + defineSection({ + group: 'platform', + title: 'Platform', + rows: [ + { key: 'builderType', label: 'Builder type' }, + { key: 'learningCurve', label: 'Learning curve' }, + { key: 'selfHostOption', label: 'Self-hosting' }, + { key: 'deploymentOptions', label: 'Deployment options' }, + { key: 'templates', label: 'Templates' }, + { key: 'license', label: 'License' }, + { key: 'environmentPromotion', label: 'Environment promotion' }, + { key: 'versionControlDepth', label: 'Version control' }, + { key: 'realtimeCollaboration', label: 'Realtime collaboration' }, + { key: 'nativeFileStorage', label: 'Native file storage' }, + ], + }), + defineSection({ + group: 'pricing', + title: 'Pricing', + rows: [ + { key: 'pricingModel', label: 'Pricing model' }, + { key: 'entryPaidPlan', label: 'Entry paid plan' }, + { key: 'freeTier', label: 'Free tier' }, + { key: 'byok', label: 'Bring your own key' }, + ], + }), + defineSection({ + group: 'security', + title: 'Security & compliance', + rows: [ + { key: 'soc2', label: 'SOC 2' }, + { key: 'dataResidency', label: 'Data residency' }, + { key: 'rbac', label: 'Role-based access control' }, + { key: 'auditLogging', label: 'Audit logging' }, + { key: 'additionalCompliance', label: 'Additional compliance' }, + { key: 'modelAndToolGovernance', label: 'Model & tool governance' }, + { key: 'credentialGovernance', label: 'Credential governance' }, + { key: 'sso', label: 'Single sign-on (SSO)' }, + { key: 'piiRedaction', label: 'PII redaction' }, + { key: 'dataRetention', label: 'Custom data retention' }, + { key: 'whiteLabeling', label: 'White-labeling' }, + ], + }), + defineSection({ + group: 'aiCapabilities', + title: 'AI capabilities', + rows: [ + { key: 'multiLlmSupport', label: 'Multi-LLM support' }, + { key: 'agentReasoningBlocks', label: 'Agent reasoning blocks' }, + { key: 'naturalLanguageBuilding', label: 'Natural-language building' }, + { key: 'knowledgeBaseRag', label: 'Knowledge base / RAG' }, + { key: 'mcpSupport', label: 'MCP support' }, + { key: 'evaluationGuardrails', label: 'Evaluation & guardrails' }, + { key: 'humanInTheLoop', label: 'Human-in-the-loop' }, + { key: 'generativeMedia', label: 'Generative media' }, + { key: 'dynamicToolUse', label: 'Dynamic tool use' }, + { key: 'modelFallback', label: 'Automatic model fallback' }, + { key: 'agentSkills', label: 'Agent skills' }, + { key: 'nativeChatDeployment', label: 'Native chat deployment' }, + { key: 'parallelExecution', label: 'Parallel execution' }, + { key: 'a2aProtocol', label: 'Agent2Agent (A2A) protocol' }, + ], + }), + defineSection({ + group: 'integrations', + title: 'Integrations', + rows: [ + { key: 'integrationCount', label: 'Integrations' }, + { key: 'triggerTypes', label: 'Trigger types' }, + { key: 'customCodeSteps', label: 'Custom code steps' }, + { key: 'apiPublishing', label: 'API publishing' }, + { key: 'extensibilitySdk', label: 'SDKs & extensibility' }, + { key: 'mcpPublishing', label: 'Publish as MCP server' }, + ], + }), + defineSection({ + group: 'observability', + title: 'Observability & durability', + rows: [ + { key: 'tracingDepth', label: 'Tracing & observability' }, + { key: 'durabilityModel', label: 'Durability & retries' }, + { key: 'failureAlerting', label: 'Failure alerting' }, + { key: 'dataDrains', label: 'Data drains' }, + { key: 'asyncExecution', label: 'Async execution' }, + { key: 'executionLimits', label: 'Execution limits' }, + { key: 'partialFailureHandling', label: 'Partial-failure handling' }, + ], + }), + defineSection({ + group: 'support', + title: 'Support', + rows: [ + { key: 'supportChannels', label: 'Support channels' }, + { key: 'sla', label: 'SLA' }, + { key: 'community', label: 'Community' }, + { key: 'academy', label: 'Academy / training' }, + ], + }), +] diff --git a/apps/sim/app/(landing)/comparison/components/brand-icon-tile/brand-icon-tile.tsx b/apps/sim/app/(landing)/comparison/components/brand-icon-tile/brand-icon-tile.tsx new file mode 100644 index 00000000000..6c8911a805d --- /dev/null +++ b/apps/sim/app/(landing)/comparison/components/brand-icon-tile/brand-icon-tile.tsx @@ -0,0 +1,83 @@ +import type { ComponentType, SVGProps } from 'react' +import { cn } from '@sim/emcn' +import type { CompetitorBrand } from '@/lib/compare/data' +import { SimWordmark } from '@/app/(landing)/components/navbar/components/sim-wordmark' + +export interface BrandIconTileProps { + icon: ComponentType> + /** + * Whether `icon` already renders a full, self-contained brand-colored + * square (e.g. a fetched app-store-style logo) rather than a bare + * transparent glyph. See {@link CompetitorBrand.selfFramed}. + */ + selfFramed?: boolean + /** Outer tile size, e.g. `size-8`. Defaults to the integrations-page card size. */ + className?: string + /** Icon glyph size inside the tile, e.g. `size-4`. Ignored when `selfFramed`. */ + iconClassName?: string +} + +/** + * A rounded, bordered icon tile matching the platform's app-icon chrome + * conventions (border radius, border token, background), so competitor brand + * logos read as the same first-class "app icon" chrome as the rest of the + * product, instead of a bare, unframed SVG floating in the layout. + * + * A self-framed logo (already a complete brand-colored square) fills the + * tile edge-to-edge, clipped to the same rounded corners. Otherwise the icon + * is a small transparent glyph centered on a plain bordered background. + */ +export function BrandIconTile({ + icon: Icon, + selfFramed = false, + className = 'size-8', + iconClassName = 'size-4', +}: BrandIconTileProps) { + if (selfFramed) { + return ( +
+
+ ) + } + return ( +
+
+ ) +} + +export interface SimIconTileProps { + /** Outer tile size, e.g. `size-8`. Defaults to the integrations-page card size. */ + className?: string +} + +/** + * The same rounded, bordered tile as {@link BrandIconTile}, but for Sim's own + * wordmark. So "Sim" gets the identical icon-chip treatment as every + * competitor it's compared against, instead of appearing as bare text. + */ +export function SimIconTile({ className = 'size-8' }: SimIconTileProps) { + return ( +
+ + + +
+ ) +} diff --git a/apps/sim/app/(landing)/comparison/components/brand-icon-tile/index.ts b/apps/sim/app/(landing)/comparison/components/brand-icon-tile/index.ts new file mode 100644 index 00000000000..fe31f6d7a24 --- /dev/null +++ b/apps/sim/app/(landing)/comparison/components/brand-icon-tile/index.ts @@ -0,0 +1,2 @@ +export type { BrandIconTileProps, SimIconTileProps } from './brand-icon-tile' +export { BrandIconTile, SimIconTile } from './brand-icon-tile' diff --git a/apps/sim/app/(landing)/comparison/components/comparison-cards/comparison-cards.tsx b/apps/sim/app/(landing)/comparison/components/comparison-cards/comparison-cards.tsx new file mode 100644 index 00000000000..8a3d62e4745 --- /dev/null +++ b/apps/sim/app/(landing)/comparison/components/comparison-cards/comparison-cards.tsx @@ -0,0 +1,47 @@ +import type { FactSource } from '@/lib/compare/data' +import { SourceLink } from '@/app/(landing)/comparison/components/source-info' + +interface ComparisonCardItem { + title: string + description: string + shortDescription?: string + source: FactSource +} + +export interface ComparisonCardsProps { + items: ComparisonCardItem[] +} + +/** + * A vertically stacked list of atomic, independently quotable fact cards, + * each self-contained title + a one-line `shortDescription` (falling back to + * `description` if a short version hasn't been authored yet). Used for both + * a competitor's standout features and its documented limitations. + * + * The full `description` is always present as `sr-only` text. Server + * rendered regardless of hover/JS state. So an LLM or crawler reading the + * page still gets the complete claim even though a human sees only the + * one-line summary. Hovering the title itself (`SourceLink`) shows a short + * "Source: X" tooltip and clicking it opens the source, rather than a + * separate info-icon affordance next to every card. + */ +export function ComparisonCards({ items }: ComparisonCardsProps) { + return ( +
+ {items.map((item, index) => ( +
0 ? 'border-[var(--border)] border-t px-6 py-4' : 'px-6 py-4'} + > +

+ {item.title} +

+

+ {item.shortDescription ?? item.description} +

+ {item.shortDescription ? {item.description} : null} +
+ ))} +
+ ) +} diff --git a/apps/sim/app/(landing)/comparison/components/comparison-cards/index.ts b/apps/sim/app/(landing)/comparison/components/comparison-cards/index.ts new file mode 100644 index 00000000000..e5fa934bb4f --- /dev/null +++ b/apps/sim/app/(landing)/comparison/components/comparison-cards/index.ts @@ -0,0 +1,2 @@ +export type { ComparisonCardsProps } from './comparison-cards' +export { ComparisonCards } from './comparison-cards' diff --git a/apps/sim/app/(landing)/comparison/components/comparison-table/comparison-table.tsx b/apps/sim/app/(landing)/comparison/components/comparison-table/comparison-table.tsx new file mode 100644 index 00000000000..faaa75caa3a --- /dev/null +++ b/apps/sim/app/(landing)/comparison/components/comparison-table/comparison-table.tsx @@ -0,0 +1,151 @@ +import type { ReactNode } from 'react' +import { cn } from '@sim/emcn' +import type { CompetitorProfile } from '@/lib/compare/data' +import { COMPARISON_SECTIONS, getFactGroup } from '@/app/(landing)/comparison/comparison-sections' +import { BrandIconTile, SimIconTile } from '@/app/(landing)/comparison/components/brand-icon-tile' +import { FactValue } from '@/app/(landing)/comparison/components/fact-value' + +export interface ComparisonTableProps { + sim: CompetitorProfile + competitor: CompetitorProfile +} + +function ColumnHeader({ + name, + iconTile, + isSim, +}: { + name: string + iconTile: ReactNode + isSim: boolean +}) { + return ( +
+ {iconTile} + {name} +
+ ) +} + +/** + * Two-column "Sim vs {Competitor}" fact table, styled after the billing + * upgrade-page comparison table (same border/hairline rhythm and section + * headers) but data-driven off {@link CompetitorProfile.facts} instead of the + * fixed 4-tier plan schema. Data cells share one neutral surface for both + * columns. The Sim column is called out only in the header row (a bottom + * accent border), so the table reads as one clean grid rather than a + * checkerboard. Pure server component: every value is plain server-rendered + * text so crawlers and AI answer engines read the full comparison without + * any client-side hydration. + */ +export function ComparisonTable({ sim, competitor }: ComparisonTableProps) { + return ( +
+
+
+
+ Compare + + {sim.name} vs {competitor.name} + +
+ } isSim /> + + ) : null + } + isSim={false} + /> +
+ + {COMPARISON_SECTIONS.map((section, sectionIdx) => { + const simGroupFacts = getFactGroup(sim, section.group) + const competitorGroupFacts = getFactGroup(competitor, section.group) + + return ( +
+
+
0 && 'border-[var(--border-1)] border-t' + )} + > + + {section.title} + +
+
0 && 'border-[var(--border-1)] border-t' + )} + /> +
+ + {section.rows.map((row, rowIdx) => { + const simFact = simGroupFacts[row.key] + const competitorFact = competitorGroupFacts[row.key] + const isLastRow = rowIdx < section.rows.length - 1 + + return ( +
+
+ {row.label} +
+
+ +
+
+ +
+
+ ) + })} +
+ ) + })} +
+
+ ) +} diff --git a/apps/sim/app/(landing)/comparison/components/comparison-table/index.ts b/apps/sim/app/(landing)/comparison/components/comparison-table/index.ts new file mode 100644 index 00000000000..18ea827718e --- /dev/null +++ b/apps/sim/app/(landing)/comparison/components/comparison-table/index.ts @@ -0,0 +1,2 @@ +export type { ComparisonTableProps } from './comparison-table' +export { ComparisonTable } from './comparison-table' diff --git a/apps/sim/app/(landing)/comparison/components/fact-value/fact-value.tsx b/apps/sim/app/(landing)/comparison/components/fact-value/fact-value.tsx new file mode 100644 index 00000000000..2e95baf01dd --- /dev/null +++ b/apps/sim/app/(landing)/comparison/components/fact-value/fact-value.tsx @@ -0,0 +1,67 @@ +import { Check, X } from '@sim/emcn/icons' +import type { Fact } from '@/lib/compare/data' +import { SourceLink } from '@/app/(landing)/comparison/components/source-info' +import { parseFactValue } from '@/app/(landing)/comparison/fact-status' + +export interface FactValueProps { + fact: Fact +} + +/** + * Renders one {@link Fact} for a glancing reader while keeping the full + * granular fact server-rendered for crawlers and AI answer engines. + * + * - A true "Yes"/"No" fact renders as an icon alone (a monochrome check or + * muted cross, no colored pass/fail styling), no visible text, since the + * label column and surrounding context already say what's being asked. + * - Any other fact shows its `shortValue` (a compact, pre-authored + * restatement of `value`), never the full sentence. + * - `Tooltip` here is a cursor-following mini-bubble meant for a short + * one-line label (see its own docs/usages: "Refresh", "last updated: X") + * . It is deliberately NOT used to hold paragraph-length detail text, only + * the compact source citation, which is exactly what it's designed for. + * - When a source exists, the visible glance (icon or `shortValue` text) + * IS the hover/click target for that source, via `SourceLink`, rather + * than a separate info-icon next to every value. One affordance per + * fact keeps a 58-row table from reading as icon-cluttered. + * - A `sr-only` span always carries the complete value, detail, and source + * in the initial server-rendered HTML, independent of hover/JS state, so + * an LLM or crawler reading the page gets full granularity even though a + * human sees only the compact glance. + */ +export function FactValue({ fact }: FactValueProps) { + const { status, text } = parseFactValue(fact.value) + const isBoolean = status === 'yes' || status === 'no' + const primarySource = fact.sources[0] + + const fullText = [fact.value, fact.detail].filter(Boolean).join('. ') + + const glance = isBoolean ? ( + status === 'yes' ? ( +