histogram width divide by mode, not total #2
12 changed files with 96 additions and 29 deletions
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
}
|
}
|
||||||
|
|
16
README.md
16
README.md
|
@ -22,6 +22,7 @@ I implemented Tailwind mostly because I wanted to learn how to use Tailwind CSS,
|
||||||
|
|
||||||
_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 wordle
|
||||||
$ npm install
|
$ npm install
|
||||||
|
@ -29,9 +30,24 @@ $ npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
_To build/run docker container:_
|
_To build/run docker container:_
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ docker build -t notwordle .
|
$ docker build -t notwordle .
|
||||||
$ docker run -d -p 3000:3000 notwordle
|
$ docker run -d -p 3000:3000 notwordle
|
||||||
```
|
```
|
||||||
|
|
||||||
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`
|
||||||
|
|
|
@ -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="Not Wordle" />
|
||||||
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>Not Wordle</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>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"short_name": "React App",
|
"short_name": "Not Wordle",
|
||||||
"name": "Create React App Sample",
|
"name": "Not Wordle",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "favicon.ico",
|
"src": "favicon.ico",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
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 { WORDLE_TITLE } from './constants/strings'
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
|
@ -20,6 +21,6 @@ beforeEach(() => {
|
||||||
|
|
||||||
test('renders App component', () => {
|
test('renders App component', () => {
|
||||||
render(<App />)
|
render(<App />)
|
||||||
const linkElement = screen.getByText(/Not Wordle/)
|
const linkElement = screen.getByText(WORDLE_TITLE)
|
||||||
expect(linkElement).toBeInTheDocument()
|
expect(linkElement).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
27
src/App.tsx
27
src/App.tsx
|
@ -10,7 +10,15 @@ 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 { StatsModal } from './components/modals/StatsModal'
|
import { StatsModal } from './components/modals/StatsModal'
|
||||||
import { WIN_MESSAGES } from './constants/strings'
|
import {
|
||||||
|
WORDLE_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 {
|
||||||
|
@ -143,7 +151,9 @@ 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 mt-12">
|
<div className="flex w-80 mx-auto items-center mb-8 mt-12">
|
||||||
<h1 className="text-xl grow font-bold dark:text-white">Not Wordle</h1>
|
<h1 className="text-xl grow font-bold dark:text-white">
|
||||||
|
{WORDLE_TITLE}
|
||||||
|
</h1>
|
||||||
<SunIcon
|
<SunIcon
|
||||||
className="h-6 w-6 cursor-pointer dark:stroke-white"
|
className="h-6 w-6 cursor-pointer dark:stroke-white"
|
||||||
onClick={() => handleDarkMode(!isDarkMode)}
|
onClick={() => handleDarkMode(!isDarkMode)}
|
||||||
|
@ -176,7 +186,7 @@ function App() {
|
||||||
isGameLost={isGameLost}
|
isGameLost={isGameLost}
|
||||||
isGameWon={isGameWon}
|
isGameWon={isGameWon}
|
||||||
handleShare={() => {
|
handleShare={() => {
|
||||||
setSuccessAlert('Game copied to clipboard')
|
setSuccessAlert(GAME_COPIED_MESSAGE)
|
||||||
return setTimeout(() => setSuccessAlert(''), ALERT_TIME_MS)
|
return setTimeout(() => setSuccessAlert(''), ALERT_TIME_MS)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -190,12 +200,15 @@ 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={`The word was ${solution}`} isOpen={isGameLost} />
|
message={WORD_NOT_FOUND_MESSAGE}
|
||||||
|
isOpen={isWordNotFoundAlertOpen}
|
||||||
|
/>
|
||||||
|
<Alert message={CORRECT_WORD_MESSAGE(solution)} isOpen={isGameLost} />
|
||||||
<Alert
|
<Alert
|
||||||
message={successAlert}
|
message={successAlert}
|
||||||
isOpen={successAlert !== ''}
|
isOpen={successAlert !== ''}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -5,6 +5,12 @@ import { GameStats } from '../../lib/localStorage'
|
||||||
import { shareStatus } from '../../lib/share'
|
import { shareStatus } from '../../lib/share'
|
||||||
import { tomorrow } from '../../lib/words'
|
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
|
||||||
|
@ -27,22 +33,30 @@ export const StatsModal = ({
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
if (gameStats.totalGames <= 0) {
|
if (gameStats.totalGames <= 0) {
|
||||||
return (
|
return (
|
||||||
<BaseModal title="Statistics" isOpen={isOpen} handleClose={handleClose}>
|
<BaseModal
|
||||||
|
title={STATISTICS_TITLE}
|
||||||
|
isOpen={isOpen}
|
||||||
|
handleClose={handleClose}
|
||||||
|
>
|
||||||
<StatBar gameStats={gameStats} />
|
<StatBar gameStats={gameStats} />
|
||||||
</BaseModal>
|
</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 dark:text-gray-100">
|
<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) && (
|
{(isGameLost || isGameWon) && (
|
||||||
<div className="mt-5 sm:mt-6 columns-2 dark:text-white">
|
<div className="mt-5 sm:mt-6 columns-2 dark:text-white">
|
||||||
<div>
|
<div>
|
||||||
<h5>New word in</h5>
|
<h5>{NEW_WORD_TEXT}</h5>
|
||||||
<Countdown
|
<Countdown
|
||||||
className="text-lg font-medium text-gray-900 dark:text-gray-100"
|
className="text-lg font-medium text-gray-900 dark:text-gray-100"
|
||||||
date={tomorrow}
|
date={tomorrow}
|
||||||
|
@ -57,7 +71,7 @@ export const StatsModal = ({
|
||||||
handleShare()
|
handleShare()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Share
|
{SHARE_TEXT}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,19 @@
|
||||||
|
export const WORDLE_TITLE = 'Not Wordle'
|
||||||
|
|
||||||
export const WIN_MESSAGES = ['Great Job!', 'Awesome', 'Well done!']
|
export const WIN_MESSAGES = ['Great Job!', 'Awesome', 'Well done!']
|
||||||
|
export const GAME_COPIED_MESSAGE = 'Game copied to clipboard'
|
||||||
|
export const ABOUT_GAME_MESSAGE = 'About this game'
|
||||||
|
export const NOT_ENOUGH_LETTERS_MESSAGE = 'Not enough letters'
|
||||||
|
export const WORD_NOT_FOUND_MESSAGE = 'Word not found'
|
||||||
|
export const CORRECT_WORD_MESSAGE = (solution: string) =>
|
||||||
|
`The word was ${solution}`
|
||||||
|
export const ENTER_TEXT = 'Enter'
|
||||||
|
export const DELETE_TEXT = 'Delete'
|
||||||
|
export const STATISTICS_TITLE = 'Statistics'
|
||||||
|
export const GUESS_DISTRIBUTION_TEXT = 'Guess Distribution'
|
||||||
|
export const NEW_WORD_TEXT = 'New word in'
|
||||||
|
export const SHARE_TEXT = 'Share'
|
||||||
|
export const TOTAL_TRIES_TEXT = 'Total tries'
|
||||||
|
export const SUCCESS_RATE_TEXT = 'Success rate'
|
||||||
|
export const CURRENT_STREAK_TEXT = 'Current streak'
|
||||||
|
export const BEST_STREAK_TEXT = 'Best streak'
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { getGuessStatuses } from './statuses'
|
import { getGuessStatuses } from './statuses'
|
||||||
import { solutionIndex } from './words'
|
import { solutionIndex } from './words'
|
||||||
|
import { WORDLE_TITLE } from '../constants/strings'
|
||||||
|
|
||||||
export const shareStatus = (guesses: string[], lost: boolean) => {
|
export const shareStatus = (guesses: string[], lost: boolean) => {
|
||||||
navigator.clipboard.writeText(
|
navigator.clipboard.writeText(
|
||||||
`Not Wordle ${solutionIndex} ${lost ? 'X' : guesses.length}/6\n\n` +
|
`${WORDLE_TITLE} ${solutionIndex} ${lost ? 'X' : guesses.length}/6\n\n` +
|
||||||
generateEmojiGrid(guesses)
|
generateEmojiGrid(guesses)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|
Loading…
Add table
Reference in a new issue