Merge branch 'main' of git://github.com/hannahcode/wordle into open-win-modal-on-page-load

This commit is contained in:
gbear605 2022-01-16 21:22:48 -05:00
commit 5961723d23
31 changed files with 13359 additions and 13345 deletions

2
.prettierrc Normal file
View file

@ -0,0 +1,2 @@
singleQuote: true
semi: false

17
package-lock.json generated
View file

@ -18,6 +18,7 @@
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"classnames": "^2.3.1",
"prettier": "^2.5.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
@ -12628,6 +12629,17 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
@ -24849,6 +24861,11 @@
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
},
"prettier": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg=="
},
"pretty-bytes": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",

View file

@ -46,6 +46,7 @@
"devDependencies": {
"autoprefixer": "^10.4.2",
"postcss": "^8.4.5",
"tailwindcss": "^3.0.12"
"tailwindcss": "^3.0.12",
"prettier": "^2.5.1"
}
}

View file

@ -1,9 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App';
import React from 'react'
import { render, screen } from '@testing-library/react'
import App from './App'
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});
render(<App />)
const linkElement = screen.getByText(/learn react/i)
expect(linkElement).toBeInTheDocument()
})

View file

@ -1,83 +1,83 @@
import { InformationCircleIcon } from "@heroicons/react/outline";
import { useState, useEffect } from "react";
import { Alert } from "./components/alerts/Alert";
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 { isWordInWordList, isWinningWord, solution } from "./lib/words";
import { InformationCircleIcon } from '@heroicons/react/outline'
import { useState, useEffect } from 'react'
import { Alert } from './components/alerts/Alert'
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 { isWordInWordList, isWinningWord, solution } from './lib/words'
import {
loadGameStateFromLocalStorage,
saveGameStateToLocalStorage,
} from "./lib/localStorage";
} from './lib/localStorage'
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 [isWordNotFoundAlertOpen, setIsWordNotFoundAlertOpen] = useState(false);
const [isGameLost, setIsGameLost] = useState(false);
const [shareComplete, setShareComplete] = useState(false);
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 [isWordNotFoundAlertOpen, setIsWordNotFoundAlertOpen] = useState(false)
const [isGameLost, setIsGameLost] = useState(false)
const [shareComplete, setShareComplete] = useState(false)
const [guesses, setGuesses] = useState<string[]>(() => {
const loaded = loadGameStateFromLocalStorage();
const loaded = loadGameStateFromLocalStorage()
if (loaded?.solution !== solution) {
return [];
return []
}
if (loaded.guesses.includes(solution)) {
setIsGameWon(true);
setIsGameWon(true)
}
return loaded.guesses;
});
return loaded.guesses
})
useEffect(() => {
saveGameStateToLocalStorage({ guesses, solution });
}, [guesses]);
saveGameStateToLocalStorage({ guesses, solution })
}, [guesses])
useEffect(() => {
if (isGameWon) {
setIsWinModalOpen(true);
setIsWinModalOpen(true)
}
}, [isGameWon]);
}, [isGameWon])
const onChar = (value: string) => {
if (currentGuess.length < 5 && guesses.length < 6) {
setCurrentGuess(`${currentGuess}${value}`);
setCurrentGuess(`${currentGuess}${value}`)
}
}
};
const onDelete = () => {
setCurrentGuess(currentGuess.slice(0, -1));
};
setCurrentGuess(currentGuess.slice(0, -1))
}
const onEnter = () => {
if (!isWordInWordList(currentGuess)) {
setIsWordNotFoundAlertOpen(true);
setIsWordNotFoundAlertOpen(true)
return setTimeout(() => {
setIsWordNotFoundAlertOpen(false);
}, 2000);
setIsWordNotFoundAlertOpen(false)
}, 2000)
}
const winningWord = isWinningWord(currentGuess);
const winningWord = isWinningWord(currentGuess)
if (currentGuess.length === 5 && guesses.length < 6 && !isGameWon) {
setGuesses([...guesses, currentGuess]);
setCurrentGuess("");
setGuesses([...guesses, currentGuess])
setCurrentGuess('')
if (winningWord) {
return setIsGameWon(true);
return setIsGameWon(true)
}
if (guesses.length === 5) {
setIsGameLost(true);
setIsGameLost(true)
return setTimeout(() => {
setIsGameLost(false);
}, 2000);
setIsGameLost(false)
}, 2000)
}
}
}
};
return (
<div className="py-8 max-w-7xl mx-auto sm:px-6 lg:px-8">
@ -110,11 +110,11 @@ function App() {
handleClose={() => setIsWinModalOpen(false)}
guesses={guesses}
handleShare={() => {
setIsWinModalOpen(false);
setShareComplete(true);
setIsWinModalOpen(false)
setShareComplete(true)
return setTimeout(() => {
setShareComplete(false);
}, 2000);
setShareComplete(false)
}, 2000)
}}
/>
<InfoModal
@ -134,7 +134,7 @@ function App() {
About this game
</button>
</div>
);
)
}
export default App;
export default App

