add drum-machine-react

This commit is contained in:
Trent Palmer 2020-05-26 07:01:12 -07:00
parent 06590ce07f
commit b659e8549a
55 changed files with 18652 additions and 0 deletions

23
drum-machine-react/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

View File

@ -0,0 +1,68 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting
### Analyzing the Bundle Size
This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size
### Making a Progressive Web App
This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app
### Advanced Configuration
This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration
### Deployment
This section has moved here: https://facebook.github.io/create-react-app/docs/deployment
### `npm run build` fails to minify
This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify

15307
drum-machine-react/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
{
"name": "drum-machine-react",
"version": "0.1.0",
"private": true,
"homepage": "https://trentspalmer.github.io/fcc-challenges/drum-machine-react/build",
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.28",
"@fortawesome/free-solid-svg-icons": "^5.13.0",
"@fortawesome/react-fontawesome": "^0.1.9",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-redux": "^7.2.0",
"react-scripts": "3.4.1",
"redux": "^4.0.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="#" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>
Drum Machine - Build a Drum Machine - Front End Libraries Projects
</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">
<div>
<script src="https://cdn.freecodecamp.org/testable-projects-fcc/v1/bundle.js"></script>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,42 @@
.App {
text-align: center;
}
.attachment-full {
position: absolute;
right: 0px;
height: 149px;
width: 149px;
}
.githubLabel {
background-color: #00293C;
border: none;
width: 149px;
height: 0px;
position: absolute;
right: 0px;
margin-top: -10px;
}
@media (orientation: portrait) {
.attachment-full {
height: 99px;
width: 99px;
}
.githubLabel {
width: 99px;
}
}
@media (orientation: landscape) and (max-height: 400px) {
.attachment-full {
height: 99px;
width: 99px;
}
.githubLabel {
width: 99px;
}
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import './App.css';
import DrumMachine from "./DrumMachine";
class App extends React.Component {
render() {
return (
<div className="App">
<a href="https://github.com/TrentSPalmer/fcc-challenges/tree/gh-pages/drum-machine-react" className="githubLabel" target="_blank" rel="noopener noreferrer">
<img src="https://github.blog/wp-content/uploads/2008/12/forkme_right_white_ffffff.png?resize=149%2C149"
className="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1"></img>
</a>
<DrumMachine />
</div>
);
}
}
export default App;

View File

@ -0,0 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,147 @@
#display-middle {
flex-basis: 62%;
width: 100%;
display: flex;
flex-direction: row;
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10+ and Edge */
user-select: none; /* Standard syntax */
}
#display-middle-left {
flex-basis: 55%;
display: flex;
flex-direction: row;
}
#display-middle-right {
flex-basis: 45%;
display: flex;
flex-direction: row;
}
#display-middle-left-a, #display-middle-left-c {
flex-basis: 5%;
}
#display-middle-left-b {
flex-basis: 90%;
display: flex;
flex-direction: row;
justify-content: space-around;
}
#display-middle-right-c {
flex-basis: 12%;
}
#display-middle-right-c {
min-width: 15px;
}
#display-middle-right-e, #display-middle-right-g {
flex-basis: 5%;
}
#display-middle-right-e {
min-width: 50px;
}
#display-middle-right-f {
flex-basis: 20%;
display: flex;
flex-direction: row;
}
.volumeToolTipContainer {
position: relative;
width: 0;
height: 0;
top: 105%;
right: 25px;
}
.volumeToolTip {
background-color: var(--global-first-color);
width: 50px;
font-size: x-large;
font-style: oblique;
border-color: black !important;
border-width: 2px !important;
border-style: solid;
border-radius: 6px;
text-align: center;
}
#menuScrollTip {
font-size: x-large;
height: 16px;
}
@media (orientation: portrait) {
#display-middle {
flex-basis: 80%;
height: 100%;
width: 100%;
display: flex;
flex-direction: column-reverse;
justify-content: space-around;
}
#display-middle-left {
flex-basis: unset;
}
#display-middle-right {
flex-basis: unset;
justify-content: space-around;
width: 80%;
margin-left: auto;
margin-right: auto;
}
#display-middle-right-f, #display-middle-right-g, #volumeTip, #menuScrollTip {
display: none;
}
#display-middle-right-a, #display-middle-right-c, #display-middle-right-e {
display: none;
}
}
@media (orientation: portrait) and (max-device-width: 750px) {
#display-middle {
margin-top: 10px;
}
}
@media (orientation: landscape) and (max-device-width: 1024px) {
#display-middle-left {
flex-basis: 65%;
}
#display-middle-right {
flex-basis: 35%;
justify-content: space-around;
margin-left: auto;
margin-right: auto;
}
#display-middle-right-f, #display-middle-right-g, #volumeTip, #menuScrollTip {
display: none;
}
#display-middle-right-a, #display-middle-right-c, #display-middle-right-e {
display: none;
}
}
@media (orientation: landscape) and (max-device-height: 767px) {
#display-middle-left {
flex-basis: 60%;
}
#display-middle-right {
flex-basis: 40%;
}
}

View File

@ -0,0 +1,70 @@
import React from 'react';
import { connect } from "react-redux";
import './DisplayMiddle.css';
import DrumPadGrid from "./DrumPadGrid";
import SelectionLeft from "./SelectionLeft";
import SelectionRight from "./SelectionRight";
import VolumeContainer from "./VolumeContainer";
import SelectionMenu from "./SelectionMenu";
const mapStateToProps = (state) => ({ ...state });
class DisplayMiddle extends React.Component {
constructor(props) {
super(props);
this.state = {
showVolumeToolTip: false,
};
this.handleMouseOverOut = this.handleMouseOverOut.bind(this);
};
handleMouseOverOut(willShow) {
if (willShow !== this.state.showVolumeToolTip) {
this.setState({
showVolumeToolTip: willShow,
});
}
};
render() {
const selectionMenus = ['selectionMenu','volumeSelectionMenu','metronomeSelectionMenu'];
return (
<div id="display-middle">
<div id="display-middle-left">
<div id="display-middle-left-a">
</div>
<div id="display-middle-left-b">
{ this.props.drumPadGrid === 'drumPadGrid' && <DrumPadGrid /> }
{ selectionMenus.includes(this.props.drumPadGrid) && <SelectionMenu /> }
</div>
<div id="display-middle-left-c">
</div>
</div>
<div id="display-middle-right">
<div id="display-middle-right-a">
</div>
<SelectionLeft />
<div id="display-middle-right-c">
</div>
<SelectionRight />
<div id="display-middle-right-e">
</div>
<div id="display-middle-right-f" onMouseEnter={(event) => this.handleMouseOverOut(true)} onMouseLeave={(event) => this.handleMouseOverOut(false)}>
{
this.state.showVolumeToolTip && <div className="volumeToolTipContainer">
<div className="volumeToolTip">{this.props.volume}
</div>
</div>
}
<VolumeContainer />
</div>
<div id="display-middle-right-g">
</div>
</div>
</div>
);
}
}
export default connect(mapStateToProps)(DisplayMiddle);

View File

