Checking to see if I clicked the same image twice (React) - javascript

I'm working on a Memory Game where the user cannot click the same image twice.
I have it set up so that each time you click an image, you gain a point.
I have an Images.js file that imports (and exports) all the images. I should note each image is the same 250x250 size image of a random animal.
const Images = [
card01,
card02,
etc...
];
In my Card.js folder I loop through the images to display all 10 on screen.
<div className="card">
{Images.map((img) => (
<img
key={img}
src={img}
alt={img}
onClick={incrementScore}
/>
))}
</div>
For the onClick event handler I have the following code which increases my score by 1 for a max value of 10.
const [score, setScore] = useState(0);
const incrementScore = () => {
setScore(score + 1);
if (score >= 10) {
setScore(0);
}
shuffleArray();
console.log(score);
};
I also wasn't sure if it was the React way but I included the shuffleArray() function inside my incrementScore() function.
const shuffleArray = () => {
Images.sort(() => Math.random() - 0.5);
};
All that code does it make it so any time I click an image, it shuffles all the images around in a random order.
What I'm trying to do
My next step is the one that's giving me the most trouble. Currently, any time I click an image I have a point added to my score. However, the point of this game is to not click the same image twice as you have to rely on memory skills to click each image once as they all shuffle around. I'm having trouble coming up with that logic. The only thing I can think of is to use an array method but I'm not trying to completely remove any images from the array. I still need all 10 images to show on screen at all times.

You can do this in multiple ways. The most easy solution is to keep track of the image src that the user has clicked on in an Array.
But, I would first make sure that the Image variable is React-friendly. Currently, it does update the image order, but this is not guaranteed because it is a variable outside the React world.
// static images
const Images = [
card01,
card02,
etc...
];
const shuffleArray = (input) => {
// prevent sorting the original Array in place
return input.slice().sort(() => Math.random() - 0.5);
};
const Component = () => {
const [images, setImages] = useState(shuffleArray(Images));
const [clickedImages, setClickedImages] = useState([]);
const [score, setScore] = useState(0);
const incrementScore = (image) => {
if (clickedImages.includes(image)) {
console.log(`Please don't click me again!`, image);
return;
}
setScore(score + 1);
// update clicked images
setClickedImages(current => [...current, image]);
if (score >= 10) {
setScore(0);
// reset clicked images
setClickedImages([]);
}
setImages(shuffleArray(Images));
console.log(score);
};
return (
<div className="card">
{images.map((img) => (
<img
key={img}
src={img}
alt={img}
onClick={() => incrementScore(img)}
/>
))}
</div>
);
};

Related

How to put one Card on top of another in a 3d stack?

I was wondering how can emulate Tinder's swipe cards mechanism by getting my cards representing each user's information to be fixed in a single position overlapping one another completely so that the only visible one is the one on top until you swipe it away.
I have used the react-tinder-card library to implement the swiping mechanism, however I don't understand how to get these cards to stay on top of one another so the next one becomes visible when swiped.
Currently, they are positioned in a 2d tower/column and when the top is swiped, after about 3 seconds, the below cards all move up by one:
OnSwipe:
Swiped:
Code:
export const SwipeCard = () => {
//array of compatible users fetched for a user.
const [users, setUsers] = useState([]);
const [lastDirection, setLastDirection] = useState()
const [isLoading, setLoading] = React.useState(true);
useEffect(() => {
getUsers().then(() => {
setLoading(false);
});
}, []);
const swiped = (direction, nameToDelete) => {
console.log('removing: ' + nameToDelete)
setLastDirection(direction)
}
const outOfFrame = (firstName) => {
console.log(firstName + ' left the screen!')
}
const getUsers = async () => {
//Currently hardcoded value until authorisation implemented.
const userId = 7;
const response = await UserService.getUsers(userId)
.then(response => response.json())
.then(data => {
for(let i = 0; i < data.length; i++){
users[i] = data[i];
}
});
}
if (isLoading) {
return (
<div/>
)
}
return (
<div>
<div id='tinderCards'>
{lastDirection ? <h2 className='text'>You swiped {lastDirection}</h2> : <h2 className='text' />}
{users.map((user) =>
<TinderCard className='swipeCard' key={user.firstName} onSwipe={(dir) => swiped(dir, user.firstName)} onCardLeftScreen={() => outOfFrame(user.firstName)}>
<div className='card'>
<img id='profileImg' src={config.URL + '/users/' + user.userID + '/image/download'} />
<h2>{user.firstName} {user.lastName}</h2>
</div>
</TinderCard>
)}
</div>
</div>
)
}
The closest I have been able to come was by setting the parent to position = relative, and then the children cards to position = absolute, but then they all overlap one another, making it unreadable, and also they are stacked in reverse order, so Nike Tyson was on the top when he should've been at the bottom.
Thanks for any help!
I think the easiest way is to use an animation library like framer-motion for this. I've seen this article that you can maybe follow.