View file

@ -1,21 +1,21 @@
import { Fragment } from "react";
import { Transition } from "@headlessui/react";
import classNames from "classnames";
import { Fragment } from 'react'
import { Transition } from '@headlessui/react'
import classNames from 'classnames'
type Props = {
isOpen: boolean;
message: string;
variant?: "success" | "warning";
};
export const Alert = ({ isOpen, message, variant = "warning" }: Props) => {
const classes = classNames(
"fixed top-2.5 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",
isOpen: boolean
message: string
variant?: 'success' | 'warning'
}
);
export const Alert = ({ isOpen, message, variant = 'warning' }: Props) => {
const classes = classNames(
'fixed top-2.5 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',
}
)
return (
<Transition
@ -36,5 +36,5 @@ export const Alert = ({ isOpen, message, variant = "warning" }: Props) => {
</div>
</div>
</Transition>
);
};
)
}

View file

@ -1,25 +1,25 @@
import { CharStatus } from "../../lib/statuses";
import classnames from "classnames";
import { CharStatus } from '../../lib/statuses'
import classnames from 'classnames'
type Props = {
value?: string;
status?: CharStatus;
};
value?: string
status?: CharStatus
}
export const Cell = ({ value, status }: Props) => {
const classes = classnames(
"w-14 h-14 border-solid border-2 flex items-center justify-center mx-0.5 text-lg font-bold rounded",
'w-14 h-14 border-solid border-2 flex items-center justify-center mx-0.5 text-lg font-bold rounded',
{
"bg-white border-slate-200": !status,
"bg-slate-400 text-white border-slate-400": status === "absent",
"bg-green-500 text-white border-green-500": status === "correct",
"bg-yellow-500 text-white border-yellow-500": status === "present",
'bg-white border-slate-200': !status,
'bg-slate-400 text-white border-slate-400': status === 'absent',
'bg-green-500 text-white border-green-500': status === 'correct',
'bg-yellow-500 text-white border-yellow-500': status === 'present',
}
);
)
return (
<>
<div className={classes}>{value}</div>
</>
);
};
)
}

View file

@ -1,18 +1,18 @@
import { getGuessStatuses } from "../../lib/statuses";
import { Cell } from "./Cell";
import { getGuessStatuses } from '../../lib/statuses'
import { Cell } from './Cell'
type Props = {
guess: string;
};
guess: string
}
export const CompletedRow = ({ guess }: Props) => {
const statuses = getGuessStatuses(guess);
const statuses = getGuessStatuses(guess)
return (
<div className="flex justify-center mb-1">
{guess.split("").map((letter, i) => (
{guess.split('').map((letter, i) => (
<Cell key={i} value={letter} status={statuses[i]} />
))}
</div>
);
};
)
}

View file

@ -1,12 +1,12 @@
import { Cell } from "./Cell";
import { Cell } from './Cell'
type Props = {
guess: string;
};
guess: string
}
export const CurrentRow = ({ guess }: Props) => {
const splitGuess = guess.split("");
const emptyCells = Array.from(Array(5 - splitGuess.length));
const splitGuess = guess.split('')
const emptyCells = Array.from(Array(5 - splitGuess.length))
return (
<div className="flex justify-center mb-1">
@ -17,5 +17,5 @@ export const CurrentRow = ({ guess }: Props) => {
<Cell key={i} />
))}
</div>
);
};
)
}