@ -0,0 +1,107 @@
@import 'globalCss.css';
#drum-machine {
background: var(--global-first-color);
height: 100vh;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
#display {
background-color: var(--global-second-color);
margin: auto;
height: 70vh;
min-height: 400px;
width: 80vw;
border-radius: 1rem;
display: flex;
flex-direction: column;
}
#display-top, #display-bottom {
flex-basis: 19%;
width: 100%;
}
#display-top {
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
font-size: x-large;
font-style: oblique;
}
#display-top p {
height: 10px;
margin-top: 0px;
}
#menuToolTip {
position: relative;
left: 10px;
top: 20px;
background-color: var(--global-first-color);
max-width: max-content;
padding: 0px 10px 0px 10px;
font-size: x-large;
font-style: oblique;
border-color: black !important;
border-width: 2px !important;
border-style: solid;
border-radius: 6px;
text-align: center;
}
@media (orientation: landscape) and (max-width: 1520px) {
#display {
width: 90vw;
}
}
@media (orientation: landscape) and (max-width: 1258px) {
#display {
width: 99vw;
}
}
@media (orientation: portrait) {
#display {
margin: auto;
height: 90vh;
width: 95vw;
min-width: unset;
min-height: unset;
}
#display-top {
flex-basis: 12%;
font-size: x-large;
}
#display-bottom {
flex-basis: 8%;
}
}
@media (orientation: portrait) and (max-device-width: 750px) {
#display-top {
font-size: large;
}
}
@media (orientation: landscape) and (max-device-width: 1024px) {
#display {
height: 90vh;
width: 90vw;
min-height: unset;
}
}
@media (orientation: landscape) and (max-height: 400px) {
#display-top {
font-size: large;
}
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import './DrumMachine.css';
import DisplayMiddle from "./DisplayMiddle";
const DrumMachine = () => {
return (
<div id="drum-machine">
<div id="display">
<div id="display-top">
</div>
<DisplayMiddle />
<div id="display-bottom">
</div>
</div>
</div>
);
}
export default DrumMachine;

View File

@ -0,0 +1,150 @@
#drum-pad-grid {
margin: auto;
background-color: var(--global-third-color);
border-radius: 1rem;
width: 54vh;
height: 45vh;
display: flex;
flex-direction: column;
justify-content: space-around;
padding: 0 0 2vh 2vh;
}
.drum-pad-row {
height: 33%;
display: flex;
flex-direction: row;
justify-content: space-around;
}
.drum-pad {
border-radius: 1rem;
width: 33%;
background-color: var(--global-fourth-color);
border: black;
border-width: 2px;
border-style: solid;
margin: 2vh 2vh 0 0;
text-align: center;
display: flex;
flex-direction: column;
justify-content: space-around;
font-size: x-large;
font-style: oblique;
}
.drum-pad:active {
background-color: var(--global-first-color);
}
.drum-pad > p {
margin-top: auto;
margin-bottom: auto;
}
.selectionMenu {
margin: auto;
background-color: var(--global-third-color);
border-radius: 5px;
width: 54vh;
height: 45vh;
display: flex;
flex-direction: column;
flex: 1;
overflow: auto;
scrollbar-color: var(--global-black-color) var(--global-second-color);
scrollbar-width: thin;
padding-top: 10px;
padding-bottom: 10px;
direction: rtl;
}
.selectionMenu::-webkit-scrollbar {
background-color: var(--global-third-color);
border-radius: 5px;
}
.selectionMenu::-webkit-scrollbar-thumb {
background-color: var(--global-black-color);
border-radius: 5px;
}
.selectionMenuItem {
min-height: min-content;
background-color: var(--global-fourth-color);
margin: 10px 20px 20px 10px;
padding: 15px;
border-radius: 1rem;
border-width: 2px;
border-style: solid;
text-align: center;
font-style: bold;
font-size: 1.6rem;
}
.selectionMenuItem:active {
background-color: var(--global-first-color);
}
.metronomeIcon {
/* increasing the third number lightens the black */
color: hsl(0, 0%, 11%);
}
@media (orientation: portrait) {
#drum-pad-grid {
min-width: unset;
min-height: unset;
}
#drum-pad-grid, .selectionMenu {
width: 72vw;
height: 60vw;
}
.selectionMenuItem {
padding: unset;
}
}
@media (orientation: portrait) and (max-device-width: 767px) {
#drum-pad-grid, .selectionMenu {
width: 84vw;
height: 70vw;
}
}
@media (orientation: portrait) and (max-device-width: 750px) {
#drum-pad-grid, .selectionMenu {
width: 72vw;
height: 60vw;
}
}
@media (orientation: landscape) and (max-device-width: 1024px) {
#drum-pad-grid {
min-width: unset;
min-height: unset;
}
#drum-pad-grid, .selectionMenu {
width: 72vh;
height: 60vh;
}
.selectionMenuItem {
padding: unset;
}
}
@media (orientation: landscape) and (max-device-height: 767px) {
#drum-pad-grid {
min-width: unset;
min-height: unset;
}
#drum-pad-grid, .selectionMenu {
width: 84vh;
height: 70vh;
}
}

View File

