Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Level up #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 28 additions & 7 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,42 @@
import { CSSProperties, useEffect, useRef, useState } from 'react'
import { useEffect, useRef, useState } from 'react'

import type { HexColor } from '@/utils'
import { randomColors, randomInt } from '@/utils'

import styles from './App.module.scss'
import ColorDisplay from '@/components/ColorDisplay'
import type { ColorComponentsDisplay } from '@/components/ColorButton'
import ColorButton from '@/components/ColorButton'
import Score from '@/components/Score'
import NextButton from '@/components/NextButton'
import TutorialInfo from '@/components/TutorialInfo'

import type { HexColor } from '@/utils'
import { randomColors, randomInt } from '@/utils'
import LevelUp from '@/components/LevelUp'

function App() {
const [colorChoices, setColorChoices] = useState<HexColor[] | null>(null)
const [trueColorId, setTrueColorId] = useState<number | null>(null)

const [userColorAnswer, setUserColorAnswer] = useState<HexColor | null>(null)
const [score, setScore] = useState<number>(0)
const [previousScore, setPreviousScore] = useState<number>(-1)

const [canGoToNextColorQuestion, setCanGoToNextColorQuestion] = useState<boolean>(false)
const [isLevelUpAnimationDone, setIsLevelUpAnimationDone] = useState<boolean>(true)

const colorDisplayRef = useRef<HTMLElement>(null)

const hasClickedAnAnswer = userColorAnswer !== null
const trueColor = colorChoices !== null && trueColorId !== null ? colorChoices[trueColorId] : null
const revealed = userColorAnswer !== null
const revealed = hasClickedAnAnswer

const isLevelUp = (score: number, previousScore: number): boolean =>
score > previousScore && score > 0 && score !== 10 && score % 5 === 0
const getNumChoicesBasedOnScore = (score: number): number =>
score < 5 ? 2 : score < 20 ? 3 : Math.min(2 + Math.floor(score / 10), 8)
const getColorComponentsDisplayBasedOnScore = (score: number): ColorComponentsDisplay =>
score < 15 ? 'always' : score < 25 ? 'all-on-hover' : score < 35 ? 'individual-on-hover' : 'none' // TODO: update to include new modes e.g. "show-random-2"
// TODO: extract levels to json file
// TODO: add animated "Level Up! ⬆" when difficulty changes
// TODO: add animated "Level Up! ⬆" when difficulty changes (on phone only?)
// TODO: add instructions when behavior changes (e.g. on tactile: long press on button to reveal colors)
// TODO: add sound effects

Expand All @@ -53,8 +60,19 @@ function App() {
}
}, [])

useEffect(() => {
if (!isLevelUpAnimationDone) {
setTimeout(() => {
setIsLevelUpAnimationDone(true)
}, 2500)
}
}, [isLevelUpAnimationDone])

