Files
Jonathan Summers-MuirandFrancesco Sansalvadore 1f1f8a5fa4 Feat/whale init (#28243)
* duplicate page

* whale init

* table background

* prepare

* sync undocumented remote schema from master

* revert remote schema sync

* add lw12 tickets migration file

* ticket init

* set up lw12 ticket layout

* finish ticket layout and customization form

* lw12 ticket og

* ticket styling

* code highlight border

* launch week migrations

* local dev progress

* keep local gh config

* comment section

* comment section

* comment out username.tsx

* remove lw/ticekts temp

* update copy

* fix migration view

* redirect to try

* use misc

* lw12 og

* share

* share correct

* env var

* username page

* update example env

* push new db schema

* trigger deploy

* update ticket og

* ticket themes

* bypass browser

* change env var name

* fix

* fix

* process env

* create client server side

* lw announcements

* promoToast

* animated bg

* update ticket og bg

* minor details

* secret ticket

* social share text

* social share textgst

* flow text

* update og

* update og handler

* Update index.ts

* use functions.invoke and use generic supabase URL

* Update index.ts

* Update handler.tsx

* Update package-lock.json

* Update handler.tsx

* add next api route

* moved to vercel edge function

* set revalidate

* Update route.tsx

* Delete lw-ticket-og.tsx

* Update route.tsx

* Update [username].tsx

* Update [username].tsx

* add more fetches

* Update turbo.json

* ticket themes updated

* copy and layout updated

* Update index.tsx

* Update Ticket.tsx

* Update TicketingFlow.tsx

* update countdown

* updated bg

* small updates

* moat

* Update [username].tsx

* Update 20240723155310_add_lw12_ticketing_schema.sql

* optimistic ticket stuff og generation

* Update index.tsx

* updated layout

* update themes in og

* Update constants.ts

* attr renamed

* Update TicketingFlow.tsx

* Update TicketActions.tsx

* grammar

* moar updates

* Update TicketActions.tsx

* Update TicketActions2.tsx

* Update TicketActions.tsx

* Update Hero.tsx

* Update TicketingFlow.tsx

* remove console logs

---------

Co-authored-by: Francesco Sansalvadore <f.sansalvadore@gmail.com>
2024-08-01 01:59:01 +08:00

244 lines
7.4 KiB
TypeScript

import { Dispatch, SetStateAction, useEffect, useMemo, useRef, useState } from 'react'
import { Button, cn } from 'ui'
import { SITE_ORIGIN } from '~/lib/constants'
import useConfData from '~/components/LaunchWeek/hooks/use-conf-data'
import { VALID_KEYS } from '~/components/LaunchWeek/hooks/useLwGame'
import { InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from 'ui'
const REGEXP_ONLY_CHARS = '^[a-zA-Z]+$'
const COMPLIMENTS = [
'Congratulations!',
'You won!',
'Hurray!',
'Great job!',
'Nice job!',
"That's right!",
]
interface Props {
setIsGameMode: Dispatch<SetStateAction<boolean>>
}
const LWXGame = ({ setIsGameMode }: Props) => {
const { supabase, userData: user } = useConfData()
const phrase = process.env.NEXT_PUBLIC_LW_GAME_WORD ?? 'its_just_postgres'
const inputRef = useRef(null)
const winningPhrase = phrase?.split('_').map((word) => word.split(''))
const phraseLength = phrase?.replaceAll('_', '').split('').length
const [gameState, setGameState] = useState<'playing' | 'winner' | 'loading'>('playing')
const [hasKeyDown, setHasKeyDown] = useState(false)
const [value, setValue] = useState('')
const hasWon = gameState === 'winner'
const winningCompliment = useMemo(
() => COMPLIMENTS[Math.floor(Math.random() * COMPLIMENTS.length)],
[]
)
function onKeyDown(event: KeyboardEvent) {
const newKey = event.key.toLocaleLowerCase()
if (!(event.metaKey || event.ctrlKey) && VALID_KEYS.includes(newKey)) {
setHasKeyDown(true)
}
setTimeout(() => {
setHasKeyDown(false)
}, 100)
}
useEffect(() => {
window.addEventListener('keydown', onKeyDown)
return () => window.removeEventListener('keydown', onKeyDown)
}, [onKeyDown])
async function handleGithubSignIn() {
const redirectTo = `${SITE_ORIGIN}/launch-week/${
user.username ? '?referral=' + user.username : ''
}`
supabase?.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo,
},
})
}
const handleClaimTicket = async (e: any) => {
e.preventDefault()
setGameState('loading')
if (supabase) {
if (user.id) {
await supabase
.from('tickets')
.update({ game_won_at: new Date() })
.eq('launch_week', 'lw12')
.eq('username', user.username)
.then((res) => {
if (res.error) return console.log('error', res.error)
setIsGameMode(false)
})
await fetch(`/api-v2/ticket-og?username=${user.username}&secret=true`)
} else {
localStorage.setItem('lw12_secret', 'true')
handleGithubSignIn()
}
}
}
function handleIndexCount(word_idx: number, char_idx: number) {
let index = -1
for (let i = 0; i < word_idx + 1; i++) {
const word = winningPhrase[i]
for (let j = 0; j < word.length; j++) {
if (i < word_idx || j <= char_idx) index++
}
}
return index
}
if (gameState === 'loading')
return (
<div className="relative w-full mt-[100px] md:mt-44 lg:mt-32 xl:mt-32 2xl:mt-[120px] flex flex-col items-center gap-6 text-foreground">
<svg
className="animate-spinner opacity-50 w-5 h-5 md:w-6 md:h-6"
width="100%"
height="100%"
viewBox="0 0 62 61"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M61 31C61 14.4315 47.5685 1 31 1C14.4315 1 1 14.4315 1 31"
stroke="white"
strokeWidth="2"
/>
</svg>
</div>
)
return (
<form
onSubmit={handleClaimTicket}
className="flex flex-col items-center text-center gap-12 md:gap-16 max-w-6xl"
>
<div className="flex flex-col items-center h-10 text-foreground-light">
<div
className={cn(
'absolute flex flex-col gap-2 opacity-0 translate-y-2 transition-all',
hasWon && 'opacity-100 translate-y-0'
)}
>
<p className="tracking-wider text-foreground text-lg font-mono uppercase">
{winningCompliment}
</p>
<p className="text-foreground-lighter font-san text-sm">
Claim and share the secret ticket to boost your chances of winning limited swag.
</p>
</div>
<div
className={cn(
'absolute flex justify-center opacity-0 translate-y-2 transition-all',
!hasWon && 'opacity-100 translate-y-0'
)}
>
Guess the word
</div>
</div>
<div className="flex items-center justify-center gap-2 flex-wrap font-mono min-h-[100px]">
<InputOTP
ref={inputRef}
maxLength={phraseLength}
pattern={REGEXP_ONLY_CHARS}
autoFocus
containerClassName="flex-wrap justify-center gap-y-4 max-w-screen"
inputMode="text"
value={value}
spellCheck={false}
onChange={(e) => {
// @ts-ignore
const index = inputRef.current?.dataset.inputOtpMss
const valueAtPhraseIndex = phrase?.replaceAll('_', '').split('')[index]
const currentVal = e.split('')[e.length - 1]
const isMatch = valueAtPhraseIndex === currentVal
if (isMatch) {
setValue(e)
}
}}
onComplete={() => setGameState('winner')}
>
{winningPhrase.map((word, w_idx) => (
<>
<InputOTPGroup key={`${word.join('')}-${w_idx}`}>
{word.map((_, c_idx) => {
// index is sum of every letter of every previous word + index of current wo
const currentIndex = handleIndexCount(w_idx, c_idx)
return (
<InputOTPSlot
className={cn(
// 'h-6 w-6 sm:h-10 sm:w-10',
hasWon && 'border-foreground-light animate-pulse',
hasKeyDown && 'border-strong'
)}
key={`otp-${currentIndex}`}
index={currentIndex}
/>
)
})}
</InputOTPGroup>
{w_idx !== winningPhrase.length - 1 && <InputOTPSeparator className="mx-1" />}
</>
))}
</InputOTP>
</div>
<div className="flex flex-col items-center h-10">
<Button
type="secondary"
onClick={() => null}
htmlType="submit"
className={cn(
'absolute opacity-0 translate-y-2 transition-all',
hasWon && 'opacity-100 translate-y-0'
)}
>
Claim secret ticket
</Button>
</div>
<div className="flex gap-4 md:gap-10 items-center h-10 text-xs text-foreground-lighter">
<div className="flex items-center gap-2">
<Button
type="outline"
onClick={() => null}
disabled
className="pointer-events-none"
size="tiny"
>
A-Z
</Button>
<span>Play</span>
</div>
<div className="flex items-center gap-2 text-foreground-muted">
<Button
type="outline"
onClick={() => setIsGameMode(false)}
className="opacity-50"
size="tiny"
>
Esc
</Button>
<span>Exit</span>
</div>
</div>
</form>
)
}
export default LWXGame