@ -0,0 +1,152 @@
import React from 'react';
import { connect } from "react-redux";
import { padsArray } from "./Globals";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
import './DrumPadGrid.css';
import { toggleMetronomeIsPlayingAction } from "./actions/toggleMetronomeIsPlayingAction";
import { shouldMetronomeRestartAction } from "./actions/shouldMetronomeRestartAction";
const mapStateToProps = (state) => ({ ...state });
const mapDispatchToProps = (dispatch) => ({
toggleMetronomeIsPlayingAction: (key,metronomeIsPlaying) => dispatch(toggleMetronomeIsPlayingAction(key,metronomeIsPlaying)),
shouldMetronomeRestartAction: (key,restartMetronome) => dispatch(shouldMetronomeRestartAction(key,restartMetronome)),
});
class DrumPadGrid extends React.Component {
constructor(props) {
super(props);
this.playSample = this.playSample.bind(this);
this.metronome = this.metronome.bind(this);
this.getVolume = this.getVolume.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
};
playSample(key) {
if (this.props.metronomeStatuses[key + 'isMetronome'] === 'false') {
const audioFileText = document.getElementById(key).src.slice(36);
document.getElementById('display-top').innerHTML = "<p>" + key +": " + audioFileText.replace(/\//g," ").replace(/-/g,"&#8209;") + "</p>";
const keyDuration = 1000;
const sound = document.getElementById(key);
const audioFile = this.props.samplesUrls[key];
const thisVolume = this.getVolume(key);
if (sound.currentTime === 0) {
sound.volume = thisVolume;
sound.play();
setTimeout(function(){
const soundDuration = Math.floor(sound.duration);
if (soundDuration > (keyDuration / 1000)) {
sound.currentTime = soundDuration;
}
}, keyDuration);
} else {
const newSound = new Audio('https://trentpalmer.org/drumsamples/' + audioFile);
newSound.volume = thisVolume;
newSound.play();
setTimeout(function(){
const newSoundDuration = Math.floor(newSound.duration);
if (newSoundDuration > (keyDuration / 1000)) {
newSound.currentTime = newSoundDuration;
}
}, keyDuration);
}
} else {
if (this.props.metronomePlayingStates[key + 'metronomeIsPlaying'] === false) {
this.props.toggleMetronomeIsPlayingAction(key,true);
this.metronome(key);
} else {
this.props.toggleMetronomeIsPlayingAction(key,false);
}
}
};
handleKeyPress(e) {
if ([81,87,69,65,83,68,90,88,67].includes(e.keyCode)) {
this.playSample(String.fromCharCode(e.keyCode));
} else if (e.keyCode === 86){
document.getElementById('volume').focus();
}
};
getVolume(key) {
const machineVolume = parseInt(sessionStorage.getItem('volume'));
const padVolumeOffSet = parseInt(sessionStorage.getItem(key + 'volume'));
const thisVolume = (machineVolume + padVolumeOffSet) / 100;
return thisVolume > 1 ? 1 : thisVolume < 0 ? 0 : thisVolume;
};
metronome(key) {
const audioFileText = document.getElementById(key).src.slice(36);
document.getElementById('display-top').innerHTML = "<p>" + key +": " + audioFileText.replace(/\//g," ").replace(/-/g,"&#8209;") + "</p>";
const keyDuration = this.props.metronomeTempos[key+'metronomeTempo'];
const sound = document.getElementById(key);
sound.volume = this.getVolume(key);
sound.play();
const self = this;
let refreshMetronome = setInterval(function(){
sound.pause();
sound.currentTime = 0;
sound.volume = self.getVolume(key);
sound.play();
if (self.props.shouldMetronomeRestart[key + 'restartMetronome']) {
self.props.shouldMetronomeRestartAction(key,false);
clearInterval(refreshMetronome);
self.metronome(key);
}
if (keyDuration !== self.props.metronomeTempos[key+'metronomeTempo']) {
clearInterval(refreshMetronome);
self.metronome(key);
}
if (self.props.metronomePlayingStates[key + 'metronomeIsPlaying'] === false) {
clearInterval(refreshMetronome);
}
},keyDuration);
};
componentDidMount() {
document.addEventListener("keydown", this.handleKeyPress, false);
};
componentWillUnmount() {
document.removeEventListener("keydown", this.handleKeyPress, false);
};
render() {
const rowOfPads = (keys) => {
return keys.map((item,index) => {
return <div key={index} id={item+"pad"} className="drum-pad" onClick={() => this.playSample(item)}>
<audio id={item} className="clip" src={"https://trentpalmer.org/drumsamples/"+this.props.samplesUrls[item]}></audio>
{(() => {
if (this.props.metronomeStatuses[item + 'isMetronome'] === 'false') {
return <p>{item}</p>;
} else if (this.props.metronomeStatuses[item + 'isMetronome'] === 'true' && this.props.metronomePlayingStates[item + 'metronomeIsPlaying'] === false) {
return <p>{item + ' '}<FontAwesomeIcon id="metronomeIcon" icon={faSyncAlt} className="metronomeIcon"/></p>
} else if (this.props.metronomeStatuses[item + 'isMetronome'] === 'true' && this.props.metronomePlayingStates[item + 'metronomeIsPlaying'] === true) {
const tempo = Math.round(60000 / this.props.metronomeTempos[item +'metronomeTempo']);
return <p>{tempo + ' '}<FontAwesomeIcon id="metronomeIcon" icon={faSyncAlt} className="metronomeIcon"/></p>
}
})()}
</div>;
});
};
return (
<div id="drum-pad-grid">
<div className="drum-pad-row">
{rowOfPads(padsArray.slice(0,3))}
</div>
<div className="drum-pad-row">
{rowOfPads(padsArray.slice(3,6))}
</div>
<div className="drum-pad-row">
{rowOfPads(padsArray.slice(6,))}
</div>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(DrumPadGrid);

View File

@ -0,0 +1,62 @@
export const padsArray = ['Q','W','E','A','S','D','Z','X','C'];
export const resetDefaults = () => {
initialSamples();
initialPadVolumes();
initialIsMetronome();
initialMetronomeTempos();
};
export const initialPadVolumes = () => {
padsArray.forEach(pad => {
if (!sessionStorage.hasOwnProperty(pad + "volume")) {
sessionStorage.setItem(pad + "volume","+0");
}
});
};
export const initialIsMetronome = () => {
padsArray.forEach(pad => {
if (!sessionStorage.hasOwnProperty(pad + "isMetronome")) {
sessionStorage.setItem(pad + "isMetronome",false);
}
});
};
export const initialMetronomeTempos = () => {
padsArray.forEach(pad => {
if (!sessionStorage.hasOwnProperty(pad + "metronomeTempo")) {
sessionStorage.setItem(pad + "metronomeTempo",652);
}
});
};
export const initialSamples = () => {
if (!sessionStorage.hasOwnProperty(padsArray[0])) {
sessionStorage.setItem(padsArray[0],"Assorted-Hits/Cymbals/CYCdh_Crash-01.wav");
}
if (!sessionStorage.hasOwnProperty(padsArray[1])) {
sessionStorage.setItem(padsArray[1],"Assorted-Hits/Cymbals/CYCdh_MultiCrash-01.wav");
}
if (!sessionStorage.hasOwnProperty(padsArray[2])) {
sessionStorage.setItem(padsArray[2],"Assorted-Hits/Cymbals/CYCdh_MultiCrashHi-01.wav");
}
if (!sessionStorage.hasOwnProperty(padsArray[3])) {
sessionStorage.setItem(padsArray[3],"Assorted-Hits/Cymbals/CYCdh_MultiCrashLo-01.wav");
}
if (!sessionStorage.hasOwnProperty(padsArray[4])) {
sessionStorage.setItem(padsArray[4],"Assorted-Hits/Snares/Ludwig-A/CYCdh_LudFlamA-05.wav");
}
if (!sessionStorage.hasOwnProperty(padsArray[5])) {
sessionStorage.setItem(padsArray[5],"Assorted-Hits/Snares/Ludwig-A/CYCdh_LudRimA-07.wav");
}
if (!sessionStorage.hasOwnProperty(padsArray[6])) {
sessionStorage.setItem(padsArray[6],"Assorted-Hits/Kicks/Loose-Kick/CYCdh_LooseKick-08.wav");
}
if (!sessionStorage.hasOwnProperty(padsArray[7])) {
sessionStorage.setItem(padsArray[7],"Assorted-Hits/Snares/Ludwig-A/CYCdh_LudSnrA-05.wav");
}
if (!sessionStorage.hasOwnProperty(padsArray[8])) {
sessionStorage.setItem(padsArray[8],"Assorted-Hits/Snares/Ludwig-A/CYCdh_LudSnrOffA-08.wav");
}
};

View File

@ -0,0 +1,36 @@
import React from 'react';
import { connect } from "react-redux";
import './Selection.css';
import { setDrumPadGridAction } from "./actions/setDrumPadGridAction";
import { setSelectionMenuAction } from "./actions/setSelectionMenuAction";
const mapStateToProps = (state) => ({ ...state });
const mapDispatchToProps = (dispatch) => ({
setDrumPadGridAction: (drumPadGrid) => dispatch(setDrumPadGridAction(drumPadGrid)),
setSelectionMenuAction: (selectionMenu) => dispatch(setSelectionMenuAction(selectionMenu)),
});
class Metronome extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
};
handleClick() {
this.props.setSelectionMenuAction('pads');
this.props.setDrumPadGridAction('metronomeSelectionMenu');
}
render() {
return (
<div id="metronome" className="selection" onClick={this.handleClick}>
<p>metronome</p>
</div>
);
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Metronome);

View File

@ -0,0 +1,44 @@
import React from 'react';
import { connect } from "react-redux";
import './Selection.css';
import { resetDefaults,padsArray } from "./Globals";
import { setVolumeAction } from "./actions/setVolumeAction";
import { toggleMetronomeIsPlayingAction } from "./actions/toggleMetronomeIsPlayingAction";
const mapStateToProps = (state) => ({ ...state });
const mapDispatchToProps = (dispatch) => ({
setVolumeAction: (volume) => dispatch(setVolumeAction(volume)),
toggleMetronomeIsPlayingAction: (key,metronomeIsPlaying) => dispatch(toggleMetronomeIsPlayingAction(key,metronomeIsPlaying)),
});
class Reset extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
};
handleClick() {
sessionStorage.clear();
resetDefaults();
this.props.setVolumeAction(30);
padsArray.forEach(pad => {
if (this.props.metronomePlayingStates[pad + 'metronomeIsPlaying'] === true) {
this.props.toggleMetronomeIsPlayingAction(pad,false);
}
});
sessionStorage.setItem('volume',30);
};
render() {
return (
<div id="reset" className="selection" onClick={this.handleClick}>
<p>RESET</p>
</div>
);
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Reset);

View File

@ -0,0 +1,36 @@
import React from 'react';
import { connect } from "react-redux";
import './Selection.css';
import { setDrumPadGridAction } from "./actions/setDrumPadGridAction";
import { setSelectionMenuAction } from "./actions/setSelectionMenuAction";
const mapStateToProps = (state) => ({ ...state });
const mapDispatchToProps = (dispatch) => ({
setDrumPadGridAction: (drumPadGrid) => dispatch(setDrumPadGridAction(drumPadGrid)),
setSelectionMenuAction: (selectionMenu) => dispatch(setSelectionMenuAction(selectionMenu)),
});
class SelectVolume extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
};
handleClick() {
this.props.setSelectionMenuAction('pads');
this.props.setDrumPadGridAction('volumeSelectionMenu');
}
render() {
return (
<div id="select-volume" className="selection" onClick={this.handleClick}>
<p>select<br/>a volume</p>
</div>
);
};
}
export default connect(mapStateToProps, mapDispatchToProps)(SelectVolume);

View File

@ -0,0 +1,70 @@
#selection-left, #selection-right {
flex-basis: 12%;
display: flex;
flex-direction: column;
justify-content: space-around;
}
.selection {
margin-left: auto;
margin-right: auto;
width: 15vh;
min-width: 110px;
height: 12vh;
min-height: 80px;
background-color: var(--global-fourth-color);
border-radius: 1rem;
border-width: 2px;
border-style: solid;
display: flex;
flex-direction: row;
justify-content: space-around;
}
.selection > p {
margin: auto;
text-align: center;
font-size: large;
}
.selection:active {
background-color: var(--global-first-color);
}
@media (orientation: portrait) {
#select-volume, #volumeTip, #menuScrollTip {
display: none;
}
#selection-left, #selection-right {
flex-basis: unset;
min-width: unset;
justify-content: space-around;
}
.selection {
max-height: 80px;
margin-top: 20px;
margin-bottom: 20px;
}
}
@media (orientation: portrait) and (max-device-width: 750px) {
.selection {
max-height: 40px;
margin-top: 10px;
margin-bottom: 10px;
}
}
@media (orientation: landscape) and (max-device-width: 1024px) {
#select-volume, #volumeTip, #menuScrollTip {
display: none;
}
#selection-left, #selection-right {
flex-basis: unset;
min-width: unset;
}
}

