mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 12:25:05 +03:00
product page concept.
This commit is contained in:
1 parent
f397548388
commit
7b370db080
14 files changed
+932
-3
No files matched your search
@@ -103,7 +103,7 @@ export function BuiltWithSupabaseSection() {
|
||||
{/* 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-end justify-between pt-20 pb-8">
|
||||
<div className="flex items-end justify-between pt-32 pb-8">
|
||||
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
|
||||
Kickstart your next project <br />
|
||||
<span className="text-foreground">with production ready templates</span>
|
||||
@@ -132,7 +132,7 @@ export function BuiltWithSupabaseSection() {
|
||||
target="_blank"
|
||||
className="group flex flex-col border-b md:border-b-0 md:border-r border-border last:border-r-0 last:border-b-0 hover:bg-surface-75/50 transition-colors"
|
||||
>
|
||||
<div className="flex-1 flex items-center justify-center px-10 pt-16 pb-0 overflow-hidden">
|
||||
<div className="flex-1 flex items-center justify-center px-6 pt-16 pb-0 overflow-hidden">
|
||||
{/* Browser frame */}
|
||||
<div className="w-full rounded-t-lg border border-b-0 border-border-stronger/50 shadow-lg -mb-[10%]">
|
||||
{/* Title bar */}
|
||||
@@ -191,7 +191,7 @@ export function BuiltWithSupabaseSection() {
|
||||
)}
|
||||
</div>
|
||||
<h4 className="text-foreground text-sm font-medium">{example.title}</h4>
|
||||
<p className="text-foreground-lighter text-xs mt-1 line-clamp-2">
|
||||
<p className="text-foreground-lighter text-sm mt-1 line-clamp-2">
|
||||
{example.description}
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { createHighlighter, type ThemeRegistration } from 'shiki'
|
||||
import { ApiSectionClient } from './ApiSectionClient'
|
||||
|
||||
// ── Shiki themes ────────────────────────────────────────────────────────────
|
||||
|
||||
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' } },
|
||||
],
|
||||
}
|
||||
|
||||
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' } },
|
||||
],
|
||||
}
|
||||
|
||||
// ── API examples ────────────────────────────────────────────────────────────
|
||||
|
||||
const API_EXAMPLES = [
|
||||
{
|
||||
title: 'Fetch records',
|
||||
code: `// Fetch all public rooms with their messages
|
||||
const { data: rooms } = await supabase
|
||||
.from('rooms')
|
||||
.select(\`
|
||||
id, name, created_at,
|
||||
messages ( id, text, user_id )
|
||||
\`)
|
||||
.eq('public', true)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(20)`,
|
||||
},
|
||||
{
|
||||
title: 'Insert with relations',
|
||||
code: `// Create a room and its first message
|
||||
const { data: room } = await supabase
|
||||
.from('rooms')
|
||||
.insert({ name: 'Design Team', public: false })
|
||||
.select()
|
||||
.single()
|
||||
|
||||
await supabase
|
||||
.from('messages')
|
||||
.insert({
|
||||
room_id: room.id,
|
||||
text: 'Welcome to the team channel!',
|
||||
user_id: session.user.id,
|
||||
})`,
|
||||
},
|
||||
{
|
||||
title: 'Update records',
|
||||
code: `// Update a room and return the updated row
|
||||
const { data: updated } = await supabase
|
||||
.from('rooms')
|
||||
.update({
|
||||
name: 'Engineering Team',
|
||||
public: false,
|
||||
updated_at: new Date().toISOString(),
|
||||
})
|
||||
.eq('id', roomId)
|
||||
.select()
|
||||
.single()`,
|
||||
},
|
||||
{
|
||||
title: 'Filter & paginate',
|
||||
code: `// Full-text search with pagination
|
||||
const { data, count } = await supabase
|
||||
.from('rooms')
|
||||
.select('id, name, member_count', { count: 'exact' })
|
||||
.textSearch('name', 'design')
|
||||
.gte('member_count', 5)
|
||||
.range(0, 9)
|
||||
.order('member_count', { ascending: false })`,
|
||||
},
|
||||
]
|
||||
|
||||
// ── Server component: pre-highlights all code ───────────────────────────────
|
||||
|
||||
export async function ApiSection() {
|
||||
const hl = await createHighlighter({
|
||||
themes: [supabaseDark, supabaseLight],
|
||||
langs: ['javascript'],
|
||||
})
|
||||
|
||||
const examples = API_EXAMPLES.map((example) => ({
|
||||
title: example.title,
|
||||
darkHtml: hl.codeToHtml(example.code, { lang: 'javascript', theme: 'supabase-dark' }),
|
||||
lightHtml: hl.codeToHtml(example.code, { lang: 'javascript', theme: 'supabase-light' }),
|
||||
}))
|
||||
|
||||
hl.dispose()
|
||||
|
||||
return <ApiSectionClient examples={examples} />
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
'use client'
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { Badge, cn } from 'ui'
|
||||
|
||||
type ApiExample = {
|
||||
title: string
|
||||
darkHtml: string
|
||||
lightHtml: string
|
||||
}
|
||||
|
||||
const LIBRARIES = [
|
||||
{ label: 'Javascript', href: '/docs/reference/javascript/introduction' },
|
||||
{ label: 'Flutter', href: '/docs/reference/dart/introduction' },
|
||||
{ label: 'Python', href: '/docs/reference/python/introduction' },
|
||||
{ label: 'C#', href: '/docs/reference/csharp/introduction' },
|
||||
{ label: 'Kotlin', href: '/docs/reference/kotlin/introduction' },
|
||||
{ label: 'Swift', href: '/docs/reference/swift/introduction' },
|
||||
]
|
||||
|
||||
export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
|
||||
const [activeIdx, setActiveIdx] = useState(0)
|
||||
const active = examples[activeIdx]
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0">
|
||||
{/* Left: title + libraries */}
|
||||
<div className="min-w-0 flex flex-col justify-between pt-16 px-6 lg:border-r border-border">
|
||||
<div>
|
||||
<div className="text-2xl md:text-4xl text-foreground-lighter">
|
||||
Never write an API
|
||||
<span className="block">
|
||||
<AnimatePresence mode="popLayout" initial={false}>
|
||||
<motion.span
|
||||
key={active.title}
|
||||
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"
|
||||
>
|
||||
again
|
||||
</motion.span>
|
||||
</AnimatePresence>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-foreground-lighter text-sm mt-4 max-w-md">
|
||||
We introspect your database and provide instant APIs. Focus on building your product,
|
||||
while Supabase handles the CRUD.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Available libraries */}
|
||||
<div className="flex flex-col gap-2 pb-4">
|
||||
<span className="text-foreground-muted text-sm">Available libraries</span>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{LIBRARIES.map((lib) => (
|
||||
<Link key={lib.label} href={lib.href} target="_blank" className="flex">
|
||||
<Badge>{lib.label}</Badge>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: tabs + code */}
|
||||
<div className="min-w-0 flex flex-col">
|
||||
{/* Tab row */}
|
||||
<div className="flex border-b border-border">
|
||||
{examples.map((example, index) => (
|
||||
<button
|
||||
key={example.title}
|
||||
onClick={() => setActiveIdx(index)}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center text-xs px-3 py-4 border-r border-border last:border-r-0 transition-colors whitespace-nowrap',
|
||||
index === activeIdx
|
||||
? 'bg-surface-75 text-foreground'
|
||||
: 'text-foreground-muted hover:text-foreground-light hover:bg-surface-75/50'
|
||||
)}
|
||||
>
|
||||
{example.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Code area */}
|
||||
<div className="relative h-[440px] overflow-auto">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={active.title}
|
||||
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="hidden dark:block [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
|
||||
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
|
||||
/>
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: active.lightHtml }}
|
||||
className="block dark:hidden [&_pre]:!bg-transparent [&_pre]:m-0 [&_pre]:p-6"
|
||||
style={{ fontSize: '0.8125rem', fontWeight: 500, lineHeight: 1.7 }}
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
<Link
|
||||
href="/docs/guides/database"
|
||||
className="absolute bottom-4 right-4 flex items-center gap-1.5 rounded-full bg-surface-100 border border-border px-3 py-1.5 text-xs text-foreground-light hover:text-foreground hover:bg-surface-200 transition-colors whitespace-nowrap"
|
||||
>
|
||||
Documentation
|
||||
<svg
|
||||
width={12}
|
||||
height={12}
|
||||
viewBox="0 0 12 12"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="shrink-0"
|
||||
>
|
||||
<path
|
||||
d="M3.5 2.5H9.5V8.5M9.5 2.5L2.5 9.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { BuiltWithSupabaseSection } from '~/app/(home)/_components/BuiltWithSupabaseSection'
|
||||
import { CTASection } from '~/app/(home)/_components/CTASection'
|
||||
import { FeaturesSection } from './FeaturesSection'
|
||||
import { Hero } from './Hero'
|
||||
import { HighlightsSection } from './HighlightsSection'
|
||||
import { QuoteSection } from './QuoteSection'
|
||||
import { SqlEditorSection } from './SqlEditorSection'
|
||||
import { TableEditorSection } from './TableEditorSection'
|
||||
import { VisualDemo } from './VisualDemo'
|
||||
|
||||
export function DatabaseContent({ apiSlot }: { apiSlot: React.ReactNode }) {
|
||||
return (
|
||||
<div className="overflow-x-clip">
|
||||
<section>
|
||||
<Hero />
|
||||
<VisualDemo />
|
||||
</section>
|
||||
<section className="border-t border-border">
|
||||
<FeaturesSection />
|
||||
</section>
|
||||
<section className="border-t border-border">
|
||||
<QuoteSection />
|
||||
</section>
|
||||
<section className="border-t border-border">
|
||||
<HighlightsSection />
|
||||
</section>
|
||||
<section className="border-t border-border">
|
||||
<TableEditorSection />
|
||||
</section>
|
||||
<section className="border-t border-border">
|
||||
<SqlEditorSection />
|
||||
</section>
|
||||
<section className="border-t border-border">
|
||||
{apiSlot}
|
||||
</section>
|
||||
<section className="border-t border-border">
|
||||
<BuiltWithSupabaseSection />
|
||||
</section>
|
||||
<section className="border-t border-border">
|
||||
<CTASection />
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { cn } from 'ui'
|
||||
|
||||
function SkeletonBar({ className }: { className?: string }) {
|
||||
return <div className={cn('rounded bg-foreground-muted/10', className)} />
|
||||
}
|
||||
|
||||
const FEATURES = [
|
||||
{
|
||||
title: 'Just Postgres',
|
||||
description: 'Every Supabase project is a dedicated Postgres database.',
|
||||
detail: '100% portable. Bring your existing Postgres database, or migrate away at any time.',
|
||||
visual: PostgresSkeleton,
|
||||
},
|
||||
{
|
||||
title: 'Secure by default',
|
||||
description: "Leveraging Postgres's proven Row Level Security.",
|
||||
detail: 'Integrated with JWT authentication which controls exactly what your users can access.',
|
||||
visual: RLSSkeleton,
|
||||
},
|
||||
{
|
||||
title: 'Realtime enabled',
|
||||
description: 'Data-change listeners over websockets.',
|
||||
detail: 'Subscribe and react to database changes, milliseconds after they happen.',
|
||||
visual: RealtimeSkeleton,
|
||||
},
|
||||
]
|
||||
|
||||
function PostgresSkeleton() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-4 w-full">
|
||||
<SkeletonBar className="h-2.5 w-20 bg-brand/20" />
|
||||
<div className="flex flex-col gap-1.5 mt-2">
|
||||
<SkeletonBar className="h-2 w-full" />
|
||||
<SkeletonBar className="h-2 w-3/4" />
|
||||
<SkeletonBar className="h-2 w-5/6" />
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-1.5 mt-3">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
<SkeletonBar key={i} className="h-6 rounded-sm" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RLSSkeleton() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-4 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<SkeletonBar className="h-3 w-3 rounded-sm bg-brand/20" />
|
||||
<SkeletonBar className="h-2 w-24" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 mt-2 border border-foreground-muted/10 rounded p-2">
|
||||
<SkeletonBar className="h-2 w-full bg-muted/70" />
|
||||
<SkeletonBar className="h-2 w-2/3" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 border border-foreground-muted/10 rounded p-2">
|
||||
<SkeletonBar className="h-2 w-5/6 bg-muted/70" />
|
||||
<SkeletonBar className="h-2 w-1/2" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RealtimeSkeleton() {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 p-4 w-full">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="h-2 w-2 rounded-full bg-brand/40 animate-pulse" />
|
||||
<SkeletonBar className="h-2 w-16 bg-brand/15" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5 mt-2">
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<SkeletonBar className="h-1.5 w-1.5 rounded-full shrink-0" />
|
||||
<SkeletonBar className={cn('h-2', i % 2 === 0 ? 'w-full' : 'w-3/4')} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function FeaturesSection() {
|
||||
return (
|
||||
<div>
|
||||
{/* Header */}
|
||||
<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="pt-32 pb-8">
|
||||
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
|
||||
Everything you need
|
||||
<br />
|
||||
<span className="text-foreground">from your database</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3-col grid */}
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3">
|
||||
{FEATURES.map((feature) => {
|
||||
const Visual = feature.visual
|
||||
return (
|
||||
<div
|
||||
key={feature.title}
|
||||
className="flex flex-col border-b md:border-b-0 md:border-r border-border last:border-r-0 last:border-b-0"
|
||||
>
|
||||
{/* Visual with overlaid title */}
|
||||
<div className="relative flex items-center justify-center h-[320px] border-b border-border">
|
||||
<Visual />
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2">
|
||||
<span className="bg-background/80 backdrop-blur-sm text-foreground text-xs font-medium px-2 py-1 rounded-full border border-border">
|
||||
{feature.title}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="px-6 py-6 flex flex-col gap-2">
|
||||
<h4 className="text-foreground text-sm font-medium">{feature.description}</h4>
|
||||
<p className="text-foreground-muted text-sm">{feature.detail}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import Link from 'next/link'
|
||||
import { Button } from 'ui'
|
||||
|
||||
export function Hero() {
|
||||
return (
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pt-32 pb-16 md:pt-40 md:pb-32">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
|
||||
<div className="flex flex-col gap-6 lg:gap-8">
|
||||
<h1 className="text-foreground text-3xl sm:text-5xl sm:leading-none">
|
||||
<span className="block">Postgres</span>
|
||||
<span className="text-foreground-lighter block">without the hassle</span>
|
||||
</h1>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button asChild size="medium">
|
||||
<Link href="https://supabase.com/dashboard">Start your project</Link>
|
||||
</Button>
|
||||
<Button asChild size="medium" type="default">
|
||||
<Link href="/docs/guides/database/overview">Documentation</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-foreground-lighter text-sm lg:text-base">
|
||||
Every Supabase project is a dedicated PostgreSQL database. 100% portable with no vendor
|
||||
lock-in.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Check } from 'lucide-react'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { Badge, Button } from 'ui'
|
||||
|
||||
const HIGHLIGHTS = [
|
||||
{
|
||||
title: 'Branching',
|
||||
badge: 'New',
|
||||
features: [
|
||||
'Branch your Supabase project',
|
||||
'Sync with your git branches',
|
||||
'Manage every Preview from the Dashboard',
|
||||
'Support for Vercel Previews',
|
||||
],
|
||||
cta: { label: 'Learn more', href: '/docs/guides/platform/branching' },
|
||||
image: {
|
||||
dark: '/images/product/database/branching.svg',
|
||||
light: '/images/product/database/branching-light.svg',
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Read Replicas',
|
||||
badge: 'New',
|
||||
features: [
|
||||
'Serve data closer to your users',
|
||||
'Provide data redundancy',
|
||||
'Run complex queries without affecting your primary database',
|
||||
'Distribute load across various databases',
|
||||
],
|
||||
cta: { label: 'Learn more', href: '/docs/guides/platform/read-replicas' },
|
||||
image: {
|
||||
dark: '/images/product/database/read-replicas.svg',
|
||||
light: '/images/product/database/read-replicas-light.svg',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
export function HighlightsSection() {
|
||||
return (
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
|
||||
{/* Header */}
|
||||
<div className="pt-32 pb-8 border-b border-border px-6">
|
||||
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
|
||||
Built for scale
|
||||
<br />
|
||||
<span className="text-foreground">with powerful new features</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2">
|
||||
{HIGHLIGHTS.map((highlight) => (
|
||||
<div
|
||||
key={highlight.title}
|
||||
className="relative flex flex-col border-b lg:border-b-0 lg:border-r border-border last:border-r-0 last:border-b-0 overflow-hidden min-h-[400px]"
|
||||
>
|
||||
<div className="relative z-10 flex flex-col justify-between h-full p-6 md:p-8 pr-0 md:pr-0">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-foreground text-lg font-medium">{highlight.title}</h4>
|
||||
{highlight.badge && <Badge variant="success">{highlight.badge}</Badge>}
|
||||
</div>
|
||||
<ul className="flex flex-col text-foreground-lighter text-sm gap-1.5">
|
||||
{highlight.features.map((feature) => (
|
||||
<li key={feature} className="flex items-start gap-2">
|
||||
<Check className="w-3.5 h-3.5 mt-0.5 shrink-0 stroke-2 text-brand" />
|
||||
<span>{feature}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Button type="default" size="small" asChild>
|
||||
<Link href={highlight.cta.href}>{highlight.cta.label}</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Decorative image */}
|
||||
<div className="hidden sm:flex lg:hidden xl:flex absolute top-0 right-0 bottom-0 items-end h-full opacity-50 dark:opacity-20 select-none">
|
||||
<Image
|
||||
src={highlight.image.dark}
|
||||
alt={highlight.title}
|
||||
width={296}
|
||||
height={275}
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
<Image
|
||||
src={highlight.image.light}
|
||||
alt={highlight.title}
|
||||
width={296}
|
||||
height={275}
|
||||
className="dark:hidden"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
export function QuoteSection() {
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 border-x border-border bg-surface-200">
|
||||
<div className="flex flex-col items-center text-center gap-8 md:gap-12 py-16 md:py-24">
|
||||
<q className="text-2xl xl:text-4xl max-w-screen-lg text-foreground-lighter text-balance">
|
||||
You can have a really great product, but you need to want to work with the people behind
|
||||
it. <span className="text-brand">With Supabase, we always felt very aligned.</span>
|
||||
</q>
|
||||
|
||||
<Link href="/customers/next-door-lending" className="hover:opacity-90 transition-opacity">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<Image
|
||||
draggable={false}
|
||||
src="/images/blog/avatars/howard-haynes.webp"
|
||||
alt="Howard Haynes, CPO at Next Door Lending"
|
||||
className="w-12 h-12 rounded-full overflow-hidden object-cover"
|
||||
width={48}
|
||||
height={48}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<span className="text-foreground text-sm">Howard Haynes</span>
|
||||
<span className="text-foreground-lighter text-xs">CPO at Next Door Lending</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import { useState } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
const TABS = [
|
||||
{
|
||||
label: 'SQL Editor built in',
|
||||
title: 'Full SQL',
|
||||
description:
|
||||
'Create tables, functions, join tables — a full SQL editor built into the dashboard.',
|
||||
image: '/images/product/database/sql-view/sql-editor.png',
|
||||
},
|
||||
{
|
||||
label: 'Monaco editor',
|
||||
title: 'Monaco editor',
|
||||
description: 'Built-in Monaco editor, with rich validation and autocomplete.',
|
||||
image: '/images/product/database/sql-view/manaco-editor.png',
|
||||
},
|
||||
{
|
||||
label: 'Favorite queries',
|
||||
title: 'Favorite queries',
|
||||
description: 'Save your favorite queries to use again and again.',
|
||||
image: '/images/product/database/sql-view/favorites.png',
|
||||
},
|
||||
{
|
||||
label: 'Select and Export',
|
||||
title: 'Export to CSV',
|
||||
description: 'Any output can be exported into a CSV.',
|
||||
image: '/images/product/database/sql-view/export.png',
|
||||
},
|
||||
]
|
||||
|
||||
export function SqlEditorSection() {
|
||||
const [activeIdx, setActiveIdx] = useState(0)
|
||||
const active = TABS[activeIdx]
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-32 pb-8 border-b border-border">
|
||||
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
|
||||
The power of
|
||||
<br />
|
||||
<span className="text-foreground">SQL Editor</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2">
|
||||
{/* Left: tabs + content */}
|
||||
<div className="flex flex-col border-b lg:border-b-0 lg:border-r border-border">
|
||||
{/* Tab row */}
|
||||
<div className="flex border-b border-border">
|
||||
{TABS.map((tab, index) => (
|
||||
<button
|
||||
key={tab.label}
|
||||
onClick={() => setActiveIdx(index)}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center text-xs px-3 py-4 border-r border-border last:border-r-0 transition-colors whitespace-nowrap',
|
||||
index === activeIdx
|
||||
? 'bg-surface-75 text-foreground'
|
||||
: 'text-foreground-muted hover:text-foreground-light hover:bg-surface-75/50'
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab content + tweet */}
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
{/* Active tab content */}
|
||||
<div className="relative h-[160px] px-6 flex items-center">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={active.label}
|
||||
initial={{ opacity: 0, filter: 'blur(2px)' }}
|
||||
animate={{ opacity: 1, filter: 'blur(0px)', transition: { duration: 0.2 } }}
|
||||
exit={{ opacity: 0, filter: 'blur(2px)', transition: { duration: 0.1 } }}
|
||||
className="absolute inset-x-6 flex flex-col gap-3"
|
||||
>
|
||||
<h4 className="text-foreground text-lg font-medium">{active.title}</h4>
|
||||
<p className="text-foreground-lighter text-sm max-w-md">{active.description}</p>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Tweet card */}
|
||||
<div className="border-t border-border p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<img
|
||||
src="/images/twitter-profiles/rLgwUZSB_400x400.jpg"
|
||||
alt="@jim_bisenius"
|
||||
className="w-12 h-12 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-foreground text-sm font-medium">@jim_bisenius</span>
|
||||
<p className="text-foreground-lighter text-sm">
|
||||
@MongoDB or @MySQL?!?! Please, let me introduce you to @supabase and the
|
||||
wonderful world of @Postgres before it's too late!!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: visual */}
|
||||
<div className="relative h-[400px] flex items-center justify-center overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={active.label}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { duration: 0.2, delay: 0.05 } }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.1 } }}
|
||||
className="absolute inset-0 flex items-center justify-center p-6 -mb-16"
|
||||
>
|
||||
<Image
|
||||
src={active.image}
|
||||
alt={active.title}
|
||||
width={600}
|
||||
height={400}
|
||||
className="rounded-lg border border-border shadow-sm object-contain"
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import Image from 'next/image'
|
||||
import { useState } from 'react'
|
||||
import { cn } from 'ui'
|
||||
|
||||
const TABS = [
|
||||
{
|
||||
label: 'Spreadsheet like editing',
|
||||
title: 'The simplicity of a spreadsheet',
|
||||
description: 'Add, edit, and update your data with the simplicity of a no-code tool.',
|
||||
image: '/images/product/database/table-view/spreadsheet-interface.png',
|
||||
},
|
||||
{
|
||||
label: 'Create tables',
|
||||
title: 'Create tables',
|
||||
description:
|
||||
'Add tables, columns and rows right in the dashboard. Without a single line of SQL.',
|
||||
image: '/images/product/database/table-view/create-table.png',
|
||||
},
|
||||
{
|
||||
label: 'Set up foreign keys',
|
||||
title: 'Set up foreign keys',
|
||||
description:
|
||||
'Build connections throughout your data with the full power of a Relational Database.',
|
||||
image: '/images/product/database/table-view/foreign-keys.png',
|
||||
},
|
||||
{
|
||||
label: 'Select and Export',
|
||||
title: 'Select and Export',
|
||||
description: 'Pick the rows you want and export them into a CSV.',
|
||||
image: '/images/product/database/table-view/export.png',
|
||||
},
|
||||
]
|
||||
|
||||
export function TableEditorSection() {
|
||||
const [activeIdx, setActiveIdx] = useState(0)
|
||||
const active = TABS[activeIdx]
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] border-x border-border">
|
||||
{/* Header */}
|
||||
<div className="px-6 pt-32 pb-8 border-b border-border">
|
||||
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
|
||||
Easy to use
|
||||
<br />
|
||||
<span className="text-foreground">Table Editor</span>
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2">
|
||||
{/* Left: tabs + content */}
|
||||
<div className="flex flex-col border-b lg:border-b-0 lg:border-r border-border">
|
||||
{/* Tab row */}
|
||||
<div className="flex border-b border-border">
|
||||
{TABS.map((tab, index) => (
|
||||
<button
|
||||
key={tab.label}
|
||||
onClick={() => setActiveIdx(index)}
|
||||
className={cn(
|
||||
'flex-1 flex items-center justify-center text-xs px-3 py-4 border-r border-border last:border-r-0 transition-colors whitespace-nowrap',
|
||||
index === activeIdx
|
||||
? 'bg-surface-75 text-foreground'
|
||||
: 'text-foreground-muted hover:text-foreground-light hover:bg-surface-75/50'
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab content + tweet */}
|
||||
<div className="flex-1 flex flex-col justify-between">
|
||||
{/* Active tab content */}
|
||||
<div className="relative h-[160px] px-6 flex items-center">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={active.label}
|
||||
initial={{ opacity: 0, filter: 'blur(2px)' }}
|
||||
animate={{ opacity: 1, filter: 'blur(0px)', transition: { duration: 0.2 } }}
|
||||
exit={{ opacity: 0, filter: 'blur(2px)', transition: { duration: 0.1 } }}
|
||||
className="absolute inset-x-6 flex flex-col gap-3"
|
||||
>
|
||||
<h4 className="text-foreground text-lg font-medium">{active.title}</h4>
|
||||
<p className="text-foreground-lighter text-sm max-w-md">{active.description}</p>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Tweet card */}
|
||||
<div className="border-t border-border p-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<img
|
||||
src="/images/twitter-profiles/v6citnk33y2wpeyzrq05_400x400.jpeg"
|
||||
alt="@Elsolo244"
|
||||
className="w-12 h-12 rounded-full object-cover shrink-0"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-foreground text-sm font-medium">@Elsolo244</span>
|
||||
<p className="text-foreground-lighter text-sm">
|
||||
Where has @supabase been all my life?
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: visual */}
|
||||
<div className="relative h-[400px] flex items-center justify-center overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={active.label}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1, transition: { duration: 0.2, delay: 0.05 } }}
|
||||
exit={{ opacity: 0, transition: { duration: 0.1 } }}
|
||||
className="absolute inset-0 flex items-center justify-center p-6 -mb-16"
|
||||
>
|
||||
<Image
|
||||
src={active.image}
|
||||
alt={active.title}
|
||||
width={600}
|
||||
height={400}
|
||||
className="rounded-lg border border-border shadow-sm object-contain"
|
||||
/>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
export function VisualDemo() {
|
||||
return (
|
||||
<div className="border-t border-border relative">
|
||||
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 pb-16 md:pb-24">
|
||||
<div className="-translate-y-16 relative w-full aspect-[16/9]">
|
||||
{/* Gradient blob behind card */}
|
||||
<div
|
||||
className="pointer-events-none absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 h-[105%] w-[102%] rounded-[300px] blur-[50px]"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'linear-gradient(90deg, rgba(62, 207, 142, 0.2) 0%, rgba(151, 62, 198, 0.2) 50%, rgba(62, 207, 142, 0.2) 100%)',
|
||||
}}
|
||||
/>
|
||||
{/* Card */}
|
||||
<div
|
||||
className="relative z-10 w-full h-full rounded-xl border border-border bg-surface-75 flex items-center justify-center"
|
||||
style={{
|
||||
boxShadow:
|
||||
'0 0 0 0.5px var(--shadow-border-color), 0 149px 199px 0 rgba(0,0,0,0.07), 0 62.249px 83.137px 0 rgba(0,0,0,0.05), 0 33.281px 44.449px 0 rgba(0,0,0,0.04), 0 18.657px 24.918px 0 rgba(0,0,0,0.04), 0 9.909px 13.234px 0 rgba(0,0,0,0.03), 0 4.123px 5.507px 0 rgba(0,0,0,0.02)',
|
||||
}}
|
||||
>
|
||||
<span className="text-foreground-muted text-sm">Visual demo</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
import { ApiSection } from './_components/ApiSection'
|
||||
import { DatabaseContent } from './_components/DatabaseContent'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Database | Supabase',
|
||||
description:
|
||||
'Every Supabase project is a dedicated PostgreSQL database. 100% portable with no vendor lock-in.',
|
||||
}
|
||||
|
||||
export default function DatabasePage() {
|
||||
return <DatabaseContent apiSlot={<ApiSection />} />
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import Footer from '~/components/Footer'
|
||||
import Nav from '~/components/Nav'
|
||||
import { useForceDeepDark } from 'lib/theme.utils'
|
||||
|
||||
export default function ProductsLayout({ children }: { children: React.ReactNode }) {
|
||||
useForceDeepDark()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Nav hideNavbar={false} />
|
||||
<div className="relative w-full [--container-max-w:75rem]">
|
||||
<main className="relative min-h-screen">{children}</main>
|
||||
</div>
|
||||
<Footer />
|
||||
</>
|
||||
)
|
||||
}
|
||||
File renamed without changes.
Reference in new issue
Block a user