Feat/learn (#41566)

wip

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

# Release Notes

* **New Features**
* Added a new Learn application offering foundational Supabase courses
with interactive documentation
* Courses include Architecture, Authentication, Data Fundamentals,
Security, Storage, Realtime, and Edge Functions
  * Chapter tracking and progress indicators for course completions
  * Responsive sidebar navigation with search/command menu
  * Theme switching support (light, dark, classic dark modes)
  * Mobile-friendly course interface

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Alan Daniel <stylesshjs@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-02-04 21:36:24 -03:30
1 parent aa9a10e65d
commit dda0b526ac
124 files changed
+6478 -266

No files matched your search

+38
View File
@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.contentlayer
+54
View File
@@ -0,0 +1,54 @@
Plan:
Course 1: Supabase Foundations Learn the basics of Supabase: database, auth, and RLS. 5 chapters.
-
Course 2: Project: Smart Office 15 Build a realtime room-booking dashboard using Supabase. 15 chapters.
Course 3: Supabase Internals: Performance & Scaling. Learn how to profile queries, tune indexes, and scale Postgres with Supabase. 20 chapters.
Course 4: Supabase Internals: Debugging & Operations. Understand how to diagnose slow queries, use read replicas, and manage production workloads. 20 chapters.
———
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
## Supabase types
To regenerate the Supabase database types, run
```
supabase gen types --local > registry/default/fixtures/database.types.ts
```
+158
View File
@@ -0,0 +1,158 @@
import { metadata as mainMetadata } from '@/app/layout'
import { ChapterCompletion } from '@/components/chapter-completion'
import { CourseHero } from '@/components/course-hero'
import { ExploreMore } from '@/components/explore-more'
import { Mdx } from '@/components/mdx-components'
import { NextUp } from '@/components/next-up'
import { DashboardTableOfContents } from '@/components/toc'
import { getTableOfContents } from '@/lib/toc'
import { getCurrentChapter } from '@/lib/get-current-chapter'
import { getNextPage } from '@/lib/get-next-page'
import { absoluteUrl, cn } from '@/lib/utils'
import '@/styles/code-block-variables.css'
import '@/styles/mdx.css'
import { allDocs } from 'contentlayer/generated'
import { ChevronRight } from 'lucide-react'
import type { Metadata } from 'next'
import { notFound } from 'next/navigation'
import Balancer from 'react-wrap-balancer'
import { ScrollArea } from 'ui'
interface DocPageProps {
params: Promise<{
slug: string[]
}>
}
async function getDocFromParams({ params }: { params: { slug: string[] } }) {
const slug = params.slug?.join('/') || ''
const doc = allDocs.find((doc) => doc.slugAsParams === slug)
if (!doc) {
return null
}
return doc
}
export async function generateMetadata(props: DocPageProps): Promise<Metadata> {
const params = await props.params
const doc = await getDocFromParams({ params })
// get page params so we can check if it's the introduction page
const slugSegments = doc?.slugAsParams.split('/')
const isIntroductionPage = slugSegments?.[slugSegments.length - 1] === 'introduction'
if (!doc) {
return {}
}
const metadata: Metadata = {
...mainMetadata,
title: doc.title,
description: doc.description,
openGraph: {
...(mainMetadata.openGraph || {}),
title: doc.title,
description: doc.description,
type: 'article',
url: absoluteUrl(doc.slug),
},
}
return metadata
}
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
return allDocs.map((doc) => ({
slug: doc.slugAsParams.split('/'),
}))
}
export default async function DocPage(props: DocPageProps) {
const params = await props.params
const doc = await getDocFromParams({ params })
if (!doc) {
notFound()
}
const toc = await getTableOfContents(doc.body.raw)
const nextPage = getNextPage(doc.slugAsParams)
const currentChapter = getCurrentChapter(doc.slugAsParams)
const slugSegments = doc.slugAsParams.split('/')
const isIntroductionPage = slugSegments[slugSegments.length - 1] === 'introduction'
const exploreItems = (
doc as {
explore?: Array<{ title: string; link: string; itemType?: string; description?: string }>
}
).explore
return (
<div className={cn('relative')}>
{isIntroductionPage && doc.courseHero && (
<CourseHero
title={doc.courseHero.title}
subtitle={doc.courseHero.subtitle}
description={doc.courseHero.description}
//instructors={doc.courseHero.instructors}
/>
)}
<div
className={cn(
'relative lg:gap-10 xl:grid xl:grid-cols-[1fr_200px] py-20',
isIntroductionPage ? 'px-0' : 'px-8 md:px-16'
)}
>
<div className="mx-auto w-full min-w-0 max-w-4xl">
<div className="mb-4 flex items-center space-x-1 text-sm text-foreground-muted">
<div className="overflow-hidden text-ellipsis whitespace-nowrap">Learn</div>
<ChevronRight className="h-4 w-4 text-foreground-muted" />
<div className="text-foreground-lighter">{doc.title}</div>
</div>
<div className="flex flex-col lg:flex-row lg:items-end justify-between mb-5">
<div className="space-y-2">
<h1 className={cn('scroll-m-20 text-2xl lg:text-4xl tracking-tight')}>{doc.title}</h1>
{doc.description && (
<p className="text-base lg:text-lg text-foreground-light">
<Balancer>{doc.description}</Balancer>
</p>
)}
</div>
</div>
<div className="pb-12">
<Mdx code={doc.body.code} />
</div>
{exploreItems && exploreItems.length > 0 && <ExploreMore items={exploreItems} />}
{currentChapter && nextPage && (
<ChapterCompletion
chapterNumber={currentChapter.chapterNumber!}
completionMessage={currentChapter.completionMessage}
/>
)}
{nextPage && (
<div className="flex w-2xl grow">
<NextUp
title={nextPage.title}
description={nextPage.description || ''}
href={nextPage.href}
chapterNumber={nextPage.chapterNumber}
/>
</div>
)}
</div>
{doc.toc && (
<div className="hidden text-sm xl:block">
<div className="sticky top-16 -mt-10 pt-4">
<ScrollArea className="pb-10">
<div className="sticky top-16 -mt-10 h-[calc(100vh-3.5rem)] py-12">
<DashboardTableOfContents toc={toc} />
</div>
</ScrollArea>
</div>
</div>
)}
</div>
</div>
)
}
+31
View File
@@ -0,0 +1,31 @@
import { Sidebar } from '@/components/sidebar'
import { SiteFooter } from '@/components/site-footer'
import { TelemetryWrapper } from './telemetry-wrapper'
interface AppLayoutProps {
children: React.ReactNode
}
export default function AppLayout({ children }: AppLayoutProps) {
return (
<>
{/* main container */}
<div className="pt-10 md:pt-0">
{/* main content */}
<main className="flex-1 max-w-site mx-auto w-full p-0">
{/* {children} */}
<div className="border-b">
<div className="flex-1 items-start md:grid md:grid-cols-[240px_minmax(0,1fr)] lg:grid-cols-[280px_minmax(0,1fr)]">
<Sidebar />
<div vaul-drawer-wrapper="">
<div className="relative flex min-h-screen flex-col bg-background">{children}</div>
</div>
</div>
</div>
</main>
</div>
<SiteFooter />
<TelemetryWrapper />
</>
)
}
+136
View File
@@ -0,0 +1,136 @@
import { WhatWillILearn } from '@/components/what-will-i-learn'
import Link from 'next/link'
import { Activity, BookOpen, Database, Gauge, Wrench } from 'lucide-react'
import { Badge, Button, Card, CardDescription, CardHeader } from 'ui'
// Horizontal grid line component
const HorizontalGridLine = () => <div className="col-span-12 h-px bg-border/30" />
export const dynamic = 'force-dynamic'
export const revalidate = 0
export default function Home() {
const courses = [
{
id: 1,
title: 'Supabase Foundations',
description: 'Learn the basics of Supabase: database, auth, and RLS.',
chapters: 5,
icon: Database,
level: 'Beginner',
},
{
id: 2,
title: 'Project: Smart Office',
description: 'Build a realtime room-booking dashboard using Supabase.',
chapters: 15,
icon: Activity,
level: 'Intermediate',
},
{
id: 3,
title: 'Supabase Internals: Performance & Scaling',
description: 'Learn how to profile queries, tune indexes, and scale Postgres with Supabase.',
chapters: 20,
icon: Gauge,
level: 'Advanced',
},
{
id: 4,
title: 'Supabase Internals: Debugging & Operations',
description:
'Understand how to diagnose slow queries, use read replicas, and manage production workloads.',
chapters: 20,
icon: Wrench,
level: 'Advanced',
},
]
return (
<main className="relative lg:-ml-10">
<div className="mx-auto w-full min-w-0 flex flex-col gap-16">
{/* Component Showcase with Grid */}
<div className="relative z-10 h-full w-full overflow-y-auto">
{/* Grid Container */}
<div className="relative">
{/* Grid Lines - Vertical (Columns) */}
{Array.from({ length: 13 }).map((_, i) => (
<div
key={`col-line-${i}`}
className="absolute top-0 bottom-0 w-px bg-border/30 z-10 first:hidden last:hidden"
style={{
left: `${(i / 12) * 100}%`,
height: '100%',
}}
/>
))}
{/* Grid Content */}
<div className="grid grid-cols-12 gap-0 relative z-20 pb-32">
{/* Heading Section */}
<div className="col-start-2 col-span-10 md:col-start-3 md:col-span-8 pt-8 pb-8">
<div className="flex flex-col gap-8 justify-start pt-16 md:pt-32">
<div className="max-w-2xl">
<h1 className="text-4xl text-foreground mb-3 font-medium tracking-tight">
Learn Supabase
</h1>
<h2 className="text-lg text-foreground-light mb-4">
Learn how to build your own projects with Supabase. Our courses and projects
help you get started no matter your skill level, teaching you how to build
production-ready apps.
</h2>
</div>
</div>
<div className="mb-8 mt-12">
<h3 className="text-2xl font-bold mb-6">Courses</h3>
<div className="space-y-4">
{courses.map((course) => {
const Icon = course.icon
return (
<Card key={course.id} className="hover:border-primary/30 transition-colors">
<CardHeader>
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-brand-500/10 flex items-center justify-center flex-shrink-0">
<Icon className="w-5 h-5 text-brand-500" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-4 mb-2">
<h3 className="text-lg font-bold">{course.title}</h3>
<Badge variant="secondary" className="flex-shrink-0">
{course.level}
</Badge>
</div>
<CardDescription className="text-sm mb-3 text-foreground-light">
{course.description}
</CardDescription>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<BookOpen className="w-4 h-4" />
<span className="text-foreground-lighter">
{course.chapters} chapters
</span>
</div>
<Button type="text" size="small" asChild>
<Link href="/...">Start course ➔</Link>
</Button>
</div>
</div>
</div>
</CardHeader>
</Card>
)
})}
</div>
</div>
<div className="mt-16">
<WhatWillILearn />
</div>
</div>
</div>
</div>
</div>
</div>
</main>
)
}
@@ -0,0 +1,16 @@
'use client'
import { API_URL } from '@/lib/constants'
import { IS_PLATFORM, PageTelemetry } from 'common'
import { useConsentToast } from 'ui-patterns/consent'
export const TelemetryWrapper = () => {
const { hasAcceptedConsent } = useConsentToast()
return (
<PageTelemetry
API_URL={API_URL}
hasAcceptedConsent={hasAcceptedConsent}
enabled={IS_PLATFORM}
/>
)
}
+26
View File
@@ -0,0 +1,26 @@
'use client'
import { Provider as JotaiProvider } from 'jotai'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { ThemeProviderProps } from 'next-themes/dist/types'
import { FrameworkProvider } from '@/context/framework-context'
import { MobileMenuProvider } from '@/context/mobile-menu-context'
import { AuthProvider } from 'common'
import { TooltipProvider } from 'ui'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return (
<AuthProvider>
<JotaiProvider>
<NextThemesProvider {...props}>
<MobileMenuProvider>
<FrameworkProvider>
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
</FrameworkProvider>
</MobileMenuProvider>
</NextThemesProvider>
</JotaiProvider>
</AuthProvider>
)
}
+9
View File
@@ -0,0 +1,9 @@
'use client'
import { useConfig } from '@/hooks/use-config'
import { SonnerToaster as Toaster } from 'ui'
export function SonnerToaster() {
const [config] = useConfig()
return <Toaster position={config.sonnerPosition} expand={config.sonnerExpand} />
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+60
View File
@@ -0,0 +1,60 @@
import type { Metadata } from 'next'
import '@/styles/globals.css'
import { API_URL } from '@/lib/constants'
import { FeatureFlagProvider, TelemetryTagManager } from 'common'
import { genFaviconData } from 'common/MetaFavicons/app-router'
import { Inter } from 'next/font/google'
import { ThemeProvider } from './Providers'
import { SonnerToaster } from './SonnerToast'
const inter = Inter({ subsets: ['latin'] })
const BASE_PATH = process.env.NEXT_PUBLIC_BASE_PATH ?? ''
export const metadata: Metadata = {
applicationName: 'Learn Supabase',
title: 'Learn Supabase',
description: 'Learn Supabase.',
metadataBase: new URL('https://supabase.com/learn'),
icons: genFaviconData(BASE_PATH),
openGraph: {
type: 'article',
authors: 'Supabase',
url: `${BASE_PATH}`,
images: `${BASE_PATH}/img/supabase-og-image.png`,
publishedTime: new Date().toISOString(),
modifiedTime: new Date().toISOString(),
},
twitter: {
card: 'summary_large_image',
site: '@supabase',
creator: '@supabase',
images: `${BASE_PATH}/img/supabase-og-image.png`,
},
}
interface RootLayoutProps {
children: React.ReactNode
}
export default async function Layout({ children }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<head />
<body className={`${inter.className} antialiased`}>
<TelemetryTagManager />
<FeatureFlagProvider API_URL={API_URL}>
<ThemeProvider
themes={['dark', 'light', 'classic-dark']}
defaultTheme="system"
enableSystem
>
{children}
<SonnerToaster />
</ThemeProvider>
</FeatureFlagProvider>
</body>
</html>
)
}
+17
View File
@@ -0,0 +1,17 @@
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui'
interface CalloutProps {
icon?: string
title?: string
children?: React.ReactNode
}
export function Callout({ title, children, icon, ...props }: CalloutProps) {
return (
<Alert_Shadcn_ {...props}>
{icon && <span className="mr-4 text-2xl">{icon}</span>}
{title && <AlertTitle_Shadcn_>{title}</AlertTitle_Shadcn_>}
<AlertDescription_Shadcn_>{children}</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)
}
@@ -0,0 +1,147 @@
'use client'
import { Check } from 'lucide-react'
import { useEffect, useRef, useState } from 'react'
import { useLocalStorage } from './use-local-storage'
interface ChapterCompletionProps {
chapterNumber: number
completionMessage?: string
}
export function ChapterCompletion({ chapterNumber, completionMessage }: ChapterCompletionProps) {
const [completedChapters, setCompletedChapters] = useLocalStorage<number[]>(
'completed-chapters',
[]
)
const [isCompleted, setIsCompleted] = useState(false)
const [isVisible, setIsVisible] = useState(false)
const containerRef = useRef<HTMLDivElement>(null)
const timerRef = useRef<NodeJS.Timeout | null>(null)
// Check if chapter is already completed on mount
useEffect(() => {
if (completedChapters.includes(chapterNumber)) {
setIsCompleted(true)
}
}, [chapterNumber, completedChapters])
useEffect(() => {
const container = containerRef.current
if (!container) return
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setIsVisible(true)
} else {
setIsVisible(false)
// Reset timer if user scrolls away
if (timerRef.current) {
clearTimeout(timerRef.current)
timerRef.current = null
}
}
})
},
{
threshold: 0.5, // Trigger when 50% of the component is visible
rootMargin: '0px',
}
)
observer.observe(container)
return () => {
observer.disconnect()
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [])
useEffect(() => {
if (isVisible && !isCompleted) {
// Start timer when component becomes visible
timerRef.current = setTimeout(() => {
setIsCompleted(true)
// Save to local storage
if (!completedChapters.includes(chapterNumber)) {
setCompletedChapters([...completedChapters, chapterNumber])
}
}, 5000) // 5 seconds
} else if (!isVisible && timerRef.current) {
// Clear timer if user scrolls away before completion
clearTimeout(timerRef.current)
timerRef.current = null
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
}
}, [isVisible, isCompleted, chapterNumber, completedChapters, setCompletedChapters])
return (
<div ref={containerRef} className="mb-16 mt-8">
<div className="flex items-center gap-12">
{/* Large circle with chapter number */}
<div className="relative mb-6">
<div
className={`w-24 h-24 rounded-full flex items-center justify-center transition-all duration-500 ${
isCompleted ? 'bg-green-50' : 'bg-muted'
}`}
>
<span
className={`text-4xl font-bold transition-all duration-500 ${
isCompleted ? 'text-brand-500' : 'text-foreground-muted'
}`}
>
{chapterNumber}
</span>
</div>
{/* Small checkmark circle overlapping bottom-right */}
<div
className={`absolute -bottom-1 -right-1 w-8 h-8 rounded-full flex items-center justify-center shadow-md transition-all duration-500 ${
isCompleted ? 'bg-brand-500 scale-100 opacity-100' : 'bg-muted scale-75 opacity-0'
}`}
>
<Check
className={`h-6 w-6 text-white transition-all duration-300 ${
isCompleted ? 'scale-100 opacity-100' : 'scale-0 opacity-0'
}`}
strokeWidth={3}
/>
</div>
</div>
<div>
{/* Completion text */}
<h3
className={`text-2xl font-bold mb-2 transition-all duration-500 ${
isCompleted
? 'text-foreground opacity-100 translate-y-0'
: 'text-foreground-muted opacity-60 translate-y-2'
}`}
>
You&apos;ve completed Chapter {chapterNumber}
</h3>
{completionMessage && (
<p
className={`text-base max-w-2xl transition-all duration-500 delay-100 ${
isCompleted
? 'text-foreground-light opacity-100 translate-y-0'
: 'text-foreground-light opacity-0 translate-y-2'
}`}
>
{completionMessage}
</p>
)}
</div>
</div>
</div>
)
}
@@ -0,0 +1,77 @@
'use client'
import { Check, Copy } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useSendTelemetryEvent } from 'lib/telemetry'
import { Button_Shadcn_ } from 'ui'
export function CommandCopyButton({ command }: { command: string }) {
const [copied, setCopied] = useState(false)
const sendTelemetryEvent = useSendTelemetryEvent()
useEffect(() => {
if (copied) {
const timeout = setTimeout(() => setCopied(false), 2000)
return () => clearTimeout(timeout)
}
}, [copied])
const parseCommandForTelemetry = (cmd: string) => {
// Extract framework from URL (e.g., 'nextjs' from 'password-based-auth-nextjs.json')
const frameworkMatch = cmd.match(/ui\/r\/.*?-(nextjs|react|react-router|tanstack)\.json/)
// if the block doesn't have a framework defined (like infinite query), default to react
const framework = frameworkMatch
? (frameworkMatch[1] as 'nextjs' | 'react-router' | 'tanstack' | 'react')
: 'react'
// Extract package manager from command prefix (npx, pnpm, yarn, bun)
const packageManager = cmd.startsWith('npx')
? ('npm' as const)
: cmd.startsWith('pnpm')
? ('pnpm' as const)
: cmd.startsWith('yarn')
? ('yarn' as const)
: cmd.startsWith('bun')
? ('bun' as const)
: ('npm' as const)
// Extract template title from URL (e.g., 'password-based-auth' from 'password-based-auth-nextjs.json')
const titleMatch = cmd.match(/\/ui\/r\/(.*?)\.json/)
const title = (titleMatch ? titleMatch[1] : '').replaceAll(`-${framework}`, '')
return {
framework,
packageManager,
title,
}
}
return (
<Button_Shadcn_
variant="ghost"
size="icon"
onClick={() => {
// Copy command to clipboard
navigator.clipboard.writeText(command)
setCopied(true)
// Parse command and send telemetry event
const { framework, packageManager, title } = parseCommandForTelemetry(command)
sendTelemetryEvent({
action: 'supabase_ui_command_copy_button_clicked',
properties: {
templateTitle: title,
command: command,
framework,
packageManager,
},
})
}}
>
{copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
</Button_Shadcn_>
)
}
+112
View File
@@ -0,0 +1,112 @@
'use client'
import { CircleIcon, LaptopIcon, MoonIcon, SunIcon } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useRouter } from 'next/navigation'
import * as React from 'react'
import { COMMAND_ITEMS } from '@/config/docs'
import { cn } from '@/lib/utils'
import {
Button,
CommandDialog,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
CommandSeparator_Shadcn_,
DialogProps,
} from 'ui'
export function CommandMenu({ ...props }: DialogProps) {
const router = useRouter()
const [open, setOpen] = React.useState(false)
const { setTheme } = useTheme()
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === 'k' && (e.metaKey || e.ctrlKey)) || e.key === '/') {
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
e.preventDefault()
setOpen((open) => !open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [])
const runCommand = React.useCallback((command: () => unknown) => {
setOpen(false)
command()
}, [])
return (
<>
<Button
type="outline"
className={cn(
`relative h-8 w-full justify-start rounded-[0.5rem] bg-background text-sm font-normal text-foreground-muted shadow-none sm:pr-12
hover:border-foreground-muted hover:bg-surface-100 hover:text-foreground-lighter
`
)}
onClick={() => setOpen(true)}
{...props}
>
<span className="hidden lg:inline-flex">Search...</span>
<span className="inline-flex lg:hidden">Search...</span>
<kbd className="pointer-events-none absolute right-[0.3rem] top-[0.3rem] hidden h-5 select-none items-center gap-1 rounded border bg-surface-200 px-1.5 font-mono text-[10px] font-medium opacity-100 sm:flex text-foreground-light">
<span className="text-sm">⌘</span>K
</kbd>
</Button>
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput_Shadcn_ placeholder="Type a command or search..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_ key="pages" heading="Pages">
{COMMAND_ITEMS.map((navItem) => (
<CommandItem_Shadcn_
key={navItem.href}
value={navItem.label}
onSelect={() => runCommand(() => router.push(navItem.href as string))}
>
<div className="mr-2 flex h-4 w-4 items-center justify-center">
<CircleIcon className="h-3 w-3" strokeWidth={1} />
</div>
{navItem.label}
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
<CommandSeparator_Shadcn_ />
<CommandGroup_Shadcn_ heading="Theme">
<CommandItem_Shadcn_ onSelect={() => runCommand(() => setTheme('light'))}>
<SunIcon className="mr-2 h-4 w-4" strokeWidth={1} />
Light
</CommandItem_Shadcn_>
<CommandItem_Shadcn_ onSelect={() => runCommand(() => setTheme('dark'))}>
<MoonIcon className="mr-2 h-4 w-4" strokeWidth={1} />
Dark
</CommandItem_Shadcn_>
<CommandItem_Shadcn_ onSelect={() => runCommand(() => setTheme('classic-dark'))}>
<MoonIcon className="mr-2 h-4 w-4" strokeWidth={1} />
Classic dark
</CommandItem_Shadcn_>
<CommandItem_Shadcn_ onSelect={() => runCommand(() => setTheme('system'))}>
<LaptopIcon className="mr-2 h-4 w-4" strokeWidth={1} />
System
</CommandItem_Shadcn_>
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</CommandDialog>
</>
)
}
+102
View File
@@ -0,0 +1,102 @@
'use client'
import { motion } from 'framer-motion'
import { Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_ } from 'ui'
import { CommandCopyButton } from './command-copy-button'
import { useLocalStorage } from './use-local-storage'
interface CommandCopyProps {
name: string
highlight?: boolean
// For Vue, we need to use the `shadcn-vue` package instead of `shadcn`
framework?: 'react' | 'vue'
}
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
const LOCAL_STORAGE_KEY = 'package-manager-copy-command'
const getBaseUrl = () => {
if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'production') {
// we have a special alias for the production environment, added in https://github.com/shadcn-ui/ui/pull/8161
return `@supabase`
} else if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'preview') {
return `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
} else {
return 'http://localhost:3004'
}
}
const getComponentPath = (name: string) => {
if (process.env.NEXT_PUBLIC_VERCEL_TARGET_ENV === 'production') {
return `/${name}`
} else {
return `${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/r/${name}.json`
}
}
export function Command({ name, highlight, framework = 'react' }: CommandCopyProps) {
const [value, setValue] = useLocalStorage(LOCAL_STORAGE_KEY, 'npm')
const baseUrl = getBaseUrl()
const componentPath = getComponentPath(name)
const commands: Record<PackageManager, string> =
framework === 'vue'
? {
npm: `npx shadcn-vue@latest add ${baseUrl}${componentPath}`,
pnpm: `pnpm dlx shadcn-vue@latest add ${baseUrl}${componentPath}`,
yarn: `yarn dlx shadcn-vue@latest add ${baseUrl}${componentPath}`,
bun: `bunx --bun shadcn-vue@latest add ${baseUrl}${componentPath}`,
}
: {
npm: `npx shadcn@latest add ${baseUrl}${componentPath}`,
pnpm: `pnpm dlx shadcn@latest add ${baseUrl}${componentPath}`,
yarn: `yarn dlx shadcn@latest add ${baseUrl}${componentPath}`,
bun: `bunx --bun shadcn@latest add ${baseUrl}${componentPath}`,
}
return (
<Tabs_Shadcn_ value={value} onValueChange={setValue} className="w-full">
<div className="w-full group relative rounded-lg bg-surface-200 dark:bg-surface-100 px-4 py-2 overflow-hidden">
{highlight && (
<motion.div
className="absolute inset-0 bg-gradient-to-l from-transparent via-[#bbb] dark:via-white to-transparent opacity-10 z-0"
initial={{ x: '100%' }}
animate={{ x: '-100%' }}
transition={{
repeat: Infinity,
duration: 2.5,
ease: 'linear',
repeatType: 'loop',
}}
/>
)}
<div className="flex flex-col">
<TabsList_Shadcn_ className="gap-2 relative mb-2 z-10">
{(Object.keys(commands) as PackageManager[]).map((manager) => (
<TabsTrigger_Shadcn_ key={manager} value={manager} className="text-xs">
{manager}
</TabsTrigger_Shadcn_>
))}
</TabsList_Shadcn_>
{(Object.keys(commands) as PackageManager[]).map((manager) => (
<TabsContent_Shadcn_ key={manager} value={manager} className="m-0">
<div className="flex items-center">
<div className="flex-1 font-mono text-sm text-foreground relative z-10">
<span className="mr-2 text-[#888] select-none">$</span>
{commands[manager]}
</div>
<div className="relative z-10">
<CommandCopyButton command={commands[manager]} />
</div>
</div>
</TabsContent_Shadcn_>
))}
</div>
</div>
</Tabs_Shadcn_>
)
}
+40
View File
@@ -0,0 +1,40 @@
'use client'
import { Check, Copy } from 'lucide-react'
import * as React from 'react'
import { Button, cn, copyToClipboard } from 'ui'
interface CopyButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
value: string
src?: string
}
export function CopyButton({ value, className, src, ...props }: CopyButtonProps) {
const [hasCopied, setHasCopied] = React.useState(false)
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false)
}, 2000)
}, [hasCopied])
return (
<Button
size="small"
type="outline"
className={cn(
'relative z-10 h-6 w-6 text-foreground-muted hover:bg-surface-100 hover:text-foreground p-0',
className
)}
onClick={() => {
copyToClipboard(value)
setHasCopied(true)
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <Check className="h-3 w-3 text-brand-600" /> : <Copy className="h-3 w-3" />}
</Button>
)
}
+68
View File
@@ -0,0 +1,68 @@
import { BookA, Clock, GraduationCap } from 'lucide-react'
interface CourseHeroProps {
title: string
subtitle: string
description: string
stats?: {
label: string
icon: React.ReactNode
accent: string
}[]
instructors?: {
name: string
icon: React.ReactNode
accent: string
}[]
}
export function CourseHero({ title, subtitle, description, instructors }: CourseHeroProps) {
return (
<div className="relative w-full mx-auto py-16 sm:py-24 border-b bg-[radial-gradient(circle_at_top,_rgba(16,185,129,0.14),_transparent_55%),radial-gradient(circle_at_bottom,_rgba(34,197,94,0.10),_transparent_50%)]">
{/* Chapter label */}
<div className="flex items-center justify-center mb-6">
<span className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-muted border border-border text-xs font-medium text-muted-foreground uppercase tracking-wider">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500" />
Chapter Introduction
</span>
</div>
<h1 className="text-4xl sm:text-5xl lg:text-6xl font-bold mb-6 text-balance text-center bg-gradient-to-br from-foreground to-foreground/70 bg-clip-text text-transparent">
{title}
</h1>
<p className="text-lg sm:text-xl text-center text-muted-foreground mb-4 text-balance max-w-3xl mx-auto">
Learn the foundations of Supabase, the Postgres development platform.
</p>
<p className="text-sm sm:text-base text-center text-muted-foreground/80 text-pretty max-w-2xl mx-auto">
In this short course, you&apos;ll explore how Supabase brings together Database, Auth,
Storage, Edge Functions, and Realtime into a unified developer platform.
</p>
{/* Course metadata */}
<div className="flex flex-wrap items-center justify-center gap-6 mt-10 text-sm">
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-lg bg-brand-500/10 flex items-center justify-center flex-shrink-0">
<BookA className="w-5 h-5 text-brand-500" />
</div>
<span className="font-medium text-muted-foreground">5 Chapters</span>
</div>
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-lg bg-brand-500/10 flex items-center justify-center flex-shrink-0">
<Clock className="w-5 h-5 text-brand-500" />
</div>
<span className="font-medium text-muted-foreground">~1 hour</span>
</div>
<div className="flex items-center gap-2">
<div className="w-10 h-10 rounded-lg bg-brand-500/10 flex items-center justify-center flex-shrink-0">
<GraduationCap className="w-5 h-5 text-brand-500" />
</div>
<span className="font-medium text-muted-foreground">Beginner</span>
</div>
</div>
</div>
)
}
+110
View File
@@ -0,0 +1,110 @@
import Link from 'next/link'
import { BookOpen, Code, Users, ExternalLink, BookA } from 'lucide-react'
import { IconYoutubeSolid } from 'ui'
interface ExploreItem {
title: string
link: string
itemType?: string
description?: string
}
interface ExploreMoreProps {
items: ExploreItem[]
}
// Icon mapping based on itemType
function getIcon(itemType?: string) {
if (!itemType) return BookOpen
const lowerType = itemType.toLowerCase()
if (lowerType === 'doc' || lowerType === 'documentation') {
return BookA
}
if (lowerType === 'reference' || lowerType === 'api') {
return Code
}
if (lowerType === 'community' || lowerType === 'forum') {
return Users
}
if (lowerType === 'video' || lowerType === 'tutorial') {
return IconYoutubeSolid
}
return BookA // default icon
}
// Default descriptions based on itemType
function getDescription(itemType?: string): string {
if (!itemType) return 'Learn more about this topic'
const lowerType = itemType.toLowerCase()
if (lowerType === 'doc' || lowerType === 'documentation') {
return 'Complete guides and references'
}
if (lowerType === 'reference' || lowerType === 'api') {
return 'Detailed API documentation'
}
if (lowerType === 'community' || lowerType === 'forum') {
return 'Get help from the community'
}
if (lowerType === 'video' || lowerType === 'tutorial') {
return 'Watch video tutorials and guides'
}
return 'Learn more about this topic'
}
export function ExploreMore({ items }: ExploreMoreProps) {
if (!items || items.length === 0) {
return null
}
return (
<div className="mt-16 mb-8">
<div className="border-t border-border pt-8">
<h3 className="text-sm font-semibold text-foreground-lighter uppercase tracking-wider mb-6">
Explore More
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{items.map((item, index) => {
const isExternal = item.link.startsWith('http://') || item.link.startsWith('https://')
const Icon = getIcon(item.itemType)
const description = item.description || getDescription(item.itemType)
const cardContent = (
<div className="relative bg-background border border-border rounded-lg shadow-sm p-6 h-full hover:shadow-md transition-shadow group">
<div className="absolute top-4 right-4">
<ExternalLink className="h-4 w-4 text-foreground-lighter opacity-50 group-hover:opacity-100 transition-opacity" />
</div>
<div className="flex flex-col gap-3">
<Icon className="h-6 w-6 text-foreground" />
<div>
<h4 className="font-bold text-foreground mb-1">{item.title}</h4>
<p className="text-sm text-foreground-light">{description}</p>
</div>
</div>
</div>
)
if (isExternal) {
return (
<a
key={index}
href={item.link}
target="_blank"
rel="noopener noreferrer"
className="block"
>
{cardContent}
</a>
)
}
return (
<Link key={index} href={item.link} className="block">
{cardContent}
</Link>
)
})}
</div>
</div>
</div>
)
}
+186
View File
@@ -0,0 +1,186 @@
import { useMDXComponent } from 'next-contentlayer2/hooks'
import Link from 'next/link'
import {
Accordion_Shadcn_ as Accordion,
AccordionContent_Shadcn_ as AccordionContent,
AccordionItem_Shadcn_ as AccordionItem,
AccordionTrigger_Shadcn_ as AccordionTrigger,
cn,
} from 'ui'
import { Callout } from './callout'
import { CopyButton } from './copy-button'
import TanStackBeta from './tanstack-beta'
const components = {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
h1: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h1 className={cn('font-heading mt-2 scroll-m-20 text-4xl font-bold', className)} {...props} />
),
h2: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h2
className={cn(
'font-heading mt-12 scroll-m-20 text-2xl font-medium tracking-tight first:mt-0',
className
)}
{...props}
/>
),
h3: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h3
className={cn('font-heading mt-8 scroll-m-20 text-xl tracking-tight', className)}
{...props}
/>
),
h4: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h4
className={cn('font-heading mt-8 scroll-m-20 text-lg tracking-tight', className)}
{...props}
/>
),
h5: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h5 className={cn('mt-8 scroll-m-20 text-lg tracking-tight', className)} {...props} />
),
h6: ({ className, ...props }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h6 className={cn('mt-8 scroll-m-20 text-base tracking-tight', className)} {...props} />
),
a: ({ className, ...props }: React.HTMLAttributes<HTMLAnchorElement>) => (
<a
className={cn(
'text-foreground underline decoration-1 decoration-foreground-muted underline-offset-4 transition-colors hover:decoration-brand hover:decoration-2',
className
)}
{...props}
/>
),
p: ({ className, ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p
className={cn('leading-7 [&:not(:first-child)]:mt-6 text-foreground-light', className)}
{...props}
/>
),
ul: ({ className, ...props }: React.HTMLAttributes<HTMLUListElement>) => (
<ul className={cn('my-6 ml-6 list-disc text-foreground-light', className)} {...props} />
),
ol: ({ className, ...props }: React.HTMLAttributes<HTMLOListElement>) => (
<ol className={cn('my-6 ml-6 list-decimal text-foreground-light', className)} {...props} />
),
li: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
<li className={cn('mt-2', className)} {...props} />
),
blockquote: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
<blockquote className={cn('mt-6 border-l-2 pl-6 italic', className)} {...props} />
),
img: ({ className, alt, ...props }: React.ImgHTMLAttributes<HTMLImageElement>) => (
// eslint-disable-next-line @next/next/no-img-element
<img className={cn('rounded-md', className)} alt={alt} {...props} />
),
hr: ({ ...props }: React.HTMLAttributes<HTMLHRElement>) => (
<hr className="my-4 md:my-8" {...props} />
),
table: ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
<div className="my-6 w-full overflow-y-auto">
<table className={cn('w-full', className)} {...props} />
</div>
),
tr: ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr className={cn('m-0 border-t p-0 even:bg-surface-75/75', className)} {...props} />
),
th: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
<th
className={cn(
'border px-4 py-2 text-left font-normal [&[align=center]]:text-center [&[align=right]]:text-right',
className
)}
{...props}
/>
),
td: ({ className, ...props }: React.HTMLAttributes<HTMLTableCellElement>) => (
<td
className={cn(
'border text-foreground-light px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right',
className
)}
{...props}
/>
),
pre: ({
className,
__rawString__,
__withMeta__,
__src__,
...props
}: React.HTMLAttributes<HTMLPreElement> & {
__rawString__?: string
__withMeta__?: boolean
__src__?: string
}) => {
return (
<div className="relative">
<pre
className={cn(
'mb-4 mt-6 max-h-[650px] overflow-x-auto rounded-lg border bg-surface-75/75 py-4 text-foreground-light',
className
)}
{...props}
/>
{__rawString__ && (
<CopyButton
value={__rawString__}
src={__src__}
className={cn('absolute right-4 top-4', __withMeta__ && 'top-16')}
/>
)}
</div>
)
},
code: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
<code
className={cn(
'relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm',
className
)}
{...props}
/>
),
Callout,
CopyButton,
TanStackBeta,
Card: ({ className, ...props }: React.HTMLAttributes<HTMLElement>) => (
<div
className={cn(
'flex w-full flex-col items-center rounded-xl border bg-surface-100 text-card-background py-6 px-4 shadow transition-colors hover:bg-muted/50 sm:p-10',
className
)}
{...props}
/>
),
LinkedCard: ({ className, ...props }: React.ComponentProps<typeof Link>) => (
<Link
className={cn(
'flex w-full flex-col items-center justify-center rounded-xl border bg-surface-100 text-card-background py-6 px-4 shadow transition-colors hover:bg-muted/50 sm:p-10 h-52',
className
)}
{...props}
/>
),
}
interface MdxProps {
code: string
}
export function Mdx({ code }: MdxProps) {
const Component = useMDXComponent(code, {
style: 'default',
})
return (
<div className="mdx">
<Component components={components} />
</div>
)
}
@@ -0,0 +1,18 @@
'use client'
import { useMobileMenu } from '@/hooks/use-mobile-menu'
import { Sheet } from 'ui'
interface MobileMenuSheetProps {
children: React.ReactNode
}
export function MobileMenuSheet({ children }: MobileMenuSheetProps) {
const { open, setOpen } = useMobileMenu()
return (
<Sheet open={open} onOpenChange={setOpen}>
{children}
</Sheet>
)
}
+38
View File
@@ -0,0 +1,38 @@
import Link from 'next/link'
import { ArrowRight } from 'lucide-react'
import { Button } from 'ui'
interface NextUpProps {
title: string
description: string
href: string
chapterNumber?: number
}
export function NextUp({ title, description, href, chapterNumber }: NextUpProps) {
return (
<div className="mt-16 mb-8 grow">
<div className="bg-background border border-border rounded-lg shadow-sm p-8 max-w-2xl">
<div className="grid gap-4">
<p className="text-sm text-foreground-lighter">Next Up</p>
<h2 className="text-xl font-bold text-foreground">
{chapterNumber && <span>{chapterNumber}: </span>}
{title}
</h2>
<p className="text-foreground text-base">{description}</p>
<div className="mt-2">
<Link href={href}>
<Button
type="default"
iconRight={<ArrowRight className="h-4 w-4" />}
className="flex items-center gap-2"
>
Start {chapterNumber ? `Chapter ${chapterNumber}` : 'Next'}
</Button>
</Link>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,168 @@
'use client'
import Link, { LinkProps } from 'next/link'
import { usePathname } from 'next/navigation'
import React, { useState, useEffect } from 'react'
import { ChevronRight, Lock } from 'lucide-react'
import { useMobileMenu } from '@/hooks/use-mobile-menu'
import { SidebarNavItem } from '@/types/nav'
import { Badge, cn } from 'ui'
// We extend:
// 1. LinkProps - for Next.js Link component props (prefetch, etc)
// 2. AnchorHTMLAttributes - for standard HTML anchor props (className, etc)
// We omit href from both since we compute it internally from item.href
interface NavigationItemProps
extends Omit<LinkProps, 'href'>,
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'href'> {
item: SidebarNavItem
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void
level?: number
internalPaths?: string[]
isLoggedIn?: boolean
}
const NavigationItem: React.FC<NavigationItemProps> = ({
item,
onClick,
level = 0,
internalPaths,
isLoggedIn = true,
...props
}) => {
const { setOpen } = useMobileMenu()
const pathname = usePathname()
const [isOpen, setIsOpen] = useState(false)
const pathParts = pathname.split('/')
const slug = pathParts[pathParts.length - 1]
const hasChildren = item.items && item.items.length > 0
// Check if internal content exists for this item and user is logged in
const hasInternal = item.href && internalPaths?.includes(item.href) && isLoggedIn
// Auto-expand if any child is active
useEffect(() => {
if (hasChildren) {
const hasActiveChild = item.items?.some((child) => {
return pathname === child.href
})
if (hasActiveChild) {
setIsOpen(true)
}
}
}, [hasChildren, item.items, pathname])
// Use item.href if available, otherwise build from slug
let href = item.href
if (!href && slug) {
href = `/docs/${slug}`
}
// Determine if this link represents the current page
const isActive = pathname === href
const handleLinkClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
// Close the mobile menu when navigating
setOpen(false)
// Call the onClick prop if it exists
if (onClick) {
onClick(e)
}
}
const handleButtonClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault()
setIsOpen(!isOpen)
}
const itemClasses = cn(
'flex text-sm rounded-md transition-colors',
level === 0 ? 'px-3 py-2' : 'px-3 py-1.5',
isActive
? 'bg-surface-200 text-foreground'
: hasChildren && isOpen
? 'bg-surface-100 text-foreground'
: 'text-foreground-lighter hover:bg-surface-100 hover:text-foreground'
)
return (
<li>
{hasChildren ? (
<>
<button
onClick={handleButtonClick}
className={cn('w-full flex items-center justify-between gap-2 zans', itemClasses)}
>
<span className="flex items-center gap-2 flex-1 min-w-0">
{item.title}
{item.new && (
<Badge variant="default" className="capitalize flex-shrink-0">
NEW
</Badge>
)}
</span>
<ChevronRight
className={cn(
'w-4 h-4 transition-transform flex-shrink-0',
isOpen && 'rotate-90',
(hasChildren && isOpen) || isActive ? 'text-foreground' : 'text-foreground-lighter'
)}
/>
</button>
{isOpen && (
<ul className="mt-1 ml-3 space-y-1 border-l border-border pl-3">
{item.items?.map((childItem, i) => (
<NavigationItem
item={childItem}
key={`${childItem.href}-${i}`}
level={level + 1}
internalPaths={internalPaths}
isLoggedIn={isLoggedIn}
/>
))}
</ul>
)}
</>
) : (
<>
<Link href={href || '#'} {...props} onClick={handleLinkClick} className={itemClasses}>
<span className="flex items-center gap-2">
<span className="truncate">{item.title}</span>
{item.new && (
<Badge variant="default" className="capitalize flex-shrink-0">
NEW
</Badge>
)}
</span>
</Link>
{hasInternal && (
<Link
href={`/internal${href}`}
onClick={handleLinkClick}
className={cn(
'flex text-sm rounded-md transition-colors mt-1',
level === 0 ? 'px-3 py-2' : 'px-3 py-1.5',
pathname === `/internal${href}`
? 'bg-surface-200 text-foreground'
: 'text-foreground-lighter hover:bg-surface-100 hover:text-foreground'
)}
>
<span className="flex items-center gap-2">
<Lock className="w-3 h-3 text-foreground-muted flex-shrink-0" />
<span className="truncate">{item.title} (Internal)</span>
</span>
</Link>
)}
</>
)}
</li>
)
}
NavigationItem.displayName = 'NavigationItem'
export default NavigationItem
+162
View File
@@ -0,0 +1,162 @@
'use client'
import Link from 'next/link'
import { useIsLoggedIn, useUser, logOut } from 'common'
import { AuthenticatedDropdownMenu, type menuItem } from 'ui-patterns'
import { LogOut, Settings, UserIcon } from 'lucide-react'
import { useRouter } from 'next/navigation'
import NavigationItem from '@/components/side-navigation-item'
import { courses } from '@/config/docs'
import { mergeInternalContentIntoSections } from '@/lib/merge-internal-content'
import { SidebarNavItem } from '@/types/nav'
import { CommandMenu } from './command-menu'
import { ThemeSwitcherDropdown } from './theme-switcher-dropdown'
interface SideNavigationProps {
internalPaths: string[]
}
function SideNavigation({ internalPaths }: SideNavigationProps) {
const isLoggedIn = useIsLoggedIn()
const user = useUser()
const router = useRouter()
// First, merge orphaned internal content into their respective sections
// Note: All content is visible to everyone. Auth is only for saving progress.
const coursesWithInternalContent = {
...courses,
items: mergeInternalContentIntoSections(courses.items, internalPaths),
}
// User menu for authenticated dropdown
const userMenu: menuItem[][] = [
[
{
label: user?.email ?? "You're logged in",
type: 'text',
icon: UserIcon,
},
{
label: 'Account Preferences',
icon: Settings,
href: 'https://supabase.com/dashboard/account/me',
},
],
[
{
label: 'Logout',
type: 'button',
icon: LogOut,
onClick: async () => {
await logOut()
router.refresh()
},
},
],
]
return (
<nav className="flex flex-col h-full min-w-[220px]">
<div className="flex-1 flex flex-col overflow-hidden">
<div className="p-6 flex-shrink-0">
<div className="flex items-start justify-between mb-4">
<Link href="/">
<svg
xmlns="http://www.w3.org/2000/svg"
width="109"
height="113"
viewBox="0 0 109 113"
fill="none"
className="w-6 h-6"
>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0625L99.1935 40.0625C107.384 40.0625 111.952 49.5226 106.859 55.9372L63.7076 110.284Z"
fill="url(#paint0_linear)"
/>
<path
d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0625L99.1935 40.0625C107.384 40.0625 111.952 49.5226 106.859 55.9372L63.7076 110.284Z"
fill="url(#paint1_linear)"
fillOpacity="0.2"
/>
<path
d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z"
fill="#3ECF8E"
/>
<defs>
<linearGradient
id="paint0_linear"
x1="53.9738"
y1="54.9738"
x2="94.1635"
y2="71.8293"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#249361" />
<stop offset="1" stopColor="#3ECF8E" />
</linearGradient>
<linearGradient
id="paint1_linear"
x1="36.1558"
y1="30.5779"
x2="54.4844"
y2="65.0804"
gradientUnits="userSpaceOnUse"
>
<stop />
<stop offset="1" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
</Link>
<ThemeSwitcherDropdown />
</div>
<Link href="/" className="mb-4 block">
<h1>Learn Supabase</h1>
</Link>
{/* <TopNavigationSearch /> */}
<CommandMenu />
</div>
<div className="flex-1 overflow-auto">
<div className="pb-6 px-6">
<h3 className="text-xs font-semibold text-foreground-lighter/75 uppercase tracking-wider mb-3">
{courses.title}
</h3>
<ul className="space-y-1">
{coursesWithInternalContent.items.map((item: SidebarNavItem, i: number) => (
<NavigationItem
item={item}
key={`${item.href}-${i}`}
internalPaths={internalPaths}
isLoggedIn={isLoggedIn}
/>
))}
</ul>
</div>
</div>
</div>
<div className="border-t border-muted/50 p-4 flex-shrink-0">
{isLoggedIn ? (
<div className="flex items-center gap-3">
<AuthenticatedDropdownMenu menu={userMenu} user={user} site="docs" />
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">{user?.email}</p>
</div>
</div>
) : (
<div className="text-center">
<Link
href="https://supabase.com/dashboard/sign-in"
className="text-sm text-foreground-light hover:text-foreground transition-colors underline decoration-1 underline-offset-4"
>
Sign in to save your progress
</Link>
</div>
)}
</div>
</nav>
)
}
export default SideNavigation
+36
View File
@@ -0,0 +1,36 @@
import { Menu } from 'lucide-react'
import SideNavigation from '@/components/side-navigation'
import { getInternalContentPaths } from '@/lib/get-internal-content'
import { Button, ScrollArea, SheetContent, SheetTrigger } from 'ui'
import { MobileMenuSheet } from './mobile-menu-sheet'
import { ThemeSwitcherDropdown } from './theme-switcher-dropdown'
export function Sidebar() {
// Get internal content paths on server side
const internalPaths = Array.from(getInternalContentPaths())
return (
<>
<div className="md:hidden fixed top-0 left-0 right-0 z-50 bg-background justify-between flex items-center px-8 py-3 border-b">
<MobileMenuSheet>
<SheetTrigger asChild>
<Button type="outline" icon={<Menu />} />
</SheetTrigger>
<SheetContent side="left" className="p-0 w-80" showClose={false}>
<ScrollArea className="h-full">
<SideNavigation internalPaths={internalPaths} />
</ScrollArea>
</SheetContent>
</MobileMenuSheet>
<ThemeSwitcherDropdown />
</div>
<aside className="fixed z-30 top-0 hidden h-screen w-full shrink-0 md:sticky md:block bg-200 border-r border-muted/50">
<ScrollArea className="h-full">
<SideNavigation internalPaths={internalPaths} />
</ScrollArea>
</aside>
</>
)
}
+29
View File
@@ -0,0 +1,29 @@
export function SiteFooter() {
return (
<footer className="py-6 px-4 md:px-8 md:py-0 mx-auto w-full max-w-site">
<div className="flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row">
<p className="text-balance text-center text-sm leading-loose text-foreground-muted md:text-left">
Built by{' '}
<a
href="https://twitter.com/supabase"
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-4 hover:text-foreground-lighter"
>
Supabase
</a>
. The source code is available on{' '}
<a
href="https://github.com/supabase/supabase/tree/master/apps/ui-library"
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-4 hover:text-foreground-lighter"
>
GitHub
</a>
.
</p>
</div>
</footer>
)
}
+29
View File
@@ -0,0 +1,29 @@
import { TriangleAlert } from 'lucide-react'
import { Callout } from './callout'
export default function TanStackBeta() {
return (
<div className="mt-4">
<Callout>
<div className="grid gap-2">
<div className="flex items-center gap-2 font-medium">
<TriangleAlert />
Heads up: TanStack Start is in beta.
</div>
We&apos;re excited to support TanStack Start in our UI library! But since it&apos;s still
in beta, things may change quickly — expect breaking changes and some rough edges.
We&apos;ll do our best to keep up and make integration with Supabase as smooth as
possible. If you run into issues, have a look at the TanStack docs.
<a
href="https://tanstack.com/start/latest/docs/framework/react/quick-start"
target="_blank"
rel="noopener noreferrer"
className="text-foreground underline decoration-1 decoration-foreground-muted underline-offset-4 transition-colors hover:decoration-brand hover:decoration-2"
>
TanStack Quickstart guide
</a>
</div>
</Callout>
</div>
)
}
@@ -0,0 +1,91 @@
'use client'
import { Moon, Sun } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useEffect, useState } from 'react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
RadioGroup_Shadcn_,
Theme,
singleThemes,
} from 'ui'
const ThemeSwitcherDropdown = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme, resolvedTheme } = useTheme()
/**
* Avoid Hydration Mismatch
* https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch
*/
// useEffect only runs on the client, so now we can safely show the UI
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return null
}
function SingleThemeSelection() {
return (
<form>
<RadioGroup_Shadcn_
name="theme"
onValueChange={setTheme}
aria-label="Choose a theme"
defaultValue={theme}
value={theme}
className="flex flex-wrap gap-3"
></RadioGroup_Shadcn_>
</form>
)
}
const iconClasses = 'text-foreground-light group-data-[state=open]:text-foreground'
return (
<>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="text"
size="tiny"
className="px-1 group"
icon={
resolvedTheme?.includes('light') ? (
<Sun className={iconClasses} />
) : (
<Moon className={iconClasses} />
)
}
></Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<DropdownMenuLabel>Theme</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={theme}
onValueChange={(themeValue) => setTheme(themeValue)}
>
{singleThemes.map((theme: Theme) => (
<DropdownMenuRadioItem key={theme.value} value={theme.value}>
{theme.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</>
)
}
export { ThemeSwitcherDropdown }
+107
View File
@@ -0,0 +1,107 @@
// @ts-nocheck
'use client'
import * as React from 'react'
import { TableOfContents } from '@/lib/toc'
import { cn } from '@/lib/utils'
import { useMounted } from '@/hooks/use-mounted'
interface TocProps {
toc: TableOfContents
}
export function DashboardTableOfContents({ toc }: TocProps) {
const itemIds = React.useMemo(
() =>
toc.items
? toc.items
.flatMap((item) => [item.url, item?.items?.map((item) => item.url)])
.flat()
.filter(Boolean)
.map((id) => id?.split('#')[1])
: [],
[toc]
)
const activeHeading = useActiveItem(itemIds)
const mounted = useMounted()
if (!toc?.items || !mounted) {
return null
}
return (
<div className="space-y-2">
<p className="font-medium text-foreground-light">On This Page</p>
<Tree tree={toc} activeItem={activeHeading} />
</div>
)
}
function useActiveItem(itemIds: string[]) {
const [activeId, setActiveId] = React.useState(null)
React.useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setActiveId(entry.target.id)
}
})
},
{ rootMargin: `0% 0% -80% 0%` }
)
itemIds?.forEach((id) => {
const element = document.getElementById(id)
if (element) {
observer.observe(element)
}
})
return () => {
itemIds?.forEach((id) => {
const element = document.getElementById(id)
if (element) {
observer.unobserve(element)
}
})
}
}, [itemIds])
return activeId
}
interface TreeProps {
tree: TableOfContents
level?: number
activeItem?: string
}
function Tree({ tree, level = 1, activeItem }: TreeProps) {
return tree?.items?.length && level < 3 ? (
<ul className={cn('m-0 list-none', { 'pl-4': level !== 1 })}>
{tree.items.map((item, index) => {
return (
<li key={index} className={cn('mt-0 pt-2')}>
<a
href={item.url}
className={cn(
'inline-block no-underline transition-colors hover:text-foreground',
item.url === `#${activeItem}`
? 'font-medium text-foreground'
: 'text-foreground-muted'
)}
>
{item.title}
</a>
{item.items?.length ? (
<Tree tree={item} level={level + 1} activeItem={activeItem} />
) : null}
</li>
)
})}
</ul>
) : null
}
@@ -0,0 +1,47 @@
// Reference: https://usehooks.com/useLocalStorage/
import { useCallback, useState } from 'react'
export function useLocalStorage<T>(key: string, initialValue: T) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue
}
try {
// Get from local storage by key
const item = window.localStorage.getItem(key)
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue
} catch (error) {
// If error also return initialValue
console.log(error)
return initialValue
}
})
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = useCallback(
(value: T | ((val: T) => T)) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value
// Save state
setStoredValue(valueToStore)
// Save to local storage
if (typeof window !== 'undefined') {
window.localStorage.setItem(key, JSON.stringify(valueToStore))
}
} catch (error) {
// A more advanced implementation would handle the error case
console.log(error)
}
},
[key, storedValue]
)
return [storedValue, setValue] as const
}
+138
View File
@@ -0,0 +1,138 @@
import {
BarChart3,
CalendarRange,
Database,
History,
Lock,
Network,
Rocket,
Timer,
} from 'lucide-react'
import Image from 'next/image'
import { Card, CardDescription, CardHeader, CardTitle } from 'ui'
const learningTracks = [
{
icon: Database,
title: 'Model and manage your data',
description: 'Design relational schemas, automate migrations, and keep your project tidy.',
takeaways: [
'set up tables, relationships, and seed data',
'work with realtime subscriptions and triggers',
'version and deploy database changes safely',
],
},
{
icon: Lock,
title: 'Secure every request',
description: 'Master authentication and Row Level Security to keep data scoped correctly.',
takeaways: [
'configure supabase auth providers',
'write row level security policies with confidence',
'audit, monitor, and debug access issues fast',
],
},
{
icon: Network,
title: 'Go realtime by default',
description: 'Stream updates into your UI with channels, presence, and edge functions.',
takeaways: [
'broadcast updates with realtime channels',
'listen for database changes from the client',
'connect edge functions to your realtime flows',
],
},
{
icon: Rocket,
title: 'Deploy production-grade apps',
description: 'Integrate storage, vector search, and observability for resilient launches.',
takeaways: [
'serve files securely from supabase storage',
'index and query embeddings for ai features',
'monitor performance and scale without surprises',
],
},
]
export function WhatWillILearn() {
return (
<section className="grid gap-8">
<div className="grid gap-2 max-w-2xl">
<h3 className="text-2xl font-bold mb-4">What will I learn?</h3>
<p className="text-base text-foreground-light">
In the fundamental courses, you&apos;ll learn all the basics:
</p>
</div>
<div className="grid gap-6 md:grid-cols-2">
{learningTracks.map((track) => {
const Icon = track.icon
return (
<Card key={track.title} className="h-full border-border/60">
<CardHeader className="grid gap-2 border-b border-border/40">
<Icon className="h-5 w-5 text-brand-500" />
<CardTitle className="text-base font-semibold text-foreground">
{track.title}
</CardTitle>
<CardDescription className="text-sm text-foreground-light">
{track.description}
</CardDescription>
</CardHeader>
</Card>
)
})}
</div>
<div className="mt-8">
<h4 className="text-lg font-bold mb-2">Smart Office project</h4>
<p className="text-base text-foreground-light">
In the Smart Office project, you&apos;ll learn how to build a full-featured Dashboard:
</p>
<div className="grid grid-cols-12 gap-12 mt-6">
<div className="col-span-6">
<Image
src={'/img/smart-office.png'}
alt="Smart Office project dashboard screenshot"
className="w-full h-auto"
width={1000}
height={875}
/>
</div>
<div className="col-span-6">
<ul className="grid gap-6 text-foreground-light ">
<li className="grid grid-cols-[auto,1fr] gap-3 border-b border-border/40 pb-3">
<span className="flex h-9 w-9 items-center justify-center rounded-md bg-brand-500/10 text-brand-500">
<Timer className="h-5 w-5" />
</span>
<span>Real-time room monitoring that tracks live occupancy, sensor readings</span>
</li>
<li className="grid grid-cols-[auto,1fr] gap-3 border-b border-border/40 pb-3">
<span className="flex h-9 w-9 items-center justify-center rounded-md bg-brand-500/10 text-brand-500">
<CalendarRange className="h-5 w-5" />
</span>
<span>Booking management with live status filters, calendar integrations.</span>
</li>
<li className="grid grid-cols-[auto,1fr] gap-3 border-b border-border/40 pb-3">
<span className="flex h-9 w-9 items-center justify-center rounded-md bg-brand-500/10 text-brand-500">
<History className="h-5 w-5" />
</span>
<span>
Advanced analytics that surface 30+ days of utilization trends and patterns.
</span>
</li>
<li className="grid grid-cols-[auto,1fr] gap-3">
<span className="flex h-9 w-9 items-center justify-center rounded-md bg-brand-500/10 text-brand-500">
<BarChart3 className="h-5 w-5" />
</span>
<span>
Detect capacity issues, open facilities tasks, and track resolution SLAs without
leaving the app.
</span>
</li>
</ul>
</div>
</div>
</div>
</section>
)
}
+74
View File
@@ -0,0 +1,74 @@
import { SidebarNavGroup } from 'types/nav'
export const courses: SidebarNavGroup = {
title: 'Courses',
items: [
{
title: 'Foundations',
href: '/foundations',
items: [
// ✅ Always visible (no requiresAuth)
{
title: 'Introduction',
href: '/foundations/introduction',
},
{
title: 'Architecture',
href: '/foundations/architecture',
},
{
title: 'Data Fundamentals',
href: '/foundations/data-fundamentals',
},
{
title: 'Authentication',
href: '/foundations/authentication',
},
{
title: 'Security',
href: '/foundations/security',
},
{
title: 'Realtime',
href: '/foundations/realtime',
},
{
title: 'Storage',
href: '/foundations/storage',
},
{
title: 'Edge Functions',
href: '/foundations/edge-functions',
},
{
title: 'Vector Search',
href: '/foundations/vector',
},
],
commandItemLabel: 'Foundations',
},
{
title: 'Project: Smart Office',
href: '/projects/smart-office',
commandItemLabel: 'Project: Smart Office',
},
{
title: 'Performance & Scaling',
href: '/internals/performance-scaling',
commandItemLabel: 'Supabase Internals: Performance & Scaling',
},
{
title: 'Debugging & Operations',
href: '/internals/debugging-operations',
commandItemLabel: 'Supabase Internals: Debugging & Operations',
},
],
}
export const COMMAND_ITEMS = [
...courses.items.map((item) => ({
label: item.commandItemLabel,
href: item.href,
})),
]
@@ -0,0 +1,179 @@
---
title: Architecture
description: Learn how Supabase's core products work together.
chapterNumber: 1
explore:
- title: Supabase Docs
link: https://supabase.com/docs/guides/database/overview
itemType: doc
- title: Postgres for Beginners
link: https://www.postgresqltutorial.com/
itemType: doc
---
This page explains how Supabase is structured and how its main services work
together. Supabase is built on top of a powerful open source database called
Postgres, but it includes much more than a database. Around Postgres,
Supabase adds services for authentication, serverless functions, file storage, realtime updates, AI-powered search, and easy APIs to access it — all designed to work easily together.
The goal is to give you a complete backend that feels consistent and connected,
so you can focus on building your app instead of wiring together separate tools.
---
## The mental model
At the center of Supabase is Postgres. Every other part connects to it.
```text
Clients (web, mobile, scripts)
|
v
API layer (REST via PostgREST, GraphQL)
|
v
Postgres <---- Realtime (listens for changes)
| ^
| |
v |
Storage | Auth (manages users)
| | Edge Functions (runs code)
v | Vector Search (pgvector)
Files + URLs Studio, CLI, Policies (RLS)
```
You interact with Supabase mainly through the API layer or the client libraries.
The API talks to Postgres, and Postgres enforces security rules that you define.
All the other services stay in sync because they share the same database.
---
## The main components
### Database (Postgres)
Your data lives in Postgres tables. You can design tables, columns, and
relationships using SQL or the visual editor in Supabase Studio. Supabase
manages backups, scaling, and migrations for you.
**Why this matters:** Postgres is a full relational database with features like
foreign keys, indexes, and extensions. Supabase gives you these tools without
the usual setup.
---
### API layer (REST and GraphQL)
Supabase automatically exposes your tables as APIs using PostgREST (for REST)
and PostGraphile (for GraphQL). You can read, insert, update, and delete rows
with simple HTTP requests. You can also expose your own SQL functions as custom
endpoints.
**Why this matters:** You can connect any frontend or script directly to your
database without building a separate API server.
---
### Auth (users and tokens)
Auth manages users and issues tokens that identify them. When someone signs in,
their token is sent with every request, and Postgres uses that identity to apply
the right access rules.
**Why this matters:** Authentication is built into your database layer, so you
don't need an external service to handle sign-ups or sessions.
---
### Row Level Security (RLS) and policies
RLS lets you write SQL rules that decide who can read or modify each row in your
tables. These policies apply everywhere — through REST, GraphQL, or Realtime.
**Why this matters:** Security lives directly in the database. You define it
once, and it's enforced across every API call.
---
### Storage (files and buckets)
Storage provides an S3-style interface for storing files such as images, videos,
and documents. File metadata lives in Postgres, so you can apply the same access
policies you use for your data.
**Why this matters:** File access and data access follow the same security rules,
which keeps your system consistent and simple.
---
### Realtime
Realtime listens to changes in Postgres and sends them to connected clients over
WebSocket. You can subscribe to table events and see updates as they happen.
**Why this matters:** You can build collaborative or live-updating interfaces
without managing your own socket server.
---
### Edge Functions
Edge Functions let you run server-side JavaScript or TypeScript using Deno.
They're good for webhooks, background tasks, and any logic that shouldn't run
on the client.
**Why this matters:** You can write custom backend logic without maintaining a
full server or worrying about scaling.
---
### Vector Search
The pgvector extension adds support for storing and searching **embeddings** —
numerical representations of text, images, or other data. This allows you to
search by meaning, not just by keywords.
**Why this matters:** You can add AI-style semantic search or recommendations
directly to your database.
---
### Studio and CLI
Supabase Studio is the web dashboard for managing your project: running SQL,
browsing data, editing policies, and managing users. The CLI is for local
development and migrations, so your schema can live alongside your code.
**Why this matters:** You can move smoothly between GUI and code, keeping your
database versioned and reproducible.
---
## How everything connects
When your app makes a request, it goes through a simple path:
1. The client calls a Supabase API endpoint, sending a project key and (usually)
a user token.
2. The API turns that request into a SQL query.
3. Postgres runs the query and applies any RLS policies.
4. The API returns the results as JSON.
5. If there are changes, Realtime broadcasts them to any connected clients that
are allowed to see them.
Edge Functions can follow the same pattern — they can call the APIs or connect
directly to Postgres when they need more control.
---
## Why this architecture helps
- **Everything shares one source of truth:** Postgres is at the center.
- **Security is consistent:** RLS policies apply across every service.
- **No separate backend required:** APIs are generated automatically from you database.
- **You can grow gradually:** Start with just a table, add realtime, storage, or functions later.
- **It's just Postgres:** You can use standard SQL, extensions, and tools.
Supabase gives you the power of Postgres with the convenience of a fully
integrated backend platform.
@@ -0,0 +1,125 @@
---
title: Authentication
description: Learn how to manage users, and how signed-in requests connect to the database.
chapterNumber: 3
---
Most applications need to know who is using them. Authentication is how we identify
a user and attach their actions to their own data. Supabase Authentication works closely with the database, so each request carries the user's identity.
Authentication in Supabase is handled by the Supabase Auth service. It manages creating accounts, signing in, and identifying the user on each request.
---
## Users
A user in Supabase has an id and some optional profile information (such as
email, phone number, name, etc.). When someone signs up, Supabase creates a new user record and assigns
them a unique identifier.
You do not need to create or manage the users table yourself. Supabase stores it
internally and keeps it updated.
Example of the kind of data Supabase stores for a user:
```text
id: 74d23f1b-1c94-3e7b-4d6a-9fa71e2a3c4ba987
email: maria@example.com
created_at: 2025-01-12 09:44:21
updated_at: 2025-01-12 09:44:21
last_sign_in_at: 2025-01-12 09:44:21
```
You can reference this user id in your tables when you want to link data to a
specific user.
For example, you can associate notes with a user by storing the user's id in the note table.
```text
| id | user_id | content | created_at |
|----|--------------------------------------|------------------|----------------------|
| 1 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Buy groceries | 2025-01-10 14:22:11 |
| 2 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Call the dentist | 2025-01-10 15:08:54 |
```
---
## Tokens and identity
When a user signs in, Supabase returns a token. This token proves who the user
is. This token is stored locally (usually in memory or local
storage) and automatically included in each API request.
Inside the database, this token becomes the value returned by `auth.uid()`.
This lets security policies decide which data belongs to which user.
For example:
```text
select auth.uid();
```
returns the id of the signed-in user who made the request.
---
## Sign up and sign in
Supabase Auth supports several sign-in methods:
- Email and password
- Magic links (sign in by clicking a link in an email)
- Social providers (Google, GitHub, etc.)
- Phone number (SMS)
For now, we will use email and password, since it is the simplest starting point.
---
## Where authentication connects to data
Authentication tells us who the user is. But it does not decide what they are
allowed to access. To handle that part, we use a Postgres feature called Row Level Security (RLS).
RLS lets the database check each row before returning it. The database uses the
user's id to decide whether that row should be visible or changeable. This means
the rules for access live inside the database itself, not in the application
code.
From our previous notes example, RLS would allow a user to select only their own notes, but not the notes of other users.
So the flow looks like this:
```text
| id | user_id | content | created_at |
|----|--------------------------------------|------------------|----------------------|
| 1 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Buy groceries | 2025-01-10 14:22:11 |
| 2 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Call the dentist | 2025-01-10 15:08:54 |
```
```text
User signs in
↓
Supabase gives the client a user token
↓
Client sends token with API requests
↓
Postgres receives token and sets auth.uid()
↓
RLS policies use auth.uid() to enforce access rules
```
Authentication says who the user is.
RLS decides what they are allowed to do.
---
## What we learned
- Supabase Auth manages user accounts and sign-in.
- Each user has a unique id that you can reference in your tables.
- When a user signs in, Supabase provides a token.
- The token tells the database who the user is through `auth.uid()`.
- Authentication by itself does not control access. RLS does that part.
Understanding this identity flow is important before we begin writing policies.
@@ -0,0 +1,144 @@
---
title: Data Fundamentals
description: 'Learn how data is structured in Postgres: tables, rows, columns, and keys.'
chapterNumber: 2
explore:
- title: Supabase Docs
link: https://supabase.com/docs/guides/database/overview
itemType: doc
- title: Postgres for Beginners
link: https://www.postgresqltutorial.com/
itemType: reference
---
Before we store any information in Supabase, we need to understand how data is
organized in Postgres. Supabase uses Postgres as its database, which means data
is arranged in a structured and predictable way. This helps keep your app
organized, consistent, and easy to query.
The core ideas are simple: you create tables to hold related data, and each
table has rows (records) and columns (fields). Once you understand that pattern,
you can model almost anything.
---
## Tables, rows, and columns
A table is like a spreadsheet. It has:
- **Columns**: the fields that define what kind of data the table stores
- **Rows**: the individual entries in the table
Imagine a table for storing notes:
```text
| id | content | created_at |
|----|------------------|----------------------|
| 1 | Buy groceries | 2025-01-10 14:22:11 |
| 2 | Call the dentist | 2025-01-10 15:08:54 |
```
- Each **row** is a single note.
- Each **column** represents one piece of information about the note.
---
## Data types
Every column has a data type. This describes the kind of value that can be
stored there.
Some common data types:
```text
| Data type | Example value | Meaning |
| ----------- | ---------------- | ----------------------------- |
| text | Hello world | A string of characters |
| integer | 42 | A number without decimals |
| boolean | true | Yes/no or on/off values |
| timestamptz | 2025-01-10 14:22 | Date and time (with timezone) |
| json | {"name": "John"} | A JSON object |
```
Choosing correct types makes your data easier to work with and protects you from
unexpected values.
---
## Primary keys
Most tables include a primary key. This is a column that uniquely identifies each
row. In many Supabase projects, this column is named `id` and uses a type called
`uuid`, which automatically generates a unique value.
You create this column in the table definition like this:
```text
id uuid primary key default gen_random_uuid()
```
You generally don't need to think about it after setting it up. It simply gives
each record a stable identity.
---
## Relationships between tables
You can link tables together using foreign keys. This is how you express things
like:
- A user has many notes
- A project has many tasks
- A team has many members
For example:
```text
| id | user_id | content | created_at |
|----|--------------------------------------|------------------|----------------------|
| 1 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Buy groceries | 2025-01-10 14:22:11 |
| 2 | 9f1b1c94-3e7b-4d6a-9fa7-1e2a3c4ba987 | Call the dentist | 2025-01-10 15:08:54 |
```
Here, `user_id` refers to a row in a `users` table. This means the note belongs
to that user, and we can see that this user has two notes.
Relationships let you organize data without repeating information.
---
## Why relational data is useful in practice
Relational data is really useful in practice because it allows you to:
- Keep data valid with constraints <br />
_Example: no duplicate emails._
- Link records safely with foreign keys <br />
_Example: delete a user and their notes in one action._
- Express real permissions with RLS <br />
_Example: users can read their own notes; admins can read all._
- Query exactly what you need with table joins <br />
_Example: fetch notes with the author's email._
---
## What we learned
A database in Supabase is made up of tables.
Each table has:
- columns (which describe the type of information stored)
- rows (the individual pieces of data in that table)
You can relate tables to each other using foreign keys.
This lets you model real situations, such as:
- a user has many notes
- a team has many members
- a project has many tasks
Thinking in tables, rows, and relationships is the foundation of working with data in Supabase.
@@ -0,0 +1,7 @@
---
title: Edge Functions
description: 'Learn how to run custom backend logic close to your users, using Deno and the Supabase serverless runtime.'
chapterNumber: 6
---
Soon...
+69
View File
@@ -0,0 +1,69 @@
---
title: Supabase Foundations
description: Learn the foundations of Supabase, the Postgres development platform.
courseHero:
title: Supabase Foundations
subtitle: Learn the foundations of Supabase, the Postgres development platform.
description: In this short course, you'll explore how Supabase brings together **Database**, **Auth**, **Storage**, **Edge Functions**, and **Realtime** into a unified developer platform.
instructors:
- name: Jane Doe
gradientFrom: '#00c3ff'
gradientTo: '#ffff1c'
- name: John Doe
gradientFrom: '#ff00ea'
---
In this short course, you'll explore how Supabase brings together **Database**, **Auth**, **Storage**, **Edge Functions**, and **Realtime** into a unified developer platform.
By the end, you'll understand how each piece of Supabase fits together, how to persist and protect your data, and how to start confidently building your own applications.
## What you'll learn
- **Architecture**: how Supabase's core products work together.
- **Data fundamentals**: create tables, insert and retrieve data, and explore our generated APIs.
- **Authentication**: create and sign in users, and make secure requests.
- **Security**: enable Row Level Security (RLS) and write policies to protect your data.
- **Storage**: upload and serve files using buckets.
- **Edge Functions**: run custom backend logic close to your users, using Deno and Supabase's serverless runtime.
- **Vector search**: store and query embeddings for AI-powered search and recommendations.
---
## Prerequisites
- A [Supabase account](https://supabase.com/) (free)
- Basic familiarity with a terminal to run queries.
---
## Course outline
The course is split into 7 chapters, each approximately 10 minutes long.
1. **What is Supabase?**
Get an overview of Supabase's architecture and core products. Create your first project and make a simple API call.
2. **Your First Table**
Create a Postgres table in Supabase, insert data, and query it through the REST API.
3. **Authentication Basics**
Add users, sign them in, and understand how Supabase secures your APIs with JWTs.
4. **Secure Your Data with Row-Level Security**
Enable Row-Level Security and write policies to ensure users only access their own data.
5. **Storage**
Store files in Supabase Storage and upload and serve files from your buckets.
6. **Edge Functions**
Deploy simple functions to run backend logic close to your users using Deno.
7. **Vector Search**
Store and query embeddings for AI-powered search and recommendations using Supabase Vector.
---
## Before you begin
Create a new project at [supabase.com](https://supabase.com), open the **Supabase Dashboard**, and grab your **API URL** and **anon key** from the [Connect dialog](https://supabase.com/dashboard/project/_/settings/api?showConnect=true&connectTab=frameworks).
You'll use them throughout the course to send real requests to your database and APIs.
---
## Duration
Approx. **1 hour** — a fast, hands-on introduction to the Supabase platform.
@@ -0,0 +1,79 @@
---
title: Supabase Foundations
description: Learn the foundations of Supabase, the Postgres development platform.
explore:
- title: Supabase Docs
link: https://supabase.com/docs
itemType: doc
courseHero:
title: Supabase Foundations
subtitle: Learn the foundations of Supabase, the Postgres development platform.
description: In this short course, you'll explore how Supabase brings together **Database**, **Auth**, **Storage**, **Edge Functions**, and **Realtime** into a unified developer platform.
stats:
- label: 5 Chapters
icon: book-open
accent: emerald
- label: ~1 hour
icon: clock
accent: blue
- label: Beginner
icon: zap
accent: amber
---
In this short course, you'll explore how Supabase brings together **Database**, **Auth**, **Storage**, **Edge Functions**, and **Realtime** into a unified developer platform.
By the end, you'll understand how each piece of Supabase fits together, how to persist and protect your data, and how to start confidently building your own applications.
## What you'll learn
- **Architecture**: how Supabase's core products work together.
- **Data fundamentals**: create tables, insert and retrieve data, and explore our generated APIs.
- **Authentication**: create and sign in users, and make secure requests.
- **Security**: enable Row Level Security (RLS) and write policies to protect your data.
- **Storage**: upload and serve files using buckets.
- **Edge Functions**: run custom backend logic close to your users, using Deno and Supabase's serverless runtime.
- **Vector search**: store and query embeddings for AI-powered search and recommendations.
---
## Prerequisites
- A [Supabase account](https://supabase.com/) (free)
- Basic familiarity with a terminal to run queries.
---
## Course outline
The course is split into 7 chapters, each approximately 10 minutes long.
1. **What is Supabase?**
Get an overview of Supabase's architecture and core products. Create your first project and make a simple API call.
2. **Your First Table**
Create a Postgres table in Supabase, insert data, and query it through the REST API.
3. **Authentication Basics**
Add users, sign them in, and understand how Supabase secures your APIs with JWTs.
4. **Secure Your Data with Row-Level Security**
Enable Row-Level Security and write policies to ensure users only access their own data.
5. **Storage**
Store files in Supabase Storage and upload and serve files from your buckets.
6. **Edge Functions**
Deploy simple functions to run backend logic close to your users using Deno.
7. **Vector Search**
Store and query embeddings for AI-powered search and recommendations using Supabase Vector.
---
## Before you begin
Create a new project at [supabase.com](https://supabase.com), open the **Supabase Dashboard**, and grab your **API URL** and **anon key** from the [Connect dialog](https://supabase.com/dashboard/project/_/settings/api?showConnect=true&connectTab=frameworks).
You'll use them throughout the course to send real requests to your database and APIs.
---
## Duration
Approx. **1 hour** — a fast, hands-on introduction to the Supabase platform.
@@ -0,0 +1,77 @@
---
title: Quick Start
description: Install shadcn/ui and use the components in your project
---
## Set up shadcn/ui
Start by installing [shadcn/ui](https://ui.shadcn.com/) in your project. Use the appropriate setup guide linked below for your framework of choice.
If you're already using a recent version of shadcn/ui in your existing project, you can skip this step—this library should work with your current setup. However, if you encounter issues, try deleting your existing `components.json` file and follow one of the guides to reset your setup.
<div className="grid grid-cols-2 gap-4 mt-8 sm:gap-6">
<Card>
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="w-10 h-10"
fill="currentColor"
>
<title>Next.js</title>
<path d="M11.5725 0c-.1763 0-.3098.0013-.3584.0067-.0516.0053-.2159.021-.3636.0328-3.4088.3073-6.6017 2.1463-8.624 4.9728C1.1004 6.584.3802 8.3666.1082 10.255c-.0962.659-.108.8537-.108 1.7474s.012 1.0884.108 1.7476c.652 4.506 3.8591 8.2919 8.2087 9.6945.7789.2511 1.6.4223 2.5337.5255.3636.04 1.9354.04 2.299 0 1.6117-.1783 2.9772-.577 4.3237-1.2643.2065-.1056.2464-.1337.2183-.1573-.0188-.0139-.8987-1.1938-1.9543-2.62l-1.919-2.592-2.4047-3.5583c-1.3231-1.9564-2.4117-3.556-2.4211-3.556-.0094-.0026-.0187 1.5787-.0235 3.509-.0067 3.3802-.0093 3.5162-.0516 3.596-.061.115-.108.1618-.2064.2134-.075.0374-.1408.0445-.495.0445h-.406l-.1078-.068a.4383.4383 0 01-.1572-.1712l-.0493-.1056.0053-4.703.0067-4.7054.0726-.0915c.0376-.0493.1174-.1125.1736-.143.0962-.047.1338-.0517.5396-.0517.4787 0 .5584.0187.6827.1547.0353.0377 1.3373 1.9987 2.895 4.3608a10760.433 10760.433 0 004.7344 7.1706l1.9002 2.8782.096-.0633c.8518-.5536 1.7525-1.3418 2.4657-2.1627 1.5179-1.7429 2.4963-3.868 2.8247-6.134.0961-.6591.1078-.854.1078-1.7475 0-.8937-.012-1.0884-.1078-1.7476-.6522-4.506-3.8592-8.2919-8.2087-9.6945-.7672-.2487-1.5836-.42-2.4985-.5232-.169-.0176-1.0835-.0366-1.6123-.037zm4.0685 7.217c.3473 0 .4082.0053.4857.047.1127.0562.204.1642.237.2767.0186.061.0234 1.3653.0186 4.3044l-.0067 4.2175-.7436-1.14-.7461-1.14v-3.066c0-1.982.0093-3.0963.0234-3.1502.0375-.1313.1196-.2346.2323-.2955.0961-.0494.1313-.054.4997-.054z" />
</svg>
<p className="font-medium mt-2 mb-6">Next.js</p>
<div className="border rounded-md py-1 px-2 bg-muted flex items-center gap-2">
<p>
<span className="select-none">`$`</span>`npx create-next-app -e with-supabase`
</p>
<CopyButton value="npx create-next-app -e with-supabase" />
</div>
</Card>
<LinkedCard target="_blank" href="https://ui.shadcn.com/docs/installation/react-router">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="w-10 h-10"
fill="currentColor"
>
<path d="M12.118 5.466a2.306 2.306 0 0 0-.623.08c-.278.067-.702.332-.953.583-.41.423-.49.609-.662 1.469-.08.423.41 1.43.847 1.734.45.317 1.085.502 2.065.608 1.429.16 1.84.636 1.84 2.197 0 1.377-.385 1.747-1.96 1.906-1.707.172-2.58.834-2.765 2.117-.106.781.41 1.76 1.125 2.091 1.627.768 3.15-.198 3.467-2.196.211-1.284.622-1.642 1.998-1.747 1.588-.133 2.409-.675 2.713-1.787.278-1.02-.304-2.157-1.297-2.554-.264-.106-.873-.238-1.35-.291-1.495-.16-1.879-.424-2.038-1.39-.225-1.337-.317-1.562-.794-2.09a2.174 2.174 0 0 0-1.613-.73zm-4.785 4.36a2.145 2.145 0 0 0-.497.048c-1.469.318-2.17 2.051-1.35 3.295 1.178 1.774 3.944.953 3.97-1.177.012-1.193-.98-2.143-2.123-2.166zM2.089 14.19a2.22 2.22 0 0 0-.427.052c-2.158.476-2.237 3.626-.106 4.182.53.145.582.145 1.111.013 1.191-.318 1.866-1.456 1.549-2.607-.278-1.02-1.144-1.664-2.127-1.64zm19.824.008c-.233.002-.477.058-.784.162-1.39.477-1.866 2.092-.98 3.336.557.794 1.96 1.058 2.82.516 1.416-.874 1.363-3.057-.093-3.746-.38-.186-.663-.271-.963-.268z" />
</svg>
<p className="font-medium mt-2">React Router</p>
</LinkedCard>
<LinkedCard target="_blank" href="https://ui.shadcn.com/docs/installation/tanstack">
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
className="w-10 h-10"
fill="currentColor"
>
<path d="M6.93 13.688a.343.343 0 0 1 .468.132l.063.106c.48.851.98 1.66 1.5 2.426a35.65 35.65 0 0 0 2.074 2.742.345.345 0 0 1-.039.484l-.074.066c-2.543 2.223-4.191 2.665-4.953 1.333-.746-1.305-.477-3.672.808-7.11a.344.344 0 0 1 .153-.18ZM17.75 16.3a.34.34 0 0 1 .395.27l.02.1c.628 3.286.187 4.93-1.325 4.93-1.48 0-3.36-1.402-5.649-4.203a.327.327 0 0 1-.074-.222c0-.188.156-.34.344-.34h.121a32.984 32.984 0 0 0 2.809-.098c1.07-.086 2.191-.23 3.359-.437zm.871-6.977a.353.353 0 0 1 .445-.21l.102.034c3.262 1.11 4.504 2.332 3.719 3.664-.766 1.305-2.993 2.254-6.684 2.848a.362.362 0 0 1-.238-.047.343.343 0 0 1-.125-.476l.062-.106a34.07 34.07 0 0 0 1.367-2.523c.477-.989.93-2.051 1.352-3.184zM7.797 8.34a.362.362 0 0 1 .238.047.343.343 0 0 1 .125.476l-.062.106a34.088 34.088 0 0 0-1.367 2.523c-.477.988-.93 2.051-1.352 3.184a.353.353 0 0 1-.445.21l-.102-.034C1.57 13.742.328 12.52 1.113 11.188 1.88 9.883 4.106 8.934 7.797 8.34Zm5.281-3.984c2.543-2.223 4.192-2.664 4.953-1.332.746 1.304.477 3.671-.808 7.109a.344.344 0 0 1-.153.18.343.343 0 0 1-.468-.133l-.063-.106a34.64 34.64 0 0 0-1.5-2.426 35.65 35.65 0 0 0-2.074-2.742.345.345 0 0 1 .039-.484ZM7.285 2.274c1.48 0 3.364 1.402 5.649 4.203a.349.349 0 0 1 .078.218.348.348 0 0 1-.348.344l-.117-.004a34.584 34.584 0 0 0-2.809.102 35.54 35.54 0 0 0-3.363.437.343.343 0 0 1-.394-.273l-.02-.098c-.629-3.285-.188-4.93 1.324-4.93Zm2.871 5.812h3.688a.638.638 0 0 1 .55.316l1.848 3.22a.644.644 0 0 1 0 .628l-1.847 3.223a.638.638 0 0 1-.551.316h-3.688a.627.627 0 0 1-.547-.316L7.758 12.25a.644.644 0 0 1 0-.629L9.61 8.402a.627.627 0 0 1 .546-.316Zm3.23.793a.638.638 0 0 1 .552.316l1.39 2.426a.644.644 0 0 1 0 .629l-1.39 2.43a.638.638 0 0 1-.551.316h-2.774a.627.627 0 0 1-.546-.316l-1.395-2.43a.644.644 0 0 1 0-.629l1.395-2.426a.627.627 0 0 1 .546-.316Zm-.491.867h-1.79a.624.624 0 0 0-.546.316l-.899 1.56a.644.644 0 0 0 0 .628l.899 1.563a.632.632 0 0 0 .547.316h1.789a.632.632 0 0 0 .547-.316l.898-1.563a.644.644 0 0 0 0-.629l-.898-1.558a.624.624 0 0 0-.547-.317Zm-.477.828c.227 0 .438.121.547.317l.422.73a.625.625 0 0 1 0 .629l-.422.734a.627.627 0 0 1-.547.317h-.836a.632.632 0 0 1-.547-.317l-.422-.734a.625.625 0 0 1 0-.629l.422-.73a.632.632 0 0 1 .547-.317zm-.418.817a.548.548 0 0 0-.473.273.547.547 0 0 0 0 .547.544.544 0 0 0 .473.27.544.544 0 0 0 .473-.27.547.547 0 0 0 0-.547.548.548 0 0 0-.473-.273Zm-4.422.546h.98M18.98 7.75c.391-1.895.477-3.344.223-4.398-.148-.63-.422-1.137-.84-1.508-.441-.39-1-.582-1.625-.582-1.035 0-2.12.472-3.281 1.367a14.9 14.9 0 0 0-1.473 1.316 1.206 1.206 0 0 0-.136-.144c-1.446-1.285-2.66-2.082-3.7-2.39-.617-.184-1.195-.2-1.722-.024-.559.187-1.004.574-1.317 1.117-.515.894-.652 2.074-.46 3.527.078.59.214 1.235.402 1.934a1.119 1.119 0 0 0-.215.047C3.008 8.62 1.71 9.269.926 10.015c-.465.442-.77.938-.883 1.481-.113.578 0 1.156.312 1.7.516.894 1.465 1.597 2.817 2.155.543.223 1.156.426 1.844.61a1.023 1.023 0 0 0-.07.226c-.391 1.891-.477 3.344-.223 4.395.148.629.425 1.14.84 1.508.44.39 1 .582 1.625.582 1.035 0 2.12-.473 3.28-1.364.477-.37.973-.816 1.489-1.336a1.2 1.2 0 0 0 .195.227c1.446 1.285 2.66 2.082 3.7 2.39.617.184 1.195.2 1.722.024.559-.187 1.004-.574 1.317-1.117.515-.894.652-2.074.46-3.527a14.941 14.941 0 0 0-.425-2.012 1.225 1.225 0 0 0 .238-.047c1.828-.61 3.125-1.258 3.91-2.004.465-.441.77-.937.883-1.48.113-.578 0-1.157-.313-1.7-.515-.894-1.464-1.597-2.816-2.156a14.576 14.576 0 0 0-1.906-.625.865.865 0 0 0 .059-.195z" />
</svg>
<p className="font-medium mt-2">TanStack Start</p>
</LinkedCard>
<LinkedCard target="_blank" href="https://ui.shadcn.com/docs/installation/manual">
<svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
className="w-10 h-10"
fill="currentColor"
>
<title>React SPA</title>
<path d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38-.318-.184-.688-.277-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44-.96-.236-2.006-.417-3.107-.534-.66-.905-1.345-1.727-2.035-2.447 1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442-1.107.117-2.154.298-3.113.538-.112-.49-.195-.964-.254-1.42-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87-.728.063-1.466.098-2.21.098-.74 0-1.477-.035-2.202-.093-.406-.582-.802-1.204-1.183-1.86-.372-.64-.71-1.29-1.018-1.946.303-.657.646-1.313 1.013-1.954.38-.66.773-1.286 1.18-1.868.728-.064 1.466-.098 2.21-.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933-.2-.39-.41-.783-.64-1.174-.225-.392-.465-.774-.705-1.146zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493-.28-.958-.646-1.956-1.1-2.98.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98-.45 1.017-.812 2.01-1.086 2.964-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39.24-.375.48-.762.705-1.158.225-.39.435-.788.636-1.18zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143-.695-.102-1.365-.23-2.006-.386.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295-.22-.005-.406-.05-.553-.132-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z" />
</svg>
<p className="font-medium mt-2">React SPA</p>
</LinkedCard>
</div>
<TanStackBeta />
When you've finished setting up shadcn/ui, you can start exploring the components in the sidebar
## Troubleshooting
If you encounter issues, try deleting your existing `components.json` file and follow one of the guides to reset your setup.
For issues and questions about specific frameworks, see the [FAQs](/ui/docs/getting-started/faq)
@@ -0,0 +1,7 @@
---
title: Realtime
description: 'Learn how to build real-time applications with Supabase.'
chapterNumber: 6
---
Soon...
+112
View File
@@ -0,0 +1,112 @@
---
title: Security
description: Learn how Supabase uses Row Level Security (RLS) to control which data each user can access.
chapterNumber: 4
explore:
- title: Supabase Docs
link: https://supabase.com/docs/guides/database/postgres/row-level-security
itemType: doc
- title: RLS and Policies
link: https://www.youtube.com/watch?v=WAa3a-HxLVs
itemType: video
---
Authentication tells us who the user is. But we still need to decide what data
each user is allowed to see or change. Supabase handles this using a Postgres
feature called Row Level Security (RLS).
RLS lets the database check each row before returning it. For every request, the
database asks: _should this user be allowed to access this row?_
Here is an example of a table with some notes:
```text
+--------------------------------------------------------------------------+
| user_id | content | created_at |
+---------+------------------+---------------------------------------------+
| 1 | Buy groceries | 2025-01-10 14:22:11 | ← belongs to user Alice
| 2 | Call the dentist | 2025-01-10 15:08:54 | ← belongs to user Bob
+--------------------------------------------------------------------------+
```
Because this check happens in the database, the rules can be applied to every request:
REST, GraphQL, Realtime, and Edge Functions all follow the same security
behavior. There is no separate code path to maintain.
---
## Enabling RLS
RLS is turned off by default so you can design your tables first. Once you are
ready to secure a table, you turn it on via the Dashboard or via SQL:
```text
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
```
Enabling RLS does not allow any access. It simply tells Postgres that row access
must now follow policies.
At this point, your table becomes locked down. No one can read or write data
until you add policies.
---
## Policies
A policy is a rule that describes who can do what. Policies are written in SQL,
but we will use very small, readable examples.
Example: Allow a signed-in user to see only their own rows.
```text
create policy "Users can view their own notes"
on notes
for select
using (user_id = auth.uid());
```
Explanation:
- `for select` means the policy applies to reading rows
- `auth.uid()` is the id of the signed-in user
- The user is only allowed to read rows where the `user_id` matches theirs
This gives us personal data isolation without writing any backend code.
---
## A mental model for policies
Think of policies as sentences:
`Allow X to do Y when Z is true.`
For example:
```text
Allow a user to read a note when they created it.
Allow a user to update a note when they created it.
Allow a user to delete an item when they own the project it belongs to.
Allow an admin to do everything.
```
Policies describe access in plain language.
SQL just expresses the rule.
In a typical app, a table may have one policy or several. Each policy represents
a small piece of business logic — who can do what and under what conditions.
Together, they form the full access rules for that table.
---
## What we learned
- Authentication identifies the user.
- RLS decides what data that user can access.
- RLS is a Postgres feature that checks every row before returning it.
- When RLS is enabled, access is blocked until policies are added.
- Policies describe who can read or write data and under what conditions.
- `auth.uid()` gives you the id of the signed-in user, which is used in policy
conditions.
Understanding RLS is the foundation of secure Supabase applications.
@@ -0,0 +1,7 @@
---
title: Storage
description: 'Learn how to store files in Supabase using buckets.'
chapterNumber: 5
---
Soon...
@@ -0,0 +1,7 @@
---
title: Vector Search
description: 'Learn how to store and query embeddings for AI-powered search and recommendations.'
chapterNumber: 8
---
Soon...
@@ -0,0 +1,8 @@
---
title: Grafana
description: 'Learn how to use Grafana'
chapterNumber: 8
---
This is an internal guide for Grafana.
It doesn't have an external equivalent.
@@ -0,0 +1,7 @@
---
title: Realtime
description: 'Learn how to build real-time applications with Supabase.'
chapterNumber: 6
---
This is the internal realtime guide.
@@ -0,0 +1,103 @@
---
title: Platform Kit
description: The easiest way to build platforms on top of Supabase
---
<video width="100%" muted autoPlay controls>
<source
src="https://xguihxuzqibwxjnimxev.supabase.co/storage/v1/object/public/videos/marketing/blog/platform-kit/platform-kit.mp4"
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
## Installation
See the [Supabase Platform Kit documentation](https://supabase.com/docs) for installation instructions.
## Introduction
The Platform Kit is a collection of customizable API's, hooks and components you can use to provide an embedded Supabase experience within your own platform. It comes in the form of a single dialog that enables the management of database, authentication, storage, users, secrets, logs, and performance monitoring.
**Features**
- Database, Auth, Storage, User, Secrets, Logs, and Performance management
- Responsive dialog/drawer interface (desktop & mobile)
- API proxy for Management API
- AI-powered SQL generation (optional)
- Customize to your liking
## Who is it for
Anyone who is providing Postgres databases to their users.
## Usage
Embed the manager dialog in your app and manage its state:
```tsx
import { useState } from 'react'
import { useMobile } from '@/hooks/use-mobile'
import { Button } from '@/components/ui/button'
import SupabaseManagerDialog from '@/components/supabase-manager'
export default function Example() {
const [open, setOpen] = useState(false)
const projectRef = 'your-project-ref' // Replace with your actual project ref
const isMobile = useMobile()
return (
<>
<Button onClick={() => setOpen(true)}>Open Supabase Manager</Button>
<SupabaseManagerDialog
projectRef={projectRef}
open={open}
onOpenChange={setOpen}
isMobile={isMobile}
/>
</>
)
}
```
## Quick Start
1. **Set up environment variables:** in your `.env.local` file:
```bash
SUPABASE_MANAGEMENT_API_TOKEN=your-personal-access-token
NEXT_PUBLIC_ENABLE_AI_QUERIES=true
OPENAI_API_KEY=your-openai-api-key
```
2. **Add project-level authentication checks** in your API proxy at `app/api/supabase-proxy/[...path]/route.ts` as well as your ai/sql route at `app/api/ai/sql/route.ts` to ensure only authorized users can access their own project resources.
3. **Add a Toaster for notifications:**
Place the following component at the root of your app (e.g., in your `layout.tsx` or `App.tsx`) to enable toast notifications:
```tsx
import { Toaster } from '@/components/ui/sonner'
export default function RootLayout({ children }) {
return (
<html lang="en">
<head />
<body>
<main>{children}</main>
<Toaster />
</body>
</html>
)
}
```
That's it! The default setup uses your Supabase personal access token for the Management API.
## Security
- Never expose your Management API token to the client
- Always implement authentication and permission checks in your proxy
## Further reading
- [Supabase Management API](https://supabase.com/docs/reference/api/introduction)
+254
View File
@@ -0,0 +1,254 @@
import { getHighlighter, loadTheme } from '@shikijs/compat'
import { defineDocumentType, defineNestedType, makeSource } from 'contentlayer2/source-files'
import path from 'path'
import rehypeAutolinkHeadings from 'rehype-autolink-headings'
import rehypePrettyCode from 'rehype-pretty-code'
import rehypeSlug from 'rehype-slug'
import { codeImport } from 'remark-code-import'
import remarkGfm from 'remark-gfm'
import { visit } from 'unist-util-visit'
/** @type {import('contentlayer2/source-files').ComputedFields} */
const computedFields = {
slug: {
type: 'string',
resolve: (doc) => `/${doc._raw.flattenedPath}`,
},
slugAsParams: {
type: 'string',
resolve: (doc) => doc._raw.flattenedPath,
},
}
const LinksProperties = defineNestedType(() => ({
name: 'LinksProperties',
fields: {
doc: {
type: 'string',
},
api: {
type: 'string',
},
},
}))
const ExploreItem = defineNestedType(() => ({
name: 'ExploreItem',
fields: {
title: {
type: 'string',
required: true,
},
link: {
type: 'string',
required: true,
},
itemType: {
type: 'string',
required: false,
},
description: {
type: 'string',
required: false,
},
},
}))
const CourseHero = defineNestedType(() => ({
name: 'CourseHero',
fields: {
title: {
type: 'string',
required: true,
},
subtitle: {
type: 'string',
required: true,
},
description: {
type: 'string',
required: true,
},
},
}))
const NestedProperties = defineNestedType(() => ({
name: 'NestedProperties',
fields: {
radix: {
type: 'boolean',
},
shadcn: {
type: 'boolean',
},
vaul: {
type: 'boolean',
},
inputOtp: {
type: 'boolean',
},
reactAccessibleTreeview: {
type: 'boolean',
},
},
}))
export const Doc = defineDocumentType(() => ({
name: 'Doc',
filePathPattern: `**/*.mdx`,
contentType: 'mdx',
fields: {
title: {
type: 'string',
required: true,
},
description: {
type: 'string',
required: true,
},
published: {
type: 'boolean',
default: true,
},
links: {
type: 'nested',
of: LinksProperties,
},
featured: {
type: 'boolean',
default: false,
required: false,
},
component: {
type: 'boolean',
default: false,
required: false,
},
fragment: {
type: 'boolean',
default: false,
required: false,
},
toc: {
type: 'boolean',
default: true,
required: false,
},
chapterNumber: {
type: 'number',
required: false,
},
explore: {
type: 'list',
of: ExploreItem,
required: false,
},
courseHero: {
type: 'nested',
of: CourseHero,
required: false,
},
source: {
type: 'nested',
of: NestedProperties,
},
},
computedFields,
}))
export default makeSource({
contentDirPath: './content',
documentTypes: [Doc],
mdx: {
remarkPlugins: [remarkGfm, codeImport],
rehypePlugins: [
rehypeSlug,
() => (tree) => {
visit(tree, (node) => {
if (node?.type === 'element' && node?.tagName === 'pre') {
const [codeEl] = node.children
if (codeEl.tagName !== 'code') {
return
}
if (codeEl.data?.meta) {
// Extract event from meta and pass it down the tree.
const regex = /event="([^"]*)"/
const match = codeEl.data?.meta.match(regex)
if (match) {
node.__event__ = match ? match[1] : null
codeEl.data.meta = codeEl.data.meta.replace(regex, '')
}
}
node.__rawString__ = codeEl.children?.[0].value
node.__src__ = node.properties?.__src__
node.__style__ = node.properties?.__style__
}
})
},
[
rehypePrettyCode,
// rehypePrettyCodeOptions,
{
getHighlighter: async () => {
const theme = await loadTheme(path.join(process.cwd(), '/lib/themes/supabase-2.json'))
return await getHighlighter({ theme })
},
onVisitLine(node) {
// Prevent lines from collapsing in `display: grid` mode, and allow empty
// lines to be copy/pasted
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }]
}
},
onVisitHighlightedLine(node) {
node.properties.className.push('line--highlighted')
},
onVisitHighlightedWord(node) {
node.properties.className = ['word--highlighted']
},
},
],
() => (tree) => {
visit(tree, (node) => {
if (node?.type === 'element' && node?.tagName === 'div') {
if (!('data-rehype-pretty-code-fragment' in node.properties)) {
return
}
const preElement = node.children.at(-1)
if (preElement.tagName !== 'pre') {
return
}
preElement.properties['__withMeta__'] = node.children.at(0).tagName === 'div'
preElement.properties['__rawString__'] = node.__rawString__
if (node.__src__) {
preElement.properties['__src__'] = node.__src__
}
if (node.__event__) {
preElement.properties['__event__'] = node.__event__
}
if (node.__style__) {
preElement.properties['__style__'] = node.__style__
}
}
})
},
// rehypeNpmCommand,
[
rehypeAutolinkHeadings,
{
properties: {
className: ['subheading-anchor'],
ariaLabel: 'Link to section',
},
},
],
],
},
})
+46
View File
@@ -0,0 +1,46 @@
'use client'
import { createContext, useContext, useEffect, useState } from 'react'
type Course = 'foundations' | 'smart-office' | 'performance-scaling' | 'debugging-operations'
type CourseContextType = {
course: Course
setCourse: (course: Course) => void
}
const CourseContext = createContext<CourseContextType | undefined>(undefined)
export function FrameworkProvider({ children }: { children: React.ReactNode }) {
const [course, setCourseState] = useState<Course>('foundations')
// Initialize from localStorage on mount (client-side only)
useEffect(() => {
const storedCourse = localStorage.getItem('preferredCourse')
const validCourses: Course[] = [
'foundations',
'smart-office',
'performance-scaling',
'debugging-operations',
]
if (storedCourse && validCourses.includes(storedCourse as Course)) {
setCourseState(storedCourse as Course)
}
}, [])
// Update localStorage when framework changes
const setCourse = (newCourse: Course) => {
setCourseState(newCourse)
localStorage.setItem('preferredCourse', newCourse)
}
return <CourseContext.Provider value={{ course, setCourse }}>{children}</CourseContext.Provider>
}
// Custom hook to use the framework context
export function useCourse() {
const context = useContext(CourseContext)
if (context === undefined) {
throw new Error('useCourse must be used within a CourseProvider')
}
return context
}
@@ -0,0 +1,36 @@
'use client'
import React, { ReactNode, useState, useCallback } from 'react'
import { MobileMenuContext, MobileMenuContextType } from '@/hooks/use-mobile-menu'
interface MobileMenuProviderProps {
children: ReactNode
}
/**
* Provider component for mobile menu state
* Wraps children with context that provides mobile menu state and controls
*/
export function MobileMenuProvider({ children }: MobileMenuProviderProps) {
const [open, setOpen] = useState(false)
// Use useCallback for stable function references
const handleSetOpen = useCallback((value: boolean) => {
setOpen(value)
}, [])
const toggle = useCallback(() => {
setOpen((prev) => {
return !prev
})
}, [])
// Memoize the context value to prevent unnecessary re-renders
const value: MobileMenuContextType = {
open,
setOpen: handleSetOpen,
toggle,
}
return <MobileMenuContext.Provider value={value}>{children}</MobileMenuContext.Provider>
}
+4
View File
@@ -0,0 +1,4 @@
const { defineConfig } = require('eslint/config')
const supabaseConfig = require('eslint-config-supabase/next')
module.exports = defineConfig([supabaseConfig])
+22
View File
@@ -0,0 +1,22 @@
import { useAtom } from 'jotai'
import { atomWithStorage } from 'jotai/utils'
import { ComponentProps } from 'react'
import { SonnerToaster } from 'ui'
type Config = {
style: string
radius: number
sonnerPosition: ComponentProps<typeof SonnerToaster>['position']
sonnerExpand: boolean
}
const configAtom = atomWithStorage<Config>('config', {
style: 'default',
radius: 0.5,
sonnerPosition: 'bottom-right',
sonnerExpand: false,
})
export function useConfig() {
return useAtom(configAtom)
}
+35
View File
@@ -0,0 +1,35 @@
'use client'
import { createContext, useContext } from 'react'
/**
* Context type for mobile menu state and controls
*/
export interface MobileMenuContextType {
/** Current state of the mobile menu */
open: boolean
/** Function to set the mobile menu state */
setOpen: (open: boolean) => void
/** Function to toggle the mobile menu state */
toggle: () => void
}
// Create context with null as default value to indicate it's not provided
export const MobileMenuContext = createContext<MobileMenuContextType | null>(null)
/**
* Hook to access the mobile menu state and controls
* Must be used within a MobileMenuProvider
*/
export function useMobileMenu(): MobileMenuContextType {
const context = useContext(MobileMenuContext)
if (context === null) {
throw new Error(
'useMobileMenu must be used within a MobileMenuProvider. ' +
'Ensure your component is wrapped with MobileMenuProvider.'
)
}
return context
}
+11
View File
@@ -0,0 +1,11 @@
import * as React from 'react'
export function useMounted() {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => {
setMounted(true)
}, [])
return mounted
}
+1
View File
@@ -0,0 +1 @@
export const API_URL = process.env.NEXT_PUBLIC_API_URL!
+36
View File
@@ -0,0 +1,36 @@
import { allDocs } from 'contentlayer/generated'
import { normalizeSlug } from './get-next-page'
/**
* Gets the current chapter number from a doc
*/
export function getCurrentChapter(
slug: string
): { chapterNumber?: number; completionMessage?: string } | null {
const normalizedSlug = normalizeSlug(slug)
const doc = allDocs.find((doc) => normalizeSlug(doc.slugAsParams) === normalizedSlug)
if (!doc) {
return null
}
// Get chapterNumber from frontmatter first, then fallback to parsing from title
let chapterNumber: number | undefined = (doc as any)?.chapterNumber
if (!chapterNumber) {
// Try to extract chapter number from title (e.g., "2: CSS Styling" -> 2)
const chapterMatch = doc.title.match(/^(\d+):/)
chapterNumber = chapterMatch ? parseInt(chapterMatch[1], 10) : undefined
}
if (!chapterNumber) {
return null
}
// Use description as completion message, or generate a default one
const completionMessage = doc.description || undefined
return {
chapterNumber,
completionMessage,
}
}
+44
View File
@@ -0,0 +1,44 @@
import { existsSync, readdirSync, statSync } from 'fs'
import path from 'path'
/**
* Get all internal content file paths recursively
* Returns a Set of paths like '/foundations/realtime', '/foundations/grafana'
*/
export function getInternalContentPaths(): Set<string> {
const internalDir = path.join(process.cwd(), 'content/internal')
const paths = new Set<string>()
if (!existsSync(internalDir)) {
return paths
}
function scanDirectory(dir: string, prefix: string = '') {
const entries = readdirSync(dir)
for (const entry of entries) {
const fullPath = path.join(dir, entry)
const stat = statSync(fullPath)
if (stat.isDirectory()) {
// Recursively scan subdirectories
scanDirectory(fullPath, prefix ? `${prefix}/${entry}` : entry)
} else if (entry.endsWith('.mdx') || entry.endsWith('.md')) {
// Remove file extension to get the path
const filename = entry.replace(/\.(mdx|md)$/, '')
const contentPath = prefix ? `/${prefix}/${filename}` : `/${filename}`
paths.add(contentPath)
}
}
}
scanDirectory(internalDir)
return paths
}
/**
* Check if internal content exists for a given href
*/
export function hasInternalContent(href: string, internalPaths: Set<string>): boolean {
return internalPaths.has(href)
}
+82
View File
@@ -0,0 +1,82 @@
import { allDocs } from 'contentlayer/generated'
import { courses } from '@/config/docs'
import { SidebarNavItem } from '@/types/nav'
/**
* Flattens the navigation structure to get all pages in order
* Only includes items that have an href (leaf nodes)
*/
function flattenNavItems(items: SidebarNavItem[]): SidebarNavItem[] {
const result: SidebarNavItem[] = []
for (const item of items) {
if (item.items && item.items.length > 0) {
// Recursively process children first
result.push(...flattenNavItems(item.items))
} else if (item.href) {
// Only add items with href (leaf nodes)
result.push(item)
}
}
return result
}
/**
* Normalizes a slug/href for comparison
*/
export function normalizeSlug(slug: string): string {
return slug.replace(/^\//, '').toLowerCase()
}
/**
* Gets the next page in the navigation structure based on the current page slug
*/
export function getNextPage(
currentSlug: string
): { title: string; href: string; description?: string; chapterNumber?: number } | null {
// Flatten all navigation items
const allPages = flattenNavItems(courses.items)
// Normalize the current slug for comparison
const normalizedCurrentSlug = normalizeSlug(currentSlug)
// Find the current page index
const currentIndex = allPages.findIndex((page) => {
if (!page.href) return false
const pageSlug = normalizeSlug(page.href)
return pageSlug === normalizedCurrentSlug
})
// If current page not found or it's the last page, return null
if (currentIndex === -1 || currentIndex === allPages.length - 1) {
return null
}
// Get the next page
const nextPage = allPages[currentIndex + 1]
if (!nextPage || !nextPage.href) {
return null
}
// Try to get description and chapterNumber from the doc
const nextPageSlug = normalizeSlug(nextPage.href)
const doc = allDocs.find((doc) => normalizeSlug(doc.slugAsParams) === nextPageSlug)
const description = doc?.description || nextPage.title
// Get chapterNumber from frontmatter first, then fallback to parsing from title
// Using type assertion since contentlayer types may need regeneration
let chapterNumber: number | undefined = (doc as any)?.chapterNumber
if (!chapterNumber) {
// Try to extract chapter number from title (e.g., "2: CSS Styling" -> 2)
const chapterMatch = nextPage.title.match(/^(\d+):/)
chapterNumber = chapterMatch ? parseInt(chapterMatch[1], 10) : undefined
}
return {
title: nextPage.title,
href: nextPage.href,
description,
chapterNumber,
}
}
+85
View File
@@ -0,0 +1,85 @@
import { SidebarNavItem } from '@/types/nav'
/**
* Get all hrefs from navigation items (only direct children, not nested)
*/
function getNavHrefsInSection(items?: SidebarNavItem[]): Set<string> {
const hrefs = new Set<string>()
if (!items) return hrefs
items.forEach((item) => {
if (item.href) {
hrefs.add(item.href)
}
})
return hrefs
}
/**
* Merge orphaned internal content into their respective parent sections
* For example, if /internal/foundations/grafana exists but /foundations/grafana doesn't,
* it will be added to the end of the Foundations section
*/
export function mergeInternalContentIntoSections(
navItems: SidebarNavItem[],
internalPaths: string[]
): SidebarNavItem[] {
return navItems.map((parentItem) => {
// If this item doesn't have children, return as is
if (!parentItem.items) return parentItem
// Get the section path (e.g., "/foundations" from parent href)
const parentPath = parentItem.href
if (!parentPath) return parentItem
// Get all existing hrefs in this section
const existingHrefs = getNavHrefsInSection(parentItem.items)
// Find orphaned internal items that belong to this section
const orphanedItems: SidebarNavItem[] = []
internalPaths.forEach((internalPath) => {
// Check if this internal path belongs to this parent section
// e.g., /foundations/grafana belongs to /foundations
if (internalPath.startsWith(parentPath + '/')) {
// Check if there's no corresponding public item
const publicPath = internalPath // e.g., /foundations/grafana
if (!existingHrefs.has(publicPath)) {
// Extract the title from the path
const pathParts = internalPath.split('/').filter(Boolean)
const fileName = pathParts[pathParts.length - 1]
// Convert kebab-case to Title Case
const title = fileName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ')
orphanedItems.push({
title,
href: `/internal${internalPath}`,
requiresAuth: true,
})
}
}
})
// If there are orphaned items, add them to the end of this section with a heading
if (orphanedItems.length > 0) {
// Create a separator item for "Internal Resources"
const internalResourcesHeading: SidebarNavItem = {
title: 'Internal Resources',
href: undefined,
items: orphanedItems.sort((a, b) => a.title.localeCompare(b.title)),
}
return {
...parentItem,
items: [...parentItem.items, internalResourcesHeading],
}
}
return parentItem
})
}
+19
View File
@@ -0,0 +1,19 @@
'use client'
import { usePathname } from 'next/navigation'
import { useCallback } from 'react'
import { sendTelemetryEvent } from 'common'
import { TelemetryEvent } from 'common/telemetry-constants'
import { API_URL } from 'lib/constants'
export function useSendTelemetryEvent() {
const pathname = usePathname()
return useCallback(
(event: TelemetryEvent) => {
return sendTelemetryEvent(API_URL, event, pathname)
},
[pathname]
)
}
+207
View File
@@ -0,0 +1,207 @@
{
"name": "Supabase Theme",
"displayName": "Supabase Theme",
"semanticHighlighting": true,
"colors": {
"editor.background": "var(--background-surface-75) / 0.75",
"editor.foreground": "var(--code-foreground)",
"editor.lineHighlightBackground": "var(--code-highlight-color)",
"editorCursor.foreground": "var(--code-foreground)",
"editorWhitespace.foreground": "var(--code-foreground)",
"editorIndentGuide.background": "var(--code-foreground)",
"editor.selectionBackground": "var(--code-highlight-color)",
"editor.selectionHighlightBackground": "var(--code-highlight-color)"
},
"tokenColors": [
{
"scope": ["comment", "punctuation.definition.comment", "string.comment"],
"settings": {
"foreground": "var(--code-token-comment)",
"fontStyle": "italic"
}
},
{
"scope": ["constant", "entity.name.constant", "variable.other.constant"],
"settings": {
"foreground": "var(--code-token-constant)"
}
},
{
"scope": ["entity", "entity.name", "entity.name.type", "support.type"],
"settings": {
"foreground": "var(--code-token-function)"
}
},
{
"scope": ["variable", "support.variable", "variable.parameter"],
"settings": {
"foreground": "var(--code-token-parameter)"
}
},
{
"scope": ["keyword", "storage.type", "storage.modifier", "keyword.control"],
"settings": {
"foreground": "var(--code-token-keyword)"
}
},
{
"scope": ["string", "constant.other.symbol", "constant.other.key", "string.quoted"],
"settings": {
"foreground": "var(--code-token-string)"
}
},
{
"scope": ["support.function", "entity.name.function"],
"settings": {
"foreground": "var(--code-token-function)"
}
},
{
"scope": ["punctuation", "punctuation.separator", "meta.brace", "punctuation.definition"],
"settings": {
"foreground": "var(--code-token-punctuation)"
}
},
{
"scope": ["meta.tag", "declaration.tag", "markup.deleted"],
"settings": {
"foreground": "var(--code-token-function)"
}
},
{
"scope": ["markup.inserted"],
"settings": {
"foreground": "var(--code-token-string)"
}
},
{
"scope": ["markup.changed"],
"settings": {
"foreground": "var(--code-token-string-expression)"
}
},
{
"scope": ["invalid", "invalid.deprecated"],
"settings": {
"foreground": "var(--code-foreground)",
"background": "var(--code-token-comment)"
}
},
{
"scope": ["support.type.property-name"],
"settings": {
"foreground": "var(--code-token-property)"
}
},
{
"scope": ["entity.other.attribute-name"],
"settings": {
"foreground": "var(--code-token-property)"
}
},
{
"scope": ["meta.object-literal.key"],
"settings": {
"foreground": "var(--code-token-property)"
}
},
{
"scope": ["entity.other.attribute-name", "entity.other.inherited-class"],
"settings": {
"foreground": "var(--code-token-property)"
}
},
{
"scope": ["storage", "storage.type", "storage.modifier"],
"settings": {
"foreground": "var(--code-token-keyword)"
}
},
{
"scope": ["support.class"],
"settings": {
"foreground": "var(--code-token-function)"
}
},
{
"scope": ["constant.language"],
"settings": {
"foreground": "var(--code-token-constant)"
}
},
{
"scope": ["meta.embedded"],
"settings": {
"foreground": "var(--code-token-string-expression)"
}
},
{
"scope": ["meta.function-call"],
"settings": {
"foreground": "var(--code-token-function)"
}
},
{
"scope": ["meta.method-call"],
"settings": {
"foreground": "var(--code-token-function)"
}
},
{
"scope": ["meta.return-type"],
"settings": {
"foreground": "var(--code-token-keyword)"
}
},
{
"scope": [
"meta.import",
"meta.import.js",
"meta.import.ts",
"meta.import.tsx",
"meta.import.jsx",
"string.quoted.single.js",
"string.quoted.double.js",
"string.quoted.single.ts",
"string.quoted.double.ts",
"string.quoted.single.tsx",
"string.quoted.double.tsx",
"string.quoted.single.jsx",
"string.quoted.double.jsx"
],
"settings": {
"foreground": "var(--code-token-string-expression)"
}
},
{
"scope": ["meta.var.expr"],
"settings": {
"foreground": "var(--code-token-variable)"
}
},
{
"scope": ["meta.block"],
"settings": {
"foreground": "var(--code-foreground)"
}
},
{
"scope": ["meta.delimiter.period"],
"settings": {
"foreground": "var(--code-foreground)"
}
},
{
"scope": ["meta.brace.round"],
"settings": {
"foreground": "var(--code-foreground)"
}
},
{
"scope": ["meta.paren.expr"],
"settings": {
"foreground": "var(--code-foreground)"
}
}
]
}
+79
View File
@@ -0,0 +1,79 @@
// @ts-nocheck
// TODO: I'll fix this later.
import { toc } from 'mdast-util-toc'
import { remark } from 'remark'
import { visit } from 'unist-util-visit'
const textTypes = ['text', 'emphasis', 'strong', 'inlineCode']
function flattenNode(node) {
const p = []
visit(node, (node) => {
if (!textTypes.includes(node.type)) return
p.push(node.value)
})
return p.join(``)
}
interface Item {
title: string
url: string
items?: Item[]
}
interface Items {
items?: Item[]
}
function getItems(node, current): Items {
if (!node) {
return {}
}
if (node.type === 'paragraph') {
visit(node, (item) => {
if (item.type === 'link') {
current.url = item.url
current.title = flattenNode(node)
}
if (item.type === 'text') {
current.title = flattenNode(node)
}
})
return current
}
if (node.type === 'list') {
current.items = node.children.map((i) => getItems(i, {}))
return current
} else if (node.type === 'listItem') {
const heading = getItems(node.children[0], {})
if (node.children.length > 1) {
getItems(node.children[1], heading)
}
return heading
}
return {}
}
const getToc = () => (node, file) => {
const table = toc(node)
const items = getItems(table.map, {})
file.data = items
}
export type TableOfContents = Items
export async function getTableOfContents(content: string): Promise<TableOfContents> {
const result = await remark().use(getToc).process(content)
return result.data
}
+7
View File
@@ -0,0 +1,7 @@
import { cn as uiCN } from 'ui'
export const cn = uiCN
export function absoluteUrl(path: string) {
return `${process.env.NEXT_PUBLIC_APP_URL}${path}`
}
+27
View File
@@ -0,0 +1,27 @@
import { withContentlayer } from 'next-contentlayer2'
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['ui', 'common', 'shared-data', 'icons', 'tsconfig'],
basePath: process.env.NEXT_PUBLIC_BASE_PATH,
async redirects() {
return [
...(process.env.NEXT_PUBLIC_BASE_PATH?.length
? [
{
source: '/',
destination: process.env.NEXT_PUBLIC_BASE_PATH,
basePath: false,
permanent: false,
},
]
: []),
]
},
eslint: {
// We are already running linting via GH action, this will skip linting during production build on Vercel
ignoreDuringBuilds: true,
},
}
export default withContentlayer(nextConfig)
+116
View File
@@ -0,0 +1,116 @@
{
"name": "learn",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "next dev --port 3007",
"build": "pnpm run content:build && next build --turbopack",
"internal:sync": "tsx --tsconfig ./tsconfig.scripts.json ./scripts/sync-internal-content.mts",
"start": "next start",
"lint": "eslint .",
"lint:mdx": "supa-mdx-lint content --config ../../supa-mdx-lint.config.toml",
"content:build": "contentlayer2 build",
"clean": "rimraf node_modules .next .turbo",
"typecheck": "contentlayer2 build && tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@hookform/resolvers": "^3.1.1",
"@monaco-editor/react": "^4.6.0",
"@radix-ui/react-accordion": "*",
"@radix-ui/react-alert-dialog": "*",
"@radix-ui/react-aspect-ratio": "*",
"@radix-ui/react-avatar": "*",
"@radix-ui/react-checkbox": "*",
"@radix-ui/react-collapsible": "*",
"@radix-ui/react-context-menu": "*",
"@radix-ui/react-dialog": "*",
"@radix-ui/react-dropdown-menu": "*",
"@radix-ui/react-hover-card": "*",
"@radix-ui/react-label": "*",
"@radix-ui/react-menubar": "*",
"@radix-ui/react-navigation-menu": "*",
"@radix-ui/react-popover": "*",
"@radix-ui/react-progress": "*",
"@radix-ui/react-radio-group": "*",
"@radix-ui/react-scroll-area": "*",
"@radix-ui/react-select": "*",
"@radix-ui/react-separator": "*",
"@radix-ui/react-slider": "*",
"@radix-ui/react-slot": "*",
"@radix-ui/react-switch": "*",
"@radix-ui/react-tabs": "*",
"@radix-ui/react-toast": "*",
"@radix-ui/react-toggle": "*",
"@radix-ui/react-toggle-group": "*",
"@radix-ui/react-tooltip": "*",
"@react-router/fs-routes": "^7.4.0",
"@supabase/postgrest-js": "catalog:",
"@supabase/supa-mdx-lint": "0.2.6-alpha",
"@supabase/vue-blocks": "workspace:*",
"@tanstack/react-query": "^5.83.0",
"axios": "^1.12.0",
"class-variance-authority": "*",
"cmdk": "^1.0.0",
"common": "workspace:*",
"common-tags": "^1.8.2",
"contentlayer2": "0.4.6",
"eslint-config-supabase": "workspace:*",
"framer-motion": "^11.0.3",
"icons": "workspace:*",
"jotai": "^2.8.0",
"lucide-react": "*",
"next": "catalog:",
"next-contentlayer2": "0.4.6",
"next-themes": "^0.3.0",
"openai": "^5.9.0",
"openapi-fetch": "0.12.4",
"react": "catalog:",
"react-docgen": "^7.0.3",
"react-dom": "catalog:",
"react-hook-form": "^7.45.0",
"react-markdown": "^10.1.0",
"react-wrap-balancer": "^1.1.0",
"recharts": "^2.8.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-pretty-code": "^0.9.0",
"rehype-slug": "^6.0.0",
"remark": "^14.0.3",
"remark-code-import": "^1.2.0",
"remark-gfm": "^4.0.0",
"sonner": "^1.5.0",
"ui": "workspace:*",
"ui-patterns": "workspace:*",
"unist-util-visit": "^5.0.0",
"vaul": "^0.9.6",
"zod": "catalog:"
},
"devDependencies": {
"@react-router/dev": "^7.1.5",
"@shikijs/compat": "^1.1.7",
"@supabase/ssr": "^0.7.0",
"@supabase/supabase-js": "catalog:",
"@tanstack/react-router": "^1.114.27",
"@tanstack/react-start": "^1.114.25",
"@types/common-tags": "^1.8.4",
"@types/lodash": "^4.17.16",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"config": "workspace:^",
"dotenv": "^17.2.3",
"lodash": "^4.17.21",
"mdast-util-toc": "^6.1.1",
"postcss": "^8.5.3",
"react-dropzone": "^14.3.8",
"react-router": "^7.5.2",
"rimraf": "^4.1.3",
"shadcn": "^3.0.0",
"shiki": "^1.1.7",
"tailwindcss": "catalog:",
"tsconfig": "workspace:*",
"tsx": "catalog:",
"typescript": "catalog:",
"vite": "catalog:"
}
}
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
plugins: {
tailwindcss: {},
},
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig><msapplication><tile><square70x70logo src="/favicon/ms-icon-70x70.png"/><square150x150logo src="/favicon/ms-icon-150x150.png"/><square310x310logo src="/favicon/ms-icon-310x310.png"/><TileColor>#ffffff</TileColor></tile></msapplication></browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 568 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+46
View File
@@ -0,0 +1,46 @@
{
"name": "Supabase",
"short_name": "Supabase",
"description": "The Postgres Development Platform.",
"display": "standalone",
"theme_color": "#1C1C1C",
"background_color": "#1C1C1C",
"icons": [
{
"src": "/favicon/android-icon-36x36.png",
"sizes": "36x36",
"type": "image/png",
"density": "0.75"
},
{
"src": "/favicon/android-icon-48x48.png",
"sizes": "48x48",
"type": "image/png",
"density": "1.0"
},
{
"src": "/favicon/android-icon-72x72.png",
"sizes": "72x72",
"type": "image/png",
"density": "1.5"
},
{
"src": "/favicon/android-icon-96x96.png",
"sizes": "96x96",
"type": "image/png",
"density": "2.0"
},
{
"src": "/favicon/android-icon-144x144.png",
"sizes": "144x144",
"type": "image/png",
"density": "3.0"
},
{
"src": "/favicon/android-icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"density": "4.0"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 B

@@ -0,0 +1,100 @@
<svg width="570" height="527" viewBox="0 0 570 527" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0027 21.0027L30.7733 24.7733L44.2293 38.2293L48 42L51.7707 38.2293L65.2267 24.7733L68.9973 21.0027L65.2267 17.2267L51.7707 3.77067L48 0L44.2293 3.77067L30.7733 17.2267L27.0027 21.0027ZM61.456 21.0027L48 34.4533L34.544 20.9973L48 7.54133L61.456 21.0027ZM27.0027 74.9973L30.7733 78.7733L44.2293 92.2293L48 96L51.7707 92.2293L65.2267 78.7733L68.9973 75.0027L65.2267 71.2267L51.7707 57.7707L48 54L44.2293 57.7707L30.7733 71.2267L27.0027 74.9973ZM61.456 74.9973L48 88.464L34.544 75.008L48 61.536L61.456 74.9973ZM3.77067 51.7707L0 48L3.77067 44.2293L17.2267 30.7733L21.0027 27.0027L24.7733 30.7733L38.2293 44.2293L42 48L38.2293 51.7707L24.7733 65.2267L21.0027 68.9973L17.2267 65.2267L3.77067 51.7707ZM20.9973 61.456L34.464 48L20.9973 34.544L7.54133 48L20.9973 61.456ZM54 48L57.7707 51.7707L71.2267 65.2267L74.9973 68.9973L78.7733 65.2267L92.2293 51.7707L96 48L92.2293 44.2293L78.7733 30.7733L75.0027 27.0027L71.2267 30.7733L57.7707 44.2293L54 48ZM88.4533 48L74.9973 61.456L61.5467 48L75.008 34.544L88.4533 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip1_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 48L206 0L158 48L206 96L254 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip2_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 48L364 0L316 48L364 96L412 48ZM323.541 48L364 7.54133L404.459 48L364 88.4587L323.541 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip3_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 21.0027L504.773 24.7733L518.229 38.2293L522 42L525.771 38.2293L539.227 24.7733L542.997 21.0027L539.227 17.2267L525.771 3.77067L522 0L518.229 3.77067L504.773 17.2267L501.003 21.0027ZM501.003 74.9973L504.773 78.7733L518.229 92.2293L522 96L525.771 92.2293L539.227 78.7733L542.997 75.0027L539.227 71.2267L525.771 57.7707L522 54L518.229 57.7707L504.773 71.2267L501.003 74.9973ZM477.771 51.7707L474 48L477.771 44.2293L491.227 30.7733L495.003 27.0027L498.773 30.7733L512.229 44.2293L516 48L512.229 51.7707L498.773 65.2267L495.003 68.9973L491.227 65.2267L477.771 51.7707ZM528 48L531.771 51.7707L545.227 65.2267L548.997 68.9973L552.773 65.2267L566.229 51.7707L570 48L566.229 44.2293L552.773 30.7733L549.003 27.0027L545.227 30.7733L531.771 44.2293L528 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip4_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 195L48 147L0 195L48 243L96 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip5_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 195L206 147L158 195L206 243L254 195ZM165.541 195L206 154.541L246.459 195L206 235.459L165.541 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip6_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M343.003 168.003L346.773 171.773L360.229 185.229L364 189L367.771 185.229L381.227 171.773L384.997 168.003L381.227 164.227L367.771 150.771L364 147L360.229 150.771L346.773 164.227L343.003 168.003ZM377.456 168.003L364 181.453L350.544 167.997L364 154.541L377.456 168.003ZM343.003 221.997L346.773 225.773L360.229 239.229L364 243L367.771 239.229L381.227 225.773L384.997 222.003L381.227 218.227L367.771 204.771L364 201L360.229 204.771L346.773 218.227L343.003 221.997ZM377.456 221.997L364 235.464L350.544 222.008L364 208.536L377.456 221.997ZM319.771 198.771L316 195L319.771 191.229L333.227 177.773L337.003 174.003L340.773 177.773L354.229 191.229L358 195L354.229 198.771L340.773 212.227L337.003 215.997L333.227 212.227L319.771 198.771ZM336.997 208.456L350.464 195L336.997 181.544L323.541 195L336.997 208.456ZM370 195L373.771 198.771L387.227 212.227L390.997 215.997L394.773 212.227L408.229 198.771L412 195L408.229 191.229L394.773 177.773L391.003 174.003L387.227 177.773L373.771 191.229L370 195ZM404.453 195L390.997 208.456L377.547 195L391.008 181.544L404.453 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip7_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 168.003L504.773 171.773L518.229 185.229L522 189L525.771 185.229L539.227 171.773L542.997 168.003L539.227 164.227L525.771 150.771L522 147L518.229 150.771L504.773 164.227L501.003 168.003ZM501.003 221.997L504.773 225.773L518.229 239.229L522 243L525.771 239.229L539.227 225.773L542.997 222.003L539.227 218.227L525.771 204.771L522 201L518.229 204.771L504.773 218.227L501.003 221.997ZM477.771 198.771L474 195L477.771 191.229L491.227 177.773L495.003 174.003L498.773 177.773L512.229 191.229L516 195L512.229 198.771L498.773 212.227L495.003 215.997L491.227 212.227L477.771 198.771ZM528 195L531.771 198.771L545.227 212.227L548.997 215.997L552.773 212.227L566.229 198.771L570 195L566.229 191.229L552.773 177.773L549.003 174.003L545.227 177.773L531.771 191.229L528 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip8_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 342L48 294L0 342L48 390L96 342ZM7.54133 342L48 301.541L88.4587 342L48 382.459L7.54133 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip9_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.003 315.003L188.773 318.773L202.229 332.229L206 336L209.771 332.229L223.227 318.773L226.997 315.003L223.227 311.227L209.771 297.771L206 294L202.229 297.771L188.773 311.227L185.003 315.003ZM185.003 368.997L188.773 372.773L202.229 386.229L206 390L209.771 386.229L223.227 372.773L226.997 369.003L223.227 365.227L209.771 351.771L206 348L202.229 351.771L188.773 365.227L185.003 368.997ZM161.771 345.771L158 342L161.771 338.229L175.227 324.773L179.003 321.003L182.773 324.773L196.229 338.229L200 342L196.229 345.771L182.773 359.227L179.003 362.997L175.227 359.227L161.771 345.771ZM212 342L215.771 345.771L229.227 359.227L232.997 362.997L236.773 359.227L250.229 345.771L254 342L250.229 338.229L236.773 324.773L233.003 321.003L229.227 324.773L215.771 338.229L212 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip10_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 342L364 294L316 342L364 390L412 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip11_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 315.003L504.773 318.773L518.229 332.229L522 336L525.771 332.229L539.227 318.773L542.997 315.003L539.227 311.227L525.771 297.771L522 294L518.229 297.771L504.773 311.227L501.003 315.003ZM535.456 315.003L522 328.453L508.544 314.997L522 301.541L535.456 315.003ZM501.003 368.997L504.773 372.773L518.229 386.229L522 390L525.771 386.229L539.227 372.773L542.997 369.003L539.227 365.227L525.771 351.771L522 348L518.229 351.771L504.773 365.227L501.003 368.997ZM535.456 368.997L522 382.464L508.544 369.008L522 355.536L535.456 368.997ZM477.771 345.771L474 342L477.771 338.229L491.227 324.773L495.003 321.003L498.773 324.773L512.229 338.229L516 342L512.229 345.771L498.773 359.227L495.003 362.997L491.227 359.227L477.771 345.771ZM494.997 355.456L508.464 342L494.997 328.544L481.541 342L494.997 355.456ZM528 342L531.771 345.771L545.227 359.227L548.997 362.997L552.773 359.227L566.229 345.771L570 342L566.229 338.229L552.773 324.773L549.003 321.003L545.227 324.773L531.771 338.229L528 342ZM562.453 342L548.997 355.456L535.547 342L549.008 328.544L562.453 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip12_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 479L48 431L0 479L48 527L96 479ZM7.54133 479L48 438.541L88.4587 479L48 519.459L7.54133 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip13_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.003 452.003L188.773 455.773L202.229 469.229L206 473L209.771 469.229L223.227 455.773L226.997 452.003L223.227 448.227L209.771 434.771L206 431L202.229 434.771L188.773 448.227L185.003 452.003ZM219.456 452.003L206 465.453L192.544 451.997L206 438.541L219.456 452.003ZM185.003 505.997L188.773 509.773L202.229 523.229L206 527L209.771 523.229L223.227 509.773L226.997 506.003L223.227 502.227L209.771 488.771L206 485L202.229 488.771L188.773 502.227L185.003 505.997ZM219.456 505.997L206 519.464L192.544 506.008L206 492.536L219.456 505.997ZM161.771 482.771L158 479L161.771 475.229L175.227 461.773L179.003 458.003L182.773 461.773L196.229 475.229L200 479L196.229 482.771L182.773 496.227L179.003 499.997L175.227 496.227L161.771 482.771ZM178.997 492.456L192.464 479L178.997 465.544L165.541 479L178.997 492.456ZM212 479L215.771 482.771L229.227 496.227L232.997 499.997L236.773 496.227L250.229 482.771L254 479L250.229 475.229L236.773 461.773L233.003 458.003L229.227 461.773L215.771 475.229L212 479ZM246.453 479L232.997 492.456L219.547 479L233.008 465.544L246.453 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip14_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 479L364 431L316 479L364 527L412 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip15_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 452.003L504.773 455.773L518.229 469.229L522 473L525.771 469.229L539.227 455.773L542.997 452.003L539.227 448.227L525.771 434.771L522 431L518.229 434.771L504.773 448.227L501.003 452.003ZM501.003 505.997L504.773 509.773L518.229 523.229L522 527L525.771 523.229L539.227 509.773L542.997 506.003L539.227 502.227L525.771 488.771L522 485L518.229 488.771L504.773 502.227L501.003 505.997ZM477.771 482.771L474 479L477.771 475.229L491.227 461.773L495.003 458.003L498.773 461.773L512.229 475.229L516 479L512.229 482.771L498.773 496.227L495.003 499.997L491.227 496.227L477.771 482.771ZM528 479L531.771 482.771L545.227 496.227L548.997 499.997L552.773 496.227L566.229 482.771L570 479L566.229 475.229L552.773 461.773L549.003 458.003L545.227 461.773L531.771 475.229L528 479Z" fill="#B4B4B4"/>
</g>
<defs>
<clipPath id="clip0_1709_1536">
<rect width="96" height="96" fill="white"/>
</clipPath>
<clipPath id="clip1_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158)"/>
</clipPath>
<clipPath id="clip2_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316)"/>
</clipPath>
<clipPath id="clip3_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474)"/>
</clipPath>
<clipPath id="clip4_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 147)"/>
</clipPath>
<clipPath id="clip5_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 147)"/>
</clipPath>
<clipPath id="clip6_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 147)"/>
</clipPath>
<clipPath id="clip7_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 147)"/>
</clipPath>
<clipPath id="clip8_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 294)"/>
</clipPath>
<clipPath id="clip9_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 294)"/>
</clipPath>
<clipPath id="clip10_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 294)"/>
</clipPath>
<clipPath id="clip11_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 294)"/>
</clipPath>
<clipPath id="clip12_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 431)"/>
</clipPath>
<clipPath id="clip13_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 431)"/>
</clipPath>
<clipPath id="clip14_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 431)"/>
</clipPath>
<clipPath id="clip15_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 431)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,100 @@
<svg width="570" height="527" viewBox="0 0 570 527" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0027 21.0027L30.7733 24.7733L44.2293 38.2293L48 42L51.7707 38.2293L65.2267 24.7733L68.9973 21.0027L65.2267 17.2267L51.7707 3.77067L48 0L44.2293 3.77067L30.7733 17.2267L27.0027 21.0027ZM61.456 21.0027L48 34.4533L34.544 20.9973L48 7.54133L61.456 21.0027ZM27.0027 74.9973L30.7733 78.7733L44.2293 92.2293L48 96L51.7707 92.2293L65.2267 78.7733L68.9973 75.0027L65.2267 71.2267L51.7707 57.7707L48 54L44.2293 57.7707L30.7733 71.2267L27.0027 74.9973ZM61.456 74.9973L48 88.464L34.544 75.008L48 61.536L61.456 74.9973ZM3.77067 51.7707L0 48L3.77067 44.2293L17.2267 30.7733L21.0027 27.0027L24.7733 30.7733L38.2293 44.2293L42 48L38.2293 51.7707L24.7733 65.2267L21.0027 68.9973L17.2267 65.2267L3.77067 51.7707ZM20.9973 61.456L34.464 48L20.9973 34.544L7.54133 48L20.9973 61.456ZM54 48L57.7707 51.7707L71.2267 65.2267L74.9973 68.9973L78.7733 65.2267L92.2293 51.7707L96 48L92.2293 44.2293L78.7733 30.7733L75.0027 27.0027L71.2267 30.7733L57.7707 44.2293L54 48ZM88.4533 48L74.9973 61.456L61.5467 48L75.008 34.544L88.4533 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip1_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 48L206 0L158 48L206 96L254 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip2_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 48L364 0L316 48L364 96L412 48ZM323.541 48L364 7.54133L404.459 48L364 88.4587L323.541 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip3_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 21.0027L504.773 24.7733L518.229 38.2293L522 42L525.771 38.2293L539.227 24.7733L542.997 21.0027L539.227 17.2267L525.771 3.77067L522 0L518.229 3.77067L504.773 17.2267L501.003 21.0027ZM501.003 74.9973L504.773 78.7733L518.229 92.2293L522 96L525.771 92.2293L539.227 78.7733L542.997 75.0027L539.227 71.2267L525.771 57.7707L522 54L518.229 57.7707L504.773 71.2267L501.003 74.9973ZM477.771 51.7707L474 48L477.771 44.2293L491.227 30.7733L495.003 27.0027L498.773 30.7733L512.229 44.2293L516 48L512.229 51.7707L498.773 65.2267L495.003 68.9973L491.227 65.2267L477.771 51.7707ZM528 48L531.771 51.7707L545.227 65.2267L548.997 68.9973L552.773 65.2267L566.229 51.7707L570 48L566.229 44.2293L552.773 30.7733L549.003 27.0027L545.227 30.7733L531.771 44.2293L528 48Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip4_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 195L48 147L0 195L48 243L96 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip5_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 195L206 147L158 195L206 243L254 195ZM165.541 195L206 154.541L246.459 195L206 235.459L165.541 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip6_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M343.003 168.003L346.773 171.773L360.229 185.229L364 189L367.771 185.229L381.227 171.773L384.997 168.003L381.227 164.227L367.771 150.771L364 147L360.229 150.771L346.773 164.227L343.003 168.003ZM377.456 168.003L364 181.453L350.544 167.997L364 154.541L377.456 168.003ZM343.003 221.997L346.773 225.773L360.229 239.229L364 243L367.771 239.229L381.227 225.773L384.997 222.003L381.227 218.227L367.771 204.771L364 201L360.229 204.771L346.773 218.227L343.003 221.997ZM377.456 221.997L364 235.464L350.544 222.008L364 208.536L377.456 221.997ZM319.771 198.771L316 195L319.771 191.229L333.227 177.773L337.003 174.003L340.773 177.773L354.229 191.229L358 195L354.229 198.771L340.773 212.227L337.003 215.997L333.227 212.227L319.771 198.771ZM336.997 208.456L350.464 195L336.997 181.544L323.541 195L336.997 208.456ZM370 195L373.771 198.771L387.227 212.227L390.997 215.997L394.773 212.227L408.229 198.771L412 195L408.229 191.229L394.773 177.773L391.003 174.003L387.227 177.773L373.771 191.229L370 195ZM404.453 195L390.997 208.456L377.547 195L391.008 181.544L404.453 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip7_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 168.003L504.773 171.773L518.229 185.229L522 189L525.771 185.229L539.227 171.773L542.997 168.003L539.227 164.227L525.771 150.771L522 147L518.229 150.771L504.773 164.227L501.003 168.003ZM501.003 221.997L504.773 225.773L518.229 239.229L522 243L525.771 239.229L539.227 225.773L542.997 222.003L539.227 218.227L525.771 204.771L522 201L518.229 204.771L504.773 218.227L501.003 221.997ZM477.771 198.771L474 195L477.771 191.229L491.227 177.773L495.003 174.003L498.773 177.773L512.229 191.229L516 195L512.229 198.771L498.773 212.227L495.003 215.997L491.227 212.227L477.771 198.771ZM528 195L531.771 198.771L545.227 212.227L548.997 215.997L552.773 212.227L566.229 198.771L570 195L566.229 191.229L552.773 177.773L549.003 174.003L545.227 177.773L531.771 191.229L528 195Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip8_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 342L48 294L0 342L48 390L96 342ZM7.54133 342L48 301.541L88.4587 342L48 382.459L7.54133 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip9_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.003 315.003L188.773 318.773L202.229 332.229L206 336L209.771 332.229L223.227 318.773L226.997 315.003L223.227 311.227L209.771 297.771L206 294L202.229 297.771L188.773 311.227L185.003 315.003ZM185.003 368.997L188.773 372.773L202.229 386.229L206 390L209.771 386.229L223.227 372.773L226.997 369.003L223.227 365.227L209.771 351.771L206 348L202.229 351.771L188.773 365.227L185.003 368.997ZM161.771 345.771L158 342L161.771 338.229L175.227 324.773L179.003 321.003L182.773 324.773L196.229 338.229L200 342L196.229 345.771L182.773 359.227L179.003 362.997L175.227 359.227L161.771 345.771ZM212 342L215.771 345.771L229.227 359.227L232.997 362.997L236.773 359.227L250.229 345.771L254 342L250.229 338.229L236.773 324.773L233.003 321.003L229.227 324.773L215.771 338.229L212 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip10_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 342L364 294L316 342L364 390L412 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip11_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 315.003L504.773 318.773L518.229 332.229L522 336L525.771 332.229L539.227 318.773L542.997 315.003L539.227 311.227L525.771 297.771L522 294L518.229 297.771L504.773 311.227L501.003 315.003ZM535.456 315.003L522 328.453L508.544 314.997L522 301.541L535.456 315.003ZM501.003 368.997L504.773 372.773L518.229 386.229L522 390L525.771 386.229L539.227 372.773L542.997 369.003L539.227 365.227L525.771 351.771L522 348L518.229 351.771L504.773 365.227L501.003 368.997ZM535.456 368.997L522 382.464L508.544 369.008L522 355.536L535.456 368.997ZM477.771 345.771L474 342L477.771 338.229L491.227 324.773L495.003 321.003L498.773 324.773L512.229 338.229L516 342L512.229 345.771L498.773 359.227L495.003 362.997L491.227 359.227L477.771 345.771ZM494.997 355.456L508.464 342L494.997 328.544L481.541 342L494.997 355.456ZM528 342L531.771 345.771L545.227 359.227L548.997 362.997L552.773 359.227L566.229 345.771L570 342L566.229 338.229L552.773 324.773L549.003 321.003L545.227 324.773L531.771 338.229L528 342ZM562.453 342L548.997 355.456L535.547 342L549.008 328.544L562.453 342Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip12_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 479L48 431L0 479L48 527L96 479ZM7.54133 479L48 438.541L88.4587 479L48 519.459L7.54133 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip13_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.003 452.003L188.773 455.773L202.229 469.229L206 473L209.771 469.229L223.227 455.773L226.997 452.003L223.227 448.227L209.771 434.771L206 431L202.229 434.771L188.773 448.227L185.003 452.003ZM219.456 452.003L206 465.453L192.544 451.997L206 438.541L219.456 452.003ZM185.003 505.997L188.773 509.773L202.229 523.229L206 527L209.771 523.229L223.227 509.773L226.997 506.003L223.227 502.227L209.771 488.771L206 485L202.229 488.771L188.773 502.227L185.003 505.997ZM219.456 505.997L206 519.464L192.544 506.008L206 492.536L219.456 505.997ZM161.771 482.771L158 479L161.771 475.229L175.227 461.773L179.003 458.003L182.773 461.773L196.229 475.229L200 479L196.229 482.771L182.773 496.227L179.003 499.997L175.227 496.227L161.771 482.771ZM178.997 492.456L192.464 479L178.997 465.544L165.541 479L178.997 492.456ZM212 479L215.771 482.771L229.227 496.227L232.997 499.997L236.773 496.227L250.229 482.771L254 479L250.229 475.229L236.773 461.773L233.003 458.003L229.227 461.773L215.771 475.229L212 479ZM246.453 479L232.997 492.456L219.547 479L233.008 465.544L246.453 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip14_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 479L364 431L316 479L364 527L412 479Z" fill="#B4B4B4"/>
</g>
<g clip-path="url(#clip15_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 452.003L504.773 455.773L518.229 469.229L522 473L525.771 469.229L539.227 455.773L542.997 452.003L539.227 448.227L525.771 434.771L522 431L518.229 434.771L504.773 448.227L501.003 452.003ZM501.003 505.997L504.773 509.773L518.229 523.229L522 527L525.771 523.229L539.227 509.773L542.997 506.003L539.227 502.227L525.771 488.771L522 485L518.229 488.771L504.773 502.227L501.003 505.997ZM477.771 482.771L474 479L477.771 475.229L491.227 461.773L495.003 458.003L498.773 461.773L512.229 475.229L516 479L512.229 482.771L498.773 496.227L495.003 499.997L491.227 496.227L477.771 482.771ZM528 479L531.771 482.771L545.227 496.227L548.997 499.997L552.773 496.227L566.229 482.771L570 479L566.229 475.229L552.773 461.773L549.003 458.003L545.227 461.773L531.771 475.229L528 479Z" fill="#B4B4B4"/>
</g>
<defs>
<clipPath id="clip0_1709_1536">
<rect width="96" height="96" fill="white"/>
</clipPath>
<clipPath id="clip1_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158)"/>
</clipPath>
<clipPath id="clip2_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316)"/>
</clipPath>
<clipPath id="clip3_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474)"/>
</clipPath>
<clipPath id="clip4_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 147)"/>
</clipPath>
<clipPath id="clip5_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 147)"/>
</clipPath>
<clipPath id="clip6_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 147)"/>
</clipPath>
<clipPath id="clip7_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 147)"/>
</clipPath>
<clipPath id="clip8_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 294)"/>
</clipPath>
<clipPath id="clip9_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 294)"/>
</clipPath>
<clipPath id="clip10_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 294)"/>
</clipPath>
<clipPath id="clip11_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 294)"/>
</clipPath>
<clipPath id="clip12_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 431)"/>
</clipPath>
<clipPath id="clip13_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 431)"/>
</clipPath>
<clipPath id="clip14_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 431)"/>
</clipPath>
<clipPath id="clip15_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 431)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,100 @@
<svg width="570" height="527" viewBox="0 0 570 527" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0027 21.0027L30.7733 24.7733L44.2293 38.2293L48 42L51.7707 38.2293L65.2267 24.7733L68.9973 21.0027L65.2267 17.2267L51.7707 3.77067L48 0L44.2293 3.77067L30.7733 17.2267L27.0027 21.0027ZM61.456 21.0027L48 34.4533L34.544 20.9973L48 7.54133L61.456 21.0027ZM27.0027 74.9973L30.7733 78.7733L44.2293 92.2293L48 96L51.7707 92.2293L65.2267 78.7733L68.9973 75.0027L65.2267 71.2267L51.7707 57.7707L48 54L44.2293 57.7707L30.7733 71.2267L27.0027 74.9973ZM61.456 74.9973L48 88.464L34.544 75.008L48 61.536L61.456 74.9973ZM3.77067 51.7707L0 48L3.77067 44.2293L17.2267 30.7733L21.0027 27.0027L24.7733 30.7733L38.2293 44.2293L42 48L38.2293 51.7707L24.7733 65.2267L21.0027 68.9973L17.2267 65.2267L3.77067 51.7707ZM20.9973 61.456L34.464 48L20.9973 34.544L7.54133 48L20.9973 61.456ZM54 48L57.7707 51.7707L71.2267 65.2267L74.9973 68.9973L78.7733 65.2267L92.2293 51.7707L96 48L92.2293 44.2293L78.7733 30.7733L75.0027 27.0027L71.2267 30.7733L57.7707 44.2293L54 48ZM88.4533 48L74.9973 61.456L61.5467 48L75.008 34.544L88.4533 48Z" fill="#525252"/>
</g>
<g clip-path="url(#clip1_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 48L206 0L158 48L206 96L254 48Z" fill="#525252"/>
</g>
<g clip-path="url(#clip2_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 48L364 0L316 48L364 96L412 48ZM323.541 48L364 7.54133L404.459 48L364 88.4587L323.541 48Z" fill="#525252"/>
</g>
<g clip-path="url(#clip3_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 21.0027L504.773 24.7733L518.229 38.2293L522 42L525.771 38.2293L539.227 24.7733L542.997 21.0027L539.227 17.2267L525.771 3.77067L522 0L518.229 3.77067L504.773 17.2267L501.003 21.0027ZM501.003 74.9973L504.773 78.7733L518.229 92.2293L522 96L525.771 92.2293L539.227 78.7733L542.997 75.0027L539.227 71.2267L525.771 57.7707L522 54L518.229 57.7707L504.773 71.2267L501.003 74.9973ZM477.771 51.7707L474 48L477.771 44.2293L491.227 30.7733L495.003 27.0027L498.773 30.7733L512.229 44.2293L516 48L512.229 51.7707L498.773 65.2267L495.003 68.9973L491.227 65.2267L477.771 51.7707ZM528 48L531.771 51.7707L545.227 65.2267L548.997 68.9973L552.773 65.2267L566.229 51.7707L570 48L566.229 44.2293L552.773 30.7733L549.003 27.0027L545.227 30.7733L531.771 44.2293L528 48Z" fill="#525252"/>
</g>
<g clip-path="url(#clip4_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 195L48 147L0 195L48 243L96 195Z" fill="#525252"/>
</g>
<g clip-path="url(#clip5_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 195L206 147L158 195L206 243L254 195ZM165.541 195L206 154.541L246.459 195L206 235.459L165.541 195Z" fill="#525252"/>
</g>
<g clip-path="url(#clip6_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M343.003 168.003L346.773 171.773L360.229 185.229L364 189L367.771 185.229L381.227 171.773L384.997 168.003L381.227 164.227L367.771 150.771L364 147L360.229 150.771L346.773 164.227L343.003 168.003ZM377.456 168.003L364 181.453L350.544 167.997L364 154.541L377.456 168.003ZM343.003 221.997L346.773 225.773L360.229 239.229L364 243L367.771 239.229L381.227 225.773L384.997 222.003L381.227 218.227L367.771 204.771L364 201L360.229 204.771L346.773 218.227L343.003 221.997ZM377.456 221.997L364 235.464L350.544 222.008L364 208.536L377.456 221.997ZM319.771 198.771L316 195L319.771 191.229L333.227 177.773L337.003 174.003L340.773 177.773L354.229 191.229L358 195L354.229 198.771L340.773 212.227L337.003 215.997L333.227 212.227L319.771 198.771ZM336.997 208.456L350.464 195L336.997 181.544L323.541 195L336.997 208.456ZM370 195L373.771 198.771L387.227 212.227L390.997 215.997L394.773 212.227L408.229 198.771L412 195L408.229 191.229L394.773 177.773L391.003 174.003L387.227 177.773L373.771 191.229L370 195ZM404.453 195L390.997 208.456L377.547 195L391.008 181.544L404.453 195Z" fill="#525252"/>
</g>
<g clip-path="url(#clip7_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 168.003L504.773 171.773L518.229 185.229L522 189L525.771 185.229L539.227 171.773L542.997 168.003L539.227 164.227L525.771 150.771L522 147L518.229 150.771L504.773 164.227L501.003 168.003ZM501.003 221.997L504.773 225.773L518.229 239.229L522 243L525.771 239.229L539.227 225.773L542.997 222.003L539.227 218.227L525.771 204.771L522 201L518.229 204.771L504.773 218.227L501.003 221.997ZM477.771 198.771L474 195L477.771 191.229L491.227 177.773L495.003 174.003L498.773 177.773L512.229 191.229L516 195L512.229 198.771L498.773 212.227L495.003 215.997L491.227 212.227L477.771 198.771ZM528 195L531.771 198.771L545.227 212.227L548.997 215.997L552.773 212.227L566.229 198.771L570 195L566.229 191.229L552.773 177.773L549.003 174.003L545.227 177.773L531.771 191.229L528 195Z" fill="#525252"/>
</g>
<g clip-path="url(#clip8_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 342L48 294L0 342L48 390L96 342ZM7.54133 342L48 301.541L88.4587 342L48 382.459L7.54133 342Z" fill="#525252"/>
</g>
<g clip-path="url(#clip9_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.003 315.003L188.773 318.773L202.229 332.229L206 336L209.771 332.229L223.227 318.773L226.997 315.003L223.227 311.227L209.771 297.771L206 294L202.229 297.771L188.773 311.227L185.003 315.003ZM185.003 368.997L188.773 372.773L202.229 386.229L206 390L209.771 386.229L223.227 372.773L226.997 369.003L223.227 365.227L209.771 351.771L206 348L202.229 351.771L188.773 365.227L185.003 368.997ZM161.771 345.771L158 342L161.771 338.229L175.227 324.773L179.003 321.003L182.773 324.773L196.229 338.229L200 342L196.229 345.771L182.773 359.227L179.003 362.997L175.227 359.227L161.771 345.771ZM212 342L215.771 345.771L229.227 359.227L232.997 362.997L236.773 359.227L250.229 345.771L254 342L250.229 338.229L236.773 324.773L233.003 321.003L229.227 324.773L215.771 338.229L212 342Z" fill="#525252"/>
</g>
<g clip-path="url(#clip10_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 342L364 294L316 342L364 390L412 342Z" fill="#525252"/>
</g>
<g clip-path="url(#clip11_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 315.003L504.773 318.773L518.229 332.229L522 336L525.771 332.229L539.227 318.773L542.997 315.003L539.227 311.227L525.771 297.771L522 294L518.229 297.771L504.773 311.227L501.003 315.003ZM535.456 315.003L522 328.453L508.544 314.997L522 301.541L535.456 315.003ZM501.003 368.997L504.773 372.773L518.229 386.229L522 390L525.771 386.229L539.227 372.773L542.997 369.003L539.227 365.227L525.771 351.771L522 348L518.229 351.771L504.773 365.227L501.003 368.997ZM535.456 368.997L522 382.464L508.544 369.008L522 355.536L535.456 368.997ZM477.771 345.771L474 342L477.771 338.229L491.227 324.773L495.003 321.003L498.773 324.773L512.229 338.229L516 342L512.229 345.771L498.773 359.227L495.003 362.997L491.227 359.227L477.771 345.771ZM494.997 355.456L508.464 342L494.997 328.544L481.541 342L494.997 355.456ZM528 342L531.771 345.771L545.227 359.227L548.997 362.997L552.773 359.227L566.229 345.771L570 342L566.229 338.229L552.773 324.773L549.003 321.003L545.227 324.773L531.771 338.229L528 342ZM562.453 342L548.997 355.456L535.547 342L549.008 328.544L562.453 342Z" fill="#525252"/>
</g>
<g clip-path="url(#clip12_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M96 479L48 431L0 479L48 527L96 479ZM7.54133 479L48 438.541L88.4587 479L48 519.459L7.54133 479Z" fill="#525252"/>
</g>
<g clip-path="url(#clip13_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M185.003 452.003L188.773 455.773L202.229 469.229L206 473L209.771 469.229L223.227 455.773L226.997 452.003L223.227 448.227L209.771 434.771L206 431L202.229 434.771L188.773 448.227L185.003 452.003ZM219.456 452.003L206 465.453L192.544 451.997L206 438.541L219.456 452.003ZM185.003 505.997L188.773 509.773L202.229 523.229L206 527L209.771 523.229L223.227 509.773L226.997 506.003L223.227 502.227L209.771 488.771L206 485L202.229 488.771L188.773 502.227L185.003 505.997ZM219.456 505.997L206 519.464L192.544 506.008L206 492.536L219.456 505.997ZM161.771 482.771L158 479L161.771 475.229L175.227 461.773L179.003 458.003L182.773 461.773L196.229 475.229L200 479L196.229 482.771L182.773 496.227L179.003 499.997L175.227 496.227L161.771 482.771ZM178.997 492.456L192.464 479L178.997 465.544L165.541 479L178.997 492.456ZM212 479L215.771 482.771L229.227 496.227L232.997 499.997L236.773 496.227L250.229 482.771L254 479L250.229 475.229L236.773 461.773L233.003 458.003L229.227 461.773L215.771 475.229L212 479ZM246.453 479L232.997 492.456L219.547 479L233.008 465.544L246.453 479Z" fill="#525252"/>
</g>
<g clip-path="url(#clip14_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 479L364 431L316 479L364 527L412 479Z" fill="#525252"/>
</g>
<g clip-path="url(#clip15_1709_1536)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 452.003L504.773 455.773L518.229 469.229L522 473L525.771 469.229L539.227 455.773L542.997 452.003L539.227 448.227L525.771 434.771L522 431L518.229 434.771L504.773 448.227L501.003 452.003ZM501.003 505.997L504.773 509.773L518.229 523.229L522 527L525.771 523.229L539.227 509.773L542.997 506.003L539.227 502.227L525.771 488.771L522 485L518.229 488.771L504.773 502.227L501.003 505.997ZM477.771 482.771L474 479L477.771 475.229L491.227 461.773L495.003 458.003L498.773 461.773L512.229 475.229L516 479L512.229 482.771L498.773 496.227L495.003 499.997L491.227 496.227L477.771 482.771ZM528 479L531.771 482.771L545.227 496.227L548.997 499.997L552.773 496.227L566.229 482.771L570 479L566.229 475.229L552.773 461.773L549.003 458.003L545.227 461.773L531.771 475.229L528 479Z" fill="#525252"/>
</g>
<defs>
<clipPath id="clip0_1709_1536">
<rect width="96" height="96" fill="white"/>
</clipPath>
<clipPath id="clip1_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158)"/>
</clipPath>
<clipPath id="clip2_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316)"/>
</clipPath>
<clipPath id="clip3_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474)"/>
</clipPath>
<clipPath id="clip4_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 147)"/>
</clipPath>
<clipPath id="clip5_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 147)"/>
</clipPath>
<clipPath id="clip6_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 147)"/>
</clipPath>
<clipPath id="clip7_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 147)"/>
</clipPath>
<clipPath id="clip8_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 294)"/>
</clipPath>
<clipPath id="clip9_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 294)"/>
</clipPath>
<clipPath id="clip10_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 294)"/>
</clipPath>
<clipPath id="clip11_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 294)"/>
</clipPath>
<clipPath id="clip12_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(0 431)"/>
</clipPath>
<clipPath id="clip13_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(158 431)"/>
</clipPath>
<clipPath id="clip14_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(316 431)"/>
</clipPath>
<clipPath id="clip15_1709_1536">
<rect width="96" height="96" fill="white" transform="translate(474 431)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,61 @@
<svg width="1360" height="96" viewBox="0 0 1360 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0027 21.0027L30.7733 24.7733L44.2293 38.2293L48 42L51.7707 38.2293L65.2267 24.7733L68.9973 21.0027L65.2267 17.2267L51.7707 3.77067L48 0L44.2293 3.77067L30.7733 17.2267L27.0027 21.0027ZM61.456 21.0027L48 34.4533L34.544 20.9973L48 7.54133L61.456 21.0027ZM27.0027 74.9973L30.7733 78.7733L44.2293 92.2293L48 96L51.7707 92.2293L65.2267 78.7733L68.9973 75.0027L65.2267 71.2267L51.7707 57.7707L48 54L44.2293 57.7707L30.7733 71.2267L27.0027 74.9973ZM61.456 74.9973L48 88.464L34.544 75.008L48 61.536L61.456 74.9973ZM3.77067 51.7707L0 48L3.77067 44.2293L17.2267 30.7733L21.0027 27.0027L24.7733 30.7733L38.2293 44.2293L42 48L38.2293 51.7707L24.7733 65.2267L21.0027 68.9973L17.2267 65.2267L3.77067 51.7707ZM20.9973 61.456L34.464 48L20.9973 34.544L7.54133 48L20.9973 61.456ZM54 48L57.7707 51.7707L71.2267 65.2267L74.9973 68.9973L78.7733 65.2267L92.2293 51.7707L96 48L92.2293 44.2293L78.7733 30.7733L75.0027 27.0027L71.2267 30.7733L57.7707 44.2293L54 48ZM88.4533 48L74.9973 61.456L61.5467 48L75.008 34.544L88.4533 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip1_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 48L206 0L158 48L206 96L254 48ZM165.541 48L206 7.54133L246.459 48L206 88.4587L165.541 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip2_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 48L364 0L316 48L364 96L412 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip3_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 21.0027L504.773 24.7733L518.229 38.2293L522 42L525.771 38.2293L539.227 24.7733L542.997 21.0027L539.227 17.2267L525.771 3.77067L522 0L518.229 3.77067L504.773 17.2267L501.003 21.0027ZM501.003 74.9973L504.773 78.7733L518.229 92.2293L522 96L525.771 92.2293L539.227 78.7733L542.997 75.0027L539.227 71.2267L525.771 57.7707L522 54L518.229 57.7707L504.773 71.2267L501.003 74.9973ZM477.771 51.7707L474 48L477.771 44.2293L491.227 30.7733L495.003 27.0027L498.773 30.7733L512.229 44.2293L516 48L512.229 51.7707L498.773 65.2267L495.003 68.9973L491.227 65.2267L477.771 51.7707ZM528 48L531.771 51.7707L545.227 65.2267L548.997 68.9973L552.773 65.2267L566.229 51.7707L570 48L566.229 44.2293L552.773 30.7733L549.003 27.0027L545.227 30.7733L531.771 44.2293L528 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip4_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M728 48L680 0L632 48L680 96L728 48ZM639.541 48L680 7.54133L720.459 48L680 88.4587L639.541 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip5_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M886 48L838 0L790 48L838 96L886 48Z" fill="#15593B"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1017 21L996 0L975 21L996 42L1017 21ZM978.299 21L996 3.29933L1013.7 21L996 38.7007L978.299 21Z" fill="white"/>
<path d="M978.299 21L996 3.29933L1013.7 21L996 38.7007L978.299 21Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1017 75L996 54L975 75L996 96L1017 75ZM978.299 75L996 57.2993L1013.7 75L996 92.7007L978.299 75Z" fill="white"/>
<path d="M978.299 75L996 57.2993L1013.7 75L996 92.7007L978.299 75Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M990 48L969 27L948 48L969 69L990 48ZM951.299 48L969 30.2993L986.701 48L969 65.7007L951.299 48Z" fill="white"/>
<path d="M951.299 48L969 30.2993L986.701 48L969 65.7007L951.299 48Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1044 48L1023 27L1002 48L1023 69L1044 48ZM1005.3 48L1023 30.2993L1040.7 48L1023 65.7007L1005.3 48Z" fill="white"/>
<path d="M1005.3 48L1023 30.2993L1040.7 48L1023 65.7007L1005.3 48Z" fill="#3ECF8E"/>
<g clip-path="url(#clip6_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1202 48L1154 0L1106 48L1154 96L1202 48ZM1113.54 48L1154 7.54133L1194.46 48L1154 88.4587L1113.54 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip7_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1360 48L1312 0L1264 48L1312 96L1360 48ZM1271.54 48L1312 7.54133L1352.46 48L1312 88.4587L1271.54 48Z" fill="#15593B"/>
<path d="M1271.54 48L1312 7.54133L1352.46 48L1312 88.4587L1271.54 48Z" fill="#3ECF8E"/>
</g>
<defs>
<clipPath id="clip0_1709_1278">
<rect width="96" height="96" fill="white"/>
</clipPath>
<clipPath id="clip1_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(158)"/>
</clipPath>
<clipPath id="clip2_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(316)"/>
</clipPath>
<clipPath id="clip3_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(474)"/>
</clipPath>
<clipPath id="clip4_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(632)"/>
</clipPath>
<clipPath id="clip5_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(790)"/>
</clipPath>
<clipPath id="clip6_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(1106)"/>
</clipPath>
<clipPath id="clip7_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(1264)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

@@ -0,0 +1,61 @@
<svg width="1360" height="96" viewBox="0 0 1360 96" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M27.0027 21.0027L30.7733 24.7733L44.2293 38.2293L48 42L51.7707 38.2293L65.2267 24.7733L68.9973 21.0027L65.2267 17.2267L51.7707 3.77067L48 0L44.2293 3.77067L30.7733 17.2267L27.0027 21.0027ZM61.456 21.0027L48 34.4533L34.544 20.9973L48 7.54133L61.456 21.0027ZM27.0027 74.9973L30.7733 78.7733L44.2293 92.2293L48 96L51.7707 92.2293L65.2267 78.7733L68.9973 75.0027L65.2267 71.2267L51.7707 57.7707L48 54L44.2293 57.7707L30.7733 71.2267L27.0027 74.9973ZM61.456 74.9973L48 88.464L34.544 75.008L48 61.536L61.456 74.9973ZM3.77067 51.7707L0 48L3.77067 44.2293L17.2267 30.7733L21.0027 27.0027L24.7733 30.7733L38.2293 44.2293L42 48L38.2293 51.7707L24.7733 65.2267L21.0027 68.9973L17.2267 65.2267L3.77067 51.7707ZM20.9973 61.456L34.464 48L20.9973 34.544L7.54133 48L20.9973 61.456ZM54 48L57.7707 51.7707L71.2267 65.2267L74.9973 68.9973L78.7733 65.2267L92.2293 51.7707L96 48L92.2293 44.2293L78.7733 30.7733L75.0027 27.0027L71.2267 30.7733L57.7707 44.2293L54 48ZM88.4533 48L74.9973 61.456L61.5467 48L75.008 34.544L88.4533 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip1_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M254 48L206 0L158 48L206 96L254 48ZM165.541 48L206 7.54133L246.459 48L206 88.4587L165.541 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip2_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M412 48L364 0L316 48L364 96L412 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip3_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M501.003 21.0027L504.773 24.7733L518.229 38.2293L522 42L525.771 38.2293L539.227 24.7733L542.997 21.0027L539.227 17.2267L525.771 3.77067L522 0L518.229 3.77067L504.773 17.2267L501.003 21.0027ZM501.003 74.9973L504.773 78.7733L518.229 92.2293L522 96L525.771 92.2293L539.227 78.7733L542.997 75.0027L539.227 71.2267L525.771 57.7707L522 54L518.229 57.7707L504.773 71.2267L501.003 74.9973ZM477.771 51.7707L474 48L477.771 44.2293L491.227 30.7733L495.003 27.0027L498.773 30.7733L512.229 44.2293L516 48L512.229 51.7707L498.773 65.2267L495.003 68.9973L491.227 65.2267L477.771 51.7707ZM528 48L531.771 51.7707L545.227 65.2267L548.997 68.9973L552.773 65.2267L566.229 51.7707L570 48L566.229 44.2293L552.773 30.7733L549.003 27.0027L545.227 30.7733L531.771 44.2293L528 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip4_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M728 48L680 0L632 48L680 96L728 48ZM639.541 48L680 7.54133L720.459 48L680 88.4587L639.541 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip5_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M886 48L838 0L790 48L838 96L886 48Z" fill="#15593B"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1017 21L996 0L975 21L996 42L1017 21ZM978.299 21L996 3.29933L1013.7 21L996 38.7007L978.299 21Z" fill="white"/>
<path d="M978.299 21L996 3.29933L1013.7 21L996 38.7007L978.299 21Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1017 75L996 54L975 75L996 96L1017 75ZM978.299 75L996 57.2993L1013.7 75L996 92.7007L978.299 75Z" fill="white"/>
<path d="M978.299 75L996 57.2993L1013.7 75L996 92.7007L978.299 75Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M990 48L969 27L948 48L969 69L990 48ZM951.299 48L969 30.2993L986.701 48L969 65.7007L951.299 48Z" fill="white"/>
<path d="M951.299 48L969 30.2993L986.701 48L969 65.7007L951.299 48Z" fill="#3ECF8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1044 48L1023 27L1002 48L1023 69L1044 48ZM1005.3 48L1023 30.2993L1040.7 48L1023 65.7007L1005.3 48Z" fill="white"/>
<path d="M1005.3 48L1023 30.2993L1040.7 48L1023 65.7007L1005.3 48Z" fill="#3ECF8E"/>
<g clip-path="url(#clip6_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1202 48L1154 0L1106 48L1154 96L1202 48ZM1113.54 48L1154 7.54133L1194.46 48L1154 88.4587L1113.54 48Z" fill="#15593B"/>
</g>
<g clip-path="url(#clip7_1709_1278)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M1360 48L1312 0L1264 48L1312 96L1360 48ZM1271.54 48L1312 7.54133L1352.46 48L1312 88.4587L1271.54 48Z" fill="#15593B"/>
<path d="M1271.54 48L1312 7.54133L1352.46 48L1312 88.4587L1271.54 48Z" fill="#3ECF8E"/>
</g>
<defs>
<clipPath id="clip0_1709_1278">
<rect width="96" height="96" fill="white"/>
</clipPath>
<clipPath id="clip1_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(158)"/>
</clipPath>
<clipPath id="clip2_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(316)"/>
</clipPath>
<clipPath id="clip3_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(474)"/>
</clipPath>
<clipPath id="clip4_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(632)"/>
</clipPath>
<clipPath id="clip5_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(790)"/>
</clipPath>
<clipPath id="clip6_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(1106)"/>
</clipPath>
<clipPath id="clip7_1709_1278">
<rect width="96" height="96" fill="white" transform="translate(1264)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

Loaded 100 of 124 files, more files were not shown because too many files have changed in this diff. Show more