View File

@ -0,0 +1,36 @@
import React from 'react';
import { connect } from "react-redux";
import './Selection.css';
import { setDrumPadGridAction } from "./actions/setDrumPadGridAction";
import { setSelectionMenuAction } from "./actions/setSelectionMenuAction";
const mapStateToProps = (state) => ({ ...state });
const mapDispatchToProps = (dispatch) => ({
setDrumPadGridAction: (drumPadGrid) => dispatch(setDrumPadGridAction(drumPadGrid)),
setSelectionMenuAction: (selectionMenu) => dispatch(setSelectionMenuAction(selectionMenu)),
});
class Selection extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
};
handleClick() {
this.props.setSelectionMenuAction('pads');
this.props.setDrumPadGridAction('selectionMenu');
}
render() {
return (
<div id="selection" className="selection" onClick={this.handleClick}>
<p>select<br/>a sound</p>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Selection);

View File

@ -0,0 +1,17 @@
import React from 'react';
import Selection from "./Selection";
import SelectVolume from "./SelectVolume";
import Metronome from "./Metronome";
const SelectionLeft = () => {
return (
<div id="selection-left">
<Selection />
<SelectVolume />
<Metronome />
</div>
);
}
export default SelectionLeft;

View File

@ -0,0 +1,304 @@
import React from 'react';
import { connect } from "react-redux";
import { padsArray } from "./Globals";
import { wavFiles } from "./wavFiles";
import { metronomeTempos } from "./metronomeTempos";
import { setDrumPadGridAction } from "./actions/setDrumPadGridAction";
import { setSelectionMenuAction } from "./actions/setSelectionMenuAction";
import { setMetronomeTempoAction } from "./actions/setMetronomeTempoAction";
import { toggleMetronomeIsPlayingAction } from "./actions/toggleMetronomeIsPlayingAction";
import { setSampleAction } from "./actions/setSampleAction";
import { shouldMetronomeRestartAction } from "./actions/shouldMetronomeRestartAction";
const volumeSelectionMenuItems = ['+30','+20','+10','+0','-10','-20','-30'];
const mapStateToProps = (state) => ({ ...state });
const mapDispatchToProps = (dispatch) => ({
setDrumPadGridAction: (drumPadGrid) => dispatch(setDrumPadGridAction(drumPadGrid)),
setSelectionMenuAction: (selectionMenu) => dispatch(setSelectionMenuAction(selectionMenu)),
setMetronomeTempoAction: (key,tempo) => dispatch(setMetronomeTempoAction(key,tempo)),
toggleMetronomeIsPlayingAction: (key,metronomeIsPlaying) => dispatch(toggleMetronomeIsPlayingAction(key,metronomeIsPlaying)),
setSampleAction: (key,sample) => dispatch(setSampleAction(key,sample)),
shouldMetronomeRestartAction: (key,restartMetronome) => dispatch(shouldMetronomeRestartAction(key,restartMetronome)),
});
class SelectionMenu extends React.Component {
constructor(props) {
super(props);
this.state = {
fileStringArray: [],
padSelectingFor: '',
};
this.handleMouseOver = this.handleMouseOver.bind(this);
this.handleMouseOut = this.handleMouseOut.bind(this);
this.makeMenuToolTipText = this.makeMenuToolTipText.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleBackNav = this.handleBackNav.bind(this);
this.backToDrumPad = this.backToDrumPad.bind(this);
this.handleEscKey = this.handleEscKey.bind(this);
};
makeMenuToolTipText(item) {
if (item === '( cancel -- back )') {
return item;
} else {
if (this.props.drumPadGrid === 'selectionMenu') {
if (this.props.selectionMenu === 'pads') {
return 'select for ' + item;
} else if (this.props.selectionMenu === 'firstDirMenu') {
return 'select for ' + this.state.padSelectingFor + ': ' + item;
} else if (this.props.selectionMenu === 'secondDirMenu') {
return 'select for ' + this.state.padSelectingFor + ': ' + this.state.fileStringArray[0] + '/' + item;
} else {
return 'select for ' + this.state.padSelectingFor + ': ' + this.state.fileStringArray.join('/') + '/' + item;
}
} else if (this.props.drumPadGrid === 'volumeSelectionMenu') {
if (this.props.selectionMenu === 'pads') {
return 'volume offset for ' + item;
} else if (this.props.selectionMenu === 'volumeSelectionMenuItems') {
return 'set volume offset for ' + this.state.padSelectingFor + ' ' + item;
}
} else if (this.props.drumPadGrid === 'metronomeSelectionMenu') {
if (this.props.selectionMenu === 'pads') {
return 'set metronome tempo for ' + item + ' (or disable)';
} else if (this.props.selectionMenu === 'metronomeSelectionMenuItems') {
if (item === 'Metronome Off') {
return 'turn ' + item + ' for ' + this.state.padSelectingFor;
} else {
return 'set metronome tempo for ' + this.state.padSelectingFor + ':' + item.split(':')[1] + ' BPM';
}
}
}
}
};
handleBackNav() {
if (this.props.selectionMenu === 'pads') {
this.backToDrumPad();
} else {
if (this.props.drumPadGrid === 'selectionMenu') {
if (this.props.selectionMenu === 'firstDirMenu') {
this.props.setSelectionMenuAction('pads');
} else if (this.props.selectionMenu === 'secondDirMenu') {
this.props.setSelectionMenuAction('firstDirMenu');
} else if (this.props.selectionMenu === 'thirdDirMenu') {
this.props.setSelectionMenuAction('secondDirMenu');
} else if (this.props.selectionMenu === 'thirdDirMenu') {
this.props.setSelectionMenuAction('secondDirMenu');
} else if (this.props.selectionMenu === 'fourthDirMenu') {
this.props.setSelectionMenuAction('thirdDirMenu');
}
} else {
this.props.setSelectionMenuAction('pads');
this.setState({ padSelectingFor: '', });
}
}
};
backToDrumPad() {
this.setState({
fileStringArray: [],
padSelectingFor: '',
});
this.props.setDrumPadGridAction('drumPadGrid');
};
handleEscKey(event) {
if (event.keyCode === 27) {
this.handleClick('( cancel -- back )');
}
};
handleMouseOver(item) {
let menuToolTip = document.createElement('div');
menuToolTip.setAttribute('id','menuToolTip');
menuToolTip.textContent = this.makeMenuToolTipText(item);
document.getElementById('display-bottom').appendChild(menuToolTip);
};
handleMouseOut() {
let menuToolTip = document.getElementById('menuToolTip');
if (menuToolTip) {
menuToolTip.parentNode.removeChild(menuToolTip);
}
};
componentDidMount() {
document.addEventListener("keyup",this.handleEscKey,false);
document.getElementById(this.props.drumPadGrid).tabIndex = '0';
document.getElementById(this.props.drumPadGrid).focus();
if (!document.getElementById('menuScrollTip')) {
let menuScrollTip = document.createElement('p');
menuScrollTip.setAttribute('id','menuScrollTip');
menuScrollTip.textContent = ('scroll menu with arrow keys or PgUpDn, "esc" key to go back');
document.getElementById('display-top').appendChild(menuScrollTip);
}
};
componentWillUnmount() {
document.removeEventListener("keyup",this.handleEscKey,false);
let menuScrollTip = document.getElementById('menuScrollTip');
if (menuScrollTip) {
menuScrollTip.parentNode.removeChild(menuScrollTip);
}
};
componentDidUpdate(prevProps) {
if (this.props.drumPadGrid === 'selectionMenu') {
if ((prevProps.selectionMenu === 'secondDirMenu') && (this.props.selectionMenu === 'firstDirMenu')) {
this.setState({
fileStringArray: [],
});
} else if ((prevProps.selectionMenu === 'thirdDirMenu') && (this.props.selectionMenu === 'secondDirMenu')) {
this.setState(state => {
return { fileStringArray: [state.fileStringArray[0],] }
});
} else if ((prevProps.selectionMenu === 'fourthDirMenu') && (this.props.selectionMenu === 'thirdDirMenu')) {
this.setState(state => {
return { fileStringArray: [state.fileStringArray[0],state.fileStringArray[1]] }
});
}
}
};
handleClick(item) {
if (item === '( cancel -- back )') {
this.handleBackNav();
} else {
if (this.props.drumPadGrid === 'selectionMenu') {
if (this.props.selectionMenu === 'pads') {
this.setState({
fileStringArray: [],
padSelectingFor: item,
});
this.props.setSelectionMenuAction('firstDirMenu');
} else if (this.props.selectionMenu === 'firstDirMenu') {
this.setState({
fileStringArray: [item,],
});
this.props.setSelectionMenuAction('secondDirMenu');
} else if (this.props.selectionMenu === 'secondDirMenu') {
this.setState(state => {
return { fileStringArray: [state.fileStringArray[0],item] }
});
this.props.setSelectionMenuAction('thirdDirMenu');
} else if (this.props.selectionMenu === 'thirdDirMenu') {
if (item.substring(item.length - 4) === '.wav') {
const sample = this.state.fileStringArray.join('/') + '/' + item;
sessionStorage.setItem(this.state.padSelectingFor,sample);
this.props.shouldMetronomeRestartAction(this.state.padSelectingFor,true);
this.backToDrumPad();
} else {
this.setState(state => {
return { fileStringArray: [...state.fileStringArray,item] }
});
this.props.setSelectionMenuAction('fourthDirMenu');
}
} else if (this.props.selectionMenu === 'fourthDirMenu') {
const sample = this.state.fileStringArray.join('/') + '/' + item;
sessionStorage.setItem(this.state.padSelectingFor,sample);
this.props.shouldMetronomeRestartAction(this.state.padSelectingFor,true);
this.backToDrumPad();
}
} else if (this.props.drumPadGrid === 'volumeSelectionMenu') {
if (this.props.selectionMenu === 'pads') {
this.setState({ padSelectingFor: item, });
this.props.setSelectionMenuAction('volumeSelectionMenuItems');
} else if (this.props.selectionMenu === 'volumeSelectionMenuItems') {
sessionStorage.setItem(this.state.padSelectingFor + 'volume',item);
this.backToDrumPad();
}
} else if (this.props.drumPadGrid === 'metronomeSelectionMenu') {
if (this.props.selectionMenu === 'pads') {
this.setState({ padSelectingFor: item, });
this.props.setSelectionMenuAction('metronomeSelectionMenuItems');
} else if (this.props.selectionMenu === 'metronomeSelectionMenuItems') {
if (item === 'Metronome Off') {
sessionStorage.setItem(this.state.padSelectingFor + 'isMetronome',false);
this.props.toggleMetronomeIsPlayingAction(this.state.padSelectingFor,false);
this.backToDrumPad();
} else {
sessionStorage.setItem(this.state.padSelectingFor + 'isMetronome',true);
const tempo = Math.round(60000 / parseInt(item.split(': ')[1]));
sessionStorage.setItem(this.state.padSelectingFor + 'metronomeTempo',tempo);
this.props.setMetronomeTempoAction(this.state.padSelectingFor,tempo);
this.backToDrumPad();
}
}
}
}
this.handleMouseOut();
};
render() {
const makeSelectionMenuItem = (item) => {
return (
<div
key={this.props.drumPadGrid + this.props.selectionMenu + item}
className="selectionMenuItem"
onMouseEnter={(event) => this.handleMouseOver(item)}
onMouseLeave={(event) => this.handleMouseOut()}
onClick={() => this.handleClick(item)}
>
{item}
</div>
);
};
const makeSelectionMenu = (menuItems) => {
let resultItems = menuItems.map(item => item);
resultItems.unshift('( cancel -- back )')
return resultItems.map(item => makeSelectionMenuItem(item));
};
if (this.props.drumPadGrid === 'selectionMenu') {
return(
<div id="selectionMenu" className="selectionMenu">
{ this.props.selectionMenu === 'pads' && makeSelectionMenu(padsArray) }
{ this.props.selectionMenu === 'firstDirMenu' && makeSelectionMenu(Object.keys(wavFiles)) }
{ this.props.selectionMenu === 'secondDirMenu' && makeSelectionMenu(Object.keys(wavFiles[this.state.fileStringArray[0]])) }
{ this.props.selectionMenu === 'thirdDirMenu' && makeSelectionMenu((() => {
const menuItems = wavFiles[this.state.fileStringArray[0]][this.state.fileStringArray[1]]
.filter(item => typeof item === 'string');
if (!(typeof wavFiles[this.state.fileStringArray[0]][this.state.fileStringArray[1]][0] === 'string')) {
const moreDirs = Object.keys(wavFiles[this.state.fileStringArray[0]][this.state.fileStringArray[1]][0]);
moreDirs.forEach(item => menuItems.push(item));
}
return menuItems;
})()) }
{ this.props.selectionMenu === 'fourthDirMenu' && makeSelectionMenu(wavFiles[this.state.fileStringArray[0]][this.state.fileStringArray[1]][0][this.state.fileStringArray[2]]) }
</div>
);
} else if (this.props.drumPadGrid === 'volumeSelectionMenu') {
return(
<div id="volumeSelectionMenu" className="selectionMenu">
{ this.props.selectionMenu === 'pads' && makeSelectionMenu(padsArray) }
{ this.props.selectionMenu === 'volumeSelectionMenuItems' && makeSelectionMenu(volumeSelectionMenuItems) }
</div>
);
} else if (this.props.drumPadGrid === 'metronomeSelectionMenu') {
return(
<div id="metronomeSelectionMenu" className="selectionMenu">
{ this.props.selectionMenu === 'pads' && makeSelectionMenu(padsArray) }
{ this.props.selectionMenu === 'metronomeSelectionMenuItems' && makeSelectionMenu((() => {
const tempos = Object.keys(metronomeTempos).map(item => {
const keyItems = item.split(' ');
if (keyItems.length === 3) {
return keyItems[0] + ' ' + keyItems[1] + ': ' + metronomeTempos[item].slice(0,-4);
} else {
return keyItems[0] + ': ' + metronomeTempos[item].slice(0,-4);
}
});
tempos.unshift('Metronome Off');
return tempos;
})()) }
</div>
);
}
};
};
export default connect(mapStateToProps, mapDispatchToProps)(SelectionMenu);

View File

@ -0,0 +1,15 @@
import React from 'react';
import Reset from "./Reset";
import Stop from "./Stop";
const SelectionRight = () => {
return (
<div id="selection-right">
<Reset />
<Stop />
</div>
);
}
export default SelectionRight;

View File

@ -0,0 +1,37 @@
import React from 'react';
import { connect } from "react-redux";
import './Selection.css';
import { toggleMetronomeIsPlayingAction } from "./actions/toggleMetronomeIsPlayingAction";
import { padsArray } from "./Globals";
const mapStateToProps = (state) => ({ ...state });
const mapDispatchToProps = (dispatch) => ({
toggleMetronomeIsPlayingAction: (key,metronomeIsPlaying) => dispatch(toggleMetronomeIsPlayingAction(key,metronomeIsPlaying)),
});
class Stop extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
};
handleClick() {
padsArray.forEach(pad => {
if (this.props.metronomePlayingStates[pad + 'metronomeIsPlaying'] === true) {
this.props.toggleMetronomeIsPlayingAction(pad,false);
}
});
}
render() {
return (
<div id="stop" className="selection" onClick={this.handleClick}>
<p>STOP</p>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Stop);

View File

@ -0,0 +1,54 @@
#volume-container {
margin: auto;
height: 300px;
width: 25px;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 32px;
}
#volume-input-container {
height: 210px;
}
#volume {
-webkit-appearance: none;
position: relative;
top: 75px;
left: -95px;
width: 205px;
height: 4px;
background: var(--global-black-color);
transform: rotate(270deg);
border-radius: 4px;
border: none;
}
#volume:focus {
outline: none;
}
#volume::-webkit-slider-thumb {
-webkit-appearance: none;
height: 30px;
width: 30px;
border: none;
border-radius: 50%;
cursor: pointer;
background: var(--global-black-color);
margin-top: -2px;
}
#volume::-moz-range-thumb {
height: 30px;
width: 30px;
border: none;
border-radius: 50%;
background: var(--global-black-color);
cursor: pointer;
}
.volumeIcon {
color: var(--global-black-color);
}

