Skip to content

Commit

Permalink
22 add timer visual cue (#23)
Browse files Browse the repository at this point in the history
* Save.

* Visual cue - not right

* Sync intervals

* Fix visual cue
  • Loading branch information
cyrilgourgouillon committed Nov 28, 2023
1 parent 2eef331 commit 6c76db7
Show file tree
Hide file tree
Showing 13 changed files with 236 additions and 67 deletions.
49 changes: 33 additions & 16 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand All @@ -12,28 +13,44 @@ const App = () => {
<ChakraProvider>
<div className="h-screen flex flex-col items-center justify-between">
{selectedPage === AppPages.notes && (
<Button variant="ghost" colorScheme="purple" onClick={() => setSelectedPage(AppPages.chords)}>
<Button
variant="ghost"
colorScheme="purple"
onClick={() => setSelectedPage(AppPages.chords)}
>
Switch to chords
</Button>
)}
{selectedPage === AppPages.chords && (
<Button variant="ghost" colorScheme="blue" onClick={() => setSelectedPage(AppPages.notes)}>
<Button
variant="ghost"
colorScheme="blue"
onClick={() => setSelectedPage(AppPages.notes)}
>
Switch to notes
</Button>
)}
{selectedPage === AppPages.notes && (
<NoteSettingsContextProvider>
<NotesPage />
</NoteSettingsContextProvider>
<SpeedContextProvider>
<NoteSettingsContextProvider>
<NotesPage />
</NoteSettingsContextProvider>
</SpeedContextProvider>
)}
{selectedPage === AppPages.chords && (
<ChordSettingsContextProvider>
<ChordsPage />
</ChordSettingsContextProvider>
<SpeedContextProvider>
<ChordSettingsContextProvider>
<ChordsPage />
</ChordSettingsContextProvider>
</SpeedContextProvider>
)}
<div className="mb-5">
{'Made with ❤️ by '}
<a href="https://github.com/cyrilgourgouillon" target="_blank" className="text-red-700">
{"Made with ❤️ by "}
<a
href="https://github.com/cyrilgourgouillon"
target="_blank"
className="text-red-700"
>
Cyril Gourgouillon
</a>
</div>
Expand Down
32 changes: 11 additions & 21 deletions src/components/AutoSkipper.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,37 @@
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<Speed | undefined>(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 (
<>
<ButtonGroup isAttached className='flex justify-center'>
<ButtonGroup isAttached className="flex justify-center">
{speeds.map((s, i) => (
<IconButton
key={i}
aria-label="minus"
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)}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/ChordsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export const ChordsSettings = () => {
>
Toggle shape complexity
</Button>
<AutoSkipper onSkip={getRandomChordsOnClick} />
<AutoSkipper />
</div>
</PopoverBody>
</PopoverContent>
Expand Down
2 changes: 1 addition & 1 deletion src/components/NotesSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const NotesSettings = () => {
<Button variant="outline" leftIcon={<MdBuild />} onClick={toggleStringVisible}>
Toggle string complexity
</Button>
<AutoSkipper onSkip={getRandomNotesOnClick} />
<AutoSkipper />
</div>
</PopoverBody>
</PopoverContent>
Expand Down
25 changes: 25 additions & 0 deletions src/components/TimerCue.tsx
Original file line number Diff line number Diff line change
@@ -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(<GoDotFill />);
}
for (let i = 0; i < numberOfDots - secondsElapsed; i++) {
dots.push(<GoDot />);
}

if (speed === Speed.rush) {
return <></>;
}

return (
<div className="flex mb-3 text-2xl text-blue-600">{dots.map((d, i) => <div key={i}>{d}</div>)}</div>
);
};
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './ChordsList';
export * from './AutoSkipper';
export * from './NotesSettings'
export * from './ChordsSettings';
export * from './TimerCue';
53 changes: 42 additions & 11 deletions src/contexts/ChordContext.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -13,13 +18,27 @@ interface ChordSettingsContextProps {
toggleShapeVisible: () => void;
}

export const ChordSettingsContext = createContext<ChordSettingsContextProps | undefined>(undefined);
export const ChordSettingsContext = createContext<
ChordSettingsContextProps | undefined
>(undefined);

export const ChordSettingsContextProvider = ({ children }: { children: React.ReactNode }) => {
const [chords, setChords] = useState<Chord[]>(getListOfRandomChords(DEFAULT_NUMBER_OF_CHORD));
const [numberOfChordDisplayed, setNumberOfChordDisplayed] = useState(DEFAULT_NUMBER_OF_CHORD);
export const ChordSettingsContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [chords, setChords] = useState<Chord[]>(
getListOfRandomChords(DEFAULT_NUMBER_OF_CHORD)
);
const [numberOfChordDisplayed, setNumberOfChordDisplayed] = useState(
DEFAULT_NUMBER_OF_CHORD
);
const [isShapeVisible, setIsShapeVisible] = useState(false);
const [cagedPosition, setCagedPosition] = useState<CagedType>(getRandomNoteFromCaged());
const [cagedPosition, setCagedPosition] = useState<CagedType>(
getRandomNoteFromCaged()
);

const { speed, secondsElapsed, resetSecondsElapsed } = useSpeedContext();

const changeNumberOfChordDisplayed = (step: number) => {
setNumberOfChordDisplayed((prevState: number) => {
Expand All @@ -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 (
<ChordSettingsContext.Provider
Expand Down
53 changes: 42 additions & 11 deletions src/contexts/NoteContext.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { createContext, useState } from 'react';
import { DEFAULT_NUMBER_OF_NOTE } from '../config/constants';
import { getListOfRandomNotes, getRandomString, isValidNoteCountList } from '../services';
import { GuitarString, Note } from '../config';
import { createContext, useCallback, useEffect, useState } from "react";
import { DEFAULT_NUMBER_OF_NOTE } from "../config/constants";
import {
getListOfRandomNotes,
getRandomString,
isValidNoteCountList,
} from "../services";
import { GuitarString, Note } from "../config";
import { useSpeedContext } from "../hooks";

interface NoteSettingsContextProps {
notes: Note[];
Expand All @@ -13,13 +18,27 @@ interface NoteSettingsContextProps {
toggleStringVisible: () => void;
}

export const NoteSettingsContext = createContext<NoteSettingsContextProps | undefined>(undefined);
export const NoteSettingsContext = createContext<
NoteSettingsContextProps | undefined
>(undefined);

export const NoteSettingsContextProvider = ({ children }: { children: React.ReactNode }) => {
const [notes, setNotes] = useState<Note[]>(getListOfRandomNotes(DEFAULT_NUMBER_OF_NOTE));
const [numberOfNoteDisplayed, setNumberOfNoteDisplayed] = useState(DEFAULT_NUMBER_OF_NOTE);
export const NoteSettingsContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [notes, setNotes] = useState<Note[]>(
getListOfRandomNotes(DEFAULT_NUMBER_OF_NOTE)
);
const [numberOfNoteDisplayed, setNumberOfNoteDisplayed] = useState(
DEFAULT_NUMBER_OF_NOTE
);
const [isStringVisible, setIsStringVisible] = useState(false);
const [guitarString, setGuitarString] = useState<GuitarString>(getRandomString());
const [guitarString, setGuitarString] = useState<GuitarString>(
getRandomString()
);

const { speed, secondsElapsed, resetSecondsElapsed } = useSpeedContext();

const changeNumberOfNoteDisplayed = (step: number) => {
setNumberOfNoteDisplayed((prevState: number) => {
Expand All @@ -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 (
<NoteSettingsContext.Provider
Expand Down
49 changes: 49 additions & 0 deletions src/contexts/SpeedContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Dispatch, SetStateAction, createContext, useEffect, useState } from 'react';
import { Speed } from '../config';

interface SpeedContextProps {
speed: Speed | undefined,
setCurrentSpeed: (speed?: Speed) => void,
secondsElapsed: number,
setSecondsElapsed: Dispatch<SetStateAction<number>>,
resetSecondsElapsed: () => void,
}

export const SpeedContext = createContext<SpeedContextProps | undefined>(undefined);

export const SpeedContextProvider = ({ children }: { children: React.ReactNode }) => {
const [speed, setSpeed] = useState<Speed>();
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 (
<SpeedContext.Provider
value={{
speed,
setCurrentSpeed,
secondsElapsed,
setSecondsElapsed,
resetSecondsElapsed,
}}
>
{children}
</SpeedContext.Provider>
);
};
1 change: 1 addition & 0 deletions src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './useNoteSettingsContext';
export * from './useChordSettingsContext';
export * from './useSpeedContext';
10 changes: 10 additions & 0 deletions src/hooks/useSpeedContext.ts
Original file line number Diff line number Diff line change
@@ -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;
};
Loading

0 comments on commit 6c76db7

Please sign in to comment.