From 49bac3edede0b5a53e6d50cd5ecd959ee4fc84ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Goszczy=C5=84ski?= Date: Tue, 18 Mar 2025 12:47:49 +0100 Subject: [PATCH] chore: state cleanup to use reducer Migrate logic to centralized reducer for ease of debugging --- apps/www/components/LaunchWeek/14/LwView.tsx | 43 +---- .../LaunchWeek/14/TicketActions.tsx | 8 +- .../components/LaunchWeek/14/TicketCanvas.tsx | 102 +++++----- .../components/LaunchWeek/14/TicketClaim.tsx | 1 - .../components/LaunchWeek/14/TicketCopy.tsx | 5 +- .../components/LaunchWeek/14/TicketForm.tsx | 171 ----------------- .../LaunchWeek/14/hooks/use-conf-data.tsx | 147 +++++++++++++++ .../14/{ => hooks}/use-partymode.ts | 0 .../LaunchWeek/14/hooks/use-registration.ts | 176 ++++++++++++++++++ .../LaunchWeek/14/scenes/TicketScene.ts | 3 - apps/www/components/LaunchWeek/14/supabase.ts | 9 + .../LaunchWeek/14/use-registration.ts | 164 ---------------- apps/www/pages/launch-week/14/index.tsx | 61 +----- 13 files changed, 402 insertions(+), 488 deletions(-) delete mode 100644 apps/www/components/LaunchWeek/14/TicketForm.tsx create mode 100644 apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx rename apps/www/components/LaunchWeek/14/{ => hooks}/use-partymode.ts (100%) create mode 100644 apps/www/components/LaunchWeek/14/hooks/use-registration.ts create mode 100644 apps/www/components/LaunchWeek/14/supabase.ts delete mode 100644 apps/www/components/LaunchWeek/14/use-registration.ts diff --git a/apps/www/components/LaunchWeek/14/LwView.tsx b/apps/www/components/LaunchWeek/14/LwView.tsx index c23952f1bba..ec66fe4c2d6 100644 --- a/apps/www/components/LaunchWeek/14/LwView.tsx +++ b/apps/www/components/LaunchWeek/14/LwView.tsx @@ -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 ( @@ -62,15 +42,8 @@ export const LwView = () => { - - {isRegistering && ( + + {state.claimFormState === 'visible' && ( @@ -96,7 +69,7 @@ export const LwView = () => { )} - {hasTicket && ( + {state.ticketVisibility && ( diff --git a/apps/www/components/LaunchWeek/14/TicketActions.tsx b/apps/www/components/LaunchWeek/14/TicketActions.tsx index d12fdf2968c..ca51fbdc2cb 100644 --- a/apps/www/components/LaunchWeek/14/TicketActions.tsx +++ b/apps/www/components/LaunchWeek/14/TicketActions.tsx @@ -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 diff --git a/apps/www/components/LaunchWeek/14/TicketCanvas.tsx b/apps/www/components/LaunchWeek/14/TicketCanvas.tsx index 4f8582ea2bf..696a59ec087 100644 --- a/apps/www/components/LaunchWeek/14/TicketCanvas.tsx +++ b/apps/www/components/LaunchWeek/14/TicketCanvas.tsx @@ -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(null) const tunnelRef = useRef(null) const initQueue = useRef<{ init: Promise; 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 ( diff --git a/apps/www/components/LaunchWeek/14/TicketClaim.tsx b/apps/www/components/LaunchWeek/14/TicketClaim.tsx index 185ce784624..dbb9c82e1f8 100644 --- a/apps/www/components/LaunchWeek/14/TicketClaim.tsx +++ b/apps/www/components/LaunchWeek/14/TicketClaim.tsx @@ -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 ( diff --git a/apps/www/components/LaunchWeek/14/TicketCopy.tsx b/apps/www/components/LaunchWeek/14/TicketCopy.tsx index 794d820de44..80dd38f1ac6 100644 --- a/apps/www/components/LaunchWeek/14/TicketCopy.tsx +++ b/apps/www/components/LaunchWeek/14/TicketCopy.tsx @@ -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(null) diff --git a/apps/www/components/LaunchWeek/14/TicketForm.tsx b/apps/www/components/LaunchWeek/14/TicketForm.tsx deleted file mode 100644 index 346653ee1f1..00000000000 --- a/apps/www/components/LaunchWeek/14/TicketForm.tsx +++ /dev/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('default') - const [realtimeChannel, setRealtimeChannel] = useState | 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' ? ( -
-
{errorMsg}
- -
- ) : ( -
- -
- ) -} diff --git a/apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx b/apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx new file mode 100644 index 00000000000..271c771ded1 --- /dev/null +++ b/apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx @@ -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] | null>(null) + +export const Lw14ConfDataProvider = ({ + children, + initState, +}: { + children: React.ReactNode + initState?: Partial +}) => { + const state = useReducer(lwReducer, { + userTicketData: {}, + ticketState: 'loading', + session: null, + ticketLoadingState: 'unloaded', + ticketVisibility: false, + userTicketDataState: 'unloaded', + userTicketDataError: null, + claimFormState: 'initial', + ...initState, + }) + + return {children} +} + +export default function useLw14ConfData() { + const result = useContext(Lw14ConfDataContext) + if (!result) { + throw new Error('useLw14ConfData must be used within a Lw14ConfDataProvider') + } + return result +} diff --git a/apps/www/components/LaunchWeek/14/use-partymode.ts b/apps/www/components/LaunchWeek/14/hooks/use-partymode.ts similarity index 100% rename from apps/www/components/LaunchWeek/14/use-partymode.ts rename to apps/www/components/LaunchWeek/14/hooks/use-partymode.ts diff --git a/apps/www/components/LaunchWeek/14/hooks/use-registration.ts b/apps/www/components/LaunchWeek/14/hooks/use-registration.ts new file mode 100644 index 00000000000..716a081a4c0 --- /dev/null +++ b/apps/www/components/LaunchWeek/14/hooks/use-registration.ts @@ -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(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, + } +} diff --git a/apps/www/components/LaunchWeek/14/scenes/TicketScene.ts b/apps/www/components/LaunchWeek/14/scenes/TicketScene.ts index b8c996dd083..b33834b7066 100644 --- a/apps/www/components/LaunchWeek/14/scenes/TicketScene.ts +++ b/apps/www/components/LaunchWeek/14/scenes/TicketScene.ts @@ -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 ?? '', diff --git a/apps/www/components/LaunchWeek/14/supabase.ts b/apps/www/components/LaunchWeek/14/supabase.ts new file mode 100644 index 00000000000..ef8b754949a --- /dev/null +++ b/apps/www/components/LaunchWeek/14/supabase.ts @@ -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; diff --git a/apps/www/components/LaunchWeek/14/use-registration.ts b/apps/www/components/LaunchWeek/14/use-registration.ts deleted file mode 100644 index 36a6f96126b..00000000000 --- a/apps/www/components/LaunchWeek/14/use-registration.ts +++ /dev/null @@ -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('default') - const [realtimeChannel, setRealtimeChannel] = useState | 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, - } -} diff --git a/apps/www/pages/launch-week/14/index.tsx b/apps/www/pages/launch-week/14/index.tsx index edf6a47773d..ecb117f2532 100644 --- a/apps/www/pages/launch-week/14/index.tsx +++ b/apps/www/pages/launch-week/14/index.tsx @@ -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(null) - const [showCustomizationForm, setShowCustomizationForm] = useState(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(defaultUserData) - const [ticketState, setTicketState] = useState('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 ( <> { }} /> - - - + + + ) }