View File

@ -0,0 +1,49 @@
import React from 'react';
import { connect } from "react-redux";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faVolumeDown } from "@fortawesome/free-solid-svg-icons";
import { faVolumeUp } from "@fortawesome/free-solid-svg-icons";
import './VolumeContainer.css';
import { setVolumeAction } from "./actions/setVolumeAction";
const mapStateToProps = (state) => ({ ...state });
const mapDispatchToProps = (dispatch) => ({
setVolumeAction: (volume) => dispatch(setVolumeAction(volume)),
});
class VolumeContainer extends React.Component {
constructor(props) {
super(props);
this.handleVolumeChange = this.handleVolumeChange.bind(this);
};
handleVolumeChange(e) {
this.props.setVolumeAction(e.target.value);
sessionStorage.setItem('volume',e.target.value);
};
componentDidMount() {
document.getElementById('display-top').innerHTML = '<p id="volumeTip">press "V" to focus Volume so you can adjust with arrow keys</p>';
};
render() {
return (
<div id="volume-container">
<div id="volume-up-icon-container">
<FontAwesomeIcon id="volumeUpIcon" icon={faVolumeUp} className="volumeIcon"/>
</div>
<div id="volume-input-container">
<input id="volume" type="range" min="1" max="100" value={this.props.volume} onChange={this.handleVolumeChange}/>
</div>
<div id="volume-down-icon-container">
<FontAwesomeIcon id="volumeDownIcon" icon={faVolumeDown} className="volumeIcon"/>
</div>
</div>
);
};
}
export default connect(mapStateToProps, mapDispatchToProps)(VolumeContainer);

