Fix #52: Put countdown timer and share button in stats modal
This commit is contained in:
parent
51457ee8d4
commit
762750e67d
12 changed files with 92 additions and 138 deletions
21
package-lock.json
generated
21
package-lock.json
generated
|
@ -19,6 +19,7 @@
|
|||
"@types/react-dom": "^17.0.11",
|
||||
"classnames": "^2.3.1",
|
||||
"react": "^17.0.2",
|
||||
"react-countdown": "^2.3.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "5.0.0",
|
||||
"typescript": "^4.5.4",
|
||||
|
@ -12901,6 +12902,18 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/react-countdown": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz",
|
||||
"integrity": "sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">= 15",
|
||||
"react-dom": ">= 15"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dev-utils": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
|
||||
|
@ -25058,6 +25071,14 @@
|
|||
"whatwg-fetch": "^3.6.2"
|
||||
}
|
||||
},
|
||||
"react-countdown": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.2.tgz",
|
||||
"integrity": "sha512-Q4SADotHtgOxNWhDdvgupmKVL0pMB9DvoFcxv5AzjsxVhzOVxnttMbAywgqeOdruwEAmnPhOhNv/awAgkwru2w==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"react-dev-utils": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
|
||||
|
|
|
@ -17,7 +17,8 @@
|
|||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "5.0.0",
|
||||
"typescript": "^4.5.4",
|
||||
"web-vitals": "^2.1.3"
|
||||
"web-vitals": "^2.1.3",
|
||||
"react-countdown": "^2.3.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
|
52
src/App.tsx
52
src/App.tsx
|
@ -6,8 +6,8 @@ import { Grid } from './components/grid/Grid'
|
|||
import { Keyboard } from './components/keyboard/Keyboard'
|
||||
import { AboutModal } from './components/modals/AboutModal'
|
||||
import { InfoModal } from './components/modals/InfoModal'
|
||||
import { WinModal } from './components/modals/WinModal'
|
||||
import { StatsModal } from './components/modals/StatsModal'
|
||||
import { WIN_MESSAGES } from './constants/strings'
|
||||
import { isWordInWordList, isWinningWord, solution } from './lib/words'
|
||||
import { addStatsForCompletedGame, loadStats } from './lib/stats'
|
||||
import {
|
||||
|
@ -15,17 +15,18 @@ import {
|
|||
saveGameStateToLocalStorage,
|
||||
} from './lib/localStorage'
|
||||
|
||||
const ALERT_TIME_MS = 2000;
|
||||
|
||||
function App() {
|
||||
const [currentGuess, setCurrentGuess] = useState('')
|
||||
const [isGameWon, setIsGameWon] = useState(false)
|
||||
const [isWinModalOpen, setIsWinModalOpen] = useState(false)
|
||||
const [isInfoModalOpen, setIsInfoModalOpen] = useState(false)
|
||||
const [isAboutModalOpen, setIsAboutModalOpen] = useState(false)
|
||||
const [isNotEnoughLetters, setIsNotEnoughLetters] = useState(false)
|
||||
const [isStatsModalOpen, setIsStatsModalOpen] = useState(false)
|
||||
const [isWordNotFoundAlertOpen, setIsWordNotFoundAlertOpen] = useState(false)
|
||||
const [isGameLost, setIsGameLost] = useState(false)
|
||||
const [shareComplete, setShareComplete] = useState(false)
|
||||
const [successAlert, setSuccessAlert] = useState('')
|
||||
const [guesses, setGuesses] = useState<string[]>(() => {
|
||||
const loaded = loadGameStateFromLocalStorage()
|
||||
if (loaded?.solution !== solution) {
|
||||
|
@ -49,9 +50,18 @@ function App() {
|
|||
|
||||
useEffect(() => {
|
||||
if (isGameWon) {
|
||||
setIsWinModalOpen(true)
|
||||
setSuccessAlert(WIN_MESSAGES[Math.floor(Math.random()*WIN_MESSAGES.length)]);
|
||||
setTimeout(() => {
|
||||
setSuccessAlert('');
|
||||
setIsStatsModalOpen(true);
|
||||
}, ALERT_TIME_MS);
|
||||
}
|
||||
}, [isGameWon])
|
||||
if (isGameLost) {
|
||||
setTimeout(() => {
|
||||
setIsStatsModalOpen(true);
|
||||
}, ALERT_TIME_MS);
|
||||
}
|
||||
}, [isGameWon, isGameLost])
|
||||
|
||||
const onChar = (value: string) => {
|
||||
if (currentGuess.length < 5 && guesses.length < 6 && !isGameWon) {
|
||||
|
@ -64,18 +74,19 @@ function App() {
|
|||
}
|
||||
|
||||
const onEnter = () => {
|
||||
if (!(currentGuess.length === 5) && !isGameLost) {
|
||||
if (isGameWon || isGameLost) { return; }
|
||||
if (!(currentGuess.length === 5)) {
|
||||
setIsNotEnoughLetters(true)
|
||||
return setTimeout(() => {
|
||||
setIsNotEnoughLetters(false)
|
||||
}, 2000)
|
||||
}, ALERT_TIME_MS)
|
||||
}
|
||||
|
||||
if (!isWordInWordList(currentGuess)) {
|
||||
setIsWordNotFoundAlertOpen(true)
|
||||
return setTimeout(() => {
|
||||
setIsWordNotFoundAlertOpen(false)
|
||||
}, 2000)
|
||||
}, ALERT_TIME_MS)
|
||||
}
|
||||
|
||||
const winningWord = isWinningWord(currentGuess)
|
||||
|
@ -116,18 +127,6 @@ function App() {
|
|||
onEnter={onEnter}
|
||||
guesses={guesses}
|
||||
/>
|
||||
<WinModal
|
||||
isOpen={isWinModalOpen}
|
||||
handleClose={() => setIsWinModalOpen(false)}
|
||||
guesses={guesses}
|
||||
handleShare={() => {
|
||||
setIsWinModalOpen(false)
|
||||
setShareComplete(true)
|
||||
return setTimeout(() => {
|
||||
setShareComplete(false)
|
||||
}, 2000)
|
||||
}}
|
||||
/>
|
||||
<InfoModal
|
||||
isOpen={isInfoModalOpen}
|
||||
handleClose={() => setIsInfoModalOpen(false)}
|
||||
|
@ -135,7 +134,14 @@ function App() {
|
|||
<StatsModal
|
||||
isOpen={isStatsModalOpen}
|
||||
handleClose={() => setIsStatsModalOpen(false)}
|
||||
guesses={guesses}
|
||||
gameStats={stats}
|
||||
isGameLost={isGameLost}
|
||||
isGameWon={isGameWon}
|
||||
handleShare={() => {
|
||||
setSuccessAlert("Game copied to clipboard");
|
||||
return setTimeout(() => setSuccessAlert(''), ALERT_TIME_MS);
|
||||
}}
|
||||
/>
|
||||
<AboutModal
|
||||
isOpen={isAboutModalOpen}
|
||||
|
@ -153,12 +159,12 @@ function App() {
|
|||
<Alert message="Not enough letters" isOpen={isNotEnoughLetters} />
|
||||
<Alert message="Word not found" isOpen={isWordNotFoundAlertOpen} />
|
||||
<Alert
|
||||
message={`You lost, the word was ${solution}`}
|
||||
message={`The word was ${solution}`}
|
||||
isOpen={isGameLost}
|
||||
/>
|
||||
<Alert
|
||||
message="Game copied to clipboard"
|
||||
isOpen={shareComplete}
|
||||
message={successAlert}
|
||||
isOpen={successAlert!==''}
|
||||
variant="success"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -13,7 +13,7 @@ export const Alert = ({ isOpen, message, variant = 'warning' }: Props) => {
|
|||
'fixed top-20 left-1/2 transform -translate-x-1/2 max-w-sm w-full shadow-lg rounded-lg pointer-events-auto ring-1 ring-black ring-opacity-5 overflow-hidden',
|
||||
{
|
||||
'bg-rose-200': variant === 'warning',
|
||||
'bg-green-200': variant === 'success',
|
||||
'bg-green-200 z-20': variant === 'success',
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import { CharStatus } from '../../lib/statuses'
|
||||
import classnames from 'classnames'
|
||||
|
||||
type Props = {
|
||||
status: CharStatus
|
||||
}
|
||||
|
||||
export const MiniCell = ({ status }: Props) => {
|
||||
const classes = classnames(
|
||||
'w-10 h-10 border-solid border-2 border-slate-200 flex items-center justify-center mx-0.5 text-lg font-bold rounded',
|
||||
{
|
||||
'bg-white': status === 'absent',
|
||||
'bg-green-500': status === 'correct',
|
||||
'bg-yellow-500': status === 'present',
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes}></div>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
import { getGuessStatuses } from '../../lib/statuses'
|
||||
import { MiniCell } from './MiniCell'
|
||||
|
||||
type Props = {
|
||||
guess: string
|
||||
}
|
||||
|
||||
export const MiniCompletedRow = ({ guess }: Props) => {
|
||||
const statuses = getGuessStatuses(guess)
|
||||
|
||||
return (
|
||||
<div className="flex justify-center mb-1">
|
||||
{guess.split('').map((letter, i) => (
|
||||
<MiniCell key={i} status={statuses[i]} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
import { MiniCompletedRow } from './MiniCompletedRow'
|
||||
|
||||
type Props = {
|
||||
guesses: string[]
|
||||
}
|
||||
|
||||
export const MiniGrid = ({ guesses }: Props) => {
|
||||
return (
|
||||
<div className="pb-6">
|
||||
{guesses.map((guess, i) => (
|
||||
<MiniCompletedRow key={i} guess={guess} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,15 +1,29 @@
|
|||
import Countdown from "react-countdown"
|
||||
import { StatBar } from '../stats/StatBar'
|
||||
import { Histogram } from '../stats/Histogram'
|
||||
import { GameStats } from '../../lib/localStorage'
|
||||
import { shareStatus } from '../../lib/share'
|
||||
import { tomorrow } from '../../lib/words'
|
||||
import { BaseModal } from './BaseModal'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
handleClose: () => void
|
||||
guesses: string[]
|
||||
gameStats: GameStats
|
||||
isGameLost: boolean
|
||||
isGameWon: boolean
|
||||
handleShare: () => void
|
||||
}
|
||||
|
||||
export const StatsModal = ({ isOpen, handleClose, gameStats }: Props) => {
|
||||
export const StatsModal = ({ isOpen, handleClose, guesses, gameStats, isGameLost, isGameWon, handleShare }: Props) => {
|
||||
if (gameStats.totalGames <= 0) {
|
||||
return (
|
||||
<BaseModal title="Statistics" isOpen={isOpen} handleClose={handleClose}>
|
||||
<StatBar gameStats={gameStats} />
|
||||
</BaseModal>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<BaseModal title="Statistics" isOpen={isOpen} handleClose={handleClose}>
|
||||
<StatBar gameStats={gameStats} />
|
||||
|
@ -17,6 +31,24 @@ export const StatsModal = ({ isOpen, handleClose, gameStats }: Props) => {
|
|||
Guess Distribution
|
||||
</h4>
|
||||
<Histogram gameStats={gameStats} />
|
||||
{(isGameLost || isGameWon) &&
|
||||
<div className="mt-5 sm:mt-6 columns-2">
|
||||
<div>
|
||||
<h5>New word in</h5>
|
||||
<Countdown className="text-lg font-medium text-gray-900" date={tomorrow} daysInHours={true} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-2 w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm"
|
||||
onClick={() => {
|
||||
shareStatus(guesses, isGameLost)
|
||||
handleShare()
|
||||
}}
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</BaseModal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { Dialog } from '@headlessui/react'
|
||||
import { CheckIcon } from '@heroicons/react/outline'
|
||||
import { MiniGrid } from '../mini-grid/MiniGrid'
|
||||
import { shareStatus } from '../../lib/share'
|
||||
import { BaseModal } from './BaseModal'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
handleClose: () => void
|
||||
guesses: string[]
|
||||
handleShare: () => void
|
||||
}
|
||||
|
||||
export const WinModal = ({
|
||||
isOpen,
|
||||
handleClose,
|
||||
guesses,
|
||||
handleShare,
|
||||
}: Props) => {
|
||||
return (
|
||||
<BaseModal title="You won!" isOpen={isOpen} handleClose={handleClose}>
|
||||
<div>
|
||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||
<CheckIcon className="h-6 w-6 text-green-600" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-lg leading-6 font-medium text-gray-900"
|
||||
>
|
||||
You won!
|
||||
</Dialog.Title>
|
||||
<div className="mt-2">
|
||||
<MiniGrid guesses={guesses} />
|
||||
<p className="text-sm text-gray-500">Great job.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex justify-center w-full rounded-md border border-transparent shadow-sm px-4 py-2 bg-indigo-600 text-base font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 sm:text-sm"
|
||||
onClick={() => {
|
||||
shareStatus(guesses)
|
||||
handleShare()
|
||||
}}
|
||||
>
|
||||
Share
|
||||
</button>
|
||||
</div>
|
||||
</BaseModal>
|
||||
)
|
||||
}
|
1
src/constants/strings.ts
Normal file
1
src/constants/strings.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export const WIN_MESSAGES = ["Great Job!", "Awesome", "Well done!"];
|
|
@ -1,9 +1,9 @@
|
|||
import { getGuessStatuses } from './statuses'
|
||||
import { solutionIndex } from './words'
|
||||
|
||||
export const shareStatus = (guesses: string[]) => {
|
||||
export const shareStatus = (guesses: string[], lost: boolean) => {
|
||||
navigator.clipboard.writeText(
|
||||
`Not Wordle ${solutionIndex} ${guesses.length}/6\n\n` +
|
||||
`Not Wordle ${solutionIndex} ${lost?"X":guesses.length}/6\n\n` +
|
||||
generateEmojiGrid(guesses)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,11 +18,13 @@ export const getWordOfDay = () => {
|
|||
const now = Date.now()
|
||||
const msInDay = 86400000
|
||||
const index = Math.floor((now - epochMs) / msInDay)
|
||||
const nextday = (index+1)*msInDay + epochMs;
|
||||
|
||||
return {
|
||||
solution: WORDS[index].toUpperCase(),
|
||||
solutionIndex: index,
|
||||
tomorrow: nextday,
|
||||
}
|
||||
}
|
||||
|
||||
export const { solution, solutionIndex } = getWordOfDay()
|
||||
export const { solution, solutionIndex, tomorrow } = getWordOfDay()
|
||||
|
|
Loading…
Add table
Reference in a new issue