diff --git a/src/App.tsx b/src/App.tsx index c6438cb..7d80eb8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,9 @@ -import { Button, ChakraProvider } from "@chakra-ui/react"; -import { ChordsPage, NotesPage } from "./pages"; -import { AppPages } from "./config"; -import { useState } from "react"; +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'; const App = () => { const [selectedPage, setSelectedPage] = useState(AppPages.notes); @@ -19,15 +21,19 @@ const App = () => { Switch to notes )} - {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 486970e..22b71ce 100644 --- a/src/components/AutoSkipper.tsx +++ b/src/components/AutoSkipper.tsx @@ -1,12 +1,11 @@ -import { ButtonGroup, Button, 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 { useEffect, useState } from 'react'; +import { Speed, speeds } from '../config'; +import { getSpeedColor, getSpeedIcon } from '../services'; export const AutoSkipper = ({ onSkip }: { onSkip: () => void }) => { const toast = useToast(); const [speed, setSpeed] = useState(undefined); - const [intervalId, setIntervalId] = useState(); useEffect(() => { if (speed) { @@ -14,49 +13,37 @@ export const AutoSkipper = ({ onSkip }: { onSkip: () => void }) => { onSkip(); }, speed); - setIntervalId(skipInterval); - return () => clearInterval(skipInterval); } }, [onSkip, speed]); - const handleStopAutoSkipper = () => { - clearInterval(intervalId); - setSpeed(undefined); - }; - const triggerToast = (skipDuration: Speed) => { toast({ - title: `Auto-skip every ${skipDuration / 1000} seconds`, - status: "info", + title: `Auto skip every ${skipDuration / 1000}"`, + status: 'info', duration: 1000, }); }; return ( <> - - + {speeds.map((s, i) => ( { - triggerToast(s); - setSpeed(s); + if (speed === s) { + setSpeed(undefined); + } else { + triggerToast(s); + setSpeed(s); + } }} + variant={'outline'} isActive={speed === s} - colorScheme={speed === s ? getSpeedColor(s) : ""} + colorScheme={getSpeedColor(s)} /> ))} diff --git a/src/components/ChordsList.tsx b/src/components/ChordsList.tsx index 1945531..30b6aa0 100644 --- a/src/components/ChordsList.tsx +++ b/src/components/ChordsList.tsx @@ -1,19 +1,22 @@ -import { Chord } from "../config"; -import { chordToString } from "../services/chordService"; +import { Chord } from '../config'; +import { chordToString } from '../services/chordService'; export const ChordsList = ({ chords, ShapeDecorator, + onClick, }: { chords: Chord[]; ShapeDecorator?: React.ReactNode; + onClick?: () => void; }) => { return ( <> -
- {ShapeDecorator} -
-
+
{ShapeDecorator}
+
{chords.map((chord: Chord, index: number) => (
{chordToString(chord)}
))} diff --git a/src/components/ChordsSettings.tsx b/src/components/ChordsSettings.tsx new file mode 100644 index 0000000..e7bd9e7 --- /dev/null +++ b/src/components/ChordsSettings.tsx @@ -0,0 +1,66 @@ +import { + ButtonGroup, + IconButton, + Button, + PopoverContent, + Popover, + PopoverArrow, + PopoverBody, + PopoverTrigger, +} from "@chakra-ui/react"; +import { FaMinus, FaPlus } from "react-icons/fa"; +import { MdBuild } from "react-icons/md"; +import { CHORDS_LIST_MIN, CHORDS_LIST_MAX } from "../config/constants"; +import { AutoSkipper } from "./AutoSkipper"; +import { useChordSettingsContext } from "../hooks"; + +export const ChordsSettings = () => { + const { + numberOfChordDisplayed, + getRandomChordsOnClick, + changeNumberOfChordDisplayed, + toggleShapeVisible, + } = useChordSettingsContext(); + + return ( + + + } aria-label={"settings"}> + Settings + + + + + +
+ + } + onClick={() => changeNumberOfChordDisplayed(-1)} + disabled={numberOfChordDisplayed === CHORDS_LIST_MIN} + /> + + } + onClick={() => changeNumberOfChordDisplayed(1)} + disabled={numberOfChordDisplayed === CHORDS_LIST_MAX} + /> + + + +
+
+
+
+ ); +}; diff --git a/src/components/NotesList.tsx b/src/components/NotesList.tsx index 8994108..26bdb98 100644 --- a/src/components/NotesList.tsx +++ b/src/components/NotesList.tsx @@ -3,16 +3,21 @@ import { Note } from "../config"; export const NotesList = ({ notes, GuitarStringDecorator, + onClick, }: { notes: Note[]; GuitarStringDecorator?: React.ReactNode; + onClick?: () => void; }) => { return ( <>
{GuitarStringDecorator}
-
+
{notes.map((note: Note, index: number) => (
{note}
))} diff --git a/src/components/NotesSettings.tsx b/src/components/NotesSettings.tsx new file mode 100644 index 0000000..0a9146f --- /dev/null +++ b/src/components/NotesSettings.tsx @@ -0,0 +1,56 @@ +import { + ButtonGroup, + IconButton, + Button, + PopoverContent, + Popover, + PopoverArrow, + PopoverBody, + PopoverTrigger, +} from '@chakra-ui/react'; +import { FaMinus, FaPlus } from 'react-icons/fa'; +import { MdBuild } from 'react-icons/md'; +import { NOTES_LIST_MIN, NOTES_LIST_MAX } from '../config/constants'; +import { AutoSkipper } from './AutoSkipper'; +import { useNoteSettingsContext } from '../hooks'; + +export const NotesSettings = () => { + const { numberOfNoteDisplayed, getRandomNotesOnClick, changeNumberOfNoteDisplayed, toggleStringVisible } = + useNoteSettingsContext(); + + return ( + + + } aria-label={'settings'}> + Settings + + + + + +
+ + } + onClick={() => changeNumberOfNoteDisplayed(-1)} + disabled={numberOfNoteDisplayed === NOTES_LIST_MIN} + /> + + } + onClick={() => changeNumberOfNoteDisplayed(1)} + disabled={numberOfNoteDisplayed === NOTES_LIST_MAX} + /> + + + +
+
+
+
+ ); +}; diff --git a/src/components/index.ts b/src/components/index.ts index 16198b8..149e1be 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,2 +1,5 @@ export * from './NotesList'; export * from './ChordsList'; +export * from './AutoSkipper'; +export * from './NotesSettings' +export * from './ChordsSettings'; diff --git a/src/contexts/ChordContext.tsx b/src/contexts/ChordContext.tsx new file mode 100644 index 0000000..cb82f72 --- /dev/null +++ b/src/contexts/ChordContext.tsx @@ -0,0 +1,58 @@ +import { createContext, useState } from 'react'; +import { DEFAULT_NUMBER_OF_CHORD, } from '../config/constants'; +import { getListOfRandomChords, getRandomNoteFromCaged, isValidChordCountList } from '../services'; +import { CagedType, Chord } from '../config'; + +interface ChordSettingsContextProps { + chords: Chord[]; + numberOfChordDisplayed: number; + isShapeVisible: boolean; + cagedPosition: CagedType; + getRandomChordsOnClick: () => void; + changeNumberOfChordDisplayed: (step: number) => void; + toggleShapeVisible: () => void; +} + +export const ChordSettingsContext = createContext(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); + const [isShapeVisible, setIsShapeVisible] = useState(false); + const [cagedPosition, setCagedPosition] = useState(getRandomNoteFromCaged()); + + const changeNumberOfChordDisplayed = (step: number) => { + setNumberOfChordDisplayed((prevState: number) => { + const value = prevState + step; + if (isValidChordCountList(value)) { + return value; + } + return prevState; + }); + }; + + const toggleShapeVisible = () => { + setIsShapeVisible((prevState: boolean) => !prevState); + }; + + const getRandomChordsOnClick = () => { + setChords(getListOfRandomChords(numberOfChordDisplayed)); + setCagedPosition(getRandomNoteFromCaged()); + }; + + return ( + + {children} + + ); +}; diff --git a/src/contexts/NoteContext.tsx b/src/contexts/NoteContext.tsx new file mode 100644 index 0000000..c847a75 --- /dev/null +++ b/src/contexts/NoteContext.tsx @@ -0,0 +1,58 @@ +import { createContext, useState } from 'react'; +import { DEFAULT_NUMBER_OF_NOTE } from '../config/constants'; +import { getListOfRandomNotes, getRandomString, isValidNoteCountList } from '../services'; +import { GuitarString, Note } from '../config'; + +interface NoteSettingsContextProps { + notes: Note[]; + numberOfNoteDisplayed: number; + isStringVisible: boolean; + guitarString: GuitarString; + getRandomNotesOnClick: () => void; + changeNumberOfNoteDisplayed: (step: number) => void; + toggleStringVisible: () => void; +} + +export const NoteSettingsContext = createContext(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); + const [isStringVisible, setIsStringVisible] = useState(false); + const [guitarString, setGuitarString] = useState(getRandomString()); + + const changeNumberOfNoteDisplayed = (step: number) => { + setNumberOfNoteDisplayed((prevState: number) => { + const value = prevState + step; + if (isValidNoteCountList(value)) { + return value; + } + return prevState; + }); + }; + + const toggleStringVisible = () => { + setIsStringVisible((prevState: boolean) => !prevState); + }; + + const getRandomNotesOnClick = () => { + setNotes(getListOfRandomNotes(numberOfNoteDisplayed)); + setGuitarString(getRandomString()); + }; + + return ( + + {children} + + ); +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..61c262f --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './useNoteSettingsContext'; +export * from './useChordSettingsContext'; diff --git a/src/hooks/useChordSettingsContext.ts b/src/hooks/useChordSettingsContext.ts new file mode 100644 index 0000000..bbd403d --- /dev/null +++ b/src/hooks/useChordSettingsContext.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { ChordSettingsContext } from "../contexts/ChordContext"; + +export const useChordSettingsContext = () => { + const context = useContext(ChordSettingsContext); + if (!context) { + throw new Error('useChordSettingsContext must be used within a ChordSettingsContextProvider'); + } + return context; +}; diff --git a/src/hooks/useNoteSettingsContext.ts b/src/hooks/useNoteSettingsContext.ts new file mode 100644 index 0000000..d15ad6d --- /dev/null +++ b/src/hooks/useNoteSettingsContext.ts @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { NoteSettingsContext } from "../contexts/NoteContext"; + +export const useNoteSettingsContext = () => { + const context = useContext(NoteSettingsContext); + if (!context) { + throw new Error('useNoteSettingsContext must be used within a NoteSettingsContextProvider'); + } + return context; +}; diff --git a/src/pages/ChordsPage.tsx b/src/pages/ChordsPage.tsx index 15fce19..af5859f 100644 --- a/src/pages/ChordsPage.tsx +++ b/src/pages/ChordsPage.tsx @@ -1,55 +1,10 @@ -import { useState } from "react"; -import { Button, ButtonGroup, IconButton } from "@chakra-ui/react"; +import { ChordsList, ChordsSettings } from "../components"; -import { FaPlus, FaMinus } from "react-icons/fa"; - -import { ChordsList } from "../components"; - -import { - getListOfRandomChords, - getRandomNoteFromCaged, - isValidChordCountList, -} from "../services"; -import { - DEFAULT_NUMBER_OF_CHORD, - CHORDS_LIST_MAX, - CHORDS_LIST_MIN, -} from "../config/constants"; -import { CagedType } from "../config"; -import { MdBuild } from "react-icons/md"; -import { AutoSkipper } from "../components/AutoSkipper"; +import { useChordSettingsContext } from "../hooks"; export const ChordsPage = () => { - const [chords, setChords] = useState( - getListOfRandomChords(DEFAULT_NUMBER_OF_CHORD) - ); - const [isShapeVisible, setIsShapeVisible] = useState(false); - const [cagedPosition, setCagedPosition] = useState( - getRandomNoteFromCaged() - ); - - const [numberOfChordDisplayed, setNumberOfChordDisplayed] = useState( - DEFAULT_NUMBER_OF_CHORD - ); - - const toggleCagedVisible = () => { - setIsShapeVisible((prevState: boolean) => !prevState); - }; - - const handleGetRandomChordsOnClick = () => { - setChords(getListOfRandomChords(numberOfChordDisplayed)); - setCagedPosition(getRandomNoteFromCaged()); - }; - - const handleChangeNumberOfChordDisplayed = (step: number) => { - setNumberOfChordDisplayed((prevState: number) => { - const value = prevState + step; - if (isValidChordCountList(value)) { - return value; - } - return prevState; - }); - }; + const { chords, isShapeVisible, cagedPosition, getRandomChordsOnClick } = + useChordSettingsContext(); const ShapeDecorator: React.ReactNode = (
@@ -61,34 +16,12 @@ export const ChordsPage = () => {
- -
- - } - onClick={() => handleChangeNumberOfChordDisplayed(-1)} - disabled={numberOfChordDisplayed === CHORDS_LIST_MIN} - /> - - } - onClick={() => handleChangeNumberOfChordDisplayed(1)} - disabled={numberOfChordDisplayed === CHORDS_LIST_MAX} - /> - - - -
+ +
diff --git a/src/pages/NotesPage.tsx b/src/pages/NotesPage.tsx index 14781a2..fe4d6df 100644 --- a/src/pages/NotesPage.tsx +++ b/src/pages/NotesPage.tsx @@ -1,96 +1,19 @@ -import { useState } from "react"; -import { Button, ButtonGroup, IconButton } from "@chakra-ui/react"; - -import { MdBuild } from "react-icons/md"; -import { FaPlus, FaMinus } from "react-icons/fa"; - -import { GuitarString, Note } from "../config"; -import { NotesList } from "../components"; - -import { - getListOfRandomNotes, - getRandomString, - isValidNoteCountList, -} from "../services"; -import { - DEFAULT_NUMBER_OF_NOTE, - NOTES_LIST_MAX, - NOTES_LIST_MIN, -} from "../config/constants"; -import { AutoSkipper } from "../components/AutoSkipper"; +import { NotesList, NotesSettings } from '../components'; +import { useNoteSettingsContext } from '../hooks'; export const NotesPage = () => { - const [notes, setNotes] = useState( - getListOfRandomNotes(DEFAULT_NUMBER_OF_NOTE) - ); - const [isStringVisible, setIsStringVisible] = useState(false); - const [guitarString, setGuitarString] = useState( - getRandomString() - ); - const [numberOfNoteDisplayed, setNumberOfNoteDisplayed] = useState( - DEFAULT_NUMBER_OF_NOTE - ); - - const handleGetRandomNotesOnClick = () => { - setNotes(getListOfRandomNotes(numberOfNoteDisplayed)); - setGuitarString(getRandomString()); - }; - - const toggleStringVisible = () => { - setIsStringVisible((prevState: boolean) => !prevState); - }; - - const handleChangeNumberOfNoteDisplayed = (step: number) => { - setNumberOfNoteDisplayed((prevState: number) => { - const value = prevState + step; - if (isValidNoteCountList(value)) { - return value; - } - return prevState; - }); - }; + const { notes, isStringVisible, guitarString, getRandomNotesOnClick } = useNoteSettingsContext(); const GuitarStringDecorator: React.ReactNode = ( -
- {guitarString} -
+
{guitarString}
); return (
- -
- - } - onClick={() => handleChangeNumberOfNoteDisplayed(-1)} - disabled={numberOfNoteDisplayed === NOTES_LIST_MIN} - /> - - } - onClick={() => handleChangeNumberOfNoteDisplayed(1)} - disabled={numberOfNoteDisplayed === NOTES_LIST_MAX} - /> - - - -
+ +
diff --git a/src/services/speedService.tsx b/src/services/speedService.tsx index 1f10eb3..323c150 100644 --- a/src/services/speedService.tsx +++ b/src/services/speedService.tsx @@ -34,5 +34,7 @@ export const getSpeedColor = (speed: Speed) => { return "pink"; case Speed.rush: return "purple"; + default: + return "grey"; } };