mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 11:55:05 +03:00
Merge branch 'master' into fix-endpoint
This commit is contained in:
39 files changed
+14745
-399
No files matched your search
@@ -10,9 +10,9 @@ Want to contribute? Why not jump into our GitHub repo and:
|
||||
|
||||
- [Sponsor Supabase](https://github.com/sponsors/supabase).
|
||||
- [Answer Discussions](https://github.com/supabase/supabase/discussions).
|
||||
- Submit an issue.
|
||||
- [Submit an issue](https://github.com/supabase/supabase/issue)
|
||||
- Report a performance issue or a part of the documentation that you find confusing.
|
||||
- Create a pull request.
|
||||
- [Create a pull request](https://github.com/supabase/supabase/pulls)
|
||||
- [Translate our Readme](https://github.com/supabase/supabase/issues/1341).
|
||||
- Try our products and give feedback.
|
||||
- Spread the word if you like what we are doing.
|
||||
@@ -13,4 +13,8 @@ services:
|
||||
- 5555:8080
|
||||
db:
|
||||
volumes:
|
||||
- /var/lib/postgresql/data
|
||||
- ./dev/data.sql:/docker-entrypoint-initdb.d/data.sql
|
||||
storage:
|
||||
volumes:
|
||||
- /var/lib/storage
|
||||
@@ -154,5 +154,5 @@ services:
|
||||
environment:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
volumes:
|
||||
- /var/lib/postgresql/data
|
||||
- ./volumes/db/data:/var/lib/postgresql/data
|
||||
- ./volumes/db/init:/docker-entrypoint-initdb.d
|
||||
@@ -2,15 +2,32 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import Head from 'next/head'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Typography } from '@supabase/ui'
|
||||
import { Button, IconGitHub, Typography } from '@supabase/ui'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
const Landing = () => {
|
||||
const { ui } = useStore()
|
||||
const { theme } = ui
|
||||
|
||||
async function handleGithubSignIn() {
|
||||
try {
|
||||
const { error } = await auth.signIn(
|
||||
{
|
||||
provider: 'github',
|
||||
},
|
||||
{
|
||||
redirectTo: process.env.NEXT_PUBLIC_SITE_URL,
|
||||
}
|
||||
)
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="z-10 relative h-screen flex flex-col">
|
||||
<Head>
|
||||
@@ -38,7 +55,13 @@ const Landing = () => {
|
||||
type="success"
|
||||
className="ml-8 font-medium hover:text-gray-300 focus:outline-none focus:text-gray-300 transition duration-150 ease-in-out"
|
||||
>
|
||||
<Link href={`${API_URL}/login`}>Sign In</Link>
|
||||
{GOTRUE_ENABLED ? (
|
||||
<Button onClick={handleGithubSignIn} icon={<IconGitHub />}>
|
||||
Sign In With Github
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={`${API_URL}/login`}>Sign In</Link>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -55,9 +78,15 @@ const Landing = () => {
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center space-x-3">
|
||||
<Link href={`${API_URL}/login`}>
|
||||
<Button size="large">Sign In</Button>
|
||||
</Link>
|
||||
{GOTRUE_ENABLED ? (
|
||||
<Button onClick={handleGithubSignIn} size="large" icon={<IconGitHub />}>
|
||||
Sign In With Github
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={`${API_URL}/login`}>
|
||||
<Button size="large">Sign In</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link href="https://supabase.com/docs">
|
||||
<Button size="large" type="default">
|
||||
Docs
|
||||
|
||||
@@ -101,7 +101,7 @@ const LogPanel: FC<Props> = ({
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
value={searchValue}
|
||||
actions={
|
||||
showReset && <IconX size="tiny" className="cursor-pointer mx-1" onClick={onReset} />
|
||||
showReset && <IconX size="tiny" className="cursor-pointer mx-1" title="Clear search" onClick={onReset} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import WithSidebar from './WithSidebar'
|
||||
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
/**
|
||||
* layout for dashboard homepage, account and org settings
|
||||
@@ -19,6 +20,22 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
|
||||
const { app, ui } = useStore()
|
||||
|
||||
const onClickLogout = async () => {
|
||||
await auth.signOut()
|
||||
router.reload()
|
||||
}
|
||||
|
||||
let baseLogoutLink = {
|
||||
icon: '/icons/feather/power.svg',
|
||||
label: 'Logout',
|
||||
href: `${API_URL}/logout`,
|
||||
key: `${API_URL}/logout`,
|
||||
}
|
||||
|
||||
let logoutLink = GOTRUE_ENABLED
|
||||
? { ...baseLogoutLink, href: undefined, onClick: onClickLogout }
|
||||
: baseLogoutLink
|
||||
|
||||
const organizationsLinks = app.organizations
|
||||
.list()
|
||||
.map((x: any) => ({
|
||||
@@ -36,7 +53,6 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
isActive: router.pathname == '/',
|
||||
label: 'All projects',
|
||||
href: '/',
|
||||
as: `/`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -60,12 +76,7 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
href: `/account/me`,
|
||||
key: `/account/me`,
|
||||
},
|
||||
{
|
||||
icon: '/img/power.svg',
|
||||
label: 'Logout',
|
||||
href: `${API_URL}/logout`,
|
||||
key: `${API_URL}/logout`,
|
||||
},
|
||||
logoutLink,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from 'next/link'
|
||||
import { CSSProperties, FC, ReactNode } from 'react'
|
||||
import { Menu, Typography, IconArrowUpRight, Badge } from '@supabase/ui'
|
||||
import { isUndefined } from 'lodash'
|
||||
import LayoutHeader from '../ProjectLayout/LayoutHeader'
|
||||
|
||||
interface Props {
|
||||
@@ -142,7 +143,24 @@ const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) =>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarItem: FC<any> = ({ id, label, as, href, isActive, isSubitem, onClick, external }) => {
|
||||
const SidebarItem: FC<any> = ({ id, label, href, isActive, isSubitem, onClick, external }) => {
|
||||
if (isUndefined(href)) {
|
||||
return (
|
||||
<Menu.Item
|
||||
rounded
|
||||
key={id}
|
||||
style={{
|
||||
marginLeft: isSubitem && '.5rem',
|
||||
}}
|
||||
active={isActive ? true : false}
|
||||
onClick={onClick || (() => {})}
|
||||
icon={external && <IconArrowUpRight size={'tiny'} />}
|
||||
>
|
||||
{isSubitem ? <Typography.Text small>{label}</Typography.Text> : label}
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href || ''}>
|
||||
<a className="block" target={external ? '_blank' : '_self'}>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { FC } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Typography } from '@supabase/ui'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
|
||||
import { Route } from 'components/ui/ui.types'
|
||||
|
||||
interface Props {
|
||||
@@ -9,18 +12,34 @@ interface Props {
|
||||
|
||||
const NavigationIconButton: FC<Props> = ({ route, isActive = false }) => {
|
||||
return (
|
||||
<Link href={route.link}>
|
||||
<a
|
||||
className={[
|
||||
'flex items-center justify-center h-10 w-10 rounded', // Layout
|
||||
'text-gray-600 hover:bg-gray-100', // Light mode
|
||||
'dark:text-gray-400 dark:hover:bg-bg-alt-dark dark:hover:text-white', // Dark mode
|
||||
`${isActive ? 'bg-gray-100 dark:bg-bg-alt-dark dark:!text-white' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{route.icon}
|
||||
</a>
|
||||
</Link>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger>
|
||||
<Link href={route.link}>
|
||||
<a
|
||||
className={[
|
||||
'flex items-center justify-center h-10 w-10 rounded', // Layout
|
||||
'text-gray-600 hover:bg-gray-100', // Light mode
|
||||
'dark:text-gray-400 dark:hover:bg-bg-alt-dark dark:hover:text-white', // Dark mode
|
||||
`${isActive ? 'bg-gray-100 dark:bg-bg-alt-dark dark:!text-white' : ''}`,
|
||||
].join(' ')}
|
||||
>
|
||||
{route.icon}
|
||||
</a>
|
||||
</Link>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side="right">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'shadow px-2 py-1 rounded border',
|
||||
'bg-white',
|
||||
'dark:bg-gray-800 dark:border-gray-700',
|
||||
].join(' ')}
|
||||
>
|
||||
<Typography.Text small>{route.label}</Typography.Text>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { API_URL } from 'lib/constants'
|
||||
import Panel from './Panel'
|
||||
|
||||
const { Text, Title } = Typography
|
||||
const API_SERVICE_ID = 1
|
||||
|
||||
export const DisplayApiSettings = () => {
|
||||
const router = useRouter()
|
||||
@@ -36,12 +37,9 @@ export const DisplayApiSettings = () => {
|
||||
</ApiContentWrapper>
|
||||
)
|
||||
|
||||
const { services } = data
|
||||
|
||||
// Get the API service
|
||||
const API_SERVICE_ID = 1
|
||||
let apiService = services.find((x: any) => x.app.id == API_SERVICE_ID)
|
||||
let apiKeys = apiService.service_api_keys
|
||||
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
|
||||
const apiKeys = apiService.service_api_keys
|
||||
|
||||
return (
|
||||
<ApiContentWrapper>
|
||||
@@ -124,13 +122,10 @@ export const DisplayConfigSettings = () => {
|
||||
</ConfigContentWrapper>
|
||||
)
|
||||
|
||||
const { project, services } = data
|
||||
|
||||
// Get the API service
|
||||
const API_SERVICE_ID = 1
|
||||
let jwtSecret = project.jwt_secret ?? ''
|
||||
let apiService = services.find((x: any) => x.app.id == API_SERVICE_ID)
|
||||
let apiConfig = apiService.app_config
|
||||
const jwtSecret = data?.project.jwt_secret ?? ''
|
||||
const apiService = (data?.services ?? []).find((x: any) => x.app.id == API_SERVICE_ID)
|
||||
const apiConfig = apiService.app_config
|
||||
|
||||
return (
|
||||
<section>
|
||||
@@ -159,7 +154,7 @@ export const DisplayConfigSettings = () => {
|
||||
readOnly
|
||||
copy
|
||||
reveal
|
||||
disabled={true}
|
||||
disabled
|
||||
value={jwtSecret}
|
||||
className="input-mono"
|
||||
descriptionText="Used to decode your JWTs. You can also use this to mint your own JWTs."
|
||||
|
||||
@@ -4,6 +4,8 @@ import { readOnly } from './supabaseClient'
|
||||
import { SupaResponse, User } from 'types'
|
||||
import { auth0 } from './auth0'
|
||||
import { flattenNamespaceOnUser } from './apiHelpers'
|
||||
import { config } from 'process'
|
||||
import { getAuth0Id, getAuthUser, getIdentity, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
/**
|
||||
* Use this method on api routes to check if user is authenticated and having required permissions.
|
||||
@@ -32,7 +34,7 @@ export async function apiAuthenticate(
|
||||
try {
|
||||
// Check that they are logged in
|
||||
// If no error throw from getAccessToken. That's good, we can continue
|
||||
await auth0.getAccessToken(req, res)
|
||||
if (!(GOTRUE_ENABLED)) await auth0.getAccessToken(req, res)
|
||||
|
||||
const user = await fetchUser(req, res)
|
||||
if (!user) {
|
||||
@@ -53,16 +55,41 @@ export async function apiAuthenticate(
|
||||
* user with only id prop or detail object. It depends on requireUserDetail config
|
||||
*/
|
||||
async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any> {
|
||||
const session = auth0.getSession(req, res)
|
||||
let user_id_supabase = null
|
||||
let user_id_auth0 = null
|
||||
let gotrue_id = null
|
||||
let email = null
|
||||
if (GOTRUE_ENABLED) {
|
||||
const token = req.headers.authorization
|
||||
if (!token) {
|
||||
return res.status(401).end('Unauthorized: missing access token')
|
||||
}
|
||||
let { user: gotrue_user, error: authError } = await getAuthUser(token)
|
||||
if (authError) {
|
||||
throw authError
|
||||
}
|
||||
if (gotrue_user !== null) {
|
||||
gotrue_id = gotrue_user?.id
|
||||
email = gotrue_user.email
|
||||
|
||||
if (!session) {
|
||||
return null
|
||||
let { identity, error } = getIdentity(gotrue_user)
|
||||
if (error) throw error
|
||||
if (identity?.provider !== undefined) {
|
||||
user_id_auth0 = getAuth0Id(identity?.provider, identity?.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const session = auth0.getSession(req, res)
|
||||
if (!session) {
|
||||
return null
|
||||
}
|
||||
const { user: auth0_user } = session
|
||||
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
|
||||
user_id_supabase = flattened.user_id_supabase
|
||||
user_id_auth0 = flattened.user_id_auth0
|
||||
email = flattened.email
|
||||
}
|
||||
|
||||
const { user: auth0_user } = session
|
||||
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
|
||||
const { user_id_supabase, user_id_auth0, email } = flattened
|
||||
|
||||
if (user_id_supabase) {
|
||||
return {
|
||||
id: user_id_supabase,
|
||||
@@ -70,15 +97,15 @@ async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = await readOnly
|
||||
.from('users')
|
||||
.select(
|
||||
`
|
||||
const query = readOnly.from('users').select(
|
||||
`
|
||||
id, auth0_id, primary_email, username, first_name, last_name, mobile, is_alpha_user
|
||||
`
|
||||
)
|
||||
.eq('auth0_id', user_id_auth0)
|
||||
.single()
|
||||
)
|
||||
|
||||
const { data } = GOTRUE_ENABLED
|
||||
? await query.eq('gotrue_id', gotrue_id).single()
|
||||
: await await query.eq('auth0_id', user_id_auth0).single()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
export function handleError<T>(e: any, requestId: string): SupaResponse<T> {
|
||||
@@ -65,3 +68,53 @@ export async function handleResponseError<T = unknown>(
|
||||
return { error } as unknown as SupaResponse<T>
|
||||
}
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return ''
|
||||
|
||||
const tokenData = window?.localStorage['supabase.auth.token']
|
||||
if (!tokenData) {
|
||||
// try to get from url fragment
|
||||
const access_token = getParameterByName('access_token')
|
||||
if (access_token) return access_token
|
||||
else return undefined
|
||||
}
|
||||
const tokenObj = tryParseJson(tokenData)
|
||||
if (tokenObj === false) {
|
||||
return ''
|
||||
}
|
||||
return tokenObj.currentSession.access_token
|
||||
}
|
||||
|
||||
// get param from URL fragment
|
||||
function getParameterByName(name: string, url?: string) {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return ''
|
||||
|
||||
if (!url) url = window?.location?.href || ''
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
name = name.replace(/[\[\]]/g, '\\$&')
|
||||
const regex = new RegExp('[?&#]' + name + '(=([^&#]*)|&|#|$)'),
|
||||
results = regex.exec(url)
|
||||
if (!results) return null
|
||||
if (!results[2]) return ''
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '))
|
||||
}
|
||||
|
||||
export function constructHeaders(requestId: string, optionHeaders?: { [prop: string]: any }) {
|
||||
let headers: { [prop: string]: any } = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...optionHeaders,
|
||||
}
|
||||
|
||||
const hasAuthHeader = !isUndefined(optionHeaders) && 'Authorization' in optionHeaders
|
||||
if (GOTRUE_ENABLED && !hasAuthHeader) {
|
||||
const accessToken = getAccessToken()
|
||||
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -8,19 +8,15 @@ export async function delete_<T = any>(
|
||||
options?: { [prop: string]: any }
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
try {
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -8,17 +8,13 @@ export async function get<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
if (!response.ok) return handleResponseError(response, requestId)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { constructHeaders, handleError, handleResponse, handleResponseError } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -9,18 +9,14 @@ export async function patch<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -9,18 +9,14 @@ export async function post<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
if (!response.ok) return handleResponseError(response, requestId)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { GoTrueClient, User } from '@supabase/gotrue-js'
|
||||
|
||||
export const GOTRUE_ENABLED = (process.env.NEXT_PUBLIC_GOTRUE_ENABLED === 'true') || false
|
||||
export const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL || 'http://localhost:8000/auth/v1'
|
||||
|
||||
export const auth = new GoTrueClient({
|
||||
url: GOTRUE_URL,
|
||||
autoRefreshToken: true,
|
||||
})
|
||||
|
||||
export const getAuthUser = async (token: String): Promise<any> => {
|
||||
try {
|
||||
const { data: user, error } = await auth.api.getUser(token.replace('Bearer ', ''))
|
||||
if (error) throw error
|
||||
|
||||
return { user, error: null }
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return { user: null, error: err }
|
||||
}
|
||||
}
|
||||
|
||||
export const getAuth0Id = (provider: String, providerId: String): String => {
|
||||
return `${provider}|${providerId}`
|
||||
}
|
||||
|
||||
export const getIdentity = (gotrueUser: User) => {
|
||||
try {
|
||||
if (gotrueUser !== undefined && gotrueUser.identities !== undefined) {
|
||||
return { identity: gotrueUser.identities[0], error: null }
|
||||
}
|
||||
throw 'Missing identity'
|
||||
} catch (err) {
|
||||
return { identity: null, error: err }
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,8 @@ export const timeout = (ms: number) => {
|
||||
|
||||
export const getURL = () => {
|
||||
const url =
|
||||
process?.env?.SITE_URL && process.env.SITE_URL !== ''
|
||||
? process.env.SITE_URL
|
||||
process?.env?.NEXT_PUBLIC_SITE_URL && process.env.NEXT_PUBLIC_SITE_URL !== ''
|
||||
? process.env.NEXT_PUBLIC_SITE_URL
|
||||
: process?.env?.VERCEL_URL && process.env.VERCEL_URL !== ''
|
||||
? process.env.VERCEL_URL
|
||||
: 'https://app.supabase.io'
|
||||
|
||||
@@ -104,7 +104,16 @@ class StorageExplorerStore {
|
||||
initializeSupabaseClient = (serviceKey, serviceEndpoint) => {
|
||||
this.supabaseClient = createClient(
|
||||
this.normalizeSupabaseURL(`https://${serviceEndpoint}`),
|
||||
serviceKey
|
||||
serviceKey,
|
||||
{
|
||||
localStorage: {
|
||||
getItem: (key) => {
|
||||
return undefined
|
||||
},
|
||||
setItem: (key, value) => {},
|
||||
removeItem: (key) => {},
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Generated
+14182
-183
File diff suppressed because it is too large.
Load diff
+3
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "studio",
|
||||
"version": "0.0.3",
|
||||
"version": "0.0.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 8082",
|
||||
@@ -15,12 +15,13 @@
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@next/bundle-analyzer": "^12.0.4",
|
||||
"@radix-ui/react-portal": "^0.1.1",
|
||||
"@radix-ui/react-tooltip": "^0.1.6",
|
||||
"@sentry/browser": "^6.15.0",
|
||||
"@sentry/nextjs": "^6.15.0",
|
||||
"@supabase/grid": "^2.3.9",
|
||||
"@supabase/postgres-meta": "^0.26.1",
|
||||
"@supabase/react-data-grid": "^7.1.0-beta.7",
|
||||
"@supabase/supabase-js": "^1.27.0",
|
||||
"@supabase/supabase-js": "^1.28.0",
|
||||
"@supabase/ui": "^0.35.0",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
|
||||
@@ -20,7 +20,7 @@ const Error404: NextPage = ({}) => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen relative flex flex-col items-center justify-center mx-auto relative">
|
||||
<div className="w-full h-screen relative flex flex-col items-center justify-center mx-auto">
|
||||
<div className="absolute top-0 w-full max-w-7xl mx-auto pt-6 px-8 sm:px-6 lg:px-8">
|
||||
<nav className="relative flex items-center justify-between sm:h-10">
|
||||
<div className="flex items-center flex-grow flex-shrink-0 lg:flex-grow-0">
|
||||
@@ -39,7 +39,7 @@ const Error404: NextPage = ({}) => {
|
||||
</div>
|
||||
<div className="absolute">
|
||||
<Typography.Title
|
||||
className={`filter transition opacity-[5%] duration-200 ${
|
||||
className={`select-none filter transition opacity-[5%] duration-200 ${
|
||||
show404 ? 'blur-sm' : 'blur-none'
|
||||
}`}
|
||||
style={{ fontSize: '28rem' }}
|
||||
|
||||
@@ -78,9 +78,8 @@ const DocView: FC<any> = observer(({}) => {
|
||||
const swaggerUrl = data?.autoApiService?.restUrl
|
||||
const headers: any = { apikey: API_KEY }
|
||||
|
||||
if(IS_PLATFORM) data?.autoApiService?.endpoint = `https://${data?.autoApiService?.endpoint}`
|
||||
|
||||
if (API_KEY?.length > 40) headers['authorization'] = `Bearer ${API_KEY}`
|
||||
if (IS_PLATFORM) data?.autoApiService?.endpoint = `https://${data?.autoApiService?.endpoint}`
|
||||
if (API_KEY?.length > 40) headers['Authorization'] = `Bearer ${API_KEY}`
|
||||
|
||||
const { data: jsonSchema, error: jsonSchemaError } = useSWR(
|
||||
() => swaggerUrl,
|
||||
|
||||
@@ -116,6 +116,7 @@ export default class MetaStore implements IMetaStore {
|
||||
|
||||
connectionString?: string
|
||||
baseUrl: string
|
||||
queryBaseUrl: string
|
||||
excludedSchemas = [
|
||||
'auth',
|
||||
'extensions',
|
||||
@@ -132,6 +133,7 @@ export default class MetaStore implements IMetaStore {
|
||||
const { projectRef, connectionString } = options
|
||||
this.rootStore = rootStore
|
||||
this.baseUrl = `/api/pg-meta/${projectRef}`
|
||||
this.queryBaseUrl = `${API_URL}/pg-meta/${projectRef}`
|
||||
|
||||
const headers: any = {}
|
||||
if (IS_PLATFORM && connectionString) {
|
||||
@@ -166,7 +168,7 @@ export default class MetaStore implements IMetaStore {
|
||||
try {
|
||||
const headers: any = { 'Content-Type': 'application/json' }
|
||||
if (this.connectionString) headers['x-connection-encrypted'] = this.connectionString
|
||||
const url = `${this.baseUrl}/query`
|
||||
const url = `${this.queryBaseUrl}/query`
|
||||
const response = await post(url, { query: value }, { headers })
|
||||
if (response.error) throw response.error
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ export default class OpenApiStore implements IOpenApiStore {
|
||||
const restApiUrl = projectConfig.autoApiService?.restUrl
|
||||
|
||||
const response = await get<OpenAPIV2.Document>(`${restApiUrl}?apikey=${apiKey}`, {
|
||||
headers: { ...headers, apiKey: apiKey, authorization: `Bearer ${apiKey}` },
|
||||
headers: { ...headers, apiKey: apiKey, Authorization: `Bearer ${apiKey}` },
|
||||
credentials: 'omit',
|
||||
})
|
||||
if (response.error) throw response.error
|
||||
|
||||
@@ -310,3 +310,10 @@ label:not(.sbui-formlayout__label):not(.sbui-checkbox__label-container__label):n
|
||||
cursor: row-resize;
|
||||
}
|
||||
}
|
||||
|
||||
.radix-tooltip-arrow {
|
||||
polygon {
|
||||
@apply dark:fill-[#1f1f1f];
|
||||
fill: #eeeeee;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { screen, getByText } from '@testing-library/react'
|
||||
|
||||
interface SelectorOptions {
|
||||
container?: HTMLElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the toggle button given a text matcher
|
||||
*
|
||||
* Defaults to screen if container option is not provided
|
||||
*/
|
||||
export const getToggleByText = (text: string | RegExp, options: SelectorOptions = {}): HTMLElement | null => {
|
||||
const container = options?.container
|
||||
let textNode;
|
||||
if (container) {
|
||||
textNode = getByText(container as HTMLElement, text)
|
||||
}
|
||||
else {
|
||||
textNode = screen.getByText(text)
|
||||
}
|
||||
if (textNode && textNode.parentElement) {
|
||||
return textNode.parentElement.querySelector("button[class*='toggle']")
|
||||
} else {
|
||||
return textNode
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import LogPanel from 'components/ui/Logs/LogPanel'
|
||||
import LogPanel from 'components/interfaces/Settings/Logs/LogPanel'
|
||||
import { render, waitFor, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
|
||||
@@ -7,7 +7,7 @@ test('templates', async () => {
|
||||
render(<LogPanel templates={[
|
||||
{ label: 'Some option', onClick: mockFn }
|
||||
]} />)
|
||||
const search = screen.getByPlaceholderText(/Filter/)
|
||||
const search = screen.getByPlaceholderText(/Search/)
|
||||
userEvent.type(search, "12345")
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ test('templates', async () => {
|
||||
test("filter input change", async () => {
|
||||
const mockFn = jest.fn()
|
||||
render(<LogPanel onSearch={mockFn} />)
|
||||
const search = screen.getByPlaceholderText(/Filter/)
|
||||
const search = screen.getByPlaceholderText(/Search/)
|
||||
userEvent.type(search, "12345")
|
||||
expect(mockFn).toBeCalled()
|
||||
})
|
||||
@@ -49,9 +49,9 @@ test("Manual refresh", async () => {
|
||||
test("reset filters", () => {
|
||||
const mockFn = jest.fn()
|
||||
const { rerender } = render(<LogPanel showReset={false} />)
|
||||
expect(() => screen.getByText(/Reset filters/)).toThrow()
|
||||
expect(() => screen.getByText(/Clear search/)).toThrow()
|
||||
|
||||
rerender(<LogPanel showReset={true} onReset={mockFn} />)
|
||||
userEvent.click(screen.getByText(/Reset filters/))
|
||||
userEvent.click(screen.getByTitle(/Clear search/))
|
||||
expect(mockFn).toBeCalled()
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import LogTable from 'components/ui/Logs/LogTable'
|
||||
import LogTable from 'components/interfaces/Settings/Logs/LogTable'
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react'
|
||||
test('can display log data', async () => {
|
||||
render(<LogTable data={[{
|
||||
|
||||
@@ -4,50 +4,49 @@ import { get } from 'lib/common/fetch'
|
||||
|
||||
// mock the settings layout
|
||||
jest.mock('components/layouts', () => ({
|
||||
SettingsLayout: jest.fn()
|
||||
.mockImplementation(({ children }) => <div>{children}</div>)
|
||||
SettingsLayout: jest.fn().mockImplementation(({ children }) => <div>{children}</div>),
|
||||
}))
|
||||
|
||||
// mock mobx
|
||||
jest.mock('mobx-react-lite')
|
||||
import { observer } from 'mobx-react-lite'
|
||||
observer.mockImplementation(v => v)
|
||||
observer.mockImplementation((v) => v)
|
||||
|
||||
// mock the router
|
||||
jest.mock('next/router')
|
||||
import { useRouter } from 'next/router'
|
||||
useRouter.mockReturnValue({ query: { ref: "123", type: "auth" } })
|
||||
useRouter.mockReturnValue({ query: { ref: '123', type: 'auth' } })
|
||||
|
||||
// mock monaco editor
|
||||
jest.mock("@monaco-editor/react")
|
||||
import Editor, { useMonaco } from "@monaco-editor/react"
|
||||
jest.mock('@monaco-editor/react')
|
||||
import Editor, { useMonaco } from '@monaco-editor/react'
|
||||
Editor = jest.fn()
|
||||
Editor.mockImplementation((props) => {
|
||||
return (
|
||||
<textarea
|
||||
className="monaco-editor"
|
||||
onChange={e => props.onChange(e.target.value)}
|
||||
></textarea>
|
||||
<textarea className="monaco-editor" onChange={(e) => props.onChange(e.target.value)}></textarea>
|
||||
)
|
||||
})
|
||||
useMonaco.mockImplementation(v => v)
|
||||
useMonaco.mockImplementation((v) => v)
|
||||
|
||||
import { LogPage } from 'pages/project/[ref]/settings/logs/[type]'
|
||||
import { render, fireEvent, waitFor, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {getToggleByText} from "../../helpers"
|
||||
|
||||
beforeEach(() => {
|
||||
get.mockReset()
|
||||
})
|
||||
test('can display log data and metadata', async () => {
|
||||
const data = [{
|
||||
id: "seome-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {
|
||||
my_key: "something_value"
|
||||
}
|
||||
}]
|
||||
const data = [
|
||||
{
|
||||
id: 'seome-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {
|
||||
my_key: 'something_value',
|
||||
},
|
||||
},
|
||||
]
|
||||
get.mockResolvedValue({ data })
|
||||
render(<LogPage />)
|
||||
|
||||
@@ -59,15 +58,16 @@ test('can display log data and metadata', async () => {
|
||||
})
|
||||
|
||||
test('Refresh', async () => {
|
||||
|
||||
const data = [{
|
||||
id: "some-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {
|
||||
my_key: "something_value"
|
||||
}
|
||||
}]
|
||||
const data = [
|
||||
{
|
||||
id: 'some-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {
|
||||
my_key: 'something_value',
|
||||
},
|
||||
},
|
||||
]
|
||||
get.mockResolvedValueOnce({ data }).mockResolvedValueOnce({ data: [] })
|
||||
render(<LogPage />)
|
||||
|
||||
@@ -82,84 +82,98 @@ test('Refresh', async () => {
|
||||
await waitFor(() => screen.queryByText(/happened/) === null, { timeout: 1000 })
|
||||
})
|
||||
|
||||
|
||||
test("Search will trigger a log refresh", async () => {
|
||||
test('Search will trigger a log refresh', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
|
||||
if (url.includes("search_query") && url.includes("something")) {
|
||||
if (url.includes('search_query') && url.includes('something')) {
|
||||
return {
|
||||
data: [{
|
||||
id: "some-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {}
|
||||
}]
|
||||
data: [
|
||||
{
|
||||
id: 'some-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
return { data: [] }
|
||||
})
|
||||
render(<LogPage />)
|
||||
|
||||
userEvent.type(screen.getByPlaceholderText(/Filter/), "something")
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining("search_query"))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining("something"))
|
||||
}, { timeout: 1500 })
|
||||
userEvent.type(screen.getByPlaceholderText(/Search/), 'something')
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('search_query'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('something'))
|
||||
},
|
||||
{ timeout: 1500 }
|
||||
)
|
||||
|
||||
await waitFor(() => screen.getByText(/happened/), { timeout: 1000 })
|
||||
})
|
||||
|
||||
test("poll count for new messages", async () => {
|
||||
test('poll count for new messages', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
if (url.includes("count")) {
|
||||
return { data: [{ count: 3 }] }
|
||||
if (url.includes('count')) {
|
||||
return { data: [{ count: 125 }] }
|
||||
}
|
||||
return {
|
||||
data: [{
|
||||
id: "some-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {}
|
||||
}]
|
||||
data: [
|
||||
{
|
||||
id: 'some-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
})
|
||||
render(
|
||||
<LogPage />
|
||||
)
|
||||
render(<LogPage />)
|
||||
await waitFor(() => screen.queryByText(/happened/) === null)
|
||||
await waitFor(() => screen.getByText(/Load new logs/))
|
||||
// should display new logs count
|
||||
await waitFor(() => screen.getByText(/125/))
|
||||
|
||||
userEvent.click(screen.getByText(/Load new logs/))
|
||||
await waitFor(() => screen.queryByText(/Load new logs/) === null)
|
||||
userEvent.click(screen.getByText(/Refresh/))
|
||||
await waitFor(() => screen.queryByText(/125/) === null)
|
||||
await waitFor(() => screen.getByText(/happened/))
|
||||
})
|
||||
|
||||
|
||||
test("where clause will trigger a log refresh", async () => {
|
||||
test('where clause will trigger a log refresh', async () => {
|
||||
get.mockImplementation((url) => {
|
||||
|
||||
if (url.includes("where") && url.includes("something")) {
|
||||
if (url.includes('where') && url.includes('something')) {
|
||||
return {
|
||||
data: [{
|
||||
id: "some-uuid",
|
||||
timestamp: 1621323232312,
|
||||
event_message: "some event happened",
|
||||
metadata: {}
|
||||
}]
|
||||
data: [
|
||||
{
|
||||
id: 'some-uuid',
|
||||
timestamp: 1621323232312,
|
||||
event_message: 'some event happened',
|
||||
metadata: {},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
return { data: [] }
|
||||
})
|
||||
const { container } = render(<LogPage />)
|
||||
let editor = container.querySelector('.monaco-editor');
|
||||
let editor = container.querySelector('.monaco-editor')
|
||||
expect(editor).toBeFalsy()
|
||||
userEvent.click(screen.getByText("Custom query"))
|
||||
editor = container.querySelector('.monaco-editor');
|
||||
userEvent.type(editor, "metadata.field = something")
|
||||
// TODO: abstract this out into a toggle selection helper
|
||||
const toggle = getToggleByText(/via query/)
|
||||
expect(toggle).toBeTruthy()
|
||||
userEvent.click(toggle)
|
||||
await waitFor(() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining("where"))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining("metadata.field"))
|
||||
}, { timeout: 1000 })
|
||||
editor = container.querySelector('.monaco-editor')
|
||||
expect(editor).toBeTruthy()
|
||||
})
|
||||
editor = container.querySelector('.monaco-editor')
|
||||
userEvent.type(editor, 'metadata.field = something')
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('where'))
|
||||
expect(get).toHaveBeenCalledWith(expect.stringContaining('metadata.field'))
|
||||
},
|
||||
{ timeout: 1000 }
|
||||
)
|
||||
|
||||
await waitFor(() => screen.getByText(/happened/))
|
||||
})
|
||||
})
|
||||
+9
-14
@@ -6,23 +6,21 @@ This is a single repository for two different websites. Our documents were creat
|
||||
|
||||
Install the [Vercel CLI](https://vercel.com/cli).
|
||||
|
||||
You will need to run the marketing website and docs website separately.
|
||||
You will need to run the marketing website and docs website separately. To learn how to run both websites, see the [Development Guide](../DEVELOPERS.md) for more information.
|
||||
|
||||
```
|
||||
# tab 1
|
||||
make dev
|
||||
```sh
|
||||
# step 1
|
||||
cd supabase
|
||||
|
||||
# tab 2
|
||||
# step 2
|
||||
cd www
|
||||
|
||||
# step 3
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# tab 3
|
||||
cd web/spec
|
||||
make
|
||||
npm install
|
||||
npm run gen:supabase
|
||||
npm run start
|
||||
# visit website
|
||||
http://localhost:3000
|
||||
```
|
||||
|
||||
In production, this setup is deployed as two different vercel websites (`docs` and `www`)
|
||||
@@ -31,6 +29,3 @@ In production, this setup is deployed as two different vercel websites (`docs` a
|
||||
|
||||
- Referencing resources in nested folders may not work for `web` (eg `/web/static/folder/nestedfolder/asset`) may not work.
|
||||
- Possibly need to prepend all images/assets in docs site with baseUrl.
|
||||
|
||||
<!-- // we need to check this -->
|
||||
<!-- - Don't use `Link` since that adds `/new` to the links. Use plain `a` tags when possible. -->
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: 'Holiday Hackdays Winners 2021'
|
||||
description: Celebrating many amazing projects submitted to our Holiday Hackdays Hackathon.
|
||||
author: thor_schaeff
|
||||
author_url: https://github.com/thorwebdev
|
||||
author_image_url: https://github.com/thorwebdev.png
|
||||
authorURL: https://github.com/thorwebdev
|
||||
image: hackathon-winners/holiday-hackdays/holiday-hackdays-winners-og.png
|
||||
thumb: hackathon-winners/holiday-hackdays/holiday-hackdays-winners-og.png
|
||||
tags:
|
||||
- community
|
||||
- hackathon
|
||||
date: '2021-12-17'
|
||||
toc_depth: 2
|
||||
---
|
||||
|
||||
To cool down from [our latest launch week](/blog/2021/12/15/beta-november-2021-launch-week-recap) we (virtually) [sat down with y'all](https://youtu.be/TijBVcV4jXw) and started hacking, and once again we absolutely loved your hackathon submissions and are excited to send some extremely limited swag your way!
|
||||
|
||||
As always, it was challenging to pick the winners from all the great submissions! You can see all the amazing projects submitted on [madewithsupabase.com](https://www.madewithsupabase.com/holiday-hackdays). And now, without further ado, let's look at some of our favourites in more detail:
|
||||
|
||||
## Best Overall Project
|
||||
|
||||
### Winner
|
||||
|
||||
[Swappy.one](https://github.com/zernonia/swappy-one) - by [@zernonia](https://twitter.com/zernonia)
|
||||
|
||||
Zernonia is one of our [SupaSquad](https://supabase.com/docs/handbook/supasquad) members and continuously blows our minds with his creativity for the open-source projects he creates. For example, he's also the mastermind behind [madewithsupabase.com](https://www.madewithsupabase.com/), one of our favourite resources to see what y'all are building.
|
||||
|
||||
Now he can add a gold tee to his collection, and we're starting to run out of swag ideas to show our appreciation! (Send us some ideas on [Twitter](https://twitter.com/supabase)!)
|
||||
|
||||
[](https://github.com/zernonia/swappy-one)
|
||||
|
||||
### Runner Up
|
||||
|
||||
[Chivel](https://github.com/lalit2005/chivel) - by [@lalitcodes](https://twitter.com/lalitcodes) and [@ChapagainAshik](https://twitter.com/ChapagainAshik)
|
||||
|
||||
Chivel is a tool to quickly generate a landing page for your YouTube channel. For example check out this snazzy page that they've generated for the Supabase channel: [supabase.chivel.tk](https://supabase.chivel.tk/)!
|
||||
|
||||
A great idea that deserves some silver tees!
|
||||
|
||||
[](https://github.com/lalit2005/chivel)
|
||||
|
||||
## Best Realtime Project
|
||||
|
||||
### Winner
|
||||
|
||||
[rtPoll](https://github.com/emilioschepis/rtpoll) - by [@emilioschepis](https://twitter.com/emilioschepis) & brother Federico.
|
||||
|
||||
Brothers Emilio and Federico built a platform to quickly create polls and share them around. Seeing poll results is great, but you know what's even better? Exactly, seeing poll results in realtime!
|
||||
|
||||
[](https://github.com/emilioschepis/rtpoll)
|
||||
|
||||
### Runner Up
|
||||
|
||||
[e2ee-chat](https://github.com/arnu515/supabase-e2ee-chat) - by [@arnu5152](https://twitter.com/arnu5152)
|
||||
|
||||
A chat app is a fairly obvious use case for realtime syncing, but this project caught our eye because the chat is end-to-end encrypted!
|
||||
|
||||
[](https://github.com/arnu515/supabase-e2ee-chat)
|
||||
|
||||
## Best Christmas Themed
|
||||
|
||||
### Winner
|
||||
|
||||
[Santa Banter](https://github.com/Keoooo/santa-banter) - by [@AndyKeogh](https://twitter.com/AndyKeogh)
|
||||
|
||||
Not only do we love a good rhyme, but we also love Christmas themed jokes, so Andy's Santa Banter checks all the boxes. Head over there now to submit your best jokes!
|
||||
|
||||
[](https://github.com/Keoooo/santa-banter)
|
||||
|
||||
### Runner Up
|
||||
|
||||
[Holiday Sweater](https://github.com/Morel-Tech/ugly_sweater_app) - by [@mtwichel](https://twitter.com/mtwichel)
|
||||
|
||||
Ugly sweaters are an integral part of the Christmas Holidays. With Marcus' awesome Flutter app you can now vote on your favourite sweaters from around the globe!
|
||||
|
||||
[](https://github.com/Morel-Tech/ugly_sweater_app)
|
||||
|
||||
## Most Visually Pleasing
|
||||
|
||||
### Winner
|
||||
|
||||
[the get list](https://github.com/glowdex/wishlist) - by [@glowdexapp](https://github.com/glowdexapp)
|
||||
|
||||
This visually stunning website allows you to create a wishlist to share with your friends so they don't need to wreck their brains for gift ideas. Here, have a look at what I want this Christmas: [thegetlist.co/list/thorwebdev](https://www.thegetlist.co/list/thorwebdev).
|
||||
|
||||
[](https://github.com/glowdex/wishlist)
|
||||
|
||||
### Runner Up
|
||||
|
||||
[card creator](https://github.com/maggie-j-liu/card-creator) - by [maggie-j-liu](https://github.com/maggie-j-liu), [Arash](https://github.com/arashnrim), [@eiilla11](https://twitter.com/eiilla11), and [@_pranavnt](https://twitter.com/_pranavnt)
|
||||
|
||||
This team of high school hackers just keeps knocking it out of the park again. In our [Hacktoberfest Hackathon](https://supabase.com/blog/2021/10/14/hacktoberfest-hackathon-winners-2021#most-spookyfun) they already made it onto the podium. This time around they built this stunning greeting card platform that lets you send well wishes to your friends around the globe in no time. We love it!
|
||||
|
||||
[](https://youtu.be/Yx4N2bONA44)
|
||||
|
||||
## The Prizes
|
||||
|
||||
As is now tradition with Supabase Hackathons, the winners will receive an extremely limited edition gold medal shirt.
|
||||
|
||||

|
||||
|
||||
And the runner uppers will receive the same shirt but in silver. These shirts are limited to winners of the hackathon and will never be produced again. Keep them safe like your favourite NFTs!
|
||||
|
||||
## Hang out with us and the community
|
||||
|
||||
If you want to join the community and build with us, find other people using Supabase, or if you just want to chill and watch people build, come and join us in Discord!
|
||||
|
||||
[Join our Discord](https://discord.supabase.com)
|
||||
|
||||

|
||||
|
||||
### Get Started Guides
|
||||
|
||||
If you're inspired to build, check out some of the latest resources:
|
||||
|
||||
- [Build a SaaS product with Next.js, Supabase and Stripe](https://egghead.io/courses/build-a-saas-product-with-next-js-supabase-and-stripe-61f2bc20)
|
||||
- [Supabase & Sveltekit - Build Twitter in 75 minutes](https://youtu.be/mPQyckogDYc)
|
||||
- [Supabase Auth With Next.Js 12 Middleware](https://jitsu.com/blog/supabase-nextjs-middleware#what-were-going-to-build)
|
||||
- [Vue 3 & Supabase | Workout Tracker App](https://www.youtube.com/watch?v=3tF0fGkd4ho)
|
||||
- [Use Supabase Auth with Vue.js 3](https://vueschool.io/articles/vuejs-tutorials/use-supabase-auth-with-vue-js-3/)
|
||||
- [Dynamic Jamstack with Stencil and Supabase](https://ionicframework.com/blog/dynamic-jamstack-with-stencil-and-supabase)
|
||||
@@ -24,9 +24,7 @@ const BlogListItem = ({ blog }: Props) => {
|
||||
<Image
|
||||
layout="fill"
|
||||
src={
|
||||
!blog.thumb
|
||||
? `https://images.unsplash.com/photo-1569982175971-d92b01cf8694?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1`
|
||||
: `/images/blog/${blog.thumb}`
|
||||
!blog.thumb ? `/images/blog/blog-placeholder.png` : `/images/blog/${blog.thumb}`
|
||||
}
|
||||
objectFit="cover"
|
||||
className="transform duration-100 ease-in scale-100 group-hover:scale-105"
|
||||
|
||||
@@ -24,7 +24,7 @@ const Footer = (props: Props) => {
|
||||
<SectionContainer>
|
||||
<div className="xl:grid xl:grid-cols-3 xl:gap-8">
|
||||
<div className="space-y-8 xl:col-span-1">
|
||||
<Link href="#" as=" ">
|
||||
<Link href="#" as="/">
|
||||
<a>
|
||||
<img
|
||||
className="w-40"
|
||||
@@ -102,7 +102,9 @@ const Footer = (props: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-32 border-t dark:border-dark pt-8 flex justify-between">
|
||||
<p className="text-base text-gray-400 dark:text-dark-400">© Supabase Inc</p>
|
||||
<p className="mb-0 self-center text-base text-gray-400 dark:text-dark-400">
|
||||
© Supabase Inc
|
||||
</p>
|
||||
<DarkModeToggle darkMode={darkMode} updateTheme={updateTheme} />
|
||||
</div>
|
||||
</SectionContainer>
|
||||
|
||||
@@ -27,10 +27,7 @@ const Hero = () => {
|
||||
<p className="mt-3 text-base">Serverless functions coming soon</p>
|
||||
</Typography.Text>
|
||||
<Space>
|
||||
<Link
|
||||
href="https://api.supabase.io/platform/login"
|
||||
as="https://api.supabase.io/platform/login"
|
||||
>
|
||||
<Link href="https://app.supabase.io" as="https://app.supabase.io">
|
||||
<a className="mt-10">
|
||||
<Button className="py-3" size="medium">
|
||||
Start your project
|
||||
|
||||
@@ -3,15 +3,13 @@ import Link from 'next/link'
|
||||
|
||||
function TextLink({ url, label }: any) {
|
||||
return (
|
||||
<a href={url}>
|
||||
<a className="block text-sm text-gray-400 dark:text-gray-400 mt-3">
|
||||
<div className="flex items-center">
|
||||
<span>{label}</span>
|
||||
<span className="ml-2">
|
||||
<IconArrowRight size="tiny" />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<a href={url} className="block text-sm text-gray-400 dark:text-gray-400 mt-3">
|
||||
<div className="flex items-center">
|
||||
<span>{label}</span>
|
||||
<span className="ml-2">
|
||||
<IconArrowRight size="tiny" />
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,12 +50,12 @@ const Index = () => {
|
||||
</div>
|
||||
</SectionContainer>
|
||||
<SectionContainer>
|
||||
<div className="grid grid-cols-12 border dark:border-gray-600">
|
||||
<div className="grid grid-cols-12 border dark:border-gray-600 rounded-lg shadow-small">
|
||||
<div className="col-span-12 lg:col-span-5">
|
||||
<img
|
||||
src={`/brand-assets/logo-preview.jpg`}
|
||||
width="100%"
|
||||
className="object-cover h-full"
|
||||
className="object-cover h-full rounded-l-lg"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-12 lg:col-span-7 flex items-center">
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 832 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 869 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Reference in new issue
Block a user