histogram width divide by mode, not total #2

Merged
christofsteel merged 47 commits from honigle into main 2022-02-02 13:59:36 +01:00
34 changed files with 1070 additions and 13262 deletions

16
.github/workflows/lint.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: Lint
on:
pull_request:
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Prettify code
uses: creyD/prettier_action@v4.2
with:
prettier_options: --check src

16
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,16 @@
name: Test
on:
pull_request:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- run: |
npm install
- run: |
npm run test

24
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,24 @@
# This file is a template, and might need editing before it works on your project.
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Getting-Started.gitlab-ci.yml
# This is a sample GitLab CI/CD configuration file that should run without any modifications.
# It demonstrates a basic 3 stage CI/CD pipeline. Instead of real tests or scripts,
# it uses echo commands to simulate the pipeline execution.
#
# A pipeline is composed of independent jobs that run scripts, grouped into stages.
# Stages run in sequential order, but jobs within stages run in parallel.
#
# For more information, see: https://docs.gitlab.com/ee/ci/yaml/index.html#stages
image: docker:latest
stages: # List of stages for jobs, and their order of execution
- build
build-job: # This job runs in the build stage, which runs first.
stage: build
script:
- echo "Compiling the code..."
- docker build -t honigle .
- echo "Compile complete."

1
.husky/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
_

4
.husky/pre-commit Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged

View file

