diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4a2e42a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,7 @@ +FROM node + +COPY . . +RUN npm install + +EXPOSE 3000 +CMD npm run start diff --git a/README.md b/README.md index 1225d9b..ce6c142 100644 --- a/README.md +++ b/README.md @@ -28,5 +28,12 @@ $ npm install $ npm run start ``` +_To build/run docker container:_ +```bash +$ docker build -t notwordle . +$ docker run -d -p 3000:3000 notwordle +``` +open http://localhost:3000 in browser. + ### I'm looking for a junior developer role Please feel free to contact me on [linkedin](https://www.linkedin.com/in/hannahpark1000/) and learn more about me [here](https://www.hannahmariepark.com/) diff --git a/src/App.tsx b/src/App.tsx index d970edf..832d089 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,5 @@ import { InformationCircleIcon } from '@heroicons/react/outline' +import { ChartBarIcon } from '@heroicons/react/outline' import { useState, useEffect } from 'react' import { Alert } from './components/alerts/Alert' import { Grid } from './components/grid/Grid' @@ -6,7 +7,9 @@ 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 { isWordInWordList, isWinningWord, solution } from './lib/words' +import { addEvent, loadStats } from './lib/stats' import { loadGameStateFromLocalStorage, saveGameStateToLocalStorage, @@ -19,6 +22,7 @@ function App() { 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) @@ -33,10 +37,15 @@ function App() { return loaded.guesses }) + const [stats, setStats] = useState(() => { + const loaded = loadStats() + return loaded + }) + useEffect(() => { saveGameStateToLocalStorage({ guesses, solution }) }, [guesses]) - + useEffect(() => { if (isGameWon) { setIsWinModalOpen(true) @@ -75,10 +84,12 @@ function App() { setCurrentGuess('') if (winningWord) { + setStats(addEvent(stats, guesses.length)) return setIsGameWon(true) } if (guesses.length === 5) { + setStats(addEvent(stats, guesses.length + 1)) setIsGameLost(true) return setTimeout(() => { setIsGameLost(false) @@ -106,6 +117,10 @@ function App() { className="h-6 w-6 cursor-pointer" onClick={() => setIsInfoModalOpen(true)} /> + setIsStatsModalOpen(true)} + /> setIsInfoModalOpen(false)} /> + setIsStatsModalOpen(false)} + stats={stats} + /> setIsAboutModalOpen(false)} diff --git a/src/components/histogram/histogram.tsx b/src/components/histogram/histogram.tsx new file mode 100644 index 0000000..edab521 --- /dev/null +++ b/src/components/histogram/histogram.tsx @@ -0,0 +1,19 @@ + import {Progress} from './progress' + +type Props = { + data: number[] +} + +export const Histogram = ({ data }: Props) => { + const min = 10 + const max = Math.ceil(Math.max.apply(null, data)*1.2) + return( +
+ { data.map(( value, i ) => ( + + )) + } +
+ ) +} diff --git a/src/components/histogram/progress.tsx b/src/components/histogram/progress.tsx new file mode 100644 index 0000000..53e3e3b --- /dev/null +++ b/src/components/histogram/progress.tsx @@ -0,0 +1,23 @@ + +type Props = { + index: number, + size: number, + label: string +} + +export const Progress = ( {index, size, label}: Props ) => { + return( +
+
{index+1}
+
+
{label} +
+
+
+ ) +} + + diff --git a/src/components/modals/AboutModal.tsx b/src/components/modals/AboutModal.tsx index 2ed1dbc..025c09e 100644 --- a/src/components/modals/AboutModal.tsx +++ b/src/components/modals/AboutModal.tsx @@ -1,5 +1,6 @@ import { Fragment } from 'react' import { Dialog, Transition } from '@headlessui/react' +import { XCircleIcon } from '@heroicons/react/outline' type Props = { isOpen: boolean @@ -44,6 +45,12 @@ export const AboutModal = ({ isOpen, handleClose }: Props) => { leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" >
+
+ handleClose()} + /> +
{ leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" >
+
+ handleClose()} + /> +
void + stats: number[] +} + +export const StatsModal = ({ isOpen, handleClose, stats }: Props) => { + const labels = ["Total trys", "Success rate", + "Current streak", "Best streak"] + const values = [String(trys(stats)), String(successRate(stats))+'%', + String(currentStreak(stats)), String(bestStreak(stats))] + return ( + + +
+ + + + + {/* This element is to trick the browser into centering the modal contents. */} + + +
+
+ handleClose()} + /> +
+
+
+ + Statistics + + + + Guess Distribution + + +
+
+
+
+
+
+
+ ) +} diff --git a/src/components/modals/WinModal.tsx b/src/components/modals/WinModal.tsx index ab36b72..faf8254 100644 --- a/src/components/modals/WinModal.tsx +++ b/src/components/modals/WinModal.tsx @@ -3,6 +3,7 @@ import { Dialog, Transition } from '@headlessui/react' import { CheckIcon } from '@heroicons/react/outline' import { MiniGrid } from '../mini-grid/MiniGrid' import { shareStatus } from '../../lib/share' +import { XCircleIcon } from '@heroicons/react/outline' type Props = { isOpen: boolean @@ -36,7 +37,7 @@ export const WinModal = ({ > - + {/* This element is to trick the browser into centering the modal contents. */}
+
+ handleClose()} + /> +
{ + return ( +
+ {values.map((value,i ) => ( +
+
{value}
+
{labels[i]}
+
+ ))} +
+ ) +} diff --git a/src/lib/localStorage.ts b/src/lib/localStorage.ts index 4f5c825..81fca8e 100644 --- a/src/lib/localStorage.ts +++ b/src/lib/localStorage.ts @@ -11,6 +11,22 @@ export const saveGameStateToLocalStorage = (gameState: StoredGameState) => { export const loadGameStateFromLocalStorage = () => { const state = localStorage.getItem(gameStateKey) - return state ? (JSON.parse(state) as StoredGameState) : null } + +const gameStatKey = 'gameStats' + +type StoredGameStats = { + distribution: number[] + current: number + best: number +} + +export const saveStatsToLocalStorage = ( gameStats: StoredGameStats) => { + localStorage.setItem(gameStatKey, JSON.stringify(gameStats)) +} + +export const loadStatsFromLocalStorage = () => { + const stats = localStorage.getItem(gameStatKey) + return stats ? (JSON.parse(stats) as StoredGameStats) : null +} diff --git a/src/lib/stats.ts b/src/lib/stats.ts new file mode 100644 index 0000000..c0e08f6 --- /dev/null +++ b/src/lib/stats.ts @@ -0,0 +1,67 @@ +/** +** An attempt at a statistics object and its interface +**/ + +import { + loadStatsFromLocalStorage, + saveStatsToLocalStorage +} from './localStorage' + +// In stats array elements 0-5 are successes in 1-6 trys +// stats[6] is the number of failures +// stats[7] is the currentStreak +// stats[8] is the bestStreak + +export const failures = (stats: number[] ) => { return stats[6] } +export const currentStreak = (stats: number[] ) => { return stats[7] } +export const bestStreak = (stats: number[] ) => { return stats[8] } + +export const addEvent = (stats: number[], count: number) => { + // Count is number of incorrect guesses before end. + if(count < 0) { count = 0 } // Should not really need this + if( count > 5 ){ // A fail situation + stats[7] = 0 // End current streak + stats[6] += 1 // Increase number of fails + } else { + stats[count] += 1 // Increase counters + stats[7] += 1 + if( bestStreak(stats) < currentStreak(stats) ){ + stats[8] = currentStreak(stats) + } + } + saveStats(stats) + return stats +} + +export const resetStats = () => { + return [0,0,0,0,0,0,0,0,0] +} + +export const saveStats = (stats: number[]) => { + const distribution = stats.slice(0,7) + const current = currentStreak(stats) + const best = bestStreak(stats) + saveStatsToLocalStorage({ distribution , current, best }) +} + +export const loadStats = () => { + const loaded = loadStatsFromLocalStorage() + var stats = resetStats() + if( loaded ){ + stats = loaded.distribution + stats[7] = loaded.current + stats[8] = loaded.best + } + return ( stats ) +} + +export const trys = (stats: number[] ) => { + return(stats.slice(0,7).reduce((a,b) => a+b , 0 )) +} + +export const successRate = (stats: number[] ) => { + return(Math.round((100*(trys(stats) - failures(stats)))/Math.max(trys(stats),1))) +} + + +