more nits

This commit is contained in:
Alan Daniel committed 2026-03-09 15:58:39 -04:00
1 parent fad0e28c32
commit 6a8455e7b8
16 files changed
+462 -367

No files matched your search

@@ -31,7 +31,13 @@ export function CommunitySection() {
</Link>
</Button>
</div>
<div className="relative overflow-hidden max-h-[600px] pt-24">
<div
className="relative overflow-hidden max-h-[600px] pt-24"
style={{
maskImage: 'linear-gradient(to bottom, black 60%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(to bottom, black 60%, transparent 100%)',
}}
>
<div className="flex gap-4 max-w-[1480px] mx-auto">
{columns.map((col, colIdx) => (
<div
@@ -3,48 +3,48 @@
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
import { useState } from 'react'
import { cn } from 'ui'
import { Button, cn } from 'ui'
const customerStories = [
{
name: 'Resend',
logo: '/images/customers/logos/resend.png',
description:
'Scaling seamlessly to 5,000+ paying customers and millions of emails sent daily with Supabase.',
author: 'Zeno Rocha, CEO',
slug: 'resend',
},
{
name: 'Hyper',
logo: '/images/customers/logos/hyper.svg',
description:
'We store embeddings in a PostgreSQL database, hosted by Supabase, to perform a similarity search to identify the most relevant sections within the MDN.',
author: 'Hermina Condei, Director at MDN, Mozilla',
slug: 'mozilla-mdn',
'Hyper is an AI-native marketing platform with agents that operate across the entire marketing workflow, powered by Supabase.',
author: 'Hyper Team',
slug: 'hyper',
},
{
name: 'Firecrawl',
logo: '/images/customers/logos/firecrawl.png',
description:
'Firecrawl switched from Pinecone to Supabase Vector to boost efficiency and accuracy of chat powered search for documentation.',
author: 'Mendable Team',
slug: 'firecrawl',
},
{
name: 'Mobbin',
logo: '/images/customers/logos/mobbin.png',
description:
'Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.',
'How Mobbin migrated 200,000 users from Firebase for a better authentication experience with Supabase.',
author: 'Anselm Bild, Co-Founder',
slug: 'mobbin',
},
{
name: 'Chatbase',
logo: '/images/customers/logos/chatbase.png',
name: 'E2B',
logo: '/images/customers/logos/e2b.png',
description:
'How Yasser leveraged Supabase to build Chatbase and became one of the most successful single-founder AI products.',
author: 'Yasser Elsaid, Founder',
slug: 'chatbase',
},
{
name: 'Markprompt',
logo: '/images/customers/logos/markprompt.png',
description:
'Having access to the full features of Postgres, colocated with the embeddings, makes it the perfect vector database.',
author: 'Michael Fester, Co-Founder, Markprompt',
slug: 'markprompt',
},
{
name: 'Resend',
logo: '/images/customers/logos/resend.png',
description:
'Supabase gave us a complete backend with auth, database and storage so we could focus on our core product.',
author: 'Zeno Rocha, CEO, Resend',
slug: 'resend',
'E2B leveraged Supabase to streamline its platform, allowing secure, scalable execution of AI-generated code in the cloud.',
author: 'Vasek Mlejnsky, Co-Founder',
slug: 'e2b',
},
]
@@ -77,73 +77,11 @@ export function CustomerStoriesSection() {
{/* Header row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<div className="flex items-center justify-between py-8">
<div className="flex items-end justify-between pt-20 pb-8">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
How industry leaders <br />{' '}
<span className="text-foreground">are building with Supabase</span>
</h3>
</div>
</div>
</div>
{/* Cards row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
<div className="flex min-h-[380px]">
{customerStories.map((story, index) => {
const isActive = index === activeIdx
return (
<motion.button
key={story.slug}
onClick={() => setActiveIdx(index)}
className={cn(
'text-left border-r border-border last:border-r-0 p-5 flex flex-col gap-3 overflow-hidden',
isActive ? 'bg-surface-75' : 'items-center hover:bg-surface-75/50'
)}
animate={{ flex: isActive ? '0 0 50%' : '1 1 0%' }}
transition={{ type: 'spring', duration: 0.5, bounce: 0 }}
>
<img
src={story.logo}
alt={story.name}
className="size-16 rounded object-contain shrink-0 dark:invert"
/>
<AnimatePresence mode="popLayout">
{isActive && (
<motion.div
initial={{ opacity: 0, filter: 'blur(4px)' }}
animate={{ opacity: 1, filter: 'blur(0px)' }}
exit={{ opacity: 0, filter: 'blur(4px)', transition: { duration: 0.1 } }}
transition={{ duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }}
className="flex flex-col gap-1.5 mt-1 flex-1 min-w-[280px]"
>
<p className="text-foreground text-sm font-medium">{story.name}</p>
<p className="text-foreground-lighter text-sm leading-relaxed">
{story.description}
</p>
<p className="text-foreground-muted text-xs mt-1">{story.author}</p>
<Link
href={`/customers/${story.slug}`}
className="text-sm text-foreground-light hover:text-foreground underline mt-auto"
>
View more about {story.name}
</Link>
</motion.div>
)}
</AnimatePresence>
</motion.button>
)
})}
</div>
</div>
</div>
{/* Footer row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<div className="flex items-center justify-between py-4">
<p className="text-sm text-foreground-lighter">Powering the worlds best brands</p>
<Link
href="/customers"
className="text-sm text-foreground-light hover:text-foreground underline"
@@ -154,16 +92,89 @@ export function CustomerStoriesSection() {
</div>
</div>
{/* Cards row */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
<motion.div
className="grid min-h-[480px]"
initial={false}
animate={{
gridTemplateColumns: customerStories
.map((_, i) => (i === activeIdx ? '4fr' : '1fr'))
.join(' '),
}}
transition={{ type: 'spring', duration: 0.4, bounce: 0 }}
>
{customerStories.map((story, index) => {
const isActive = index === activeIdx
return (
<button
key={story.slug}
onClick={() => setActiveIdx(index)}
className={cn(
'text-left border-r border-border last:border-r-0 p-6 flex flex-col gap-3 overflow-hidden',
isActive ? 'bg-surface-75' : 'hover:bg-surface-75/50'
)}
>
<motion.img
layoutId={`logo-${story.slug}`}
src={story.logo}
alt={story.name}
className="size-20 rounded object-contain shrink-0 dark:invert"
style={{ alignSelf: isActive ? 'flex-start' : 'center' }}
transition={{ type: 'spring', duration: 0.4, bounce: 0 }}
/>
<motion.div
className="flex flex-col gap-1.5 flex-1 w-[30rem]"
initial={false}
animate={{
opacity: isActive ? 1 : 0,
filter: isActive ? 'blur(0px)' : 'blur(2px)',
}}
transition={{
duration: 0.42,
ease: [0.165, 0.84, 0.44, 1],
delay: isActive ? 0.2 : 0,
}}
>
<p className="text-foreground text-sm font-medium">{story.name}</p>
<p className="text-foreground-lighter text-sm leading-relaxed text-pretty">
{story.description}
</p>
<p className="text-foreground-muted text-xs mt-1">{story.author}</p>
<Link
href={`/customers/${story.slug}`}
className="text-sm text-foreground-light hover:text-foreground underline mt-auto"
>
View more about {story.name}
</Link>
</motion.div>
</button>
)
})}
</motion.div>
</div>
</div>
{/* AI Builder stories */}
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border">
<div className="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-32 py-12">
<div className="flex items-start">
<div className="grid grid-cols-1 lg:grid-cols-[auto_1fr] gap-16 py-20">
<div className="flex flex-col min-h-[320px] gap-4 justify-between">
<h3 className="text-2xl md:text-4xl text-foreground-lighter">
Learn about
Powering the next wave
<br />
AI Builder stories
<span className="text-foreground">of AI builders</span>
</h3>
<p className="text-sm text-foreground-lighter leading-relaxed text-pretty max-w-sm">
Create full backend experiences in behalf of your users with Supabase for Platforms,
the all-in-one solution for building AI-native platforms and marketplaces.
</p>
<Button type="default" size="medium" asChild className="w-fit mt-auto">
<Link href="/partners/integrations">Explore Supabase for Platforms</Link>
</Button>
</div>
<div className="grid grid-cols-2 gap-2">
@@ -171,7 +182,7 @@ export function CustomerStoriesSection() {
<Link
key={story.slug}
href={`/customers/${story.slug}`}
className="group relative overflow-hidden rounded-xl aspect-[4/3] flex items-center justify-center"
className="group relative overflow-hidden rounded-xl w-full h-full flex items-center justify-center"
style={{ background: story.gradient }}
>
<img
@@ -9,7 +9,7 @@ import { cn } from 'ui'
const dashboardStories = [
{
logo: '/images/customers/logos/hyper.svg',
logo: '/images/customers/logos/hyper-icon.svg',
name: 'Hyper',
quote:
'We store embeddings in a PostgreSQL database, hosted by Supabase, to perform a similarity search to identify the most relevant sections within the MDN.',
@@ -17,7 +17,7 @@ const dashboardStories = [
slug: 'mozilla-mdn',
},
{
logo: '/images/customers/logos/chatbase.png',
logo: '/images/customers/logos/chatbase-icon.svg',
name: 'Chatbase',
quote:
'The SQL Editor in Supabase is my favorite feature — it lets us run complex queries, debug issues, and iterate on our schema without ever leaving the browser.',
@@ -25,7 +25,7 @@ const dashboardStories = [
slug: 'chatbase',
},
{
logo: '/images/customers/logos/markprompt.png',
logo: '/images/customers/logos/markprompt-icon.svg',
name: 'Markprompt',
quote:
'Row Level Security in Supabase allows us to set granular access policies directly in Postgres, so our multi-tenant architecture stays secure by default.',
@@ -1,107 +1,59 @@
'use client'
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { FrameworksSectionClient } from './FrameworksSectionClient'
import { AnimatePresence, motion } from 'framer-motion'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'
import dart from 'react-syntax-highlighter/dist/cjs/languages/hljs/dart'
import js from 'react-syntax-highlighter/dist/cjs/languages/hljs/javascript'
import xml from 'react-syntax-highlighter/dist/cjs/languages/hljs/xml'
import { cn } from 'ui'
// ── Shiki themes ────────────────────────────────────────────────────────────
// Supabase docs theme: purple keywords, green functions/constants, peach strings
const supabaseDocsTheme = {
dark: {
hljs: {
display: 'block',
overflowX: 'auto' as const,
background: 'transparent',
color: '#ffffff',
},
'hljs-keyword': { color: '#bda4ff' },
'hljs-selector-tag': { color: '#bda4ff' },
'hljs-literal': { color: '#bda4ff' },
'hljs-strong': { color: '#bda4ff' },
'hljs-tag': { color: '#3ecf8e' },
'hljs-name': { color: '#3ecf8e' },
'hljs-title': { color: '#3ecf8e', fontWeight: 'normal' as const },
'hljs-type': { color: '#3ecf8e', fontWeight: 'normal' as const },
'hljs-built_in': { color: '#3ecf8e' },
'hljs-builtin-name': { color: '#3ecf8e' },
'hljs-section': { color: '#3ecf8e', fontWeight: 'normal' as const },
'hljs-attribute': { color: '#3ecf8e' },
'hljs-variable': { color: '#3ecf8e' },
'hljs-string': { color: '#ffcda1' },
'hljs-bullet': { color: '#ffcda1' },
'hljs-subst': { color: '#ffcda1' },
'hljs-emphasis': { color: '#ffcda1' },
'hljs-addition': { color: '#ffcda1' },
'hljs-template-tag': { color: '#ffcda1' },
'hljs-template-variable': { color: '#ffcda1' },
'hljs-selector-attr': { color: '#ffcda1' },
'hljs-selector-pseudo': { color: '#ffcda1' },
'hljs-symbol': { color: '#ffcda1' },
'hljs-regexp': { color: '#ffcda1' },
'hljs-link': { color: '#ffffff' },
'hljs-comment': { color: '#7e7e7e' },
'hljs-quote': { color: '#7e7e7e' },
'hljs-deletion': { color: '#7e7e7e' },
'hljs-meta': { color: '#7e7e7e' },
'hljs-code': { color: '#3ecf8e' },
'hljs-doctag': { fontWeight: 'normal' as const },
'hljs-selector-id': { fontWeight: 'normal' as const },
},
light: {
hljs: {
display: 'block',
overflowX: 'auto' as const,
background: 'transparent',
color: '#1f1f1f',
},
'hljs-keyword': { color: '#6b35dc' },
'hljs-selector-tag': { color: '#6b35dc' },
'hljs-literal': { color: '#6b35dc' },
'hljs-strong': { color: '#6b35dc' },
'hljs-tag': { color: '#15593b' },
'hljs-name': { color: '#15593b' },
'hljs-title': { color: '#15593b', fontWeight: 'normal' as const },
'hljs-type': { color: '#15593b', fontWeight: 'normal' as const },
'hljs-built_in': { color: '#15593b' },
'hljs-builtin-name': { color: '#15593b' },
'hljs-section': { color: '#15593b', fontWeight: 'normal' as const },
'hljs-attribute': { color: '#15593b' },
'hljs-variable': { color: '#15593b' },
'hljs-string': { color: '#f1a10d' },
'hljs-bullet': { color: '#f1a10d' },
'hljs-subst': { color: '#f1a10d' },
'hljs-emphasis': { color: '#f1a10d' },
'hljs-addition': { color: '#f1a10d' },
'hljs-template-tag': { color: '#f1a10d' },
'hljs-template-variable': { color: '#f1a10d' },
'hljs-selector-attr': { color: '#f1a10d' },
'hljs-selector-pseudo': { color: '#f1a10d' },
'hljs-symbol': { color: '#f1a10d' },
'hljs-regexp': { color: '#f1a10d' },
'hljs-link': { color: '#1f1f1f' },
'hljs-comment': { color: '#7e7e7e' },
'hljs-quote': { color: '#7e7e7e' },
'hljs-deletion': { color: '#7e7e7e' },
'hljs-meta': { color: '#7e7e7e' },
'hljs-code': { color: '#15593b' },
'hljs-doctag': { fontWeight: 'normal' as const },
'hljs-selector-id': { fontWeight: 'normal' as const },
const supabaseDark: ThemeRegistration = {
name: 'supabase-dark',
type: 'dark',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#ffffff',
},
tokenColors: [
{ scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#bda4ff' } },
{ scope: ['entity.name.function', 'support.function', 'entity.name.tag', 'support.class.component'], settings: { foreground: '#3ecf8e' } },
{ scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#3ecf8e' } },
{ scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key', 'entity.other.attribute-name'], settings: { foreground: '#3ecf8e' } },
{ scope: ['string', 'string.quoted'], settings: { foreground: '#ffcda1' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#ffffff' } },
{ scope: ['punctuation'], settings: { foreground: '#ffffff' } },
{ scope: ['constant.numeric'], settings: { foreground: '#ededed' } },
{ scope: ['markup.underline.link'], settings: { foreground: '#ffffff' } },
{ scope: ['markup.inserted'], settings: { foreground: '#3ecf8e' } },
{ scope: ['markup.deleted'], settings: { foreground: '#F06A50' } },
],
}
SyntaxHighlighter.registerLanguage('js', js)
SyntaxHighlighter.registerLanguage('dart', dart)
SyntaxHighlighter.registerLanguage('xml', xml)
const supabaseLight: ThemeRegistration = {
name: 'supabase-light',
type: 'light',
colors: {
'editor.background': '#00000000',
'editor.foreground': '#525252',
},
tokenColors: [
{ scope: ['keyword', 'storage', 'storage.type', 'storage.modifier'], settings: { foreground: '#6b35dc' } },
{ scope: ['entity.name.function', 'support.function', 'entity.name.tag', 'support.class.component'], settings: { foreground: '#15593b' } },
{ scope: ['constant', 'variable.other.constant', 'support.constant'], settings: { foreground: '#15593b' } },
{ scope: ['variable.other.property', 'support.type.property-name', 'meta.object-literal.key', 'entity.other.attribute-name'], settings: { foreground: '#15593b' } },
{ scope: ['string', 'string.quoted'], settings: { foreground: '#f1a10d' } },
{ scope: ['comment', 'punctuation.definition.comment'], settings: { foreground: '#7e7e7e' } },
{ scope: ['variable.parameter'], settings: { foreground: '#525252' } },
{ scope: ['punctuation'], settings: { foreground: '#a0a0a0' } },
{ scope: ['constant.numeric'], settings: { foreground: '#525252' } },
{ scope: ['markup.underline.link'], settings: { foreground: '#525252' } },
],
}
// ── Framework data ──────────────────────────────────────────────────────────
const frameworksList: {
name: string
icon: string
code: string
lang: 'js' | 'dart' | 'xml'
lang: 'javascript' | 'dart' | 'svelte' | 'vue'
}[] = [
{
name: 'React',
@@ -123,7 +75,7 @@ export default function App() {
return <TodoList items={todos} />
}`,
lang: 'js' as const,
lang: 'javascript' as const,
},
{
name: 'Next.js',
@@ -142,7 +94,7 @@ export default async function Page() {
return <TodoList items={todos} />
}`,
lang: 'js' as const,
lang: 'javascript' as const,
},
{
name: 'Flutter',
@@ -183,7 +135,7 @@ final data = await supabase
todos = data
}
</script>`,
lang: 'xml' as const,
lang: 'svelte' as const,
},
{
name: 'Vue',
@@ -205,7 +157,7 @@ onMounted(async () => {
todos.value = data
})
</script>`,
lang: 'xml' as const,
lang: 'vue' as const,
},
{
name: 'Nuxt',
@@ -227,105 +179,26 @@ const { data: todos } = await useAsyncData(
<template>
<TodoList :items="todos" />
</template>`,
lang: 'xml' as const,
lang: 'vue' as const,
},
]
export function FrameworksSection() {
const { resolvedTheme } = useTheme()
const isDark = resolvedTheme?.includes('dark') ?? false
const [activeIdx, setActiveIdx] = useState(0)
const [mounted, setMounted] = useState(false)
const active = frameworksList[activeIdx]
// ── Server component: pre-highlights all code ───────────────────────────────
useEffect(() => setMounted(true), [])
export async function FrameworksSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: ['javascript', 'dart', 'svelte', 'vue'],
})
return (
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] pl-6 border-x border-border">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0">
{/* Left: title */}
<div className="flex items-center py-16 lg:py-24 pr-6">
<div className="text-2xl md:text-4xl text-foreground-lighter">
Use Supabase with{' '}
<span className="block">
<AnimatePresence mode="wait">
<motion.span
key={active.name}
initial={{ y: 10, opacity: 0 }}
animate={{ y: 0, opacity: 1, transition: { duration: 0.25 } }}
exit={{ y: 0, opacity: 0, transition: { duration: 0.1 } }}
className="inline-block text-foreground"
>
{active.name}
</motion.span>
</AnimatePresence>
</span>
</div>
</div>
const frameworks = frameworksList.map((fw) => ({
name: fw.name,
icon: fw.icon,
darkHtml: hl.codeToHtml(fw.code, { lang: fw.lang, theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(fw.code, { lang: fw.lang, theme: 'supabase-light' }),
}))
{/* Right: icon tabs + code */}
<div className="border-l border-border flex flex-col">
{/* 6-col icon row */}
<div className="grid grid-cols-6 border-b border-border">
{frameworksList.map((framework, index) => (
<button
key={framework.name}
onClick={() => setActiveIdx(index)}
className={cn(
'flex items-center justify-center py-4 border-r border-border last:border-r-0 transition-colors',
index === activeIdx
? 'bg-surface-75 text-foreground'
: 'text-foreground-muted hover:text-foreground-light hover:bg-surface-75/50'
)}
>
<svg
width={28}
height={28}
fillRule="evenodd"
clipRule="evenodd"
viewBox="0 0 61 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d={framework.icon} fill="currentColor" />
</svg>
</button>
))}
</div>
hl.dispose()
{/* Code area */}
<div className="h-[440px] overflow-auto">
{mounted && (
<AnimatePresence mode="wait">
<motion.div
key={active.name}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
>
{/* @ts-ignore */}
<SyntaxHighlighter
language={active.lang}
style={isDark ? supabaseDocsTheme.dark : supabaseDocsTheme.light}
customStyle={{
margin: 0,
padding: '1.5rem',
background: 'transparent',
fontSize: '0.8125rem',
fontWeight: 500,
lineHeight: 1.7,
}}
>
{active.code}
</SyntaxHighlighter>
</motion.div>
</AnimatePresence>
)}
</div>
</div>
</div>
</div>
</div>
)
return <FrameworksSectionClient frameworks={frameworks} />
}
@@ -0,0 +1,94 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { useState } from 'react'
import { cn } from 'ui'
type Framework = {
name: string
icon: string
darkHtml: string
lightHtml: string
}
export function FrameworksSectionClient({ frameworks }: { frameworks: Framework[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const active = frameworks[activeIdx]
return (
<div className="border-b border-border">
<div className="dark mx-auto max-w-[var(--container-max-w,75rem)] pl-6 border-x border-border bg-background text-white">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0">
{/* Left: title */}
<div className="flex items-center py-16 lg:py-24 pr-6">
<div className="text-2xl md:text-4xl text-foreground-lighter">
Use Supabase with{' '}
<span className="block">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span
key={active.name}
initial={{ filter: 'blur(2px)', opacity: 0 }}
animate={{ filter: 'blur(0px)', opacity: 1, transition: { duration: 0.25 } }}
exit={{ filter: 'blur(2px)', opacity: 0, transition: { duration: 0.1 } }}
className="inline-block text-foreground"
>
{active.name}
</motion.span>
</AnimatePresence>
</span>
</div>
</div>
{/* Right: icon tabs + code */}
<div className="border-l border-border flex flex-col">
{/* 6-col icon row */}
<div className="grid grid-cols-6 border-b border-border">
{frameworks.map((framework, index) => (
<button
key={framework.name}
onClick={() => setActiveIdx(index)}
className={cn(
'flex items-center justify-center py-4 border-r border-border last:border-r-0 transition-colors',
index === activeIdx
? 'bg-surface-75 text-foreground'
: 'text-foreground-muted hover:text-foreground-light hover:bg-surface-75/50'
)}
>
<svg
width={28}
height={28}
fillRule="evenodd"
clipRule="evenodd"
viewBox="0 0 61 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d={framework.icon} fill="currentColor" />
</svg>
</button>
))}
</div>
{/* Code area */}
<div className="h-[440px] overflow-auto">
<AnimatePresence mode="wait">
<motion.div
key={active.name}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
>
<div
dangerouslySetInnerHTML={{ __html: active.darkHtml }}
className="[&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
/>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,57 @@
'use client'
import dynamic from 'next/dynamic'
import getContent from '~/data/home/content'
import { BuiltWithSupabaseSection } from './BuiltWithSupabaseSection'
import { CommunitySection } from './CommunitySection'
import { CTASection } from './CTASection'
import { CustomerStoriesSection } from './CustomerStoriesSection'
import { DashboardFeaturesSection } from './DashboardFeaturesSection'
import { Hero } from './Hero'
import { LogosGrid } from './LogosGrid'
import { OpenSourceSection } from './OpenSourceSection'
const Products = dynamic(() => import('~/components/Products/index'))
function SectionBlock({ children, className }: { children: React.ReactNode; className?: string }) {
return <section className={`border-b border-border ${className ?? ''}`}>{children}</section>
}
export function HomeContent({ frameworksSlot }: { frameworksSlot: React.ReactNode }) {
const content = getContent()
return (
<div className="[&_.container]:!max-w-[var(--container-max-w,75rem)] [&_.container]:!px-6">
<section>
<Hero />
</section>
<SectionBlock className="border-t border-b-0 border-border relative">
<div className="-translate-y-16">
<Products {...content.productsSection} />
</div>
</SectionBlock>
<section>
<LogosGrid />
</section>
<section>
<DashboardFeaturesSection {...content.dashboardFeatures} />
</section>
<section>{frameworksSlot}</section>
<section className="border-b border-border">
<BuiltWithSupabaseSection />
</section>
<section>
<CommunitySection />
</section>
<section>
<CustomerStoriesSection />
</section>
<section>
<OpenSourceSection />
</section>
<section>
<CTASection />
</section>
</div>
)
}
@@ -1,6 +1,5 @@
'use client'
import { ComponentProps } from 'react'
import {
BetasharesLogo,
BoltLogo,
@@ -68,7 +67,10 @@ export function LogosGrid() {
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border py-10">
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-y-10 gap-x-6 opacity-70">
{gridLogos.map(({ name, Logo }) => (
<div key={name} className="flex items-center justify-center h-10 text-foreground-lighter">
<div
key={name}
className="flex items-center justify-center h-10 text-foreground-lighter"
>
<Logo className="h-8 lg:h-12 w-auto" />
</div>
))}
@@ -1,10 +1,10 @@
'use client'
import { animate, motion, useInView } from 'framer-motion'
import Link from 'next/link'
import { useEffect, useRef, useState } from 'react'
import { Button } from 'ui'
import { useSendTelemetryEvent } from '~/lib/telemetry'
import { motion, useInView } from 'framer-motion'
import Link from 'next/link'
import { useRef } from 'react'
import { Button } from 'ui'
// ── Contribution graph ──────────────────────────────────────────────────────
@@ -85,23 +85,14 @@ export function OpenSourceSection() {
const inView = useInView(panelRef, { once: true, amount: 0.4 })
const sendTelemetryEvent = useSendTelemetryEvent()
const [stars, setStars] = useState('0.0K')
useEffect(() => {
if (!inView) return
const controls = animate(0, 98400, {
duration: 2.2,
ease: 'easeOut',
onUpdate: (v: number) => setStars(`${(v / 1000).toFixed(1)}K`),
})
return controls.stop
}, [inView])
const stars = '98.4K'
return (
<div className="border-b border-border">
<div className="mx-auto max-w-[var(--container-max-w,75rem)] pl-6 border-x border-border">
<div className="grid grid-cols-1 md:grid-cols-3">
{/* Main content — spans 2 cols */}
<div className="col-span-1 md:col-span-2 flex flex-col justify-center gap-6 py-24">
<div className="col-span-1 md:col-span-2 flex flex-col justify-between gap-6 py-20 min-h-[400px]">
<div>
<h2 className="text-4xl text-foreground text-balance">Open source from day one</h2>
<p className="mt-4 max-w-lg text-sm leading-relaxed text-foreground-lighter">
+2 -56
View File
@@ -1,60 +1,6 @@
'use client'
import dynamic from 'next/dynamic'
import getContent from '~/data/home/content'
import { BuiltWithSupabaseSection } from './_components/BuiltWithSupabaseSection'
import { CommunitySection } from './_components/CommunitySection'
import { CTASection } from './_components/CTASection'
import { CustomerStoriesSection } from './_components/CustomerStoriesSection'
import { DashboardFeaturesSection } from './_components/DashboardFeaturesSection'
import { FrameworksSection } from './_components/FrameworksSection'
import { Hero } from './_components/Hero'
import { LogosGrid } from './_components/LogosGrid'
import { OpenSourceSection } from './_components/OpenSourceSection'
const Products = dynamic(() => import('~/components/Products/index'))
function SectionBlock({ children, className }: { children: React.ReactNode; className?: string }) {
return <section className={`border-b border-border ${className ?? ''}`}>{children}</section>
}
import { HomeContent } from './_components/HomeContent'
export default function HomePage() {
const content = getContent()
return (
<div className="[&_.container]:!max-w-[var(--container-max-w,75rem)] [&_.container]:!px-6">
<section>
<Hero />
</section>
<SectionBlock className="border-t border-b-0 border-border relative">
<div className="-translate-y-16">
<Products {...content.productsSection} />
</div>
</SectionBlock>
<section>
<LogosGrid />
</section>
<section>
<DashboardFeaturesSection {...content.dashboardFeatures} />
</section>
<section>
<FrameworksSection />
</section>
<section className="border-b border-border">
<BuiltWithSupabaseSection />
</section>
<section>
<CommunitySection />
</section>
<section>
<CustomerStoriesSection />
</section>
<section>
<OpenSourceSection />
</section>
<section>
<CTASection />
</section>
</div>
)
return <HomeContent frameworksSlot={<FrameworksSection />} />
}
+5 -3
View File
@@ -1,7 +1,9 @@
import React, { PropsWithChildren, useEffect, useRef } from 'react'
import { motion } from 'framer-motion'
import { cn } from 'ui'
'use client'
import { detectBrowser, isBrowser } from 'common'
import { motion } from 'framer-motion'
import React, { PropsWithChildren, useEffect, useRef } from 'react'
import { cn } from 'ui'
interface Props {
outerClassName?: string
+2
View File
@@ -1,3 +1,5 @@
'use client'
import SectionContainer from '~/components/Layouts/SectionContainer'
import type { ProductType } from '~/data/MainProducts'
import { useSendTelemetryEvent } from '~/lib/telemetry'
+1
View File
@@ -83,6 +83,7 @@
"remark": "^15.0.1",
"remark-gfm": "^4.0.1",
"shared-data": "workspace:*",
"shiki": "^4.0.1",
"swiper": "^12.1.2",
"typed.js": "^2.0.16",
"typescript": "catalog:",
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 256 256">
<rect width="256" height="256" fill="#000" rx="60"/>
<path fill="#FFF" d="M197.9 149.8a59.8 59.8 0 0 1-28.3 45 72.7 72.7 0 0 1-18.3 8L148 165c5-3.6 8.5-9.4 8.9-15.6l41 .3v.1ZM198 110.3h-41.1c.1-6.8-3.5-13.4-8.9-17.5-5.5-4-12.4-6-19.2-5.3-3 .3-5.9 1-8.6 2l-20.6-31c12.2-5.2 26-6.8 39.4-5.6 13.8 1 27 6 37.9 14.5a61.5 61.5 0 0 1 21 43l.1-.1Z"/>
<path fill="#B2AEB9" d="M151.3 202.8a80.6 80.6 0 0 1-34.7 1.5 70.8 70.8 0 0 1-36-16.8c-24.1-22.3-27.8-63-16.2-92.4a63.4 63.4 0 0 1 35.1-36.7l20.7 31c-2.6 1-5 2.4-7.3 4.2-10.4 8.2-13 23-12.8 36.3.1 12.8 2.8 27 13 34.8a29.4 29.4 0 0 0 16.1 5.6c6.4.3 13-1.1 18.3-4.8l.5-.4 3.3 37.7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 714 B

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 270 270">
<path d="M64.5279 215.649C67.4858 212.89 71.4656 211.345 75.6116 211.345H153.628C162.365 211.345 169.448 218.073 169.448 226.371V229.309C169.448 232.633 168.288 235.863 166.149 238.494L147.553 261.365C144.558 265.049 139.933 267.207 135.033 267.207H50.2686C41.5313 267.207 34.4482 260.479 34.4482 252.18V250.005C34.4482 245.972 36.1551 242.108 39.185 239.283L64.5279 215.649Z" fill="black"/>
<path d="M37.457 3.7644C40.358 1.3412 44.1278 0 48.0365 0H215.111C223.829 0 230.897 6.5373 230.897 14.6014V15.7312C230.897 19.8446 229.02 23.7673 225.73 26.5345L189.841 56.719C186.934 59.1632 183.149 60.5172 179.222 60.5172H15.7856C7.06743 60.5172 0 53.9802 0 45.916V41.5403C0 37.4095 1.89156 33.472 5.20594 30.7036L37.457 3.7644Z" fill="black"/>
<path d="M201.62 79.3816C198.904 82.3501 197.38 86.3647 197.38 90.5482V254.15C197.38 262.903 203.917 270 211.981 270H215.603C219.91 270 223.997 267.935 226.771 264.36L254.464 228.668C256.681 225.811 257.897 222.195 257.897 218.459V52.4134C257.897 44.5481 252.023 38.1719 244.777 38.1719C241.281 38.1719 237.93 39.6863 235.467 42.3791L201.62 79.3816Z" fill="black"/>
<path d="M5.42304 116.297C1.98937 119.291 0 123.754 0 128.464V214.79C0 223.171 6.41511 229.966 14.3285 229.966C17.8574 229.966 21.2623 228.586 23.8905 226.092L50.9126 200.447C54.0623 197.457 55.8621 193.19 55.8621 188.711V112.586C55.8621 103.883 49.201 96.828 40.984 96.828H33.0916C29.6422 96.828 26.3 98.098 23.6366 100.419L5.42304 116.297Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 320 320">
<path d="M112 25.7273H208C227.647 25.7273 242.218 25.7294 253.754 26.6719C265.258 27.6118 273.503 29.4733 280.541 33.0596C293.199 39.5092 303.491 49.8006 309.94 62.4588C313.527 69.4972 315.388 77.7422 316.328 89.2462C317.271 100.782 317.273 115.353 317.273 135V185C317.273 204.647 317.271 219.218 316.328 230.754C315.388 242.258 313.527 250.503 309.94 257.541C303.491 270.199 293.199 280.491 280.541 286.94C273.503 290.527 265.258 292.388 253.754 293.328C242.218 294.271 227.647 294.273 208 294.273H112C92.3532 294.273 77.7821 294.271 66.2462 293.328C54.7422 292.388 46.4972 290.527 39.4588 286.94C26.8006 280.491 16.5092 270.199 10.0596 257.541C6.47334 250.503 4.61182 242.258 3.67191 230.754C2.72939 219.218 2.72727 204.647 2.72727 185V135C2.72727 115.353 2.72939 100.782 3.67191 89.2462C4.61182 77.7422 6.47334 69.4972 10.0596 62.4588C16.5092 49.8006 26.8006 39.5092 39.4588 33.0596C46.4972 29.4733 54.7422 27.6118 66.2462 26.6719C77.7821 25.7294 92.3532 25.7273 112 25.7273Z" fill="black" stroke="white" stroke-width="5.45455"/>
<path d="M236.363 108C236.363 102.979 240.433 98.9091 245.454 98.9091H263.636C268.657 98.9091 272.727 102.979 272.727 108V153.455C272.727 158.475 268.657 162.545 263.636 162.545H245.454C240.433 162.545 236.363 158.475 236.363 153.455V108Z" fill="white"/>
<path d="M276.363 200.727C276.363 212.777 266.595 222.545 254.545 222.545C242.495 222.545 232.727 212.777 232.727 200.727C232.727 188.677 242.495 178.909 254.545 178.909C266.595 178.909 276.363 188.677 276.363 200.727Z" fill="white"/>
<path d="M56.3631 222.545C51.3424 222.545 47.2722 218.475 47.2722 213.455V108C47.2722 102.979 51.3424 98.9091 56.3631 98.9091H79.2665C82.0282 98.9091 84.6401 100.164 86.3653 102.321L112.901 135.49C116.54 140.039 123.459 140.039 127.098 135.49L153.634 102.321C155.359 100.164 157.971 98.9091 160.732 98.9091H183.636C188.657 98.9091 192.727 102.979 192.727 108V213.455C192.727 218.475 188.657 222.545 183.636 222.545H165.454C160.433 222.545 156.363 218.475 156.363 213.455V177.553C156.363 168.959 145.542 165.163 140.173 171.873L127.098 188.217C123.459 192.767 116.54 192.767 112.901 188.217L99.8256 171.873C94.4573 165.163 83.6359 168.959 83.6359 177.553V213.455C83.6359 218.475 79.5657 222.545 74.5449 222.545H56.3631Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+93
View File
@@ -1751,6 +1751,9 @@ importers:
shared-data:
specifier: workspace:*
version: link:../../packages/shared-data
shiki:
specifier: ^4.0.1
version: 4.0.1
swiper:
specifier: ^12.1.2
version: 12.1.2
@@ -7937,24 +7940,52 @@ packages:
'@shikijs/core@3.13.0':
resolution: {integrity: sha512-3P8rGsg2Eh2qIHekwuQjzWhKI4jV97PhvYjYUzGqjvJfqdQPz+nMlfWahU24GZAyW1FxFI1sYjyhfh5CoLmIUA==}
'@shikijs/core@4.0.1':
resolution: {integrity: sha512-vWvqi9JNgz1dRL9Nvog5wtx7RuNkf7MEPl2mU/cyUUxJeH1CAr3t+81h8zO8zs7DK6cKLMoU9TvukWIDjP4Lzg==}
engines: {node: '>=20'}
'@shikijs/engine-javascript@3.13.0':
resolution: {integrity: sha512-Ty7xv32XCp8u0eQt8rItpMs6rU9Ki6LJ1dQOW3V/56PKDcpvfHPnYFbsx5FFUP2Yim34m/UkazidamMNVR4vKg==}
'@shikijs/engine-javascript@4.0.1':
resolution: {integrity: sha512-DJK9NiwtGYqMuKCRO4Ip0FKNDQpmaiS+K5bFjJ7DWFn4zHueDWgaUG8kAofkrnXF6zPPYYQY7J5FYVW9MbZyBg==}
engines: {node: '>=20'}
'@shikijs/engine-oniguruma@3.13.0':
resolution: {integrity: sha512-O42rBGr4UDSlhT2ZFMxqM7QzIU+IcpoTMzb3W7AlziI1ZF7R8eS2M0yt5Ry35nnnTX/LTLXFPUjRFCIW+Operg==}
'@shikijs/engine-oniguruma@4.0.1':
resolution: {integrity: sha512-oCWdCTDch3J8Kc0OZJ98KuUPC02O1VqIE3W/e2uvrHqTxYRR21RGEJMtchrgrxhsoJJCzmIciKsqG+q/yD+Cxg==}
engines: {node: '>=20'}
'@shikijs/langs@3.13.0':
resolution: {integrity: sha512-672c3WAETDYHwrRP0yLy3W1QYB89Hbpj+pO4KhxK6FzIrDI2FoEXNiNCut6BQmEApYLfuYfpgOZaqbY+E9b8wQ==}
'@shikijs/langs@4.0.1':
resolution: {integrity: sha512-v/mluaybWdnGJR4GqAR6zh8qAZohW9k+cGYT28Y7M8+jLbC0l4yG085O1A+WkseHTn+awd+P3UBymb2+MXFc8w==}
engines: {node: '>=20'}
'@shikijs/primitive@4.0.1':
resolution: {integrity: sha512-ns0hHZc5eWZuvuIEJz2pTx3Qecz0aRVYumVQJ8JgWY2tq/dH8WxdcVM49Fc2NsHEILNIT6vfdW9MF26RANWiTA==}
engines: {node: '>=20'}
'@shikijs/themes@3.13.0':
resolution: {integrity: sha512-Vxw1Nm1/Od8jyA7QuAenaV78BG2nSr3/gCGdBkLpfLscddCkzkL36Q5b67SrLLfvAJTOUzW39x4FHVCFriPVgg==}
'@shikijs/themes@4.0.1':
resolution: {integrity: sha512-FW41C/D6j/yKQkzVdjrRPiJCtgeDaYRJFEyCKFCINuRJRj9WcmubhP4KQHPZ4+9eT87jruSrYPyoblNRyDFzvA==}
engines: {node: '>=20'}
'@shikijs/transformers@1.6.0':
resolution: {integrity: sha512-qGfHe1ECiqfE2STPWvfogIj/9Q0SK+MCRJdoITkW7AmFuB7DmbFnBT2US84+zklJOB51MzNO8RUXZiauWssJlQ==}
'@shikijs/types@3.13.0':
resolution: {integrity: sha512-oM9P+NCFri/mmQ8LoFGVfVyemm5Hi27330zuOBp0annwJdKH1kOLndw3zCtAVDehPLg9fKqoEx3Ht/wNZxolfw==}
'@shikijs/types@4.0.1':
resolution: {integrity: sha512-EaygPEn57+jJ76mw+nTLvIpJMAcMPokFbrF8lufsZP7Ukk+ToJYEcswN1G0e49nUZAq7aCQtoeW219A8HK1ZOw==}
engines: {node: '>=20'}
'@shikijs/vscode-textmate@10.0.2':
resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==}
@@ -14880,6 +14911,9 @@ packages:
oniguruma-to-es@4.3.3:
resolution: {integrity: sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg==}
oniguruma-to-es@4.3.4:
resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==}
oo-ascii-tree@1.98.0:
resolution: {integrity: sha512-+GE7ywhtS6MctbfcO+vZzqIxcFzucZCwmawcwCVo89DxQDakV1JFfFViTXG4A90UzTAsU4tQteGmwDtwOlOXLw==}
engines: {node: '>= 14.17.0'}
@@ -16726,6 +16760,10 @@ packages:
shiki@3.13.0:
resolution: {integrity: sha512-aZW4l8Og16CokuCLf8CF8kq+KK2yOygapU5m3+hoGw0Mdosc6fPitjM+ujYarppj5ZIKGyPDPP1vqmQhr+5/0g==}
shiki@4.0.1:
resolution: {integrity: sha512-EkAEhDTN5WhpoQFXFw79OHIrSAfHhlImeCdSyg4u4XvrpxKEmdo/9x/HWSowujAnUrFsGOwWiE58a6GVentMnQ==}
engines: {node: '>=20'}
should-equal@2.0.0:
resolution: {integrity: sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA==}
@@ -25119,25 +25157,58 @@ snapshots:
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/core@4.0.1':
dependencies:
'@shikijs/primitive': 4.0.1
'@shikijs/types': 4.0.1
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
hast-util-to-html: 9.0.5
'@shikijs/engine-javascript@3.13.0':
dependencies:
'@shikijs/types': 3.13.0
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 4.3.3
'@shikijs/engine-javascript@4.0.1':
dependencies:
'@shikijs/types': 4.0.1
'@shikijs/vscode-textmate': 10.0.2
oniguruma-to-es: 4.3.4
'@shikijs/engine-oniguruma@3.13.0':
dependencies:
'@shikijs/types': 3.13.0
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/engine-oniguruma@4.0.1':
dependencies:
'@shikijs/types': 4.0.1
'@shikijs/vscode-textmate': 10.0.2
'@shikijs/langs@3.13.0':
dependencies:
'@shikijs/types': 3.13.0
'@shikijs/langs@4.0.1':
dependencies:
'@shikijs/types': 4.0.1
'@shikijs/primitive@4.0.1':
dependencies:
'@shikijs/types': 4.0.1
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/themes@3.13.0':
dependencies:
'@shikijs/types': 3.13.0
'@shikijs/themes@4.0.1':
dependencies:
'@shikijs/types': 4.0.1
'@shikijs/transformers@1.6.0':
dependencies:
shiki: 1.6.0
@@ -25147,6 +25218,11 @@ snapshots:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/types@4.0.1':
dependencies:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
'@shikijs/vscode-textmate@10.0.2': {}
'@shuding/opentype.js@1.4.0-beta.0':
@@ -33806,6 +33882,12 @@ snapshots:
regex: 6.0.1
regex-recursion: 6.0.2
oniguruma-to-es@4.3.4:
dependencies:
oniguruma-parser: 0.12.1
regex: 6.0.1
regex-recursion: 6.0.2
oo-ascii-tree@1.98.0: {}
open@10.2.0:
@@ -36052,6 +36134,17 @@ snapshots:
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
shiki@4.0.1:
dependencies:
'@shikijs/core': 4.0.1
'@shikijs/engine-javascript': 4.0.1
'@shikijs/engine-oniguruma': 4.0.1
'@shikijs/langs': 4.0.1
'@shikijs/themes': 4.0.1
'@shikijs/types': 4.0.1
'@shikijs/vscode-textmate': 10.0.2
'@types/hast': 3.0.4
should-equal@2.0.0:
dependencies:
should-type: 1.4.0