View file

@ -1,7 +1,7 @@
import { Cell } from "./Cell";
import { Cell } from './Cell'
export const EmptyRow = () => {
const emptyCells = Array.from(Array(5));
const emptyCells = Array.from(Array(5))
return (
<div className="flex justify-center mb-1">
@ -9,5 +9,5 @@ export const EmptyRow = () => {
<Cell key={i} />
))}
</div>
);
};
)
}

View file

@ -1,15 +1,15 @@
import { CompletedRow } from "./CompletedRow";
import { CurrentRow } from "./CurrentRow";
import { EmptyRow } from "./EmptyRow";
import { CompletedRow } from './CompletedRow'
import { CurrentRow } from './CurrentRow'
import { EmptyRow } from './EmptyRow'
type Props = {
guesses: string[];
currentGuess: string;
};
guesses: string[]
currentGuess: string
}
export const Grid = ({ guesses, currentGuess }: Props) => {
const empties =
guesses.length < 5 ? Array.from(Array(5 - guesses.length)) : [];
guesses.length < 5 ? Array.from(Array(5 - guesses.length)) : []
return (
<div className="pb-6">
@ -21,5 +21,5 @@ export const Grid = ({ guesses, currentGuess }: Props) => {
<EmptyRow key={i} />
))}
</div>
);
};
)
}

View file

@ -1,15 +1,15 @@
import { ReactNode } from "react";
import classnames from "classnames";
import { KeyValue } from "../../lib/keyboard";
import { CharStatus } from "../../lib/statuses";
import { ReactNode } from 'react'
import classnames from 'classnames'
import { KeyValue } from '../../lib/keyboard'
import { CharStatus } from '../../lib/statuses'
type Props = {
children?: ReactNode;
value: KeyValue;
width?: number;
status?: CharStatus;
onClick: (value: KeyValue) => void;
};
children?: ReactNode
value: KeyValue
width?: number
status?: CharStatus
onClick: (value: KeyValue) => void
}
export const Key = ({
children,
@ -19,24 +19,24 @@ export const Key = ({
onClick,
}: Props) => {
const classes = classnames(
"flex items-center justify-center rounded mx-0.5 text-xs font-bold cursor-pointer",
'flex items-center justify-center rounded mx-0.5 text-xs font-bold cursor-pointer',
{
"bg-slate-200 hover:bg-slate-300 active:bg-slate-400": !status,
"bg-slate-400 text-white": status === "absent",
"bg-green-500 hover:bg-green-600 active:bg-green-700 text-white":
status === "correct",
"bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 text-white":
status === "present",
'bg-slate-200 hover:bg-slate-300 active:bg-slate-400': !status,
'bg-slate-400 text-white': status === 'absent',
'bg-green-500 hover:bg-green-600 active:bg-green-700 text-white':
status === 'correct',
'bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 text-white':
status === 'present',
}
);
)
return (
<div
style={{ width: `${width}px`, height: "58px" }}
style={{ width: `${width}px`, height: '58px' }}
className={classes}
onClick={() => onClick(value)}
>
{children || value}
</div>
);
};
)
}

View file

