353 lines
12 KiB
TypeScript
353 lines
12 KiB
TypeScript
import { ActionLink } from '@/components/actions/ActionLink';
|
|
import { Accordion } from '@/components/controls/Accordion';
|
|
import { Icon } from '@/components/foundation/Icon';
|
|
import { DemoTrigger } from '@/components/integrations/DemoTrigger';
|
|
import { Container, Section, Stack } from '@/components/layout/LayoutPrimitives';
|
|
import { CTA } from '@/components/marketing/CTA';
|
|
import { Comparison } from '@/components/marketing/Comparison';
|
|
import { FeatureCard } from '@/components/marketing/FeatureCard';
|
|
import { ProductPreview } from '@/components/marketing/ProductPreview';
|
|
import { SectionHeader } from '@/components/marketing/SectionHeader';
|
|
import { FeatureGrid } from '@/components/marketing/SectionPatterns';
|
|
import { Workflow } from '@/components/marketing/Workflow';
|
|
import { Callout } from '@/components/surfaces/Callout';
|
|
import { Card } from '@/components/surfaces/Card';
|
|
import { Eyebrow, Heading, Text } from '@/components/typography/Typography';
|
|
import { buildHomepageContent } from '@/content/homepage-model';
|
|
import { getPublicIntegrationConfig } from '@/lib/integrations/environment';
|
|
import { isLocale, localizedPath, type Locale } from '@/lib/localization/config';
|
|
import { getMessages } from '@/lib/localization/messages';
|
|
import { buildLocalizedMetadata } from '@/lib/metadata/build-metadata';
|
|
import type { Metadata } from 'next';
|
|
import { notFound } from 'next/navigation';
|
|
import styles from './page.module.css';
|
|
|
|
interface LocalePageProps {
|
|
params: Promise<{ locale: string }>;
|
|
}
|
|
|
|
export async function generateMetadata({ params }: LocalePageProps): Promise<Metadata> {
|
|
const { locale: localeValue } = await params;
|
|
if (!isLocale(localeValue)) notFound();
|
|
const messages = getMessages(localeValue);
|
|
return buildLocalizedMetadata({
|
|
locale: localeValue,
|
|
routeId: 'home',
|
|
title: messages.homepage.meta.title,
|
|
description: messages.homepage.meta.description,
|
|
indexable: true,
|
|
});
|
|
}
|
|
|
|
export default async function LocaleHomePage({ params }: LocalePageProps) {
|
|
const { locale: localeValue } = await params;
|
|
if (!isLocale(localeValue)) notFound();
|
|
const locale: Locale = localeValue;
|
|
const messages = getMessages(locale);
|
|
const content = buildHomepageContent(messages.homepage, messages.shell);
|
|
const integrationConfig = getPublicIntegrationConfig();
|
|
const homePath = localizedPath('home', locale);
|
|
|
|
return (
|
|
<main id="main-content" className={styles.main} tabIndex={-1}>
|
|
<Section id="product" className={styles.hero} aria-label={content.hero.title}>
|
|
<Container>
|
|
<div className={styles.heroGrid}>
|
|
<div className={styles.heroCopy}>
|
|
<Eyebrow>{content.hero.eyebrow}</Eyebrow>
|
|
<Heading level={1} appearance="display">
|
|
{content.hero.title}
|
|
</Heading>
|
|
<Text variant="lead">{content.hero.body}</Text>
|
|
<div className={styles.heroActions} role="group" aria-label={content.hero.eyebrow}>
|
|
<DemoTrigger
|
|
label={content.hero.primary.label}
|
|
source="hero"
|
|
size="large"
|
|
disabledReason={
|
|
integrationConfig.demoSubmissionEnabled
|
|
? undefined
|
|
: content.hero.primary.disabledReason
|
|
}
|
|
/>
|
|
<ActionLink
|
|
href={homePath}
|
|
variant="button-primary"
|
|
disabledReason={content.hero.secondary.disabledReason}
|
|
>
|
|
{content.hero.secondary.label}
|
|
</ActionLink>
|
|
</div>
|
|
</div>
|
|
<div className={styles.previewStage}>
|
|
<ProductPreview {...content.hero.preview} headingLevel={2} />
|
|
</div>
|
|
</div>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section className={styles.sectionBorder} aria-label={content.trust.title}>
|
|
<Container>
|
|
<Stack gap="large">
|
|
<SectionHeader
|
|
eyebrow={content.trust.eyebrow}
|
|
title={content.trust.title}
|
|
body={content.trust.body}
|
|
alignment="center"
|
|
/>
|
|
<FeatureGrid>
|
|
{content.trust.items.map((item) => (
|
|
<FeatureCard
|
|
key={item.id}
|
|
title={item.title}
|
|
description={item.description}
|
|
{...(item.icon ? { icon: item.icon } : {})}
|
|
emphasis="muted"
|
|
/>
|
|
))}
|
|
</FeatureGrid>
|
|
</Stack>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section tone="muted" aria-label={content.comparison.title}>
|
|
<Container>
|
|
<Stack gap="large">
|
|
<SectionHeader
|
|
eyebrow={content.comparison.eyebrow}
|
|
title={content.comparison.title}
|
|
body={content.comparison.body}
|
|
/>
|
|
<Comparison
|
|
columns={[
|
|
{
|
|
title: content.comparison.beforeTitle,
|
|
items: content.comparison.beforeItems,
|
|
tone: 'problem',
|
|
},
|
|
{
|
|
title: content.comparison.afterTitle,
|
|
items: content.comparison.afterItems,
|
|
tone: 'solution',
|
|
},
|
|
]}
|
|
/>
|
|
</Stack>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section id="workflow" aria-label={content.workflow.title}>
|
|
<Container>
|
|
<Stack gap="large">
|
|
<SectionHeader
|
|
eyebrow={content.workflow.eyebrow}
|
|
title={content.workflow.title}
|
|
body={content.workflow.body}
|
|
/>
|
|
<Workflow items={content.workflow.steps} label={content.workflow.title} />
|
|
</Stack>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section tone="strong" aria-label={content.roles.title}>
|
|
<Container>
|
|
<Stack gap="large">
|
|
<SectionHeader
|
|
eyebrow={content.roles.eyebrow}
|
|
title={content.roles.title}
|
|
alignment="center"
|
|
/>
|
|
<FeatureGrid>
|
|
{content.roles.items.map((item) => (
|
|
<FeatureCard
|
|
key={item.id}
|
|
title={item.title}
|
|
description={item.description}
|
|
{...(item.icon ? { icon: item.icon } : {})}
|
|
emphasis="primary"
|
|
/>
|
|
))}
|
|
</FeatureGrid>
|
|
</Stack>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section id="modules" aria-label={content.modules.title}>
|
|
<Container>
|
|
<Stack gap="large">
|
|
<SectionHeader
|
|
eyebrow={content.modules.eyebrow}
|
|
title={content.modules.title}
|
|
body={content.modules.body}
|
|
/>
|
|
<FeatureGrid>
|
|
{content.modules.items.map((item) => (
|
|
<FeatureCard
|
|
key={item.id}
|
|
title={item.title}
|
|
description={item.description}
|
|
{...(item.icon ? { icon: item.icon } : {})}
|
|
/>
|
|
))}
|
|
</FeatureGrid>
|
|
</Stack>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section tone="muted" aria-label={content.results.title}>
|
|
<Container>
|
|
<Stack gap="large">
|
|
<SectionHeader
|
|
eyebrow={content.results.eyebrow}
|
|
title={content.results.title}
|
|
body={content.results.body}
|
|
/>
|
|
<FeatureGrid>
|
|
{content.results.metrics.map((metric) => (
|
|
<FeatureCard
|
|
key={metric.id}
|
|
title={metric.title}
|
|
description={metric.description}
|
|
{...(metric.icon ? { icon: metric.icon } : {})}
|
|
emphasis="muted"
|
|
/>
|
|
))}
|
|
</FeatureGrid>
|
|
<Callout icon="info" title={content.results.methodTitle}>
|
|
<ol className={styles.methodList}>
|
|
{content.results.method.map((item) => (
|
|
<li key={item}>{item}</li>
|
|
))}
|
|
</ol>
|
|
</Callout>
|
|
</Stack>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section aria-label={content.integrations.title}>
|
|
<Container>
|
|
<div className={styles.splitSection}>
|
|
<SectionHeader
|
|
eyebrow={content.integrations.eyebrow}
|
|
title={content.integrations.title}
|
|
body={content.integrations.body}
|
|
/>
|
|
<Card as="div" tone="elevated" padding="spacious">
|
|
<ul className={styles.capabilityList}>
|
|
{content.integrations.items.map((item) => (
|
|
<li key={item}>
|
|
<Icon name="plus" size="sm" />
|
|
<span>{item}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</Card>
|
|
</div>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section tone="strong" aria-label={content.security.title}>
|
|
<Container>
|
|
<Stack gap="large">
|
|
<SectionHeader
|
|
eyebrow={content.security.eyebrow}
|
|
title={content.security.title}
|
|
body={content.security.body}
|
|
/>
|
|
<FeatureGrid>
|
|
{content.security.checks.map((check) => (
|
|
<FeatureCard
|
|
key={check.id}
|
|
title={check.title}
|
|
description={check.description}
|
|
{...(check.icon ? { icon: check.icon } : {})}
|
|
emphasis="primary"
|
|
/>
|
|
))}
|
|
</FeatureGrid>
|
|
</Stack>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section id="pricing" aria-label={content.pricing.title}>
|
|
<Container>
|
|
<div className={styles.pricingGrid}>
|
|
<div className={styles.pricingCopy}>
|
|
<SectionHeader
|
|
eyebrow={content.pricing.eyebrow}
|
|
title={content.pricing.title}
|
|
body={content.pricing.body}
|
|
/>
|
|
<ul className={styles.factorList}>
|
|
{content.pricing.factors.map((factor) => (
|
|
<li key={factor}>
|
|
<Icon name="check" size="sm" />
|
|
<span>{factor}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
<aside>
|
|
<Card as="div" tone="elevated" padding="spacious" className={styles.pricingCard}>
|
|
<Icon name="info" size="lg" />
|
|
<Text variant="supporting">{content.pricing.note}</Text>
|
|
<ActionLink
|
|
href={homePath}
|
|
variant="button-conversion"
|
|
disabledReason={content.pricing.action.disabledReason}
|
|
>
|
|
{content.pricing.action.label}
|
|
</ActionLink>
|
|
</Card>
|
|
</aside>
|
|
</div>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section id="faq" tone="muted" aria-label={content.faq.title}>
|
|
<Container width="content">
|
|
<Stack gap="large">
|
|
<SectionHeader
|
|
eyebrow={content.faq.eyebrow}
|
|
title={content.faq.title}
|
|
alignment="center"
|
|
/>
|
|
<Accordion
|
|
items={content.faq.items.map((item, index) => ({
|
|
id: item.id,
|
|
title: item.title,
|
|
content: <Text variant="supporting">{item.body}</Text>,
|
|
open: index === 0,
|
|
}))}
|
|
/>
|
|
</Stack>
|
|
</Container>
|
|
</Section>
|
|
|
|
<Section className={styles.finalSection} aria-label={content.final.title}>
|
|
<Container>
|
|
<CTA
|
|
eyebrow={content.final.eyebrow}
|
|
title={content.final.title}
|
|
body={content.final.body}
|
|
primary={content.final.primary}
|
|
secondary={content.final.secondary}
|
|
primaryNode={
|
|
<DemoTrigger
|
|
label={content.final.primary.label}
|
|
source="final-cta"
|
|
size="large"
|
|
disabledReason={
|
|
integrationConfig.demoSubmissionEnabled
|
|
? undefined
|
|
: content.final.primary.disabledReason
|
|
}
|
|
/>
|
|
}
|
|
/>
|
|
</Container>
|
|
</Section>
|
|
</main>
|
|
);
|
|
}
|