View File

@ -0,0 +1,8 @@
export const DRUMPADGRID = "DRUMPADGRID";
export const setDrumPadGridAction = (drumPadGrid) => {
return {
type: DRUMPADGRID,
drumPadGrid: drumPadGrid,
}
}

View File

@ -0,0 +1,9 @@
export const SETMETRONOMETEMPO = "SETMETRONOMETEMPO";
export const setMetronomeTempoAction = (key,tempo) => {
return {
type: SETMETRONOMETEMPO,
key: key,
tempo: tempo,
};
};

View File

@ -0,0 +1,9 @@
export const SETSAMPLE = "SETSAMPLE";
export const setSampleAction = (key,sample) => {
return {
type: SETSAMPLE,
key: key,
sample: sample,
};
};

View File

@ -0,0 +1,8 @@
export const SELECTIONMENU = "SELECTIONMENU";
export const setSelectionMenuAction = (selectionMenu) => {
return {
type: SELECTIONMENU,
selectionMenu: selectionMenu,
}
}

View File

@ -0,0 +1,8 @@
export const CHANGEVOLUME = "CHANGEVOLUME";
export const setVolumeAction = (volume) => {
return {
type: CHANGEVOLUME,
volume: volume,
};
};

View File

@ -0,0 +1,9 @@
export const RESTARTMETRONOME = "RESTARTMETRONOME";
export const shouldMetronomeRestartAction = (key,restartMetronome) => {
return {
type: RESTARTMETRONOME,
key: key,
restartMetronome: restartMetronome,
};
};

View File

@ -0,0 +1,9 @@
export const TOGGLEMETRONOME = "TOGGLEMETRONOME";
export const toggleMetronomeIsPlayingAction = (key,metronomeIsPlaying) => {
return {
type: TOGGLEMETRONOME,
key: key,
metronomeIsPlaying: metronomeIsPlaying,
};
};

