mirror of
https://github.com/supabase/supabase.git
synced 2026-10-09 03:15:06 +03:00
Merge branch 'master' of github.com:supabase/supabase into feat/vault
This commit is contained in:
commit
2ee6350fba
52 files changed
+2900
-167
No files matched your search
@@ -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()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
Generated
+14
-2
@@ -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>
|
||||
)}
|
||||
)} */}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user