diff --git a/apps/www/app/api-v2/submit-form-contact-sales/route.tsx b/apps/www/app/api-v2/submit-form-contact-sales/route.tsx index b41b5d08117..4f2e1aed08e 100644 --- a/apps/www/app/api-v2/submit-form-contact-sales/route.tsx +++ b/apps/www/app/api-v2/submit-form-contact-sales/route.tsx @@ -3,6 +3,34 @@ const corsHeaders = { 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', } +const personalEmailDomains = [ + '@gmail.com', + '@yahoo.com', + '@hotmail.', + '@outlook.com', + '@aol.com', + '@icloud.com', + '@live.com', + '@protonmail.com', + '@mail.com', + '@example.com', +] + +const isValidEmail = (email: string): boolean => { + const emailPattern = /^[\w-\.+]+@([\w-]+\.)+[\w-]{2,8}$/ + return emailPattern.test(email) +} + +const isCompanyEmail = (email: string): boolean => { + for (const domain of personalEmailDomains) { + if (email.includes(domain)) { + return false + } + } + + return true +} + export async function POST(req: Request) { const HUBSPOT_PORTAL_ID = process.env.HUBSPOT_PORTAL_ID const HUBSPOT_FORM_GUID = process.env.HUBSPOT_ENTERPRISE_FORM_GUID @@ -13,7 +41,23 @@ export async function POST(req: Request) { if (!firstName || !secondName || !companyEmail || !message) { return new Response(JSON.stringify({ message: 'All fields are required' }), { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, + status: 422, + }) + } + + // Validate email + if (companyEmail && !isValidEmail(companyEmail)) { + return new Response(JSON.stringify({ message: 'Invalid email address' }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 422, + }) + } + + // Validate company email + if (companyEmail && !isCompanyEmail(companyEmail)) { + return new Response(JSON.stringify({ message: 'Please use a company email address' }), { + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + status: 422, }) } diff --git a/apps/www/components/Enterprise/CTAForm.tsx b/apps/www/components/Enterprise/CTAForm.tsx new file mode 100644 index 00000000000..c4bc02edc18 --- /dev/null +++ b/apps/www/components/Enterprise/CTAForm.tsx @@ -0,0 +1,92 @@ +import React, { FC } from 'react' +import SectionContainer from '~/components/Layouts/SectionContainer' + +import { CustomerLogo, CustomerQuote } from '../../pages/contact/sales' +import EnterpriseFormQuotes from '../EnterpriseFormQuotes' +import RequestADemoForm from '../Forms/RequestADemoForm' + +interface Props {} + +const UseCases: FC = (props) => { + return ( + +
+
+

Request a demo

+

+ We can take your requirements and show you how Supabase can help you achieve your goals. +

+
+ , + panel: ( + + ), + }, + { + label: , + panel: ( + + ), + }, + { + label: , + panel: ( + + ), + }, + ]} + /> +
+ + , + panel: ( + + ), + }, + { + label: , + panel: ( + + ), + }, + { + label: , + panel: ( + + ), + }, + ]} + /> +
+ ) +} + +export default UseCases diff --git a/apps/www/components/Enterprise/Performance.tsx b/apps/www/components/Enterprise/Performance.tsx new file mode 100644 index 00000000000..73a32399be8 --- /dev/null +++ b/apps/www/components/Enterprise/Performance.tsx @@ -0,0 +1,111 @@ +import React, { FC } from 'react' +import { cn, Badge } from 'ui' + +import SectionContainer from '~/components/Layouts/SectionContainer' + +interface Props { + id: string + heading: string | JSX.Element + subheading: string | JSX.Element + highlights: Highlight[] +} + +type Highlight = { + heading: string + subheading: string +} + +const Performance: FC = (props) => { + return ( + +
+
+ {/* {props.label} */} +

{props.heading}

+

{props.subheading}

+
+
+ {props.highlights.map((highlight) => ( + + ))} +
+
+
+ + + + + + + + + + + +
+
+ + ) +} + +const GraphLabel: FC<{ className?: string }> = ({ className }) => ( +
+
+ Users +
+ 930,550 + + +13.4% + +
+
+
+
+) + +interface HighlightItemProps { + highlight: Highlight +} + +const HighlightItem: FC = ({ highlight }) => { + return ( +
  • + {highlight.heading} +

    {highlight.subheading}

    +
  • + ) +} + +export default Performance diff --git a/apps/www/components/Enterprise/Security.tsx b/apps/www/components/Enterprise/Security.tsx new file mode 100644 index 00000000000..a885e9993db --- /dev/null +++ b/apps/www/components/Enterprise/Security.tsx @@ -0,0 +1,68 @@ +import React, { FC } from 'react' +import type { LucideIcon } from 'lucide-react' + +import { TextLink } from 'ui' +import SectionContainer from '~/components/Layouts/SectionContainer' + +interface Props { + id: string + label: string | JSX.Element + heading: string | JSX.Element + subheading: string | JSX.Element + features: FeatureProps[] + cta: { + label: string + url: string + } +} + +export type Story = { + url: string + heading: string + subheading: string | JSX.Element +} + +type FeatureProps = { + icon: LucideIcon + heading: string +} + +const EnterpriseSecurity: FC = (props) => { + return ( + +
    + {props.label} +

    {props.heading}

    +

    {props.subheading}

    + +
    +
      + {props.features.map((feature) => ( + + ))} +
    +
    + ) +} + +interface FeatureItemProps { + feature: FeatureProps +} + +const FeatureItem: FC = ({ feature }) => { + const Icon: LucideIcon = feature.icon + + return ( +
  • +
    + +
    +

    {feature.heading}

    +
  • + ) +} + +export default EnterpriseSecurity diff --git a/apps/www/components/Enterprise/SingleQuote.tsx b/apps/www/components/Enterprise/SingleQuote.tsx new file mode 100644 index 00000000000..fba833cea88 --- /dev/null +++ b/apps/www/components/Enterprise/SingleQuote.tsx @@ -0,0 +1,33 @@ +import React, { FC } from 'react' + +import SectionContainer from '~/components/Layouts/SectionContainer' + +interface Props { + id: string + quote: Quote +} + +type Quote = { + text: string + author: string + logo: string | JSX.Element + role: string +} + +const SingleQuote: FC = ({ id: sectionId, quote }) => { + return ( + + {quote.text} +
    +
    {quote.logo}
    + {quote.author} + {quote.role} +
    +
    + ) +} + +export default SingleQuote diff --git a/apps/www/components/Enterprise/Support.tsx b/apps/www/components/Enterprise/Support.tsx new file mode 100644 index 00000000000..d10c11ff775 --- /dev/null +++ b/apps/www/components/Enterprise/Support.tsx @@ -0,0 +1,60 @@ +import React, { FC } from 'react' + +import { cn } from 'ui' +import SectionContainer from '~/components/Layouts/SectionContainer' + +import type { LucideIcon } from 'lucide-react' + +interface Props { + id: string + label: string | JSX.Element + heading: string | JSX.Element + features: Feature[] +} + +type Feature = { + icon: LucideIcon + heading: string + subheading: string +} + +const Support: FC = (props) => { + return ( + +
    + {props.label} +

    {props.heading}

    +
    +
      + {props.features.map((feature) => ( + + ))} +
    +
    + ) +} + +interface FeatureItemProps { + feature: Feature +} + +const FeatureItem: FC = ({ feature }) => { + const Icon: LucideIcon = feature.icon + const iconSize = 7 + const iconWidth = `w-${iconSize}` + const iconHeight = `h-${iconSize}` + + return ( +
  • + +
    + +
    +

    {feature.heading}

    +

    {feature.subheading}

    + {/* */} +
  • + ) +} + +export default Support diff --git a/apps/www/components/Enterprise/UseCases.tsx b/apps/www/components/Enterprise/UseCases.tsx new file mode 100644 index 00000000000..ec6488a85a2 --- /dev/null +++ b/apps/www/components/Enterprise/UseCases.tsx @@ -0,0 +1,141 @@ +import 'swiper/css' + +import React, { FC } from 'react' +import { Swiper, SwiperSlide } from 'swiper/react' +import Link from 'next/link' + +import { TextLink } from 'ui' +import SectionContainer from '~/components/Layouts/SectionContainer' +import Panel from '~/components/Panel' + +import type { LucideIcon } from 'lucide-react' +import Image from 'next/image' + +interface Props { + id: string + label: string | JSX.Element + heading: string | JSX.Element + stories: Story[] + highlights: Highlight[] +} + +export type Story = { + icon: string + url: string + target?: '_blank' | string + heading: string + subheading: string | JSX.Element +} + +type Highlight = { + icon: LucideIcon + heading: string + subheading: string + url: string +} + +const UseCases: FC = (props) => ( +
    + +
    + {props.label} +

    {props.heading}

    +
    +
    +
    + +
      + {props.stories.map((story) => ( +
    • + +
    • + ))} +
    +
    + + {props.stories.map((story: Story, i: number) => ( + + + + ))} + +
    +
    +
    + +
      + {props.highlights.map((highlight) => ( + + ))} +
    +
    +
    +) + +interface StoryCardProps { + story: Story +} + +const StoryCard: FC = ({ story }) => ( + + +
    + {story.heading} +

    {story.heading}

    +
    +
    + {story.subheading} +
    +
    + +) + +interface HighlightCardProps { + highlight: Highlight +} + +const HighlightCard: FC = ({ highlight }) => { + const Icon: LucideIcon = highlight.icon + + return ( +
  • + +

    {highlight.heading}

    +

    {highlight.subheading}

    + +
  • + ) +} + +export default UseCases diff --git a/apps/www/components/Forms/RequestADemoForm.tsx b/apps/www/components/Forms/RequestADemoForm.tsx index 81f59845535..f3853266a92 100644 --- a/apps/www/components/Forms/RequestADemoForm.tsx +++ b/apps/www/components/Forms/RequestADemoForm.tsx @@ -68,6 +68,29 @@ const isValidEmail = (email: string): boolean => { return emailPattern.test(email) } +const personalEmailDomains = [ + '@gmail.com', + '@yahoo.com', + '@hotmail.', + '@outlook.com', + '@aol.com', + '@icloud.com', + '@live.com', + '@protonmail.com', + '@mail.com', + '@example.com', +] + +const isCompanyEmail = (email: string): boolean => { + for (const domain of personalEmailDomains) { + if (email.includes(domain)) { + return false + } + } + + return true +} + const defaultFormValue: FormData = { firstName: '', secondName: '', @@ -111,6 +134,11 @@ const RequestADemoForm: FC = ({ className }) => { newErrors.companyEmail = 'Invalid email address' } + // Validate company email + if (formData.companyEmail && !isCompanyEmail(formData.companyEmail)) { + newErrors.companyEmail = 'Please use a company email address' + } + setErrors(newErrors) // Return validation status, also check if honeypot is filled (indicating a bot) @@ -170,7 +198,7 @@ const RequestADemoForm: FC = ({ className }) => { className )} > -
    +
    {success ? (

    {success}

    diff --git a/apps/www/components/Hero/Hero.tsx b/apps/www/components/Hero/Hero.tsx index 344ad525b3a..0d6f50a6bd9 100644 --- a/apps/www/components/Hero/Hero.tsx +++ b/apps/www/components/Hero/Hero.tsx @@ -16,7 +16,7 @@ const Hero = () => { return (
    - +
    diff --git a/apps/www/components/Sections/ProductHeader2.tsx b/apps/www/components/Sections/ProductHeader2.tsx index 57d92e32737..f4bfb35d121 100644 --- a/apps/www/components/Sections/ProductHeader2.tsx +++ b/apps/www/components/Sections/ProductHeader2.tsx @@ -1,22 +1,30 @@ import React from 'react' -import { Button } from 'ui' +import { Button, cn } from 'ui' import Link from 'next/link' import ProductIcon from '../ProductIcon' import SectionContainer from '../Layouts/SectionContainer' import { CTA } from '~/types/common' interface Props { + label?: string | React.ReactNode h1: string | React.ReactNode subheader?: string[] | React.ReactNode[] icon?: string title?: string image?: React.ReactNode footer?: React.ReactNode + footerPosition?: 'bottom' | 'left' ctas?: CTA[] + className?: string } const ProductHeader = (props: Props) => ( -
    +
    @@ -54,13 +62,18 @@ const ProductHeader = (props: Props) => ( ))}
    + {props.footer && props.footerPosition === 'left' && ( +
    + {props.footer} +
    + )}
    {props.image && (
    {props.image}
    )} - {props.footer && ( + {props.footer && props.footerPosition !== 'left' && (
    {props.footer}
    diff --git a/apps/www/components/logos.tsx b/apps/www/components/logos.tsx index b7cd6650d99..4d0a2e054c6 100644 --- a/apps/www/components/logos.tsx +++ b/apps/www/components/logos.tsx @@ -1,18 +1,27 @@ import { cn } from 'ui' import { range } from 'lodash' -const Logos: React.FC = () => { +interface Props { + showHeading?: boolean + className?: string + align?: 'left' | 'center' + maskBackgroundColor?: 'hsl(var(--background-default)' | 'hsl(var(--background-alternative)' +} + +const Logos: React.FC = ({ className, showHeading = true, align = 'center' }) => { const gap = 'gap-4 lg:gap-8' return ( -
    -
    +
    +
    @@ -29,9 +38,11 @@ const Logos: React.FC = () => { ))}
    -

    - Trusted by fast-growing companies worldwide -

    + {showHeading && ( +

    + Trusted by fast-growing companies worldwide +

    + )}
    ) } diff --git a/apps/www/data/enterprise/content.tsx b/apps/www/data/enterprise/content.tsx new file mode 100644 index 00000000000..b7738d610df --- /dev/null +++ b/apps/www/data/enterprise/content.tsx @@ -0,0 +1,291 @@ +import { + Activity, + ArrowLeftRight, + ClipboardCheck, + DollarSign, + FolderLock, + Globe2, + LayoutList, + Lightbulb, + LineChart, + Lock, + Scale, + ShieldAlert, + ShieldCheck, + Timer, + Users, + UserX, +} from 'lucide-react' +import RequestADemoForm from '../../components/Forms/RequestADemoForm' + +export default { + metadata: { + metaTitle: 'Supabase for Enterprise', + metaDescription: + 'Leading enterprises use Supabase to build faster, better, and more scalable products.', + }, + heroSection: { + id: 'hero', + title: 'Supabase for Enterprise', + h1: ( + <> + Innovative Enterprises + use Supabase + + ), + subheader: [ + <> + Leading enterprises use Supabase to build faster, better, and more scalable products. From + GitHub to PwC, innovative companies trust Supabase to drive their digital transformation + strategy. + , + ], + image: , + logos: [ + { + name: 'GitHub', + image: '/images/enterprise/github.svg', + }, + { + name: 'PwC', + image: '/images/enterprise/pwc.svg', + }, + ], + }, + 'use-cases': { + id: 'use-cases', + label: <>Build with Supabase, + heading: <>Stay on the forefront, + stories: [ + { + icon: '/images/customers/logos/light/mozilla.png', + url: 'https://developer.mozilla.org/en-US/blog/introducing-ai-help/', + target: '_blank', + heading: 'Mozilla use Supabase for GenAI and RAG', + subheading: ( + <> + We store embeddings in a PostgreSQL database, hosted by Supabase, to perform a{' '} + + similarity search to identify the most relevant sections within the MDN + + . + + ), + }, + { + icon: '/images/customers/logos/light/epsilon3.png', + url: '/customers/epsilon3', + heading: 'Epsilon3 use Supabase to build software for NASA', + subheading: ( + <> + + Billion dollar missions need to run reliably and securely + + . We use Supabase because they give us an open-source scalable back-end built by + database experts that we can self-host. + + ), + }, + { + icon: '/images/customers/logos/light/pebblely.png', + url: '/customers/pebblely', + heading: 'Pebblely use Supabase to scale to millions of users.', + subheading: ( + <> + It streamlined the database, the API, and authentication.{' '} + Everything was up and running in two days. It + is easy to get started and provides all the solutions we require as we continue to grow. + + ), + }, + ], + highlights: [ + { + icon: LineChart, + heading: '100x scale', + subheading: 'Maergo handled 100x their highest sustained traffic with Supabase.', + url: '/customers/maergo', + }, + { + icon: DollarSign, + heading: '83% cost reduction', + subheading: 'Shotgun reduced costs by 83% by migrating to Supabase.', + url: '/customers/shotgun', + }, + { + icon: Timer, + heading: '20% faster dev', + subheading: 'Voypost enjoyed a 20% faster development process.', + url: '/customers/voypost', + }, + { + icon: Scale, + heading: 'GDPR compliance', + subheading: 'Markprompt use Supabase to build GDPR-compliant AI chatbots.', + url: '/customers/markprompt', + }, + ], + }, + performance: { + id: 'performance', + heading: ( + <> + Top performance, +
    at any scale + + ), + subheading: + "Supabase ensures optimal database performance at any scale, so you can focus on innovating and growing without worrying about infrastructure limitations—whether you're handling high-traffic applications, complex queries, or massive data volumes.", + highlights: [ + { + heading: 'Databases managed', + subheading: '1,000,000+', + }, + { + heading: 'Databases launched daily', + subheading: '3,500+', + }, + ], + }, + security: { + id: 'security', + label: 'Security', + heading: <>Trusted for medical records, missions to the moon, and everything in between, + subheading: + 'Keep your data secure with SOC 2, HIPAA, and GDPR compliance. Your customers’ data is encrypted at rest and in transit, with built-in tools for monitoring and managing security threats.', + cta: { + label: 'Learn more about Security', + url: '/security', + }, + features: [ + { + icon: ShieldCheck, + heading: 'SOC 2 Type II certified', + }, + { + icon: Activity, + heading: 'HIPAA compliant', + }, + { + icon: ShieldAlert, + heading: 'DDoS Protection', + }, + { + icon: Lock, + heading: 'Multi-factor Authentication', + }, + { + icon: ClipboardCheck, + heading: 'Vulnerability Management', + }, + { + icon: Users, + heading: 'Role-based access control', + }, + { + icon: LayoutList, + heading: 'Database Audit Logs', + }, + { + icon: Lightbulb, + heading: 'Security Advisors', + }, + { + icon: FolderLock, + heading: 'Encrypted Storage', + }, + { + icon: UserX, + heading: 'Network restrictions', + }, + ], + }, + support: { + id: 'support', + label: 'Support', + heading: ( + <> + Get expert help, +
    whenever you need it + + ), + features: [ + { + icon: Globe2, + heading: 'Global Support, 24/7', + subheading: + 'Our team has 100% global coverage. No matter where you are, we’re always available to resolve issues and keep your operations running smoothly.', + }, + { + icon: Users, + heading: 'Dedicated team of experts', + subheading: + 'Get direct access to talented engineers. From onboarding to optimizations, our expert team is here to provide personalized, hands-on support whenever you need it.', + }, + { + icon: ArrowLeftRight, + heading: 'Migration & Success Support', + subheading: + 'Our team ensures a smooth transition to Supabase while guiding you with best practices for scaling. We’re dedicated to your long-term success, every step of the way.', + }, + ], + }, + quote: { + id: 'quote', + quote: { + text: 'Supabase powers prototyping for fast-moving teams such as GitHub Next.', + author: 'Idan Gazit', + logo: ( + + + + + + + + + + ), + role: 'Senior Director of Research, GitHub Next', + }, + }, + 'request-a-demo': { + id: 'request-a-demo', + heading: 'Request a demo', + subheading: + 'We can take your requirements and show you how Supabase can help you achieve your goals.', + quote: { + text: 'Supabase powers prototyping for fast-moving teams such as GitHub Next.', + author: '', + logo: '', + company: 'GitHub', + }, + }, +} diff --git a/apps/www/data/nav.tsx b/apps/www/data/nav.tsx index b02f4c01cb8..4ac31ec1373 100644 --- a/apps/www/data/nav.tsx +++ b/apps/www/data/nav.tsx @@ -21,6 +21,10 @@ export const getMenu = (latestBlogPosts: BlogPost[]) => ({ dropdownContainerClassName: 'rounded-xl', subMenu: DevelopersData, }, + { + title: 'Enterprise', + url: '/enterprise', + }, { title: 'Pricing', url: '/pricing', diff --git a/apps/www/pages/contact/sales.tsx b/apps/www/pages/contact/sales.tsx index 8bda2255f55..15462bce40f 100644 --- a/apps/www/pages/contact/sales.tsx +++ b/apps/www/pages/contact/sales.tsx @@ -119,7 +119,7 @@ const ContactSales = () => { ) } -const CustomerQuote = ({ +export const CustomerQuote = ({ quote, author, className, @@ -136,7 +136,7 @@ const CustomerQuote = ({
    ) -const CustomerLogo = ({ title, logo }: { title: string; logo: string }) => ( +export const CustomerLogo = ({ title, logo }: { title: string; logo: string }) => (
    import('components/Enterprise/UseCases')) +const EnterprisePerformance = dynamic(() => import('components/Enterprise/Performance')) +const EnterpriseSecurity = dynamic(() => import('components/Enterprise/Security')) +const EnterpriseSupport = dynamic(() => import('components/Enterprise/Support')) +const EnterpriseQuote = dynamic(() => import('components/Enterprise/SingleQuote')) +const CTAForm = dynamic(() => import('components/Enterprise/CTAForm')) + +const Enterprise: NextPage = () => ( + <> + + + } + footerPosition="left" + /> + + + + + + + + +) + +export default Enterprise diff --git a/apps/www/public/images/customers/logos/light/mozilla.png b/apps/www/public/images/customers/logos/light/mozilla.png index f0775d1a496..8d34b1fa0f5 100644 Binary files a/apps/www/public/images/customers/logos/light/mozilla.png and b/apps/www/public/images/customers/logos/light/mozilla.png differ diff --git a/apps/www/public/images/customers/logos/mozilla.png b/apps/www/public/images/customers/logos/mozilla.png index f8a834198c8..6545c3c04b4 100644 Binary files a/apps/www/public/images/customers/logos/mozilla.png and b/apps/www/public/images/customers/logos/mozilla.png differ diff --git a/apps/www/public/images/enterprise/enterprise-og.png b/apps/www/public/images/enterprise/enterprise-og.png new file mode 100644 index 00000000000..d931e5aa638 Binary files /dev/null and b/apps/www/public/images/enterprise/enterprise-og.png differ diff --git a/apps/www/styles/index.css b/apps/www/styles/index.css index 6d25078c878..4a5a2aed3ae 100644 --- a/apps/www/styles/index.css +++ b/apps/www/styles/index.css @@ -101,6 +101,10 @@ h6:not(.overwrite) { line-height: 1.2 !important; } +.label:not(.prose *):not(.overwrite) { + @apply text-xs font-mono uppercase text-foreground-lighter tracking-wider; +} + .heading-gradient { @apply text-[#F4FFFA00] bg-clip-text bg-gradient-to-b from-foreground to-foreground/70; }