@ -1,87 +1,87 @@
import { KeyValue } from "../../lib/keyboard";
import { getStatuses } from "../../lib/statuses";
import { Key } from "./Key";
import {useEffect} from "react";
import { KeyValue } from '../../lib/keyboard'
import { getStatuses } from '../../lib/statuses'
import { Key } from './Key'
import { useEffect } from 'react'
type Props = {
onChar: (value: string) => void;
onDelete: () => void;
onEnter: () => void;
guesses: string[];
};
onChar: (value: string) => void
onDelete: () => void
onEnter: () => void
guesses: string[]
}
export const Keyboard = ({ onChar, onDelete, onEnter, guesses }: Props) => {
const charStatuses = getStatuses(guesses);
const charStatuses = getStatuses(guesses)
const onClick = (value: KeyValue) => {
if (value === "ENTER") {
onEnter();
} else if (value === "DELETE") {
onDelete();
if (value === 'ENTER') {
onEnter()
} else if (value === 'DELETE') {
onDelete()
} else {
onChar(value);
onChar(value)
}
}
};
useEffect(() => {
const listener = (e: KeyboardEvent) => {
if(e.code === "Enter") {
onEnter();
} else if(e.code === "Backspace") {
onDelete();
if (e.code === 'Enter') {
onEnter()
} else if (e.code === 'Backspace') {
onDelete()
} else {
const key = e.key.toUpperCase();
if(key.length === 1 && key >= "A" && key <= "Z") {
onChar(key);
const key = e.key.toUpperCase()
if (key.length === 1 && key >= 'A' && key <= 'Z') {
onChar(key)
}
}
};
window.addEventListener("keyup", listener);
}
window.addEventListener('keyup', listener)
return () => {
window.removeEventListener("keyup", listener);
};
}, [onEnter, onDelete, onChar]);
window.removeEventListener('keyup', listener)
}
}, [onEnter, onDelete, onChar])
return (
<div>
<div className="flex justify-center mb-1">
<Key value="Q" onClick={onClick} status={charStatuses["Q"]} />
<Key value="W" onClick={onClick} status={charStatuses["W"]} />
<Key value="E" onClick={onClick} status={charStatuses["E"]} />
<Key value="R" onClick={onClick} status={charStatuses["R"]} />
<Key value="T" onClick={onClick} status={charStatuses["T"]} />
<Key value="Y" onClick={onClick} status={charStatuses["Y"]} />
<Key value="U" onClick={onClick} status={charStatuses["U"]} />
<Key value="I" onClick={onClick} status={charStatuses["I"]} />
<Key value="O" onClick={onClick} status={charStatuses["O"]} />
<Key value="P" onClick={onClick} status={charStatuses["P"]} />
<Key value="Q" onClick={onClick} status={charStatuses['Q']} />
<Key value="W" onClick={onClick} status={charStatuses['W']} />
<Key value="E" onClick={onClick} status={charStatuses['E']} />
<Key value="R" onClick={onClick} status={charStatuses['R']} />
<Key value="T" onClick={onClick} status={charStatuses['T']} />
<Key value="Y" onClick={onClick} status={charStatuses['Y']} />
<Key value="U" onClick={onClick} status={charStatuses['U']} />
<Key value="I" onClick={onClick} status={charStatuses['I']} />
<Key value="O" onClick={onClick} status={charStatuses['O']} />
<Key value="P" onClick={onClick} status={charStatuses['P']} />
</div>
<div className="flex justify-center mb-1">
<Key value="A" onClick={onClick} status={charStatuses["A"]} />
<Key value="S" onClick={onClick} status={charStatuses["S"]} />
<Key value="D" onClick={onClick} status={charStatuses["D"]} />
<Key value="F" onClick={onClick} status={charStatuses["F"]} />
<Key value="G" onClick={onClick} status={charStatuses["G"]} />
<Key value="H" onClick={onClick} status={charStatuses["H"]} />
<Key value="J" onClick={onClick} status={charStatuses["J"]} />
<Key value="K" onClick={onClick} status={charStatuses["K"]} />
<Key value="L" onClick={onClick} status={charStatuses["L"]} />
<Key value="A" onClick={onClick} status={charStatuses['A']} />
<Key value="S" onClick={onClick} status={charStatuses['S']} />
<Key value="D" onClick={onClick} status={charStatuses['D']} />
<Key value="F" onClick={onClick} status={charStatuses['F']} />
<Key value="G" onClick={onClick} status={charStatuses['G']} />
<Key value="H" onClick={onClick} status={charStatuses['H']} />
<Key value="J" onClick={onClick} status={charStatuses['J']} />
<Key value="K" onClick={onClick} status={charStatuses['K']} />
<Key value="L" onClick={onClick} status={charStatuses['L']} />
</div>
<div className="flex justify-center">
<Key width={65.4} value="ENTER" onClick={onClick}>
Enter
</Key>
<Key value="Z" onClick={onClick} status={charStatuses["Z"]} />
<Key value="X" onClick={onClick} status={charStatuses["X"]} />
<Key value="C" onClick={onClick} status={charStatuses["C"]} />
<Key value="V" onClick={onClick} status={charStatuses["V"]} />
<Key value="B" onClick={onClick} status={charStatuses["B"]} />
<Key value="N" onClick={onClick} status={charStatuses["N"]} />
<Key value="M" onClick={onClick} status={charStatuses["M"]} />
<Key value="Z" onClick={onClick} status={charStatuses['Z']} />
<Key value="X" onClick={onClick} status={charStatuses['X']} />
<Key value="C" onClick={onClick} status={charStatuses['C']} />
<Key value="V" onClick={onClick} status={charStatuses['V']} />
<Key value="B" onClick={onClick} status={charStatuses['B']} />
<Key value="N" onClick={onClick} status={charStatuses['N']} />
<Key value="M" onClick={onClick} status={charStatuses['M']} />
<Key width={65.4} value="DELETE" onClick={onClick}>
Delete
</Key>
</div>
</div>
);
};
)
}

