More parts of the new website. (#19094)

This PR brings in a few more components and some refactor of the
existing components for the website due to design requirements.
This commit is contained in:
Abdullah.
2026-03-30 09:28:43 +00:00
committed by GitHub
parent 0b2f435b3e
commit 4be934a192
165 changed files with 4469 additions and 287 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 930 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

@@ -0,0 +1,71 @@
import { FaqDataType } from '@/sections/Faq/types';
export const FAQ_DATA: FaqDataType = {
illustration: {
src: 'https://app.endlesstools.io/embed/0510f6d4-e09b-4470-86f8-7fb8d9db4c45',
title: 'Endless Tools Editor',
},
eyebrow: {
heading: {
text: 'Any Questions?',
fontFamily: 'sans',
},
},
heading: [
{
text: 'Stop fighting custom.\n',
fontFamily: 'serif',
},
{
text: ' Start building, with Twenty',
fontFamily: 'sans',
},
],
questions: [
{
question: {
text: 'How long does it take to get started?',
fontFamily: 'sans',
},
answer: {
text: "Toute personne souhaitant investir dans l'immobilier de luxe, qu'elle soit expérimentée ou novice, peut soumettre une candidature pour rejoindre le Kretz Club.",
},
},
{
question: {
text: 'Can I migrate from Salesforce or HubSpot?',
fontFamily: 'sans',
},
answer: {
text: 'Yes. We provide migration tools and support to help you move your data from Salesforce, HubSpot, and other CRMs into Twenty.',
},
},
{
question: {
text: 'Do I need a developer to customize Twenty?',
fontFamily: 'sans',
},
answer: {
text: 'Twenty is designed to be customizable without code. For advanced customizations, our API and documentation support developer-led extensions.',
},
},
{
question: {
text: 'Is my data secure?',
fontFamily: 'sans',
},
answer: {
text: 'We take security seriously. Your data is encrypted at rest and in transit, and you can self-host to keep full control.',
},
},
{
question: {
text: 'How does AI usage pricing work?',
fontFamily: 'sans',
},
answer: {
text: 'AI features are billed based on usage. See our pricing page for details and current rates.',
},
},
],
};
@@ -1,6 +1,6 @@
import { HeroDataType } from '@/sections/Hero/types';
import { HeroHomeDataType } from '@/sections/Hero/types';
export const HERO_DATA: HeroDataType = {
export const HERO_DATA: HeroHomeDataType = {
heading: [
{ text: 'Build', fontFamily: 'sans' },
{ text: ' your Enterprise CRM ', fontFamily: 'serif' },
@@ -9,4 +9,12 @@ export const HERO_DATA: HeroDataType = {
body: {
text: 'Twenty gives technical teams the building blocks for a custom CRM that meets complex business needs and quickly adapts as the business evolves.',
},
background: {
src: '/images/home/hero/background.png',
alt: '',
},
foreground: {
src: '/images/home/hero/foreground.png',
alt: '',
},
};
@@ -0,0 +1,43 @@
import type { MenuDataType } from '@/sections/Menu/types';
export const MENU_DATA: MenuDataType = {
navItems: [
{ label: 'Product', href: '/product' },
{ label: 'Pricing', href: '/pricing' },
{ label: 'Partners', href: '/partner' },
{ label: 'Why Twenty', href: '/why-twenty' },
],
socialLinks: [
{
ariaLabel: 'GitHub (opens in new tab)',
href: 'https://github.com/twentyhq/twenty',
icon: 'github',
label: '39.8K',
showInDesktop: true,
showInDrawer: true,
},
{
ariaLabel: 'Discord (opens in new tab)',
className: 'discord-link',
href: 'https://discord.gg/cx5n4Jzs57',
icon: 'discord',
label: '5.6K',
showInDesktop: true,
showInDrawer: true,
},
{
ariaLabel: 'LinkedIn (opens in new tab)',
href: 'https://www.linkedin.com/company/twenty',
icon: 'linkedin',
showInDesktop: false,
showInDrawer: true,
},
{
ariaLabel: 'X (opens in new tab)',
href: 'https://x.com/twentycrm',
icon: 'x',
showInDesktop: false,
showInDrawer: true,
},
],
};
@@ -0,0 +1,101 @@
import type { HeroStepperDataType } from '@/sections/HeroStepper/types';
export const STEPPER_DATA: HeroStepperDataType = {
steps: [
{
heading: [
{
text: 'Begin with production-grade',
fontFamily: 'serif',
},
{
text: ' building blocks',
fontFamily: 'sans',
},
],
body: {
text: 'Compose your CRM and internal apps with a single extensibility toolkit. Data model, layout, and automation.',
},
images: [
{
src: '/images/home/stepper/step-one-one.png',
alt: 'Twenty modules for logic, data model, and layout',
},
{
src: '/images/home/stepper/step-one-two.png',
alt: 'Twenty data model and layout building blocks',
},
{
src: '/images/home/stepper/step-one-three.png',
alt: 'Twenty automation and extensibility toolkit',
},
],
},
{
heading: [
{
text: 'Continue iteration',
fontFamily: 'serif',
},
{
text: ' without friction',
fontFamily: 'sans',
},
],
body: {
text: 'Enjoy unlimited customization using the AI coding tools you already love. Adapt your CRM to fit the way your business grows and wins.',
},
images: [
{
src: '/images/home/stepper/step-two-one.png',
alt: 'Twenty CRM adapted with familiar development tools',
},
{
src: '/images/home/stepper/step-two-two.png',
alt: 'Customizing Twenty with AI-assisted coding workflows',
},
{
src: '/images/home/stepper/step-two-three.png',
alt: 'Iterating on Twenty as your business grows',
},
{
src: '/images/home/stepper/step-two-four.png',
alt: 'Twenty workspace tailored to your team',
},
],
},
{
heading: [
{
text: 'Stay in control with our',
fontFamily: 'serif',
},
{
text: ' open-source software',
fontFamily: 'sans',
},
],
body: {
text: "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.",
},
images: [
{
src: '/images/home/stepper/step-three-one.png',
alt: 'Twenty open-source CRM in a normal local development setup',
},
{
src: '/images/home/stepper/step-three-two.png',
alt: 'Twenty developer experience with real data and live testing',
},
{
src: '/images/home/stepper/step-three-three.png',
alt: 'Twenty without proprietary vendor lock-in',
},
{
src: '/images/home/stepper/step-three-four.png',
alt: 'Twenty as transparent, self-hosted software',
},
],
},
],
};
@@ -17,18 +17,30 @@ export const THREE_CARDS_FEATURE_DATA: ThreeCardsFeatureCardsDataType = {
body: {
text: "Twenty makes it simple. It's clean, intuitive, and built to feel like Notion.",
},
image: {
src: '/images/home/three-cards-feature/familiar-interface.png',
alt: 'Familiar interface',
},
},
{
heading: { text: 'Fast path to action', fontFamily: 'sans' },
body: {
text: 'Smart patterns, shortcuts, and layouts make everyday tasks faster and easier to execute.',
},
image: {
src: '/images/home/three-cards-feature/fast-path.png',
alt: 'Fast path',
},
},
{
heading: { text: 'Live data and AI Built', fontFamily: 'sans' },
body: {
text: 'Everything updates in real time, with AI chat always ready to help you move faster.',
},
image: {
src: '/images/home/three-cards-feature/live-data.png',
alt: 'Live data',
},
},
],
};
@@ -22,6 +22,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
role: { text: 'Head of Engineering' },
company: { text: 'Mid-Market Fintech' },
},
illustration: {
src: 'https://app.endlesstools.io/embed/ea42603a-8644-4634-8b7a-9b7bd9a8d899',
title: 'Endless Tools Editor',
},
},
{
heading: { text: 'AI for rapid iterations', fontFamily: 'sans' },
@@ -33,6 +37,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
role: { text: 'Head of Engineering' },
company: { text: 'Mid-Market Fintech' },
},
illustration: {
src: 'https://app.endlesstools.io/embed/99a4f00e-bb70-4192-82e1-24e9ce8cd136',
title: 'Endless Tools Editor',
},
},
{
heading: { text: 'Control without drag', fontFamily: 'sans' },
@@ -44,6 +52,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
role: { text: 'Head of Engineering' },
company: { text: 'Mid-Market Fintech' },
},
illustration: {
src: 'https://app.endlesstools.io/embed/ba47c943-9565-412e-ae45-4ec092c149aa',
title: 'Endless Tools Editor',
},
},
],
};
@@ -1,11 +1,15 @@
import { FAQ_DATA } from '@/app/(home)/constants/faq';
import { HERO_DATA } from '@/app/(home)/constants/hero';
import { MENU_DATA } from '@/app/(home)/constants/menu';
import { PROBLEM_DATA } from '@/app/(home)/constants/problem';
import { THREE_CARDS_FEATURE_DATA } from '@/app/(home)/constants/three-cards-feature';
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/(home)/constants/three-cards-illustration';
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { Faq } from '@/sections/Faq/components';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { Problem } from '@/sections/Problem/components';
import { ThreeCards } from '@/sections/ThreeCards/components';
import { TrustedBy } from '@/sections/TrustedBy/components';
@@ -14,6 +18,18 @@ import { theme } from '@/theme';
export default function HomePage() {
return (
<>
<Menu.Root
backgroundColor={theme.colors.primary.background[100]}
scheme="primary"
navItems={MENU_DATA.navItems}
socialLinks={MENU_DATA.socialLinks}
>
<Menu.Logo scheme="primary" />
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="primary" socialLinks={MENU_DATA.socialLinks} />
<Menu.Cta scheme="primary" />
</Menu.Root>
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
<Hero.Heading page={Pages.Home} segments={HERO_DATA.heading} />
<Hero.Body page={Pages.Home} body={HERO_DATA.body} size="sm" />
@@ -26,7 +42,10 @@ export default function HomePage() {
variant="contained"
/>
</Hero.Cta>
<Hero.HomeVisual />
<Hero.HomeVisual
background={HERO_DATA.background}
foreground={HERO_DATA.foreground}
/>
</Hero.Root>
<TrustedBy.Root>
@@ -67,6 +86,8 @@ export default function HomePage() {
/>
</ThreeCards.Root>
{/* <HeroStepper.Flow steps={STEPPER_DATA.steps} /> */}
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
<ThreeCards.Intro page={Pages.Home} align="center">
<Eyebrow
@@ -83,6 +104,33 @@ export default function HomePage() {
featureCards={THREE_CARDS_FEATURE_DATA.featureCards}
/>
</ThreeCards.Root>
<Faq.Root illustration={FAQ_DATA.illustration}>
<Faq.Intro>
<Eyebrow
colorScheme="secondary"
heading={FAQ_DATA.eyebrow.heading}
/>
<Faq.Heading segments={FAQ_DATA.heading} />
<Faq.Cta>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<LinkButton
color="primary"
href="https://twenty.com/contact"
label="Talk to us"
type="anchor"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
</>
);
}
@@ -1,5 +1,4 @@
import { Footer } from '@/sections/Footer/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
import { cssVariables } from '@/theme/css-variables';
import { css } from '@linaria/core';
@@ -69,12 +68,6 @@ export default function RootLayout({
<body
className={`${cssVariables} ${hostGrotesk.variable} ${aleo.variable} ${azeretMono.variable}`}
>
<Menu.Root>
<Menu.Logo />
<Menu.Nav />
<Menu.Social />
<Menu.Cta />
</Menu.Root>
<StyledMain>{children}</StyledMain>
<Footer.Root>
<Footer.Logo />
@@ -0,0 +1,11 @@
import type { EngagementBandDataType } from '@/sections/EngagementBand/types';
export const ENGAGEMENT_BAND_DATA: EngagementBandDataType = {
heading: {
text: 'Looking for a partners',
fontFamily: 'serif',
},
body: {
text: 'Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor.',
},
};
@@ -1,6 +1,6 @@
import { HeroDataType } from '@/sections/Hero/types';
import { HeroIllustrationDataType } from '@/sections/Hero/types';
export const HERO_DATA: HeroDataType = {
export const HERO_DATA: HeroIllustrationDataType = {
heading: [
{ text: 'Become a ', fontFamily: 'serif' },
{ text: 'Twenty Partner', fontFamily: 'sans' },
@@ -8,4 +8,8 @@ export const HERO_DATA: HeroDataType = {
body: {
text: "We're building the #1 open-source CRM, but we can't do it alone. Join our partner ecosystem and grow with us.",
},
illustration: {
src: 'https://app.endlesstools.io/embed/1c6c8259-3276-4cf2-84d8-6b7e87e7ec95',
title: 'Endless Tools Editor',
},
};
@@ -24,6 +24,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
{ text: 'Soon: earn revenue' },
],
attribution: undefined,
illustration: {
src: 'https://app.endlesstools.io/embed/627cb43a-5b5f-41a7-9642-26e66b9c4890',
title: 'Endless Tools Editor',
},
},
{
heading: { text: 'Content & Community Partners', fontFamily: 'sans' },
@@ -36,6 +40,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
{ text: 'Marketing assets & brand resources' },
],
attribution: undefined,
illustration: {
src: 'https://app.endlesstools.io/embed/4ef11ab1-d013-46dd-bf2e-cbf913370a8a',
title: 'Endless Tools Editor',
},
},
{
heading: { text: 'Solutions Partners', fontFamily: 'sans' },
@@ -48,6 +56,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
{ text: 'Dedicated partner support' },
],
attribution: undefined,
illustration: {
src: 'https://app.endlesstools.io/embed/624e9b40-f52c-4da2-9a2e-72d1fb2b5356',
title: 'Endless Tools Editor',
},
},
],
};
@@ -1,9 +1,15 @@
import { FAQ_DATA } from '@/app/(home)/constants/faq';
import { MENU_DATA } from '@/app/(home)/constants/menu';
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
import { ENGAGEMENT_BAND_DATA } from '@/app/partner/constants/engagement-band';
import { HERO_DATA } from '@/app/partner/constants/hero';
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/partner/constants/three-cards-illustration';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { EngagementBand } from '@/sections/EngagementBand/components';
import { Faq } from '@/sections/Faq/components';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { ThreeCards } from '@/sections/ThreeCards/components';
import { TrustedBy } from '@/sections/TrustedBy/components';
import { theme } from '@/theme';
@@ -11,6 +17,18 @@ import { theme } from '@/theme';
export default function PartnerPage() {
return (
<>
<Menu.Root
backgroundColor={theme.colors.primary.background[100]}
scheme="primary"
navItems={MENU_DATA.navItems}
socialLinks={MENU_DATA.socialLinks}
>
<Menu.Logo scheme="primary" />
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="primary" socialLinks={MENU_DATA.socialLinks} />
<Menu.Cta scheme="primary" />
</Menu.Root>
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
<Hero.Heading page={Pages.Partner} segments={HERO_DATA.heading} />
<Hero.Body page={Pages.Partner} body={HERO_DATA.body} />
@@ -31,8 +49,7 @@ export default function PartnerPage() {
/>
</Hero.Cta>
<Hero.Illustration
src="https://app.endlesstools.io/embed/1c6c8259-3276-4cf2-84d8-6b7e87e7ec95"
title="Endless Tools Editor"
illustration={HERO_DATA.illustration}
backgroundColor={theme.colors.secondary.background[100]}
/>
</Hero.Root>
@@ -45,8 +62,31 @@ export default function PartnerPage() {
/>
</TrustedBy.Root>
<EngagementBand.Root
backgroundColor={theme.colors.primary.background[100]}
>
<EngagementBand.Strip
fillColor={theme.colors.secondary.background[100]}
variant="secondary"
>
<EngagementBand.Copy>
<EngagementBand.Heading segments={ENGAGEMENT_BAND_DATA.heading} />
<EngagementBand.Body body={ENGAGEMENT_BAND_DATA.body} />
</EngagementBand.Copy>
<EngagementBand.Actions>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Read our case studies"
type="anchor"
variant="contained"
/>
</EngagementBand.Actions>
</EngagementBand.Strip>
</EngagementBand.Root>
<ThreeCards.Root backgroundColor={theme.colors.secondary.background[5]}>
<ThreeCards.Intro align="left">
<ThreeCards.Intro page={Pages.Partner} align="left">
<Eyebrow
colorScheme="primary"
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
@@ -63,6 +103,30 @@ export default function PartnerPage() {
variant="simple"
/>
</ThreeCards.Root>
<Faq.Root illustration={FAQ_DATA.illustration}>
<Faq.Intro>
<Eyebrow colorScheme="secondary" heading={FAQ_DATA.eyebrow.heading} />
<Faq.Heading segments={FAQ_DATA.heading} />
<Faq.Cta>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<LinkButton
color="primary"
href="https://twenty.com/contact"
label="Talk to us"
type="anchor"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
</>
);
}
@@ -0,0 +1,11 @@
import type { EngagementBandDataType } from '@/sections/EngagementBand/types';
export const ENGAGEMENT_BAND_DATA: EngagementBandDataType = {
heading: {
text: 'Want a tailored made plan?',
fontFamily: 'serif',
},
body: {
text: 'Help customers implement, customize,and succeed with Twenty. Combine sales and services to grow your business.',
},
};
@@ -1,6 +1,6 @@
import { HeroDataType } from '@/sections/Hero/types';
import { HeroBaseDataType } from '@/sections/Hero/types';
export const HERO_DATA: HeroDataType = {
export const HERO_DATA: HeroBaseDataType = {
heading: [
{ text: 'A pricing that scales ', fontFamily: 'serif' },
{ text: 'with your need', fontFamily: 'sans' },
@@ -0,0 +1,131 @@
const PRO_HEADING = { fontFamily: 'sans' as const, text: 'Pro' };
const ORGANIZATION_HEADING = {
fontFamily: 'sans' as const,
text: 'Organization',
};
const PRO_ILLUSTRATION = {
src: 'https://app.endlesstools.io/embed/207b0732-c6ef-414e-add5-462e68689a2e',
title: 'Endless Tools Editor',
};
const ORGANIZATION_ILLUSTRATION = {
src: 'https://app.endlesstools.io/embed/588638be-e22e-4467-b701-3f36e900613d',
title: 'Endless Tools Editor',
};
const PRO_FEATURES_TITLE = { text: 'Key Features' };
const ORGANIZATION_FEATURES_TITLE = { text: 'everything in pro +' };
const PRO_BULLETS_DEFAULT = [
{ text: 'Full customisation' },
{ text: 'AI Agents with custom skills' },
{ text: '1K automation credits' },
{ text: 'Standard support' },
{ text: 'Add-on AI (1-month trial)' },
];
const PRO_BULLETS_SELF_HOST_MONTHLY = [
{ text: 'Full customisation' },
{ text: 'AI Agents with custom skills' },
{ text: 'Standard support' },
{ text: 'Add-on AI (1-month trial)' },
];
const PRO_BULLETS_SELF_HOST_YEARLY = [
{ text: 'Full customisation' },
{ text: 'AI Agents with custom skills' },
{ text: 'Standard support' },
{ text: 'Add-on AI (1-month trial)' },
];
const ORGANIZATION_BULLETS = [
{ text: 'Roles & Permissions' },
{ text: 'SAML/OIDC SSO' },
{ text: '2K automation credits' },
{ text: 'Priority support' },
{ text: 'Add-on AI full time' },
];
import type { PlansDataType } from '@/sections/Plans/types';
export const PLANS_DATA = {
organization: {
cells: {
cloud: {
monthly: {
featureBullets: ORGANIZATION_BULLETS,
price: {
body: { text: '/month, paid monthly' },
heading: { fontFamily: 'sans', text: '$25 USD' },
},
},
yearly: {
featureBullets: ORGANIZATION_BULLETS,
price: {
body: { text: '/month, paid yearly' },
heading: { fontFamily: 'sans', text: '$19 USD' },
},
},
},
selfHost: {
monthly: {
featureBullets: ORGANIZATION_BULLETS,
price: {
body: { text: '/month, paid monthly' },
heading: { fontFamily: 'sans', text: '$25 USD' },
},
},
yearly: {
featureBullets: ORGANIZATION_BULLETS,
price: {
body: { text: '/month, paid yearly' },
heading: { fontFamily: 'sans', text: '$19 USD' },
},
},
},
},
featuresTitle: ORGANIZATION_FEATURES_TITLE,
heading: ORGANIZATION_HEADING,
illustration: ORGANIZATION_ILLUSTRATION,
},
pro: {
cells: {
cloud: {
monthly: {
featureBullets: PRO_BULLETS_DEFAULT,
price: {
body: { text: '/month, paid monthly' },
heading: { fontFamily: 'sans', text: '$12 USD' },
},
},
yearly: {
featureBullets: PRO_BULLETS_DEFAULT,
price: {
body: { text: '/month, paid yearly' },
heading: { fontFamily: 'sans', text: '$9 USD' },
},
},
},
selfHost: {
monthly: {
featureBullets: PRO_BULLETS_SELF_HOST_MONTHLY,
price: {
body: { text: '/month, paid monthly' },
heading: { fontFamily: 'sans', text: '$0 USD' },
},
},
yearly: {
featureBullets: PRO_BULLETS_SELF_HOST_YEARLY,
price: {
body: { text: '/month, paid yearly' },
heading: { fontFamily: 'sans', text: '$0 USD' },
},
},
},
},
featuresTitle: PRO_FEATURES_TITLE,
heading: PRO_HEADING,
illustration: PRO_ILLUSTRATION,
},
} satisfies PlansDataType;
@@ -1,15 +1,86 @@
import { FAQ_DATA } from '@/app/(home)/constants/faq';
import { MENU_DATA } from '@/app/(home)/constants/menu';
import { ENGAGEMENT_BAND_DATA } from '@/app/partner/constants/engagement-band';
import { HERO_DATA } from '@/app/pricing/constants/hero';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { EngagementBand } from '@/sections/EngagementBand/components';
import { Faq } from '@/sections/Faq/components';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { Plans } from '@/sections/Plans/components';
import { theme } from '@/theme';
export default function PricingPage() {
return (
<>
<Menu.Root
backgroundColor={theme.colors.secondary.background[5]}
scheme="primary"
navItems={MENU_DATA.navItems}
socialLinks={MENU_DATA.socialLinks}
>
<Menu.Logo scheme="primary" />
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="primary" socialLinks={MENU_DATA.socialLinks} />
<Menu.Cta scheme="primary" />
</Menu.Root>
<Hero.Root backgroundColor={theme.colors.secondary.background[5]}>
<Hero.Heading page={Pages.Pricing} segments={HERO_DATA.heading} />
<Hero.Body page={Pages.Pricing} body={HERO_DATA.body} />
</Hero.Root>
<Plans.Root backgroundColor={theme.colors.secondary.background[5]}>
<Plans.Content />
</Plans.Root>
<EngagementBand.Root
backgroundColor={theme.colors.secondary.background[5]}
>
<EngagementBand.Strip
fillColor={theme.colors.primary.background[100]}
variant="primary"
>
<EngagementBand.Copy>
<EngagementBand.Heading segments={ENGAGEMENT_BAND_DATA.heading} />
<EngagementBand.Body body={ENGAGEMENT_BAND_DATA.body} />
</EngagementBand.Copy>
<EngagementBand.Actions>
<LinkButton
color="secondary"
href="https://app.twenty.com/welcome"
label="Read our case studies"
type="anchor"
variant="contained"
/>
</EngagementBand.Actions>
</EngagementBand.Strip>
</EngagementBand.Root>
<Faq.Root illustration={FAQ_DATA.illustration}>
<Faq.Intro>
<Eyebrow colorScheme="secondary" heading={FAQ_DATA.eyebrow.heading} />
<Faq.Heading segments={FAQ_DATA.heading} />
<Faq.Cta>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<LinkButton
color="primary"
href="https://twenty.com/contact"
label="Talk to us"
type="anchor"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
</>
);
}
@@ -0,0 +1,24 @@
import type { DemoDataType } from '@/sections/Demo/types';
export const DEMO_DATA: DemoDataType = {
eyebrow: {
heading: {
text: 'Try it live',
fontFamily: 'sans',
},
},
heading: [
{
text: 'A demo worth a',
fontFamily: 'serif',
},
{
text: ' thousand words',
fontFamily: 'sans',
},
],
image: {
src: '/images/product/demo/kanban.png',
alt: '',
},
};
@@ -1,6 +1,6 @@
import { HeroDataType } from '@/sections/Hero/types';
import { HeroIllustrationDataType } from '@/sections/Hero/types';
export const HERO_DATA: HeroDataType = {
export const HERO_DATA: HeroIllustrationDataType = {
heading: [
{ text: 'The CRM that moves ', fontFamily: 'serif' },
{ text: 'as fast as you do', fontFamily: 'sans' },
@@ -8,4 +8,8 @@ export const HERO_DATA: HeroDataType = {
body: {
text: 'Modern interface. AI assistance. All the features you need, ready from day one.',
},
illustration: {
src: 'https://app.endlesstools.io/embed/0bcf3ac2-58cf-4cd5-90bd-e8fada9816a9',
title: 'Endless Tools Editor',
},
};
@@ -0,0 +1,67 @@
import type { ProductStepperDataType } from '@/sections/ProductStepper/types';
export const STEPPER_DATA: ProductStepperDataType = {
eyebrow: {
heading: {
text: 'Customization',
fontFamily: 'sans',
},
},
heading: [
{
text: 'Go the extra mile',
fontFamily: 'serif',
},
{
text: ' with no-code',
fontFamily: 'sans',
},
],
body: {
text: 'Need a quick change? Skip the engineering ticket. Customize your workspace in minutes.',
},
steps: [
{
icon: 'users',
heading: {
text: 'Data model',
fontFamily: 'sans',
},
body: {
text: 'Add objects and fields',
},
image: {
src: '/images/product/stepper/step-one.png',
alt: 'Twenty data model: add objects and fields',
},
},
{
icon: 'check',
heading: {
text: 'Automation',
fontFamily: 'sans',
},
body: {
text: 'Create a workflow',
},
image: {
src: '/images/product/stepper/step-two.png',
alt: 'Twenty automation: create a workflow',
},
},
{
icon: 'eye',
heading: {
text: 'Layout',
fontFamily: 'sans',
},
body: {
text: 'Tailor record pages, menus, and views',
},
image: {
src: '/images/product/stepper/step-three.png',
alt: 'Twenty layout: record pages, menus, and views',
},
},
],
};
@@ -0,0 +1,65 @@
import type { TabsDataType } from '@/sections/Tabs/types';
export const TABS_DATA: TabsDataType = {
eyebrow: {
heading: {
text: 'AI & Automation',
fontFamily: 'sans',
},
},
heading: [
{
text: 'AI that actually\nhelps you ',
fontFamily: 'serif',
},
{
text: 'work faster',
fontFamily: 'sans',
},
],
body: {
text: 'The AI understands your CRM and takes action.',
},
tabs: [
{
body: {
text: 'Show me all deals closing this month',
},
icon: 'search',
image: {
src: '/images/product/tabs/deals.png',
alt: 'Deals view',
},
},
{
body: {
text: 'Create follow-up tasks for my top 10 accounts',
},
icon: 'eye',
image: {
src: '/images/product/tabs/tasks.png',
alt: 'Tasks view',
},
},
{
body: {
text: "Summarize this customer's history",
},
icon: 'edit',
image: {
src: '/images/product/tabs/history.png',
alt: 'History view',
},
},
{
body: {
text: 'Create a workflow that send an email sequence',
},
icon: 'check',
image: {
src: '/images/product/tabs/workflow.png',
alt: 'Workflow view',
},
},
],
};
@@ -17,6 +17,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
},
benefits: undefined,
attribution: undefined,
illustration: {
src: 'https://app.endlesstools.io/embed/21767ad6-67f4-4941-a668-6cf772c27621',
title: 'Endless Tools Editor',
},
},
{
heading: { text: 'Real-time data', fontFamily: 'sans' },
@@ -25,6 +29,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
},
benefits: undefined,
attribution: undefined,
illustration: {
src: 'https://app.endlesstools.io/embed/9c504c9c-46f9-423c-98b3-2e840ddb3b1e',
title: 'Endless Tools Editor',
},
},
{
heading: { text: 'Stay in Flow', fontFamily: 'sans' },
@@ -33,6 +41,10 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
},
benefits: undefined,
attribution: undefined,
illustration: {
src: 'https://app.endlesstools.io/embed/8d577747-2718-4e7e-87a6-dd9c334f3903',
title: 'Endless Tools Editor',
},
},
],
};
@@ -1,9 +1,17 @@
import { FAQ_DATA } from '@/app/(home)/constants/faq';
import { MENU_DATA } from '@/app/(home)/constants/menu';
import { TRUSTED_BY_DATA } from '@/app/(home)/constants/trusted-by';
import { DEMO_DATA } from '@/app/product/constants/demo';
import { HERO_DATA } from '@/app/product/constants/hero';
import { TABS_DATA } from '@/app/product/constants/tabs';
import { THREE_CARDS_ILLUSTRATION_DATA } from '@/app/product/constants/three-cards';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { Demo } from '@/sections/Demo/components';
import { Faq } from '@/sections/Faq/components';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { Tabs } from '@/sections/Tabs/components';
import { ThreeCards } from '@/sections/ThreeCards/components';
import { TrustedBy } from '@/sections/TrustedBy/components';
import { theme } from '@/theme';
@@ -11,6 +19,18 @@ import { theme } from '@/theme';
export default function ProductPage() {
return (
<>
<Menu.Root
backgroundColor={theme.colors.primary.background[100]}
scheme="primary"
navItems={MENU_DATA.navItems}
socialLinks={MENU_DATA.socialLinks}
>
<Menu.Logo scheme="primary" />
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="primary" socialLinks={MENU_DATA.socialLinks} />
<Menu.Cta scheme="primary" />
</Menu.Root>
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
<Hero.Heading page={Pages.Product} segments={HERO_DATA.heading} />
<Hero.Body page={Pages.Product} body={HERO_DATA.body} />
@@ -24,37 +44,89 @@ export default function ProductPage() {
/>
</Hero.Cta>
<Hero.Illustration
src="https://app.endlesstools.io/embed/0bcf3ac2-58cf-4cd5-90bd-e8fada9816a9"
title="Endless Tools Editor"
illustration={HERO_DATA.illustration}
backgroundColor={theme.colors.secondary.background[5]}
/>
<TrustedBy.Root>
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
<TrustedBy.Logos
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
logos={TRUSTED_BY_DATA.logos}
/>
</TrustedBy.Root>
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
<ThreeCards.Intro page={Pages.Product} align="left">
<Eyebrow
colorScheme="primary"
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
/>
<Heading
segments={THREE_CARDS_ILLUSTRATION_DATA.heading}
size="lg"
weight="light"
/>
<Body body={THREE_CARDS_ILLUSTRATION_DATA.body} size="sm" />
</ThreeCards.Intro>
<ThreeCards.IllustrationCards
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
/>
</ThreeCards.Root>
</Hero.Root>
<TrustedBy.Root>
<TrustedBy.Separator separator={TRUSTED_BY_DATA.separator} />
<TrustedBy.Logos
clientCountLabel={TRUSTED_BY_DATA.clientCountLabel}
logos={TRUSTED_BY_DATA.logos}
/>
</TrustedBy.Root>
<Tabs.Root>
<Eyebrow colorScheme="secondary" heading={TABS_DATA.eyebrow.heading} />
<Tabs.Heading segments={TABS_DATA.heading} />
<Tabs.Body body={TABS_DATA.body} />
<Tabs.TabGroup tabs={TABS_DATA.tabs} />
</Tabs.Root>
<ThreeCards.Root backgroundColor={theme.colors.primary.background[100]}>
<ThreeCards.Intro page={Pages.Product} align="left">
<Eyebrow
colorScheme="primary"
heading={THREE_CARDS_ILLUSTRATION_DATA.eyebrow.heading}
/>
<Heading
segments={THREE_CARDS_ILLUSTRATION_DATA.heading}
size="lg"
weight="light"
/>
<Body body={THREE_CARDS_ILLUSTRATION_DATA.body} size="sm" />
</ThreeCards.Intro>
<ThreeCards.IllustrationCards
illustrationCards={THREE_CARDS_ILLUSTRATION_DATA.illustrationCards}
/>
</ThreeCards.Root>
{/* <ProductStepper.Flow
body={STEPPER_DATA.body}
eyebrow={STEPPER_DATA.eyebrow}
heading={STEPPER_DATA.heading}
steps={STEPPER_DATA.steps}
/> */}
<Demo.Root>
<Eyebrow colorScheme="primary" heading={DEMO_DATA.eyebrow.heading} />
<Demo.Heading segments={DEMO_DATA.heading} />
<Demo.Cta>
<LinkButton
color="secondary"
href="https://app.twenty.com/welcome"
label="Try twenty cloud"
type="anchor"
variant="contained"
/>
</Demo.Cta>
<Demo.Screenshot image={DEMO_DATA.image} />
</Demo.Root>
<Faq.Root illustration={FAQ_DATA.illustration}>
<Faq.Intro>
<Eyebrow colorScheme="secondary" heading={FAQ_DATA.eyebrow.heading} />
<Faq.Heading segments={FAQ_DATA.heading} />
<Faq.Cta>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<LinkButton
color="primary"
href="https://twenty.com/contact"
label="Talk to us"
type="anchor"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
</>
);
}
@@ -1,6 +1,6 @@
import { HeroDataType } from '@/sections/Hero/types';
import { HeroWhyTwentyDataType } from '@/sections/Hero/types';
export const HERO_DATA: HeroDataType = {
export const HERO_DATA: HeroWhyTwentyDataType = {
heading: [
{ text: 'The future of CRM is built, ', fontFamily: 'serif' },
{ text: 'not bought.', fontFamily: 'sans' },
@@ -8,4 +8,12 @@ export const HERO_DATA: HeroDataType = {
body: {
text: 'Most packaged software makes companies more similar. CRM is no exception. AI is turning software into a commodity. That does not make your go-to-market a commodity. It makes it the battleground.',
},
image: {
src: '/images/why-twenty/hero/background.png',
alt: '',
},
illustration: {
src: 'https://app.endlesstools.io/embed/479566da-0f55-44ef-99fe-90350aa88b9c',
title: 'Endless Tools Editor',
},
};
@@ -1,14 +1,37 @@
import { MENU_DATA } from '@/app/(home)/constants/menu';
import { HERO_DATA } from '@/app/why-twenty/constants/hero';
import { Pages } from '@/enums/pages';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
export default function WhyTwentyPage() {
return (
<Hero.Root backgroundColor={theme.colors.secondary.background[100]}>
<Hero.Heading page={Pages.WhyTwenty} segments={HERO_DATA.heading} size="xl" />
<Hero.Body page={Pages.WhyTwenty} body={HERO_DATA.body} />
<Hero.WhyTwentyVisual />
</Hero.Root>
<>
<Menu.Root
backgroundColor={theme.colors.secondary.background[100]}
scheme="secondary"
navItems={MENU_DATA.navItems}
socialLinks={MENU_DATA.socialLinks}
>
<Menu.Logo scheme="secondary" />
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="secondary" socialLinks={MENU_DATA.socialLinks} />
<Menu.Cta scheme="secondary" />
</Menu.Root>
<Hero.Root backgroundColor={theme.colors.secondary.background[100]}>
<Hero.Heading
page={Pages.WhyTwenty}
segments={HERO_DATA.heading}
size="xl"
/>
<Hero.Body page={Pages.WhyTwenty} body={HERO_DATA.body} />
<Hero.WhyTwentyVisual
image={HERO_DATA.image}
illustration={HERO_DATA.illustration}
/>
</Hero.Root>
</>
);
}
+17 -4
View File
@@ -1,4 +1,13 @@
export { CLIENT_ICONS, FlexportIcon, ZapierIcon } from "./clients";
export {
CheckIcon,
EditIcon,
EyeIcon,
INFORMATIVE_ICONS,
SearchIcon,
UsersIcon,
type InformativeIconProps,
} from "./informative";
export { ArrowLeftIcon } from "./functional/ArrowLeft";
export { ArrowRightIcon } from "./functional/ArrowRight";
export { ArrowRightUpIcon } from "./functional/ArrowRightUp";
@@ -9,7 +18,11 @@ export { PlusIcon } from "./functional/Plus";
export { RectangleFillIcon } from "./functional/RectangleFill";
export { RectangleOutlineIcon } from "./functional/RectangleOutline";
export { Logo } from "./logo/Logo";
export { DiscordIcon } from "./social/Discord";
export { GitHubIcon } from "./social/GitHub";
export { LinkedInIcon } from "./social/LinkedIn";
export { XIcon } from "./social/X";
export {
DiscordIcon,
GitHubIcon,
LinkedInIcon,
SOCIAL_ICONS,
XIcon,
type SocialIconProps,
} from './social';
@@ -0,0 +1,25 @@
type CheckIconProps = { size: number; color: string };
export function CheckIcon({ size, color }: CheckIconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<path
d="m11 6-3.75 4L5 8"
stroke={color}
strokeWidth={2}
/>
<path
d="M8 14a6 6 0 1 0-4.243-1.757"
stroke={color}
strokeWidth={2}
/>
</svg>
);
}
@@ -0,0 +1,30 @@
'use client';
import { useId } from 'react';
type EditIconProps = { size: number; color: string };
export function EditIcon({ size, color }: EditIconProps) {
const clipPathId = useId().replace(/:/g, '');
return (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<g clipPath={`url(#${clipPathId})`} stroke={color} strokeWidth={2}>
<path d="M7.604 3.621c-.744.034-1.476.112-2.19.194-.96.11-1.729.88-1.835 1.84-.104.94-.2 1.911-.2 2.904s.096 1.963.2 2.904c.106.96.875 1.729 1.835 1.839.946.109 1.922.211 2.919.211s1.973-.102 2.92-.211a2.094 2.094 0 0 0 1.834-1.839 35 35 0 0 0 .178-2.032" />
<path d="M11.916 2.61 8.847 6.09a1.74 1.74 0 0 0-.394.791L8.087 8.6c-.074.349.29.68.63.572l1.724-.542c.287-.09.541-.247.743-.46l3.171-3.336c.578-.608.477-1.625-.222-2.243-.683-.604-1.676-.596-2.217.018Z" />
</g>
<defs>
<clipPath id={clipPathId}>
<path fill="#fff" d="M0 0h16v16H0z" />
</clipPath>
</defs>
</svg>
);
}
@@ -0,0 +1,27 @@
type EyeIconProps = { size: number; color: string };
export function EyeIcon({ size, color }: EyeIconProps) {
const height = (size * 13) / 16;
return (
<svg
width={size}
height={height}
viewBox="0 0 16 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<path
d="M9.437 5.719a1.719 1.719 0 1 1-3.437 0 1.719 1.719 0 0 1 3.437 0Z"
stroke={color}
strokeWidth={2}
/>
<path
d="M11.458 2.363C10.38 1.583 9.07 1 7.656 1 3.98 1 1 4.94 1 6.059 1 6.77 2.207 8.626 4.033 9.88m9.265-5.765c.643.801 1.015 1.549 1.015 1.944 0 1.118-2.98 5.059-6.657 5.059a5.5 5.5 0 0 1-1.362-.177"
stroke={color}
strokeWidth={2}
/>
</svg>
);
}
@@ -0,0 +1,29 @@
type SearchIconProps = { size: number; color: string };
export function SearchIcon({ size, color }: SearchIconProps) {
return (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden
>
<circle
cx={6.5}
cy={6.5}
r={4.5}
stroke={color}
strokeWidth={2}
strokeLinejoin="round"
/>
<path
d="m11 11 4 4"
stroke={color}
strokeWidth={2}
strokeLinejoin="round"
/>
</svg>
);
}
@@ -0,0 +1,29 @@
import { createElement, type ComponentType } from 'react';
import { CheckIcon } from './Check';
import { EditIcon } from './Edit';
import { EyeIcon } from './Eye';
import { SearchIcon } from './Search';
import { UsersIcon } from './Users';
export { CheckIcon } from './Check';
export { EditIcon } from './Edit';
export { EyeIcon } from './Eye';
export { SearchIcon } from './Search';
export { UsersIcon } from './Users';
export type InformativeIconProps = { size: number; color: string };
function InformativeUsersIcon({ size, color }: InformativeIconProps) {
return createElement(UsersIcon, { size, fillColor: color });
}
export const INFORMATIVE_ICONS: Record<
string,
ComponentType<InformativeIconProps>
> = {
check: CheckIcon,
edit: EditIcon,
eye: EyeIcon,
search: SearchIcon,
users: InformativeUsersIcon,
};
@@ -0,0 +1,19 @@
import type { ComponentType } from 'react';
import { DiscordIcon } from './Discord';
import { GitHubIcon } from './GitHub';
import { LinkedInIcon } from './LinkedIn';
import { XIcon } from './X';
export { DiscordIcon } from './Discord';
export { GitHubIcon } from './GitHub';
export { LinkedInIcon } from './LinkedIn';
export { XIcon } from './X';
export type SocialIconProps = { size: number; fillColor: string };
export const SOCIAL_ICONS: Record<string, ComponentType<SocialIconProps>> = {
discord: DiscordIcon,
github: GitHubIcon,
linkedin: LinkedInIcon,
x: XIcon,
};
@@ -0,0 +1,16 @@
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
const CTAsContainer = styled.div`
display: flex;
flex-wrap: wrap;
gap: ${theme.spacing(4)};
justify-content: center;
`;
type CtaProps = { children: ReactNode };
export function Cta({ children }: CtaProps) {
return <CTAsContainer>{children}</CTAsContainer>;
}
@@ -0,0 +1,20 @@
import { Heading as BaseHeading } from '@/design-system/components';
import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
const StyledHeading = styled(BaseHeading)`
max-width: 360px;
@media (min-width: ${theme.breakpoints.md}px) {
max-width: 672px;
}
`;
type HeadingProps = {
segments: HeadingType[];
};
export function Heading({ segments }: HeadingProps) {
return <StyledHeading segments={segments} size="lg" weight="light" />;
}
@@ -0,0 +1,89 @@
import { Container } from '@/design-system/components';
import { theme } from '@/theme';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import NextImage from 'next/image';
import type { ReactNode } from 'react';
const RootWrapper = styled.div`
display: grid;
grid-template-columns: 1fr;
position: relative;
width: 100%;
`;
const BackgroundLayer = styled.div`
align-self: stretch;
grid-column: 1;
grid-row: 1;
opacity: 0.6;
pointer-events: none;
position: relative;
width: 100%;
z-index: 0;
`;
const PatternLayer = styled.div`
bottom: 0;
height: 60%;
left: 50%;
position: absolute;
transform: translateX(-50%);
width: 100%;
`;
const patternImageClassName = css`
object-fit: cover;
object-position: center top;
`;
const StyledSection = styled.section`
grid-column: 1;
grid-row: 1;
position: relative;
width: 100%;
z-index: 1;
`;
const StyledContainer = styled(Container)`
display: grid;
grid-template-columns: 1fr;
justify-items: center;
padding-bottom: ${theme.spacing(16)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(16)};
row-gap: ${theme.spacing(6)};
text-align: center;
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
padding-bottom: ${theme.spacing(24)};
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
padding-top: ${theme.spacing(24)};
}
`;
type RootProps = { children: ReactNode };
export function Root({ children }: RootProps) {
return (
<RootWrapper>
<BackgroundLayer aria-hidden>
<PatternLayer>
<NextImage
alt=""
className={patternImageClassName}
fill
sizes="(min-width: 921px) 100vw"
src="/images/product/demo/background.png"
/>
</PatternLayer>
</BackgroundLayer>
<StyledSection>
<StyledContainer>{children}</StyledContainer>
</StyledSection>
</RootWrapper>
);
}
@@ -0,0 +1,42 @@
import type { ImageType } from '@/design-system/components/Image/types/Image';
import { theme } from '@/theme';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import NextImage from 'next/image';
const ScreenshotRoot = styled.div`
aspect-ratio: 136 / 69;
border-radius: ${theme.radius(4)};
overflow: hidden;
position: relative;
width: 100%;
max-width: 800px;
margin-top: ${theme.spacing(12)};
margin-bottom: ${theme.spacing(11)};
@media (min-width: ${theme.breakpoints.md}px) {
border-radius: ${theme.radius(6)};
margin-top: ${theme.spacing(19)};
margin-bottom: ${theme.spacing(11)};
}
`;
const screenshotImageClassName = css`
object-fit: cover;
`;
type ScreenshotProps = { image: ImageType };
export function Screenshot({ image }: ScreenshotProps) {
return (
<ScreenshotRoot>
<NextImage
alt={image.alt}
className={screenshotImageClassName}
fill
sizes="(min-width: 1440px) 1360px, 100vw"
src={image.src}
/>
</ScreenshotRoot>
);
}
@@ -0,0 +1,6 @@
import { Cta } from './Cta/Cta';
import { Heading } from './Heading/Heading';
import { Root } from './Root/Root';
import { Screenshot } from './Screenshot/Screenshot';
export const Demo = { Cta, Heading, Root, Screenshot };
@@ -0,0 +1,9 @@
import type { EyebrowType } from '@/design-system/components/Eyebrow/types/Eyebrow';
import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
import type { ImageType } from '@/design-system/components/Image/types/Image';
export type DemoDataType = {
eyebrow: EyebrowType;
heading: HeadingType[];
image: ImageType;
};
@@ -0,0 +1 @@
export type { DemoDataType } from './DemoData';
@@ -0,0 +1,34 @@
const ENGAGEMENT_BAND_SHAPE_PATH =
'M0 4a4 4 0 0 1 4-4h1352a4 4 0 0 1 4 4v89.502a8 8 0 0 1-2.57 5.873l-35.13 32.498a8 8 0 0 1-5.43 2.127H4a4 4 0 0 1-4-4z';
type EngagementBandShapeProps = {
fillColor: string;
};
export function EngagementBandShape({ fillColor }: EngagementBandShapeProps) {
return (
<div
aria-hidden
style={{
height: '100%',
inset: 0,
pointerEvents: 'none',
position: 'absolute',
width: '100%',
zIndex: -1,
}}
>
<svg
fill="none"
height="100%"
preserveAspectRatio="none"
style={{ display: 'block' }}
viewBox="0 0 1360 134"
width="100%"
xmlns="http://www.w3.org/2000/svg"
>
<path d={ENGAGEMENT_BAND_SHAPE_PATH} fill={fillColor} />
</svg>
</div>
);
}
@@ -0,0 +1,24 @@
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
const StyledActions = styled.div`
display: grid;
grid-template-columns: 1fr;
justify-items: start;
min-width: 0;
position: relative;
z-index: 1;
@media (min-width: ${theme.breakpoints.md}px) {
justify-items: end;
}
`;
type ActionsProps = {
children: ReactNode;
};
export function Actions({ children }: ActionsProps) {
return <StyledActions>{children}</StyledActions>;
}
@@ -0,0 +1,16 @@
import { Body as BaseBody } from '@/design-system/components';
import type { BodyType } from '@/design-system/components/Body/types/Body';
import { styled } from '@linaria/react';
const StyledBody = styled(BaseBody)`
max-width: 668px;
opacity: 0.9;
`;
type BodyProps = {
body: BodyType;
};
export function Body({ body }: BodyProps) {
return <StyledBody body={body} size="sm" weight="regular" />;
}
@@ -0,0 +1,21 @@
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
const StyledCopy = styled.div`
display: grid;
grid-template-columns: 1fr;
max-width: 100%;
min-width: 0;
position: relative;
row-gap: ${theme.spacing(2)};
z-index: 1;
`;
type CopyProps = {
children: ReactNode;
};
export function Copy({ children }: CopyProps) {
return <StyledCopy>{children}</StyledCopy>;
}
@@ -0,0 +1,20 @@
import { Heading as BaseHeading } from '@/design-system/components';
import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
const StyledHeading = styled(BaseHeading)`
white-space: pre-line;
@media (min-width: ${theme.breakpoints.md}px) {
max-width: 668px;
}
`;
type HeadingProps = {
segments: HeadingType;
};
export function Heading({ segments }: HeadingProps) {
return <StyledHeading segments={segments} size="md" weight="light" />;
}
@@ -0,0 +1,35 @@
import { Container } from '@/design-system/components';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
const StyledSection = styled.section`
width: 100%;
`;
const StyledContainer = styled(Container)`
min-width: 0;
padding-bottom: ${theme.spacing(20)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(6)};
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
}
`;
type RootProps = {
backgroundColor: string;
children: ReactNode;
};
export function Root({ backgroundColor, children }: RootProps) {
return (
<StyledSection style={{ backgroundColor }}>
<StyledContainer>{children}</StyledContainer>
</StyledSection>
);
}
@@ -0,0 +1,80 @@
import { EngagementBandShape } from '@/sections/EngagementBand/EngagementBandShape';
import { theme } from '@/theme';
import { css } from '@linaria/core';
import { styled } from '@linaria/react';
import NextImage from 'next/image';
import type { ReactNode } from 'react';
const ENGAGEMENT_BAND_OVERLAY_SRC =
'/images/pricing/engagement-band/overlay.png';
const OverlayLayer = styled.div`
bottom: 0;
left: 60%;
pointer-events: none;
position: absolute;
right: 0;
top: 0;
z-index: 0;
`;
const overlayImageClassName = css`
object-fit: cover;
object-position: right center;
`;
const StyledStrip = styled.div`
border-radius: ${theme.radius(1)};
display: grid;
grid-template-columns: 1fr;
max-width: 100%;
min-width: 0;
overflow: hidden;
padding-top: ${theme.spacing(6)};
padding-right: ${theme.spacing(4)};
padding-bottom: ${theme.spacing(6)};
padding-left: ${theme.spacing(6)};
position: relative;
row-gap: ${theme.spacing(6)};
z-index: 0;
@media (min-width: ${theme.breakpoints.md}px) {
align-items: center;
column-gap: ${theme.spacing(2)};
grid-template-columns: minmax(0, 668px) minmax(0, 1fr);
padding-right: ${theme.spacing(16)};
row-gap: 0;
}
&[data-variant='primary'] {
color: ${theme.colors.primary.text[100]};
}
&[data-variant='secondary'] {
color: ${theme.colors.secondary.text[100]};
}
`;
type StripProps = {
children: ReactNode;
fillColor: string;
variant: 'primary' | 'secondary';
};
export function Strip({ children, fillColor, variant }: StripProps) {
return (
<StyledStrip data-variant={variant}>
<EngagementBandShape fillColor={fillColor} />
<OverlayLayer aria-hidden>
<NextImage
alt=""
className={overlayImageClassName}
fill
sizes="50vw"
src={ENGAGEMENT_BAND_OVERLAY_SRC}
/>
</OverlayLayer>
{children}
</StyledStrip>
);
}
@@ -0,0 +1,15 @@
import { Actions } from './Actions/Actions';
import { Body } from './Body/Body';
import { Copy } from './Copy/Copy';
import { Heading } from './Heading/Heading';
import { Root } from './Root/Root';
import { Strip } from './Strip/Strip';
export const EngagementBand = {
Actions,
Body,
Copy,
Heading,
Root,
Strip,
};
@@ -0,0 +1,7 @@
import type { BodyType } from '@/design-system/components/Body/types/Body';
import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
export type EngagementBandDataType = {
body: BodyType;
heading: HeadingType;
};
@@ -0,0 +1 @@
export type { EngagementBandDataType } from './EngagementBandData';
@@ -0,0 +1,18 @@
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
const StyledCta = styled.div`
column-gap: ${theme.spacing(2)};
display: grid;
grid-template-columns: auto auto;
justify-content: start;
`;
type CtaProps = {
children: ReactNode;
};
export function Cta({ children }: CtaProps) {
return <StyledCta>{children}</StyledCta>;
}
@@ -0,0 +1,28 @@
import { Heading as BaseHeading } from '@/design-system/components';
import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
import { theme } from '@/theme';
import { css } from '@linaria/core';
const faqHeadingClassName = css`
white-space: pre-line;
@media (min-width: ${theme.breakpoints.md}px) {
max-width: 700px;
}
`;
type HeadingProps = {
segments: HeadingType[];
};
export function Heading({ segments }: HeadingProps) {
return (
<BaseHeading
as="h2"
className={faqHeadingClassName}
segments={segments}
size="lg"
weight="light"
/>
);
}
@@ -0,0 +1,17 @@
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
const StyledIntro = styled.div`
display: grid;
grid-template-columns: 1fr;
row-gap: ${theme.spacing(6)};
`;
type IntroProps = {
children: ReactNode;
};
export function Intro({ children }: IntroProps) {
return <StyledIntro>{children}</StyledIntro>;
}
@@ -0,0 +1,206 @@
'use client';
import {
Accordion as BaseAccordion,
type AccordionTriggerState,
} from '@base-ui/react/accordion';
import {
MinusIcon,
PlusIcon,
RectangleFillIcon,
RectangleOutlineIcon,
} from '@/icons';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { IconButton } from '@/design-system/components';
import type { FaqQuestionType } from '@/sections/Faq/types/FaqQuestion';
const QuestionText = styled.span`
color: ${theme.colors.secondary.text[40]};
font-size: 2rem;
font-weight: ${theme.font.weight.regular};
grid-column: 3;
line-height: 1.25;
min-width: 0;
overflow-wrap: break-word;
transition: color 0.3s cubic-bezier(0.4, 0, 0.2, 1);
`;
const QuestionIconLayer = styled.span`
bottom: 0;
display: block;
left: 0;
position: absolute;
right: 0;
top: 7px;
transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&[data-layer='outline'] {
opacity: 1;
}
&[data-layer='fill'] {
opacity: 0;
}
`;
const ToggleContainer = styled.div`
grid-column: 5;
margin-top: ${theme.spacing(0.5)};
button::after {
bottom: 0;
content: '';
left: 0;
position: absolute;
right: 0;
top: 0;
}
`;
export const ItemRow = styled.div`
border-top: 1px solid ${theme.colors.secondary.text[40]};
display: grid;
grid-template-columns: 14px 12px 1fr 32px 36px;
padding-bottom: ${theme.spacing(6)};
padding-top: ${theme.spacing(6)};
row-gap: ${theme.spacing(2)};
&[data-open] ${QuestionText} {
color: ${theme.colors.secondary.text[100]};
}
&[data-open] ${QuestionIconLayer}[data-layer='outline'] {
opacity: 0;
}
&[data-open] ${QuestionIconLayer}[data-layer='fill'] {
opacity: 1;
}
@media (min-width: ${theme.breakpoints.md}px) {
grid-template-columns: 14px 60px 1fr 80px 36px;
row-gap: ${theme.spacing(4)};
}
`;
const Header = styled.h3`
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
margin-bottom: 0;
margin-left: 0;
margin-right: 0;
margin-top: 0;
position: relative;
&:hover ${QuestionText} {
color: #ffffff;
}
&:hover ${QuestionIconLayer}[data-layer='outline'] {
opacity: 0;
}
&:hover ${QuestionIconLayer}[data-layer='fill'] {
opacity: 1;
}
&:hover ${ToggleContainer} button {
border-color: ${theme.colors.secondary.text[100]};
}
`;
const QuestionIconContainer = styled.span`
grid-column: 1;
height: 12px;
position: relative;
width: 12px;
`;
const AnswerWrapper = styled.div`
display: grid;
grid-column: 3;
grid-template-rows: 0fr;
opacity: 0;
transition:
grid-template-rows 0.4s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1),
visibility 0.4s cubic-bezier(0.4, 0, 0.2, 1);
visibility: hidden;
&[data-open] {
grid-template-rows: 1fr;
opacity: 1;
visibility: visible;
}
`;
const AnswerInner = styled.div`
overflow: hidden;
`;
const AnswerText = styled.div`
color: ${theme.colors.secondary.text[100]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
line-height: 1.5;
margin: 0;
`;
type ItemProps = {
question: FaqQuestionType;
value: string;
};
export function Item({ question, value }: ItemProps) {
return (
<BaseAccordion.Item key={value} value={value} render={<ItemRow />}>
<BaseAccordion.Header render={<Header />}>
<QuestionIconContainer aria-hidden>
<QuestionIconLayer data-layer="outline">
<RectangleOutlineIcon
size={14}
strokeColor={theme.colors.secondary.text[100]}
/>
</QuestionIconLayer>
<QuestionIconLayer data-layer="fill">
<RectangleFillIcon
size={14}
fillColor={theme.colors.secondary.text[100]}
/>
</QuestionIconLayer>
</QuestionIconContainer>
<QuestionText>{question.question.text}</QuestionText>
<ToggleContainer>
<BaseAccordion.Trigger
render={(props, state: AccordionTriggerState) => {
const isOpen = state.open;
return (
<IconButton
icon={isOpen ? MinusIcon : PlusIcon}
ariaLabel={isOpen ? 'Collapse' : 'Expand'}
borderColor={theme.colors.secondary.border[40]}
iconFillColor="none"
iconSize={12}
iconStrokeColor={theme.colors.secondary.border[80]}
size={36}
onClick={props.onClick}
ariaExpanded={isOpen}
/>
);
}}
/>
</ToggleContainer>
</BaseAccordion.Header>
<BaseAccordion.Panel render={<AnswerWrapper />} keepMounted>
<AnswerInner>
<AnswerText>{question.answer.text}</AnswerText>
</AnswerInner>
</BaseAccordion.Panel>
</BaseAccordion.Item>
);
}
@@ -0,0 +1,36 @@
'use client';
import { Accordion as BaseAccordion } from '@base-ui/react/accordion';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { FaqQuestionType } from '@/sections/Faq/types/FaqQuestion';
import { Item } from '../Item/Item';
type ItemsProps = {
questions: FaqQuestionType[];
};
const AccordionList = styled.div`
margin-bottom: 0;
margin-left: auto;
margin-right: auto;
margin-top: 0;
@media (min-width: ${theme.breakpoints.md}px) {
width: 66%;
}
`;
export function Items({ questions }: ItemsProps) {
return (
<BaseAccordion.Root render={<AccordionList />}>
{questions.map((faqQuestion, index) => (
<Item
key={index}
question={faqQuestion}
value={`faq-${index}`}
/>
))}
</BaseAccordion.Root>
);
}
@@ -0,0 +1,43 @@
import { Container } from '@/design-system/components';
import type { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
const StyledSection = styled.section`
background-color: ${theme.colors.secondary.background[100]};
color: ${theme.colors.secondary.text[100]};
overflow: hidden;
position: relative;
width: 100%;
`;
const StyledContainer = styled(Container)`
display: grid;
grid-template-columns: 1fr;
padding-bottom: ${theme.spacing(30)};
padding-left: ${theme.spacing(6)};
padding-right: ${theme.spacing(6)};
padding-top: ${theme.spacing(30)};
position: relative;
row-gap: ${theme.spacing(20)};
z-index: 1;
@media (min-width: ${theme.breakpoints.md}px) {
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
}
`;
type RootProps = {
children: ReactNode;
illustration: IllustrationType;
};
export function Root({ children, illustration }: RootProps) {
return (
<StyledSection>
<StyledContainer>{children}</StyledContainer>
</StyledSection>
);
}
@@ -0,0 +1,13 @@
import { Cta } from './Cta/Cta';
import { Heading } from './Heading/Heading';
import { Intro } from './Intro/Intro';
import { Items } from './Items/Items';
import { Root } from './Root/Root';
export const Faq = {
Cta,
Heading,
Intro,
Items,
Root,
};
@@ -0,0 +1,11 @@
import { EyebrowType } from '@/design-system/components/Eyebrow/types/Eyebrow';
import { HeadingType } from '@/design-system/components/Heading/types/Heading';
import { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
import { FaqQuestionType } from '@/sections/Faq/types/FaqQuestion';
export type FaqDataType = {
illustration: IllustrationType;
eyebrow: EyebrowType;
heading: HeadingType[];
questions: FaqQuestionType[];
};
@@ -0,0 +1,7 @@
import { BodyType } from '@/design-system/components/Body/types/Body';
import { HeadingType } from '@/design-system/components/Heading/types/Heading';
export type FaqQuestionType = {
question: HeadingType;
answer: BodyType;
};
@@ -0,0 +1,2 @@
export type { FaqDataType } from './FaqData';
export type { FaqQuestionType } from './FaqQuestion';
@@ -6,8 +6,10 @@ const BottomGrid = styled.div`
display: grid;
font-size: ${theme.font.size(3)};
gap: ${theme.spacing(6)};
grid-template-columns: 1fr 1fr;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-rows: auto auto;
min-width: 0;
width: 100%;
`;
const Copyright = styled.div`
@@ -8,12 +8,18 @@ import React from 'react';
const SocialContainer = styled.nav`
align-items: center;
display: flex;
flex-wrap: wrap;
gap: ${theme.spacing(6)};
grid-column: 1 / -1;
grid-row: 1;
justify-content: center;
max-width: 100%;
min-width: 0;
@media (min-width: ${theme.breakpoints.md}px) {
flex-wrap: nowrap;
grid-column: 2;
justify-content: flex-end;
justify-self: end;
}
`;
@@ -29,11 +35,13 @@ const SocialItem = styled.a`
align-items: center;
color: ${theme.colors.primary.text[100]};
display: flex;
flex-shrink: 0;
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.medium};
gap: ${theme.spacing(3)};
line-height: 14px;
text-decoration: none;
white-space: nowrap;
&:focus-visible {
outline: 1px solid ${theme.colors.highlight[100]};
@@ -9,28 +9,32 @@ import { styled } from '@linaria/react';
const BodyContainer = styled.div`
color: ${theme.colors.primary.text[60]};
margin-top: calc(${theme.spacing(2)} - ${theme.spacing(6)});
width: 360px;
max-width: 360px;
width: 100%;
&[data-page=${Pages.WhyTwenty}] {
color: ${theme.colors.secondary.text[100]};
}
@media (min-width: ${theme.breakpoints.md}px) {
&[data-page=${Pages.Home}] {
width: 591px;
max-width: 591px;
}
&[data-page=${Pages.Partner}] {
width: 500px;
max-width: 500px;
}
&[data-page=${Pages.Pricing}] {
width: 500px;
max-width: 500px;
}
&[data-page=${Pages.Product}] {
width: 591px;
max-width: 591px;
}
&[data-page=${Pages.WhyTwenty}] {
width: 443px;
color: ${theme.colors.secondary.text[100]};
max-width: 443px;
}
}
`;
@@ -7,15 +7,16 @@ import { theme } from '@/theme';
import { styled } from '@linaria/react';
const HeadingContainer = styled.div`
width: 360px;
max-width: 360px;
width: 100%;
word-wrap: break-word;
@media (min-width: ${theme.breakpoints.md}px) {
width: 672px;
&[data-page=${Pages.WhyTwenty}] {
color: ${theme.colors.secondary.text[100]};
}
&[data-page=${Pages.WhyTwenty}] {
color: ${theme.colors.secondary.text[100]};
}
@media (min-width: ${theme.breakpoints.md}px) {
max-width: 672px;
}
`;
@@ -1,4 +1,5 @@
import { Image } from '@/design-system/components';
import type { ImageType } from '@/design-system/components/Image/types/Image';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
@@ -25,7 +26,8 @@ const StyledBackgroundImage = styled(Image)`
width: 98%;
@media (min-width: ${theme.breakpoints.md}px) {
width: 850px;
max-width: 850px;
width: 100%;
}
`;
@@ -41,15 +43,21 @@ const StyledForegroundImage = styled(Image)`
justify-self: center;
margin-right: ${theme.spacing(40)};
margin-top: ${theme.spacing(17.5)};
width: 800px;
max-width: 800px;
width: 100%;
}
`;
export function HomeVisual() {
type HomeVisualProps = {
background: ImageType;
foreground: ImageType;
};
export function HomeVisual({ background, foreground }: HomeVisualProps) {
return (
<StyledHomeVisual>
<StyledBackgroundImage src="/images/home/hero/background.png" alt="" />
<StyledForegroundImage src="/images/home/hero/foreground.png" alt="" />
<StyledBackgroundImage src={background.src} alt={background.alt} />
<StyledForegroundImage src={foreground.src} alt={foreground.alt} />
</StyledHomeVisual>
);
}
@@ -1,31 +1,36 @@
import { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
import type { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
const StyledIllustrationContainer = styled.div`
justify-self: stretch;
margin-top: ${theme.spacing(6)};
max-width: 100%;
min-width: 0;
width: 100%;
`;
type IllustrationProps = IllustrationType & {
type IllustrationProps = {
illustration: IllustrationType;
backgroundColor: string;
};
export function Illustration({
src,
title,
illustration,
backgroundColor,
}: IllustrationProps) {
return (
<StyledIllustrationContainer>
<iframe
src={src}
title={title}
src={illustration.src}
title={illustration.title}
allow="clipboard-write; encrypted-media; gyroscope; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
style={{
width: '100%',
maxWidth: '100%',
minWidth: 0,
height: '462px',
border: 'none',
backgroundColor: backgroundColor,
@@ -4,13 +4,15 @@ import { styled } from '@linaria/react';
import type { ReactNode } from 'react';
const StyledSection = styled.section`
min-width: 0;
width: 100%;
`;
const StyledContainer = styled(Container)`
display: grid;
grid-template-columns: 1fr;
grid-template-columns: minmax(0, 1fr);
justify-items: center;
min-width: 0;
text-align: center;
padding-top: ${theme.spacing(7.5)};
padding-left: ${theme.spacing(4)};
@@ -1,4 +1,6 @@
import { Image } from '@/design-system/components';
import { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
import { ImageType } from '@/design-system/components/Image/types/Image';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
@@ -26,13 +28,18 @@ const StyledIframe = styled.iframe`
width: 200%;
`;
export function WhyTwentyVisual() {
type WhyTwentyVisualProps = {
image: ImageType;
illustration: IllustrationType;
};
export function WhyTwentyVisual({ image, illustration }: WhyTwentyVisualProps) {
return (
<VisualContainer>
<StyledBackground src="/images/why-twenty/hero/background.png" alt="" />
<StyledBackground src={image.src} alt={image.alt} />
<StyledIframe
src="https://app.endlesstools.io/embed/479566da-0f55-44ef-99fe-90350aa88b9c"
title="Endless Tools Editor"
src={illustration.src}
title={illustration.title}
allow="clipboard-write; encrypted-media; gyroscope; web-share"
referrerPolicy="strict-origin-when-cross-origin"
allowFullScreen
@@ -1,7 +1,7 @@
import { BodyType } from '@/design-system/components/Body/types/Body';
import { HeadingType } from '@/design-system/components/Heading/types/Heading';
export type HeroDataType = {
export type HeroBaseDataType = {
heading: HeadingType[];
body: BodyType;
};
@@ -0,0 +1,7 @@
import { ImageType } from '@/design-system/components/Image/types/Image';
import { HeroBaseDataType } from '@/sections/Hero/types/HeroBaseData';
export type HeroHomeDataType = HeroBaseDataType & {
background: ImageType;
foreground: ImageType;
};
@@ -0,0 +1,6 @@
import { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
import { HeroBaseDataType } from '@/sections/Hero/types/HeroBaseData';
export type HeroIllustrationDataType = HeroBaseDataType & {
illustration: IllustrationType;
};
@@ -0,0 +1,8 @@
import { IllustrationType } from '@/design-system/components/Illustration/types/Illustration';
import { ImageType } from '@/design-system/components/Image/types/Image';
import { HeroBaseDataType } from '@/sections/Hero/types/HeroBaseData';
export type HeroWhyTwentyDataType = HeroBaseDataType & {
image: ImageType;
illustration: IllustrationType;
};
@@ -1 +1,4 @@
export type { HeroDataType } from './HeroData';
export type { HeroBaseDataType } from './HeroBaseData';
export type { HeroHomeDataType } from './HeroHomeData';
export type { HeroIllustrationDataType } from './HeroIllustrationData';
export type { HeroWhyTwentyDataType } from './HeroWhyTwentyData';
@@ -0,0 +1,217 @@
'use client';
import { Body, Heading } from '@/design-system/components';
import type { HeroStepperContentProps } from '@/sections/HeroStepper/types';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { useEffect, useRef, type ReactNode } from 'react';
const ContentRoot = styled.div`
display: grid;
grid-template-columns: auto 1fr;
gap: ${theme.spacing(6)};
min-width: 0;
@media (min-width: ${theme.breakpoints.md}px) {
gap: ${theme.spacing(10)};
}
`;
const ProgressRail = styled.div`
display: none;
@media (min-width: ${theme.breakpoints.md}px) {
display: flex;
flex-direction: column;
align-items: center;
gap: ${theme.spacing(2)};
position: sticky;
top: calc(50vh - ${theme.spacing(10)});
height: max-content;
}
`;
const StepIndicatorRow = styled.div`
display: flex;
align-items: flex-start;
gap: ${theme.spacing(4)};
`;
const PillBackground = styled.div`
background-color: ${theme.colors.primary.border[80]};
border-radius: ${theme.radius(8)};
display: flex;
height: ${theme.spacing(20)};
overflow: hidden;
width: ${theme.spacing(1)};
`;
const PillFill = styled.div`
background-color: ${theme.colors.highlight[100]};
border-radius: ${theme.radius(10)};
transition: height 0.4s ease;
width: 100%;
`;
const ActiveLabel = styled.p`
color: ${theme.colors.highlight[100]};
font-family: ${theme.font.family.mono};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.medium};
margin: 0;
text-transform: uppercase;
line-height: ${theme.spacing(4)};
`;
const InactiveDotWrapper = styled.div`
display: flex;
align-items: center;
gap: ${theme.spacing(4)};
`;
const InactiveDot = styled.div`
background-color: ${theme.colors.primary.border[80]};
border-radius: 50%;
height: ${theme.spacing(1)};
width: ${theme.spacing(1)};
`;
const StepsColumn = styled.div`
display: grid;
grid-template-columns: 1fr;
min-width: 0;
@media (min-width: ${theme.breakpoints.md}px) {
max-width: 454px;
}
`;
const StepBlock = styled.div`
align-content: center;
display: grid;
grid-template-columns: 1fr;
min-height: 80vh;
opacity: 0.3;
row-gap: ${theme.spacing(4)};
transition: opacity 0.4s ease;
@media (min-width: ${theme.breakpoints.md}px) {
min-height: 100vh;
row-gap: ${theme.spacing(6)};
}
&[data-active='true'] {
opacity: 1;
}
`;
type ProgressRailProps = {
activeImageIndex: number;
activeStepIndex: number;
steps: HeroStepperContentProps['steps'];
};
function HeroProgressRail({
activeImageIndex,
activeStepIndex,
steps,
}: ProgressRailProps) {
const nodes: ReactNode[] = [];
const stepCount = steps.length;
for (let index = 0; index < stepCount; index += 1) {
if (index === activeStepIndex) {
const stepImagesCount = steps[index]?.images.length || 1;
const fillPercentage = ((activeImageIndex + 1) / stepImagesCount) * 100;
nodes.push(
<StepIndicatorRow key={`step-${index}`}>
<PillBackground>
<PillFill style={{ height: `${fillPercentage}%` }} />
</PillBackground>
<ActiveLabel>{String(index + 1).padStart(2, '0')}</ActiveLabel>
</StepIndicatorRow>,
);
} else {
nodes.push(
<InactiveDotWrapper key={`step-${index}`}>
<InactiveDot />
</InactiveDotWrapper>,
);
}
}
return <ProgressRail>{nodes}</ProgressRail>;
}
export function Content({
activeImageIndex,
activeStepIndex,
onActiveStepChange,
steps,
}: HeroStepperContentProps) {
const stepRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
stepRefs.current = stepRefs.current.slice(0, steps.length);
}, [steps.length]);
useEffect(() => {
const elements = stepRefs.current.filter(Boolean) as HTMLDivElement[];
if (elements.length === 0) {
return;
}
const observer = new IntersectionObserver(
(entries) => {
const candidates = entries.filter(
(entry) => entry.isIntersecting && entry.intersectionRatio > 0.15,
);
if (candidates.length === 0) {
return;
}
const best = candidates.reduce((previous, current) =>
current.intersectionRatio > previous.intersectionRatio
? current
: previous,
);
const indexAttribute = best.target.getAttribute('data-step-index');
const index = indexAttribute === null ? NaN : Number(indexAttribute);
if (!Number.isNaN(index)) {
onActiveStepChange(index);
}
},
{ root: null, threshold: [0, 0.15, 0.3, 0.5, 0.75, 1] },
);
elements.forEach((element) => observer.observe(element));
return () => observer.disconnect();
}, [onActiveStepChange, steps]);
return (
<ContentRoot>
<HeroProgressRail
activeImageIndex={activeImageIndex}
activeStepIndex={activeStepIndex}
steps={steps}
/>
<StepsColumn>
{steps.map((step, index) => (
<StepBlock
data-active={String(index === activeStepIndex)}
data-step-index={index}
key={index}
ref={(element) => {
stepRefs.current[index] = element;
}}
>
<Heading segments={step.heading} size="lg" weight="light" />
<Body body={step.body} size="sm" />
</StepBlock>
))}
</StepsColumn>
</ContentRoot>
);
}
@@ -0,0 +1,45 @@
'use client';
import type { HeroStepperStepType } from '@/sections/HeroStepper/types/HeroStepperStep';
import { useEffect, useState } from 'react';
import { Content } from '../Content/Content';
import { Root } from '../Root/Root';
import { Visual } from '../Visual/Visual';
type FlowProps = {
steps: HeroStepperStepType[];
};
export function Flow({ steps }: FlowProps) {
const [activeStepIndex, setActiveStepIndex] = useState(0);
const [activeImageIndex, setActiveImageIndex] = useState(0);
useEffect(() => {
setActiveImageIndex(0);
}, [activeStepIndex]);
useEffect(() => {
const imageList = steps[activeStepIndex]?.images ?? [];
if (imageList.length <= 1) {
return;
}
const timerId = window.setInterval(() => {
setActiveImageIndex((previous) => (previous + 1) % imageList.length);
}, 5000);
return () => window.clearInterval(timerId);
}, [activeStepIndex, steps]);
const images = steps[activeStepIndex]?.images ?? [];
return (
<Root>
<Content
activeImageIndex={activeImageIndex}
activeStepIndex={activeStepIndex}
onActiveStepChange={setActiveStepIndex}
steps={steps}
/>
<Visual activeImageIndex={activeImageIndex} images={images} />
</Root>
);
}

Some files were not shown because too many files have changed in this diff Show More