Merge branch 'main' into main
This commit is contained in:
commit
d8dfee1e80
12 changed files with 290 additions and 3 deletions
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
FROM node
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD npm run start
|
|
@ -28,5 +28,12 @@ $ npm install
|
||||||
$ npm run start
|
$ 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
|
### 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/)
|
Please feel free to contact me on [linkedin](https://www.linkedin.com/in/hannahpark1000/) and learn more about me [here](https://www.hannahmariepark.com/)
|
||||||
|
|
20
src/App.tsx
20
src/App.tsx
|
@ -1,4 +1,5 @@
|
||||||
import { InformationCircleIcon } from '@heroicons/react/outline'
|
import { InformationCircleIcon } from '@heroicons/react/outline'
|
||||||
|
import { ChartBarIcon } from '@heroicons/react/outline'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Alert } from './components/alerts/Alert'
|
import { Alert } from './components/alerts/Alert'
|
||||||
import { Grid } from './components/grid/Grid'
|
import { Grid } from './components/grid/Grid'
|
||||||
|
@ -6,7 +7,9 @@ import { Keyboard } from './components/keyboard/Keyboard'
|
||||||
import { AboutModal } from './components/modals/AboutModal'
|
import { AboutModal } from './components/modals/AboutModal'
|
||||||
import { InfoModal } from './components/modals/InfoModal'
|
import { InfoModal } from './components/modals/InfoModal'
|
||||||
import { WinModal } from './components/modals/WinModal'
|
import { WinModal } from './components/modals/WinModal'
|
||||||
|
import { StatsModal } from './components/modals/StatsModal'
|
||||||
import { isWordInWordList, isWinningWord, solution } from './lib/words'
|
import { isWordInWordList, isWinningWord, solution } from './lib/words'
|
||||||
|
import { addEvent, loadStats } from './lib/stats'
|
||||||
import {
|
import {
|
||||||
loadGameStateFromLocalStorage,
|
loadGameStateFromLocalStorage,
|
||||||
saveGameStateToLocalStorage,
|
saveGameStateToLocalStorage,
|
||||||
|
@ -19,6 +22,7 @@ function App() {
|
||||||
const [isInfoModalOpen, setIsInfoModalOpen] = useState(false)
|
const [isInfoModalOpen, setIsInfoModalOpen] = useState(false)
|
||||||
const [isAboutModalOpen, setIsAboutModalOpen] = useState(false)
|
const [isAboutModalOpen, setIsAboutModalOpen] = useState(false)
|
||||||
const [isNotEnoughLetters, setIsNotEnoughLetters] = useState(false)
|
const [isNotEnoughLetters, setIsNotEnoughLetters] = useState(false)
|
||||||
|
const [isStatsModalOpen, setIsStatsModalOpen] = useState(false)
|
||||||
const [isWordNotFoundAlertOpen, setIsWordNotFoundAlertOpen] = useState(false)
|
const [isWordNotFoundAlertOpen, setIsWordNotFoundAlertOpen] = useState(false)
|
||||||
const [isGameLost, setIsGameLost] = useState(false)
|
const [isGameLost, setIsGameLost] = useState(false)
|
||||||
const [shareComplete, setShareComplete] = useState(false)
|
const [shareComplete, setShareComplete] = useState(false)
|
||||||
|
@ -33,6 +37,11 @@ function App() {
|
||||||
return loaded.guesses
|
return loaded.guesses
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const [stats, setStats] = useState<number[]>(() => {
|
||||||
|
const loaded = loadStats()
|
||||||
|
return loaded
|
||||||
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
saveGameStateToLocalStorage({ guesses, solution })
|
saveGameStateToLocalStorage({ guesses, solution })
|
||||||
}, [guesses])
|
}, [guesses])
|
||||||
|
@ -75,10 +84,12 @@ function App() {
|
||||||
setCurrentGuess('')
|
setCurrentGuess('')
|
||||||
|
|
||||||
if (winningWord) {
|
if (winningWord) {
|
||||||
|
setStats(addEvent(stats, guesses.length))
|
||||||
return setIsGameWon(true)
|
return setIsGameWon(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (guesses.length === 5) {
|
if (guesses.length === 5) {
|
||||||
|
setStats(addEvent(stats, guesses.length + 1))
|
||||||
setIsGameLost(true)
|
setIsGameLost(true)
|
||||||
return setTimeout(() => {
|
return setTimeout(() => {
|
||||||
setIsGameLost(false)
|
setIsGameLost(false)
|
||||||
|
@ -106,6 +117,10 @@ function App() {
|
||||||
className="h-6 w-6 cursor-pointer"
|
className="h-6 w-6 cursor-pointer"
|
||||||
onClick={() => setIsInfoModalOpen(true)}
|
onClick={() => setIsInfoModalOpen(true)}
|
||||||
/>
|
/>
|
||||||
|
<ChartBarIcon
|
||||||
|
className="h-6 w-6 cursor-pointer"
|
||||||
|
onClick={() => setIsStatsModalOpen(true)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Grid guesses={guesses} currentGuess={currentGuess} />
|
<Grid guesses={guesses} currentGuess={currentGuess} />
|
||||||
<Keyboard
|
<Keyboard
|
||||||
|
@ -130,6 +145,11 @@ function App() {
|
||||||
isOpen={isInfoModalOpen}
|
isOpen={isInfoModalOpen}
|
||||||
handleClose={() => setIsInfoModalOpen(false)}
|
handleClose={() => setIsInfoModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
<StatsModal
|
||||||
|
isOpen={isStatsModalOpen}
|
||||||
|
handleClose={() => setIsStatsModalOpen(false)}
|
||||||
|
stats={stats}
|
||||||
|
/>
|
||||||
<AboutModal
|
<AboutModal
|
||||||
isOpen={isAboutModalOpen}
|
isOpen={isAboutModalOpen}
|
||||||
handleClose={() => setIsAboutModalOpen(false)}
|
handleClose={() => setIsAboutModalOpen(false)}
|
||||||
|
|
19
src/components/histogram/histogram.tsx
Normal file
19
src/components/histogram/histogram.tsx
Normal file
|
@ -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(
|
||||||
|
<div className="columns-1 justify-left m-2 text-sm">
|
||||||
|
{ data.map(( value, i ) => (
|
||||||
|
<Progress key={i} index={i} size={min+100*value/max}
|
||||||
|
label={String(value)} />
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
23
src/components/histogram/progress.tsx
Normal file
23
src/components/histogram/progress.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
index: number,
|
||||||
|
size: number,
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Progress = ( {index, size, label}: Props ) => {
|
||||||
|
return(
|
||||||
|
<div className="flex justify-left m-1">
|
||||||
|
<div className="items-center justify-center w-10%">{index+1}</div>
|
||||||
|
<div className="bg-gray-200 rounded-full w-full ml-2">
|
||||||
|
<div
|
||||||
|
style={{ width: `${size}%`}}
|
||||||
|
className="bg-blue-600 text-xs font-medium text-blue-100 text-center p-0.5
|
||||||
|
rounded-l-full">{label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
import { XCircleIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
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"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<XCircleIcon
|
||||||
|
className="h-6 w-6 cursor-pointer"
|
||||||
|
onClick={() => handleClose()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { Dialog, Transition } from '@headlessui/react'
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { Cell } from '../grid/Cell'
|
import { Cell } from '../grid/Cell'
|
||||||
|
import { XCircleIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
@ -45,6 +46,12 @@ export const InfoModal = ({ isOpen, handleClose }: Props) => {
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<XCircleIcon
|
||||||
|
className="h-6 w-6 cursor-pointer"
|
||||||
|
onClick={() => handleClose()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<Dialog.Title
|
<Dialog.Title
|
||||||
|
|
88
src/components/modals/StatsModal.tsx
Normal file
88
src/components/modals/StatsModal.tsx
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
import { XCircleIcon } from '@heroicons/react/outline'
|
||||||
|
import { trys, successRate, currentStreak, bestStreak } from '../../lib/stats'
|
||||||
|
import { Histogram } from '../histogram/histogram'
|
||||||
|
import { StatLine } from '../statline/statline'
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean
|
||||||
|
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 (
|
||||||
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
<Dialog
|
||||||
|
as="div"
|
||||||
|
className="fixed z-10 inset-0 overflow-y-auto"
|
||||||
|
onClose={handleClose}
|
||||||
|
>
|
||||||
|
<div className="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<Dialog.Overlay className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
{/* This element is to trick the browser into centering the modal contents. */}
|
||||||
|
<span
|
||||||
|
className="hidden sm:inline-block sm:align-middle sm:h-screen"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
​
|
||||||
|
</span>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<div className="inline-block align-bottom bg-white rounded-lg px-4
|
||||||
|
pt-5 pb-4 text-left overflow-hidden shadow-xl transform
|
||||||
|
transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<XCircleIcon
|
||||||
|
className="h-6 w-6 cursor-pointer"
|
||||||
|
onClick={() => handleClose()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-center">
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg leading-6 font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
Statistics
|
||||||
|
</Dialog.Title>
|
||||||
|
<StatLine labels={labels} values={values} />
|
||||||
|
<Dialog.Title
|
||||||
|
as="h3"
|
||||||
|
className="text-lg leading-6 font-medium text-gray-900"
|
||||||
|
>
|
||||||
|
Guess Distribution
|
||||||
|
</Dialog.Title>
|
||||||
|
<Histogram data={stats.slice(0,6)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import { Dialog, Transition } from '@headlessui/react'
|
||||||
import { CheckIcon } from '@heroicons/react/outline'
|
import { CheckIcon } from '@heroicons/react/outline'
|
||||||
import { MiniGrid } from '../mini-grid/MiniGrid'
|
import { MiniGrid } from '../mini-grid/MiniGrid'
|
||||||
import { shareStatus } from '../../lib/share'
|
import { shareStatus } from '../../lib/share'
|
||||||
|
import { XCircleIcon } from '@heroicons/react/outline'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
|
@ -54,6 +55,12 @@ export const WinModal = ({
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
<div className="inline-block align-bottom bg-white rounded-lg px-4 pt-5 pb-4 text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-sm sm:w-full sm:p-6">
|
||||||
|
<div className="absolute right-4 top-4">
|
||||||
|
<XCircleIcon
|
||||||
|
className="h-6 w-6 cursor-pointer"
|
||||||
|
onClick={() => handleClose()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
<div className="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-green-100">
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
|
|
19
src/components/statline/statline.tsx
Normal file
19
src/components/statline/statline.tsx
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
labels: string[]
|
||||||
|
values: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StatLine = ({labels, values}: Props) => {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center m-1">
|
||||||
|
{values.map((value,i ) => (
|
||||||
|
<div key={i} className="items-center justify-center m-1 w-1/4">
|
||||||
|
<div className="text-3xl font-bold">{value} </div>
|
||||||
|
<div className="text-sm">{labels[i]}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -11,6 +11,22 @@ export const saveGameStateToLocalStorage = (gameState: StoredGameState) => {
|
||||||
|
|
||||||
export const loadGameStateFromLocalStorage = () => {
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
67
src/lib/stats.ts
Normal file
67
src/lib/stats.ts
Normal file
|
@ -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)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue