product page concept.

This commit is contained in:
Alan Daniel committed 2026-03-11 15:55:21 -04:00
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&apos;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>
)
}
+14
View File
@@ -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 />} />
}
+19
View File
@@ -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.