React Child Component Not Rendering After Change

I am new to React and am trying to create my own scrollbar. I am using a local JSON API to simulate getting data which is then listing the data as 'cards'. I built a couple of other components to organize and handle the information, it looks like this:
Scrollbar (Buttons to Navigate)
|-->CardList (Handles iterating over the cards)
|-->Cards (Template for displaying an individual card)
The issue I am having is that when I trigger the Button event handleNext it will successfully update offset & limit and pass these to CardList. However, it does not reiterate over the array and output the next 5 items. Instead it removes all the Cards from CardList and I am left with an empty screen.
Scrollbar.js:
const { data isPending, error} = useFetch ('http://localhost:8000/data');
const [limit, setLimit] = useState(5);
const [offset, setOffset] = useState(0);
const handleNext = () => {
setOffset(offset + limit);
console.log("Offset: " +offset);
}
return (
<div className="scrollbar">
{error && <div>{error}</div>}
{isPending && <div>Loading...</div>}
{data && <CardList data={data} offset={offset} limit={limit}/> }
<Button onClick={handleNext}/>
</div>
);
}
export default Scrollbar;
CardList.js:
const CardList = ({data , offset, limit}) => {
return (
<div className="card-list" >
{data.slice(offset,limit).map((data) => (
<Card data={data} key={data.id}/>
))}
</div>
);
}
export default CardList;
The problem is when handleNext is triggered offset is going to be equal to 5 while limit keeps the value of 5 too, then when u do slice(offset,limit) is going to be replaced by slice(5,5) that returns an []. What u want is increasing the limit too if u increase the offset, for example:
const handleNext = () => {
setOffset(offset + limit);
setLimit(limit + limit)
}

React hooks - how to force useEffect to run when state changes to the same value?

