diff --git a/packages/classic-webview/src/hooks/useGame.tsx b/packages/classic-webview/src/hooks/useGame.tsx index 56578d14..65eb6a9e 100644 --- a/packages/classic-webview/src/hooks/useGame.tsx +++ b/packages/classic-webview/src/hooks/useGame.tsx @@ -4,6 +4,7 @@ import { sendMessageToDevvit } from '../utils'; import { useDevvitListener } from './useDevvitListener'; import { logger } from '../utils/logger'; import { useMocks } from './useMocks'; +import { useWordSubmission } from './useWordSubmission'; const GameContext = createContext>({}); const GameUpdaterContext = createContext { const mocks = useMocks(); const [game, setGame] = useState>(mocks.getMock('mocks')?.game ?? {}); + const { setIsSubmitting } = useWordSubmission(); + const initResponse = useDevvitListener('GAME_INIT_RESPONSE'); const submissionResponse = useDevvitListener('WORD_SUBMITTED_RESPONSE'); const hintResponse = useDevvitListener('HINT_RESPONSE'); @@ -37,8 +40,9 @@ export const GameContextProvider = ({ children }: { children: React.ReactNode }) logger.log('Submission response: ', submissionResponse); if (submissionResponse) { setGame(submissionResponse); + setIsSubmitting(false); } - }, [submissionResponse]); + }, [submissionResponse, setIsSubmitting]); useEffect(() => { logger.log('Hint response: ', hintResponse); 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 11546bcf..85483053 100644 --- a/packages/classic-webview/src/pages/PlayPage.tsx +++ b/packages/classic-webview/src/pages/PlayPage.tsx @@ -3,6 +3,7 @@ import { sendMessageToDevvit } from '../utils'; import { WordInput } from '@hotandcold/webview-common/components/wordInput'; import { Guesses } from '../components/guesses'; 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'; @@ -16,11 +17,16 @@ import { UnlockHardcoreCTAContent } from '../components/UnlockHardcoreCTAContent const useFeedback = (): { feedback: FeedbackResponse | null; dismissFeedback: () => void } => { const [feedback, setFeedback] = useState(null); const message = useDevvitListener('FEEDBACK'); + const { setIsSubmitting } = useWordSubmission(); useEffect(() => { if (!message) return; + + // Reset the submission state when feedback is received + // This handles the case where the user submits a word they've already guessed + setIsSubmitting(false); setFeedback(message); - }, [message]); + }, [message, setIsSubmitting]); const dismissFeedback = () => { setFeedback(null); @@ -144,6 +150,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 +194,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..a0600489 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/useWordSubmission'; type PixelData = { x: number; @@ -10,6 +11,32 @@ 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 +55,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 +260,7 @@ export function WordInput({ }; const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !animating) { + if (e.key === 'Enter' && !animating && !isSubmitting) { handleSubmit(); } }; @@ -248,7 +276,7 @@ export function WordInput({ /> { - if (!animating && onChange) { + if (!animating && !isSubmitting && onChange) { onChange(e); } }} @@ -260,26 +288,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'}