@ -1,37 +1,35 @@
# Wordle Clone Word Guessing Game
- Go play the real Wordle [here](https://www.powerlanguage.co.uk/wordle/) This is a clone project of a popular word guessing game made using React, Typescript, and Tailwind.
- Read the story behind it [here](https://www.nytimes.com/2022/01/03/technology/wordle-word-game-creator.html)
- Try a demo of this clone project [here](https://wordle.hannahmariepark.com)
_Inspiration:_
This game is an open source clone of the immensely popular online word guessing game Wordle. Like many others all over the world, I saw the signature pattern of green, yellow, and white squares popping up all over social media and the web and had to check it out. After a few days of play, I decided it would be great for my learning to try to rebuild Wordle in React!
_Design Decisions:_
I used a combination of React, Typescript, and Tailwind to build this Wordle Clone. When examining the original Wordle, I assumed the list might come from an external API or database, but after investigating in chrome dev tools I found that the list of words is simply stored in an array on the front end. I'm using the same list as the OG Wordle uses, but watch out for spoilers if you go find the file in this repo! The word match functionality is simple: the word array index increments each day from a fixed game epoch timestamp (only one puzzle per day!) roughly like so:
```
WORDS[Math.floor((NOW_IN_MS - GAME_EPOCH_IN_MS) / ONE_DAY_IN_MS)]
```
React enabled me to componentize the littlest parts of the game - keys and letter cells - and use them as the building blocks for the keyboard, word grid, and winning solution graphic. As for handling state, I used the built in useState and useEffect hooks to track guesses, whether the game is won, and to conditionally render popups.
In addition to other things, Typescript helped ensure type safety for the statuses of each guessed letter, which were used in many areas of the app and needed to be accurate for the game to work correctly.
I implemented Tailwind mostly because I wanted to learn how to use Tailwind CSS, but I also took advantage of [Tailwind UI](https://tailwindui.com/) with their [headless package](https://headlessui.dev/) to build the modals and notifications. This was such an easy way to build simple popups for how to play, winning the game, and invalid words.
_To Run Locally:_ _To Run Locally:_
Clone the repository and perform the following command line actions: Clone the repository and perform the following command line actions:
```bash ```bash
$ cd wordle $ cd word-guessing-game
$ npm install $ npm install
$ npm run start $ npm run start
``` ```
_To build/run docker container:_ _To build/run docker container:_
```bash ```bash
$ docker build -t notwordle . $ docker build -t game .
$ docker run -d -p 3000:3000 notwordle $ docker run -d -p 3000:3000 game
``` ```
open http://localhost:3000 in browser. open http://localhost:3000 in browser.
_To create a version in a different language:_
- Update the title, the description, and the "You need to enable JavaScript" message in `public/index.html`
- Update the language attribute in the HTML tag in `public/index.html`
- Update the name and short name in `public/manifest.json`
- Update the strings in `src/constants/strings.ts`
- Add all of the five letter words in the language to `src/constants/validGuesses.ts`, replacing the English words
- Add a list of goal words in the language to `src/constants/wordlist.ts`, replacing the English words
- Update the "About" modal in `src/components/modals/AboutModel.tsx`
- Update the "Info" modal in `src/components/modals/InfoModal.tsx`
- If the language has letters that are not present in English, add them to the `CharValue` type in `src/lib/statuses.ts` and update the keyboard in `src/lib/components/keyboard/Keyboard.tsx`
- If the language's letters are made of multiple unicode characters, use a grapheme splitter at various points throughout the app or normalize the input so that all of the letters are made of a single character
- If the language is written right-to-left, add `dir="rtl"` to the HTML tag in `public/index.html` and prepend `\u202E` (the unicode right-to-left override character) to the return statement of the inner function in `generateEmojiGrid` in `src/lib/share.ts`

706
package-lock.json generated
View file

@ -1,11 +1,11 @@
{ {
"name": "wordle", "name": "game",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wordle", "name": "game",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@headlessui/react": "^1.4.2", "@headlessui/react": "^1.4.2",
@ -19,6 +19,7 @@
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-countdown": "^2.3.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"typescript": "^4.5.4", "typescript": "^4.5.4",
@ -26,8 +27,10 @@
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"husky": "^7.0.4",
"lint-staged": "^12.3.2",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"prettier": "^2.5.1", "prettier": "2.5.1",
"tailwindcss": "^3.0.12" "tailwindcss": "^3.0.12"
} }
}, },
@ -4263,6 +4266,15 @@
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
"integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0="
}, },
"node_modules/astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/async": { "node_modules/async": {
"version": "2.6.3", "version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
@ -5028,6 +5040,78 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dev": true,
"dependencies": {
"restore-cursor": "^3.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/cli-truncate": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
"integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
"dev": true,
"dependencies": {
"slice-ansi": "^5.0.0",
"string-width": "^5.0.0"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"node_modules/cli-truncate/node_modules/string-width": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.0.tgz",
"integrity": "sha512-7x54QnN21P+XL/v8SuNKvfgsUre6PXpN7mc77N3HlZv+f1SBRGmjxtOud2Z6FZ8DmdkD/IdjCaf9XXbnqmTZGQ==",
"dev": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-truncate/node_modules/strip-ansi": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"dev": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/cliui": { "node_modules/cliui": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@ -6116,6 +6200,12 @@
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
}, },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -8078,6 +8168,21 @@
"node": ">=10.17.0" "node": ">=10.17.0"
} }
}, },
"node_modules/husky": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz",
"integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==",
"dev": true,
"bin": {
"husky": "lib/bin.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -10582,6 +10687,138 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
}, },
"node_modules/lint-staged": {
"version": "12.3.2",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.3.2.tgz",
"integrity": "sha512-gtw4Cbj01SuVSfAOXC6ivd/7VKHTj51yj5xV8TgktFmYNMsZzXuSd5/brqJEA93v63wL7R6iDlunMANOechC0A==",
"dev": true,
"dependencies": {
"cli-truncate": "^3.1.0",
"colorette": "^2.0.16",
"commander": "^8.3.0",
"debug": "^4.3.3",
"execa": "^5.1.1",
"lilconfig": "2.0.4",
"listr2": "^4.0.1",
"micromatch": "^4.0.4",
"normalize-path": "^3.0.0",
"object-inspect": "^1.12.0",
"string-argv": "^0.3.1",
"supports-color": "^9.2.1",
"yaml": "^1.10.2"
},
"bin": {
"lint-staged": "bin/lint-staged.js"
},
"engines": {
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/lint-staged"
}
},
"node_modules/lint-staged/node_modules/supports-color": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.1.tgz",
"integrity": "sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/listr2": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.1.tgz",
"integrity": "sha512-D65Nl+zyYHL2jQBGmxtH/pU8koPZo5C8iCNE8EoB04RwPgQG1wuaKwVbeZv9LJpiH4Nxs0FCp+nNcG8OqpniiA==",
"dev": true,
"dependencies": {
"cli-truncate": "^2.1.0",
"colorette": "^2.0.16",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
"rfdc": "^1.3.0",
"rxjs": "^7.5.2",
"through": "^2.3.8",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
},
"peerDependencies": {
"enquirer": ">= 2.3.0 < 3"
},
"peerDependenciesMeta": {
"enquirer": {
"optional": true
}
}
},
"node_modules/listr2/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/listr2/node_modules/cli-truncate": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
"dependencies": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/listr2/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/listr2/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/listr2/node_modules/slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/loader-runner": { "node_modules/loader-runner": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz",
@ -10647,6 +10884,88 @@
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
}, },
"node_modules/log-update": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"dev": true,
"dependencies": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
"slice-ansi": "^4.0.0",
"wrap-ansi": "^6.2.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/log-update/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/log-update/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"node_modules/log-update/node_modules/slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/log-update/node_modules/wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/loose-envify": { "node_modules/loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -12901,6 +13220,18 @@
"node": ">=14" "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": { "node_modules/react-dev-utils": {
"version": "12.0.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
@ -13408,6 +13739,19 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dev": true,
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/retry": { "node_modules/retry": {
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@ -13425,6 +13769,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/rfdc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
"dev": true
},
"node_modules/rimraf": { "node_modules/rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -13529,6 +13879,15 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/rxjs": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.2.tgz",
"integrity": "sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w==",
"dev": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -13823,6 +14182,46 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/slice-ansi": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
"dev": true,
"dependencies": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/ansi-styles": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz",
"integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/slice-ansi/node_modules/is-fullwidth-code-point": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/sockjs": { "node_modules/sockjs": {
"version": "0.3.24", "version": "0.3.24",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
@ -14007,6 +14406,15 @@
} }
] ]
}, },
"node_modules/string-argv": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
"integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
"dev": true,
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/string-length": { "node_modules/string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -14589,6 +14997,12 @@
"resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz",
"integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w=="
}, },
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
"node_modules/thunky": { "node_modules/thunky": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
@ -18902,6 +19316,12 @@
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz",
"integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=" "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0="
}, },
"astral-regex": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
"integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
"dev": true
},
"async": { "async": {
"version": "2.6.3", "version": "2.6.3",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz",
@ -19486,6 +19906,53 @@
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
"integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==" "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="
}, },
"cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"dev": true,
"requires": {
"restore-cursor": "^3.1.0"
}
},
"cli-truncate": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-3.1.0.tgz",
"integrity": "sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA==",
"dev": true,
"requires": {
"slice-ansi": "^5.0.0",
"string-width": "^5.0.0"
},
"dependencies": {
"ansi-regex": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
"integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
"dev": true
},
"string-width": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.0.tgz",
"integrity": "sha512-7x54QnN21P+XL/v8SuNKvfgsUre6PXpN7mc77N3HlZv+f1SBRGmjxtOud2Z6FZ8DmdkD/IdjCaf9XXbnqmTZGQ==",
"dev": true,
"requires": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
"strip-ansi": "^7.0.1"
}
},
"strip-ansi": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz",
"integrity": "sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw==",
"dev": true,
"requires": {
"ansi-regex": "^6.0.1"
}
}
}
},
"cliui": { "cliui": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@ -20290,6 +20757,12 @@
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
}, },
"eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"ee-first": { "ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -21710,6 +22183,12 @@
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
"integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="
}, },
"husky": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/husky/-/husky-7.0.4.tgz",
"integrity": "sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==",
"dev": true
},
"iconv-lite": { "iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@ -23498,6 +23977,98 @@
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
}, },
"lint-staged": {
"version": "12.3.2",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-12.3.2.tgz",
"integrity": "sha512-gtw4Cbj01SuVSfAOXC6ivd/7VKHTj51yj5xV8TgktFmYNMsZzXuSd5/brqJEA93v63wL7R6iDlunMANOechC0A==",
"dev": true,
"requires": {
"cli-truncate": "^3.1.0",
"colorette": "^2.0.16",
"commander": "^8.3.0",
"debug": "^4.3.3",
"execa": "^5.1.1",
"lilconfig": "2.0.4",
"listr2": "^4.0.1",
"micromatch": "^4.0.4",
"normalize-path": "^3.0.0",
"object-inspect": "^1.12.0",
"string-argv": "^0.3.1",
"supports-color": "^9.2.1",
"yaml": "^1.10.2"
},
"dependencies": {
"supports-color": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.2.1.tgz",
"integrity": "sha512-Obv7ycoCTG51N7y175StI9BlAXrmgZrFhZOb0/PyjHBher/NmsdBgbbQ1Inhq+gIhz6+7Gb+jWF2Vqi7Mf1xnQ==",
"dev": true
}
}
},
"listr2": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/listr2/-/listr2-4.0.1.tgz",
"integrity": "sha512-D65Nl+zyYHL2jQBGmxtH/pU8koPZo5C8iCNE8EoB04RwPgQG1wuaKwVbeZv9LJpiH4Nxs0FCp+nNcG8OqpniiA==",
"dev": true,
"requires": {
"cli-truncate": "^2.1.0",
"colorette": "^2.0.16",
"log-update": "^4.0.0",
"p-map": "^4.0.0",
"rfdc": "^1.3.0",
"rxjs": "^7.5.2",
"through": "^2.3.8",
"wrap-ansi": "^7.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"cli-truncate": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-2.1.0.tgz",
"integrity": "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg==",
"dev": true,
"requires": {
"slice-ansi": "^3.0.0",
"string-width": "^4.2.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",
"integrity": "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
}
}
}
},
"loader-runner": { "loader-runner": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz",
@ -23551,6 +24122,66 @@
"resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz",
"integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=" "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M="
}, },
"log-update": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz",
"integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==",
"dev": true,
"requires": {
"ansi-escapes": "^4.3.0",
"cli-cursor": "^3.1.0",
"slice-ansi": "^4.0.0",
"wrap-ansi": "^6.2.0"
},
"dependencies": {
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"slice-ansi": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
"integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"astral-regex": "^2.0.0",
"is-fullwidth-code-point": "^3.0.0"
}
},
"wrap-ansi": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
"dev": true,
"requires": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
}
}
}
},
"loose-envify": { "loose-envify": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
@ -25058,6 +25689,14 @@
"whatwg-fetch": "^3.6.2" "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": { "react-dev-utils": {
"version": "12.0.0", "version": "12.0.0",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz",
@ -25430,6 +26069,16 @@
"resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz",
"integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==" "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ=="
}, },
"restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"dev": true,
"requires": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
}
},
"retry": { "retry": {
"version": "0.13.1", "version": "0.13.1",
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
@ -25440,6 +26089,12 @@
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==" "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
}, },
"rfdc": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz",
"integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==",
"dev": true
},
"rimraf": { "rimraf": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@ -25508,6 +26163,15 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"rxjs": {
"version": "7.5.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.2.tgz",
"integrity": "sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w==",
"dev": true,
"requires": {
"tslib": "^2.1.0"
}
},
"safe-buffer": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -25744,6 +26408,30 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==" "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="
}, },
"slice-ansi": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
"integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
"dev": true,
"requires": {
"ansi-styles": "^6.0.0",
"is-fullwidth-code-point": "^4.0.0"
},
"dependencies": {
"ansi-styles": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.1.0.tgz",
"integrity": "sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ==",
"dev": true
},
"is-fullwidth-code-point": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
"integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
"dev": true
}
}
},
"sockjs": { "sockjs": {
"version": "0.3.24", "version": "0.3.24",
"resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz",
@ -25889,6 +26577,12 @@
} }
} }
}, },
"string-argv": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz",
"integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==",
"dev": true
},
"string-length": { "string-length": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz",
@ -26302,6 +26996,12 @@
"resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz", "resolved": "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz",
"integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==" "integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w=="
}, },
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=",
"dev": true
},
"thunky": { "thunky": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",

View file

@ -14,16 +14,20 @@
"@types/react-dom": "^17.0.11", "@types/react-dom": "^17.0.11",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"react": "^17.0.2", "react": "^17.0.2",
"react-countdown": "^2.3.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-scripts": "5.0.0", "react-scripts": "5.0.0",
"typescript": "^4.5.4", "typescript": "^4.5.4",
"web-vitals": "^2.1.3" "web-vitals": "^2.1.3"
}, },
"scripts": { "scripts": {
"start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"eject": "react-scripts eject",
"fix": "prettier --write src",
"lint": "prettier --check src",
"start": "react-scripts start",
"test": "react-scripts test", "test": "react-scripts test",
"eject": "react-scripts eject" "prepare": "husky install"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -45,8 +49,13 @@
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^10.4.2", "autoprefixer": "^10.4.2",
"husky": "^7.0.4",
"lint-staged": "^12.3.2",
"postcss": "^8.4.5", "postcss": "^8.4.5",
"tailwindcss": "^3.0.12", "prettier": "2.5.1",
"prettier": "^2.5.1" "tailwindcss": "^3.0.12"
},
"lint-staged": {
"src/*.{ts,tsx,js,jsx,css,md}": "prettier --write"
} }
} }