So I'm building a drum-pad type of app, and almost everything is working, except this.
Edit: Put the whole thing on codesandbox, if anyone wants to have a look: codesandbox.io/s/sleepy-darwin-jc9b5?file=/src/App.js
const [index, setIndex] = useState(0);
const [text, setText] = useState("");
const [theSound] = useSound(drumBank[index].url)
function playThis(num) {
setIndex(num)
}
useEffect(()=>{
if (index !== null) {
setText(drumBank[index].id);
theSound(index);
console.log(index)
}
}, [index])
When I press a button, the index changes to the value associated with the button and then the useEffect hook plays the sound from an array at that index. However, when I press the same button more than once, it only plays once, because useState doesn't re-render the app when the index is changed to the same value.
I want to be able to press the same button multiple times and get the app to re-render, and therefore useEffect to run every time I press the button. Can anyone help me how to do this?
Here's what I could come up with from your sandbox.
According to the docs each useSound is just a single sound, so when trying to update an index into a soundbank to use via React state the sound played will always be at least one render cycle delayed. I suggest creating a new custom hook to encapsulate your 9 drum sounds.
useDrumBank consumes the drumbank array and instantiates the 9 drum sounds into an array.
const useDrumBank = (drumbank) => {
const [drum0] = useSound(drumbank[0].url);
const [drum1] = useSound(drumbank[1].url);
const [drum2] = useSound(drumbank[2].url);
const [drum3] = useSound(drumbank[3].url);
const [drum4] = useSound(drumbank[4].url);
const [drum5] = useSound(drumbank[5].url);
const [drum6] = useSound(drumbank[6].url);
const [drum7] = useSound(drumbank[7].url);
const [drum8] = useSound(drumbank[8].url);
return [drum0, drum1, drum2, drum3, drum4, drum5, drum6, drum7, drum8];
};
Update the component logic to pass the drumBank array to the new custom hook.
const sounds = useDrumBank(drumBank);
Here's the full code:
function App() {
useEffect(() => {
document.addEventListener("keypress", key);
return () => document.removeEventListener("keypress", key);
}, []);
const [text, setText] = useState("");
const sounds = useDrumBank(drumBank);
function playThis(index) {
drumBank[index]?.id && setText(drumBank[index].id);
sounds[index]();
}
function key(e) {
const index = drumBank.findIndex((drum) => drum.keyTrigger === e.key);
index !== -1 && playThis(index);
}
return (
<div id="drum-machine" className="drumpad-container">
<div id="display" className="drumpad-display">
<p>{text}</p>
</div>
<button className="drum-pad" id="drum-pad-1" onClick={() => playThis(0)}>
Q
</button>
<button className="drum-pad" id="drum-pad-2" onClick={() => playThis(1)}>
W
</button>
<button className="drum-pad" id="drum-pad-3" onClick={() => playThis(2)}>
E
</button>
<button className="drum-pad" id="drum-pad-4" onClick={() => playThis(3)}>
A
</button>
<button className="drum-pad" id="drum-pad-5" onClick={() => playThis(4)}>
S
</button>
<button className="drum-pad" id="drum-pad-6" onClick={() => playThis(5)}>
D
</button>
<button className="drum-pad" id="drum-pad-7" onClick={() => playThis(6)}>
Z
</button>
<button className="drum-pad" id="drum-pad-8" onClick={() => playThis(7)}>
X
</button>
<button className="drum-pad" id="drum-pad-9" onClick={() => playThis(8)}>
C
</button>
</div>
);
}
Demo
Usage Notes
No sounds immediately after load
For the user's sake, browsers don't allow websites to produce sound
until the user has interacted with them (eg. by clicking on
something). No sound will be produced until the user clicks, taps, or
triggers something.
Getting the keypresses to consistently work seems to be an issue and I don't immediately have a solution in mind, but at least at this point the button clicks work and the sounds are played synchronously.
Have you considered combining those discrete state variables (value types) into a single reference type state object?
Instead of having an effect that sets the text when the index changes you just set the entire state at the same time.
As long as you ensure this is a new object/reference then the effect will fire. The effect is then only responsible for playing the sound based on the current state.
Here's some sample code
const [state, setState] = useState({ index: 0, text: '', });
const playThis = (num) => {
setState({ index: num, text: drumBank[num].id, });
};
useEffect(() => {
theSound(state.index);
}, [state]);

When I run AudioBufferSourceNode.start() when I have multiple tracks, I sometimes get a delay