View file

@ -1,23 +1,23 @@
import { CharStatus } from "../../lib/statuses";
import classnames from "classnames";
import { CharStatus } from '../../lib/statuses'
import classnames from 'classnames'
type Props = {
status: CharStatus;
};
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",
'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",
'bg-white': status === 'absent',
'bg-green-500': status === 'correct',
'bg-yellow-500': status === 'present',
}
);
)
return (
<>
<div className={classes}></div>
</>
);
};
)
}

View file

@ -1,18 +1,18 @@
import { getGuessStatuses } from "../../lib/statuses";
import { MiniCell } from "./MiniCell";
import { getGuessStatuses } from '../../lib/statuses'
import { MiniCell } from './MiniCell'
type Props = {
guess: string;
};
guess: string
}
export const MiniCompletedRow = ({ guess }: Props) => {
const statuses = getGuessStatuses(guess);
const statuses = getGuessStatuses(guess)
return (
<div className="flex justify-center mb-1">
{guess.split("").map((letter, i) => (
{guess.split('').map((letter, i) => (
<MiniCell key={i} status={statuses[i]} />
))}
</div>
);
};
)
}

View file

@ -1,8 +1,8 @@
import { MiniCompletedRow } from "./MiniCompletedRow";
import { MiniCompletedRow } from './MiniCompletedRow'
type Props = {
guesses: string[];
};
guesses: string[]
}
export const MiniGrid = ({ guesses }: Props) => {
return (
@ -11,5 +11,5 @@ export const MiniGrid = ({ guesses }: Props) => {
<MiniCompletedRow key={i} guess={guess} />
))}
</div>
);
};
)
}

View file

@ -1,10 +1,10 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
type Props = {
isOpen: boolean;
handleClose: () => void;
};
isOpen: boolean
handleClose: () => void
}
export const AboutModal = ({ isOpen, handleClose }: Props) => {
return (
@ -54,14 +54,14 @@ export const AboutModal = ({ isOpen, handleClose }: Props) => {
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-500">
This is an open source clone of the game Wordle -{" "}
This is an open source clone of the game Wordle -{' '}
<a
href="https://github.com/hannahcode/wordle"
className="underline font-bold"
>
check out the code here
</a>{" "}
and{" "}
</a>{' '}
and{' '}
<a
href="https://www.powerlanguage.co.uk/wordle/"
className="underline font-bold"
@ -77,5 +77,5 @@ export const AboutModal = ({ isOpen, handleClose }: Props) => {
</div>
</Dialog>
</Transition.Root>
);
};
)
}

View file

@ -1,11 +1,11 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { Cell } from "../grid/Cell";
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { Cell } from '../grid/Cell'
type Props = {
isOpen: boolean;
handleClose: () => void;
};
isOpen: boolean
handleClose: () => void
}
export const InfoModal = ({ isOpen, handleClose }: Props) => {
return (
@ -100,5 +100,5 @@ export const InfoModal = ({ isOpen, handleClose }: Props) => {
</div>
</Dialog>
</Transition.Root>
);
};
)
}

