From 433b7484d52aa6f154e8917b5013ded21416a2fd Mon Sep 17 00:00:00 2001 From: Alan Daniel Date: Fri, 13 Mar 2026 15:25:08 -0400 Subject: [PATCH] more animated visuals --- .../_components/BuiltWithSupabaseSection.tsx | 2 +- .../_components/CustomerStoriesSection.tsx | 2 +- .../database/_components/FeaturesSection.tsx | 2 +- .../_components/HighlightsSection.tsx | 2 +- .../database/_components/SqlEditorSection.tsx | 2 +- .../_components/TableEditorSection.tsx | 2 +- .../edge-functions/_components/CTASection.tsx | 2 +- .../_components/ExamplesSection.tsx | 2 +- .../_components/IntegratesSectionClient.tsx | 2 +- .../_components/LocalDXSection.tsx | 187 +++++++++++------- .../_components/ObservabilitySection.tsx | 2 +- apps/www/components/Products/Functions/CI.tsx | 158 ++++++++++++--- .../Products/Functions/LocalDXImage.tsx | 118 +++++++++-- .../Products/Functions/NpmEcosystem.tsx | 169 ++++++++++++---- .../Products/Functions/ParityImage.tsx | 127 ++++++++++-- 15 files changed, 587 insertions(+), 192 deletions(-) diff --git a/apps/www/app/(home)/_components/BuiltWithSupabaseSection.tsx b/apps/www/app/(home)/_components/BuiltWithSupabaseSection.tsx index 1bd45a7de64..255fc32fd55 100644 --- a/apps/www/app/(home)/_components/BuiltWithSupabaseSection.tsx +++ b/apps/www/app/(home)/_components/BuiltWithSupabaseSection.tsx @@ -103,7 +103,7 @@ export function BuiltWithSupabaseSection() { {/* Header row */}
-
+

Kickstart your next project
with production ready templates diff --git a/apps/www/app/(home)/_components/CustomerStoriesSection.tsx b/apps/www/app/(home)/_components/CustomerStoriesSection.tsx index 7dc87d2bb6a..df630a65f8a 100644 --- a/apps/www/app/(home)/_components/CustomerStoriesSection.tsx +++ b/apps/www/app/(home)/_components/CustomerStoriesSection.tsx @@ -77,7 +77,7 @@ export function CustomerStoriesSection() { {/* Header row */}
-
+

How industry leaders
{' '} are building with Supabase diff --git a/apps/www/app/(products)/database/_components/FeaturesSection.tsx b/apps/www/app/(products)/database/_components/FeaturesSection.tsx index d287813b314..8751699577a 100644 --- a/apps/www/app/(products)/database/_components/FeaturesSection.tsx +++ b/apps/www/app/(products)/database/_components/FeaturesSection.tsx @@ -87,7 +87,7 @@ export function FeaturesSection() { {/* Header */}
-
+

Everything you need
diff --git a/apps/www/app/(products)/database/_components/HighlightsSection.tsx b/apps/www/app/(products)/database/_components/HighlightsSection.tsx index 122171a89f5..e1fa5e4a7f8 100644 --- a/apps/www/app/(products)/database/_components/HighlightsSection.tsx +++ b/apps/www/app/(products)/database/_components/HighlightsSection.tsx @@ -40,7 +40,7 @@ export function HighlightsSection() { return (
{/* Header */} -
+

Built for scale
diff --git a/apps/www/app/(products)/database/_components/SqlEditorSection.tsx b/apps/www/app/(products)/database/_components/SqlEditorSection.tsx index ea24867899f..3fa534a8af0 100644 --- a/apps/www/app/(products)/database/_components/SqlEditorSection.tsx +++ b/apps/www/app/(products)/database/_components/SqlEditorSection.tsx @@ -40,7 +40,7 @@ export function SqlEditorSection() { return (
{/* Header */} -
+

The power of
diff --git a/apps/www/app/(products)/database/_components/TableEditorSection.tsx b/apps/www/app/(products)/database/_components/TableEditorSection.tsx index 5e15c37f67f..136fa9e1253 100644 --- a/apps/www/app/(products)/database/_components/TableEditorSection.tsx +++ b/apps/www/app/(products)/database/_components/TableEditorSection.tsx @@ -41,7 +41,7 @@ export function TableEditorSection() { return (
{/* Header */} -
+

Easy to use
diff --git a/apps/www/app/(products)/edge-functions/_components/CTASection.tsx b/apps/www/app/(products)/edge-functions/_components/CTASection.tsx index bb93b403a13..d640e2ddbcb 100644 --- a/apps/www/app/(products)/edge-functions/_components/CTASection.tsx +++ b/apps/www/app/(products)/edge-functions/_components/CTASection.tsx @@ -41,7 +41,7 @@ function TerminalAnimation() { Terminal

{/* Lines */} -
+
{TERMINAL_LINES.slice(0, visibleCount).map((line, i) => (
-
+

What you can build
with Edge Functions diff --git a/apps/www/app/(products)/edge-functions/_components/IntegratesSectionClient.tsx b/apps/www/app/(products)/edge-functions/_components/IntegratesSectionClient.tsx index 71583d14ade..4620ac2e857 100644 --- a/apps/www/app/(products)/edge-functions/_components/IntegratesSectionClient.tsx +++ b/apps/www/app/(products)/edge-functions/_components/IntegratesSectionClient.tsx @@ -76,7 +76,7 @@ export function IntegratesSectionClient({ useCases }: { useCases: UseCase[] }) { {/* Header row */}
-

+

Integrates with the Supabase ecosystem

diff --git a/apps/www/app/(products)/edge-functions/_components/LocalDXSection.tsx b/apps/www/app/(products)/edge-functions/_components/LocalDXSection.tsx index df0a716187e..3e43da05a57 100644 --- a/apps/www/app/(products)/edge-functions/_components/LocalDXSection.tsx +++ b/apps/www/app/(products)/edge-functions/_components/LocalDXSection.tsx @@ -2,6 +2,7 @@ import dynamic from 'next/dynamic' import Link from 'next/link' +import { useState } from 'react' import { cn } from 'ui' const LocalDXImage = dynamic(() => import('~/components/Products/Functions/LocalDXImage')) @@ -9,75 +10,124 @@ const ParityImage = dynamic(() => import('~/components/Products/Functions/Parity const NpmEcosystem = dynamic(() => import('~/components/Products/Functions/NpmEcosystem')) const CI = dynamic(() => import('~/components/Products/Functions/CI')) -const cards = [ - { - id: 'localDX', - label: 'First-class local dev experience', - paragraph: - 'Write code with hot code reloading, a fast Language server for autocompletion, type checking and linting', - image: , - imageClassname: 'pb-8 md:pb-12', - }, - { - id: 'parity', - label: 'Dev and Prod parity', - paragraph: ( - <> - The open source{' '} - - Edge runtime - {' '} - runs locally in dev and powers functions in production - - ), - image: , - imageClassname: 'pb-8 md:pb-12 pt-7', - }, - { - id: 'ecosystem', - label: 'Use any NPM module', - paragraph: 'Tap into the 2+ million modules in the Deno and NPM ecosystem', - image: , - }, - { - id: 'ci', - label: 'Continuous Integration', - paragraph: ( - <> - Use the{' '} - - Supabase CLI with GitHub actions - {' '} - to preview and deploy your functions along with the rest of your application - - ), - image: , - imageClassname: 'pl-2 pt-8 md:pt-12', - }, -] +function EcosystemCard() { + const [isHovered, setIsHovered] = useState(false) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ +
+
+

Use any NPM module

+

+ Tap into the 2+ million modules in the Deno and NPM ecosystem +

+
+
+ ) +} const GRID_CLASS: Record = { localDX: 'col-span-1 md:border-r', parity: 'col-span-1 xl:border-r', - ecosystem: 'col-span-1 md:row-span-2 !border-b-0', ci: 'col-span-1 md:col-span-2 xl:border-r !border-b-0', } +function ParityCard() { + const [isHovered, setIsHovered] = useState(false) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ +
+
+

Dev and Prod parity

+

+ The open source{' '} + + Edge runtime + {' '} + runs locally in dev and powers functions in production +

+
+
+ ) +} + +function CICard() { + const [isHovered, setIsHovered] = useState(false) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ +
+
+

Continuous Integration

+

+ Use the{' '} + + Supabase CLI with GitHub actions + {' '} + to preview and deploy your functions along with the rest of your application +

+
+
+ ) +} + +function LocalDXCard() { + const [isHovered, setIsHovered] = useState(false) + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+ +
+
+

First-class local dev experience

+

+ Write code with hot code reloading, a fast Language server for autocompletion, type + checking and linting +

+
+
+ ) +} + export function LocalDXSection() { return (
{/* Header row */}
-
+

Delightful DX from
local to production @@ -100,26 +150,11 @@ export function LocalDXSection() { {/* Bento grid */}
-
- {cards.map((card, i) => ( -
-
- {card.image} -
-
-

{card.label}

-

{card.paragraph}

-
-
- ))} +
+ + + +
diff --git a/apps/www/app/(products)/edge-functions/_components/ObservabilitySection.tsx b/apps/www/app/(products)/edge-functions/_components/ObservabilitySection.tsx index 54a67c65839..33fc3e1f9b1 100644 --- a/apps/www/app/(products)/edge-functions/_components/ObservabilitySection.tsx +++ b/apps/www/app/(products)/edge-functions/_components/ObservabilitySection.tsx @@ -59,7 +59,7 @@ export function ObservabilitySection() { {/* Header row */}
-

+

Built-in observability

diff --git a/apps/www/components/Products/Functions/CI.tsx b/apps/www/components/Products/Functions/CI.tsx index 4155490875a..fdcce77f888 100644 --- a/apps/www/components/Products/Functions/CI.tsx +++ b/apps/www/components/Products/Functions/CI.tsx @@ -1,36 +1,130 @@ -import React from 'react' -import CodeWindow from '~/components/CodeWindow' +'use client' -const ciCode = `jobs: - deploy: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: supabase/setup-cli@v1 - with: - version: latest - - run: supabase functions deploy -` +import { motion } from 'framer-motion' +import { useEffect, useState } from 'react' -const CI = () => ( -
- -
-) +const CODE_LINES = [ + 'jobs:', + ' deploy:', + ' runs-on: ubuntu-latest', + '', + ' steps:', + ' - uses: actions/checkout@v4', + ' - uses: supabase/setup-cli@v1', + ' with:', + ' version: latest', + ' - run: supabase functions deploy', +] + +type TokenType = 'key' | 'value' | 'punct' | 'dim' | 'plain' + +interface Token { + text: string + type: TokenType +} + +// Colors from supabase-light / supabase-dark shiki themes +const TOKEN_CLASS: Record = { + key: '[color:#6b35dc] dark:[color:#bda4ff]', + value: '[color:#f1a10d] dark:[color:#ffcda1]', + punct: '[color:#a0a0a0] dark:[color:#ffffff99]', + dim: '[color:#7e7e7e]', + plain: '[color:#525252] dark:[color:#ffffff]', +} + +function tokenizeLine(line: string): Token[] { + if (!line.trim()) return [{ text: '\u00a0', type: 'plain' }] + + const indent = line.match(/^(\s*)/)?.[1] ?? '' + const trimmed = line.trimStart() + + const tokens: Token[] = [] + if (indent) tokens.push({ text: indent, type: 'plain' }) + + // List item: "- ..." + if (trimmed.startsWith('- ')) { + const rest = trimmed.slice(2) + const ci = rest.indexOf(': ') + tokens.push({ text: '- ', type: 'dim' }) + if (ci >= 0) { + tokens.push({ text: rest.slice(0, ci), type: 'key' }) + tokens.push({ text: ': ', type: 'punct' }) + tokens.push({ text: rest.slice(ci + 2), type: 'value' }) + } else { + tokens.push({ text: rest, type: 'value' }) + } + return tokens + } + + // "key: value" + const ci = trimmed.indexOf(': ') + if (ci >= 0) { + tokens.push({ text: trimmed.slice(0, ci), type: 'key' }) + tokens.push({ text: ': ', type: 'punct' }) + tokens.push({ text: trimmed.slice(ci + 2), type: 'value' }) + return tokens + } + + // "key:" only + if (trimmed.endsWith(':')) { + tokens.push({ text: trimmed.slice(0, -1), type: 'key' }) + tokens.push({ text: ':', type: 'punct' }) + return tokens + } + + tokens.push({ text: trimmed, type: 'plain' }) + return tokens +} + +const containerVariants = { + visible: { transition: { staggerChildren: 0.08 } }, +} + +const lineVariants = { + hidden: { opacity: 0, x: -8 }, + visible: { opacity: 1, x: 0, transition: { duration: 0.15, ease: 'easeOut' } }, +} + +const CI = ({ isHovered = false }: { isHovered?: boolean }) => { + const [hoverCount, setHoverCount] = useState(0) + + useEffect(() => { + if (isHovered) setHoverCount((c) => c + 1) + }, [isHovered]) + + return ( +
+ {/* CodeWindow shell */} +
+ {/* Title bar */} +
+
+
+
+
+ + {/* Code area */} +
+ + {CODE_LINES.map((line, i) => ( + + {tokenizeLine(line).map((token, j) => ( + + {token.text} + + ))} + + ))} + +
+
+
+ ) +} export default CI diff --git a/apps/www/components/Products/Functions/LocalDXImage.tsx b/apps/www/components/Products/Functions/LocalDXImage.tsx index 34da459cfec..567d3a78933 100644 --- a/apps/www/components/Products/Functions/LocalDXImage.tsx +++ b/apps/www/components/Products/Functions/LocalDXImage.tsx @@ -1,31 +1,63 @@ +'use client' + +import { AnimatePresence, motion } from 'framer-motion' import { Check, Copy } from 'lucide-react' import Image from 'next/image' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import CopyToClipboard from 'react-copy-to-clipboard' -const LocalDXImage = () => { +const COMMAND = 'supabase functions serve' +const OUTPUT = 'Functions listening on 127.0.0.1:54321/functions/v1/' + +const LocalDXImage = ({ isHovered = false }: { isHovered?: boolean }) => { const [copied, setCopied] = useState(false) + const [visibleChars, setVisibleChars] = useState(COMMAND.length) + const [showOutput, setShowOutput] = useState(false) + const visibleCharsRef = useRef(COMMAND.length) + const timeoutsRef = useRef[]>([]) + + const clearTimeouts = () => { + timeoutsRef.current.forEach(clearTimeout) + timeoutsRef.current = [] + } + + const setChars = (n: number) => { + visibleCharsRef.current = n + setVisibleChars(n) + } + + useEffect(() => { + clearTimeouts() + if (isHovered) { + setShowOutput(false) + setChars(0) + COMMAND.split('').forEach((_, i) => { + const t = setTimeout(() => setChars(i + 1), i * 40) + timeoutsRef.current.push(t) + }) + // Show output line shortly after typing completes + const t = setTimeout(() => setShowOutput(true), COMMAND.length * 40 + 120) + timeoutsRef.current.push(t) + } else { + setShowOutput(false) + const current = visibleCharsRef.current + for (let i = current; i < COMMAND.length; i++) { + const t = setTimeout(() => setChars(i + 1), (i - current) * 18) + timeoutsRef.current.push(t) + } + } + return clearTimeouts + }, [isHovered]) const handleCopy = () => { setCopied(true) - setTimeout(() => { - setCopied(false) - }, 1000) + setTimeout(() => setCopied(false), 1000) } return ( -
+
- { draggable={false} className="object-cover absolute z-0 inset-0 dark:hidden block" /> +