chore: state cleanup to use reducer

Migrate logic to centralized reducer for ease of debugging
This commit is contained in:
Piotr Goszczyński committed 2025-03-18 12:47:49 +01:00
1 parent 22605cfe15
commit 49bac3eded
13 files changed
+402 -488

No files matched your search

+8 -35
View File
@@ -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
}
@@ -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,
}
}
+6 -55
View File
@@ -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>
</>
)
}