View file

@ -1,15 +1,15 @@
import { Fragment } from "react";
import { Dialog, Transition } from "@headlessui/react";
import { CheckIcon } from "@heroicons/react/outline";
import { MiniGrid } from "../mini-grid/MiniGrid";
import { shareStatus } from "../../lib/share";
import { Fragment } from 'react'
import { Dialog, Transition } from '@headlessui/react'
import { CheckIcon } from '@heroicons/react/outline'
import { MiniGrid } from '../mini-grid/MiniGrid'
import { shareStatus } from '../../lib/share'
type Props = {
isOpen: boolean;
handleClose: () => void;
guesses: string[];
handleShare: () => void;
};
isOpen: boolean
handleClose: () => void
guesses: string[]
handleShare: () => void
}
export const WinModal = ({
isOpen,
@ -79,8 +79,8 @@ export const WinModal = ({
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();
shareStatus(guesses)
handleShare()
}}
>
Share
@ -91,5 +91,5 @@ export const WinModal = ({
</div>
</Dialog>
</Transition.Root>
);
};
)
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -1,17 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
)
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
reportWebVitals()

View file

@ -1,3 +1,3 @@
import { CharValue } from "./statuses";
import { CharValue } from './statuses'
export type KeyValue = CharValue | "ENTER" | "DELETE";
export type KeyValue = CharValue | 'ENTER' | 'DELETE'

View file

@ -1,16 +1,16 @@
const gameStateKey = "gameState";
const gameStateKey = 'gameState'
type StoredGameState = {
guesses: string[];
solution: string;
};
guesses: string[]
solution: string
}
export const saveGameStateToLocalStorage = (gameState: StoredGameState) => {
localStorage.setItem(gameStateKey, JSON.stringify(gameState));
};
localStorage.setItem(gameStateKey, JSON.stringify(gameState))
}
export const loadGameStateFromLocalStorage = () => {
const state = localStorage.getItem(gameStateKey);
const state = localStorage.getItem(gameStateKey)
return state ? (JSON.parse(state) as StoredGameState) : null;
};
return state ? (JSON.parse(state) as StoredGameState) : null
}

View file

@ -1,34 +1,34 @@
import { getGuessStatuses } from "./statuses";
import { solutionIndex } from "./words";
import { getGuessStatuses } from './statuses'
import { solutionIndex } from './words'
export const shareStatus = (guesses: string[]) => {
navigator.clipboard.writeText(
"Wordle " +
'Wordle ' +
solutionIndex +
" " +
' ' +
guesses.length +
"/6\n\n" +
'/6\n\n' +
generateEmojiGrid(guesses)
);
};
)
}
export const generateEmojiGrid = (guesses: string[]) => {
return guesses
.map((guess) => {
const status = getGuessStatuses(guess);
const status = getGuessStatuses(guess)
return guess
.split("")
.split('')
.map((letter, i) => {
switch (status[i]) {
case "correct":
return "🟩";
case "present":
return "🟨";
case 'correct':
return '🟩'
case 'present':
return '🟨'
default:
return "⬜";
return '⬜'
}
})
.join("");
.join('')
})
.join("\n");
};
.join('\n')
}

View file

