Merge branch 'master' into pr/13270

This commit is contained in:
Jonathan Summers-Muir committed 2023-03-31 17:36:25 +08:00
commit ef134fca0b
451 files changed
+26075 -34877

No files matched your search

+1 -2
View File
@@ -1,7 +1,6 @@
import React, { useState } from 'react'
import KJUR from 'jsrsasign'
import CodeBlock from './CodeBlock/CodeBlock'
import { Button, Select, Input } from 'ui'
import { Button, Select, Input, CodeBlock } from 'ui'
const JWT_HEADER = { alg: 'HS256', typ: 'JWT' }
const now = new Date()
@@ -1,169 +0,0 @@
import { render } from 'react-dom'
import { IconCommand } from 'ui'
import { createElement, FC, useEffect, useRef, Fragment } from 'react'
import algoliasearch from 'algoliasearch/lite'
import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js'
import { createLocalStorageRecentSearchesPlugin } from '@algolia/autocomplete-plugin-recent-searches'
import { useRouter } from 'next/router'
// [Joshen] We're currently using DocSearch from Algolia as it provides a nice
// UI out of the box + some good preconfigured search settings (e.g hierarchy).
// However, we're using our own Algolia account to store the records in the indexes
// (rather than going through the DocSearch program from Algolia where they'll crawl
// our site for us). Refer to scripts/build-search on how we're saving the records.
// Using Algolia's autocomplete library gives us full flexbility in terms of customizing
// our search experience, but that will take time to figure out. Hence why for now we're just
// using DocSearch with our own records.
// Potentially for search, we could
// - Go ahead with the DocSearch program and let them crawl our site to generate the records
// - But we need to ensure that our site is semantically correct first
// - Use Algolia itself to flesh out our own search logic
// - The basics are already set up to be honest, but will take time to make it great
// - Go back to Typesense if we deem that Algolia is not helpful in the long run
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY
)
// [Joshen] Not working properly, but lets not get stuck on this
// Priority just to get a search working
const recentSearchesPlugin = createLocalStorageRecentSearchesPlugin({
key: 'docs-search',
limit: 3,
//@ts-ignore
transformSource({ source }) {
return {
...source,
templates: {
...source.templates,
header({ state }) {
if (state.query) return null
return (
<Fragment>
<span className="aa-SourceHeaderTitle">Your searches</span>
<div className="aa-SourceHeaderLine" />
</Fragment>
)
},
},
}
},
})
interface Props {}
const AlgoliaSearch: FC<Props> = ({}) => {
const searchRef = useRef(null)
const router = useRouter()
useEffect(() => {
if (!searchRef.current) {
return undefined
}
const search = autocomplete({
openOnFocus: true,
container: searchRef.current,
defaultActiveItemId: 0,
detachedMediaQuery: '',
// @ts-ignore
renderer: { createElement, Fragment, render },
placeholder: 'Search docs',
plugins: [recentSearchesPlugin],
renderNoResults({ state, render }, root) {
render(
<div className="text-scale-1100 py-2 text-sm px-4">
No results found for {state.query}.
</div>,
root
)
},
navigator: {
navigate({ itemUrl }) {
router.push(itemUrl)
},
},
onSelect({ item, setQuery, setIsOpen, refresh }) {
//console.log('onSelect', item.url)
},
// @ts-ignore
getSources({ query }) {
return [
{
sourceId: 'pages',
templates: {
item({ item, components }) {
return (
<a
href={item.url as string}
className="aa-ItemLink truncate flex justify-between space-x-4"
>
<div className="aa-ItemContent w-full">
<div className="aa-ItemTitle flex items-center space-x-1">
{item.category && (
<p
className={`${
['cli', 'api'].includes(item.category as string)
? 'uppercase'
: 'capitalize'
}`}
>
<>
{item.category}
{item.version ? ` (${item.version})` : ''}:
</>
</p>
)}
<p>
<components.Highlight hit={item} attribute="title" />
</p>
</div>
<p className="aa-ItemContentSubtitle">{item.description as string}</p>
</div>
</a>
)
},
},
getItemUrl({ item }) {
return item.url
},
getItems() {
// if (!query) return []
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'dev_docs',
query,
},
],
})
},
},
]
},
})
return () => {
search.destroy()
}
}, [])
return (
<div className="w-[200px] relative">
<div ref={searchRef} />
<div className="flex items-center space-x-1 absolute top-[7px] right-2">
<div className="text-scale-1200 flex items-center justify-center h-6 w-6 rounded bg-scale-500">
<IconCommand size={12} strokeWidth={1.5} />
</div>
<div className="text-xs text-scale-1200 flex items-center justify-center h-6 w-6 rounded bg-scale-500">
K
</div>
</div>
</div>
)
}
export default AlgoliaSearch
+4 -13
View File
@@ -1,13 +1,12 @@
import { useTheme } from 'common/Providers'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState, useEffect, FC } from 'react'
import { IconMenu, IconMoon, IconSearch, IconSun, IconCommand, Listbox } from 'ui'
import { useTheme } from 'common/Providers'
import { FC, useEffect, useState } from 'react'
import { IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Listbox, SearchButton } from 'ui'
import { REFERENCES } from './Navigation.constants'
import { getPageType } from '~/lib/helpers'
import SearchButton from '../Search/SearchButton'
const NavBar: FC = () => {
const { isDarkMode, toggleTheme } = useTheme()
@@ -36,14 +35,6 @@ const NavBar: FC = () => {
{ text: 'Reference', key: 'reference', link: '/reference' },
]
const toggleDarkMode = () => {
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
toggleTheme()
const key = localStorage.getItem('supabaseDarkMode')
document.documentElement.className = key === 'true' ? 'dark' : ''
}
const onSelectVersion = (version: string) => {
// [Joshen] Ideally we use <Link> but this works for now
if (!library) return
@@ -183,7 +174,7 @@ const NavBar: FC = () => {
</a>
</li>
<li className="px-4">
<div className="cursor-pointer" onClick={toggleDarkMode}>
<div className="cursor-pointer" onClick={() => toggleTheme()}>
{isDarkMode ? (
<IconMoon size={18} strokeWidth={2} className="text-scale-1200" />
) : (
@@ -276,6 +276,11 @@ export const menuItems: NavMenu = {
url: '/guides/realtime/subscribing-to-database-changes',
items: [],
},
{
name: 'Bring Your Own Database',
url: '/guides/realtime/bring-your-own-database',
items: [],
},
{
name: 'Using Realtime with Next.js',
url: '/guides/realtime/realtime-with-nextjs',
@@ -48,6 +48,7 @@ export const gettingstarted = {
items: [
{ name: 'Features', url: '/guides/getting-started/features' },
{ name: 'Architecture', url: '/guides/getting-started/architecture' },
{ name: 'Local Development', url: '/guides/cli/local-development' },
{
name: 'Framework Quickstarts',
items: [
@@ -602,6 +603,11 @@ export const realtime = {
name: 'Subscribing to Database Changes',
url: '/guides/realtime/subscribing-to-database-changes',
},
{
name: 'Bring Your Own Database',
url: '/guides/realtime/bring-your-own-database',
items: [],
},
{
name: 'Using Realtime with Next.js',
url: '/guides/realtime/realtime-with-nextjs',
@@ -1,13 +1,21 @@
import { useTheme } from 'common/Providers'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC, useEffect, useState } from 'react'
import { Button, IconCommand, IconMenu, IconMoon, IconSearch, IconSun, Input, Listbox } from 'ui'
import {
Button,
IconCommand,
IconMenu,
IconMoon,
IconSearch,
IconSun,
Listbox,
SearchButton,
} from 'ui'
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
import { useTheme } from 'common/Providers'
import { getPageType } from '~/lib/helpers'
import SearchButton from '~/components/Search/SearchButton'
const TopNavBar: FC = () => {
const { isDarkMode, toggleTheme } = useTheme()
@@ -36,14 +44,6 @@ const TopNavBar: FC = () => {
{ text: 'Reference', key: 'reference', link: '/reference' },
]
const toggleDarkMode = () => {
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
toggleTheme()
const key = localStorage.getItem('supabaseDarkMode')
document.documentElement.className = key === 'true' ? 'dark' : ''
}
const onSelectVersion = (version: string) => {
// [Joshen] Ideally we use <Link> but this works for now
if (!library) return
@@ -147,7 +147,7 @@ const TopNavBar: FC = () => {
</Button>
<ul className="flex items-center">
<li className="px-4">
<div className="cursor-pointer" onClick={toggleDarkMode}>
<div className="cursor-pointer" onClick={() => toggleTheme()}>
{isDarkMode ? (
<IconMoon
size={16}
@@ -3,10 +3,9 @@ import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { FC, useEffect, useState } from 'react'
import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun } from 'ui'
import { Button, IconCommand, IconGitHub, IconMoon, IconSearch, IconSun, SearchButton } from 'ui'
import { REFERENCES } from '~/components/Navigation/Navigation.constants'
import SearchButton from '~/components/Search/SearchButton'
import { getPageType } from '~/lib/helpers'
const TopNavBarRef: FC = () => {
@@ -36,15 +35,6 @@ const TopNavBarRef: FC = () => {
{ text: 'Reference', key: 'reference', link: '/reference' },
]
const toggleDarkMode = () => {
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
toggleTheme()
const key = localStorage.getItem('supabaseDarkMode')
document.documentElement.className = key === 'true' ? 'dark' : ''
document.documentElement.style.colorScheme = key === 'true' ? 'dark' : ''
}
const onSelectVersion = (version: string) => {
// [Joshen] Ideally we use <Link> but this works for now
if (!library) return
@@ -149,7 +139,7 @@ const TopNavBarRef: FC = () => {
</ul>
<ul className="flex items-center">
<li className="px-4">
<div className="cursor-pointer" onClick={toggleDarkMode}>
<div className="cursor-pointer" onClick={() => toggleTheme()}>
{isDarkMode ? (
<IconMoon
size={16}
@@ -1,40 +0,0 @@
import {
ButtonHTMLAttributes,
DetailedHTMLProps,
FC,
PropsWithChildren,
useEffect,
useRef,
} from 'react'
import { useActionKey } from '~/hooks/useActionKey'
import { useSearch } from './SearchProvider'
const SearchButton: FC<
PropsWithChildren<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>>
> = ({ children, ...props }) => {
const searchButtonRef = useRef<HTMLButtonElement>()
const { open, setQuery } = useSearch()
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (searchButtonRef.current === document.activeElement) {
if (/[a-zA-Z0-9]/.test(String.fromCharCode(event.keyCode))) {
open()
setQuery?.(event.key)
}
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [open, setQuery])
return (
<button type="button" ref={searchButtonRef} onClick={open} {...props}>
{children}
</button>
)
}
export default SearchButton
-423
View File
@@ -1,423 +0,0 @@
import type { CreateCompletionResponse } from 'openai'
import { FC, useCallback, useRef, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { SSE } from 'sse.js'
import clippyImageDark from '../../public/img/clippy-dark.png'
import clippyImage from '../../public/img/clippy.png'
import { useSupabaseClient } from '@supabase/auth-helpers-react'
import { useTheme } from 'common/Providers'
import Image from 'next/image'
import {
Button,
IconAlertCircle,
IconAlertTriangle,
IconLoader,
IconSearch,
Input,
Loading,
Modal,
Tabs,
} from 'ui'
import components from '~/components'
import { IS_PLATFORM } from '~/lib/constants'
import { useSearch } from './SearchProvider'
import SearchResult, { SearchResultType } from './SearchResult'
const questions = [
'How do I get started with Supabase?',
'How do I run Supabase locally?',
'How do I connect to my database?',
'How do I run migrations? ',
'How do I listen to changes in a table?',
'How do I set up authentication?',
]
function getEdgeFunctionUrl() {
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL?.replace(/\/$/, '')
if (IS_PLATFORM) {
const [schemeAndProjectId, domain, tld] = supabaseUrl.split('.')
return `${schemeAndProjectId}.functions.${domain}.${tld}`
} else {
return `${supabaseUrl}/functions/v1`
}
}
const edgeFunctionUrl = getEdgeFunctionUrl()
const SearchModal: FC = () => {
const { isDarkMode } = useTheme()
const { close, query, setQuery } = useSearch()
const [answer, setAnswer] = useState('')
const [results, setResults] = useState<any[]>()
const [isLoading, setIsLoading] = useState(false)
const [isResponding, setIsResponding] = useState(false)
const [hasClippyError, setHasClippyError] = useState(false)
const [hasSearchError, setHasSearchError] = useState(false)
const [selectedTab, setSelectedTab] = useState('search-panel')
const eventSourceRef = useRef<SSE>()
const supabaseClient = useSupabaseClient()
const cantHelp = answer?.trim() === "Sorry, I don't know how to help with that."
const status = isLoading
? 'Clippy is searching...'
: isResponding
? 'Clippy is responding...'
: cantHelp || hasClippyError
? 'Clippy has failed you'
: undefined
const handleSearchConfirm = useCallback(
async (query: string) => {
setResults(undefined)
setAnswer(undefined)
setIsResponding(false)
setHasClippyError(false)
setHasSearchError(false)
setIsLoading(true)
const { error, data: pageSections } = await supabaseClient.functions.invoke('search', {
body: { query },
})
setIsLoading(false)
if (error) {
setIsLoading(false)
setIsResponding(false)
setHasSearchError(true)
console.error(error)
return
}
if (!Array.isArray(pageSections)) {
setIsLoading(false)
setIsResponding(false)
setHasSearchError(true)
console.error('Malformed response')
return
}
setResults(pageSections)
},
[supabaseClient]
)
const handleClippyConfirm = useCallback(async (query: string) => {
setResults(undefined)
setAnswer(undefined)
setIsResponding(false)
setHasClippyError(false)
setHasSearchError(false)
setIsLoading(true)
const eventSource = new SSE(`${edgeFunctionUrl}/clippy-search`, {
headers: {
apikey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,
'Content-Type': 'application/json',
},
payload: JSON.stringify({ query }),
})
function handleError<T>(err: T) {
setIsLoading(false)
setIsResponding(false)
setHasClippyError(true)
console.error(err)
}
eventSource.addEventListener('error', handleError)
eventSource.addEventListener('message', (e) => {
try {
setIsLoading(false)
if (e.data === '[DONE]') {
setIsResponding(false)
return
}
setIsResponding(true)
const completionResponse: CreateCompletionResponse = JSON.parse(e.data)
const [{ text }] = completionResponse.choices
setAnswer((answer) => {
return (answer ?? '') + text
})
} catch (err) {
handleError(err)
}
})
eventSource.stream()
eventSourceRef.current = eventSource
setIsLoading(true)
}, [])
const handleConfirm = useCallback(
(selectedTab: string, query: string) => {
switch (selectedTab) {
case 'search-panel':
return handleSearchConfirm(query)
case 'clippy-panel':
return handleClippyConfirm(query)
}
},
[handleSearchConfirm, handleClippyConfirm]
)
function handleResetPrompt() {
eventSourceRef.current?.close()
eventSourceRef.current = undefined
setQuery('')
setResults(undefined)
setAnswer(undefined)
setIsResponding(false)
setHasClippyError(false)
setHasSearchError(false)
}
return (
<Modal size="xlarge" visible={true} onCancel={close} closable={false} hideFooter>
<div
className={`mx-auto max-h-[90vh] lg:max-h-[75vh] flex flex-col gap-4 rounded-lg p-4 md:pt-6 md:px-6 pb-2 w-full shadow-2xl overflow-hidden border text-left border-scale-500 bg-scale-100 dark:bg-scale-300 cursor-auto relative min-w-[340px]`}
onClick={(e) => e.stopPropagation()}
>
<div className="relative">
<Input
className="w-full"
size="xlarge"
autoFocus
placeholder={selectedTab === 'search-panel' ? 'Search documentation' : 'Ask a question'}
value={query}
onChange={(e) => setQuery(e.target.value)}
icon={<IconSearch size="small" />}
onKeyDown={(e) => {
switch (e.key) {
case 'Enter':
if (!query) {
return
}
handleConfirm(selectedTab, query)
return
default:
return
}
}}
/>
<div className="absolute right-0 top-0 mt-3 mr-4 hidden md:block">
<Button type="default" size="tiny" onClick={close}>
esc
</Button>
</div>
{!isLoading && answer && (
<div className="absolute right-0 top-0 mt-3 mr-16 hidden md:block">
<Button type="text" size="tiny" onClick={handleResetPrompt}>
Try again
</Button>
</div>
)}
</div>
<Tabs
activeId={selectedTab}
onChange={(tabId) => {
setSelectedTab(tabId)
if (!query) {
handleResetPrompt()
return
}
handleConfirm(tabId, query)
}}
>
<Tabs.Panel id="search-panel" label="Guides & Reference">
<div className="mb-6">
{!isLoading && !hasSearchError && !results && (
<div className="p-10 grid">
<h2 className="text-lg text-center text-scale-1100">
Search Supabase guides & reference docs
</h2>
</div>
)}
{results && results.length > 0 && (
<div className="flex flex-col gap-3 max-h-[70vh] lg:max-h-[50vh] overflow-y-auto px-4 py-4 rounded-lg bg-scale-200">
{results.map((page) => {
const pageSections = page.sections.filter((section) => !!section.heading)
return (
<div key={page.id} className="flex flex-col gap-3">
<SearchResult
href={page.path}
type={SearchResultType.Document}
title={page.meta.title}
/>
{pageSections.length > 0 && (
<div className="flex flex-row">
<div className="border bg-scale-300 rounded-xl self-stretch p-[1px] ml-4 mr-4"></div>
<div className="flex flex-col gap-3 items-stretch grow">
{pageSections.map((section) => (
<SearchResult
key={section.id}
href={`${page.path}${page.type === 'reference' ? '/' : '#'}${
section.slug
}`}
type={SearchResultType.Section}
title={section.heading}
chip={page.meta.title}
/>
))}
</div>
</div>
)}
</div>
)
})}
</div>
)}
{isLoading && (
<div className="p-6 grid gap-6 mt-4">
<Loading active>{}</Loading>
<p className="text-lg text-center">Searching for results</p>
</div>
)}
{results && results.length === 0 && (
<div className="p-6 flex flex-col items-center gap-6 mt-4">
<IconAlertTriangle strokeWidth={1.5} size={40} />
<p className="text-lg text-center">No results found.</p>
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
Try again?
</Button>
</div>
)}
{hasSearchError && (
<div className="p-6 flex flex-col items-center gap-6 mt-4">
<IconAlertTriangle strokeWidth={1.5} size={40} />
<p className="text-lg text-center">
Sorry, looks like we&apos;re having some issues with search!
</p>
<p className="text-sm text-center">Please try again in a bit.</p>
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
Try again?
</Button>
</div>
)}
</div>
</Tabs.Panel>
<Tabs.Panel id="clippy-panel" label="Ask Clippy">
{!isLoading && !answer && !hasClippyError && (
<div className="">
<div className="mt-2">
<h2 className="text-sm text-scale-1100">Not sure where to start?</h2>
<ul className="text-sm mt-4 text-scale-1100 grid md:flex gap-4 flex-wrap max-w-3xl">
{questions.map((question) => {
const key = question.replace(/\s+/g, '_')
return (
<li key={key}>
<button
className="hover:bg-slate-400 hover:dark:bg-slate-400 px-4 py-2 bg-slate-300 dark:bg-slate-200 rounded-lg transition-colors"
onClick={() => {
setQuery(question)
handleClippyConfirm(question)
}}
>
{question}
</button>
</li>
)
})}
</ul>
</div>
</div>
)}
{answer && (
<div className="px-4 py-4 rounded-lg overflow-y-auto bg-scale-200 max-h-[70vh] lg:max-h-[50vh]">
{cantHelp ? (
<p className="flex flex-col gap-4 items-center p-4">
<div className="grid md:flex items-center gap-2 mt-4 text-center justify-items-center">
<IconAlertCircle />
<p>Sorry, I don&apos;t know how to help with that.</p>
</div>
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
Try again?
</Button>
</p>
) : (
<div className="prose dark:prose-dark">
<ReactMarkdown
linkTarget="_blank"
remarkPlugins={[remarkGfm]}
transformLinkUri={(href) => {
const supabaseUrl = new URL('https://supabase.com')
const linkUrl = new URL(href, 'https://supabase.com')
if (linkUrl.origin === supabaseUrl.origin) {
return linkUrl.toString()
}
return href
}}
components={components}
>
{answer}
</ReactMarkdown>
</div>
)}
</div>
)}
{isLoading && (
<div className="p-6 grid gap-6 mt-4">
<Loading active>{}</Loading>
<p className="text-lg text-center">Searching for results</p>
</div>
)}
{hasClippyError && (
<div className="p-6 flex flex-col items-center gap-6 mt-4">
<IconAlertTriangle strokeWidth={1.5} size={40} />
<p className="text-lg text-center">
Sorry, looks like Clippy is having a hard time!
</p>
<p className="text-sm text-center">Please try again in a bit.</p>
<Button size="tiny" type="secondary" onClick={handleResetPrompt}>
Try again?
</Button>
</div>
)}
<div className="border-t border-scale-600 mt-4 text-scale-1100">
<div className="flex justify-between items-center py-2 text-xs">
<div className="flex items-centerp gap-1 pt-3 pb-1">
<span>Powered by OpenAI.</span>
<a href="/blog/chatgpt-supabase-docs" className="underline">
Read the blog post
</a>
</div>
<div className="flex items-center gap-6 py-1">
{status ? (
<span className="bg-scale-400 rounded-lg py-1 px-2 items-center gap-2 hidden md:flex">
{(isLoading || isResponding) && (
<IconLoader size={14} className="animate-spin" />
)}
{status}
</span>
) : (
<></>
)}
<Image
width={30}
height={34}
src={isDarkMode ? clippyImageDark : clippyImage}
alt="Clippy"
/>
</div>
</div>
</div>
</Tabs.Panel>
</Tabs>
</div>
</Modal>
)
}
export default SearchModal
@@ -1,80 +0,0 @@
import {
createContext,
FC,
PropsWithChildren,
useCallback,
useContext,
useEffect,
useState,
} from 'react'
import { createPortal } from 'react-dom'
import SearchModal from './SearchModal'
export type SearchContextValue = {
isOpen: boolean
open: () => void
close: () => void
query: string
setQuery: (query: string) => void
}
export const SearchContext = createContext<SearchContextValue>(null)
export const useSearch = () => {
const { isOpen, open, close, query, setQuery } = useContext(SearchContext)
return { isOpen, open, close, query, setQuery }
}
const SearchProvider: FC<PropsWithChildren<{}>> = ({ children }) => {
const [isOpen, setIsOpen] = useState(false)
const [query, setQuery] = useState('')
const open = useCallback(() => {
setIsOpen(true)
document.body.classList.add('DocSearch--active')
}, [])
const close = useCallback(() => {
setIsOpen(false)
document.body.classList.remove('DocSearch--active')
}, [])
useSearchKeyboardEvents({
open,
close,
})
return (
<SearchContext.Provider value={{ isOpen, open, close, query, setQuery }}>
{children}
{isOpen && createPortal(<SearchModal />, document.body)}
</SearchContext.Provider>
)
}
function useSearchKeyboardEvents({ open, close }) {
useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
switch (event.key) {
case 'Escape':
close()
return
case 'k':
case '/':
if (event.metaKey || event.ctrlKey) {
open()
}
return
}
}
window.addEventListener('keydown', onKeyDown)
return () => {
window.removeEventListener('keydown', onKeyDown)
}
}, [open, close])
}
export default SearchProvider
@@ -1,54 +0,0 @@
import Link from 'next/link'
import { FC } from 'react'
import { IconBookOpen, IconHash } from '~/../../packages/ui'
import { useSearch } from './SearchProvider'
export enum SearchResultType {
Document = 'document',
Section = 'section',
}
type Props = {
href: string
type: SearchResultType
title: string
chip?: string
}
const SearchResult: FC<Props> = ({ href, type, title, chip }) => {
const { close } = useSearch()
return (
<Link href={href}>
<a
className="flex flex-row items-center bg-scale-400 hover:bg-scale-600 transition p-4 rounded-md border border-scale-600 text-sm cursor-pointer"
onClick={close}
>
<div className="w-6 h-6 p-1 flex items-center justify-center mr-4 text-brand-1100 rounded-md bg-scale-700">
{getIconByType(type)}
</div>
<div className="flex flex-col gap-2 items-start">
{chip && (
<div className="rounded-xl bg-scale-700 pl-3 pr-3 pt-0.5 pb-0.5 text-xs text-scale-1100">
{chip}
</div>
)}
<div>{title}</div>
</div>
</a>
</Link>
)
}
function getIconByType(type: SearchResultType) {
switch (type) {
case SearchResultType.Document:
return <IconBookOpen />
case SearchResultType.Section:
return <IconHash />
default:
throw new Error(`Unknown search result type '${type}'`)
}
}
export default SearchResult
+10 -29
View File
@@ -1,31 +1,27 @@
import { Button, Tabs, Alert, GlassPanel } from 'ui'
import Link from 'next/link'
import { Alert, Button, CodeBlock, GlassPanel, markdownComponents, Tabs } from 'ui'
import StepHikeCompact from '~/components/StepHikeCompact'
// Common components
import Admonition from './Admonition'
import ButtonCard from './ButtonCard'
import CodeBlock from './CodeBlock/CodeBlock'
import { parseNumericRange } from './CodeBlock/CodeBlock.utils'
import JwtGenerator from './JwtGenerator'
// Page specific components
import Frameworks from './Frameworks'
import AuthProviders from './AuthProviders'
import FunctionsExamples from './FunctionsExamples'
import Extensions from './Extensions'
import Frameworks from './Frameworks'
import FunctionsExamples from './FunctionsExamples'
// Other components
import { Heading } from './CustomHTMLElements'
import QuickstartIntro from './MDX/quickstart_intro.mdx'
import ProjectSetup from './MDX/project_setup.mdx'
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
import StorageManagement from './MDX/storage_management.mdx'
import { Mermaid } from 'mdx-mermaid/lib/Mermaid'
import InlineCodeTag from './CustomHTMLElements/InlineCode'
import React from 'react'
import RefSubLayout from '~/layouts/ref/RefSubLayout'
import { Heading } from './CustomHTMLElements'
import ProjectSetup from './MDX/project_setup.mdx'
import QuickstartIntro from './MDX/quickstart_intro.mdx'
import SocialProviderSettingsSupabase from './MDX/social_provider_settings_supabase.mdx'
import SocialProviderSetup from './MDX/social_provider_setup.mdx'
import StorageManagement from './MDX/storage_management.mdx'
// import { CH } from '@code-hike/mdx/components'
import RefHeaderSection from './reference/RefHeaderSection'
@@ -34,9 +30,9 @@ import CliGlobalFlagsHandler from '~/components/reference/enrichments/cli/CliGlo
import Options from '~/components/Options'
import Param from '~/components/Params'
import Image from 'next/image'
const components = {
...markdownComponents,
Admonition,
Button,
ButtonCard,
@@ -72,26 +68,11 @@ const components = {
{props.children}
</Heading>
),
// pre: (props: any) => {
// const linesToHighlight = parseNumericRange(props.lines ?? '')
// return <CodeBlock {...props} linesToHighlight={linesToHighlight} />
// },
mono: (props: any) => <code className="text-sm">{props.children}</code>,
// inlineCode: (props: { children: string }) => <InlineCodeTag {...props} />,
RefSubLayout,
// CH,
code: (props: any) => <CodeBlock {...props} />,
RefHeaderSection: (props: any) => <RefHeaderSection {...props} />,
CliGlobalFlagsHandler: () => <CliGlobalFlagsHandler />,
Options,
Param,
img: (props: any) => {
return (
<span className={['next-image--dynamic-fill'].join(' ')}>
<Image {...props} className={['rounded-md border'].join(' ')} layout="fill" />
</span>
)
},
}
export default components
@@ -1,5 +1,4 @@
import { Tabs } from '~/../../packages/ui'
import CodeBlock from '~/components/CodeBlock/CodeBlock'
import { CodeBlock, Tabs } from 'ui'
import Param from '~/components/Params'
import RefSubLayout from '~/layouts/ref/RefSubLayout'
@@ -1,8 +1,7 @@
import ReactMarkdown from 'react-markdown'
import { IconChevronRight, Tabs } from '~/../../packages/ui'
import { CodeBlock, IconChevronRight, Tabs } from 'ui'
// @ts-expect-error
import spec from '~/../../spec/cli_v1_commands.yaml' assert { type: 'yml' }
import CodeBlock from '~/components/CodeBlock/CodeBlock'
import Options from '~/components/Options'
import Param from '~/components/Params'
import RefSubLayout from '~/layouts/ref/RefSubLayout'
@@ -1,7 +1,6 @@
import ReactMarkdown from 'react-markdown'
import { IconDatabase, Tabs } from 'ui'
import CodeBlock from '~/components/CodeBlock/CodeBlock'
import { CodeBlock, IconDatabase, Tabs } from 'ui'
import Options from '~/components/Options'
import Param from '~/components/Params'
+2 -2
View File
@@ -45,9 +45,9 @@
"@mdx-js/react": "^1.6.22",
"@next/mdx": "^12.0.4",
"@radix-ui/react-accordion": "^1.0.1",
"@supabase/auth-helpers-nextjs": "^0.5.4",
"@supabase/auth-helpers-nextjs": "^0.5.6",
"@supabase/auth-helpers-react": "^0.3.1",
"@supabase/supabase-js": "^2.8.0",
"@supabase/supabase-js": "^2.13.0",
"algoliasearch": "^4.14.2",
"babel": "^6.23.0",
"clsx": "^1.2.1",
+24 -20
View File
@@ -1,19 +1,18 @@
import { createBrowserSupabaseClient } from '@supabase/auth-helpers-nextjs'
import { SessionContextProvider } from '@supabase/auth-helpers-react'
import { ThemeProvider } from 'common/Providers'
import { DefaultSeo } from 'next-seo'
import Head from 'next/head'
import { AuthProvider, ThemeProvider } from 'common'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { AppPropsWithLayout } from 'types'
import { CommandMenuProvider } from 'ui'
import components from '~/components'
import Favicons from '~/components/Favicons'
import SearchProvider from '~/components/Search/SearchProvider'
import SiteLayout from '~/layouts/SiteLayout'
import { IS_PLATFORM, LOCAL_SUPABASE } from '~/lib/constants'
import { post } from '~/lib/fetchWrappers'
import '../styles/algolia-search.scss'
import '../styles/ch.scss'
import '../styles/docsearch.scss'
import '../styles/main.scss?v=1.0.0'
import '../styles/new-docs.scss'
import '../styles/prism-okaidia.scss'
@@ -62,28 +61,33 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
const SITE_TITLE = 'Supabase Documentation'
const AuthContainer = (props) => {
return IS_PLATFORM || LOCAL_SUPABASE ? (
<SessionContextProvider supabaseClient={supabase}>
<AuthProvider>{props.children}</AuthProvider>
</SessionContextProvider>
) : (
<AuthProvider>{props.children}</AuthProvider>
)
}
return (
<>
<Favicons />
{IS_PLATFORM || LOCAL_SUPABASE ? (
<SessionContextProvider supabaseClient={supabase}>
<ThemeProvider>
<SearchProvider>
<SiteLayout>
<Component {...pageProps} />
</SiteLayout>
</SearchProvider>
</ThemeProvider>
</SessionContextProvider>
) : (
<AuthContainer>
<ThemeProvider>
<SearchProvider>
<CommandMenuProvider
site="docs"
MarkdownHandler={({ ...props }) => (
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components} {...props} />
)}
>
<SiteLayout>
<Component {...pageProps} />
</SiteLayout>
</SearchProvider>
</CommandMenuProvider>
</ThemeProvider>
)}
</AuthContainer>
</>
)
}
@@ -218,6 +218,12 @@ export const resources = [
href: '/guides/getting-started/architecture',
description: "An overview of Supabase's architecture and product principles.",
},
{
title: 'Local Development',
hasLightIcon: true,
href: '/guides/cli/local-development',
description: 'Use the Supabase CLI to develop locally and collaborate between teams.',
},
]
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -0,0 +1,107 @@
import Layout from '~/layouts/DefaultGuideLayout'
export const meta = {
id: 'bring-your-own-database',
title: 'Bring Your Own Database',
description: 'Bring your own Postgres database to subscribe to Realtime changes',
sidebar_label: 'Bring Your Own Database',
}
Realtime Database Changes works with any Postgres database that has logical replication enabled and the [wal2json](https://github.com/eulerto/wal2json) extension installed.
<Admonition type="note">
After setting up your database for Realtime, [submit a support ticket](https://app.supabase.com/support/new) if you want to use your database with hosted Supabase Realtime.
</Admonition>
The following steps will make sure that your database is properly set up to work with Realtime.
## `wal2json` Extension
Realtime relies on the `wal2json` Postgres extension to format database changes to JSON which are then sent to Realtime subscribers.
Postgres databases managed by AWS RDS and Google Cloud SQL should already have `wal2json` installed.
Please check to make sure this is indeed the case with your Postgres database.
## Logical Replication Configuration
Realtime relies on Postgres' logical replication functionality to get database changes. Please enable logical replication on your database and configure the following settings:
- `max_replication_slots`: we recommend `10` because Realtime requires a few slots plus the slots you'll need for your non-Realtime logical replication needs.
- `max_slot_wal_keep_size`: we recommend `1024` (MB) so Realtime can attempt to deliver more database changes stored in Postgres.
## Realtime Database Setup
### `supabase_realtime` Publication
Create `supabase_realtime` publication and add tables you want Realtime to listen to:
```sql
create publication supabase_realtime with (publish = 'insert, update, delete');
alter publication supabase_realtime add table messages, users;
```
<Admonition type="tip">
If the payload contains a `401 Unauthorized` then you will need to grant `select` privileges to the table for the database role you want to authorize to receive database changes:
```sql
grant select on table messages to anon;
```
</Admonition>
### `realtime` Schema
Create a `realtime` schema:
```sql
create schema realtime;
```
### `supabase_realtime_admin` Role
Create a `supabase_realtime_admin` database role and grant it replication permissions:
```sql
create role supabase_realtime_admin with noinherit login password 'secure-password';
```
Make sure to grant `supabase_realtime_admin` role with replication permissions. This step will vary based on your database provider.
For example, if your database is managed by AWS RDS then you can run:
```sql
grant rds_replication to supabase_realtime_admin;
```
### `supabase_realtime_admin` Privileges
Grant `supabase_realtime_admin` privileges for `realtime` schema and all related Realtime objects:
```sql
grant all on schema realtime to supabase_realtime_admin;
grant all on all tables in schema realtime to supabase_realtime_admin;
grant all on all sequences in schema realtime to supabase_realtime_admin;
grant all on all routines in schema realtime to supabase_realtime_admin;
```
### `authenticated` Role
Create an `authenticated` role:
```sql
create role authenticated nologin noinherit;
```
<Admonition type="note">
This role is hardcoded into Realtime migrations so it's required for the time being.
</Admonition>
export const Page = ({ children }) => <Layout meta={meta} children={children} />
export default Page
@@ -11,6 +11,8 @@ export const meta = {
<div>{spec.info.description}</div>
See [this guide](/docs/guides/realtime/bring-your-own-database) if you want to configure your database to work with Realtime.
<div>
{spec.info.tags.map(tag => {
return (
+2 -4
View File
@@ -1,9 +1,7 @@
import { CodeBlock, Tabs } from 'ui'
import specFile from '~/../../spec/transforms/auth_v1_openapi_deparsed.json' assert { type: 'json' }
import { gen_v3, enrichedOperation } from '~/lib/refGenerator/helpers'
import { Tabs } from '~/../../packages/ui'
import { gen_v3 } from '~/lib/refGenerator/helpers'
// @ts-ignore
import CodeBlock from '~/components/CodeBlock/CodeBlock'
import RefSubLayout from '~/layouts/ref/RefSubLayout'
export type AcceptedValue = {
@@ -1,9 +1,7 @@
import { CodeBlock, Tabs } from 'ui'
import specFile from '~/../../spec/transforms/storage_v0_openapi_deparsed.json' assert { type: 'json' }
import { gen_v3, enrichedOperation } from '~/lib/refGenerator/helpers'
import { Tabs } from '~/../../packages/ui'
import { gen_v3 } from '~/lib/refGenerator/helpers'
// @ts-ignore
import CodeBlock from '~/components/CodeBlock/CodeBlock'
import RefSubLayout from '~/layouts/ref/RefSubLayout'
export type AcceptedValue = {
+401 -299
View File
@@ -18,6 +18,24 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/contributing</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/introduction</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/cli</loc>
<changefreq>weekly</changefreq>
@@ -78,24 +96,42 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/contributing</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/handbook/introduction</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/index</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-google-oauth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-gotrue</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-policies</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/auth-captcha</loc>
<changefreq>weekly</changefreq>
@@ -133,13 +169,13 @@
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/managing-user-data</loc>
<loc>https://supabase.com/docs/guides/auth/enterprise-sso</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/overview</loc>
<loc>https://supabase.com/docs/guides/auth/managing-user-data</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
@@ -180,6 +216,84 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/auth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/cicd-workflow</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/connect-to-postgres</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/cors</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/debugging</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/global-deployments</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/import-maps</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/local-development</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/quickstart</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/schedule-functions</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/secrets</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/storage-caching</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/typescript-support</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/database/api</loc>
<changefreq>weekly</changefreq>
@@ -450,60 +564,6 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/auth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/cicd-workflow</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/cors</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/debugging</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/import-maps</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/local-development</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/quickstart</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/schedule-functions</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/secrets</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/access-control</loc>
<changefreq>weekly</changefreq>
@@ -582,6 +642,12 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/spend-cap</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/ssl-enforcement</loc>
<changefreq>weekly</changefreq>
@@ -601,19 +667,25 @@
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/broadcast</loc>
<loc>https://supabase.com/docs/guides/realtime/architecture</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/postgres-changes</loc>
<loc>https://supabase.com/docs/guides/realtime/channels</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/presence</loc>
<loc>https://supabase.com/docs/guides/realtime/extensions</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/protocol</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
@@ -630,6 +702,18 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/realtime-with-nextjs</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/subscribing-to-database-changes</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/resources/examples</loc>
<changefreq>weekly</changefreq>
@@ -678,36 +762,6 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-deep-dive-jwts</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-google-oauth</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-gotrue</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-policies</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/learn/auth-deep-dive/auth-row-level-security</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/auth-helpers/auth-ui</loc>
<changefreq>weekly</changefreq>
@@ -738,6 +792,24 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/phone-login/messagebird</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/phone-login/twilio</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/phone-login/vonage</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/social-login/auth-apple</loc>
<changefreq>weekly</changefreq>
@@ -840,12 +912,102 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/sso/auth-sso-saml</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/cloudflare-turnstile</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/discord-bot</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/github-actions</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/og-image</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/openai</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/rate-limiting</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/screenshots</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/stripe-webhooks</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/telegram-bot</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/upstash-redis</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/database/api/generating-types</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/database/postgres/dropping-all-tables-in-schema</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/database/postgres/first-row-in-group</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/database/postgres/indexes</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/database/postgres/which-version-of-postgres</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/quickstarts/flutter</loc>
<changefreq>weekly</changefreq>
@@ -888,186 +1050,6 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-angular</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-expo</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-flutter</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-angular</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-react</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-vue</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-nuxt-3</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-react</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-redwoodjs</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-solidjs</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-svelte</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-vue-3</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/phone-login/messagebird</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/phone-login/twilio</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/auth/phone-login/vonage</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/cloudflare-turnstile</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/connect-to-postgres</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/discord-bot</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/github-actions</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/og-image</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/openai</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/storage-caching</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/stripe-webhooks</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/telegram-bot</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/functions/examples/upstash-redis</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/sso/azure</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/sso/gsuite</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/sso/okta</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/database/extensions/http</loc>
<changefreq>weekly</changefreq>
@@ -1224,6 +1206,126 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/sso/azure</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/sso/gsuite</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/platform/sso/okta</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-angular</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-expo</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-flutter</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-angular</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-react</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-ionic-vue</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-nextjs</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-nuxt-3</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-react</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-redwoodjs</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-solidjs</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-svelte</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-sveltekit</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/getting-started/tutorials/with-vue-3</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/extensions/broadcast</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/extensions/postgres-changes</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/realtime/extensions/presence</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/resources/migrating-to-supabase/firebase-auth</loc>
<changefreq>weekly</changefreq>
@@ -1260,24 +1362,6 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/resources/postgres/dropping-all-tables-in-schema</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/resources/postgres/first-row-in-group</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/resources/postgres/which-version-of-postgres</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/guides/self-hosting/realtime/config</loc>
<changefreq>weekly</changefreq>
@@ -3560,6 +3644,24 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/cli/supabase-ssl-enforcement</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/cli/supabase-ssl-enforcement-update</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/cli/supabase-ssl-enforcement-get</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/docs/reference/cli/supabase-secrets</loc>
<changefreq>weekly</changefreq>
-962
View File
@@ -1,962 +0,0 @@
// ----------------
// 1. CSS Variables
// 2. Dark Mode
// 3. Autocomplete
// 4. Panel
// 5. Sources
// 6. Hit Layout
// 7. Panel Header
// 8. Panel Footer
// 9. Detached Mode
// 10. Gradients
// 11. Utilities
// ----------------
// Note:
// This theme reflects the markup structure of autocomplete with SCSS indentation.
// We use the SASS `@at-root` function to keep specificity low.
// ----------------
// 1. CSS Variables
// ----------------
:root {
// Input
--aa-search-input-height: 38px;
--aa-input-icon-size: 20px;
// Size and spacing
--aa-base-unit: 16;
--aa-spacing-factor: 1;
--aa-spacing: calc(var(--aa-base-unit) * var(--aa-spacing-factor) * 1px);
--aa-spacing-half: calc(var(--aa-spacing) / 2);
--aa-panel-max-height: 650px;
// Z-index
--aa-base-z-index: 9999;
// Font
--aa-font-size: calc(var(--aa-base-unit) * 1px);
--aa-font-family: inherit;
--aa-font-weight-medium: 500;
--aa-font-weight-semibold: 600;
--aa-font-weight-bold: 700;
// Icons
--aa-icon-size: 20px;
--aa-icon-stroke-width: 1.6;
--aa-icon-color-rgb: 119, 119, 163;
--aa-icon-color-alpha: 1;
--aa-action-icon-size: 20px;
// Text colors
--aa-text-color-rgb: 38, 38, 39;
--aa-text-color-alpha: 1;
--aa-primary-color-rgb: 62, 52, 211;
--aa-primary-color-alpha: 0.2;
--aa-muted-color-rgb: 128, 126, 163;
--aa-muted-color-alpha: 0.6;
// Border colors
--aa-panel-border-color-rgb: 128, 126, 163;
--aa-panel-border-color-alpha: 0.3;
--aa-input-border-color-rgb: 128, 126, 163;
--aa-input-border-color-alpha: 0.8;
// Background colors
--aa-background-color-rgb: 255, 255, 255;
--aa-background-color-alpha: 1;
--aa-input-background-color-rgb: 255, 255, 255;
--aa-input-background-color-alpha: 1;
--aa-selected-color-rgb: 179, 173, 214;
--aa-selected-color-alpha: 0.205;
--aa-description-highlight-background-color-rgb: 245, 223, 77;
--aa-description-highlight-background-color-alpha: 0.5;
// Detached mode
--aa-detached-media-query: (max-width: 680px);
--aa-detached-modal-media-query: (min-width: 680px);
--aa-detached-modal-max-width: 680px;
--aa-detached-modal-max-height: 500px;
--aa-overlay-color-rgb: 115, 114, 129;
--aa-overlay-color-alpha: 0.4;
// Shadows
--aa-panel-shadow: 0 0 0 1px rgba(35, 38, 59, 0.1), 0 6px 16px -4px rgba(35, 38, 59, 0.15);
// Scrollbar
--aa-scrollbar-width: 13px;
--aa-scrollbar-track-background-color-rgb: 234, 234, 234;
--aa-scrollbar-track-background-color-alpha: 1;
--aa-scrollbar-thumb-background-color-rgb: var(--aa-background-color-rgb);
--aa-scrollbar-thumb-background-color-alpha: 1;
// Touch screens
@media (hover: none) and (pointer: coarse) {
--aa-spacing-factor: 1.2;
--aa-action-icon-size: 22px;
}
}
// ----------------
// 2. Dark Mode
// ----------------
body {
/* stylelint-disable selector-no-qualifying-type, selector-class-pattern */
&[data-theme='dark'],
&.dark {
// Text colors
--aa-text-color-rgb: 183, 192, 199;
--aa-primary-color-rgb: 146, 138, 255;
--aa-muted-color-rgb: 146, 138, 255;
// Background colors
--aa-input-background-color-rgb: 0, 3, 9;
--aa-background-color-rgb: 21, 24, 42;
--aa-selected-color-rgb: 146, 138, 255;
--aa-selected-color-alpha: 0.25;
--aa-description-highlight-background-color-rgb: 0 255 255;
--aa-description-highlight-background-color-alpha: 0.25;
// Icons
--aa-icon-color-rgb: 119, 119, 163;
// Shadows
--aa-panel-shadow: inset 1px 1px 0 0 rgb(44, 46, 64), 0 3px 8px 0 rgb(0, 3, 9);
// Scrollbar
--aa-scrollbar-track-background-color-rgb: 44, 46, 64;
--aa-scrollbar-thumb-background-color-rgb: var(--aa-background-color-rgb);
}
/* stylelint-enable selector-no-qualifying-type, selector-class-pattern */
}
// Reset for `@extend`
%reset {
box-sizing: border-box;
}
// Init for `@extend`
%init {
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
font-family: var(--aa-font-family);
font-size: var(--aa-font-size);
font-weight: normal;
line-height: 1em;
margin: 0;
padding: 0;
text-align: left;
}
// ----------------
// 3. Autocomplete
// ----------------
.aa-Autocomplete,
.aa-DetachedFormContainer {
@extend %init;
* {
@extend %reset;
}
// Search box
@at-root .aa-Form {
@apply transition bg-scale-200 border border-scale-500;
align-items: center;
border-radius: 3px;
display: flex;
line-height: 1em;
margin: 0;
position: relative;
width: 100%;
&:focus-within {
@apply border-brand-900;
outline: currentColor none medium;
}
@at-root .aa-InputWrapperPrefix {
align-items: center;
display: flex;
flex-shrink: 0;
height: var(--aa-search-input-height);
order: 1;
// Container for search and loading icons
@at-root .aa-Label,
.aa-LoadingIndicator {
cursor: initial;
flex-shrink: 0;
height: 100%;
padding: 0;
text-align: left;
svg {
@apply text-brand-900;
height: auto;
max-height: var(--aa-input-icon-size);
stroke-width: var(--aa-icon-stroke-width);
width: var(--aa-input-icon-size);
}
}
@at-root .aa-SubmitButton,
.aa-LoadingIndicator {
height: 100%;
padding-left: calc(var(--aa-spacing) * 0.75 - 1px);
padding-right: var(--aa-spacing-half);
width: calc(var(--aa-spacing) * 1.75 + var(--aa-icon-size) - 1px);
@media (hover: none) and (pointer: coarse) {
padding-left: calc(var(--aa-spacing-half) / 2 - 1px);
width: calc(var(--aa-icon-size) + (var(--aa-spacing) * 1.25) - 1px);
}
}
@at-root .aa-SubmitButton {
appearance: none;
background: none;
border: 0;
margin: 0;
}
@at-root .aa-LoadingIndicator {
align-items: center;
display: flex;
justify-content: center;
&[hidden] {
display: none;
}
}
}
@at-root .aa-InputWrapper {
order: 3;
position: relative;
width: 100%;
// Search box input (with placeholder and query)
@at-root .aa-Input {
@apply text-sm text-scale-1200;
appearance: none;
background: none;
border: 0;
height: var(--aa-search-input-height);
padding: 0;
width: 100%;
&::placeholder {
@apply text-scale-700;
opacity: 1;
}
// Focus is set and styled on the parent, it isn't necessary here
&:focus {
border-color: none;
box-shadow: none;
outline: none;
}
// Remove native appearence
&::-webkit-search-decoration,
&::-webkit-search-cancel-button,
&::-webkit-search-results-button,
&::-webkit-search-results-decoration {
appearance: none;
}
}
}
@at-root .aa-InputWrapperSuffix {
align-items: center;
display: flex;
height: var(--aa-search-input-height);
order: 4;
// Accelerator to clear the query
@at-root .aa-ClearButton {
align-items: center;
background: none;
border: 0;
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
cursor: pointer;
display: flex;
height: 100%;
margin: 0;
padding: 0 calc(var(--aa-spacing) * (5 / 6) - 0.5px);
@media (hover: none) and (pointer: coarse) {
padding: 0 calc(var(--aa-spacing) * (2 / 3) - 0.5px);
}
&:hover,
&:focus {
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
}
&[hidden] {
display: none;
}
svg {
stroke-width: var(--aa-icon-stroke-width);
width: var(--aa-icon-size);
}
}
}
}
}
// ----------------
// 4. Panel
// ----------------
.aa-Panel {
@extend %init;
@apply bg-scale-200;
border-radius: calc(var(--aa-spacing) / 4);
box-shadow: var(--aa-panel-shadow);
margin: 8px 0 0;
overflow: hidden;
position: absolute;
transition: opacity 200ms ease-in, filter 200ms ease-in;
@media screen and (prefers-reduced-motion) {
transition: none;
}
* {
@extend %reset;
}
button {
appearance: none;
background: none;
border: 0;
margin: 0;
padding: 0;
}
@at-root .aa-PanelLayout {
@apply bg-scale-200;
height: 100%;
margin: 0;
max-height: var(--aa-panel-max-height);
overflow-y: auto;
padding: 0;
position: relative;
text-align: left;
@at-root .aa-PanelLayoutColumns--twoGolden {
display: grid;
grid-template-columns: 39.2% auto;
overflow: hidden;
padding: 0;
}
@at-root .aa-PanelLayoutColumns--two {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
overflow: hidden;
padding: 0;
}
@at-root .aa-PanelLayoutColumns--three {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
overflow: hidden;
padding: 0;
}
}
// When a request isn't resolved yet
@at-root .aa-Panel--stalled {
.aa-Source {
filter: grayscale(1);
opacity: 0.8;
}
}
@at-root .aa-Panel--scrollable {
margin: 0;
max-height: var(--aa-panel-max-height);
overflow-x: hidden;
overflow-y: auto;
padding: var(--aa-spacing-half);
scrollbar-color: rgba(
var(--aa-scrollbar-thumb-background-color-rgb),
var(--aa-scrollbar-thumb-background-color-alpha)
)
rgba(
var(--aa-scrollbar-track-background-color-rgb),
var(--aa-scrollbar-track-background-color-alpha)
);
scrollbar-width: thin;
&::-webkit-scrollbar {
width: var(--aa-scrollbar-width);
}
&::-webkit-scrollbar-track {
background-color: rgba(
var(--aa-scrollbar-track-background-color-rgb),
var(--aa-scrollbar-track-background-color-alpha)
);
}
&::-webkit-scrollbar-thumb {
background-color: rgba(
var(--aa-scrollbar-thumb-background-color-rgb),
var(--aa-scrollbar-thumb-background-color-alpha)
);
border-color: rgba(
var(--aa-scrollbar-track-background-color-rgb),
var(--aa-scrollbar-track-background-color-alpha)
);
border-radius: 9999px;
border-style: solid;
border-width: 3px 2px 3px 3px;
}
}
}
// ----------------
// 5. Sources
// Each source can be styled independently
// ----------------
.aa-Source {
margin: 0;
padding: 0;
position: relative;
width: 100%;
&:empty {
// Hide empty section
display: none;
}
@at-root .aa-SourceNoResults {
font-size: 1em;
margin: 0;
padding: var(--aa-spacing);
}
// List of results inside the source
@at-root .aa-List {
list-style: none;
margin: 0;
padding: 0;
position: relative;
}
// Source title
@at-root .aa-SourceHeader {
margin: var(--aa-spacing-half) 0.5em var(--aa-spacing-half) 0;
padding: 0;
position: relative;
// Hide empty header
&:empty {
display: none;
}
// Title typography
@at-root .aa-SourceHeaderTitle {
background: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
color: rgba(var(--aa-primary-color-rgb), 1);
display: inline-block;
font-size: 0.8em;
font-weight: var(--aa-font-weight-semibold);
margin: 0;
padding: 0 var(--aa-spacing-half) 0 0;
position: relative;
z-index: var(--aa-base-z-index);
}
// Line separator
@at-root .aa-SourceHeaderLine {
border-bottom: solid 1px rgba(var(--aa-primary-color-rgb), 1);
display: block;
height: 2px;
left: 0;
margin: 0;
opacity: 0.3;
padding: 0;
position: absolute;
right: 0;
top: var(--aa-spacing-half);
z-index: calc(var(--aa-base-z-index) - 1);
}
}
// See all button
@at-root .aa-SourceFooterSeeAll {
background: linear-gradient(
180deg,
rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha)),
rgba(128, 126, 163, 0.14)
);
border: 1px solid rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
border-radius: 5px;
box-shadow: inset 0 0 2px #fff, 0 2px 2px -1px rgba(76, 69, 88, 0.15);
color: inherit;
font-size: 0.95em;
font-weight: var(--aa-font-weight-medium);
padding: 0.475em 1em 0.6em;
text-decoration: none;
&:focus,
&:hover {
border: 1px solid rgba(var(--aa-primary-color-rgb), 1);
color: rgba(var(--aa-primary-color-rgb), 1);
}
}
}
// ----------------
// 6. Hit Layout
// ----------------
.aa-Item {
@apply px-4 py-4;
align-items: center;
border-radius: 3px;
cursor: pointer;
display: grid;
min-height: calc(var(--aa-spacing) * 2.5);
// When the result is active
&[aria-selected='true'] {
@apply bg-scale-400;
.aa-ItemActionButton,
.aa-ActiveOnly {
visibility: visible;
}
}
// The result type icon inlined SVG or image
@at-root .aa-ItemIcon {
align-items: center;
background: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
border-radius: 3px;
box-shadow: inset 0 0 0 1px
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
color: rgba(var(--aa-icon-color-rgb), var(--aa-icon-color-alpha));
display: flex;
flex-shrink: 0;
font-size: 0.7em;
height: calc(var(--aa-icon-size) + var(--aa-spacing-half));
justify-content: center;
overflow: hidden;
stroke-width: var(--aa-icon-stroke-width);
text-align: center;
width: calc(var(--aa-icon-size) + var(--aa-spacing-half));
img {
height: auto;
max-height: calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px);
max-width: calc(var(--aa-icon-size) + var(--aa-spacing-half) - 8px);
width: auto;
}
svg {
height: var(--aa-icon-size);
width: var(--aa-icon-size);
}
@at-root .aa-ItemIcon--alignTop {
align-self: flex-start;
}
@at-root .aa-ItemIcon--noBorder {
background: none;
box-shadow: none;
}
@at-root .aa-ItemIcon--picture {
height: 96px;
width: 96px;
img {
max-height: 100%;
max-width: 100%;
padding: var(--aa-spacing-half);
}
}
}
@at-root .aa-ItemContent {
@apply flex flex-col space-y-1;
cursor: pointer;
line-height: 1.25em;
overflow: hidden;
&:empty {
display: none;
}
mark {
@apply text-brand-900;
background: none;
font-style: normal;
font-weight: var(--aa-font-weight-bold);
}
.aa-ItemTitle {
@apply text-scale-1200 text-sm;
}
@at-root .aa-ItemContent--dual {
display: flex;
flex-direction: column;
justify-content: space-between;
text-align: left;
.aa-ItemContentTitle,
.aa-ItemContentSubtitle {
display: block;
}
}
@at-root .aa-ItemContent--indented {
padding-left: calc(var(--aa-icon-size) + var(--aa-spacing));
}
@at-root .aa-ItemContentBody {
display: grid;
gap: calc(var(--aa-spacing-half) / 2);
}
@at-root .aa-ItemContentTitle {
display: inline-block;
margin: 0 0.5em 0 0;
max-width: 100%;
overflow: hidden;
padding: 0;
text-overflow: ellipsis;
white-space: nowrap;
}
@at-root .aa-ItemContentSubtitle {
@apply text-scale-1000 text-sm truncate;
@at-root .aa-ItemContentSubtitleIcon {
&::before {
border-color: rgba(var(--aa-muted-color-rgb), 0.64);
border-style: solid;
content: '';
display: inline-block;
left: 1px;
position: relative;
top: -3px;
}
}
@at-root .aa-ItemContentSubtitle--inline {
.aa-ItemContentSubtitleIcon {
&::before {
border-width: 0 0 1.5px;
margin-left: var(--aa-spacing-half);
margin-right: calc(var(--aa-spacing-half) / 2);
width: calc(var(--aa-spacing-half) + 2px);
}
}
}
@at-root .aa-ItemContentSubtitle--standalone {
align-items: center;
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
display: grid;
gap: var(--aa-spacing-half);
grid-auto-flow: column;
justify-content: start;
.aa-ItemContentSubtitleIcon {
&::before {
border-radius: 0 0 0 3px;
border-width: 0 0 1.5px 1.5px;
height: var(--aa-spacing-half);
width: var(--aa-spacing-half);
}
}
}
@at-root .aa-ItemContentSubtitleCategory {
color: rgba(var(--aa-muted-color-rgb), 1);
font-weight: 500;
}
}
@at-root .aa-ItemContentDescription {
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
font-size: 0.85em;
max-width: 100%;
overflow-x: hidden;
text-overflow: ellipsis;
&:empty {
display: none;
}
mark {
background: rgba(
var(--aa-description-highlight-background-color-rgb),
var(--aa-description-highlight-background-color-alpha)
);
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
font-style: normal;
font-weight: var(--aa-font-weight-medium);
}
}
@at-root .aa-ItemContentDash {
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
display: none;
opacity: 0.4;
}
@at-root .aa-ItemContentTag {
background-color: rgba(var(--aa-primary-color-rgb), var(--aa-primary-color-alpha));
border-radius: 3px;
margin: 0 0.4em 0 0;
padding: 0.08em 0.3em;
}
}
// wrap hit with url but we don't need to see it
@at-root .aa-ItemWrapper,
.aa-ItemLink {
align-items: center;
color: inherit;
gap: calc(var(--aa-spacing-half) / 2);
grid-auto-flow: column;
justify-content: space-between;
width: 100%;
}
@at-root .aa-ItemLink {
color: inherit;
text-decoration: none;
}
// Secondary click actions
@at-root .aa-ItemActions {
display: grid;
grid-auto-flow: column;
height: 100%;
justify-self: end;
margin: 0 calc(var(--aa-spacing) / -3);
padding: 0 2px 0 0;
}
@at-root .aa-ItemActionButton {
align-items: center;
background: none;
border: 0;
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
cursor: pointer;
display: flex;
flex-shrink: 0;
padding: 0;
&:hover,
&:focus {
svg {
color: rgba(var(--aa-text-color-rgb), var(--aa-text-color-alpha));
@media (hover: none) and (pointer: coarse) {
color: inherit;
}
}
}
svg {
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
margin: 0;
margin: calc(var(--aa-spacing) / 3);
stroke-width: var(--aa-icon-stroke-width);
width: var(--aa-action-icon-size);
}
}
@at-root .aa-ActiveOnly {
visibility: hidden;
}
}
//----------------
// 7. Panel Header
//----------------
.aa-PanelHeader {
align-items: center;
background: rgba(var(--aa-primary-color-rgb), 1);
color: #fff;
display: grid;
height: var(--aa-modal-header-height);
margin: 0;
padding: var(--aa-spacing-half) var(--aa-spacing);
position: relative;
&::after {
background-image: linear-gradient(
rgba(var(--aa-background-color-rgb), 1),
rgba(var(--aa-background-color-rgb), 0)
);
bottom: calc(var(--aa-spacing-half) * -1);
content: '';
height: var(--aa-spacing-half);
left: 0;
pointer-events: none;
position: absolute;
right: 0;
z-index: var(--aa-base-z-index);
}
}
//----------------
// 8. Panel Footer
//----------------
.aa-PanelFooter {
background-color: rgba(var(--aa-background-color-rgb), var(--aa-background-color-alpha));
box-shadow: inset 0 1px 0
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
display: flex;
justify-content: space-between;
margin: 0;
padding: var(--aa-spacing);
position: relative;
z-index: var(--aa-base-z-index);
&::after {
background-image: linear-gradient(
rgba(var(--aa-background-color-rgb), 0),
rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha))
);
content: '';
height: var(--aa-spacing);
left: 0;
opacity: 0.12;
pointer-events: none;
position: absolute;
right: 0;
top: calc(var(--aa-spacing) * -1);
z-index: calc(var(--aa-base-z-index) - 1);
}
}
//----------------
// 9. Detached Mode
//----------------
.aa-DetachedContainer {
@apply bg-scale-200 border border-scale-500;
bottom: 0;
box-shadow: var(--aa-panel-shadow);
display: flex;
flex-direction: column;
left: 0;
margin: 0;
overflow: hidden;
padding: 0;
position: fixed;
right: 0;
top: 0;
z-index: var(--aa-base-z-index);
&::after {
height: 32px;
}
.aa-SourceHeader {
margin: var(--aa-spacing-half) 0 var(--aa-spacing-half) 2px;
}
.aa-Panel {
@apply bg-scale-200;
border-radius: 0;
box-shadow: none;
flex-grow: 1;
margin: 0;
padding: 0;
position: relative;
}
.aa-PanelLayout {
bottom: 0;
box-shadow: none;
left: 0;
margin: 0;
max-height: none;
overflow-y: auto;
position: absolute;
right: 0;
top: 0;
width: 100%;
}
@at-root .aa-DetachedFormContainer {
@apply bg-scale-200;
border-bottom: solid 1px
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
display: flex;
flex-direction: row;
justify-content: space-between;
margin: 0;
padding: var(--aa-spacing-half);
@at-root .aa-DetachedCancelButton {
@apply text-scale-1100 text-sm;
background: none;
border: 0;
border-radius: 3px;
cursor: pointer;
margin: 0 0 0 var(--aa-spacing-half);
padding: 0 var(--aa-spacing-half);
&:hover,
&:focus {
box-shadow: inset 0 0 0 1px
rgba(var(--aa-panel-border-color-rgb), var(--aa-panel-border-color-alpha));
}
}
}
@at-root .aa-DetachedContainer--modal {
border-radius: 6px;
bottom: inherit;
height: auto;
margin: 0 auto;
max-width: var(--aa-detached-modal-max-width);
position: absolute;
top: 3%;
.aa-PanelLayout {
max-height: var(--aa-detached-modal-max-height);
padding-bottom: var(--aa-spacing-half);
position: static;
}
}
}
// Search Button
.aa-DetachedSearchButton {
@apply rounded-md bg-scaleA-200 border border-scale-700;
align-items: center;
color: rgba(var(--aa-muted-color-rgb), var(--aa-muted-color-alpha));
cursor: pointer;
display: flex;
font: inherit;
font-family: var(--aa-font-family);
font-size: var(--aa-font-size);
height: var(--aa-search-input-height);
margin: 0;
padding: 0 calc(var(--aa-search-input-height) / 8);
position: relative;
text-align: left;
width: 100%;
&:focus {
@apply border-brand-900;
outline: currentColor none medium;
}
@at-root .aa-DetachedSearchButtonIcon {
@apply text-scale-1100;
align-items: center;
cursor: initial;
display: flex;
height: 100%;
justify-content: center;
width: calc(var(--aa-icon-size) + var(--aa-spacing));
}
.aa-DetachedSearchButtonPlaceholder {
@apply text-sm text-scale-800;
}
}
// Remove scroll on `body`
.aa-Detached {
height: 100vh;
overflow: hidden;
}
.aa-DetachedOverlay {
@apply backdrop-blur-sm;
background-color: rgba(0, 0, 0, 0.4);
height: 100vh;
left: 0;
margin: 0;
padding: 0;
position: fixed;
right: 0;
top: 0;
z-index: calc(var(--aa-base-z-index) - 1);
}
//----------------
// 10. Gradients
//----------------
.aa-GradientTop,
.aa-GradientBottom {
height: var(--aa-spacing-half);
left: 0;
pointer-events: none;
position: absolute;
right: 0;
z-index: var(--aa-base-z-index);
}
.aa-GradientTop {
// background-image: linear-gradient(
// rgba(var(--aa-background-color-rgb), 1),
// rgba(var(--aa-background-color-rgb), 0)
// );
top: 0;
}
.aa-GradientBottom {
// background-image: linear-gradient(
// rgba(var(--aa-background-color-rgb), 0),
// rgba(var(--aa-background-color-rgb), 1)
// );
// border-bottom-left-radius: calc(var(--aa-spacing) / 4);
// border-bottom-right-radius: calc(var(--aa-spacing) / 4);
bottom: 0;
}
//----------------
// 11. Utilities
//----------------
.aa-DesktopOnly {
@media (hover: none) and (pointer: coarse) {
display: none;
}
}
.aa-TouchOnly {
@media (hover: hover) {
display: none;
}
}
// search bar
input[type='search'].DocSearch-Input {
background-color: var(--colors-scale1);
box-shadow: none;
}
.dark .DocSearch-Input {
background-color: var(--colors-scale2);
color: white;
}
-535
View File
@@ -1,535 +0,0 @@
/* search modal */
.DocSearch--active {
@apply overflow-hidden #{!important};
}
.DocSearch-Container {
background: rgba(0, 0, 0, 0.3);
@apply fixed top-0 left-0 z-[200] flex h-screen w-screen flex-col p-4 backdrop-blur-sm sm:p-6 md:p-[10vh] lg:p-[12vh];
@apply cursor-auto #{!important};
}
.DocSearch-LoadingIndicator svg {
@apply hidden;
}
.DocSearch-LoadingIndicator {
display: none;
width: 1.5rem;
height: 1.5rem;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none'%3E%3Ccircle cx='12' cy='12' r='9' stroke-width='2' stroke='%23cffafe' /%3E%3Cpath d='M3,12a9,9 0 1,0 18,0a9,9 0 1,0 -18,0' stroke-width='2' stroke='%2306b6d4' stroke-dasharray='56.5486677646' stroke-dashoffset='37.6991118431' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-size: 100% 100%;
}
.DocSearch-Container--Stalled .DocSearch-LoadingIndicator {
@apply block;
}
.DocSearch-Modal {
@apply dark:bg-scale-200 bg-scale-100;
margin: 0 auto;
width: 100%;
max-width: 47.375rem;
display: flex;
flex-direction: column;
min-height: 0;
border-radius: theme('borderRadius.lg');
box-shadow: theme('boxShadow.lg');
}
.DocSearch-SearchBar {
z-index: 1;
padding: 0 1rem;
@apply border-scale-400 relative flex flex-none items-center border-b;
}
.DocSearch-Form {
@apply flex min-w-0 flex-auto items-center;
}
.DocSearch-Dropdown-Container {
@apply pb-6;
}
.DocSearch-Dropdown {
@apply flex-auto overflow-auto;
}
.DocSearch-Hit--Result {
@apply relative mx-6;
}
.DocSearch-Hit--Result.DocSearch-Hit--Child {
@apply ml-12;
}
.DocSearch-Hit--Result.DocSearch-Hit--Child::before {
content: '';
position: absolute;
top: -0.25rem;
bottom: -0.25rem;
left: -1rem;
width: 1px;
background: theme('colors.scale.400');
// @apply dark:bg-slate-700;
}
.DocSearch-Hit > a {
position: relative;
font-size: 0.875rem;
// border-bottom: 1px solid theme('colors.slate.100');
// @apply dark:border-slate-200;
}
.DocSearch-Hit--Result {
// @apply border-b-0 #{!important};
}
.DocSearch-MagnifierLabel {
@apply h-6 w-6 flex-none;
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m19 19-3.5-3.5' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='11' r='6' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m19 19-3.5-3.5' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3Ccircle cx='11' cy='11' r='6' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-MagnifierLabel svg {
display: none;
}
.DocSearch-Container--Stalled .DocSearch-MagnifierLabel {
display: none;
}
.DocSearch-Input {
@apply text-scale-1200 placeholder-scale-900;
appearance: none;
background: transparent;
height: 3.5rem;
font-size: 1rem;
margin-left: 0.75rem;
margin-right: 1rem;
flex: auto;
min-width: 0;
border: none;
}
@screen sm {
.DocSearch-Input {
font-size: 0.875rem;
}
}
.DocSearch-Input:focus {
outline: 2px dotted transparent;
}
.DocSearch-Input::-webkit-search-cancel-button,
.DocSearch-Input::-webkit-search-decoration,
.DocSearch-Input::-webkit-search-results-button,
.DocSearch-Input::-webkit-search-results-decoration {
display: none;
}
.DocSearch-Reset {
display: none;
}
button[type='reset'].DocSearch-Cancel {
appearance: none;
flex: none;
font-size: 0;
border-radius: 0.375rem;
padding: 0.25rem 0.375rem;
@apply ring-scale-900 hover:ring-scale-900 dark:bg-scale-600 shadow-sm ring-1 hover:shadow dark:ring-0;
width: 1.75rem;
height: 1.5rem;
/* esc */
background-image: url("data:image/svg+xml,%3Csvg width='16' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M.506 6h3.931V4.986H1.736v-1.39h2.488V2.583H1.736V1.196h2.69V.182H.506V6ZM8.56 1.855h1.18C9.721.818 8.87.102 7.574.102c-1.276 0-2.21.705-2.205 1.762-.003.858.602 1.35 1.585 1.585l.634.159c.633.153.986.335.988.727-.002.426-.406.716-1.03.716-.64 0-1.1-.295-1.14-.878h-1.19c.03 1.259.931 1.91 2.343 1.91 1.42 0 2.256-.68 2.259-1.745-.003-.969-.733-1.483-1.744-1.71l-.523-.125c-.506-.117-.93-.304-.92-.722 0-.375.332-.65.934-.65.588 0 .949.267.994.724ZM15.78 2.219C15.618.875 14.6.102 13.254.102c-1.537 0-2.71 1.086-2.71 2.989 0 1.898 1.153 2.989 2.71 2.989 1.492 0 2.392-.992 2.526-2.063l-1.244-.006c-.117.623-.606.98-1.262.98-.883 0-1.483-.656-1.483-1.9 0-1.21.591-1.9 1.492-1.9.673 0 1.159.389 1.253 1.028h1.244Z' fill='%23334155'/%3E%3C/svg%3E");
background-position: center;
background-repeat: no-repeat;
background-size: 57.1428571429% auto;
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='16' height='7' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M.506 6h3.931V4.986H1.736v-1.39h2.488V2.583H1.736V1.196h2.69V.182H.506V6ZM8.56 1.855h1.18C9.721.818 8.87.102 7.574.102c-1.276 0-2.21.705-2.205 1.762-.003.858.602 1.35 1.585 1.585l.634.159c.633.153.986.335.988.727-.002.426-.406.716-1.03.716-.64 0-1.1-.295-1.14-.878h-1.19c.03 1.259.931 1.91 2.343 1.91 1.42 0 2.256-.68 2.259-1.745-.003-.969-.733-1.483-1.744-1.71l-.523-.125c-.506-.117-.93-.304-.92-.722 0-.375.332-.65.934-.65.588 0 .949.267.994.724ZM15.78 2.219C15.618.875 14.6.102 13.254.102c-1.537 0-2.71 1.086-2.71 2.989 0 1.898 1.153 2.989 2.71 2.989 1.492 0 2.392-.992 2.526-2.063l-1.244-.006c-.117.623-.606.98-1.262.98-.883 0-1.483-.656-1.483-1.9 0-1.21.591-1.9 1.492-1.9.673 0 1.159.389 1.253 1.028h1.244Z' fill='%2394a3b8'/%3E%3C/svg%3E");
}
}
.DocSearch-Reset svg {
display: none;
}
.DocSearch-Hit-source {
line-height: 1.5rem;
font-weight: 400;
color: theme('colors.scale.900');
padding-top: 2.5rem;
margin: 0 1.5rem 1rem;
}
.DocSearch-Hits:first-child .DocSearch-Hit-source {
padding-top: 1.5rem;
}
.DocSearch-Hit-Container {
display: flex;
align-items: center;
}
.DocSearch-Hit-Tree {
display: none;
}
.DocSearch-Hit-icon {
display: none;
}
.DocSearch-Hit--Result .DocSearch-Hit-icon {
display: block;
flex: none;
margin-right: 1rem;
width: 1.5rem;
height: 1.5rem;
border-radius: theme('borderRadius.md');
/* hash icon */
background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10' stroke='%2394A3B8' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: center center;
@apply ring-scale-300 bg-white shadow-sm ring-1 dark:bg-slate-700 dark:ring-0;
}
.DocSearch-Hit--Parent.DocSearch-Hit--Guide .DocSearch-Hit-icon {
/* book icon */
background-image: url("data:image/svg+xml,%3Csvg width='14' height='10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3.5 0c1.093 0 2.117.27 3 .743V10a6.344 6.344 0 0 0-3-.743c-1.093 0-2.617.27-3.5.743V.743C.883.27 2.407 0 3.5 0Z' fill='%23BAE6FD'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 0c1.093 0 2.617.27 3.5.743V10c-.883-.473-2.407-.743-3.5-.743s-2.117.27-3 .743V.743a6.344 6.344 0 0 1 3-.743Z' fill='%2338BDF8'/%3E%3C/svg%3E");
}
.DocSearch-Hit-icon svg {
display: none;
}
.DocSearch-Hit-content-wrapper {
flex: auto;
display: flex;
flex-direction: column-reverse;
min-width: 0;
z-index: 1;
}
.DocSearch-Hit-path {
align-self: flex-start;
font-size: 0.75rem;
line-height: 1.5rem;
font-weight: 400;
color: theme('colors.scale.1100');
border-radius: 999px;
padding: 0 0.75rem;
@apply bg-scale-300 dark:bg-scale-500;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-icon {
// @apply bg-transparent shadow-none ring-1 ring-white/30 dark:ring-white/20;
// background-image: url("data:image/svg+xml,%3Csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M3.75 1v10M8.25 1v10M1 3.75h10M1 8.25h10' stroke='%23fff' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit--Parent .DocSearch-Hit-icon {
/* book icon */
// background-image: url("data:image/svg+xml,%3Csvg width='14' height='10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M3.5 0c1.093 0 2.117.27 3 .743V10a6.344 6.344 0 0 0-3-.743c-1.093 0-2.617.27-3.5.743V.743C.883.27 2.407 0 3.5 0Z' fill='%23fff'/%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M10.5 0c1.093 0 2.617.27 3.5.743V10c-.883-.473-2.407-.743-3.5-.743s-2.117.27-3 .743V.743a6.344 6.344 0 0 1 3-.743Z' fill='%23fff'/%3E%3C/svg%3E");
}
.DocSearch-Hit-title {
@apply text-scale-1100 dark:text-scale-1100 truncate leading-6;
}
.DocSearch-Hit-title + .DocSearch-Hit-path {
@apply mb-1;
}
.DocSearch-Hit-action {
@apply ml-3.5 flex-none;
}
.DocSearch-Hit--Result {
.DocSearch-Hit-action svg {
@apply hidden;
}
.DocSearch-Hit-action {
// @apply h-6 w-6;
// background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
[aria-selected='true'] & .DocSearch-Hit-action {
// background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit[aria-selected='true'] & .DocSearch-Hit-path {
// @apply bg-scale-400 dark:bg-scale-400;
}
.DocSearch-Hit[aria-selected='true'] & .DocSearch-Hit-title {
// @apply text-white;
}
}
.DocSearch-Hit-action-button {
@apply flex;
}
.DocSearch-Hit-action + .DocSearch-Hit-action {
@apply ml-2;
}
.DocSearch-Hit-action path {
stroke-width: 2px;
stroke: #71717a;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit-action path {
stroke: #777;
.dark & {
stroke: #aaa;
}
}
.DocSearch-Hit > a {
@apply block py-4 px-6;
}
.DocSearch-Hit--Result {
@apply py-3 px-4 #{!important};
}
.DocSearch-Hit {
@apply relative;
}
.DocSearch-Hit:first-child > a {
// @apply border-t border-slate-100 dark:border-slate-200;
}
.DocSearch-Hit--Result {
// @apply border-t-0 #{!important};
}
.DocSearch-Hit + .DocSearch-Hit .DocSearch-Hit--Result {
@apply mt-2;
}
.DocSearch-Hit--Result {
@apply dark:bg-scale-300 border-scale-400 rounded-lg border bg-white transition-all;
}
.DocSearch-Hit[aria-selected='true'] > a {
@apply bg-slate-50 dark:bg-slate-700;
}
.DocSearch-Hit[aria-selected='true'] .DocSearch-Hit--Result {
@apply bg-scale-200 dark:bg-scale-600;
}
.DocSearch-Hit--FirstChild::before {
@apply top-0 #{!important};
}
.DocSearch-Hit--LastChild::before {
@apply bottom-0 #{!important};
}
.DocSearch-Hits mark {
background: none;
@apply text-scale-1200 border-scale-900 border-b-2 font-semibold;
}
.DocSearch-Hit-path mark {
@apply border-0;
}
.DocSearch-Hit[aria-selected='true'] mark {
@apply text-scale-1200;
border-bottom-color: currentColor;
}
.DocSearch-Footer {
@apply flex flex-none justify-end border-t border-slate-100 py-4 px-6 dark:border-slate-200;
}
.DocSearch-Commands {
@apply hidden;
}
.DocSearch-Logo a {
@apply text-scale-900 flex items-center text-xs font-medium;
}
.DocSearch-Logo svg {
@apply text-scale-900 ml-3 text-[#5468ff];
}
.DocSearch-Hit--deleting,
.DocSearch-Hit--favoriting {
opacity: 0;
transition: all 250ms linear;
}
.DocSearch-NoResults .DocSearch-Screen-Icon {
@apply hidden;
}
.DocSearch-Title {
@apply mb-10 text-lg leading-6;
}
.DocSearch-Title strong {
@apply text-slate-1200 dark:text-scale-1200 font-normal;
}
.DocSearch-NoResults-Prefill-List {
.DocSearch-Help {
color: var(--colors-scale11) !important;
}
}
.DocSearch-StartScreen {
.DocSearch-Help {
@apply text-center py-16;
}
}
.DocSearch-Help {
@apply text-scale-1100;
font-size: 0.875rem !important;
a {
color: var(--colors-brand9) !important;
font-weight: 400 !important;
}
}
.DocSearch-NoResults {
@apply px-4 pt-10 pb-8;
}
.DocSearch-NoResults .DocSearch-Title {
text-align: center;
color: theme('colors.scale.1100');
}
.DocSearch-NoResults-Prefill-List .DocSearch-Help {
@apply text-scale-900 dark:text-scale-200 mb-3 text-sm leading-6;
}
.DocSearch-NoResults-Prefill-List ul {
@apply border-scale-300 dark:border-scale-400 dark:bg-scale-300 overflow-hidden rounded-lg border bg-white;
}
.DocSearch-NoResults-Prefill-List button {
padding: 0.5rem 0.75rem;
display: block;
width: 100%;
text-align: left;
font-size: theme('fontSize.sm');
line-height: theme('lineHeight.6');
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m11 9 3 3-3 3' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 0.75rem center;
@apply text-scale-900 hover:text-slate-1200 dark:bg-scale-300 hover:bg-scale-200 dark:hover:bg-scale-400 bg-white;
}
.DocSearch-NoResults-Prefill-List li + li button {
@apply border-scale-300 border-t dark:border-slate-400;
}
.DocSearch-NoResults-Prefill-List + .DocSearch-Help {
@apply mt-3 text-xs leading-6;
}
.DocSearch-NoResults-Prefill-List + .DocSearch-Help a {
@apply text-scale-900 dark:text-scale-900 font-semibold;
}
.DocSearch-Hit-action [data-cy] {
width: 1.5rem;
height: 1.5rem;
}
.DocSearch-Hit-action [data-cy] svg {
display: none;
}
.DocSearch-Hit-action [data-cy='fav-recent'] {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%23CBD5E1' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action [data-cy='fav-recent']:hover {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' stroke='%230ea5e9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
.DocSearch-Hit-action [data-cy='remove-recent'] {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action [data-cy='remove-recent']:hover {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action [data-cy='remove-fav']::before {
@apply pointer-events-none absolute inset-0 bg-sky-50 content-[''] dark:bg-slate-700;
}
.DocSearch-Hit[aria-selected='true'] [data-cy='remove-fav']::before {
@apply bg-sky-100 dark:bg-slate-700;
}
.DocSearch-Hit-action [data-cy='remove-fav']::after {
content: '';
position: absolute;
pointer-events: none;
width: calc(4.5rem + 1px);
height: 1.5rem;
margin-left: calc(-3rem - 1px);
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394A3B8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat, repeat-y, no-repeat;
background-position: left, center, right;
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' opacity='0.05' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2364748b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action [data-cy='remove-fav']:hover::after {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
.dark & {
background-image: url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m12 5 2 5h5l-4 4 2 5-5-3-5 3 2-5-4-4h5l2-5Z' fill='%230EA5E9' stroke='%230EA5E9' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='1' height='1' fill='%23e2e8f0' opacity='0.05' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='1' height='1'/%3E%3C/svg%3E"),
url("data:image/svg+xml,%3Csvg width='24' height='24' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M17 7 7 17M7 7l10 10' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}
}
.DocSearch-Hit-action + .DocSearch-Hit-action {
// @apply ml-3 border-l border-slate-200 pl-3 dark:border-slate-200;
}
+97
View File
@@ -0,0 +1,97 @@
---
title: Epsilon3 digitize paper-based procedures in the space industry using telemetry data, simplifying testing and operations.
name: Epsilon3
description: Epsilon3 uses Supabase to help teams execute secure and reliable operations in an industry where project spend runs into the billions.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/epsilon3.png
logo_inverse: /images/customers/logos/light/epsilon3.png
og_image: /images/customers/og/epsilon3.jpg
tags:
- supabase
date: '2023-02-14'
company_url: 'https://epsilon3.io'
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: Los Angeles, California }]
about: Epsilon3 is the OS for spacecraft and complex operations. Epsilon3’s software platform manages complex operational procedures, saving operators time and reducing errors.
---
Epsilon3 is a USA-based Y Combinator startup. They digitize paper-based procedures in the space industry using telemetry data,
simplifying testing and operations. Their product is rapidly becoming the OS for space projects and complex operations.
## Space Operations
Epsilon3's customers run testing and operational procedures in real-time. Epsilon3's solution allows online collaboration as a team,
instead of being siloed and separated which is an increasingly important requirement in a post-pandemic working world.
Epsilon3 is powering an industry where reliability is paramount. In the space industry, missions cost billions and need to run reliably.
![Epsilon3 digitizes paper-based processes.](/images/blog/epsilon3/epsilon3-product.gif)
Epsilon3's software saves operators time and reduces errors to improve complex operations. Their customers are using their software and
successfully sending satellites into space and are about to use Epsilon3's software for an operational Rocket Launch. As a result, their
reputation for improving the quality standard for complex operations has led to demand from other industries. The team is receiving
enquiries from clients in the robotics, logistics and medical equipment sectors who need a structured and standardized way of running
procedures and keeping audit trails.
## Moving fast in regulated markets
As a startup, Epsilon3 needs to move fast and scale. They require security, reliability, and performance.
The team has a background in engineering from Northrop, Google, and SpaceX and understands the technical benefits Postgres offers as a
battle-tested open-source relational database.
Real-time functionality is also a key for their customers to see how running procedures are progressing as the changes occur.
The space industry is highly regulated, adding complexity. For example, in the USA, deployments must be in an ITAR-compliant way
(for instance AWS GovCloud supports ITAR compliance) while customers in countries outside the USA may need to host their data within their own
country. These customer requirements mean Epsilon3 must be able to offer flexible hosting options, including on-premises deployment.
<Quote
img="aaron-epsilon3.png"
caption="Aaron Sullivan, ex-Google engineer, Stanford University Alumni, Principal Software Engineer Epsilon3."
>
<p>Billion dollar missions need to run reliably and securely.</p>
<p>
We didn’t just want something that works, we wanted a solution implementing best practices. We
also needed a setup that we could self-host to meet compliance needs and allow on-premises
deployment options for our customers.
</p>
<p>
We use Supabase because they give us an open-source scalable back-end built by database experts
that we can self-host.
</p>
<p>
Supabase takes out the mental effort from our back-end infrastructure so we can focus on our
customers needs.
</p>
</Quote>
## Supabase gives Postgres users superpowers
The team implemented a self-hosted Supabase setup. They use AWS with a self-hosted Supabase API powering an RDS Postgres instance.
This setup gives a balance of rapid development, reduced DevOps, and flexible hosting options. As Supabase uniquely offers [real-time](https://supabase.com/realtime) for
Postgres, it allows their customers to see updates from running procedures. Supabase offers well-supported client libraries that make
Postgres Row Level Security easy to use, which is a critical item on the Epsilon3 roadmap.
All this was easy for the team to set up, even with self-hosting, so the team was able to focus on what they do best&mdash;helping their customers
run complex operations smoothly. With this implementation, Epsilon3 gets all the benefits of Postgres, plus the smooth developer experience and
support that Supabase is known for, without compromising their compliance needs.
![Epsilon3 uses Supabase for hosting their critical infrastructure.](/images/blog/epsilon3/supabase-epsilon3-architecture.png)
## Self-hosted Supabase, a superpower
Self-hosting Supabase allows the crew at Epsilon3 to move fast. They are revolutionizing space operations securely and reliably in real-time whilst
staying compliant. The team is now seeing demand from other industries and is confident in their scaling capability because they know it's
just Postgres under the hood.
Check out the [Epsilon3 website](https://www.epsilon3.io/) to learn more about the team and their OS for space operations. If you are interested
in self-hosting Supabase then you can **[contact us](https://forms.supabase.com/enterprise)** to learn more.
+72
View File
@@ -0,0 +1,72 @@
---
title: HappyTeams unlocks better performance and reduces cost with Supabase.
name: HappyTeams
description: How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/happyteams.png
logo_inverse: /images/customers/logos/light/happyteams.png
og_image: /images/customers/og/happyteams.jpg
tags:
- supabase
date: '2023-02-16'
company_url: 'https://happyteams.io/'
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: 'Dublin, Ireland' }]
about: HappyTeams is a purpose-built solution that empowers management to unlock valuable team insights.
---
## The Challenge
> Finding a painless and hands-free database experience to support growth
[HappyTeams](https://happyteams.io/) is a growing startup with an ambitious vision. The company is bootstrapped, so technical resources are scarce. Devs need to do more with less. This means staying laser-focused on delivering features, rather than database timeouts or performance issues.
The company launched its app on Heroku Postgres. But one year in, they were experiencing scaling issues. They needed to handle thousands of users while also shipping new features and building out integrations. Issues with database slowness and concurrent connections were holding them back.
Michael Dever, the founder and CTO of HappyTeams, realized it was time for a change. A large overhaul of the app in early 2022 presented the opportunity to do so: "When you're in that first sprint of building a product, you're kind of locked with that first database decision you make. When we did a rebuild, it was time to look at something like Supabase."
![HappyTeams Home Page](/images/blog/2023-02-16-case-study-happyteams/happyteams-home-page.png)
## Why Supabase?
There were four main factors that led HappyTeams to switch from Heroku Postgres to Supabase:
1. **Better performance**. The team was experiencing uneven performance with Heroku, which was sluggish at times. Supabase was consistently fast.
2. **Enabling a large number of direct connections and connection pooling with PgBouncer**. HappyTeams was constantly hitting limits on the number of database connections. They could solve this problem with Supabase's [flexible plans](https://supabase.com/pricing) and [support for PgBouncer](https://supabase.com/blog/supabase-pgbouncer).
3. **Quality of support, documentation, and onboarding**: While Heroku provided no support at the tier they were on, the Supabase team was readily available: "Responses were powerful and they answered all my questions, even on a Sunday. That just further sold the product for me." He was also impressed with the [documentation](https://supabase.com/docs) on the Supabase website and the onboarding process.
4. **Lower costs**. Moving to Supabase reduced HappyTeams's costs: The base plan comes with everything they need to scale the product.
<Quote img="michael-dever-happyteams.jpeg" caption="Michael Dever - CTO of HappyTeams">
Great documentation, great product, great pricing. It sells itself, really.
</Quote>
## Supabase provides a fast and reliable production database
After a 30 minute migration, HappyTeams can iterate quickly, add new features, and improve their product based on customer feedback
Once they made the decision, migrating from Heroku to Supabase was simple. HappyTeams completed their database migration in less than an hour using pg_dump and pg_restore.
Performance is no longer an issue. "Supabase just worked", says Michael. Pooling connections using PgBouncer was effortless - solving a recurring pain point for HappyTeams.
"Supabase is so fast. Way, way, way faster than our Heroku Postgres database, which I think was located in the same region."
Today, Supabase is an integral part of HappyTeams's tech stack. It has allowed the company to move fast and rapidly release new features - with minimal tech resources devoted to their backend infrastructure.
Thanks to Supabase's ease of use and reliability, HappyTeams can iterate quickly based on feature requests and deliver more value to their user base. They are planning to expand their usage, exploring functionality such as [Edge Functions](https://supabase.com/edge-functions) and the [API client](https://supabase.com/docs/guides/api).
<Quote img="michael-dever-happyteams.jpeg" caption="Michael Dever - CTO of HappyTeams">
When we needed to add a comments feature to our app, we could get it done in just two days.
Supabase is really like a dream come true for a developer.
</Quote>
+97
View File
@@ -0,0 +1,97 @@
---
title: How Mobbin migrated 200,000 users from Firebase for a better authentication experience.
name: Mobbin
description: Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/mobbin.png
logo_inverse: /images/customers/logos/light/mobbin.png
og_image: /images/customers/og/mobbin.jpg
tags:
- supabase
date: '2023-02-14'
company_url: 'https://mobbin.design'
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: Singapore }]
about: Mobbin is a comprehensive, expert-curated library of interfaces from the best-in-class digital products that helps product designers find relevant design inspiration and references with significantly less time and effort.
---
Mobbin helps over 200,000 creators globally search and view the latest design patterns from well-known apps.
Their platform allows creators to search screenshots of mobile apps to inspire product development.
Creators use Mobbin to combine inspiration and develop new experiences.
## From Idea to 30,000 users in one Product Hunt launch
The initial concept for Mobbin was just a Dropbox full of screenshots. After sharing with their early users,
the team tried organizing them into folders but this quickly became unscalable. Clear interest from users encouraged them to build a full website. They used Firebase for the backend and launched on
[Product Hunt](https://www.producthunt.com/posts/mobbin-1).
Mobbin went viral, and within the first week, they had 30,000 users. They have now more than 400,000
registered users through word of mouth. Their curation continues to attract designers and creators globally.
![Mobbin cover](/images/blog/mobbin/mobbin-cover.png)
## Scaling issues put Mobbin growth at risk
Firebase helped the team launch quickly, but at scale, they found Firebase lacked the functionality required for a high-quality user experience.
The first problem was duplicate user logins. Firebase Authentication doesn't automatically merge users with the same email address.
For example, users cannot log in with Google and Facebook if they share the same email address.
Users struggled to upgrade to a paid account because they couldn't remember which authentication provider they signed up with.
Next, they had issues with data integrity. While Firebase has 99.95% uptime, application code can (and does) fail.
Mobbin didn't want to implement idempotence for every mutative Firebase Function. Between a failed Function execution and a retry,
a document state might change without strict validation. The team could never be confident their data had the level of integrity they required.
Finally, the team grew concerned with their increasing Firebase bill. Because of the document-based data structure,
their API requests required several round trips. At one point, they were making 5 million API requests per month.
These round trips made their website slow and led to a notable time investment hacking together a fix.
![Mobbing screenshots](/images/blog/mobbin/mobbin-screenshots.jpg)
## Migrating to Supabase
Mobbin's initial product gave them a comprehensive understanding of customer requirements. For their second version,
they needed a better authentication experience, fairer pricing, and a relational database to deliver a quality
application sustainable for their business model.
The team found Supabase and realized it covered all the bases. Not only that, their team had experience with the
tools open source tools that Supabase is built with.
[Authentication](https://supabase.com/auth), built using [GoTrue](https://github.com/supabase/gotrue), instantly solved their users' login issues.
Under the hood, Supabase is just Postgres, and Jian Jie (co-founder and CTO of Mobbin) knows this is one of the best battle-tested
open source relational databases.
They decided that moving to Supabase was a no-brainer. The process itself was smooth - the real challenge was validating their
data integrity due to historical failures on their serverless functions.
Mobbin is now successfully running on top of Supabase, with superior performance compared to their old Firebase setup.
Because Supabase does not charge based on API requests, Mobbin significantly reduced their monthly spending without maintaining a custom backend.
<Quote img="jian-mobbin.jpg" caption="Jian Jie Liau, Co-founder and CTO at Mobbin.">
<p>Migrating to Supabase meant that we could instantly fix our Auth problems and save money.</p>
<p>
Just being on Supabase alone gives us confidence we can deliver on whatever users need in the
future.
</p>
<p>
The cool thing is now we know under the hood it's just Postgres, it really does feel like
anything is possible for the future of our product
</p>
</Quote>
## Supabase saves Mobbin time and money
Migrating to Supabase helped the team at Mobbin instantly improve the end-user experience and save costs.
They now add new features with confidence, and they continue to help creators all over the globe find inspiration for their next project.
You can [check out Mobbin on their website](https://mobbin.design/browse/ios/apps).
[Sign up](https://app.supabase.com/) for Supabase's free tier and set up a scalable backend in less than 2 minutes.
+87
View File
@@ -0,0 +1,87 @@
---
title: Replenysh uses Supabase to implement OTP in less than 24 hours.
name: Replenysh
description: With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/replenysh.png
logo_inverse: /images/customers/logos/light/replenysh.png
og_image: /images/customers/og/replenysh.jpg
tags:
- supabase
date: '2023-02-14'
company_url: https://replenysh.com/
stats:
[
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: Southern California }]
about: Replenysh is a digital platform that connects global brands with local communities to recover materials and eliminate the concept of waste.
---
Replenysh connects brands who want to recover their products and packaging with communities looking to monetize used materials.
Learn how Replenysh uses Supabase to power the circular economy, redefining how brands interact with their customers & products.
## Material recovery offers brands a new way to connect to their customers
At the end of a linear economy, products usually end up in landfills or the ocean. Every year, 8 million metric tons of plastics enter our ocean. This is on top of the estimated 150 million metric tons that currently circulate our marine environments. In the USA alone, every year, 139 million tons of waste is sent to landfills.
The problem is our current waste and recycling infrastructure can't support that demand. With less than 10% of material actually getting recycled, the vast majority just ends up in a landfill or as pollution.
Replenysh is building a solution to this problem by creating entirely new infrastructure to help brands get their materials back. They're building an ecosystem of tools to enable any community in the world to do the right thing.
Whenever material is dropped off at a host, brands have the opportunity to engage with that person, resulting in a win-win situation for the planet, the brands, and the individual who all want to make a positive impact. Anyone in the community can take their used products and materials to the hosts — with that material being tracked all the way back to the brands and manufacturers who will reuse it. Brands have the opportunity to engage with that person, resulting in a win-win situation for the planet, the brands, and the individual who all want to make a positive impact. This turns a traditional consumption model into an opportunity for brands and customers to participate in the circular economy.
Brands are adopting Replenysh to demonstrate their commitment to making a positive environmental impact, while seeking to engage with consumers in a new, authentic way. Replenysh provides the technology to communities that are helping build the future infrastructure where all materials are traceable and reused.
![Linear vs Circular economy](/images/blog/replenysh/circular-economy.png)
## Refactoring code leads to a better way forward
Historically the team created their own backend infrastructure, setting up APIs and Authentication to run their own OTP system with Twilio and passwords. Postgres is essential to their requirements because Replenysh use Row Level Security for authorization. Before adopting Supabase, the team used a Haskell server with a different Postgres instance deployed on Heroku.
The team decided to do a large refactor of their codebase and began investigating alternative hosting options to help speed up development time and reduce maintenance.
Firebase seemed appealing due to its ability to get started quickly, but the team didn't want to migrate away from a relational database. Specifically, Postgres and RLS fit their requirements.
When the team discovered Supabase on Hacker News they felt excitement. Supabase has a reputation for a smooth developer experience, providing Postgres and Row Level Security as a core feature.
As a bonus, Supabase provided more than just a database, reducing their DevOps overhead. After reading the [Supabase Auth documentation](https://supabase.com/docs/guides/auth/overview) the team determined they can have the functionality they need, like SMS OTP. When they saw [Mobbin are using Supabase at scale](https://supabase.com/case-studies-mobbin), they decided to migrate to Supabase.
## Globally recognized brands need quality at scale
Less than a day after beginning development with Supabase, the team completed their SMS OTP implementation, and deployed their app to the app store a few weeks later.
![Replenysh-Mobile-App](/images/blog/replenysh/Replenysh-Mobile-App.png)
Replenysh clients include brands with global recognition and millions of customers. With their reputation at stake, major brands have a high-quality threshold. If a global brand asks its customers to adopt a technology, the user experience must be high quality and reliable. Their customers' primary interaction point is mobile-first. This means a robust and smooth mobile Auth experience is non-negotiable. After switching to Supabase, when they demo, clients are instantly impressed with just how easy it is for users to log in with an SMS OTP and start participating in the circular economy. As a result, some of the world's largest retailers and beverage companies have recently joined Replenysh and the circular economy.
![Replenysh-large-screen-view](/images/blog/replenysh/Replenysh-large-screen-view.png)
## Supabase means faster development and reduced maintenance
<Quote img="clark-dinnison.jpeg" caption="Clark Dinnison, Head of Product at Replenysh.">
<p>
We saw the Auth update for launch week we thought it was the perfect time to get going with
Supabase.
</p>
<p>
We implemented & pushed live phone (OTP) auth and had something ready to deploy to the app store
in less than 24-hours!
</p>
<p>
Supabase's developer experience lived up to its reputation. Our app is ready to launch with
major brands to help them reach sustainability goals and connect with their customers in a
meaningful way
</p>
</Quote>
## Supabase help Replenysh continue to scale
Supabase turbo-charged Replenysh's development time with a seamless end-user mobile login experience. They are ready to help major brands engage with their users through participation in the circular economy. With Supabase, the team knows they have a slick auth experience, reduced DevOps overhead, and can continue to scale with Postgres. You can test out the Supabase developer experience today by [starting a new project on the free tier!.](https://app.supabase.com)
+43
View File
@@ -0,0 +1,43 @@
---
title: Xendit use Supabase and create a full solution shipped to production in less than one week.
name: Xendit
description: As a payment processor, Xendit are responsible for verifying that all transactions are legal.
author: rory_wilding
author_title: Supabase
author_url: https://github.com/kiwicopple
author_image_url: https://avatars2.githubusercontent.com/u/10214025?s=400&u=c6775be2ae667e2acae3ccd347fed62bb3f5b3e7&v=4
authorURL: https://github.com/kiwicopple
logo: /images/customers/logos/xendit.png
logo_inverse: /images/customers/logos/light/xendit.png
og_image: /images/customers/og/xendit.jpg
company_url: https://xendit.co
tags:
- supabase
date: '2023-02-14'
stats:
[
{ stat: '120', label: Staff count },
{ stat: '00,000', label: Example stat },
{ stat: '00,000', label: Example stat },
]
misc: [{ label: 'Founded', text: Jakarta Selatan }]
about: Xendit is a financial technology company that provides payment solutions and simplifies the payment process for businesses in Indonesia, the Philippines, and Southeast Asia, from SMEs and e-commerce startups to large enterprises.
---
## Challenge
As a payment processor, Xendit are responsible for verifying that all transactions are legal. Any transactions which are suspicions must be verified against a strict set of criteria, and the parties involved need to be checked against international sanctions lists. This is a critical anti-money-laundering operation and needs to be performed in realtime to prevent any delays on legitimate payments.
## Why they chose Supabase
Xendit needed something fast. Something that was cheaper than using the global players like Worldcheck or Refinitiv. Xendit already uses Postgres for a lot of their critical infrastructure, and so Xendit team are familiar with the technology and comfortable in it's ability to scale.
## What they built
Xendit parses international sanctions lists from the UN and the Indonesian government and loads them into Supabase. Since Supabase provides a full Postgres server, they can then use the [Trigram](https://www.postgresql.org/docs/current/pgtrgm.html) extension to perform full-text search on the lists, with a relevance score on every search.
Supabase was perfect for their use case, as they needed something built fast. The full solution was built and in production in less than one week.
> The full solution was built and in production in less than one week.
Xendit created a database function for searching, which they are able to call directly using their Python clients. They have plans to iterate on the current implementation using more advanced techniques, like machine learning, but for now the Supabase system has been in Production for 9 months without a problem.
@@ -16,18 +16,22 @@ const BuiltExamples = () => {
<p className="p">There are many example apps and starter projects to get going</p>
<div className="flex justify-center gap-2 py-4">
<Link href="/docs/guides/examples" as="/docs/guides/examples" passHref>
<Button as="a" type="default" size="small">
View all examples
</Button>
<a>
<Button type="default" size="small">
View all examples
</Button>
</a>
</Link>
<Link
href="https://github.com/supabase/supabase/tree/master/examples"
as="https://github.com/supabase/supabase/tree/master/examples"
passHref
>
<Button as="a" type="default" icon={<IconGitHub />} size="small">
Official github library
</Button>
<a>
<Button type="default" icon={<IconGitHub />} size="small">
Official github library
</Button>
</a>
</Link>
</div>
</div>
@@ -126,9 +126,7 @@ function SplitCodeBlockCarousel(props: SplitCodeBlockCarousel) {
{extension.url && (
<Link href={extension.url} as={extension.url}>
<a className="ml-px">
<Button as="a" type="default">
View documentation
</Button>
<Button type="default">View documentation</Button>
</a>
</Link>
)}
@@ -0,0 +1,54 @@
import SectionHeader from 'components/UI/SectionHeader'
import PostTypes from '~/types/post'
import SectionContainer from '../Layouts/SectionContainer'
import Link from 'next/link'
import { GlassPanel } from 'ui'
import TextLink from '../TextLink'
const CustomerStories = ({ customerStories }: any) => {
const caseStudyThumbs = customerStories.map((blog: PostTypes, idx: number) => {
return {
logo: blog.logo,
logoInverse: blog.logo_inverse,
title: blog.title,
link: blog.url,
}
})
return (
<SectionContainer>
<div className="mb-12">
<SectionHeader
title={'Infrastructure'}
title_alt={' to innovate and scale with ease.'}
subtitle={'Customer Stories'}
paragraph={
'See how Supabase empowers companies of all sizes to accelerate their growth and streamline their work.'
}
/>
</div>
<div className="mx-auto mt-5 grid grid-cols-12 gap-8 lg:gap-12">
{caseStudyThumbs.map((caseStudy: any, i: number) => (
<Link href={`${caseStudy.link}`} key={caseStudy.title} passHref>
<a className="col-span-12 md:col-span-4">
<GlassPanel
{...caseStudy}
background={true}
showIconBg={true}
showLink={true}
hasLightIcon
>
{caseStudy.description}
</GlassPanel>
</a>
</Link>
))}
</div>
<div className="mt-12">
<TextLink url="/customers" label="Explore more" />
</div>
</SectionContainer>
)
}
export default CustomerStories
+1 -9
View File
@@ -4,14 +4,6 @@ import { IconSun, IconMoon } from 'ui'
function DarkModeToggle() {
const { isDarkMode, toggleTheme } = useTheme()
const toggleDarkMode = () => {
localStorage.setItem('supabaseDarkMode', (!isDarkMode).toString())
toggleTheme()
const key = localStorage.getItem('supabaseDarkMode')
document.documentElement.className = key === 'true' ? 'dark' : ''
}
return (
<div className="flex items-center">
<IconSun className="text-scale-900" strokeWidth={2} />
@@ -24,7 +16,7 @@ function DarkModeToggle() {
isDarkMode ? 'bg-scale-500' : 'bg-scale-900'
} mx-5
`}
onClick={() => toggleDarkMode()}
onClick={() => toggleTheme()}
>
<span className="sr-only">Toggle Themes</span>
<span
+10 -1
View File
@@ -7,9 +7,12 @@ import { Badge } from 'ui'
import Image from 'next/image'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-wordmark--light.png'
import { useRouter } from 'next/router'
const Footer = () => {
const { isDarkMode } = useTheme()
const { pathname } = useRouter()
const isLaunchWeekPage = pathname.includes('launch-week')
return (
<footer
@@ -25,7 +28,13 @@ const Footer = () => {
<Link href="#" as="/">
<a className="w-40">
<Image
src={isDarkMode ? supabaseLogoWordmarkDark : supabaseLogoWordmarkLight}
src={
isLaunchWeekPage
? supabaseLogoWordmarkDark
: isDarkMode
? supabaseLogoWordmarkDark
: supabaseLogoWordmarkLight
}
width={160}
height={30}
alt="Supabase"
+10 -6
View File
@@ -40,14 +40,18 @@ const Hero = () => {
</div>
<div className="flex items-center gap-2">
<Link href="https://app.supabase.com" as="https://app.supabase.com" passHref>
<Button as="a" size="small" className="text-white">
Start your project
</Button>
<a>
<Button size="small" className="text-white">
Start your project
</Button>
</a>
</Link>
<Link href="/docs" as="/docs" passHref>
<Button as="a" size="small" type="default" icon={<IconBookOpen />}>
Documentation
</Button>
<a>
<Button size="small" type="default" icon={<IconBookOpen />}>
Documentation
</Button>
</a>
</Link>
</div>
<div className="flex flex-col gap-4">
+20 -5
View File
@@ -1,7 +1,7 @@
import { useRouter } from 'next/router'
import Image from 'next/image'
import Link from 'next/link'
import { motion } from 'framer-motion'
interface iImages {
name: string
image: string
@@ -19,6 +19,7 @@ interface iImageGrid {
lgCols?: colSizes
padding?: paddingSizes
className?: string
animated?: boolean
}
const ImageGrid = ({
@@ -28,6 +29,7 @@ const ImageGrid = ({
lgCols = 8,
padding = 8,
className,
animated = false,
}: iImageGrid) => {
const smBreakpoint = {
3: 'grid-cols-3',
@@ -70,7 +72,7 @@ const ImageGrid = ({
if (x.link) {
return (
<Link href={link}>
<div className="scale-100 transform cursor-pointer duration-100 ease-in-out hover:z-50 hover:scale-105 hover:shadow-sm">
<div className="scale-100 transform cursor-pointer duration-100 ease-in-out hover:shadow-sm">
{children}
</div>
</Link>
@@ -80,9 +82,22 @@ const ImageGrid = ({
}
}
const MaybeAnimatedDiv = ({ children, ...rest }: any) =>
animated ? (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0, transition: { delay: i / 10 } }}
{...rest}
>
{children}
</motion.div>
) : (
<div {...rest}>{children}</div>
)
return (
<Container link={x.link} key={i}>
<div
<MaybeAnimatedDiv
key={`${x.name}-${i}`}
className={`
dark:bg-scale-400 col-span-1 flex items-center
@@ -106,7 +121,7 @@ const ImageGrid = ({
"
/>
</div>
</div>
</MaybeAnimatedDiv>
</Container>
)
})}
@@ -0,0 +1,45 @@
import { motion, useScroll, useTransform } from 'framer-motion'
import Image from 'next/image'
import { useMobileViewport } from '../../hooks/useMobileViewport'
import styles from './lw7-bg-graphic.module.css'
export default function LW7BgGraphic() {
const { scrollYProgress } = useScroll()
const isMobile = useMobileViewport(768)
const graphicsY = useTransform(
scrollYProgress,
// Map scrollYProgress from these values:
[0, 0.5],
// Into these values:
[0, isMobile ? 70 : 200]
)
return (
<div
className={[
"relative h-[300px] md:h-[700px] overflow-hidden before:content[' '] before:absolute before:bottom-0 before:h-[200px] md:before:h-[600px] before:z-20 before:w-full before:bg-gradient-to-t before:from-[#1C1C1C] before:via-[#1C1C1C40] before:to-transparent",
styles['gradient-overlay'],
].join(' ')}
>
<motion.div
className="absolute bottom-0 w-full h-full"
style={{
y: graphicsY,
willChange: 'transform',
}}
>
<Image
src="/images/launchweek/seven/lw-7-bg-blur.png"
loading="eager"
layout="fill"
objectFit="cover"
objectPosition="top"
quality={100}
unoptimized
priority
/>
</motion.div>
</div>
)
}
@@ -0,0 +1,14 @@
import React from 'react'
interface Props {
text: string
gradientText?: boolean
}
export default function LabelBadge({ text, gradientText = false }: Props) {
return (
<span className="bg-[#32313F] rounded px-1 py-[2px] text-xs">
<span className={!gradientText ? 'text-[#D87BF5]' : 'gradient-text-purple-500'}>{text}</span>
</span>
)
}
@@ -0,0 +1,33 @@
import classNames from 'classnames'
import Image from 'next/image'
import { motion } from 'framer-motion'
import { useMobileViewport } from '../../../hooks/useMobileViewport'
export function LaunchWeekLogoHeader() {
const isMobile = useMobileViewport(640)
return (
<div className="flex flex-col gap-1 md:gap-2 items-center my-4 lg:pt-8 justify-start">
<motion.div
className={classNames('flex flex-col justify-center gap-3')}
initial={{ y: -10, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ duration: 0.4, ease: [0.24, 0.25, 0.05, 1], delay: 0.2 }}
>
<h1 className="flex gap-4 justify-center font-normal uppercase text-[28px] sm:text-[32px] items-center">
<span className="tracking-[4px] text-white">Launch week</span>
<span className="flex justify-center">
<Image
src="/images/launchweek/seven/lw7-seven.svg"
width={isMobile ? 36 : 40}
height={isMobile ? 36 : 40}
/>
</span>
</h1>
<p className="text-white radial-gradient-text-scale-600 text-md sm:text-lg text-center">
April 10th – 14th at 7AM PT | 10AM ET
</p>
</motion.div>
</div>
)
}
@@ -0,0 +1,68 @@
import Image from 'next/image'
import React from 'react'
import { motion } from 'framer-motion'
type animateFromType = 'left' | 'right' | 'up' | 'down' | 'opacityOnly'
export default function LaunchWeekPrizeCard({
imageUrl = '',
imageWrapperClassName,
content,
className,
animateFrom,
imgObjectPosition = 'center',
}: {
imageUrl: string
imageWrapperClassName?: string
content: any
className?: string
animateFrom?: animateFromType
imgObjectPosition?: string
}) {
const finalState = { x: 0, y: 0, opacity: 1 }
function getAnimationFrom(animationDirection: animateFromType) {
switch (animationDirection) {
case 'left':
return { x: -20, opacity: 0 }
case 'up':
return { y: -20, opacity: 0 }
case 'right':
return { x: 20, opacity: 0 }
case 'down':
return { y: 20, opacity: 0 }
case 'opacityOnly':
return { opacity: 0 }
}
}
return (
<motion.div
className={[
'relative p-[1px] bg-gradient-to-b from-[#484848] to-[#1C1C1C] rounded-lg overflow-hidden shadow-lg',
className && className,
].join(' ')}
initial={animateFrom ? getAnimationFrom(animateFrom) : finalState}
whileInView={finalState}
viewport={{ once: true, margin: '-50px' }}
transition={{ type: 'spring', bounce: 0, delay: 0.5 }}
>
<div className="relative h-full flex flex-col bg-[#1C1C1C] rounded-lg overflow-hidden">
<div
className={[
"relative w-full flex-grow before:content[' '] before:absolute before:inset-0 before:z-10 before:bg-gradient-to-t before:from-[#1C1C1C]",
imageWrapperClassName && imageWrapperClassName,
].join(' ')}
>
<Image
src={imageUrl}
layout="fill"
objectFit="cover"
objectPosition={imgObjectPosition}
/>
</div>
<div className="p-4">{content && content}</div>
</div>
</motion.div>
)
}
@@ -0,0 +1,122 @@
import React from 'react'
import LabelBadge from '../LabelBadge'
import LaunchWeekPrizeCard from './LaunchWeekPrizeCard'
import { motion } from 'framer-motion'
export default function LaunchWeekPrizeSection({ className }: { className?: string }) {
const finalAnimationState = { y: 0, opacity: 1 }
return (
<div id="lw-7-prizes" className={[className].join(' ')}>
<div className="">
<div className="text-center relative z-10 text-white">
<motion.div
className="max-w-[38rem] mx-auto px-4 flex flex-col items-center gap-4"
initial={{ y: -20, opacity: 0 }}
whileInView={finalAnimationState}
viewport={{ once: true, margin: '-150px' }}
transition={{ type: 'spring', bounce: 0, delay: 0.2 }}
>
<div className="w-[40px] h-[40px] rounded-sm bg-[#32313F] flex items-center justify-center">
<svg
width="21"
height="22"
viewBox="0 0 21 22"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.313 14.7478L7.5 17.5938L6.687 14.7478C6.47687 14.0126 6.0829 13.3431 5.54226 12.8025C5.00162 12.2619 4.33214 11.8679 3.597 11.6578L0.75 10.8438L3.596 10.0308C4.33114 9.82062 5.00062 9.42665 5.54126 8.88601C6.0819 8.34537 6.47587 7.67589 6.686 6.94075L7.5 4.09375L8.313 6.93975C8.52313 7.67489 8.9171 8.34437 9.45774 8.88501C9.99838 9.42565 10.6679 9.81962 11.403 10.0297L14.25 10.8438L11.404 11.6567C10.6689 11.8669 9.99938 12.2609 9.45874 12.8015C8.9181 13.3421 8.52413 14.0116 8.314 14.7467L8.313 14.7478ZM16.759 7.55875L16.5 8.59375L16.241 7.55875C16.0927 6.96534 15.786 6.42337 15.3536 5.99078C14.9212 5.55819 14.3794 5.25124 13.786 5.10275L12.75 4.84375L13.786 4.58475C14.3794 4.43626 14.9212 4.12931 15.3536 3.69672C15.786 3.26413 16.0927 2.72216 16.241 2.12875L16.5 1.09375L16.759 2.12875C16.9073 2.72229 17.2142 3.26434 17.6468 3.69694C18.0794 4.12954 18.6215 4.43642 19.215 4.58475L20.25 4.84375L19.215 5.10275C18.6215 5.25108 18.0794 5.55796 17.6468 5.99056C17.2142 6.42316 16.9073 6.96522 16.759 7.55875ZM15.394 19.4107L15 20.5938L14.606 19.4107C14.4955 19.0794 14.3094 18.7783 14.0625 18.5313C13.8155 18.2843 13.5144 18.0982 13.183 17.9877L12 17.5938L13.183 17.1998C13.5144 17.0893 13.8155 16.9032 14.0625 16.6562C14.3094 16.4092 14.4955 16.1081 14.606 15.7768L15 14.5938L15.394 15.7768C15.5045 16.1081 15.6906 16.4092 15.9375 16.6562C16.1845 16.9032 16.4856 17.0893 16.817 17.1998L18 17.5938L16.817 17.9877C16.4856 18.0982 16.1845 18.2843 15.9375 18.5313C15.6906 18.7783 15.5045 19.0794 15.394 19.4107Z"
stroke="url(#paint0_linear_1704_106833)"
strokeLinecap="round"
strokeLinejoin="round"
/>
<defs>
<linearGradient
id="paint0_linear_1704_106833"
x1="10.5"
y1="1.09375"
x2="10.5"
y2="20.5938"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#7915FD" />
<stop offset="1" stopColor="#C667F7" />
</linearGradient>
</defs>
</svg>
</div>
<h2 className="text-4xl">
Get your <span className="gradient-text-pink-500">winning ticket</span>
</h2>
<p className="radial-gradient-text-scale-500">
Join us on April 16th for Launch Week 7's final day
<br className="hidden md:inline-block" /> and find out if you are one of the lucky
winners. Get sharing!
</p>
</motion.div>
</div>
<div className="px-4 pt-24">
<div className="grid grid-cols-1 md:grid-cols-2 col-span-2 lg:grid-cols-5 gap-4 max-w-7xl mx-auto text-white">
<LaunchWeekPrizeCard
imageUrl="/images/launchweek/seven/keyboard.jpg"
imageWrapperClassName="h-[40vw] lg:max-h-[360px]"
className="col-span-2 lg:col-span-3"
animateFrom="left"
content={
<>
<h3 className="gradient-text-purple-500 font-mono uppercase font-medium">
Main award
</h3>
<h4 className="flex items-center gap-3 text-[19px] mt-4">
Mechanical Keyboard <LabelBadge text="3 keyboards" />
</h4>
<p className="text-[#707070] mt-1">
Increase your chances of winning limited edition 62-Key ISO Custom Mechanical
Keyboard by sharing your ticket on Twitter and LinkedIn.
</p>
</>
}
/>
<div className="col-span-2 grid gap-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<LaunchWeekPrizeCard
imageUrl="/images/launchweek/seven/tshirt.jpg"
imageWrapperClassName="h-[40vw] md:h-[30vw] lg:h-auto"
imgObjectPosition="right"
content={
<h3 className="text-sm flex items-center gap-4">
Supabase T-shirt <LabelBadge text="25 shirts" />
</h3>
}
animateFrom="up"
/>
<LaunchWeekPrizeCard
imageUrl="/images/launchweek/seven/socks.jpg"
imageWrapperClassName="h-[40vw] md:h-[30vw] lg:h-auto"
content={
<h3 className="text-sm flex items-center gap-4">
Supabase Socks <LabelBadge text="15 pairs" />
</h3>
}
animateFrom="right"
/>
</div>
<LaunchWeekPrizeCard
imageUrl="/images/launchweek/seven/stickers.jpg"
imageWrapperClassName="h-[40vw] lg:h-auto"
animateFrom="down"
content={
<h3 className="text-sm flex items-center gap-4">
Supabase Sticker Pack <LabelBadge text="200 packs" />
</h3>
}
/>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,62 @@
import React, { useState } from 'react'
import TicketHeader from '../Ticket/TicketHeader'
import TicketNumber from '../Ticket/TicketNumber'
import { UserData } from '../Ticket/hooks/use-conf-data'
import Image from 'next/image'
interface Props {
user: UserData
}
export function TicketBrick({ user }: Props) {
const [isLoading, setLoading] = useState(true)
const golden = user.sharedOnLinkedIn && user.sharedOnTwitter
// reg_bg_57.png
const baseImagePath = `https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7/tickets_bg/`
const randomNumber = Math.floor(Math.random() * 200) + 1
const goldRandomNumber = Math.floor(Math.random() * 56) + 1
return (
<>
<div
key={user.name}
className="relative w-[450px] h-[230px] border border-red-100 rounded-2xl overflow-hidden"
>
<Image
src={
golden
? `${baseImagePath}/reg_bg_${randomNumber}.png`
: `${baseImagePath}/gold/gold_bg_${goldRandomNumber}.png`
}
layout="fill"
objectFit="cover"
className={[
'duration-700 ease-in-out',
isLoading ? 'grayscale blur-2xl scale-110' : 'grayscale-0 blur-0 scale-100',
].join(' ')}
onLoadingComplete={() => setLoading(false)}
/>
<div className="z-20 relative">
<TicketHeader size="small" />
<div className="rounded-full grid gap-4 mx-auto justify-center mt-8">
<img
src={`https://github.com/${user.username}.png`}
alt={user.username}
className="w-20 h-20 rounded-full mx-auto"
/>
<div className="text-center">
<h3 className="gradient-text-scale-100 text-xl">
{user.name ? user.name : user.username}
</h3>
{user.name && <p className="gradient-text-scale-100 text-sm">@{user.username}</p>}
</div>
</div>
<TicketNumber number={user.ticketNumber} size="small" />
</div>
</div>
</>
)
}
@@ -0,0 +1,47 @@
import Link from 'next/link'
import React from 'react'
import { Button } from 'ui'
import { useMobileViewport } from '../../../hooks/useMobileViewport'
import { UserData } from '../Ticket/hooks/use-conf-data'
import { TicketBrickWallSlider } from './TicketBrickWallSlider'
type user = UserData
interface Props {
users: user[]
}
export default function TicketBrickWall({ users }: Props) {
const isMobile = useMobileViewport(768)
return (
<div className="relative pt-28 pb-20 md:pt-40">
<div className="mx-auto bg-[#1C1C1C]">
<div className="max-w-[38rem] mb-8 mx-auto">
<h2 className="radial-gradient-text-scale-500 text-center">
Explore the unique tickets generated by our community
</h2>
</div>
<div
className="grid py-4 relative
before:content-[' '] before:absolute before:inset-0 before:right-auto before:w-1/4 before:bg-gradient-to-r before:from-[#1C1C1C] before:to-[#1C1C1C00] before:z-10 before:pointer-events-none
after:content-[' '] after:absolute after:inset-0 after:left-auto after:w-1/4 after:bg-gradient-to-l after:from-[#1C1C1C] after:to-[#1C1C1C00] after:z-10 after:pointer-events-none
"
>
<TicketBrickWallSlider users={users.slice(0, isMobile ? 5 : 7)} />
<TicketBrickWallSlider
users={users.slice(isMobile ? 5 : 7, isMobile ? 11 : 14)}
reverse
/>
{isMobile && <TicketBrickWallSlider users={users.slice(11, 17)} speed={16000} />}
</div>
<div className="w-full mt-2 lg:mt-4 mx-auto flex justify-center">
<Link href="/launch-week/tickets">
<Button as="a" type="outline" size="medium">
View all tickets
</Button>
</Link>
</div>
</div>
</div>
)
}
@@ -0,0 +1,92 @@
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { UserData } from '../Ticket/hooks/use-conf-data'
import { useWindowSize } from 'react-use'
import { Swiper, SwiperSlide } from 'swiper/react'
import SwiperCore, { Autoplay } from 'swiper'
import 'swiper/swiper.min.css'
SwiperCore.use([Autoplay])
interface Props {
users: UserData[]
reverse?: boolean
speed?: number
}
export function TicketBrickWallSlider({ users, reverse, speed = 10000 }: Props) {
const ref = useRef(null)
const [swiperInstace, setSwiperInstance] = useState<SwiperCore | any | null>(null)
const { width } = useWindowSize()
const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7'
const BUCKET_FOLDER_VERSION = 'v3'
const getOgUrl = (username: string, isGold: boolean) =>
`${STORAGE_URL}/tickets/gallery/${
isGold ? 'golden' : 'regular'
}/${BUCKET_FOLDER_VERSION}/${username}.png`
useEffect(() => {
// trigger autplay if viewport resize
if (swiperInstace) swiperInstace?.autoplay?.run()
}, [width])
return (
<div ref={ref} className="relative h-auto w-full m-0 overflow-hidden mb-2.5">
<div className="flex ticket-brick-swiper">
<Swiper
onSwiper={(swiper) => setSwiperInstance(swiper)}
centeredSlides={true}
spaceBetween={10}
slidesPerView={5}
speed={speed}
loop={true}
watchOverflow
threshold={2}
updateOnWindowResize
allowTouchMove={false}
autoplay={{
delay: 0,
disableOnInteraction: true,
reverseDirection: reverse,
}}
breakpoints={{
320: {
slidesPerView: 2.5,
},
720: {
slidesPerView: 3.5,
},
1440: {
slidesPerView: 5,
},
}}
>
{users.map((user, i) => (
<SwiperSlide key={user.username}>
<Link href={`/launch-week/tickets/${user.username}`} key={user.username}>
<a className="relative !w-[230px] md:w-[450px] !h-[200px] rounded-md md:rounded-lg transition-transform">
<div className="relative w-full pt-[50%] transform rounded-md md:rounded-lg overflow-hidden bg-gradient-to-b from-[#ffffff60] to-[#ffffff10]">
<div className="absolute inset-[1px] w-full h-full rounded-md md:rounded-lg overflow-hidden p-[1px]">
<Image
src={getOgUrl(user.username!, !!user.golden)}
alt={user.username}
layout="fill"
objectFit="cover"
objectPosition="center"
placeholder="blur"
blurDataURL="/images/blur.png"
className="absolute inset-[1px] rounded-md md:rounded-lg"
/>
</div>
</div>
</a>
</Link>
</SwiperSlide>
))}
</Swiper>
</div>
</div>
)
}
@@ -0,0 +1,6 @@
/* mask overlay on left & right */
.brick-wall-mask--left,
.brick-wall-mask--right {
background: linear-gradient(358.88deg, #1c1c1c 24.57%, #1c1c1c00 87.73%),
linear-gradient(358.88deg, #1c1c1c 24.57%, #1c1c1c00 87.73%);
}
@@ -0,0 +1,3 @@
.swiper-wrapper .swiper-slide {
width: 300px !important;
}
@@ -0,0 +1,21 @@
import React from 'react'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import useWinningChances from './Ticket/hooks/useWinningChances'
export default function ReferralIndicator() {
const winningChances = useWinningChances()
return (
<div className="bg-[#EAEAEA75] rounded-full mt-4 flex w-full">
<div
className={[
'bg-gradient-to-l from-[#A141E4] w-1/3 to-[#6300F569] flex items-center rounded-full pl-4 pr-6 py-1.5 text-xs shadow-[inset_0_0_2px_#ffffff50,0_0_1px_#00000050]',
winningChances === 2 && 'w-2/3',
winningChances === 3 && '!w-full bg-gradient-to-l from-[#E4B641] to-[#F5BF0069]',
].join(' ')}
>
<span style={{ textShadow: '0 0 4px #00000050' }}>{winningChances}x chance</span>
</div>
</div>
)
}
@@ -0,0 +1,189 @@
import cn from 'classnames'
import { useRef, useState } from 'react'
import styles from './ticket.module.css'
import styleUtils from './utils.module.css'
import TicketVisual from './TicketVisual'
import TicketActions from './TicketActions'
import TicketCopy from './ticket-copy'
import { UserData } from './hooks/use-conf-data'
import ReferralIndicator from '../ReferralIndicator'
import useWinningChances from './hooks/useWinningChances'
import { SITE_URL } from '~/lib/constants'
import { useMobileViewport } from '../../../hooks/useMobileViewport'
type TicketGenerationState = 'default' | 'loading'
type Props = {
username: UserData['username']
ticketNumber: UserData['ticketNumber']
name: UserData['name']
golden: UserData['golden']
bgImageId: UserData['bg_image_id']
referrals: number
sharePage?: boolean
}
export default function Ticket({
username,
name,
ticketNumber,
sharePage,
golden,
bgImageId,
referrals,
}: Props) {
const isMobile = useMobileViewport(1023)
const [ticketGenerationState, setTicketGenerationState] =
useState<TicketGenerationState>('default')
const divRef = useRef<HTMLDivElement>(null)
const winningChances = useWinningChances()
return (
<div
className={[
`relative w-full max-w-[700px] lg:max-w-[1100px] flex flex-col items-center lg:grid lg:grid-cols-12 gap-4 lg:gap-8 lg:p-2 rounded-3xl backdrop-blur lg:items-stretch h-auto"`,
!isMobile && styles['ticket-hero'],
].join(' ')}
id="wayfinding--ticket-visual-wrapper-container"
>
<div
className={cn(styles['ticket-visual-wrapper'], 'flex-1 col-span-8')}
id="wayfinding--ticket-visual-wrapper"
>
<div
className={cn(
styles['ticket-visual'],
styleUtils.appear,
styleUtils['appear-fourth'],
'relative flex flex-col items-center gap-2 w-full h-fit rounded-xl'
)}
id="wayfinding--ticket-visual-outer-container"
>
<TicketVisual
username={username ?? undefined}
name={name ?? undefined}
ticketNumber={ticketNumber ?? 0}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
golden={golden}
bgImageId={bgImageId}
/>
{username && (
<div className="w-full">
<TicketCopy username={username} />
</div>
)}
</div>
</div>
<div
ref={divRef}
className={[
'flex flex-col !w-full h-full justify-center col-span-full p-2 pt-4 lg:p-0 lg:col-span-4 mt-1 lg:m-0 lg:pr-8 max-h-[400px] rounded-3xl backdrop-blur lg:backdrop-blur-none',
isMobile && styles['ticket-hero'],
].join(' ')}
>
<div className="text-scale-1200 flex flex-col w-full items-center text-white text-center lg:text-left lg:items-start gap-2 lg:gap-3 mb-3 lg:mb-6">
<h1
className={cn(
styleUtils.appear,
styleUtils['appear-first'],
'text-xl text-white lg:text-3xl '
)}
>
{!sharePage ? (
<>
{name ? (
<>
{winningChances === 1 && (
<span className="text-2xl tracking-[0.02rem] leading-7 block">
You're <span className="gradient-text-purple-800">in the draw!</span> <br />
Now make it gold.
</span>
)}
{winningChances === 2 && (
<span className="text-2xl tracking-[0.02rem] leading-7 block">
You've <span className="gradient-text-purple-800">doubled</span> your{' '}
<br className="hidden lg:inline" />
chance!
<br className="inline lg:hidden" /> Almost{' '}
<span className={styles['gold-text']}>gold</span>.
</span>
)}
{winningChances === 3 && (
<span className="text-2xl tracking-[0.02rem] leading-7 block">
You're <span className={styles['gold-text']}>gold</span>!<br />
You've maxed your <br className="hidden lg:inline" /> chances of winning!
</span>
)}
</>
) : (
<span className="text-2xl leading-7 block">
Generate your ticket. <br />
Win the <span className="gradient-text-purple-800">SupaKeyboard</span>.
</span>
)}
</>
) : (
<span className="tracking-[-0.02px] leading-7 block">
{name ? name : username}'s <br className="hidden lg:inline" />
unique ticket
</span>
)}
</h1>
<div className="text-base text-white leading-5 max-w-[520px]">
{!sharePage ? (
<>
{golden ? (
<p>
Join us on April 16th for Launch Week 7's final day and find out if you are one
of the lucky winners.
</p>
) : username ? (
<p>
Why stop there? Increase your chances of winning by sharing your unique ticket.
Get sharing!
</p>
) : (
<p>
We have some fantastic swag up for grabs, including 3 limited-edition mechanical
keyboards that you won't want to miss.
</p>
)}
</>
) : (
<>
<p>
Get yours and win some fantastic swag, including a limited-edition mechanical
keyboard that you won't want to miss.
</p>
<div className="mt-4 lg:mt-8 rounded-full bg-[#E6E8EB] py-1 px-3 -mb-3 border border-[#bbbbbb] text-xs transition-all ease-out hover:bg-[#dfe1e3]">
<a
href={`${SITE_URL}/${username ? '?referral=' + username : ''}`}
className={`flex items-center justify-center gap-2 text-[#2e2e2e]`}
>
Get your ticket
</a>
</div>
</>
)}
</div>
{!sharePage && username && <ReferralIndicator />}
</div>
<div>
{username && (
<TicketActions
username={username}
golden={golden}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
)}
</div>
</div>
</div>
)
}
@@ -0,0 +1,122 @@
import { useEffect, useRef, useState } from 'react'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants'
import { IconCheckCircle } from 'ui'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import { useParams } from '~/hooks/useParams'
import TicketForm from './TicketForm'
type TicketGenerationState = 'default' | 'loading'
type Props = {
username: string
golden?: boolean
ticketGenerationState?: TicketGenerationState
setTicketGenerationState: (ticketGenerationState: TicketGenerationState) => void
}
export default function TicketActions({
username,
golden = false,
ticketGenerationState,
setTicketGenerationState,
}: Props) {
const [imgReady, setImgReady] = useState(false)
const [loading, setLoading] = useState(false)
const downloadLink = useRef<HTMLAnchorElement>()
const permalink = (medium: string) =>
encodeURIComponent(`${SITE_URL}/tickets/${username}?lw=7${golden ? `&golden=true` : ''}`)
const text = encodeURIComponent(golden ? TWEET_TEXT_GOLDEN : TWEET_TEXT)
const { userData } = useConfData()
const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink(
'twitter'
)}&via=supabase&text=${text}`
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink('linkedin')}`
const downloadUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/lw7-ticket-og?username=${encodeURIComponent(
username
)}`
const params = useParams()
const sharePage = params.username
useEffect(() => {
setImgReady(false)
const img = new Image()
img.src = downloadUrl
img.onload = () => {
setImgReady(true)
setLoading(false)
if (downloadLink.current) {
downloadLink.current.click()
downloadLink.current = undefined
}
}
}, [downloadUrl])
return (
<div className="grid gap-1 grid-cols-1 sm:grid-cols-3 lg:grid-cols-1">
{!sharePage ? (
<>
<div className="rounded-full bg-[#E6E8EB] text-scale-500 py-1 px-3 border border-[#dfe1e3] text-xs mb-1">
<div className="flex items-center justify-center gap-2">
<div className="text-scale-900">
<IconCheckCircle size={10} strokeWidth={1} />
</div>
Connect with Github
</div>
</div>
<div
className={`rounded-full ${
userData.sharedOnTwitter ? 'bg-[#E6E8EB] text-scale-500' : 'text-white'
} text-scale-500 py-1 px-3 border border-[#dfe1e3] text-xs mb-1 transition-all ease-out hover:bg-[#dfe1e3]`}
>
<a
href={tweetUrl}
rel="noopener noreferrer prefetch"
target="_blank"
className={`flex items-center justify-center gap-2 ${
userData.sharedOnTwitter ? 'text-scale-500' : 'text-white hover:text-scale-500'
}`}
>
{userData.sharedOnTwitter && (
<div className="text-scale-900">
<IconCheckCircle size={10} strokeWidth={1} />
</div>
)}
Share on Twitter
</a>
</div>
<div
className={`rounded-full ${
userData.sharedOnLinkedIn ? 'bg-[#E6E8EB] text-scale-500' : 'text-white'
} text-scale-500 py-1 px-3 border border-[#dfe1e3] text-xs mb-1 transition-all ease-out hover:bg-[#dfe1e3]`}
>
<a
href={linkedInUrl}
rel="noopener noreferrer prefetch"
target="_blank"
className={`flex items-center justify-center gap-2 ${
userData.sharedOnLinkedIn ? 'text-scale-500' : 'text-white hover:text-scale-500'
}`}
>
{userData.sharedOnLinkedIn && (
<div className="text-scale-900">
<IconCheckCircle size={10} strokeWidth={1} />
</div>
)}
Share on Linkedin
</a>
</div>
</>
) : (
!username && (
<TicketForm
defaultUsername={username ?? undefined}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
/>
)
)}
</div>
)
}
@@ -1,7 +1,6 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { PageState, ConfDataContext, UserData } from './hooks/use-conf-data'
import Ticket from './ticket'
import ConfContainer from './conf-container'
import Ticket from './ActualTicket'
import Form from './form'
import { SupabaseClient, Session } from '@supabase/supabase-js'
@@ -23,6 +22,10 @@ export default function Conf({
const [userData, setUserData] = useState<UserData>(defaultUserData)
const [pageState, setPageState] = useState<PageState>(defaultPageState)
useEffect(() => {
localStorage.setItem('isGolden', !!userData?.golden ? 'true' : 'false')
}, [userData])
return (
<ConfDataContext.Provider
value={{
@@ -42,6 +45,8 @@ export default function Conf({
ticketNumber={userData.ticketNumber}
sharePage={sharePage}
golden={userData.golden}
bgImageId={userData.bg_image_id}
referrals={userData.referrals ?? 0}
/>
)}
</ConfDataContext.Provider>
@@ -0,0 +1,174 @@
import { useState, useRef, useEffect } from 'react'
import { useRouter } from 'next/router'
import cn from 'classnames'
import { SITE_ORIGIN } from '~/lib/constants'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import formStyles from './form.module.css'
import ticketFormStyles from './ticket-form.module.css'
import { Button, IconCheckCircle, IconLoader } from 'ui'
type FormState = 'default' | 'loading' | 'error'
type TicketGenerationState = 'default' | 'loading'
type Props = {
defaultUsername?: string
ticketGenerationState?: TicketGenerationState
setTicketGenerationState: any
}
export default function TicketForm({ defaultUsername = '', setTicketGenerationState }: Props) {
const [username, setUsername] = useState(defaultUsername)
const [formState, setFormState] = useState<FormState>('default')
const [errorMsg] = useState('')
const { supabase, session, setUserData, setPageState, userData } = useConfData()
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
(typeof supabase)['channel']
> | null>(null)
const formRef = useRef<HTMLFormElement>(null)
const router = useRouter()
useEffect(() => {
if (session?.user && !userData.id) {
document.body.classList.add('ticket-generated')
const username = session.user.user_metadata.user_name
setUsername(username)
const name = session.user.user_metadata.full_name
const email = session.user.email
supabase
.from('lw7_tickets')
.insert({ email, name, username, referred_by: router.query?.referral ?? null })
.eq('email', email)
.select()
.single()
.then(async ({ error }) => {
// If error because of duplicate email, ignore and proceed, otherwise sign out.
if (error && error?.code !== '23505') return supabase.auth.signOut()
const { data } = await supabase
.from('lw7_tickets_golden')
.select('*')
.eq('username', username)
.single()
setUserData(data)
setFormState('default')
// Prefetch GitHub avatar
new Image().src = `https://github.com/${username}.png`
// Prefetch the twitter share URL to eagerly generate the page
fetch(`/launch-week/tickets/${username}`).catch((_) => {})
// Prefetch ticket og image.
fetch(
`https://obuldanrptloktxcffvn.functions.supabase.co/lw7-ticket-og?username=${encodeURIComponent(
username ?? ''
)}`
).catch((_) => {})
setPageState('ticket')
// Listen to realtime changes
if (!realtimeChannel && !data.golden) {
const channel = supabase
.channel('changes')
.on(
'postgres_changes',
{
event: 'UPDATE',
schema: 'public',
table: 'lw7_tickets',
filter: `username=eq.${username}`,
},
(payload) => {
const golden = !!payload.new.sharedOnTwitter && !!payload.new.sharedOnLinkedIn
setUserData({
...payload.new,
golden,
})
if (golden) {
channel.unsubscribe()
}
}
)
.subscribe()
setRealtimeChannel(channel)
}
})
}
return () => {
// Cleanup realtime subscription on unmount
realtimeChannel?.unsubscribe()
}
}, [session])
return formState === 'error' ? (
<div className="h-full">
<div className={cn(formStyles['form-row'], ticketFormStyles['form-row'])}>
<div className={cn(formStyles['input-label'], formStyles.error)}>
<div className={cn(formStyles.input, formStyles['input-text'])}>{errorMsg}</div>
<button
type="button"
className={cn(formStyles.submit, formStyles.error)}
onClick={() => {
setFormState('default')
setTicketGenerationState('default')
}}
>
Try Again
</button>
</div>
</div>
</div>
) : (
<form
ref={formRef}
onSubmit={async (e) => {
e.preventDefault()
if (formState !== 'default') {
setTicketGenerationState('default')
setFormState('default')
return
}
setFormState('loading')
setTicketGenerationState('loading')
await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: `${SITE_ORIGIN}/launch-week/${
userData.username ? '?referral=' + userData.username : ''
}`,
},
})
}}
className="flex flex-col items-center xl:block relative z-20"
id="wayfinding--connect-with-github-form"
>
<div className="flex flex-col gap-3">
<div>
<Button
type="secondary"
htmlType="submit"
disabled={formState === 'loading' || Boolean(session)}
>
<span className={`${username && 'text-scale-900'}`}>
{session ? (
<>
<IconCheckCircle />
Connect with Github
</>
) : (
<span className="flex items-center gap-2">
{formState === 'loading' && <IconLoader size={14} className="animate-spin" />}
Connect with Github
</span>
)}
</span>
{session ? <span className={ticketFormStyles.checkIcon}></span> : null}
</Button>
</div>
{/* {!session && <p className={'text-xs text-scale-900'}>Only public info will be used.</p>} */}
</div>
</form>
)
}
@@ -0,0 +1,19 @@
import React from 'react'
import { DATE } from '~/lib/constants'
interface Props {
size?: 'default' | 'small'
}
export default function TicketHeader({ size }: Props) {
return (
<div className="flex w-full justify-center pt-4 md:pt-6" id="wayfinding--ticket-header">
<div className="flex flex-col md:flex-row gap-1 justify-center items-center md:gap-5">
<img
className={size === 'small' ? 'w-[165px]' : 'w-[220px] md:w-[230px]'}
src={`/images/launchweek/ticket-header-logo.png`}
/>
<span className="text-white text-xs md:text-sm">{DATE}</span>
</div>
</div>
)
}
@@ -0,0 +1,95 @@
type Props = {
number: number | undefined
size?: 'default' | 'small'
}
export default function TicketNumber({ number, size }: Props) {
const numDigits = `${number}`.length
const prefix = `00000000`.slice(numDigits)
const ticketNumberText = `№ ${prefix}${number}`
return (
<>
<div
className="z-10 absolute md:flex inset-0 top-auto md:left-auto md:right-0 md:top-0 md:w-[110px] md:h-100% dark:text-white"
id="wayfinding--ticket-number-outer"
>
<div
className={[
'flex flex-col md:flex-row items-center justify-center w-full text-center',
size === 'small' ? 'text-[20px]' : 'text-[32px] md:text-[42px] ',
].join(' ')}
id="wayfinding--ticket-number-inner"
>
{/* Mobile line */}
<div className="block md:hidden">
<svg
width="234"
height="2"
viewBox="0 0 234 2"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.0390625 1L233.294 0.99999"
stroke="url(#paint0_linear_1701_100991)"
strokeWidth="0.71381"
strokeDasharray="1.43 1.43"
/>
<defs>
<linearGradient
id="paint0_linear_1701_100991"
x1="0.0390625"
y1="0.5"
x2="233.294"
y2="0.49999"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" stopOpacity="0" />
<stop offset="0.53125" stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
</div>
{/* Vertical line: desktop */}
<div
className="h-full hidden md:flex items-center absolute left-0"
id="wayfinding--ticket-stitch"
>
<svg
width="2"
height={size === 'small' ? '210' : '328'}
viewBox="0 0 2 328"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M0.601562 0.988281V327.763"
stroke="url(#paint0_linear_1736_81388)"
strokeDasharray="2 2"
/>
<defs>
<linearGradient
id="paint0_linear_1736_81388"
x1="1.10156"
y1="0.988281"
x2="1.10156"
y2="327.763"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="white" stopOpacity="0" />
<stop offset="0.53125" stopColor="white" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
</defs>
</svg>
</div>
<div className="md:absolute px-2 py-8 md:w-[max-content] leading-[1] md:transform md:rotate-90 md:origin-center bg-gradient-to-r from-[#F8F9FA] via-[#F8F9FA] to-[#F8F9FA50] bg-clip-text text-[#F8F9FA50] text-center">
{ticketNumberText}
</div>
</div>
</div>
</>
)
}
@@ -0,0 +1,86 @@
// import GithubIcon from '~/components/LaunchWeek/Ticket/icons/icon-github'
import cn from 'classnames'
import TicketForm from './TicketForm'
// import IconAvatar from '~/components/LaunchWeek/Ticket/icons/icon-avatar'
import styles from './ticket-profile.module.css'
import Image from 'next/image'
type TicketGenerationState = 'default' | 'loading'
type Props = {
name?: string
username?: string
size?: number
ticketGenerationState?: TicketGenerationState
setTicketGenerationState?: (ticketGenerationState: TicketGenerationState) => void
golden?: boolean
}
// The middle part of the ticket
// avatar / Yourn Name / Username
export default function TicketProfile({
name,
username,
size = 1,
ticketGenerationState,
setTicketGenerationState,
golden = false,
}: Props) {
return (
<div className="grid gap-4 items-center justify-center px-2" id="wayfinding--ticket-middle">
{username && (
<span
className={cn('rounded-full inline-block mx-auto', styles.wrapper, styles.rounded, {
[styles.show]: ticketGenerationState === 'loading',
})}
>
{username ? (
<Image
src={`https://github.com/${username}.png`}
alt={username}
layout="fill"
objectFit="contain"
priority
className={styles.image}
/>
) : (
<>
{/* <span
className={cn(
styles.image,
golden ? styles['empty-icon--golden'] : styles['empty-icon']
)}
>
{ <IconAvatar />}
</span> */}
</>
)}
</span>
)}
<div>
{username ? (
<p
className={`${cn(
styles.name,
{ [styles['name-blank']]: !username },
{ [styles['name-golden']]: golden }
)} dark:text-white text-center`}
>
<div
className={`${cn(styles.skeleton, styles.wrapper, {
[styles.show]: ticketGenerationState === 'loading',
})} text-3xl sm:text-4xl bg-gradient-to-r from-[#F8F9FA] via-[#F8F9FA] to-[#F8F9FA60] bg-clip-text text-transparent text-center`}
>
{name || username || 'Your Name'}
<p>{name && <p className="gradient-text-scale-100 text-sm">@{username}</p>}</p>
</div>
</p>
) : (
<TicketForm
defaultUsername={username ?? undefined}
setTicketGenerationState={setTicketGenerationState}
/>
)}
</div>
</div>
)
}
@@ -0,0 +1,130 @@
import styles from './ticket-visual.module.css'
import TicketProfile from './TicketProfile'
import TicketNumber from './TicketNumber'
import Tilt from 'vanilla-tilt'
import useConfData from '~/components/LaunchWeek/Ticket/hooks/use-conf-data'
import TicketHeader from './TicketHeader'
import { useEffect, useRef, useState } from 'react'
import Image from 'next/image'
type TicketGenerationState = 'default' | 'loading'
type Props = {
size?: number
name?: string
ticketNumber?: number
bgImageId?: number
username?: string
ticketGenerationState?: TicketGenerationState
setTicketGenerationState?: any
golden?: boolean
}
export default function TicketVisual({
size = 1,
name,
username,
bgImageId,
ticketNumber,
ticketGenerationState = 'default',
setTicketGenerationState,
golden = false,
}: Props) {
const { session } = useConfData()
const [imageIsLoading, setImageIsLoading] = useState(true)
const ticketRef = useRef<HTMLDivElement>(null)
const storageBaseFilepath = `https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7/tickets_bg`
const ticketBg = {
regular: {
image: `${storageBaseFilepath}/blurred/regular/jpg/reg_bg_${bgImageId}.jpg`,
overlay: `/images/launchweek/seven/ticket-overlay-reg.png`,
},
gold: {
image: `${storageBaseFilepath}/blurred/golden/jpg/gold_bg_${bgImageId}.jpg`,
overlay: `/images/launchweek/seven/ticket-overlay-gold.png`,
},
}
useEffect(() => {
if (ticketRef.current && !window.matchMedia('(pointer: coarse)').matches) {
Tilt.init(ticketRef.current, {
glare: !golden,
max: 4,
gyroscope: true,
'max-glare': 0.3,
'full-page-listening': true,
})
}
}, [ticketRef])
return (
<div
className="flex relative flex-col w-[300px] md:w-full md:max-w-none h-auto"
style={{
transform: 'translate3d(0, 0, 100px)',
transformStyle: 'preserve-3d',
}}
>
<div
ref={ticketRef}
className={[
styles.visual,
golden ? styles['visual--gold'] : '',
session ? styles['visual--logged-in'] : '',
!golden && 'overflow-hidden',
'flex relative flex-col justify-between w-full pt-[150%] md:pt-[50%] bg-gradient-to-b from-[#ffffff80] to-[#ffffff20] before:rounded-2xl h-0 box-border',
].join(' ')}
style={{
['--size' as string]: size,
}}
id="wayfinding--ticket-visual-inner-container"
>
<div className="absolute inset-0 h-[calc(100%-100px)] z-10 flex flex-col items-center justify-between w-full md:h-full flex-1 md:pl-[6%] md:pr-[15%] overflow-hidden">
{username && <TicketHeader />}
<div
className="flex-1 w-full h-full md:h-auto flex py-6 md:py-4 flex-col justify-center"
id="wayfinding--TicketProfile-container"
>
<TicketProfile
name={name}
username={username}
size={size}
ticketGenerationState={ticketGenerationState}
setTicketGenerationState={setTicketGenerationState}
golden={golden}
/>
</div>
</div>
<TicketNumber number={ticketNumber} />
<div
id="wayfinding--ticket-dynamic-bg-image"
className="absolute inset-[1px] z-0 rounded-2xl overflow-hidden"
>
{username && (
<Image
src={golden ? ticketBg.gold.overlay : ticketBg.regular.overlay}
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
className="absolute inset-[1px] z-[1]"
/>
)}
<Image
src={golden ? ticketBg.gold.image : ticketBg.regular.image}
layout="fill"
objectFit="cover"
placeholder="blur"
blurDataURL="/images/blur.png"
priority
className={[
'duration-700 ease-in-out transform transition-all',
imageIsLoading ? 'grayscale blur-xl scale-110' : 'scale-100 grayscale-0 blur-0',
].join(' ')}
onLoadingComplete={() => setImageIsLoading(false)}
/>
</div>
</div>
</div>
)
}
@@ -0,0 +1,90 @@
import { motion } from 'framer-motion'
import Image from 'next/image'
import Link from 'next/link'
import { useMobileViewport } from '../../../hooks/useMobileViewport'
import { UserData } from './hooks/use-conf-data'
interface Props {
loadedUsers: UserData[]
isLoading: boolean
pageCount: number
offset: number
}
export default function TicketsGrid({ loadedUsers, isLoading, pageCount, offset }: Props) {
const STORAGE_URL = 'https://obuldanrptloktxcffvn.supabase.co/storage/v1/object/public/images/lw7'
const BUCKET_FOLDER_VERSION = 'v3'
const getOgUrl = (username: string, isGold: boolean) =>
`${STORAGE_URL}/tickets/gallery/${
isGold ? 'golden' : 'regular'
}/${BUCKET_FOLDER_VERSION}/${username}.png`
const isMobile = useMobileViewport(768)
const isTablet = useMobileViewport(1024)
return (
<div
className={[
'grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 mx-[-10vw] gap-3 py-12 relative',
].join(' ')}
>
{loadedUsers?.map((user, i) => {
const rowTickets = isMobile ? 2 : isTablet ? 3 : 5
const divider = Math.floor(i / rowTickets)
const isOddRow = divider % 2 === 0
// Delay should be no more than pageCount for new loaded users
const recalculatedDelay = i >= pageCount * 2 ? (i - pageCount * (offset - 1)) / 15 : i / 15
return (
<Link href={`/launch-week/tickets/${user.username}`} key={`${user.username}-000${i}`}>
<motion.a
className="relative w-full p-[1px] rounded-lg sm:rounded-xl overflow-hidden hover:cursor-pointer bg-gradient-to-b from-[#ffffff60] to-[#ffffff10]"
initial={{ opacity: 0, y: 20, x: isOddRow ? 30 : -30 }}
animate={{
opacity: !isTablet && (i === 0 || i === 4) ? 0 : 1,
y: 0,
x: isOddRow ? 30 : -30,
transition: {
duration: 0.4,
ease: [0.24, 0.25, 0.05, 1],
delay: recalculatedDelay,
},
}}
>
<div className="relative inset-0 w-full pt-[50%] bg-[#2f2e2e] overflow-hidden rounded-lg sm:rounded-xl">
<Image
src={getOgUrl(user.username!, !!user.golden)}
alt={user.username}
layout="fill"
objectFit="cover"
objectPosition="center"
placeholder="blur"
blurDataURL="/images/blur.png"
/>
</div>
</motion.a>
</Link>
)
})}
{isLoading &&
Array.from({ length: pageCount }, (_, i) => (
<motion.div
className="relative rounded-lg sm:rounded-xl overflow-hidden h-0 w-full pt-[50%]"
initial={{ opacity: 0, y: 20 }}
animate={{
opacity: 1,
y: 0,
transition: {
duration: 0.4,
ease: [0.24, 0.25, 0.05, 1],
delay: i / 15,
},
}}
>
<div className="absolute inset-0 rounded-lg sm:rounded-xl bg-gradient-to-b from-[#ffffff60] to-[#ffffff10] p-[1px] overflow-hidden">
<div className="absolute inset-[1px] rounded-lg sm:rounded-xl bg-gradient-to-b from-[#3d3c3c] to-[#2f2e2e]" />
</div>
</motion.div>
))}
</div>
)
}
+2 -42
View File
@@ -1,13 +1,11 @@
import { useState, useCallback, useEffect } from 'react'
import { useState, useCallback } from 'react'
import cn from 'classnames'
import useConfData from '~/components/LaunchWeek/Ticket/hooks/use-conf-data'
import { useRouter } from 'next/router'
import LoadingDots from './loading-dots'
import styleUtils from './utils.module.css'
import styles from './form.module.css'
import useEmailQueryParam from '~/components/LaunchWeek/Ticket/hooks/use-email-query-param'
import { IconLoader } from '~/../../packages/ui'
// import Captcha, { useCaptcha } from './captcha'
type FormState = 'default' | 'loading' | 'error'
@@ -37,45 +35,7 @@ export default function Form({ sharePage, align = 'Center' }: Props) {
const router = useRouter()
const isCaptchaEnabled = false
useEffect(() => {
if (session?.user) {
document.body.classList.add('ticket-generated')
const username = session.user.user_metadata.user_name
const name = session.user.user_metadata.full_name
const email = session.user.email
supabase
.from('lw6_tickets')
.upsert({ email, name, username }, { onConflict: 'email', ignoreDuplicates: false })
.eq('email', email)
.select()
.single()
.then(async ({ error }) => {
if (error) return supabase.auth.signOut()
const { data } = await supabase
.from('lw6_tickets_golden')
.select('*')
.eq('username', username)
.single()
setUserData(data)
setFormState('default')
// Prefetch GitHub avatar
new Image().src = `https://github.com/${username}.png`
// Prefetch the twitter share URL to eagerly generate the page
fetch(`/launch-week/tickets/${username}`).catch((_) => {})
// Prefetch ticket og image.
fetch(
`https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent(
username ?? ''
)}`
).catch((_) => {})
setPageState('ticket')
})
}
}, [session])
// NOTE: this is not in use anymore as we're only allowing GitHub signups since LW7!
async function register(email: string, token?: string): Promise<ConfUser> {
const { error } = await supabase!.from('lw6_tickets').insert({ email })
if (error) {
@@ -9,6 +9,10 @@ export type UserData = {
username?: string
name?: string
golden?: boolean
referrals?: number
bg_image_id?: number
sharedOnTwitter?: string
sharedOnLinkedIn?: string
}
type ConfDataContextType = {
@@ -0,0 +1,25 @@
import { UserData } from './use-conf-data'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
function calculateWinChances(userData: UserData): number {
// Just signing up gives you 1
// Then signing up with LinkedIn and Twitter get you one more each for a total of 3
// When the chance === 3, you have a golden ticket
let numChances = 1
if (userData.sharedOnLinkedIn) {
numChances++
}
if (userData.sharedOnTwitter) {
numChances++
}
return numChances
}
export default function useWinningChances() {
const { userData } = useConfData()
const chances = calculateWinChances(userData)
return chances
}
@@ -1,100 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants'
import { IconDownload, IconLinkedin, IconTwitter } from 'ui'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import LoadingDots from './loading-dots'
type Props = {
username: string
golden?: boolean
}
export default function TicketActions({ username, golden = false }: Props) {
const [imgReady, setImgReady] = useState(false)
const [loading, setLoading] = useState(false)
const downloadLink = useRef<HTMLAnchorElement>()
const permalink = encodeURIComponent(`${SITE_URL}/tickets/${username}?v=6`)
const text = encodeURIComponent(golden ? TWEET_TEXT_GOLDEN : TWEET_TEXT)
const { userData } = useConfData()
const tweetUrl = `https://twitter.com/intent/tweet?url=${permalink}&via=supabase&text=${text}`
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${permalink}`
const downloadUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent(
userData.username ?? ''
)}`
useEffect(() => {
setImgReady(false)
const img = new Image()
img.src = downloadUrl
img.onload = () => {
setImgReady(true)
setLoading(false)
if (downloadLink.current) {
downloadLink.current.click()
downloadLink.current = undefined
}
}
}, [downloadUrl])
const ActionStyle = ({ children }: any) => {
return (
<div className="rounded-full bg-white dark:bg-scale-400 dark:hover:bg-scale-500 py-1 px-3 border border-scale-500 dark:text-white text-sm mb-1 transition-all ease-out hover:bg-scale-500">
{children}
</div>
)
}
return (
<>
<ActionStyle>
<a
href={tweetUrl}
rel="noopener noreferrer"
target="_blank"
className="flex items-center gap-2"
>
<div className="text-scale-900">
<IconTwitter size={12} strokeWidth={1.5} />
</div>
Tweet it
</a>
</ActionStyle>
<ActionStyle>
<a
href={linkedInUrl}
rel="noopener noreferrer"
target="_blank"
className="flex items-center gap-2"
>
<div className="text-scale-900">
<IconLinkedin size={12} strokeWidth={1.5} />
</div>
Share on Linkedin
</a>
</ActionStyle>
<ActionStyle>
<a
href={loading ? undefined : downloadUrl}
onClick={(e) => {
if (imgReady) return
e.preventDefault()
downloadLink.current = e.currentTarget
// Wait for the image download to finish
setLoading(true)
}}
download="ticket.png"
className="flex items-center gap-2"
>
<div className="text-scale-900">
<IconDownload size={12} strokeWidth={1.5} />
</div>
Download
{loading ? <LoadingDots size={4} /> : <>{/* <IconDownload width={24} /> Download */}</>}
</a>
</ActionStyle>
</>
)
}
@@ -2,8 +2,6 @@ import { useEffect, useState, useRef } from 'react'
import cn from 'classnames'
import { SITE_URL } from '~/lib/constants'
import styleUtils from './utils.module.css'
// import IconCopy from '~/components/LaunchWeek/Ticket/icons/icon-copy'
import styles from './ticket-copy.module.css'
import { IconCopy, IconCheck } from 'ui'
type Props = {
@@ -17,31 +15,18 @@ export default function TicketCopy({ username }: Props) {
const [copied, setCopied] = useState(false)
const scrollRef = useRef<HTMLParagraphElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const url = `${SITE_URL}/tickets/${username}`
const url = `${SITE_URL}/tickets/${username}?lw=7`
useEffect(() => {
if (navigator.clipboard) {
setCopyEnabled(true)
}
}, [])
const copiedText = <span className="text-xs text-scale-1200">Copied!</span>
// background: none;
// outline: none;
// border: none;
// z-index: 2;
// border-radius: var(--space-2x);
// width: 40px;
// display: flex;
// align-items: center;
// justify-content: center;
// margin-right: -10px;
const copyButton = (
<button
type="button"
name="Copy"
className="text-scale-900 hover:text-scale-1200 w-21 flex items-center cursor-pointer"
className="bg-[#E6E8EB] text-[#2e2e2e] text-xs w-21 flex items-center cursor-pointer py-1 px-2 rounded-full"
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
@@ -52,13 +37,17 @@ export default function TicketCopy({ username }: Props) {
})
}}
>
{copied ? (
<div className="text-brand-900">
<IconCheck size={14} />
</div>
) : (
<IconCopy size={14} />
)}
<div className="flex items-center gap-1 ">
{copied ? (
<>
<IconCheck size={14} /> Copied!
</>
) : (
<>
<IconCopy size={14} /> Copy your unique URL{' '}
</>
)}
</div>
</button>
)
@@ -67,14 +56,14 @@ export default function TicketCopy({ username }: Props) {
className={cn(
styleUtils.appear,
styleUtils['appear-third'],
'bg-scaleA-200 h-8 rounded border border-scale-400 w-full'
'bg-scaleA-200 h-8 rounded-full border border-[#E3E3E370] w-full overflow-hidden'
)}
id="wayfinding--ticket-copy"
>
<div className="px-3 h-full flex items-center gap-3 w-full truncate relative pr-20">
<div className="text-scale-900 text-sm hidden lg:flex">Your ticket URL:</div>
<div className="px-3 h-full flex items-center gap-3 w-full truncate relative pr-20 bg-[#D9D9D94D]">
<div className="flex items-center truncate">
<p
className={['text-xs font-mono text-scale-1200 truncate'].join(' ')}
className={['text-xs font-mono text-[#ededed] truncate'].join(' ')}
ref={scrollRef}
onScroll={() => {
if (!scrolling) {
@@ -95,10 +84,7 @@ export default function TicketCopy({ username }: Props) {
{url}
</p>
</div>
<div className="absolute right-3 with-auto height-auto flex items-center">
{copied && copiedText}
{copyButton}
</div>
<div className="absolute right-1 with-auto height-auto flex items-center">{copyButton}</div>
</div>
</div>
)
@@ -1,84 +0,0 @@
import { useState, useRef } from 'react'
// import { scrollTo } from '@lib/smooth-scroll';
import cn from 'classnames'
// import GithubIcon from '~/components/LaunchWeek/Ticket/icons/icon-github'
// import CheckIcon from '~/components/LaunchWeek/Ticket/icons/icon-check'
import { SITE_ORIGIN } from '~/lib/constants'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
import LoadingDots from './loading-dots'
import formStyles from './form.module.css'
import ticketFormStyles from './ticket-form.module.css'
type FormState = 'default' | 'loading' | 'error'
type TicketGenerationState = 'default' | 'loading'
type Props = {
defaultUsername?: string
setTicketGenerationState: React.Dispatch<React.SetStateAction<TicketGenerationState>>
}
export default function Form({ defaultUsername = '', setTicketGenerationState }: Props) {
const [username] = useState(defaultUsername)
const [formState, setFormState] = useState<FormState>('default')
const [errorMsg] = useState('')
const { supabase } = useConfData()
const formRef = useRef<HTMLFormElement>(null)
return formState === 'error' ? (
<div>
<div className={cn(formStyles['form-row'], ticketFormStyles['form-row'])}>
<div className={cn(formStyles['input-label'], formStyles.error)}>
<div className={cn(formStyles.input, formStyles['input-text'])}>{errorMsg}</div>
<button
type="button"
className={cn(formStyles.submit, formStyles.error)}
onClick={() => {
setFormState('default')
setTicketGenerationState('default')
}}
>
Try Again
</button>
</div>
</div>
</div>
) : (
<form
ref={formRef}
onSubmit={async (e) => {
e.preventDefault()
if (formState !== 'default') {
setTicketGenerationState('default')
setFormState('default')
return
}
setFormState('loading')
setTicketGenerationState('loading')
await supabase.auth.signInWithOAuth({
provider: 'github',
options: { redirectTo: `${SITE_ORIGIN}/launch-week/tickets` },
})
}}
className="flex flex-col items-center xl:block"
>
<div className="flex flex-col gap-3">
<div>
<button
type="submit"
className="rounded-full bg-scale-400 py-1 px-3 border border-scale-500 dark:text-white text-sm mb-1 transition-all ease-out hover:bg-scale-500"
disabled={formState === 'loading' || Boolean(username)}
>
<span className={`${username && 'text-scale-900'}`}>
{username ? 'Done!' : 'Connect with GitHub'}
</span>
{username ? <span className={ticketFormStyles.checkIcon}></span> : null}
</button>
</div>
{!username && <p className={'text-xs text-scale-900'}>Only public info will be used.</p>}
</div>
</form>
)
}
@@ -1,6 +1,6 @@
import { useRouter } from 'next/router'
import Head from 'next/head'
import TicketVisual from './ticket-visual'
import TicketVisual from './TicketVisual'
import styles from './ticket-image.module.css'
export default function TicketImage() {
@@ -16,7 +16,7 @@ export default function TicketImage() {
rel="stylesheet"
/>
</Head>
<TicketVisual
{/* <TicketVisual
size={1700 / 650}
username={query.username ? query.username.toString() : undefined}
ticketNumber={parseInt(query.ticketNumber.toString(), 10)}
@@ -28,7 +28,7 @@ export default function TicketImage() {
: undefined
}
golden={query.golden ? true : false}
/>
/> */}
</div>
</div>
)
@@ -5,12 +5,12 @@ import { DATE, SITE_URL } from '~/lib/constants'
const siteUrl = new URL(SITE_URL)
const siteUrlForTicket = `${siteUrl.host}${siteUrl.pathname}`.replace(/\/$/, '')
export default function TicketInfo({
export default function TicketInfoFooter({
logoTextSecondaryColor = 'var(--accents-5)',
golden = false,
}) {
return (
<div className={styles.info}>
<div className=" flex gap-0" id="wayfinding--TicketInfo-footer">
<div
className={`${cn(styles.date, { [styles['date-golden']]: golden })} text-sm mr-4 ${
golden && '!text-white'
@@ -23,7 +23,7 @@ export default function TicketInfo({
golden && '!text-white'
}`}
>
<div>supabase.com/launch-week</div>
{/* <div>supabase.com/launch-week</div> */}
</div>
</div>
)
@@ -1,8 +1,4 @@
.info {
display: flex;
gap: 0px;
margin-bottom: 64px;
padding: 32px;
}
@media (min-width: 768px) {
@@ -12,7 +12,7 @@ export default function TicketMonoMobile({ golden = false }: { golden?: boolean
<svg
width="100%"
height="100%"
viewBox="0 0 330 560"
viewBox="0 0 330 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
@@ -6,10 +6,11 @@ export default function TicketMono({ golden = false }: { golden?: boolean }) {
return (
<svg
width="100%"
height="100%"
viewBox="0 0 650 330"
height="auto"
viewBox="0 0 650 100"
fill="none"
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="none"
>
<path
fillRule="evenodd"
@@ -17,7 +18,6 @@ export default function TicketMono({ golden = false }: { golden?: boolean }) {
d="M21 5C12.1634 5 5 12.1634 5 21V133.388C20.2981 135.789 32 149.028 32 165C32 180.972 20.2981 194.211 5 196.612V309C5 317.837 12.1634 325 21 325H629C637.837 325 645 317.837 645 309V196.612C629.702 194.211 618 180.972 618 165C618 149.028 629.702 135.789 645 133.388V21C645 12.1634 637.837 5 629 5H21Z"
// fill="black"
/>
<path d="M512 5V326" stroke={perforationColor} strokeDasharray="4 4" />
</svg>
)
}
@@ -1,14 +0,0 @@
type Props = {
number: number
}
export default function TicketNumber({ number }: Props) {
const numDigits = `${number}`.length
const prefix = `00000000`.slice(numDigits)
return (
<>
№ {prefix}
{number}
</>
)
}
@@ -1,30 +1,14 @@
.profile {
display: flex;
align-items: center;
}
.image {
width: 60px;
height: 60px;
width: 50px;
height: 50px;
border-radius: 50%;
border: 1px solid #999;
}
@media (min-width: 768px) {
.image {
width: calc(80px * var(--size));
height: calc(80px * var(--size));
}
}
.text {
flex: 1;
margin-left: 10px;
}
@media (min-width: 768px) {
.text {
margin-left: calc(15px * var(--size));
width: calc(100px * var(--size));
height: calc(100px * var(--size));
}
}
@@ -34,7 +18,6 @@
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0;
max-width: 300px;
}
.name-golden {
@@ -102,12 +85,12 @@
background-position: -200% 0;
}
}
/*
.skeleton {
display: flex;
align-items: center;
border-radius: 5px;
}
border-radius: 25px;
} */
.skeleton.loaded {
width: unset !important;
@@ -152,16 +135,15 @@
.rounded,
.rounded.wrapper::before {
width: 62px;
height: 62px;
width: calc(80px * var(--size));
height: calc(80px * var(--size));
border-radius: 50% !important;
}
@media (min-width: 768px) {
.rounded,
.rounded.wrapper::before {
width: calc(82px * var(--size));
height: calc(82px * var(--size));
border-radius: 50% !important;
width: calc(110px * var(--size));
height: calc(110px * var(--size));
}
}
@@ -1,86 +0,0 @@
// import GithubIcon from '~/components/LaunchWeek/Ticket/icons/icon-github'
import cn from 'classnames'
// import IconAvatar from '~/components/LaunchWeek/Ticket/icons/icon-avatar'
import styles from './ticket-profile.module.css'
type TicketGenerationState = 'default' | 'loading'
type Props = {
name?: string
username?: string
size?: number
ticketGenerationState: TicketGenerationState
golden?: boolean
}
export default function TicketProfile({
name,
username,
size = 1,
ticketGenerationState,
golden = false,
}: Props) {
return (
<div className={styles.profile}>
<span
className={cn(styles.skeleton, styles.wrapper, styles.inline, styles.rounded, {
[styles.show]: ticketGenerationState === 'loading',
})}
>
{username ? (
<img src={`https://github.com/${username}.png`} alt={username} className={styles.image} />
) : (
<span
className={cn(
styles.image,
golden ? styles['empty-icon--golden'] : styles['empty-icon']
)}
>
{/* <IconAvatar /> */}
</span>
)}
</span>
<div className={styles.text}>
<p
className={`${cn(
styles.name,
{ [styles['name-blank']]: !username },
{ [styles['name-golden']]: golden }
)} dark:text-white`}
>
<span
className={`${cn(styles.skeleton, styles.wrapper, {
[styles.show]: ticketGenerationState === 'loading',
})} text-4xl bg-gradient-to-r ${
golden ? 'to-[#ffe8af]' : 'to-slate-900'
} from-white via-white bg-clip-text text-transparent`}
>
{name || username || 'Your Name'}
</span>
</p>
<p className={cn(styles.username, { [styles['username-golden']]: golden })}>
<span
className={cn(styles.skeleton, styles.wrapper, {
[styles.show]: ticketGenerationState === 'loading',
})}
>
<span className={`${golden ? 'text-white' : 'text-scale-1100'}`}>
{/* <GithubIcon
color={golden ? 'var(--gold-primary)' : 'var(--secondary-color)'}
size={20 * size}
/> */}
{username ? `@${username}` : <>username</>}
</span>
<img
alt="Supabase disconnected badge"
src={`/images/launchweek/supabadge-${
golden ? 'gold' : username ? 'connected' : 'disconnected'
}.svg`}
className="ml-2"
/>
</span>
</p>
</div>
</div>
)
}
@@ -1,90 +1,62 @@
.visual {
position: relative;
transform: translateZ(0);
display: flex;
flex-direction: column;
justify-content: space-between;
/* background-color: black; */
border: 2px solid rgba(0, 0, 0, 0.1);
background-image: linear-gradient(154.77deg, #cb88ff 29.4%, #cb88ff 120.47%);
box-shadow: 0px 1px 27px #9e44ef10, 0 0 0 1px rgba(121, 89, 134, 0.237),
inset 0 0px 0 -2px rgba(0, 0, 0, 0.55);
/* border: 1px solid rgba(0, 0, 0, 0.05); */
border-radius: 16px;
background-color: rgba(0, 0, 0, 1);
-webkit-backdrop-filter: blur(2px);
backdrop-filter: blur(2px);
/* background: linear-gradient(#fff000, var(--gold-secondary)); */
border: 1px solid #1c5f52;
animation: blank-svg-shadow 2s ease-in-out infinite alternate;
/* border: 1px solid #1c1c1c; */
/* animation: 2s ease-in-out infinite alternate; */
background-image: url('/images/launchweek/regular-ticket-background.png');
background-repeat: no-repeat;
background-size: contain;
}
.visual--gold {
/* border: 10px solid;
border-image-slice: 1;
border-width: 2px;
border-radius: 16px; */
background-color: linear-gradient(#fff000, #dea513);
background-color: #dea513;
background: radial-gradient(#fcdf9d, #dbb333);
}
.visual--gold::before {
content: ' ';
position: absolute;
inset: 0;
filter: drop-shadow(0 0 6px #fff) drop-shadow(0 0 6px #d99f0c) drop-shadow(0 0 9px #dbb333);
background: radial-gradient(#fcdf9d, #dbb333);
border: 1px solid rgba(255, 255, 0, 0.629);
animation: svg-shadow 2s ease-in-out infinite alternate;
background-image: url('/images/launchweek/gold-ticket-background.png');
/* filter: drop-shadow(2.45223px 8.5828px 73.5669px #FFBD14); */
/* border-image-source: linear-gradient(to left, var(--gold-secondary), var(--gold-accent)); */
animation: opacity-pulse 2s ease-in-out infinite alternate;
}
@media (max-width: 768px) {
.visual {
background-size: cover;
background-image: url('/images/launchweek/2x-regular-ticket-background.jpg');
@keyframes opacity-pulse {
from {
opacity: 0.5;
}
.visual--gold {
background-image: url('/images/launchweek/2x-gold-ticket-background.jpg');
to {
opacity: 1;
}
}
/* .svg-shadow {
} */
@keyframes svg-shadow {
from {
filter: drop-shadow(0 0 2px #fff) drop-shadow(0 0 4px #d99f0c) drop-shadow(0 0 6px #d99f0c);
}
to {
filter: drop-shadow(0 0 3px #fff) drop-shadow(0 0 6px #d99f0c) drop-shadow(0 0 9px #d99f0c);
filter: drop-shadow(0 0 10px #fff) drop-shadow(0 0 6px #d99f0c) drop-shadow(0 0 9px #d99f0c);
}
}
@keyframes blank-svg-shadow {
from {
filter: drop-shadow(0 0 2px #1c5f52) drop-shadow(0 0 4px #1c5f52) drop-shadow(0 0 6px #1c5f52);
filter: drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55);
}
to {
filter: drop-shadow(0 0 3px #1c5f52) drop-shadow(0 0 6px #1c5f52) drop-shadow(0 0 9px #1c5f52);
}
}
.horizontal-ticket {
display: none;
}
.vertical-ticket {
display: flex;
}
@media (min-width: 768px) {
.horizontal-ticket {
display: flex;
}
.vertical-ticket {
display: none;
filter: drop-shadow(0 0 3px #bada55) drop-shadow(0 0 2px #bada55) drop-shadow(0 0 2px #bada55);
}
}
@@ -94,22 +66,18 @@
top: 0;
width: 100%;
height: 100%;
/* padding: 46px 29px;/ */
}
.logo img {
/* margin-top: 64px; */
margin-top: calc(64px * var(--size));
margin-left: calc(32px * var(--size));
margin-left: calc(32px * var(--size));
/* width: 272px; */
width: calc(272px * var(--size));
}
@media (min-width: 768px) {
.logo img {
margin-top: calc(32px * var(--size));
/* width: 280px; */
width: calc(280px * var(--size));
margin-left: calc(160px * var(--size));
}
@@ -140,48 +108,10 @@
display: flex;
flex-direction: column;
justify-content: flex-end;
/* padding: calc(46px * var(--size)) calc(29px * var(--size)); */
}
@media (min-width: 768px) {
.info {
bottom: 0;
/* padding: 0 calc(160px * var(--size)) calc(44px * var(--size))
calc(63px * var(--size)); */
}
}
.ticket-number-wrapper {
position: absolute;
bottom: 40px;
right: 0;
}
@media (min-width: 768px) {
.ticket-number-wrapper {
bottom: 0;
}
}
.ticket-number {
font-size: calc(40px * var(--size));
line-height: 1;
width: calc(330px * var(--size));
text-align: center;
background-clip: text;
}
.ticket-number-golden {
color: var(--gold-primary);
}
@media (min-width: 768px) {
.ticket-number {
transform: rotate(90deg) translateY(calc(100px * var(--size)));
transform-origin: bottom right;
}
.ticket-number-golden {
color: var(--gold-accent);
}
}
@@ -1,91 +0,0 @@
import styles from './ticket-visual.module.css'
import TicketProfile from './ticket-profile'
import TicketNumber from './ticket-number'
import TicketMono from './ticket-mono'
import TicketInfo from './ticket-info'
import TicketMonoMobile from './ticket-mono-mobile'
import cn from 'classnames'
import { useRouter } from 'next/router'
type TicketGenerationState = 'default' | 'loading'
type Props = {
size?: number
name?: string
ticketNumber?: number
username?: string
ticketGenerationState?: TicketGenerationState
golden?: boolean
}
export default function TicketVisual({
size = 1,
name,
username,
ticketNumber,
ticketGenerationState = 'default',
golden = false,
}: Props) {
// golden = true
const router = useRouter()
const basePath = router.basePath
return (
<>
<div
className={[styles.visual, golden ? styles['visual--gold'] : ''].join(' ')}
// className={`rounded-2xl shadow-xl shadow-brand-500`}
style={{
['--size' as string]: size,
}}
>
<div className={styles['horizontal-ticket']}>
{/* {username ? <TicketColored golden={golden} /> : <TicketMono golden={golden} />} */}
<TicketMono golden={golden} />
</div>
<div className={styles['vertical-ticket']}>
{/* {username ? (
<TicketColoredMobile golden={golden} />
) : (
<TicketMonoMobile golden={golden} />
)} */}
<TicketMonoMobile golden={golden} />
</div>
<div className={styles.logo}>
<img src={`/images/launchweek/ticket-logo-${golden ? 'light' : 'dark'}.svg`} />
</div>
<div className={styles.profile}>
<TicketProfile
name={name}
username={username}
size={size}
ticketGenerationState={ticketGenerationState}
golden={golden}
/>
</div>
<div className={styles.info}>
<TicketInfo
golden={golden}
logoTextSecondaryColor={
ticketNumber ? (golden ? '#F2C94C' : 'var(--brand)') : undefined
}
/>
</div>
{ticketNumber && (
<div className={`${styles['ticket-number-wrapper']} dark:text-white`}>
<div
className={`${cn(styles['ticket-number'], {
[styles['ticket-number-golden']]: golden,
})} bg-gradient-to-r from-white via-white ${
golden ? 'to-[#ffe8af]' : 'to-slate-900'
}`}
>
<TicketNumber number={ticketNumber} />
</div>
</div>
)}
</div>
</>
)
}
@@ -1,3 +1,16 @@
.ticket-hero {
background: #c58cd2;
background: linear-gradient(
137.46deg,
rgba(255, 255, 255, 0.1) 6.19%,
rgba(255, 255, 255, 0.08) 103.29%
);
z-index: 10;
box-shadow: 0 1px 1px rgba(131, 6, 184, 0.08), 0 2px 2px rgba(128, 6, 184, 0.08),
inset 0 0.5px 0px 0.5px rgba(255, 255, 255, 0.14), inset 0 -3px 0 -2px rgba(255, 255, 255, 0.08),
0px 4px 20px rgba(118, 7, 170, 0.15);
}
.hero {
line-height: 1.15;
letter-spacing: -0.05em;
@@ -9,10 +22,6 @@
@media (min-width: 768px) {
.hero {
line-height: 1;
/* font-weight: 800; */
/* max-width: 800px; */
/* margin-left: auto;
margin-right: auto; */
}
}
@@ -58,26 +67,6 @@
margin-bottom: auto;
}
.ticket-visual {
position: relative;
border-radius: calc(16px * var(--size));
/* overflow: hidden; */
width: 330px;
margin: 0 auto;
}
/* .ticket-visual:before,
.ticket-visual:after { */
/* content: '';
display: block;
width: 72px;
height: 58px; */
/* background-color: #1C1C1C; */
/* position: absolute;
z-index: 100;
border-radius: 50%; */
/* } */
.ticket-visual:before {
left: 50%;
top: -7.5%;
@@ -91,10 +80,6 @@
}
@media (min-width: 768px) {
.ticket-visual {
width: 650px;
}
.ticket-visual:before {
top: 50%;
left: -7%;
@@ -147,6 +132,16 @@
}
.ticket-visual-wrapper {
border-radius: calc(16px * var(--size));
width: 100%;
}
.gold-text {
color: #f4cf72;
text-shadow: 0 0 1px rgba(0, 0, 0, 0.5), 0 0 4px rgba(0, 0, 0, 0.1);
}
@media (min-width: 768px) {
.gold-text {
color: #e4b641;
}
}
@@ -1,142 +0,0 @@
import cn from 'classnames'
import Tilt from 'vanilla-tilt'
import { useRef, useEffect, useState } from 'react'
// import { TicketGenerationState } from '@lib/constants'
// import {isMobileOrTablet} from '~/lib/helpers'
// import { scrollTo } from '@lib/smooth-scroll'
import styles from './ticket.module.css'
import styleUtils from './utils.module.css'
import TicketForm from './ticket-form'
import TicketVisual from './ticket-visual'
import TicketActions from './ticket-actions'
import TicketCopy from './ticket-copy'
import { DATE, SITE_NAME } from '~/lib/constants'
import Form from './form'
import { UserData } from './hooks/use-conf-data'
type TicketGenerationState = 'default' | 'loading'
type Props = {
username: UserData['username']
ticketNumber: UserData['ticketNumber']
name: UserData['name']
golden: UserData['golden']
sharePage?: boolean
}
export default function Ticket({ username, name, ticketNumber, sharePage, golden }: Props) {
const ticketRef = useRef<HTMLDivElement>(null)
const [ticketGenerationState, setTicketGenerationState] =
useState<TicketGenerationState>('default')
const divRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (ticketRef.current && !window.matchMedia('(pointer: coarse)').matches) {
Tilt.init(ticketRef.current, {
glare: true,
max: 5,
'max-glare': 0.16,
'full-page-listening': true,
})
}
}, [ticketRef])
// useEffect(() => {
// if (!sharePage && divRef && divRef.current && isMobileOrTablet()) {
// scrollTo(divRef.current, -30)
// }
// }, [divRef, sharePage])
// golden = true
return (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8 xl:gap-8 2xl:gap-16">
<div ref={divRef} className="flex flex-col gap-6">
<div
className={`
text-scale-1200
flex flex-col
items-center
text-center xl:text-left
xl:items-start
gap-3
`}
>
<h1 className={cn(styleUtils.appear, styleUtils['appear-first'], 'text-xl xl:text-3xl')}>
{sharePage ? (
name ? (
<p className="text-3xl dark:text-scale-1200 tracking-[0.02rem]">{name}'s ticket</p>
) : (
<p className="text-3xl dark:text-scale-1200 tracking-[0.02rem]">{SITE_NAME}</p>
)
) : golden ? (
<p className="text-xl dark:text-scale-1200 tracking-[0.02rem]">Congratulations!</p>
) : (
<p className="text-xl dark:text-scale-1200 tracking-[0.02rem]">You have a ticket!</p>
)}
</h1>
<h2 className="text-base dark:text-scale-1000 max-w-[520px]">
{sharePage ? (
<p>
Join {name ?? 'us'} on {DATE}.
</p>
) : golden ? (
<>
You got a Golden ticket. This means you’re in, and you also won a Supabase goodie
bag. Tweet it to redeem your swag pack!
</>
) : username ? (
<>Here is your unique ticket image for bragging on socials!</>
) : (
<>
This means you're in. Generate a unique ticket image with your GitHub profile, cause
a few of the lucky attendees will get a limited edition Supabase goodie bag. Make
sure you don't skip your chance.
</>
)}
</h2>
</div>
<div className={cn(styleUtils.appear, styleUtils['appear-third'])}>
{!sharePage ? (
<>
<TicketForm
defaultUsername={username ?? undefined}
setTicketGenerationState={setTicketGenerationState}
/>
</>
) : (
<Form sharePage align="Left" />
)}
</div>
</div>
<div className={styles['ticket-visual-wrapper']}>
<div
ref={ticketRef}
className={cn(styles['ticket-visual'], styleUtils.appear, styleUtils['appear-fourth'])}
>
<TicketVisual
username={username ?? undefined}
name={name ?? undefined}
ticketNumber={ticketNumber ?? undefined}
ticketGenerationState={ticketGenerationState}
golden={golden}
/>
</div>
{!sharePage && (
<>
{username ? (
<div className="flex flex-col gap-6 py-16">
<div className={`flex flex-col xl:flex-row gap-3 items-center justify-center`}>
<TicketActions username={username} golden={golden} />
</div>
<div>
<TicketCopy username={username} />
</div>
</div>
) : (
<div className={styles['ticket-actions-placeholder']} />
)}
</>
)}
</div>
</div>
)
}
@@ -0,0 +1,5 @@
.gradient-overlay::before {
background: linear-gradient(0deg, #1c1c1c 0%, #1c1c1c20 60%, transparent 100%),
linear-gradient(0deg, #1c1c1c 0%, #1c1c1c80 20%, #1c1c1c60 40%, #1c1c1c05 75%, transparent 100%),
linear-gradient(0deg, #1c1c1c 0%, #1c1c1c80 15%, #1c1c1c50 40%, #1c1c1c05 65%, transparent 100%);
}
@@ -0,0 +1,114 @@
[
{
"title": "Docs Updated",
"shipped": true,
"date": "12 Dec 2022",
"description": "Supabase Docs",
"d": 1,
"dd": "Monday",
"youtube_id": "OpPOaJI_Z28",
"blogpost": "https://supabase.com/blog/new-supabase-docs-built-with-nextjs",
"docs": "https://supabase.com/docs",
"steps": [
{
"title": "Supabase Docs",
"blog": "/blog/new-supabase-docs-built-with-nextjs",
"docs": "/docs",
"isNew": true,
"description": ""
}
]
},
{
"title": "Image Processing: Resize V2 + CDN",
"shipped": true,
"date": "13 Dec 2022",
"description": "Image Transformations",
"d": 2,
"dd": "Tuesday",
"youtube_id": "iqZlPtl_b-I",
"blogpost": "https://supabase.com/blog",
"docs": "https://supabase.com/docs",
"steps": [
{
"title": "Image Transformations",
"blog": "/blog/storage-image-resizing-smart-cdn",
"docs": "/docs/guides/storage/image-transformations",
"isNew": true,
"description": ""
},
{
"title": "Smart CDN",
"description": "Faster asset delivery, now even faster.",
"blog": "/blog/storage-image-resizing-smart-cdn",
"docs": "/docs/guides/storage/cdn",
"isNew": true
}
]
},
{
"title": "Multi-factor Authentication",
"shipped": true,
"date": "14 Dec 2022",
"description": "Multi-factor Authentication",
"d": 3,
"dd": "Wednesday",
"youtube_id": "CGZr5tybW18",
"blogpost": "https://supabase.com/blog",
"docs": "https://supabase.com/docs",
"steps": [
{
"title": "Multi-factor Authentication",
"blog": "/blog/mfa-auth-via-rls",
"docs": "/docs/guides/auth/auth-mfa",
"isNew": false,
"description": ""
}
]
},
{
"title": "Supabase Wrappers - FDW Framework",
"shipped": true,
"date": "15 Dec 2022",
"description": "Supabase Wrappers",
"d": 4,
"dd": "Thursday",
"youtube_id": "QA2qC5F-4OU",
"blogpost": "/blog/postgres-foreign-data-wrappers-rust",
"docs": "https://supabase.com/docs",
"steps": [
{
"title": "Supabase Wrappers",
"blog": "/blog/postgres-foreign-data-wrappers-rust",
"docs": "https://supabase.github.io/wrappers/",
"isNew": true,
"description": "Connect Postgres to external systems with Postgres Foreign Data Wrappers"
}
]
},
{
"title": "Supabase Vault Release",
"shipped": true,
"date": "16 Dec 2022",
"description": "Vault Release",
"d": 5,
"dd": "Friday",
"youtube_id": "6bGQotxisoY",
"blogpost": "https://supabase.com/blog",
"docs": "https://supabase.com/docs",
"steps": [
{
"title": "Vault Release",
"blog": "/blog/vault-now-in-beta",
"isNew": true,
"description": ""
},
{
"title": "Transparent Column Encryption",
"description": "Faster asset delivery, now even faster.",
"blog": "/blog/transparent-column-encryption-with-postgres",
"isNew": true
}
]
}
]
+6 -6
View File
@@ -62,23 +62,23 @@ const Developers = () => {
<div className="m-3 mx-6">
<p className="p">Latest announcements</p>
<ul className="mt-6 space-y-3 pb-6">
{AnnouncementsData.map((caseStudy: any, idx: number) => (
{AnnouncementsData.map((announcement: any, idx: number) => (
<li className="flow-root" key={`flyout_case_${idx}`}>
<Link href={caseStudy.url}>
<Link href={announcement.url}>
<a className="dark:hover:bg-dark-700 flex items-center rounded-lg border p-3 transition duration-150 ease-in-out hover:bg-gray-100">
<div className="relative hidden h-20 w-32 flex-shrink-0 overflow-auto rounded-md sm:block">
<Image
src={`${basePath}/${caseStudy.imgUrl}`}
alt="caseStudyThumb"
src={`${basePath}${announcement.imgUrl}`}
alt={announcement.title}
layout="fill"
objectFit="cover"
/>
</div>
<div className="min-w-0 flex-1 sm:ml-4">
<h4 className="text-scale-1200 text-normal mb-0 text-base">
{caseStudy.title}
{announcement.title}
</h4>
<p className="p text-sm">{caseStudy.description}</p>
<p className="p text-sm">{announcement.description}</p>
</div>
</a>
</Link>
+16 -11
View File
@@ -1,11 +1,12 @@
import React from 'react'
import SolutionsData from 'data/Solutions.json'
import CaseStudiesData from 'data/CaseStudies.json'
import CustomerStoriesData from 'data/CustomerStories.json'
import { useRouter } from 'next/router'
import ProductIcon from '../ProductIcon'
import { Badge } from 'ui'
import Image from 'next/image'
import Link from 'next/link'
import TextLink from '../TextLink'
const Product = () => {
const { basePath } = useRouter()
@@ -46,30 +47,31 @@ const Product = () => {
<div className="m-3 grid grid-cols-12 gap-x-8 gap-y-4 py-4 pr-3">{iconSections}</div>
</nav>
<div className="col-span-6">
<div className="m-3 mx-6">
<p className="p">Latest case studies</p>
<div className="m-3 mx-6 flex flex-col items-start">
<p className="p">Latest customer stories</p>
<ul className="mt-6 space-y-3">
{CaseStudiesData.map((caseStudy: any, idx: number) => {
{CustomerStoriesData.map((customer: any, idx: number) => {
if (idx > 1) {
return null
}
return (
<li className="flow-root" key={`flyout_case_${idx}`}>
<Link href={caseStudy.url}>
<Link href={customer.url}>
<a className="dark:hover:bg-dark-700 flex items-center rounded-lg border p-3 transition duration-150 ease-in-out hover:bg-gray-100">
<div className="relative hidden h-20 w-32 flex-shrink-0 overflow-auto rounded-md sm:block">
<div className="relative hidden mx-2 h-12 w-32 flex-shrink-0 overflow-auto rounded-md sm:block">
<Image
src={`${basePath}/${caseStudy.imgUrl}`}
alt="caseStudyThumb"
src={`${basePath}/${customer.imgUrl}`}
alt={customer.title}
layout="fill"
objectFit="cover"
objectFit="contain"
className="dark:brightness-200 dark:contrast-0 dark:filter"
/>
</div>
<div className="min-w-0 flex-1 sm:ml-4">
<h4 className="text-scale-1200 text-normal mb-0 text-base">
{caseStudy.title}
{customer.title}
</h4>
<p className="p text-sm">{caseStudy.description}</p>
<p className="p text-sm !mb-0">{customer.description}</p>
</div>
</a>
</Link>
@@ -77,6 +79,9 @@ const Product = () => {
)
})}
</ul>
<div className="mt-6">
<TextLink url="/customers" label="Explore more" />
</div>
</div>
</div>
</div>
+130 -72
View File
@@ -1,5 +1,6 @@
import React, { useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Button, Badge, IconStar, IconChevronDown } from 'ui'
import FlyOut from '~/components/UI/FlyOut'
@@ -11,7 +12,7 @@ import Solutions from '~/components/Nav/Product'
import Developers from '~/components/Nav/Developers'
import Announcement from '~/components/Nav/Announcement'
import { useTheme } from 'common/Providers'
import { useIsLoggedIn, useTheme } from 'common'
import TextLink from '../TextLink'
import Image from 'next/image'
import * as supabaseLogoWordmarkDark from 'common/assets/images/supabase-logo-wordmark--dark.png'
@@ -19,10 +20,14 @@ import * as supabaseLogoWordmarkLight from 'common/assets/images/supabase-logo-w
const Nav = () => {
const { isDarkMode } = useTheme()
const { pathname } = useRouter()
const [open, setOpen] = useState(false)
const [openProduct, setOpenProduct] = useState(false)
const [openDevelopers, setOpenDevelopers] = useState(false)
const isLoggedIn = useIsLoggedIn()
const isLaunchWeekPage = pathname.includes('launch-week')
const showLaunchWeekNavMode = isLaunchWeekPage && !open && !openProduct && !openDevelopers
React.useEffect(() => {
if (open) {
@@ -47,12 +52,12 @@ const Nav = () => {
const { name, description, icon, label, url } = solution
const content = (
<div className="mb-3 flex md:h-full lg:flex-col">
<div className="flex mb-3 md:h-full lg:flex-col">
<div className="flex-shrink-0">
<div className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-gray-800 text-white dark:bg-white dark:text-gray-800 sm:h-12 sm:w-12">
<div className="inline-flex items-center justify-center w-10 h-10 text-white bg-gray-800 rounded-md dark:bg-white dark:text-gray-800 sm:h-12 sm:w-12">
{/* <!-- Heroicon name: chart-bar --> */}
<svg
className="h-6 w-6"
className="w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -73,10 +78,10 @@ const Nav = () => {
</Badge>
)}
</p>
<p className="text-scale-1100 dark:text-dark-100 mt-1 text-sm">{description}</p>
<p className="mt-1 text-sm text-scale-1100 dark:text-dark-100">{description}</p>
</div>
{url && (
<p className="text-brand-900 mt-2 text-sm font-medium lg:mt-4">
<p className="mt-2 text-sm font-medium text-brand-900 lg:mt-4">
<TextLink label={label ? 'Get notified' : 'Learn more'} url={url} />
</p>
)}
@@ -85,14 +90,14 @@ const Nav = () => {
)
return url ? (
<Link href={url} key={`solution_${idx}`}>
<a className="dark:hover:bg-scale-600 -m-3 my-2 flex flex-col justify-between rounded-lg p-3 transition duration-150 ease-in-out hover:bg-gray-50">
<a className="flex flex-col justify-between p-3 my-2 -m-3 transition duration-150 ease-in-out rounded-lg dark:hover:bg-scale-600 hover:bg-gray-50">
{content}
</a>
</Link>
) : (
<div
key={`solution_${idx}`}
className="-m-3 flex flex-col justify-between rounded-lg p-3 transition duration-150 ease-in-out"
className="flex flex-col justify-between p-3 -m-3 transition duration-150 ease-in-out rounded-lg"
>
{content}
</div>
@@ -101,6 +106,7 @@ const Nav = () => {
type HamburgerButtonProps = {
toggleFlyOut: Function
showLaunchWeekNavMode?: boolean
}
const HamburgerButton = (props: HamburgerButtonProps) => (
@@ -109,17 +115,20 @@ const Nav = () => {
onClick={() => props.toggleFlyOut()}
>
<button
className="text-scale-900 focus:ring-brand-900 dark:bg-scale-200 dark:hover:bg-scale-300 inline-flex items-center justify-center rounded-md bg-gray-50 p-2 hover:bg-white focus:outline-none focus:ring-2 focus:ring-inset"
className={[
'text-scale-900 focus:ring-brand-900 dark:bg-scale-200 dark:hover:bg-scale-300 inline-flex items-center justify-center rounded-md bg-gray-50 p-2 hover:bg-white focus:outline-none focus:ring-2 focus:ring-inset',
showLaunchWeekNavMode && '!bg-transparent border border-[#be9eea]',
].join(' ')}
aria-expanded="false"
>
<span className="sr-only">Open main menu</span>
<svg
className="block h-6 w-6"
className="block w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke={showLaunchWeekNavMode ? '#be9eea' : 'currentColor'}
aria-hidden="true"
>
<path
@@ -131,7 +140,7 @@ const Nav = () => {
</svg>
<svg
className="hidden h-6 w-6"
className="hidden w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -151,7 +160,7 @@ const Nav = () => {
const FlyOutNavButton = (props: any) => (
<div
className={
className={[
`
text-scale-1200 hover:text-brand-900
inline-flex cursor-pointer items-center
@@ -159,9 +168,10 @@ const Nav = () => {
border-transparent
px-1
text-sm font-medium
transition-colors
` + props.active
}
transition-colors`,
showLaunchWeekNavMode && '!text-white',
props.active,
].join(' ')}
onClick={props.onClick}
>
<>
@@ -172,7 +182,11 @@ const Nav = () => {
(props.active && ' rotate-180 transform transition-all duration-100')
}
>
<IconChevronDown size={14} strokeWidth={2} />
<IconChevronDown
size={14}
strokeWidth={2}
className={showLaunchWeekNavMode ? 'text-white' : ''}
/>
</div>
</>
</div>
@@ -181,19 +195,39 @@ const Nav = () => {
return (
<>
{/* <Announcement /> */}
<div className="sticky top-0 z-50">
<div className="bg-scale-200 absolute top-0 h-full w-full opacity-80"></div>
<nav className="border-scale-400 border-b backdrop-blur-sm">
{/* <div className="lg:container mx-auto relative flex justify-between h-16 lg:px-10 xl:px-0"> */}
<div className="relative mx-auto flex h-16 justify-between lg:container lg:px-16 xl:px-20">
<HamburgerButton toggleFlyOut={() => setOpen(true)} />
<div className="flex flex-1 items-center justify-center sm:items-stretch lg:justify-between">
<div className="sticky top-0 z-50 transform" style={{ transform: 'translate3d(0,0,999px)' }}>
<div
className={[
'absolute inset-0 h-full w-full opacity-80 bg-scale-200',
!showLaunchWeekNavMode && '!opacity-100 transition-opacity',
showLaunchWeekNavMode && '!bg-transparent transition-all',
].join(' ')}
/>
<nav
className={[
`border-scale-400 border-b backdrop-blur-sm transition-opacity`,
showLaunchWeekNavMode && '!opacity-100 border-[#e0d2f430]',
].join(' ')}
>
{/* <div className="relative flex justify-between h-16 mx-auto lg:container lg:px-10 xl:px-0"> */}
<div className="relative flex justify-between h-16 mx-auto lg:container lg:px-16 xl:px-20">
<HamburgerButton
toggleFlyOut={() => setOpen(true)}
showLaunchWeekNavMode={showLaunchWeekNavMode}
/>
<div className="flex items-center justify-center flex-1 sm:items-stretch lg:justify-between">
<div className="flex items-center">
<div className="flex flex-shrink-0 items-center">
<div className="flex items-center flex-shrink-0">
<Link href="/" as="/">
<a className="block h-6 w-auto">
<a className="block w-auto h-6">
<Image
src={isDarkMode ? supabaseLogoWordmarkDark : supabaseLogoWordmarkLight}
src={
isLaunchWeekPage
? supabaseLogoWordmarkDark
: isDarkMode
? supabaseLogoWordmarkDark
: supabaseLogoWordmarkLight
}
width={124}
height={24}
alt="Supabase Logo"
@@ -214,22 +248,24 @@ const Nav = () => {
/>
<Link href="/pricing">
<a
className={`
text-scale-1200 hover:text-brand-900 hover:border-brand-900 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
className={[
`text-scale-1200 hover:text-brand-900 hover:border-brand-900 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
border-b-2 border-transparent p-5 px-1
text-sm font-medium
`}
text-sm font-medium`,
showLaunchWeekNavMode && '!text-white',
].join(' ')}
>
Pricing
</a>
</Link>
<Link href="/blog">
<a
className={`
text-scale-1200 hover:text-brand-900 hover:border-brand-900 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
className={[
`text-scale-1200 hover:text-brand-900 hover:border-brand-900 dark:text-dark-100 dark:hover:border-dark-100 inline-flex items-center
border-b-2 border-transparent p-5 px-1
text-sm font-medium
`}
text-sm font-medium`,
showLaunchWeekNavMode && '!text-white',
].join(' ')}
>
Blog
</a>
@@ -239,45 +275,62 @@ const Nav = () => {
<div className="flex items-center gap-2">
<Button
as="a"
className="group hidden lg:flex"
className="hidden group lg:flex"
// @ts-ignore
href="https://github.com/supabase/supabase"
target="_blank"
type="text"
icon={
<div className="text-brand-800 flex h-4 w-4 items-center justify-center">
<div className="flex items-center justify-center w-4 h-4 text-brand-800">
<div
className="text-scale-900 flex h-3 w-3 items-center justify-center
className={[
`text-scale-900 flex h-3 w-3 items-center justify-center
transition-all
group-hover:h-4
group-hover:w-4
group-hover:text-yellow-900
group-focus:h-4
group-focus:w-4
transition-all
group-hover:h-4
group-hover:w-4
group-hover:text-yellow-900
group-focus:h-4
group-focus:w-4
group-focus:text-yellow-900"
group-focus:text-yellow-900`,
showLaunchWeekNavMode && '!text-white',
].join(' ')}
>
<IconStar strokeWidth={2} />
</div>
</div>
}
>
Star us on GitHub
<span
// className={isLaunchWeekPage ? '!text-white' : ''}
>
Star us on GitHub
</span>
</Button>
<Link href="https://app.supabase.com/">
<a>
<Button type="default" className="hidden lg:block">
Sign in
</Button>
</a>
</Link>
<Link href="https://app.supabase.com/">
<a>
<Button className="hidden text-white lg:block">Start your project</Button>
</a>
</Link>
{isLoggedIn ? (
<Link href="/dashboard/projects">
<a>
<Button className="hidden text-white lg:block">Dashboard</Button>
</a>
</Link>
) : (
<>
<Link href="https://app.supabase.com/">
<a>
<Button type="default" className="hidden lg:block">
Sign in
</Button>
</a>
</Link>
<Link href="https://app.supabase.com/">
<a>
<Button className="hidden text-white lg:block">Start your project</Button>
</a>
</Link>
</>
)}
</div>
</div>
{/* <div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0"></div> */}
@@ -294,17 +347,22 @@ const Nav = () => {
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<div className="dark:bg-scale-300 fixed -inset-y-0 z-50 h-screen w-screen transform overflow-y-scroll bg-white p-4 md:p-8">
<div className="absolute right-4 top-4 items-center justify-between">
<div
className={[
'dark:bg-scale-300 fixed -inset-y-0 z-50 h-screen w-screen transform overflow-y-scroll bg-white p-4 md:p-8',
open && '!bg-scale-300',
].join(' ')}
>
<div className="absolute items-center justify-between right-4 top-4">
<div className="-mr-2">
<button
onClick={() => setOpen(false)}
type="button"
className="text-scale-900 focus:ring-brand-900 dark:bg-scale-300 dark:hover:bg-scale-400 inline-flex items-center justify-center rounded-md bg-white p-2 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset"
className="inline-flex items-center justify-center p-2 bg-white rounded-md text-scale-900 focus:ring-brand-900 dark:bg-scale-300 dark:hover:bg-scale-400 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset"
>
<span className="sr-only">Close menu</span>
<svg
className="h-6 w-6"
className="w-6 h-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
@@ -323,26 +381,26 @@ const Nav = () => {
</div>
{/* </div> */}
<div className="mt-6 mb-12">
<div className="space-y-1 pt-2 pb-4">
<div className="pt-2 pb-4 space-y-1">
<Link href="https://app.supabase.com/">
<a className="text-scale-900 block pl-3 pr-4 text-base font-medium dark:text-white">
<a className="block pl-3 pr-4 text-base font-medium text-scale-900 dark:text-white">
Sign in
</a>
</Link>
</div>
<div className="space-y-1 pt-2 pb-4">
<div className="pt-2 pb-4 space-y-1">
<Link href="/docs">
<a className="text-scale-900 dark:hover:bg-scale-600 block rounded-md py-2 pl-3 pr-4 text-base font-medium hover:border-gray-300 hover:bg-gray-50 dark:text-white">
<a className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white">
Developers
</a>
</Link>
<Link href="/company">
<a className="text-scale-900 dark:hover:bg-scale-600 block rounded-md py-2 pl-3 pr-4 text-base font-medium hover:border-gray-300 hover:bg-gray-50 dark:text-white">
<a className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white">
Company
</a>
</Link>
<Link href="/pricing">
<a className="text-scale-900 dark:hover:bg-scale-600 block rounded-md py-2 pl-3 pr-4 text-base font-medium hover:border-gray-300 hover:bg-gray-50 dark:text-white">
<a className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white">
Pricing
</a>
</Link>
@@ -350,7 +408,7 @@ const Nav = () => {
<Link href="https://github.com/supabase/supabase">
<a
target="_blank"
className="text-scale-900 dark:hover:bg-scale-600 block rounded-md py-2 pl-3 pr-4 text-base font-medium hover:border-gray-300 hover:bg-gray-50 dark:text-white"
className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white"
>
GitHub
</a>
@@ -358,14 +416,14 @@ const Nav = () => {
<Link href="/blog">
<a
target="_blank"
className="text-scale-900 dark:hover:bg-scale-600 block rounded-md py-2 pl-3 pr-4 text-base font-medium hover:border-gray-300 hover:bg-gray-50 dark:text-white"
className="block py-2 pl-3 pr-4 text-base font-medium rounded-md text-scale-900 dark:hover:bg-scale-600 hover:border-gray-300 hover:bg-gray-50 dark:text-white"
>
Blog
</a>
</Link>
</div>
<div className="p-3">
<p className="text-scale-900 mb-6 text-sm">Products available:</p>
<p className="mb-6 text-sm text-scale-900">Products available:</p>
{iconSections}
</div>
</div>
+1 -1
View File
@@ -36,7 +36,7 @@ function APISection(props: Props) {
<div className="grid grid-cols-12 lg:gap-16">
<div className="col-span-12 pb-8 lg:col-span-5 xl:col-span-5">
<h2 className="h3">{props.title}</h2>
<p className="p">{props.text}</p>
<div className="p">{props.text}</div>
{props.documentation_link && (
<Link href={props.documentation_link} as={props.documentation_link}>
<a>
@@ -32,18 +32,22 @@ function GithubExamples() {
</p>
<div className="flex items-center justify-center gap-2 py-4">
<Link href="/docs/guides/auth/overview" passHref>
<Button size="small" as="a" type="default" icon={<IconBookOpen size={12} />}>
View guides
</Button>
<a>
<Button size="small" type="default" icon={<IconBookOpen size={12} />}>
View guides
</Button>
</a>
</Link>
<Link
href="https://github.com/supabase/supabase/tree/master/examples"
as="https://github.com/supabase/supabase/tree/master/examples"
passHref
>
<Button size="small" as="a" type="default" icon={<IconGitHub size={12} />}>
Official GitHub library
</Button>
<a>
<Button size="small" type="default" icon={<IconGitHub size={12} />}>
Official GitHub library
</Button>
</a>
</Link>
</div>
</div>
@@ -37,28 +37,18 @@ function TwitterSocialProof() {
</p>
<div className="my-8 flex justify-center gap-2">
<Link href={'https://github.com/supabase/supabase/discussions'} passHref>
<Button
as="a"
// @ts-ignore
target="_blank"
size="small"
iconRight={<IconMessageCircle size={14} />}
type="default"
>
GitHub discussions
</Button>
<a target="_blank">
<Button size="small" iconRight={<IconMessageCircle size={14} />} type="default">
GitHub discussions
</Button>
</a>
</Link>
<Link href={'https://discord.supabase.com/'} passHref>
<Button
as="a"
type="default"
// @ts-ignore
target="_blank"
size="small"
iconRight={<IconMessageCircle size={14} />}
>
Discord
</Button>
<a target="_blank">
<Button type="default" size="small" iconRight={<IconMessageCircle size={14} />}>
Discord
</Button>
</a>
</Link>
</div>
</div>
+1 -3
View File
@@ -43,9 +43,7 @@ function TweetCard(props: TweetCard) {
</div>
</div>
<p>
<p className="text-scale-1100 mt-3 text-base">"{props.quote}"</p>
</p>
<p className="text-scale-1100 mt-3 text-base">"{props.quote}"</p>
</div>
)
}
+4 -4
View File
@@ -1,15 +1,15 @@
[
{
"type": "Announcement",
"title": "Supabase Launch Week 6",
"title": "Supabase Launch Week 7",
"description": "One new feature (or more) every day during a week.",
"imgUrl": "/images/launchweek/launch-week-6.jpg",
"logoUrl": "/images/launchweek/launch-week-6.jpg",
"imgUrl": "/images/launchweek/seven/launch-week-7.jpg",
"logoUrl": "/images/launchweek/seven/launch-week-7.jpg",
"organization": "Supabase",
"url": "/launch-week",
"postMeta": {
"name": "Paul Copplestone",
"publishDate": "Dec 12, 2022",
"publishDate": "Apr 03, 2023",
"readLength": 5
},
"ctaText": "Learn more"
+1 -1
View File
@@ -2,7 +2,7 @@
{
"type": "Case Study",
"title": "A web crawler handling millions of API requests.",
"description": "See how Monitoro built an automated a scraping platform using Supabase.",
"description": "See how Monitoro built and automated a scraping platform using Supabase.",
"imgUrl": "images/case-study-monitoro.jpg",
"logoUrl": "images/logos/monitoro.png",
"organization": "Monitoro",
+1 -1
View File
@@ -42,7 +42,7 @@ const data: CommunityItem[] = [
},
{
title: 'Twitter',
stat: '60,000+',
stat: '64,000+',
statLabel: 'Followers',
img: 'twitter.png',
detail: () => (
+53
View File
@@ -0,0 +1,53 @@
[
{
"type": "Customer Story",
"title": "HappyTeams unlocks better performance and reduces cost with Supabase.",
"description": "How a bootstrapped startup migrated from Heroku to Supabase in 30 minutes and never looked back.",
"imgUrl": "images/customers/logos/happyteams.png",
"logoUrl": "images/customers/logos/happyteams.png",
"organization": "HappyTeams",
"url": "/customers/happyteams",
"path": "/customers/happyteams",
"postMeta": {
"name": "Paul Copplestone",
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
"publishDate": "Feb 16, 2023",
"readLength": 6
},
"ctaText": "View story"
},
{
"type": "Customer Story",
"title": "Xendit use Supabase and create a full solution shipped to production in less than one week.",
"description": "As a payment processor, Xendit are responsible for verifying that all transactions are legal.",
"imgUrl": "images/customers/logos/xendit.png",
"logoUrl": "images/customers/logos/xendit.png",
"organization": "Xendit",
"url": "/customers/xendit",
"path": "/customers/xendit",
"postMeta": {
"name": "Paul Copplestone",
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
"publishDate": "Feb 14, 2023",
"readLength": 6
},
"ctaText": "View story"
},
{
"type": "Customer Story",
"title": "Replenysh uses Supabase to implement OTP in less than 24 hours.",
"description": "With Supabase, Replenysh gets a slick auth experience, reduces DevOps overhead, and continues to scale with Postgres.",
"imgUrl": "images/customers/logos/replenysh.png",
"logoUrl": "images/customers/logos/replenysh.png",
"organization": "Replenysh",
"url": "/customers/replenysh",
"path": "/customers/replenysh",
"postMeta": {
"name": "Paul Copplestone",
"avatarUrl": "https://avatars0.githubusercontent.com/u/10214025?v=4",
"publishDate": "Feb 14, 2023",
"readLength": 6
},
"ctaText": "View story"
}
]
+5 -1
View File
@@ -27,7 +27,7 @@
"url": "/pricing"
},
{
"text": "Launch Week 6",
"text": "Launch Week 7",
"url": "/launch-week"
}
]
@@ -105,6 +105,10 @@
"text": "Blog",
"url": "/blog"
},
{
"text": "Customer Stories",
"url": "/customers"
},
{
"text": "Careers",
"url": "/careers"
+17
View File
@@ -0,0 +1,17 @@
import { useEffect, useState } from 'react'
import { useWindowSize } from 'react-use'
export function useMobileViewport(breakpoint = 768) {
const [isMobile, setIsMobile] = useState(false)
const { width } = useWindowSize()
useEffect(() => {
if (width <= breakpoint) {
setIsMobile(true)
} else {
setIsMobile(false)
}
}, [width])
return isMobile
}
+22
View File
@@ -0,0 +1,22 @@
import { useRouter } from 'next/router'
import { useMemo } from 'react'
export function useParams(): {
[k: string]: string | undefined
} {
const { query } = useRouter()
return useMemo(
() =>
Object.fromEntries(
Object.entries(query).map(([key, value]) => {
if (Array.isArray(value)) {
return [key, value[0]]
} else {
return [key, value]
}
})
),
[query]
)
}
+16 -4
View File
@@ -15,6 +15,8 @@ async function generate() {
'pages/*/*.tsx',
'data/**/*.mdx',
'_blog/*.mdx',
'_case-studies/*.mdx',
'_customers/*.mdx',
'_alternatives/*.mdx',
'!pages/index.tsx',
'!data/*.mdx',
@@ -24,6 +26,10 @@ async function generate() {
'!pages/404.js',
])
const blogUrl = 'blog'
const caseStudiesUrl = 'case-studies'
const customerStoriesUrl = 'customers'
const sitemap = `
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
@@ -33,7 +39,9 @@ async function generate() {
const path = page
.replace('pages', '')
// add a `/` for blog posts
.replace('_blog', '/blog')
.replace('_blog', `/${blogUrl}`)
.replace('_case-studies', `/${caseStudiesUrl}`)
.replace('_customers', `/${customerStoriesUrl}`)
.replace('_alternatives', '/alternatives')
.replace('.tsx', '')
.replace('.mdx', '')
@@ -49,15 +57,19 @@ async function generate() {
if (route === '/alternatives/[slug]') return null
if (route === '/partners/[slug]') return null
if (route === '/case-studies/[slug]') return null
if (route === '/customers/[slug]') return null
if (route === '/launch-week/ticket-image') return null
/**
* Blog based urls
* handle removal of dates in filename
*/
if (route.includes('/blog/')) {
if (route.includes(`/${blogUrl}/`)) {
/**
* remove directory from route
*/
const _route = route.replace('/blog/', '')
const _route = route.replace(`/${blogUrl}/`, '')
/**
* remove the date from the file name
*/
@@ -65,7 +77,7 @@ async function generate() {
/**
* reconsruct the route
*/
route = '/blog/' + substring
route = `/${blogUrl}/` + substring
}
return `
+4 -3
View File
@@ -12,7 +12,8 @@ export const SITE_URL =
export const SITE_ORIGIN = new URL(SITE_URL).origin
export const TWITTER_USER_NAME = 'supabase'
export const COOKIE = 'supa-launch-week-ticke-user-id'
export const DATE = 'December 12th, 2022'
export const TWEET_TEXT = 'Got my ticket for #SupaLaunchWeek 6!'
export const TWEET_TEXT_GOLDEN = `Woop! I got a Golden Ticket for #SupaLaunchWeek 6! SupaSwag on its way!`
export const DATE = 'April 10th 2023'
export const TWEET_TEXT =
"It's almost #SupaLaunchWeek 7! Generate your unique ticket here 👇 you can win a mechanical keyboard 🤩"
export const TWEET_TEXT_GOLDEN = `I turned my #SupaLaunchWeek 7 ticket Gold! 🤩\n\n`
export const SITE_NAME = 'Supabase'
+1 -1
View File
@@ -3,7 +3,7 @@ import path from 'path'
import matter from 'gray-matter'
import { generateReadingTime } from './helpers'
type Directories = '_blog' | '_case-studies' | '_alternatives'
type Directories = '_blog' | '_case-studies' | '_customers' | '_alternatives'
// substring amount for file names
// based on YYYY-MM-DD format
+6 -6
View File
@@ -618,7 +618,7 @@ module.exports = [
{
permanent: true,
source: '/_app',
destination: 'https://app.supabase.com',
destination: 'https://supabase.com/dashboard',
},
{
permanent: true,
@@ -1458,17 +1458,17 @@ module.exports = [
{
permanent: true,
source: '/sign-in',
destination: 'https://app.supabase.com/sign-in',
destination: 'https://supabase.com/dashboard/sign-in',
},
{
permanent: true,
source: '/sign-up',
destination: 'https://app.supabase.com/sign-up',
destination: 'https://supabase.com/dashboard/sign-up',
},
{
permanent: true,
source: '/forgot-password',
destination: 'https://app.supabase.com/forgot-password',
destination: 'https://supabase.com/dashboard/forgot-password',
},
{
permanent: true,
@@ -1488,7 +1488,7 @@ module.exports = [
{
permanent: true,
source: '/projects',
destination: 'https://app.supabase.com/projects',
destination: 'https://supabase.com/dashboard/projects',
},
// START docs 2.0, moving pages in to structure
{
@@ -1531,7 +1531,7 @@ module.exports = [
source: '/docs/guides/auth/passwordless-login/phone-sms-otp-messagebird',
destination: '/docs/guides/auth/phone-login/messagebird',
},
{ permanent: true, source: '/docs/guides/auth', destination: '/docs/guides/auth/overview' },
{ permanent: true, source: '/docs/guides/auth/overview', destination: '/docs/guides/auth' },
{
permanent: true,
source: '/docs/guides/auth/auth-messagebird',
+4 -2
View File
@@ -19,13 +19,14 @@
"@next/bundle-analyzer": "^13.0.0",
"@next/mdx": "^12.3.2",
"@supabase/auth-ui-react": "^0.1.6",
"@supabase/supabase-js": "^2.0.0",
"@supabase/supabase-js": "^2.13.0",
"classnames": "^2.3.1",
"cobe": "^0.6.2",
"common": "*",
"config": "*",
"date-fns": "2.10.0",
"dayjs": "^1.11.4",
"dayjs": "^1.11.5",
"framer-motion": "^6.5.1",
"globby": "^12.0.2",
"gray-matter": "^4.0.2",
"markdown-toc": "^1.2.0",
@@ -46,6 +47,7 @@
"react-syntax-highlighter": "^15.5.0",
"react-tooltip": "^4.2.17",
"react-transition-group": "^4.4.1",
"react-use": "^17.4.0",
"recharts": "^2.1.15",
"rehype-slug": "^5.0.1",
"remark": "^13.0.0",
+5 -3
View File
@@ -61,9 +61,11 @@ const Error404 = () => {
</div>
<div className="flex items-center space-x-4">
<Link href="/">
<Button as="a" size="small" className="text-white">
Head back
</Button>
<a>
<Button size="small" className="text-white">
Head back
</Button>
</a>
</Link>
</div>
</div>
+6 -4
View File
@@ -6,7 +6,7 @@ import { useEffect } from 'react'
import Meta from '~/components/Favicons'
import '../styles/index.css'
import { post } from './../lib/fetchWrapper'
import { ThemeProvider } from 'common/Providers'
import { AuthProvider, ThemeProvider } from 'common'
import Head from 'next/head'
export default function MyApp({ Component, pageProps }: AppProps) {
@@ -63,9 +63,11 @@ export default function MyApp({ Component, pageProps }: AppProps) {
cardType: 'summary_large_image',
}}
/>
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
<AuthProvider>
<ThemeProvider>
<Component {...pageProps} />
</ThemeProvider>
</AuthProvider>
</>
)
}
+1 -1
View File
@@ -320,7 +320,7 @@ function BlogPostPage(props: any) {
</div>
{/* Sidebar */}
<div className="col-span-12 space-y-8 lg:col-span-5 xl:col-span-3 xl:col-start-9">
<div className="space-y-8 lg:sticky lg:top-16 lg:mb-16">
<div className="space-y-8 lg:sticky lg:top-24 lg:mb-24">
<div className="hidden lg:block">{toc}</div>
<div>
<div className="mb-4">
@@ -0,0 +1,46 @@
.toc a {
text-decoration: none !important;
font-weight: 400 !important;
}
.toc > ul > li {
padding-left: 0 !important;
}
.toc li {
@apply text-sm !important;
padding-left: 1rem !important;
}
.toc ul > li:before {
content: '';
position: absolute;
background-color: none;
border-radius: 50%;
width: 0 !important;
height: 0 !important;
top: 0.6875em;
left: 0.25em;
}
.header:hover svg,
.header svg:hover {
display: inline-block;
}
.article h1,
.article h2,
.article h3,
.article h4,
.article h5 {
font-weight: 500 !important;
}
.article {
@apply mb-16;
}
.article > div > pre {
padding: 0;
background: none;
}
Loaded 100 of 451 files, more files were not shown because too many files have changed in this diff. Show more