first commit

This commit is contained in:
Henry Hiles 2023-01-17 10:49:41 -05:00
commit 8d4d27eaf4
13 changed files with 1299 additions and 0 deletions

133
src/App.jsx Normal file
View file

@ -0,0 +1,133 @@
import { useEffect, useState } from "react"
import { FaEquals, FaMinus, FaPlus } from "react-icons/fa"
import { SlCheck, SlClose } from "react-icons/sl"
import { Nav } from "./components/Nav"
import { useLocalStorage } from "./hooks/useLocalStorage"
import { Modes } from "./Modes"
import styles from "./styles/App.module.css"
const randomNumber = () => Math.floor(Math.random() * 100) + 1
export const App = () => {
const [mode, setMode] = useLocalStorage("mode", Modes.Addition)
const [modeNumbers, setModeNumbers] = useLocalStorage("modeNumbers", 3)
const [numbers, setNumbers] = useState()
const [index, setIndex] = useState(0)
const [correctAnswers, setCorrectAnswers] = useState(0)
const [answerTimeout, setAnswerTimeout] = useState()
const [correctAnswer, setCorrectAnswer] = useState()
const [correctAnswerTimeout, setCorrectAnswerTimeout] = useState()
const Operator = () => (mode == Modes.Addition ? <FaPlus /> : <FaMinus />)
const getCorrectAnswer = (answerIndex = index) =>
numbers[answerIndex]?.reduce((accumulator, currentValue) => {
console.log(accumulator, currentValue)
return mode == Modes.Addition
? accumulator + currentValue
: accumulator - currentValue
}) || "Unknown"
useEffect(() => {
clearTimeout(correctAnswerTimeout)
setCorrectAnswerTimeout(setTimeout(() => setCorrectAnswer(), 5000))
return () => clearTimeout(correctAnswerTimeout)
}, [correctAnswer])
useEffect(() => {
setNumbers(
[...new Array(20)].map(() =>
[...new Array(modeNumbers)].map((_, index) =>
index == 0
? randomNumber() * 2
: Math.ceil(
randomNumber() /
(mode == Modes.Subtraction
? modeNumbers * 0.4
: 1)
)
)
)
)
setCorrectAnswers(0)
setIndex(0)
clearTimeout(answerTimeout)
}, [mode, modeNumbers])
return (
<>
<Nav
mode={mode}
setMode={setMode}
modeNumbers={modeNumbers}
setModeNumbers={setModeNumbers}
/>
<div className={styles.container}>
<h1>
Correct Answers: {correctAnswers}/{index}
</h1>
<h1>
Completed questions: {index}/{numbers?.length}
</h1>
{index === numbers?.length ? (
<h1>Complete.</h1>
) : (
<h3 className={styles.numbers}>
{numbers?.[index].map((number, index) => (
<>
{index == 0 || <Operator />} {number}{" "}
</>
))}
<FaEquals />{" "}
<input
type="number"
autoFocus
className={styles.input}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
event.target.value
) {
clearTimeout(answerTimeout)
const isCorrect =
event.target.value == getCorrectAnswer()
if (isCorrect)
setCorrectAnswers(
(answers) => ++answers
)
setCorrectAnswer(!isCorrect)
setIndex((index) => ++index)
event.target.value = ""
setAnswerTimeout(
setTimeout(() => {
setCorrectAnswer(true)
}, 15000)
)
}
}}
/>
</h3>
)}
{correctAnswer === true && (
<h1 className={styles.wrongAnswer}>
<SlClose /> Wrong Answer.{" "}
<span>
Correct Answer: {getCorrectAnswer(index - 1)}
</span>
</h1>
)}
{correctAnswer === false && (
<h1 className={styles.correctAnswer}>
<SlCheck /> Correct!
</h1>
)}
</div>
</>
)
}

1
src/Modes.js Normal file
View file

@ -0,0 +1 @@
export const Modes = Object.freeze({ Addition: 1, Subtraction: 2 })

49
src/components/Nav.jsx Normal file
View file

