diff --git a/apps/www/app/(products)/database/_components/ApiSection.tsx b/apps/www/app/(products)/database/_components/ApiSection.tsx index 72a6f59ab7e..d943412af56 100644 --- a/apps/www/app/(products)/database/_components/ApiSection.tsx +++ b/apps/www/app/(products)/database/_components/ApiSection.tsx @@ -47,12 +47,26 @@ const supabaseLight: ThemeRegistration = { ], } -// ── API examples ──────────────────────────────────────────────────────────── +// ── Language → Shiki lang mapping ─────────────────────────────────────────── + +const LANG_MAP: Record = { + 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() + .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() + .Insert(new Room { Name = "Design Team", Public = false }) + .Single(); + +await supabase + .From() + .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() + +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() + .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()`, + 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() + .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() diff --git a/apps/www/app/(products)/database/_components/ApiSectionClient.tsx b/apps/www/app/(products)/database/_components/ApiSectionClient.tsx index 188bcba1bb4..ffcf50740af 100644 --- a/apps/www/app/(products)/database/_components/ApiSectionClient.tsx +++ b/apps/www/app/(products)/database/_components/ApiSectionClient.tsx @@ -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(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 ( -
+
+ {/* Header */}
-
- {/* Left: title + libraries */} -
-
-

- Never write an API - again -

+
+

+ Never write an API +
+ again +

+

+ We introspect your database and provide instant APIs. Focus on building your product, + while Supabase handles the CRUD. +

+
+
-

- We introspect your database and provide instant APIs. Focus on building your - product, while Supabase handles the CRUD. -

-
- - {/* Available libraries */} -
- Available libraries -
- {LIBRARIES.map((lib) => ( - - {lib.label} - - ))} -
-
-
- - {/* Right: tabs + code */} -
- {/* Tab row */} -
- {examples.map((example, index) => ( + {/* Content */} +
+
+ {/* Left: tab cards */} +
+ {examples.map((example, index) => { + const isActive = index === activeIdx + const Icon = ICONS[example.icon] + return ( - ))} + ) + })} +
+ + {/* Right: code block */} +
+ {/* Language switcher */} +
+
+ + +
- {/* Code area */} -
- - -
-
- - - + - Documentation - - - - -
+
+
+ + + + Documentation + + + +
diff --git a/apps/www/app/(products)/database/_components/SqlEditorSection.tsx b/apps/www/app/(products)/database/_components/SqlEditorSection.tsx index ad10ba50caf..a50af24b900 100644 --- a/apps/www/app/(products)/database/_components/SqlEditorSection.tsx +++ b/apps/www/app/(products)/database/_components/SqlEditorSection.tsx @@ -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(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 ( -
+
{/* Header */}
-

- The power of -
- SQL Editor -

+
+

+ Write and run queries with a +
+ full SQL Editor +

+

+ Write, run, and save SQL queries directly from the dashboard with full autocomplete and + syntax highlighting. +

+
+ {/* Content */}
-
-
- {/* Left: tabs + content */} -
- {/* Tab row */} -
- {TABS.map((tab, index) => ( - - ))} -
+
+ {/* Left: tabs */} +
+ {TABS.map((tab, index) => { + const isActive = index === activeIdx + const Icon = ICONS[tab.icon] + return ( + + ) + })} +
- {/* Tab content + tweet */} -
- {/* Active tab content */} -
- - -

{active.title}

-

- {active.description} -

-
-
-
- - {/* Tweet card */} -
-
- @jim_bisenius -
- @jim_bisenius -

- @MongoDB or @MySQL?!?! Please, let me introduce you to @supabase and the - wonderful world of @Postgres before it's too late!! -

-
-
-
-
-
- - {/* Right: visual */} -
+ {/* Right: image + footer */} +
+
{active.title}
+ + {/* Footer: active tab info */} +
+ + +

+ {(() => { + const Icon = ICONS[active.icon] + return + })()} + {active.label} +

+

{active.description}

+
+
+
diff --git a/apps/www/app/(products)/database/_components/TableEditorSection.tsx b/apps/www/app/(products)/database/_components/TableEditorSection.tsx index 65c41bc49dd..c36b777b706 100644 --- a/apps/www/app/(products)/database/_components/TableEditorSection.tsx +++ b/apps/www/app/(products)/database/_components/TableEditorSection.tsx @@ -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(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 ( -
+
{/* Header */}
-

- Easy to use -
- Table Editor -

+
+

+ Manage your data with a +
+ built-in Table Editor +

+

+ Manage your data with a spreadsheet-like interface. Create tables, set up relationships, + and export — no SQL required. +

+
+ {/* Content */}
-
-
- {/* Left: tabs + content */} -
- {/* Tab row */} -
- {TABS.map((tab, index) => ( - - ))} -
+
+ {/* Left: tabs */} +
+ {TABS.map((tab, index) => { + const isActive = index === activeIdx + const Icon = ICONS[tab.icon] + return ( + + ) + })} +
- {/* Tab content + tweet */} -
- {/* Active tab content */} -
- - -

{active.title}

-

- {active.description} -

-
-
-
- - {/* Tweet card */} -
-
- @Elsolo244 -
- @Elsolo244 -

- Where has @supabase been all my life? -

-
-
-
-
-
- - {/* Right: visual */} -
+ {/* Right: image + footer */} +
+
{active.title}
+ + {/* Footer: active tab info */} +
+ + +

+ {(() => { + const Icon = ICONS[active.icon] + return + })()} + {active.label} +

+

{active.description}

+
+
+
diff --git a/apps/www/app/(products)/edge-functions/_components/IntegratesSection.tsx b/apps/www/app/(products)/edge-functions/_components/IntegratesSection.tsx index e575b1a874d..c390a285321 100644 --- a/apps/www/app/(products)/edge-functions/_components/IntegratesSection.tsx +++ b/apps/www/app/(products)/edge-functions/_components/IntegratesSection.tsx @@ -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{' '} - - Supabase Auth - - - ), + 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{' '} - - Supabase Storage - - - ), + 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' diff --git a/apps/www/app/(products)/edge-functions/_components/IntegratesSectionClient.tsx b/apps/www/app/(products)/edge-functions/_components/IntegratesSectionClient.tsx index e6cbb93fb7c..467d7aada7e 100644 --- a/apps/www/app/(products)/edge-functions/_components/IntegratesSectionClient.tsx +++ b/apps/www/app/(products)/edge-functions/_components/IntegratesSectionClient.tsx @@ -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 | null>(null) + const intervalRef = useRef(null) const { ref: inViewRef, inView } = useInView({ threshold: 0.3 }) const active = useCases[activeIdx] @@ -82,63 +82,34 @@ export function IntegratesSectionClient({ useCases }: { useCases: UseCase[] }) { {/* Content */}
-
-
- {/* Left: vertical tabs */} -
- {useCases.map((useCase, index) => { - const isActive = index === activeIdx - return ( - - ) - })} -
+
+ {/* Left: tab cards */} +
+ {useCases.map((useCase, index) => { + const isActive = index === activeIdx + const Icon = ICONS[useCase.icon] + return ( + + ) + })} +
- {/* Right: code area */} -
- + {/* Right: code area */} +
+
+ +
+ + {/* Footer: active tab info */} +
+ + +

+ {(() => { + const Icon = ICONS[active.icon] + return + })()} + {active.label} +

+

{active.paragraph}

+
+
Documentation