View File

@ -0,0 +1,7 @@
:root {
--global-first-color: #756d58;
--global-second-color: #586075;
--global-third-color: #607558;
--global-fourth-color: #6d5875;
--global-black-color: #333333;
}

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -0,0 +1,20 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from "react-redux";
import store from "./store";
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
<g fill="#61DAFB">
<path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
<circle cx="420.9" cy="296.5" r="45.7"/>
<path d="M520.5 78.1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,190 @@
export const metronomeTempos = {
'Larghissimo 20': '20 bpm',
'Larghissimo 21': '21 bpm',
'Larghissimo 22': '22 bpm',
'Larghissimo 23': '23 bpm',
'Adagissimo 24': '24 bpm',
'Adagissimo 25': '25 bpm',
'Adagissimo 26': '26 bpm',
'Adagissimo 27': '27 bpm',
'Adagissimo 28': '28 bpm',
'Adagissimo 29': '29 bpm',
'Adagissimo 30': '30 bpm',
'Adagissimo 31': '31 bpm',
'Adagissimo 32': '32 bpm',
'Adagissimo 33': '33 bpm',
'Adagissimo 34': '34 bpm',
'Grave 35': '35 bpm',
'Grave 36': '36 bpm',
'Grave 37': '37 bpm',
'Grave 38': '38 bpm',
'Grave 39': '39 bpm',
'Grave 40': '40 bpm',
'Grave 41': '41 bpm',
'Grave 42': '42 bpm',
'Grave 43': '43 bpm',
'Grave 44': '44 bpm',
'Grave 45': '45 bpm',
'Grave 46': '46 bpm',
'Grave 47': '47 bpm',
'Grave 48': '48 bpm',
'Grave 49': '49 bpm',
'Largo 50': '50 bpm',
'Largo 51': '51 bpm',
'Largo 52': '52 bpm',
'Lento 53': '53 bpm',
'Lento 54': '54 bpm',
'Lento 55': '55 bpm',
'Lento 56': '56 bpm',
'Lento 57': '57 bpm',
'Lento 58': '58 bpm',
'Lento 59': '59 bpm',
'Lento 60': '60 bpm',
'Lento 61': '61 bpm',
'Lento 62': '62 bpm',
'Larghetto 63': '63 bpm',
'Larghetto 64': '64 bpm',
'Larghetto 65': '65 bpm',
'Larghetto 66': '66 bpm',
'Larghetto 67': '67 bpm',
'Larghetto 68': '68 bpm',
'Larghetto 69': '69 bpm',
'Larghetto 70': '70 bpm',
'Adagio 71': '71 bpm',
'Adagio 72': '72 bpm',
'Adagio 73': '73 bpm',
'Adagio 74': '74 bpm',
'Adagietto 75': '75 bpm',
'Adagietto 76': '76 bpm',
'Adagietto 77': '77 bpm',
'Adagietto 78': '78 bpm',
'Adagietto 79': '79 bpm',
'Adagietto 80': '80 bpm',
'Adagietto 81': '81 bpm',
'Adagietto 82': '82 bpm',
'Adagietto 83': '83 bpm',
'Marcia moderato 84': '84 bpm',
'Marcia moderato 85': '85 bpm',
'Marcia moderato 86': '86 bpm',
'Marcia moderato 87': '87 bpm',
'Marcia moderato 88': '88 bpm',
'Marcia moderato 89': '89 bpm',
'Marcia moderato 90': '90 bpm',
'Marcia moderato 91': '91 bpm',
'Andante 92': '92 bpm',
'Andante 93': '93 bpm',
'Andantino 94': '94 bpm',
'Andante moderato 95': '95 bpm',
'Andante moderato 96': '96 bpm',
'Andante moderato 97': '97 bpm',
'Andante moderato 98': '98 bpm',
'Andante moderato 99': '99 bpm',
'Andante moderato 100': '100 bpm',
'Andante moderato 101': '101 bpm',
'Andante moderato 102': '102 bpm',
'Andante moderato 103': '103 bpm',
'Andante moderato 104': '104 bpm',
'Moderato 105': '105 bpm',
'Allegretto 106': '106 bpm',
'Allegretto 107': '107 bpm',
'Allegretto 108': '108 bpm',
'Allegretto 109': '109 bpm',
'Allegretto 110': '110 bpm',
'Allegretto 111': '111 bpm',
'Allegretto 112': '112 bpm',
'Allegretto 113': '113 bpm',
'Allegretto 114': '114 bpm',
'Allegretto 115': '115 bpm',
'Allegretto 116': '116 bpm',
'Allegretto 117': '117 bpm',
'Allegro moderato 118': '118 bpm',
'Allegro moderato 119': '119 bpm',
'Allegro moderato 120': '120 bpm',
'Allegro moderato 121': '121 bpm',
'Allegro moderato 122': '122 bpm',
'Allegro moderato 123': '123 bpm',
'Allegro moderato 124': '124 bpm',
'Allegro moderato 125': '125 bpm',
'Allegro moderato 126': '126 bpm',
'Allegro moderato 127': '127 bpm',
'Allegro moderato 128': '128 bpm',
'Allegro moderato 129': '129 bpm',
'Allegro moderato 130': '130 bpm',
'Allegro moderato 131': '131 bpm',
'Allegro moderato 132': '132 bpm',
'Allegro moderato 133': '133 bpm',
'Allegro moderato 134': '134 bpm',
'Allegro moderato 135': '135 bpm',
'Allegro moderato 136': '136 bpm',
'Allegro moderato 137': '137 bpm',
'Allegro 138': '138 bpm',
'Allegro 139': '139 bpm',
'Allegro 140': '140 bpm',
'Allegro 141': '141 bpm',
'Allegro 142': '142 bpm',
'Allegro 143': '143 bpm',
'Allegro 144': '144 bpm',
'Allegro 145': '145 bpm',
'Allegro 146': '146 bpm',
'Allegro 147': '147 bpm',
'Allegro 148': '148 bpm',
'Allegro 149': '149 bpm',
'Allegro 150': '150 bpm',
'Allegro 151': '151 bpm',
'Allegro 152': '152 bpm',
'Allegro 153': '153 bpm',
'Allegro 154': '154 bpm',
'Allegro 155': '155 bpm',
'Allegro 156': '156 bpm',
'Allegro 157': '157 bpm',
'Allegro 158': '158 bpm',
'Allegro 159': '159 bpm',
'Allegro 160': '160 bpm',
'Allegro 161': '161 bpm',
'Allegro 162': '162 bpm',
'Allegro 163': '163 bpm',
'Allegro 164': '164 bpm',
'Allegro 165': '165 bpm',
'Vivace 166': '166 bpm',
'Vivace 167': '167 bpm',
'Vivace 168': '168 bpm',
'Vivace 169': '169 bpm',
'Vivace 170': '170 bpm',
'Vivace 171': '171 bpm',
'Vivace 172': '172 bpm',
'Vivace 173': '173 bpm',
'Vivacissimo 174': '174 bpm',
'Allegrissimo 174': '174 bpm',
'Allegro vivace 174': '174 bpm',
'Allegro vivace 175': '175 bpm',
'Allegro vivace 176': '176 bpm',
'Allegro vivace 177': '177 bpm',
'Allegro vivace 178': '178 bpm',
'Allegro vivace 179': '179 bpm',
'Allegro vivace 180': '180 bpm',
'Allegro vivace 181': '181 bpm',
'Allegro vivace 182': '182 bpm',
'Allegro vivace 183': '183 bpm',
'Presto 184': '184 bpm',
'Presto 185': '185 bpm',
'Presto 186': '186 bpm',
'Presto 187': '187 bpm',
'Presto 188': '188 bpm',
'Presto 189': '189 bpm',
'Presto 190': '190 bpm',
'Presto 191': '191 bpm',
'Presto 192': '192 bpm',
'Presto 193': '193 bpm',
'Presto 194': '194 bpm',
'Presto 195': '195 bpm',
'Presto 196': '196 bpm',
'Presto 197': '197 bpm',
'Presto 198': '198 bpm',
'Presto 199': '199 bpm',
'Presto 200': '200 bpm',
'Presto 201': '201 bpm',
'Presto 202': '202 bpm',
'Presto 203': '203 bpm',
'Presto 204': '204 bpm',
'Prestissimo 205': '205 bpm',
};

