diff --git a/packages/twenty-website/jest.config.mjs b/packages/twenty-website/jest.config.mjs index b1662645a9..04bf99188b 100644 --- a/packages/twenty-website/jest.config.mjs +++ b/packages/twenty-website/jest.config.mjs @@ -40,6 +40,7 @@ const jestConfig = { ], }, moduleNameMapper: { + '^server-only$': '/jest/server-only-stub.ts', ...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { prefix: '/', }), diff --git a/packages/twenty-website/jest/server-only-stub.ts b/packages/twenty-website/jest/server-only-stub.ts new file mode 100644 index 0000000000..55493fc8aa --- /dev/null +++ b/packages/twenty-website/jest/server-only-stub.ts @@ -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 {}; diff --git a/packages/twenty-website/lingui.config.ts b/packages/twenty-website/lingui.config.ts index 215989deb6..7393f65cbe 100644 --- a/packages/twenty-website/lingui.config.ts +++ b/packages/twenty-website/lingui.config.ts @@ -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, diff --git a/packages/twenty-website/next.config.ts b/packages/twenty-website/next.config.ts index 1fc51e3310..8e660117db 100644 --- a/packages/twenty-website/next.config.ts +++ b/packages/twenty-website/next.config.ts @@ -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. diff --git a/packages/twenty-website/scripts/check-boundaries.mjs b/packages/twenty-website/scripts/check-boundaries.mjs index 3b77818510..7224db7bdb 100644 --- a/packages/twenty-website/scripts/check-boundaries.mjs +++ b/packages/twenty-website/scripts/check-boundaries.mjs @@ -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([ diff --git a/packages/twenty-website/scripts/check-section-shape.mjs b/packages/twenty-website/scripts/check-section-shape.mjs index 8f8106f70f..6c829acd7e 100644 --- a/packages/twenty-website/scripts/check-section-shape.mjs +++ b/packages/twenty-website/scripts/check-section-shape.mjs @@ -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 — 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
in components/Root.tsx. path.join(sectionDir, 'components', 'Root.tsx'), + // Single-file sections own it in
.tsx (e.g. TrustedBy/TrustedBy.tsx). path.join(sectionDir, `${sectionName}.tsx`), path.join(sectionDir, `${sectionName}.ts`), + // Flat-primitive sections own it in a
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 = []; diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeFeatureThreeCards.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeFeatureThreeCards.tsx new file mode 100644 index 0000000000..8a3097ce06 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeFeatureThreeCards.tsx @@ -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 ( + + + + + Skip the clunky UX that always comes with custom. + + + + + + Make your GTM team happy + +
+ with + a CRM they'll love +
+
+
+ +
+ ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx new file mode 100644 index 0000000000..d9a77f5714 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeHero.tsx @@ -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 ( + + + + } + > + + + + + + Build your Enterprise CRM + + at AI Speed + + + + + Twenty gives technical teams the building blocks for a custom CRM + that meets complex business needs and quickly adapts as the + business evolves. + + + + + + + + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeIllustrationThreeCards.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeIllustrationThreeCards.tsx new file mode 100644 index 0000000000..60d1516e47 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeIllustrationThreeCards.tsx @@ -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 ( + + + + + + + Stop settling for trade-offs. + + + + + + Assemble, iterate and adapt a robust CRM, + + + that's quick to flex + + + + + + + Compose your CRM and internal apps with a single extensibility + toolkit. + + + + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeProblem.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeProblem.tsx new file mode 100644 index 0000000000..130c435eb6 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeProblem.tsx @@ -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: ( + + The Giant Monolith + + ), + body: msg`Proprietary languages, slow deployment cycles, and "black box" logic.`, + }, + { + heading: ( + + The In-house Burden + + ), + body: msg`It's fragile. V1 ships quickly, but maintaining and making changes is a long term burden.`, + }, + ]; + + return ( + + + + + + The Problem. + + + + + + A custom CRM gives your org an edge, + + but building one + comes with + tradeoffs + + + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeStepperSection.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeStepperSection.tsx new file mode 100644 index 0000000000..f9b515e404 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeStepperSection.tsx @@ -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: ( + + + + Begin with production-grade + + building blocks + + + ), + body: msg`Compose your CRM and internal apps with a single extensibility toolkit. Data model, layout, and automation.`, + }, + { + heading: ( + + + Continue iteration + without friction + + + ), + 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: ( + + + + Stay in control with our + + open-source software + + + ), + 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 ; +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/_components/HomeTestimonials.tsx b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeTestimonials.tsx new file mode 100644 index 0000000000..0887bf6990 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/(home)/_components/HomeTestimonials.tsx @@ -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 ( + + + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/(home)/page.tsx b/packages/twenty-website/src/app/[locale]/(home)/page.tsx index f18c4a04ae..eecae88495 100644 --- a/packages/twenty-website/src/app/[locale]/(home)/page.tsx +++ b/packages/twenty-website/src/app/[locale]/(home)/page.tsx @@ -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; }; @@ -101,66 +28,6 @@ export default async function HomePage({ params }: HomePageProps) { fetchCommunityStats(), ]); - const PROBLEM_POINTS: ProblemPointType[] = [ - { - heading: ( - - {i18n._(msg`The Giant Monolith`)} - - ), - body: msg`Proprietary languages, slow deployment cycles, and "black box" logic.`, - }, - { - heading: ( - - {i18n._(msg`The In-house Burden`)} - - ), - body: msg`It's fragile. V1 ships quickly, but maintaining and making changes is a long term burden.`, - }, - ]; - - const HOME_STEPPER_STEPS: HomeStepperStepType[] = [ - { - heading: ( - - - {i18n._(msg`Begin with production-grade`)} - {' '} - - {i18n._(msg`building blocks`)} - - - ), - body: msg`Compose your CRM and internal apps with a single extensibility toolkit. Data model, layout, and automation.`, - }, - { - heading: ( - - - {i18n._(msg`Continue iteration`)} - {' '} - - {i18n._(msg`without friction`)} - - - ), - 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: ( - - - {i18n._(msg`Stay in control with our`)} - {' '} - - {i18n._(msg`open-source software`)} - - - ), - 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" /> - - - - - - - - - - - - - {i18n._(msg`Build your Enterprise CRM`)} - {' '} - - {i18n._(msg`at\u00A0AI\u00A0Speed`)} - - - - {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.`, - )} - - - - - - - - - - - - - - - - - {i18n._(msg`The Problem.`)} - - - - - {i18n._(msg`A custom CRM gives your org an edge,`)} - {' '} - - {i18n._(msg`but building one`)} - {' '} - - {i18n._(msg`comes with`)} - {' '} - - {i18n._(msg`tradeoffs`)} - - - - - + - - - - - - - {i18n._(msg`Stop settling for trade-offs.`)} - - - - - {i18n._(msg`Assemble, iterate and adapt a robust CRM,`)} - {' '} - - {i18n._(msg`that's quick to flex`)} - - - - - {i18n._( - msg`Compose your CRM and internal apps with a single extensibility toolkit.`, - )} - - - - - + - + - - - - - {i18n._(msg`Skip the clunky UX that always comes with custom.`)} - - - - - {i18n._(msg`Make your GTM team happy`)} - -
- - {i18n._(msg`with`)} - {' '} - - {i18n._(msg`a CRM they'll love`)} - -
-
- -
+ - - - + - - - - - + - - - - - {i18n._(msg`Any Questions?`)} - - - - - {i18n._(msg`Stop fighting custom.`)} - -
- - {i18n._(msg`Start building, with Twenty`)} - -
- - - - -
- -
+ + + + + ); } diff --git a/packages/twenty-website/src/app/[locale]/articles/[slug]/page.tsx b/packages/twenty-website/src/app/[locale]/articles/[slug]/page.tsx index 86dbd30539..5fe88633ba 100644 --- a/packages/twenty-website/src/app/[locale]/articles/[slug]/page.tsx +++ b/packages/twenty-website/src/app/[locale]/articles/[slug]/page.tsx @@ -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 ( <> - - - - - - - + /> + ); } diff --git a/packages/twenty-website/src/app/[locale]/articles/_components/ArticlesHero.tsx b/packages/twenty-website/src/app/[locale]/articles/_components/ArticlesHero.tsx new file mode 100644 index 0000000000..4bfe000755 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/articles/_components/ArticlesHero.tsx @@ -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 ( + + + Ideas on +
+ open-source + CRM +
+ + Ideas from the team building Twenty on open source CRM, GTM systems, and + building software that lasts. + +
+ ); +} diff --git a/packages/twenty-website/src/app/[locale]/articles/page.tsx b/packages/twenty-website/src/app/[locale]/articles/page.tsx index 486d8d29b4..e7df6bba64 100644 --- a/packages/twenty-website/src/app/[locale]/articles/page.tsx +++ b/packages/twenty-website/src/app/[locale]/articles/page.tsx @@ -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 ? ( ) : null} - - - - - - + />
- - - Ideas on -
- open-source{' '} - CRM -
- - Ideas from the team building Twenty on open source CRM, GTM systems, - and building software that lasts. - -
- +
- + ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx b/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx index 4ade697420..85c76b68cf 100644 --- a/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/9dots/page.tsx @@ -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({ {i18n._(msg`A real estate agency on WhatsApp`)} - {' '} + {i18n._(msg`built a`)}
- {i18n._(msg`CRM`)}{' '} + {i18n._(msg`CRM`)} {i18n._(msg`around it`)}
), @@ -71,7 +68,7 @@ export default async function NineDotsCaseStudyPage({ {i18n._(msg`When the channel is`)} - {' '} + {i18n._(msg`the business`)} @@ -92,7 +89,7 @@ export default async function NineDotsCaseStudyPage({ {i18n._(msg`The CRM as a`)} - {' '} + {i18n._(msg`control hub`)} @@ -112,7 +109,7 @@ export default async function NineDotsCaseStudyPage({ {i18n._(msg`150 hours`)} - {' '} + {i18n._(msg`saved every month`)} @@ -135,5 +132,10 @@ export default async function NineDotsCaseStudyPage({ }, }; - return ; + return ( + + ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/_components/CaseStudyPageLayout.tsx b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudyPageLayout.tsx similarity index 69% rename from packages/twenty-website/src/app/[locale]/customers/_components/CaseStudyPageLayout.tsx rename to packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudyPageLayout.tsx index 2a0ae8bf80..f081ef9dda 100644 --- a/packages/twenty-website/src/app/[locale]/customers/_components/CaseStudyPageLayout.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudyPageLayout.tsx @@ -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 ( - - - - - - - + /> - - - {sectionBlocks} + {sectionBlocks} - + ); diff --git a/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx index 1783a952f6..9c4dd64d18 100644 --- a/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCaseStudySignoff.tsx @@ -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 ( - - + - {i18n._(msg`Ready to grow\nwith Twenty?`)} + + Ready to grow + + with Twenty? + - - - {i18n._( - msg`Join the teams that chose to own their CRM.\nStart building with Twenty today.`, - )} - - - - - - + } + body={ + + Join the teams that chose to own their CRM. + + Start building with Twenty today. + + } + > + + + ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogHero.tsx b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogHero.tsx new file mode 100644 index 0000000000..a62183315d --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogHero.tsx @@ -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 ( + + + + See how teams +
+ build + on Twenty +
+
+ + + Real stories from real teams about how they shaped Twenty to fit their + workflow and accelerated their growth. + + +
+ ); +} diff --git a/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogSignoff.tsx b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogSignoff.tsx new file mode 100644 index 0000000000..cbdfb2bfdb --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/customers/_components/CustomersCatalogSignoff.tsx @@ -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 ( + + Ready to build +
+ your own story? + + } + body={ + + Join the teams that chose to own their CRM. + + Start building with Twenty today. + + } + > + + +
+ ); +} diff --git a/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx b/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx index 81941d6cfe..9e0e0eb117 100644 --- a/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/act-education/page.tsx @@ -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({ {i18n._(msg`When the vendor`)} - {' '} + {i18n._(msg`pulled the plug`)} @@ -82,7 +79,7 @@ export default async function ActEducationCaseStudyPage({ {i18n._(msg`No more renting someone else's`)} - {' '} + {i18n._(msg`structure`)} @@ -101,7 +98,7 @@ export default async function ActEducationCaseStudyPage({ {i18n._(msg`Control without`)} - {' '} + {i18n._(msg`the overhead`)} @@ -119,7 +116,7 @@ export default async function ActEducationCaseStudyPage({ {i18n._(msg`Costs down more than`)} - {' '} + {i18n._(msg`90%`)} ), @@ -142,7 +139,7 @@ export default async function ActEducationCaseStudyPage({ }; return ( - diff --git a/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx b/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx index 932ded4797..4012f0f1dc 100644 --- a/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/alternative-partners/page.tsx @@ -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({ {i18n._(msg`AI in the`)} - {' '} + {i18n._(msg`migration workflow`)} @@ -84,7 +81,7 @@ export default async function AlternativePartnersCaseStudyPage({ {i18n._(msg`Self-hosted`)} - {' '} + {i18n._(msg`means control`)} @@ -106,7 +103,7 @@ export default async function AlternativePartnersCaseStudyPage({ }; return ( - diff --git a/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx b/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx index d4880c51d7..1a0bbb6717 100644 --- a/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/elevate-consulting/page.tsx @@ -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({
{i18n._(msg`API backbone`)} - {' '} + {i18n._(msg`of a go-to-market stack`)} @@ -73,7 +70,7 @@ export default async function ElevateConsultingCaseStudyPage({ {i18n._(msg`From documents to`)} - {' '} + {i18n._(msg`open APIs`)} @@ -92,8 +89,8 @@ export default async function ElevateConsultingCaseStudyPage({ eyebrow: msg`Integration`, heading: ( - {i18n._(msg`One`)}{' '} - {i18n._(msg`API`)}{' '} + {i18n._(msg`One`)} + {i18n._(msg`API`)} {i18n._(msg`to rule them all`)} @@ -114,7 +111,7 @@ export default async function ElevateConsultingCaseStudyPage({ {i18n._(msg`Workflows that`)} - {' '} + {i18n._(msg`actually get used`)} @@ -131,7 +128,7 @@ export default async function ElevateConsultingCaseStudyPage({ eyebrow: msg`What is next`, heading: ( - {i18n._(msg`Beyond`)}{' '} + {i18n._(msg`Beyond`)} {i18n._(msg`internal rollout`)} @@ -156,7 +153,7 @@ export default async function ElevateConsultingCaseStudyPage({ }; return ( - diff --git a/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx b/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx index 7b0f751983..0460f63a9e 100644 --- a/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/netzero/page.tsx @@ -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`)}
- {i18n._(msg`grows`)}{' '} + {i18n._(msg`grows`)} {i18n._(msg`with you`)}
), @@ -64,7 +61,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`The right`)} - {' '} + {i18n._(msg`foundation`)} @@ -83,7 +80,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`A business that does not fit a`)} - {' '} + {i18n._(msg`template`)} ), @@ -101,7 +98,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`From simple to`)} - {' '} + {i18n._(msg`advanced`)} ), @@ -118,7 +115,7 @@ export default async function NetZeroCaseStudyPage({ {i18n._(msg`The bet is`)} - {' '} + {i18n._(msg`paying off`)} @@ -143,6 +140,9 @@ export default async function NetZeroCaseStudyPage({ }; return ( - + ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/page.tsx b/packages/twenty-website/src/app/[locale]/customers/page.tsx index 0612ce01f2..bc99e1f063 100644 --- a/packages/twenty-website/src/app/[locale]/customers/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/page.tsx @@ -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 ( <> - - - - - - + />
- - - - {i18n._(msg`See how teams`)} - -
- - {i18n._(msg`build`)} - {' '} - - {i18n._(msg`on Twenty`)} - -
- - {i18n._( - msg`Real stories from real teams about how they shaped Twenty to fit their workflow and accelerated their growth.`, - )} - -
- +
- + - - - - {i18n._(msg`Ready to build`)} - -
- - {i18n._(msg`your own story?`)} - -
- - {i18n._( - msg`Join the teams that chose to own their CRM.\nStart building with Twenty today.`, - )} - - - - - -
+ - - - - - {i18n._(msg`Any Questions?`)} - - - - - {i18n._(msg`Stop fighting custom.`)} - -
- - {i18n._(msg`Start building, with Twenty`)} - -
- - - - -
- -
+ ); } diff --git a/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx b/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx index c7ad5f1576..0f3451f552 100644 --- a/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx +++ b/packages/twenty-website/src/app/[locale]/customers/w3villa/page.tsx @@ -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({ {i18n._(msg`Scale without`)} - {' '} + {i18n._(msg`breaking operations`)} @@ -82,7 +79,7 @@ export default async function W3villaCaseStudyPage({ {i18n._(msg`Focus on the use case, not the`)} - {' '} + {i18n._(msg`plumbing`)} ), @@ -99,7 +96,7 @@ export default async function W3villaCaseStudyPage({ {i18n._(msg`A platform ready to`)} - {' '} + {i18n._(msg`grow`)} ), @@ -116,7 +113,7 @@ export default async function W3villaCaseStudyPage({ {i18n._(msg`Zero manual work`)} - {' '} + {i18n._(msg`at the core`)} @@ -141,6 +138,9 @@ export default async function W3villaCaseStudyPage({ }; return ( - + ); } diff --git a/packages/twenty-website/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx b/packages/twenty-website/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx index bd02d9ffe8..0f0ca3f0f8 100644 --- a/packages/twenty-website/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx +++ b/packages/twenty-website/src/app/[locale]/enterprise/activate/EnterpriseActivateClient.tsx @@ -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() { {loading && ( - {i18n._(msg`Activating your enterprise license…`)} + Activating your enterprise license… )} @@ -168,14 +169,14 @@ export function EnterpriseActivateClient() { {result !== null && ( <> - {i18n._( - msg`Your enterprise license has been activated successfully.`, - )} + + Your enterprise license has been activated successfully. + - {i18n._(msg`Licensee:`)} + Licensee: {' '} {result.licensee} @@ -183,13 +184,14 @@ export function EnterpriseActivateClient() { - {i18n._(msg`Your enterprise key`)} + Your enterprise key - {i18n._( - msg`Copy this key and paste it into your Twenty self-hosted instance settings.`, - )} + + Copy this key and paste it into your Twenty self-hosted instance + settings. + @@ -215,25 +217,25 @@ export function EnterpriseActivateClient() { - {i18n._(msg`Next steps`)} + Next steps
  • - {i18n._(msg`Copy the enterprise key above.`)} + Copy the enterprise key above.
  • - {i18n._( - msg`Open your Twenty self-hosted instance Settings → Enterprise.`, - )} + + Open your Twenty self-hosted instance Settings → Enterprise. +
  • - {i18n._(msg`Paste the key and click Activate.`)} + Paste the key and click Activate.
  • diff --git a/packages/twenty-website/src/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero.tsx b/packages/twenty-website/src/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero.tsx new file mode 100644 index 0000000000..d4c68f53a9 --- /dev/null +++ b/packages/twenty-website/src/app/[locale]/enterprise/activate/_components/EnterpriseActivateHero.tsx @@ -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 ( + + + + Self-hosting + + + + + Enterprise + activation + + + + + Your checkout is complete. Follow the steps below to copy your license + key into your Twenty instance. + + + + ); +} diff --git a/packages/twenty-website/src/app/[locale]/enterprise/activate/page.tsx b/packages/twenty-website/src/app/[locale]/enterprise/activate/page.tsx index dceb039881..71c8856cf5 100644 --- a/packages/twenty-website/src/app/[locale]/enterprise/activate/page.tsx +++ b/packages/twenty-website/src/app/[locale]/enterprise/activate/page.tsx @@ -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 ( <> - - - - - - + - - - - {i18n._(msg`Self-hosting`)} - - - - - {i18n._(msg`Enterprise`)} - {' '} - {i18n._(msg`activation`)} - - - {i18n._( - msg`Your checkout is complete. Follow the steps below to copy your license key into your Twenty instance.`, - )} - - + diff --git a/packages/twenty-website/src/app/[locale]/layout.tsx b/packages/twenty-website/src/app/[locale]/layout.tsx index e5d34fcad7..c31bcdc082 100644 --- a/packages/twenty-website/src/app/[locale]/layout.tsx +++ b/packages/twenty-website/src/app/[locale]/layout.tsx @@ -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 ({ {children} - - - - - +