diff --git a/apps/www/components/LaunchWeek/14/LwView.tsx b/apps/www/components/LaunchWeek/14/LwView.tsx
index d496366a8f7..589b6d2136d 100644
--- a/apps/www/components/LaunchWeek/14/LwView.tsx
+++ b/apps/www/components/LaunchWeek/14/LwView.tsx
@@ -26,7 +26,7 @@ import SectionContainerWithCn from '~/components/Layouts/SectionContainerWithCn'
const dates = [new Date('2025-03-31T07:00:00.000-07:00')]
export const LwView = () => {
- const partymode = usePartymode()
+ const partymode = usePartymode("on")
const [state] = useConfData()
const register = useRegistration()
@@ -52,13 +52,13 @@ export const LwView = () => {
CLAIM YOUR TICKET
{
partymode.toggle()
}}
>
- Party mode: {partymode.state === 'on' ? 'ON' : 'OFF'}
+ Party mode: {partymode.state === 'joined' ? 'ON' : 'OFF'}
@@ -73,13 +73,13 @@ export const LwView = () => {
{
partymode.toggle()
}}
>
- Party mode: {partymode.state === 'on' ? 'ON' : 'OFF'}
+ Party mode: {partymode.state === 'joined' ? 'ON' : 'OFF'}
>
diff --git a/apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx b/apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx
index 9d790b40b66..2045afe1d02 100644
--- a/apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx
+++ b/apps/www/components/LaunchWeek/14/hooks/use-conf-data.tsx
@@ -1,4 +1,4 @@
-import { Session, SupabaseClient } from '@supabase/supabase-js'
+import { RealtimeChannel, Session, SupabaseClient } from '@supabase/supabase-js'
import { useRouter } from 'next/router'
import { createContext, Dispatch, useContext, useMemo, useReducer } from 'react'
@@ -46,7 +46,7 @@ type LwAction =
| { type: 'TICKET_LOADING_START' }
| { type: 'TICKET_LOADING_SUCCESS' }
| { type: 'TICKET_LOADING_ERROR'; payload?: Error }
- | { type: 'PARTYMODE_ENABLE' }
+ | { type: 'PARTYMODE_ENABLE', payload: RealtimeChannel }
| { type: 'PARTYMODE_DISABLE' }
// Define state interface
@@ -59,7 +59,7 @@ interface LwState {
ticketLoadingState: 'unloaded' | 'loading' | 'error' | 'loaded'
ticketVisibility: boolean
claimFormState: 'initial' | 'visible' | 'hidden'
- partyModeState: 'unloaded' | 'on' | 'off'
+ realtimeGaugesChannel: RealtimeChannel | null
referal?: string
}
@@ -116,13 +116,13 @@ export const lwReducer = (state: LwState, action: LwAction): LwState => {
case 'PARTYMODE_ENABLE': {
return {
...state,
- partyModeState: 'on',
+ realtimeGaugesChannel: action.payload,
}
}
case 'PARTYMODE_DISABLE': {
return {
...state,
- partyModeState: 'off',
+ realtimeGaugesChannel: null,
}
}
default:
@@ -152,6 +152,7 @@ export const Lw14ConfDataProvider = ({
userTicketDataError: null,
claimFormState: 'initial',
partyModeState: 'unloaded',
+ realtimeGaugesChannel: null,
...initState,
})
diff --git a/apps/www/components/LaunchWeek/14/hooks/use-partymode.ts b/apps/www/components/LaunchWeek/14/hooks/use-partymode.ts
index f18addfb5a3..891caa31462 100644
--- a/apps/www/components/LaunchWeek/14/hooks/use-partymode.ts
+++ b/apps/www/components/LaunchWeek/14/hooks/use-partymode.ts
@@ -1,17 +1,54 @@
-import useLw14ConfData from "./use-conf-data"
+import { useCallback, useEffect, useState } from 'react'
+import useLw14ConfData from './use-conf-data'
+import supabase from '~/lib/supabase'
+import { REALTIME_CHANNEL_STATES, REALTIME_SUBSCRIBE_STATES } from '@supabase/supabase-js'
-export const usePartymode = () => {
+const LW14_TOPIC = 'lw14'
+const GAUGES_UPDATES_EVENT = 'gauges-update'
+
+export const usePartymode = (defaultState: 'on' | 'off') => {
const [state, dispatch] = useLw14ConfData()
+ const [shouldInitialize, setShouldInitialize] = useState(defaultState === 'on' && !state.realtimeGaugesChannel)
+
+ const createChannelAndSubscribe = useCallback(() => {
+ const channel = supabase
+ .channel(LW14_TOPIC)
+ .on('broadcast', { event: GAUGES_UPDATES_EVENT }, (payload) => {
+ console.log('Channel update', payload)
+ })
+
+ channel.subscribe((status, error) => {
+ console.log('Channel status', status, error)
+ if (status === REALTIME_SUBSCRIBE_STATES.SUBSCRIBED) {
+ dispatch({ type: 'PARTYMODE_ENABLE', payload: channel })
+ }
+ })
+ }, [dispatch])
+
+ const toggle = useCallback(async () => {
+ if (state.realtimeGaugesChannel?.state === REALTIME_CHANNEL_STATES.joined) {
+ await state.realtimeGaugesChannel.unsubscribe()
+ dispatch({ type: 'PARTYMODE_DISABLE' })
+ return
+ }
+
+ if (
+ state.realtimeGaugesChannel === null ||
+ state.realtimeGaugesChannel.state === REALTIME_CHANNEL_STATES.closed
+ ) {
+ createChannelAndSubscribe()
+ }
+ }, [createChannelAndSubscribe, dispatch, state.realtimeGaugesChannel])
+
+ useEffect(() => {
+ if (shouldInitialize) {
+ setShouldInitialize(false)
+ createChannelAndSubscribe()
+ }
+ }, [createChannelAndSubscribe, shouldInitialize])
return {
- state: state.partyModeState,
- toggle: () => {
- if(state.partyModeState === 'on') {
- dispatch({ type: 'PARTYMODE_DISABLE' })
- } else {
- dispatch({ type: 'PARTYMODE_ENABLE' })
- }
- }
+ state: state.realtimeGaugesChannel?.state,
+ toggle,
}
}
-
diff --git a/apps/www/components/LaunchWeek/14/hooks/use-registration.ts b/apps/www/components/LaunchWeek/14/hooks/use-registration.ts
index b93f7794c54..59dc150ef54 100644
--- a/apps/www/components/LaunchWeek/14/hooks/use-registration.ts
+++ b/apps/www/components/LaunchWeek/14/hooks/use-registration.ts
@@ -1,36 +1,54 @@
-import { useTheme } from 'next-themes'
-import { useEffect, useState } from 'react'
+import { useCallback, useEffect, useRef, 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'
+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()
+}
+
interface RegistrationProps {
onRegister?: () => void
onError?: (error: any) => void
}
-export const useRegistration = (props?: RegistrationProps) => {
- const { resolvedTheme } = useTheme()
+export const useRegistration = ({ onError, onRegister }: RegistrationProps = {}) => {
const [realtimeChannel, setRealtimeChannel] = useState(null)
- const [{ userTicketData: userData, session, userTicketDataState }, dispatch] = useConfData()
- const router = useRouter()
+ const [{ userTicketData: userData, session, userTicketDataState, referal }, dispatch] =
+ useConfData()
+ const sessionUser = session?.user
+ const callbacksRef = useRef({ onError, onRegister })
// Triggered on session
- const fetchOrCreateUser = async () => {
+ const fetchOrCreateUser = useCallback(async () => {
if (['loading', 'loaded'].includes(userTicketDataState)) return
- if (!session?.user) {
+ if (!sessionUser) {
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
+ const username = sessionUser.user_metadata?.user_name as string | undefined
+ const name = sessionUser.user_metadata.full_name
+ const email = sessionUser.email
+ const userId = sessionUser.id
if (!username) {
throw new Error('Username is required')
@@ -47,8 +65,7 @@ export const useRegistration = (props?: RegistrationProps) => {
email,
name,
username,
- metadata: { theme: resolvedTheme },
- referred_by: router.query?.referral ?? null,
+ referred_by: referal ?? null,
})
.select()
.single()
@@ -56,7 +73,7 @@ export const useRegistration = (props?: RegistrationProps) => {
// 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)
+ callbacksRef.current.onError?.(ticketInsertError)
return supabase.auth.signOut()
}
@@ -69,33 +86,36 @@ export const useRegistration = (props?: RegistrationProps) => {
if (ticketsViewError) {
dispatch({ type: 'USER_TICKET_FETCH_ERROR', payload: ticketsViewError })
- props?.onError?.(ticketsViewError)
+ callbacksRef.current.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?.()
- }
+ if (
+ (!realtimeChannel || realtimeChannel.state === REALTIME_CHANNEL_STATES.closed) &&
+ username
+ ) {
+ 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)
+ }
+
+ callbacksRef.current.onRegister?.()
+ }, [dispatch, realtimeChannel, referal, sessionUser, userData.id, userTicketDataState])
async function prefetchData(username: string) {
// Prefetch GitHub avatar
@@ -105,26 +125,7 @@ export const useRegistration = (props?: RegistrationProps) => {
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 handleGithubSignIn = useCallback(async () => {
const redirectTo = `${LW14_URL}${userData.username ? '?referral=' + userData.username : ''}`
const response = await supabase?.auth.signInWithOAuth({
@@ -136,24 +137,20 @@ export const useRegistration = (props?: RegistrationProps) => {
if (response) {
if (response.error) {
- props?.onError?.(response.error)
+ callbacksRef.current?.onError?.(response.error)
return
}
}
- }
+ }, [userData.username])
useEffect(() => {
- if (!session) {
- return
- }
-
fetchOrCreateUser()
return () => {
// Cleanup realtime subscription on unmount
realtimeChannel?.unsubscribe()
}
- }, [session])
+ }, [fetchOrCreateUser, realtimeChannel])
useEffect(() => {
supabase.auth.getSession().then(({ data }) => {
@@ -169,11 +166,15 @@ export const useRegistration = (props?: RegistrationProps) => {
return () => subscription.unsubscribe()
}, [dispatch])
+ useEffect(() => {
+ callbacksRef.current = { onError, onRegister }
+ })
+
const upgradeTicket = async () => {
if (userData.id) {
- if(userData.secret) {
+ if (userData.secret) {
console.log('User already has a secret ticket')
- return
+ return
}
const { error } = await supabase
@@ -188,7 +189,6 @@ export const useRegistration = (props?: RegistrationProps) => {
// Trigger og-image ticket generation
await fetch(`/api-v2/ticket-og?username=${userData.username}&secret=true`)
-
} else {
console.warn('Cannot upgrade ticket without user data')
}
diff --git a/supabase/migrations/20250317183330_add_lw_14.sql b/supabase/migrations/20250317183330_add_lw_14.sql
deleted file mode 100644
index 84a08daa309..00000000000
--- a/supabase/migrations/20250317183330_add_lw_14.sql
+++ /dev/null
@@ -1 +0,0 @@
-insert into public.launch_weeks (id) values ('lw14');
diff --git a/supabase/migrations/20250320114326_add_lw_14_broadcast.sql b/supabase/migrations/20250320114326_add_lw_14_broadcast.sql
new file mode 100644
index 00000000000..2c27de66fdd
--- /dev/null
+++ b/supabase/migrations/20250320114326_add_lw_14_broadcast.sql
@@ -0,0 +1,32 @@
+-- Example how to send message from DB to listening users
+--
+-- select
+-- realtime.send(
+-- jsonb_build_object('hello', 'world'), -- JSONB Payload
+-- 'event', -- Event name
+-- 'topic', -- Topic
+-- false -- Public / Private flag
+-- );
+
+CREATE OR REPLACE FUNCTION public.send_updated_stats_for_lw14() RETURNS trigger AS $$
+BEGIN
+-- Based on ticket count send following message
+-- {
+-- "fuel": number (0.2-1) calculated as a limit function 0.2 + 1 / (1 + all tickets / 10)
+-- "payload_value": each regular ticket gives $1 each platinum one $10
+-- }
+ PERFORM realtime.send(
+ jsonb_build_object('hello', 'world'), -- JSONB Payload
+ 'gauges-update', -- Event name
+ 'lw14', -- Topic
+ true -- Public / Private flag
+ );
+ RETURN NULL;
+END;
+$$ LANGUAGE plpgsql;
+
+CREATE TRIGGER broadcast_stats_for_lw14_tickets
+AFTER INSERT OR UPDATE OR DELETE ON public.tickets
+FOR EACH ROW
+WHEN (NEW.launch_week = 'lw14')
+EXECUTE FUNCTION public.send_updated_stats_for_lw14();
diff --git a/supabase/seed.sql b/supabase/seed.sql
index c9380d1bfbc..702adcfcd90 100644
--- a/supabase/seed.sql
+++ b/supabase/seed.sql
@@ -4,3 +4,5 @@ values
('New York', 'USA', 'lw12', now(), true),
('London', 'UK', 'lw12', now(), true),
('Singapore', 'Singapore', 'lw12', now(), true);
+
+insert into public.launch_weeks (id) values ('lw14');