More website updates. (#19624)

This PR introduces more updates to the website, such as real
testimonials, case studies, copy of pricing plans table. It also adds
modals for "Talk to Us" and "Become a Partner".
This commit is contained in:
Abdullah.
2026-04-13 19:02:21 +05:00
committed by GitHub
parent 88c0b24d9e
commit 84b325876d
83 changed files with 5281 additions and 625 deletions
+1
View File
@@ -10,6 +10,7 @@
"dependencies": {
"@base-ui/react": "^1.3.0",
"@base-ui/utils": "^0.2.6",
"@calcom/embed-react": "^1.5.3",
"@gsap/react": "2.1.2",
"@linaria/core": "^7.0.0",
"@linaria/react": "^7.0.1",
@@ -19,29 +19,31 @@ export const HELPED_DATA: HelpedDataType = {
],
cards: [
{
icon: 'realytics',
heading: { text: 'Increase lead qualification', fontFamily: 'sans' },
icon: 'w3villa',
heading: { text: 'Ship a product on Twenty', fontFamily: 'sans' },
body: {
text: 'Realytics built lead scoring into their CRM and increased qualified outbound by 40%.',
text: 'W3villa built W3Grads — AI mock interviews at scale — on Twenty as the operational backbone.',
},
illustration: 'target',
href: '/case-studies/w3villa',
},
{
icon: 'beagle',
heading: { text: 'Build performing onboarding', fontFamily: 'sans' },
icon: 'act-education',
heading: { text: 'Own your CRM end to end', fontFamily: 'sans' },
body: {
text: 'Beagle launched a custom onboarding pipeline and shortened their sales cycle by 30%.',
text: 'AC&T replaced a shuttered vendor CRM with self-hosted Twenty and cut CRM costs by more than 90%.',
},
illustration: 'spaceship',
href: '/case-studies/act-education',
},
{
icon: 'evergreen',
heading: { text: 'Unify workflow', fontFamily: 'sans' },
icon: 'netzero',
heading: { text: 'Grow with a flexible foundation', fontFamily: 'sans' },
body: {
text: 'Evergreen unified sales and CS workflows, boosting expansion revenue by 25%.',
text: 'NetZero runs a modular Twenty setup across carbon credits, ag products, and industrial systems.',
},
illustration: 'money',
href: '/case-studies/netzero',
},
],
};
@@ -7,47 +7,34 @@ export const TESTIMONIALS_DATA: TestimonialsDataType = {
testimonials: [
{
heading: {
text: "YES @twentycrm has json object fields. Such a simple feature and yet so many CRMs don't. It allows me to add data without caring too much about the data model, and eventually make it cleaner if the model gets stable.",
text: 'The flexibility is really what made the difference. Our needs evolve very fast. I discover a new need and in two clicks I can address it. That is a real advantage when you are moving quickly.',
fontFamily: 'sans',
},
author: {
avatar: {
src: '/images/home/testimonials/avatar-one.png',
alt: '',
},
name: { text: 'Wayne Hamadi' },
handle: { text: '@wayne_hamadi' },
date: new Date('2024-05-23'),
name: { text: 'Olivier Reinaud' },
designation: { text: 'Co-founder at NetZero' },
},
},
{
heading: {
text: 'We switched from a rigid CRM where every custom field felt like a ticket. Twenty lets our team model how we actually sell without waiting on consultants.',
text: "We didn't want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind.",
fontFamily: 'sans',
},
author: {
avatar: {
src: '/images/home/testimonials/avatar-two.png',
alt: '',
},
name: { text: 'Priya Nandakumar' },
handle: { text: '@priya_builds' },
date: new Date('2024-07-11'),
name: { text: 'Amrendra Pratap Singh' },
designation: { text: 'VP of Engineering at W3villa Technologies' },
},
},
{
heading: {
text: 'Open source was the hook; the product is what kept us. Pipelines, people, and reporting finally live in one place our engineers are happy to extend.',
text: 'It is just such a nicer experience than dealing with a Salesforce or a HubSpot. My mission has been to get every tool API-accessible, so everything talks to each other. Twenty made that possible in a way older CRM platforms simply do not.',
fontFamily: 'sans',
},
author: {
avatar: {
src: '/images/home/testimonials/avatar-three.png',
alt: '',
name: { text: 'Justin Beadle' },
designation: {
text: 'Director of Digital and Information, Elevate Consulting',
},
name: { text: 'Marcus Chen' },
handle: { text: '@marcus_ops' },
date: new Date('2024-09-02'),
},
},
],
@@ -9,7 +9,7 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
{ text: " that's quick to flex", fontFamily: 'sans' },
],
body: {
text: 'Compose your CRM and interal apps with a single exesibility toolkit.',
text: 'Compose your CRM and internal apps with a single extensibility toolkit.',
},
illustrationCards: [
{
@@ -23,6 +23,7 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
company: { text: 'Mid-Market Fintech' },
},
illustration: 'diamond',
caseStudySlug: '9dots',
},
{
heading: { text: 'AI for rapid iterations', fontFamily: 'sans' },
@@ -35,6 +36,7 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
company: { text: 'Mid-Market Fintech' },
},
illustration: 'flash',
caseStudySlug: 'alternative-partners',
},
{
heading: { text: 'Control without drag', fontFamily: 'sans' },
@@ -47,6 +49,7 @@ export const THREE_CARDS_ILLUSTRATION_DATA: ThreeCardsIllustrationDataType = {
company: { text: 'Mid-Market Fintech' },
},
illustration: 'lock',
caseStudySlug: 'elevate-consulting',
},
],
};
@@ -7,6 +7,7 @@ import {
THREE_CARDS_FEATURE_DATA,
THREE_CARDS_ILLUSTRATION_DATA,
} from '@/app/(home)/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import { FAQ_DATA, MENU_DATA, TRUSTED_BY_DATA } from '@/app/_constants';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
@@ -94,6 +95,11 @@ export default async function HomePage() {
type="anchor"
variant="contained"
/>
<TalkToUsButton
color="secondary"
label="Talk to us"
variant="outlined"
/>
</Hero.Cta>
<Hero.HomeVisual visual={HERO_DATA.visual} />
</Hero.Root>
@@ -158,6 +164,13 @@ export default async function HomePage() {
size="lg"
weight="light"
/>
<LinkButton
color="secondary"
href="/product"
label="Visit product page"
type="link"
variant="contained"
/>
</ThreeCards.Intro>
<ThreeCards.FeatureCards
featureCards={THREE_CARDS_FEATURE_DATA.featureCards}
@@ -193,11 +206,9 @@ export default async function HomePage() {
type="anchor"
variant="contained"
/>
<LinkButton
<TalkToUsButton
color="primary"
href="https://twenty.com/contact"
label="Talk to us"
type="anchor"
variant="outlined"
/>
</Faq.Cta>
@@ -0,0 +1,2 @@
export const CONTACT_CAL_FORM_CAL_LINK =
'forms/f7841033-0a20-4958-8c92-4e34ec128a81';
@@ -23,9 +23,16 @@ export const FOOTER_DATA: FooterDataType = {
title: 'Help',
ctas: [],
links: [
{ label: 'Developers', href: '#', external: false },
{ label: 'FAQ', href: '#', external: false },
{ label: 'Support', href: '#', external: false },
{
label: 'Developers',
href: 'https://docs.twenty.com/developers/introduction',
external: true,
},
{
label: 'User Guide',
href: 'https://docs.twenty.com/user-guide/introduction',
external: true,
},
{ label: 'Release Notes', href: '/release-notes', external: false },
{
label: 'Halftone generator',
@@ -39,7 +46,6 @@ export const FOOTER_DATA: FooterDataType = {
title: 'Legal',
ctas: [],
links: [
{ label: 'Cookie Policy', href: '#', external: false },
{ label: 'Privacy Policy', href: '/privacy-policy', external: false },
{ label: 'Terms and Conditions', href: '/terms', external: false },
],
@@ -50,16 +56,15 @@ export const FOOTER_DATA: FooterDataType = {
ctas: [
{
color: 'secondary',
href: 'https://app.twenty.com/welcome',
kind: 'contactModal',
label: 'Talk to us',
type: 'anchor',
variant: 'contained',
},
{
color: 'secondary',
href: 'https://app.twenty.com/welcome',
kind: 'link',
label: 'Get started',
type: 'anchor',
variant: 'outlined',
},
],
@@ -0,0 +1,157 @@
import { FAQ_DATA, MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import type { CaseStudyData } from '@/app/case-studies/_constants/types';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Faq } from '@/sections/Faq/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/home/hero/avatars/katherine-adams.jpg';
const CASE_STUDY: CaseStudyData = {
meta: {
title:
'Homeseller, WhatsApp, and a CRM built around the business — Nine Dots & Twenty',
description:
'How Nine Dots Ventures rebuilt a Singapore real estate agency on Twenty with APIs, n8n, Grafana, and AI on top of 2,000+ WhatsApp messages a day.',
},
hero: {
readingTime: '9 min',
title: [
{ text: 'A real estate agency on WhatsApp ', fontFamily: 'serif' },
{ text: 'built a CRM around it', fontFamily: 'sans' },
],
author: 'Mike Babiy & Azmat Parveen',
clientIcon: 'nine-dots',
heroImageSrc: PLACEHOLDER_HERO,
},
sections: [
{
type: 'text',
eyebrow: 'Homeseller',
heading: [
{ text: 'When the channel ', fontFamily: 'serif' },
{ text: 'is the business', fontFamily: 'sans' },
],
paragraphs: [
'Homeseller is a high-volume real estate agency in Singapore, founded by one of the country\'s top-performing property agents. The whole operation runs on WhatsApp: no email, no calendars — group chats, thousands of them, with clients, agents, and leads together.',
'That works until you need to understand the business underneath. Which deals are stuck? Where are leads coming from? What is the close rate? With spreadsheets and a legacy custom CRM that could not keep up, those questions were nearly impossible to answer.',
'Mike and Azmat from Nine Dots stepped in to fix that — not by changing how Homeseller works, but by building a system that finally fit around it.',
],
callout:
'"Twenty lets us build a CRM around the business and not the business around the CRM." — Mike Babiy, Founder, Nine Dots Ventures',
},
{
type: 'text',
eyebrow: 'Architecture',
heading: [
{ text: 'The CRM ', fontFamily: 'serif' },
{ text: 'as a control hub', fontFamily: 'sans' },
],
paragraphs: [
'Nine Dots rebuilt Homeseller\'s operations on Twenty, with a custom data model shaped around their sales flow. Because Twenty is open and everything is accessible via API, they connected it to what the business actually needed: n8n for automated workflows (in-app workflows were not available at that time), Grafana for live dashboards fed from Twenty, and a custom AI layer to parse and extract structured insights from more than 2,000 WhatsApp messages a day.',
'Homeseller kept their habits. WhatsApp stayed WhatsApp. What changed is that everything flowing through those conversations now lands in a structured system — tracked, classified, and visible in real time.',
],
callout:
'"Twenty is the heart of the system. Everything branches from it." — Azmat Parveen, Nine Dots Ventures',
},
{
type: 'text',
eyebrow: 'The result',
heading: [
{ text: '150 hours ', fontFamily: 'serif' },
{ text: 'saved every month', fontFamily: 'sans' },
],
paragraphs: [
'About 150 hours per month saved in manual operations. Real-time metrics for the business owner. Growth readiness without adding operational headcount. A team that can answer questions that used to take days to piece together.',
'The full rollout landed in July 2025. Since then, Nine Dots built a Smart Assistant on top of the system — nudging agents with tasks, reminders, and on-demand market analysis. Some agents never open Twenty directly, yet they are powered by it, outperforming peers on manual processes alone. By Q1 2026, Homeseller had recorded its best sales quarter ever.',
],
},
],
testimonial: {
eyebrow: 'What they say',
quote:
'Twenty lets us build a CRM around the business and not the business around the CRM.',
author: {
name: 'Mike Babiy',
handle: 'Founder, Nine Dots Ventures',
date: 'July 2025',
avatarSrc: PLACEHOLDER_HERO,
},
},
tableOfContents: [
'When the channel is the business',
'The CRM as a control hub',
'The result',
],
catalogCard: {
summary:
'Nine Dots put Twenty at the center of Homeseller\'s stack with APIs, automation, and AI on top of WhatsApp-heavy operations.',
date: 'Jul 2025',
},
};
export const metadata: Metadata = {
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
};
export default async function NineDotsCaseStudyPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={theme.colors.secondary.background[100]}
scheme="secondary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="secondary" />
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="secondary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="secondary" />
</Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} />
{CASE_STUDY.sections.map((block, index) => {
if (block.type === 'text') {
return <CaseStudy.TextBlock key={index} block={block} />;
}
return <CaseStudy.VisualBlock key={index} block={block} />;
})}
<CaseStudy.Testimonial testimonial={CASE_STUDY.testimonial} />
<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"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
<CaseStudy.TableOfContents items={CASE_STUDY.tableOfContents} />
</>
);
}
@@ -0,0 +1,7 @@
export type {
CaseStudyCatalogEntry,
CaseStudyContentBlock,
CaseStudyData,
CaseStudyTextBlock,
CaseStudyVisualBlock,
} from './types';
@@ -0,0 +1,52 @@
import type { HeadingType } from '@/design-system/components/Heading/types/Heading';
export type CaseStudyTextBlock = {
type: 'text';
eyebrow?: string;
heading: HeadingType[];
paragraphs: string[];
callout?: string;
};
export type CaseStudyVisualBlock = {
type: 'visual';
src: string;
alt: string;
};
export type CaseStudyContentBlock = CaseStudyTextBlock | CaseStudyVisualBlock;
export type CaseStudyTestimonialAuthor = {
name: string;
handle: string;
date: string;
avatarSrc: string;
};
export type CaseStudyData = {
meta: { title: string; description: string };
hero: {
readingTime: string;
title: HeadingType[];
author: string;
clientIcon: string;
heroImageSrc: string;
};
sections: CaseStudyContentBlock[];
testimonial: {
eyebrow: string;
quote: string;
author: CaseStudyTestimonialAuthor;
};
tableOfContents: string[];
catalogCard: {
summary: string;
date: string;
};
};
export type CaseStudyCatalogEntry = {
href: string;
hero: CaseStudyData['hero'];
catalogCard: CaseStudyData['catalogCard'];
};
@@ -0,0 +1,168 @@
import { FAQ_DATA, MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import type { CaseStudyData } from '@/app/case-studies/_constants/types';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Faq } from '@/sections/Faq/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/home/hero/avatars/katherine-adams.jpg';
const CASE_STUDY: CaseStudyData = {
meta: {
title:
'Burned by vendor lock-in, AC&T built a CRM they actually own — Twenty',
description:
'How AC&T Education Migration and Flycoder replaced a shuttered vendor CRM with self-hosted Twenty — 90%+ lower cost and full ownership.',
},
hero: {
readingTime: '7 min',
title: [
{ text: 'A CRM they ', fontFamily: 'serif' },
{ text: 'actually own', fontFamily: 'sans' },
],
author: 'Joseph Chiang',
clientIcon: 'act-education',
heroImageSrc: PLACEHOLDER_HERO,
},
sections: [
{
type: 'text',
eyebrow: 'AC&T Education Migration',
heading: [
{ text: 'When the vendor ', fontFamily: 'serif' },
{ text: 'pulled the plug', fontFamily: 'sans' },
],
paragraphs: [
'AC&T Education Migration (actimmi.com) is an education agency in Australia. They help international students with applications to education providers and visas. They had been on a previous CRM until the vendor shut the system down — leaving nothing but a CSV export.',
'Whatever came next had to be something they could own.',
],
callout:
'"They did not want to learn someone else\'s system. They wanted to keep working the way they already did and make it smoother." — Joseph Chiang, CRM Engineer, AC&T Education Migration',
},
{
type: 'text',
eyebrow: 'Implementation',
heading: [
{ text: 'No more renting ', fontFamily: 'serif' },
{ text: 'someone else\'s structure', fontFamily: 'sans' },
],
paragraphs: [
'They evaluated Salesforce, Zoho, Pipedrive, and SuiteCRM. Each came with the same tradeoffs: too expensive, too rigid, or too generic — and none fixed the underlying problem. They were still renting a structure they did not control.',
'Flycoder, a full-stack development partner, helped them set up Twenty as a self-hosted instance shaped around how AC&T actually operates. The data model centers on students, not a generic contact-and-deal pipeline. Statuses update automatically: a workflow runs nightly to keep enrollment records current. Automated email reminders cover important dates. Adding a new record takes under a minute.',
'The result is a system that fits how AC&T already worked, instead of the other way around.',
],
},
{
type: 'text',
eyebrow: 'Control',
heading: [
{ text: 'Control without ', fontFamily: 'serif' },
{ text: 'the overhead', fontFamily: 'sans' },
],
paragraphs: [
'Self-hosted means AC&T carries no vendor risk: no pricing model that can change, no platform that can disappear, no forced migration. The system is theirs.',
'Because everything is built on Twenty\'s open foundation, Flycoder could wire the exact logic AC&T needed without fighting the platform.',
],
},
{
type: 'text',
eyebrow: 'The result',
heading: [
{ text: 'Costs down ', fontFamily: 'serif' },
{ text: 'more than 90%', fontFamily: 'sans' },
],
paragraphs: [
'CRM costs dropped by more than 90%. Manual overhead tied to the old system is gone. For the first time, AC&T has a CRM they will not lose again.',
'They did not just replace a tool — they took back ownership of how their business runs.',
],
},
],
testimonial: {
eyebrow: 'What they say',
quote:
'They did not want to learn someone else\'s system. They wanted to keep working the way they already did and make it smoother.',
author: {
name: 'Joseph Chiang',
handle: 'CRM Engineer, AC&T Education Migration',
date: '2025',
avatarSrc: PLACEHOLDER_HERO,
},
},
tableOfContents: [
'When the vendor pulled the plug',
'No more renting someone else\'s structure',
'Control without the overhead',
'The result',
],
catalogCard: {
summary:
'AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty — over 90% lower CRM cost and full control.',
date: '2025',
},
};
export const metadata: Metadata = {
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
};
export default async function ActEducationCaseStudyPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={theme.colors.secondary.background[100]}
scheme="secondary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="secondary" />
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="secondary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="secondary" />
</Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} />
{CASE_STUDY.sections.map((block, index) => {
if (block.type === 'text') {
return <CaseStudy.TextBlock key={index} block={block} />;
}
return <CaseStudy.VisualBlock key={index} block={block} />;
})}
<CaseStudy.Testimonial testimonial={CASE_STUDY.testimonial} />
<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"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
<CaseStudy.TableOfContents items={CASE_STUDY.tableOfContents} />
</>
);
}
@@ -0,0 +1,136 @@
import { FAQ_DATA, MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import type { CaseStudyData } from '@/app/case-studies/_constants/types';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Faq } from '@/sections/Faq/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/home/hero/avatars/katherine-adams.jpg';
const CASE_STUDY: CaseStudyData = {
meta: {
title:
'From Salesforce to self-hosted Twenty, powered by AI — Alternative Partners',
description:
'How Alternative Partners migrated from Salesforce to self-hosted Twenty using agentic AI in the implementation loop — fast migration, durable ownership.',
},
hero: {
readingTime: '7 min',
title: [
{ text: 'From Salesforce to ', fontFamily: 'serif' },
{ text: 'self-hosted Twenty', fontFamily: 'sans' },
],
author: 'Benjamin Reynolds',
clientIcon: 'alternative-partners',
heroImageSrc: PLACEHOLDER_HERO,
},
sections: [
{
type: 'text',
eyebrow: 'Alternative Partners',
heading: [
{ text: 'AI in the ', fontFamily: 'serif' },
{ text: 'migration workflow', fontFamily: 'sans' },
],
paragraphs: [
'Alternative Partners is a consulting firm that moved from Salesforce to a self-hosted Twenty instance. Benjamin Reynolds led the migration — he had already become a Twenty expert implementing Twenty for one of Twenty\'s first cloud customers.',
'His approach was unconventional. Instead of mapping fields manually, scripting transforms, and validating data step by step, he handed the job to agentic AI tools with a brief: where the data lives, the GitHub repo for the target platform, the Railway deployment — start, and only return if something breaks beyond a 70% confidence fix.',
'It worked. This is AI-assisted iteration in practice: not AI as a product feature, but as part of implementation work, compressing what would typically be weeks into something one person can oversee without being the bottleneck.',
],
},
{
type: 'text',
eyebrow: 'Ownership',
heading: [
{ text: 'Self-hosted ', fontFamily: 'serif' },
{ text: 'means control', fontFamily: 'sans' },
],
paragraphs: [
'The self-hosted setup means Alternative Partners owns the full stack: no vendor access to their data, no dependency on a SaaS pricing model, full control over how the system evolves. The migration was fast because of AI; the result is durable because the stack is open source.',
],
},
],
testimonial: {
eyebrow: 'What they say',
quote:
'Agentic AI let us migrate faster than a traditional playbook, and self-hosted Twenty means we own where our CRM data lives.',
author: {
name: 'Benjamin Reynolds',
handle: 'Alternative Partners',
date: '2025',
avatarSrc: PLACEHOLDER_HERO,
},
},
tableOfContents: ['AI in the migration workflow', 'Self-hosted means control'],
catalogCard: {
summary:
'Alternative Partners replaced Salesforce with self-hosted Twenty, using agentic AI to compress migration work.',
date: '2025',
},
};
export const metadata: Metadata = {
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
};
export default async function AlternativePartnersCaseStudyPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={theme.colors.secondary.background[100]}
scheme="secondary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="secondary" />
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="secondary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="secondary" />
</Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} />
{CASE_STUDY.sections.map((block, index) => {
if (block.type === 'text') {
return <CaseStudy.TextBlock key={index} block={block} />;
}
return <CaseStudy.VisualBlock key={index} block={block} />;
})}
<CaseStudy.Testimonial testimonial={CASE_STUDY.testimonial} />
<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"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
<CaseStudy.TableOfContents items={CASE_STUDY.tableOfContents} />
</>
);
}
@@ -0,0 +1,172 @@
import { FAQ_DATA, MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import type { CaseStudyData } from '@/app/case-studies/_constants/types';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Faq } from '@/sections/Faq/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/home/hero/avatars/katherine-adams.jpg';
const CASE_STUDY: CaseStudyData = {
meta: {
title:
'Twenty as the API backbone of a go-to-market stack — Elevate Consulting',
description:
'How Elevate Consulting moved off documents and spreadsheets to Twenty as the API-connected CRM at the center of their stack.',
},
hero: {
readingTime: '8 min',
title: [
{ text: 'Twenty as the API backbone ', fontFamily: 'serif' },
{ text: 'of a go-to-market stack', fontFamily: 'sans' },
],
author: 'Justin Beadle',
clientIcon: 'elevate-consulting',
heroImageSrc: PLACEHOLDER_HERO,
},
sections: [
{
type: 'text',
eyebrow: 'The situation',
heading: [
{ text: 'From documents ', fontFamily: 'serif' },
{ text: 'to open APIs', fontFamily: 'sans' },
],
paragraphs: [
'Elevate Consulting is a management consultancy based in Canada. When Justin Beadle, Director of Digital and Information, joined, the company ran entirely on Word documents, Excel spreadsheets, sticky notes, emails, and reliance on people. There was no CRM, no API-accessible tools — only a patchwork trying to stand in for a single source of truth.',
'The CEO had resisted bringing in a CRM for years. The business development team had no experience using one, and the licensing costs of well-known CRMs like Salesforce or HubSpot were hard to justify without any guarantee of adoption: CRMs are only as good as the maintenance of the data inside them.',
'In June 2025, Justin learned Twenty v1 had shipped. Within two or three days, the CEO asked him to look into setting up a CRM. The shift came from the potential of what could be built on top of fully open APIs. The timing was perfect.',
],
callout:
'"It is just such a nicer experience than dealing with a Salesforce or a HubSpot. My mission has been to get every tool API-accessible, so everything talks to each other. Twenty made that possible in a way older CRM platforms simply do not." — Justin Beadle, Director of Digital and Information, Elevate Consulting',
},
{
type: 'text',
eyebrow: 'Integration',
heading: [
{ text: 'One API ', fontFamily: 'serif' },
{ text: 'to rule them all', fontFamily: 'sans' },
],
paragraphs: [
'Justin\'s broader mission at Elevate has been to move the company off static documents and onto tools with API access. By the end of 2025, that was in place: time billing, resource planning, Microsoft Teams, and project management were all accessible via API, with Twenty at the center holding client and opportunity data. Team members could use that information strategically instead of re-keying it.',
'That opened the door to something more powerful. Justin built a custom front end that pulls live data from those systems into a single view, tailored to each role. When a proposal is won, what used to require four separate people manually setting up instances across four different tools now happens in a single click, drawing on data collected in Twenty across the full opportunity lifecycle — another shift toward higher-value work for clients.',
'Twenty is not only where CRM data lives. It is the API backbone that makes the rest of the stack possible.',
],
callout:
'"Because Twenty\'s API is fully open, I could connect it to every other tool in our stack. When a proposal is won, what used to take four people manually setting things up across four different tools now happens in a single click. That is the kind of time saving that only becomes possible when everything is connected." — Justin Beadle, Director of Digital and Information, Elevate Consulting',
},
{
type: 'text',
eyebrow: 'Adoption',
heading: [
{ text: 'Workflows that ', fontFamily: 'serif' },
{ text: 'actually get used', fontFamily: 'sans' },
],
paragraphs: [
'The business development team finally had the CRM they had been asking for. Adoption came naturally: their data was already there when they logged in.',
'Justin built workflows for notifications across the team, alerting the right people in Teams when a prospect becomes a lead or when project milestones are reached. Forms in Twenty let the business development team log activity without leaving the tool. The impact is real for the organization — the tool has been adaptable from opportunity-level work at a client to executive-level decisions.',
'The flexibility to wire this together, without outside help and without fighting the platform, is what made it possible for a single person to stand up and maintain a connected stack across an entire consultancy.',
],
},
{
type: 'text',
eyebrow: 'What is next',
heading: [
{ text: 'Beyond ', fontFamily: 'serif' },
{ text: 'internal rollout', fontFamily: 'sans' },
],
paragraphs: [
'Elevate\'s CEO was so impressed with Twenty he started recommending it to clients before the internal setup was even complete. The team is exploring bringing Twenty to client projects as part of their consulting practice, including as the backend for custom-built products tailored to specific operational needs.',
'For a firm that once ran on sticky notes, this is more than an upgrade — it is a complete transformation.',
],
},
],
testimonial: {
eyebrow: 'What they say',
quote:
'Because Twenty\'s API is fully open, I could connect it to every other tool in our stack. When a proposal is won, what used to take four people across four tools now happens in a single click.',
author: {
name: 'Justin Beadle',
handle: 'Director of Digital and Information, Elevate Consulting',
date: 'June 2025',
avatarSrc: PLACEHOLDER_HERO,
},
},
tableOfContents: [
'From documents to open APIs',
'One API to rule them all',
'Workflows that actually get used',
'What is next',
],
catalogCard: {
summary:
'Elevate Consulting uses Twenty as the API backbone connecting billing, Teams, resourcing, and a custom front end around client and opportunity data.',
date: 'Jun 2025',
},
};
export const metadata: Metadata = {
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
};
export default async function ElevateConsultingCaseStudyPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={theme.colors.secondary.background[100]}
scheme="secondary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="secondary" />
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="secondary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="secondary" />
</Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} />
{CASE_STUDY.sections.map((block, index) => {
if (block.type === 'text') {
return <CaseStudy.TextBlock key={index} block={block} />;
}
return <CaseStudy.VisualBlock key={index} block={block} />;
})}
<CaseStudy.Testimonial testimonial={CASE_STUDY.testimonial} />
<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"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
<CaseStudy.TableOfContents items={CASE_STUDY.tableOfContents} />
</>
);
}
@@ -0,0 +1,168 @@
import { FAQ_DATA, MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import type { CaseStudyData } from '@/app/case-studies/_constants/types';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Faq } from '@/sections/Faq/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/home/hero/avatars/katherine-adams.jpg';
const CASE_STUDY: CaseStudyData = {
meta: {
title: 'A CRM that grows with you — NetZero & Twenty',
description:
'How NetZero uses Twenty across carbon credits, agricultural products, and franchised industrial systems with a modular CRM and a roadmap toward AI-assisted workflows.',
},
hero: {
readingTime: '8 min',
title: [
{ text: 'A CRM that ', fontFamily: 'serif' },
{ text: 'grows with you', fontFamily: 'sans' },
],
author: 'Olivier Reinaud',
clientIcon: 'netzero',
heroImageSrc: PLACEHOLDER_HERO,
},
sections: [
{
type: 'text',
eyebrow: 'NetZero',
heading: [
{ text: 'The right ', fontFamily: 'serif' },
{ text: 'foundation', fontFamily: 'sans' },
],
paragraphs: [
'NetZero works with the agro-industry, serving clients from multinationals to smallholder farmers. They sell carbon credits, agricultural products, and franchised industrial systems — three different product lines across multiple countries and company sizes. When Olivier Reinaud, co-founder of NetZero, started looking at CRMs in late 2024, he was not chasing the most feature-rich platform. He wanted the right foundation.',
],
callout:
'"Twenty delivers on what CRMs should have always been: fairly priced software with a fully modular and customizable model, a clean and modern UI, granular permissions, automations, enterprise features. A compelling solution with high potential to rightfully disrupt the CRM market." — Olivier Reinaud, co-founder of NetZero',
},
{
type: 'text',
eyebrow: 'Flexibility',
heading: [
{ text: 'A business that ', fontFamily: 'serif' },
{ text: 'does not fit a template', fontFamily: 'sans' },
],
paragraphs: [
'What convinced Olivier was the flexibility of the platform and where it was headed. Even when initial needs were basic record-keeping, he still needed a custom data model with granular permissions to manage the wide range of NetZero activities. He also needed a system that could adapt quickly to a fast-iteration company.',
'With Twenty, when a new need appears, he can address it himself — no developer required, no support ticket.',
],
callout:
'"The flexibility is really what made the difference. Our needs evolve very fast. I discover a new need and in two clicks I can address it. That is a real advantage when you are moving quickly." — Olivier Reinaud, co-founder of NetZero',
},
{
type: 'text',
eyebrow: 'Roadmap',
heading: [
{ text: 'From simple ', fontFamily: 'serif' },
{ text: 'to advanced', fontFamily: 'sans' },
],
paragraphs: [
'Olivier recognizes that NetZero\'s current use of Twenty is still relatively simple: workflows and integrations are not yet as deep as he eventually wants, because he prioritized getting foundations right first.',
'What is planned is significant. NetZero has a data lake, online forms, and multiple internal systems that he wants to connect to Twenty. The pipes are there; the next step is automations that tie them together.',
'What is coming in April 2026 is what he has been waiting for: AI-assisted workflow creation — describing what he needs and iterating from there instead of building complex logic from scratch. For a founder who runs the CRM himself, that changes what is realistically possible.',
],
},
{
type: 'text',
eyebrow: 'Results',
heading: [
{ text: 'The bet ', fontFamily: 'serif' },
{ text: 'is paying off', fontFamily: 'sans' },
],
paragraphs: [
'While NetZero still runs a second CRM in parallel for WhatsApp-heavy operations with farmers in Brazil, they expect to migrate all of it to Twenty as features and the ecosystem grow. Already, their structured, multinational pipeline is powered by Twenty.',
'The early bet on the architecture is holding, and upcoming AI features are expected to make it even more relevant.',
],
},
],
testimonial: {
eyebrow: 'What they say',
quote:
'The flexibility is really what made the difference. Our needs evolve very fast. I discover a new need and in two clicks I can address it.',
author: {
name: 'Olivier Reinaud',
handle: 'Co-founder, NetZero',
date: '2025',
avatarSrc: PLACEHOLDER_HERO,
},
},
tableOfContents: [
'The right foundation',
'A business that does not fit a template',
'From simple to advanced',
'The bet is paying off',
],
catalogCard: {
summary:
'NetZero uses Twenty as a modular CRM across product lines and countries, with a roadmap into AI-assisted workflows.',
date: '2025',
},
};
export const metadata: Metadata = {
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
};
export default async function NetZeroCaseStudyPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={theme.colors.secondary.background[100]}
scheme="secondary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="secondary" />
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="secondary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="secondary" />
</Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} />
{CASE_STUDY.sections.map((block, index) => {
if (block.type === 'text') {
return <CaseStudy.TextBlock key={index} block={block} />;
}
return <CaseStudy.VisualBlock key={index} block={block} />;
})}
<CaseStudy.Testimonial testimonial={CASE_STUDY.testimonial} />
<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"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
<CaseStudy.TableOfContents items={CASE_STUDY.tableOfContents} />
</>
);
}
@@ -0,0 +1,223 @@
import { FAQ_DATA, MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import type { CaseStudyCatalogEntry } from '@/app/case-studies/_constants/types';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudyCatalog } from '@/sections/CaseStudyCatalog/components';
import { Faq } from '@/sections/Faq/components';
import { Hero } from '@/sections/Hero/components';
import { Menu } from '@/sections/Menu/components';
import { Signoff } from '@/sections/Signoff/components';
import { theme } from '@/theme';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Case Studies — Twenty',
description:
'See how teams use Twenty to build custom CRM workflows and drive real business results.',
};
const HERO_HEADING = [
{ text: 'See how teams build ', fontFamily: 'serif' as const },
{ text: 'with Twenty', fontFamily: 'sans' as const },
];
const HERO_BODY = {
text: 'Real stories from real teams — how they shaped Twenty to fit their workflow and accelerated their growth.',
};
const SIGNOFF_HEADING = [
{ text: 'Ready to build ', fontFamily: 'serif' as const },
{ text: 'your own story?', fontFamily: 'sans' as const },
];
const SIGNOFF_BODY = {
text: 'Join the teams that chose to own their CRM. Start building with Twenty today.',
};
const PLACEHOLDER_HERO = '/images/home/hero/avatars/katherine-adams.jpg';
const CASE_STUDY_CATALOG_ENTRIES: CaseStudyCatalogEntry[] = [
{
href: '/case-studies/elevate-consulting',
hero: {
readingTime: '8 min',
title: [
{ text: 'Twenty as the API backbone ', fontFamily: 'serif' },
{ text: 'of a go-to-market stack', fontFamily: 'sans' },
],
author: 'Justin Beadle',
clientIcon: 'elevate-consulting',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'Elevate Consulting uses Twenty as the API backbone connecting billing, Teams, resourcing, and a custom front end around client and opportunity data.',
date: 'Jun 2025',
},
},
{
href: '/case-studies/9dots',
hero: {
readingTime: '9 min',
title: [
{ text: 'A real estate agency on WhatsApp ', fontFamily: 'serif' },
{ text: 'built a CRM around it', fontFamily: 'sans' },
],
author: 'Mike Babiy & Azmat Parveen',
clientIcon: 'nine-dots',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'Nine Dots put Twenty at the center of Homeseller\'s stack with APIs, automation, and AI on top of WhatsApp-heavy operations.',
date: 'Jul 2025',
},
},
{
href: '/case-studies/alternative-partners',
hero: {
readingTime: '7 min',
title: [
{ text: 'From Salesforce to ', fontFamily: 'serif' },
{ text: 'self-hosted Twenty', fontFamily: 'sans' },
],
author: 'Benjamin Reynolds',
clientIcon: 'alternative-partners',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'Alternative Partners replaced Salesforce with self-hosted Twenty, using agentic AI to compress migration work.',
date: '2025',
},
},
{
href: '/case-studies/netzero',
hero: {
readingTime: '8 min',
title: [
{ text: 'A CRM that ', fontFamily: 'serif' },
{ text: 'grows with you', fontFamily: 'sans' },
],
author: 'Olivier Reinaud',
clientIcon: 'netzero',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'NetZero uses Twenty as a modular CRM across product lines and countries, with a roadmap into AI-assisted workflows.',
date: '2025',
},
},
{
href: '/case-studies/act-education',
hero: {
readingTime: '7 min',
title: [
{ text: 'A CRM they ', fontFamily: 'serif' },
{ text: 'actually own', fontFamily: 'sans' },
],
author: 'Joseph Chiang',
clientIcon: 'act-education',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'AC&T and Flycoder moved from a dead vendor export to self-hosted Twenty — over 90% lower CRM cost and full control.',
date: '2025',
},
},
{
href: '/case-studies/w3villa',
hero: {
readingTime: '8 min',
title: [
{ text: 'When your CRM ', fontFamily: 'serif' },
{ text: 'is the product', fontFamily: 'sans' },
],
author: 'Amrendra Pratap Singh',
clientIcon: 'w3villa',
heroImageSrc: PLACEHOLDER_HERO,
},
catalogCard: {
summary:
'W3villa shipped W3Grads on Twenty — AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.',
date: '2025',
},
},
];
export default async function CaseStudiesCatalogPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={theme.colors.primary.background[100]}
scheme="primary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="primary" />
<Menu.Nav scheme="primary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="primary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="primary" />
</Menu.Root>
<Hero.Root backgroundColor={theme.colors.primary.background[100]}>
<Hero.Heading page={Pages.CaseStudies} segments={HERO_HEADING} />
<Hero.Body page={Pages.CaseStudies} body={HERO_BODY} />
</Hero.Root>
<CaseStudyCatalog.Grid entries={CASE_STUDY_CATALOG_ENTRIES} />
<Signoff.Root
backgroundColor={theme.colors.primary.background[100]}
color={theme.colors.primary.text[100]}
>
<Signoff.Heading segments={SIGNOFF_HEADING} />
<Signoff.Body body={SIGNOFF_BODY} />
<Signoff.Cta>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
label="Get started"
type="anchor"
variant="contained"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Signoff.Cta>
</Signoff.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"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
</>
);
}
@@ -0,0 +1,169 @@
import { FAQ_DATA, MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import type { CaseStudyData } from '@/app/case-studies/_constants/types';
import { Eyebrow, LinkButton } from '@/design-system/components';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { CaseStudy } from '@/sections/CaseStudy/components';
import { Faq } from '@/sections/Faq/components';
import { Menu } from '@/sections/Menu/components';
import { theme } from '@/theme';
import type { Metadata } from 'next';
const PLACEHOLDER_HERO = '/images/home/hero/avatars/katherine-adams.jpg';
const CASE_STUDY: CaseStudyData = {
meta: {
title:
'When your CRM is the product: W3Grads on Twenty — W3villa Technologies',
description:
'How W3villa Technologies shipped W3Grads, an AI mock interview platform for institutions, on Twenty as the operational backbone.',
},
hero: {
readingTime: '8 min',
title: [
{ text: 'When your CRM ', fontFamily: 'serif' },
{ text: 'is the product', fontFamily: 'sans' },
],
author: 'Amrendra Pratap Singh',
clientIcon: 'w3villa',
heroImageSrc: PLACEHOLDER_HERO,
},
sections: [
{
type: 'text',
eyebrow: 'W3Grads',
heading: [
{ text: 'Scale without ', fontFamily: 'serif' },
{ text: 'breaking operations', fontFamily: 'sans' },
],
paragraphs: [
'Running mock interview programs for hundreds of students sounds straightforward. In practice, universities and training institutes hit the same wall: registrations entered by hand, interview links sent one by one, faculty reviewing every session without scoring or classification. At real scale, it breaks.',
'W3villa Technologies set out to solve it properly — not with a workaround, but with a product.',
],
callout:
'"We did not want to patch over the problem. We wanted to build something institutions could rely on at scale, and that meant starting from a foundation solid enough to support the full complexity of what we had in mind." — Amrendra Pratap Singh, VP of Engineering, W3villa Technologies',
},
{
type: 'text',
eyebrow: 'Architecture',
heading: [
{ text: 'Focus on the use case, ', fontFamily: 'serif' },
{ text: 'not the plumbing', fontFamily: 'sans' },
],
paragraphs: [
'W3villa built W3Grads (w3grads.com), an AI-powered mock interview platform for universities and training institutes, using Twenty as its operational backbone.',
'The key decision was not to build everything from scratch. Twenty covers the data model, permissions, authentication, and workflow engine — the parts that would have taken months to rebuild — so the team could focus on product-specific logic.',
'When a student registers via QR at a campus event, the system assigns a plan, generates an interview session, and sends a link. The AI conducts the interview, scores the candidate, and classifies the result. Faculty see where each student stands without manually reviewing every session. Building and iterating on these workflows was faster with AI in the loop.',
],
},
{
type: 'text',
eyebrow: 'Scale',
heading: [
{ text: 'A platform ', fontFamily: 'serif' },
{ text: 'ready to grow', fontFamily: 'sans' },
],
paragraphs: [
'Because the foundation is solid, W3Grads is architected for what comes next — including a payment layer for future paid interview plans and nationwide scale without structural rewrites.',
],
callout:
'"Twenty gave us the flexibility to model the entire interview lifecycle as custom objects and workflows. We could build something genuinely complex without fighting the platform to do it." — Piyush Khandelwal, Director, W3villa Technologies, Partner',
},
{
type: 'text',
eyebrow: 'The result',
heading: [
{ text: 'Zero manual work ', fontFamily: 'serif' },
{ text: 'at the core', fontFamily: 'sans' },
],
paragraphs: [
'Programs that previously needed heavy manual coordination now run end-to-end with automation. Institutions get a scalable, intelligent system; students get faster preparation for interviews that matter; W3villa shipped a product institutions can build revenue around.',
'Zero manual work at the core. Full automation. Built on Twenty.',
],
},
],
testimonial: {
eyebrow: 'What they say',
quote:
'Twenty gave us the flexibility to model the entire interview lifecycle as custom objects and workflows.',
author: {
name: 'Piyush Khandelwal',
handle: 'Director, W3villa Technologies',
date: '2025',
avatarSrc: PLACEHOLDER_HERO,
},
},
tableOfContents: [
'Scale without breaking operations',
'Focus on the use case, not the plumbing',
'A platform ready to grow',
'The result',
],
catalogCard: {
summary:
'W3villa shipped W3Grads on Twenty — AI interviews, scoring, and institution-scale workflows without rebuilding CRM plumbing.',
date: '2025',
},
};
export const metadata: Metadata = {
title: CASE_STUDY.meta.title,
description: CASE_STUDY.meta.description,
};
export default async function W3villaCaseStudyPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<Menu.Root
backgroundColor={theme.colors.secondary.background[100]}
scheme="secondary"
navItems={MENU_DATA.navItems}
socialLinks={menuSocialLinks}
>
<Menu.Logo scheme="secondary" />
<Menu.Nav scheme="secondary" navItems={MENU_DATA.navItems} />
<Menu.Social scheme="secondary" socialLinks={menuSocialLinks} />
<Menu.Cta scheme="secondary" />
</Menu.Root>
<CaseStudy.Hero hero={CASE_STUDY.hero} />
{CASE_STUDY.sections.map((block, index) => {
if (block.type === 'text') {
return <CaseStudy.TextBlock key={index} block={block} />;
}
return <CaseStudy.VisualBlock key={index} block={block} />;
})}
<CaseStudy.Testimonial testimonial={CASE_STUDY.testimonial} />
<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"
/>
<TalkToUsButton
color="primary"
label="Talk to us"
variant="outlined"
/>
</Faq.Cta>
</Faq.Intro>
<Faq.Items questions={FAQ_DATA.questions} />
</Faq.Root>
<CaseStudy.TableOfContents items={CASE_STUDY.tableOfContents} />
</>
);
}
@@ -0,0 +1,14 @@
'use client';
import { CONTACT_CAL_FORM_CAL_LINK } from '@/app/_constants/contact-cal';
import Cal from '@calcom/embed-react';
export function CalFormEmbed() {
return (
<Cal
calLink={CONTACT_CAL_FORM_CAL_LINK}
config={{ theme: 'dark' }}
style={{ minHeight: 520, overflow: 'auto', width: '100%' }}
/>
);
}
@@ -0,0 +1,145 @@
'use client';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import dynamic from 'next/dynamic';
import { useCallback, useEffect } from 'react';
import { createPortal } from 'react-dom';
const EmbedFallback = styled.p`
color: ${theme.colors.secondary.text[60]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
margin: 0;
padding-bottom: ${theme.spacing(8)};
padding-top: ${theme.spacing(8)};
text-align: center;
`;
const CalFormEmbed = dynamic(
() =>
import('./CalFormEmbed').then((mod) => ({
default: mod.CalFormEmbed,
})),
{
loading: () => <EmbedFallback>Loading form</EmbedFallback>,
ssr: false,
},
);
const Overlay = styled.div`
align-items: center;
backdrop-filter: blur(4px);
background: rgba(28, 28, 28, 0.8);
box-sizing: border-box;
display: flex;
inset: 0;
justify-content: center;
padding-bottom: ${theme.spacing(4)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(4)};
position: fixed;
z-index: 300;
`;
const Panel = styled.div`
background: #0c0c0c;
border-radius: ${theme.radius(2)};
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${theme.spacing(4)};
max-height: 100%;
max-width: 100%;
overflow-y: auto;
padding-bottom: ${theme.spacing(6)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(5)};
position: relative;
width: min(100%, 720px);
@media (min-width: ${theme.breakpoints.md}px) {
padding-bottom: ${theme.spacing(8)};
padding-left: ${theme.spacing(6)};
padding-right: ${theme.spacing(6)};
padding-top: ${theme.spacing(6)};
}
`;
const Title = styled.h2`
color: ${theme.colors.secondary.text[100]};
font-family: ${theme.font.family.serif};
font-size: ${theme.font.size(10)};
font-weight: ${theme.font.weight.light};
line-height: ${theme.lineHeight(11.5)};
margin: 0;
@media (min-width: ${theme.breakpoints.md}px) {
font-size: ${theme.font.size(12)};
line-height: ${theme.lineHeight(14)};
}
`;
const EmbedShell = styled.div`
min-height: 400px;
width: 100%;
`;
type ContactCalModalProps = {
open: boolean;
onClose: () => void;
};
export function ContactCalModal({ open, onClose }: ContactCalModalProps) {
const handleOverlayPointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) {
onClose();
}
},
[onClose],
);
useEffect(() => {
if (!open) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
document.addEventListener('keydown', handleKeyDown);
return () => {
document.body.style.overflow = previousOverflow;
document.removeEventListener('keydown', handleKeyDown);
};
}, [open, onClose]);
if (!open) {
return null;
}
return createPortal(
<Overlay onPointerDown={handleOverlayPointerDown}>
<Panel
aria-labelledby="contact-cal-modal-title"
aria-modal="true"
role="dialog"
>
<Title id="contact-cal-modal-title">Talk to us</Title>
<EmbedShell>
<CalFormEmbed />
</EmbedShell>
</Panel>
</Overlay>,
document.body,
);
}
@@ -0,0 +1,53 @@
'use client';
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from 'react';
import { ContactCalModal } from './ContactCalModal';
type ContactCalModalContextValue = {
openContactCalModal: () => void;
};
const ContactCalModalContext =
createContext<ContactCalModalContextValue | null>(null);
export function useContactCalModal() {
const value = useContext(ContactCalModalContext);
if (!value) {
throw new Error(
'useContactCalModal must be used within ContactCalModalRoot',
);
}
return value;
}
export function ContactCalModalRoot({ children }: { children: ReactNode }) {
const [open, setOpen] = useState(false);
const openContactCalModal = useCallback(() => {
setOpen(true);
}, []);
const closeContactCalModal = useCallback(() => {
setOpen(false);
}, []);
const contextValue = useMemo(
() => ({ openContactCalModal }),
[openContactCalModal],
);
return (
<ContactCalModalContext.Provider value={contextValue}>
{children}
<ContactCalModal onClose={closeContactCalModal} open={open} />
</ContactCalModalContext.Provider>
);
}
@@ -0,0 +1,29 @@
'use client';
import {
BaseButton,
buttonBaseStyles,
} from '@/design-system/components/Button/BaseButton';
import { styled } from '@linaria/react';
import { useContactCalModal } from './ContactCalModalRoot';
const StyledTrigger = styled.button`
${buttonBaseStyles}
`;
type TalkToUsButtonProps = {
color: 'primary' | 'secondary';
label: string;
variant: 'contained' | 'outlined';
};
export function TalkToUsButton({ color, label, variant }: TalkToUsButtonProps) {
const { openContactCalModal } = useContactCalModal();
return (
<StyledTrigger type="button" onClick={openContactCalModal}>
<BaseButton color={color} label={label} variant={variant} />
</StyledTrigger>
);
}
@@ -0,0 +1,2 @@
export { ContactCalModalRoot, useContactCalModal } from './ContactCalModalRoot';
export { TalkToUsButton } from './TalkToUsButton';
+15 -12
View File
@@ -1,5 +1,6 @@
import { FOOTER_DATA } from '@/app/_constants/footer';
import { FooterVisibilityGate } from '@/app/_components/FooterVisibilityGate';
import { FOOTER_DATA } from '@/app/_constants/footer';
import { ContactCalModalRoot } from '@/app/components/ContactCalModal';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { Footer } from '@/sections/Footer/components';
@@ -85,17 +86,19 @@ export default async function RootLayout({
<body
className={`${cssVariables} ${hostGrotesk.variable} ${aleo.variable} ${azeretMono.variable} ${vt323.variable}`}
>
<StyledMain>{children}</StyledMain>
<FooterVisibilityGate>
<Footer.Root illustration={FOOTER_DATA.illustration}>
<Footer.Logo />
<Footer.Nav groups={FOOTER_DATA.navGroups} />
<Footer.Bottom
copyright={FOOTER_DATA.bottom.copyright}
links={footerSocialLinks}
/>
</Footer.Root>
</FooterVisibilityGate>
<ContactCalModalRoot>
<StyledMain>{children}</StyledMain>
<FooterVisibilityGate>
<Footer.Root illustration={FOOTER_DATA.illustration}>
<Footer.Logo />
<Footer.Nav groups={FOOTER_DATA.navGroups} />
<Footer.Bottom
copyright={FOOTER_DATA.bottom.copyright}
links={footerSocialLinks}
/>
</Footer.Root>
</FooterVisibilityGate>
</ContactCalModalRoot>
</body>
</html>
);
@@ -2,7 +2,7 @@ import type { EngagementBandDataType } from '@/sections/EngagementBand/types';
export const ENGAGEMENT_BAND_DATA: EngagementBandDataType = {
heading: {
text: 'Looking for a partners',
text: 'Looking for a partner?',
fontFamily: 'serif',
},
body: {
@@ -0,0 +1,32 @@
export type PartnerProgramId = 'technology' | 'content' | 'solutions';
export const PARTNER_PROGRAM_OPTIONS: ReadonlyArray<{
id: PartnerProgramId;
label: string;
}> = [
{ id: 'technology', label: 'Technology Partner' },
{ id: 'content', label: 'Content & Community Partner' },
{ id: 'solutions', label: 'Solutions Partner' },
];
export const PARTNER_APPLICATION_MODAL_COPY = {
titleSerif: 'Apply to build',
titleSans: 'the future of CRM',
subtitleLine1:
'Join our ecosystem and help businesses take control of their customer data with',
subtitleLine2: 'open-source primitives.',
selectLabel: 'Select your team',
fields: {
name: 'Your name *',
email: 'Work email *',
company: 'Company or brand *',
website: 'Website or github link *',
opportunities: 'Estimated monthly opportunities (optional)',
messageLabel: 'How do you want to partner with Twenty? *',
messageHint:
'Tell us about the custom solutions or integrations you plan to build.',
},
footnote:
"Our partner team typically reviews applications within 48 hours. Once approved, you'll get access to our partner portal and developer resources.",
submit: 'Submit application',
} as const;
@@ -7,47 +7,32 @@ export const TESTIMONIALS_DATA: TestimonialsDataType = {
testimonials: [
{
heading: {
text: 'Our clients ask for a CRM they can trust and shape. Twenty gives us a credible stack to implement fast, without hiding the data model behind opaque APIs.',
text: "Twenty gives you the kind of flexibility that actually changes what you can offer your clients. The dev experience is clean, the APIs are open, and when something needs to be customized, you can just do it. There's no fighting the platform.",
fontFamily: 'sans',
},
author: {
avatar: {
src: '/images/partner/testimonials/avatar-four.png',
alt: '',
},
name: { text: 'Elena Vasquez' },
handle: { text: '@elena_integrations' },
date: new Date('2024-04-18'),
name: { text: 'Benjamin Reynolds' },
designation: { text: 'Principal and Founder, Alternative Partners' },
},
},
{
heading: {
text: 'Partnering around an open codebase changed how we scope projects. We ship integrations with clear ownership and fewer surprises at go-live.',
text: "The flexibility is just amazing. Literally, there's nothing you cannot do. You can create objects, access everything through the API, pull notes and send them to the portal. Try doing that in HubSpot. No way. It's the true ability to build exactly what's actually needed.",
fontFamily: 'sans',
},
author: {
avatar: {
src: '/images/partner/testimonials/avatar-five.png',
alt: '',
},
name: { text: 'Jordan Okonkwo' },
handle: { text: '@jordan_solutions' },
date: new Date('2024-06-27'),
name: { text: 'Bertrams' },
designation: { text: 'Founder, Wintactix' },
},
},
{
heading: {
text: 'We needed something modern for mid-market teams that outgrow spreadsheets but dread enterprise lock-in. Twenty hits that gap for the customers we serve.',
text: "Twenty Apps opens the door to building products, not just implementations. For example, we're developing a WhatsApp Business integration that any Twentys client could get. That's a recurring revenue stream we wouldn't have if we were just configuring someone else's platform.",
fontFamily: 'sans',
},
author: {
avatar: {
src: '/images/partner/testimonials/avatar-two.png',
alt: '',
},
name: { text: 'Samira Haddad' },
handle: { text: '@samira_revenue' },
date: new Date('2024-08-14'),
name: { text: 'Mike Babiy' },
designation: { text: 'Founder, Nine Dots Ventures' },
},
},
],
@@ -0,0 +1,35 @@
'use client';
import {
BaseButton,
buttonBaseStyles,
} from '@/design-system/components/Button/BaseButton';
import { styled } from '@linaria/react';
import { usePartnerApplicationModal } from './PartnerApplicationModalRoot';
const StyledTrigger = styled.button`
${buttonBaseStyles}
`;
type BecomePartnerButtonProps = {
color?: 'primary' | 'secondary';
variant?: 'contained' | 'outlined';
};
export function BecomePartnerButton({
color = 'secondary',
variant = 'contained',
}: BecomePartnerButtonProps) {
const { openPartnerApplicationModal } = usePartnerApplicationModal();
return (
<StyledTrigger type="button" onClick={openPartnerApplicationModal}>
<BaseButton
color={color}
label="Become a partner"
variant={variant}
/>
</StyledTrigger>
);
}
@@ -0,0 +1,586 @@
'use client';
import {
PARTNER_APPLICATION_MODAL_COPY,
PARTNER_PROGRAM_OPTIONS,
type PartnerProgramId,
} from '@/app/partner/_constants/partner-application-modal';
import { buttonBaseStyles } from '@/design-system/components/Button/BaseButton';
import { ButtonShape } from '@/design-system/components/Button/ButtonShape';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { IconChevronDown } from '@tabler/icons-react';
import { useCallback, useEffect, useId, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
const PANEL_BG = '#0c0c0c';
const Overlay = styled.div`
align-items: center;
backdrop-filter: blur(4px);
background: rgba(28, 28, 28, 0.8);
box-sizing: border-box;
display: flex;
inset: 0;
justify-content: center;
padding-bottom: ${theme.spacing(4)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(4)};
position: fixed;
z-index: 300;
`;
const Panel = styled.div`
background: ${PANEL_BG};
border-radius: ${theme.radius(2)};
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: clamp(8px, 1.5vh, 16px);
max-height: 100%;
max-width: 100%;
overflow-y: auto;
padding-bottom: clamp(16px, 4vh, 40px);
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: clamp(12px, 2vh, 16px);
position: relative;
width: min(360px, 100%);
@media (min-width: ${theme.breakpoints.md}px) {
padding-bottom: clamp(16px, 4vh, 40px);
padding-left: ${theme.spacing(6)};
padding-right: ${theme.spacing(6)};
padding-top: clamp(16px, 4vh, 40px);
width: min(902px, 100%);
}
`;
const TitleBlock = styled.div`
display: flex;
flex-direction: column;
gap: clamp(8px, 2vh, 24px);
`;
const Title = styled.h2`
color: ${theme.colors.secondary.text[100]};
font-family: ${theme.font.family.serif};
font-size: clamp(${theme.font.size(12)}, 8vw, ${theme.font.size(15)});
font-weight: ${theme.font.weight.light};
line-height: 1.12;
margin: 0;
@media (min-width: ${theme.breakpoints.md}px) {
font-size: clamp(${theme.font.size(15)}, 4vw, ${theme.font.size(20)});
line-height: 1.05;
}
`;
const TitleAccent = styled.span`
display: block;
font-family: ${theme.font.family.sans};
font-weight: ${theme.font.weight.light};
letter-spacing: -0.04em;
`;
const Subtitle = styled.div`
color: ${theme.colors.secondary.text[60]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(5.5)};
& p {
margin: 0;
}
`;
const Segments = styled.div`
display: none;
gap: ${theme.spacing(4)};
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
display: flex;
}
`;
const SegmentButton = styled.button`
background: rgba(255, 255, 255, 0.1);
border: 1px solid ${theme.colors.secondary.border[10]};
border-radius: ${theme.radius(2)};
box-sizing: border-box;
color: ${theme.colors.secondary.text[100]};
cursor: pointer;
flex: 1 1 0;
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3.5)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(3.5)};
min-width: 0;
padding-bottom: ${theme.spacing(1.5)};
padding-left: ${theme.spacing(2)};
padding-right: ${theme.spacing(2)};
padding-top: ${theme.spacing(1.5)};
&[data-active='true'] {
background: ${theme.colors.primary.background[100]};
color: ${theme.colors.primary.text[100]};
}
&:focus-visible {
outline: 2px solid ${theme.colors.highlight[100]};
outline-offset: 2px;
}
`;
const MobileProgramField = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.spacing(2)};
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
display: none;
}
`;
const DropdownRoot = styled.div`
position: relative;
width: 100%;
`;
const DropdownTrigger = styled.button`
align-items: center;
background: ${theme.colors.secondary.background[100]};
border: 1px solid ${theme.colors.highlight[100]};
border-radius: ${theme.radius(2)};
box-sizing: border-box;
color: ${theme.colors.secondary.text[100]};
cursor: pointer;
display: flex;
font-family: ${theme.font.family.sans};
height: clamp(40px, 5.5vh, 56px);
justify-content: space-between;
padding-left: ${theme.spacing(3)};
padding-right: ${theme.spacing(1)};
width: 100%;
&:focus-visible {
outline: 2px solid ${theme.colors.highlight[100]};
outline-offset: 2px;
}
`;
const DropdownTriggerContent = styled.div`
display: flex;
flex-direction: column;
gap: 2px;
`;
const DropdownLabel = styled.span`
color: ${theme.colors.secondary.text[40]};
font-size: ${theme.font.size(2.5)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(4)};
text-align: left;
`;
const DropdownValue = styled.span`
font-size: ${theme.font.size(4)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(5.5)};
text-align: left;
`;
const DropdownIconContainer = styled.span`
align-items: center;
display: flex;
flex-shrink: 0;
height: 48px;
justify-content: center;
width: 48px;
`;
const DropdownPanel = styled.div`
background: ${theme.colors.secondary.background[100]};
border: 1px solid ${theme.colors.highlight[100]};
border-radius: ${theme.radius(2)};
box-shadow: 0 0 16px 0 rgba(15, 15, 15, 0.25);
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: ${theme.spacing(1)};
left: 0;
margin-top: ${theme.spacing(2)};
overflow: hidden;
padding: ${theme.spacing(1)};
position: absolute;
right: 0;
z-index: 1;
`;
const DropdownOption = styled.button`
align-items: center;
background: transparent;
border: none;
border-radius: ${theme.radius(1)};
box-sizing: border-box;
color: ${theme.colors.secondary.text[100]};
cursor: pointer;
display: flex;
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(5.5)};
padding-bottom: ${theme.spacing(3)};
padding-left: ${theme.spacing(2)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(3)};
text-align: left;
width: 100%;
&[data-selected='true'] {
background: rgba(74, 56, 245, 0.3);
}
&:hover {
background: rgba(74, 56, 245, 0.15);
}
&[data-selected='true']:hover {
background: rgba(74, 56, 245, 0.3);
}
&:focus-visible {
outline: 2px solid ${theme.colors.highlight[100]};
outline-offset: -2px;
}
`;
const FieldRow = styled.div`
display: flex;
flex-direction: column;
gap: clamp(8px, 1.5vh, 16px);
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
flex-direction: row;
gap: ${theme.spacing(6)};
}
`;
const TextInput = styled.input`
background: transparent;
border: 1px solid ${theme.colors.secondary.border[20]};
border-radius: ${theme.radius(2)};
box-sizing: border-box;
color: ${theme.colors.secondary.text[100]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
font-weight: ${theme.font.weight.regular};
height: clamp(40px, 5.5vh, 56px);
line-height: ${theme.lineHeight(5.5)};
padding-bottom: ${theme.spacing(1)};
padding-left: ${theme.spacing(3)};
padding-right: ${theme.spacing(3)};
padding-top: ${theme.spacing(1)};
width: 100%;
&::placeholder {
color: ${theme.colors.secondary.text[40]};
}
&:focus-visible {
border-color: ${theme.colors.highlight[100]};
outline: none;
}
`;
const TextArea = styled.textarea`
background: transparent;
border: 1px solid ${theme.colors.secondary.border[20]};
border-radius: ${theme.radius(2)};
box-sizing: border-box;
color: ${theme.colors.secondary.text[100]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(5.5)};
min-height: clamp(80px, 18vh, 185px);
padding-bottom: ${theme.spacing(1)};
padding-left: ${theme.spacing(3)};
padding-right: ${theme.spacing(3)};
padding-top: ${theme.spacing(1)};
resize: vertical;
width: 100%;
&::placeholder {
color: ${theme.colors.secondary.text[40]};
}
&:focus-visible {
border-color: ${theme.colors.highlight[100]};
outline: none;
}
`;
const Footnote = styled.p`
color: ${theme.colors.secondary.text[40]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(3.5)};
margin: 0;
`;
const SubmitButton = styled.button`
${buttonBaseStyles}
position: relative;
width: 100%;
`;
const SubmitLabel = styled.span`
color: ${theme.colors.primary.text[100]};
font-family: ${theme.font.family.mono};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.medium};
position: relative;
text-transform: uppercase;
z-index: 1;
`;
const FooterBlock = styled.div`
display: flex;
flex-direction: column;
gap: clamp(8px, 1.5vh, 16px);
`;
const FormFields = styled.div`
display: flex;
flex-direction: column;
gap: clamp(8px, 1.5vh, 16px);
`;
type PartnerApplicationModalProps = {
open: boolean;
onClose: () => void;
};
export function PartnerApplicationModal({
open,
onClose,
}: PartnerApplicationModalProps) {
const titleId = useId();
const panelRef = useRef<HTMLDivElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
const [programId, setProgramId] = useState<PartnerProgramId>('technology');
const [dropdownOpen, setDropdownOpen] = useState(false);
const handleOverlayPointerDown = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (event.target === event.currentTarget) {
onClose();
}
},
[onClose],
);
useEffect(() => {
if (!open) {
return;
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
onClose();
}
};
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
document.addEventListener('keydown', handleKeyDown);
const frame = window.requestAnimationFrame(() => {
panelRef.current
?.querySelector<HTMLElement>(
'button, [href], input, textarea, [tabindex]:not([tabindex="-1"])',
)
?.focus();
});
return () => {
document.body.style.overflow = previousOverflow;
document.removeEventListener('keydown', handleKeyDown);
window.cancelAnimationFrame(frame);
};
}, [open, onClose]);
const handleDropdownBlur = useCallback(
(event: React.FocusEvent<HTMLDivElement>) => {
if (!dropdownRef.current?.contains(event.relatedTarget as Node)) {
setDropdownOpen(false);
}
},
[],
);
const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
},
[],
);
if (!open) {
return null;
}
const copy = PARTNER_APPLICATION_MODAL_COPY;
return createPortal(
<Overlay onPointerDown={handleOverlayPointerDown}>
<Panel
ref={panelRef}
aria-labelledby={titleId}
aria-modal="true"
role="dialog"
>
<TitleBlock>
<Title id={titleId}>
{copy.titleSerif}
<TitleAccent>{copy.titleSans}</TitleAccent>
</Title>
<Subtitle>
<p>{copy.subtitleLine1}</p>
<p>{copy.subtitleLine2}</p>
</Subtitle>
</TitleBlock>
<form onSubmit={handleSubmit}>
<FormFields>
<Segments role="radiogroup" aria-label={copy.selectLabel}>
{PARTNER_PROGRAM_OPTIONS.map((option) => (
<SegmentButton
key={option.id}
aria-checked={programId === option.id}
data-active={programId === option.id}
role="radio"
type="button"
onClick={() => {
setProgramId(option.id);
}}
>
{option.label}
</SegmentButton>
))}
</Segments>
<MobileProgramField>
<DropdownRoot ref={dropdownRef} onBlur={handleDropdownBlur}>
<DropdownTrigger
aria-expanded={dropdownOpen}
aria-haspopup="listbox"
aria-label={copy.selectLabel}
type="button"
onClick={() => {
setDropdownOpen((previous) => !previous);
}}
>
<DropdownTriggerContent>
<DropdownLabel>{copy.selectLabel}</DropdownLabel>
<DropdownValue>
{PARTNER_PROGRAM_OPTIONS.find(
(option) => option.id === programId,
)?.label}
</DropdownValue>
</DropdownTriggerContent>
<DropdownIconContainer aria-hidden>
<IconChevronDown size={20} stroke={1.5} />
</DropdownIconContainer>
</DropdownTrigger>
{dropdownOpen && (
<DropdownPanel role="listbox" aria-label={copy.selectLabel}>
{PARTNER_PROGRAM_OPTIONS.map((option) => (
<DropdownOption
key={option.id}
aria-selected={programId === option.id}
data-selected={programId === option.id}
role="option"
type="button"
onClick={() => {
setProgramId(option.id);
setDropdownOpen(false);
}}
>
{option.label}
</DropdownOption>
))}
</DropdownPanel>
)}
</DropdownRoot>
</MobileProgramField>
<TextInput
autoComplete="name"
name="name"
placeholder={copy.fields.name}
required
type="text"
/>
<FieldRow>
<TextInput
autoComplete="email"
name="email"
placeholder={copy.fields.email}
required
type="email"
/>
<TextInput
autoComplete="organization"
name="company"
placeholder={copy.fields.company}
required
type="text"
/>
</FieldRow>
<TextInput
name="website"
placeholder={copy.fields.website}
required
type="text"
/>
<TextInput
name="opportunities"
placeholder={copy.fields.opportunities}
type="text"
/>
<TextArea
name="message"
placeholder={`${copy.fields.messageLabel}\n\n${copy.fields.messageHint}`}
required
/>
<FooterBlock>
<Footnote>{copy.footnote}</Footnote>
<SubmitButton type="submit">
<ButtonShape
fillColor={theme.colors.primary.background[100]}
strokeColor="none"
/>
<SubmitLabel>{copy.submit}</SubmitLabel>
</SubmitButton>
</FooterBlock>
</FormFields>
</form>
</Panel>
</Overlay>,
document.body,
);
}
@@ -0,0 +1,60 @@
'use client';
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from 'react';
import { PartnerApplicationModal } from './PartnerApplicationModal';
type PartnerApplicationModalContextValue = {
openPartnerApplicationModal: () => void;
};
const PartnerApplicationModalContext =
createContext<PartnerApplicationModalContextValue | null>(null);
export function usePartnerApplicationModal() {
const value = useContext(PartnerApplicationModalContext);
if (!value) {
throw new Error(
'usePartnerApplicationModal must be used within PartnerApplicationModalRoot',
);
}
return value;
}
export function PartnerApplicationModalRoot({
children,
}: {
children: ReactNode;
}) {
const [open, setOpen] = useState(false);
const openPartnerApplicationModal = useCallback(() => {
setOpen(true);
}, []);
const closePartnerApplicationModal = useCallback(() => {
setOpen(false);
}, []);
const contextValue = useMemo(
() => ({ openPartnerApplicationModal }),
[openPartnerApplicationModal],
);
return (
<PartnerApplicationModalContext.Provider value={contextValue}>
{children}
<PartnerApplicationModal
onClose={closePartnerApplicationModal}
open={open}
/>
</PartnerApplicationModalContext.Provider>
);
}
@@ -0,0 +1,11 @@
'use client';
import { BecomePartnerButton } from './BecomePartnerButton';
export function PartnerHeroCtas() {
return (
<>
<BecomePartnerButton variant="contained" />
</>
);
}
@@ -0,0 +1,18 @@
'use client';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import { BecomePartnerButton } from './BecomePartnerButton';
export function PartnerSignoffCtas() {
return (
<>
<BecomePartnerButton variant="outlined" />
<TalkToUsButton
color="secondary"
label="Talk to us"
variant="contained"
/>
</>
);
}
@@ -0,0 +1,4 @@
export { BecomePartnerButton } from './BecomePartnerButton';
export { PartnerApplicationModalRoot } from './PartnerApplicationModalRoot';
export { PartnerHeroCtas } from './PartnerHeroCtas';
export { PartnerSignoffCtas } from './PartnerSignoffCtas';
@@ -1,4 +1,5 @@
import { FAQ_DATA, MENU_DATA, TRUSTED_BY_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import {
ENGAGEMENT_BAND_DATA,
HERO_DATA,
@@ -6,6 +7,11 @@ import {
TESTIMONIALS_DATA,
THREE_CARDS_ILLUSTRATION_DATA,
} from '@/app/partner/_constants';
import {
PartnerApplicationModalRoot,
PartnerHeroCtas,
PartnerSignoffCtas,
} from '@/app/partner/components/PartnerApplication';
import { Body, Eyebrow, Heading, LinkButton } from '@/design-system/components';
import { Pages } from '@/enums/pages';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
@@ -32,7 +38,7 @@ export default async function PartnerPage() {
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<>
<PartnerApplicationModalRoot>
<Menu.Root
backgroundColor={theme.colors.primary.background[100]}
scheme="primary"
@@ -49,20 +55,7 @@ export default async function PartnerPage() {
<Hero.Heading page={Pages.Partner} segments={HERO_DATA.heading} />
<Hero.Body page={Pages.Partner} body={HERO_DATA.body} />
<Hero.Cta>
<LinkButton
color="secondary"
href="https://app.twenty.com/welcome"
label="Become a partner"
type="anchor"
variant="outlined"
/>
<LinkButton
color="secondary"
href="https://app.twenty.com/welcome"
label="Find a partner"
type="anchor"
variant="contained"
/>
<PartnerHeroCtas />
</Hero.Cta>
<Hero.PartnerVisual />
</Hero.Root>
@@ -89,9 +82,9 @@ export default async function PartnerPage() {
<EngagementBand.Actions>
<LinkButton
color="primary"
href="https://app.twenty.com/welcome"
href="/case-studies"
label="Read our case studies"
type="anchor"
type="link"
variant="contained"
/>
</EngagementBand.Actions>
@@ -136,20 +129,7 @@ export default async function PartnerPage() {
<Signoff.Heading segments={SIGNOFF_DATA.heading} />
<Signoff.Body body={SIGNOFF_DATA.body} />
<Signoff.Cta>
<LinkButton
color="secondary"
href="https://app.twenty.com/welcome"
label="Become a partner"
type="anchor"
variant="outlined"
/>
<LinkButton
color="secondary"
href="https://twenty.com/contact"
label="Talk to us"
type="anchor"
variant="contained"
/>
<PartnerSignoffCtas />
</Signoff.Cta>
</Signoff.Root>
@@ -165,17 +145,15 @@ export default async function PartnerPage() {
type="anchor"
variant="contained"
/>
<LinkButton
<TalkToUsButton
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>
</>
</PartnerApplicationModalRoot>
);
}
@@ -1,13 +1,7 @@
import type { PlanTableDataType } from '@/sections/PlanTable/types';
const EMAIL_OUTREACH_TASK = {
label: 'Tasks',
options: ['email outreach'],
value: 'email outreach',
} as const;
export const PLAN_TABLE_DATA: PlanTableDataType = {
featureColumnLabel: 'Feature',
featureColumnLabel: 'Name',
rows: [
{
featureLabel: 'Price',
@@ -18,7 +12,7 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
type: 'row',
},
{
featureLabel: 'Self Hosting discount',
featureLabel: 'Self-hosting discount',
tiers: {
organization: { kind: 'text', text: 'Free for up to 20 seats' },
pro: { kind: 'text', text: 'Free for up to 20 seats' },
@@ -33,113 +27,6 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
},
type: 'row',
},
{
title: 'Reports',
type: 'category',
},
{
featureLabel: 'Number of dashboards',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Unlimited' },
},
type: 'row',
},
{
title: 'Automations',
type: 'category',
},
{
featureLabel: 'Workflows',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'AI Agent',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'Automation credits',
tiers: {
organization: { kind: 'text', text: '1,000' },
pro: { kind: 'text', text: '1,000' },
},
type: 'row',
},
{
featureLabel: 'Extra credits',
tiers: {
organization: { kind: 'dash' },
pro: {
kind: 'text',
text: '1,000 for $10 - 5,000 for $48',
},
},
type: 'row',
},
{
calculator: {
priceLine: {
amount: '700€',
label: 'Price',
periodSuffix: '/month',
},
sections: [
{
id: 'intelligence-ai',
modelField: {
label: 'Model',
options: ['GPT 5'],
value: 'GPT 5',
},
requestField: { label: 'Requests', value: 100 },
tasksField: {
label: EMAIL_OUTREACH_TASK.label,
options: [...EMAIL_OUTREACH_TASK.options],
value: EMAIL_OUTREACH_TASK.value,
},
title: 'Intelligence AI',
},
{
id: 'compute',
requestField: { label: 'Requests', value: 100 },
tasksField: {
label: EMAIL_OUTREACH_TASK.label,
options: [...EMAIL_OUTREACH_TASK.options],
value: EMAIL_OUTREACH_TASK.value,
},
title: 'Compute',
},
{
id: 'storage',
requestField: { label: 'Requests', value: 100 },
tasksField: {
label: EMAIL_OUTREACH_TASK.label,
options: [...EMAIL_OUTREACH_TASK.options],
value: EMAIL_OUTREACH_TASK.value,
},
title: 'Storage',
},
],
visual: {
body: {
text: 'Estimate your costs effortlessly with our price calculator customize your workspace in just a few clicks!',
},
heading: [
{ fontFamily: 'serif', text: 'Get a glimpse of ' },
{ fontFamily: 'sans', text: "what it'll cost" },
],
},
},
type: 'calculator',
},
{
title: 'Workspace',
type: 'category',
@@ -161,7 +48,55 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
type: 'row',
},
{
featureLabel: 'API access',
featureLabel: 'Custom views',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Unlimited' },
},
type: 'row',
},
{
featureLabel: 'Custom layout',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Unlimited' },
},
type: 'row',
},
{
featureLabel: 'Records',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Unlimited' },
},
type: 'row',
},
{
title: 'Reports',
type: 'category',
},
{
featureLabel: 'Number of dashboards',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Unlimited' },
},
type: 'row',
},
{
title: 'Emails & Calendar',
type: 'category',
},
{
featureLabel: 'Internet accounts per user',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Unlimited' },
},
type: 'row',
},
{
featureLabel: 'Folder/Label import',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
@@ -169,19 +104,79 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
type: 'row',
},
{
featureLabel: 'Webhooks',
featureLabel: 'Email sharing',
tiers: {
organization: { kind: 'text', text: 'Fully customizable' },
pro: { kind: 'text', text: 'Fully customizable' },
},
type: 'row',
},
{
title: 'Automations',
type: 'category',
},
{
featureLabel: 'Workflows',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'AI agents',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'Automations credits',
tiers: {
organization: { kind: 'text', text: '2k' },
pro: { kind: 'text', text: '1k' },
},
type: 'row',
},
{
title: 'Security',
type: 'category',
},
{
featureLabel: 'SSO / SAML',
featureLabel: 'User roles',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Admin, Members' },
},
type: 'row',
},
{
featureLabel: 'Read/Edit/Delete permissions',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
featureLabel: 'Field-level permissions',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
featureLabel: 'Row-level permissions',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'dash' },
},
type: 'row',
},
{
featureLabel: 'SSO',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
@@ -189,7 +184,51 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
type: 'row',
},
{
featureLabel: 'Audit logs',
featureLabel: 'Environments',
tiers: {
organization: { kind: 'text', text: 'Local, Production' },
pro: { kind: 'text', text: 'Local, Production' },
},
type: 'row',
},
{
featureLabel: 'Impersonate users',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
title: 'Support',
type: 'category',
},
{
featureLabel: 'Community',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'Help center',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'Email and Chat',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
featureLabel: 'Priority support',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
@@ -197,15 +236,47 @@ export const PLAN_TABLE_DATA: PlanTableDataType = {
type: 'row',
},
{
featureLabel: 'Role-based access control',
featureLabel: 'Professional services',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'dash' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
title: 'Customization',
type: 'category',
},
{
featureLabel: 'Custom apps',
tiers: {
organization: { kind: 'text', text: 'Unlimited' },
pro: { kind: 'text', text: 'Unlimited' },
},
type: 'row',
},
{
featureLabel: 'Custom domain',
tiers: {
organization: { kind: 'yes', label: 'Yes' },
pro: { kind: 'yes', label: 'Yes' },
},
type: 'row',
},
{
title: 'Developers',
type: 'category',
},
{
featureLabel: 'API calls',
tiers: {
organization: { kind: 'text', text: '200 per minute' },
pro: { kind: 'text', text: '100 per minute' },
},
type: 'row',
},
],
initialVisibleRowCount: 13,
initialVisibleRowCount: 15,
seeMoreFeaturesCta: {
collapseLabel: 'Show less',
expandLabel: 'See more features',
@@ -1,4 +1,5 @@
import { FAQ_DATA, MENU_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import {
ENGAGEMENT_BAND_DATA,
HERO_DATA,
@@ -64,11 +65,9 @@ export default async function PricingPage() {
<EngagementBand.Body body={ENGAGEMENT_BAND_DATA.body} />
</EngagementBand.Copy>
<EngagementBand.Actions>
<LinkButton
<TalkToUsButton
color="secondary"
href="https://app.twenty.com/welcome"
label="Read our case studies"
type="anchor"
label="Contact us"
variant="contained"
/>
</EngagementBand.Actions>
@@ -98,11 +97,9 @@ export default async function PricingPage() {
type="anchor"
variant="contained"
/>
<LinkButton
<TalkToUsButton
color="primary"
href="https://twenty.com/contact"
label="Talk to us"
type="anchor"
variant="outlined"
/>
</Faq.Cta>
@@ -0,0 +1,814 @@
import Link from 'next/link';
export function PrivacyPolicyDocument() {
return (
<>
<p>
<strong>{'Effective Date:'}</strong>
{' October 13, 2025'}
</p>
<h2>
<strong>{'Introduction'}</strong>
</h2>
<p>
{'Welcome to Twenty.com PBC (“'}
<strong>{'Twenty'}</strong>
{'”, “'}
<strong>{'Company'}</strong>
{'”, “'}
<strong>{'we'}</strong>
{'”, “'}
<strong>{'us'}</strong>
{'”, or “'}
<strong>{'our'}</strong>
{'”).'}
</p>
<p>
{'Twenty operates the website '}
<a
href={'https://twenty.com'}
rel="noopener noreferrer"
target="_blank"
>
{'https://twenty.com'}
</a>
{' and related cloud services (collectively, the “'}
<strong>{'Service'}</strong>
{'”).'}
</p>
<p>
{
'This Privacy Policy governs your access to and use of the Service and explains how we collect, use, safeguard, and disclose information that results from your use of our Service.'
}
</p>
<p>
{
'We are committed to complying with applicable data protection laws, including the General Data Protection Regulation (GDPR) and the California Consumer Privacy Act (CCPA).'
}
</p>
<p>
{
'By using the Service, you agree to the collection and use of information in accordance with this Privacy Policy and our '
}
<Link href={'/terms'}>{'Terms of Service'}</Link>
{'.'}
</p>
<h2>
<strong>{'Definitions'}</strong>
</h2>
<p>{'For purposes of this Privacy Policy:'}</p>
<ul>
<li>
<p>
<strong>{'Service'}</strong>
{' means the Twenty website ('}
<a
href={'https://twenty.com'}
rel="noopener noreferrer"
target="_blank"
>
{'https://twenty.com'}
</a>
{
') and any related services or applications operated by Twenty.com PBC.'
}
</p>
</li>
<li>
<p>
<strong>{'Personal Data'}</strong>
{
' means any information about a living individual who can be identified from that data (either alone or in combination with other information in our possession).'
}
</p>
</li>
<li>
<p>
<strong>{'Usage Data'}</strong>
{
' means information collected automatically through use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).'
}
</p>
</li>
<li>
<p>
<strong>{'Cookies'}</strong>
{
' are small files placed on your device (computer or mobile) that store certain information.'
}
</p>
</li>
<li>
<p>
<strong>{'Data Controller'}</strong>
{
' means a person or organization who (alone or jointly with others) determines the purposes and means of processing Personal Data. For the purposes of this Privacy Policy, Twenty is the Data Controller for Personal Data we collect directly from you.'
}
</p>
</li>
<li>
<p>
<strong>{'Data Processor (or Service Provider)'}</strong>
{
' means a person or organization which processes Personal Data on behalf of the Data Controller. We may use the services of various third-party service providers to process your data more effectively. When you use Twentys cloud-based CRM, Twenty acts as a Data Processor for the Customer Data you input into our platform on behalf of your organization (see '
}
<strong>{'Information We Collect'}</strong>
{' below).'}
</p>
</li>
<li>
<p>
<strong>{'Data Subject'}</strong>
{
' is any living individual who is the subject of Personal Data (you, or any individual whose data you provide to us).'
}
</p>
</li>
<li>
<p>
<strong>{'User'}</strong>
{
' means the individual using our Service. The User may be the Data Subject or an authorized user acting on behalf of a company (our customer).'
}
</p>
</li>
</ul>
<h2>
<strong>{'Information We Collect'}</strong>
</h2>
<p>
{
'We collect different types of information to provide and improve the Service for our users. This includes:'
}
</p>
<p>
<strong>{'1. Personal Data You Provide:'}</strong>
{
' While using our Service (for example, when creating an account, subscribing to our newsletter, or contacting support), we may ask you to provide certain personally identifiable information. This information may include, but is not limited to: your name, email address, phone number, company/organization name, job title, billing information, or any other details you choose to provide. We use this Personal Data to identify or contact you as needed. For instance, if you subscribe to communications from us, we will use your email to send newsletters or updates (you may opt out of these communications at any time by following the unsubscribe link or contacting us).'
}
</p>
<p>
<strong>{'2. Customer Data (CRM Data):'}</strong>
{
' If you are using Twentys cloud-based CRM application, you may input or upload personal information about third parties (such as your customers, leads, or contacts) into our Service. This '
}
<strong>{'Customer Data'}</strong>
{
' may include names, contact details, communications, transaction histories, and other information relevant to customer relationship management. In these cases, you (or your organization) act as the Data Controller for such Customer Data, and Twenty acts as a Data Processor on your behalf. We process this data solely to provide our CRM services to you and in accordance with your instructions. '
}
<strong>{'If you use Twenty in a self-hosted environment,'}</strong>
{
' we do not collect or have access to the Customer Data in your self-hosted instance; all such data remains under your control on your own servers. (Note: We may collect limited information from self-hosted deployments, such as workspace or user emails for license verification and optional telemetry, but this does '
}
<em>{'not'}</em>
{
' include your customer records. Telemetry is optional and can be disabled by you at any time)'
}
</p>
<p>
<strong>{'3. Usage Data:'}</strong>
{
' When you interact with our website or app, we automatically collect certain technical information about your visit. This Usage Data may include information such as your devices Internet Protocol address (IP address), browser type and version, the pages you visit on our Service, the date and time of your visit, time spent on those pages, and other diagnostic data. If you access the Service via a mobile device, Usage Data may include your mobile device type, unique device ID, IP address, operating system, browser type, and other device identifiers. We collect this information to understand how our Service is used, to monitor and improve performance, and to ensure the security of our Service.'
}
</p>
<p>
<strong>{'4. Cookies and Similar Technologies:'}</strong>
{
' We use cookies and similar tracking technologies to operate and analyze our Service. Cookies are small data files sent to your browser from a website and stored on your device; they may contain an anonymous unique identifier. We use cookies for purposes such as keeping you logged in, remembering your preferences, and securing the site. We '
}
<strong>{'do not'}</strong>
{
' use advertising cookies or third-party tracking pixels (such as Google Analytics or social media pixels) to target you with ads. Other tracking technologies (like web beacons or scripts) may be used for strictly functional or analytic purposes to improve the user experience, not for profiling you. You can instruct your browser to refuse all cookies or to indicate when a cookie is being sent. However, note that if you disable cookies, some portions of our Service (for example, the login session) may not function properly. (See '
}
<strong>{'Cookies and Tracking Technologies'}</strong>
{' below for more details.)'}
</p>
<p>
<strong>{'5. Information from Third Parties:'}</strong>
{
' We may receive information about you from third-party services that you choose to integrate or connect with Twenty. For example, if you use a single sign-on service or import data from another platform, we will collect the information you have authorized that service to share with us. We handle any such third-party-sourced information according to this Privacy Policy.'
}
</p>
<p>
{'We do '}
<strong>{'not'}</strong>
{
' knowingly collect any sensitive personal data unless you voluntarily provide it to us. We also pledge '
}
<strong>{'never to sell'}</strong>
{' any personal data we collect to third parties.'}
</p>
<h2>
<strong>{'How We Use Your Information'}</strong>
</h2>
<p>
{
'Twenty uses the collected information for various purposes in order to operate our business, provide the Service to you, and improve our offerings. These purposes include:'
}
</p>
<ul>
<li>
<p>
<strong>{'Providing and Maintaining the Service:'}</strong>
{
' To deliver the core functionalities of our CRM platform and website, including creating and managing user accounts, hosting your CRM data, and enabling you to use our features.'
}
</p>
</li>
<li>
<p>
<strong>{'Service Communications:'}</strong>
{
' To notify you about updates, changes, or important information regarding the Service (for example, changes to features, security alerts, or support messages).'
}
</p>
</li>
<li>
<p>
<strong>{'Enabling User Features:'}</strong>
{
' To allow you to participate in interactive features of our Service when you choose to do so (for example, collaborative features, integrations, or community forums).'
}
</p>
</li>
<li>
<p>
<strong>{'Customer Support:'}</strong>
{
' To provide customer service and support, including responding to your inquiries, troubleshooting issues, and improving your experience.'
}
</p>
</li>
<li>
<p>
<strong>{'Analytics and Improvements:'}</strong>
{
' To gather analysis, metrics, and valuable information so that we can understand usage trends and improve the Services performance and features. This may include analyzing how users interact with our application to refine the user interface or determining which features are most used to inform our product development.'
}
</p>
</li>
<li>
<p>
<strong>{'Monitoring and Security:'}</strong>
{
' To monitor the usage of our Service and to detect, prevent, and address technical issues, fraud, or misuse. This helps us keep the Service safe, secure, and reliable for all users.'
}
</p>
</li>
<li>
<p>
<strong>{'Billing and Transactions:'}</strong>
{
' To process transactions, manage subscriptions, and fulfill our contractual obligations. For example, we use provided information to bill for paid plans and to collect fees owed, using secure third-party payment processors (note: we do not store full payment card details ourselves).'
}
</p>
</li>
<li>
<p>
<strong>{'Account Management:'}</strong>
{
' To send you administrative emails and reminders about your account, such as password resets, login notifications, subscription renewal notices, or policy updates.'
}
</p>
</li>
<li>
<p>
<strong>{'Marketing and Related Communications:'}</strong>
{
' To provide you with news about Twenty, special offers, or general information about our products and services, but only where you have an existing relationship with us or have otherwise consented to receive such communications. You have the right to opt out of marketing messages at any time.'
}
</p>
</li>
<li>
<p>
<strong>{'To Fulfill a Purpose You Provided It:'}</strong>
{
' If you give us information for a specific reason, we will use it for that reason. For instance, if you fill out a survey or request, we will use the data to perform or respond to that request.'
}
</p>
</li>
<li>
<p>
<strong>{'With Your Consent:'}</strong>
{
' For any other purpose that we describe at the time of collection, but only if you have given us consent to use your information in that additional way.'
}
</p>
</li>
<li>
<p>
<strong>{'Legal Compliance and Enforcement:'}</strong>
{
' To comply with applicable legal requirements, such as financial record-keeping or responding to lawful requests by public authorities. Also, to enforce our agreements (e.g., Terms of Service) and to protect the rights and safety of our company, our users, or others (for example, in investigating and preventing fraud or security issues).'
}
</p>
</li>
</ul>
<p>
{
'We will not use Personal Data for any purpose that is incompatible with the purposes outlined above without your consent.'
}
</p>
<h2>
<strong>{'How We Share Your Information'}</strong>
</h2>
<p>
{'We value your privacy and handle your Personal Data with care. We '}
<strong>{'do not sell'}</strong>
{
' or rent your personal information to third parties for their marketing purposes. However, we may share information about you in the following circumstances, or as otherwise described in this Privacy Policy:'
}
</p>
<ul>
<li>
<p>
<strong>{'Service Providers:'}</strong>
{
' We may share your information with third-party service providers and partners who perform services on our behalf or help us operate the Service. This includes, for example, cloud hosting providers, infrastructure and security services, payment processors, analytics and error monitoring services, customer support platforms, and email service providers. These third parties are authorized to use your Personal Data only as necessary to provide services to us and are contractually obligated to protect it and not disclose or use it for any other purpose. '
}
<em>
{
'For transparency, our primary subprocessors as of this policys effective date include'
}
</em>
{': '}
<strong>{'Amazon Web Services (AWS)'}</strong>
{' for cloud infrastructure, '}
<strong>{'Cloudflare'}</strong>
{' for network performance and security, '}
<strong>{'Stripe'}</strong>
{' for payment processing, '}
<strong>{'Sentry'}</strong>
{
' for error tracking. A comprehensive list of subprocessors is available in our '
}
<a
href={'https://trust.twenty.com/'}
rel="noopener noreferrer"
target="_blank"
>
{'Trust Center'}
</a>
{'.'}
</p>
</li>
<li>
<p>
<strong>{'Affiliates and Corporate Transactions:'}</strong>
{
' We may disclose your information to our subsidiaries, parent company, or other affiliates as needed to streamline our operations (for example, if we operate international branches). In the event of a merger, acquisition, financing due diligence, reorganization, bankruptcy, receivership, sale of company assets, or other business transaction, your Personal Data may be transferred as part of that deal. We will provide notice before your Personal Data is transferred and becomes subject to a different privacy policy.'
}
</p>
</li>
<li>
<p>
<strong>{'Legal Compliance:'}</strong>
{
' We may disclose your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g., a court order, subpoena, or government demand). We will only share the information necessary and will, when legally permitted, inform you of such disclosure.'
}
</p>
</li>
<li>
<p>
<strong>{'Protecting Rights and Safety:'}</strong>
{
' We may disclose information if we believe it is necessary or appropriate to protect the rights, property, or safety of Twenty, our customers, our employees, or others. This includes exchanging information with other companies and organizations for fraud prevention, spam/malware detection, or other similar purposes.'
}
</p>
</li>
<li>
<p>
<strong>{'With Your Consent or At Your Direction:'}</strong>
{
' We will share your personal information with third parties if you give us consent to do so for a specific purpose, or if you use our Service to intentionally interact with third parties. For instance, if you choose to integrate our Service with a third-party application or export data to another platform, we will share data at your direction to complete that process. Similarly, we may share data for any other purpose disclosed to you when you provided the information, with your explicit consent.'
}
</p>
</li>
</ul>
<p>
{
'Aside from the purposes listed above, we will not transfer your Personal Data to any third party without your knowledge, and we do not share personal information with third parties for their own direct marketing use. If we ever need to share your data in a materially different way, we will update this Privacy Policy and notify you as required.'
}
</p>
<h2>
<strong>{'Cookies and Tracking Technologies'}</strong>
</h2>
<p>
{
'As mentioned, Twenty uses cookies and similar tracking technologies to provide, secure, and improve our Service. This section explains those technologies in more detail and your choices regarding them.'
}
</p>
<p>
<strong>{'What Cookies Are:'}</strong>
{
' Cookies are small text files placed on your device by websites you visit. They are widely used to make websites work, or work more efficiently, as well as to provide reporting information. We also may use related technologies such as web beacons (clear pixel tags), local storage, and scripts for similar purposes. These technologies help us recognize you, remember your preferences, and understand how you interact with our Service.'
}
</p>
<p>
<strong>{'How We Use Cookies:'}</strong>
{
' We only use cookies in a limited manner, primarily for the functioning of our Service and basic analytics. For example, we use cookies to keep you logged into your account as you navigate the site, and to remember your settings or preferences. Cookies also help us improve security (for instance, by enabling additional verification or detecting malicious behavior). '
}
<strong>
{
'We do not use cookies to collect advertising information or to track your behavior across different websites'
}
</strong>
{
'. In fact, we avoid using third-party advertising or social media cookies entirely, and we do not use Google Analytics or similar invasive tracking tools on our marketing site. (We may use privacy-focused analytics or performance tracking tools that do not identify individuals for example, our content delivery network and hosting providers may gather aggregate usage statistics for infrastructure monitoring, but this data is not used for advertising or profiling purposes.)'
}
</p>
<p>
<strong>{'Types of Cookies We May Use:'}</strong>
</p>
<ul>
<li>
<p>
<strong>{'Session Cookies:'}</strong>
{
' These are temporary cookies that are erased when you close your browser. Session cookies are used to operate our Service for example, to enable you to log in and navigate between pages without having to log in again.'
}
</p>
</li>
<li>
<p>
<strong>{'Preference Cookies:'}</strong>
{
' These cookies remember information about your choices and preferences (such as your language or other settings) to provide a more personal and convenient experience.'
}
</p>
</li>
<li>
<p>
<strong>{'Security Cookies:'}</strong>
{
' These cookies are used for security purposes, such as to detect authentication abuses, protect user data, and prevent fraudulent use of login credentials.'
}
</p>
</li>
</ul>
<p>
<strong>{'Your Choices:'}</strong>
{
' Most web browsers automatically accept cookies, but you can usually modify your browser settings to decline cookies or alert you when a cookie is being placed. Please be aware that if you disable or refuse cookies, some features of our Service (like maintaining a login session) may not function properly. You can also clear cookies from your browser at any time. For more information on how to manage cookies, check your browsers help documentation. Additionally, some of our third-party service providers (e.g., Cloudflare) may set their own cookies as part of providing our Service; these cookies are also only used for the purposes weve described (like security and basic analytics). We do not control third-party cookies, but you may be able to opt-out of certain cookies via those third parties websites or browser extensions.'
}
</p>
<h2>
<strong>{'Data Security'}</strong>
</h2>
<p>
{
'We take the security of your information very seriously. Twenty implements robust technical and organizational measures to protect your Personal Data against unauthorized access, alteration, disclosure, or destruction. These measures include, for example, encryption of data in transit and at rest, access controls and authentication procedures to limit access to data, regular security audits and assessments, and staff training on data protection. While we strive to use commercially acceptable means to protect your Personal Data, please note that '
}
<strong>
{
'no method of transmission over the internet or method of electronic storage is 100% secure'
}
</strong>
{
'. Therefore, we cannot guarantee absolute security. You should also take steps to safeguard your own credentials and devices (such as choosing strong passwords and keeping them confidential, and using secure networks). '
}
<strong>
{
'In the event of any data breach that affects your Personal Data, we will notify you and the appropriate authorities as required by law.'
}
</strong>
</p>
<h2>
<strong>{'Data Retention'}</strong>
</h2>
<p>
{
'We will retain your Personal Data only for as long as necessary to fulfill the purposes for which it was collected, as outlined in this Privacy Policy. This means we keep your information for as long as you maintain an account with us or as long as needed to provide you with the Service. We may also retain and use your Personal Data as necessary to comply with our legal obligations (for example, to satisfy tax, accounting, or reporting requirements), to resolve disputes, and to enforce our agreements.'
}
</p>
<p>
{
'For users of our CRM Service, generally your account and the data within it (including Customer Data you have entered) will remain active and retained until you or your organization delete it or request its deletion. You have the ability to delete certain data through the Service interface (for instance, removing or exporting records) and you may delete your entire account data by contacting us if needed. Upon termination of your account or upon your request, we will securely delete or anonymize your Personal Data and any Customer Data stored in your account, unless we are required to retain it longer to comply with legal obligations or legitimate business interests (such as maintaining security logs or proof of consent). If complete deletion is not immediately possible (for example, because the data is stored in backups), we will isolate and protect the data from any further use until deletion is possible.'
}
</p>
<p>
{
'We retain Usage Data for internal analysis and security purposes. Usage Data is generally kept for a shorter period of time than Personal Data, except when such data is used to strengthen the security or improve the functionality of our Service, or we are legally obligated to retain it for longer periods.'
}
</p>
<p>
{
'In summary, we do not keep your data in identifiable form for longer than necessary. When data is no longer needed, we take steps to have it deleted, aggregated, or anonymized.'
}
</p>
<h2>
<strong>{'Your Rights and Choices'}</strong>
</h2>
<p>
{
'Depending on your jurisdiction and applicable data protection laws, you have certain rights regarding your Personal Data. Twenty is committed to facilitating the exercise of these rights for all users.'
}
</p>
<p>
<strong>
{'Rights Under the GDPR (for users in the European Economic Area):'}
</strong>
{
' If you are a resident of the European Union or EEA, you have the following data protection rights under the GDPR:'
}
</p>
<ol>
<li>
<p>
<strong>{'Right to Access:'}</strong>
{
' You have the right to request a copy of the Personal Data we hold about you and to obtain information about how we process it.'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Rectification:'}</strong>
{
' You have the right to request that we correct any Personal Data that is inaccurate or incomplete.'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Erasure:'}</strong>
{
' You have the right to request deletion of your Personal Data under certain circumstances (for example, if the data is no longer necessary for the purposes it was collected, or if you withdraw consent and no other legal basis for processing exists). This is sometimes called the “right to be forgotten.”'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Restrict Processing:'}</strong>
{
' You have the right to request that we restrict the processing of your Personal Data if you contest its accuracy, if the processing is unlawful and you prefer restriction over deletion, if we no longer need the data but you need it for legal claims, or if you have objected to processing pending verification of any overriding legitimate grounds.'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Data Portability:'}</strong>
{
' You have the right to obtain a copy of your Personal Data in a structured, commonly used, machine-readable format and to have that information transmitted to another controller (where technically feasible). This right applies when the data is processed by automated means and the processing is based on your consent or the performance of a contract.'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Object:'}</strong>
{
' You have the right to object to our processing of your Personal Data in certain situations, such as for direct marketing or when we are processing data based on legitimate interests. If you object, we will no longer process your Personal Data unless we can demonstrate compelling legitimate grounds for the processing that override your interests, rights, and freedoms, or unless processing is required for the establishment, exercise, or defense of legal claims.'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Withdraw Consent:'}</strong>
{
' If we are processing your Personal Data based on your consent, you have the right to withdraw your consent at any time. This will not affect the lawfulness of any processing we conducted prior to your withdrawal, and it will not affect processing of your Personal Data conducted in reliance on lawful processing grounds other than consent.'
}
</p>
</li>
</ol>
<p>
{
'To exercise your GDPR rights, please contact us using the contact information provided at the end of this policy. We may ask you to verify your identity before fulfilling certain requests, to ensure that your data is protected. We will respond to your request within the timeframes required by law. Please note that these rights are not absolute there are exceptions and limitations. For example, we might not delete data that we are required to keep by law, or we might decline a request if it adversely affects the rights and freedoms of others. If you believe our processing of your Personal Data violates the GDPR, you also have the right to lodge a complaint with your local supervisory data protection authority in the EU.'
}
</p>
<p>
<strong>{'Rights Under the CCPA (for California Residents):'}</strong>
{
' If you are a resident of California, you are protected by the California Consumer Privacy Act (CCPA) (as amended by the California Privacy Rights Act, “CPRA”) and other California privacy laws. These laws provide you (or your authorized agent) with specific rights regarding your personal information, including:'
}
</p>
<ul>
<li>
<p>
<strong>{'Right to Know:'}</strong>
{
' You have the right to request that we disclose what personal information we collect, use, disclose, and '
}
<strong>{'sell or share'}</strong>
{
' (if applicable) about you. This includes the categories of personal information we have collected, the categories of sources of that information, the business or commercial purpose for collecting it, the categories of third parties with whom we share it, and the specific pieces of personal information we hold about you.'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Delete:'}</strong>
{
' You have the right to request the deletion of personal information we have collected from you (and direct our service providers to do the same), subject to certain exceptions. For example, we may retain information needed to complete transactions, detect security incidents, comply with legal obligations, or other exceptions permitted by law.'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Opt-Out of Sale or Sharing:'}</strong>
{
' You have the right to opt out of the “sale” or “sharing” of your personal information. However, as noted above, '
}
<strong>{'Twenty does not sell your personal information'}</strong>
{
' to third parties, and we do not share it for cross-context behavioral advertising. If in the future we consider selling personal data or using it for behavioral advertising, we will provide a clear opt-out mechanism as required by law.'
}
</p>
</li>
<li>
<p>
<strong>{'Right to Non-Discrimination:'}</strong>
{
' We will not discriminate against you for exercising any of your CCPA rights. This means we will not deny you goods or services, charge you different prices, or provide a different level of quality of services just because you exercised your rights. However, in some cases, if you request deletion of certain data, we may not be able to provide services that rely on that data (for instance, deleting your account information might prevent you from continuing to use your account).'
}
</p>
</li>
</ul>
<p>
{
'To exercise your California privacy rights, you (or an authorized agent acting on your behalf) can submit a verifiable request to us using the contact information at the end of this policy. Please describe your request with sufficient detail that allows us to properly understand, evaluate, and respond to it. We will verify your identity (or your agents authority) as required before acting on a request, typically by confirming information we already have on file. We aim to respond to valid requests within 45 days as required by the CCPA (or notify you if an extension is needed).'
}
</p>
<p>
{
'Additionally, California\'s "Shine the Light" law (Civil Code Section 1798.83) permits users of our Service who are California residents to request certain information regarding our disclosure of personal information to third parties for their direct marketing purposes. We do not disclose personal information to third parties for direct marketing without consent, so this provision is generally not applicable to Twentys practices.'
}
</p>
<p>
{
'If you have any questions about your privacy rights or how to exercise them, you can always contact us at the email provided in the '
}
<strong>{'Contact Us'}</strong>
{' section. We will be happy to assist you.'}
</p>
<h2>
<strong>{'International Data Transfers'}</strong>
</h2>
<p>
{
'Twenty is based in the United States but data is currently hosted in Frankfurt (EU). We plan multiple hosting locations in 2026.'
}
</p>
<p>
{
'We will take all steps reasonably necessary to ensure that your data is treated securely and in accordance with this Privacy Policy. Twenty complies with applicable legal requirements for transferring personal data internationally. In particular, for Personal Data originating from the European Economic Area (EEA), United Kingdom, or Switzerland, we implement appropriate safeguards for cross-border data transfers as required by the GDPR and corresponding laws. These safeguards may include entering into Standard Contractual Clauses approved by the European Commission, relying on an adequacy decision by the relevant authority (if the destination country is deemed to provide adequate protection), or other lawful mechanisms. No transfer of your Personal Data will take place to an organization or a country unless adequate controls are in place to protect your information, including the security of your data and other personal information.'
}
</p>
<p>
{
'Your use of our Service, followed by your submission of information to us, represents your consent to this transfer, storage, and processing of your information in the United States and other jurisdictions as described. If you have questions about our international data transfer practices, please contact us.'
}
</p>
<h2>
<strong>{'Links to Other Sites'}</strong>
</h2>
<p>
{
'Our Service or website may contain links to other sites that are not operated by Twenty (for example, a link to an article, a partners website, or an integration provider). If you click on a third-party link, you will be directed to that third partys site. We strongly advise you to review the privacy policy of every external site or service that you visit, as their privacy practices may differ from ours.'
}
</p>
<p>
{
'We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party websites or services. When you leave our site, our Privacy Policy no longer applies. If you provide personal information to any third-party site, your information is governed by their policies. We recommend you exercise caution and read the privacy statements of those sites before engaging with them.'
}
</p>
<h2>
<strong>{'Childrens Privacy'}</strong>
</h2>
<p>
{'Our Service is '}
<strong>
{'not intended for use by children under the age of 18'}
</strong>
{
'. We do not knowingly collect personally identifiable information from anyone under 18 years of age. If you are under 18, please do not use the Service or provide any Personal Data to us.'
}
</p>
<p>
{
'In the event that we discover we have collected Personal Data from a child under 18 without verified parental consent, we will take immediate steps to delete such information from our servers or to obtain appropriate consent. If you are a parent or guardian and you become aware that your child has provided us with Personal Data, please contact us so that we can take the necessary actions.'
}
</p>
<p>
{
'Note: In certain jurisdictions, such as under U.S. COPPA law, “child” is defined as under 13 years old. However, our policy is to avoid collecting data from anyone under 18 to ensure we comply with all child protection regulations and because our services are business-oriented. We encourage parents and legal guardians to monitor their childrens internet usage and to help enforce this Privacy Policy by instructing their children never to provide Personal Data through our Service without permission.'
}
</p>
<h2>
<strong>{'Changes to This Privacy Policy'}</strong>
</h2>
<p>
{
'We may update our Privacy Policy from time to time to reflect changes in our practices, technologies, legal requirements, or for other operational reasons. When we make changes, we will post the updated Privacy Policy on this page and update the “Effective Date” at the top of the policy. '
}
<strong>{'If the changes are material, we will notify you'}</strong>
{
' either by email (using the email address associated with your account) or by means of a prominent notice on our website or within the Service, '
}
<strong>{'prior to the change becoming effective'}</strong>
{
'. We encourage you to review this Privacy Policy periodically for any updates.'
}
</p>
<p>
{
'Your continued use of the Service after any changes to this Privacy Policy become effective constitutes acceptance of those changes, to the extent permitted by law. If you do not agree with any updates or amendments, you should stop using our Service and, if you wish, delete your account or exercise your rights as described above.'
}
</p>
<h2>
<strong>{'Contact Us'}</strong>
</h2>
<p>
{
'If you have any questions about this Privacy Policy, or if you would like to exercise any of your privacy rights, please contact us:'
}
</p>
<ul>
<li>
<p>
<strong>{'By Email:'}</strong>{' '}
<a href={'mailto:privacy@twenty.com'}>{'privacy@twenty.com'}</a>
</p>
</li>
</ul>
<p>
{
'You may also write to us at our business address if one is provided on our website. Email is the quickest way to reach us for privacy concerns. We take all inquiries about privacy seriously and will respond as promptly as possible.'
}
</p>
<p>
{
'Thank you for trusting Twenty with your data. Your privacy is important to us, and we are committed to safeguarding it in accordance with this policy and applicable laws.'
}
</p>
</>
);
}
@@ -0,0 +1 @@
export { PrivacyPolicyDocument } from './PrivacyPolicyDocument';
@@ -1,63 +1,28 @@
import type { Metadata } from 'next';
import { MENU_DATA } from '@/app/_constants';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { LegalDocumentPage } from '@/sections/LegalDocument/legal-document-page';
import type { Metadata } from 'next';
import { PrivacyPolicyDocument } from './_components';
export const metadata: Metadata = {
title: 'Privacy Policy — Twenty',
description: 'How Twenty collects, uses, and protects your information.',
description:
'How Twenty collects, uses, safeguards, and discloses information when you use Twenty.com and related services.',
};
export default async function PrivacyPolicyPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(
MENU_DATA.socialLinks,
stats,
);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<LegalDocumentPage
menuData={{ navItems: MENU_DATA.navItems, socialLinks: menuSocialLinks }}
title="Privacy Policy"
>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat.
</p>
<h2>Information we collect</h2>
<p>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium.
</p>
<p>
Totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi
architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem
quia voluptas sit aspernatur aut odit aut fugit.
</p>
<h2>How we use information</h2>
<p>
At vero eos et accusamus et iusto odio dignissimos ducimus qui
blanditiis praesentium voluptatum deleniti atque corrupti quos dolores
et quas molestias excepturi sint occaecati cupiditate non provident.
</p>
<p>
Similique sunt in culpa qui officia deserunt mollitia animi, id est
laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita
distinctio nam libero tempore, cum soluta nobis est eligendi optio
cumque nihil impedit quo minus id quod maxime placeat facere possimus.
</p>
<h2>Contact</h2>
<p>
Omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem
quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet
ut et voluptates repudiandae sint et molestiae non recusandae.
</p>
<PrivacyPolicyDocument />
</LegalDocumentPage>
);
}
@@ -1,4 +1,5 @@
import { FAQ_DATA, MENU_DATA, TRUSTED_BY_DATA } from '@/app/_constants';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import {
DEMO_DATA,
FEATURE_DATA,
@@ -140,11 +141,9 @@ export default async function ProductPage() {
type="anchor"
variant="contained"
/>
<LinkButton
<TalkToUsButton
color="primary"
href="https://twenty.com/contact"
label="Talk to us"
type="anchor"
variant="outlined"
/>
</Faq.Cta>
@@ -0,0 +1,515 @@
import Link from 'next/link';
export function TermsDocument() {
return (
<>
<h2>Introduction</h2>
<p>
Herein are the Terms of Service that govern the use of products created
by Twenty.com PBC (&quot;Company&quot;, &quot;we&quot;, &quot;our&quot;,
&quot;us&quot;). The estimated reading time for the full content is
approximately 20 minutes.
</p>
<p>
These Terms of Service (&quot;Terms&quot;, &quot;Terms of Service&quot;)
govern the use of our main website on Twenty.com and any of its
sub-domain, as well as any mobile application (together or individually
&quot;Service&quot;) operated by Twenty.com PBC.
</p>
<p>
Our Privacy Policy also governs your use of our Service and explains how
we collect, safeguard and disclose information that results from your
use of our web pages. Please read it here:{' '}
<Link href="/privacy-policy">Privacy Policy</Link>.
</p>
<p>
Your agreement with us includes these Terms and our Privacy Policy
(&quot;Agreements&quot;). You acknowledge that you have read and
understood Agreements, and agree to them.
</p>
<p>
If you do not agree with (or cannot comply with) Agreements, then you
may not use the Service, but please let us know by emailing at
contact@twenty.com so we can try to find a solution. These Terms apply
to all visitors, users and others who wish to access or use Service.
</p>
<p>Thank you for being responsible.</p>
<h2>Communications</h2>
<p>
By creating an Account on our Service, you agree to subscribe to
newsletters, marketing or promotional materials and other information we
may send. However, you may opt out of receiving any, or all, of these
communications from us by following the unsubscribe link or by emailing
us at contact@twenty.com.
</p>
<h2>Purchases</h2>
<p>
If you wish to purchase any product or service made available through
Service (&quot;Purchase&quot;), you may be asked to supply certain
information relevant to your Purchase including, without limitation,
your credit card number, the expiration date of your credit card, your
billing address, and your shipping information.
</p>
<p>
You represent and warrant that: (i) you have the legal right to use any
credit card(s) or other payment method(s) in connection with any
Purchase; and that (ii) the information you supply to us is true,
correct and complete.
</p>
<p>
We may employ the use of third party services for the purpose of
facilitating payment and the completion of Purchases. By submitting your
information, you grant us the right to provide the information to these
third parties subject to our Privacy Policy.
</p>
<p>
We reserve the right to refuse or cancel your order at any time for
reasons including but not limited to: product or service availability,
errors in the description or price of the product or service, error in
your order or other reasons.
</p>
<p>
We reserve the right to refuse or cancel your order if fraud or an
unauthorized or illegal transaction is suspected.
</p>
<h2>Contests, Sweepstakes and Promotions</h2>
<p>
Any contests, sweepstakes or other promotions (collectively,
&quot;Promotions&quot;) made available through Service may be governed
by rules that are separate from these Terms of Service. If you
participate in any Promotions, please review the applicable rules as
well as our Privacy Policy. If the rules for a Promotion conflict with
these Terms of Service, Promotion rules will apply.
</p>
<h2>Subscriptions</h2>
<p>
Some parts of Service are billed on a subscription basis
(&quot;Subscription(s)&quot;). You will be billed in advance on a
recurring and periodic basis (&quot;Billing Cycle&quot;). Billing cycles
are set either on a monthly or annual basis, depending on the type of
subscription plan you select when purchasing a Subscription.
</p>
<p>
At the end of each Billing Cycle, your Subscription will automatically
renew under the exact same conditions unless you cancel it or Twenty.com
PBC cancels it. You may cancel your Subscription renewal either through
your online account management page or by contacting Twenty.com PBC
customer support team.
</p>
<p>
A valid payment method, including credit card, is required to process
the payment for your subscription. You shall provide Twenty.com PBC with
accurate and complete billing information including full name, address,
state, zip code, telephone number, and a valid payment method
information. By submitting such payment information, you automatically
authorize Twenty.com PBC to charge all Subscription fees incurred
through your account to any such payment instruments.
</p>
<p>
Should automatic billing fail to occur for any reason, Twenty.com PBC
will issue an electronic invoice indicating that you must proceed
manually, within a certain deadline date, with the full payment
corresponding to the billing period as indicated on the invoice.
</p>
<h2>Free Trial</h2>
<p>
Twenty.com PBC may, at its sole discretion, offer a Subscription with a
free trial for a limited period of time (&quot;Free Trial&quot;).
</p>
<p>
You may be required to enter your billing information in order to sign
up for Free Trial.
</p>
<p>
If you do enter your billing information when signing up for Free Trial,
you will not be charged by Twenty.com PBC until Free Trial has expired.
On the last day of Free Trial period, unless you canceled your
Subscription, you will be automatically charged the applicable
Subscription fees for the type of Subscription you have selected.
</p>
<p>
At any time and without notice, Twenty.com PBC reserves the right to (i)
modify Terms of Service of Free Trial offer, or (ii) cancel such Free
Trial offer.
</p>
<h2>Fee changes</h2>
<p>
Twenty.com PBC, in its sole discretion and at any time, may modify
Subscription fees for the Subscriptions. Any Subscription fee change
will become effective at the end of the then-current Billing Cycle.
</p>
<p>
Twenty.com PBC will provide you with a reasonable prior notice of any
change in Subscription fees to give you an opportunity to terminate your
Subscription before such change becomes effective.
</p>
<p>
Your continued use of Service after Subscription fee change comes into
effect constitutes your agreement to pay the modified Subscription fee
amount.
</p>
<h2>Refunds</h2>
<p>
Except when required by law, paid Subscription fees are non-refundable.
</p>
<h2>Content</h2>
<p>
Our Service allows you to post, link, store, share and otherwise make
available certain information, text, graphics, videos, or other material
(&quot;Content&quot;). You are responsible for Content that you post on
or through Service, including its legality, reliability, and
appropriateness.
</p>
<p>
By posting Content on or through Service, you represent and warrant
that: (i) Content is yours (you own it) and/or you have the right to use
it and the right to grant us the rights and license as provided in these
Terms, and (ii) that the posting of your Content on or through Service
does not violate the privacy rights, publicity rights, copyrights,
contract rights or any other rights of any person or entity. We reserve
the right to terminate the account of anyone found to be infringing on a
copyright.
</p>
<p>
You retain any and all of your rights to any Content you submit, post or
display on or through Service and you are responsible for protecting
those rights. We take no responsibility and assume no liability for
Content you or any third party posts on or through Service. However, by
posting Content using Service you grant us the right and license to use,
modify, publicly perform, publicly display, reproduce, and distribute
such Content on and through Service. You agree that this license
includes the right for us to make your Content available to other users
of Service, who may also use your Content subject to these Terms.
</p>
<p>
Twenty.com PBC has the right but not the obligation to monitor and edit
all Content provided by users.
</p>
<h2>Prohibited Uses</h2>
<p>
You may use Service only for lawful purposes and in accordance with
Terms. You agree not to use Service:
</p>
<ol>
<li>
In any way that violates any applicable national or international law
or regulation.
</li>
<li>
For the purpose of exploiting, harming, or attempting to exploit or
harm minors in any way by exposing them to inappropriate content or
otherwise.
</li>
<li>
To transmit, or procure the sending of, any advertising or promotional
material, including any &quot;junk mail&quot;, &quot;chain
letter,&quot; &quot;spam,&quot; or any other similar solicitation.
</li>
<li>
To impersonate or attempt to impersonate Company, a Company employee,
another user, or any other person or entity.
</li>
<li>
In any way that infringes upon the rights of others, or in any way is
illegal, threatening, fraudulent, or harmful, or in connection with
any unlawful, illegal, fraudulent, or harmful purpose or activity.
</li>
<li>
To engage in any other conduct that restricts or inhibits
anyone&apos;s use or enjoyment of Service, or which, as determined by
us, may harm or offend Company or users of Service or expose them to
liability.
</li>
</ol>
<p>Additionally, you agree not to:</p>
<ol>
<li>
Use Service in any manner that could disable, overburden, damage, or
impair Service or interfere with any other party&apos;s use of
Service, including their ability to engage in real time activities
through Service.
</li>
<li>
Use any robot, spider, or other automatic device, process, or means to
access Service for any purpose, including monitoring or copying any of
the material on Service.
</li>
<li>
Use any manual process to monitor or copy any of the material on
Service or for any other unauthorized purpose without our prior
written consent.
</li>
<li>
Use any device, software, or routine that interferes with the proper
working of Service.
</li>
<li>
Introduce any viruses, trojan horses, worms, logic bombs, or other
material which is malicious or technologically harmful.
</li>
<li>
Attempt to gain unauthorized access to, interfere with, damage, or
disrupt any parts of Service, the server on which Service is stored,
or any server, computer, or database connected to Service.
</li>
<li>
Attack Service via a denial-of-service attack or a distributed
denial-of-service attack.
</li>
<li>Take any action that may damage or falsify Company rating.</li>
<li>
Otherwise attempt to interfere with the proper working of Service.
</li>
</ol>
<h2>Analytics</h2>
<p>
We may use third-party Service Providers to monitor and analyze the use
of our Service.
</p>
<p>
<strong>Cloudflare analytics</strong>
</p>
<p>
Cloudflare analytics is a web analytics service operated by Cloudflare
Inc. Read the Privacy Policy here:{' '}
<a
href="https://www.cloudflare.com/privacypolicy/"
rel="noopener noreferrer"
target="_blank"
>
https://www.cloudflare.com/privacypolicy/
</a>
</p>
<h2>No Use By Minors</h2>
<p>
Service is intended only for access and use by individuals at least
eighteen (18) years old. By accessing or using any product of the
Company, you warrant and represent that you are at least eighteen (18)
years of age and with the full authority, right, and capacity to enter
into this agreement and abide by all of the terms and conditions of
Terms. If you are not at least eighteen (18) years old, you are
prohibited from both the access and usage of Service.
</p>
<h2>Accounts</h2>
<p>
When you create an account with us, you guarantee that you are above the
age of 18, and that the information you provide us is accurate,
complete, and current at all times. Inaccurate, incomplete, or obsolete
information may result in the immediate termination of your account on
Service.
</p>
<p>
You are responsible for maintaining the confidentiality of your account
and password, including but not limited to the restriction of access to
your computer and/or account. You agree to accept responsibility for any
and all activities or actions that occur under your account and/or
password, whether your password is with our Service or a third-party
service. You must notify us immediately upon becoming aware of any
breach of security or unauthorized use of your account.
</p>
<p>
You may not use as a username the name of another person or entity or
that is not lawfully available for use, a name or trademark that is
subject to any rights of another person or entity other than you,
without appropriate authorization. You may not use as a username any
name that is offensive, vulgar or obscene.
</p>
<p>
We reserve the right to refuse service, terminate accounts, remove or
edit content, or cancel orders at our sole discretion.
</p>
<h2>Intellectual Property</h2>
<p>
Service and its original content (excluding Content provided by users),
features and functionality are and will remain the exclusive property of
Twenty.com PBC and its licensors. Service is protected by copyright,
trademark, and other laws of the United States. Our trademarks and trade
dress may not be used in connection with any product or service without
the prior written consent of Twenty.com PBC.
</p>
<h2>Error Reporting and Feedback</h2>
<p>
You may provide us either directly at contact@twenty.com or via third
party sites and tools with information and feedback concerning errors,
suggestions for improvements, ideas, problems, complaints, and other
matters related to our Service (&quot;Feedback&quot;). You acknowledge
and agree that: (i) you shall not retain, acquire or assert any
intellectual property right or other right, title or interest in or to
the Feedback; (ii) Company may have development ideas similar to the
Feedback; (iii) Feedback does not contain confidential information or
proprietary information from you or any third party; and (iv) Company is
not under any obligation of confidentiality with respect to the
Feedback. In the event the transfer of the ownership to the Feedback is
not possible due to applicable mandatory laws, you grant Company and its
affiliates an exclusive, transferable, irrevocable, free-of-charge,
sub-licensable, unlimited and perpetual right to use (including copy,
modify, create derivative works, publish, distribute and commercialize)
Feedback in any manner and for any purpose.
</p>
<p>
The third party sites and tools mentioned above include the following:
</p>
<p>
<strong>Sentry</strong>
</p>
<p>
Sentry is an open-source error tracking solution provided by Functional
Software Inc. More information is available here:{' '}
<a
href="https://sentry.io/privacy/"
rel="noopener noreferrer"
target="_blank"
>
https://sentry.io/privacy/
</a>
</p>
<h2>Links To Other Websites</h2>
<p>
Our Service may contain links to third party websites or services that
are not owned or controlled by Twenty.com PBC.
</p>
<p>
Twenty.com PBC has no control over, and assumes no responsibility for
the content, privacy policies, or practices of any third party web sites
or services. We do not warrant the offerings of any of these
entities/individuals or their websites.
</p>
<p>
YOU ACKNOWLEDGE AND AGREE THAT TWENTY.COM PBC SHALL NOT BE RESPONSIBLE
OR LIABLE, DIRECTLY OR INDIRECTLY, FOR ANY DAMAGE OR LOSS CAUSED OR
ALLEGED TO BE CAUSED BY OR IN CONNECTION WITH USE OF OR RELIANCE ON ANY
SUCH CONTENT, GOODS OR SERVICES AVAILABLE ON OR THROUGH ANY SUCH THIRD
PARTY WEB SITES OR SERVICES.
</p>
<p>
WE STRONGLY ADVISE YOU TO READ THE TERMS OF SERVICE AND PRIVACY POLICIES
OF ANY THIRD PARTY WEB SITES OR SERVICES THAT YOU VISIT.
</p>
<h2>Limitation Of Liability</h2>
<p>
EXCEPT AS PROHIBITED BY LAW, YOU WILL HOLD US AND OUR OFFICERS,
DIRECTORS, EMPLOYEES, AND AGENTS HARMLESS FOR ANY INDIRECT, PUNITIVE,
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGE, HOWEVER IT ARISES
(INCLUDING ATTORNEYS&apos; FEES AND ALL RELATED COSTS AND EXPENSES OF
LITIGATION AND ARBITRATION, OR AT TRIAL OR ON APPEAL, IF ANY, WHETHER OR
NOT LITIGATION OR ARBITRATION IS INSTITUTED), WHETHER IN AN ACTION OF
CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, OR ARISING OUT OF OR IN
CONNECTION WITH THIS AGREEMENT, INCLUDING WITHOUT LIMITATION ANY CLAIM
FOR PERSONAL INJURY OR PROPERTY DAMAGE, ARISING FROM THIS AGREEMENT AND
ANY VIOLATION BY YOU OF ANY FEDERAL, STATE, OR LOCAL LAWS, STATUTES,
RULES, OR REGULATIONS, EVEN IF COMPANY HAS BEEN PREVIOUSLY ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE. EXCEPT AS PROHIBITED BY LAW, IF THERE IS
LIABILITY FOUND ON THE PART OF COMPANY, IT WILL BE LIMITED TO THE AMOUNT
PAID FOR THE PRODUCTS AND/OR SERVICES, AND UNDER NO CIRCUMSTANCES WILL
THERE BE CONSEQUENTIAL OR PUNITIVE DAMAGES. SOME STATES DO NOT ALLOW THE
EXCLUSION OR LIMITATION OF PUNITIVE, INCIDENTAL OR CONSEQUENTIAL
DAMAGES, SO THE PRIOR LIMITATION OR EXCLUSION MAY NOT APPLY TO YOU.
</p>
<h2>Termination</h2>
<p>
We may terminate or suspend your account and bar access to Service
immediately, without prior notice or liability, under our sole
discretion, for any reason whatsoever and without limitation, including
but not limited to a breach of Terms.
</p>
<p>
If you wish to terminate your account, you may simply discontinue using
Service.
</p>
<p>
All provisions of Terms which by their nature should survive termination
shall survive termination, including, without limitation, ownership
provisions, warranty disclaimers, indemnity and limitations of
liability.
</p>
<h2>Governing Law</h2>
<p>
These Terms shall be governed and construed in accordance with the laws
of State of Delaware without regard to its conflict of law provisions.
</p>
<p>
Our failure to enforce any right or provision of these Terms will not be
considered a waiver of those rights. If any provision of these Terms is
held to be invalid or unenforceable by a court, the remaining provisions
of these Terms will remain in effect. These Terms constitute the entire
agreement between us regarding our Service and supersede and replace any
prior agreements we might have had between us regarding Service.
</p>
<h2>Changes To Service</h2>
<p>
We reserve the right to withdraw or amend our Service, and any service
or material we provide via Service, in our sole discretion without
notice. We will not be liable if for any reason all or any part of
Service is unavailable at any time or for any period. From time to time,
we may restrict access to some parts of Service, or the entire Service,
to users, including registered users.
</p>
<h2>Amendments To Terms</h2>
<p>
We may amend Terms at any time by posting the amended terms on this
site. It is your responsibility to review these Terms periodically.
</p>
<p>
Your continued use of the Platform following the posting of revised
Terms means that you accept and agree to the changes. You are expected
to check this page frequently so you are aware of any changes, as they
are binding on you.
</p>
<p>
By continuing to access or use our Service after any revisions become
effective, you agree to be bound by the revised terms. If you do not
agree to the new terms, you are no longer authorized to use Service.
</p>
<h2>Waiver And Severability</h2>
<p>
No waiver by Company of any term or condition set forth in Terms shall
be deemed a further or continuing waiver of such term or condition or a
waiver of any other term or condition, and any failure of Company to
assert a right or provision under Terms shall not constitute a waiver of
such right or provision.
</p>
<p>
If any provision of Terms is held by a court or other tribunal of
competent jurisdiction to be invalid, illegal or unenforceable for any
reason, such provision shall be eliminated or limited to the minimum
extent such that the remaining provisions of Terms will continue in full
force and effect.
</p>
<h2>Acknowledgement</h2>
<p>
BY USING SERVICE OR OTHER SERVICES PROVIDED BY US, YOU ACKNOWLEDGE THAT
YOU HAVE READ THESE TERMS OF SERVICE AND AGREE TO BE BOUND BY THEM.
</p>
<h2>Contact Us</h2>
<p>
Please send your feedback, comments, requests for technical support:
</p>
<p>
By email: <a href="mailto:contact@twenty.com">contact@twenty.com</a>.
</p>
</>
);
}
@@ -0,0 +1 @@
export { TermsDocument } from './TermsDocument';
@@ -1,71 +1,28 @@
import type { Metadata } from 'next';
import { MENU_DATA } from '@/app/_constants';
import { fetchCommunityStats } from '@/lib/community/fetch-community-stats';
import { mergeSocialLinkLabels } from '@/lib/community/merge-social-link-labels';
import { LegalDocumentPage } from '@/sections/LegalDocument/legal-document-page';
import type { Metadata } from 'next';
import { TermsDocument } from './_components';
export const metadata: Metadata = {
title: 'Terms and Conditions — Twenty',
description: 'Terms governing your use of Twenty products and services.',
title: 'Terms of Service — Twenty',
description:
'Terms of Service for Twenty.com PBC, including use of Twenty.com, sub-domains, and related services.',
};
export default async function TermsPage() {
const stats = await fetchCommunityStats();
const menuSocialLinks = mergeSocialLinkLabels(
MENU_DATA.socialLinks,
stats,
);
const menuSocialLinks = mergeSocialLinkLabels(MENU_DATA.socialLinks, stats);
return (
<LegalDocumentPage
menuData={{ navItems: MENU_DATA.navItems, socialLinks: menuSocialLinks }}
title="Terms and Conditions"
title="Terms of Service"
>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer nec
odio. Praesent libero. Sed cursus ante dapibus diam. Sed nisi. Nulla
quis sem at nibh elementum imperdiet. Duis sagittis ipsum. Praesent
mauris.
</p>
<h2>Acceptance of terms</h2>
<p>
Fusce nec tellus sed augue semper porta. Mauris massa. Vestibulum
lacinia arcu eget nulla. Class aptent taciti sociosqu ad litora torquent
per conubia nostra, per inceptos himenaeos. Curabitur sodales ligula in
libero. Sed dignissim lacinia nunc.
</p>
<p>
Curabitur tortor. Pellentesque nibh. Aenean quam. In scelerisque sem at
dolor. Maecenas mattis. Sed convallis tristique sem. Proin ut ligula vel
nunc egestas porttitor. Morbi lectus risus, iaculis vel, suscipit quis,
luctus non, massa.
</p>
<h2>Use of the service</h2>
<p>
Fusce ac turpis quis ligula lacinia aliquet. Mauris ipsum. Nulla metus
metus, ullamcorper vel, tincidunt sed, euismod in, nibh. Quisque
volutpat condimentum velit. Class aptent taciti sociosqu ad litora
torquent per conubia nostra, per inceptos himenaeos.
</p>
<p>
Nam nec ante. Sed lacinia, urna non tincidunt mattis, tortor neque
adipiscing diam, a cursus ipsum ante quis turpis. Nulla facilisi. Ut
fringilla. Suspendisse potenti. Nunc feugiat mi a tellus consequat
imperdiet. Vestibulum sapien. Proin quam.
</p>
<h2>Limitation of liability</h2>
<p>
Etiam ultrices. Suspendisse in justo eu magna luctus suscipit. Sed
lectus. Integer euismod lacus luctus magna. Quisque cursus, metus vitae
pharetra auctor, sem massa mattis sem, at interdum magna augue eget
diam.
</p>
<p>
Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere
cubilia curae; Morbi lacinia molestie dui. Praesent blandit dolor. Sed
non quam. In vel mi sit amet augue congue elementum. Morbi in ipsum sit
amet pede facilisis laoreet.
</p>
<TermsDocument />
</LegalDocumentPage>
);
}
@@ -1,6 +1,12 @@
import type { ComponentType } from "react";
import { theme } from "@/theme";
import { styled } from "@linaria/react";
import type { ComponentType, CSSProperties } from 'react';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import Link from 'next/link';
type IconButtonSurfaceStyle = CSSProperties & {
'--icon-button-border-color': string;
'--icon-button-size': string;
};
type IconComponent =
| ComponentType<{ size: number; fillColor: string }>
@@ -14,22 +20,21 @@ interface IconButtonProps {
iconSize: number;
iconStrokeColor: string;
size: number;
href?: string;
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
ariaExpanded?: boolean;
}
const StyledButton = styled.button<{ size: number; borderColor: string }>`
const iconButtonSurfaceStyles = `
align-items: center;
background: transparent;
border: 1px solid ${(p) => p.borderColor};
border-radius: ${theme.radius(2)};
cursor: pointer;
display: inline-flex;
height: ${(p) => p.size}px;
justify-content: center;
margin: 0;
padding: 0;
width: ${(p) => p.size}px;
text-decoration: none;
&:focus-visible {
outline: 1px solid ${theme.colors.highlight[100]};
@@ -37,16 +42,31 @@ const StyledButton = styled.button<{ size: number; borderColor: string }>`
}
transition: transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
&:hover {
transform: scale(1.08); /* Apple-like slight growth */
transform: scale(1.08);
}
&:active {
transform: scale(0.96); /* Apple-like click shrink */
transform: scale(0.96);
}
`;
const StyledButton = styled.button`
${iconButtonSurfaceStyles}
border: 1px solid var(--icon-button-border-color);
height: var(--icon-button-size);
width: var(--icon-button-size);
`;
const StyledIconLink = styled(Link)`
${iconButtonSurfaceStyles}
border: 1px solid var(--icon-button-border-color);
color: inherit;
height: var(--icon-button-size);
width: var(--icon-button-size);
`;
export function IconButton({
icon: Icon,
ariaLabel,
@@ -55,24 +75,45 @@ export function IconButton({
iconSize,
iconStrokeColor,
size,
href,
onClick,
ariaExpanded,
}: IconButtonProps) {
const icon = (
<Icon
size={iconSize}
strokeColor={iconStrokeColor}
fillColor={iconFillColor}
aria-hidden="true"
/>
);
const surfaceStyle: IconButtonSurfaceStyle = {
'--icon-button-border-color': borderColor,
'--icon-button-size': `${size}px`,
};
if (href !== undefined && href !== '') {
return (
<StyledIconLink
aria-label={ariaLabel}
href={href}
style={surfaceStyle}
>
{icon}
</StyledIconLink>
);
}
return (
<StyledButton
type="button"
size={size}
borderColor={borderColor}
aria-label={ariaLabel}
aria-expanded={ariaExpanded}
aria-label={ariaLabel}
style={surfaceStyle}
onClick={onClick}
>
<Icon
size={iconSize}
strokeColor={iconStrokeColor}
fillColor={iconFillColor}
aria-hidden="true"
/>
{icon}
</StyledButton>
);
}
@@ -1,4 +1,5 @@
export enum Pages {
CaseStudies = 'caseStudies',
ReleaseNotes = 'releaseNotes',
Home = 'home',
Partner = 'partner',
@@ -1,30 +0,0 @@
const BEAGLE_VIEWBOX_WIDTH = 104;
const BEAGLE_VIEWBOX_HEIGHT = 25;
const BEAGLE_PATH_1 =
'M9.812 24.614h-.649l-1.43-1.982 1.394-4.23c-.184-.802-.777-1.859-1.548-2.844L6.056 22 0 22.19V0h19.257c1.34-.006 2.612.116 3.774.31l-.08 21.751-3.645-.054-1.946-6.218c-1.376.243-2.196-.018-3.4-.395-1.09.56-2.576 1.842-3.549 2.826l1.004 4.565-1.61 1.817zm.183-8.618c1.395-.255 2.557-.802 3.506-1.3-1.322-4.735 1.43-4.145.159-8.892 3.12 1.222-1.75 7.834 1.602 9.165.459.182 1.615.097 2.098-.17 1.958-1.088 2.148-3.44 1.56-5.488-1.278-4.473-5.303-6.99-9.928-5.567-1.59.486-3.138 1.89-2.832 3.55l.801-.736c.147-.134.747.012.74.212l-.018.645-3.756.923c.863.34 1.56.584 2.062 1.046l-1.652 1.319c-.067 1.209 1.205 2.218 2.239 2.522 1.162.34 2.808.23 3.432-.687.012-.292-.172-1.185.08-1.124l1.296.328c-.801 1.5-2.11 1.969-3.835 2.735.489.522 1.761 1.653 2.44 1.531zM4.84 13.03 3.383 9.967c-.44.644-.44 1.41-.147 2.09.19.444.911.864 1.597.973zm79.937-9.34L88.711 2c.165-.073.728-.025.85.073.16.127.416.59.416.88v12.569c0 1.604 1.395 1.629 1.199 2.443-.055.219-.68.492-.96.492l-5.01.019c-.019 5.7-13.55 5.038-14.033 1.653-.16-1.1.997-1.18 1.321-2.249.57-1.872-3.542 2.838-6.356-.486-2.697 1.677-6.404 2.212-8.13-1.124-2.33 2.364-5.737 3.214-8.826 1.963-2.08-.845-3.04-2.632-3.444-4.978-.483 2.474-1.72 4.394-4.148 5.202-1.92.645-4.025.31-5.945-.486-.887-.37-1.573.772-2.882.614-.25-.03-.501-.669-.501-.954l.012-10.994c0-1.653-1.334-1.155-1.566-2.2-.043-.195.343-.748.55-.839l3.72-1.556c.195-.079.862-.073 1.04.037.226.14.397.686.403.978l.104 5.956c1.946-1.635 3.677-2.157 5.744-1.568 2.068.59 2.973 2.425 3.512 4.88.862-3.786 4.367-5.725 8.062-4.935 2.355.505 4.172 2.407 3.762 4.923l-6.533 2.079c1.095 1.98 3.487 2.23 5.426 1.045.349-.213 1.034-.462 1.554-.68.948-3.216 3.798-3.125 6.85-2.936.209-1.216-1.063-2.437-2.477-2.115-1.712.395-3.034 1.234-3.364.535-.703-1.459 2.508-3.367 6.472-2.942 1.853.2 3.315 1.404 3.578 3.27l.606 4.224c.08.559 1.438 1.5 1.756 1.063l1.553-2.17c-.88-.875-1.535-1.604-1.553-2.594-.05-3.282 4.66-3.805 7.12-4.103.6-1.215.936-3.166 2.539-3.561 1.174-.292 2.104.54 3.125 1.495l.52-1.24zM74.672 14.561c3.548 1.841 6.551-.499 10.112 2.4.232.195.984-.522.984-.93l-.036-9.942c0-.42-.985-1.355-1.09-1.082-.079.213-.378 1.015-.61 1.07l-3.683.82c.287 1.203 3.321.796 3.413 3.586.043 1.282-.795 2.291-1.988 2.953-2.208 1.234-4.533.547-7.102 1.125m4.967-3.786a2.017 2.017 0 0 0-2.025-2.012c-1.12 0-2.025.9-2.025 2.011 0 1.113.906 2.012 2.025 2.012 1.12 0 2.025-.9 2.025-2.012m-26.236.589c.287-.42-.227-1.489-.618-1.811-.343-.28-1.517-.383-1.866-.073-.838.73-1.107 2.291-.544 3.075.984-.146 2.422-.601 3.028-1.191m-16.199 4.364c.508.79 1.597 1.209 2.362 1.033 1.86-.426 2.208-4.157.428-6.078-.373-.4-1.322-.753-1.774-.632-2 .541-2.245 3.75-1.016 5.677m28.36-.809a1.56 1.56 0 0 0-1.566-1.556c-.863 0-1.566.7-1.566 1.556s.703 1.556 1.566 1.556c.862 0 1.566-.699 1.566-1.556m15.53 5.039c-.146-.11-.647-.493-.862-.517l-5.254-.602c-.325-.036-.863.766-.716 1.033 1.224 2.213 8.044 1.01 6.827.085zm15.576-5.59c.88 2.243 3.773 2.146 5.468.936.269-.188.948-.273 1.211-.23.263.042.196.947.006 1.172-2.159 2.595-6.386 3.403-9.402 1.61-2.123-1.257-2.881-3.603-2.477-5.864s2.183-4.023 4.606-4.576c2.006-.462 4.117-.183 5.67 1.19 1.083.961 1.683 2.353 1.267 3.739l-6.35 2.03zm2.415-2.814c-.152-.93-.501-1.786-.899-2.048-.434-.285-1.584-.291-1.908.08a2.82 2.82 0 0 0-.416 3.05l3.223-1.075z';
const BEAGLE_PATH_2 =
'm11.977 8.187-.868-.62c-.19-.134-.618.048-1.187.152 0-.134.202-.766.373-1.015.202-.286 1.18-.22 1.389.066.208.286.312.96.293 1.417';
type BeagleIconProps = {
fillColor: string;
size: number;
};
export function BeagleIcon({ fillColor, size }: BeagleIconProps) {
const height = size * (BEAGLE_VIEWBOX_HEIGHT / BEAGLE_VIEWBOX_WIDTH);
return (
<svg
fill="none"
height={height}
viewBox={`0 0 ${BEAGLE_VIEWBOX_WIDTH} ${BEAGLE_VIEWBOX_HEIGHT}`}
width={size}
xmlns="http://www.w3.org/2000/svg"
>
<path d={BEAGLE_PATH_1} fill={fillColor} />
<path d={BEAGLE_PATH_2} fill={fillColor} />
</svg>
);
}
@@ -1,26 +0,0 @@
const EVERGREEN_VIEWBOX_WIDTH = 134;
const EVERGREEN_VIEWBOX_HEIGHT = 16;
const EVERGREEN_PATH =
'M75.483.012v15.632h4.756V10.21h2.598c.688 0 1.305.561 1.305 1.276v4.157h4.76V9.647c0-.946-.498-1.166-.874-1.196.376-.006.874-.272.874-1.227V2.253c0-1.83-.798-2.242-2.041-2.242zM80.24 3.88h3.183c.481 0 .72.294.72.794v1.29c0 .498-.24.73-.72.73H80.24zM105.135.012h13.081v3.867h-8.324v2.11h8.324v3.483h-8.324v2.322h8.324v3.85h-13.081zm-14.601 0h13.08v3.867h-8.323v2.11h8.323v3.483h-8.323v2.322h8.323v3.85h-13.08zM0 .012h13.08v3.867H4.757v2.11h8.323v3.483H4.757v2.322h8.323v3.85H0zm119.687 0h4.9l4.438 8.352V.012h4.931v15.632h-4.931l-4.438-8.51v8.51h-4.9zM61.176 15.36c-.32-.172-.74-.535-.934-.808-.34-.48-.352-.703-.352-6.716 0-8.465-.568-7.825 6.965-7.825 4.514 0 5.163.033 5.685.306.96.502 1.276 1.274 1.278 3.122l.002 1.583-4.725.02V4.55c0-.327-.374-.67-.69-.67h-2.987c-.344 0-.74.39-.74.747V11c0 .368.387.793.742.793h2.972c.347 0 .703-.439.703-.8V9.86h-2.548V6.62l7.273-.014v3.605c0 3.936-.071 4.293-.99 4.998-.458.351-.707.39-5.775.435-4.79.043-5.352-.002-5.879-.284M44.857.012v15.632h4.756V10.21h2.598c.688 0 1.305.561 1.305 1.276v4.157h4.76V9.647c0-.946-.498-1.166-.874-1.196.376-.006.874-.272.874-1.227V2.253c0-1.83-.798-2.242-2.041-2.242zm4.757 3.868h3.183c.481 0 .72.294.72.794v1.29c0 .498-.24.73-.72.73h-3.183zM30.248.012h13.08v3.867h-8.324v2.11h8.324v3.483h-8.324v2.322h8.324v3.85h-13.08zm-16.344 0h5.08l2.574 10.023L24.144.012h5.1l-5.006 15.632h-5.361z';
type EvergreenIconProps = {
fillColor: string;
size: number;
};
export function EvergreenIcon({ fillColor, size }: EvergreenIconProps) {
const height = size * (EVERGREEN_VIEWBOX_HEIGHT / EVERGREEN_VIEWBOX_WIDTH);
return (
<svg
fill="none"
height={height}
viewBox={`0 0 ${EVERGREEN_VIEWBOX_WIDTH} ${EVERGREEN_VIEWBOX_HEIGHT}`}
width={size}
xmlns="http://www.w3.org/2000/svg"
>
<path d={EVERGREEN_PATH} fill={fillColor} />
</svg>
);
}
@@ -1,29 +0,0 @@
const FLEXPORT_VIEWBOX_WIDTH = 115;
const FLEXPORT_VIEWBOX_HEIGHT = 27;
const FLEXPORT_PATH_1 =
"M13.462 16.78V.047h5.457v16.034c0 1.13.24 1.394 1.394 1.394h.457l.649 4.423a39 39 0 0 1-3.125.169c-2.981.024-4.832-1.034-4.832-5.289M55.241 6.562l4.23-.336.41 1.274c.84-.793 2.331-1.587 5.528-1.587 5.505 0 6.78 3.87 6.78 8.198 0 4.35-1.395 8.173-7.116 8.173-2.524 0-3.726-.577-4.4-1.202v5.505h-5.456zm11.418 7.765c0-1.851-.336-3.462-2.692-3.462-2.524 0-3.27 1.299-3.27 3.462s.554 3.365 3.27 3.365c2.308-.024 2.692-1.538 2.692-3.365m24.977-7.764 4.23-.337.553 1.659c.698-.938 2.597-1.731 4.856-1.827v5.24c-3.63.024-4.183 1.106-4.183 3.27v7.452h-5.456zm11.154-.505h2.812l.89-3.197 4.206-.914v4.11h3.438v5.241h-3.438v4.568c0 1.081.169 1.634 1.875 1.634h1.563v4.543c-.817.049-1.731.073-2.765.073-4.591 0-6.105-1.467-6.105-5.433v-5.36h-2.476zM0 6.25h2.98v-.312C2.98 1.97 4.472 0 9.592 0c1.347 0 1.587.024 2.26.072v4.423h-1.514c-1.563 0-1.9.385-1.9 1.394v.361h3.414v4.616H8.414V22.02H2.98V10.866H0zm33.294 11.058c-.914.529-1.37.649-3.173.649-2.14 0-2.957-.77-3.222-2.043H37.38c.192-5.553-.264-10.048-7.26-10.048s-8.485 3.87-8.485 8.149c0 4.399 1.466 8.293 8.485 8.293 2.765 0 4.496-.505 6.347-1.707zm-3.318-6.947c2.236 0 2.404 1.057 2.5 2.356h-5.6c.192-1.515.913-2.356 3.1-2.356";
const FLEXPORT_PATH_2 =
"m48.198 22.043-2.669-3.822-2.668 3.822h-6.106l5.721-8.149-5.192-7.332h6.154l2.115 2.981 2.116-2.98h6.226l-5.289 7.307 5.722 8.173zm25.12-7.956c0-5.24 2.404-8.197 8.558-8.197 6.178 0 8.534 2.98 8.534 8.197 0 5.24-2.38 8.22-8.534 8.22-6.154.025-8.558-2.98-8.558-8.22m11.61.168c0-1.971-.504-3.414-3.052-3.414-2.572 0-3.053 1.443-3.053 3.414 0 1.947.505 3.414 3.053 3.414 2.572 0 3.053-1.443 3.053-3.414";
interface FlexportIconProps {
size: number;
fillColor: string;
}
export function FlexportIcon({ size, fillColor }: FlexportIconProps) {
const height = size * (FLEXPORT_VIEWBOX_HEIGHT / FLEXPORT_VIEWBOX_WIDTH);
return (
<svg
width={size}
height={height}
viewBox={`0 0 ${FLEXPORT_VIEWBOX_WIDTH} ${FLEXPORT_VIEWBOX_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d={FLEXPORT_PATH_1} fill={fillColor} />
<path d={FLEXPORT_PATH_2} fill={fillColor} />
</svg>
);
}
@@ -1,26 +0,0 @@
const REALYTICS_VIEWBOX_WIDTH = 115;
const REALYTICS_VIEWBOX_HEIGHT = 27;
const REALYTICS_PATH =
'm50.998 21.111-2.184-4.264h-1.32v4.264H44.17V8.397h4.803c2.731 0 5.074.939 5.074 4.106a3.91 3.91 0 0 1-2.052 3.676l2.75 4.926zm-3.47-6.922h1.207c1.32 0 2.032-.589 2.032-1.68s-.7-1.447-2.032-1.447h-1.208zm12.29-3.009c2.751 0 4.117 1.984 4.117 4.629a5.7 5.7 0 0 1-.099 1.243h-5.792a2.34 2.34 0 0 0 2.52 2.116 3.9 3.9 0 0 0 2.091-.662l.957 1.74a6.3 6.3 0 0 1-3.437 1.097c-2.771 0-4.896-1.878-4.896-5.085s2.237-5.077 4.54-5.077m1.676 4.087c0-1.118-.468-1.898-1.616-1.898-.898 0-1.676.608-1.874 1.898zm8.846-.391a1.325 1.325 0 0 0-.934-1.369 1.3 1.3 0 0 0-.59-.04c-.92.06-1.808.363-2.573.88l-1.016-1.924a8 8 0 0 1 4.137-1.27c2.48 0 3.846 1.408 3.846 4.384v5.554h-2.342l-.218-.999h-.06a4.25 4.25 0 0 1-2.856 1.256 2.78 2.78 0 0 1-2.078-.836 2.8 2.8 0 0 1-.792-2.1c.027-2.022 1.65-3.146 5.476-3.536m-1.603 4.244a2.15 2.15 0 0 0 1.603-.82v-1.699c-2.052.271-2.711.86-2.711 1.6-.007.628.422.92 1.108.92m6.307-11.618h3.358v10.288c0 .661.31.86.548.86q.177.005.35-.04l.389 2.466a4.1 4.1 0 0 1-1.637.271c-2.263 0-3.001-1.487-3.001-3.676zm5.885 16.371c1.075 0 1.755-.86 2.144-2.036l.218-.701-3.807-9.528h1.656l1.933 5.29c.29.82.627 1.798.917 2.678h.08c.27-.86.547-1.839.797-2.678l1.703-5.29h1.563l-3.576 10.288c-.66 1.878-1.642 3.306-3.549 3.306a3.1 3.1 0 0 1-1.115-.198l.31-1.27q.354.118.726.14m8.807-10.95h-1.405v-1.21l1.485-.1.197-2.644h1.32v2.645h2.553v1.322h-2.533v5.29c0 1.177.37 1.838 1.464 1.838a3.4 3.4 0 0 0 1.076-.252l.31 1.21a5.8 5.8 0 0 1-1.755.35c-2.032 0-2.712-1.322-2.712-3.166zm5.543-4.304a1.12 1.12 0 0 1 1.11-1.032 1.114 1.114 0 0 1 1.112 1.032 1.12 1.12 0 0 1-1.111 1.031 1.114 1.114 0 0 1-1.112-1.031m.29 2.995h1.603v9.508h-1.604zm8.181-.231a3.9 3.9 0 0 1 2.771 1.137l-.798 1.038a2.87 2.87 0 0 0-1.913-.84c-1.716 0-2.969 1.488-2.969 3.676s1.187 3.637 2.929 3.637a3.38 3.38 0 0 0 2.23-.966l.719 1.058a4.7 4.7 0 0 1-3.081 1.23c-2.52 0-4.447-1.818-4.447-4.965 0-3.148 2.118-5.005 4.559-5.005m4.288 7.59a4.26 4.26 0 0 0 2.791 1.137c1.247 0 1.874-.661 1.874-1.488 0-.978-1.135-1.408-2.164-1.798-1.32-.49-2.83-1.137-2.83-2.737s1.207-2.698 3.259-2.698a4.73 4.73 0 0 1 2.949 1.071l-.759 1.018a3.52 3.52 0 0 0-2.164-.84c-1.187 0-1.735.662-1.735 1.37 0 .879 1.036 1.23 2.105 1.646 1.365.509 2.889 1.077 2.889 2.876 0 1.547-1.227 2.836-3.477 2.836a5.7 5.7 0 0 1-3.529-1.322zM9.401 12.562H.33a.33.33 0 0 0-.33.331v13.085c0 .182.148.33.33.33H9.4a.33.33 0 0 0 .33-.33V12.893a.33.33 0 0 0-.33-.33m11.824 5.758h-9.019a.33.33 0 0 0-.33.33v7.327c0 .182.148.33.33.33h9.019a.33.33 0 0 0 .33-.33v-7.326a.33.33 0 0 0-.33-.33M33.211 0h-9.21a.33.33 0 0 0-.33.33v25.648c0 .182.148.33.33.33h9.21a.33.33 0 0 0 .33-.33V.33a.33.33 0 0 0-.33-.331';
type RealyticsIconProps = {
fillColor: string;
size: number;
};
export function RealyticsIcon({ fillColor, size }: RealyticsIconProps) {
const height = size * (REALYTICS_VIEWBOX_HEIGHT / REALYTICS_VIEWBOX_WIDTH);
return (
<svg
fill="none"
height={height}
viewBox={`0 0 ${REALYTICS_VIEWBOX_WIDTH} ${REALYTICS_VIEWBOX_HEIGHT}`}
width={size}
xmlns="http://www.w3.org/2000/svg"
>
<path d={REALYTICS_PATH} fill={fillColor} />
</svg>
);
}
@@ -1,26 +0,0 @@
const ZAPIER_VIEWBOX_WIDTH = 112;
const ZAPIER_VIEWBOX_HEIGHT = 31;
const ZAPIER_PATH =
"m26.105 20.644-.016-.05 9.885-9.05v-4.36H20.115v4.36h9.473l.015.043-9.788 9.057v4.36h16.318v-4.36zM45.87 6.77q-2.744 0-4.473.827a5.87 5.87 0 0 0-2.595 2.274 8.3 8.3 0 0 0-1.089 3.29l4.812.676q.261-1.538 1.044-2.18a3.2 3.2 0 0 1 2.068-.64q1.466 0 2.126.784c.436.522.658 1.245.658 2.198v.475h-4.627q-3.532 0-5.076 1.567t-1.54 4.136q0 2.631 1.54 3.916t3.875 1.277q2.895 0 4.454-1.992a7.4 7.4 0 0 0 1.288-2.744h.174l.664 4.36h4.36V13.952q0-3.423-1.804-5.303t-5.86-1.88m1.525 13.61c-.692.577-1.598.865-2.726.865q-1.395 0-1.938-.489a1.6 1.6 0 0 1-.545-1.24 1.63 1.63 0 0 1 .451-1.203 1.68 1.68 0 0 1 1.24-.452h4.552v.3a2.78 2.78 0 0 1-1.034 2.22M79.899 7.184h-5.151v17.823h5.15zm30.415 0q-2.183 0-3.441 1.541-.903 1.108-1.373 3.271h-.152l-.674-4.812h-4.399v17.82h5.152v-9.138q0-1.728.809-2.65t2.987-.922h1.955v-5.11zm-15.152.583q-1.804-1.074-4.55-1.07-4.324 0-6.768 2.461-2.445 2.463-2.444 6.789a10.8 10.8 0 0 0 1.11 5.037 8 8 0 0 0 3.16 3.329q2.046 1.182 4.869 1.185 2.44 0 4.078-.752a6.56 6.56 0 0 0 2.595-2.05 8.9 8.9 0 0 0 1.41-2.914l-4.438-1.242a4.1 4.1 0 0 1-1.045 1.806q-.752.753-2.52.753-2.1 0-3.133-1.204c-.499-.58-.812-1.408-.95-2.48H98.69q.076-.753.076-1.262v-1.11a9.9 9.9 0 0 0-.9-4.323 6.77 6.77 0 0 0-2.704-2.953m-4.7 3.177q3.135 0 3.554 3.084h-7.389a4.24 4.24 0 0 1 .828-1.883q1.016-1.203 3.008-1.2M77.38.002a2.9 2.9 0 0 0-2.108.81 2.8 2.8 0 0 0-.828 2.088 2.83 2.83 0 0 0 2.935 2.932 2.8 2.8 0 0 0 2.089-.828 2.87 2.87 0 0 0 .807-2.104A2.77 2.77 0 0 0 77.382.004zM66.737 6.696q-2.633 0-4.23 1.653-1.183 1.225-1.736 3.655h-.165l-.673-4.812h-4.4v22.935h5.151v-9.446h.189c.157.63.377 1.242.657 1.827a5.2 5.2 0 0 0 2.05 2.293 6 6 0 0 0 3.044.734q3.236 0 4.927-2.52 1.692-2.52 1.692-7.032 0-4.357-1.636-6.822-1.638-2.465-4.87-2.465m.522 12.914q-.9 1.306-2.82 1.306a3.32 3.32 0 0 1-2.762-1.261q-1-1.256-.998-3.44v-.338q0-2.217.998-3.395t2.762-1.164q1.953 0 2.838 1.24t.885 3.46q.006 2.295-.898 3.592zm-49.436 1.034H0v4.366h17.823z";
interface ZapierIconProps {
size: number;
fillColor: string;
}
export function ZapierIcon({ size, fillColor }: ZapierIconProps) {
const height = size * (ZAPIER_VIEWBOX_HEIGHT / ZAPIER_VIEWBOX_WIDTH);
return (
<svg
width={size}
height={height}
viewBox={`0 0 ${ZAPIER_VIEWBOX_WIDTH} ${ZAPIER_VIEWBOX_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d={ZAPIER_PATH} fill={fillColor} />
</svg>
);
}
@@ -0,0 +1,31 @@
import type { ClientIconProps } from './client-icon-props';
const VIEWBOX_WIDTH = 52;
const VIEWBOX_HEIGHT = 22;
export function ActEducationIcon({ size, fillColor }: ClientIconProps) {
const height = size * (VIEWBOX_HEIGHT / VIEWBOX_WIDTH);
return (
<svg
width={size}
height={height}
viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<text
dominantBaseline="central"
fill={fillColor}
fontFamily="system-ui, -apple-system, BlinkMacSystemFont, sans-serif"
fontSize="15"
fontWeight="600"
letterSpacing="-0.04em"
x="0"
y="11"
>
{'AC&T'}
</text>
</svg>
);
}
@@ -0,0 +1,34 @@
import type { ClientIconProps } from './client-icon-props';
const VIEWBOX_WIDTH = 132;
const VIEWBOX_HEIGHT = 22;
export function AlternativePartnersIcon({
size,
fillColor,
}: ClientIconProps) {
const height = size * (VIEWBOX_HEIGHT / VIEWBOX_WIDTH);
return (
<svg
width={size}
height={height}
viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<text
dominantBaseline="central"
fill={fillColor}
fontFamily="system-ui, -apple-system, BlinkMacSystemFont, sans-serif"
fontSize="13"
fontWeight="600"
letterSpacing="-0.02em"
x="0"
y="11"
>
Alternative
</text>
</svg>
);
}
@@ -0,0 +1,4 @@
export type ClientIconProps = {
fillColor: string;
size: number;
};
@@ -0,0 +1,34 @@
import type { ClientIconProps } from './client-icon-props';
const VIEWBOX_WIDTH = 88;
const VIEWBOX_HEIGHT = 22;
export function ElevateConsultingIcon({
size,
fillColor,
}: ClientIconProps) {
const height = size * (VIEWBOX_HEIGHT / VIEWBOX_WIDTH);
return (
<svg
width={size}
height={height}
viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<text
dominantBaseline="central"
fill={fillColor}
fontFamily="system-ui, -apple-system, BlinkMacSystemFont, sans-serif"
fontSize="16"
fontWeight="600"
letterSpacing="-0.03em"
x="0"
y="11"
>
Elevate
</text>
</svg>
);
}
@@ -1,22 +1,25 @@
import type { ComponentType } from 'react';
import { BeagleIcon } from './Beagle';
import { EvergreenIcon } from './Evergreen';
import { FlexportIcon } from './Flexport';
import { RealyticsIcon } from './Realytics';
import { ZapierIcon } from './Zapier';
import { ActEducationIcon } from './act-education';
import type { ClientIconProps } from './client-icon-props';
import { AlternativePartnersIcon } from './alternative-partners';
import { ElevateConsultingIcon } from './elevate-consulting';
import { NetZeroIcon } from './netzero';
import { NineDotsIcon } from './nine-dots';
import { W3villaIcon } from './w3villa';
export { BeagleIcon } from './Beagle';
export { EvergreenIcon } from './Evergreen';
export { FlexportIcon } from './Flexport';
export { RealyticsIcon } from './Realytics';
export { ZapierIcon } from './Zapier';
export type ClientIconProps = { fillColor: string; size: number };
export { ActEducationIcon } from './act-education';
export { AlternativePartnersIcon } from './alternative-partners';
export { ElevateConsultingIcon } from './elevate-consulting';
export { NetZeroIcon } from './netzero';
export { NineDotsIcon } from './nine-dots';
export { W3villaIcon } from './w3villa';
export type { ClientIconProps } from './client-icon-props';
export const CLIENT_ICONS: Record<string, ComponentType<ClientIconProps>> = {
beagle: BeagleIcon,
evergreen: EvergreenIcon,
flexport: FlexportIcon,
realytics: RealyticsIcon,
zapier: ZapierIcon,
'act-education': ActEducationIcon,
'alternative-partners': AlternativePartnersIcon,
'elevate-consulting': ElevateConsultingIcon,
netzero: NetZeroIcon,
'nine-dots': NineDotsIcon,
w3villa: W3villaIcon,
};
@@ -0,0 +1,31 @@
import type { ClientIconProps } from './client-icon-props';
const VIEWBOX_WIDTH = 100;
const VIEWBOX_HEIGHT = 22;
export function NetZeroIcon({ size, fillColor }: ClientIconProps) {
const height = size * (VIEWBOX_HEIGHT / VIEWBOX_WIDTH);
return (
<svg
width={size}
height={height}
viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<text
dominantBaseline="central"
fill={fillColor}
fontFamily="system-ui, -apple-system, BlinkMacSystemFont, sans-serif"
fontSize="15"
fontWeight="600"
letterSpacing="-0.02em"
x="0"
y="11"
>
NetZero
</text>
</svg>
);
}
@@ -0,0 +1,41 @@
import type { ClientIconProps } from './client-icon-props';
const VIEWBOX_WIDTH = 56;
const VIEWBOX_HEIGHT = 56;
const GAP = 16;
const ORIGIN = 8;
const DOT_RADIUS = 5;
export function NineDotsIcon({ size, fillColor }: ClientIconProps) {
const height = size * (VIEWBOX_HEIGHT / VIEWBOX_WIDTH);
const dots: { cx: number; cy: number }[] = [];
for (let row = 0; row < 3; row += 1) {
for (let column = 0; column < 3; column += 1) {
dots.push({
cx: ORIGIN + column * GAP,
cy: ORIGIN + row * GAP,
});
}
}
return (
<svg
width={size}
height={height}
viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{dots.map((dot, index) => (
<circle
key={index}
cx={dot.cx}
cy={dot.cy}
fill={fillColor}
r={DOT_RADIUS}
/>
))}
</svg>
);
}
@@ -0,0 +1,31 @@
import type { ClientIconProps } from './client-icon-props';
const VIEWBOX_WIDTH = 76;
const VIEWBOX_HEIGHT = 22;
export function W3villaIcon({ size, fillColor }: ClientIconProps) {
const height = size * (VIEWBOX_HEIGHT / VIEWBOX_WIDTH);
return (
<svg
width={size}
height={height}
viewBox={`0 0 ${VIEWBOX_WIDTH} ${VIEWBOX_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<text
dominantBaseline="central"
fill={fillColor}
fontFamily="system-ui, -apple-system, BlinkMacSystemFont, sans-serif"
fontSize="15"
fontWeight="600"
letterSpacing="-0.02em"
x="0"
y="11"
>
W3villa
</text>
</svg>
);
}
+10 -1
View File
@@ -1,4 +1,13 @@
export { CLIENT_ICONS, FlexportIcon, ZapierIcon } from './clients';
export {
ActEducationIcon,
AlternativePartnersIcon,
CLIENT_ICONS,
ElevateConsultingIcon,
NetZeroIcon,
NineDotsIcon,
W3villaIcon,
type ClientIconProps,
} from './clients';
export { ArrowLeftIcon } from './functional/ArrowLeft';
export { ArrowRightIcon } from './functional/ArrowRight';
export { ArrowRightUpIcon } from './functional/ArrowRightUp';
@@ -0,0 +1,256 @@
import type { CaseStudyData } from '@/app/case-studies/_constants/types';
import { Container, Heading } from '@/design-system/components';
import { CLIENT_ICONS } from '@/icons';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import { IconArrowLeft, IconClock } from '@tabler/icons-react';
import Image from 'next/image';
import Link from 'next/link';
const Section = styled.section`
background-color: ${theme.colors.secondary.background[100]};
color: ${theme.colors.secondary.text[100]};
min-width: 0;
overflow: hidden;
position: relative;
width: 100%;
`;
const StyledContainer = styled(Container)`
display: flex;
flex-direction: column;
gap: ${theme.spacing(8)};
padding-bottom: ${theme.spacing(10)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(4)};
@media (min-width: ${theme.breakpoints.md}px) {
gap: ${theme.spacing(18)};
padding-bottom: ${theme.spacing(0)};
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
padding-top: ${theme.spacing(4)};
}
`;
const BackLink = styled(Link)`
align-items: center;
color: ${theme.colors.secondary.text[80]};
display: flex;
font-family: ${theme.font.family.mono};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.medium};
gap: ${theme.spacing(1)};
letter-spacing: 0.05em;
line-height: ${theme.lineHeight(4)};
text-decoration: none;
text-transform: uppercase;
transition: color 0.2s ease;
&:hover {
color: ${theme.colors.secondary.text[100]};
}
`;
const HeroContent = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: ${theme.spacing(4)};
text-align: center;
`;
const Badge = styled.span`
align-items: center;
background-color: ${theme.colors.highlight[100]};
border-radius: 50px;
color: ${theme.colors.secondary.text[100]};
display: inline-flex;
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.regular};
gap: ${theme.spacing(1)};
line-height: ${theme.lineHeight(3.5)};
padding-bottom: ${theme.spacing(0.5)};
padding-left: ${theme.spacing(2)};
padding-right: ${theme.spacing(2)};
padding-top: ${theme.spacing(0.5)};
`;
const TitleWrap = styled.div`
max-width: 900px;
color: ${theme.colors.secondary.text[100]};
`;
const AuthorLine = styled.p`
color: ${theme.colors.secondary.text[80]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(4)};
margin: 0;
`;
const AuthorName = styled.span`
font-weight: ${theme.font.weight.medium};
`;
const HeroVisual = styled.div`
border-radius: ${theme.radius(2)};
display: flex;
flex-direction: column;
gap: ${theme.spacing(8)};
position: relative;
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
align-items: center;
flex-direction: row;
height: 462px;
justify-content: space-between;
padding-left: ${theme.spacing(28)};
padding-right: ${theme.spacing(14)};
}
`;
const VisualContent = styled.div`
align-items: center;
display: flex;
flex-direction: column;
gap: ${theme.spacing(4)};
justify-content: center;
position: relative;
z-index: 1;
@media (min-width: ${theme.breakpoints.md}px) {
width: 350px;
}
`;
const ImageContent = styled.div`
border-radius: ${theme.radius(1)};
height: 280px;
overflow: hidden;
position: relative;
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
height: 406px;
width: 714px;
}
`;
const ClientLogoWrap = styled.div`
max-width: 280px;
`;
const Separator = styled.span`
color: ${theme.colors.secondary.text[80]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(10)};
font-weight: ${theme.font.weight.light};
line-height: ${theme.lineHeight(11.5)};
`;
const TwentyLogoWrap = styled.div`
align-items: center;
background-color: ${theme.colors.primary.background[100]};
border-radius: ${theme.radius(2)};
display: flex;
height: 80px;
justify-content: center;
overflow: hidden;
width: 80px;
@media (min-width: ${theme.breakpoints.md}px) {
height: 98px;
width: 98px;
}
`;
const TwentyLogo = styled.div`
color: ${theme.colors.primary.text[100]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(6)};
font-weight: ${theme.font.weight.medium};
`;
const HeroImageOverlay = styled.div`
background: linear-gradient(
180deg,
rgba(28, 28, 28, 0.3) 0%,
rgba(28, 28, 28, 0.8) 100%
);
inset: 0;
position: absolute;
`;
type HeroProps = {
hero: CaseStudyData['hero'];
};
export function Hero({ hero }: HeroProps) {
const ClientIcon = CLIENT_ICONS[hero.clientIcon];
return (
<Section>
<StyledContainer>
<BackLink href="/case-studies">
<IconArrowLeft size={20} stroke={1.5} />
Back to case studies
</BackLink>
<HeroContent>
<Badge>
<IconClock size={16} stroke={1.5} />
{hero.readingTime}
</Badge>
<TitleWrap>
<Heading
as="h1"
segments={hero.title}
size="xl"
weight="light"
/>
</TitleWrap>
<AuthorLine>
Written by <AuthorName>{hero.author}</AuthorName>
</AuthorLine>
</HeroContent>
<HeroVisual>
<VisualContent>
{ClientIcon && (
<ClientLogoWrap>
<ClientIcon
fillColor={theme.colors.secondary.text[100]}
size={200}
/>
</ClientLogoWrap>
)}
<Separator>x</Separator>
<TwentyLogoWrap>
<TwentyLogo>T</TwentyLogo>
</TwentyLogoWrap>
</VisualContent>
{hero.heroImageSrc && (
<ImageContent>
<Image
alt=""
fill
sizes="(max-width: 920px) 100vw, 714px"
src={hero.heroImageSrc}
style={{ objectFit: 'cover' }}
/>
<HeroImageOverlay />
</ImageContent>
)}
</HeroVisual>
</StyledContainer>
</Section>
);
}
@@ -0,0 +1,129 @@
'use client';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import {
IconBrandDiscord,
IconBrandLinkedin,
IconBrandThreads,
IconBrandX,
} from '@tabler/icons-react';
const Sidebar = styled.aside`
display: none;
flex-direction: column;
gap: ${theme.spacing(10)};
position: fixed;
right: ${theme.spacing(10)};
top: 50%;
transform: translateY(-50%);
z-index: 10;
@media (min-width: ${theme.breakpoints.lg}px) {
display: flex;
}
`;
const TocPanel = styled.div`
background-color: ${theme.colors.primary.background[100]};
border-radius: ${theme.radius(1)};
display: flex;
gap: ${theme.spacing(4)};
max-width: 213px;
padding-bottom: ${theme.spacing(4)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(4)};
`;
const ProgressBar = styled.div`
background-color: ${theme.colors.primary.border[20]};
border-radius: 1px;
flex-shrink: 0;
width: 2px;
`;
const TocItems = styled.nav`
display: flex;
flex-direction: column;
gap: ${theme.spacing(5.5)};
`;
const TocItem = styled.span`
color: ${theme.colors.primary.text[60]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(3.5)};
`;
const SocialRow = styled.div`
align-items: center;
display: flex;
gap: ${theme.spacing(6)};
`;
const SocialLink = styled.a`
color: ${theme.colors.primary.text[60]};
display: flex;
&:hover {
color: ${theme.colors.primary.text[100]};
}
`;
type TableOfContentsProps = {
items: string[];
};
export function TableOfContents({ items }: TableOfContentsProps) {
return (
<Sidebar>
<TocPanel>
<ProgressBar />
<TocItems aria-label="Table of contents">
{items.map((item, index) => (
<TocItem key={index}>
{index + 1} {item}
</TocItem>
))}
</TocItems>
</TocPanel>
<SocialRow>
<SocialLink
aria-label="Discord"
href="https://discord.gg/twenty"
rel="noopener noreferrer"
target="_blank"
>
<IconBrandDiscord size={20} stroke={1.5} />
</SocialLink>
<SocialLink
aria-label="X"
href="https://x.com/twentycrm"
rel="noopener noreferrer"
target="_blank"
>
<IconBrandX size={20} stroke={1.5} />
</SocialLink>
<SocialLink
aria-label="LinkedIn"
href="https://www.linkedin.com/company/twenty"
rel="noopener noreferrer"
target="_blank"
>
<IconBrandLinkedin size={20} stroke={1.5} />
</SocialLink>
<SocialLink
aria-label="Threads"
href="https://www.threads.net/@twentycrm"
rel="noopener noreferrer"
target="_blank"
>
<IconBrandThreads size={20} stroke={1.5} />
</SocialLink>
</SocialRow>
</Sidebar>
);
}
@@ -0,0 +1,161 @@
import type { CaseStudyData } from '@/app/case-studies/_constants/types';
import { Container, Eyebrow } from '@/design-system/components';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import Image from 'next/image';
const Section = styled.section`
background-color: ${theme.colors.secondary.background[5]};
color: ${theme.colors.primary.text[100]};
min-width: 0;
width: 100%;
`;
const StyledContainer = styled(Container)`
display: flex;
flex-direction: column;
gap: ${theme.spacing(10)};
padding-bottom: ${theme.spacing(16)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(16)};
@media (min-width: ${theme.breakpoints.md}px) {
flex-direction: row;
gap: ${theme.spacing(15)};
padding-bottom: ${theme.spacing(25)};
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
padding-top: ${theme.spacing(25)};
}
`;
const QuoteColumn = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.spacing(14)};
margin: 0 auto;
max-width: 900px;
width: 100%;
`;
const QuoteText = styled.p`
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(8)};
font-weight: ${theme.font.weight.light};
line-height: ${theme.lineHeight(10)};
margin: 0;
@media (min-width: ${theme.breakpoints.md}px) {
font-size: ${theme.font.size(12)};
line-height: ${theme.lineHeight(14)};
}
`;
const Attribution = styled.div`
align-items: center;
display: flex;
gap: ${theme.spacing(4)};
`;
const Avatar = styled.div`
border-radius: ${theme.radius(1)};
flex-shrink: 0;
height: 48px;
overflow: hidden;
position: relative;
width: 48px;
`;
const AvatarFallback = styled.div`
align-items: center;
background-color: ${theme.colors.highlight[100]};
color: ${theme.colors.primary.text[100]};
display: flex;
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(5)};
font-weight: ${theme.font.weight.medium};
height: 100%;
justify-content: center;
width: 100%;
`;
const AuthorInfo = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.spacing(2)};
`;
const AuthorNameRow = styled.div`
align-items: center;
display: flex;
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
gap: ${theme.spacing(2)};
line-height: ${theme.lineHeight(5.5)};
`;
const AuthorName = styled.span`
font-weight: ${theme.font.weight.medium};
`;
const AuthorHandle = styled.span`
font-weight: ${theme.font.weight.regular};
`;
const AuthorDate = styled.span`
color: ${theme.colors.primary.text[80]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(3.5)};
`;
type TestimonialProps = {
testimonial: CaseStudyData['testimonial'];
};
export function Testimonial({ testimonial }: TestimonialProps) {
const initials = testimonial.author.name
.split(' ')
.map((word) => word[0])
.join('');
return (
<Section>
<StyledContainer>
<QuoteColumn>
<Eyebrow
colorScheme="primary"
heading={{ fontFamily: 'sans', text: testimonial.eyebrow }}
/>
<QuoteText>{testimonial.quote}</QuoteText>
<Attribution>
<Avatar>
{testimonial.author.avatarSrc ? (
<Image
alt={testimonial.author.name}
fill
sizes="48px"
src={testimonial.author.avatarSrc}
style={{ objectFit: 'cover' }}
/>
) : (
<AvatarFallback>{initials}</AvatarFallback>
)}
</Avatar>
<AuthorInfo>
<AuthorNameRow>
<AuthorName>{testimonial.author.name}</AuthorName>
<AuthorHandle>{testimonial.author.handle}</AuthorHandle>
</AuthorNameRow>
<AuthorDate>{testimonial.author.date}</AuthorDate>
</AuthorInfo>
</Attribution>
</QuoteColumn>
</StyledContainer>
</Section>
);
}
@@ -0,0 +1,104 @@
import type { CaseStudyTextBlock } from '@/app/case-studies/_constants/types';
import { Container, Eyebrow, Heading } from '@/design-system/components';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
const Section = styled.section`
background-color: ${theme.colors.primary.background[100]};
color: ${theme.colors.primary.text[100]};
min-width: 0;
width: 100%;
`;
const StyledContainer = styled(Container)`
display: flex;
justify-content: center;
padding-bottom: ${theme.spacing(12)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(12)};
@media (min-width: ${theme.breakpoints.md}px) {
padding-bottom: ${theme.spacing(22)};
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
padding-top: ${theme.spacing(22)};
}
`;
const ContentWrap = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.spacing(6)};
max-width: 556px;
width: 100%;
`;
const Body = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.spacing(4)};
`;
const Paragraph = styled.p`
color: ${theme.colors.primary.text[60]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(6)};
margin: 0;
@media (min-width: ${theme.breakpoints.md}px) {
font-size: ${theme.font.size(4.5)};
line-height: ${theme.lineHeight(6)};
}
`;
const Callout = styled.p`
color: ${theme.colors.primary.text[100]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4.5)};
font-weight: ${theme.font.weight.medium};
line-height: ${theme.lineHeight(7)};
margin: 0;
@media (min-width: ${theme.breakpoints.md}px) {
font-size: ${theme.font.size(5.5)};
line-height: ${theme.lineHeight(7)};
}
`;
type TextBlockProps = {
block: CaseStudyTextBlock;
};
export function TextBlock({ block }: TextBlockProps) {
return (
<Section>
<StyledContainer>
<ContentWrap>
{block.eyebrow && (
<Eyebrow
colorScheme="primary"
heading={{ fontFamily: 'sans', text: block.eyebrow }}
/>
)}
<Heading
as="h2"
segments={block.heading}
size="md"
weight="light"
/>
<Body>
{block.paragraphs.map((paragraph, index) => (
<Paragraph key={index}>{paragraph}</Paragraph>
))}
{block.callout && <Callout>{block.callout}</Callout>}
</Body>
</ContentWrap>
</StyledContainer>
</Section>
);
}
@@ -0,0 +1,59 @@
import type { CaseStudyVisualBlock } from '@/app/case-studies/_constants/types';
import { Container } from '@/design-system/components';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import Image from 'next/image';
const Section = styled.section`
background-color: ${theme.colors.primary.background[100]};
min-width: 0;
width: 100%;
`;
const StyledContainer = styled(Container)`
padding-bottom: ${theme.spacing(4)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(4)};
@media (min-width: ${theme.breakpoints.md}px) {
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
}
`;
const Visual = styled.div`
background-color: ${theme.colors.secondary.background[100]};
border-radius: ${theme.radius(2)};
height: 300px;
overflow: hidden;
position: relative;
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
height: 599px;
width: 776px;
}
`;
type VisualBlockProps = {
block: CaseStudyVisualBlock;
};
export function VisualBlock({ block }: VisualBlockProps) {
return (
<Section>
<StyledContainer>
<Visual>
<Image
alt={block.alt}
fill
sizes="(max-width: 920px) 100vw, 776px"
src={block.src}
style={{ objectFit: 'cover' }}
/>
</Visual>
</StyledContainer>
</Section>
);
}
@@ -0,0 +1,13 @@
import { Hero } from './Hero/Hero';
import { TableOfContents } from './TableOfContents/TableOfContents';
import { Testimonial } from './Testimonial/Testimonial';
import { TextBlock } from './TextBlock/TextBlock';
import { VisualBlock } from './VisualBlock/VisualBlock';
export const CaseStudy = {
Hero,
TableOfContents,
Testimonial,
TextBlock,
VisualBlock,
};
@@ -0,0 +1,171 @@
import type { CaseStudyCatalogEntry } from '@/app/case-studies/_constants/types';
import { CLIENT_ICONS } from '@/icons';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import Link from 'next/link';
const CardLink = styled(Link)`
border: 1px solid ${theme.colors.primary.border[20]};
border-radius: ${theme.radius(2)};
color: inherit;
display: flex;
flex-direction: column;
overflow: hidden;
text-decoration: none;
transition: border-color 0.2s ease;
&:hover {
border-color: ${theme.colors.primary.border[40]};
}
`;
const Thumbnail = styled.div`
align-items: center;
background-color: ${theme.colors.secondary.background[5]};
display: flex;
height: 200px;
justify-content: center;
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
height: 240px;
}
`;
const CardBody = styled.div`
display: flex;
flex-direction: column;
gap: ${theme.spacing(3)};
padding-bottom: ${theme.spacing(6)};
padding-left: ${theme.spacing(6)};
padding-right: ${theme.spacing(6)};
padding-top: ${theme.spacing(5)};
`;
const Tag = styled.span`
color: ${theme.colors.highlight[100]};
font-family: ${theme.font.family.mono};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.medium};
letter-spacing: 0.05em;
line-height: ${theme.lineHeight(4)};
text-transform: uppercase;
`;
const Title = styled.h3`
color: ${theme.colors.primary.text[100]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(5)};
font-weight: ${theme.font.weight.medium};
line-height: ${theme.lineHeight(6.5)};
margin: 0;
@media (min-width: ${theme.breakpoints.md}px) {
font-size: ${theme.font.size(5.5)};
line-height: ${theme.lineHeight(7)};
}
`;
const Summary = styled.p`
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
color: ${theme.colors.primary.text[60]};
display: -webkit-box;
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(4)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(5.5)};
margin: 0;
overflow: hidden;
`;
const Footer = styled.div`
align-items: center;
border-top: 1px solid ${theme.colors.primary.border[10]};
display: flex;
gap: ${theme.spacing(3)};
margin-left: ${theme.spacing(6)};
margin-right: ${theme.spacing(6)};
padding-bottom: ${theme.spacing(5)};
padding-top: ${theme.spacing(5)};
`;
const AuthorAvatar = styled.div`
align-items: center;
background-color: ${theme.colors.highlight[100]};
border-radius: 50%;
color: ${theme.colors.primary.text[100]};
display: flex;
flex-shrink: 0;
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3)};
font-weight: ${theme.font.weight.medium};
height: 28px;
justify-content: center;
width: 28px;
`;
const AuthorName = styled.span`
color: ${theme.colors.primary.text[80]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3.5)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(4)};
`;
const Dot = styled.span`
background-color: ${theme.colors.primary.text[40]};
border-radius: 50%;
flex-shrink: 0;
height: 3px;
width: 3px;
`;
const DateLabel = styled.span`
color: ${theme.colors.primary.text[40]};
font-family: ${theme.font.family.sans};
font-size: ${theme.font.size(3.5)};
font-weight: ${theme.font.weight.regular};
line-height: ${theme.lineHeight(4)};
`;
type CardProps = {
entry: CaseStudyCatalogEntry;
};
export function Card({ entry }: CardProps) {
const ClientIcon = CLIENT_ICONS[entry.hero.clientIcon];
const logoWidth = 140;
const initials = entry.hero.author
.split(' ')
.map((word) => word[0])
.join('');
return (
<CardLink href={entry.href}>
<Thumbnail>
{ClientIcon ? (
<ClientIcon
fillColor={theme.colors.primary.text[100]}
size={logoWidth}
/>
) : null}
</Thumbnail>
<CardBody>
<Tag>Case Study</Tag>
<Title>
{entry.hero.title.map((segment) => segment.text).join('')}
</Title>
<Summary>{entry.catalogCard.summary}</Summary>
</CardBody>
<Footer>
<AuthorAvatar>{initials}</AuthorAvatar>
<AuthorName>{entry.hero.author}</AuthorName>
<Dot />
<DateLabel>{entry.catalogCard.date}</DateLabel>
</Footer>
</CardLink>
);
}
@@ -0,0 +1,52 @@
import type { CaseStudyCatalogEntry } from '@/app/case-studies/_constants/types';
import { Container } from '@/design-system/components';
import { Card } from '@/sections/CaseStudyCatalog/components/Card/Card';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
const Section = styled.section`
background-color: ${theme.colors.primary.background[100]};
min-width: 0;
width: 100%;
`;
const StyledContainer = styled(Container)`
padding-bottom: ${theme.spacing(20)};
padding-left: ${theme.spacing(4)};
padding-right: ${theme.spacing(4)};
padding-top: ${theme.spacing(10)};
@media (min-width: ${theme.breakpoints.md}px) {
padding-left: ${theme.spacing(10)};
padding-right: ${theme.spacing(10)};
padding-top: ${theme.spacing(16)};
}
`;
const CardGrid = styled.div`
display: grid;
gap: ${theme.spacing(6)};
grid-template-columns: 1fr;
@media (min-width: ${theme.breakpoints.md}px) {
grid-template-columns: repeat(2, 1fr);
}
`;
type GridProps = {
entries: readonly CaseStudyCatalogEntry[];
};
export function Grid({ entries }: GridProps) {
return (
<Section>
<StyledContainer>
<CardGrid>
{entries.map((entry) => (
<Card key={entry.href} entry={entry} />
))}
</CardGrid>
</StyledContainer>
</Section>
);
}
@@ -0,0 +1,4 @@
import { Card } from './Card/Card';
import { Grid } from './Grid/Grid';
export const CaseStudyCatalog = { Card, Grid };
@@ -0,0 +1,31 @@
'use client';
import { TalkToUsButton } from '@/app/components/ContactCalModal';
import { LinkButton } from '@/design-system/components';
import type { FooterCtaType } from '@/sections/Footer/types';
type FooterNavCtaProps = {
cta: FooterCtaType;
};
export function FooterNavCta({ cta }: FooterNavCtaProps) {
if (cta.kind === 'contactModal') {
return (
<TalkToUsButton
color={cta.color}
label={cta.label}
variant={cta.variant}
/>
);
}
return (
<LinkButton
color={cta.color}
href={cta.href}
label={cta.label}
type="anchor"
variant={cta.variant}
/>
);
}
@@ -1,4 +1,4 @@
import { LinkButton } from '@/design-system/components';
import { FooterNavCta } from '@/sections/Footer/components/FooterNavCta/FooterNavCta';
import { PlusIcon, RectangleFillIcon } from '@/icons';
import type { FooterNavGroupType } from '@/sections/Footer/types';
import { theme } from '@/theme';
@@ -179,13 +179,13 @@ export function Nav({ groups }: NavProps) {
{group.ctas.length > 0 && (
<Actions>
{group.ctas.map((cta) => (
<LinkButton
key={`${cta.label}-${cta.href}`}
color={cta.color}
href={cta.href}
label={cta.label}
type={cta.type}
variant={cta.variant}
<FooterNavCta
key={
cta.kind === 'link'
? `${cta.label}-${cta.href}`
: `${cta.label}-${cta.kind}`
}
cta={cta}
/>
))}
</Actions>
@@ -1,7 +1,14 @@
export type FooterCtaType = {
color: 'secondary';
href: string;
label: string;
type: 'anchor';
variant: 'contained' | 'outlined';
};
export type FooterCtaType =
| {
color: 'secondary';
href: string;
kind: 'link';
label: string;
variant: 'contained' | 'outlined';
}
| {
color: 'secondary';
kind: 'contactModal';
label: string;
variant: 'contained' | 'outlined';
};
@@ -78,7 +78,7 @@ type CardProps = {
export function Card({ card }: CardProps) {
const IconComponent = CLIENT_ICONS[card.icon];
const logoWidth = card.icon === 'evergreen' ? 96 : 104;
const logoWidth = 104;
return (
<CardRoot>
@@ -107,9 +107,9 @@ export function Card({ card }: CardProps) {
<CtaRow>
<LinkButton
color="primary"
href="https://twenty.com"
href={card.href}
label="Read the case"
type="anchor"
type="link"
variant="outlined"
/>
</CtaRow>
@@ -7,4 +7,5 @@ export type HeadingCardType = {
illustration: IllustrationId;
heading: HeadingType;
body: BodyType;
href: string;
};
@@ -40,6 +40,10 @@ const BodyContainer = styled.div`
&[data-page=${Pages.ReleaseNotes}] {
max-width: 591px;
}
&[data-page=${Pages.CaseStudies}] {
max-width: 550px;
}
}
`;
@@ -13,7 +13,7 @@ const StyledContainer = styled.div`
`;
const partnerHeroImageClassName = css`
object-fit: contain;
object-fit: cover;
object-position: center;
`;
@@ -4,18 +4,13 @@ function clampUnit(value: number): number {
return Math.max(0, Math.min(1, value));
}
// Lottie timeline boundaries (from the animation source):
// Step 1 content: frames 0345, transition 1→2: frames 346415
// Step 2 content: frames 416933, transition 2→3: frames 934980
// Step 3 content: frames 981end
const STEP_1_TRANSITION_END = 415;
const STEP_2_TRANSITION_END = 980;
const STEP_2_TRANSITION_END = 1045;
// Per-step hold fraction: how much of each step's scroll drives Lottie-only
// (left column stays fixed); the remainder drives the content transition.
export const HOME_STEPPER_HOLD_FRACTIONS = [
345 / STEP_1_TRANSITION_END,
(933 - STEP_1_TRANSITION_END) / (STEP_2_TRANSITION_END - STEP_1_TRANSITION_END),
(933 - STEP_1_TRANSITION_END) /
(STEP_2_TRANSITION_END - STEP_1_TRANSITION_END),
1,
] as const;
@@ -31,9 +26,17 @@ export function scrollProgressToHomeStepperLottieFrame(
const localProgress = stepFloat - stepIndex;
const rangeStart =
stepIndex === 0 ? 0 : stepIndex === 1 ? STEP_1_TRANSITION_END : STEP_2_TRANSITION_END;
stepIndex === 0
? 0
: stepIndex === 1
? STEP_1_TRANSITION_END
: STEP_2_TRANSITION_END;
const rangeEnd =
stepIndex === 0 ? STEP_1_TRANSITION_END : stepIndex === 1 ? STEP_2_TRANSITION_END : lastIndex;
stepIndex === 0
? STEP_1_TRANSITION_END
: stepIndex === 1
? STEP_2_TRANSITION_END
: lastIndex;
const frame = rangeStart + localProgress * (rangeEnd - rangeStart);
@@ -62,6 +62,34 @@ const Article = styled.article`
p:first-of-type {
margin-top: ${theme.spacing(6)};
}
ol,
ul {
color: ${theme.colors.primary.text[80]};
font-size: ${theme.font.size(4)};
line-height: 1.65;
margin: 0;
margin-top: ${theme.spacing(4)};
padding-left: ${theme.spacing(6)};
}
li {
margin-top: ${theme.spacing(2)};
}
li:first-child {
margin-top: 0;
}
strong {
color: ${theme.colors.primary.text[100]};
font-weight: ${theme.font.weight.medium};
}
a {
color: ${theme.colors.highlight[100]};
text-decoration: underline;
}
`;
type LegalDocumentPageProps = {
@@ -59,6 +59,8 @@ const IntroBlock = styled.div`
display: grid;
grid-template-columns: 1fr;
row-gap: ${theme.spacing(2)};
margin-top: calc(${theme.spacing(2)} - ${theme.spacing(26)});
margin-bottom: ${theme.spacing(4)};
`;
const StepRowHeader = styled.div`
@@ -148,7 +150,7 @@ export function Content({
/>
) : null}
</StepIconBox>
<Heading segments={step.heading} size="md" weight="regular" />
<Heading segments={step.heading} size="sm" weight="regular" />
</StepRowHeader>
<Body body={step.body} size="sm" />
</StepBlock>
@@ -13,7 +13,6 @@ const VisualColumn = styled.div`
width: 100%;
@media (min-width: ${theme.breakpoints.md}px) {
align-self: start;
max-width: 672px;
position: sticky;
top: calc(4.5rem + (100vh - 4.5rem) * 0.5 - 368px);
@@ -6,16 +6,9 @@ import { ArrowLeftIcon, ArrowRightIcon } from '@/icons';
import type { TestimonialCardType } from '@/sections/Testimonials/types/TestimonialCard';
import { theme } from '@/theme';
import { styled } from '@linaria/react';
import NextImage from 'next/image';
import { type ReactNode, useState } from 'react';
import { Separator } from '../Separator/Separator';
const DATE_FORMATTER = new Intl.DateTimeFormat('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric',
});
const StyledCarousel = styled.div`
display: grid;
grid-template-columns: 1fr;
@@ -125,7 +118,6 @@ const FooterRow = styled.div`
@media (min-width: ${theme.breakpoints.md}px) {
align-items: center;
grid-template-columns: auto 1fr;
justify-items: end;
}
`;
@@ -142,42 +134,17 @@ const NavGroup = styled.div`
`;
const AuthorBlock = styled.div`
align-items: center;
column-gap: ${theme.spacing(4)};
display: grid;
grid-template-columns: 48px 1fr;
order: 1;
@media (min-width: ${theme.breakpoints.md}px) {
order: 2;
}
`;
const AvatarFrame = styled.div`
border-radius: ${theme.radius(0.5)};
height: 48px;
overflow: hidden;
position: relative;
width: 48px;
`;
const AuthorMeta = styled.div`
display: grid;
grid-template-columns: 1fr;
row-gap: ${theme.spacing(2)};
`;
justify-items: start;
order: 1;
row-gap: ${theme.spacing(1)};
text-align: left;
const NameHandleRow = styled.div`
align-items: center;
column-gap: ${theme.spacing(2)};
display: grid;
grid-auto-flow: column;
justify-content: start;
`;
const HandleText = styled.span`
border-left: 1px solid ${theme.colors.primary.border[20]};
padding-left: ${theme.spacing(2)};
@media (min-width: ${theme.breakpoints.md}px) {
justify-self: end;
order: 2;
}
`;
type CarouselProps = {
@@ -273,33 +240,18 @@ export function Carousel({ children, eyebrow, testimonials }: CarouselProps) {
</NavGroup>
<AuthorBlock>
<AvatarFrame>
<NextImage
alt={current.author.avatar.alt || ''}
fill
sizes="48px"
src={current.author.avatar.src}
style={{ objectFit: 'cover' }}
/>
</AvatarFrame>
<AuthorMeta>
<NameHandleRow>
<Body
as="span"
body={current.author.name}
size="sm"
weight="medium"
/>
<HandleText>
<Body as="span" body={current.author.handle} size="sm" />
</HandleText>
</NameHandleRow>
<Body
as="p"
body={{ text: DATE_FORMATTER.format(current.author.date) }}
size="xs"
/>
</AuthorMeta>
<Body
as="span"
body={current.author.name}
size="sm"
weight="medium"
/>
<Body
as="span"
body={current.author.designation}
size="xs"
weight="light"
/>
</AuthorBlock>
</FooterRow>
</RightColumn>
@@ -1,9 +1,6 @@
import type { BodyType } from '@/design-system/components/Body/types/Body';
import type { ImageType } from '@/design-system/components/Image/types/Image';
export type TestimonialAuthorType = {
avatar: ImageType;
name: BodyType;
handle: BodyType;
date: Date;
designation: BodyType;
};
@@ -1,6 +1,7 @@
'use client';
import { Body, Heading } from '@/design-system/components';
import { Body, Heading, IconButton } from '@/design-system/components';
import { ArrowRightIcon } from '@/icons';
import { THREE_CARDS_ILLUSTRATIONS } from '@/illustrations';
import type { ThreeCardsIllustrationCardType } from '@/sections/ThreeCards/types';
import { theme } from '@/theme';
@@ -42,9 +43,14 @@ const CardEmbed = styled.div`
`;
const CardFooter = styled.footer`
display: flex;
align-items: center;
gap: ${theme.spacing(2)};
align-items: start;
column-gap: ${theme.spacing(2)};
display: grid;
grid-template-columns: auto auto auto 1fr;
`;
const FooterTrailingAction = styled.div`
justify-self: end;
`;
const AttributionPipe = styled.span`
@@ -112,6 +118,20 @@ export function IllustrationCard({
size="xs"
weight="regular"
/>
{illustrationCard.caseStudySlug !== undefined ? (
<FooterTrailingAction>
<IconButton
ariaLabel="Read case study"
borderColor={CARD_OUTLINE_COLOR}
href={`/case-studies/${illustrationCard.caseStudySlug}`}
icon={ArrowRightIcon}
iconFillColor="transparent"
iconSize={24}
iconStrokeColor={theme.colors.primary.text[80]}
size={48}
/>
</FooterTrailingAction>
) : null}
</CardFooter>
)}
</IllustrationCardContainer>
@@ -9,4 +9,5 @@ export type ThreeCardsIllustrationCardType = {
benefits?: BodyType[];
attribution?: ThreeCardsIllustrationCardAttributionType;
illustration: ThreeCardsIllustrationId;
caseStudySlug?: string;
};
+1
View File
@@ -60921,6 +60921,7 @@ __metadata:
dependencies:
"@base-ui/react": "npm:^1.3.0"
"@base-ui/utils": "npm:^0.2.6"
"@calcom/embed-react": "npm:^1.5.3"
"@gsap/react": "npm:2.1.2"
"@linaria/core": "npm:^7.0.0"
"@linaria/react": "npm:^7.0.1"