@ -0,0 +1,49 @@
import { Modes } from "../Modes"
import { FaMinus, FaPlus } from "react-icons/fa"
import styles from "../styles/Nav.module.css"
export const Nav = ({ mode, setMode, modeNumbers, setModeNumbers }) => {
return (
<nav className={styles.nav}>
<ul>
<li>
<h1>Mental Math</h1>
</li>
<li>
<button
onClick={() =>
setModeNumbers((numbers) =>
numbers > 2 ? --numbers : numbers
)
}
>
<FaMinus />
</button>
<span>{modeNumbers}</span>
<button
onClick={() => setModeNumbers((numbers) => ++numbers)}
>
<FaPlus />
</button>
<br />
<button
className={`${
mode == Modes.Addition ? styles.active : ""
} ${styles.modeButton}`}
onClick={() => setMode(Modes.Addition)}
>
Addition
</button>
<button
className={`${
mode == Modes.Subtraction ? styles.active : ""
} ${styles.modeButton}`}
onClick={() => setMode(Modes.Subtraction)}
>
Subtraction
</button>
</li>
</ul>
</nav>
)
}

View file

@ -0,0 +1,24 @@
import { useState, useEffect } from "react"
export const useLocalStorage = (key, defaultValue) => {
const [value, setValue] = useState(() => {
let currentValue
try {
currentValue = JSON.parse(
localStorage.getItem(key) || String(defaultValue)
)
} catch (error) {
currentValue = defaultValue
}
return currentValue
})
useEffect(
() => localStorage.setItem(key, JSON.stringify(value)),
[value, key]
)
return [value, setValue]
}

10
src/main.jsx Normal file
View file

@ -0,0 +1,10 @@
import React from "react"
import ReactDOM from "react-dom/client"
import { App } from "./App"
import "./styles/index.css"
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
)

50
src/styles/App.module.css Normal file
View file

@ -0,0 +1,50 @@
.container {
display: flex;
width: 100%;
height: 100%;
background-color: white;
padding: 20px;
align-items: center;
justify-content: center;
gap: 20px;
text-align: center;
flex-direction: column;
box-shadow: rgba(0, 0, 0, 0.35) 0px 5px 15px;
}
.numbers {
font-size: 2em;
}
.wrongAnswer {
color: #cc0029;
}
.correctAnswer {
color: #0dbe0d;
}
.input {
border: 2px solid black;
padding: 10px;
border-radius: 10px;
}
@media (prefers-color-scheme: dark) {
.container {
background-color: #161f27;
}
.input {
border-color: white;
background-color: var(--secondary);
}
}
@media (min-width: 800px) {
.container {
width: 60vw;
border-radius: 20px;
height: 65vh;
}
}

56
src/styles/Nav.module.css Normal file
View file

@ -0,0 +1,56 @@
.nav {
position: absolute;
top: 0;
width: 100%;
}
.nav ul {
padding: 25px 20px;
display: flex;
flex-direction: row;
position: sticky;
gap: 20px;
align-items: center;
position: -webkit-sticky;
background-color: white;
top: 0;
width: 100%;
margin: 0;
list-style: none;
justify-content: space-between;
}
.nav ul li {
display: flex;
gap: 10px;
}
.nav ul li * {
font-size: 1.25em;
}
.nav ul li span {
display: flex;
flex-direction: row;
align-items: flex-end;
}
.nav ul li button {
border: none;
background: transparent;
cursor: pointer;
}
.nav ul li .modeButton:not(.active) {
text-decoration: underline;
}
@media (prefers-color-scheme: dark) {
.nav ul {
background-color: #161f27;
}
}
.active {
font-weight: bold;
}

47
src/styles/index.css Normal file
View file

@ -0,0 +1,47 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
scroll-behavior: smooth;
color-scheme: light dark;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
--primary: #14bbaa;
--secondary: #2c3c4c;
}
#root {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
body {
margin: 0;
background-color: #efeff5;
height: 100vh;
}
@media (prefers-color-scheme: dark) {
body {
background-color: var(--secondary);
}
}
svg {
vertical-align: middle;
font-size: 0.75em;
}
h1 {
margin: 0;
}