Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/classic-webview/src/hooks/useGame.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Partial<Game>>({});
const GameUpdaterContext = createContext<React.Dispatch<
Expand All @@ -13,6 +14,8 @@ const GameUpdaterContext = createContext<React.Dispatch<
export const GameContextProvider = ({ children }: { children: React.ReactNode }) => {
const mocks = useMocks();
const [game, setGame] = useState<Partial<Game>>(mocks.getMock('mocks')?.game ?? {});
const { setIsSubmitting } = useWordSubmission();

const initResponse = useDevvitListener('GAME_INIT_RESPONSE');
const submissionResponse = useDevvitListener('WORD_SUBMITTED_RESPONSE');
const hintResponse = useDevvitListener('HINT_RESPONSE');
Expand All @@ -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);
Expand Down
26 changes: 26 additions & 0 deletions packages/classic-webview/src/hooks/useWordSubmission.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { createContext, useContext, ReactNode, useState } from 'react';

type WordSubmissionStateContext = {
isSubmitting: boolean;
setIsSubmitting: (isSubmitting: boolean) => void;
};

const WordSubmissionContext = createContext<WordSubmissionStateContext | null>(null);

export const WordSubmissionProvider = ({ children }: { children: ReactNode }) => {
const [isSubmitting, setIsSubmitting] = useState(false);

return (
<WordSubmissionContext.Provider value={{ isSubmitting, setIsSubmitting }}>
{children}
</WordSubmissionContext.Provider>
);
};

export const useWordSubmission = () => {
const context = useContext(WordSubmissionContext);
if (context === null) {
throw new Error('useWordSubmission must be used within a WordSubmissionProvider');
}
return context;
};
17 changes: 10 additions & 7 deletions packages/classic-webview/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -25,13 +26,15 @@ createRoot(document.getElementById('root')!).render(
<ConfirmationDialogProvider>
<UserSettingsContextProvider>
<HardcoreAccessContextProvider>
<GameContextProvider>
<ModalContextProvider>
<PageContextProvider>
<App />
</PageContextProvider>
</ModalContextProvider>
</GameContextProvider>
<WordSubmissionProvider>
<GameContextProvider>
<ModalContextProvider>
<PageContextProvider>
<App />
</PageContextProvider>
</ModalContextProvider>
</GameContextProvider>
</WordSubmissionProvider>
</HardcoreAccessContextProvider>
</UserSettingsContextProvider>
</ConfirmationDialogProvider>
Expand Down
10 changes: 9 additions & 1 deletion packages/classic-webview/src/pages/PlayPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -16,11 +17,16 @@ import { UnlockHardcoreCTAContent } from '../components/UnlockHardcoreCTAContent
const useFeedback = (): { feedback: FeedbackResponse | null; dismissFeedback: () => void } => {
const [feedback, setFeedback] = useState<FeedbackResponse | null>(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);
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -187,6 +194,7 @@ const GameplayContent = () => {
return;
}

setIsSubmitting(true);
sendMessageToDevvit({
type: 'WORD_SUBMITTED',
value: word.trim().toLowerCase(),
Expand Down
44 changes: 38 additions & 6 deletions packages/webview-common/src/components/wordInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -10,6 +11,32 @@ type PixelData = {
color: string;
};

// Spinning loading indicator component
const SpinningCircle = ({ className = 'h-5 w-5 text-white' }: { className?: string }) => (
<div className="flex h-5 w-5 items-center justify-center">
<svg
className={`animate-spin ${className}`}
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
);

export function WordInput({
placeholders,
onChange,
Expand All @@ -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(() => {
Expand Down Expand Up @@ -232,7 +260,7 @@ export function WordInput({
};

const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !animating) {
if (e.key === 'Enter' && !animating && !isSubmitting) {
handleSubmit();
}
};
Expand All @@ -248,7 +276,7 @@ export function WordInput({
/>
<input
onChange={(e) => {
if (!animating && onChange) {
if (!animating && !isSubmitting && onChange) {
onChange(e);
}
}}
Expand All @@ -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'
)}
/>

<PrimaryButton
isHighContrast={isHighContrast}
disabled={!internalValue}
disabled={!internalValue || isSubmitting}
type="submit"
className="z-50 flex-shrink-0"
onMouseDown={(e) => {
// Workaround for ios and android blurring the input on button click
e.preventDefault();
handleSubmit();
if (!isSubmitting) {
handleSubmit();
}
}}
>
Guess
{isSubmitting ? <SpinningCircle /> : 'Guess'}
</PrimaryButton>

<div className="pointer-events-none absolute inset-0 z-[1010] flex items-center rounded-full">
Expand Down