improve some sections

This commit is contained in:
Alan Daniel committed 2026-03-30 22:07:57 -04:00
1 parent bf88e63d7b
commit d902ddaa46
6 files changed
+700 -341

No files matched your search

@@ -47,12 +47,26 @@ const supabaseLight: ThemeRegistration = {
],
}
// ── API examples ────────────────────────────────────────────────────────────
// ── Language → Shiki lang mapping ───────────────────────────────────────────
const LANG_MAP: Record<string, string> = {
JavaScript: 'javascript',
Flutter: 'dart',
Python: 'python',
'C#': 'csharp',
Kotlin: 'kotlin',
Swift: 'swift',
}
// ── API examples per language ───────────────────────────────────────────────
const API_EXAMPLES = [
{
icon: 'Search' as const,
title: 'Fetch records',
code: `// Fetch all public rooms with their messages
description: 'Query tables with joins, filters, and ordering in a single request.',
code: {
JavaScript: `// Fetch all public rooms with their messages
const { data: rooms } = await supabase
.from('rooms')
.select(\`
@@ -62,10 +76,55 @@ const { data: rooms } = await supabase
.eq('public', true)
.order('created_at', { ascending: false })
.limit(20)`,
Flutter: `// Fetch all public rooms with their messages
final 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);`,
Python: `# Fetch all public rooms with their messages
rooms = supabase.table("rooms") \\
.select("id, name, created_at, messages ( id, text, user_id )") \\
.eq("public", True) \\
.order("created_at", desc=True) \\
.limit(20) \\
.execute()`,
'C#': `// Fetch all public rooms with their messages
var rooms = await supabase
.From<Room>()
.Select("id, name, created_at, messages ( id, text, user_id )")
.Filter("public", Operator.Equals, "true")
.Order("created_at", Ordering.Descending)
.Limit(20)
.Get();`,
Kotlin: `// Fetch all public rooms with their messages
val rooms = supabase.from("rooms")
.select(Columns.raw("""
id, name, created_at,
messages ( id, text, user_id )
""")) {
filter { eq("public", true) }
order("created_at", Order.DESCENDING)
limit(20)
}`,
Swift: `// Fetch all public rooms with their messages
let rooms: [Room] = try await supabase
.from("rooms")
.select("id, name, created_at, messages ( id, text, user_id )")
.eq("public", value: true)
.order("created_at", ascending: false)
.limit(20)
.execute()
.value`,
},
},
{
icon: 'Plus' as const,
title: 'Insert with relations',
code: `// Create a room and its first message
description: 'Create rows across related tables with automatic foreign key resolution.',
code: {
JavaScript: `// Create a room and its first message
const { data: room } = await supabase
.from('rooms')
.insert({ name: 'Design Team', public: false })
@@ -79,10 +138,82 @@ await supabase
text: 'Welcome to the team channel!',
user_id: session.user.id,
})`,
Flutter: `// Create a room and its first message
final 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,
});`,
Python: `# Create a room and its first message
room = supabase.table("rooms") \\
.insert({"name": "Design Team", "public": False}) \\
.execute()
supabase.table("messages") \\
.insert({
"room_id": room.data[0]["id"],
"text": "Welcome to the team channel!",
"user_id": session.user.id,
}) \\
.execute()`,
'C#': `// Create a room and its first message
var room = await supabase
.From<Room>()
.Insert(new Room { Name = "Design Team", Public = false })
.Single();
await supabase
.From<Message>()
.Insert(new Message {
RoomId = room.Id,
Text = "Welcome to the team channel!",
UserId = session.User.Id,
});`,
Kotlin: `// Create a room and its first message
val room = supabase.from("rooms")
.insert(Room(name = "Design Team", public = false)) {
select()
}.decodeSingle<Room>()
supabase.from("messages")
.insert(Message(
roomId = room.id,
text = "Welcome to the team channel!",
userId = session.user.id,
))`,
Swift: `// Create a room and its first message
let room: Room = try await supabase
.from("rooms")
.insert(["name": "Design Team", "public": false])
.select()
.single()
.execute()
.value
try await supabase
.from("messages")
.insert([
"room_id": room.id,
"text": "Welcome to the team channel!",
"user_id": session.user.id,
])
.execute()`,
},
},
{
icon: 'ArrowDownUp' as const,
title: 'Update records',
code: `// Update a room and return the updated row
description: 'Modify rows with conditions and return the updated data in one call.',
code: {
JavaScript: `// Update a room and return the updated row
const { data: updated } = await supabase
.from('rooms')
.update({
@@ -93,10 +224,66 @@ const { data: updated } = await supabase
.eq('id', roomId)
.select()
.single()`,
Flutter: `// Update a room and return the updated row
final updated = await supabase
.from('rooms')
.update({
'name': 'Engineering Team',
'public': false,
'updated_at': DateTime.now().toIso8601String(),
})
.eq('id', roomId)
.select()
.single();`,
Python: `# Update a room and return the updated row
updated = supabase.table("rooms") \\
.update({
"name": "Engineering Team",
"public": False,
"updated_at": datetime.now().isoformat(),
}) \\
.eq("id", room_id) \\
.execute()`,
'C#': `// Update a room and return the updated row
var updated = await supabase
.From<Room>()
.Filter("id", Operator.Equals, roomId)
.Set(x => x.Name, "Engineering Team")
.Set(x => x.Public, false)
.Set(x => x.UpdatedAt, DateTime.UtcNow)
.Update()
.Single();`,
Kotlin: `// Update a room and return the updated row
val updated = supabase.from("rooms")
.update({
Room::name setTo "Engineering Team"
Room::public setTo false
Room::updatedAt setTo Clock.System.now()
}) {
filter { eq("id", roomId) }
select()
}.decodeSingle<Room>()`,
Swift: `// Update a room and return the updated row
let updated: Room = try await supabase
.from("rooms")
.update([
"name": "Engineering Team",
"public": false,
"updated_at": ISO8601DateFormatter().string(from: Date()),
])
.eq("id", value: roomId)
.select()
.single()
.execute()
.value`,
},
},
{
icon: 'Filter' as const,
title: 'Filter & paginate',
code: `// Full-text search with pagination
description: 'Full-text search, range queries, and cursor-based pagination built in.',
code: {
JavaScript: `// Full-text search with pagination
const { data, count } = await supabase
.from('rooms')
.select('id, name, member_count', { count: 'exact' })
@@ -104,6 +291,51 @@ const { data, count } = await supabase
.gte('member_count', 5)
.range(0, 9)
.order('member_count', { ascending: false })`,
Flutter: `// Full-text search with pagination
final response = await supabase
.from('rooms')
.select('id, name, member_count', const FetchOptions(count: CountOption.exact))
.textSearch('name', 'design')
.gte('member_count', 5)
.range(0, 9)
.order('member_count', ascending: false);`,
Python: `# Full-text search with pagination
response = supabase.table("rooms") \\
.select("id, name, member_count", count="exact") \\
.text_search("name", "design") \\
.gte("member_count", 5) \\
.range(0, 9) \\
.order("member_count", desc=True) \\
.execute()`,
'C#': `// Full-text search with pagination
var response = await supabase
.From<Room>()
.Select("id, name, member_count")
.Filter("name", Operator.FTS, "design")
.Filter("member_count", Operator.GreaterThanOrEqual, "5")
.Range(0, 9)
.Order("member_count", Ordering.Descending)
.Get();`,
Kotlin: `// Full-text search with pagination
val response = supabase.from("rooms")
.select(Columns.list("id", "name", "member_count")) {
filter {
textSearch("name", "design")
gte("member_count", 5)
}
range(0L..9L)
order("member_count", Order.DESCENDING)
}`,
Swift: `// Full-text search with pagination
let response = try await supabase
.from("rooms")
.select("id, name, member_count", head: false, count: .exact)
.textSearch("name", query: "design")
.gte("member_count", value: 5)
.range(from: 0, to: 9)
.order("member_count", ascending: false)
.execute()`,
},
},
]
@@ -112,13 +344,22 @@ const { data, count } = await supabase
export async function ApiSection() {
const hl = await createHighlighter({
themes: [supabaseDark, supabaseLight],
langs: ['javascript'],
langs: Object.values(LANG_MAP),
})
const examples = API_EXAMPLES.map((example) => ({
icon: example.icon,
title: example.title,
darkHtml: hl.codeToHtml(example.code, { lang: 'javascript', theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(example.code, { lang: 'javascript', theme: 'supabase-light' }),
description: example.description,
languages: Object.fromEntries(
Object.entries(example.code).map(([lang, code]) => [
lang,
{
darkHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-dark' }),
lightHtml: hl.codeToHtml(code, { lang: LANG_MAP[lang], theme: 'supabase-light' }),
},
])
),
}))
hl.dispose()
@@ -1,124 +1,182 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { ArrowDownUp, ChevronDown, Filter, Plus, Search } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { Badge, cn } from 'ui'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { Search, Plus, ArrowDownUp, Filter } as const
type IconName = keyof typeof ICONS
type ApiExample = {
icon: IconName
title: string
darkHtml: string
lightHtml: string
description: string
languages: Record
}
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' },
]
const LANGUAGES = ['JavaScript', 'Flutter', 'Python', 'C#', 'Kotlin', 'Swift']
const INTERVAL_DURATION = 8000
export function ApiSectionClient({ examples }: { examples: ApiExample[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [activeLang, setActiveLang] = useState('JavaScript')
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = examples[activeIdx]
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % examples.length)
}
}, [progress, examples.length])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div className="py-24">
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16">
{/* Left: title + libraries */}
<div className="min-w-0 flex flex-col justify-between pt-4">
<div className="flex flex-col gap-2">
<h3 className="text-2xl md:text-4xl text-foreground-lighter">
Never write an API
<span className="block text-foreground">again</span>
</h3>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Never write an API
<br />
<span className="text-foreground">again</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
We introspect your database and provide instant APIs. Focus on building your product,
while Supabase handles the CRUD.
</p>
</div>
</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 border border-border rounded-md overflow-clip">
{/* Tab row */}
<div className="flex border-b border-border">
{examples.map((example, index) => (
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tab cards */}
<div className="flex flex-col gap-1 items-start justify-end">
{examples.map((example, index) => {
const isActive = index === activeIdx
const Icon = ICONS[example.icon]
return (
<button
key={example.title}
onClick={() => setActiveIdx(index)}
onClick={() => handleTabClick(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'
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{example.title}
</button>
))}
)
})}
</div>
{/* Right: code block */}
<div className="relative min-h-[440px] overflow-auto border border-border rounded-md">
{/* Language switcher */}
<div className="absolute top-3 right-4 z-10 flex items-center">
<div className="relative flex items-center gap-1 text-foreground-muted text-xs">
<select
value={activeLang}
onChange={(e) => setActiveLang(e.target.value)}
className="appearance-none bg-transparent text-foreground-light text-xs pr-4 cursor-pointer border-none outline-none shadow-none focus:border-none focus:outline-none focus:shadow-none focus:ring-0"
>
{LANGUAGES.map((lang) => (
<option key={lang} value={lang}>
{lang}
</option>
))}
</select>
<ChevronDown size={12} className="absolute right-0 pointer-events-none" />
</div>
</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"
<AnimatePresence mode="wait">
<motion.div
key={`${active.title}-${activeLang}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
>
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
dangerouslySetInnerHTML={{ __html: active.languages[activeLang]?.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.languages[activeLang]?.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>
@@ -1,118 +1,140 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { Code, FileDown, Heart, MousePointerClick } from 'lucide-react'
import Image from 'next/image'
import { useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { Code, MousePointerClick, Heart, FileDown } as const
type IconName = keyof typeof ICONS
const TABS = [
{
label: 'SQL Editor built in',
title: 'Full SQL',
icon: 'Code' as IconName,
label: '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',
},
{
icon: 'MousePointerClick' as IconName,
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',
},
{
icon: 'Heart' as IconName,
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',
icon: 'FileDown' as IconName,
label: 'Export to CSV',
description: 'Any output can be exported into a CSV.',
image: '/images/product/database/sql-view/export.png',
},
]
const INTERVAL_DURATION = 8000
export function SqlEditorSection() {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = TABS[activeIdx]
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % TABS.length)
}
}, [progress])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div className="py-24 flex flex-col gap-16">
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<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 className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Write and run queries with a
<br />
<span className="text-foreground">full SQL Editor</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Write, run, and save SQL queries directly from the dashboard with full autocomplete and
syntax highlighting.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="border border-border rounded-md overflow-clip">
<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>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tabs */}
<div className="flex flex-col gap-1 items-start justify-end">
{TABS.map((tab, index) => {
const isActive = index === activeIdx
const Icon = ICONS[tab.icon]
return (
<button
key={tab.label}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{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 px-6 py-4 bg-surface-200">
<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-[480px] flex items-center justify-center overflow-hidden">
{/* Right: image + footer */}
<div className="flex flex-col border border-border rounded-md overflow-clip">
<div className="relative h-[400px] shrink-0 overflow-hidden flex items-end justify-center">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
@@ -123,14 +145,36 @@ export function SqlEditorSection() {
>
<Image
src={active.image}
alt={active.title}
alt={active.label}
width={600}
height={400}
className="rounded-lg border border-border shadow-sm object-contain -mb-4"
className="rounded-lg object-contain -mb-4"
/>
</motion.div>
</AnimatePresence>
</div>
{/* Footer: active tab info */}
<div className="border-t border-border px-6 py-4 flex items-center gap-8 justify-between bg-surface-100">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="flex flex-col gap-1"
>
<h4 className="text-sm font-medium text-foreground flex items-center gap-2">
{(() => {
const Icon = ICONS[active.icon]
return <Icon size={14} strokeWidth={2} />
})()}
{active.label}
</h4>
<p className="text-sm text-foreground-lighter">{active.description}</p>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
@@ -1,118 +1,140 @@
'use client'
import { AnimatePresence, motion } from 'framer-motion'
import { FileDown, Link2, Table, TableProperties } from 'lucide-react'
import Image from 'next/image'
import { useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { useInView } from 'react-intersection-observer'
import { cn } from 'ui'
const ICONS = { TableProperties, Table, Link2, FileDown } as const
type IconName = keyof typeof ICONS
const TABS = [
{
label: 'Spreadsheet like editing',
title: 'The simplicity of a spreadsheet',
icon: 'TableProperties' as IconName,
label: 'Spreadsheet editing',
description: 'Add, edit, and update your data with the simplicity of a no-code tool.',
image: '/images/product/database/table-view/spreadsheet-interface.png',
},
{
icon: 'Table' as IconName,
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.',
icon: 'Link2' as IconName,
label: 'Foreign keys',
description: 'Build connections across tables with the full power of relational data.',
image: '/images/product/database/table-view/foreign-keys.png',
},
{
label: 'Select and Export',
title: 'Select and Export',
icon: 'FileDown' as IconName,
label: 'Export to CSV',
description: 'Pick the rows you want and export them into a CSV.',
image: '/images/product/database/table-view/export.png',
},
]
const INTERVAL_DURATION = 8000
export function TableEditorSection() {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = TABS[activeIdx]
const clearTimer = useCallback(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current)
intervalRef.current = null
}
}, [])
const startTimer = useCallback(() => {
clearTimer()
setProgress(0)
const updateFrequency = 30
const increment = (100 / INTERVAL_DURATION) * updateFrequency
intervalRef.current = setInterval(() => {
setProgress((prev) => {
const next = prev + increment
if (next >= 100) return 100
return next
})
}, updateFrequency)
}, [clearTimer])
useEffect(() => {
if (progress >= 100) {
setActiveIdx((prev) => (prev + 1) % TABS.length)
}
}, [progress])
useEffect(() => {
if (inView) {
startTimer()
} else {
clearTimer()
}
return clearTimer
}, [inView, activeIdx, startTimer, clearTimer])
const handleTabClick = (index: number) => {
setActiveIdx(index)
startTimer()
}
return (
<div className="py-24 flex flex-col gap-16">
<div ref={inViewRef} className="py-24 flex flex-col gap-16">
{/* Header */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<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 className="grid grid-cols-1 lg:grid-cols-2 gap-8 lg:gap-16 items-end">
<h3 className="text-2xl md:text-4xl text-foreground-lighter max-w-xl">
Manage your data with a
<br />
<span className="text-foreground">built-in Table Editor</span>
</h3>
<p className="text-foreground-lighter text-sm lg:text-base">
Manage your data with a spreadsheet-like interface. Create tables, set up relationships,
and export — no SQL required.
</p>
</div>
</div>
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="border border-border rounded-md overflow-clip">
<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>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tabs */}
<div className="flex flex-col gap-1 items-start justify-end">
{TABS.map((tab, index) => {
const isActive = index === activeIdx
const Icon = ICONS[tab.icon]
return (
<button
key={tab.label}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl font-medium transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{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 px-6 py-4 bg-surface-200">
<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-[480px] flex items-center justify-center overflow-hidden">
{/* Right: image + footer */}
<div className="flex flex-col border border-border rounded-md overflow-clip">
<div className="relative h-[400px] shrink-0 overflow-hidden flex items-end justify-center">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
@@ -123,14 +145,36 @@ export function TableEditorSection() {
>
<Image
src={active.image}
alt={active.title}
alt={active.label}
width={600}
height={400}
className="rounded-lg border border-border shadow-sm object-contain -mb-4"
className="rounded-lg object-contain -mb-4"
/>
</motion.div>
</AnimatePresence>
</div>
{/* Footer: active tab info */}
<div className="border-t border-border px-6 py-4 flex items-center gap-8 justify-between bg-surface-100">
<AnimatePresence mode="wait">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="flex flex-col gap-1"
>
<h4 className="text-sm font-medium text-foreground flex items-center gap-2">
{(() => {
const Icon = ICONS[active.icon]
return <Icon size={14} strokeWidth={2} />
})()}
{active.label}
</h4>
<p className="text-sm text-foreground-lighter">{active.description}</p>
</motion.div>
</AnimatePresence>
</div>
</div>
</div>
</div>
@@ -1,4 +1,3 @@
import Link from 'next/link'
import { createHighlighter, type ThemeRegistration } from 'shiki'
import { IntegratesSectionClient } from './IntegratesSectionClient'
@@ -100,17 +99,7 @@ execute function "supabase_functions"."http_request"(
{
icon: 'Shield' as const,
label: 'Works with Supabase Auth',
paragraph: (
<>
Edge Functions work seamlessly with{' '}
<Link
href="https://supabase.com/docs/guides/functions/auth"
className="underline hover:text-foreground-light transition-colors"
>
Supabase Auth
</Link>
</>
),
paragraph: 'Verify users and enforce permissions directly inside your Edge Functions',
lang: 'javascript' as const,
code: `import { createClient } from 'jsr:@supabase/supabase-js@2'
@@ -134,17 +123,7 @@ Deno.serve(async (req: Request) => {
{
icon: 'HardDrive' as const,
label: 'Works with Supabase Storage',
paragraph: (
<>
Edge Functions work seamlessly with{' '}
<Link
href="https://supabase.com/docs/guides/functions/storage-caching"
className="underline hover:text-foreground-light transition-colors"
>
Supabase Storage
</Link>
</>
),
paragraph: 'Upload, transform, and serve files with built-in caching from your Edge Functions',
lang: 'javascript' as const,
code: `import { createClient } from 'jsr:@supabase/supabase-js@2'
@@ -24,7 +24,7 @@ const INTERVAL_DURATION = 6000 // ms per tab
export function IntegratesSectionClient({ useCases }: { useCases: UseCase[] }) {
const [activeIdx, setActiveIdx] = useState(0)
const [progress, setProgress] = useState(0)
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null)
const intervalRef = useRef<ReturnType | null>(null)
const { ref: inViewRef, inView } = useInView({ threshold: 0.3 })
const active = useCases[activeIdx]
@@ -82,63 +82,34 @@ export function IntegratesSectionClient({ useCases }: { useCases: UseCase[] }) {
{/* Content */}
<div className="mx-auto max-w-[var(--container-max-w,75rem)] px-6 w-full">
<div className="border border-border rounded-md overflow-clip">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-0">
{/* Left: vertical tabs */}
<div className="flex flex-col">
{useCases.map((useCase, index) => {
const isActive = index === activeIdx
return (
<button
key={useCase.label}
onClick={() => handleTabClick(index)}
className={cn(
'relative text-left flex flex-col gap-1 px-6 py-5 transition-colors',
isActive
? 'bg-surface-75 text-foreground'
: 'text-foreground-muted hover:text-foreground-light hover:bg-surface-75/50'
)}
>
<h4 className="text-sm font-medium flex items-center gap-2">
{(() => {
const Icon = ICONS[useCase.icon]
return <Icon size={14} strokeWidth={2} />
})()}
{useCase.label}
</h4>
<p
className={cn(
'text-sm transition-colors',
isActive ? 'text-foreground-lighter' : 'text-foreground-muted'
)}
>
{useCase.paragraph}
</p>
{/* Progress bar — hide on last item to avoid doubling with section border */}
<div
className={cn(
'absolute bottom-0 left-0 right-0 h-[1px] bg-border-strong overflow-hidden',
index === useCases.length - 1 && 'hidden'
)}
>
{isActive && (
<motion.div
className={cn(
'absolute inset-0 w-full right-full bg-foreground-muted h-full transition-opacity',
progress > 99.5 ? 'opacity-0' : 'opacity-100'
)}
style={{ x: `${progress - 100}%` }}
/>
)}
</div>
</button>
)
})}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Left: tab cards */}
<div className="flex flex-col gap-1 items-start justify-end">
{useCases.map((useCase, index) => {
const isActive = index === activeIdx
const Icon = ICONS[useCase.icon]
return (
<button
key={useCase.label}
onClick={() => handleTabClick(index)}
className={cn(
'text-left flex items-center gap-3 py-2 text-2xl transition-colors',
isActive
? 'text-foreground'
: 'text-foreground-muted hover:text-foreground-light'
)}
>
<Icon size={22} strokeWidth={1.5} />
{useCase.label}
</button>
)
})}
</div>
{/* Right: code area */}
<div className="relative min-h-[440px] overflow-auto lg:border-l border-border">
<AnimatePresence mode="wait">
{/* Right: code area */}
<div className="flex flex-col border border-border rounded-md overflow-clip">
<div className="relative h-[440px] shrink-0 overflow-auto">
<AnimatePresence mode="popLayout">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
@@ -157,9 +128,31 @@ export function IntegratesSectionClient({ useCases }: { useCases: UseCase[] }) {
/>
</motion.div>
</AnimatePresence>
</div>
{/* Footer: active tab info */}
<div className="border-t border-border px-6 py-4 flex items-center gap-8 justify-between bg-surface-75">
<AnimatePresence mode="popLayout">
<motion.div
key={active.label}
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { duration: 0.15, delay: 0.05 } }}
exit={{ opacity: 0, transition: { duration: 0.05 } }}
className="flex flex-col gap-1"
>
<h4 className="text-sm font-medium text-foreground flex items-center gap-2">
{(() => {
const Icon = ICONS[active.icon]
return <Icon size={14} strokeWidth={2} />
})()}
{active.label}
</h4>
<p className="text-sm text-foreground-lighter">{active.paragraph}</p>
</motion.div>
</AnimatePresence>
<Link
href="/docs/guides/functions"
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"
className="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 shrink-0"
>
Documentation
<svg