const handleColorButtonClick = (color: HexColor) => {
let newScore = score // to get updated numChoices for next question
const newPreviousScore = score

setPreviousScore(score)

if (color === trueColor) {
newScore++
Expand All @@ -71,6 +89,7 @@ function App() {
colorDisplayRef.current?.removeEventListener<'click'>('click', nextColor)
setColorComponentsDisplay(getColorComponentsDisplayBasedOnScore(newScore))
newColorQuestion(getNumChoicesBasedOnScore(newScore))
if (isLevelUp(newScore, newPreviousScore)) setIsLevelUpAnimationDone(false)
}
colorDisplayRef.current?.addEventListener<'click'>('click', nextColor)
}, 600)
Expand All @@ -79,11 +98,13 @@ function App() {
return (
<main className={styles.main}>
<ColorDisplay ref={colorDisplayRef} color={trueColor}>
{isLevelUp(score, previousScore) && !hasClickedAnAnswer && <LevelUp />}
<Score value={score} />
{canGoToNextColorQuestion && <NextButton />}
</ColorDisplay>
<div className={styles.buttons}>
{colorChoices &&
isLevelUpAnimationDone &&
colorChoices.map((color) => (
<ColorButton
onClick={handleColorButtonClick}
Expand All @@ -95,7 +116,7 @@ function App() {
colorComponentsDisplay={colorComponentsDisplay}
/>
))}
{score === 0 && userColorAnswer === null && <TutorialInfo />}
{score === 0 && !hasClickedAnAnswer && <TutorialInfo />}
</div>
</main>
)
Expand Down
6 changes: 3 additions & 3 deletions src/components/ColorButton.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
}

&.correct-answer {
outline: 0.18em solid rgba(42, 180, 65, 0.85);
outline: 0.20em solid rgba(42, 180, 65, 0.85);
animation: 300ms ease-in-out jump;

position: relative;
Expand All @@ -52,12 +52,12 @@
}

&.wrong-answer {
outline: 0.18em solid rgba(180, 42, 42, 0.85);
outline: 0.20em solid rgba(180, 42, 42, 0.85);
animation: 300ms ease-in-out shake;
}

&.unpicked-correct-answer {
outline: 0.18em solid rgba(42, 180, 104, 0.85);
outline: 0.20em solid rgba(42, 180, 104, 0.85);
}

@media (min-width: 768px) {
Expand Down
4 changes: 1 addition & 3 deletions src/components/ColorButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ const ColorButton: FunctionComponent<ColorButtonProps> = ({
}) => {
const colorComponents = color.match(/^#(\w\w)(\w\w)(\w\w)$/)?.slice(1, 4) ?? null

if (colorComponents === null) throw new Error(`${color} is not an hex color!`)

return (
<button
className={`button ${styles['color-button']} ${styles[colorComponentsDisplay]} ${
Expand All @@ -61,7 +59,7 @@ const ColorButton: FunctionComponent<ColorButtonProps> = ({
disabled={revealed}
{...rest}>
#
{colorComponents.map((component, i) => {
{colorComponents?.map((component, i) => {
const value = parseInt(component, 16)
const rgbValues = [0, 0, 0]
rgbValues[i] = 255
Expand Down
86 changes: 86 additions & 0 deletions src/components/LevelUp.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.level-up {
position: absolute;
top: calc(20% + var(--adjustment));
left: 50%;
translate: -50% -50%;
z-index: 2;

font-size: clamp(1rem, 10vw, 3rem);
font-family: monospace;

white-space: nowrap;

text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5), 0px 0px 5px rgb(0 0 0 / 40%);

animation: 2500ms ease-in-out forwards appear;

&::after {
content: '⬆';
position: absolute;
animation: 400ms alternate infinite wiggle;
}

--adjustment: 60%;

@media (min-width: 768px) {
--adjustment: 0%;
}
}

@keyframes wiggle {
0% {
translate: 0 -25%;
}
100% {
translate: 0 7%;
}
}

@keyframes appear {
0% {
opacity: 0;
scale: 0;
}
20% {
scale: 1.1;
opacity: 1;
}
30% {
scale: 1;
opacity: 1;
}
70% {
scale: 1;
}
85% {
scale: 1.1;
opacity: 1;
}
100% {
scale: 0;
opacity: 0;
}
}

// @keyframes appear {
// 0% {
// top: calc(25% + var(--adjustment));
// opacity: 0;
// }
// 50% {
// top: calc(20% + var(--adjustment));
// scale: 1;
// opacity: 1;
// }
// 70% {
// scale: 1;
// }
// 85% {
// scale: 1.1;
// opacity: 1;
// }
// 100% {
// scale: 0;
// opacity: 0;
// }
// }
16 changes: 16 additions & 0 deletions src/components/LevelUp.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { FunctionComponent } from 'react'
import styles from './LevelUp.module.scss'

interface LevelUpProps {
[key: string]: any
}

const LevelUp: FunctionComponent<LevelUpProps> = ({ ...rest }) => {
return (
<span className={styles['level-up']} {...rest}>
Level Up!
</span>
)
}

export default LevelUp
2 changes: 1 addition & 1 deletion src/components/Score.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
font-size: clamp(3rem, 6vw, 5rem);
font-weight: bold;

text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5), 0px 0px 3px rgb(0 0 0 / 40%);
text-shadow: 2px 2px 3px rgba(0, 0, 0, 0.5), 0px 0px 5px rgb(0 0 0 / 40%);
}