diff --git a/src/components/landing/chrome/LakesideNavbar.tsx b/src/components/landing/chrome/LakesideNavbar.tsx index 030fe640..c118c18d 100644 --- a/src/components/landing/chrome/LakesideNavbar.tsx +++ b/src/components/landing/chrome/LakesideNavbar.tsx @@ -16,7 +16,7 @@ import useGetReleases from '@site/src/hooks/useGetReleases' import Button from '../ui/Button' import { LAKESIDE_NAV, - LAKESIDE_CTA, + lakesideCta, LAKESIDE_GITHUB, LAKESIDE_SLACK, type LakesideNavEntry, @@ -399,6 +399,7 @@ export default function LakesideNavbar({ const { pathname } = useLocation() const isBar = variant === 'bar' const currentPath = activePath ?? pathname + const cta = lakesideCta(currentPath) const drawerOpen = mobileSidebar ? mobileSidebar.shown : ownDrawerOpen const setDrawerOpen = (next: boolean | ((v: boolean) => boolean)) => { @@ -478,8 +479,8 @@ export default function LakesideNavbar({ {isBar && trailing} - diff --git a/src/components/landing/chrome/navItems.ts b/src/components/landing/chrome/navItems.ts index 5fa9d70d..f9a27a02 100644 --- a/src/components/landing/chrome/navItems.ts +++ b/src/components/landing/chrome/navItems.ts @@ -5,6 +5,8 @@ export const GITHUB_REPO_URL = 'https://github.com/datazip-inc/olake' export const FUSION_GITHUB_REPO_URL = 'https://github.com/datazip-inc/olake-fusion' export const SLACK_URL = '/slack/' +const GO_QUICKSTART = '/docs/getting-started/quickstart/' +const FUSION_QUICKSTART = '/docs/fusion/getting-started/quickstart/' export const PRICING_LINK = { label: 'Pricing', href: '/contact/' } export interface LakesideNavLink { @@ -44,7 +46,7 @@ export interface LakesideNavEntry { const PRODUCTS_MEGA: MegaMenu = { feature: { title: 'Replicate, then maintain', - cta: { label: 'Try OLake Go', href: '/docs/getting-started/quickstart/' } + cta: { label: 'Try OLake Go', href: GO_QUICKSTART } }, columns: [ { @@ -54,7 +56,7 @@ const PRODUCTS_MEGA: MegaMenu = { href: '/olake-go', links: [ { label: 'Documentation', href: '/docs' }, - { label: 'Quickstart', href: '/docs/getting-started/quickstart/' }, + { label: 'Quickstart', href: GO_QUICKSTART }, { label: 'GitHub', href: GITHUB_REPO_URL, external: true } ] }, @@ -65,7 +67,7 @@ const PRODUCTS_MEGA: MegaMenu = { href: '/olake-fusion', links: [ { label: 'Documentation', href: '/docs/fusion/getting-started/overview' }, - { label: 'Quickstart', href: '/docs/fusion/getting-started/quickstart/' }, + { label: 'Quickstart', href: FUSION_QUICKSTART }, { label: 'GitHub', href: FUSION_GITHUB_REPO_URL, external: true } ] } @@ -164,10 +166,12 @@ export const LAKESIDE_NAV: LakesideNavEntry[] = [ { label: PRICING_LINK.label, href: PRICING_LINK.href } ] -export const LAKESIDE_CTA: LakesideNavLink = { - label: "Try, It's Free", - href: '/docs/getting-started/quickstart/' -} +const LAKESIDE_CTA: LakesideNavLink = { label: "Try, It's Free", href: GO_QUICKSTART } +const FUSION_CTA: LakesideNavLink = { ...LAKESIDE_CTA, href: FUSION_QUICKSTART } + +/** The navbar's call to action: OLake Fusion's quickstart on Fusion pages, OLake Go's elsewhere. */ +export const lakesideCta = (pathname: string): LakesideNavLink => + /^\/(olake-fusion|docs\/fusion)(\/|$)/.test(pathname) ? FUSION_CTA : LAKESIDE_CTA export const LAKESIDE_GITHUB: LakesideNavLink = { label: 'GitHub', diff --git a/src/components/landing/fusion/Benchmark.tsx b/src/components/landing/fusion/Benchmark.tsx deleted file mode 100644 index 60c3545d..00000000 --- a/src/components/landing/fusion/Benchmark.tsx +++ /dev/null @@ -1,165 +0,0 @@ -import React, { useState } from 'react' -import Link from '@docusaurus/Link' -import { PiArrowRight, PiCaretDown } from 'react-icons/pi' -import Section from '../ui/Section' -import SectionHeading from '../ui/SectionHeading' -import { BENCHMARK, BENCHMARK_INFO } from '@site/src/data/landing/fusion/benchmark' -import { cn } from '@site/src/lib/utils' - -const FOCUS = - 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-olake-blue' -const RULE = 'border-0 border-r border-solid border-olake-line-rule bg-transparent' - -/** The "How Fusion does this" disclosure under the table. */ -function HowFusionDoesThis() { - const [open, setOpen] = useState(false) - return ( -
-

- -

- {open && ( - - )} -
- ) -} - -export default function Benchmark() { - const b = BENCHMARK - return ( -
- - {b.lead.before} - {b.lead.boldSpeed} - {b.lead.middle} - {b.lead.boldCost} - {b.lead.after} - - } - /> - -
- - - - - - - - - - {b.rows.map((row, i) => { - const last = i === b.rows.length - 1 - return ( - - - - - - ) - })} - -
- {b.headers.metric} - - {b.headers.spark} - - {b.headers.fusionBrand} - {b.headers.fusionProduct} -
- {row.metric} - - {row.spark} - - - {row.fusion} - - {row.delta} - - -
-
- - - - - {b.link.label} -
- ) -} diff --git a/src/components/landing/fusion/Faq.tsx b/src/components/landing/fusion/Faq.tsx deleted file mode 100644 index 65182214..00000000 --- a/src/components/landing/fusion/Faq.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import React, { useState } from 'react' -import { PiCaretDown } from 'react-icons/pi' -import Section from '../ui/Section' -import SectionHeading from '../ui/SectionHeading' -import { FAQS, FAQ_TITLE } from '@site/src/data/landing/fusion/faqs' -import { cn } from '@site/src/lib/utils' - -const FOCUS = - 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-olake-blue' - -/** One-open-at-a-time accordion. Answers stay in the DOM (hidden) so they remain readable to crawlers. */ -export default function Faq() { - const [open, setOpen] = useState(-1) - return ( -
-
- -
    - {FAQS.map((faq, i) => { - const isOpen = open === i - return ( -
  • -

    - -

    - -
  • - ) - })} -
-
-
- ) -} diff --git a/src/components/landing/fusion/FeatureVisual.tsx b/src/components/landing/fusion/FeatureVisual.tsx deleted file mode 100644 index 7b0e8ed5..00000000 --- a/src/components/landing/fusion/FeatureVisual.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React from 'react' -import { - CONFIG_COMPARE, - DECAY_LABELS, - HOSTS, - TIERS, - type FeatureKind -} from '@site/src/data/landing/fusion/features' -import { cn } from '@site/src/lib/utils' - -/** Fragment sizes (px) for the "fragmented files" cluster. */ -const FRAGMENTS = [14, 10, 16, 9, 13, 11, 15, 8] -const TIER_FILL = ['bg-olake-blue/40', 'bg-olake-blue/70', 'bg-olake-blue'] -const CAPTION = 'text-[11px] text-olake-muted' - -const Tiered = () => ( - -) - -const Decay = () => ( -
-
-
- {FRAGMENTS.map((size, i) => ( - - ))} -
-

{DECAY_LABELS.before}

-
- -
-
- - -
-

{DECAY_LABELS.after}

-
-
-) - -const Config = () => ( -
-
-

{CONFIG_COMPARE.fusion.value}

-

{CONFIG_COMPARE.fusion.label}

-
-
-

{CONFIG_COMPARE.spark.value}

-

{CONFIG_COMPARE.spark.label}

-
-
-) - -const Hosted = () => ( - -) - -const VISUALS: Record = { - tiered: Tiered, - decay: Decay, - config: Config, - hosted: Hosted -} - -/** The small product mock-up above each feature's copy. Values are the page's sample content. */ -export default function FeatureVisual({ kind }: { kind: FeatureKind }) { - const Visual = VISUALS[kind] - return ( -
-
- -
-
- ) -} diff --git a/src/components/landing/fusion/Features.tsx b/src/components/landing/fusion/Features.tsx deleted file mode 100644 index a3471e5d..00000000 --- a/src/components/landing/fusion/Features.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react' -import Section from '../ui/Section' -import SectionHeading from '../ui/SectionHeading' -import Card from '../ui/Card' -import FeatureVisual from './FeatureVisual' -import { - FEATURES, - FEATURES_EYEBROW, - FEATURES_TITLE, - FEATURE_STEPS, - FEATURE_TICK_MS -} from '@site/src/data/landing/fusion/features' -import { cn } from '@site/src/lib/utils' - -const FOCUS = - 'focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-olake-blue' - -/** - * Feature selector with auto-advance: the active feature's underline fills over 10 s and then the - * next feature takes over. The rotation starts once the section is 40% visible and stops for good - * when a visitor picks a feature (or when they prefer reduced motion). - */ -function useFeatureRotation(count: number) { - const [active, setActive] = useState(0) - const [progress, setProgress] = useState(0) - const [paused, setPaused] = useState(false) - const [started, setStarted] = useState(false) - const sectionRef = useRef(null) - - useEffect(() => { - if (window.matchMedia?.('(prefers-reduced-motion: reduce)').matches) return - const el = sectionRef.current - if (!el || !('IntersectionObserver' in window)) { - setStarted(true) - return - } - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting) { - setStarted(true) - observer.disconnect() - } - }, - { threshold: 0.4 } - ) - observer.observe(el) - return () => observer.disconnect() - }, []) - - useEffect(() => { - if (!started || paused) return - const timer = setInterval(() => { - setProgress((p) => { - if (p + 1 >= FEATURE_STEPS) { - setActive((a) => (a + 1) % count) - return 0 - } - return p + 1 - }) - }, FEATURE_TICK_MS) - return () => clearInterval(timer) - }, [started, paused, count]) - - const select = useCallback((i: number) => { - setActive(i) - setProgress(FEATURE_STEPS) - setPaused(true) - }, []) - - return { active, progress, select, sectionRef } -} - -export default function Features() { - const { active, progress, select, sectionRef } = useFeatureRotation(FEATURES.length) - const tabRefs = useRef<(HTMLButtonElement | null)[]>([]) - - const onKeyDown = (e: React.KeyboardEvent) => { - const last = FEATURES.length - 1 - let next = -1 - if (e.key === 'ArrowDown' || e.key === 'ArrowRight') next = active === last ? 0 : active + 1 - else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') next = active === 0 ? last : active - 1 - else if (e.key === 'Home') next = 0 - else if (e.key === 'End') next = last - if (next < 0) return - e.preventDefault() - select(next) - tabRefs.current[next]?.focus() - } - - return ( -
-
- - -
-
- {FEATURES.map((feature, i) => { - const selected = i === active - return ( - - ) - })} -
- - {/* Every panel sits in the same grid cell so the card keeps the height of the tallest one. */} -
- {FEATURES.map((feature, i) => ( -
- - -
-

- {feature.title} -

-

- {feature.body} -

-
-
-
- ))} -
-
-
-
- ) -} diff --git a/src/components/landing/fusion/FusionBenchmark.tsx b/src/components/landing/fusion/FusionBenchmark.tsx new file mode 100644 index 00000000..4e747d35 --- /dev/null +++ b/src/components/landing/fusion/FusionBenchmark.tsx @@ -0,0 +1,133 @@ +import React from 'react' +import { cn } from '@site/src/lib/utils' +import BenchmarkShell, { RULE } from '../product/BenchmarkShell' +import { BENCHMARK, BENCHMARK_HOW } from '@site/src/data/landing/fusion/benchmark' + +/** Column widths of the design's table (254, 380 and 367 of its 1001px). */ +/** Column widths: the design's from sm up, a wider label column on phones so the table fits. */ +const COLUMNS = ['w-[34%] sm:w-[25.37%]', 'w-[36%] sm:w-[37.96%]', 'w-[30%] sm:w-[36.67%]'] + +/** "Benchmarks" of the OLake Fusion page: Fusion against Spark compaction on the same workload. */ +export default function FusionBenchmark() { + const { headers } = BENCHMARK + return ( + ( + + {part.text} + + ))} + how={{ + title: BENCHMARK_HOW.title, + className: 'lg:pt-[38px] lg:pb-[62px]', + body: ( +
+

{BENCHMARK_HOW.lead}

+
    + {BENCHMARK_HOW.tiers.map((tier) => ( +
  • + {tier.name} {tier.text} +
  • + ))} +
+
+ {BENCHMARK_HOW.outro.map((line) => ( +

{line}

+ ))} +
+
+ ) + }} + link={BENCHMARK.link} + > +
+

+ {BENCHMARK.caption} +

+
+ + + {COLUMNS.map((width) => ( + + ))} + + + + + + + + + + {BENCHMARK.rows.map((row, r) => { + const cell = 'px-[8px] pt-[22px] align-top text-[14px] leading-[1.3]' + return ( + + + + + + ) + })} + +
+ {headers.metric} + + + {headers.fusionBrand}{' '} + {headers.fusionProduct} + + + {headers.fusionSub} + + + {headers.spark} +
+ {row.metric} + + + {row.fusion} + + {row.delta} + + + + {row.spark} +
+
+
+
+ ) +} diff --git a/src/components/landing/fusion/FusionCta.tsx b/src/components/landing/fusion/FusionCta.tsx index 113a55e2..9872cd64 100644 --- a/src/components/landing/fusion/FusionCta.tsx +++ b/src/components/landing/fusion/FusionCta.tsx @@ -1,31 +1,27 @@ import React from 'react' -import Button from '../ui/Button' -import Section from '../ui/Section' +import ProductCta from '../product/ProductCta' import { FUSION_CTA } from '@site/src/data/landing/fusion/cta' -/** Closing call to action: headline and button beside the iceberg picture. */ +const IMG = '/img/landing/fusion' + +/** Closing call to action of the OLake Fusion page: the navy card with skewed data points under the buttons. */ export default function FusionCta() { return ( -
-
-
-

- {FUSION_CTA.title} -

- -
+ -
-
+ } + /> ) } diff --git a/src/components/landing/fusion/FusionFaq.tsx b/src/components/landing/fusion/FusionFaq.tsx new file mode 100644 index 00000000..2b73645c --- /dev/null +++ b/src/components/landing/fusion/FusionFaq.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import ProductFaq from '../product/ProductFaq' +import { FAQS, FAQ_EYEBROW, FAQ_TITLE } from '@site/src/data/landing/fusion/faqs' + +/** The OLake Fusion FAQ. The page's FAQPage JSON-LD (seo.ts) is built from the same list. */ +export default function FusionFaq() { + return +} diff --git a/src/components/landing/fusion/FusionFeatures.tsx b/src/components/landing/fusion/FusionFeatures.tsx new file mode 100644 index 00000000..52ebdbc0 --- /dev/null +++ b/src/components/landing/fusion/FusionFeatures.tsx @@ -0,0 +1,103 @@ +import React from 'react' +import ProductFeatures, { type ProductFeatureArt } from '../product/ProductFeatures' +import Glow from '../product/Glow' +import { FEATURES, FEATURES_INTRO } from '@site/src/data/landing/fusion/features' + +const IMG = '/img/landing/fusion' + +/** The dark wave every tab shares, mirrored as in the design. */ +const Field = () => ( + +) + +/** The deep blue glow at the bottom left. */ +const FeatureGlow = () => ( + +) + +/** Every tab's panel sits where the design's fusion-1 to fusion-4 frames put it; only the copy moves. */ +const MOCK = 'lg:top-[52.22px] lg:right-[42.07px] lg:w-[507px]' + +const ART: Record = { + tiered: { + background: , + glow: , + copyTop: 332, + bodyWidth: 323, + image: { + src: `${IMG}/feature-tiered.webp`, + alt: 'OLake Fusion tables, with the last run of each compaction tier', + width: 507, + height: 437.4 + }, + mockClassName: MOCK + }, + decay: { + background: , + glow: , + copyTop: 377, + bodyWidth: 323, + image: { + src: `${IMG}/feature-decay.webp`, + alt: 'OLake Fusion maintenance steps completed on a table', + width: 507, + height: 437.4 + }, + mockClassName: MOCK + }, + config: { + background: , + glow: , + copyTop: 395, + bodyWidth: 323, + image: { + src: `${IMG}/feature-config.webp`, + alt: 'OLake Fusion cleanup schedule settings', + width: 507, + height: 437.4 + }, + mockClassName: MOCK + }, + hosted: { + background: , + glow: , + copyTop: 395, + bodyWidth: 323, + image: { + src: `${IMG}/feature-hosted.webp`, + alt: 'OLake Fusion deployment running on your own infrastructure', + width: 507, + height: 437.4 + }, + mockClassName: MOCK + } +} + +/** "Features" of the OLake Fusion page: the design's fusion-1 to fusion-4 frames behind a pill tab strip. */ +export default function FusionFeatures() { + return ( + + ) +} diff --git a/src/components/landing/fusion/FusionHero.tsx b/src/components/landing/fusion/FusionHero.tsx new file mode 100644 index 00000000..4493c689 --- /dev/null +++ b/src/components/landing/fusion/FusionHero.tsx @@ -0,0 +1,61 @@ +import React from 'react' +import ProductHero from '../product/ProductHero' +import Glow from '../product/Glow' +import { HERO } from '@site/src/data/landing/fusion/hero' + +const IMG = '/img/landing/fusion' + +/** + * The contour rings around the heading (drawn upside down, as the design places them), cleared + * behind the copy by a soft white glow and faded out above the problem section. + */ +const Background = () => ( + +) + +/** Hero of the OLake Fusion page. The headline is the PostHog A/B variant the page picked. */ +export default function FusionHero({ headline }: { headline: string }) { + return ( + } + className='lg:pt-[257px] lg:pb-[150px]' + > +
+ {/* The run-metrics card reaches 75px past the 1018px screen: below 1200px the whole picture + is fitted to the width; from 1200px up it is drawn full size, so the card hangs past + the 1018px column the way the design has it. */} + OLake Fusion run history dashboard +
+
+ ) +} diff --git a/src/components/landing/fusion/FusionPage.tsx b/src/components/landing/fusion/FusionPage.tsx new file mode 100644 index 00000000..580396b5 --- /dev/null +++ b/src/components/landing/fusion/FusionPage.tsx @@ -0,0 +1,31 @@ +import React from 'react' +import ProductPage from '../product/ProductPage' +import FusionHero from './FusionHero' +import FusionProblem from './FusionProblem' +import FusionFeatures from './FusionFeatures' +import FusionBenchmark from './FusionBenchmark' +import FusionCta from './FusionCta' +import FusionFaq from './FusionFaq' + +/** The OLake Fusion product page: hero, problem, features, benchmarks, a closing CTA and the FAQ. */ +export default function FusionPage({ + title, + description, + headline +}: { + title: string + description: string + /** The hero headline, from the PostHog A/B test. */ + headline: string +}) { + return ( + + + + + + + + + ) +} diff --git a/src/components/landing/fusion/FusionProblem.tsx b/src/components/landing/fusion/FusionProblem.tsx new file mode 100644 index 00000000..c5d814b7 --- /dev/null +++ b/src/components/landing/fusion/FusionProblem.tsx @@ -0,0 +1,168 @@ +import React from 'react' +import { cn } from '@site/src/lib/utils' +import { + PROBLEMS, + PROBLEM_INTRO, + type FusionProblemArt +} from '@site/src/data/landing/fusion/problems' + +const IMG = '/img/landing/fusion' + +/** + * Where each problem's copy and line drawing sit in its row, from lg up (offsets from the left of + * the ruled column, and from the top of the row), and the row's height. The design zig-zags them. + */ +const LAYOUT: Record< + FusionProblemArt, + { + height: number + text: [number, number] + /** Wrap width of the title, where the design wraps it. */ + textWidth?: number + art: [number, number] + image: React.ReactNode + } +> = { + 'small-files': { + height: 173, + text: [181, 80], + art: [22, 65], + image: ( + + ) + }, + 'slower-queries': { + height: 140, + text: [53, 44], + art: [410, 26], + image: ( + + ) + }, + 'invisible-costs': { + height: 137, + text: [173, 44], + art: [22, 26], + image: ( + + ) + }, + 'debugging': { + height: 177, + text: [53, 43], + textWidth: 324, + art: [469, 34.7], + // Drawn upright in the design: rotated a quarter turn in its 112 × 95 slot (96 × 81 below lg). + image: ( + + + + ) + } +} + +/** "Problem Statement": the four pain points down a ruled column between two guides. */ +export default function FusionProblem() { + return ( +
+
+ {/* The column's guides */} +
+
+ ) +} diff --git a/src/components/landing/fusion/Hero.tsx b/src/components/landing/fusion/Hero.tsx deleted file mode 100644 index 81a0df47..00000000 --- a/src/components/landing/fusion/Hero.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react' -import Button from '../ui/Button' -import { HERO } from '@site/src/data/landing/fusion/hero' - -/** First section of the page. It sits inside `.lakeside-hero-bg`, so it must stay a direct `
`. */ -export default function Hero({ headline }: { headline: string }) { - return ( -
-
-

- - {HERO.eyebrow} -

-

- {headline} -

-
- - -
-
-
- ) -} diff --git a/src/components/landing/fusion/ProductFlow.tsx b/src/components/landing/fusion/ProductFlow.tsx deleted file mode 100644 index 3f206363..00000000 --- a/src/components/landing/fusion/ProductFlow.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react' -import { PiCheckBold } from 'react-icons/pi' -import Section from '../ui/Section' -import { cn } from '@site/src/lib/utils' -import { FLOW } from '@site/src/data/landing/fusion/hero' - -const NODE_LABEL = 'mt-[10px] text-[11px] leading-[1.35] lg:text-[13px]' - -/** The line between two nodes, with small dots travelling along it (motion allowed only). */ -const Connector = ({ lag = false }: { lag?: boolean }) => ( - -) - -/** Iceberg tables go in, OLake Fusion works on them, optimized Iceberg tables come out. */ -export default function ProductFlow() { - return ( -
-
-
-
- - Iceberg - -

{FLOW.inputLabel}

-
- - - -
- - OLake Fusion - -

- {FLOW.engineName.brand} - {FLOW.engineName.product} -

-
- - - -
- - Iceberg - - - -

{FLOW.outputLabel}

-
-
- -
    - {FLOW.capabilities.map((item, i) => ( -
  • - {item.label} - {item.badge && ( - - {item.badge} - - )} -
  • - ))} -
-
-
- ) -} diff --git a/src/components/landing/fusion/SilentTax.tsx b/src/components/landing/fusion/SilentTax.tsx deleted file mode 100644 index f3ed1aa2..00000000 --- a/src/components/landing/fusion/SilentTax.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React from 'react' -import Section from '../ui/Section' -import { - PROBLEMS, - PROBLEMS_CLOSING, - PROBLEMS_EYEBROW, - type ProblemCard -} from '@site/src/data/landing/fusion/problems' -import { cn } from '@site/src/lib/utils' - -/** - * Each sentence is a small cloud of words: a recipe of sizes, weights, tones and tilts that repeats - * along the sentence, with the key words of the sentence large, bold and straight. The words bob - * gently out of step with each other (fusion.css, motion allowed only). - */ -const SIZES = [34, 22, 27, 24, 31, 21, 25] -const WEIGHTS = [600, 500, 600, 500, 600, 500, 600] -const TONES = ['ink', 'muted', 'ink', 'blue', 'ink', 'muted', 'ink'] as const -const TILTS = [-3, 2, -1, 3, -2, 1, 0] -const EMPHASIS_SIZE = 33 - -const normalise = (word: string) => word.toLowerCase().replace(/[^a-z']/g, '') - -interface Word { - text: string - size: number - weight: number - tone: (typeof TONES)[number] - opacity: number - tilt: number - delay: number -} - -function layoutWords(card: ProblemCard): Word[] { - const emphasis = card.emphasis.map(normalise) - const big = (card.big ?? []).map(normalise) - return card.words.map((text, i) => { - const key = normalise(text) - const isEmphasis = emphasis.includes(key) - const isBig = big.includes(key) - const slot = card.recipeAt?.[i] ?? i - const tone = isEmphasis ? 'ink' : TONES[slot % TONES.length] - return { - text, - size: isEmphasis || isBig ? EMPHASIS_SIZE : SIZES[slot % SIZES.length], - weight: isEmphasis ? 600 : WEIGHTS[slot % WEIGHTS.length], - tone, - // Only the ink words fade a little; the lighter tones keep full strength so they stay readable. - opacity: isEmphasis || tone !== 'ink' ? 1 : 0.8 + (0.2 * ((slot * 37) % 10)) / 10, - tilt: isEmphasis ? 0 : TILTS[slot % TILTS.length], - delay: (i % 5) * 0.3 - } - }) -} - -const Statement = ({ card }: { card: ProblemCard }) => ( -
  • -

    - {layoutWords(card).map((word, i) => ( - - {word.text} - - ))} -

    -
  • -) - -export default function SilentTax() { - return ( -
    -

    - {PROBLEMS_EYEBROW} -

    -
      - {PROBLEMS.map((card) => ( - - ))} -
    -

    - {PROBLEMS_CLOSING} -

    -
    - ) -} diff --git a/src/components/landing/fusion/fusion.css b/src/components/landing/fusion/fusion.css deleted file mode 100644 index bf98320f..00000000 --- a/src/components/landing/fusion/fusion.css +++ /dev/null @@ -1,250 +0,0 @@ -/* ============================================================================================= - * OLake Fusion page: the few touches that bring back the previous page's feel on top of the - * Lakeside design (word-cloud cards, travelling dots, pulses). Colours are mixes of - * the design tokens; every animation is opacity or transform and runs only when the visitor allows - * motion (static fallbacks are the default styles). - * ========================================================================================== */ - -.lakeside-page { - /* The thin lavender line around the "silent tax" cards. */ - --fusion-lavender: color-mix(in srgb, var(--olake-blue) 20%, white); - /* Word tones in those cards: deep navy ink, a quiet lavender, and a lighter brand blue. */ - --fusion-navy: color-mix(in srgb, var(--olake-blue) 25%, black); - --fusion-muted: color-mix( - in srgb, - color-mix(in srgb, var(--olake-blue) 40%, var(--olake-muted)) 88%, - white - ); - --fusion-blue-soft: color-mix(in srgb, var(--olake-blue) 70%, white); - /* A capability chip while it is "lit" in the flow diagram. */ - --fusion-lit: color-mix(in srgb, var(--olake-blue) 16%, white); -} - -/* ---- Silent tax: the words ---------------------------------------------------------------- */ - -.fusion-word { - display: inline-block; - line-height: 1.1; - letter-spacing: -0.005em; - transform: rotate(var(--r, 0deg)); -} - -.fusion-word--ink { - color: var(--fusion-navy); -} - -.fusion-word--muted { - color: var(--fusion-muted); -} - -.fusion-word--blue { - color: var(--fusion-blue-soft); -} - -/* ---- Flow diagram: dots, ring, chips -------------------------------------------------------- */ - -.fusion-dot { - display: none; -} - -/* Capability chips: the "lit" state is an overlay that fades in, so only opacity animates. */ -.fusion-pill { - position: relative; - isolation: isolate; -} - -.fusion-pill::before { - content: ''; - position: absolute; - inset: -1px; - z-index: -1; - border-radius: inherit; - background: var(--fusion-lit); - opacity: 0; -} - -.fusion-ring::after { - content: ''; - position: absolute; - inset: -2px; - border-radius: inherit; - border: 2px solid color-mix(in srgb, var(--olake-blue) 40%, transparent); - opacity: 0; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .fusion-word { - animation: fusion-word-float 4s ease-in-out infinite; - animation-delay: var(--d, 0s); - } - - .fusion-dot { - display: block; - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - transform: translateX(-100%); - opacity: 0; - animation: fusion-travel 3s linear infinite; - pointer-events: none; - } - - /* The dot is the right end of a line-wide box, so translating the box carries it along the line. */ - .fusion-dot::after { - content: ''; - position: absolute; - top: 50%; - right: 0; - width: 8px; - height: 8px; - margin: -4px -4px 0 0; - border-radius: 50%; - background: color-mix(in srgb, var(--olake-blue) 75%, white); - box-shadow: 0 0 8px color-mix(in srgb, var(--olake-blue) 60%, transparent); - } - - .fusion-dot:first-child::after { - width: 10px; - height: 10px; - margin: -5px -5px 0 0; - background: var(--olake-blue); - } - - .fusion-dot:nth-child(2) { - animation-delay: 1s; - } - - .fusion-dot:nth-child(3) { - animation-delay: 2s; - } - - /* The second line starts half a cycle later, so a dot seems to carry on through the node. */ - .fusion-conn--lag .fusion-dot { - animation-delay: 1.5s; - } - - .fusion-conn--lag .fusion-dot:nth-child(2) { - animation-delay: 2.5s; - } - - .fusion-conn--lag .fusion-dot:nth-child(3) { - animation-delay: 3.5s; - } - - .fusion-ring::after { - animation: fusion-ring 2.4s ease-in-out infinite; - } - - .fusion-pill::before { - animation: fusion-lit-1 4s ease-in-out infinite; - } - - .fusion-pill--2::before { - animation-name: fusion-lit-2; - } - - .fusion-pill--3::before { - animation-name: fusion-lit-3; - } - - /* The feature card slides in each time its feature is picked (or comes round). */ - .fusion-panel[data-active='true'] .fusion-card { - animation: fusion-slide-in 0.5s ease; - } -} - -@keyframes fusion-word-float { - 0%, - 100% { - transform: translateY(0) rotate(var(--r, 0deg)); - } - 50% { - transform: translateY(-5px) rotate(var(--r, 0deg)); - } -} - -@keyframes fusion-travel { - 0% { - transform: translateX(-100%); - opacity: 0; - } - 10%, - 90% { - opacity: 1; - } - 100% { - transform: translateX(0); - opacity: 0; - } -} - -@keyframes fusion-ring { - 0%, - 100% { - transform: scale(1); - opacity: 0.6; - } - 50% { - transform: scale(1.28); - opacity: 0; - } -} - -@keyframes fusion-lit-1 { - 0%, - 8% { - opacity: 0; - } - 16%, - 88% { - opacity: 1; - } - 96%, - 100% { - opacity: 0; - } -} - -@keyframes fusion-lit-2 { - 0%, - 32% { - opacity: 0; - } - 40%, - 88% { - opacity: 1; - } - 96%, - 100% { - opacity: 0; - } -} - -@keyframes fusion-lit-3 { - 0%, - 56% { - opacity: 0; - } - 64%, - 88% { - opacity: 1; - } - 96%, - 100% { - opacity: 0; - } -} - -@keyframes fusion-slide-in { - from { - opacity: 0; - transform: translateX(14px); - } - to { - opacity: 1; - transform: translateX(0); - } -} diff --git a/src/components/landing/go/FeatureVisual.tsx b/src/components/landing/go/FeatureVisual.tsx deleted file mode 100644 index f37b5bba..00000000 --- a/src/components/landing/go/FeatureVisual.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React from 'react' -import { cn } from '@site/src/lib/utils' -import type { GoFeatureArt } from '@site/src/data/landing/go/features' - -/** - * The small product mock-ups inside the feature card, drawn in the same grey panel + white sheet - * the home page uses for its feature cards. They are decorative (aria-hidden); the card's heading - * and body say the same thing in words. - */ - -const Panel = ({ children }: { children: React.ReactNode }) => ( - -) - -const Caption = ({ children }: { children: React.ReactNode }) => ( -

    {children}

    -) - -const Bar = ({ className }: { className?: string }) => ( - -) - -const Tiered = () => ( - -
    -
    - Full -
    - - - -
    -

    one-time load

    -
    -
    - Incremental -
    - - - -
    -

    on schedule

    -
    -
    - CDC -
    - - -
    -

    real-time

    -
    -
    -
    -) - -const Row = ({ className }: { className?: string }) => ( - -) - -const Decay = () => ( - -
    -
    - Source table -
    - - - -
    -
    - -
    - Iceberg table -
    - - -
    - - - + NEW - -
    -
    -
    -
    -
    -) - -const Chunk = () => ( - -
    -
    -
    - {['chunk 1', 'chunk 2', 'chunk 3', 'chunk 4'].map((label) => ( -
    - {label} -
    - ))} -
    -

    read in parallel

    - -) - -const Resume = () => ( - -
    - - - - ✓ checkpoint - - -
    -
    - interrupted - resumes here → -
    -
    -) - -const VISUALS: Record React.JSX.Element> = { - tiered: Tiered, - decay: Decay, - chunk: Chunk, - resume: Resume -} - -export default function FeatureVisual({ art }: { art: GoFeatureArt }) { - const Visual = VISUALS[art] - return -} diff --git a/src/components/landing/go/GoArchitecture.tsx b/src/components/landing/go/GoArchitecture.tsx deleted file mode 100644 index 8c1aac54..00000000 --- a/src/components/landing/go/GoArchitecture.tsx +++ /dev/null @@ -1,152 +0,0 @@ -import React, { useRef } from 'react' -import Section from '../ui/Section' -import Card from '../ui/Card' -import { cn } from '@site/src/lib/utils' -import { - GO_ARCHITECTURE_LABELS, - GO_DESTINATIONS, - GO_SOURCES, - GO_SYNC_MODES -} from '@site/src/data/landing/go/architecture' -import { useDiagramLinks } from './useDiagramLinks' - -const Label = ({ children }: { children: React.ReactNode }) => ( -

    {children}

    -) - -const DownArrow = () => ( - -) - -/** Sources, the OLake Go node and the two destinations. The lines are drawn after mount (see useDiagramLinks). */ -export default function GoArchitecture() { - const containerRef = useRef(null) - const nodeRef = useRef(null) - const diagram = useDiagramLinks(containerRef, nodeRef) - - return ( -
    - -
    - {diagram && ( - - )} - -
    - -
      - {GO_SOURCES.map((name) => ( -
    • -
    • - ))} -
    -
    - - - -
    -
    - {GO_ARCHITECTURE_LABELS.node} -
    -

    {GO_ARCHITECTURE_LABELS.node}

    -
      - {GO_SYNC_MODES.map((mode, i) => ( -
    • - {mode} -
    • - ))} -
    -
    - - - -
    - -
      - {GO_DESTINATIONS.map((dest) => ( -
    • - {dest.icon.alt} - {dest.label} -
    • - ))} -
    -
    -
    -
    -
    - ) -} diff --git a/src/components/landing/go/GoBenchmark.tsx b/src/components/landing/go/GoBenchmark.tsx index 5affa4e4..4cd8cdeb 100644 --- a/src/components/landing/go/GoBenchmark.tsx +++ b/src/components/landing/go/GoBenchmark.tsx @@ -1,9 +1,8 @@ -import React, { useState } from 'react' -import Link from '@docusaurus/Link' -import SectionHeading from '../ui/SectionHeading' +import React from 'react' import { cn } from '@site/src/lib/utils' +import BenchmarkShell, { FOCUS_ON_DARK as FOCUS, RULE } from '../product/BenchmarkShell' import { - GO_BENCHMARK_INFO, + GO_BENCHMARK_HOW, GO_BENCHMARK_INTRO, GO_BENCHMARK_LINK, GO_BENCHMARK_MODES, @@ -11,250 +10,207 @@ import { } from '@site/src/data/landing/go/benchmark' import { useGoBenchmark } from './useGoBenchmark' -const ExternalMark = () => ( - -) - -const Chevron = ({ open }: { open: boolean }) => ( - -) - -const FOCUS = - 'focus-visible:rounded-[4px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-olake-blue-on-dark' - -const BenchLink = ({ className }: { className?: string }) => ( - void + children: React.ReactNode +}) => ( + ) -/** The "OLake Advantage" block: the dark benchmark card with mode and source switching, and the "how" disclosure. */ +/** "7.5 hours (failed sync)" reads as the figure with its remark on a smaller second line. */ +const Figure = ({ value }: { value: string }) => { + const at = value.indexOf(' (') + if (at < 0) return <>{value} + return ( + <> + {value.slice(0, at)} + {value.slice(at + 1)} + + ) +} + +/** Column widths of the design's table (198, 172, 147, 148, 147 and 145 of its 957px); it leaves 45px free on the right. */ +const COLUMNS = ['20.69%', '17.97%', '15.36%', '15.47%', '15.36%', '15.15%'] + +/** + * "Benchmarks": the dark full-bleed section with the latest-release strip, the comparison table + * (switchable between full load and CDC and across sources), how OLake Go gets there, and a bar + * linking to the full benchmarks. + */ export default function GoBenchmark() { const t = useGoBenchmark() - const [infoOpen, setInfoOpen] = useState(false) - return ( -
    -
    -
    - - part.strong ? ( - - {part.text} - - ) : ( - {part.text} - ) - )} - className='[&_p:last-child]:max-w-[640px]' - /> -
    - -
    -
    -
    -
    - {GO_BENCHMARK_MODES.map((m) => ( - - ))} -
    - + + part.muted ? ( + + {part.text} + + ) : ( + {part.text} + ) + )} + how={{ + title: GO_BENCHMARK_HOW.title, + body: ( +
    + {GO_BENCHMARK_HOW.items.map((item) => ( +

    + {item.term} / {item.body} +

    + ))} +
    + ) + }} + link={GO_BENCHMARK_LINK} + > +
    +
    +

    + {t.caption[0]} +
    + {t.caption[1]} +

    +
    +
    + {GO_BENCHMARK_MODES.map((m) => ( + t.setMode(m.id)}> + {m.label} + + ))}
    - -
    +
    {t.connectors.map((c) => ( - + ))}
    +
    +
    - {t.hasData ? ( -
    - - - - - + ) + })} + +
    - Metrics - - - {t.olakeLabel} - - - {t.olakeSub} +
    + + + {COLUMNS.map((width, i) => ( + + ))} + + + + + + {t.competitors.map((name, i) => ( + + ))} + + + + {t.rows.map((row, r) => { + const cell = 'px-[8px] pt-[29px] pb-0 align-top text-[14px] leading-[1.3]' + return ( + + - {t.competitors.map((name) => ( - - ))} - - - - {t.rows.map((row) => ( - + + {row.competitors.map((value, i) => ( + - - {row.competitors.map((value, i) => ( - - ))} - +
    + ))} -
    -
    + Metrics + + + OLake + Go + + {t.olakeSub} + + {name} +
    + {row.label} + {row.note && ( + + {row.note} - - {name} -
    +
    +
    - - - {row.label} - - {row.note && ( - - {row.note} - - )} - - {row.olake} - - {value} -
    -
    - ) : ( -
    - -

    - {t.sourceName} benchmarks coming soon -

    -

    - We're running head-to-head {t.sourceName} ingestion tests now. Check back - shortly for the full comparison. -

    -
    - )} - -
    - -
    - - -
    - - -
    +
    -
    + ) } diff --git a/src/components/landing/go/GoCta.tsx b/src/components/landing/go/GoCta.tsx index bac7c04c..59138d41 100644 --- a/src/components/landing/go/GoCta.tsx +++ b/src/components/landing/go/GoCta.tsx @@ -1,34 +1,25 @@ import React from 'react' -import Button from '../ui/Button' -import Section from '../ui/Section' +import ProductCta from '../product/ProductCta' import { GO_CTA } from '@site/src/data/landing/go/cta' -/** - * Closing call-to-action. It follows the shared CtaBanner (outlined card, copy on top, one primary - * button) but crops its 16:9 iceberg picture into a band, which CtaBanner's image slot cannot do. - */ +/** Closing call to action of the OLake Go page: the blue card with a field of dots rising behind the buttons. */ export default function GoCta() { return ( -
    -
    -
    -

    - {GO_CTA.title} -

    - -
    + -
    -
    + } + /> ) } diff --git a/src/components/landing/go/GoFaq.tsx b/src/components/landing/go/GoFaq.tsx index f36408c9..53f67754 100644 --- a/src/components/landing/go/GoFaq.tsx +++ b/src/components/landing/go/GoFaq.tsx @@ -1,67 +1,10 @@ -import React, { useState } from 'react' -import Section from '../ui/Section' -import SectionHeading from '../ui/SectionHeading' -import { cn } from '@site/src/lib/utils' -import { GO_FAQS, GO_FAQ_TITLE } from '@site/src/data/landing/go/faq' +import React from 'react' +import ProductFaq from '../product/ProductFaq' +import { GO_FAQS, GO_FAQ_EYEBROW, GO_FAQ_TITLE } from '@site/src/data/landing/go/faq' -const Plus = ({ open }: { open: boolean }) => ( - -) - -/** - * Accordion, one answer open at a time. Answers stay in the DOM (hidden) so the text the FAQPage - * JSON-LD describes is always on the page. - */ +/** The OLake Go FAQ. The page's FAQPage JSON-LD (seo.ts) is built from the same list. */ export default function GoFaq() { - const [open, setOpen] = useState(null) - return ( -
    -
    - -
      - {GO_FAQS.map((faq, i) => { - const isOpen = open === i - return ( -
    • -

      - -

      - -
    • - ) - })} -
    -
    -
    + ) } diff --git a/src/components/landing/go/GoFeatures.tsx b/src/components/landing/go/GoFeatures.tsx index efaa4fa7..1e27d7f9 100644 --- a/src/components/landing/go/GoFeatures.tsx +++ b/src/components/landing/go/GoFeatures.tsx @@ -1,131 +1,117 @@ -import React, { useEffect, useRef, useState } from 'react' -import { PiArrowClockwise, PiChartBar, PiColumns, PiTable } from 'react-icons/pi' -import Section from '../ui/Section' -import SectionHeading from '../ui/SectionHeading' -import Card from '../ui/Card' +import React from 'react' import { cn } from '@site/src/lib/utils' -import { - GO_FEATURES, - GO_FEATURES_INTRO, - type GoFeatureArt -} from '@site/src/data/landing/go/features' -import FeatureVisual from './FeatureVisual' -import { useFeatureCycle } from './useFeatureCycle' +import ProductFeatures, { type ProductFeatureArt } from '../product/ProductFeatures' +import Glow from '../product/Glow' +import { GO_FEATURES, GO_FEATURES_INTRO } from '@site/src/data/landing/go/features' -const ICONS: Record> = { - tiered: PiChartBar, - decay: PiTable, - chunk: PiColumns, - resume: PiArrowClockwise -} +const IMG = '/img/landing/go' -/** The feature selector: four tabs on the left, the active capability's card on the right. */ -export default function GoFeatures() { - const { ref, active, progress, select } = useFeatureCycle(GO_FEATURES.length) - const tabRefs = useRef<(HTMLButtonElement | null)[]>([]) - // The card slides in when the feature changes, but not on the first paint. - const [changed, setChanged] = useState(false) - const first = useRef(true) - useEffect(() => { - if (first.current) { - first.current = false - return - } - setChanged(true) - }, [active]) +/** The dotted field shared by the last three tabs, placed and flipped as each of their frames has it. */ +const field = (className: string) => ( + +) - const onKeyDown = (event: React.KeyboardEvent, index: number) => { - const last = GO_FEATURES.length - 1 - let next = index - if (event.key === 'ArrowDown' || event.key === 'ArrowRight') next = index === last ? 0 : index + 1 - else if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') next = index === 0 ? last : index - 1 - else if (event.key === 'Home') next = 0 - else if (event.key === 'End') next = last - else return - event.preventDefault() - select(next) - tabRefs.current[next]?.focus() - } +/** The blue glow at the bottom left that every tab shares. */ +const FeatureGlow = () => ( + +) - const feature = GO_FEATURES[active] - const Icon = ICONS[feature.art] +/** + * Each tab's picture (the design's go-1 to go-4 frames): the background field, where the copy sits + * from lg up (its top, and the body's width) and the editor panel with its lg position. + */ +const ART: Record = { + 'cdc': { + background: ( + + ), + glow: , + copyTop: 338, + bodyWidth: 323, + image: { + src: `${IMG}/feature-cdc.webp`, + alt: 'OLake Go job editor, choosing the sync mode of a stream', + width: 514, + height: 438 + }, + mockClassName: 'lg:top-[52px] lg:right-[36px] lg:w-[514px]' + }, + 'schema-evolution': { + background: field('right-[-281px]'), + glow: , + copyTop: 338, + bodyWidth: 323, + image: { + src: `${IMG}/feature-schema-evolution.webp`, + alt: 'OLake Go stream schema, mapping source columns to the destination', + width: 514.1, + height: 391.4 + }, + mockClassName: 'lg:top-[67.3px] lg:right-[36.1px] lg:w-[514.1px]' + }, + 'parallel-chunking': { + background: field('right-[-129px] -scale-x-100'), + glow: , + copyTop: 354, + bodyWidth: 288, + image: { + src: `${IMG}/feature-parallel-chunking.webp`, + alt: 'OLake Go splitting a large table into chunks synced in parallel', + width: 514.1, + height: 391.4 + }, + mockClassName: 'lg:top-[67.3px] lg:right-[36.1px] lg:w-[514.1px]' + }, + 'resumable-syncs': { + background: field('right-[-129px] -scale-y-100'), + glow: , + copyTop: 333, + bodyWidth: 288, + image: { + src: `${IMG}/feature-resumable-syncs.webp`, + alt: 'OLake Go sync resuming from its last checkpoint', + width: 514.1, + height: 391.4 + }, + mockClassName: 'lg:top-[67.3px] lg:right-[36.1px] lg:w-[514.1px]' + } +} +/** "Features" of the OLake Go page: the design's go-1 to go-4 frames behind a pill tab strip. */ +export default function GoFeatures() { return ( -
    - -
    -
    - {GO_FEATURES.map((item, i) => { - const selected = i === active - return ( - - ) - })} -
    - - -
    - -
    - -
    -
    -

    - {feature.title} -

    -

    - {feature.body} -

    -
    -
    -
    -
    -
    + ) } diff --git a/src/components/landing/go/GoHero.tsx b/src/components/landing/go/GoHero.tsx index 35e2edeb..1b732bf5 100644 --- a/src/components/landing/go/GoHero.tsx +++ b/src/components/landing/go/GoHero.tsx @@ -1,36 +1,95 @@ import React from 'react' -import Button from '../ui/Button' -import { GO_HERO } from '@site/src/data/landing/go/hero' +import ProductHero from '../product/ProductHero' +import Glow from '../product/Glow' +import { GO_HERO, GO_SOURCES, GO_SOURCES_LABEL } from '@site/src/data/landing/go/hero' +import { GO_LOGOS } from './logos' -/** Hero of the OLake Go page. Its wrapper (`.lakeside-hero-bg`) lives in LakesidePage. */ +const IMG = '/img/landing/go' + +const SourceList = ({ hidden }: { hidden?: boolean }) => ( +
      + {GO_SOURCES.map((source) => ( +
    • + + + {source.name} + +
    • + ))} +
    +) + +/** + * "Available Sources": a tab-shaped strip whose logos scroll by (paused on hover and for reduced + * motion). From md up the label is pinned left and the logos run in their own lane after it, fading + * in past the label and out at the right edge as in the design; below md the label sits above a + * full-width lane. + */ +function SourcesStrip() { + return ( +
    +

    + {/* The live dot: a 12px circle in a 4px halo */} +

    +
    +
    + +
    +
    +
    + ) +} + +/** The strata lines, cleared behind the copy by a soft white glow. */ +const Background = () => ( + +) + +/** Hero of the OLake Go page: strata background, copy, the two calls to action, sources and the product shot. */ export default function GoHero() { return ( -
    -
    -

    - {/* First image on the page and above the fold, so it is not lazy-loaded. */} + } className='lg:pt-[209px]'> +

    + +
    - {GO_HERO.badge} -

    -

    - {GO_HERO.headline} -

    -
    - -
    -
    + ) } diff --git a/src/components/landing/go/GoPage.tsx b/src/components/landing/go/GoPage.tsx index 9724f8d1..d5faf62e 100644 --- a/src/components/landing/go/GoPage.tsx +++ b/src/components/landing/go/GoPage.tsx @@ -1,37 +1,23 @@ import React from 'react' -import LakesidePage from '../ui/LakesidePage' +import ProductPage from '../product/ProductPage' import GoHero from './GoHero' -import GoArchitecture from './GoArchitecture' import GoProblem from './GoProblem' import GoFeatures from './GoFeatures' import GoBenchmark from './GoBenchmark' -import GoFaq from './GoFaq' import GoCta from './GoCta' +import GoFaq from './GoFaq' import './go.css' -/** The OLake Go product page: hero, diagram, problem, features, benchmarks, a closing CTA and the FAQ. */ +/** The OLake Go product page: hero, problem, features, benchmarks, a closing CTA and the FAQ. */ export default function GoPage({ title, description }: { title: string; description: string }) { return ( - } - > - {/* Infima gives headings, paragraphs and lists bottom margins and lists a left padding; - the lakeside reset is zero-specificity and loses, so it is undone here for the whole page. */} -
    - - - -
    -
    - -
    -
    - - -
    -
    + + + + + + + + ) } diff --git a/src/components/landing/go/GoProblem.tsx b/src/components/landing/go/GoProblem.tsx index fe1148b3..c8d9a4a3 100644 --- a/src/components/landing/go/GoProblem.tsx +++ b/src/components/landing/go/GoProblem.tsx @@ -1,79 +1,138 @@ import React from 'react' -import Section from '../ui/Section' -import SectionHeading from '../ui/SectionHeading' -import { cn } from '@site/src/lib/utils' -import { GO_PROBLEM } from '@site/src/data/landing/go/problem' +import { GO_PROBLEMS, GO_PROBLEM_INTRO, type GoProblemArt } from '@site/src/data/landing/go/problem' -/** The cloud outline, stretched to the box it sits in. */ -const CLOUD_PATH = - 'M205,278 C150,278 108,246 100,206 C60,200 34,168 34,132 C34,96 62,66 100,60 C108,26 146,2 192,2 C224,2 253,15 272,37 C296,16 331,4 370,4 C424,4 470,32 486,72 C505,58 530,50 557,50 C607,50 650,80 662,120 C712,120 752,152 752,192 C752,228 720,258 678,262 C670,290 636,310 596,310 C572,310 550,303 533,290 C512,306 481,316 447,316 C420,316 396,310 377,299 C356,314 328,323 297,323 C258,323 224,308 205,278 Z' +const IMG = '/img/landing/go' -/** - * "The Problem": four statements tilted inside a cloud, the key words larger and in the brand blue, - * then the answer headline under the cloud. Each statement drifts up and down slowly (go.css). - */ +/** Each problem's line drawing, at its own size and where the design places it in the card's 203px art column. */ +const ART: Record = { + 'slow-syncs': ( + + ), + 'legacy-costs': ( + + ), + 'cdc-breaks': ( + <> + + {/* The stale-source mark stands upright in the design: rotated a quarter turn in its 48 × 74 slot. */} + + + + + ), + 'schema-drift': ( + + ) +} + +/** The frame's dashed outline (22px dashes, as in the design), drawn on the pixel grid so it stays crisp. */ +const DashedFrame = () => ( + +) + +/** One of the frame's two vertical dashed guides. */ +const Guide = ({ className }: { className: string }) => ( +
    ) } diff --git a/src/components/landing/go/go.css b/src/components/landing/go/go.css index 8484885c..0aa55adb 100644 --- a/src/components/landing/go/go.css +++ b/src/components/landing/go/go.css @@ -1,241 +1,27 @@ /* ============================================================================================= - * OLake Go page: the few pieces Tailwind utilities do not express well (the cloud, the tinted - * features background, keyframes). Every colour is a token or a color-mix of tokens. Motion only - * runs for visitors without a reduced-motion preference and moves opacity or transform. + * OLake Go page: the sources marquee under the hero. The page's palette, grain and mock-up scaling + * are shared with the other product pages (../product/product.css). Motion only runs for visitors + * without a reduced-motion preference. * ========================================================================================== */ -.go-page { - --go-cloud-fill: color-mix(in srgb, var(--olake-blue) 3%, var(--olake-surface)); - --go-cloud-line: color-mix(in srgb, var(--olake-blue) 18%, var(--olake-line)); - --go-tint: color-mix(in srgb, var(--olake-blue) 4%, var(--olake-surface)); +/* ---- The "Available Sources" marquee -------------------------------------------------------- */ +.go-marquee-track { + display: flex; + width: max-content; } -/* ---- The cloud --------------------------------------------------------------------------- */ -.go-cloud { - position: relative; - width: 100%; - max-width: 920px; - height: 300px; - margin-inline: auto; -} - -.go-cloud-shape { - position: absolute; - inset: -6% -4%; - width: 108%; - height: 112%; -} - -.go-cloud-line { - position: absolute; - white-space: nowrap; - transform: translate(-50%, -50%) rotate(var(--go-rot, 0deg)); -} - -.go-cloud-line p { - font-size: 13px; - line-height: 1.1; - letter-spacing: -0.005em; -} - -.go-cloud-line .go-key { - font-size: 16px; -} - -@media (min-width: 640px) { - .go-cloud { - height: 400px; - } - - .go-cloud-line p { - font-size: calc(var(--go-size, 24) * 1px); - } - - .go-cloud-line .go-key { - font-size: calc((var(--go-size, 24) + 7) * 1px); - } -} - -/* ---- Features: a very light brand tint that fades in and out ------------------------------- */ -.go-features-tint { - background: linear-gradient( - 180deg, - transparent 0, - var(--go-tint) 96px, - var(--go-tint) calc(100% - 96px), - transparent 100% - ); -} - -.go-feature-tile { - background: linear-gradient( - 135deg, - var(--olake-blue), - color-mix(in srgb, var(--olake-blue) 72%, var(--olake-surface)) - ); -} - -/* ---- Motion ------------------------------------------------------------------------------ */ @media (prefers-reduced-motion: no-preference) { - @keyframes go-word-float { - 0%, - 100% { - transform: translateY(0); - } - 50% { - transform: translateY(-5px); - } - } - - @keyframes go-fade-slide-in { - from { - opacity: 0; - transform: translateX(14px); - } - to { - opacity: 1; - transform: translateX(0); - } - } - - @keyframes go-fade-in { - from { - opacity: 0; - } + @keyframes go-marquee { to { - opacity: 1; + transform: translateX(-50%); } } - @keyframes go-pulse-ring { - 0%, - 100% { - opacity: 0.4; - transform: scale(1); - } - 50% { - opacity: 0; - transform: scale(1.22); - } + .go-marquee-track { + animation: go-marquee 36s linear infinite; } - /* each sync mode lights up in turn: Full Load, then CDC, then Incremental */ - @keyframes go-light-1 { - 0%, - 8% { - opacity: 0; - } - 16%, - 88% { - opacity: 1; - } - 96%, - 100% { - opacity: 0; - } - } - - @keyframes go-light-2 { - 0%, - 32% { - opacity: 0; - } - 40%, - 88% { - opacity: 1; - } - 96%, - 100% { - opacity: 0; - } - } - - @keyframes go-light-3 { - 0%, - 56% { - opacity: 0; - } - 64%, - 88% { - opacity: 1; - } - 96%, - 100% { - opacity: 0; - } - } - - /* each cloud statement drifts up and down, offset from the others */ - .go-cloud-float { - animation: go-word-float 5s ease-in-out infinite; - animation-delay: var(--go-delay, 0s); - } - - /* the ring around the OLake Go node */ - .go-node { - position: relative; - } - - .go-node::after { - content: ''; - position: absolute; - inset: -2px; - border-radius: inherit; - border: 2px solid var(--olake-blue); - pointer-events: none; - animation: go-pulse-ring 2.4s ease-in-out infinite; - } - - .go-chip { - position: relative; - isolation: isolate; - overflow: hidden; - } - - .go-chip::before { - content: ''; - position: absolute; - z-index: -1; - inset: 0; - opacity: 0; - background: var(--olake-blue-tint); - } - - .go-chip-1::before { - animation: go-light-1 4s ease-in-out infinite; - } - - .go-chip-2::before { - animation: go-light-2 4s ease-in-out infinite; - } - - .go-chip-3::before { - animation: go-light-3 4s ease-in-out infinite; - } - - /* the feature card slides in when the active feature changes */ - .go-card-in { - animation: go-fade-slide-in 500ms var(--olake-ease-out) both; - } - - /* opened FAQ answers and the "how" disclosure fade in */ - .go-reveal { - animation: go-fade-in 220ms var(--olake-ease-out) both; - } - - /* the live CDC dot in the first feature preview */ - .go-live-dot { - animation: go-word-float 1.6s ease-in-out infinite; - } - - .go-tab-fill { - transition: transform 100ms linear; - } -} - -/* With reduced motion nothing cycles, so the selected feature shows a full underline. */ -@media (prefers-reduced-motion: reduce) { - .go-tab[aria-selected='true'] .go-tab-fill { - transform: scaleX(1) !important; + .go-marquee:hover .go-marquee-track { + animation-play-state: paused; } } diff --git a/src/components/landing/go/logos.ts b/src/components/landing/go/logos.ts new file mode 100644 index 00000000..38fcc13b --- /dev/null +++ b/src/components/landing/go/logos.ts @@ -0,0 +1,23 @@ +import type { GoSourceId } from '@site/src/data/landing/go/hero' + +/** A logo as a plain image: its file and the size the design shows it at. */ +export interface GoLogo { + src: string + width: number + height: number +} + +/** + * The logo of each source OLake Go reads from, for the hero's sources strip. Most reuse the site's + * existing logo files; MSSQL and DB2 are the design's own artwork, cropped to the mark. + */ +export const GO_LOGOS: Record = { + kafka: { src: '/img/site/kafka.png', width: 10.44, height: 17.31 }, + postgres: { src: '/img/site/postgres.svg', width: 20.81, height: 20.81 }, + mysql: { src: '/img/site/mysql.svg', width: 20.81, height: 20.81 }, + mongodb: { src: '/img/site/mongodb.svg', width: 20.81, height: 20.81 }, + s3: { src: '/img/landing/v2/connector-s3.webp', width: 15.55, height: 18.6 }, + mssql: { src: '/img/landing/go/source-mssql.webp', width: 18.51, height: 20 }, + db2: { src: '/img/landing/go/source-db2.webp', width: 17.74, height: 17.74 }, + oracle: { src: '/img/site/oracle.png', width: 21, height: 21 } +} diff --git a/src/components/landing/go/useDiagramLinks.ts b/src/components/landing/go/useDiagramLinks.ts deleted file mode 100644 index b5f01a85..00000000 --- a/src/components/landing/go/useDiagramLinks.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useEffect, useState, type RefObject } from 'react' - -export interface DiagramLink { - d: string - /** Seconds before the travelling dot starts, so the dots do not move in lockstep. */ - begin: number - side: 'in' | 'out' -} - -export interface DiagramLinks { - width: number - height: number - links: DiagramLink[] - /** False when the visitor prefers reduced motion: the lines stay, the dots do not travel. */ - animate: boolean -} - -/** - * Measures the source chips, the OLake Go node and the destination chips (found by `data-src` and - * `data-dest`) and returns the bezier paths that join them. It runs after mount only, so the first - * render matches the server's. On narrow screens the diagram stacks and no links are returned. - */ -export function useDiagramLinks( - containerRef: RefObject, - nodeRef: RefObject -): DiagramLinks | null { - const [state, setState] = useState(null) - - useEffect(() => { - const container = containerRef.current - const node = nodeRef.current - if (!container || !node) return - - const measure = () => { - const srcs = Array.from(container.querySelectorAll('[data-src]')) - const dests = Array.from(container.querySelectorAll('[data-dest]')) - const c = container.getBoundingClientRect() - const n = node.getBoundingClientRect() - // Stacked layout (mobile): the first source is not to the left of the node. - if (!srcs.length || !dests.length || srcs[0].getBoundingClientRect().right > n.left + 4) { - setState(null) - return - } - const nodeL = { x: n.left - c.left + 10, y: n.top - c.top + n.height / 2 } - const nodeR = { x: n.right - c.left - 10, y: n.top - c.top + n.height / 2 } - const links: DiagramLink[] = [] - srcs.forEach((el, i) => { - const r = el.getBoundingClientRect() - const p = { x: r.right - c.left - 8, y: r.top - c.top + r.height / 2 } - const dx = Math.max(28, (nodeL.x - p.x) * 0.5) - links.push({ - side: 'in', - begin: i * 0.28, - d: `M${p.x},${p.y} C${p.x + dx},${p.y} ${nodeL.x - dx},${nodeL.y} ${nodeL.x},${nodeL.y}` - }) - }) - dests.forEach((el, i) => { - const r = el.getBoundingClientRect() - const p = { x: r.left - c.left + 8, y: r.top - c.top + r.height / 2 } - const dx = Math.max(28, (p.x - nodeR.x) * 0.5) - links.push({ - side: 'out', - begin: 0.6 + i * 0.6, - d: `M${nodeR.x},${nodeR.y} C${nodeR.x + dx},${nodeR.y} ${p.x - dx},${p.y} ${p.x},${p.y}` - }) - }) - setState({ - width: c.width, - height: c.height, - links, - animate: !window.matchMedia('(prefers-reduced-motion: reduce)').matches - }) - } - - measure() - const ro = new ResizeObserver(measure) - ro.observe(container) - return () => ro.disconnect() - }, [containerRef, nodeRef]) - - return state -} diff --git a/src/components/landing/go/useFeatureCycle.ts b/src/components/landing/go/useFeatureCycle.ts deleted file mode 100644 index 71733976..00000000 --- a/src/components/landing/go/useFeatureCycle.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react' - -const TICK_MS = 100 -/** 100 ticks of 100ms: each feature is on screen for ten seconds before the next one. */ -const TICKS_PER_FEATURE = 100 - -/** - * Drives the feature selector: once the section scrolls into view the active feature advances every - * ten seconds and `progress` (0 to 100) fills its underline. Picking a feature by hand stops the - * cycle for good, and it never starts for visitors who prefer reduced motion. - */ -export function useFeatureCycle(count: number) { - const ref = useRef(null) - const [active, setActive] = useState(0) - const [progress, setProgress] = useState(0) - const [running, setRunning] = useState(false) - const [manual, setManual] = useState(false) - - useEffect(() => { - const el = ref.current - if (!el || manual) return - if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return - if (!('IntersectionObserver' in window)) { - setRunning(true) - return - } - const observer = new IntersectionObserver( - ([entry]) => { - if (entry.isIntersecting) { - setRunning(true) - observer.disconnect() - } - }, - { threshold: 0.3 } - ) - observer.observe(el) - return () => observer.disconnect() - }, [manual]) - - useEffect(() => { - if (!running || manual) return - const id = window.setInterval(() => { - setProgress((p) => { - if (p + 1 < TICKS_PER_FEATURE) return p + 1 - setActive((a) => (a + 1) % count) - return 0 - }) - }, TICK_MS) - return () => window.clearInterval(id) - }, [running, manual, count]) - - const select = useCallback((index: number) => { - setActive(index) - setProgress(TICKS_PER_FEATURE) - setManual(true) - }, []) - - return { ref, active, progress, select, manual } -} diff --git a/src/components/landing/go/useGoBenchmark.ts b/src/components/landing/go/useGoBenchmark.ts index 33a53926..468042fa 100644 --- a/src/components/landing/go/useGoBenchmark.ts +++ b/src/components/landing/go/useGoBenchmark.ts @@ -3,12 +3,16 @@ import { CONNECTORS, CONNECTOR_BENCHMARKS, CONNECTOR_CDC_BENCHMARKS, - CONNECTOR_METRIC_LABELS, TOOLS, type BenchmarkMode, type ConnectorId } from '@site/src/data/benchmarkData' -import { GO_BENCHMARK_COST_NOTE } from '@site/src/data/landing/go/benchmark' +import { + GO_BENCHMARK_CONNECTORS, + GO_BENCHMARK_COST_NOTE, + GO_BENCHMARK_METRIC_LABELS, + goBenchmarkCaption +} from '@site/src/data/landing/go/benchmark' /** Row order of the table, top to bottom. */ const METRICS = ['rowsSynced', 'elapsedTime', 'speed', 'comparison', 'cost'] as const @@ -27,21 +31,26 @@ export interface GoBenchmarkRow { note?: string olake: string competitors: string[] - /** The comparison row is tinted. */ + /** The comparison row is set in blue. */ highlight: boolean } /** - * State and table data of the OLake Go benchmark block. Unlike the home page's table, every - * connector keeps its tab: the ones without published numbers show a "coming soon" panel. + * State and table data of the OLake Go benchmark block. Like the home page's table, a source only + * gets a tab in the modes it has published numbers for (Kafka and MSSQL have full-load numbers but + * no CDC ones), and switching to a mode the selected source lacks falls back to the first source + * that has them. */ export function useGoBenchmark() { - const [activeConnector, setActiveConnector] = useState(CONNECTORS[0].id) + const [selectedConnector, setActiveConnector] = useState(GO_BENCHMARK_CONNECTORS[0]) const [mode, setMode] = useState('full_load') return useMemo(() => { const dataset = mode === 'cdc' ? CONNECTOR_CDC_BENCHMARKS : CONNECTOR_BENCHMARKS - const connector = CONNECTORS.find((c) => c.id === activeConnector) ?? CONNECTORS[0] + const all = GO_BENCHMARK_CONNECTORS.map((id) => CONNECTORS.find((c) => c.id === id)!) + const withData = all.filter((c) => dataset[c.id]?.hasData) + const connectors = withData.length ? withData : all + const connector = connectors.find((c) => c.id === selectedConnector) ?? connectors[0] const bench = dataset[connector.id] const keys = competitorKeys(bench.rowsSynced.flink !== undefined) @@ -49,27 +58,25 @@ export function useGoBenchmark() { const row = bench[metric] as unknown as Record const isComparison = metric === 'comparison' return { - label: CONNECTOR_METRIC_LABELS[metric], + label: GO_BENCHMARK_METRIC_LABELS[metric], note: metric === 'cost' ? GO_BENCHMARK_COST_NOTE : undefined, // OLake is the baseline of the comparison row, so it shows a dash instead of a multiplier. - olake: isComparison ? '–' : (row.olake ?? '-'), + olake: isComparison ? '-' : (row.olake ?? '-'), competitors: keys.map((key) => row[key] ?? '-'), highlight: isComparison } }) return { - connectors: CONNECTORS.map(({ id, name }) => ({ id, name })), + connectors: connectors.map(({ id, name }) => ({ id, name })), activeConnector: connector.id, setActiveConnector, mode, setMode, competitors: keys.map((key) => TOOLS[key].name), - olakeLabel: 'OLake Go', olakeSub: TOOLS.olake.description ?? '', - rows, - hasData: bench.hasData, - sourceName: connector.name + caption: goBenchmarkCaption(mode, bench.rowsSynced.olake, connector.name), + rows } - }, [activeConnector, mode]) + }, [selectedConnector, mode]) } diff --git a/src/components/landing/product/BenchmarkShell.tsx b/src/components/landing/product/BenchmarkShell.tsx new file mode 100644 index 00000000..d8e61918 --- /dev/null +++ b/src/components/landing/product/BenchmarkShell.tsx @@ -0,0 +1,116 @@ +import React from 'react' +import Link from '@docusaurus/Link' +import useDocusaurusContext from '@docusaurus/useDocusaurusContext' +import { cn } from '@site/src/lib/utils' +import { PiArrowSquareOut } from 'react-icons/pi' + +/** Rules on the dark section. It goes first in cn(): a later border-0 would drop the side classes. */ +export const RULE = 'border-0 border-solid border-olake-line-dark' + +/** Focus ring for controls on the dark section. */ +export const FOCUS_ON_DARK = + 'focus-visible:rounded-[4px] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-olake-blue-on-dark' + +/** The latest-release strip; the version comes from docusaurus.config.js. */ +function ReleaseStrip() { + const { siteConfig } = useDocusaurusContext() + const label = siteConfig.customFields?.latestOlakeReleaseLabel as string | undefined + const path = + (siteConfig.customFields?.latestOlakeReleasePath as string | undefined) || + '/docs/release/ingestion' + if (!label) return null + return ( +
    + Latest Release + We just released our {label} + + Read More + +
    + ) +} + +/** + * The dark, full-bleed benchmark section of a product page: a ruled column with (optionally) the + * latest-release strip, the eyebrow, title and summary, the page's comparison (`children`), the + * "how it does this" notes set in Geist Mono, and a bar linking to the detailed benchmarks. + */ +export default function BenchmarkShell({ + release, + eyebrow, + title, + summary, + how, + link, + children +}: { + /** Show the latest-release strip on top. */ + release?: boolean + eyebrow: string + title: string + summary: React.ReactNode + how: { title: string; body: React.ReactNode; className?: string } + link: { label: string; href: string } + children: React.ReactNode +}) { + return ( +
    +
    + {release && } + +
    +

    + {eyebrow} +

    +

    + {title} +

    +

    + {summary} +

    +
    + + {children} + +
    +

    {how.title}

    +
    + {how.body} +
    +
    + + + {link.label} +
    +
    + ) +} diff --git a/src/components/landing/product/Glow.tsx b/src/components/landing/product/Glow.tsx new file mode 100644 index 00000000..39755355 --- /dev/null +++ b/src/components/landing/product/Glow.tsx @@ -0,0 +1,39 @@ +import React from 'react' +import { cn } from '@site/src/lib/utils' + +/** + * A soft glow drawn in CSS rather than loaded as an image: the design's blurred ellipse layer, an + * `rx` × `ry` ellipse in `color` blurred by `blur` px (the Gaussian's standard deviation, as the + * design's layer blur). `className` places the layer's `width` × `height` frame the way the design + * does; the ellipse sits at its centre. + */ +export default function Glow({ + width, + height, + rx, + ry, + blur, + color, + className +}: { + width: number + height: number + rx: number + ry: number + blur: number + color: string + className?: string +}) { + return ( +