Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 0 additions & 32 deletions apps/sim/app/(landing)/components/cta/components/cta-chat.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -913,11 +913,16 @@ export function HeroVisual() {
return () => cancelAnimationFrame(raf)
}, [loaderPainting, paintFrame])

const phaseRef = useRef<Phase>(phase)
useEffect(() => {
const onResize = () => positionCursor(phase, true)
phaseRef.current = phase
}, [phase])

useEffect(() => {
const onResize = () => positionCursor(phaseRef.current, true)
window.addEventListener('resize', onResize)
return () => window.removeEventListener('resize', onResize)
}, [phase, positionCursor])
}, [positionCursor])

useEffect(
() => () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
'use client'

import { useEffect, useRef, useState } from 'react'
import dynamic from 'next/dynamic'
import type { SidebarView } from '@/app/(landing)/components/landing-preview/components/landing-preview-sidebar/landing-preview-sidebar'

/** Dimension-stable placeholder sized to the preview's exact footprint (zero CLS). */
const PLACEHOLDER_CLASS = 'aspect-[1116/615] w-full rounded bg-[var(--surface-1)]'

/**
* Load the preview chunk a little before it scrolls into view so it's ready by
* the time the user reaches it, without paying for it on initial load.
*/
const PRELOAD_ROOT_MARGIN = '400px'

/**
* Client mount for the {@link LandingPreview} - the heavy, animated workspace
* island (framer-motion + reactflow). Isolated here so the sections that show it
* stay Server Components: only this leaf is `'use client'`.
*
* Loaded with `ssr: false` so the framer-motion/reactflow bundle never ships in
* the server-rendered HTML, and behind a dimension-stable placeholder sized to
* the preview's exact `aspect-[1116/615]` footprint so there is zero layout
* shift while it streams in. The placeholder fills with the canvas surface
* (`--surface-1`) so there is no flash as the island mounts.
* the server-rendered HTML, and **gated on viewport proximity**: the chunk only
* downloads once an {@link IntersectionObserver} reports the mount is near the
* viewport, so the below-the-fold previews don't pull the heavy bundle into the
* initial homepage load. A dimension-stable placeholder (the preview's exact
* `aspect-[1116/615]` footprint, filled with the canvas surface) holds the space
* before and during load, so there is zero layout shift or flash.
*/
const LandingPreview = dynamic(
() =>
Expand All @@ -21,7 +33,7 @@ const LandingPreview = dynamic(
),
{
ssr: false,
loading: () => <div className='aspect-[1116/615] w-full rounded bg-[var(--surface-1)]' />,
loading: () => <div className={PLACEHOLDER_CLASS} />,
}
)

Expand All @@ -35,5 +47,36 @@ interface LandingPreviewMountProps {
}

export function LandingPreviewMount({ autoplay, view, workflowId }: LandingPreviewMountProps) {
return <LandingPreview autoplay={autoplay} view={view} workflowId={workflowId} />
const ref = useRef<HTMLDivElement>(null)
const [inView, setInView] = useState(false)

useEffect(() => {
if (inView) return
// Graceful degradation: without IntersectionObserver support, load eagerly
// rather than leave the preview stuck on its placeholder.
if (typeof IntersectionObserver === 'undefined') {
setInView(true)
return
}
const el = ref.current
if (!el) return
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) setInView(true)
},
{ rootMargin: PRELOAD_ROOT_MARGIN }
)
observer.observe(el)
return () => observer.disconnect()
}, [inView])
Comment thread
waleedlatif1 marked this conversation as resolved.

return (
<div ref={ref}>
{inView ? (
<LandingPreview autoplay={autoplay} view={view} workflowId={workflowId} />
) : (
<div className={PLACEHOLDER_CLASS} />
)}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ComponentType, SVGProps } from 'react'
import { memo } from 'react'
import Link from 'next/link'
import type { Integration } from '@/lib/integrations'
import { ChevronArrow } from '@/app/(landing)/components/chevron-arrow'
Expand Down Expand Up @@ -46,7 +47,10 @@ export function IntegrationCard({ integration, IconComponent }: IntegrationItemP
* Integration list row - matches blog remaining post pattern.
* Each row followed by an h-px divider.
*/
export function IntegrationRow({ integration, IconComponent }: IntegrationItemProps) {
export const IntegrationRow = memo(function IntegrationRow({
integration,
IconComponent,
}: IntegrationItemProps) {
const { slug, name, description, bgColor } = integration

return (
Expand Down Expand Up @@ -80,4 +84,4 @@ export function IntegrationRow({ integration, IconComponent }: IntegrationItemPr
<div className='h-px w-full bg-[var(--border)]' />
</>
)
}
})
54 changes: 34 additions & 20 deletions apps/sim/app/(landing)/integrations/components/integration-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { useMemo } from 'react'
import { ChipInput, Search } from '@sim/emcn'
import { debounce, useQueryStates } from 'nuqs'
import { blockTypeToIconMap, formatIntegrationType, type Integration } from '@/lib/integrations'
Expand Down Expand Up @@ -28,29 +29,42 @@ export function IntegrationGrid({ integrations }: IntegrationGridProps) {
)
const activeCategory = category || null

const counts = new Map<string, number>()
for (const i of integrations) {
if (i.integrationType) {
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
// Category facets and a per-integration lowercased search index, derived once
// from the (stable) integration list instead of rebuilt on every keystroke.
// The index keeps each searchable field as its own entry so matching stays
// identical to a per-field `includes` (no cross-field boundary matches).
const { availableCategories, searchIndex } = useMemo(() => {
const counts = new Map<string, number>()
const searchIndex = new Map<string, string[]>()
for (const i of integrations) {
if (i.integrationType) {
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
}
searchIndex.set(i.type, [
i.name.toLowerCase(),
i.description.toLowerCase(),
...i.operations.flatMap((op) => [op.name.toLowerCase(), op.description.toLowerCase()]),
...i.triggers.map((t) => t.name.toLowerCase()),
])
}
}
const availableCategories = Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.map(([key]) => key)
return {
availableCategories: Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.map(([key]) => key),
searchIndex,
}
}, [integrations])

const q = query.trim().toLowerCase()
const filtered = integrations.filter((i) => {
if (activeCategory && i.integrationType !== activeCategory) return false
if (!q) return true
return (
i.name.toLowerCase().includes(q) ||
i.description.toLowerCase().includes(q) ||
i.operations.some(
(op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
) ||
i.triggers.some((t) => t.name.toLowerCase().includes(q))
)
})
const filtered = useMemo(
() =>
integrations.filter((i) => {
if (activeCategory && i.integrationType !== activeCategory) return false
if (!q) return true
return searchIndex.get(i.type)?.some((field) => field.includes(q)) ?? false
}),
[integrations, searchIndex, q, activeCategory]
)

return (
<div>
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed apps/sim/public/landing/sim-mothership.webp
Binary file not shown.
Binary file removed apps/sim/public/static/mothership.gif
Binary file not shown.
Loading