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.
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 170 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 6.8 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 280 KiB |
|
After Width: | Height: | Size: 930 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 631 KiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||