View file

@ -5,10 +5,7 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Honigle" />
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
@ -24,7 +21,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>Wordle Clone</title> <title>Honigle</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

View file

@ -1,6 +1,6 @@
{ {
"short_name": "React App", "short_name": "Game",
"name": "Create React App Sample", "name": "Game",
"icons": [ "icons": [
{ {
"src": "favicon.ico", "src": "favicon.ico",

View file

@ -1,38 +1,3 @@
.App { html.dark {
text-align: center; background-color: rgb(15, 23, 42);
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
} }

View file

@ -1,9 +1,26 @@
import React from 'react' import React from 'react'
import { render, screen } from '@testing-library/react' import { render, screen } from '@testing-library/react'
import App from './App' import App from './App'
import { GAME_TITLE } from './constants/strings'
test('renders learn react link', () => { beforeEach(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
addListener: jest.fn(), // deprecated
removeListener: jest.fn(), // deprecated
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
dispatchEvent: jest.fn(),
})),
})
})
test('renders App component', () => {
render(<App />) render(<App />)
const linkElement = screen.getByText(/learn react/i) const linkElement = screen.getByText(GAME_TITLE)
expect(linkElement).toBeInTheDocument() expect(linkElement).toBeInTheDocument()
}) })

View file

@ -1,13 +1,24 @@
import { InformationCircleIcon } from '@heroicons/react/outline' import {
import { ChartBarIcon } from '@heroicons/react/outline' InformationCircleIcon,
ChartBarIcon,
SunIcon,
} 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'
import { Keyboard } from './components/keyboard/Keyboard' 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 { StatsModal } from './components/modals/StatsModal' import { StatsModal } from './components/modals/StatsModal'
import {
GAME_TITLE,
WIN_MESSAGES,
GAME_COPIED_MESSAGE,
ABOUT_GAME_MESSAGE,
NOT_ENOUGH_LETTERS_MESSAGE,
WORD_NOT_FOUND_MESSAGE,
CORRECT_WORD_MESSAGE,
} from './constants/strings'
import { isWordInWordList, isWinningWord, solution } from './lib/words' import { isWordInWordList, isWinningWord, solution } from './lib/words'
import { addStatsForCompletedGame, loadStats } from './lib/stats' import { addStatsForCompletedGame, loadStats } from './lib/stats'
import { import {
@ -15,17 +26,31 @@ import {
saveGameStateToLocalStorage, saveGameStateToLocalStorage,
} from './lib/localStorage' } from './lib/localStorage'
import './App.css'
const ALERT_TIME_MS = 2000
function App() { function App() {
const prefersDarkMode = window.matchMedia(
'(prefers-color-scheme: dark)'
).matches
const [currentGuess, setCurrentGuess] = useState('') const [currentGuess, setCurrentGuess] = useState('')
const [isGameWon, setIsGameWon] = useState(false) const [isGameWon, setIsGameWon] = useState(false)
const [isWinModalOpen, setIsWinModalOpen] = useState(false)
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 [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 [isDarkMode, setIsDarkMode] = useState(
localStorage.getItem('theme')
? localStorage.getItem('theme') === 'dark'
: prefersDarkMode
? true
: false
)
const [successAlert, setSuccessAlert] = useState('')
const [guesses, setGuesses] = useState<string[]>(() => { const [guesses, setGuesses] = useState<string[]>(() => {
const loaded = loadGameStateFromLocalStorage() const loaded = loadGameStateFromLocalStorage()
if (loaded?.solution !== solution) { if (loaded?.solution !== solution) {
@ -43,15 +68,39 @@ function App() {
const [stats, setStats] = useState(() => loadStats()) const [stats, setStats] = useState(() => loadStats())
useEffect(() => {
if (isDarkMode) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}, [isDarkMode])
const handleDarkMode = (isDark: boolean) => {
setIsDarkMode(isDark)
localStorage.setItem('theme', isDark ? 'dark' : 'light')
}
useEffect(() => { useEffect(() => {
saveGameStateToLocalStorage({ guesses, solution }) saveGameStateToLocalStorage({ guesses, solution })
}, [guesses]) }, [guesses])
useEffect(() => { useEffect(() => {
if (isGameWon) { 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) => { const onChar = (value: string) => {
if (currentGuess.length < 5 && guesses.length < 6 && !isGameWon) { if (currentGuess.length < 5 && guesses.length < 6 && !isGameWon) {
@ -64,18 +113,21 @@ function App() {
} }
const onEnter = () => { const onEnter = () => {
if (!(currentGuess.length === 5) && !isGameLost) { if (isGameWon || isGameLost) {
return
}
if (!(currentGuess.length === 5)) {
setIsNotEnoughLetters(true) setIsNotEnoughLetters(true)
return setTimeout(() => { return setTimeout(() => {
setIsNotEnoughLetters(false) setIsNotEnoughLetters(false)
}, 2000) }, ALERT_TIME_MS)
} }
if (!isWordInWordList(currentGuess)) { if (!isWordInWordList(currentGuess)) {
setIsWordNotFoundAlertOpen(true) setIsWordNotFoundAlertOpen(true)
return setTimeout(() => { return setTimeout(() => {
setIsWordNotFoundAlertOpen(false) setIsWordNotFoundAlertOpen(false)
}, 2000) }, ALERT_TIME_MS)
} }
const winningWord = isWinningWord(currentGuess) const winningWord = isWinningWord(currentGuess)
@ -98,14 +150,18 @@ function App() {
return ( return (
<div className="py-8 max-w-7xl mx-auto sm:px-6 lg:px-8"> <div className="py-8 max-w-7xl mx-auto sm:px-6 lg:px-8">
<div className="flex w-80 mx-auto items-center mb-8"> <div className="flex w-80 mx-auto items-center mb-8 mt-12">
<h1 className="text-xl grow font-bold">Not Wordle</h1> <h1 className="text-xl grow font-bold dark:text-white">{GAME_TITLE}</h1>
<SunIcon
className="h-6 w-6 cursor-pointer dark:stroke-white"
onClick={() => handleDarkMode(!isDarkMode)}
/>
<InformationCircleIcon <InformationCircleIcon
className="h-6 w-6 cursor-pointer" className="h-6 w-6 cursor-pointer dark:stroke-white"
onClick={() => setIsInfoModalOpen(true)} onClick={() => setIsInfoModalOpen(true)}
/> />
<ChartBarIcon <ChartBarIcon
className="h-6 w-6 cursor-pointer" className="h-6 w-6 cursor-pointer dark:stroke-white"
onClick={() => setIsStatsModalOpen(true)} onClick={() => setIsStatsModalOpen(true)}
/> />
</div> </div>
@ -116,18 +172,6 @@ function App() {
onEnter={onEnter} onEnter={onEnter}
guesses={guesses} guesses={guesses}
/> />
<WinModal
isOpen={isWinModalOpen}
handleClose={() => setIsWinModalOpen(false)}
guesses={guesses}
handleShare={() => {
setIsWinModalOpen(false)
setShareComplete(true)
return setTimeout(() => {
setShareComplete(false)
}, 2000)
}}
/>
<InfoModal <InfoModal
isOpen={isInfoModalOpen} isOpen={isInfoModalOpen}
handleClose={() => setIsInfoModalOpen(false)} handleClose={() => setIsInfoModalOpen(false)}
@ -135,7 +179,14 @@ function App() {
<StatsModal <StatsModal
isOpen={isStatsModalOpen} isOpen={isStatsModalOpen}
handleClose={() => setIsStatsModalOpen(false)} handleClose={() => setIsStatsModalOpen(false)}
guesses={guesses}
gameStats={stats} gameStats={stats}
isGameLost={isGameLost}
isGameWon={isGameWon}
handleShare={() => {
setSuccessAlert(GAME_COPIED_MESSAGE)
return setTimeout(() => setSuccessAlert(''), ALERT_TIME_MS)
}}
/> />
<AboutModal <AboutModal
isOpen={isAboutModalOpen} isOpen={isAboutModalOpen}
@ -147,18 +198,18 @@ function App() {
className="mx-auto mt-8 flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 select-none" className="mx-auto mt-8 flex items-center px-2.5 py-1.5 border border-transparent text-xs font-medium rounded text-indigo-700 bg-indigo-100 hover:bg-indigo-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 select-none"
onClick={() => setIsAboutModalOpen(true)} onClick={() => setIsAboutModalOpen(true)}
> >
About this game {ABOUT_GAME_MESSAGE}
</button> </button>
<Alert message="Not enough letters" isOpen={isNotEnoughLetters} /> <Alert message={NOT_ENOUGH_LETTERS_MESSAGE} isOpen={isNotEnoughLetters} />
<Alert message="Word not found" isOpen={isWordNotFoundAlertOpen} />
<Alert <Alert
message={`You lost, the word was ${solution}`} message={WORD_NOT_FOUND_MESSAGE}
isOpen={isGameLost} isOpen={isWordNotFoundAlertOpen}
/> />
<Alert message={CORRECT_WORD_MESSAGE(solution)} isOpen={isGameLost} />
<Alert <Alert
message="Game copied to clipboard" message={successAlert}
isOpen={shareComplete} isOpen={successAlert !== ''}
variant="success" variant="success"
/> />
</div> </div>

View file

@ -10,10 +10,10 @@ type Props = {
export const Alert = ({ isOpen, message, variant = 'warning' }: Props) => { export const Alert = ({ isOpen, message, variant = 'warning' }: Props) => {
const classes = classNames( const classes = classNames(
'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', 'fixed top-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-rose-200': variant === 'warning',
'bg-green-200': variant === 'success', 'bg-blue-200 z-20': variant === 'success',
} }
) )

View file

@ -8,13 +8,16 @@ type Props = {
export const Cell = ({ value, status }: Props) => { export const Cell = ({ value, status }: Props) => {
const classes = classnames( 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 dark:text-white',
{ {
'bg-white border-slate-200': !status, 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-600':
'border-black': value && !status, !status,
'bg-slate-400 text-white border-slate-400': status === 'absent', 'border-black dark:border-slate-100': value && !status,
'bg-green-500 text-white border-green-500': status === 'correct', 'bg-slate-400 dark:bg-slate-700 text-white border-slate-400 dark:border-slate-700':
'bg-yellow-500 text-white border-yellow-500': status === 'present', status === 'absent',
'bg-blue-500 text-white border-blue-500': status === 'correct',
'bg-orange-500 dark:bg-orange-700 text-white border-orange-500 dark:border-orange-700':
status === 'present',
'cell-animation': !!value, 'cell-animation': !!value,
} }
) )

View file

@ -19,13 +19,14 @@ export const Key = ({
onClick, onClick,
}: Props) => { }: Props) => {
const classes = classnames( const classes = classnames(
'flex items-center justify-center rounded mx-0.5 text-xs font-bold cursor-pointer select-none', 'flex items-center justify-center rounded mx-0.5 text-xs font-bold cursor-pointer select-none dark:text-white',
{ {
'bg-slate-200 hover:bg-slate-300 active:bg-slate-400': !status, 'bg-slate-200 dark:bg-slate-600 hover:bg-slate-300 active:bg-slate-400':
!status,
'bg-slate-400 text-white': status === 'absent', 'bg-slate-400 text-white': status === 'absent',
'bg-green-500 hover:bg-green-600 active:bg-green-700 text-white': 'bg-blue-500 hover:bg-blue-600 active:bg-blue-700 text-white':
status === 'correct', status === 'correct',
'bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 text-white': 'bg-orange-500 hover:bg-orange-600 active:bg-orange-700 dark:bg-orange-700 text-white':
status === 'present', status === 'present',
} }
) )

View file

@ -2,6 +2,7 @@ import { KeyValue } from '../../lib/keyboard'
import { getStatuses } from '../../lib/statuses' import { getStatuses } from '../../lib/statuses'
import { Key } from './Key' import { Key } from './Key'
import { useEffect } from 'react' import { useEffect } from 'react'
import { ENTER_TEXT, DELETE_TEXT } from '../../constants/strings'
type Props = { type Props = {
onChar: (value: string) => void onChar: (value: string) => void
@ -69,7 +70,7 @@ export const Keyboard = ({ onChar, onDelete, onEnter, guesses }: Props) => {
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center">
<Key width={65.4} value="ENTER" onClick={onClick}> <Key width={65.4} value="ENTER" onClick={onClick}>
Enter {ENTER_TEXT}
</Key> </Key>
<Key value="Z" onClick={onClick} status={charStatuses['Z']} /> <Key value="Z" onClick={onClick} status={charStatuses['Z']} />
<Key value="X" onClick={onClick} status={charStatuses['X']} /> <Key value="X" onClick={onClick} status={charStatuses['X']} />
@ -79,7 +80,7 @@ export const Keyboard = ({ onChar, onDelete, onEnter, guesses }: Props) => {
<Key value="N" onClick={onClick} status={charStatuses['N']} /> <Key value="N" onClick={onClick} status={charStatuses['N']} />
<Key value="M" onClick={onClick} status={charStatuses['M']} /> <Key value="M" onClick={onClick} status={charStatuses['M']} />
<Key width={65.4} value="DELETE" onClick={onClick}> <Key width={65.4} value="DELETE" onClick={onClick}>
Delete {DELETE_TEXT}
</Key> </Key>
</div> </div>
</div> </div>

View file

@ -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>
</>
)
}

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -8,21 +8,20 @@ type Props = {
export const AboutModal = ({ isOpen, handleClose }: Props) => { export const AboutModal = ({ isOpen, handleClose }: Props) => {
return ( return (
<BaseModal title="About" isOpen={isOpen} handleClose={handleClose}> <BaseModal title="About" isOpen={isOpen} handleClose={handleClose}>
<p className="text-sm text-gray-500"> This is a honey powered fork of an open source word guessing game -{' '}
This is an open source clone of the game Wordle -{' '}
<a <a
href="https://github.com/hannahcode/wordle" href="https://git.k-fortytwo.de/christofsteel/honigle"
className="underline font-bold" className="underline font-bold"
> >
check out the code here check out the code here
</a>{' '} </a>{' '}
and{' '} and{' '}
<a <a
href="https://www.powerlanguage.co.uk/wordle/" href="https://github.com/hannahcode/word-guessing-game"
className="underline font-bold" className="underline font-bold"
> >
play the original here the original code here
</a> </a>{' '}
</p> </p>
</BaseModal> </BaseModal>
) )

View file

@ -46,10 +46,10 @@ export const BaseModal = ({ title, children, isOpen, handleClose }: Props) => {
leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveFrom="opacity-100 translate-y-0 sm:scale-100"
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 dark:bg-gray-800">
<div className="absolute right-4 top-4"> <div className="absolute right-4 top-4">
<XCircleIcon <XCircleIcon
className="h-6 w-6 cursor-pointer" className="h-6 w-6 cursor-pointer dark:stroke-white"
onClick={() => handleClose()} onClick={() => handleClose()}
/> />
</div> </div>
@ -57,13 +57,11 @@ export const BaseModal = ({ title, children, isOpen, handleClose }: Props) => {
<div className="text-center"> <div className="text-center">
<Dialog.Title <Dialog.Title
as="h3" as="h3"
className="text-lg leading-6 font-medium text-gray-900" className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100"
> >
{title} {title}
</Dialog.Title> </Dialog.Title>
<div className="mt-2"> <div className="mt-2">{children}</div>
{children}
</div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -9,42 +9,42 @@ type Props = {
export const InfoModal = ({ isOpen, handleClose }: Props) => { export const InfoModal = ({ isOpen, handleClose }: Props) => {
return ( return (
<BaseModal title="How to play" isOpen={isOpen} handleClose={handleClose}> <BaseModal title="How to play" isOpen={isOpen} handleClose={handleClose}>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-300">
Guess the WORDLE in 6 tries. After each guess, the color of the tiles Rate das 🍯le in 6 Versuchen. Nach jedem Versuch ändert sich die Farbe des Kästchens
will change to show how close your guess was to the word. um anzuzeigen, wie nah man an der korrekten Lösung ist.
</p> </p>
<div className="flex justify-center mb-1 mt-4"> <div className="flex justify-center mb-1 mt-4">
<Cell value="W" status="correct" /> <Cell value="I" status="correct" />
<Cell value="M" />
<Cell value="K" />
<Cell value="E" /> <Cell value="E" />
<Cell value="A" />
<Cell value="R" /> <Cell value="R" />
<Cell value="Y" />
</div> </div>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-300">
The letter W is in the word and in the correct spot. Der Buchstabe I ist an der korrekten Stelle.
</p> </p>
<div className="flex justify-center mb-1 mt-4"> <div className="flex justify-center mb-1 mt-4">
<Cell value="P" /> <Cell value="B" />
<Cell value="I" /> <Cell value="L" />
<Cell value="L" status="present" /> <Cell value="U" status="present" />
<Cell value="O" /> <Cell value="M" />
<Cell value="T" />
</div>
<p className="text-sm text-gray-500">
The letter L is in the word but in the wrong spot.
</p>
<div className="flex justify-center mb-1 mt-4">
<Cell value="V" />
<Cell value="A" />
<Cell value="G" />
<Cell value="U" status="absent" />
<Cell value="E" /> <Cell value="E" />
</div> </div>
<p className="text-sm text-gray-500"> <p className="text-sm text-gray-500 dark:text-gray-300">
The letter U is not in the word in any spot. Der Buchstabe U ist in dem Wort enthalten, aber an der falschen Stelle.
</p>
<div className="flex justify-center mb-1 mt-4">
<Cell value="W" />
<Cell value="A" />
<Cell value="B" />
<Cell value="E" status="absent" />
<Cell value="N" />
</div>
<p className="text-sm text-gray-500 dark:text-gray-300">
Der Buchstabe E ist nicht in dem Wort enthalten.
</p> </p>
</BaseModal> </BaseModal>
) )

View file

@ -1,22 +1,80 @@
import Countdown from 'react-countdown'
import { StatBar } from '../stats/StatBar' import { StatBar } from '../stats/StatBar'
import { Histogram } from '../stats/Histogram' import { Histogram } from '../stats/Histogram'
import { GameStats } from '../../lib/localStorage' import { GameStats } from '../../lib/localStorage'
import { shareStatus } from '../../lib/share'
import { tomorrow } from '../../lib/words'
import { BaseModal } from './BaseModal' import { BaseModal } from './BaseModal'
import {
STATISTICS_TITLE,
GUESS_DISTRIBUTION_TEXT,
NEW_WORD_TEXT,
SHARE_TEXT,
} from '../../constants/strings'
type Props = { type Props = {
isOpen: boolean isOpen: boolean
handleClose: () => void handleClose: () => void
guesses: string[]
gameStats: GameStats 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_TITLE}
isOpen={isOpen}
handleClose={handleClose}
>
<StatBar gameStats={gameStats} />
</BaseModal>
)
}
return ( return (
<BaseModal title="Statistics" isOpen={isOpen} handleClose={handleClose}> <BaseModal
title={STATISTICS_TITLE}
isOpen={isOpen}
handleClose={handleClose}
>
<StatBar gameStats={gameStats} /> <StatBar gameStats={gameStats} />
<h4 className="text-lg leading-6 font-medium text-gray-900"> <h4 className="text-lg leading-6 font-medium text-gray-900 dark:text-gray-100">
Guess Distribution {GUESS_DISTRIBUTION_TEXT}
</h4> </h4>
<Histogram gameStats={gameStats} /> <Histogram gameStats={gameStats} />
{(isGameLost || isGameWon) && (
<div className="mt-5 sm:mt-6 columns-2 dark:text-white">
<div>
<h5>{NEW_WORD_TEXT}</h5>
<Countdown
className="text-lg font-medium text-gray-900 dark:text-gray-100"
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_TEXT}
</button>
</div>
)}
</BaseModal> </BaseModal>
) )
} }

View file

@ -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>
)
}

View file

@ -6,15 +6,16 @@ type Props = {
} }
export const Histogram = ({ gameStats }: Props) => { export const Histogram = ({ gameStats }: Props) => {
const { totalGames, winDistribution } = gameStats const winDistribution = gameStats.winDistribution
const maxValue = Math.max(...winDistribution)
return ( return (
<div className="columns-1 justify-left m-2 text-sm"> <div className="columns-1 justify-left m-2 text-sm dark:text-white">
{winDistribution.map((value, i) => ( {winDistribution.map((value, i) => (
<Progress <Progress
key={i} key={i}
index={i} index={i}
size={95 * (value / totalGames)} size={90 * (value / maxValue)}
label={String(value)} label={String(value)}
/> />
))} ))}

View file

@ -1,4 +1,10 @@
import { GameStats } from '../../lib/localStorage' import { GameStats } from '../../lib/localStorage'
import {
TOTAL_TRIES_TEXT,
SUCCESS_RATE_TEXT,
CURRENT_STREAK_TEXT,
BEST_STREAK_TEXT,
} from '../../constants/strings'
type Props = { type Props = {
gameStats: GameStats gameStats: GameStats
@ -12,7 +18,7 @@ const StatItem = ({
value: string | number value: string | number
}) => { }) => {
return ( return (
<div className="items-center justify-center m-1 w-1/4"> <div className="items-center justify-center m-1 w-1/4 dark:text-white">
<div className="text-3xl font-bold">{value}</div> <div className="text-3xl font-bold">{value}</div>
<div className="text-xs">{label}</div> <div className="text-xs">{label}</div>
</div> </div>
@ -22,10 +28,10 @@ const StatItem = ({
export const StatBar = ({ gameStats }: Props) => { export const StatBar = ({ gameStats }: Props) => {
return ( return (
<div className="flex justify-center my-2"> <div className="flex justify-center my-2">
<StatItem label="Total tries" value={gameStats.totalGames} /> <StatItem label={TOTAL_TRIES_TEXT} value={gameStats.totalGames} />
<StatItem label="Success rate" value={`${gameStats.successRate}%`} /> <StatItem label={SUCCESS_RATE_TEXT} value={`${gameStats.successRate}%`} />
<StatItem label="Current streak" value={gameStats.currentStreak} /> <StatItem label={CURRENT_STREAK_TEXT} value={gameStats.currentStreak} />
<StatItem label="Best streak" value={gameStats.bestStreak} /> <StatItem label={BEST_STREAK_TEXT} value={gameStats.bestStreak} />
</div> </div>
) )
} }

19
src/constants/strings.ts Normal file
View file

@ -0,0 +1,19 @@
export const GAME_TITLE = 'Honigle'
export const WIN_MESSAGES = ['Uhuhuh Richtig!', 'Hmmmm, ich mag Honig', 'Nomnomnomnom!']
export const GAME_COPIED_MESSAGE = 'In die Zwischenablage kopiert'
export const ABOUT_GAME_MESSAGE = 'Über dieses Spiel'
export const NOT_ENOUGH_LETTERS_MESSAGE = 'Nicht genug Buchstaben'
export const WORD_NOT_FOUND_MESSAGE = 'Wort nicht erkannt'
export const CORRECT_WORD_MESSAGE = (solution: string) =>
`Oh Nein! Das Wort war ${solution}`
export const ENTER_TEXT = 'Enter'
export const DELETE_TEXT = 'Delete'
export const STATISTICS_TITLE = 'Statistiken'
export const GUESS_DISTRIBUTION_TEXT = 'Rateverteilung'
export const NEW_WORD_TEXT = 'Nächstes Wort in'
export const SHARE_TEXT = 'Teilen'
export const TOTAL_TRIES_TEXT = 'Anzahl Versuche'
export const SUCCESS_RATE_TEXT = 'Erfolgsquote'
export const CURRENT_STREAK_TEXT = 'Siegserie'
export const BEST_STREAK_TEXT = 'Beste Siegserie'

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,11 @@
import { getGuessStatuses } from './statuses' import { getGuessStatuses } from './statuses'
import { solutionIndex } from './words' import { solutionIndex } from './words'
import { GAME_TITLE } from '../constants/strings'
export const shareStatus = (guesses: string[]) => { export const shareStatus = (guesses: string[], lost: boolean) => {
navigator.clipboard.writeText( navigator.clipboard.writeText(
`Not Wordle ${solutionIndex} ${guesses.length}/6\n\n` + `${GAME_TITLE} ${solutionIndex} ${lost ? 'X' : guesses.length}/6\n\n` +
generateEmojiGrid(guesses) generateEmojiGrid(guesses)
) )
} }

View file

@ -14,15 +14,17 @@ export const isWinningWord = (word: string) => {
export const getWordOfDay = () => { export const getWordOfDay = () => {
// January 1, 2022 Game Epoch // January 1, 2022 Game Epoch
const epochMs = 1641013200000 const epochMs = new Date('January 1, 2022 00:00:00').valueOf()
const now = Date.now() const now = Date.now()
const msInDay = 86400000 const msInDay = 86400000
const index = Math.floor((now - epochMs) / msInDay) const index = Math.floor((now - epochMs) / msInDay)
const nextday = (index + 1) * msInDay + epochMs
return { return {
solution: WORDS[index].toUpperCase(), solution: WORDS[0].toUpperCase(),
solutionIndex: index, solutionIndex: index,
tomorrow: nextday,
} }
} }
export const { solution, solutionIndex } = getWordOfDay() export const { solution, solutionIndex, tomorrow } = getWordOfDay()

View file

@ -1,5 +1,6 @@
module.exports = { module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'], content: ['./src/**/*.{js,jsx,ts,tsx}'],
darkMode: 'class',
theme: { theme: {
extend: {}, extend: {},
}, },