@ -1,102 +1,102 @@
import { solution } from "./words";
import { solution } from './words'
export type CharStatus = "absent" | "present" | "correct";
export type CharStatus = 'absent' | 'present' | 'correct'
export type CharValue =
| "Q"
| "W"
| "E"
| "R"
| "T"
| "Y"
| "U"
| "I"
| "O"
| "P"
| "A"
| "S"
| "D"
| "F"
| "G"
| "H"
| "J"
| "K"
| "L"
| "Z"
| "X"
| "C"
| "V"
| "B"
| "N"
| "M";
| 'Q'
| 'W'
| 'E'
| 'R'
| 'T'
| 'Y'
| 'U'
| 'I'
| 'O'
| 'P'
| 'A'
| 'S'
| 'D'
| 'F'
| 'G'
| 'H'
| 'J'
| 'K'
| 'L'
| 'Z'
| 'X'
| 'C'
| 'V'
| 'B'
| 'N'
| 'M'
export const getStatuses = (
guesses: string[]
): { [key: string]: CharStatus } => {
const charObj: { [key: string]: CharStatus } = {};
const charObj: { [key: string]: CharStatus } = {}
guesses.forEach((word) => {
word.split("").forEach((letter, i) => {
word.split('').forEach((letter, i) => {
if (!solution.includes(letter)) {
// make status absent
return (charObj[letter] = "absent");
return (charObj[letter] = 'absent')
}
if (letter === solution[i]) {
//make status correct
return (charObj[letter] = "correct");
return (charObj[letter] = 'correct')
}
if (charObj[letter] !== "correct") {
if (charObj[letter] !== 'correct') {
//make status present
return (charObj[letter] = "present");
return (charObj[letter] = 'present')
}
});
});
})
})
return charObj;
};
return charObj
}
export const getGuessStatuses = (guess: string): CharStatus[] => {
const splitSolution = solution.split("");
const splitGuess = guess.split("");
const splitSolution = solution.split('')
const splitGuess = guess.split('')
const solutionCharsTaken = splitSolution.map((_) => false);
const solutionCharsTaken = splitSolution.map((_) => false)
const statuses: CharStatus[] = Array.from(Array(guess.length));
const statuses: CharStatus[] = Array.from(Array(guess.length))
// handle all correct cases first
splitGuess.forEach((letter, i) => {
if (letter === splitSolution[i]) {
statuses[i] = "correct";
solutionCharsTaken[i] = true;
return;
statuses[i] = 'correct'
solutionCharsTaken[i] = true
return
}
});
})
splitGuess.forEach((letter, i) => {
if (statuses[i]) return;
if (statuses[i]) return
if (!splitSolution.includes(letter)) {
// handles the absent case
statuses[i] = "absent";
return;
statuses[i] = 'absent'
return
}
// now we are left with "present"s
const indexOfPresentChar = splitSolution.findIndex(
(x, index) => x === letter && !solutionCharsTaken[index]
);
)
if (indexOfPresentChar > -1) {
statuses[i] = "present";
solutionCharsTaken[indexOfPresentChar] = true;
return;
statuses[i] = 'present'
solutionCharsTaken[indexOfPresentChar] = true
return
} else {
statuses[i] = "absent";
return;
statuses[i] = 'absent'
return
}
});
})
return statuses;
};
return statuses
}

View file

@ -1,28 +1,28 @@
import { WORDS } from "../constants/wordlist";
import { VALIDGUESSES } from "../constants/validGuesses";
import { WORDS } from '../constants/wordlist'
import { VALIDGUESSES } from '../constants/validGuesses'
export const isWordInWordList = (word: string) => {
return (
WORDS.includes(word.toLowerCase()) ||
VALIDGUESSES.includes(word.toLowerCase())
);
};
)
}
export const isWinningWord = (word: string) => {
return solution === word;
};
return solution === word
}
export const getWordOfDay = () => {
// January 1, 2022 Game Epoch
const epochMs = 1641013200000;
const now = Date.now();
const msInDay = 86400000;
const index = Math.floor((now - epochMs) / msInDay);
const epochMs = 1641013200000
const now = Date.now()
const msInDay = 86400000
const index = Math.floor((now - epochMs) / msInDay)
return {
solution: WORDS[index].toUpperCase(),
solutionIndex: index,
};
};
}
}
export const { solution, solutionIndex } = getWordOfDay();
export const { solution, solutionIndex } = getWordOfDay()

View file

@ -1,15 +1,15 @@
import { ReportHandler } from 'web-vitals';
import { ReportHandler } from 'web-vitals'
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(onPerfEntry)
getTTFB(onPerfEntry)
})
}
}
};
export default reportWebVitals;
export default reportWebVitals

View file

@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import '@testing-library/jest-dom'

View file

@ -1,7 +1,7 @@
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {},
},
plugins: [],
};
}

View file

@ -1,11 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
@ -20,7 +16,5 @@
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
"include": ["src"]
}