View File

@ -0,0 +1,24 @@
import { padsArray } from "../Globals";
import { TOGGLEMETRONOME } from "../actions/toggleMetronomeIsPlayingAction.js";
const initMetronomeIsPlaying = () => {
const metronomePlayingStates = {};
padsArray.forEach(key => {
metronomePlayingStates[key + 'metronomeIsPlaying'] = false;
});
return metronomePlayingStates;
};
export default (state, action) => {
if (!state) {
state = initMetronomeIsPlaying();
}
switch (action.type) {
case TOGGLEMETRONOME:
state[action.key + 'metronomeIsPlaying'] = action.metronomeIsPlaying;
return state;
default:
return state;
}
}

View File

@ -0,0 +1,18 @@
import { padsArray,initialIsMetronome } from "../Globals";
const getIsMetronomeStatuses = () => {
const metronomeStatuses = {};
padsArray.forEach(key => {
metronomeStatuses[key + 'isMetronome'] = sessionStorage.getItem(key + 'isMetronome');
});
return metronomeStatuses;
}
export default (state, action) => {
initialIsMetronome();
switch (action.type) {
default:
return getIsMetronomeStatuses();
}
};

View File

@ -0,0 +1,25 @@
import { SETMETRONOMETEMPO } from "../actions/setMetronomeTempoAction.js";
import { padsArray,initialMetronomeTempos } from "../Globals";
const getMetronomeTempos = () => {
const metronomeTempos = {};
padsArray.forEach(key => {
metronomeTempos[key + 'metronomeTempo'] = parseInt(sessionStorage.getItem(key + 'metronomeTempo'));
});
return metronomeTempos;
}
export default (state, action) => {
if (!state) {
initialMetronomeTempos();
state = getMetronomeTempos();
}
switch (action.type) {
case SETMETRONOMETEMPO:
state[action.key + 'metronomeTempo'] = action.tempo;
return state;
default:
return state;
}
};

View File

@ -0,0 +1,22 @@
import { combineReducers } from "redux";
import samplesReducer from "./samplesReducer";
import volumeOffSetsReducer from "./volumeOffSetsReducer";
import setVolumeReducer from "./setVolumeReducer";
import setDrumPadGridReducer from "./setDrumPadGridReducer";
import setSelectionMenuReducer from "./setSelectionMenuReducer";
import isMetronomeReducer from "./isMetronomeReducer";
import metronomeTemposReducer from "./metronomeTemposReducer";
import isMetronomePlayingReducer from "./isMetronomePlayingReducer";
import shouldMetronomeRestartReducer from "./shouldMetronomeRestartReducer";
export default combineReducers({
samplesUrls: samplesReducer,
volumeOffSets: volumeOffSetsReducer,
volume: setVolumeReducer,
drumPadGrid: setDrumPadGridReducer,
selectionMenu: setSelectionMenuReducer,
metronomeStatuses: isMetronomeReducer,
metronomeTempos: metronomeTemposReducer,
metronomePlayingStates: isMetronomePlayingReducer,
shouldMetronomeRestart: shouldMetronomeRestartReducer,
});

View File

@ -0,0 +1,26 @@
import { padsArray,initialSamples } from "../Globals";
import { SETSAMPLE } from "../actions/setSampleAction.js";
const getUrls = () => {
const urls = {};
padsArray.forEach(key => {
urls[key] = sessionStorage.getItem(key);
});
return urls;
}
export default (state, action) => {
if (!state) {
initialSamples();
state = getUrls();
}
switch (action.type) {
case SETSAMPLE:
state[action.key] = action.sample;
return state;
default:
state = getUrls();
return state;
}
};

View File

@ -0,0 +1,10 @@
import { DRUMPADGRID } from "../actions/setDrumPadGridAction.js";
export default (state = 'drumPadGrid', action) => {
switch(action.type) {
case DRUMPADGRID:
return action.drumPadGrid;
default:
return state;
}
}

View File

@ -0,0 +1,10 @@
import { SELECTIONMENU } from "../actions/setSelectionMenuAction";
export default (state = 'pads', action) => {
switch(action.type) {
case SELECTIONMENU:
return action.selectionMenu;
default:
return state;
}
}

View File

@ -0,0 +1,18 @@
import { CHANGEVOLUME } from "../actions/setVolumeAction.js";
let DEFAULTVOLUME = 30;
if (!sessionStorage.hasOwnProperty('volume')) {
sessionStorage.setItem('volume','30');
} else {
DEFAULTVOLUME = sessionStorage.getItem('volume');
}
export default (state = DEFAULTVOLUME, action) => {
switch (action.type) {
case CHANGEVOLUME:
return action.volume;
default:
return state;
}
}

View File

@ -0,0 +1,24 @@
import { padsArray } from "../Globals";
import { RESTARTMETRONOME } from "../actions/shouldMetronomeRestartAction.js";
const initRestartMetronome = () => {
const restartMetronomes = {};
padsArray.forEach(key => {
restartMetronomes[key + 'restartMetronome'] = false;
});
return restartMetronomes;
};
export default (state, action) => {
if (!state) {
state = initRestartMetronome();
}
switch (action.type) {
case RESTARTMETRONOME:
state[action.key + 'restartMetronome'] = action.restartMetronome;
return state;
default:
return state;
}
}

View File

@ -0,0 +1,21 @@
import { padsArray,initialPadVolumes } from "../Globals";
const getVolumeOffSets = () => {
const volumeOffSets = {};
padsArray.forEach(key => {
volumeOffSets[key + 'volume'] = sessionStorage.getItem(key + 'volume');
});
return volumeOffSets;
}
export default (state, action) => {
if (!state) {
initialPadVolumes();
state = getVolumeOffSets();
}
switch (action.type) {
default:
return state;
}
};

View File

@ -0,0 +1,141 @@
// This optional code is used to register a service worker.
// register() is not called by default.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on subsequent visits to a page, after all the
// existing tabs open on the page have been closed, since previously cached
// resources are updated in the background.
// To learn more about the benefits of this model and instructions on how to
// opt-in, read https://bit.ly/CRA-PWA
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://bit.ly/CRA-PWA'
);
});
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
}
});
}
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker == null) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
);
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then(registration => {
registration.unregister();
})
.catch(error => {
console.error(error.message);
});
}
}

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect';

View File

@ -0,0 +1,6 @@
import { createStore } from "redux";
import rootReducer from "./reducers/rootReducer";
const store = createStore(rootReducer);
export default store;

File diff suppressed because it is too large Load Diff

View File

@ -41,5 +41,8 @@
<div>
<a class="link" href="drum-machine" target="_blank">Drum Machine</a>
</div>
<div>
<a class="link" href="drum-machine-react/build" target="_blank">Drum Machine React</a>
</div>
</body>
</html>