[Website] i18n module, page-local sections, translatable copy (#21082)
**i18n** — collapsed the ~22 scattered i18n files into a single module and turned on Spanish alongside French. **Sections** — dropped the old compound pattern (`Section.Root`, `Section.Heading`, …). Reusable layout shells moved to `src/templates/`, atomic bits stay in `design-system/`, and each page now owns its copy in local `_components` blocks instead of pulling it out of shared sections. Data files hold arrays only, no prose. **Copy → `<Trans>`** — A lot of headings were split across several `<HeadingPart>`s just for font styling, which meant each piece was a separate translation string. A translator got "Build your Enterprise CRM" and "at AI Speed" as two unrelated strings and had no way to reorder them for their language. Those are now single `<Trans>` units with placeholders. Same idea for the old `\n` + `white-space: pre-line` line-break trick: replaced with a small `ResponsiveLineBreak` element so the break is doesn't quietly rot, and did a dead-code pass. The de-fragmentation changes the message IDs, so around 60 strings will fall back to English in fr/es until Crowdin re-syncs.
This commit is contained in:
@@ -40,6 +40,7 @@ const jestConfig = {
|
||||
],
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^server-only$': '<rootDir>/jest/server-only-stub.ts',
|
||||
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, {
|
||||
prefix: '<rootDir>/',
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// jest stub for the `server-only` package. `server-only` is a Next.js build
|
||||
// guard (errors if a server module is pulled into a client bundle); it has no
|
||||
// behaviour at runtime and no resolution under the jest node environment, so we
|
||||
// map it to this empty module. The real guard still runs during `next build`.
|
||||
export {};
|
||||
@@ -2,7 +2,7 @@ import { defineConfig } from '@lingui/conf';
|
||||
import { formatter } from '@lingui/format-po';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { WEBSITE_LOCALE_LIST } from './src/lib/i18n/utils/website-locale-list';
|
||||
import { WEBSITE_LOCALE_LIST } from './src/lib/i18n/locales';
|
||||
|
||||
export default defineConfig({
|
||||
sourceLocale: SOURCE_LOCALE,
|
||||
|
||||
@@ -3,9 +3,17 @@ import { initOpenNextCloudflareForDev } from '@opennextjs/cloudflare';
|
||||
import withLinaria, { type LinariaConfig } from 'next-with-linaria';
|
||||
import { APP_LOCALES } from 'twenty-shared/translations';
|
||||
|
||||
// Locale URL segments that are actually served (others are normalised away).
|
||||
// Mirrors LOCALE_BY_URL_SEGMENT keys in src/lib/i18n/utils/website-locale-segments.ts.
|
||||
const DEPLOYED_LOCALE_URL_SEGMENTS = ['en', 'fr'] as const;
|
||||
import {
|
||||
WEBSITE_LOCALE_LIST,
|
||||
localeToUrlSegment,
|
||||
} from './src/lib/i18n/locales';
|
||||
|
||||
// Locale URL segments that are actually served (others are normalised away),
|
||||
// derived from the single WEBSITE_LOCALE_LIST source of truth so adding a
|
||||
// locale never requires editing this config. These are exactly the keys of
|
||||
// LOCALE_BY_URL_SEGMENT in src/lib/i18n/locales.ts.
|
||||
const DEPLOYED_LOCALE_URL_SEGMENTS =
|
||||
WEBSITE_LOCALE_LIST.map(localeToUrlSegment);
|
||||
|
||||
// Raw locale codes (e.g. fr-FR, de-DE) that should redirect to the un-prefixed
|
||||
// path. Excludes pseudo-* locales and the deployed URL segments themselves.
|
||||
|
||||
@@ -52,6 +52,36 @@ const RULES = [
|
||||
'element visibility, cleanup, and render failures are handled consistently.',
|
||||
].join('\n '),
|
||||
},
|
||||
{
|
||||
id: 'design-system-no-upward-import',
|
||||
description:
|
||||
'design-system is the lowest UI layer — it must not import from @/sections, @/templates, or @/app.',
|
||||
pattern: /from\s+['"]@\/(sections|templates|app)(\/|['"])/,
|
||||
appliesTo: (rel) =>
|
||||
rel.startsWith('src/design-system/') && /\.(ts|tsx)$/.test(rel),
|
||||
exempt: (rel) => rel.includes('__tests__') || rel.endsWith('.d.ts'),
|
||||
help: [
|
||||
'design-system holds atomic, domain-agnostic primitives. It may depend on',
|
||||
'@/theme, @/icons and @/lib, but never on the higher layers (sections,',
|
||||
'templates, app). If a primitive needs section/page data, lift that data up',
|
||||
'to the consumer and pass it in as a prop.',
|
||||
].join('\n '),
|
||||
},
|
||||
{
|
||||
id: 'templates-no-upward-import',
|
||||
description:
|
||||
'templates (section layout shells) must not import from @/sections or @/app — only @/design-system and @/theme.',
|
||||
pattern: /from\s+['"]@\/(sections|app)(\/|['"])/,
|
||||
appliesTo: (rel) =>
|
||||
rel.startsWith('src/templates/') && /\.(ts|tsx)$/.test(rel),
|
||||
exempt: (rel) => rel.includes('__tests__') || rel.endsWith('.d.ts'),
|
||||
help: [
|
||||
'templates are reusable section layout shells composed from design-system',
|
||||
'primitives. They must not reach into sections (visuals) or app (pages).',
|
||||
'Page-local blocks compose a template with its section visual — inject the',
|
||||
'visual as children/a prop rather than importing it into the template.',
|
||||
].join('\n '),
|
||||
},
|
||||
];
|
||||
|
||||
const SKIP_DIRS = new Set([
|
||||
|
||||
@@ -7,8 +7,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(__dirname, '..');
|
||||
const SECTIONS_DIR = path.join(ROOT, 'src', 'sections');
|
||||
|
||||
const SECTIONS_USING_NAMED_SLOTS = new Map([]);
|
||||
|
||||
const LEAF_SECTIONS = new Set([
|
||||
'CaseStudy',
|
||||
'CaseStudyCatalog',
|
||||
@@ -16,6 +14,11 @@ const LEAF_SECTIONS = new Set([
|
||||
'LegalDocument',
|
||||
'PartnerApplication',
|
||||
'Stepper',
|
||||
// Visual-only modules: their layout shells moved to src/templates/, so they
|
||||
// no longer own a section <Root> — they just expose visual components.
|
||||
'Hero',
|
||||
'ThreeCards',
|
||||
'Testimonials',
|
||||
]);
|
||||
|
||||
async function listSections() {
|
||||
@@ -59,9 +62,14 @@ async function findBarrel(sectionDir) {
|
||||
async function findRoot(sectionDir) {
|
||||
const sectionName = path.basename(sectionDir);
|
||||
const candidates = [
|
||||
// Legacy compound sections own the <section> in components/Root.tsx.
|
||||
path.join(sectionDir, 'components', 'Root.tsx'),
|
||||
// Single-file sections own it in <Section>.tsx (e.g. TrustedBy/TrustedBy.tsx).
|
||||
path.join(sectionDir, `${sectionName}.tsx`),
|
||||
path.join(sectionDir, `${sectionName}.ts`),
|
||||
// Flat-primitive sections own it in a <Section>Section shell
|
||||
// (e.g. Hero/components/HeroSection.tsx) consumed by page-local blocks.
|
||||
path.join(sectionDir, 'components', `${sectionName}Section.tsx`),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (await fileExists(candidate)) return candidate;
|
||||
@@ -69,22 +77,6 @@ async function findRoot(sectionDir) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseSlotIdentifiers(barrelContents) {
|
||||
const exportMatch = barrelContents.match(
|
||||
/export\s+const\s+\w+\s*=\s*\{([^}]+)\}/m,
|
||||
);
|
||||
if (!exportMatch) return null;
|
||||
const body = exportMatch[1];
|
||||
return body
|
||||
.split(',')
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => entry.length > 0)
|
||||
.map((entry) => {
|
||||
const colon = entry.indexOf(':');
|
||||
return colon === -1 ? entry : entry.slice(0, colon).trim();
|
||||
});
|
||||
}
|
||||
|
||||
const TOARRAY_REGEX = /Children\.toArray\s*\(/;
|
||||
|
||||
function stripComments(source) {
|
||||
@@ -100,7 +92,7 @@ async function checkSection(name) {
|
||||
const barrel = await findBarrel(sectionDir);
|
||||
if (barrel === null) {
|
||||
violations.push(
|
||||
`${name}: missing components/index.{ts,tsx} barrel — every section must expose a single compound export.`,
|
||||
`${name}: missing a barrel (index.{ts,tsx}) — every section must expose its public API through one barrel (flat named exports; no compound objects).`,
|
||||
);
|
||||
return violations;
|
||||
}
|
||||
@@ -124,57 +116,9 @@ async function checkSection(name) {
|
||||
}
|
||||
}
|
||||
|
||||
const slotsToCheck = SECTIONS_USING_NAMED_SLOTS.get(name);
|
||||
if (slotsToCheck !== undefined) {
|
||||
const barrelContents = await readFileOrNull(barrel);
|
||||
const exportedSlotNames = barrelContents
|
||||
? parseSlotIdentifiers(barrelContents)
|
||||
: null;
|
||||
for (const slot of slotsToCheck) {
|
||||
if (exportedSlotNames !== null && !exportedSlotNames.includes(slot)) {
|
||||
violations.push(
|
||||
`${name}: slot "${slot}" is declared in SECTIONS_USING_NAMED_SLOTS but is not exported from ${path.relative(
|
||||
ROOT,
|
||||
barrel,
|
||||
)}. Either export it or remove the entry from check-section-shape.mjs.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const expected = `${name}.${slot}`;
|
||||
const slotFile = await locateSlotFile(sectionDir, slot);
|
||||
if (slotFile === null) {
|
||||
violations.push(
|
||||
`${name}: slot "${slot}" is declared in SECTIONS_USING_NAMED_SLOTS but no source file matches the conventional path (components/${slot}.tsx or components/${slot}/${slot}.tsx).`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const contents = await readFileOrNull(slotFile);
|
||||
if (contents === null) continue;
|
||||
if (!contents.includes(`displayName = '${expected}'`)) {
|
||||
violations.push(
|
||||
`${name}: slot "${slot}" source (${path.relative(
|
||||
ROOT,
|
||||
slotFile,
|
||||
)}) does not set ${slot}.displayName = '${expected}'. Root looks slots up by displayName; without it the slot silently fails to render.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return violations;
|
||||
}
|
||||
|
||||
async function locateSlotFile(sectionDir, slot) {
|
||||
const candidates = [
|
||||
path.join(sectionDir, 'components', `${slot}.tsx`),
|
||||
path.join(sectionDir, 'components', slot, `${slot}.tsx`),
|
||||
];
|
||||
for (const candidate of candidates) {
|
||||
if (await fileExists(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const sections = await listSections();
|
||||
const allViolations = [];
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { FEATURE_CARDS } from '@/app/[locale]/(home)/three-cards-feature.data';
|
||||
import { Eyebrow, Heading, HeadingPart } from '@/design-system/components';
|
||||
import { FeatureCards } from '@/sections/ThreeCards';
|
||||
import { ThreeCardsIntro, ThreeCardsSection } from '@/templates/ThreeCards';
|
||||
|
||||
export function HomeFeatureThreeCards() {
|
||||
return (
|
||||
<ThreeCardsSection scheme="light">
|
||||
<ThreeCardsIntro align="center" maxWidthMd={900}>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Skip the clunky UX that always comes with custom.</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
Make your GTM team happy
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">with</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">a CRM they'll love</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
</ThreeCardsIntro>
|
||||
<FeatureCards featureCards={FEATURE_CARDS} />
|
||||
</ThreeCardsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data';
|
||||
import { HeadingPart, LinkButton } from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { WebGlMount } from '@/lib/visual-runtime';
|
||||
import { AppPreview } from '@/sections/AppPreview';
|
||||
import { TalkToUsButton } from '@/sections/ContactCal';
|
||||
import { HeroBody, HeroCta, HeroHeading, HeroSection } from '@/templates/Hero';
|
||||
import { HomeBackgroundHalftone } from '@/sections/Hero/visuals/components/HomeBackgroundHalftone';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
const HeroHeadingGroup = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(3)};
|
||||
width: 100%;
|
||||
|
||||
> *:last-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeroIntroGroup = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(8)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function HomeHero() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<HeroSection
|
||||
scheme="muted"
|
||||
background={
|
||||
<WebGlMount priority>
|
||||
<HomeBackgroundHalftone />
|
||||
</WebGlMount>
|
||||
}
|
||||
>
|
||||
<HeroIntroGroup data-halftone-exclude>
|
||||
<HeroHeadingGroup>
|
||||
<HeroHeading>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
Build your Enterprise CRM
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">at AI Speed</HeadingPart>
|
||||
</Trans>
|
||||
</HeroHeading>
|
||||
<HeroBody maxWidthMd={591} size="sm">
|
||||
<Trans>
|
||||
Twenty gives technical teams the building blocks for a custom CRM
|
||||
that meets complex business needs and quickly adapts as the
|
||||
business evolves.
|
||||
</Trans>
|
||||
</HeroBody>
|
||||
</HeroHeadingGroup>
|
||||
<HeroCta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="secondary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</HeroCta>
|
||||
</HeroIntroGroup>
|
||||
<AppPreview visual={APP_PREVIEW_DATA.visual} />
|
||||
</HeroSection>
|
||||
);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { ILLUSTRATION_CARDS } from '@/app/[locale]/(home)/three-cards-illustration.data';
|
||||
import {
|
||||
Body,
|
||||
Eyebrow,
|
||||
Heading,
|
||||
HeadingPart,
|
||||
} from '@/design-system/components';
|
||||
import { IllustrationCards } from '@/sections/ThreeCards';
|
||||
import { ThreeCardsIntro, ThreeCardsSection } from '@/templates/ThreeCards';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
const IntroContent = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: ${theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const IntroHeader = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: ${theme.spacing(6)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const headingClassName = css`
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
max-width: ${theme.layout.editorial};
|
||||
}
|
||||
|
||||
[data-family='sans'] {
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
`;
|
||||
|
||||
const bodyClassName = css`
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
max-width: 571px;
|
||||
}
|
||||
`;
|
||||
|
||||
export function HomeIllustrationThreeCards() {
|
||||
return (
|
||||
<ThreeCardsSection scheme="light">
|
||||
<ThreeCardsIntro align="left">
|
||||
<IntroContent>
|
||||
<IntroHeader>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Stop settling for trade-offs.</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading className={headingClassName} size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
Assemble, iterate and adapt a robust CRM,
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
that's quick to flex
|
||||
</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
</IntroHeader>
|
||||
<Body className={bodyClassName} size="sm">
|
||||
<Trans>
|
||||
Compose your CRM and internal apps with a single extensibility
|
||||
toolkit.
|
||||
</Trans>
|
||||
</Body>
|
||||
</IntroContent>
|
||||
</ThreeCardsIntro>
|
||||
<IllustrationCards illustrationCards={ILLUSTRATION_CARDS} />
|
||||
</ThreeCardsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { Eyebrow, HeadingPart } from '@/design-system/components';
|
||||
import {
|
||||
ProblemContent,
|
||||
ProblemHeading,
|
||||
ProblemPoints,
|
||||
ProblemSection,
|
||||
ProblemVisual,
|
||||
type ProblemPointType,
|
||||
} from '@/sections/Problem';
|
||||
|
||||
export function HomeProblem() {
|
||||
const points: ProblemPointType[] = [
|
||||
{
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>The Giant Monolith</Trans>
|
||||
</HeadingPart>
|
||||
),
|
||||
body: msg`Proprietary languages, slow deployment cycles, and "black box" logic.`,
|
||||
},
|
||||
{
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>The In-house Burden</Trans>
|
||||
</HeadingPart>
|
||||
),
|
||||
body: msg`It's fragile. V1 ships quickly, but maintaining and making changes is a long term burden.`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<ProblemSection>
|
||||
<ProblemVisual />
|
||||
<ProblemContent>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>The Problem.</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<ProblemHeading>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
A custom CRM gives your org an edge,
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">but building one</HeadingPart>
|
||||
<HeadingPart fontFamily="serif">comes with</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">tradeoffs</HeadingPart>
|
||||
</Trans>
|
||||
</ProblemHeading>
|
||||
<ProblemPoints points={points} />
|
||||
</ProblemContent>
|
||||
</ProblemSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { Heading, HeadingPart } from '@/design-system/components';
|
||||
import {
|
||||
HomeStepperScrollSection,
|
||||
type HomeStepperStepType,
|
||||
} from '@/sections/HomeStepper';
|
||||
|
||||
export function HomeStepperSection() {
|
||||
const steps: HomeStepperStepType[] = [
|
||||
{
|
||||
heading: (
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
Begin with production-grade
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">building blocks</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
),
|
||||
body: msg`Compose your CRM and internal apps with a single extensibility toolkit. Data model, layout, and automation.`,
|
||||
},
|
||||
{
|
||||
heading: (
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Continue iteration</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">without friction</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
),
|
||||
body: msg`Enjoy unlimited customization using the AI coding tools you already love. Adapt your CRM to fit the way your business grows and wins.`,
|
||||
},
|
||||
{
|
||||
heading: (
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
Stay in control with our
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">open-source software</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
),
|
||||
body: msg`Don't get locked into someone else's ecosystem. Twenty's developer experience looks like normal software, with local setup, real data, live testing, and no proprietary tooling.`,
|
||||
},
|
||||
];
|
||||
|
||||
return <HomeStepperScrollSection steps={steps} />;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { HOME_TESTIMONIALS } from '@/app/[locale]/(home)/testimonials.data';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import {
|
||||
TestimonialsCarousel,
|
||||
TestimonialsHourglassVisual,
|
||||
} from '@/sections/Testimonials';
|
||||
import { TestimonialsSection } from '@/templates/Testimonials';
|
||||
|
||||
export function HomeTestimonials() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<TestimonialsSection scheme="muted">
|
||||
<TestimonialsCarousel
|
||||
eyebrow={i18n._(msg`They are the real sales`)}
|
||||
testimonials={HOME_TESTIMONIALS}
|
||||
>
|
||||
<TestimonialsHourglassVisual />
|
||||
</TestimonialsCarousel>
|
||||
</TestimonialsSection>
|
||||
);
|
||||
}
|
||||
@@ -1,96 +1,23 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { HELPED_CARDS } from '@/app/[locale]/(home)/helped.data';
|
||||
import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data';
|
||||
import { Problem, type ProblemPointType } from '@/sections/Problem';
|
||||
import { HOME_TESTIMONIALS } from '@/app/[locale]/(home)/testimonials.data';
|
||||
import { FEATURE_CARDS } from '@/app/[locale]/(home)/three-cards-feature.data';
|
||||
import { ILLUSTRATION_CARDS } from '@/app/[locale]/(home)/three-cards-illustration.data';
|
||||
import { TalkToUsButton } from '@/sections/ContactCal';
|
||||
import { HomeProblem } from '@/app/[locale]/(home)/_components/HomeProblem';
|
||||
import { Faq, FAQ_QUESTIONS } from '@/sections/Faq';
|
||||
import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy';
|
||||
import {
|
||||
Body,
|
||||
Eyebrow,
|
||||
Heading,
|
||||
HeadingPart,
|
||||
LinkButton,
|
||||
} from '@/design-system/components';
|
||||
import { TrustedBy } from '@/sections/TrustedBy';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { Helped } from '@/sections/Helped';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { HomeStepper, type HomeStepperStepType } from '@/sections/HomeStepper';
|
||||
import { HomeHero } from '@/app/[locale]/(home)/_components/HomeHero';
|
||||
import { HomeStepperSection } from '@/app/[locale]/(home)/_components/HomeStepperSection';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { Testimonials } from '@/sections/Testimonials';
|
||||
import { ThreeCards } from '@/sections/ThreeCards';
|
||||
import { HomeTestimonials } from '@/app/[locale]/(home)/_components/HomeTestimonials';
|
||||
import { HomeFeatureThreeCards } from '@/app/[locale]/(home)/_components/HomeFeatureThreeCards';
|
||||
import { HomeIllustrationThreeCards } from '@/app/[locale]/(home)/_components/HomeIllustrationThreeCards';
|
||||
import { buildFaqPageJsonLd, buildRouteMetadata, JsonLd } from '@/lib/seo';
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
export const generateMetadata = buildRouteMetadata('home');
|
||||
|
||||
const HOME_TOP_BACKGROUND_COLOR = '#F4F4F4';
|
||||
|
||||
const HeroHeadingGroup = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(3)};
|
||||
width: 100%;
|
||||
|
||||
> *:last-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const HeroIntroGroup = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${theme.spacing(8)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ThreeCardsIllustrationIntroContent = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: ${theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const ThreeCardsIllustrationIntroHeader = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
row-gap: ${theme.spacing(6)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const threeCardsIllustrationHeadingClassName = css`
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
max-width: ${theme.layout.editorial};
|
||||
}
|
||||
|
||||
[data-family='sans'] {
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
`;
|
||||
|
||||
const threeCardsIllustrationBodyClassName = css`
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
max-width: 571px;
|
||||
}
|
||||
`;
|
||||
|
||||
type HomePageProps = {
|
||||
params: Promise<LocaleRouteParams>;
|
||||
};
|
||||
@@ -101,66 +28,6 @@ export default async function HomePage({ params }: HomePageProps) {
|
||||
fetchCommunityStats(),
|
||||
]);
|
||||
|
||||
const PROBLEM_POINTS: ProblemPointType[] = [
|
||||
{
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`The Giant Monolith`)}
|
||||
</HeadingPart>
|
||||
),
|
||||
body: msg`Proprietary languages, slow deployment cycles, and "black box" logic.`,
|
||||
},
|
||||
{
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`The In-house Burden`)}
|
||||
</HeadingPart>
|
||||
),
|
||||
body: msg`It's fragile. V1 ships quickly, but maintaining and making changes is a long term burden.`,
|
||||
},
|
||||
];
|
||||
|
||||
const HOME_STEPPER_STEPS: HomeStepperStepType[] = [
|
||||
{
|
||||
heading: (
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Begin with production-grade`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`building blocks`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
body: msg`Compose your CRM and internal apps with a single extensibility toolkit. Data model, layout, and automation.`,
|
||||
},
|
||||
{
|
||||
heading: (
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Continue iteration`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`without friction`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
body: msg`Enjoy unlimited customization using the AI coding tools you already love. Adapt your CRM to fit the way your business grows and wins.`,
|
||||
},
|
||||
{
|
||||
heading: (
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Stay in control with our`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`open-source software`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
body: msg`Don't get locked into someone else's ecosystem. Twenty's developer experience looks like normal software, with local setup, real data, live testing, and no proprietary tooling.`,
|
||||
},
|
||||
];
|
||||
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
|
||||
|
||||
return (
|
||||
@@ -192,186 +59,28 @@ export default async function HomePage({ params }: HomePageProps) {
|
||||
href="/illustrations/home/helped/money.glb"
|
||||
as="fetch"
|
||||
/>
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={HOME_TOP_BACKGROUND_COLOR}
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
|
||||
<Hero.Root scheme="muted" showHomeBackground>
|
||||
<HeroIntroGroup data-halftone-exclude>
|
||||
<HeroHeadingGroup>
|
||||
<Hero.Heading page={Pages.Home}>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Build your Enterprise CRM`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`at\u00A0AI\u00A0Speed`)}
|
||||
</HeadingPart>
|
||||
</Hero.Heading>
|
||||
<Hero.Body page={Pages.Home} size="sm">
|
||||
{i18n._(
|
||||
msg`Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves.`,
|
||||
)}
|
||||
</Hero.Body>
|
||||
</HeroHeadingGroup>
|
||||
<Hero.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="secondary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
</HeroIntroGroup>
|
||||
<Hero.AppPreview visual={APP_PREVIEW_DATA.visual} />
|
||||
</Hero.Root>
|
||||
|
||||
<TrustedBy.Root
|
||||
separator={i18n._(msg`trusted by`)}
|
||||
logos={TRUSTED_BY_LOGOS}
|
||||
clientCount={i18n._(msg`+10k others`)}
|
||||
/>
|
||||
|
||||
<Problem.Root>
|
||||
<Problem.Visual />
|
||||
<Problem.Content>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`The Problem.`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Problem.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`A custom CRM gives your org an edge,`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`but building one`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`comes with`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`tradeoffs`)}
|
||||
</HeadingPart>
|
||||
</Problem.Heading>
|
||||
<Problem.Points points={PROBLEM_POINTS} />
|
||||
</Problem.Content>
|
||||
</Problem.Root>
|
||||
<HomeHero />
|
||||
|
||||
<ThreeCards.Root scheme="light">
|
||||
<ThreeCards.Intro page={Pages.Home} align="left">
|
||||
<ThreeCardsIllustrationIntroContent>
|
||||
<ThreeCardsIllustrationIntroHeader>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Stop settling for trade-offs.`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading
|
||||
className={threeCardsIllustrationHeadingClassName}
|
||||
size="lg"
|
||||
weight="light"
|
||||
>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Assemble, iterate and adapt a robust CRM,`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`that's quick to flex`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
</ThreeCardsIllustrationIntroHeader>
|
||||
<Body className={threeCardsIllustrationBodyClassName} size="sm">
|
||||
{i18n._(
|
||||
msg`Compose your CRM and internal apps with a single extensibility toolkit.`,
|
||||
)}
|
||||
</Body>
|
||||
</ThreeCardsIllustrationIntroContent>
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards illustrationCards={ILLUSTRATION_CARDS} />
|
||||
</ThreeCards.Root>
|
||||
<TrustedBy />
|
||||
|
||||
<HomeStepper.ScrollSection steps={HOME_STEPPER_STEPS} />
|
||||
<HomeProblem />
|
||||
|
||||
<ThreeCards.Root scheme="light">
|
||||
<ThreeCards.Intro page={Pages.Home} align="center">
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Skip the clunky UX that always comes with custom.`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Make your GTM team happy`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`with`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`a CRM they'll love`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.FeatureCards featureCards={FEATURE_CARDS} />
|
||||
</ThreeCards.Root>
|
||||
<HomeIllustrationThreeCards />
|
||||
|
||||
<Helped.Root scheme="muted">
|
||||
<Helped.Scene cards={HELPED_CARDS} />
|
||||
</Helped.Root>
|
||||
<HomeStepperSection />
|
||||
|
||||
<Testimonials.Root scheme="muted">
|
||||
<Testimonials.Carousel
|
||||
eyebrow={i18n._(msg`They are the real sales`)}
|
||||
testimonials={HOME_TESTIMONIALS}
|
||||
>
|
||||
<Testimonials.HourglassVisual />
|
||||
</Testimonials.Carousel>
|
||||
</Testimonials.Root>
|
||||
<HomeFeatureThreeCards />
|
||||
|
||||
<Faq.Root>
|
||||
<Faq.Intro>
|
||||
<Eyebrow colorScheme="secondary">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Any Questions?`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Faq.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Stop fighting custom.`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Start building, with Twenty`)}
|
||||
</HeadingPart>
|
||||
</Faq.Heading>
|
||||
<Faq.Cta>
|
||||
<LinkButton
|
||||
color="primary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="primary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Faq.Cta>
|
||||
</Faq.Intro>
|
||||
<Faq.Items questions={FAQ_QUESTIONS} />
|
||||
</Faq.Root>
|
||||
<Helped scheme="muted" cards={HELPED_CARDS} />
|
||||
|
||||
<HomeTestimonials />
|
||||
|
||||
<Faq />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,12 +4,11 @@ import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { getPublishedArticle, getPublishedArticles } from '@/lib/articles';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import { type LocaleRouteParams } from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { localeToUrlSegment } from '@/lib/i18n/utils/website-locale-segments';
|
||||
import { resolveLocaleParam } from '@/lib/i18n';
|
||||
import { type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { localeToUrlSegment, resolveLocaleParam } from '@/lib/i18n';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { buildArticleJsonLd, buildPageMetadata, JsonLd } from '@/lib/seo';
|
||||
import { Articles } from '@/sections/Articles';
|
||||
import { ArticlesArticle } from '@/sections/Articles';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
@@ -68,18 +67,11 @@ export default async function ArticlePage({ params }: ArticlePageProps) {
|
||||
return (
|
||||
<>
|
||||
<JsonLd data={buildArticleJsonLd(post)} />
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={theme.colors.primary.background[100]}
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
<Articles.Article post={post} />
|
||||
/>
|
||||
<ArticlesArticle post={post} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { HeroBody, HeroHeading, HeroSection } from '@/templates/Hero';
|
||||
|
||||
// Articles is English-only (SOURCE_LOCALE-gated), so the copy is literal.
|
||||
export function ArticlesHero() {
|
||||
return (
|
||||
<HeroSection scheme="muted">
|
||||
<HeroHeading>
|
||||
<HeadingPart fontFamily="serif">Ideas on</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">open-source</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">CRM</HeadingPart>
|
||||
</HeroHeading>
|
||||
<HeroBody maxWidthMd={550}>
|
||||
Ideas from the team building Twenty on open source CRM, GTM systems, and
|
||||
building software that lasts.
|
||||
</HeroBody>
|
||||
</HeroSection>
|
||||
);
|
||||
}
|
||||
@@ -1,27 +1,21 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { getPublishedArticles } from '@/lib/articles';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { resolveLocaleParam } from '@/lib/i18n';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import {
|
||||
buildArticleListJsonLd,
|
||||
buildBreadcrumbListJsonLd,
|
||||
buildRouteMetadata,
|
||||
JsonLd,
|
||||
} from '@/lib/seo';
|
||||
import { Articles } from '@/sections/Articles';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { ArticlesIndex } from '@/sections/Articles';
|
||||
import { ArticlesHero } from '@/app/[locale]/articles/_components/ArticlesHero';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy';
|
||||
import { TrustedBy } from '@/sections/TrustedBy';
|
||||
import { css } from '@linaria/core';
|
||||
|
||||
export const generateMetadata = buildRouteMetadata('articles');
|
||||
@@ -66,7 +60,10 @@ export default async function ArticlesPage({ params }: ArticlesPageProps) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const [i18n, stats] = await Promise.all([
|
||||
// getRouteI18n sets the request-scoped i18n context that TrustedBy/Menu/
|
||||
// Articles read; this page's own copy is English-only (SOURCE_LOCALE-gated
|
||||
// above), so the returned instance is intentionally unused here.
|
||||
const [, stats] = await Promise.all([
|
||||
getRouteI18n(params),
|
||||
fetchCommunityStats(),
|
||||
]);
|
||||
@@ -87,41 +84,20 @@ export default async function ArticlesPage({ params }: ArticlesPageProps) {
|
||||
{posts.length > 0 ? (
|
||||
<JsonLd data={buildArticleListJsonLd(posts)} />
|
||||
) : null}
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={ARTICLES_TOP_BACKGROUND_COLOR}
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
/>
|
||||
|
||||
<div className={pageRevealClassName}>
|
||||
<Hero.Root scheme="muted">
|
||||
<Hero.Heading page={Pages.Articles}>
|
||||
<HeadingPart fontFamily="serif">Ideas on</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">open-source</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">CRM</HeadingPart>
|
||||
</Hero.Heading>
|
||||
<Hero.Body page={Pages.Articles}>
|
||||
Ideas from the team building Twenty on open source CRM, GTM systems,
|
||||
and building software that lasts.
|
||||
</Hero.Body>
|
||||
</Hero.Root>
|
||||
<TrustedBy.Root
|
||||
<ArticlesHero />
|
||||
<TrustedBy
|
||||
cardBackgroundColor={ARTICLES_TOP_BACKGROUND_COLOR}
|
||||
compactBottom
|
||||
separator={i18n._(msg`trusted by`)}
|
||||
logos={TRUSTED_BY_LOGOS}
|
||||
clientCount={i18n._(msg`+10k others`)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Articles.Index posts={posts} />
|
||||
<ArticlesIndex posts={posts} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout';
|
||||
import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout';
|
||||
import { Heading, HeadingPart } from '@/design-system/components';
|
||||
import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { buildLocalizedMetadata } from '@/lib/seo';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
@@ -39,10 +36,10 @@ export default async function NineDotsCaseStudyPage({
|
||||
<Heading as="h1" size="xl" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`A real estate agency on WhatsApp`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`built a`)}</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`CRM`)}</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`CRM`)}</HeadingPart>
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`around it`)}</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
@@ -71,7 +68,7 @@ export default async function NineDotsCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`When the channel is`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`the business`)}
|
||||
</HeadingPart>
|
||||
@@ -92,7 +89,7 @@ export default async function NineDotsCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`The CRM as a`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`control hub`)}
|
||||
</HeadingPart>
|
||||
@@ -112,7 +109,7 @@ export default async function NineDotsCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`150 hours`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`saved every month`)}
|
||||
</HeadingPart>
|
||||
@@ -135,5 +132,10 @@ export default async function NineDotsCaseStudyPage({
|
||||
},
|
||||
};
|
||||
|
||||
return <CaseStudyPageLayout caseStudy={caseStudy} path="/customers/9dots" />;
|
||||
return (
|
||||
<CustomersCaseStudyPageLayout
|
||||
caseStudy={caseStudy}
|
||||
path="/customers/9dots"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+19
-18
@@ -1,21 +1,28 @@
|
||||
import { getCaseStudyPalette, type CaseStudyData } from '@/lib/customers';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { CaseStudy } from '@/sections/CaseStudy';
|
||||
import {
|
||||
CaseStudyBody,
|
||||
CaseStudyHero,
|
||||
CaseStudyHighlights,
|
||||
CaseStudySectionNav,
|
||||
CaseStudyTextBlock,
|
||||
CaseStudyVisualBlock,
|
||||
} from '@/sections/CaseStudy';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
import { CustomersCaseStudySignoff } from './CustomersCaseStudySignoff';
|
||||
|
||||
type CaseStudyPageLayoutProps = {
|
||||
type CustomersCaseStudyPageLayoutProps = {
|
||||
caseStudy: CaseStudyData;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export async function CaseStudyPageLayout({
|
||||
export async function CustomersCaseStudyPageLayout({
|
||||
caseStudy,
|
||||
path,
|
||||
}: CaseStudyPageLayoutProps) {
|
||||
}: CustomersCaseStudyPageLayoutProps) {
|
||||
const stats = await fetchCommunityStats();
|
||||
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
|
||||
const palette = getCaseStudyPalette(path);
|
||||
@@ -26,7 +33,7 @@ export async function CaseStudyPageLayout({
|
||||
const sectionId = `case-study-section-${storySectionIndex}`;
|
||||
storySectionIndex += 1;
|
||||
return (
|
||||
<CaseStudy.TextBlock
|
||||
<CaseStudyTextBlock
|
||||
key={index}
|
||||
block={block}
|
||||
isLast={index === caseStudy.sections.length - 1}
|
||||
@@ -35,7 +42,7 @@ export async function CaseStudyPageLayout({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<CaseStudy.VisualBlock
|
||||
<CaseStudyVisualBlock
|
||||
key={index}
|
||||
block={block}
|
||||
isLast={index === caseStudy.sections.length - 1}
|
||||
@@ -45,32 +52,26 @@ export async function CaseStudyPageLayout({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={theme.colors.secondary.background[100]}
|
||||
scheme="secondary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="secondary" />
|
||||
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="secondary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="secondary" />
|
||||
</Menu.Root>
|
||||
/>
|
||||
|
||||
<CaseStudy.Hero
|
||||
<CaseStudyHero
|
||||
dashColor={palette.dashColor}
|
||||
hero={caseStudy.hero}
|
||||
hoverDashColor={palette.hoverDashColor}
|
||||
/>
|
||||
|
||||
<CaseStudy.Highlights
|
||||
<CaseStudyHighlights
|
||||
industry={caseStudy.hero.industry}
|
||||
kpis={caseStudy.hero.kpis}
|
||||
/>
|
||||
|
||||
<CaseStudy.Body>{sectionBlocks}</CaseStudy.Body>
|
||||
<CaseStudyBody>{sectionBlocks}</CaseStudyBody>
|
||||
|
||||
<CaseStudy.SectionNav items={caseStudy.tableOfContents} />
|
||||
<CaseStudySectionNav items={caseStudy.tableOfContents} />
|
||||
<CustomersCaseStudySignoff />
|
||||
</>
|
||||
);
|
||||
+40
-27
@@ -1,37 +1,50 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import {
|
||||
HeadingPart,
|
||||
LinkButton,
|
||||
ResponsiveLineBreak,
|
||||
} from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { TalkToUsButton } from '@/sections/ContactCal';
|
||||
import { HeadingPart, LinkButton } from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/utils/get-server-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { Signoff } from '@/sections/Signoff';
|
||||
import { Signoff } from '@/templates/Signoff';
|
||||
|
||||
export function CustomersCaseStudySignoff() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<Signoff.Root scheme="muted" page={Pages.Partners}>
|
||||
<Signoff.Heading page={Pages.Partners}>
|
||||
<Signoff
|
||||
scheme="muted"
|
||||
centered
|
||||
heading={
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Ready to grow\nwith Twenty?`)}
|
||||
<Trans>
|
||||
Ready to grow
|
||||
<ResponsiveLineBreak />
|
||||
with Twenty?
|
||||
</Trans>
|
||||
</HeadingPart>
|
||||
</Signoff.Heading>
|
||||
<Signoff.Body page={Pages.Partners}>
|
||||
{i18n._(
|
||||
msg`Join the teams that chose to own their CRM.\nStart building with Twenty today.`,
|
||||
)}
|
||||
</Signoff.Body>
|
||||
<Signoff.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="secondary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Signoff.Cta>
|
||||
</Signoff.Root>
|
||||
}
|
||||
body={
|
||||
<Trans>
|
||||
Join the teams that chose to own their CRM.
|
||||
<ResponsiveLineBreak />
|
||||
Start building with Twenty today.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="secondary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Signoff>
|
||||
);
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { HeroBody, HeroHeading, HeroSection } from '@/templates/Hero';
|
||||
|
||||
export function CustomersCatalogHero() {
|
||||
return (
|
||||
<HeroSection scheme="muted">
|
||||
<HeroHeading>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">See how teams</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">build</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">on Twenty</HeadingPart>
|
||||
</Trans>
|
||||
</HeroHeading>
|
||||
<HeroBody maxWidthMd={550}>
|
||||
<Trans>
|
||||
Real stories from real teams about how they shaped Twenty to fit their
|
||||
workflow and accelerated their growth.
|
||||
</Trans>
|
||||
</HeroBody>
|
||||
</HeroSection>
|
||||
);
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import {
|
||||
HeadingPart,
|
||||
LinkButton,
|
||||
ResponsiveLineBreak,
|
||||
} from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { TalkToUsButton } from '@/sections/ContactCal';
|
||||
import { Signoff } from '@/templates/Signoff';
|
||||
|
||||
export function CustomersCatalogSignoff() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<Signoff
|
||||
scheme="light"
|
||||
centered
|
||||
heading={
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Ready to build</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">your own story?</HeadingPart>
|
||||
</Trans>
|
||||
}
|
||||
body={
|
||||
<Trans>
|
||||
Join the teams that chose to own their CRM.
|
||||
<ResponsiveLineBreak />
|
||||
Start building with Twenty today.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="secondary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Signoff>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { buildLocalizedMetadata } from '@/lib/seo';
|
||||
import { Heading, HeadingPart } from '@/design-system/components';
|
||||
import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout';
|
||||
import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout';
|
||||
|
||||
const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.actEducation;
|
||||
|
||||
@@ -62,7 +59,7 @@ export default async function ActEducationCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`When the vendor`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`pulled the plug`)}
|
||||
</HeadingPart>
|
||||
@@ -82,7 +79,7 @@ export default async function ActEducationCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`No more renting someone else's`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`structure`)}
|
||||
</HeadingPart>
|
||||
@@ -101,7 +98,7 @@ export default async function ActEducationCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Control without`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`the overhead`)}
|
||||
</HeadingPart>
|
||||
@@ -119,7 +116,7 @@ export default async function ActEducationCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Costs down more than`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`90%`)}</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
@@ -142,7 +139,7 @@ export default async function ActEducationCaseStudyPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<CaseStudyPageLayout
|
||||
<CustomersCaseStudyPageLayout
|
||||
caseStudy={caseStudy}
|
||||
path="/customers/act-education"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Heading, HeadingPart } from '@/design-system/components';
|
||||
import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { buildLocalizedMetadata } from '@/lib/seo';
|
||||
import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout';
|
||||
import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout';
|
||||
|
||||
const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.alternativePartners;
|
||||
|
||||
@@ -65,7 +62,7 @@ export default async function AlternativePartnersCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`AI in the`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`migration workflow`)}
|
||||
</HeadingPart>
|
||||
@@ -84,7 +81,7 @@ export default async function AlternativePartnersCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Self-hosted`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`means control`)}
|
||||
</HeadingPart>
|
||||
@@ -106,7 +103,7 @@ export default async function AlternativePartnersCaseStudyPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<CaseStudyPageLayout
|
||||
<CustomersCaseStudyPageLayout
|
||||
caseStudy={caseStudy}
|
||||
path="/customers/alternative-partners"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { buildLocalizedMetadata } from '@/lib/seo';
|
||||
import { Heading, HeadingPart } from '@/design-system/components';
|
||||
import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout';
|
||||
import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout';
|
||||
|
||||
const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.elevateConsulting;
|
||||
|
||||
@@ -43,7 +40,7 @@ export default async function ElevateConsultingCaseStudyPage({
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`API backbone`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`of a go-to-market stack`)}
|
||||
</HeadingPart>
|
||||
@@ -73,7 +70,7 @@ export default async function ElevateConsultingCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`From documents to`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`open APIs`)}
|
||||
</HeadingPart>
|
||||
@@ -92,8 +89,8 @@ export default async function ElevateConsultingCaseStudyPage({
|
||||
eyebrow: msg`Integration`,
|
||||
heading: (
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`One`)}</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`API`)}</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`One`)}</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`API`)}</HeadingPart>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`to rule them all`)}
|
||||
</HeadingPart>
|
||||
@@ -114,7 +111,7 @@ export default async function ElevateConsultingCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Workflows that`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`actually get used`)}
|
||||
</HeadingPart>
|
||||
@@ -131,7 +128,7 @@ export default async function ElevateConsultingCaseStudyPage({
|
||||
eyebrow: msg`What is next`,
|
||||
heading: (
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`Beyond`)}</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`Beyond`)}</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`internal rollout`)}
|
||||
</HeadingPart>
|
||||
@@ -156,7 +153,7 @@ export default async function ElevateConsultingCaseStudyPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<CaseStudyPageLayout
|
||||
<CustomersCaseStudyPageLayout
|
||||
caseStudy={caseStudy}
|
||||
path="/customers/elevate-consulting"
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { buildLocalizedMetadata } from '@/lib/seo';
|
||||
import { Heading, HeadingPart } from '@/design-system/components';
|
||||
import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout';
|
||||
import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout';
|
||||
|
||||
const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.netzero;
|
||||
|
||||
@@ -41,7 +38,7 @@ export default async function NetZeroCaseStudyPage({
|
||||
{i18n._(msg`A CRM that`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`grows`)}</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`grows`)}</HeadingPart>
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`with you`)}</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
@@ -64,7 +61,7 @@ export default async function NetZeroCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`The right`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`foundation`)}
|
||||
</HeadingPart>
|
||||
@@ -83,7 +80,7 @@ export default async function NetZeroCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`A business that does not fit a`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`template`)}</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
@@ -101,7 +98,7 @@ export default async function NetZeroCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`From simple to`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`advanced`)}</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
@@ -118,7 +115,7 @@ export default async function NetZeroCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`The bet is`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`paying off`)}
|
||||
</HeadingPart>
|
||||
@@ -143,6 +140,9 @@ export default async function NetZeroCaseStudyPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<CaseStudyPageLayout caseStudy={caseStudy} path="/customers/netzero" />
|
||||
<CustomersCaseStudyPageLayout
|
||||
caseStudy={caseStudy}
|
||||
path="/customers/netzero"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Faq, FAQ_QUESTIONS } from '@/sections/Faq';
|
||||
import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy';
|
||||
import { TalkToUsButton } from '@/sections/ContactCal';
|
||||
import { Faq } from '@/sections/Faq';
|
||||
import { TrustedBy } from '@/sections/TrustedBy';
|
||||
import { CASE_STUDY_CATALOG_ENTRIES } from '@/lib/customers';
|
||||
import { Eyebrow, HeadingPart, LinkButton } from '@/design-system/components';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { CaseStudyCatalog } from '@/sections/CaseStudyCatalog';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { CaseStudyCatalogGrid } from '@/sections/CaseStudyCatalog';
|
||||
import { CustomersCatalogHero } from '@/app/[locale]/customers/_components/CustomersCatalogHero';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { Signoff } from '@/sections/Signoff';
|
||||
import { CustomersCatalogSignoff } from '@/app/[locale]/customers/_components/CustomersCatalogSignoff';
|
||||
import { buildRouteMetadata } from '@/lib/seo';
|
||||
import { css } from '@linaria/core';
|
||||
|
||||
@@ -55,7 +48,9 @@ type CaseStudiesCatalogPageProps = {
|
||||
export default async function CaseStudiesCatalogPage({
|
||||
params,
|
||||
}: CaseStudiesCatalogPageProps) {
|
||||
const [i18n, stats] = await Promise.all([
|
||||
// getRouteI18n sets the request-scoped i18n context the page's components
|
||||
// read; the page renders no copy of its own, so the instance is unused here.
|
||||
const [, stats] = await Promise.all([
|
||||
getRouteI18n(params),
|
||||
fetchCommunityStats(),
|
||||
]);
|
||||
@@ -63,111 +58,24 @@ export default async function CaseStudiesCatalogPage({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={CUSTOMERS_TOP_BACKGROUND_COLOR}
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
/>
|
||||
|
||||
<div className={pageRevealClassName}>
|
||||
<Hero.Root scheme="muted">
|
||||
<Hero.Heading page={Pages.CaseStudies}>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`See how teams`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`build`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`on Twenty`)}
|
||||
</HeadingPart>
|
||||
</Hero.Heading>
|
||||
<Hero.Body page={Pages.CaseStudies}>
|
||||
{i18n._(
|
||||
msg`Real stories from real teams about how they shaped Twenty to fit their workflow and accelerated their growth.`,
|
||||
)}
|
||||
</Hero.Body>
|
||||
</Hero.Root>
|
||||
<TrustedBy.Root
|
||||
<CustomersCatalogHero />
|
||||
<TrustedBy
|
||||
cardBackgroundColor={CUSTOMERS_TOP_BACKGROUND_COLOR}
|
||||
compactBottom
|
||||
separator={i18n._(msg`trusted by`)}
|
||||
logos={TRUSTED_BY_LOGOS}
|
||||
clientCount={i18n._(msg`+10k others`)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CaseStudyCatalog.Grid compactTop entries={CASE_STUDY_CATALOG_ENTRIES} />
|
||||
<CaseStudyCatalogGrid compactTop entries={CASE_STUDY_CATALOG_ENTRIES} />
|
||||
|
||||
<Signoff.Root scheme="light" page={Pages.Partners}>
|
||||
<Signoff.Heading page={Pages.Partners}>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Ready to build`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`your own story?`)}
|
||||
</HeadingPart>
|
||||
</Signoff.Heading>
|
||||
<Signoff.Body page={Pages.Partners}>
|
||||
{i18n._(
|
||||
msg`Join the teams that chose to own their CRM.\nStart building with Twenty today.`,
|
||||
)}
|
||||
</Signoff.Body>
|
||||
<Signoff.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="secondary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Signoff.Cta>
|
||||
</Signoff.Root>
|
||||
<CustomersCatalogSignoff />
|
||||
|
||||
<Faq.Root>
|
||||
<Faq.Intro>
|
||||
<Eyebrow colorScheme="secondary">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Any Questions?`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Faq.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Stop fighting custom.`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Start building, with Twenty`)}
|
||||
</HeadingPart>
|
||||
</Faq.Heading>
|
||||
<Faq.Cta>
|
||||
<LinkButton
|
||||
color="primary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="primary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Faq.Cta>
|
||||
</Faq.Intro>
|
||||
<Faq.Items questions={FAQ_QUESTIONS} />
|
||||
</Faq.Root>
|
||||
<Faq />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { CASE_STUDY_HERO_IMAGES, type CaseStudyData } from '@/lib/customers';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { buildLocalizedMetadata } from '@/lib/seo';
|
||||
import { Heading, HeadingPart } from '@/design-system/components';
|
||||
import { CaseStudyPageLayout } from '@/app/[locale]/customers/_components/CaseStudyPageLayout';
|
||||
import { CustomersCaseStudyPageLayout } from '@/app/[locale]/customers/_components/CustomersCaseStudyPageLayout';
|
||||
|
||||
const HERO_IMAGE = CASE_STUDY_HERO_IMAGES.w3villa;
|
||||
|
||||
@@ -62,7 +59,7 @@ export default async function W3villaCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Scale without`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`breaking operations`)}
|
||||
</HeadingPart>
|
||||
@@ -82,7 +79,7 @@ export default async function W3villaCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Focus on the use case, not the`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`plumbing`)}</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
@@ -99,7 +96,7 @@ export default async function W3villaCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`A platform ready to`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`grow`)}</HeadingPart>
|
||||
</Heading>
|
||||
),
|
||||
@@ -116,7 +113,7 @@ export default async function W3villaCaseStudyPage({
|
||||
<Heading size="md" weight="light">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Zero manual work`)}
|
||||
</HeadingPart>{' '}
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`at the core`)}
|
||||
</HeadingPart>
|
||||
@@ -141,6 +138,9 @@ export default async function W3villaCaseStudyPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<CaseStudyPageLayout caseStudy={caseStudy} path="/customers/w3villa" />
|
||||
<CustomersCaseStudyPageLayout
|
||||
caseStudy={caseStudy}
|
||||
path="/customers/w3villa"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+17
-15
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import {
|
||||
BaseButton,
|
||||
buttonBaseStyles,
|
||||
@@ -159,7 +160,7 @@ export function EnterpriseActivateClient() {
|
||||
<ContentStack>
|
||||
{loading && (
|
||||
<Body size="sm" variant="body-paragraph">
|
||||
{i18n._(msg`Activating your enterprise license…`)}
|
||||
<Trans>Activating your enterprise license…</Trans>
|
||||
</Body>
|
||||
)}
|
||||
|
||||
@@ -168,14 +169,14 @@ export function EnterpriseActivateClient() {
|
||||
{result !== null && (
|
||||
<>
|
||||
<Body className={successLeadClassName} size="md" weight="medium">
|
||||
{i18n._(
|
||||
msg`Your enterprise license has been activated successfully.`,
|
||||
)}
|
||||
<Trans>
|
||||
Your enterprise license has been activated successfully.
|
||||
</Trans>
|
||||
</Body>
|
||||
|
||||
<LicenseeRow>
|
||||
<Body as="span" size="sm" weight="medium">
|
||||
{i18n._(msg`Licensee:`)}
|
||||
<Trans>Licensee:</Trans>
|
||||
</Body>{' '}
|
||||
<LicenseeValue>{result.licensee}</LicenseeValue>
|
||||
</LicenseeRow>
|
||||
@@ -183,13 +184,14 @@ export function EnterpriseActivateClient() {
|
||||
<KeySection>
|
||||
<Heading as="h2" size="xs" weight="medium">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Your enterprise key`)}
|
||||
<Trans>Your enterprise key</Trans>
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
<Body size="sm" variant="body-paragraph">
|
||||
{i18n._(
|
||||
msg`Copy this key and paste it into your Twenty self-hosted instance settings.`,
|
||||
)}
|
||||
<Trans>
|
||||
Copy this key and paste it into your Twenty self-hosted instance
|
||||
settings.
|
||||
</Trans>
|
||||
</Body>
|
||||
|
||||
<KeyBlock>
|
||||
@@ -215,25 +217,25 @@ export function EnterpriseActivateClient() {
|
||||
<NextStepsBox>
|
||||
<Heading as="h3" size="xs" weight="medium">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Next steps`)}
|
||||
<Trans>Next steps</Trans>
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
<NextStepsList>
|
||||
<li className={nextStepItemClassName}>
|
||||
<Body size="sm">
|
||||
{i18n._(msg`Copy the enterprise key above.`)}
|
||||
<Trans>Copy the enterprise key above.</Trans>
|
||||
</Body>
|
||||
</li>
|
||||
<li className={nextStepItemClassName}>
|
||||
<Body size="sm">
|
||||
{i18n._(
|
||||
msg`Open your Twenty self-hosted instance Settings → Enterprise.`,
|
||||
)}
|
||||
<Trans>
|
||||
Open your Twenty self-hosted instance Settings → Enterprise.
|
||||
</Trans>
|
||||
</Body>
|
||||
</li>
|
||||
<li className={nextStepItemClassName}>
|
||||
<Body size="sm">
|
||||
{i18n._(msg`Paste the key and click Activate.`)}
|
||||
<Trans>Paste the key and click Activate.</Trans>
|
||||
</Body>
|
||||
</li>
|
||||
</NextStepsList>
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { Eyebrow, HeadingPart } from '@/design-system/components';
|
||||
import { HeroBody, HeroHeading, HeroSection } from '@/templates/Hero';
|
||||
|
||||
export function EnterpriseActivateHero() {
|
||||
return (
|
||||
<HeroSection scheme="muted">
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Self-hosting</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<HeroHeading>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Enterprise</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">activation</HeadingPart>
|
||||
</Trans>
|
||||
</HeroHeading>
|
||||
<HeroBody maxWidthMd={500}>
|
||||
<Trans>
|
||||
Your checkout is complete. Follow the steps below to copy your license
|
||||
key into your Twenty instance.
|
||||
</Trans>
|
||||
</HeroBody>
|
||||
</HeroSection>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,10 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { EnterpriseActivateClient } from '@/app/[locale]/enterprise/activate/EnterpriseActivateClient';
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
Eyebrow,
|
||||
HeadingPart,
|
||||
} from '@/design-system/components';
|
||||
import { Body, Container } from '@/design-system/components';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { EnterpriseActivateHero } from '@/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { theme } from '@/theme';
|
||||
import { buildRouteMetadata } from '@/lib/seo';
|
||||
@@ -66,36 +57,9 @@ export default async function EnterpriseActivatePage({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Root
|
||||
backgroundColor="#F3F3F3"
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
<Menu backgroundColor="#F3F3F3" socialLinks={menuSocialLinks} />
|
||||
|
||||
<Hero.Root scheme="muted">
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Self-hosting`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Hero.Heading page={Pages.Pricing}>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Enterprise`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`activation`)}</HeadingPart>
|
||||
</Hero.Heading>
|
||||
<Hero.Body page={Pages.Pricing}>
|
||||
{i18n._(
|
||||
msg`Your checkout is complete. Follow the steps below to copy your license key into your Twenty instance.`,
|
||||
)}
|
||||
</Hero.Body>
|
||||
</Hero.Root>
|
||||
<EnterpriseActivateHero />
|
||||
|
||||
<ActivatePageContent>
|
||||
<Container>
|
||||
|
||||
@@ -18,13 +18,12 @@ import { ScrollToTopOnRouteChange } from '@/app/_components/ScrollToTopOnRouteCh
|
||||
import {
|
||||
I18nProvider,
|
||||
localeToUrlSegment,
|
||||
PUBLIC_APP_LOCALE_LIST,
|
||||
WEBSITE_LOCALE_LIST,
|
||||
resolveLocaleParam,
|
||||
} from '@/lib/i18n';
|
||||
import { getLocaleMessages } from '@/lib/i18n/utils/messages-by-locale';
|
||||
import { setServerI18n } from '@/lib/i18n/utils/set-server-i18n';
|
||||
import { getLocaleMessages, setServerI18n } from '@/lib/i18n/server';
|
||||
import { ContactCalModalRoot } from '@/sections/ContactCal';
|
||||
import { Footer, FOOTER_DATA } from '@/sections/Footer';
|
||||
import { Footer } from '@/sections/Footer';
|
||||
import { PartnerApplicationModalRoot } from '@/sections/PartnerApplication';
|
||||
|
||||
const hostGrotesk = Host_Grotesk({
|
||||
@@ -124,7 +123,7 @@ type LocaleLayoutParams = { locale: string };
|
||||
export const dynamicParams = false;
|
||||
|
||||
export const generateStaticParams = (): LocaleLayoutParams[] =>
|
||||
PUBLIC_APP_LOCALE_LIST.map((locale) => ({
|
||||
WEBSITE_LOCALE_LIST.map((locale) => ({
|
||||
locale: localeToUrlSegment(locale),
|
||||
}));
|
||||
|
||||
@@ -162,14 +161,7 @@ const LocaleLayout = async ({
|
||||
<ScrollToTopOnRouteChange />
|
||||
<StyledMain>{children}</StyledMain>
|
||||
<FooterVisibilityGate>
|
||||
<Footer.Root>
|
||||
<Footer.Logo />
|
||||
<Footer.Nav groups={FOOTER_DATA.navGroups} />
|
||||
<Footer.Bottom
|
||||
copyright={FOOTER_DATA.bottom.copyright}
|
||||
links={FOOTER_DATA.socialLinks}
|
||||
/>
|
||||
</Footer.Root>
|
||||
<Footer />
|
||||
</FooterVisibilityGate>
|
||||
</PartnerApplicationModalRoot>
|
||||
</ContactCalModalRoot>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { PartnerHeroCtas } from '@/app/[locale]/partners/components/PartnerApplication';
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { HeroBody, HeroCta, HeroHeading, HeroSection } from '@/templates/Hero';
|
||||
import { PartnerVisual } from '@/sections/Hero';
|
||||
|
||||
export function PartnerHero() {
|
||||
return (
|
||||
<HeroSection scheme="light">
|
||||
<HeroHeading>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Become</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">our partner</HeadingPart>
|
||||
</Trans>
|
||||
</HeroHeading>
|
||||
<HeroBody maxWidthMd={500}>
|
||||
<Trans>
|
||||
We're building the #1 Open Source CRM, but we can't do it alone. Join
|
||||
our partner ecosystem and grow with us.
|
||||
</Trans>
|
||||
</HeroBody>
|
||||
<HeroCta>
|
||||
<PartnerHeroCtas />
|
||||
</HeroCta>
|
||||
<PartnerVisual />
|
||||
</HeroSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { PartnerSignoffCtas } from '@/app/[locale]/partners/components/PartnerApplication';
|
||||
import { HeadingPart, ResponsiveLineBreak } from '@/design-system/components';
|
||||
import { Signoff } from '@/templates/Signoff';
|
||||
|
||||
export function PartnerSignoff() {
|
||||
return (
|
||||
<Signoff
|
||||
scheme="light"
|
||||
centered
|
||||
heading={
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Ready to grow</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">with Twenty?</HeadingPart>
|
||||
</Trans>
|
||||
}
|
||||
body={
|
||||
<Trans>
|
||||
Join our partner ecosystem and help businesses
|
||||
<ResponsiveLineBreak />
|
||||
take control of their CRM.
|
||||
</Trans>
|
||||
}
|
||||
>
|
||||
<PartnerSignoffCtas />
|
||||
</Signoff>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import { PARTNER_TESTIMONIALS } from '@/app/[locale]/partners/testimonials.data';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import {
|
||||
TestimonialsPartnerCarousel,
|
||||
TestimonialsPartnerVisual,
|
||||
} from '@/sections/Testimonials';
|
||||
import { TestimonialsSection } from '@/templates/Testimonials';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export function PartnerTestimonials() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<TestimonialsSection
|
||||
scheme="muted"
|
||||
shapeFillColor={theme.colors.secondary.background[100]}
|
||||
>
|
||||
<TestimonialsPartnerCarousel
|
||||
eyebrow={i18n._(msg`Join our growing partner ecosystem`)}
|
||||
testimonials={PARTNER_TESTIMONIALS}
|
||||
>
|
||||
<TestimonialsPartnerVisual />
|
||||
</TestimonialsPartnerCarousel>
|
||||
</TestimonialsSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { PARTNER_ILLUSTRATION_CARDS } from '@/app/[locale]/partners/three-cards-illustration.data';
|
||||
import { Eyebrow, Heading, HeadingPart } from '@/design-system/components';
|
||||
import {
|
||||
IllustrationCards,
|
||||
type ThreeCardsScrollLayoutOptions,
|
||||
} from '@/sections/ThreeCards';
|
||||
import { ThreeCardsIntro, ThreeCardsSection } from '@/templates/ThreeCards';
|
||||
|
||||
const SCROLL_LAYOUT_OPTIONS: ThreeCardsScrollLayoutOptions = {
|
||||
endEdgeRatio: 0.28,
|
||||
initialScale: 0.935,
|
||||
initialTranslateY: 132,
|
||||
opacityRamp: 0.28,
|
||||
stagger: 0.16,
|
||||
};
|
||||
|
||||
export function PartnerThreeCards() {
|
||||
return (
|
||||
<ThreeCardsSection scheme="muted">
|
||||
<ThreeCardsIntro align="left">
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Which partner program is right for you?</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
Find the program that fits your business
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
and unlock new opportunities with Twenty
|
||||
</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
</ThreeCardsIntro>
|
||||
<IllustrationCards
|
||||
illustrationCards={PARTNER_ILLUSTRATION_CARDS}
|
||||
layoutOptions={SCROLL_LAYOUT_OPTIONS}
|
||||
variant="simple"
|
||||
/>
|
||||
</ThreeCardsSection>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { theme } from '@/theme';
|
||||
@@ -44,16 +43,16 @@ const EmptyStateClearButton = styled(ClearFiltersButton)`
|
||||
`;
|
||||
|
||||
export function EmptyState({ onClearFilters }: EmptyStateProps) {
|
||||
const { i18n } = useLingui();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Heading>{i18n._(msg`No partners match your filters`)}</Heading>
|
||||
<Heading>
|
||||
<Trans>No partners match your filters</Trans>
|
||||
</Heading>
|
||||
<Subtitle>
|
||||
{i18n._(msg`Try removing some filters or browse all partners.`)}
|
||||
<Trans>Try removing some filters or browse all partners.</Trans>
|
||||
</Subtitle>
|
||||
<EmptyStateClearButton onClick={onClearFilters}>
|
||||
{i18n._(msg`Clear filters`)}
|
||||
<Trans>Clear filters</Trans>
|
||||
</EmptyStateClearButton>
|
||||
</Wrapper>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useLingui } from '@lingui/react';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
@@ -94,13 +95,17 @@ export function FilterBar({
|
||||
/>
|
||||
<Footer>
|
||||
<ResultCount aria-live="polite">
|
||||
{hasAnyFilter
|
||||
? i18n._(msg`Showing ${visibleCount} of ${totalCount} partners`)
|
||||
: i18n._(msg`Showing all ${totalCount} partners`)}
|
||||
{hasAnyFilter ? (
|
||||
<Trans>
|
||||
Showing {visibleCount} of {totalCount} partners
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>Showing all {totalCount} partners</Trans>
|
||||
)}
|
||||
</ResultCount>
|
||||
{hasAnyFilter && (
|
||||
<ClearFiltersButton onClick={onClearAll}>
|
||||
{i18n._(msg`Clear filters`)}
|
||||
<Trans>Clear filters</Trans>
|
||||
</ClearFiltersButton>
|
||||
)}
|
||||
</Footer>
|
||||
|
||||
+11
-12
@@ -1,4 +1,4 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import {
|
||||
Body,
|
||||
Container,
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Heading,
|
||||
HeadingPart,
|
||||
} from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/utils/get-server-i18n';
|
||||
import { theme } from '@/theme';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
@@ -38,26 +37,26 @@ const HeaderBody = styled(Body)`
|
||||
`;
|
||||
|
||||
export function MarketplaceHeader() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledContainer>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Marketplace`)}
|
||||
<Trans>Marketplace</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`Find your`)}</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Twenty partner`)}
|
||||
</HeadingPart>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Find your</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">Twenty partner</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
<HeaderBody>
|
||||
{i18n._(
|
||||
msg`Twenty's certified partners help teams migrate, customise, and operate the open source CRM across regions, languages, and deployment models. Browse profiles and book a call.`,
|
||||
)}
|
||||
<Trans>
|
||||
Twenty's certified partners help teams migrate, customise, and
|
||||
operate the open source CRM across regions, languages, and
|
||||
deployment models. Browse profiles and book a call.
|
||||
</Trans>
|
||||
</HeaderBody>
|
||||
</StyledContainer>
|
||||
</Section>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { theme } from '@/theme';
|
||||
@@ -32,17 +29,10 @@ export default async function PartnersMarketplacePage({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={theme.colors.primary.background[100]}
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
/>
|
||||
|
||||
<MarketplaceHeader />
|
||||
|
||||
|
||||
@@ -1,49 +1,19 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Faq, FAQ_QUESTIONS } from '@/sections/Faq';
|
||||
import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy';
|
||||
import { TalkToUsButton } from '@/sections/ContactCal';
|
||||
import { Faq } from '@/sections/Faq';
|
||||
import { TrustedBy } from '@/sections/TrustedBy';
|
||||
import { CASE_STUDY_CATALOG_ENTRIES } from '@/lib/customers';
|
||||
import { PARTNER_ILLUSTRATION_CARDS } from '@/app/[locale]/partners/three-cards-illustration.data';
|
||||
import { PARTNER_TESTIMONIALS } from '@/app/[locale]/partners/testimonials.data';
|
||||
import {
|
||||
PartnerHeroCtas,
|
||||
PartnerSignoffCtas,
|
||||
} from '@/app/[locale]/partners/components/PartnerApplication';
|
||||
import {
|
||||
Eyebrow,
|
||||
Heading,
|
||||
HeadingPart,
|
||||
LinkButton,
|
||||
} from '@/design-system/components';
|
||||
import { PartnerHero } from '@/app/[locale]/partners/components/PartnerHero';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { CaseStudyCatalog } from '@/sections/CaseStudyCatalog';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { CaseStudyCatalogPromo } from '@/sections/CaseStudyCatalog';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { Signoff } from '@/sections/Signoff';
|
||||
import { Testimonials } from '@/sections/Testimonials';
|
||||
import {
|
||||
ThreeCards,
|
||||
type ThreeCardsScrollLayoutOptions,
|
||||
} from '@/sections/ThreeCards';
|
||||
import { PartnerSignoff } from '@/app/[locale]/partners/components/PartnerSignoff';
|
||||
import { PartnerTestimonials } from '@/app/[locale]/partners/components/PartnerTestimonials';
|
||||
import { PartnerThreeCards } from '@/app/[locale]/partners/components/PartnerThreeCards';
|
||||
import { theme } from '@/theme';
|
||||
import { buildRouteMetadata } from '@/lib/seo';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const PARTNER_ILLUSTRATION_CARDS_SCROLL_LAYOUT_OPTIONS: ThreeCardsScrollLayoutOptions =
|
||||
{
|
||||
endEdgeRatio: 0.28,
|
||||
initialScale: 0.935,
|
||||
initialTranslateY: 132,
|
||||
opacityRamp: 0.28,
|
||||
stagger: 0.16,
|
||||
};
|
||||
|
||||
const PromoSpacing = styled.div`
|
||||
margin-bottom: ${theme.spacing(8)};
|
||||
|
||||
@@ -59,7 +29,9 @@ type PartnerPageProps = {
|
||||
};
|
||||
|
||||
export default async function PartnerPage({ params }: PartnerPageProps) {
|
||||
const [i18n, stats] = await Promise.all([
|
||||
// getRouteI18n sets the request-scoped i18n context the page's components
|
||||
// read; the page renders no copy of its own, so the instance is unused here.
|
||||
const [, stats] = await Promise.all([
|
||||
getRouteI18n(params),
|
||||
fetchCommunityStats(),
|
||||
]);
|
||||
@@ -67,139 +39,32 @@ export default async function PartnerPage({ params }: PartnerPageProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={theme.colors.primary.background[100]}
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
/>
|
||||
|
||||
<Hero.Root scheme="light">
|
||||
<Hero.Heading page={Pages.Partners}>
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`Become`)}</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`our partner`)}
|
||||
</HeadingPart>
|
||||
</Hero.Heading>
|
||||
<Hero.Body page={Pages.Partners}>
|
||||
{i18n._(
|
||||
msg`We're building the #1 Open Source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.`,
|
||||
)}
|
||||
</Hero.Body>
|
||||
<Hero.Cta>
|
||||
<PartnerHeroCtas />
|
||||
</Hero.Cta>
|
||||
<Hero.PartnerVisual />
|
||||
</Hero.Root>
|
||||
<PartnerHero />
|
||||
|
||||
<TrustedBy.Root
|
||||
<TrustedBy
|
||||
backgroundColor={theme.colors.primary.background[100]}
|
||||
compactBottom
|
||||
separator={i18n._(msg`trusted by`)}
|
||||
logos={TRUSTED_BY_LOGOS}
|
||||
clientCount={i18n._(msg`+10k others`)}
|
||||
/>
|
||||
|
||||
<PromoSpacing>
|
||||
<CaseStudyCatalog.Promo
|
||||
<CaseStudyCatalogPromo
|
||||
compactTop
|
||||
entries={CASE_STUDY_CATALOG_ENTRIES}
|
||||
/>
|
||||
</PromoSpacing>
|
||||
|
||||
<ThreeCards.Root scheme="muted">
|
||||
<ThreeCards.Intro page={Pages.Partners} align="left">
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Which partner program is right for you?`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Find the program that fits your business`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`and unlock new opportunities with Twenty`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards
|
||||
illustrationCards={PARTNER_ILLUSTRATION_CARDS}
|
||||
layoutOptions={PARTNER_ILLUSTRATION_CARDS_SCROLL_LAYOUT_OPTIONS}
|
||||
variant="simple"
|
||||
/>
|
||||
</ThreeCards.Root>
|
||||
<PartnerThreeCards />
|
||||
|
||||
<Testimonials.Root
|
||||
scheme="muted"
|
||||
shapeFillColor={theme.colors.secondary.background[100]}
|
||||
>
|
||||
<Testimonials.PartnerCarousel
|
||||
eyebrow={i18n._(msg`Join our growing partner ecosystem`)}
|
||||
testimonials={PARTNER_TESTIMONIALS}
|
||||
>
|
||||
<Testimonials.PartnerVisual />
|
||||
</Testimonials.PartnerCarousel>
|
||||
</Testimonials.Root>
|
||||
<PartnerTestimonials />
|
||||
|
||||
<Signoff.Root scheme="light" page={Pages.Partners}>
|
||||
<Signoff.Heading page={Pages.Partners}>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Ready to grow`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`with Twenty?`)}
|
||||
</HeadingPart>
|
||||
</Signoff.Heading>
|
||||
<Signoff.Body page={Pages.Partners}>
|
||||
{i18n._(
|
||||
msg`Join our partner ecosystem and help businesses\ntake control of their CRM.`,
|
||||
)}
|
||||
</Signoff.Body>
|
||||
<Signoff.Cta>
|
||||
<PartnerSignoffCtas />
|
||||
</Signoff.Cta>
|
||||
</Signoff.Root>
|
||||
<PartnerSignoff />
|
||||
|
||||
<Faq.Root>
|
||||
<Faq.Intro>
|
||||
<Eyebrow colorScheme="secondary">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Any Questions?`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Faq.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Stop fighting custom.`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Start building, with Twenty`)}
|
||||
</HeadingPart>
|
||||
</Faq.Heading>
|
||||
<Faq.Cta>
|
||||
<LinkButton
|
||||
color="primary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="primary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Faq.Cta>
|
||||
</Faq.Intro>
|
||||
<Faq.Items questions={FAQ_QUESTIONS} />
|
||||
</Faq.Root>
|
||||
<Faq />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { BecomePartnerButton } from '@/app/[locale]/partners/components/PartnerApplication';
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import {
|
||||
EngagementBandActions,
|
||||
EngagementBandBody,
|
||||
EngagementBandCopy,
|
||||
EngagementBandHeading,
|
||||
EngagementBandSection,
|
||||
EngagementBandStrip,
|
||||
} from '@/sections/EngagementBand';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
const PricingBannerContainer = styled.div`
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function PricingEngagementBand() {
|
||||
return (
|
||||
<EngagementBandSection scheme="muted">
|
||||
<PricingBannerContainer>
|
||||
<EngagementBandStrip
|
||||
desktopCopyMaxWidth="60%"
|
||||
fillColor={theme.colors.primary.background[100]}
|
||||
variant="primary"
|
||||
>
|
||||
<EngagementBandCopy>
|
||||
<EngagementBandHeading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
<Trans>Need help with customization?</Trans>
|
||||
</HeadingPart>
|
||||
</EngagementBandHeading>
|
||||
<EngagementBandBody>
|
||||
<Trans>
|
||||
Find the right partner to implement, customize, and tailor
|
||||
Twenty to your team.
|
||||
</Trans>
|
||||
</EngagementBandBody>
|
||||
</EngagementBandCopy>
|
||||
<EngagementBandActions>
|
||||
<BecomePartnerButton
|
||||
color="secondary"
|
||||
label={msg`Find a partner`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</EngagementBandActions>
|
||||
</EngagementBandStrip>
|
||||
</PricingBannerContainer>
|
||||
</EngagementBandSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { HeroBody, HeroHeading, HeroSection } from '@/templates/Hero';
|
||||
|
||||
export function PricingHero() {
|
||||
return (
|
||||
<HeroSection scheme="muted">
|
||||
<HeroHeading>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Simple</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">Pricing</HeadingPart>
|
||||
</Trans>
|
||||
</HeroHeading>
|
||||
<HeroBody maxWidthMd={500}>
|
||||
<Trans>
|
||||
Start your free trial today
|
||||
<br />
|
||||
without credit card.
|
||||
</Trans>
|
||||
</HeroBody>
|
||||
</HeroSection>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { SALESFORCE_DATA } from '@/app/[locale]/pricing/salesforce.data';
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { Salesforce } from '@/sections/Salesforce';
|
||||
|
||||
export function PricingSalesforce() {
|
||||
return (
|
||||
<Salesforce
|
||||
scheme="muted"
|
||||
body={SALESFORCE_DATA.body}
|
||||
pricing={SALESFORCE_DATA.pricing}
|
||||
>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Trust the n°1 CRM,</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">or not!</HeadingPart>
|
||||
</Trans>
|
||||
</Salesforce>
|
||||
);
|
||||
}
|
||||
@@ -1,23 +1,18 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Faq, FAQ_QUESTIONS } from '@/sections/Faq';
|
||||
import { TalkToUsButton } from '@/sections/ContactCal';
|
||||
import { BecomePartnerButton } from '@/app/[locale]/partners/components/PartnerApplication';
|
||||
import { PLAN_TABLE_DATA } from '@/app/[locale]/pricing/plan-table.data';
|
||||
import { SALESFORCE_DATA } from '@/app/[locale]/pricing/salesforce.data';
|
||||
import { Eyebrow, HeadingPart, LinkButton } from '@/design-system/components';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { EngagementBand } from '@/sections/EngagementBand';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { PricingEngagementBand } from '@/app/[locale]/pricing/_components/PricingEngagementBand';
|
||||
import { PricingHero } from '@/app/[locale]/pricing/_components/PricingHero';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { Plans, PricingStateProvider } from '@/sections/Plans';
|
||||
import { PlanTable } from '@/sections/PlanTable';
|
||||
import { Salesforce } from '@/sections/Salesforce';
|
||||
import {
|
||||
PlansContent,
|
||||
PlansSection,
|
||||
PricingStateProvider,
|
||||
} from '@/sections/Plans';
|
||||
import { PlanTableContent, PlanTableSection } from '@/sections/PlanTable';
|
||||
import { PricingSalesforce } from '@/app/[locale]/pricing/_components/PricingSalesforce';
|
||||
import { theme } from '@/theme';
|
||||
import { buildFaqPageJsonLd, buildRouteMetadata, JsonLd } from '@/lib/seo';
|
||||
import { styled } from '@linaria/react';
|
||||
@@ -29,11 +24,6 @@ const PricingPlansContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const PricingBannerContainer = styled.div`
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const generateMetadata = buildRouteMetadata('pricing');
|
||||
|
||||
type PricingPageProps = {
|
||||
@@ -50,114 +40,27 @@ export default async function PricingPage({ params }: PricingPageProps) {
|
||||
return (
|
||||
<>
|
||||
<JsonLd data={buildFaqPageJsonLd(FAQ_QUESTIONS, (d) => i18n._(d))} />
|
||||
<Menu.Root
|
||||
backgroundColor="#F3F3F3"
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
<Menu backgroundColor="#F3F3F3" socialLinks={menuSocialLinks} />
|
||||
|
||||
<Hero.Root scheme="muted">
|
||||
<Hero.Heading page={Pages.Pricing}>
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`Simple`)}</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`Pricing`)}</HeadingPart>
|
||||
</Hero.Heading>
|
||||
<Hero.Body page={Pages.Pricing} preserveLineBreaks>
|
||||
{i18n._(msg`Start your free trial today\nwithout credit card.`)}
|
||||
</Hero.Body>
|
||||
</Hero.Root>
|
||||
<PricingHero />
|
||||
|
||||
<PricingStateProvider>
|
||||
<Plans.Root scheme="muted">
|
||||
<PlansSection scheme="muted">
|
||||
<PricingPlansContainer>
|
||||
<Plans.Content />
|
||||
<PlansContent />
|
||||
</PricingPlansContainer>
|
||||
</Plans.Root>
|
||||
</PlansSection>
|
||||
|
||||
<EngagementBand.Root scheme="muted">
|
||||
<PricingBannerContainer>
|
||||
<EngagementBand.Strip
|
||||
desktopCopyMaxWidth="60%"
|
||||
fillColor={theme.colors.primary.background[100]}
|
||||
variant="primary"
|
||||
>
|
||||
<EngagementBand.Copy>
|
||||
<EngagementBand.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Need help with customization?`)}
|
||||
</HeadingPart>
|
||||
</EngagementBand.Heading>
|
||||
<EngagementBand.Body>
|
||||
{i18n._(
|
||||
msg`Find the right partner to implement, customize, and tailor Twenty to your team.`,
|
||||
)}
|
||||
</EngagementBand.Body>
|
||||
</EngagementBand.Copy>
|
||||
<EngagementBand.Actions>
|
||||
<BecomePartnerButton
|
||||
color="secondary"
|
||||
label={msg`Find a partner`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</EngagementBand.Actions>
|
||||
</EngagementBand.Strip>
|
||||
</PricingBannerContainer>
|
||||
</EngagementBand.Root>
|
||||
<PricingEngagementBand />
|
||||
|
||||
<PlanTable.Root scheme="dark">
|
||||
<PlanTable.Content data={PLAN_TABLE_DATA} />
|
||||
</PlanTable.Root>
|
||||
<PlanTableSection scheme="dark">
|
||||
<PlanTableContent data={PLAN_TABLE_DATA} />
|
||||
</PlanTableSection>
|
||||
</PricingStateProvider>
|
||||
|
||||
<Salesforce.Flow
|
||||
scheme="muted"
|
||||
body={SALESFORCE_DATA.body}
|
||||
pricing={SALESFORCE_DATA.pricing}
|
||||
>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Trust the n°1 CRM,`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`or not !`)}</HeadingPart>
|
||||
</Salesforce.Flow>
|
||||
<PricingSalesforce />
|
||||
|
||||
<Faq.Root>
|
||||
<Faq.Intro>
|
||||
<Eyebrow colorScheme="secondary">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Any Questions?`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Faq.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Stop fighting custom.`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Start building, with Twenty`)}
|
||||
</HeadingPart>
|
||||
</Faq.Heading>
|
||||
<Faq.Cta>
|
||||
<LinkButton
|
||||
color="primary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="primary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Faq.Cta>
|
||||
</Faq.Intro>
|
||||
<Faq.Items questions={FAQ_QUESTIONS} />
|
||||
</Faq.Root>
|
||||
<Faq />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MENU_DATA } from '@/sections/Menu';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { LegalDocument } from '@/sections/LegalDocument';
|
||||
import { LegalDocumentPage } from '@/sections/LegalDocument';
|
||||
import { buildRouteMetadata } from '@/lib/seo';
|
||||
|
||||
import { PrivacyPolicyDocument } from './_components';
|
||||
@@ -13,11 +13,11 @@ export default async function PrivacyPolicyPage() {
|
||||
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
|
||||
|
||||
return (
|
||||
<LegalDocument.Page
|
||||
<LegalDocumentPage
|
||||
menuData={{ navItems: MENU_DATA.navItems, socialLinks: menuSocialLinks }}
|
||||
title="Privacy Policy"
|
||||
>
|
||||
<PrivacyPolicyDocument />
|
||||
</LegalDocument.Page>
|
||||
</LegalDocumentPage>
|
||||
);
|
||||
}
|
||||
|
||||
+56
-5
@@ -1,9 +1,19 @@
|
||||
import { Container } from '@/design-system/components';
|
||||
import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data';
|
||||
import {
|
||||
Container,
|
||||
Eyebrow,
|
||||
Heading,
|
||||
HeadingPart,
|
||||
LinkButton,
|
||||
} from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { AppPreview } from '@/sections/AppPreview';
|
||||
import { theme } from '@/theme';
|
||||
import { css } from '@linaria/core';
|
||||
import { styled } from '@linaria/react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import NextImage from 'next/image';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
const RootWrapper = styled.div`
|
||||
display: grid;
|
||||
@@ -65,9 +75,26 @@ const StyledContainer = styled(Container)`
|
||||
}
|
||||
`;
|
||||
|
||||
type RootProps = { children: ReactNode };
|
||||
const CtasContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${theme.spacing(4)};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const PreviewRoot = styled.div`
|
||||
margin-bottom: ${theme.spacing(11)};
|
||||
margin-top: ${theme.spacing(12)};
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
margin-top: ${theme.spacing(19)};
|
||||
}
|
||||
`;
|
||||
|
||||
export function ProductDemo() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
export function Root({ children }: RootProps) {
|
||||
return (
|
||||
<RootWrapper>
|
||||
<BackgroundLayer aria-hidden>
|
||||
@@ -82,7 +109,31 @@ export function Root({ children }: RootProps) {
|
||||
</PatternLayer>
|
||||
</BackgroundLayer>
|
||||
<StyledSection>
|
||||
<StyledContainer>{children}</StyledContainer>
|
||||
<StyledContainer>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Try it live</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">A demo worth a</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">thousand words</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
<CtasContainer>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Try Twenty Cloud`)}
|
||||
variant="contained"
|
||||
/>
|
||||
</CtasContainer>
|
||||
<PreviewRoot>
|
||||
<AppPreview showTerminal={false} visual={APP_PREVIEW_DATA.visual} />
|
||||
</PreviewRoot>
|
||||
</StyledContainer>
|
||||
</StyledSection>
|
||||
</RootWrapper>
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data';
|
||||
import { AI_HERO_TABS } from '@/app/[locale]/product/ai-hero-tabs.data';
|
||||
import { Heading, HeadingPart } from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { HeroVisualScroll } from '@/sections/Hero';
|
||||
|
||||
export function ProductHero() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<HeroVisualScroll
|
||||
aiBody={i18n._(
|
||||
msg`Ask questions, automate tasks, and get insights. All powered by AI that understands your data.`,
|
||||
)}
|
||||
aiHeading={
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">AI that actually</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">helps you</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">work faster</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
}
|
||||
ctaHref="https://app.twenty.com/welcome"
|
||||
ctaLabel={i18n._(msg`Get started`)}
|
||||
introBody={i18n._(
|
||||
msg`Track relationships, manage pipelines, and take action quickly with a CRM that feels intuitive from day one.`,
|
||||
)}
|
||||
introHeading={
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">A CRM for teams</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">that</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">move fast</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
}
|
||||
tabs={AI_HERO_TABS}
|
||||
visual={APP_PREVIEW_DATA.visual}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { ILLUSTRATION_CARDS } from '@/app/[locale]/product/three-cards.data';
|
||||
import { Eyebrow, Heading, HeadingPart } from '@/design-system/components';
|
||||
import { IllustrationCards } from '@/sections/ThreeCards';
|
||||
import { ThreeCardsIntro, ThreeCardsSection } from '@/templates/ThreeCards';
|
||||
|
||||
export function ProductThreeCards() {
|
||||
return (
|
||||
<ThreeCardsSection scheme="light">
|
||||
<ThreeCardsIntro align="left" maxWidthMd={600}>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Stop settling for trade-offs.</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">A modern CRM with</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">an intuitive interface</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
</ThreeCardsIntro>
|
||||
<IllustrationCards illustrationCards={ILLUSTRATION_CARDS} />
|
||||
</ThreeCardsSection>
|
||||
);
|
||||
}
|
||||
+29
-9
@@ -1,10 +1,17 @@
|
||||
import { Container } from '@/design-system/components';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import {
|
||||
Container,
|
||||
Eyebrow,
|
||||
Heading,
|
||||
HeadingPart,
|
||||
} from '@/design-system/components';
|
||||
import { type Page, Pages } from '@/lib/pages';
|
||||
import { theme, type Scheme } from '@/theme';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import type { MessageDescriptor } from '@lingui/core';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ComponentType, ReactNode } from 'react';
|
||||
|
||||
import { FEATURE_TILES } from './feature-tiles.data';
|
||||
import { FeatureScrollEntrance } from './FeatureScrollEntrance';
|
||||
import { TileContent } from './TileContent';
|
||||
import { TileVisual } from './TileVisual';
|
||||
@@ -22,8 +29,6 @@ export type FeatureTileType = {
|
||||
visual: ComponentType<{ active: boolean }>;
|
||||
};
|
||||
|
||||
// --- Root ---
|
||||
|
||||
const StyledSection = styled.section`
|
||||
width: 100%;
|
||||
|
||||
@@ -75,8 +80,6 @@ function Root({ backgroundColor, children, scheme }: RootProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Intro ---
|
||||
|
||||
const StyledIntro = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
@@ -113,8 +116,6 @@ function Intro({ align, children, page }: IntroProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// --- Tiles ---
|
||||
|
||||
const BORDER = `1px solid ${theme.colors.primary.border[20]}`;
|
||||
|
||||
const Grid = styled.div`
|
||||
@@ -251,4 +252,23 @@ function Tiles({ tiles }: TilesProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export const Feature = { Intro, Root, Tiles };
|
||||
export function ProductFeature() {
|
||||
return (
|
||||
<Root scheme="light">
|
||||
<Intro align="center" page={Pages.Product}>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Core Features</Trans>
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Everything you need,</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">out of the box</HeadingPart>
|
||||
</Trans>
|
||||
</Heading>
|
||||
</Intro>
|
||||
<Tiles tiles={FEATURE_TILES} />
|
||||
</Root>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -3,10 +3,10 @@ import { styled } from '@linaria/react';
|
||||
|
||||
import { Body, Heading, HeadingPart } from '@/design-system/components';
|
||||
import { INFORMATIVE_ICONS } from '@/icons';
|
||||
import { getServerI18n } from '@/lib/i18n/utils/get-server-i18n';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
import type { FeatureTileType } from './Feature';
|
||||
import type { FeatureTileType } from './ProductFeature';
|
||||
|
||||
const DIVIDER_COLOR = theme.colors.primary.border[10];
|
||||
const ICON_STROKE_WIDTH = 1.5;
|
||||
+8
-8
@@ -1,13 +1,13 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
|
||||
import type { FeatureTileType } from '@/sections/Feature';
|
||||
import { ContactsVisual } from '@/sections/Feature/visuals/ContactsVisual';
|
||||
import { DashboardVisual } from '@/sections/Feature/visuals/DashboardVisual';
|
||||
import { EmailsVisual } from '@/sections/Feature/visuals/EmailsVisual';
|
||||
import { FilesVisual } from '@/sections/Feature/visuals/FilesVisual';
|
||||
import { ImportVisual } from '@/sections/Feature/visuals/ImportVisual';
|
||||
import { PipelineVisual } from '@/sections/Feature/visuals/PipelineVisual';
|
||||
import { TasksVisual } from '@/sections/Feature/visuals/TasksVisual';
|
||||
import type { FeatureTileType } from './ProductFeature';
|
||||
import { ContactsVisual } from './visuals/ContactsVisual';
|
||||
import { DashboardVisual } from './visuals/DashboardVisual';
|
||||
import { EmailsVisual } from './visuals/EmailsVisual';
|
||||
import { FilesVisual } from './visuals/FilesVisual';
|
||||
import { ImportVisual } from './visuals/ImportVisual';
|
||||
import { PipelineVisual } from './visuals/PipelineVisual';
|
||||
import { TasksVisual } from './visuals/TasksVisual';
|
||||
|
||||
export const FEATURE_TILES: FeatureTileType[] = [
|
||||
{
|
||||
@@ -0,0 +1 @@
|
||||
export { ProductFeature, type FeatureTileType } from './ProductFeature';
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { Flow } from './Flow';
|
||||
import type { ProductStepperStepType } from './types';
|
||||
import { DataModelVisual } from './visuals/DataModelVisual';
|
||||
import { LayoutVisual } from './visuals/LayoutVisual';
|
||||
import { WorkflowVisual } from './visuals/WorkflowVisual';
|
||||
|
||||
export function ProductStepperSection() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
const steps: ProductStepperStepType[] = [
|
||||
{
|
||||
icon: 'users',
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Data model</Trans>
|
||||
</HeadingPart>
|
||||
),
|
||||
body: msg`Add objects and fields`,
|
||||
visual: DataModelVisual,
|
||||
},
|
||||
{
|
||||
icon: 'check',
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Automation</Trans>
|
||||
</HeadingPart>
|
||||
),
|
||||
body: msg`Create a workflow`,
|
||||
visual: WorkflowVisual,
|
||||
},
|
||||
{
|
||||
icon: 'eye',
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">
|
||||
<Trans>Layout</Trans>
|
||||
</HeadingPart>
|
||||
),
|
||||
body: msg`Tailor record pages, menus, and views`,
|
||||
visual: LayoutVisual,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Flow
|
||||
body={i18n._(
|
||||
msg`Need a quick change? Skip the engineering ticket. Customize your workspace in minutes.`,
|
||||
)}
|
||||
eyebrow={i18n._(msg`Customization`)}
|
||||
steps={steps}
|
||||
>
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Go the extra mile</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">with no-code</HeadingPart>
|
||||
</Trans>
|
||||
</Flow>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProductStepperSection } from './ProductStepperSection';
|
||||
@@ -1,42 +1,20 @@
|
||||
import { APP_PREVIEW_DATA } from '@/app/[locale]/(home)/app-preview.data';
|
||||
import { AI_HERO_TABS } from '@/app/[locale]/product/ai-hero-tabs.data';
|
||||
import { FEATURE_TILES } from '@/app/[locale]/product/feature.data';
|
||||
import { ILLUSTRATION_CARDS } from '@/app/[locale]/product/three-cards.data';
|
||||
import {
|
||||
Eyebrow,
|
||||
Heading,
|
||||
HeadingPart,
|
||||
LinkButton,
|
||||
} from '@/design-system/components';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import {
|
||||
buildBreadcrumbListJsonLd,
|
||||
buildRouteMetadata,
|
||||
JsonLd,
|
||||
} from '@/lib/seo';
|
||||
import { TalkToUsButton } from '@/sections/ContactCal';
|
||||
import { Demo } from '@/sections/Demo';
|
||||
import { Faq, FAQ_QUESTIONS } from '@/sections/Faq';
|
||||
import { Feature } from '@/sections/Feature';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { ProductDemo } from '@/app/[locale]/product/_components/ProductDemo';
|
||||
import { ProductFeature } from '@/app/[locale]/product/_components/feature';
|
||||
import { Faq } from '@/sections/Faq';
|
||||
import { ProductHero } from '@/app/[locale]/product/_components/ProductHero';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import {
|
||||
ProductStepper,
|
||||
type ProductStepperStepType,
|
||||
} from '@/sections/ProductStepper';
|
||||
import { DataModelVisual } from '@/sections/ProductStepper/visuals/DataModelVisual';
|
||||
import { LayoutVisual } from '@/sections/ProductStepper/visuals/LayoutVisual';
|
||||
import { WorkflowVisual } from '@/sections/ProductStepper/visuals/WorkflowVisual';
|
||||
import { ThreeCards } from '@/sections/ThreeCards';
|
||||
import { TRUSTED_BY_LOGOS, TrustedBy } from '@/sections/TrustedBy';
|
||||
import { ProductStepperSection } from '@/app/[locale]/product/_components/product-stepper';
|
||||
import { ProductThreeCards } from '@/app/[locale]/product/_components/ProductThreeCards';
|
||||
import { TrustedBy } from '@/sections/TrustedBy';
|
||||
import { theme } from '@/theme';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import type { AppLocale } from 'twenty-shared/translations';
|
||||
|
||||
export const generateMetadata = buildRouteMetadata('product');
|
||||
@@ -52,33 +30,6 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
]);
|
||||
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
|
||||
|
||||
const PRODUCT_STEPS: ProductStepperStepType[] = [
|
||||
{
|
||||
icon: 'users',
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`Data model`)}</HeadingPart>
|
||||
),
|
||||
body: msg`Add objects and fields`,
|
||||
visual: DataModelVisual,
|
||||
},
|
||||
{
|
||||
icon: 'check',
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`Automation`)}</HeadingPart>
|
||||
),
|
||||
body: msg`Create a workflow`,
|
||||
visual: WorkflowVisual,
|
||||
},
|
||||
{
|
||||
icon: 'eye',
|
||||
heading: (
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`Layout`)}</HeadingPart>
|
||||
),
|
||||
body: msg`Tailor record pages, menus, and views`,
|
||||
visual: LayoutVisual,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonLd
|
||||
@@ -90,174 +41,24 @@ export default async function ProductPage({ params }: ProductPageProps) {
|
||||
i18n.locale as AppLocale,
|
||||
)}
|
||||
/>
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={theme.colors.primary.background[100]}
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
|
||||
<Hero.HeroVisualScroll
|
||||
aiBody={i18n._(
|
||||
msg`Ask questions, automate tasks, and get insights. All powered by AI that understands your data.`,
|
||||
)}
|
||||
aiHeading={
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`AI that actually`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`helps you`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`work faster`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
}
|
||||
ctaHref="https://app.twenty.com/welcome"
|
||||
ctaLabel={i18n._(msg`Get started`)}
|
||||
introBody={i18n._(
|
||||
msg`Track relationships, manage pipelines, and take action quickly with a CRM that feels intuitive from day one.`,
|
||||
)}
|
||||
introHeading={
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`A CRM for teams`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`that`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`move fast`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
}
|
||||
tabs={AI_HERO_TABS}
|
||||
visual={APP_PREVIEW_DATA.visual}
|
||||
/>
|
||||
|
||||
<TrustedBy.Root
|
||||
separator={i18n._(msg`trusted by`)}
|
||||
logos={TRUSTED_BY_LOGOS}
|
||||
clientCount={i18n._(msg`+10k others`)}
|
||||
/>
|
||||
<ProductHero />
|
||||
|
||||
<Feature.Root scheme="light">
|
||||
<Feature.Intro align="center" page={Pages.Product}>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Core Features`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Everything you need,`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`out of the box`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
</Feature.Intro>
|
||||
<Feature.Tiles tiles={FEATURE_TILES} />
|
||||
</Feature.Root>
|
||||
<TrustedBy />
|
||||
|
||||
<ThreeCards.Root scheme="light">
|
||||
<ThreeCards.Intro page={Pages.Product} align="left">
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Stop settling for trade-offs.`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`A modern CRM with`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`an intuitive interface`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
</ThreeCards.Intro>
|
||||
<ThreeCards.IllustrationCards illustrationCards={ILLUSTRATION_CARDS} />
|
||||
</ThreeCards.Root>
|
||||
<ProductFeature />
|
||||
|
||||
<ProductStepper.Flow
|
||||
body={i18n._(
|
||||
msg`Need a quick change? Skip the engineering ticket. Customize your workspace in minutes.`,
|
||||
)}
|
||||
eyebrow={i18n._(msg`Customization`)}
|
||||
steps={PRODUCT_STEPS}
|
||||
>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Go the extra mile`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`with no-code`)}</HeadingPart>
|
||||
</ProductStepper.Flow>
|
||||
<ProductThreeCards />
|
||||
|
||||
<Demo.Root>
|
||||
<Eyebrow>
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Try it live`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Heading size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`A demo worth a`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`thousand words`)}
|
||||
</HeadingPart>
|
||||
</Heading>
|
||||
<Demo.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Try Twenty Cloud`)}
|
||||
variant="contained"
|
||||
/>
|
||||
</Demo.Cta>
|
||||
<Demo.Preview visual={APP_PREVIEW_DATA.visual} />
|
||||
</Demo.Root>
|
||||
<ProductStepperSection />
|
||||
|
||||
<Faq.Root>
|
||||
<Faq.Intro>
|
||||
<Eyebrow colorScheme="secondary">
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Any Questions?`)}
|
||||
</HeadingPart>
|
||||
</Eyebrow>
|
||||
<Faq.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Stop fighting custom.`)}
|
||||
</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`Start building, with Twenty`)}
|
||||
</HeadingPart>
|
||||
</Faq.Heading>
|
||||
<Faq.Cta>
|
||||
<LinkButton
|
||||
color="primary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
<TalkToUsButton
|
||||
color="primary"
|
||||
label={msg`Talk to us`}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Faq.Cta>
|
||||
</Faq.Intro>
|
||||
<Faq.Items questions={FAQ_QUESTIONS} />
|
||||
</Faq.Root>
|
||||
<ProductDemo />
|
||||
|
||||
<Faq />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { HeadingPart, LinkButton } from '@/design-system/components';
|
||||
import { GitHubIcon } from '@/icons';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { HeroBody, HeroCta, HeroHeading, HeroSection } from '@/templates/Hero';
|
||||
import { ReleaseNotesVisual } from '@/sections/Hero';
|
||||
|
||||
export function ReleasesHero() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<HeroSection scheme="light">
|
||||
<HeroHeading size="lg" weight="light">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Latest</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">Releases</HeadingPart>
|
||||
</Trans>
|
||||
</HeroHeading>
|
||||
<HeroBody maxWidthMd={591} size="sm">
|
||||
<Trans>
|
||||
Discover the newest features and improvements in Twenty,
|
||||
<br />
|
||||
the #1 Open Source CRM.
|
||||
</Trans>
|
||||
</HeroBody>
|
||||
<HeroCta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://github.com/twentyhq/twenty/releases"
|
||||
label={i18n._(msg`Technical notes`)}
|
||||
leadingIcon={<GitHubIcon fillColor="currentColor" size={14} />}
|
||||
variant="outlined"
|
||||
/>
|
||||
</HeroCta>
|
||||
<ReleaseNotesVisual />
|
||||
</HeroSection>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { HeadingPart, LinkButton } from '@/design-system/components';
|
||||
import { GitHubIcon } from '@/icons';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { fetchLatestGithubReleaseTag } from '@/lib/releases/fetch-latest-release-tag';
|
||||
import { getVisibleReleaseNotes } from '@/lib/releases/get-visible-releases';
|
||||
import { loadLocalReleaseNotes } from '@/lib/releases/load-local-release-notes';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { ReleasesHero } from '@/app/[locale]/releases/_components/ReleasesHero';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { ReleaseNotes } from '@/sections/ReleaseNotes';
|
||||
import {
|
||||
ReleaseNotesDivider,
|
||||
ReleaseNotesEmptyMessage,
|
||||
ReleaseNotesReleaseEntry,
|
||||
ReleaseNotesSection,
|
||||
} from '@/sections/ReleaseNotes';
|
||||
import { theme } from '@/theme';
|
||||
import { buildReleaseListJsonLd, buildRouteMetadata, JsonLd } from '@/lib/seo';
|
||||
import { Fragment } from 'react';
|
||||
@@ -28,7 +26,7 @@ type ReleasesPageProps = {
|
||||
|
||||
export default async function ReleasesPage({ params }: ReleasesPageProps) {
|
||||
const allNotes = loadLocalReleaseNotes();
|
||||
const [i18n, latestTag, stats] = await Promise.all([
|
||||
const [, latestTag, stats] = await Promise.all([
|
||||
getRouteI18n(params),
|
||||
fetchLatestGithubReleaseTag(),
|
||||
fetchCommunityStats(),
|
||||
@@ -53,68 +51,38 @@ export default async function ReleasesPage({ params }: ReleasesPageProps) {
|
||||
href="/illustrations/generated/milestone.jpg"
|
||||
rel="preload"
|
||||
/>
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={theme.colors.primary.background[100]}
|
||||
scheme="primary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="primary" />
|
||||
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="primary" />
|
||||
</Menu.Root>
|
||||
/>
|
||||
|
||||
<Hero.Root scheme="light">
|
||||
<Hero.Heading page={Pages.ReleaseNotes} size="lg" weight="light">
|
||||
<HeadingPart fontFamily="serif">{i18n._(msg`Latest`)}</HeadingPart>
|
||||
<br />
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`Releases`)}</HeadingPart>
|
||||
</Hero.Heading>
|
||||
<Hero.Body page={Pages.ReleaseNotes} size="sm">
|
||||
{i18n._(
|
||||
msg`Discover the newest features and improvements in Twenty,\nthe #1 Open Source CRM.`,
|
||||
)}
|
||||
</Hero.Body>
|
||||
<Hero.Cta>
|
||||
<LinkButton
|
||||
color="secondary"
|
||||
href="https://github.com/twentyhq/twenty/releases"
|
||||
label={i18n._(msg`Technical notes`)}
|
||||
leadingIcon={<GitHubIcon fillColor="currentColor" size={14} />}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Hero.Cta>
|
||||
<Hero.ReleaseNotesVisual />
|
||||
</Hero.Root>
|
||||
<ReleasesHero />
|
||||
|
||||
<ReleaseNotes.Root>
|
||||
<ReleaseNotesSection>
|
||||
{allNotes.length === 0 ? (
|
||||
<ReleaseNotes.EmptyMessage>
|
||||
<ReleaseNotesEmptyMessage>
|
||||
Releases were not found. Add MDX under{' '}
|
||||
<strong>packages/twenty-website/src/content/releases</strong> and
|
||||
images under{' '}
|
||||
<strong>packages/twenty-website/public/images/releases</strong>.
|
||||
</ReleaseNotes.EmptyMessage>
|
||||
</ReleaseNotesEmptyMessage>
|
||||
) : visibleNotes.length === 0 ? (
|
||||
<ReleaseNotes.EmptyMessage>
|
||||
<ReleaseNotesEmptyMessage>
|
||||
No releases are visible yet for the current published version.
|
||||
</ReleaseNotes.EmptyMessage>
|
||||
</ReleaseNotesEmptyMessage>
|
||||
) : (
|
||||
visibleNotes.map((note, index) => (
|
||||
<Fragment key={note.slug}>
|
||||
<ReleaseNotes.ReleaseEntry
|
||||
<ReleaseNotesReleaseEntry
|
||||
content={note.content}
|
||||
date={note.date}
|
||||
release={note.release}
|
||||
/>
|
||||
{index < visibleNotes.length - 1 ? (
|
||||
<ReleaseNotes.Divider />
|
||||
) : null}
|
||||
{index < visibleNotes.length - 1 ? <ReleaseNotesDivider /> : null}
|
||||
</Fragment>
|
||||
))
|
||||
)}
|
||||
</ReleaseNotes.Root>
|
||||
</ReleaseNotesSection>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { MENU_DATA } from '@/sections/Menu';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { LegalDocument } from '@/sections/LegalDocument';
|
||||
import { LegalDocumentPage } from '@/sections/LegalDocument';
|
||||
import { buildRouteMetadata } from '@/lib/seo';
|
||||
|
||||
import { TermsDocument } from './_components';
|
||||
@@ -13,11 +13,11 @@ export default async function TermsPage() {
|
||||
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
|
||||
|
||||
return (
|
||||
<LegalDocument.Page
|
||||
<LegalDocumentPage
|
||||
menuData={{ navItems: MENU_DATA.navItems, socialLinks: menuSocialLinks }}
|
||||
title="Terms of Service"
|
||||
>
|
||||
<TermsDocument />
|
||||
</LegalDocument.Page>
|
||||
</LegalDocumentPage>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { css } from '@linaria/core';
|
||||
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { HeroBody, HeroHeading, HeroSection } from '@/templates/Hero';
|
||||
import { WhyTwentyVisual } from '@/sections/Hero';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
const headingClassName = css`
|
||||
color: ${theme.colors.secondary.text[100]};
|
||||
`;
|
||||
|
||||
export function WhyTwentyHero() {
|
||||
return (
|
||||
<HeroSection scheme="dark">
|
||||
<HeroHeading className={headingClassName} size="xl">
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
The future of CRM is built,
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">not bought.</HeadingPart>
|
||||
</Trans>
|
||||
</HeroHeading>
|
||||
<HeroBody maxWidthMd={443}>
|
||||
<Trans>
|
||||
CRM was a database you filled on Fridays. AI turned it into the system
|
||||
that runs your go-to-market. To differentiate, you have to build what
|
||||
your competitors can't buy.
|
||||
</Trans>
|
||||
</HeroBody>
|
||||
<WhyTwentyVisual />
|
||||
</HeroSection>
|
||||
);
|
||||
}
|
||||
+13
-33
@@ -1,11 +1,8 @@
|
||||
import { theme } from '@/theme';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { styled } from '@linaria/react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type MarqueeSegment = {
|
||||
fontFamily: string;
|
||||
text: ReactNode;
|
||||
};
|
||||
import { theme } from '@/theme';
|
||||
|
||||
const MARQUEE_REPEAT = 6;
|
||||
|
||||
@@ -16,16 +13,6 @@ const StyledSection = styled.section`
|
||||
padding-top: ${theme.spacing(20)};
|
||||
width: 100%;
|
||||
|
||||
&[data-scheme='light'] {
|
||||
background-color: var(--color-white);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&[data-scheme='muted'] {
|
||||
background-color: var(--color-neutral);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&[data-scheme='dark'] {
|
||||
background-color: var(--color-black);
|
||||
color: var(--color-text);
|
||||
@@ -111,14 +98,12 @@ const Segment = styled.span`
|
||||
}
|
||||
`;
|
||||
|
||||
type Scheme = 'light' | 'muted' | 'dark';
|
||||
|
||||
function Phrase({ segments }: { segments: MarqueeSegment[] }) {
|
||||
function Phrase({ segments }: { segments: ReactNode[] }) {
|
||||
return (
|
||||
<PhraseGroup>
|
||||
{segments.map((segment, segmentIndex) => (
|
||||
<Segment key={segmentIndex} data-segment-index={segmentIndex}>
|
||||
{segment.text}
|
||||
{segment}
|
||||
</Segment>
|
||||
))}
|
||||
</PhraseGroup>
|
||||
@@ -129,7 +114,7 @@ function Track({
|
||||
segments,
|
||||
reversed,
|
||||
}: {
|
||||
segments: MarqueeSegment[];
|
||||
segments: ReactNode[];
|
||||
reversed: boolean;
|
||||
}) {
|
||||
const phrases = Array.from({ length: MARQUEE_REPEAT }, (_, index) => (
|
||||
@@ -144,19 +129,16 @@ function Track({
|
||||
);
|
||||
}
|
||||
|
||||
type RootProps = {
|
||||
backgroundColor?: string;
|
||||
color?: string;
|
||||
segments: MarqueeSegment[];
|
||||
scheme?: Scheme;
|
||||
};
|
||||
// Segment styling is positional via data-segment-index.
|
||||
export function WhyTwentyMarquee() {
|
||||
const segments: ReactNode[] = [
|
||||
<Trans>Same CRM</Trans>,
|
||||
<Trans>Same output</Trans>,
|
||||
<Trans>Same results</Trans>,
|
||||
];
|
||||
|
||||
function Root({ backgroundColor, color, segments, scheme }: RootProps) {
|
||||
return (
|
||||
<StyledSection
|
||||
data-scheme={scheme}
|
||||
style={scheme ? undefined : { backgroundColor, color }}
|
||||
>
|
||||
<StyledSection data-scheme="dark">
|
||||
<Viewport>
|
||||
<Row>
|
||||
<Track segments={segments} reversed={false} />
|
||||
@@ -168,5 +150,3 @@ function Root({ backgroundColor, color, segments, scheme }: RootProps) {
|
||||
</StyledSection>
|
||||
);
|
||||
}
|
||||
|
||||
export const Marquee = { Root };
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { Editorial } from '@/templates/Editorial';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export function WhyTwentyMeaningEditorial() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<Editorial
|
||||
scheme="dark"
|
||||
crosshair={{
|
||||
crossX: '120px',
|
||||
crossY: '0px',
|
||||
lineColor: theme.colors.secondary.border[10],
|
||||
}}
|
||||
introAlign="right"
|
||||
eyebrowColorScheme="secondary"
|
||||
eyebrow={i18n._(msg`What this means`)}
|
||||
heading={
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">Differentiation now</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
lives in the code you own.
|
||||
</HeadingPart>
|
||||
</Trans>
|
||||
}
|
||||
bodyLayout="two-column-right"
|
||||
bodyParagraphs={[
|
||||
i18n._(
|
||||
msg`You don't buy your deployment pipeline off the shelf. You don't rent your data warehouse from a vendor who decides the schema. You build it, you own it, you iterate on it every week. CRM is going the same way. The teams that treat it as infrastructure they own will compound an advantage every quarter.`,
|
||||
),
|
||||
i18n._(
|
||||
msg`Tuesday your team learns that deals with a technical champion close 3x faster. Wednesday you add the field, wire up the scoring, adjust the workflow. By Thursday your agents are acting on it. That feedback loop is the edge. And it only works if the CRM is yours.`,
|
||||
),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { Editorial } from '@/templates/Editorial';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export function WhyTwentyOpportunityEditorial() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<Editorial
|
||||
scheme="dark"
|
||||
crosshair={{
|
||||
crossX: 'calc(100% - 120px)',
|
||||
crossY: '0px',
|
||||
lineColor: theme.colors.secondary.border[10],
|
||||
}}
|
||||
eyebrowColorScheme="secondary"
|
||||
eyebrow={i18n._(msg`The opportunity`)}
|
||||
heading={
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
Build it in an afternoon.
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
AI made the gap that small.
|
||||
</HeadingPart>
|
||||
</Trans>
|
||||
}
|
||||
bodyLayout="two-column-left"
|
||||
bodyParagraphs={[
|
||||
i18n._(
|
||||
msg`A year ago, customizing your CRM meant hiring a Salesforce consultant, learning Apex, waiting months. The gap between "I want this" and "it's live" was measured in quarters and invoices. So people settled. They bent their process to fit the tool and called it adoption.`,
|
||||
),
|
||||
i18n._(
|
||||
msg`Now a developer can describe what they want to Claude Code and have a working app in an afternoon. A custom object, a scoring workflow, a new view, an integration. The bottleneck isn't building anymore. It's whether your platform lets you.`,
|
||||
),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { HeadingPart } from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { Editorial } from '@/templates/Editorial';
|
||||
import { theme } from '@/theme';
|
||||
|
||||
export function WhyTwentyShiftEditorial() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<Editorial
|
||||
scheme="dark"
|
||||
crosshair={{
|
||||
crossX: 'calc(100% - 120px)',
|
||||
crossY: '0px',
|
||||
lineColor: theme.colors.secondary.border[10],
|
||||
}}
|
||||
eyebrowColorScheme="secondary"
|
||||
eyebrow={i18n._(msg`The shift`)}
|
||||
heading={
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">CRM was a ledger.</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">
|
||||
AI turned it into an operating system.
|
||||
</HeadingPart>
|
||||
</Trans>
|
||||
}
|
||||
bodyLayout="two-column-left"
|
||||
bodyParagraphs={[
|
||||
i18n._(
|
||||
msg`For twenty years, CRM meant the same thing: a place to log calls, track deals, and pull reports on Friday. The real work happened in people's heads, in Slack threads, in hallway conversations. The CRM kept score. Nobody expected more from it.`,
|
||||
),
|
||||
i18n._(
|
||||
msg`AI agents are starting to draft outreach, score leads, research accounts, write follow-ups, update deal stages. Every one of these actions reads from and writes to the CRM. The scoreboard became the playbook. The database became the brain.`,
|
||||
),
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
import { HeadingPart, LinkButton } from '@/design-system/components';
|
||||
import { getServerI18n } from '@/lib/i18n/server';
|
||||
import { Signoff } from '@/templates/Signoff';
|
||||
|
||||
export function WhyTwentySignoff() {
|
||||
const i18n = getServerI18n();
|
||||
|
||||
return (
|
||||
<Signoff
|
||||
scheme="dark"
|
||||
heading={
|
||||
<Trans>
|
||||
<HeadingPart fontFamily="serif">
|
||||
Build a CRM your competitors
|
||||
</HeadingPart>
|
||||
<HeadingPart fontFamily="sans">can't buy.</HeadingPart>
|
||||
</Trans>
|
||||
}
|
||||
body={<Trans>Open-source, AI-ready, and yours to shape.</Trans>}
|
||||
>
|
||||
<LinkButton
|
||||
color="primary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
</Signoff>
|
||||
);
|
||||
}
|
||||
@@ -1,59 +1,15 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { HeadingPart, LinkButton } from '@/design-system/components';
|
||||
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
|
||||
import {
|
||||
getRouteI18n,
|
||||
type LocaleRouteParams,
|
||||
} from '@/lib/i18n/utils/get-route-i18n';
|
||||
import { Pages } from '@/lib/pages';
|
||||
import { getRouteI18n, type LocaleRouteParams } from '@/lib/i18n/server';
|
||||
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
|
||||
import { Editorial } from '@/sections/Editorial';
|
||||
import { Hero } from '@/sections/Hero';
|
||||
import { Marquee } from '@/sections/Marquee';
|
||||
import { WhyTwentyMarquee } from '@/app/[locale]/why-twenty/_components/WhyTwentyMarquee';
|
||||
import { WhyTwentyHero } from '@/app/[locale]/why-twenty/_components/WhyTwentyHero';
|
||||
import { WhyTwentyMeaningEditorial } from '@/app/[locale]/why-twenty/_components/WhyTwentyMeaningEditorial';
|
||||
import { WhyTwentyOpportunityEditorial } from '@/app/[locale]/why-twenty/_components/WhyTwentyOpportunityEditorial';
|
||||
import { WhyTwentyShiftEditorial } from '@/app/[locale]/why-twenty/_components/WhyTwentyShiftEditorial';
|
||||
import { Menu, MENU_DATA } from '@/sections/Menu';
|
||||
import { Signoff } from '@/sections/Signoff';
|
||||
import { WhyTwentySignoff } from '@/app/[locale]/why-twenty/_components/WhyTwentySignoff';
|
||||
import { theme } from '@/theme';
|
||||
import { buildRouteMetadata } from '@/lib/seo';
|
||||
import { css } from '@linaria/core';
|
||||
|
||||
const editorialOneIntroClass = css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
--editorial-heading-max-width: 760px;
|
||||
--editorial-intro-max-width: 760px;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
margin-bottom: ${theme.spacing(8)};
|
||||
}
|
||||
`;
|
||||
|
||||
const editorialRightIntroClass = css`
|
||||
margin-bottom: ${theme.spacing(4)};
|
||||
--editorial-heading-max-width: 760px;
|
||||
--editorial-intro-max-width: 760px;
|
||||
|
||||
@media (min-width: ${theme.breakpoints.md}px) {
|
||||
align-items: flex-end;
|
||||
margin-bottom: ${theme.spacing(8)};
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
text-align: right;
|
||||
width: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
const crosshairLineColor = theme.colors.secondary.border[10];
|
||||
|
||||
const sectionCrosshairLeft = {
|
||||
crossX: '120px',
|
||||
crossY: '0px',
|
||||
lineColor: crosshairLineColor,
|
||||
};
|
||||
|
||||
const sectionCrosshairRight = {
|
||||
crossX: 'calc(100% - 120px)',
|
||||
crossY: '0px',
|
||||
lineColor: crosshairLineColor,
|
||||
};
|
||||
|
||||
export const generateMetadata = buildRouteMetadata('whyTwenty');
|
||||
|
||||
@@ -62,7 +18,9 @@ type WhyTwentyPageProps = {
|
||||
};
|
||||
|
||||
export default async function WhyTwentyPage({ params }: WhyTwentyPageProps) {
|
||||
const [i18n, stats] = await Promise.all([
|
||||
// getRouteI18n sets the request-scoped i18n context the page's components
|
||||
// read; the page renders no copy of its own, so the instance is unused here.
|
||||
const [, stats] = await Promise.all([
|
||||
getRouteI18n(params),
|
||||
fetchCommunityStats(),
|
||||
]);
|
||||
@@ -80,135 +38,23 @@ export default async function WhyTwentyPage({ params }: WhyTwentyPageProps) {
|
||||
href="/illustrations/why-twenty/hero/hero.glb"
|
||||
rel="preload"
|
||||
/>
|
||||
<Menu.Root
|
||||
<Menu
|
||||
backgroundColor={theme.colors.secondary.background[100]}
|
||||
scheme="secondary"
|
||||
navItems={MENU_DATA.navItems}
|
||||
socialLinks={menuSocialLinks}
|
||||
>
|
||||
<Menu.Logo scheme="secondary" />
|
||||
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
|
||||
<Menu.Social scheme="secondary" socialLinks={menuSocialLinks} />
|
||||
<Menu.Cta scheme="secondary" />
|
||||
</Menu.Root>
|
||||
|
||||
<Hero.Root scheme="dark">
|
||||
<Hero.Heading page={Pages.WhyTwenty} size="xl">
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`The future of CRM is built,`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`not bought.`)}
|
||||
</HeadingPart>
|
||||
</Hero.Heading>
|
||||
<Hero.Body page={Pages.WhyTwenty}>
|
||||
{i18n._(
|
||||
msg`CRM was a database you filled on Fridays. AI turned it into the system that runs your go-to-market. To differentiate, you have to build what your competitors can't buy.`,
|
||||
)}
|
||||
</Hero.Body>
|
||||
<Hero.WhyTwentyVisual />
|
||||
</Hero.Root>
|
||||
|
||||
<Editorial.Root scheme="dark" crosshair={sectionCrosshairRight}>
|
||||
<Editorial.Intro className={editorialOneIntroClass}>
|
||||
<Editorial.Eyebrow colorScheme="secondary">
|
||||
{i18n._(msg`The shift`)}
|
||||
</Editorial.Eyebrow>
|
||||
<Editorial.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`CRM was a ledger.`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`AI turned it into an operating system.`)}
|
||||
</HeadingPart>
|
||||
</Editorial.Heading>
|
||||
</Editorial.Intro>
|
||||
<Editorial.Body layout="two-column-left">
|
||||
{i18n._(
|
||||
msg`For twenty years, CRM meant the same thing: a place to log calls, track deals, and pull reports on Friday. The real work happened in people's heads, in Slack threads, in hallway conversations. The CRM kept score. Nobody expected more from it.`,
|
||||
)}
|
||||
{i18n._(
|
||||
msg`AI agents are starting to draft outreach, score leads, research accounts, write follow-ups, update deal stages. Every one of these actions reads from and writes to the CRM. The scoreboard became the playbook. The database became the brain.`,
|
||||
)}
|
||||
</Editorial.Body>
|
||||
</Editorial.Root>
|
||||
|
||||
<Editorial.Root scheme="dark" crosshair={sectionCrosshairLeft}>
|
||||
<Editorial.Intro className={editorialRightIntroClass}>
|
||||
<Editorial.Eyebrow colorScheme="secondary">
|
||||
{i18n._(msg`What this means`)}
|
||||
</Editorial.Eyebrow>
|
||||
<Editorial.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Differentiation now`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`lives in the code you own.`)}
|
||||
</HeadingPart>
|
||||
</Editorial.Heading>
|
||||
</Editorial.Intro>
|
||||
<Editorial.Body layout="two-column-right">
|
||||
{i18n._(
|
||||
msg`You don't buy your deployment pipeline off the shelf. You don't rent your data warehouse from a vendor who decides the schema. You build it, you own it, you iterate on it every week. CRM is going the same way. The teams that treat it as infrastructure they own will compound an advantage every quarter.`,
|
||||
)}
|
||||
{i18n._(
|
||||
msg`Tuesday your team learns that deals with a technical champion close 3x faster. Wednesday you add the field, wire up the scoring, adjust the workflow. By Thursday your agents are acting on it. That feedback loop is the edge. And it only works if the CRM is yours.`,
|
||||
)}
|
||||
</Editorial.Body>
|
||||
</Editorial.Root>
|
||||
|
||||
<Editorial.Root scheme="dark" crosshair={sectionCrosshairRight}>
|
||||
<Editorial.Intro className={editorialOneIntroClass}>
|
||||
<Editorial.Eyebrow colorScheme="secondary">
|
||||
{i18n._(msg`The opportunity`)}
|
||||
</Editorial.Eyebrow>
|
||||
<Editorial.Heading>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Build it in an afternoon.`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">
|
||||
{i18n._(msg`AI made the gap that small.`)}
|
||||
</HeadingPart>
|
||||
</Editorial.Heading>
|
||||
</Editorial.Intro>
|
||||
<Editorial.Body layout="two-column-left">
|
||||
{i18n._(
|
||||
msg`A year ago, customizing your CRM meant hiring a Salesforce consultant, learning Apex, waiting months. The gap between "I want this" and "it's live" was measured in quarters and invoices. So people settled. They bent their process to fit the tool and called it adoption.`,
|
||||
)}
|
||||
{i18n._(
|
||||
msg`Now a developer can describe what they want to Claude Code and have a working app in an afternoon. A custom object, a scoring workflow, a new view, an integration. The bottleneck isn't building anymore. It's whether your platform lets you.`,
|
||||
)}
|
||||
</Editorial.Body>
|
||||
</Editorial.Root>
|
||||
|
||||
<Marquee.Root
|
||||
scheme="dark"
|
||||
segments={[
|
||||
{ fontFamily: 'serif', text: i18n._(msg`Same CRM`) },
|
||||
{ fontFamily: 'sans', text: i18n._(msg`Same output`) },
|
||||
{ fontFamily: 'serif', text: i18n._(msg`Same results`) },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Signoff.Root scheme="dark" page={Pages.WhyTwenty}>
|
||||
<Signoff.Heading page={Pages.WhyTwenty}>
|
||||
<HeadingPart fontFamily="serif">
|
||||
{i18n._(msg`Build a CRM your competitors`)}
|
||||
</HeadingPart>{' '}
|
||||
<HeadingPart fontFamily="sans">{i18n._(msg`can't buy.`)}</HeadingPart>
|
||||
</Signoff.Heading>
|
||||
<Signoff.Body page={Pages.WhyTwenty}>
|
||||
{i18n._(msg`Open-source, AI-ready, and yours to shape.`)}
|
||||
</Signoff.Body>
|
||||
<Signoff.Cta>
|
||||
<LinkButton
|
||||
color="primary"
|
||||
href="https://app.twenty.com/welcome"
|
||||
label={i18n._(msg`Get started`)}
|
||||
variant="contained"
|
||||
/>
|
||||
</Signoff.Cta>
|
||||
</Signoff.Root>
|
||||
<WhyTwentyHero />
|
||||
|
||||
<WhyTwentyShiftEditorial />
|
||||
|
||||
<WhyTwentyMeaningEditorial />
|
||||
|
||||
<WhyTwentyOpportunityEditorial />
|
||||
|
||||
<WhyTwentyMarquee />
|
||||
|
||||
<WhyTwentySignoff />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { SOURCE_LOCALE, type AppLocale } from 'twenty-shared/translations';
|
||||
|
||||
import { PUBLIC_APP_LOCALE_LIST, localeToUrlSegment } from '@/lib/i18n';
|
||||
import { WEBSITE_LOCALE_LIST, localeToUrlSegment } from '@/lib/i18n';
|
||||
import { getSiteUrl } from '@/lib/seo';
|
||||
import {
|
||||
getIndexedWebsiteRoutes,
|
||||
@@ -19,7 +19,7 @@ const buildLocalizedUrl = (locale: AppLocale, path: string): string => {
|
||||
};
|
||||
|
||||
const getRouteLocales = (route: WebsiteRoute): readonly AppLocale[] =>
|
||||
route.localeMode === 'source' ? [SOURCE_LOCALE] : PUBLIC_APP_LOCALE_LIST;
|
||||
route.localeMode === 'source' ? [SOURCE_LOCALE] : WEBSITE_LOCALE_LIST;
|
||||
|
||||
const buildLanguageAlternates = (
|
||||
path: string,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user