first commit
This commit is contained in:
commit
8d4d27eaf4
13 changed files with 1299 additions and 0 deletions
133
src/App.jsx
Normal file
133
src/App.jsx
Normal 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
1
src/Modes.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const Modes = Object.freeze({ Addition: 1, Subtraction: 2 })
|
||||
49
src/components/Nav.jsx
Normal file
49
src/components/Nav.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
24
src/hooks/useLocalStorage.jsx
Normal file
24
src/hooks/useLocalStorage.jsx
Normal 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
10
src/main.jsx
Normal 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
50
src/styles/App.module.css
Normal 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
56
src/styles/Nav.module.css
Normal 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
47
src/styles/index.css
Normal 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;
|
||||
}
|
||||
Reference in a new issue