From 61683d8bda0adebc25155da93971ea8b35ef4df9 Mon Sep 17 00:00:00 2001 From: "Abdullah." <125115953+mabdullahabaid@users.noreply.github.com> Date: Thu, 14 May 2026 12:48:14 +0500 Subject: [PATCH] [Website] Replace product page hero visual with interactive CRM depicting AI chat in action. (#20566) Before: image After: https://github.com/user-attachments/assets/c019586f-ef9f-4ae0-8afe-14f08e8cb057 --- .../shared/people/avatars/sam-altman.webp | Bin 0 -> 7572 bytes .../src/app/[locale]/product/page.tsx | 7 +- .../src/content/site/asset-paths.ts | 1 + .../src/sections/Feature/visuals/BarChart.tsx | 202 ++++++-- .../Feature/visuals/DashboardVisual.tsx | 159 +++++- .../sections/Feature/visuals/DonutChart.tsx | 2 +- .../Feature/visuals/dashboard-visual.data.ts | 24 +- .../Hero/components/ProductVisual.tsx | 11 - .../ProductVisual/ProductVisual.tsx | 469 ++++++++++++++++++ .../ProductVisual/product-visual.data.tsx | 127 +++++ .../use-product-visual-autoplay.ts | 118 +++++ .../src/sections/Hero/components/index.ts | 2 +- 12 files changed, 1034 insertions(+), 88 deletions(-) create mode 100644 packages/twenty-website-new/public/images/shared/people/avatars/sam-altman.webp delete mode 100644 packages/twenty-website-new/src/sections/Hero/components/ProductVisual.tsx create mode 100644 packages/twenty-website-new/src/sections/Hero/components/ProductVisual/ProductVisual.tsx create mode 100644 packages/twenty-website-new/src/sections/Hero/components/ProductVisual/product-visual.data.tsx create mode 100644 packages/twenty-website-new/src/sections/Hero/components/ProductVisual/use-product-visual-autoplay.ts diff --git a/packages/twenty-website-new/public/images/shared/people/avatars/sam-altman.webp b/packages/twenty-website-new/public/images/shared/people/avatars/sam-altman.webp new file mode 100644 index 0000000000000000000000000000000000000000..01e10dde700bbeaf336c66c7063f80c6acf310a4 GIT binary patch literal 7572 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 literal 0 HcmV?d00001 diff --git a/packages/twenty-website-new/src/app/[locale]/product/page.tsx b/packages/twenty-website-new/src/app/[locale]/product/page.tsx index 2eb2d22a5fa..fb53bd5e9d3 100644 --- a/packages/twenty-website-new/src/app/[locale]/product/page.tsx +++ b/packages/twenty-website-new/src/app/[locale]/product/page.tsx @@ -98,11 +98,6 @@ export default async function ProductPage({ params }: ProductPageProps) { i18n.locale as AppLocale, )} /> - - + (null); - const rawMax = Math.max(...data.map((datum) => datum.value)); - const maxValue = rawMax * 1.3; + const allValues = data.flatMap((datum) => [datum.value, datum.value2]); + const maxValue = Math.max(...allValues) * 1.05; + const tickStep = Math.ceil(maxValue / Y_TICK_COUNT); + const yTicks = Array.from( + { length: Y_TICK_COUNT + 1 }, + (_, i) => i * tickStep, + ); return ( - Monthly volume - - {data.map((datum, index) => { - const heightPercent = (datum.value / maxValue) * 100; - const isHovered = hoveredIndex === index; + Widget name + + + Values + + {[...yTicks].reverse().map((tick) => ( + {tick === 0 ? '0' : `${tick}K`} + ))} + + + + {data.map((datum, index) => { + const h1 = (datum.value / maxValue) * 100; + const h2 = (datum.value2 / maxValue) * 100; + const isHovered = hoveredIndex === index; - return ( - - {active ? ( - - {datum.value} - - ) : null} - setHoveredIndex(index)} - onPointerLeave={() => setHoveredIndex(null)} - style={{ - backgroundColor: ACCENT, - filter: isHovered ? 'brightness(1.3)' : 'none', - height: active ? `${heightPercent}%` : '0%', - transitionDelay: active ? `${index * 50}ms` : '0ms', - }} - /> - {datum.label} - - ); - })} - + return ( + setHoveredIndex(index)} + onPointerLeave={() => setHoveredIndex(null)} + > + + + {active ? ( + + {datum.value} + + ) : null} + + + + {active ? ( + + {datum.value2} + + ) : null} + + + + {datum.label} + + ); + })} + + Months + + + ); } diff --git a/packages/twenty-website-new/src/sections/Feature/visuals/DashboardVisual.tsx b/packages/twenty-website-new/src/sections/Feature/visuals/DashboardVisual.tsx index bd9c40cae3a..f04866141b8 100644 --- a/packages/twenty-website-new/src/sections/Feature/visuals/DashboardVisual.tsx +++ b/packages/twenty-website-new/src/sections/Feature/visuals/DashboardVisual.tsx @@ -5,20 +5,109 @@ import { styled } from '@linaria/react'; import { BarChart } from './BarChart'; import { BAR_DATA, DONUT_VALUE } from './dashboard-visual.data'; import { DonutChart } from './DonutChart'; -import { WindowChrome } from './WindowChrome'; +import { + BG_DARK, + BG_PANEL, + BORDER_COLOR, + TEXT_MUTED, + TEXT_PRIMARY, + TEXT_SECONDARY, +} from './visual-tokens'; + +const Window = styled.div` + background-color: ${BG_DARK}; + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; + width: 100%; +`; + +const Topbar = styled.div` + align-items: center; + border-bottom: 1px solid ${BORDER_COLOR}; + display: flex; + flex-shrink: 0; + gap: 8px; + padding: 10px 16px; +`; + +const BreadcrumbNav = styled.div` + align-items: center; + display: flex; + gap: 6px; +`; + +const BreadcrumbIcon = styled.span` + align-items: center; + color: ${TEXT_SECONDARY}; + display: flex; +`; + +const BreadcrumbText = styled.span` + color: ${TEXT_SECONDARY}; + font-size: 12px; + letter-spacing: 0.01em; +`; + +const BreadcrumbBold = styled.span` + color: ${TEXT_PRIMARY}; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.01em; +`; + +const TopbarActions = styled.div` + align-items: center; + display: flex; + gap: 8px; + margin-left: auto; +`; + +const ActionBtn = styled.span` + align-items: center; + border: 1px solid ${BORDER_COLOR}; + border-radius: 4px; + color: ${TEXT_SECONDARY}; + display: flex; + font-size: 11px; + gap: 4px; + padding: 4px 8px; +`; + +const ActionIcon = styled.span` + color: ${TEXT_MUTED}; + display: flex; +`; const Body = styled.div` display: grid; flex: 1; gap: 12px; grid-template-columns: 200px 1fr; + grid-template-rows: 1fr 60px; min-height: 0; padding: 14px; +`; - @media (max-width: 600px) { - grid-template-columns: 1fr; - grid-template-rows: auto 1fr; - } +const BarChartCell = styled.div` + grid-column: 2; + grid-row: 1 / -1; + min-height: 0; +`; + +const BottomPanel = styled.div` + background-color: ${BG_PANEL}; + border: 1px solid ${BORDER_COLOR}; + border-radius: 10px; + padding: 14px 16px; +`; + +const WidgetTitle = styled.span` + color: ${TEXT_SECONDARY}; + font-size: 12px; + font-weight: 500; + letter-spacing: 0.02em; `; type DashboardVisualProps = { @@ -27,15 +116,61 @@ type DashboardVisualProps = { export function DashboardVisual({ active }: DashboardVisualProps) { return ( - + + + + + + + + + + + + Dashboard / + Sales performances + + + + + + + + Delete + + + + + + + + + ⌘K + + - + + + + + Widget name + - + ); } diff --git a/packages/twenty-website-new/src/sections/Feature/visuals/DonutChart.tsx b/packages/twenty-website-new/src/sections/Feature/visuals/DonutChart.tsx index 8fda9580ae6..7b42a0ef214 100644 --- a/packages/twenty-website-new/src/sections/Feature/visuals/DonutChart.tsx +++ b/packages/twenty-website-new/src/sections/Feature/visuals/DonutChart.tsx @@ -92,7 +92,7 @@ export function DonutChart({ active, value }: DonutChartProps) { return ( - Conversion rate + Widget name }> - - - ); -} diff --git a/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/ProductVisual.tsx b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/ProductVisual.tsx new file mode 100644 index 00000000000..bfee5f94215 --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/ProductVisual.tsx @@ -0,0 +1,469 @@ +'use client'; + +import { styled } from '@linaria/react'; + +import type { AppPreviewConfig } from '@/sections/AppPreview'; +import { COLORS } from '@/sections/AppPreview/Shared/utils/app-preview-theme'; +import { VISUAL_TOKENS } from '@/sections/AppPreview/Shared/utils/app-preview-tokens'; +import { AppPreviewNavbar } from '@/sections/AppPreview/Shell/AppPreviewNavbar'; +import { AppPreviewSidebar } from '@/sections/AppPreview/Shell/AppPreviewSidebar'; +import { AppPreviewViewbar } from '@/sections/AppPreview/Shell/AppPreviewViewbar'; +import { renderPageDefinition } from '@/sections/AppPreview/Shell/PageRenderers'; +import { AppWindow } from '@/sections/AppPreview/AppWindow/AppWindow'; +import { WindowOrderProvider } from '@/sections/AppPreview/WindowOrder/WindowOrderProvider'; +import { theme } from '@/theme'; + +import { PROMPT_OPTIONS } from './product-visual.data'; +import { useProductVisualAutoplay } from './use-product-visual-autoplay'; + +const StyledRoot = styled.div` + isolation: isolate; + margin-top: ${theme.spacing(5)}; + position: relative; + text-align: left; + width: 100%; + + @media (min-width: ${theme.breakpoints.md}px) { + margin-top: ${theme.spacing(8)}; + } +`; + +const ShellScene = styled.div` + aspect-ratio: 1 / 1; + margin: 0 auto; + max-height: 740px; + position: relative; + width: 100%; + + @media (min-width: ${theme.breakpoints.md}px) { + aspect-ratio: 1280 / 832; + } +`; + +const AppLayout = styled.div` + display: flex; + flex: 1 1 auto; + height: 100%; + min-height: 0; + min-width: 0; + overflow: hidden; + position: relative; + width: 100%; + z-index: 1; +`; + +const RightColumn = styled.div` + display: flex; + flex: 1 1 0; + flex-direction: column; + gap: 12px; + min-height: 0; + min-width: 0; + padding: 12px 12px 12px 0; +`; + +const ContentRow = styled.div` + display: flex; + flex: 1 1 auto; + gap: 8px; + min-height: 0; +`; + +const IndexSurface = styled.div` + background: ${COLORS.background}; + border: 1px solid ${COLORS.border}; + border-radius: 8px; + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; + min-width: 0; + overflow: hidden; + + [aria-label*='workflow'] > div > div { + left: 0; + transform: scale(0.65) translateX(-20%); + transform-origin: top left; + } +`; + +const AiPanel = styled.aside` + background: ${VISUAL_TOKENS.background.primary}; + border: 1px solid ${VISUAL_TOKENS.border.color.medium}; + border-radius: 8px; + display: flex; + flex-direction: column; + flex-shrink: 0; + height: 100%; + min-height: 0; + overflow: hidden; + width: 280px; + + @media (max-width: ${theme.breakpoints.md}px) { + display: none; + } +`; + +const AiPanelHeader = styled.div` + align-items: center; + background-color: ${VISUAL_TOKENS.background.secondary}; + border-bottom: 1px solid ${VISUAL_TOKENS.border.color.medium}; + display: flex; + flex-shrink: 0; + gap: 4px; + height: 40px; + padding: 0 8px; +`; + +const AiHeaderBtn = styled.span` + align-items: center; + border-radius: 4px; + color: ${VISUAL_TOKENS.font.color.secondary}; + display: flex; + height: 28px; + justify-content: center; + width: 28px; +`; + +const AiPanelTitle = styled.span` + color: ${VISUAL_TOKENS.font.color.primary}; + flex: 1; + font-size: 13px; + font-weight: 600; + text-align: center; +`; + +const AiMessages = styled.div` + display: flex; + flex: 1; + flex-direction: column; + gap: 8px; + overflow-y: auto; + padding: 12px; +`; + +const UserMsg = styled.div` + background: ${VISUAL_TOKENS.background.transparent.medium}; + border-radius: ${VISUAL_TOKENS.border.radius.sm}; + color: ${VISUAL_TOKENS.font.color.secondary}; + font-size: 13px; + font-weight: 500; + line-height: 1.4em; + padding: 4px 8px; + width: fit-content; +`; + +const AiMsg = styled.div` + color: ${VISUAL_TOKENS.font.color.primary}; + font-size: 13px; + font-weight: 400; + line-height: 1.4em; + width: 100%; +`; + +const ThinkingText = styled.span` + color: ${VISUAL_TOKENS.font.color.tertiary}; + font-size: 13px; +`; + +const PromptOption = styled.button` + align-items: center; + background: none; + border: none; + border-radius: 4px; + color: ${VISUAL_TOKENS.font.color.primary}; + cursor: pointer; + display: flex; + font-size: 13px; + gap: 8px; + line-height: 1.4; + padding: 6px 4px; + text-align: left; + width: 100%; + + &:hover { + background: ${VISUAL_TOKENS.background.transparent.light}; + } +`; + +const PromptOptionIcon = styled.span` + align-items: center; + color: ${VISUAL_TOKENS.font.color.secondary}; + display: flex; + flex-shrink: 0; +`; + +const PromptOptions = styled.div` + display: flex; + flex-direction: column; + padding: 0 12px; +`; + +const AiInputArea = styled.div` + align-items: flex-end; + display: flex; + flex-direction: column; + flex-shrink: 0; + gap: 8px; + padding: 12px; +`; + +const AiInputBox = styled.div` + background-color: ${VISUAL_TOKENS.background.transparent.lighter}; + border: 1px solid ${VISUAL_TOKENS.border.color.medium}; + border-radius: 8px; + display: flex; + flex-direction: column; + min-height: 100px; + padding: 12px; + width: 100%; +`; + +const AiInputPlaceholder = styled.span` + color: ${VISUAL_TOKENS.font.color.light}; + font-size: 13px; + font-weight: 400; +`; + +const AiInputBtnRow = styled.div` + align-items: center; + display: flex; + justify-content: space-between; + margin-top: auto; +`; + +const AiInputLeftBtns = styled.div` + align-items: center; + color: ${VISUAL_TOKENS.font.color.tertiary}; + display: flex; + gap: 2px; +`; + +const AiInputRightBtns = styled.div` + align-items: center; + display: flex; + gap: 4px; +`; + +const ModelChip = styled.span` + align-items: center; + border: 1px solid ${VISUAL_TOKENS.border.color.medium}; + border-radius: 6px; + color: ${VISUAL_TOKENS.font.color.secondary}; + display: flex; + font-size: 11px; + gap: 4px; + padding: 3px 8px; +`; + +const SendBtn = styled.span` + align-items: center; + background: ${VISUAL_TOKENS.border.color.medium}; + border-radius: 50%; + color: ${VISUAL_TOKENS.font.color.tertiary}; + display: flex; + height: 24px; + justify-content: center; + width: 24px; +`; + +type ProductVisualProps = { + visual: AppPreviewConfig; +}; + +export function ProductVisual({ visual }: ProductVisualProps) { + const { + activeItem, + activeLabel, + displayPage, + handleOptionSelect, + handleSelectLabel, + handleToggleFolder, + highlightedItemId, + openFolderIds, + revealedObjectIds, + selectedOption, + streamComplete, + streamedText, + workspaceNav, + } = useProductVisualAutoplay(visual); + + const activeHeader = displayPage?.header; + const showViewBar = + displayPage !== null && + displayPage !== undefined && + displayPage.type !== 'dashboard' && + displayPage.type !== 'workflow'; + + return ( + + + + + + + + + + + + + {showViewBar ? ( + + ) : null} + + {displayPage + ? renderPageDefinition( + displayPage, + handleSelectLabel, + activeItem?.id ?? activeLabel, + ) + : null} + + + + + + + + + + + Ask AI + + + + + + + + + {PROMPT_OPTIONS[selectedOption].label} + {streamedText ? ( + {streamedText} + ) : ( + Thinking... + )} + + {streamComplete ? ( + + {PROMPT_OPTIONS.filter( + (_, index) => index !== selectedOption, + ).map((option, index) => ( + + handleOptionSelect(PROMPT_OPTIONS.indexOf(option)) + } + > + {option.icon} + {option.label} + + ))} + + ) : null} + + + + Ask, search or make anything... + + + + + + + + + + + + + Claude Haiku 4.5 + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/product-visual.data.tsx b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/product-visual.data.tsx new file mode 100644 index 00000000000..583b8617fe6 --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/product-visual.data.tsx @@ -0,0 +1,127 @@ +import { SHARED_PEOPLE_AVATAR_URLS } from '@/content/site/asset-paths'; + +export const NEW_COMPANY_ROW = { + id: 'openai', + cells: { + company: { type: 'entity' as const, name: 'OpenAI', domain: 'openai.com' }, + url: { type: 'link' as const, value: 'openai.com' }, + createdBy: { + type: 'person' as const, + name: 'AI Agent', + tone: 'gray' as const, + kind: 'system' as const, + shortLabel: 'AI', + }, + address: { type: 'text' as const, value: '3180 18th St' }, + accountOwner: { + type: 'person' as const, + name: 'Sam Altman', + tone: 'amber' as const, + kind: 'person' as const, + avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman, + }, + icp: { type: 'boolean' as const, value: true }, + arr: { type: 'number' as const, value: '$2,000,000' }, + linkedin: { type: 'link' as const, value: 'openai' }, + industry: { type: 'tag' as const, value: 'AI Research' }, + mainContact: { + type: 'person' as const, + name: 'Sam Altman', + shortLabel: 'S', + tone: 'amber' as const, + kind: 'person' as const, + avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman, + }, + employees: { type: 'number' as const, value: '3,500' }, + opportunities: { type: 'relation' as const, items: [] }, + added: { type: 'text' as const, value: 'Just now' }, + }, +}; + +export const NEW_PERSON_ROW = { + id: 'sam-altman', + cells: { + name: { + type: 'person' as const, + name: 'Sam Altman', + tone: 'amber' as const, + kind: 'person' as const, + avatarUrl: SHARED_PEOPLE_AVATAR_URLS.samAltman, + }, + company: { type: 'entity' as const, name: 'OpenAI', domain: 'openai.com' }, + email: { type: 'link' as const, value: 'sam@openai.com' }, + phone: { type: 'text' as const, value: '+1 415 555 0199' }, + jobTitle: { type: 'text' as const, value: 'CEO' }, + city: { type: 'text' as const, value: 'San Francisco' }, + linkedin: { type: 'link' as const, value: 'sama' }, + added: { type: 'text' as const, value: 'Just now' }, + }, +}; + +export const PROMPT_OPTIONS = [ + { + icon: ( + + + + + ), + label: 'Add a new lead', + navSteps: [ + { at: 0.25, target: 'Companies' }, + { at: 0.65, target: 'People' }, + ], + response: + 'Adding OpenAI as a new company. Setting domain to openai.com, industry to AI Research, and ARR to $2,000,000. Account owner assigned to Sam Altman. Company record is live in your CRM.\n\nNow creating the contact — adding Sam Altman as CEO at OpenAI, based in San Francisco. Person record linked to the company.', + }, + { + icon: ( + + + + + + + ), + label: 'Create a dashboard', + navSteps: [ + { at: 0.35, target: 'Dashboards' }, + { at: 0.75, target: 'Sales Dashboard' }, + ], + response: + "Building a Sales Performance dashboard. I'll include revenue by month, new subscriptions, churn rate, and a distribution chart. Navigating to dashboards now... Opening the Sales Dashboard — your metrics are ready.", + }, + { + icon: ( + + + + + ), + label: 'Set up a workflow', + navSteps: [{ at: 0.45, target: 'Create company when adding a new person' }], + response: + 'Creating an automation: when a new person is added, automatically create their company record and link them. Setting the trigger and actions now... Workflow is active and ready to run.', + }, +]; diff --git a/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/use-product-visual-autoplay.ts b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/use-product-visual-autoplay.ts new file mode 100644 index 00000000000..3b2001e0e5b --- /dev/null +++ b/packages/twenty-website-new/src/sections/Hero/components/ProductVisual/use-product-visual-autoplay.ts @@ -0,0 +1,118 @@ +import { useCallback, useEffect, useState } from 'react'; + +import type { AppPreviewConfig } from '@/sections/AppPreview'; +import { useAppPreviewState } from '@/sections/AppPreview/Shell/use-app-preview-state'; + +import { + NEW_COMPANY_ROW, + NEW_PERSON_ROW, + PROMPT_OPTIONS, +} from './product-visual.data'; + +export function useProductVisualAutoplay(visual: AppPreviewConfig) { + const [selectedOption, setSelectedOption] = useState(0); + const [streamedText, setStreamedText] = useState(''); + const [streamComplete, setStreamComplete] = useState(false); + const [companyAdded, setCompanyAdded] = useState(false); + const [personAdded, setPersonAdded] = useState(false); + + const { + activeItem, + activeLabel, + activePage, + handleSelectLabel, + handleToggleFolder, + highlightedItemId, + openFolderIds, + revealedObjectIds, + workspaceNav, + } = useAppPreviewState(visual); + + let displayPage = activePage; + if ( + activePage !== null && + activePage !== undefined && + activePage.type === 'table' + ) { + const title = activePage.header?.title; + if (companyAdded && title === 'All Companies') { + displayPage = { + ...activePage, + header: { + ...activePage.header, + count: (activePage.header.count ?? 0) + 1, + }, + rows: [NEW_COMPANY_ROW, ...activePage.rows], + }; + } else if (personAdded && title === 'All People') { + displayPage = { + ...activePage, + header: { + ...activePage.header, + count: (activePage.header.count ?? 0) + 1, + }, + rows: [NEW_PERSON_ROW, ...activePage.rows], + }; + } + } + + useEffect(() => { + const option = PROMPT_OPTIONS[selectedOption]; + const fullText = option.response; + let index = 0; + const completedSteps = new Set(); + let companyInjected = false; + let personInjected = false; + setStreamedText(''); + setStreamComplete(false); + setCompanyAdded(false); + setPersonAdded(false); + const interval = setInterval(() => { + index += 1; + setStreamedText(fullText.slice(0, index)); + const progress = index / fullText.length; + option.navSteps.forEach((step, stepIndex) => { + if (!completedSteps.has(stepIndex) && progress >= step.at) { + completedSteps.add(stepIndex); + handleSelectLabel(step.target); + } + }); + if (selectedOption === 0) { + if (!companyInjected && progress >= 0.2) { + companyInjected = true; + setCompanyAdded(true); + } + if (!personInjected && progress >= 0.6) { + personInjected = true; + setPersonAdded(true); + } + } + if (index >= fullText.length) { + clearInterval(interval); + setStreamComplete(true); + } + }, 20); + return () => clearInterval(interval); + }, [selectedOption, handleSelectLabel]); + + const handleOptionSelect = useCallback( + (optionIndex: number) => setSelectedOption(optionIndex), + [], + ); + + return { + activeItem, + activeLabel, + displayPage, + handleOptionSelect, + handleSelectLabel, + handleToggleFolder, + highlightedItemId, + openFolderIds, + revealedObjectIds, + selectedOption, + streamComplete, + streamedText, + workspaceNav, + }; +} diff --git a/packages/twenty-website-new/src/sections/Hero/components/index.ts b/packages/twenty-website-new/src/sections/Hero/components/index.ts index a65c978fb5f..e90d200c8b2 100644 --- a/packages/twenty-website-new/src/sections/Hero/components/index.ts +++ b/packages/twenty-website-new/src/sections/Hero/components/index.ts @@ -3,7 +3,7 @@ import { Cta } from './Cta'; import { Heading } from './Heading'; import { AppPreview } from '@/sections/AppPreview'; import { PartnerVisual } from './PartnerVisual/PartnerVisual'; -import { ProductVisual } from './ProductVisual'; +import { ProductVisual } from './ProductVisual/ProductVisual'; import { ReleaseNotesVisual } from './ReleaseNotesVisual'; import { Root } from './Root'; import { WhyTwentyVisual } from './WhyTwentyVisual';