Merge branch 'master' of github.com:supabase/supabase into feat/vault

This commit is contained in:
Joshen Lim committed 2022-11-29 19:29:00 +08:00
commit 2ee6350fba
52 files changed
+2900 -167

No files matched your search

+20 -8
View File
@@ -55,31 +55,43 @@ You can enable third-party providers with the click of a button by navigating to
When using third-party providers, the [Supabase client library](/docs/reference/javascript/auth-signinwithoauth#sign-in-using-a-third-party-provider-with-redirect) redirects the user to the provider. When the third-party provider successfully authenticates the user, the provider redirects the user to the Supabase Auth callback URL where they are further redirected to the URL specified in the `redirectTo` parameter. This parameter defaults to the [`SITE_URL`](/docs/reference/auth/config#site_url). You can modify the `SITE_URL` or [add additional redirect URLs](https://app.supabase.com/project/_/auth/settings).
You can use wildcard match patterns to support preview URLs from providers like Netlify and Vercel. See the [full list of supported patterns](https://pkg.go.dev/github.com/gobwas/glob#Compile).
You can use wildcard match patterns to support preview URLs from providers like Netlify and Vercel. See the [full list of supported patterns](https://pkg.go.dev/github.com/gobwas/glob#Compile). Use [this tool](https://www.digitalocean.com/community/tools/glob?comments=true&glob=http%3A%2F%2Flocalhost%3A3000%2F%2A%2A&matches=false&tests=http%3A%2F%2Flocalhost%3A3000&tests=http%3A%2F%2Flocalhost%3A3000%2F&tests=http%3A%2F%2Flocalhost%3A3000%2F%3Ftest%3Dtest&tests=http%3A%2F%2Flocalhost%3A3000%2Ftest-test%3Ftest%3Dtest&tests=http%3A%2F%2Flocalhost%3A3000%2Ftest%2Ftest%3Ftest%3Dtest) to test your patterns.
> ⚠️ WARNING: While the "globstar" (`**`) is useful for local development and preview URLs, we recommend setting the exact redirect URL path for your site URL in production.
#### Netlify preview URLs
For deployments with Netlify, set the `SITE_URL` to your official site URL. Add the following additional redirect URLs for local development and deployment previews:
- `http://localhost:3000/*/*`
- `https://**--my_org.netlify.app/*`
- `http://localhost:3000/**`
- `https://**--my_org.netlify.app/**`
#### Vercel preview URLs
For deployments with Vercel, set the `SITE_URL` to your official site URL. Add the following additional redirect URLs for local development and deployment previews:
- `http://localhost:3000/*/*`
- `https://**vercel.app/*/*`
- `http://localhost:3000/**`
- `https://**.vercel.app/**`
Vercel provides an environment variable for the URL of the deployment called `NEXT_PUBLIC_VERCEL_URL`. See the [Vercel docs](https://vercel.com/docs/concepts/projects/environment-variables#system-environment-variables) for more details. You can use this variable to dynamically redirect depending on the environment:
```js
const getURL = () => {
let url =
process?.env?.NEXT_PUBLIC_SITE_URL ?? // Set this to your site URL in production env.
process?.env?.NEXT_PUBLIC_VERCEL_URL ?? // Automatically set by Vercel.
'http://localhost:3000/';
// Make sure to include `https://` when not localhost.
url = url.includes('http') ? url : `https://${url}`;
// Make sure to including trailing `/`.
url = url.charAt(url.length - 1) === '/' ? url : `${url}/`;
return url;
};
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github'
options: {
redirectTo: process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}`
: "http://localhost:3000"
redirectTo: getURL()
}
}
```
+1 -1
View File
@@ -85,7 +85,7 @@ Read about resetting your database password [here](/docs/guides/database/managin
## Next steps
- Read more about [Postgres](/docs/postgres/server/about)
- Read more about [Postgres](https://www.postgresql.org/about/)
- Sign in: [app.supabase.com](https://app.supabase.com)
export const Page = ({ children }) => <Layout meta={meta} children={children} />
@@ -52,7 +52,7 @@ flutter_bloc: ^8.0.0
Since the app has evolved, we also need to update our table schema. In order to store rooms data, we will add a rooms table. We will also modify the messages table to add a foreign key constraint to the rooms table so that we can tell which message belongs to which room.
We will also intorudce a `create_new_room` function, which is a [database function](https://supabase.com/docs/guides/database/functions) that handles chat room creation. It knows to create a new room if a chat room with the two users does not exist yet, or to just return the room ID if it already exists.
We will also introduce a `create_new_room` function, which is a [database function](https://supabase.com/docs/guides/database/functions) that handles chat room creation. It knows to create a new room if a chat room with the two users does not exist yet, or to just return the room ID if it already exists.
```sql
-- *** Table definitions ***
@@ -1154,7 +1154,7 @@ import 'package:my_chat_app/pages/rooms_page.dart';
import 'package:my_chat_app/utils/constants.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
/// Page to redirect users to the appropreate page depending on the initial auth state
/// Page to redirect users to the appropriate page depending on the initial auth state
class SplashPage extends StatefulWidget {
const SplashPage({Key? key}) : super(key: key);
@@ -0,0 +1,55 @@
import { useState } from 'react'
import { PageState, ConfDataContext, UserData } from './hooks/use-conf-data'
import Ticket from './ticket'
import ConfContainer from './conf-container'
import Form from './form'
import { SupabaseClient, Session } from '@supabase/supabase-js'
type Props = {
supabase: SupabaseClient
session: Session | null
defaultUserData: UserData
sharePage?: boolean
defaultPageState?: PageState
}
export default function Conf({
supabase,
session,
defaultUserData,
sharePage,
defaultPageState = 'registration',
}: Props) {
const [userData, setUserData] = useState<UserData>(defaultUserData)
const [pageState, setPageState] = useState<PageState>(defaultPageState)
return (
<ConfDataContext.Provider
value={{
supabase,
session,
userData,
setUserData,
setPageState,
}}
>
<ConfContainer>
{pageState === 'registration' && !sharePage ? (
<>
{/* <Hero /> */}
<Form />
{/* <LearnMore /> */}
</>
) : (
<Ticket
username={userData.username}
name={userData.name}
ticketNumber={userData.ticketNumber}
sharePage={sharePage}
golden={userData.golden}
/>
)}
</ConfContainer>
</ConfDataContext.Provider>
)
}
@@ -0,0 +1,11 @@
.container {
margin: auto 0;
padding: 0 var(--space-4x);
}
@media (min-width: 768px) {
.container {
margin: auto;
padding: 0 var(--space-8x);
}
}
@@ -0,0 +1,5 @@
import styles from './conf-container.module.css'
export default function ConfContainer({ children }: { children: React.ReactNode }) {
return <div className={styles.container}>{children}</div>
}
@@ -0,0 +1,246 @@
.form {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 12px;
gap: 12px;
}
.formLeft {
align-items: center;
}
.formCenter {
align-items: center;
}
@media (min-width: 1200px) {
.formLeft {
align-items: start;
}
.formCenter {
align-items: center;
}
}
.formInfo {
max-width: 520px;
}
.formInfo h3 {
font-size: 1.25em;
font-weight: 400;
}
.formInfo p {
font-size: 1.1em;
line-height: 1.4;
margin-bottom: 32px;
color: var(--lw-secondary-color);
}
@media (min-width: 1200px) {
/* .form.share-page {
justify-content: flex-start;
} */
}
.formInfoLeft {
text-align: center;
margin: 0 auto;
}
.formInfoCenter {
text-align: center;
margin: 0 auto;
}
@media (min-width: 1200px) {
.formInfoLeft {
text-align: left;
margin: inherit;
}
.formInfoCenter {
text-align: center;
margin: 0 auto;
}
}
.input {
border: none;
width: 100%;
background: transparent;
outline: none;
height: 56px;
padding-left: 15px;
padding-right: 15px;
font-size: var(--text-md);
color: #fff;
font-family: inherit;
}
@media (min-width: 768px) {
.input {
width: calc(100% - var(--space-32x));
padding-right: 0;
}
}
.input::placeholder {
color: var(--lw-secondary-color);
}
.input-label {
background-color: var(--gray);
border-radius: var(--space-2x);
border: 1px solid transparent;
transition: border-color 0.2s ease, background-color 0.2s ease;
display: block;
}
.input-label.focused {
background-color: #33373c;
}
.input-label.error {
background: red;
}
.input-label.success {
background: #0070f3;
}
.input-text {
display: flex;
align-items: center;
width: 100%;
}
.form-row {
position: relative;
max-width: 400px;
width: 100%;
}
@media (min-width: 768px) {
.form-row {
max-width: 480px;
}
}
.submit {
width: 100%;
height: 56px;
margin-top: var(--space-4x);
border-radius: var(--space-2x);
border: 1px solid var(--brand);
background: var(--brand);
cursor: pointer;
font-family: inherit;
font-size: var(--text-md);
letter-spacing: -0.02em;
outline: none;
font-weight: 500;
color: white;
transition: background-color 0.2s ease, color 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
}
.submit.generate-with-github {
display: flex;
margin-bottom: 12px;
position: relative;
}
@media (min-width: 768px) {
.submit.register {
width: 120px;
height: 40px;
margin-top: 0;
position: absolute;
border-radius: 5px;
right: var(--space-2x);
top: var(--space-2x);
}
}
.submit.loading {
cursor: default;
}
.submit.default:hover,
.submit.default:focus {
background: black;
color: var(--brand);
}
.submit.default.generate-with-github:hover path,
.submit.default.generate-with-github:focus path {
fill: var(--brand);
}
.submit.error:hover,
.submit.error:focus {
background: #000;
color: #fff;
}
.submit.default:disabled,
.submit.default:disabled:hover,
.submit.default:disabled:focus {
cursor: default;
background: var(--gray);
border-color: var(--gray);
color: #fff;
justify-content: flex-start;
overflow: hidden;
}
.submit.default.generate-with-github:disabled path,
.submit.default.generate-with-github:disabled:hover path,
.submit.default.generate-with-github:disabled:focus path {
fill: #fff;
}
.submit.default.generate-with-github.not-allowed:disabled {
cursor: not-allowed;
}
@media (min-width: 1200px) {
.form-row {
margin: 0;
}
.submit.generate-with-github {
width: 240px;
}
}
.stage-btn {
background: #323332;
border: 2px solid #34b27b;
}
.github-wrapper {
display: flex;
align-items: center;
flex-direction: column;
}
.or-divider {
width: 100%;
text-align: center;
color: var(--lw-secondary-color);
margin: var(--space-4x) 0;
}
@media (min-width: 1200px) {
.github-wrapper {
flex-direction: row;
}
.or-divider {
width: 240px;
margin: 0;
}
}
@@ -0,0 +1,233 @@
import { useState, useCallback, useEffect } 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 Captcha, { useCaptcha } from './captcha'
type FormState = 'default' | 'loading' | 'error'
export type ConfUser = {
id?: string
email?: string
ticketNumber?: number
name?: string
username?: string
createdAt?: number
golden?: boolean
}
type Props = {
sharePage?: boolean
align?: 'Left' | 'Center'
}
export default function Form({ sharePage, align = 'Center' }: Props) {
const [email, setEmail] = useState('')
const [errorMsg, setErrorMsg] = useState('')
const [errorTryAgain, setErrorTryAgain] = useState(false)
const [focused, setFocused] = useState(false)
const [formState, setFormState] = useState<FormState>('default')
const { setPageState, setUserData, session, userData, supabase } = useConfData()
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((_) => {})
setPageState('ticket')
})
}
}, [session])
async function register(email: string, token?: string): Promise<ConfUser> {
const { error } = await supabase!.from('lw6_tickets').insert({ email })
if (error) {
console.log({ error })
return {
id: 'new',
ticketNumber: 1234,
name: '',
username: '',
golden: false,
}
}
const { data } = await supabase!.from('lw6_tickets_golden').select('*').limit(1).single()
return {
id: data?.id ?? 'new',
ticketNumber: data?.ticketNumber ?? 1234,
name: data?.name ?? '',
username: data?.username ?? '',
golden: data?.golden ?? false,
}
}
const handleRegister = useCallback(
(token?: string) => {
register(email, token)
.then(async (params) => {
if (!params) {
throw new Error()
}
if (sharePage) {
const queryString = Object.keys(params)
.map(
(key) =>
`${encodeURIComponent(key)}=${encodeURIComponent(
params[key as keyof typeof params] || ''
)}`
)
.join('&')
await router.replace(`/launch-week/tickets?${queryString}`, '/launch-week/tickets')
} else {
setUserData(params)
setPageState('ticket')
}
})
.catch(async (err) => {
let message = 'Error! Please try again.'
setErrorMsg(message)
setFormState('error')
})
},
[email, router, setPageState, setUserData, sharePage]
)
const onSubmit = useCallback(
(e: React.FormEvent) => {
e.preventDefault()
if (formState === 'default') {
setFormState('loading')
if (isCaptchaEnabled) {
// return executeCaptcha()
}
return handleRegister()
} else {
setFormState('default')
}
},
[formState, isCaptchaEnabled, handleRegister]
)
const onTryAgainClick = useCallback((e: React.MouseEvent) => {
e.preventDefault()
setFormState('default')
setErrorTryAgain(true)
// resetCaptcha()
}, [])
useEmailQueryParam('email', setEmail)
return (
<>
<div
className={cn(
styleUtils.appear,
styleUtils['appear-fifth'],
[styles.formInfo, styles[`formInfo${align}`]].join(' ')
)}
>
<h3>Get a ticket</h3>
<p>
A few of the lucky attendees will get a limited edition Supabase goodie bag. Make sure you
don’t skip your chance.
</p>
</div>
{formState === 'error' ? (
<div
className={cn(styles.form, {
[styles['share-page']]: sharePage,
})}
>
<div className={styles['form-row']}>
<div className={cn(styles['input-label'], styles.error)}>
<div className={cn(styles.input, styles['input-text'])}>{errorMsg}</div>
<button
type="button"
className={cn(styles.submit, styles.register, styles.error)}
onClick={onTryAgainClick}
>
Try Again
</button>
</div>
</div>
</div>
) : (
<form
className={cn(styles.form, styles[`form${align}`], {
[styles['share-page']]: sharePage,
[styleUtils.appear]: !errorTryAgain,
[styleUtils['appear-fifth']]: !errorTryAgain && !sharePage,
[styleUtils['appear-third']]: !errorTryAgain && sharePage,
})}
onSubmit={onSubmit}
>
<div className={styles['form-row']}>
<label
htmlFor="email-input-field"
className={cn(styles['input-label'], {
[styles.focused]: focused,
})}
>
<input
className={styles.input}
autoComplete="off"
type="email"
id="email-input-field"
value={email}
onChange={(e) => setEmail(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Enter email to register free"
aria-label="Your email address"
required
/>
</label>
<button
type="submit"
className={cn(styles.submit, styles.register, styles[formState])}
disabled={formState === 'loading'}
>
{formState === 'loading' ? <LoadingDots size={4} /> : <>Register</>}
</button>
</div>
{/* <Captcha ref={captchaRef} onVerify={handleRegister} /> */}
</form>
)}
</>
)
}
@@ -0,0 +1,30 @@
import { Session, SupabaseClient } from '@supabase/supabase-js'
import { createContext, useContext } from 'react'
export type PageState = 'registration' | 'ticket'
export type UserData = {
id?: string
ticketNumber?: number
username?: string
name?: string
golden?: boolean
}
type ConfDataContextType = {
supabase: SupabaseClient
session: Session | null
userData: UserData
setUserData: React.Dispatch<React.SetStateAction<UserData>>
setPageState: React.Dispatch<React.SetStateAction<PageState>>
}
export const ConfDataContext = createContext<ConfDataContextType | null>(null)
export default function useConfData() {
const result = useContext(ConfDataContext)
if (!result) {
throw new Error()
}
return result
}
@@ -0,0 +1,32 @@
import { useEffect } from 'react'
import { useRouter } from 'next/router'
/**
* If `paramName` exists in query string, then call `setEmail()` with the value
* and delete it from the URL.
*/
export default function useEmailQueryParam(
paramName: string,
setEmail: (email: string) => unknown
) {
const router = useRouter()
useEffect(() => {
if ('URLSearchParams' in window) {
const { search, pathname } = window.location
const params = new URLSearchParams(search)
const email = params.get(paramName)
if (email) {
setEmail(email)
params.delete(paramName)
const newSearch = params.toString()
const newAsPath = pathname + (newSearch ? `?${newSearch}` : '')
const newPathname = router.pathname + (newSearch ? `?${newSearch}` : '')
history.replaceState(
{ url: newPathname, as: newAsPath, options: { shallow: true } },
'',
newAsPath
)
}
}
}, [setEmail, router.pathname, paramName])
}
@@ -0,0 +1,48 @@
.loading {
display: inline-flex;
align-items: center;
--loading-dots-height: auto;
--loading-dots-size: 2px;
height: var(--loading-dots-height);
}
.loading .spacer {
margin-right: var(--space-3x);
}
.loading span {
animation-name: blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
width: var(--loading-dots-size);
height: var(--loading-dots-size);
border-radius: 50%;
background-color: var(--accents-6);
display: inline-block;
margin: 0 1px;
}
.loading.reverse span {
background-color: var(--accents-2);
}
.loading span:nth-of-type(2) {
animation-delay: 0.2s;
}
.loading span:nth-of-type(3) {
animation-delay: 0.4s;
}
@keyframes blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
@@ -0,0 +1,27 @@
import { toPixels as px } from '~/lib/helpers'
import cn from 'classnames'
import styles from './loading-dots.module.css'
interface Props {
size?: number
height?: number | string
reverse?: boolean
children?: React.ReactNode
}
export default function LoadingDots({ size = 2, height, children, reverse }: Props) {
return (
<span
className={cn(styles.loading, { [styles.reverse]: reverse })}
style={{
['--loading-dots-height' as string]: height ? px(height) : undefined,
['--loading-dots-size' as string]: size !== 2 ? px(size) : undefined,
}}
>
{children && <div className={styles.spacer}>{children}</div>}
<span />
<span />
<span />
</span>
)
}
@@ -0,0 +1,70 @@
.button {
border-radius: var(--space-2x);
border: 1px solid #fff;
color: #000;
background: #fff;
cursor: pointer;
font-family: inherit;
font-size: var(--text-md);
letter-spacing: -0.02em;
font-weight: 500;
outline: none;
transition: background-color 0.2s ease;
display: grid;
grid-template-columns: 24px 1fr;
text-align: center;
align-items: center;
padding: 0 var(--space-4x);
height: 55px;
width: 100%;
max-width: 400px;
margin: var(--space-2x) auto;
}
.loading {
grid-template-columns: 1fr;
justify-items: center;
}
@media (min-width: 768px) {
.button {
width: 200px;
margin: 0 6px;
}
}
.button:hover,
.button:focus {
background: black;
color: #fff;
}
.first {
animation-delay: 2s;
}
.second {
animation-delay: 2.2s;
}
.third {
animation-delay: 2.4s;
}
:global(.ticket-generated) .first {
animation-delay: 0.6s;
}
:global(.ticket-generated) .second {
animation-delay: 0.8s;
}
:global(.ticket-generated) .third {
animation-delay: 1s;
}
.linkedin-button {
display: none !important;
}
@media (min-width: 768px) {
.linkedin-button {
display: grid !important;
}
}
@@ -0,0 +1,90 @@
import { useState, useRef, useEffect } from 'react'
import cn from 'classnames'
import { SITE_URL, TWEET_TEXT, TWEET_TEXT_GOLDEN } from '~/lib/constants'
// import IconTwitter from '~/components/LaunchWeek/Ticket/icons/icon-twitter'
// import IconLinkedin from '~/components/LaunchWeek/Ticket/icons/icon-linkedin'
// import IconDownload from '~/components/LaunchWeek/Ticket/icons/icon-download'
import LoadingDots from './loading-dots'
import styleUtils from './utils.module.css'
import styles from './ticket-actions.module.css'
import useConfData from '~/components/LaunchWeek/Ticket//hooks/use-conf-data'
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}`)
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])
return (
<>
<a
className={cn(styles.button, styleUtils.appear, styles.first, 'icon-button')}
href={tweetUrl}
rel="noopener noreferrer"
target="_blank"
>
{/* <IconTwitter width={24} /> Tweet it! */}
</a>
<a
className={cn(
styles.button,
styleUtils.appear,
styles.second,
'icon-button',
// LinkedIn Share widget doesn’t work on mobile
styles['linkedin-button']
)}
href={linkedInUrl}
rel="noopener noreferrer"
target="_blank"
>
{/* <IconLinkedin width={20} /> Share on LinkedIn */}
</a>
<a
className={cn(styles.button, styleUtils.appear, styles.third, 'icon-button', {
[styles.loading]: loading,
})}
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"
>
{loading ? <LoadingDots size={4} /> : <>{/* <IconDownload width={24} /> Download */}</>}
</a>
</>
)
}
@@ -0,0 +1,190 @@
.wrapper {
color: var(--lw-secondary-color);
display: flex;
align-items: center;
justify-content: center;
animation-delay: 2.6s;
flex-direction: column;
}
:global(.ticket-generated) .wrapper {
animation-delay: 1.2s;
}
.label {
width: 100%;
margin-bottom: var(--space-2x);
}
.label-wrapper {
display: flex;
align-items: center;
max-width: 400px;
width: 100%;
justify-content: space-between;
}
@media (min-width: 768px) {
.wrapper {
flex-direction: row;
}
.label {
width: auto;
margin-right: 10px;
margin-bottom: 0px;
}
.label-wrapper {
width: auto;
}
}
.field {
position: relative;
border-radius: var(--space-2x);
overflow: hidden;
background: var(--gray);
}
@media (min-width: 768px) {
.field {
padding-right: 50px;
}
.field.desktop-copy-disabled {
padding-right: 0px;
}
}
.mobile-copy {
margin-bottom: 10px;
display: flex;
}
.mobile-copy-disabled.mobile-copy-disabled {
display: none;
}
.mobile-copy * {
color: var(--lw-secondary-color);
transition: color 0.2s ease;
}
.mobile-copy:hover * {
color: #fff;
transition: color 0.2s ease;
}
.field * {
color: var(--lw-secondary-color);
transition: color 0.2s ease;
}
.field:hover * {
color: #fff;
transition: color 0.2s ease;
}
.url {
width: calc(100vw - 36px);
user-select: all;
overflow-x: scroll;
white-space: nowrap;
display: block;
padding: var(--space-2x) 14px;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
@media (min-width: 436px) {
.url {
width: 400px;
}
}
@media (min-width: 768px) {
.url {
width: 400px;
}
.mobile-copy {
margin-bottom: 0;
display: none;
}
}
.url::-webkit-scrollbar {
display: none;
}
.fade {
position: absolute;
background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0%, #252729 100%);
top: 0;
bottom: 0;
right: 0px;
width: 50px;
z-index: 1;
display: block;
border-radius: var(--space-2x);
pointer-events: none;
}
.desktop-copy.desktop-copy-disabled {
display: none;
}
@media (min-width: 768px) {
.fade {
right: 50px;
}
.fade.desktop-copy-disabled {
right: 0px;
}
}
.copied {
display: flex;
z-index: 2;
align-items: center;
justify-content: flex-end;
opacity: 0;
transition: opacity 0.2s ease !important;
}
.copied.visible {
opacity: 1;
}
.copy-button {
background: none;
outline: none;
border: none;
cursor: pointer;
z-index: 2;
border-radius: var(--space-2x);
width: 40px;
display: flex;
align-items: center;
justify-content: center;
margin-right: -10px;
}
@media (min-width: 768px) {
.copied {
position: absolute;
right: 50px;
top: 0px;
bottom: 0px;
background: linear-gradient(90deg, rgba(37, 39, 41, 0%) 0px, #252729 40px);
padding-left: 50px;
}
.copy-button {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
margin-right: 0px;
width: 50px;
}
}
@@ -0,0 +1,109 @@
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'
type Props = {
username: string
}
export default function TicketCopy({ username }: Props) {
const [fadeOpacity, setFadeOpacity] = useState(1)
const [scrolling, setScrolling] = useState(false)
const [copyEnabled, setCopyEnabled] = useState(false)
const [copied, setCopied] = useState(false)
const scrollRef = useRef<HTMLSpanElement>(null)
const buttonRef = useRef<HTMLButtonElement>(null)
const url = `${SITE_URL}/tickets/${username}`
useEffect(() => {
if (navigator.clipboard) {
setCopyEnabled(true)
}
}, [])
const copiedText = (
<span
className={cn(styles.copied, {
[styles.visible]: copied,
})}
>
Copied!
</span>
)
const copyButton = (
<button
type="button"
className={styles['copy-button']}
ref={buttonRef}
onClick={() => {
navigator.clipboard.writeText(url).then(() => {
setCopied(true)
setTimeout(() => {
setCopied(false)
}, 2000)
})
}}
>
{/* <IconCopy /> */}
</button>
)
return (
<div className={cn(styles.wrapper, styleUtils.appear)}>
<div className={styles['label-wrapper']}>
<div className={styles.label}>Your ticket URL:</div>
<div
className={cn(styles['mobile-copy'], {
[styles['mobile-copy-disabled']]: !copyEnabled,
})}
>
{copiedText}
{copyButton}
</div>
</div>
<div
className={cn(styles.field, {
[styles['desktop-copy-disabled']]: !copyEnabled,
})}
>
<span
className={styles.url}
ref={scrollRef}
onScroll={() => {
if (!scrolling) {
setScrolling(true)
const animationFrame = requestAnimationFrame(() => {
const scrollableWidth =
(scrollRef.current?.scrollWidth || 0) - (scrollRef.current?.clientWidth || 0)
setFadeOpacity(
(scrollableWidth - (scrollRef.current?.scrollLeft || 0)) / (scrollableWidth || 1)
)
cancelAnimationFrame(animationFrame)
setScrolling(false)
})
}
}}
>
{url}
</span>
<span
className={cn(styles.fade, {
[styles['desktop-copy-disabled']]: !copyEnabled,
})}
style={{ opacity: fadeOpacity }}
/>
<div
className={cn(styles['desktop-copy'], styleUtils['hide-on-mobile'], {
[styles['desktop-copy-disabled']]: !copyEnabled,
})}
>
{copiedText}
{copyButton}
</div>
</div>
</div>
)
}
@@ -0,0 +1,65 @@
.githubIcon {
margin-left: 12px;
margin-right: var(--space-4x);
display: inline-flex;
}
.checkIcon {
position: absolute;
right: var(--space-4x);
display: inline-flex;
}
.generateWithGithub {
display: flex;
align-items: center;
font-weight: 500;
}
.stageIcon {
position: absolute;
left: var(--space-4x);
display: inline-flex;
}
.form-row {
margin-left: auto;
margin-right: auto;
max-width: 600px;
}
.description {
color: var(--secondary-color);
margin: 0;
text-align: center;
margin-left: var(--space-4x);
}
@media (min-width: 768px) {
.form-row {
margin-bottom: 20px;
}
}
@media (min-width: 1200px) {
.form-row {
margin: 0;
}
.description {
text-align: left;
}
.generateWithGithub {
width: 100%;
}
}
.learn-more {
color: #fff;
}
.learn-more:hover {
text-decoration: underline;
color: #fff;
}
@@ -0,0 +1,103 @@
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` },
})
}}
>
<div className={cn(formStyles['form-row'], ticketFormStyles['form-row'])}>
<div className={cn(formStyles['github-wrapper'])}>
<button
type="submit"
className={cn(
formStyles.submit,
formStyles['generate-with-github'],
formStyles[formState]
)}
disabled={formState === 'loading' || Boolean(username)}
onClick={() => {
// if (formRef && formRef.current && isMobileOrTablet()) {
// scrollTo(formRef.current, formRef.current.offsetHeight);
// }
}}
>
<div className={ticketFormStyles.generateWithGithub}>
<span className={ticketFormStyles.githubIcon}>
{/* <GithubIcon color="#fff" size={24} /> */}
</span>
{formState === 'loading' ? (
<LoadingDots size={4} />
) : (
username || 'Generate with GitHub'
)}
</div>
{username ? (
<span className={ticketFormStyles.checkIcon}>
{/* <CheckIcon color="#fff" size={24} /> */}
</span>
) : null}
</button>
<p className={ticketFormStyles.description}>Only public info will be used.</p>
</div>
</div>
</form>
)
}
@@ -0,0 +1,17 @@
.background {
width: 2000px;
height: 1000px;
background: #000;
color: #fff;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.page {
width: 1700px;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 'Noto Sans', 'Noto Sans JP',
'Noto Sans KR', 'Noto Sans SC', 'Noto Sans TC', 'Noto Sans HK', sans-serif;
}
@@ -0,0 +1,37 @@
import { useRouter } from 'next/router'
import Head from 'next/head'
import TicketVisual from './ticket-visual'
import styles from './ticket-image.module.css'
export default function TicketImage() {
const { query } = useRouter()
if (query.ticketNumber) {
return (
<div className={styles.background}>
<div className={styles.page}>
<Head>
<link rel="preconnect" href="https://fonts.gstatic.com" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&Noto+Sans+HK:wght@700&family=Noto+Sans+JP:wght@700&family=Noto+Sans+KR:wght@700&family=Noto+Sans+SC:wght@700&family=Noto+Sans+TC:wght@700&family=Noto+Sans:wght@700&display=swap"
rel="stylesheet"
/>
</Head>
<TicketVisual
size={1700 / 650}
username={query.username ? query.username.toString() : undefined}
ticketNumber={parseInt(query.ticketNumber.toString(), 10)}
name={
query.name
? query.name?.toString()
: query.username
? query.username.toString()
: undefined
}
golden={query.golden ? true : false}
/>
</div>
</div>
)
}
return <></>
}
@@ -0,0 +1,93 @@
.info {
display: flex;
flex-direction: column;
gap: 0px;
margin-bottom: 64px;
padding: 32px;
}
@media (min-width: 768px) {
.info {
/* display: grid;
grid-template-columns: 1fr 2fr;
gap: calc(var(--space-4x) * var(--size));
font-size: calc(1em * var(--size)); */
gap: calc(8px * var(--size));
padding: 0;
margin-bottom: calc(32px * var(--size));
margin-left: calc(160px * var(--size));
}
}
.logo {
/* margin-bottom: var(--space-6x); */
/* max-width: 96px; */
font-size: 18px;
}
/* @media (min-width: 768px) {
.logo {
margin-bottom: 0;
font-size: calc(16px * var(--size));
}
} */
.date {
/* text-transform: uppercase; */
font-size: calc(18px * var(--size));
color: var(--lw-secondary-color);
}
.date-golden {
color: var(--gold-primary);
}
@media (min-width: 768px) {
.date {
margin-bottom: 0;
line-height: 1.15;
font-size: calc(20px * var(--size));
}
}
.created-by {
display: flex;
align-items: center;
color: var(--accents-4);
}
@media (min-width: 768px) {
.created-by {
margin-right: var(--space-4x);
}
}
.created-by-text {
white-space: nowrap;
margin-right: var(--space);
}
/* .created-by-logo {
height: calc(16px * var(--size));
display: inline-flex;
} */
.url {
margin-bottom: var(--space-12x);
font-size: calc(18px * var(--size));
color: var(--lw-secondary-color);
}
.url-golden {
color: var(--gold-primary);
}
@media (min-width: 768px) {
.url {
margin-bottom: 0;
font-size: calc(20px * var(--size));
line-height: 1.15;
}
}
@@ -0,0 +1,20 @@
import styles from './ticket-info.module.css'
import cn from 'classnames'
import { DATE, SITE_URL } from '~/lib/constants'
const siteUrl = new URL(SITE_URL)
const siteUrlForTicket = `${siteUrl.host}${siteUrl.pathname}`.replace(/\/$/, '')
export default function TicketInfo({
logoTextSecondaryColor = 'var(--accents-5)',
golden = false,
}) {
return (
<div className={styles.info}>
<div className={cn(styles.date, { [styles['date-golden']]: golden })}>
<div>{DATE}</div>
</div>
<div className={cn(styles.url, { [styles['url-golden']]: golden })}>{siteUrlForTicket}</div>
</div>
)
}
@@ -0,0 +1,34 @@
// NOTE: When you export SVG from Figma, you must:
// - Update width and height as 100%
// - Change id to "mobile-..."
// - Change url(#) to url(#mobile-)
// This is because if a page has two same IDs it will fail.
export default function TicketMonoMobile({ golden = false }: { golden?: boolean }) {
const frameColor = golden ? '#F2C94C' : '#3fcf8e'
const perforationColor = golden ? 'var(--gold-accent)' : '#252729'
return (
<svg
width="100%"
height="100%"
viewBox="0 0 330 560"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
{/* <path
fillRule="evenodd"
clipRule="evenodd"
d="M2.9193e-06 540C3.40212e-06 551.046 8.95431 560 20 560L138 560C138 545.088 150.088 533 165 533C179.912 533 192 545.088 192 560L310 560C321.046 560 330 551.046 330 540L330 20C330 8.95427 321.046 -1.40334e-05 310 -1.35505e-05L192 -8.39259e-06C192 14.9117 179.912 27 165 27C150.088 27 138 14.9117 138 -6.03217e-06L20 -8.74228e-07C8.95428 -3.91405e-07 -2.41646e-05 8.95428 -2.36041e-05 20L2.9193e-06 540Z"
fill={frameColor}
/> */}
{/* <path
fillRule="evenodd"
clipRule="evenodd"
d="M5 539C5 547.837 12.1634 555 21 555L133.388 555C135.789 539.702 149.028 528 165 528C180.972 528 194.211 539.702 196.612 555L309 555C317.837 555 325 547.837 325 539L325 21C325 12.1634 317.837 4.99999 309 4.99999L196.612 4.99999C194.211 20.2981 180.972 32 165 32C149.028 32 135.789 20.2982 133.388 4.99999L21 5C12.1634 5 4.99998 12.1635 4.99998 21L5 539Z"
fill="black"
/> */}
<path d="M326 446H5" stroke={perforationColor} strokeDasharray="6 6" />
</svg>
)
}
@@ -0,0 +1,23 @@
export default function TicketMono({ golden = false }: { golden?: boolean }) {
const frameColor = golden ? '#F2C94C' : '#252729'
const perforationColor = golden ? 'var(--gold-accent)' : '#252729'
return (
<svg
width="100%"
height="100%"
viewBox="0 0 650 330"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
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>
)
}
@@ -0,0 +1,14 @@
type Props = {
number: number
}
export default function TicketNumber({ number }: Props) {
const numDigits = `${number}`.length
const prefix = `000000`.slice(numDigits)
return (
<>
№ {prefix}
{number}
</>
)
}
@@ -0,0 +1,155 @@
.profile {
display: flex;
align-items: center;
}
.image {
width: 60px;
height: 60px;
border-radius: 50%;
border: 1px solid #333;
}
@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));
}
}
.name {
font-size: 24px;
display: inline-block;
line-height: 1.15;
letter-spacing: -0.02em;
margin: 0;
}
.name-golden {
color: var(--gold-primary);
}
@media (min-width: 768px) {
.name {
font-size: calc(var(--space-8x) * var(--size));
margin-bottom: 5px;
}
}
.username {
display: inline-block;
font-size: calc(1em * var(--size));
color: var(--lw-secondary-color);
display: flex;
align-items: center;
margin: 0;
}
.username-golden {
color: var(--gold-secondary);
}
.githubIcon {
margin-right: calc(5px * var(--size));
display: inline-flex;
}
.githubIcon-golden svg {
color: var(--gold-secondary);
opacity: 0.5;
}
.empty-icon {
display: block;
background: linear-gradient(135deg, #b4b6b8, #7f8183);
width: 60px;
}
@media (min-width: 768px) {
.empty-icon {
width: calc(80px * var(--size));
}
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.skeleton {
display: flex;
align-items: center;
border-radius: 5px;
}
.skeleton.loaded {
width: unset !important;
}
.skeleton:not(.wrapper):not(.show) {
display: none;
}
.wrapper:not(.show)::before {
content: none;
}
.skeleton:not(.wrapper):not(.loaded) {
border-radius: var(--space);
background-image: linear-gradient(270deg, #252729, #34383d, #34383d, #252729);
background-size: 200% 100%;
animation: loading 2s ease-in-out infinite;
}
.wrapper {
position: relative;
}
.wrapper::before {
content: '';
position: absolute;
top: -1px;
left: -1px;
right: 0;
bottom: 0;
border-radius: var(--space);
z-index: 100;
background-image: linear-gradient(270deg, #111111, #333333, #333333, #111111);
background-size: 200% 100%;
animation: loading 2s ease-in-out infinite;
}
.inline {
display: inline-block !important;
}
.rounded,
.rounded.wrapper::before {
width: 62px;
height: 62px;
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;
}
}
@@ -0,0 +1,69 @@
// 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, styles['empty-icon'])}>{/* <IconAvatar /> */}</span>
)}
</span>
<div className={styles.text}>
<p
className={cn(
styles.name,
{ [styles['name-blank']]: !username },
{ [styles['name-golden']]: golden }
)}
>
<span
className={cn(styles.skeleton, styles.wrapper, {
[styles.show]: ticketGenerationState === 'loading',
})}
>
{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={cn(styles.githubIcon, { [styles['githubIcon-golden']]: golden })}>
{/* <GithubIcon
color={golden ? 'var(--gold-primary)' : 'var(--secondary-color)'}
size={20 * size}
/> */}
</span>
{username || <>username</>}
</span>
</p>
</div>
</div>
)
}
@@ -0,0 +1,173 @@
.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);
border-radius: 16px;
background-color: rgba(0, 0, 0, 1);
-webkit-backdrop-filter: blur(5px);
backdrop-filter: blur(5px);
/* background: linear-gradient(#fff000, var(--gold-secondary)); */
border: 1px solid rgba(255, 255, 0, 0.629);
animation: blank-svg-shadow 2s ease-in-out infinite alternate;
}
.visual--gold {
background-color: gold;
/* border: 10px solid;
border-image-slice: 1;
border-width: 2px;
border-radius: 16px; */
background: linear-gradient(#fff000, var(--gold-secondary));
border: 1px solid rgba(255, 255, 0, 0.629);
animation: svg-shadow 2s ease-in-out infinite alternate;
/* filter: drop-shadow(2.45223px 8.5828px 73.5669px #FFBD14); */
/* border-image-source: linear-gradient(to left, var(--gold-secondary), var(--gold-accent)); */
}
/* .svg-shadow {
} */
@keyframes svg-shadow {
from {
filter: drop-shadow(0 0 5px #fff) drop-shadow(0 0 15px var(--gold-primary))
drop-shadow(0 0 20px var(--gold-accent));
}
to {
filter: drop-shadow(0 0 20px #fff) drop-shadow(0 0 10px var(--gold-accent))
drop-shadow(0 0 40px var(--gold-primary));
}
}
@keyframes blank-svg-shadow {
from {
filter: drop-shadow(0 0 5px #fff) drop-shadow(0 0 15px #12f28d) drop-shadow(0 0 20px #12f28d);
}
to {
filter: drop-shadow(0 0 20px #fff) drop-shadow(0 0 10px #12f28d) drop-shadow(0 0 40px #12f28d);
}
}
.horizontal-ticket {
display: none;
}
.vertical-ticket {
display: flex;
}
@media (min-width: 768px) {
.horizontal-ticket {
display: flex;
}
.vertical-ticket {
display: none;
}
}
.logo {
z-index: 1;
position: absolute;
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));
}
}
.profile {
z-index: 1;
position: absolute;
top: 90px;
width: 100%;
height: 100%;
padding: 46px 29px;
}
@media (min-width: 768px) {
.profile {
top: calc(48px * var(--size));
padding: calc(44px * var(--size)) calc(160px * var(--size)) calc(44px * var(--size))
calc(63px * var(--size));
}
}
.info {
z-index: 1;
position: absolute;
bottom: 0;
width: 100%;
height: 100%;
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-weight: bold;
font-size: calc(40px * var(--size));
line-height: 1;
width: calc(330px * var(--size));
text-align: center;
}
.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);
}
}
@@ -0,0 +1,83 @@
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(' ')}
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/launchweek-logo--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']}>
<div
className={cn(styles['ticket-number'], { [styles['ticket-number-golden']]: golden })}
>
<TicketNumber number={ticketNumber} />
</div>
</div>
)}
</div>
</>
)
}
@@ -0,0 +1,161 @@
.hero {
font-size: 30px;
line-height: 1.15;
letter-spacing: -0.05em;
font-weight: 400;
margin: 0;
text-align: center;
}
@media (min-width: 768px) {
.hero {
font-size: 30px;
line-height: 1;
/* font-weight: 800; */
/* max-width: 800px; */
/* margin-left: auto;
margin-right: auto; */
}
}
.description {
font-size: 20px;
line-height: 1.4;
color: var(--lw-secondary-color);
text-align: center;
margin: 30px auto;
max-width: 350px;
}
@media (min-width: 768px) {
.description {
font-size: 24px;
max-width: 415px;
}
}
.ticket-text {
margin-bottom: 30px;
}
.ticket-layout {
display: grid;
grid-template-columns: 1fr;
gap: 45px;
margin: auto;
padding: var(--space-4x);
padding-top: 64px;
padding-bottom: 64px;
}
@media (min-width: 1200px) {
.ticket-layout {
padding-top: 0;
padding-bottom: 0;
}
}
.ticket-share-layout {
align-items: center;
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%;
transform: translateZ(0) translateX(-50%) rotate(90deg);
}
.ticket-visual:after {
left: 50%;
bottom: -7.5%;
transform: translateZ(0) translateX(-50%) rotate(90deg);
}
@media (min-width: 768px) {
.ticket-visual {
width: 650px;
}
.ticket-visual:before {
top: 50%;
left: -7%;
transform: translateZ(0) translateY(-50%);
}
.ticket-visual:after {
top: 50%;
left: 96%;
transform: translateZ(0) translateY(-50%);
}
}
.ticket-actions {
margin-top: 30px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.ticket-copy {
margin-top: 30px;
}
.ticket-actions-placeholder {
height: 280px;
}
@media (min-width: 768px) {
.ticket-actions {
flex-direction: row;
}
.ticket-actions-placeholder {
height: 157px;
}
}
@media (min-width: 1200px) {
.ticket-layout {
grid-template-columns: 1fr 1fr;
}
.hero {
font-size: 48px;
text-align: left;
}
.description {
text-align: left;
margin: 30px 0;
}
}
.ticket-visual-wrapper {
border-radius: calc(16px * var(--size));
width: 100%;
}
@media screen and (min-width: 1280px) {
.ticket-layout {
padding: 0 40px;
}
}
@@ -0,0 +1,132 @@
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])
return (
<div
className={cn(styles['ticket-layout'], {
[styles['ticket-share-layout']]: sharePage,
})}
>
<div ref={divRef}>
<div className={styles['ticket-text']}>
<h2 className={cn(styles.hero, styleUtils.appear, styleUtils['appear-first'])}>
{sharePage ? (
name ? (
<>{name}’s Ticket</>
) : (
<>{SITE_NAME}</>
)
) : golden ? (
<>
You won a golden ticket! <br /> Claim it now!
</>
) : (
<>
You're in. <br /> Make it unique.
</>
)}
</h2>
<p className={cn(styles.description, styleUtils.appear, styleUtils['appear-second'])}>
{sharePage ? (
<>
Join {name ?? 'us'} on {DATE}.
</>
) : golden ? (
<>Claim your ticket with GitHub and Tweet it to redeem your swag pack!</>
) : (
<>
Generate a unique ticket image with <br className={styleUtils['hide-on-mobile']} />
your GitHub profile.
</>
)}
</p>
</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>
<div className={styles['ticket-actions']}>
<TicketActions username={username} golden={golden} />
</div>
<div className={styles['ticket-copy']}>
<TicketCopy username={username} />
</div>
</div>
) : (
<div className={styles['ticket-actions-placeholder']} />
)}
</>
)}
</div>
</div>
)
}
@@ -0,0 +1,76 @@
.appear {
opacity: 0;
animation: appear 0.8s cubic-bezier(0.1, 0, 0.175, 1) forwards;
}
.appear-first {
animation-delay: 0.4s;
}
.appear-second {
animation-delay: 0.8s;
}
.appear-third {
animation-delay: 1.2s;
}
.appear-fourth {
animation-delay: 1.6s;
}
.appear-fifth {
animation-delay: 2s;
}
.appear-sixth {
animation-delay: 2.4s;
}
@keyframes appear {
to {
opacity: 1;
}
}
@media (min-width: 1200px) {
.hide-on-desktop {
display: none;
}
}
.show-on-mobile {
display: block;
}
.hide-on-mobile {
display: none;
}
@media (min-width: 768px) {
.show-on-mobile {
display: none;
}
.hide-on-mobile {
display: block;
}
}
.hide-on-tablet {
display: block;
}
.show-on-tablet {
display: none;
}
@media (max-width: 1199px) and (min-width: 768px) {
.show-on-tablet {
display: block;
}
.hide-on-tablet {
display: none;
}
}
+13
View File
@@ -1,2 +1,15 @@
export const APP_NAME = 'Supabase'
export const DESCRIPTION = 'The Open Source Alternative to Firebase.'
// Launch Weeek
export const SAMPLE_TICKET_NUMBER = 1234
export const SITE_URL = process.env.NEXT_PUBLIC_VERCEL_URL
? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}/launch-week`
: 'http://localhost:3000/launch-week'
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 12 8:00 PT | 11:00 ET'
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 SITE_NAME = 'Supabase'
+18
View File
@@ -11,3 +11,21 @@ export const generateReadingTime = (text: string) => {
export function capitalize(word: string) {
return word[0].toUpperCase() + word.substring(1).toLowerCase()
}
export function isMobileOrTablet() {
// https://stackoverflow.com/a/8876069/114157
const viewportWidth = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0)
return viewportWidth < 1200
}
// Convert numbers or strings to pixel value
// Helpful for styled-jsx when using a prop
// height: ${toPixels(height)}; (supports height={20} and height="20px")
export const toPixels = (value: string | number) => {
if (typeof value === 'number') {
return `${value}px`
}
return value
}
-14
View File
@@ -17,20 +17,6 @@ module.exports = [
source: '/docs/:path*',
destination: `${process.env.NEXT_PUBLIC_DOCS_URL}/:path*`,
},
// rewrite to keep existing ticket urls working
{
source: '/launch-week/tickets/:path',
destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}/tickets/:path`,
},
// rewrite to move ticket website to another path
{
source: '/launch-week-register',
destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}`,
},
{
source: '/launch-week-register/:path*',
destination: `${process.env.NEXT_PUBLIC_LAUNCHWEEKSITE_URL}/:path*`,
},
{
source: '/new-docs',
destination: `${process.env.NEXT_PUBLIC_REFERENCE_DOCS_URL}`,
+2 -1
View File
@@ -29,7 +29,7 @@
"gray-matter": "^4.0.2",
"markdown-toc": "^1.2.0",
"marked": "^4.0.12",
"next": "latest",
"next": "12.1.0",
"next-compose-plugins": "^2.2.1",
"next-mdx-remote": "^4.1.0",
"next-seo": "^4.20.0",
@@ -55,6 +55,7 @@
"ui": "*",
"use-debounce": "^7.0.1",
"uuid": "^8.3.2",
"vanilla-tilt": "1.7.0",
"yup": "^0.32.11"
},
"devDependencies": {
@@ -0,0 +1,5 @@
import TicketImage from '~/components/LaunchWeek/Ticket/ticket-image'
export default function TicketOnlyPage() {
return <TicketImage />
}
@@ -0,0 +1,114 @@
import { NextSeo } from 'next-seo'
import { GetStaticProps, GetStaticPaths } from 'next'
import Error from 'next/error'
import DefaultLayout from '~/components/Layouts/Default'
import SectionContainer from '~/components/Layouts/SectionContainer'
import TicketContainer from '~/components/LaunchWeek/Ticket/TicketContainer'
import { SITE_URL, SAMPLE_TICKET_NUMBER } from '~/lib/constants'
import { createClient } from '@supabase/supabase-js'
import { useState } from 'react'
import { FastGlobOptionsWithoutCwd } from 'globby'
type Props = {
username: string | null
name: string | null
ticketNumber: number | null
golden: boolean
}
const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_SECRET!
)
export default function TicketShare({ username, ticketNumber, name, golden }: Props) {
const [supabase] = useState(() =>
createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
)
const description = 'Supabase Launch Week 6 | 12-16 Dec 2022'
if (!ticketNumber) {
return <Error statusCode={404} />
}
const ogImageUrl = `https://obuldanrptloktxcffvn.functions.supabase.co/launchweek-ticket-og?username=${encodeURIComponent(
username ?? ''
)}`
return (
<>
<NextSeo
title={`${name ? name + '’s' : 'Get your'} #SupaLaunchWeek Ticket`}
openGraph={{
title: `${name ? name + '’s' : 'Get your'} #SupaLaunchWeek Ticket`,
description: description,
url: `${SITE_URL}/tickets/${username}`,
images: [
{
url: ogImageUrl,
},
],
}}
/>
<div className="launch-week-gradientBg"></div>
<DefaultLayout>
<SectionContainer className="flex flex-col gap-8 !pb-0 md:gap-16 lg:gap-16">
<img
src="/images/launchweek/launchweek-logo--light.svg"
className="md:40 w-28 dark:hidden lg:w-48"
/>
<img
src="/images/launchweek/launchweek-logo--dark.svg"
className="md:40 hidden w-28 dark:block lg:w-48"
/>
<TicketContainer
supabase={supabase}
session={null}
defaultUserData={{
username: username || undefined,
name: name || '',
ticketNumber,
golden,
}}
sharePage
/>
</SectionContainer>
</DefaultLayout>
</>
)
}
export const getStaticProps: GetStaticProps<Props> = async ({ params }) => {
const username = params?.username?.toString() || null
let name: string | null | undefined
let ticketNumber: number | null | undefined
let golden = false
if (username) {
const { data: user } = await supabaseAdmin!
.from('lw6_tickets_golden')
.select('name, ticketNumber, golden')
.eq('username', username)
.single()
name = user?.name
ticketNumber = user?.ticketNumber
golden = user?.golden ?? false
}
return {
props: {
username: ticketNumber ? username : null,
usernameFromParams: username || null,
name: ticketNumber ? name || username || null : null,
ticketNumber: ticketNumber || SAMPLE_TICKET_NUMBER,
golden,
},
revalidate: 5,
}
}
export const getStaticPaths: GetStaticPaths = async () => {
return Promise.resolve({
paths: [],
fallback: 'blocking',
})
}
@@ -0,0 +1,67 @@
import { NextSeo } from 'next-seo'
import DefaultLayout from '~/components/Layouts/Default'
import SectionContainer from '~/components/Layouts/SectionContainer'
import TicketContainer from '~/components/LaunchWeek/Ticket/TicketContainer'
import { SITE_URL } from '~/lib/constants'
import { useRouter } from 'next/router'
import { createClient, Session } from '@supabase/supabase-js'
import { useState, useEffect } from 'react'
export default function TicketHome() {
const [supabase] = useState(() =>
createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!)
)
const [session, setSession] = useState<Session | null>(null)
const description = 'Supabase Launch Week 6 | 12-16 Dec 2022'
const { query } = useRouter()
const ticketNumber = query.ticketNumber?.toString()
const defaultUserData = {
id: query.id?.toString(),
ticketNumber: ticketNumber ? parseInt(ticketNumber, 10) : undefined,
name: query.name?.toString(),
username: query.username?.toString(),
}
useEffect(() => {
supabase.auth.onAuthStateChange((event, session) => {
setSession(session)
})
}, [])
return (
<>
<NextSeo
title={`Get your #SupaLaunchWeek Ticket`}
openGraph={{
title: `Get your #SupaLaunchWeek Ticket`,
description: description,
url: `${SITE_URL}/tickets`,
images: [
{
url: `https://supabase.com/images/launchweek/og-image.jpg`, // TODO
},
],
}}
/>
<div className="launch-week-gradientBg"></div>
<DefaultLayout>
<SectionContainer className="flex flex-col gap-8 !pb-0 md:gap-16 lg:gap-16">
<img
src="/images/launchweek/launchweek-logo--light.svg"
className="md:40 w-28 dark:hidden lg:w-48"
/>
<img
src="/images/launchweek/launchweek-logo--dark.svg"
className="md:40 hidden w-28 dark:block lg:w-48"
/>
<TicketContainer
supabase={supabase}
session={session}
defaultUserData={defaultUserData}
defaultPageState={query.ticketNumber ? 'ticket' : 'registration'}
/>
</SectionContainer>
</DefaultLayout>
</>
)
}
+139 -132
View File
File diff suppressed because it is too large. Load diff
+12
View File
@@ -84,6 +84,12 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/launch-week/ticket-image</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/legal/dpa</loc>
<changefreq>weekly</changefreq>
@@ -902,6 +908,12 @@
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/blog/flutter-authentication-and-authorization-with-rls</loc>
<changefreq>weekly</changefreq>
<changefreq>0.5</changefreq>
</url>
<url>
<loc>https://supabase.com/alternatives/supabase-vs-firebase</loc>
<changefreq>weekly</changefreq>
+1
View File
@@ -373,6 +373,7 @@ launch week
:root {
--auth-ui-card-height: 50vh;
--lw-secondary-color: #8a8f98;
}
.glow-area {
@@ -0,0 +1,24 @@
# Writing functions using Oak Server Middleware
This example shows how you can write functions using Oak server middleware (https://oakserver.github.io/oak/)
## Run locally
```bash
supabase functions serve oak-server
```
Use cURL or Postman to make a POST request to http://localhost:54321/functions/v1/oak-server.
```
curl --location --request POST 'http://localhost:54321/functions/v1/oak-server' \
--header 'Authorization: Bearer YOUR_TOKEN' \
--header 'Content-Type: application/json' \
--data-raw '{ "name": "John Doe" }'
```
## Deploy
```bash
supabase functions deploy oak-server
```
@@ -0,0 +1,25 @@
import { Application, Router } from 'https://deno.land/x/oak/mod.ts'
const router = new Router()
router
// Note: path will be prefixed with function name
.get('/oak-server', (context) => {
context.response.body = 'This is an example Oak server running on Edge Functions!'
})
.post('/oak-server/greet', (context) => {
// Note: request body will be streamed to the function as chunks, set limit to 0 to fully read it.
const result = context.request.body({ type: 'json', limit: 0 })
const body = await result.value
const name = body.name || 'you'
context.response.body = { msg: `Hey ${name}!` }
})
.get('/oak-server/redirect', (context) => {
context.response.redirect('https://www.example.com')
})
const app = new Application()
app.use(router.routes())
app.use(router.allowedMethods())
await app.listen({ port: 8000 })
+14 -2
View File
@@ -228,7 +228,7 @@
"gray-matter": "^4.0.2",
"markdown-toc": "^1.2.0",
"marked": "^4.0.12",
"next": "latest",
"next": "12.1.0",
"next-compose-plugins": "^2.2.1",
"next-mdx-remote": "^4.1.0",
"next-seo": "^4.20.0",
@@ -254,6 +254,7 @@
"ui": "*",
"use-debounce": "^7.0.1",
"uuid": "^8.3.2",
"vanilla-tilt": "1.7.0",
"yup": "^0.32.11"
},
"devDependencies": {
@@ -35302,6 +35303,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"node_modules/vanilla-tilt": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/vanilla-tilt/-/vanilla-tilt-1.7.0.tgz",
"integrity": "sha512-u9yUhpSasFeqQCuiTon+RSb0aHzcj9stvWVXQIswzKL5oG491lkYk7U1GmhOAEZt7yPT6EiYZRJhIh2MFBncOA=="
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -64972,6 +64978,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"vanilla-tilt": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/vanilla-tilt/-/vanilla-tilt-1.7.0.tgz",
"integrity": "sha512-u9yUhpSasFeqQCuiTon+RSb0aHzcj9stvWVXQIswzKL5oG491lkYk7U1GmhOAEZt7yPT6EiYZRJhIh2MFBncOA=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@@ -65997,7 +66008,7 @@
"gray-matter": "^4.0.2",
"markdown-toc": "^1.2.0",
"marked": "^4.0.12",
"next": "latest",
"next": "12.1.0",
"next-compose-plugins": "^2.2.1",
"next-mdx-remote": "^4.1.0",
"next-seo": "^4.20.0",
@@ -66028,6 +66039,7 @@
"ui": "*",
"use-debounce": "^7.0.1",
"uuid": "^8.3.2",
"vanilla-tilt": "1.7.0",
"yup": "^0.32.11"
},
"dependencies": {
@@ -1,4 +1,4 @@
import { IconArchive, IconDatabase, IconKey } from 'ui'
import { IconArchive, IconCode, IconDatabase, IconKey } from 'ui'
export const CANCELLATION_REASONS = [
'Pricing',
@@ -71,4 +71,25 @@ export const USAGE_BASED_PRODUCTS = [
},
],
},
{
title: 'Functions',
icon: <IconCode className="dark:text-scale-100" size={16} strokeWidth={2} />,
features: [
{
key: 'func_count',
attribute: 'func_count',
title: 'Function Count',
units: 'absolute',
costPerUnit: 0.1,
},
{
key: 'func_invocations',
attribute: 'func_invocations',
title: 'Function Invocations',
units: 'absolute',
costPerUnit: 0.000002,
}
],
},
]
@@ -146,7 +146,9 @@ const ProjectUsage: FC<Props> = ({ projectRef }) => {
/>
) : (
<span>
{feature.units === 'bytes' ? formatBytes(usageValue) : ''}
{feature.units === 'bytes'
? formatBytes(usageValue)
: usageValue}
</span>
)}
</td>
@@ -63,7 +63,9 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
</span>
{/* Project Dropdown */}
<ProjectDropdown />
{showOverUsageBadge && (
{/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */}
{/* {showOverUsageBadge && (
<div className="ml-2">
<Link href={`/project/${ref}/settings/billing/subscription`}>
<a>
@@ -71,7 +73,7 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
</a>
</Link>
</div>
)}
)} */}
</>
)}
</>
+7
View File
@@ -144,6 +144,13 @@ export const METRICS = [
// provider: 'daily-stats',
// category: METRIC_CATEGORIES.API_AUTH,
// },
{
key: 'total_auth_billing_period_mau',
label: 'Auth Monthly Active User',
provider: 'daily-stats',
category: METRIC_CATEGORIES.API_AUTH,
},
{
key: 'total_auth_ingress',
label: 'Auth Ingress',
+2 -1
View File
@@ -28,7 +28,8 @@ const Home: NextPageWithLayout = () => {
</div>
<div className="mx-6">
{projectTier !== undefined && <OveragesBanner minimal tier={projectTier} />}
{/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */}
{/* {projectTier !== undefined && <OveragesBanner minimal tier={projectTier} />} */}
</div>
{project?.status === PROJECT_STATUS.INACTIVE && <ProjectPausedState project={project} />}
@@ -57,7 +57,8 @@ const Settings: FC<SettingsProps> = ({ project }) => {
return (
<div className="container max-w-4xl space-y-8 p-4">
{projectTier !== undefined && <OveragesBanner tier={projectTier} />}
{/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */}
{/* {projectTier !== undefined && <OveragesBanner tier={projectTier} />} */}
<div className="space-y-2">
<h4 className="text-lg">Invoices</h4>
@@ -58,7 +58,8 @@ const Settings: FC<SettingsProps> = ({ project }) => {
return (
<div className="container max-w-4xl space-y-8 p-4">
{projectTier !== undefined && <OveragesBanner tier={projectTier} />}
{/* [Joshen TODO] Temporarily hidden until usage endpoint is sorted out */}
{/* {projectTier !== undefined && <OveragesBanner tier={projectTier} />} */}
<Subscription
loading={loading}
project={project}