mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Merge branch 'master' into pr/13270
This commit is contained in:
commit
ef134fca0b
451 files changed
+26075
-34877
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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'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'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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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'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—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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@@ -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."
|
||||
|
||||

|
||||
|
||||
## 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>
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
|
||||

|
||||
|
||||
## 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.
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
## 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 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.
|
||||
|
||||

|
||||
|
||||
## 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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
+3
-3
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -42,7 +42,7 @@ const data: CommunityItem[] = [
|
||||
},
|
||||
{
|
||||
title: 'Twitter',
|
||||
stat: '60,000+',
|
||||
stat: '64,000+',
|
||||
statLabel: 'Followers',
|
||||
img: 'twitter.png',
|
||||
detail: () => (
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -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 `
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -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
Reference in new issue
Block a user