From c7fb868c604af74e9d36dec2ae8a7cf620b98fd4 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 17:03:53 -0700 Subject: [PATCH 1/8] fix(landing): update broken links, change colors --- apps/docs/components/icons.tsx | 4 +- .../content/docs/en/tools/microsoft_ad.mdx | 2 +- apps/docs/content/docs/en/tools/okta.mdx | 3 +- .../components/enterprise/enterprise.tsx | 2 +- .../app/(home)/components/pricing/pricing.tsx | 2 +- .../integrations/data/icon-mapping.ts | 4 + .../integrations/data/integrations.json | 106 ++++++++++++++++++ apps/sim/components/icons.tsx | 4 +- 8 files changed, 119 insertions(+), 8 deletions(-) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 440333adea..e0d75ee00f 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4140,10 +4140,10 @@ export function IncidentioIcon(props: SVGProps) { export function InfisicalIcon(props: SVGProps) { return ( - + ) diff --git a/apps/docs/content/docs/en/tools/microsoft_ad.mdx b/apps/docs/content/docs/en/tools/microsoft_ad.mdx index 10095e0888..3f16dfd31a 100644 --- a/apps/docs/content/docs/en/tools/microsoft_ad.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_ad.mdx @@ -5,7 +5,7 @@ description: Manage users and groups in Azure AD (Microsoft Entra ID) import { BlockInfoCard } from "@/components/ui/block-info-card" - diff --git a/apps/docs/content/docs/en/tools/okta.mdx b/apps/docs/content/docs/en/tools/okta.mdx index 09b2f4ec0a..04eaa51532 100644 --- a/apps/docs/content/docs/en/tools/okta.mdx +++ b/apps/docs/content/docs/en/tools/okta.mdx @@ -5,7 +5,7 @@ description: Manage users and groups in Okta import { BlockInfoCard } from "@/components/ui/block-info-card" - @@ -29,6 +29,7 @@ In Sim, the Okta integration enables your agents to automate identity management If you encounter issues with the Okta integration, contact us at [help@sim.ai](mailto:help@sim.ai) {/* MANUAL-CONTENT-END */} + ## Usage Instructions Integrate Okta identity management into your workflow. List, create, update, activate, suspend, and delete users. Reset passwords. Manage groups and group membership. diff --git a/apps/sim/app/(home)/components/enterprise/enterprise.tsx b/apps/sim/app/(home)/components/enterprise/enterprise.tsx index 17c36b9b17..feef2f837f 100644 --- a/apps/sim/app/(home)/components/enterprise/enterprise.tsx +++ b/apps/sim/app/(home)/components/enterprise/enterprise.tsx @@ -612,7 +612,7 @@ export default function Enterprise() { Ready for growth?

Book a demo diff --git a/apps/sim/app/(home)/components/pricing/pricing.tsx b/apps/sim/app/(home)/components/pricing/pricing.tsx index cc803a4af3..b827161b39 100644 --- a/apps/sim/app/(home)/components/pricing/pricing.tsx +++ b/apps/sim/app/(home)/components/pricing/pricing.tsx @@ -78,7 +78,7 @@ const PRICING_TIERS: PricingTier[] = [ 'SSO & SCIM · SOC2 & HIPAA', 'Self hosting · Dedicated support', ], - cta: { label: 'Book a demo', href: '/contact' }, + cta: { label: 'Book a demo', href: 'https://form.typeform.com/to/jqCO12pF' }, }, ] diff --git a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts index 68518b87b0..056e06c9f9 100644 --- a/apps/sim/app/(landing)/integrations/data/icon-mapping.ts +++ b/apps/sim/app/(landing)/integrations/data/icon-mapping.ts @@ -16,6 +16,7 @@ import { AsanaIcon, AshbyIcon, AttioIcon, + AzureIcon, BoxCompanyIcon, BrainIcon, BrandfetchIcon, @@ -81,6 +82,7 @@ import { HunterIOIcon, ImageIcon, IncidentioIcon, + InfisicalIcon, IntercomIcon, JinaAIIcon, JiraIcon, @@ -252,6 +254,7 @@ export const blockTypeToIconMap: Record = { image_generator: ImageIcon, imap: MailServerIcon, incidentio: IncidentioIcon, + infisical: InfisicalIcon, intercom_v2: IntercomIcon, jina: JinaAIIcon, jira: JiraIcon, @@ -269,6 +272,7 @@ export const blockTypeToIconMap: Record = { mailgun: MailgunIcon, mem0: Mem0Icon, memory: BrainIcon, + microsoft_ad: AzureIcon, microsoft_dataverse: MicrosoftDataverseIcon, microsoft_excel_v2: MicrosoftExcelIcon, microsoft_planner: MicrosoftPlannerIcon, diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index cf510fd1be..11fc05bce5 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -1122,6 +1122,75 @@ "authType": "none", "category": "tools" }, + { + "type": "microsoft_ad", + "slug": "azure-ad", + "name": "Azure AD", + "description": "Manage users and groups in Azure AD (Microsoft Entra ID)", + "longDescription": "Integrate Azure Active Directory into your workflows. List, create, update, and delete users and groups. Manage group memberships programmatically.", + "bgColor": "#0078D4", + "iconName": "AzureIcon", + "docsUrl": "https://docs.sim.ai/tools/microsoft_ad", + "operations": [ + { + "name": "List Users", + "description": "List users in Azure AD (Microsoft Entra ID)" + }, + { + "name": "Get User", + "description": "Get a user by ID or user principal name from Azure AD" + }, + { + "name": "Create User", + "description": "Create a new user in Azure AD (Microsoft Entra ID)" + }, + { + "name": "Update User", + "description": "Update user properties in Azure AD (Microsoft Entra ID)" + }, + { + "name": "Delete User", + "description": "Delete a user from Azure AD (Microsoft Entra ID). The user is moved to a temporary container and can be restored within 30 days." + }, + { + "name": "List Groups", + "description": "List groups in Azure AD (Microsoft Entra ID)" + }, + { + "name": "Get Group", + "description": "Get a group by ID from Azure AD (Microsoft Entra ID)" + }, + { + "name": "Create Group", + "description": "Create a new group in Azure AD (Microsoft Entra ID)" + }, + { + "name": "Update Group", + "description": "Update group properties in Azure AD (Microsoft Entra ID)" + }, + { + "name": "Delete Group", + "description": "Delete a group from Azure AD (Microsoft Entra ID). Microsoft 365 and security groups can be restored within 30 days." + }, + { + "name": "List Group Members", + "description": "List members of a group in Azure AD (Microsoft Entra ID)" + }, + { + "name": "Add Group Member", + "description": "Add a member to a group in Azure AD (Microsoft Entra ID)" + }, + { + "name": "Remove Group Member", + "description": "Remove a member from a group in Azure AD (Microsoft Entra ID)" + } + ], + "operationCount": 13, + "triggers": [], + "triggerCount": 0, + "authType": "oauth", + "category": "tools" + }, { "type": "box", "slug": "box", @@ -5267,6 +5336,43 @@ "authType": "api-key", "category": "tools" }, + { + "type": "infisical", + "slug": "infisical", + "name": "Infisical", + "description": "Manage secrets with Infisical", + "longDescription": "Integrate Infisical into your workflow. List, get, create, update, and delete secrets across project environments.", + "bgColor": "#F7FE62", + "iconName": "InfisicalIcon", + "docsUrl": "https://docs.sim.ai/tools/infisical", + "operations": [ + { + "name": "List Secrets", + "description": "List all secrets in a project environment. Returns secret keys, values, comments, tags, and metadata." + }, + { + "name": "Get Secret", + "description": "Retrieve a single secret by name from a project environment." + }, + { + "name": "Create Secret", + "description": "Create a new secret in a project environment." + }, + { + "name": "Update Secret", + "description": "Update an existing secret in a project environment." + }, + { + "name": "Delete Secret", + "description": "Delete a secret from a project environment." + } + ], + "operationCount": 5, + "triggers": [], + "triggerCount": 0, + "authType": "api-key", + "category": "tools" + }, { "type": "intercom_v2", "slug": "intercom", diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 440333adea..e0d75ee00f 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -4140,10 +4140,10 @@ export function IncidentioIcon(props: SVGProps) { export function InfisicalIcon(props: SVGProps) { return ( - + ) From cbd7f08fabb00a9eded867c370e3720f1aba6580 Mon Sep 17 00:00:00 2001 From: waleed Date: Thu, 19 Mar 2026 18:07:37 -0700 Subject: [PATCH 2/8] update integration pages --- apps/docs/components/icons.tsx | 2 +- apps/sim/app/(home)/landing.tsx | 9 +- apps/sim/app/(landing)/blog/layout.tsx | 6 +- .../app/(landing)/components/legal-layout.tsx | 7 +- .../(landing)/integrations/[slug]/page.tsx | 307 +++++++++--------- .../components/integration-icon.tsx | 8 +- .../components/request-integration-modal.tsx | 179 ++++++++++ .../integrations/data/integrations.json | 144 ++++---- .../app/(landing)/integrations/data/utils.ts | 15 - .../sim/app/(landing)/integrations/layout.tsx | 6 +- apps/sim/app/(landing)/integrations/page.tsx | 22 +- .../app/api/help/integration-request/route.ts | 81 +++++ apps/sim/app/changelog/layout.tsx | 6 +- apps/sim/app/not-found.tsx | 6 +- apps/sim/components/icons.tsx | 2 +- apps/sim/lib/blog/registry.ts | 15 + apps/sim/public/brandbook/logo/large.png | Bin 0 -> 725281 bytes apps/sim/public/brandbook/logo/md.png | Bin 0 -> 218027 bytes apps/sim/public/brandbook/logo/small.png | Bin 0 -> 52084 bytes 19 files changed, 523 insertions(+), 292 deletions(-) create mode 100644 apps/sim/app/(landing)/integrations/components/request-integration-modal.tsx delete mode 100644 apps/sim/app/(landing)/integrations/data/utils.ts create mode 100644 apps/sim/app/api/help/integration-request/route.ts create mode 100644 apps/sim/public/brandbook/logo/large.png create mode 100644 apps/sim/public/brandbook/logo/md.png create mode 100644 apps/sim/public/brandbook/logo/small.png diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index e0d75ee00f..da4f45cf13 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -4253,7 +4253,7 @@ export function ZoomIcon(props: SVGProps) { fill='currentColor' width='800px' height='800px' - viewBox='0 0 32 32' + viewBox='-1 8 34 16' version='1.1' xmlns='http://www.w3.org/2000/svg' > diff --git a/apps/sim/app/(home)/landing.tsx b/apps/sim/app/(home)/landing.tsx index dc676efa4f..79689dd57a 100644 --- a/apps/sim/app/(home)/landing.tsx +++ b/apps/sim/app/(home)/landing.tsx @@ -1,4 +1,4 @@ -import { getAllPostMeta } from '@/lib/blog/registry' +import { getNavBlogPosts } from '@/lib/blog/registry' import { martianMono } from '@/app/_styles/fonts/martian-mono/martian-mono' import { season } from '@/app/_styles/fonts/season/season' import { @@ -33,12 +33,7 @@ import { * enterprise (Enterprise) -> pricing (Pricing) -> testimonials (Testimonials). */ export default async function Landing() { - const allPosts = await getAllPostMeta() - const featuredPost = allPosts.find((p) => p.featured) ?? allPosts[0] - const recentPosts = allPosts.filter((p) => p !== featuredPost).slice(0, 4) - const blogPosts = [featuredPost, ...recentPosts] - .filter(Boolean) - .map((p) => ({ slug: p.slug, title: p.title, ogImage: p.ogImage })) + const blogPosts = await getNavBlogPosts() return (
diff --git a/apps/sim/app/(landing)/blog/layout.tsx b/apps/sim/app/(landing)/blog/layout.tsx index 0387f7ea60..59f7adaf70 100644 --- a/apps/sim/app/(landing)/blog/layout.tsx +++ b/apps/sim/app/(landing)/blog/layout.tsx @@ -1,7 +1,9 @@ +import { getNavBlogPosts } from '@/lib/blog/registry' import Footer from '@/app/(home)/components/footer/footer' import Navbar from '@/app/(home)/components/navbar/navbar' -export default function StudioLayout({ children }: { children: React.ReactNode }) { +export default async function StudioLayout({ children }: { children: React.ReactNode }) { + const blogPosts = await getNavBlogPosts() const orgJsonLd = { '@context': 'https://schema.org', '@type': 'Organization', @@ -34,7 +36,7 @@ export default function StudioLayout({ children }: { children: React.ReactNode } dangerouslySetInnerHTML={{ __html: JSON.stringify(websiteJsonLd) }} />
- +
{children}
diff --git a/apps/sim/app/(landing)/components/legal-layout.tsx b/apps/sim/app/(landing)/components/legal-layout.tsx index 2e115c0a28..552bb5dd9e 100644 --- a/apps/sim/app/(landing)/components/legal-layout.tsx +++ b/apps/sim/app/(landing)/components/legal-layout.tsx @@ -1,3 +1,4 @@ +import { getNavBlogPosts } from '@/lib/blog/registry' import { isHosted } from '@/lib/core/config/feature-flags' import Footer from '@/app/(home)/components/footer/footer' import Navbar from '@/app/(home)/components/navbar/navbar' @@ -7,11 +8,13 @@ interface LegalLayoutProps { children: React.ReactNode } -export default function LegalLayout({ title, children }: LegalLayoutProps) { +export default async function LegalLayout({ title, children }: LegalLayoutProps) { + const blogPosts = await getNavBlogPosts() + return (
- +
diff --git a/apps/sim/app/(landing)/integrations/[slug]/page.tsx b/apps/sim/app/(landing)/integrations/[slug]/page.tsx index 693e0fee68..743fc2f006 100644 --- a/apps/sim/app/(landing)/integrations/[slug]/page.tsx +++ b/apps/sim/app/(landing)/integrations/[slug]/page.tsx @@ -5,7 +5,6 @@ import { TEMPLATES } from '@/app/workspace/[workspaceId]/home/components/templat import { IntegrationIcon } from '../components/integration-icon' import { blockTypeToIconMap } from '../data/icon-mapping' import integrations from '../data/integrations.json' -import { POPULAR_WORKFLOWS } from '../data/popular-workflows' import type { AuthType, FAQItem, Integration } from '../data/types' import { IntegrationFAQ } from './components/integration-faq' import { TemplateCardButton } from './components/template-card-button' @@ -14,44 +13,52 @@ const allIntegrations = integrations as Integration[] const INTEGRATION_COUNT = allIntegrations.length /** Fast O(1) lookups — avoids repeated linear scans inside render loops. */ -const byName = new Map(allIntegrations.map((i) => [i.name, i])) const bySlug = new Map(allIntegrations.map((i) => [i.slug, i])) const byType = new Map(allIntegrations.map((i) => [i.type, i])) -/** Returns workflow pairs that feature the given integration on either side. */ -function getPairsFor(name: string) { - return POPULAR_WORKFLOWS.filter((p) => p.from === name || p.to === name) -} - /** * Returns up to `limit` related integration slugs. * - * Scoring: - * +100 — integration appears as a workflow pair partner (explicit editorial signal) - * +N — N operation names shared with the current integration (semantic similarity) + * Scoring (additive): + * +3 per shared operation name — strongest signal (same capability) + * +2 per shared operation word — weaker signal (e.g. both have "create" ops) + * +1 same auth type — comparable setup experience * - * This means genuine partners always rank first; operation-similar integrations - * (e.g. Slack → Teams → Discord for "Send Message") fill the rest organically. + * Every integration gets a score, so the sidebar always has suggestions. + * Ties are broken by alphabetical slug order for determinism. */ function getRelatedSlugs( - name: string, slug: string, operations: Integration['operations'], + authType: AuthType, limit = 6 ): string[] { - const partners = new Set(getPairsFor(name).map((p) => (p.from === name ? p.to : p.from))) - const currentOps = new Set(operations.map((o) => o.name.toLowerCase())) + const currentOpNames = new Set(operations.map((o) => o.name.toLowerCase())) + const currentOpWords = new Set( + operations.flatMap((o) => + o.name + .toLowerCase() + .split(/\s+/) + .filter((w) => w.length > 3) + ) + ) return allIntegrations .filter((i) => i.slug !== slug) - .map((i) => ({ - slug: i.slug, - score: - (partners.has(i.name) ? 100 : 0) + - i.operations.filter((o) => currentOps.has(o.name.toLowerCase())).length, - })) - .filter(({ score }) => score > 0) - .sort((a, b) => b.score - a.score) + .map((i) => { + const sharedNames = i.operations.filter((o) => + currentOpNames.has(o.name.toLowerCase()) + ).length + const sharedWords = i.operations.filter((o) => + o.name + .toLowerCase() + .split(/\s+/) + .some((w) => w.length > 3 && currentOpWords.has(w)) + ).length + const sameAuth = i.authType === authType ? 1 : 0 + return { slug: i.slug, score: sharedNames * 3 + sharedWords * 2 + sameAuth } + }) + .sort((a, b) => b.score - a.score || a.slug.localeCompare(b.slug)) .slice(0, limit) .map(({ slug: s }) => s) } @@ -70,7 +77,6 @@ function buildFAQs(integration: Integration): FAQItem[] { const { name, description, operations, triggers, authType } = integration const topOps = operations.slice(0, 5) const topOpNames = topOps.map((o) => o.name) - const pairs = getPairsFor(name) const authStep = AUTH_STEP[authType] const faqs: FAQItem[] = [ @@ -89,6 +95,10 @@ function buildFAQs(integration: Integration): FAQItem[] { question: `How do I connect ${name} to Sim?`, answer: `Getting started takes under five minutes: (1) Create a free account at sim.ai. (2) Open a new workflow. (3) Drag a ${name} block onto the canvas. (4) ${authStep} (5) Choose the tool you want to use, wire it to the inputs you need, and click Run. Your automation is live.`, }, + { + question: `Can I use ${name} as a tool inside an AI agent in Sim?`, + answer: `Yes — this is one of Sim's core capabilities. Instead of hard-coding when and how ${name} is used, you give an AI agent access to ${name} tools and describe the goal in plain language. The agent decides which tools to call, in what order, and how to handle the results. This means your automation adapts to context rather than breaking when inputs change.`, + }, ...(topOpNames.length >= 2 ? [ { @@ -97,19 +107,15 @@ function buildFAQs(integration: Integration): FAQItem[] { }, ] : []), - ...(pairs.length > 0 + ...(triggers.length > 0 ? [ { - question: `Can I connect ${name} to ${pairs[0].from === name ? pairs[0].to : pairs[0].from} with Sim?`, - answer: `Yes. ${pairs[0].description} In Sim, you set this up by adding both a ${name} block and a ${pairs[0].from === name ? pairs[0].to : pairs[0].from} block to the same workflow and connecting them through an AI agent that orchestrates the logic between them.`, + question: `How do I trigger a Sim workflow from ${name} automatically?`, + answer: `In your Sim workflow, switch the ${name} block to Trigger mode and copy the generated webhook URL. Paste that URL into ${name}'s webhook settings and select the events you want to listen for (${triggers.map((t) => t.name).join(', ')}). From that point on, every matching event in ${name} instantly fires your workflow — no polling, no delay.`, }, - ] - : []), - ...(triggers.length > 0 - ? [ { - question: `Can ${name} trigger a Sim workflow automatically?`, - answer: `Yes. ${name} supports ${triggers.length} webhook trigger${triggers.length === 1 ? '' : 's'} that can instantly start a Sim workflow: ${triggers.map((t) => t.name).join(', ')}. No polling needed — the workflow fires the moment the event occurs in ${name}.`, + question: `What data does Sim receive when a ${name} event triggers a workflow?`, + answer: `When ${name} fires a webhook, Sim receives the full event payload that ${name} sends — typically the record or object that changed, along with metadata like the event type and timestamp. Inside your workflow, every field from that payload is available as a variable you can pass to AI agents, conditions, or other integrations.`, }, ] : []), @@ -190,11 +196,10 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl const IconComponent = blockTypeToIconMap[integration.type] const faqs = buildFAQs(integration) - const relatedSlugs = getRelatedSlugs(name, slug, operations) + const relatedSlugs = getRelatedSlugs(slug, operations, authType) const relatedIntegrations = relatedSlugs .map((s) => bySlug.get(s)) .filter((i): i is Integration => i !== undefined) - const featuredPairs = getPairsFor(name) const baseType = integration.type.replace(/_v\d+$/, '') const matchingTemplates = TEMPLATES.filter( (t) => @@ -420,15 +425,18 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl {triggers.length > 0 && (

- Triggers + Real-time triggers

-

- These events in {name} can automatically start a Sim workflow — no polling - required. +

+ Connect a {name} webhook to Sim and your workflow fires the instant an event + happens — no polling, no delay. Sim receives the full event payload and makes + every field available as a variable inside your workflow.

+ + {/* Event cards */}
    {triggers.map((trigger) => (
  • - Trigger + Event

{trigger.name}

@@ -462,73 +470,6 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl )} - {/* Popular workflows featuring this integration */} - {featuredPairs.length > 0 && ( -
-

- Popular workflows with {name} -

-

- Common automation patterns teams build on Sim using {name}. -

-
    - {featuredPairs.map(({ from, to, headline, description: desc }) => { - const fromInt = byName.get(from) - const toInt = byName.get(to) - const FromIcon = fromInt ? blockTypeToIconMap[fromInt.type] : undefined - const ToIcon = toInt ? blockTypeToIconMap[toInt.type] : undefined - const fromBg = fromInt?.bgColor ?? '#6B7280' - const toBg = toInt?.bgColor ?? '#6B7280' - - return ( -
  • -
    -
    - - {FromIcon && ( - - - - {ToIcon && ( - -
    -

    {headline}

    -

    {desc}

    -
    -
  • - ) - })} -
-
- )} - {/* Workflow templates */} {matchingTemplates.length > 0 && (
@@ -539,7 +480,7 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl Ready-to-use workflows featuring {name}. Click any to build it instantly.

    {matchingTemplates.map((template) => { @@ -551,34 +492,49 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl return (
  • - {/* Integration icons */} -
    - {allTypes.map((bt) => { - const int = byType.get(bt) + {/* Integration pills row */} +
    + {allTypes.map((bt, idx) => { + // Templates may use unversioned keys (e.g. "notion") while the + // icon map has versioned keys ("notion_v2") — fall back to _v2. + const resolvedBt = byType.get(bt) + ? bt + : byType.get(`${bt}_v2`) + ? `${bt}_v2` + : bt + const int = byType.get(resolvedBt) const intName = int?.name ?? bt return ( - + + {idx > 0 && ( + + )} + + + ) })}
    -

    +

    {template.title}

    - +

    Try this workflow → - +

  • ) @@ -660,40 +616,34 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl
    Free to start
- - Get started free - - - - {/* Docs */} -
-

Documentation

-

- Full API reference, authentication setup, and usage examples for {name}. -

- - docs.sim.ai - - + Get started free + + + View docs + + +
{/* Related integrations — internal linking for SEO */} @@ -738,6 +688,43 @@ export default async function IntegrationPage({ params }: { params: Promise<{ sl aria-labelledby='cta-heading' className='mt-20 rounded-xl border border-[#2A2A2A] bg-[#242424] p-8 text-center sm:p-12' > + {/* Logo pair: Sim × Integration */} +
+ Sim +
+
+

{ bgColor: string @@ -33,9 +32,6 @@ export function IntegrationIcon({ as: Tag = 'div', ...rest }: IntegrationIconProps) { - const isLight = isLightBg(bgColor) - const fgColor = isLight ? 'text-[#1C1C1C]' : 'text-white' - return ( {Icon ? ( - + ) : ( - + {name.charAt(0)} )} diff --git a/apps/sim/app/(landing)/integrations/components/request-integration-modal.tsx b/apps/sim/app/(landing)/integrations/components/request-integration-modal.tsx new file mode 100644 index 0000000000..a5acd60fe0 --- /dev/null +++ b/apps/sim/app/(landing)/integrations/components/request-integration-modal.tsx @@ -0,0 +1,179 @@ +'use client' + +import { useCallback, useState } from 'react' +import { + Button, + Input, + Label, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, + Textarea, +} from '@/components/emcn' + +type SubmitStatus = 'idle' | 'submitting' | 'success' | 'error' + +export function RequestIntegrationModal() { + const [open, setOpen] = useState(false) + const [status, setStatus] = useState('idle') + + const [integrationName, setIntegrationName] = useState('') + const [email, setEmail] = useState('') + const [useCase, setUseCase] = useState('') + + const resetForm = useCallback(() => { + setIntegrationName('') + setEmail('') + setUseCase('') + setStatus('idle') + }, []) + + const handleOpenChange = useCallback( + (nextOpen: boolean) => { + setOpen(nextOpen) + if (!nextOpen) resetForm() + }, + [resetForm] + ) + + const handleSubmit = useCallback( + async (e: React.FormEvent) => { + e.preventDefault() + if (!integrationName.trim() || !email.trim()) return + + setStatus('submitting') + + try { + const res = await fetch('/api/help/integration-request', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + integrationName: integrationName.trim(), + email: email.trim(), + useCase: useCase.trim() || undefined, + }), + }) + + if (!res.ok) throw new Error('Request failed') + + setStatus('success') + setTimeout(() => setOpen(false), 1500) + } catch { + setStatus('error') + } + }, + [integrationName, email, useCase] + ) + + const canSubmit = integrationName.trim() && email.trim() && status === 'idle' + + return ( + <> + + + + + Request an Integration + + {status === 'success' ? ( + +
+
+ + + +
+

+ Request submitted — we'll follow up at{' '} + {email}. +

+
+
+ ) : ( +
+ +
+
+ + setIntegrationName(e.target.value)} + maxLength={200} + autoComplete='off' + required + /> +
+ +
+ + setEmail(e.target.value)} + autoComplete='email' + required + /> +
+ +
+ +