I am developing a React web application designed for music composers. The app features a dynamic UI that allows users to select musical scales and chords, with visual feedback to indicate compatibility between chosen chords and scales. However, I’ve encountered some bugs related to the color highlighting logic, which I’m struggling to resolve.
App Description:
The application displays a set of musical notes, scales, and chords in a table format. Users can select chords and scales, and the app highlights incompatible chords or scales in red based on the selection. Here are the key functionalities:
Selecting a Chord: When a chord is selected, scales not containing all notes of the chord and chords not fully included in any non-red-highlighted scale are highlighted in red.
Selecting a Scale: Selecting a scale highlights in red the chords that don’t have all their notes in the selected scale and scales not including all notes of at least one non-red-highlighted chord.
Deselecting a Chord: Deselecting a chord removes red highlights from chords and scales affected solely by that chord’s selection.
Deselecting a Scale: Deselecting a scale removes red highlights from chords and scales affected solely by that scale’s selection.
(https://i.stack.imgur.com/te9R0.png)
Bugs Identified:
-
If I first press a chord and then a scale, scales that were marked in red due to the chord selection stop being red. The expected behavior is that the app should keep scales already highlighted in red and mark additional ones as needed.
-
When a chord is selected, followed by a scale, and then the scale is deselected, only those chords/scales that turned red upon the scale’s selection should revert to white. Similarly, if a scale is selected, followed by a chord, and then the chord is deselected, only the scales/chords that turned red upon the chord’s selection should revert to white.
NOTE: The application functions correctly when only chords or only scales are selected, and also when scales are selected before chords. The issue arises when selecting scales after chords.
I’ve included my React component (App.js) and CSS (App.css) code below for reference. I am looking for guidance on how to debug and fix these issues.
App.js
import React, { useState } from 'react';
import './App.css';
const notes = ['Do', 'Do#', 'Re', 'Re#', 'Mi', 'Fa', 'Fa#', 'Sol', 'Sol#', 'La', 'La#', 'Si'];
const scaleQualities = ['Mayor', 'Menor', "Pent Mayor"]; // Used only for scales
const chordQualities = ['Mayor', 'Menor', 'Sus2', 'Sus4']; // Used for chords
// Get the index of a note within an octave
const getIndexInOctave = (note) => {
return notes.indexOf(note);
};
// Define intervals for different scale qualities
const scaleIntervals = {
'Mayor': [2, 2, 1, 2, 2, 2, 1],
'Menor': [2, 1, 2, 2, 1, 2, 2],
'Pent Mayor': [2, 2, 3, 2, 3]
};
// Generate a scale based on a root note and quality
const generateScale = (rootNote, quality) => {
let scale = [rootNote];
let intervals = scaleIntervals[quality];
let currentNote = rootNote;
intervals.forEach(interval => {
currentNote = getNoteAtInterval(currentNote, interval);
scale.push(currentNote);
});
return scale;
};
const getNoteAtInterval = (startNote, interval) => {
let startIndex = getIndexInOctave(startNote);
let targetIndex = (startIndex + interval) % notes.length;
return notes[targetIndex];
};
// Define intervals for different chord qualities
const chordIntervals = {
'Mayor': [4, 3],
'Menor': [3, 4],
'Sus2': [2, 5],
'Sus4': [5, 2]
};
// Generate a chord based on a root note and quality
const generateChord = (rootNote, quality) => {
let chord = [rootNote];
let intervals = chordIntervals[quality];
let currentNote = rootNote;
intervals.forEach(interval => {
currentNote = getNoteAtInterval(currentNote, interval);
chord.push(currentNote);
});
return chord;
};
// Generate all major and minor scales
let generatedScales = {};
notes.forEach(note => {
scaleQualities.forEach(quality => {
let scaleName = `${note} ${quality}`;
generatedScales[scaleName] = generateScale(note, quality);
});
});
// Generate all chords
let generatedChords = {};
notes.forEach(note => {
chordQualities.forEach(quality => {
let chordName = `${note} ${quality}`;
generatedChords[chordName] = generateChord(note, quality);
});
});
// Initial state for the application
const initialState = {
buttons: notes.reduce((acc, note) => {
scaleQualities.forEach(quality => {
acc[`scale-${note} ${quality}`] = 'white';
});
chordQualities.forEach(quality => {
acc[`chord-${note} ${quality}`] = 'white';
});
return acc;
}, {}),
selectedChord: '',
selectedScale: ''
};
const App = () => {
const [selectedChord, setSelectedChord] = useState('');
const [selectedScale, setSelectedScale] = useState('');
const [buttons, setButtons] = useState(initialState.buttons); // Agregado
// Check if a chord is compatible with a scale
const isChordCompatibleWithScale = (chord, scale) => {
if (!generatedChords[chord] || !generatedScales[scale]) {
return false;
}
return generatedChords[chord].every(note => generatedScales[scale].includes(note));
};
// Check if any chord is incompatible with a scale
const isAnyChordIncompatibleWithScale = (buttons, scale) => {
return Object.keys(buttons).some(key => {
if (key.startsWith("chord-") && buttons[key] === 'green') {
const chord = key.substring(6);
return !isChordCompatibleWithScale(chord, scale);
}
return false;
});
};
// Check if any scale is incompatible with a chord
const isAnyScaleIncompatibleWithChord = (buttons, chord) => {
return Object.keys(buttons).some(key => {
if (key.startsWith("scale-") && buttons[key] === 'green') {
const scale = key.substring(6);
return !isChordCompatibleWithScale(chord, scale);
}
return false;
});
};
// Handle click on a chord button
const handleChordClick = (chord) => {
const chordKey = `chord-${chord}`;
if (buttons[chordKey] === 'red') {
return; // Do nothing if the button is red
}
let newButtons = { ...buttons };
const wasSelected = newButtons[chordKey] === 'green';
newButtons[chordKey] = wasSelected ? 'white' : 'green';
// Update the compatibility of scales with the selected/deselected chord
Object.keys(newButtons).forEach(key => {
if (key.startsWith("scale-")) {
if (!wasSelected) {
newButtons[key] = isChordCompatibleWithScale(chord, key.substring(6)) ? newButtons[key] : 'red';
} else if (newButtons[key] === 'red') {
newButtons[key] = isAnyChordIncompatibleWithScale(newButtons, key.substring(6)) ? 'red' : 'white';
}
}
});
// Update the compatibility of chords with the updated scales
Object.keys(newButtons).forEach(key => {
if (key.startsWith("chord-") && key !== chordKey) {
const currentChord = key.substring(6);
const isCompatibleWithAnyNonRedScale = Object.keys(newButtons).some(scaleKey => {
return scaleKey.startsWith("scale-") && newButtons[scaleKey] !== 'red' && isChordCompatibleWithScale(currentChord, scaleKey.substring(6));
});
newButtons[key] = newButtons[key] === 'green' || isCompatibleWithAnyNonRedScale ? newButtons[key] : 'red';
}
});
// If a chord is deselected, check all chords and update their color state
if (wasSelected) {
Object.keys(newButtons).forEach(key => {
if (key.startsWith("chord-")) {
const currentChord = key.substring(6);
// Check if the chord is not currently selected
if (newButtons[key] !== 'green') {
const isCompatibleWithAnyNonRedScale = Object.keys(newButtons).some(scaleKey => {
return scaleKey.startsWith("scale-") && newButtons[scaleKey] !== 'red' && isChordCompatibleWithScale(currentChord, scaleKey.substring(6));
});
newButtons[key] = isCompatibleWithAnyNonRedScale ? 'white' : 'red';
}
}
});
}
setButtons(newButtons);
};
// Handle click on a scale button
const handleScaleClick = (scale) => {
const scaleKey = `scale-${scale}`;
if (buttons[scaleKey] === 'red') {
return; // Do nothing if the button is red
}
let newButtons = { ...buttons };
// Toggle the state of the current scale between selected and unselected
newButtons[scaleKey] = newButtons[scaleKey] === 'green' ? 'white' : 'green';
// Check compatibility of all chords with the current scale
Object.keys(newButtons).forEach(key => {
if (key.startsWith("chord-")) {
const currentChord = key.substring(6);
if (newButtons[scaleKey] === 'green') {
newButtons[key] = isChordCompatibleWithScale(currentChord, scale) ? newButtons[key] : 'red';
} else {
newButtons[key] = isAnyScaleIncompatibleWithChord(newButtons, currentChord) ? 'red' : (newButtons[key] === 'green' ? 'green' : 'white');
}
}
});
// Update the state of scales based on the chords not marked in red
Object.keys(newButtons).forEach(scaleKey => {
if (scaleKey.startsWith("scale-")) {
const currentScale = scaleKey.substring(6);
newButtons[scaleKey] = !Object.keys(generatedChords).some(chordKey => {
if (newButtons[`chord-${chordKey}`] !== 'red') {
return generatedChords[chordKey].every(note => generatedScales[currentScale].includes(note));
}
return false;
}) ? 'red' : (newButtons[scaleKey] === 'green' ? 'green' : 'white');
}
});
setButtons(newButtons);
};
// Generate cells for a row of chords or scales
const generateRowCells = (type, rowQuality) => {
return notes.map((note) => {
const combination = `${note} ${rowQuality}`;
const buttonType = type === 'chord' ? `chord-${combination}` : `scale-${combination}`;
const buttonColor = buttons[buttonType];
const isButtonIncompatible = buttonColor === 'red';
return (
<td
key={note}
onClick={() => {
if (type === 'chord') {
handleChordClick(combination);
} else {
handleScaleClick(combination);
}
}}
style={{ backgroundColor: buttonColor }}
className={isButtonIncompatible ? "incompatible" : ""}
>
{combination}
</td>
);
});
};
// Generate table rows for chords or scales
const generateTableRows = (type) => {
const qualities = type === 'chord' ? chordQualities : scaleQualities;
return qualities.map((quality) => (
<tr key={quality}>
<th>{quality}</th>
{generateRowCells(type, quality)}
</tr>
));
};
return (
<div className="App">
<h1 className="title">ChordProg</h1>
<div className="table-container">
<h2>Acordes</h2>
<table className="chord-table">
<thead>
<tr>
<th></th> {/* Empty cell in the corner */}
{notes.map((note) => (
<th key={note}>{note}</th>
))}
</tr>
</thead>
<tbody>{generateTableRows('chord')}</tbody>
</table>
</div>
<div className="table-container">
<h2>Escalas</h2>
<table className="scale-table">
<thead>
<tr>
<th></th> {/* Empty cell in the corner */}
{notes.map((note) => (
<th key={note}>{note}</th>
))}
</tr>
</thead>
<tbody>{generateTableRows('scale')}</tbody>
</table>
</div>
</div>
);
};
export default App;
App.css
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');
body, h1, h2, h3, p, figure, ul, li, table, td, th {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', sans-serif;
background-color: #f0f2f5;
color: #333;
line-height: 1.6;
}
.App {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 20px;
text-align: center;
zoom: 0.70;
}
.title {
font-size: 2.5em;
margin-bottom: 30px;
color: #4a90e2;
}
.table-container {
margin-top: 30px;
width: 90%;
max-width: 1000px;
}
.chord-table, .scale-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
background-color: white;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
th, td {
border: 1px solid #ddd;
padding: 10px;
text-align: center;
}
th {
background-color: #eaeaea;
font-weight: 700;
}
td {
cursor: pointer;
transition: background-color 0.3s, color 0.3s;
}
td:hover {
color: #0401d6;
}
.selected {
background-color: #ff6347;
color: white;
}
.table-container h2 {
margin-bottom: 15px;
color: #333;
font-size: 1.5em;
}
@media (max-width: 768px) {
.title {
font-size: 2em;
}
.table-container {
width: 100%;
}
}`
.selected {
background-color: #4CAF50;
color: white;
}
.compatible {
}
.incompatible {
cursor: not-allowed;
background-color: #ff4a4a;
color: white;
}
What I Tried:
I have implemented the logic for changing the color states of the buttons representing chords and scales based on their compatibility. The logic involves iterating over the buttons and updating their color state to ‘red’ or ‘white’ depending on the selection and compatibility criteria. When a chord or scale is selected, the app checks for compatibility with other chords and scales and updates the UI accordingly.
What I Expected:
My expectation was that upon selecting a chord or a scale, the application would correctly identify and highlight in red those scales and chords that are not compatible with the selected one. Moreover, upon deselecting a chord or a scale, only the highlights affected by that specific selection should revert back to white, while keeping other red highlights intact.
What Actually Happened:
The issue arises in the sequence of actions. If I first select a chord and then a scale, the red highlights (indicating incompatibility) that were applied due to the chord selection are incorrectly cleared when a scale is selected. Conversely, if I select a scale first and then a chord, the application behaves as expected. This inconsistency suggests that there might be a flaw in the logic that handles the state updates of the buttons when interacting with them in a specific order. The problem seems to lie in the way the application handles state updates when a new selection is made, particularly when it involves interacting with the chords after the scales.