diff --git a/src/App.tsx b/src/App.tsx index 7d80eb8..87f1eeb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,9 +1,10 @@ -import { Button, ChakraProvider } from '@chakra-ui/react'; -import { ChordsPage, NotesPage } from './pages'; -import { AppPages } from './config'; -import { useState } from 'react'; -import { NoteSettingsContextProvider } from './contexts/NoteContext'; -import { ChordSettingsContextProvider } from './contexts/ChordContext'; +import { Button, ChakraProvider } from "@chakra-ui/react"; +import { ChordsPage, NotesPage } from "./pages"; +import { AppPages } from "./config"; +import { useState } from "react"; +import { NoteSettingsContextProvider } from "./contexts/NoteContext"; +import { ChordSettingsContextProvider } from "./contexts/ChordContext"; +import { SpeedContextProvider } from "./contexts/SpeedContext"; const App = () => { const [selectedPage, setSelectedPage] = useState(AppPages.notes); @@ -12,28 +13,44 @@ const App = () => {
{selectedPage === AppPages.notes && ( - )} {selectedPage === AppPages.chords && ( - )} {selectedPage === AppPages.notes && ( - - - + + + + + )} {selectedPage === AppPages.chords && ( - - - + + + + + )}
- {'Made with ❤️ by '} - + {"Made with ❤️ by "} + Cyril Gourgouillon
diff --git a/src/components/AutoSkipper.tsx b/src/components/AutoSkipper.tsx index 22b71ce..ca207e3 100644 --- a/src/components/AutoSkipper.tsx +++ b/src/components/AutoSkipper.tsx @@ -1,33 +1,23 @@ -import { ButtonGroup, IconButton, useToast } from '@chakra-ui/react'; -import { useEffect, useState } from 'react'; -import { Speed, speeds } from '../config'; -import { getSpeedColor, getSpeedIcon } from '../services'; +import { ButtonGroup, IconButton, useToast } from "@chakra-ui/react"; +import { Speed, speeds } from "../config"; +import { getSpeedColor, getSpeedIcon } from "../services"; +import { useSpeedContext } from "../hooks"; -export const AutoSkipper = ({ onSkip }: { onSkip: () => void }) => { +export const AutoSkipper = () => { const toast = useToast(); - const [speed, setSpeed] = useState(undefined); - - useEffect(() => { - if (speed) { - const skipInterval = setInterval(() => { - onSkip(); - }, speed); - - return () => clearInterval(skipInterval); - } - }, [onSkip, speed]); + const {speed, setCurrentSpeed} = useSpeedContext(); const triggerToast = (skipDuration: Speed) => { toast({ title: `Auto skip every ${skipDuration / 1000}"`, - status: 'info', + status: "info", duration: 1000, }); }; return ( <> - + {speeds.map((s, i) => ( void }) => { icon={getSpeedIcon(s)} onClick={() => { if (speed === s) { - setSpeed(undefined); + setCurrentSpeed(undefined); } else { triggerToast(s); - setSpeed(s); + setCurrentSpeed(s); } }} - variant={'outline'} + variant={"outline"} isActive={speed === s} colorScheme={getSpeedColor(s)} /> diff --git a/src/components/ChordsSettings.tsx b/src/components/ChordsSettings.tsx index e7bd9e7..2a62a6c 100644 --- a/src/components/ChordsSettings.tsx +++ b/src/components/ChordsSettings.tsx @@ -57,7 +57,7 @@ export const ChordsSettings = () => { > Toggle shape complexity - +
diff --git a/src/components/NotesSettings.tsx b/src/components/NotesSettings.tsx index 0a9146f..86f372f 100644 --- a/src/components/NotesSettings.tsx +++ b/src/components/NotesSettings.tsx @@ -47,7 +47,7 @@ export const NotesSettings = () => { - + diff --git a/src/components/TimerCue.tsx b/src/components/TimerCue.tsx new file mode 100644 index 0000000..e759101 --- /dev/null +++ b/src/components/TimerCue.tsx @@ -0,0 +1,25 @@ +import { Speed } from "../config"; +import { GoDot, GoDotFill } from "react-icons/go"; +import { useSpeedContext } from "../hooks"; + +export const TimerCue = () => { + const { speed, secondsElapsed } = useSpeedContext(); + + const numberOfDots = speed ? speed / 1000: 0; + const dots: React.ReactNode[] = []; + + for (let i = 0; i < secondsElapsed; i++) { + dots.push(); + } + for (let i = 0; i < numberOfDots - secondsElapsed; i++) { + dots.push(); + } + + if (speed === Speed.rush) { + return <>; + } + + return ( +
{dots.map((d, i) =>
{d}
)}
+ ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 149e1be..f63221c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -3,3 +3,4 @@ export * from './ChordsList'; export * from './AutoSkipper'; export * from './NotesSettings' export * from './ChordsSettings'; +export * from './TimerCue'; diff --git a/src/contexts/ChordContext.tsx b/src/contexts/ChordContext.tsx index cb82f72..17c3c4b 100644 --- a/src/contexts/ChordContext.tsx +++ b/src/contexts/ChordContext.tsx @@ -1,7 +1,12 @@ -import { createContext, useState } from 'react'; -import { DEFAULT_NUMBER_OF_CHORD, } from '../config/constants'; -import { getListOfRandomChords, getRandomNoteFromCaged, isValidChordCountList } from '../services'; -import { CagedType, Chord } from '../config'; +import { createContext, useCallback, useEffect, useState } from "react"; +import { DEFAULT_NUMBER_OF_CHORD } from "../config/constants"; +import { + getListOfRandomChords, + getRandomNoteFromCaged, + isValidChordCountList, +} from "../services"; +import { CagedType, Chord } from "../config"; +import { useSpeedContext } from "../hooks"; interface ChordSettingsContextProps { chords: Chord[]; @@ -13,13 +18,27 @@ interface ChordSettingsContextProps { toggleShapeVisible: () => void; } -export const ChordSettingsContext = createContext(undefined); +export const ChordSettingsContext = createContext< + ChordSettingsContextProps | undefined +>(undefined); -export const ChordSettingsContextProvider = ({ children }: { children: React.ReactNode }) => { - const [chords, setChords] = useState(getListOfRandomChords(DEFAULT_NUMBER_OF_CHORD)); - const [numberOfChordDisplayed, setNumberOfChordDisplayed] = useState(DEFAULT_NUMBER_OF_CHORD); +export const ChordSettingsContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [chords, setChords] = useState( + getListOfRandomChords(DEFAULT_NUMBER_OF_CHORD) + ); + const [numberOfChordDisplayed, setNumberOfChordDisplayed] = useState( + DEFAULT_NUMBER_OF_CHORD + ); const [isShapeVisible, setIsShapeVisible] = useState(false); - const [cagedPosition, setCagedPosition] = useState(getRandomNoteFromCaged()); + const [cagedPosition, setCagedPosition] = useState( + getRandomNoteFromCaged() + ); + + const { speed, secondsElapsed, resetSecondsElapsed } = useSpeedContext(); const changeNumberOfChordDisplayed = (step: number) => { setNumberOfChordDisplayed((prevState: number) => { @@ -35,10 +54,22 @@ export const ChordSettingsContextProvider = ({ children }: { children: React.Rea setIsShapeVisible((prevState: boolean) => !prevState); }; - const getRandomChordsOnClick = () => { + const getRandomChordsOnClick = useCallback(() => { setChords(getListOfRandomChords(numberOfChordDisplayed)); setCagedPosition(getRandomNoteFromCaged()); - }; + }, [numberOfChordDisplayed]); + + useEffect(() => { + if (speed && speed / 1000 === secondsElapsed) { + getRandomChordsOnClick(); + resetSecondsElapsed(); + } + }, [getRandomChordsOnClick, resetSecondsElapsed, secondsElapsed, speed]); + + useEffect(() => { + resetSecondsElapsed(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [speed]); return ( void; } -export const NoteSettingsContext = createContext(undefined); +export const NoteSettingsContext = createContext< + NoteSettingsContextProps | undefined +>(undefined); -export const NoteSettingsContextProvider = ({ children }: { children: React.ReactNode }) => { - const [notes, setNotes] = useState(getListOfRandomNotes(DEFAULT_NUMBER_OF_NOTE)); - const [numberOfNoteDisplayed, setNumberOfNoteDisplayed] = useState(DEFAULT_NUMBER_OF_NOTE); +export const NoteSettingsContextProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [notes, setNotes] = useState( + getListOfRandomNotes(DEFAULT_NUMBER_OF_NOTE) + ); + const [numberOfNoteDisplayed, setNumberOfNoteDisplayed] = useState( + DEFAULT_NUMBER_OF_NOTE + ); const [isStringVisible, setIsStringVisible] = useState(false); - const [guitarString, setGuitarString] = useState(getRandomString()); + const [guitarString, setGuitarString] = useState( + getRandomString() + ); + + const { speed, secondsElapsed, resetSecondsElapsed } = useSpeedContext(); const changeNumberOfNoteDisplayed = (step: number) => { setNumberOfNoteDisplayed((prevState: number) => { @@ -35,10 +54,22 @@ export const NoteSettingsContextProvider = ({ children }: { children: React.Reac setIsStringVisible((prevState: boolean) => !prevState); }; - const getRandomNotesOnClick = () => { + const getRandomNotesOnClick = useCallback(() => { setNotes(getListOfRandomNotes(numberOfNoteDisplayed)); setGuitarString(getRandomString()); - }; + }, [numberOfNoteDisplayed]); + + useEffect(() => { + if (speed && speed / 1000 === secondsElapsed) { + getRandomNotesOnClick(); + resetSecondsElapsed(); + } + }, [getRandomNotesOnClick, resetSecondsElapsed, secondsElapsed, speed]); + + useEffect(() => { + resetSecondsElapsed(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [speed]); return ( void, + secondsElapsed: number, + setSecondsElapsed: Dispatch>, + resetSecondsElapsed: () => void, +} + +export const SpeedContext = createContext(undefined); + +export const SpeedContextProvider = ({ children }: { children: React.ReactNode }) => { + const [speed, setSpeed] = useState(); + const [secondsElapsed, setSecondsElapsed] = useState(0); + + const setCurrentSpeed = (speed?: Speed) => { + setSpeed(speed); + } + + const resetSecondsElapsed = () => { + setSecondsElapsed(0); + } + + useEffect(() => { + if (speed) { + const stepInterval = setInterval(() => { + setSecondsElapsed((prevState: number) => prevState + 1); + }, 1000); + + return () => clearInterval(stepInterval); + } + }, [secondsElapsed, setSecondsElapsed, speed]); + + return ( + + {children} + + ); +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 61c262f..3ba731b 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -1,2 +1,3 @@ export * from './useNoteSettingsContext'; export * from './useChordSettingsContext'; +export * from './useSpeedContext'; diff --git a/src/hooks/useSpeedContext.ts b/src/hooks/useSpeedContext.ts new file mode 100644 index 0000000..fe9b945 --- /dev/null +++ b/src/hooks/useSpeedContext.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { SpeedContext } from "../contexts/SpeedContext"; + +export const useSpeedContext = () => { + const context = useContext(SpeedContext); + if (!context) { + throw new Error('useSpeedContext must be used within a SpeedContextProvider'); + } + return context; +}; diff --git a/src/pages/ChordsPage.tsx b/src/pages/ChordsPage.tsx index af5859f..861441b 100644 --- a/src/pages/ChordsPage.tsx +++ b/src/pages/ChordsPage.tsx @@ -1,10 +1,16 @@ -import { ChordsList, ChordsSettings } from "../components"; +import { ChordsList, ChordsSettings, TimerCue } from "../components"; -import { useChordSettingsContext } from "../hooks"; +import { useChordSettingsContext, useSpeedContext } from "../hooks"; export const ChordsPage = () => { const { chords, isShapeVisible, cagedPosition, getRandomChordsOnClick } = useChordSettingsContext(); + const { resetSecondsElapsed } = useSpeedContext(); + + const handleChordsListOnClick = () => { + getRandomChordsOnClick(); + resetSecondsElapsed(); + } const ShapeDecorator: React.ReactNode = (
@@ -19,8 +25,9 @@ export const ChordsPage = () => { +
diff --git a/src/pages/NotesPage.tsx b/src/pages/NotesPage.tsx index fe4d6df..bbc4837 100644 --- a/src/pages/NotesPage.tsx +++ b/src/pages/NotesPage.tsx @@ -1,8 +1,14 @@ -import { NotesList, NotesSettings } from '../components'; -import { useNoteSettingsContext } from '../hooks'; +import { NotesList, NotesSettings, TimerCue } from '../components'; +import { useNoteSettingsContext, useSpeedContext } from '../hooks'; export const NotesPage = () => { const { notes, isStringVisible, guitarString, getRandomNotesOnClick } = useNoteSettingsContext(); + const { resetSecondsElapsed } = useSpeedContext(); + + const handleNotesListOnClick = () => { + getRandomNotesOnClick(); + resetSecondsElapsed(); + } const GuitarStringDecorator: React.ReactNode = (
{guitarString}
@@ -12,7 +18,8 @@ export const NotesPage = () => {
- + +