histogram width divide by mode, not total #2
12 changed files with 83 additions and 64 deletions
39
src/App.css
39
src/App.css
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,22 @@ import { render, screen } from '@testing-library/react'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { WORDLE_TITLE } from './constants/strings'
|
import { WORDLE_TITLE } from './constants/strings'
|
||||||
|
|
||||||
|
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', () => {
|
test('renders App component', () => {
|
||||||
render(<App />)
|
render(<App />)
|
||||||
const linkElement = screen.getByText(WORDLE_TITLE)
|
const linkElement = screen.getByText(WORDLE_TITLE)
|
||||||
|
|
43
src/App.tsx
43
src/App.tsx
|
@ -1,5 +1,8 @@
|
||||||
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'
|
||||||
|
@ -23,9 +26,15 @@ import {
|
||||||
saveGameStateToLocalStorage,
|
saveGameStateToLocalStorage,
|
||||||
} from './lib/localStorage'
|
} from './lib/localStorage'
|
||||||
|
|
||||||
|
import './App.css'
|
||||||
|
|
||||||
const ALERT_TIME_MS = 2000
|
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 [isInfoModalOpen, setIsInfoModalOpen] = useState(false)
|
const [isInfoModalOpen, setIsInfoModalOpen] = useState(false)
|
||||||
|
@ -34,6 +43,13 @@ function App() {
|
||||||
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 [isDarkMode, setIsDarkMode] = useState(
|
||||||
|
localStorage.getItem('theme')
|
||||||
|
? localStorage.getItem('theme') === 'dark'
|
||||||
|
: prefersDarkMode
|
||||||
|
? true
|
||||||
|
: false
|
||||||
|
)
|
||||||
const [successAlert, setSuccessAlert] = useState('')
|
const [successAlert, setSuccessAlert] = useState('')
|
||||||
const [guesses, setGuesses] = useState<string[]>(() => {
|
const [guesses, setGuesses] = useState<string[]>(() => {
|
||||||
const loaded = loadGameStateFromLocalStorage()
|
const loaded = loadGameStateFromLocalStorage()
|
||||||
|
@ -52,6 +68,19 @@ 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])
|
||||||
|
@ -122,13 +151,17 @@ 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">
|
||||||
<h1 className="text-xl grow font-bold">{WORDLE_TITLE}</h1>
|
<h1 className="text-xl grow font-bold dark:text-white">{WORDLE_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>
|
||||||
|
|
|
@ -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-slate-400 dark:bg-slate-700 text-white border-slate-400 dark:border-slate-700':
|
||||||
|
status === 'absent',
|
||||||
'bg-green-500 text-white border-green-500': status === 'correct',
|
'bg-green-500 text-white border-green-500': status === 'correct',
|
||||||
'bg-yellow-500 text-white border-yellow-500': status === 'present',
|
'bg-yellow-500 dark:bg-yellow-700 text-white border-yellow-500 dark:border-yellow-700':
|
||||||
|
status === 'present',
|
||||||
'cell-animation': !!value,
|
'cell-animation': !!value,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -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-green-500 hover:bg-green-600 active:bg-green-700 text-white':
|
||||||
status === 'correct',
|
status === 'correct',
|
||||||
'bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 text-white':
|
'bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 dark:bg-yellow-700 text-white':
|
||||||
status === 'present',
|
status === 'present',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -8,7 +8,7 @@ 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">
|
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||||
This is an open source clone of the game Wordle -{' '}
|
This is an open source clone of the game Wordle -{' '}
|
||||||
<a
|
<a
|
||||||
href="https://github.com/hannahcode/wordle"
|
href="https://github.com/hannahcode/wordle"
|
||||||
|
|
|
@ -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,7 +57,7 @@ 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>
|
||||||
|
|
|
@ -9,7 +9,7 @@ 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
|
Guess the WORDLE in 6 tries. After each guess, the color of the tiles
|
||||||
will change to show how close your guess was to the word.
|
will change to show how close your guess was to the word.
|
||||||
</p>
|
</p>
|
||||||
|
@ -21,7 +21,7 @@ export const InfoModal = ({ isOpen, handleClose }: Props) => {
|
||||||
<Cell value="R" />
|
<Cell value="R" />
|
||||||
<Cell value="Y" />
|
<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.
|
The letter W is in the word and in the correct spot.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ export const InfoModal = ({ isOpen, handleClose }: Props) => {
|
||||||
<Cell value="O" />
|
<Cell value="O" />
|
||||||
<Cell value="T" />
|
<Cell value="T" />
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-300">
|
||||||
The letter L is in the word but in the wrong spot.
|
The letter L is in the word but in the wrong spot.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ export const InfoModal = ({ isOpen, handleClose }: Props) => {
|
||||||
<Cell value="U" status="absent" />
|
<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.
|
The letter U is not in the word in any spot.
|
||||||
</p>
|
</p>
|
||||||
</BaseModal>
|
</BaseModal>
|
||||||
|
|
|
@ -49,16 +49,16 @@ export const StatsModal = ({
|
||||||
handleClose={handleClose}
|
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_TEXT}
|
{GUESS_DISTRIBUTION_TEXT}
|
||||||
</h4>
|
</h4>
|
||||||
<Histogram gameStats={gameStats} />
|
<Histogram gameStats={gameStats} />
|
||||||
{(isGameLost || isGameWon) && (
|
{(isGameLost || isGameWon) && (
|
||||||
<div className="mt-5 sm:mt-6 columns-2">
|
<div className="mt-5 sm:mt-6 columns-2 dark:text-white">
|
||||||
<div>
|
<div>
|
||||||
<h5>{NEW_WORD_TEXT}</h5>
|
<h5>{NEW_WORD_TEXT}</h5>
|
||||||
<Countdown
|
<Countdown
|
||||||
className="text-lg font-medium text-gray-900"
|
className="text-lg font-medium text-gray-900 dark:text-gray-100"
|
||||||
date={tomorrow}
|
date={tomorrow}
|
||||||
daysInHours={true}
|
daysInHours={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,7 +10,7 @@ export const Histogram = ({ gameStats }: Props) => {
|
||||||
const maxValue = Math.max(...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}
|
||||||
|
|
|
@ -18,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>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
||||||
theme: {
|
darkMode: 'class',
|
||||||
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|
Loading…
Add table
Reference in a new issue