mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
add partymode part 1
This commit is contained in:
1 parent
0f77cd2247
commit
fdeb74abb3
7 files changed
+158
-87
No files matched your search
@@ -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();
|
||||
@@ -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');
|
||||
Reference in new issue
Block a user