I am making an application that reads and plays two audio files.
CodeSnadBox
The above CodeSandBox has the following specifications.
Press the "play" button to play the audio.
The volume of each of the two audio tracks can be changed.
Problem
When playing audio, there is sometimes a delay.
However, there is not always an audio delay, and there are times when two tracks can be played back at exactly the same time.
Although not implemented in the CodeSandBox above, the application I am currently working on implements a seek bar to indicate the current playback position.
By moving the seek bar to indicate the current playback position, the audio is reloaded and the resulting delay may be cured.
On the other hand, moving the seek bar may cause a delay even though the audio was playing at exactly the same timing.
Anyway, is there a way to play multiple audio tracks at the same time in a stable and consistent manner?
Code
let ctx,
tr1,
tr2,
tr1gain = 0,
tr2gain = 0,
start = false;
const trackList = ["track1", "track2"];
const App = () => {
useEffect(() => {
ctx = new AudioContext();
tr1 = ctx.createBufferSource();
tr2 = ctx.createBufferSource();
tr1gain = ctx.createGain();
tr2gain = ctx.createGain();
trackList.forEach(async (item) => {
const res = await fetch("/" + item + ".mp3");
const arrayBuffer = await res.arrayBuffer();
const audioBuffer = await ctx.decodeAudioData(arrayBuffer);
item === "track1"
? (tr1.buffer = audioBuffer)
: (tr2.buffer = audioBuffer);
});
tr1.connect(tr1gain);
tr1gain.connect(ctx.destination);
tr2.connect(tr2gain);
tr2gain.connect(ctx.destination);
return () => ctx.close();
}, []);
const [playing, setPlaying] = useState(false);
const playAudio = () => {
if (!start) {
tr1.start();
tr2.start();
start = true;
}
ctx.resume();
setPlaying(true);
};
const pauseAudio = () => {
ctx.suspend();
setPlaying(false);
};
const changeVolume = (e) => {
const target = e.target.ariaLabel;
target === "track1"
? (tr1gain.gain.value = e.target.value)
: (tr2gain.gain.value = e.target.value);
};
const Inputs = trackList.map((item, index) => (
<div key={index}>
<span>{item}</span>
<input
type="range"
onChange={changeVolume}
step="any"
max="1"
aria-label={item}
/>
</div>
));
return (
<>
<button
onClick={playing ? pauseAudio : playAudio}
style={{ display: "block" }}
>
{playing ? "pause" : "play"}
</button>
{Inputs}
</>
);
};
When calling start() without a parameter it's the same as calling start with currentTime of the AudioContext as the first parameter. In your example that would look like this:
tr1.start(tr1.context.currentTime);
tr2.start(tr2.context.currentTime);
By definition the currentTime of an AudioContext increases over time. It's totally possible that this happens between the two calls. Therefore a first attempt to fix the problem could be to make sure both function calls use the same value.
const currentTime = tr1.context.currentTime;
tr1.start(currentTime);
tr2.start(currentTime);
Since currentTime usually increases by the time of a render quantum you could add an extra safety net by adding a little delay.
const currentTime = tr1.context.currentTime + 128 / tr1.context.sampleRate;
tr1.start(currentTime);
tr2.start(currentTime);
If this doesn't help you could also use an OfflineAudioContext to render your mix upfront into a single AudioBuffer.

Callback doesn't update state

The below function handle uploaded files, for some reason the setFiles doesn't update the files list after the callback ends so it causes the previous uploaded file to show up on the page, for example the user uploaded an image 1.jpg, nothing will show up on the page, next the user uploads a second file- now the first image 1.jpg will show up, and so on.
On setFiles the state is correct and updated but the return doesn't update the files state.
Any idea why?
const [files, setFiles] = useState([])
const addFiles = addedFiles => {
const newFiles = Array.from(addedFiles, file => newFileDecorator(file))
setFiles([...files, ...newFiles])
newFiles.forEach(file => {
file.reader.onload = async () => {
const dimensions = await getImageDimensions(file.reader.result)
setFiles(state => {
const index = state.findIndex(f => f.id === file.id)
state[index].readyState = file.reader.readyState
state[index].dimensions = dimensions
return state
})
}
file.reader.readAsDataURL(file.data)
})
}
You are mutating state without creating a new reference for it, so React skips the update as the shallow comparison indicates that they are the same object. Use this pattern instead.
setFiles(state => {
const file = state.find(f => f.id === file.id)
file.readyState = file.reader.readyState
file.dimensions = dimensions
return [ ...state, file ]
})

Categories