From 173c4b608d5771419bcc6d7793245eecc89b07b8 Mon Sep 17 00:00:00 2001 From: Thomas Wang Date: Mon, 5 May 2025 16:53:53 -0400 Subject: [PATCH 1/3] impl --- .../classic-webview/src/hooks/useGame.tsx | 21 +++++++++++++- .../classic-webview/src/pages/PlayPage.tsx | 6 +++- .../src/components/wordInput.tsx | 28 +++++++++++++++---- 3 files changed, 47 insertions(+), 8 deletions(-) diff --git a/packages/classic-webview/src/hooks/useGame.tsx b/packages/classic-webview/src/hooks/useGame.tsx index 56578d14..a886760c 100644 --- a/packages/classic-webview/src/hooks/useGame.tsx +++ b/packages/classic-webview/src/hooks/useGame.tsx @@ -5,6 +5,12 @@ import { useDevvitListener } from './useDevvitListener'; import { logger } from '../utils/logger'; import { useMocks } from './useMocks'; +type WordSubmissionStateContext = { + isSubmitting: boolean; + setIsSubmitting: (isSubmitting: boolean) => void; +}; +const WordSubmissionContext = createContext(null); + const GameContext = createContext>({}); const GameUpdaterContext = createContext> @@ -13,6 +19,8 @@ const GameUpdaterContext = createContext { const mocks = useMocks(); const [game, setGame] = useState>(mocks.getMock('mocks')?.game ?? {}); + const [isSubmitting, setIsSubmitting] = useState(false); + const initResponse = useDevvitListener('GAME_INIT_RESPONSE'); const submissionResponse = useDevvitListener('WORD_SUBMITTED_RESPONSE'); const hintResponse = useDevvitListener('HINT_RESPONSE'); @@ -37,6 +45,7 @@ export const GameContextProvider = ({ children }: { children: React.ReactNode }) logger.log('Submission response: ', submissionResponse); if (submissionResponse) { setGame(submissionResponse); + setIsSubmitting(false); } }, [submissionResponse]); @@ -56,7 +65,9 @@ export const GameContextProvider = ({ children }: { children: React.ReactNode }) return ( - {children} + + {children} + ); }; @@ -76,3 +87,11 @@ export const useSetGame = () => { } return setGame; }; + +export const useWordSubmission = () => { + const context = useContext(WordSubmissionContext); + if (context === null) { + throw new Error('useWordSubmission must be used within a WordSubmissionProvider'); + } + return context; +}; diff --git a/packages/classic-webview/src/pages/PlayPage.tsx b/packages/classic-webview/src/pages/PlayPage.tsx index 11546bcf..6ba20698 100644 --- a/packages/classic-webview/src/pages/PlayPage.tsx +++ b/packages/classic-webview/src/pages/PlayPage.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { sendMessageToDevvit } from '../utils'; import { WordInput } from '@hotandcold/webview-common/components/wordInput'; import { Guesses } from '../components/guesses'; -import { useGame } from '../hooks/useGame'; +import { useGame, useWordSubmission } from '../hooks/useGame'; import { useDevvitListener } from '../hooks/useDevvitListener'; import clsx from 'clsx'; import { FeedbackResponse } from '@hotandcold/classic-shared'; @@ -15,11 +15,13 @@ import { UnlockHardcoreCTAContent } from '../components/UnlockHardcoreCTAContent const useFeedback = (): { feedback: FeedbackResponse | null; dismissFeedback: () => void } => { const [feedback, setFeedback] = useState(null); + const { setIsSubmitting } = useWordSubmission(); const message = useDevvitListener('FEEDBACK'); useEffect(() => { if (!message) return; setFeedback(message); + setIsSubmitting(false); }, [message]); const dismissFeedback = () => { @@ -144,6 +146,7 @@ const GameplayContent = () => { const [word, setWord] = useState(''); const { challengeUserInfo, mode, challengeInfo } = useGame(); const { feedback, dismissFeedback } = useFeedback(); + const { setIsSubmitting } = useWordSubmission(); const guesses = challengeUserInfo?.guesses ?? []; const hasGuessed = guesses.length > 0; @@ -187,6 +190,7 @@ const GameplayContent = () => { return; } + setIsSubmitting(true); sendMessageToDevvit({ type: 'WORD_SUBMITTED', value: word.trim().toLowerCase(), diff --git a/packages/webview-common/src/components/wordInput.tsx b/packages/webview-common/src/components/wordInput.tsx index 39084967..caebcc98 100644 --- a/packages/webview-common/src/components/wordInput.tsx +++ b/packages/webview-common/src/components/wordInput.tsx @@ -2,6 +2,7 @@ import { AnimatePresence, motion } from 'motion/react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { cn } from '@hotandcold/webview-common/utils'; import { PrimaryButton } from './button'; +import { useWordSubmission } from '@hotandcold/classic-webview/src/hooks/useGame'; type PixelData = { x: number; @@ -10,6 +11,16 @@ type PixelData = { color: string; }; +// Spinning loading indicator component +const SpinningCircle = ({ className = "h-5 w-5 text-white" }: { className?: string }) => ( +
+ + + + +
+); + export function WordInput({ placeholders, onChange, @@ -28,6 +39,7 @@ export function WordInput({ const [currentPlaceholder, setCurrentPlaceholder] = useState(0); const [animating, setAnimating] = useState(false); const [internalValue, setInternalValue] = useState(externalValue); + const { isSubmitting } = useWordSubmission(); // Sync internal value with external value useEffect(() => { @@ -232,7 +244,7 @@ export function WordInput({ }; const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !animating) { + if (e.key === 'Enter' && !animating && !isSubmitting) { handleSubmit(); } }; @@ -248,7 +260,7 @@ export function WordInput({ /> { - if (!animating && onChange) { + if (!animating && !isSubmitting && onChange) { onChange(e); } }} @@ -260,26 +272,30 @@ export function WordInput({ autoCorrect="on" autoComplete="off" enterKeyHint="send" + disabled={isSubmitting} className={cn( 'text-md relative z-50 h-14 w-full rounded-full border-none px-4 text-black shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),_0px_1px_0px_0px_rgba(25,28,33,0.02),_0px_0px_0px_1px_rgba(25,28,33,0.08)] transition duration-200 focus:outline-none focus:ring-0 dark:text-white', animating && 'text-transparent dark:text-transparent', internalValue && 'bg-gray-50 dark:bg-gray-800', - isHighContrast ? 'bg-white dark:bg-black' : 'bg-gray-50 dark:bg-gray-800' + isHighContrast ? 'bg-white dark:bg-black' : 'bg-gray-50 dark:bg-gray-800', + isSubmitting && 'cursor-not-allowed opacity-70' )} /> { // Workaround for ios and android blurring the input on button click e.preventDefault(); - handleSubmit(); + if (!isSubmitting) { + handleSubmit(); + } }} > - Guess + {isSubmitting ? : 'Guess'}
From b8607ba78e084f9aa014b7322cbc89b1d273b700 Mon Sep 17 00:00:00 2001 From: Thomas Wang Date: Mon, 5 May 2025 18:01:04 -0400 Subject: [PATCH 2/3] its own file --- .../classic-webview/src/hooks/useGame.tsx | 23 +++------------- .../src/hooks/useWordSubmission.tsx | 26 +++++++++++++++++++ packages/classic-webview/src/main.tsx | 17 +++++++----- .../classic-webview/src/pages/PlayPage.tsx | 12 ++++++--- 4 files changed, 48 insertions(+), 30 deletions(-) create mode 100644 packages/classic-webview/src/hooks/useWordSubmission.tsx diff --git a/packages/classic-webview/src/hooks/useGame.tsx b/packages/classic-webview/src/hooks/useGame.tsx index a886760c..65eb6a9e 100644 --- a/packages/classic-webview/src/hooks/useGame.tsx +++ b/packages/classic-webview/src/hooks/useGame.tsx @@ -4,12 +4,7 @@ import { sendMessageToDevvit } from '../utils'; import { useDevvitListener } from './useDevvitListener'; import { logger } from '../utils/logger'; import { useMocks } from './useMocks'; - -type WordSubmissionStateContext = { - isSubmitting: boolean; - setIsSubmitting: (isSubmitting: boolean) => void; -}; -const WordSubmissionContext = createContext(null); +import { useWordSubmission } from './useWordSubmission'; const GameContext = createContext>({}); const GameUpdaterContext = createContext { const mocks = useMocks(); const [game, setGame] = useState>(mocks.getMock('mocks')?.game ?? {}); - const [isSubmitting, setIsSubmitting] = useState(false); + const { setIsSubmitting } = useWordSubmission(); const initResponse = useDevvitListener('GAME_INIT_RESPONSE'); const submissionResponse = useDevvitListener('WORD_SUBMITTED_RESPONSE'); @@ -47,7 +42,7 @@ export const GameContextProvider = ({ children }: { children: React.ReactNode }) setGame(submissionResponse); setIsSubmitting(false); } - }, [submissionResponse]); + }, [submissionResponse, setIsSubmitting]); useEffect(() => { logger.log('Hint response: ', hintResponse); @@ -65,9 +60,7 @@ export const GameContextProvider = ({ children }: { children: React.ReactNode }) return ( - - {children} - + {children} ); }; @@ -87,11 +80,3 @@ export const useSetGame = () => { } return setGame; }; - -export const useWordSubmission = () => { - const context = useContext(WordSubmissionContext); - if (context === null) { - throw new Error('useWordSubmission must be used within a WordSubmissionProvider'); - } - return context; -}; diff --git a/packages/classic-webview/src/hooks/useWordSubmission.tsx b/packages/classic-webview/src/hooks/useWordSubmission.tsx new file mode 100644 index 00000000..2dbcd908 --- /dev/null +++ b/packages/classic-webview/src/hooks/useWordSubmission.tsx @@ -0,0 +1,26 @@ +import { createContext, useContext, ReactNode, useState } from 'react'; + +type WordSubmissionStateContext = { + isSubmitting: boolean; + setIsSubmitting: (isSubmitting: boolean) => void; +}; + +const WordSubmissionContext = createContext(null); + +export const WordSubmissionProvider = ({ children }: { children: ReactNode }) => { + const [isSubmitting, setIsSubmitting] = useState(false); + + return ( + + {children} + + ); +}; + +export const useWordSubmission = () => { + const context = useContext(WordSubmissionContext); + if (context === null) { + throw new Error('useWordSubmission must be used within a WordSubmissionProvider'); + } + return context; +}; diff --git a/packages/classic-webview/src/main.tsx b/packages/classic-webview/src/main.tsx index 37b8d239..668c97f5 100644 --- a/packages/classic-webview/src/main.tsx +++ b/packages/classic-webview/src/main.tsx @@ -12,6 +12,7 @@ import { ConfirmationDialogProvider } from '@hotandcold/webview-common/hooks/use import { IS_DETACHED } from './constants'; import { ModalContextProvider } from './hooks/useModal'; import { HardcoreAccessContextProvider } from './hooks/useHardcoreAccess'; +import { WordSubmissionProvider } from './hooks/useWordSubmission'; console.log('webview main called'); @@ -25,13 +26,15 @@ createRoot(document.getElementById('root')!).render( - - - - - - - + + + + + + + + + diff --git a/packages/classic-webview/src/pages/PlayPage.tsx b/packages/classic-webview/src/pages/PlayPage.tsx index 6ba20698..85483053 100644 --- a/packages/classic-webview/src/pages/PlayPage.tsx +++ b/packages/classic-webview/src/pages/PlayPage.tsx @@ -2,7 +2,8 @@ import { useEffect, useState } from 'react'; import { sendMessageToDevvit } from '../utils'; import { WordInput } from '@hotandcold/webview-common/components/wordInput'; import { Guesses } from '../components/guesses'; -import { useGame, useWordSubmission } from '../hooks/useGame'; +import { useGame } from '../hooks/useGame'; +import { useWordSubmission } from '../hooks/useWordSubmission'; import { useDevvitListener } from '../hooks/useDevvitListener'; import clsx from 'clsx'; import { FeedbackResponse } from '@hotandcold/classic-shared'; @@ -15,14 +16,17 @@ import { UnlockHardcoreCTAContent } from '../components/UnlockHardcoreCTAContent const useFeedback = (): { feedback: FeedbackResponse | null; dismissFeedback: () => void } => { const [feedback, setFeedback] = useState(null); - const { setIsSubmitting } = useWordSubmission(); const message = useDevvitListener('FEEDBACK'); + const { setIsSubmitting } = useWordSubmission(); useEffect(() => { if (!message) return; - setFeedback(message); + + // Reset the submission state when feedback is received + // This handles the case where the user submits a word they've already guessed setIsSubmitting(false); - }, [message]); + setFeedback(message); + }, [message, setIsSubmitting]); const dismissFeedback = () => { setFeedback(null); From c5f67e74da7423a23adb6e53b1bb7b41cc6d3fc3 Mon Sep 17 00:00:00 2001 From: Thomas Wang Date: Tue, 6 May 2025 11:24:19 -0400 Subject: [PATCH 3/3] lint fix --- .../src/components/wordInput.tsx | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/packages/webview-common/src/components/wordInput.tsx b/packages/webview-common/src/components/wordInput.tsx index caebcc98..a0600489 100644 --- a/packages/webview-common/src/components/wordInput.tsx +++ b/packages/webview-common/src/components/wordInput.tsx @@ -2,7 +2,7 @@ import { AnimatePresence, motion } from 'motion/react'; import { useCallback, useEffect, useRef, useState } from 'react'; import { cn } from '@hotandcold/webview-common/utils'; import { PrimaryButton } from './button'; -import { useWordSubmission } from '@hotandcold/classic-webview/src/hooks/useGame'; +import { useWordSubmission } from '@hotandcold/classic-webview/src/hooks/useWordSubmission'; type PixelData = { x: number; @@ -12,11 +12,27 @@ type PixelData = { }; // Spinning loading indicator component -const SpinningCircle = ({ className = "h-5 w-5 text-white" }: { className?: string }) => ( -
- - - +const SpinningCircle = ({ className = 'h-5 w-5 text-white' }: { className?: string }) => ( +
+ + +
);