add partymode part 1

This commit is contained in:
Piotr Goszczyński committed 2025-03-20 15:57:22 +01:00
1 parent 0f77cd2247
commit fdeb74abb3
7 files changed
+158 -87

No files matched your search

+5 -5
View File
@@ -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
</ActionButton>
<ActionButton
variant={partymode.state === 'on' ? 'primary' : 'secondary'}
variant={partymode.state === 'joined' ? 'primary' : 'secondary'}
icon="P"
onClick={() => {
partymode.toggle()
}}
>
Party mode: {partymode.state === 'on' ? 'ON' : 'OFF'}
Party mode: {partymode.state === 'joined' ? 'ON' : 'OFF'}
</ActionButton>
</TicketClaimButtons>
</TicketClaimContent>
@@ -73,13 +73,13 @@ export const LwView = () => {
<div className="absolute bottom-4 md:bottom-auto md:top-4 left-0 right-0 flex justify-center">
<ActionButton
variant={partymode.state === 'on' ? 'primary' : 'secondary'}
variant={partymode.state === 'joined' ? 'primary' : 'secondary'}
icon="P"
onClick={() => {
partymode.toggle()
}}
>
Party mode: {partymode.state === 'on' ? 'ON' : 'OFF'}
Party mode: {partymode.state === 'joined' ? 'ON' : 'OFF'}
</ActionButton>
</div>
</>
@@ -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,
})
@@ -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,
}
}
@@ -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<RealtimeChannel | null>(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')
}
@@ -1 +0,0 @@
insert into public.launch_weeks (id) values ('lw14');
@@ -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();
+2
View File
@@ -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');