mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
chore: state cleanup to use reducer
Migrate logic to centralized reducer for ease of debugging
This commit is contained in:
1 parent
22605cfe15
commit
49bac3eded
13 files changed
+402
-488
No files matched your search
@@ -1,8 +1,6 @@
|
||||
import { useMemo } from 'react'
|
||||
import TicketCanvas from '~/components/LaunchWeek/14/TicketCanvas'
|
||||
import DefaultLayout from '~/components/Layouts/Default'
|
||||
import SectionContainer from '~/components/Layouts/SectionContainer'
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { TicketLayout, TicketLayoutCanvas } from '~/components/LaunchWeek/14/TicketLayout'
|
||||
import {
|
||||
TicketHeader,
|
||||
@@ -18,39 +16,21 @@ import {
|
||||
TicketClaimMessage,
|
||||
} from '~/components/LaunchWeek/14/TicketClaim'
|
||||
import { ActionButton } from '~/components/LaunchWeek/14/ActionButton'
|
||||
import { usePartymode } from '~/components/LaunchWeek/14/use-partymode'
|
||||
import { usePartymode } from '~/components/LaunchWeek/14/hooks/use-partymode'
|
||||
import { TicketShare } from '~/components/LaunchWeek/14/TicketShare'
|
||||
import TicketCopy from '~/components/LaunchWeek/14/TicketCopy'
|
||||
import TicketActions from '~/components/LaunchWeek/14/TicketActions'
|
||||
import { useRegistration } from '~/components/LaunchWeek/14/use-registration'
|
||||
import { useRegistration } from '~/components/LaunchWeek/14/hooks/use-registration'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
|
||||
const dates = [new Date('2025-03-31T07:00:00.000-07:00')]
|
||||
|
||||
export const LwView = () => {
|
||||
const partymode = usePartymode()
|
||||
|
||||
const { userData, ticketState, setTicketState } = useConfData()
|
||||
const [state] = useConfData()
|
||||
|
||||
const hasTicket = ticketState === 'ticket'
|
||||
const isLoading = ticketState === 'loading'
|
||||
const isRegistering = ticketState === 'registration'
|
||||
|
||||
const register = useRegistration({
|
||||
onRegister: () => setTicketState('ticket'),
|
||||
onError: () => setTicketState('registration'),
|
||||
})
|
||||
|
||||
console.log({ hasTicket, isLoading, isRegistering })
|
||||
|
||||
const user = useMemo(
|
||||
() => ({
|
||||
id: userData.id,
|
||||
name: userData.name,
|
||||
ticketNumber: userData.ticket_number,
|
||||
}),
|
||||
[userData.id, userData.name, userData.ticket_number]
|
||||
)
|
||||
const playmodeRTChannel = useMemo(() => ({}), [])
|
||||
const register = useRegistration()
|
||||
|
||||
return (
|
||||
<DefaultLayout className='font-["Departure_Mono"]'>
|
||||
@@ -62,15 +42,8 @@ export const LwView = () => {
|
||||
<TicketHeaderDate />
|
||||
</TicketHeader>
|
||||
<TicketLayoutCanvas>
|
||||
<TicketCanvas
|
||||
visible={hasTicket}
|
||||
secret={userData.secret}
|
||||
platinum={userData.platinum}
|
||||
playmodeRTChannel={playmodeRTChannel}
|
||||
user={user}
|
||||
startDate={dates[0]}
|
||||
/>
|
||||
{isRegistering && (
|
||||
<TicketCanvas />
|
||||
{state.claimFormState === 'visible' && (
|
||||
<TicketClaim>
|
||||
<TicketClaimLogo />
|
||||
<TicketClaimContent>
|
||||
@@ -96,7 +69,7 @@ export const LwView = () => {
|
||||
</TicketClaimContent>
|
||||
</TicketClaim>
|
||||
)}
|
||||
{hasTicket && (
|
||||
{state.ticketVisibility && (
|
||||
<TicketShare>
|
||||
<TicketCopy />
|
||||
<TicketActions />
|
||||
|
||||
@@ -5,12 +5,14 @@ import { useTheme } from 'next-themes'
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Button, cn } from 'ui'
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
import { LW_URL, LW14_TWEET_TEXT, LW14_TWEET_TEXT_PLATINUM, LW14_TWEET_TEXT_SECRET } from '~/lib/constants'
|
||||
import supabase from './supabase'
|
||||
|
||||
export default function TicketActions() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const { userData, supabase } = useConfData()
|
||||
const [state] = useConfData()
|
||||
const userData = state.userTicketData
|
||||
const { platinum, username, metadata, secret: hasSecretTicket } = userData
|
||||
const [_imgReady, setImgReady] = useState(false)
|
||||
const [_loading, setLoading] = useState(false)
|
||||
@@ -44,8 +46,6 @@ export default function TicketActions() {
|
||||
}, [downloadUrl])
|
||||
|
||||
const handleShare = async (social: 'twitter' | 'linkedin') => {
|
||||
if (!supabase) return
|
||||
|
||||
setTimeout(async () => {
|
||||
if (social === 'twitter') {
|
||||
await supabase
|
||||
|
||||
@@ -4,81 +4,77 @@ import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import SceneRenderer, { BaseScene } from './utils/SceneRenderer'
|
||||
import TicketScene from './scenes/TicketScene'
|
||||
import TunnelScene from './scenes/TunnelScene'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
|
||||
interface TicketCanvasProps {
|
||||
visible: boolean
|
||||
secret?: boolean
|
||||
platinum?: boolean
|
||||
user: {
|
||||
id?: string
|
||||
name?: string
|
||||
ticketNumber?: number
|
||||
}
|
||||
startDate: Date
|
||||
playmodeRTChannel: unknown
|
||||
className?: string
|
||||
onUpgrade?: () => void
|
||||
}
|
||||
|
||||
const TicketCanvas = ({
|
||||
visible,
|
||||
secret,
|
||||
platinum,
|
||||
user,
|
||||
startDate,
|
||||
playmodeRTChannel,
|
||||
className,
|
||||
onUpgrade,
|
||||
}: TicketCanvasProps) => {
|
||||
const initialSceneDataRef = useRef({ visible, secret, platinum, user, startDate })
|
||||
const sceneRef = useRef<TicketScene | null>(null)
|
||||
const tunnelRef = useRef<TunnelScene | null>(null)
|
||||
const initQueue = useRef<{ init: Promise<void>; renderer: SceneRenderer }[]>([])
|
||||
const [ticketState, setState] = useState(initialSceneDataRef.current)
|
||||
const [state, dispatch] = useConfData()
|
||||
const initialSceneDataRef = useRef({
|
||||
visible: state.ticketVisibility,
|
||||
secret: state.userTicketData.secret,
|
||||
platinum: state.userTicketData.platinum,
|
||||
user: {
|
||||
id: state.userTicketData.id,
|
||||
name: state.userTicketData.name,
|
||||
ticketNumber: state.userTicketData.ticket_number,
|
||||
},
|
||||
})
|
||||
|
||||
const setup = useCallback((container: HTMLElement) => {
|
||||
const uuid = Math.random().toString(36).substring(7)
|
||||
const setup = useCallback(
|
||||
(container: HTMLElement) => {
|
||||
const uuid = Math.random().toString(36).substring(7)
|
||||
|
||||
const sceneRenderer = new SceneRenderer(container, initQueue.current, uuid)
|
||||
const sceneRenderer = new SceneRenderer(container, initQueue.current, uuid)
|
||||
|
||||
const initPromise = sceneRenderer.init(async () => {
|
||||
const scene = new TicketScene({
|
||||
defaultVisible: initialSceneDataRef.current.visible,
|
||||
defaultSecret: initialSceneDataRef.current.secret,
|
||||
defaultPlatinum: initialSceneDataRef.current.platinum,
|
||||
user: initialSceneDataRef.current.user,
|
||||
startDate: initialSceneDataRef.current.startDate,
|
||||
onSeatChartButtonClicked: () => {
|
||||
scene.showBackSide()
|
||||
scene.upgradeToSecret()
|
||||
},
|
||||
onGoBackButtonClicked: () => {
|
||||
scene.showFrontSide()
|
||||
},
|
||||
const initPromise = sceneRenderer.init(async () => {
|
||||
dispatch({ type: 'TICKET_LOADING_START' })
|
||||
const scene = new TicketScene({
|
||||
defaultVisible: initialSceneDataRef.current.visible,
|
||||
defaultSecret: initialSceneDataRef.current.secret,
|
||||
defaultPlatinum: initialSceneDataRef.current.platinum,
|
||||
user: initialSceneDataRef.current.user,
|
||||
onSeatChartButtonClicked: () => {
|
||||
scene.showBackSide()
|
||||
scene.upgradeToSecret()
|
||||
},
|
||||
onGoBackButtonClicked: () => {
|
||||
scene.showFrontSide()
|
||||
},
|
||||
})
|
||||
|
||||
const tunnel = new TunnelScene({
|
||||
defaultVisible: true,
|
||||
})
|
||||
await sceneRenderer.activateScene(scene, true)
|
||||
await sceneRenderer.activateScene(tunnel)
|
||||
sceneRef.current = scene
|
||||
tunnelRef.current = tunnel
|
||||
dispatch({ type: 'TICKET_LOADING_SUCCESS' })
|
||||
})
|
||||
|
||||
const tunnel = new TunnelScene({
|
||||
defaultVisible: true,
|
||||
})
|
||||
await sceneRenderer.activateScene(scene, true)
|
||||
await sceneRenderer.activateScene(tunnel)
|
||||
sceneRef.current = scene
|
||||
tunnelRef.current = tunnel
|
||||
})
|
||||
initQueue.current.push({ init: initPromise, renderer: sceneRenderer })
|
||||
|
||||
initQueue.current.push({ init: initPromise, renderer: sceneRenderer })
|
||||
|
||||
return sceneRenderer
|
||||
}, [])
|
||||
return sceneRenderer
|
||||
},
|
||||
[dispatch]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (sceneRef.current) {
|
||||
sceneRef.current.setVisible(visible)
|
||||
sceneRef.current.setTicketNumber(user.ticketNumber ?? 0)
|
||||
sceneRef.current.setUserName(user.name ?? '')
|
||||
sceneRef.current.setVisible(state.ticketVisibility)
|
||||
sceneRef.current.setTicketNumber(state.userTicketData.ticket_number ?? 0)
|
||||
sceneRef.current.setUserName(state.userTicketData.name ?? '')
|
||||
sceneRef.current.reloadTextures()
|
||||
}
|
||||
}, [visible, sceneRef, user.name, user.ticketNumber])
|
||||
}, [state.ticketVisibility, state.userTicketData.name, state.userTicketData.ticket_number])
|
||||
|
||||
const { containerRef } = useThreeJS(setup)
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Image from 'next/image'
|
||||
import { ReactNode } from 'react'
|
||||
import logo from './assets/logo.png'
|
||||
import { ActionButton } from './ActionButton'
|
||||
|
||||
export const TicketClaim = ({ children }: { children: ReactNode }) => {
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState, useRef } from 'react'
|
||||
import { LW_URL } from '~/lib/constants'
|
||||
import { Check, Copy } from 'lucide-react'
|
||||
import useConfData from '../hooks/use-conf-data'
|
||||
import useConfData from './hooks/use-conf-data'
|
||||
|
||||
export default function TicketCopy() {
|
||||
const { userData } = useConfData()
|
||||
const [state] = useConfData()
|
||||
const userData = state.userTicketData
|
||||
const { username, platinum, secret } = userData
|
||||
const [copied, setCopied] = useState(false)
|
||||
const buttonRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { useRouter } from 'next/router'
|
||||
import { LW_URL } from '~/lib/constants'
|
||||
import { Button } from 'ui'
|
||||
import { useKey } from 'react-use'
|
||||
|
||||
import { useCommandMenuOpen } from 'ui-patterns'
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
type FormState = 'default' | 'loading' | 'error'
|
||||
|
||||
export default function TicketForm() {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [formState, setFormState] = useState<FormState>('default')
|
||||
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
|
||||
SupabaseClient['channel']
|
||||
> | null>(null)
|
||||
const [errorMsg] = useState('')
|
||||
const { supabase, session, setUserData, setTicketState, userData } = useConfData()
|
||||
const router = useRouter()
|
||||
|
||||
// Triggered on session
|
||||
async function fetchOrCreateUser() {
|
||||
if (supabase && session?.user && !userData.id) {
|
||||
const username = session.user.user_metadata.user_name
|
||||
const name = session.user.user_metadata.full_name
|
||||
const email = session.user.email
|
||||
const userId = session.user.id
|
||||
|
||||
if (!userData.id) {
|
||||
await supabase
|
||||
.from('tickets')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
launch_week: 'lw13',
|
||||
email,
|
||||
name,
|
||||
username,
|
||||
metadata: { theme: resolvedTheme },
|
||||
referred_by: router.query?.referral ?? null,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
.then(({ data, error }: any) => {
|
||||
console.log(data, error, username, userData)
|
||||
fetchUser({ error, username })
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUser = async ({ error, username }: any) => {
|
||||
if (!supabase) return
|
||||
|
||||
// If error because of duplicate email, ignore and proceed, otherwise sign out.
|
||||
if (error && error?.code !== '23505') {
|
||||
setFormState('error')
|
||||
return supabase.auth.signOut()
|
||||
}
|
||||
|
||||
const { data } = await supabase
|
||||
.from('tickets_view')
|
||||
.select('*')
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
.single()
|
||||
.throwOnError()
|
||||
|
||||
if (data) 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}?came_from_signup=true`).catch((_) => {})
|
||||
await fetch(`/api-v2/ticket-og?username=${username}`)
|
||||
|
||||
if (!realtimeChannel) {
|
||||
const channel = supabase
|
||||
.channel('changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'tickets',
|
||||
filter: `username=eq.${username}`,
|
||||
},
|
||||
(payload: any) => {
|
||||
const platinum = !!payload.new.shared_on_twitter && !!payload.new.shared_on_linkedin
|
||||
const secret = !!payload.new.game_won_at
|
||||
setUserData({
|
||||
...payload.new,
|
||||
platinum,
|
||||
secret,
|
||||
})
|
||||
}
|
||||
)
|
||||
.subscribe()
|
||||
setRealtimeChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrCreateUser()
|
||||
|
||||
return () => {
|
||||
// Cleanup realtime subscription on unmount
|
||||
realtimeChannel?.unsubscribe()
|
||||
}
|
||||
}, [session])
|
||||
|
||||
const isCommandMenuOpen = useCommandMenuOpen()
|
||||
useKey('t', () => !isCommandMenuOpen && handleGithubSignIn(), {}, [isCommandMenuOpen])
|
||||
|
||||
async function handleGithubSignIn() {
|
||||
if (formState !== 'default') {
|
||||
setFormState('default')
|
||||
return
|
||||
}
|
||||
|
||||
setFormState('loading')
|
||||
setTicketState('loading')
|
||||
|
||||
const redirectTo = `${LW_URL}${userData.username ? '?referral=' + userData.username : ''}`
|
||||
|
||||
supabase?.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: {
|
||||
redirectTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return formState === 'error' ? (
|
||||
<div className="">
|
||||
<div>{errorMsg}</div>
|
||||
<Button
|
||||
type="secondary"
|
||||
onClick={() => {
|
||||
setFormState('default')
|
||||
setTicketState('registration')
|
||||
}}
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-10 items-start justify-center relative z-20 pointer-events-auto">
|
||||
<Button
|
||||
size="small"
|
||||
disabled={formState === 'loading' || Boolean(session)}
|
||||
onClick={handleGithubSignIn}
|
||||
loading={formState === 'loading'}
|
||||
type="default"
|
||||
className="sm:pl-1"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div className="relative h-6 w-6 border rounded bg-surface-75 mr-2 uppercase hidden sm:flex items-center justify-center">
|
||||
T
|
||||
</div>
|
||||
Claim Ticket
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import { Session, SupabaseClient } from '@supabase/supabase-js'
|
||||
import { createContext, Dispatch, useContext, useReducer } from 'react'
|
||||
|
||||
/**
|
||||
* This is copy of shared use-conf-data.ts. For laynch week 14 we need different ticket states.
|
||||
* To not break the existing functionality, we are creating a new context and hook.
|
||||
*/
|
||||
|
||||
export type TicketState = 'registration' | 'ticket-visible' | 'loading' | 'ticket-loading'
|
||||
|
||||
export type UserTicketData = {
|
||||
id?: string
|
||||
email?: string
|
||||
username?: string
|
||||
name?: string
|
||||
ticket_number?: number
|
||||
platinum?: boolean
|
||||
golden?: boolean
|
||||
referrals?: number
|
||||
bg_image_id?: number
|
||||
role?: string
|
||||
company?: string
|
||||
location?: string
|
||||
metadata?: {
|
||||
role?: string
|
||||
company?: string
|
||||
location?: string
|
||||
hasSecretTicket?: boolean
|
||||
hasSharedSecret?: boolean
|
||||
hideAvatar?: boolean
|
||||
hideMetadata?: boolean
|
||||
theme?: string
|
||||
}
|
||||
shared_on_twitter?: string
|
||||
shared_on_linkedin?: string
|
||||
secret?: boolean
|
||||
}
|
||||
|
||||
type LwAction =
|
||||
| { type: 'USER_TICKET_FETCH_STARTED' }
|
||||
| { type: 'USER_TICKET_FETCH_SUCCESS'; payload: UserTicketData }
|
||||
| { type: 'USER_TICKET_FETCH_ERROR'; payload: Error }
|
||||
| { type: 'USER_TICKET_UPDATED'; payload: UserTicketData }
|
||||
| { type: 'SESSION_UPDATED'; payload: Session | null }
|
||||
| { type: 'TICKET_LOADING_START' }
|
||||
| { type: 'TICKET_LOADING_SUCCESS' }
|
||||
| { type: 'TICKET_LOADING_ERROR'; payload?: Error }
|
||||
|
||||
// Define state interface
|
||||
interface LwState {
|
||||
userTicketData: UserTicketData
|
||||
ticketState: TicketState
|
||||
session: Session | null
|
||||
userTicketDataState: 'unloaded' | 'loading' | 'error' | 'loaded'
|
||||
userTicketDataError: Error | null
|
||||
ticketLoadingState: 'unloaded' | 'loading' | 'error' | 'loaded'
|
||||
ticketVisibility: boolean
|
||||
claimFormState: 'initial' | 'visible' | 'hidden'
|
||||
}
|
||||
|
||||
export const lwReducer = (state: LwState, action: LwAction): LwState => {
|
||||
console.log('action', action, state)
|
||||
switch (action.type) {
|
||||
case 'SESSION_UPDATED':
|
||||
return {
|
||||
...state,
|
||||
session: action.payload,
|
||||
// Show claim form if session is not available. Form triggers authentication flow.
|
||||
claimFormState: !action.payload ? 'visible' : 'hidden',
|
||||
}
|
||||
case 'USER_TICKET_FETCH_STARTED':
|
||||
return { ...state, userTicketDataState: 'loading', userTicketDataError: null }
|
||||
case 'USER_TICKET_FETCH_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
userTicketData: action.payload,
|
||||
ticketVisibility: action.payload !== null && state.ticketLoadingState === 'loaded',
|
||||
}
|
||||
case 'USER_TICKET_FETCH_ERROR':
|
||||
return {
|
||||
...state,
|
||||
userTicketDataState: 'error',
|
||||
userTicketDataError: action.payload,
|
||||
}
|
||||
case 'USER_TICKET_UPDATED':
|
||||
return {
|
||||
...state,
|
||||
userTicketData: action.payload,
|
||||
userTicketDataState: Boolean(action.payload.id) ? 'loaded' : 'unloaded',
|
||||
userTicketDataError: null,
|
||||
}
|
||||
case 'TICKET_LOADING_START':
|
||||
return {
|
||||
...state,
|
||||
ticketLoadingState: 'loading',
|
||||
ticketVisibility: false,
|
||||
}
|
||||
case 'TICKET_LOADING_SUCCESS':
|
||||
return {
|
||||
...state,
|
||||
ticketLoadingState: 'loaded',
|
||||
ticketVisibility: Boolean(state.userTicketData.id),
|
||||
}
|
||||
case 'TICKET_LOADING_ERROR':
|
||||
return {
|
||||
...state,
|
||||
ticketLoadingState: 'error',
|
||||
ticketVisibility: false,
|
||||
claimFormState: 'visible',
|
||||
}
|
||||
default:
|
||||
action satisfies never
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
export const Lw14ConfDataContext = createContext<[LwState, Dispatch<LwAction>] | null>(null)
|
||||
|
||||
export const Lw14ConfDataProvider = ({
|
||||
children,
|
||||
initState,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
initState?: Partial<LwState>
|
||||
}) => {
|
||||
const state = useReducer(lwReducer, {
|
||||
userTicketData: {},
|
||||
ticketState: 'loading',
|
||||
session: null,
|
||||
ticketLoadingState: 'unloaded',
|
||||
ticketVisibility: false,
|
||||
userTicketDataState: 'unloaded',
|
||||
userTicketDataError: null,
|
||||
claimFormState: 'initial',
|
||||
...initState,
|
||||
})
|
||||
|
||||
return <Lw14ConfDataContext.Provider value={state}>{children}</Lw14ConfDataContext.Provider>
|
||||
}
|
||||
|
||||
export default function useLw14ConfData() {
|
||||
const result = useContext(Lw14ConfDataContext)
|
||||
if (!result) {
|
||||
throw new Error('useLw14ConfData must be used within a Lw14ConfDataProvider')
|
||||
}
|
||||
return result
|
||||
}
|
||||
File renamed without changes.
@@ -0,0 +1,176 @@
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { REALTIME_CHANNEL_STATES, RealtimeChannel, SupabaseClient } from '@supabase/supabase-js'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import useConfData from './use-conf-data'
|
||||
import { LW14_URL } from '~/lib/constants'
|
||||
import supabase from '../supabase'
|
||||
|
||||
interface RegistrationProps {
|
||||
onRegister?: () => void
|
||||
onError?: (error: any) => void
|
||||
}
|
||||
|
||||
export const useRegistration = (props?: RegistrationProps) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [realtimeChannel, setRealtimeChannel] = useState<RealtimeChannel | null>(null)
|
||||
const [{ userTicketData: userData, session, userTicketDataState }, dispatch] = useConfData()
|
||||
const router = useRouter()
|
||||
|
||||
// Triggered on session
|
||||
const fetchOrCreateUser = async () => {
|
||||
if (['loading', 'loaded'].includes(userTicketDataState)) return
|
||||
|
||||
if (!session?.user) {
|
||||
console.warn('Cannot fetch user without session. Skipping...')
|
||||
return
|
||||
}
|
||||
|
||||
const username = session.user.user_metadata?.user_name as string | undefined
|
||||
const name = session.user.user_metadata.full_name
|
||||
const email = session.user.email
|
||||
const userId = session.user.id
|
||||
|
||||
if (!username) {
|
||||
throw new Error('Username is required')
|
||||
}
|
||||
|
||||
if (!userData.id) {
|
||||
dispatch({ type: 'USER_TICKET_FETCH_STARTED' })
|
||||
|
||||
const { error: ticketInsertError } = await supabase
|
||||
.from('tickets')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
launch_week: 'lw14',
|
||||
email,
|
||||
name,
|
||||
username,
|
||||
metadata: { theme: resolvedTheme },
|
||||
referred_by: router.query?.referral ?? null,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
// If error because of duplicate email, ignore and proceed, otherwise sign out.
|
||||
if (ticketInsertError && ticketInsertError?.code !== '23505') {
|
||||
dispatch({ type: 'USER_TICKET_FETCH_ERROR', payload: ticketInsertError })
|
||||
props?.onError?.(ticketInsertError)
|
||||
return supabase.auth.signOut()
|
||||
}
|
||||
|
||||
const { data, error: ticketsViewError } = await supabase
|
||||
.from('tickets_view')
|
||||
.select('*')
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
.single()
|
||||
|
||||
if (ticketsViewError) {
|
||||
dispatch({ type: 'USER_TICKET_FETCH_ERROR', payload: ticketsViewError })
|
||||
props?.onError?.(ticketsViewError)
|
||||
return
|
||||
}
|
||||
|
||||
dispatch({ type: 'USER_TICKET_FETCH_SUCCESS', payload: data })
|
||||
|
||||
await prefetchData(username)
|
||||
|
||||
if (!realtimeChannel || realtimeChannel.state === REALTIME_CHANNEL_STATES.closed) {
|
||||
const channel = subscribeToTicketChanges(username, (payload) => {
|
||||
const platinum = !!payload.new.shared_on_twitter && !!payload.new.shared_on_linkedin
|
||||
const secret = !!payload.new.game_won_at
|
||||
dispatch({
|
||||
type: 'USER_TICKET_UPDATED',
|
||||
payload: {
|
||||
...payload.new,
|
||||
platinum,
|
||||
secret,
|
||||
},
|
||||
})
|
||||
})
|
||||
setRealtimeChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
props?.onRegister?.()
|
||||
}
|
||||
|
||||
async function prefetchData(username: string) {
|
||||
// Prefetch GitHub avatar
|
||||
// new Image().src = `https://github.com/${username}.png`
|
||||
|
||||
// Prefetch the twitter share URL to eagerly generate the page
|
||||
await fetch(`/api-v2/ticket-og?username=${username}`)
|
||||
}
|
||||
|
||||
function subscribeToTicketChanges(
|
||||
username: string,
|
||||
onChange: (payload: any) => void
|
||||
): RealtimeChannel {
|
||||
return supabase
|
||||
.channel('changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'tickets',
|
||||
filter: `username=eq.${username}`,
|
||||
},
|
||||
onChange
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
async function handleGithubSignIn() {
|
||||
const redirectTo = `${LW14_URL}${userData.username ? '?referral=' + userData.username : ''}`
|
||||
|
||||
const response = await supabase?.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: {
|
||||
redirectTo,
|
||||
},
|
||||
})
|
||||
|
||||
if (response) {
|
||||
if (response.error) {
|
||||
props?.onError?.(response.error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!session) {
|
||||
return
|
||||
}
|
||||
|
||||
fetchOrCreateUser()
|
||||
|
||||
return () => {
|
||||
// Cleanup realtime subscription on unmount
|
||||
realtimeChannel?.unsubscribe()
|
||||
}
|
||||
}, [session])
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
dispatch({ type: 'SESSION_UPDATED', payload: data.session })
|
||||
})
|
||||
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
dispatch({ type: 'SESSION_UPDATED', payload: session })
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
return {
|
||||
realtimeChannel,
|
||||
signIn: handleGithubSignIn,
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,6 @@ interface TicketSceneState {
|
||||
secret: boolean
|
||||
platinum: boolean
|
||||
frontside: boolean
|
||||
startDate: Date
|
||||
ticketNumber: number
|
||||
texts: {
|
||||
username: string
|
||||
@@ -29,7 +28,6 @@ interface TicketSceneOptions {
|
||||
defaultVisible?: boolean
|
||||
defaultSecret?: boolean
|
||||
defaultPlatinum?: boolean
|
||||
startDate: Date
|
||||
user: {
|
||||
id?: string
|
||||
name?: string
|
||||
@@ -246,7 +244,6 @@ class TicketScene implements BaseScene {
|
||||
secret: options.defaultSecret || false,
|
||||
platinum: options.defaultPlatinum || false,
|
||||
frontside: true,
|
||||
startDate: options.startDate,
|
||||
ticketNumber: options.user.ticketNumber || 0,
|
||||
texts: {
|
||||
username: options.user.name ?? '',
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { SupabaseClient } from "@supabase/supabase-js";
|
||||
import supabase from "~/lib/supabase";
|
||||
|
||||
/**
|
||||
* Override supabase types similar to previous use-conf-data.ts.
|
||||
* Current apps/www supabase instance uses old types.
|
||||
* Since we don't know final db layout let's leave it as any.
|
||||
*/
|
||||
export default supabase as SupabaseClient;
|
||||
@@ -1,164 +0,0 @@
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { SupabaseClient } from '@supabase/supabase-js'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { LW14_URL } from '~/lib/constants'
|
||||
|
||||
interface RegistrationProps {
|
||||
onRegister?: () => void
|
||||
onError?: (error: any) => void
|
||||
}
|
||||
|
||||
type FormState = 'default' | 'loading' | 'error'
|
||||
|
||||
export const useRegistration = (props: RegistrationProps) => {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const [formState, setFormState] = useState<FormState>('default')
|
||||
const [realtimeChannel, setRealtimeChannel] = useState<ReturnType<
|
||||
SupabaseClient['channel']
|
||||
> | null>(null)
|
||||
const [errorMsg] = useState('')
|
||||
const { supabase, session, setUserData, setTicketState, userData } = useConfData()
|
||||
const router = useRouter()
|
||||
|
||||
// Triggered on session
|
||||
async function fetchOrCreateUser() {
|
||||
if (supabase && session?.user && !userData.id) {
|
||||
const username = session.user.user_metadata.user_name
|
||||
const name = session.user.user_metadata.full_name
|
||||
const email = session.user.email
|
||||
const userId = session.user.id
|
||||
|
||||
if (!userData.id) {
|
||||
console.log("Inserting ticket", userId)
|
||||
setTicketState('loading')
|
||||
const { error } = await supabase
|
||||
.from('tickets')
|
||||
.insert({
|
||||
user_id: userId,
|
||||
launch_week: 'lw14',
|
||||
email,
|
||||
name,
|
||||
username,
|
||||
metadata: { theme: resolvedTheme },
|
||||
referred_by: router.query?.referral ?? null,
|
||||
})
|
||||
.select()
|
||||
.single()
|
||||
|
||||
if(error) {
|
||||
console.error("Error inserting ticket", error)
|
||||
}
|
||||
|
||||
await fetchUser({ error, username })
|
||||
}
|
||||
|
||||
props.onRegister?.()
|
||||
}
|
||||
}
|
||||
|
||||
const fetchUser = async ({ error, username }: any) => {
|
||||
if (!supabase) return
|
||||
|
||||
// If error because of duplicate email, ignore and proceed, otherwise sign out.
|
||||
if (error && error?.code !== '23505') {
|
||||
setFormState('error')
|
||||
props.onError?.(error)
|
||||
return supabase.auth.signOut()
|
||||
}
|
||||
|
||||
console.log('Fetching user...')
|
||||
|
||||
const { data } = await supabase
|
||||
.from('tickets_view')
|
||||
.select('*')
|
||||
.eq('launch_week', 'lw14')
|
||||
.eq('username', username)
|
||||
.single()
|
||||
.throwOnError()
|
||||
|
||||
console.log('Fetched user', data)
|
||||
|
||||
if (data) 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}?came_from_signup=true`).catch((_) => {})
|
||||
await fetch(`/api-v2/ticket-og?username=${username}`)
|
||||
|
||||
if (!realtimeChannel) {
|
||||
const channel = supabase
|
||||
.channel('changes')
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'tickets',
|
||||
filter: `username=eq.${username}`,
|
||||
},
|
||||
(payload: any) => {
|
||||
console.log('Ticket changes', payload)
|
||||
const platinum = !!payload.new.shared_on_twitter && !!payload.new.shared_on_linkedin
|
||||
const secret = !!payload.new.game_won_at
|
||||
setUserData({
|
||||
...payload.new,
|
||||
platinum,
|
||||
secret,
|
||||
})
|
||||
}
|
||||
)
|
||||
.subscribe()
|
||||
setRealtimeChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
fetchOrCreateUser()
|
||||
|
||||
return () => {
|
||||
// Cleanup realtime subscription on unmount
|
||||
realtimeChannel?.unsubscribe()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [session])
|
||||
|
||||
async function handleGithubSignIn() {
|
||||
if (formState !== 'default') {
|
||||
setFormState('default')
|
||||
return
|
||||
}
|
||||
|
||||
setFormState('loading')
|
||||
setTicketState('loading')
|
||||
|
||||
const redirectTo = `${LW14_URL}${userData.username ? '?referral=' + userData.username : ''}`
|
||||
|
||||
const response = await supabase?.auth.signInWithOAuth({
|
||||
provider: 'github',
|
||||
options: {
|
||||
redirectTo,
|
||||
},
|
||||
})
|
||||
|
||||
if (response) {
|
||||
if (response.error) {
|
||||
setFormState('error')
|
||||
props.onError?.(response.error)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
formState,
|
||||
realtimeChannel,
|
||||
signIn: handleGithubSignIn,
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,16 @@
|
||||
import { NextSeo } from 'next-seo'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Session } from '@supabase/supabase-js'
|
||||
import { TicketState, ConfDataContext, UserData } from '~/components/LaunchWeek/hooks/use-conf-data'
|
||||
import { LW14_DATE, LW14_TITLE, LW14_URL, SITE_ORIGIN } from '~/lib/constants'
|
||||
import supabase from '~/lib/supabase'
|
||||
import { LwView } from '~/components/LaunchWeek/14/LwView'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Lw14ConfDataProvider } from '~/components/LaunchWeek/14/hooks/use-conf-data'
|
||||
|
||||
const Lw14Page = () => {
|
||||
const { query } = useRouter()
|
||||
|
||||
const TITLE = `${LW14_TITLE} | ${LW14_DATE}`
|
||||
const DESCRIPTION = 'Join us for a week of announcing new features, every day at 7 AM PT.'
|
||||
const OG_IMAGE = `${SITE_ORIGIN}/images/launchweek/14/lw14-og.png?lw=14`
|
||||
|
||||
const { query } = useRouter()
|
||||
const ticketNumber = query.ticketNumber?.toString()
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [showCustomizationForm, setShowCustomizationForm] = useState<boolean>(false)
|
||||
|
||||
const defaultUserData = {
|
||||
id: query.id?.toString(),
|
||||
ticket_number: ticketNumber ? parseInt(ticketNumber, 10) : undefined,
|
||||
@@ -26,37 +19,6 @@ const Lw14Page = () => {
|
||||
platinum: !!query.platinum,
|
||||
}
|
||||
|
||||
const [userData, setUserData] = useState<UserData>(defaultUserData)
|
||||
const [ticketState, setTicketState] = useState<TicketState>('registration')
|
||||
|
||||
useEffect(() => {
|
||||
if (supabase) {
|
||||
console.log("Calling getSession")
|
||||
supabase.auth.getSession().then(({ data, error }) => {
|
||||
console.log("Setting session", data, error)
|
||||
setSession(data.session)
|
||||
})
|
||||
const {
|
||||
data: { subscription },
|
||||
} = supabase.auth.onAuthStateChange((_event, session) => {
|
||||
setSession(session)
|
||||
})
|
||||
|
||||
return () => subscription.unsubscribe()
|
||||
}
|
||||
}, [supabase])
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user) {
|
||||
if (userData?.id) {
|
||||
return setTicketState('ticket')
|
||||
}
|
||||
return setTicketState('loading')
|
||||
}
|
||||
if (!session) return setTicketState('registration')
|
||||
}, [session, userData])
|
||||
|
||||
console.log({userData})
|
||||
return (
|
||||
<>
|
||||
<NextSeo
|
||||
@@ -74,20 +36,9 @@ const Lw14Page = () => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<ConfDataContext.Provider
|
||||
value={{
|
||||
supabase,
|
||||
session,
|
||||
userData,
|
||||
setUserData,
|
||||
ticketState,
|
||||
setTicketState,
|
||||
showCustomizationForm,
|
||||
setShowCustomizationForm,
|
||||
}}
|
||||
>
|
||||
<LwView/>
|
||||
</ConfDataContext.Provider>
|
||||
<Lw14ConfDataProvider initState={{ userTicketData: defaultUserData}}>
|
||||
<LwView />
|
||||
</Lw14ConfDataProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user