Interactive carousel with react - javascript

I've been trying to create a component similar to instagram stories for a few hours. That is nothing more than an interactive carousel, where you can move forward and backward. The thing is, my strategy with setTimeOut fires every time I interact and does not cancel the state, so if the user is in the first photo and clicks 5x in a row to go to the sixth photo, in a few seconds the settimeout will be like a tsunami and the component advances 5x stories in a row starting from 6. I wanted somehow to reset my timer every time the user clicks on a new image. Seems crazy or is it possible?
I don't know if there is anyone with enough patience to see the code, if not, if someone at least knows a new approach. Thank you very much, I have been trying to resolve this for some time.
function Stories(){
const files = [ 'image1.jpg', 'image2.jpg' , 'video3.mp4' ]
const videoRef = useRef()
const imageRef = useRef()
const [ index , setIndex ] = useState(0); // the index starts at 0... first file...
const [ focus, setFocus ] = useState(null);
// Every time the index of the storie is changed. This effect happens.
useEffect(() => {
const video = videoRef.current;
const image = imageRef.current;
if( files[index].includes('mp4')){
video.style.display = "inline"
video.src = files[index];
// when i put it i put something in the "setFocus"
// triggers a useEffect for the "focus"
// that makes the setTimeOut which in the end runs
// this same script again after a certain time.
setFocus(files[index]);
// if there is any image in the DOM, hide it
if( image ) {
image.style.display = "none";
}
//In case the files [index] is an image.
} else {
image.style.display = "inline"
image.src = files[index];
setFocus(files[index])
// if there is any video in the DOM, hide it
if ( video ){
video.style.display = 'none';
video.muted = true
}
}
},[index])
function back(){
if( index <= 0 ) return
setIndex( index - 1);
}
function next(){
if ( index < files.length - 1) setIndex( index + 1 );
}
// This useeffect fires every time a file comes into focus.
useEffect(() => {
const timelapse = 5000;
// I wait for the video to finish to advance to the next index
if( files[index].includes('mp4')){
videoRef.current.addEventListener('ended',() => setIndex( index + 1 ));
}
// If it's an image, so..
else {
setTimeout(() => {
if( focus !== null && index < files.length - 1){
setIndex(index + 1);
}
}, timelapse )
}
},[focus])
// HTML
return <>
<video ref={videoRef}/>
<img ref={imageRef} />
<button onClick={back}> back </button>
<button onClick={next}> next </button>
</>
}

You can capture the ID of the timeout and cancel it like this:
const timeout = setTimeout(() => { console.log("woo") }, 5000)
window.clearTimeout(timeout)
So one approach would be to store that and cancel the timeout when your back or next functions are invoked.
Another approach would be to use state to record the remaining time and use a timer (setInterval) that counts down every second and advances the slide if it reaches zero. Then onClick you can just re-set the time remaining to 5 seconds.
const CarouselExample = () => {
const [time, setTime] = useState(5)
const [slide, setSlide] = setState(1)
useEffect(() => {
const countdown = window.setInterval(() => {
setTime(timeRemaining => {
if (timeRemaining - 1 < 0) {
setSlide(prevSlide => prevSlide + 1)
return 5
} else {
return timeRemaining - 1
}
})
}, 1000)
return () => {
window.clearInterval(countdown)
}
}, [])
return (
<div>
<div>Current Slide: {slide}</div>
<button
onClick={() => {
setTime(5)
setSlide(prev => prev + 1)
}}
>
Next
</button>
</div>
)
}

Related

Pure JavaScript: How to refresh page after countdown if the browser tab is active

I want to achieve the following scenario:
Set a timer of 60 seconds and start the countdown displaying the
timer. After the countdown is finished, refresh (not reload) the page
and restart the countdown timer and continue the process.
If the browser's tab is changed, after finishing the timer, refresh the
page and pause the timer, and only when the tab is active again refresh
the page and start the countdown timer again.
I have managed to achieve the first requirement like:
Result will Refresh in <span id="countdown"></span>
<script>
if (!document.hidden) {
function timedRefresh(e) {
var n = setInterval((function () {
e > 0 ? (e -= 1, document.getElementById("countdown").innerHTML = e) : (clearInterval(n), window.location.reload())
}), 1e3)
}
timedRefresh(59)
}
</script>
And is unable to achieve the second requirement. I have tried to implement the solution mentioned here: Is there a way to detect if a browser window is not currently active?
But it didn't work.
How do I achieve the requirements in pure JavaScript code?
The following code has solved this scenario. It's a little messy, but I think is understandable. We use both window.document.hasFocus() and the focus event to create this solution. I have changed the window.location.reload() methods to just consoles simulating a function that refresh the data inside the application, since is asked to us refresh (not reload).
function timedRefresh(intervalTime) {
let interval = null;
let currentTime = intervalTime;
const updateTimer = () => {
const countdownSpan = document.getElementById("countdown");
if (currentTime <= 0) {
console.log(`print refresh at ${new Date()}`); // refresh page
if (window.document.hasFocus()) {
currentTime = intervalTime;
} else {
clearInterval(interval);
interval = null;
currentTime = intervalTime;
}
}
if (!interval) {
countdownSpan.innerHTML = "#";
} else {
countdownSpan.innerHTML = currentTime;
currentTime -= 1;
}
};
interval = setInterval(updateTimer, 1000);
window.addEventListener("focus", () => {
if (!interval) {
interval = setInterval(updateTimer, 1000);
}
});
}
const TIMER_SECONDS = 5;
timedRefresh(TIMER_SECONDS);
Edit:
The answer above does not work with reloads using window.location.realod(). A code solving the issue may look like this:
const TIMER_SECONDS = 60;
/*
* Update span with current time
* `currentTime` is an integer bigger than zero or a string '#' meaning 'timer stopped'
*/
function updateDisplayTimer(currentTime){
const countdownSpan = document.getElementById("countdown");
countdownSpan.innerHTML = currentTime;
}
/*
* A timer with `loopDuration` seconds. After finished, the timer is cleared.
* `loopDuration` is an integer bigger than zero, representing the time duration in seconds.
*/
function timedRefresh(loopDuration) {
let currentTime = loopDuration;
let interval = setInterval(() => {
if (currentTime > 0) {
updateDisplayTimer(currentTime);
currentTime -= 1;
} else {
window.location.reload(); // refresh page
}
}, 1000);
window.addEventListener('load', () => {
if(window.document.hasFocus()){
timedRefresh(TIMER_SECONDS);
} else {
updateDisplayTimer("#");
clearInterval(interval);
interval = null;
}
});
window.addEventListener('focus', () => {
if(interval == null){
window.location.reload(); // refresh page
}
});
}
// exeute main fucntion
timedRefresh(TIMER_SECONDS);
Use window.document.hasFocus() to determine if the user is focused on your webpage or not.

Display a message based on variable value

I have a JavaScript function in a html page that returns a variable x.
Based on the value of x i want to display a message in the page.
if x has a value before 10 seconds have passed -> message: "Success"
if x has no value after 10 seconds -> message: "There is a problem"
if (x = 'y' before 10 seconds ){ -> message: "Success"}
else { message: "There is a problem"}
The problem is that i don't know how add that 10 seconds check , i was looking at the Timeout method but that didn't help me.
Use setTimeout, when you click button I stop interval and timer function because it is success, and when button isn't clicked and if x variable isn't y timer and interval continues countDown, I used setInterval for understanding how it works, also I edited code , I might this is what you want
const input = document.querySelector("input")
const button = document.querySelector("button")
const textTimer = document.querySelector("p")
let number = 10
let x = ""
textTimer.innerHTML = number
button.addEventListener("click", ()=> {
x = input.value
if(x === "y"){
alert("success")
clearTimeout(timer)
clearInterval(interval)
}
console.log(x)
})
const interval = setInterval(()=> {
number--
textTimer.innerHTML = number
if(number <= 0){
clearInterval(interval)
}
}, 1000)
const timer = setTimeout(()=> {
if(x.length > 0){
alert("Success")
} else {
alert("There is a problem")
}
}, 10000)
<input type="text">
<button>Insert Value in x</button>
<p></p>
The solution is .setTimeout() which allows you to run the function, command ... after an estimated time.
/*
// ES6
const checkVal = () => {
const input = document.querySelector('input').value;
// Ternary Operator
input === 'yes' ? alert('yes') : alert('no');
};
*/
function checkVal() {
const input = document.querySelector('input').value;
if(input.toLowerCase() === 'yes') {
alert('yes')
} else {
alert('no')
};
};
// 2000 means 2k miliseconds, if you want to set it on 10 seconds, then 10000
document.querySelector('button').addEventListener('click', () => {
setTimeout(checkVal
, 2000
)}
);
<p>If you type *yes*, it will alert yes. But if you type anything else(not yes) it will show *no*.</p>
<input type='text'>
<button type='text'>Click</button>

Can't clear the setInterval

I want to clear the interval when stop button is clicked it seems the clearInterval is not working there as expected. The timer doesn't stop when handleStop function is triggered or when handleReset id triggered
Here's what I am trying to do:
import React from "react"
import useApp from "../App"
import Interact from "./Interact";
const Timer = () => {
let timer;
const {millisec , setMillisec , sec , setSec , min , setMin , hr , setHr} = useApp();
const handleStart = () => {
timer = setInterval(() => {
setMillisec((prev) => {
if (prev === 100) {
setSec((prevB) =>{
if(prevB === 60){
setMin(prevC => {
if(prevC === 60){
setHr(prevD => prevD + 1)
return 0
}
return prevC + 1
})
return 0
}
return prevB + 1
});
return 0;
}
return prev + 1;
});
}, 10);
};
const handleStop = () => {
clearInterval(timer)
}
const handleReset = () => {
clearInterval(timer)
setMillisec(0)
setSec(0)
setMin(0)
setHr(0)
}
return(
<>
<div className="parent">
<div className="main">
<h1 className="heading">Stop Watch</h1>
<div className="timer">
<div className ="hour">{hr}</div>
<div className ="min">{min}</div>
<div className ="sec">{sec}</div>
<div className ="millisec">{millisec}</div>
</div>
<Interact handleStart = {handleStart}
handleStop = {handleStop}
handleReset ={handleReset}
/>
</div>
</div>
</>
)
}
export default Timer
variable timer re-declare every time when Timer component rendered. Thus, the setInterval reference is reset.
You can declare timer at global scope, like
...
import Interact from "./Interact";
let timer;
const Timer = () => {
const {millisec , setMillisec , sec , setSec , min , setMin , hr , setHr} = useApp();
const handleStart = () => {
...
or can also use react-hooks to maintain setInterwal reference. like this
Pass your setInterval() to a clearInterval() when you need to break it.

Stop loop on progressbar

I'm working with this progressbar:
https://codepen.io/thegamehasnoname/pen/JewZrm
The problem I have is it loops and what I want to achieve is:
stop on last progress slide (stop loop).
if the user is on last progressbar slide after it has stopped and I click on a .button-prev button it should start from the previous slide, not the first slide of the progressbar.
here is the code:
// swiper custom progressbar
const progressContainer = document.querySelector('.progress-container');
const progress = Array.from(document.querySelectorAll('.progress'));
const status = document.querySelector('.status');
const playNext = (e) => {
const current = e && e.target;
let next;
if (current) {
const currentIndex = progress.indexOf(current);
if (currentIndex < progress.length) {
next = progress[currentIndex+1];
}
current.classList.remove('active');
current.classList.add('passed');
}
if (!next) {
progress.map((el) => {
el.classList.remove('active');
el.classList.remove('passed');
})
next = progress[0];
}
next.classList.add('active');
}
progress.map(el => el.addEventListener("animationend", playNext, false));
playNext();
I tried adding this:
if (current) {
if (!next) {
$('.progress-container div').addClass('passed');
}
}
But the class passed gets deleted and the progressbar starts again.
This is the js of the previous button I have:
$(document).on('click', ".button-prev", function() {
$('.progress-container div.active').prev().removeClass('passed').addClass('active');
$('.progress-container div.active').next().removeClass('active');
});
any ideas on how to achieve this?
You almost have the fix ! Simply add a return statement to avoid to set the active calss to the first progress element.
if (current) {
const currentIndex = progress.indexOf(current);
if (currentIndex < progress.length) {
next = progress[currentIndex+1];
}
current.classList.remove('active');
current.classList.add('passed');
if (!next) {
$('.progress-container div').addClass('passed');
return;
}
}

Simple Carousel in React - How to register Scrolling to next Slide?

This is a relatively simple problem, but I haven't been able to solve it. I have built the following carousel / slider:
const BlogPostCardSlider = ({ children }) => {
const [activeSlide, setActiveSlide] = useState(0);
const activeSlideRef = useRef(null);
const wrapperRef = useRef(null);
const firstRenderRef = useRef(true);
useEffect(() => {
if (firstRenderRef.current) {
//this is checking whether its the first render of the component. If it is, we dont want useEffect to run.
firstRenderRef.current = false;
} else if (activeSlideRef.current) {
activeSlideRef.current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'nearest'
});
}
}, [activeSlide]);
const moveRight = () => {
if (activeSlide + 1 >= children.length) {
return children.length - 1;
}
return activeSlide + 1;
};
const moveLeft = () => {
if (activeSlide - 1 <= 0) {
return 0;
}
return activeSlide - 1;
};
return (
<div id="trap" tabIndex="0">
<button onClick={() => setActiveSlide(moveLeft)}>PREV</button>
{children.map((child, i) => {
return (
<SlideLink
key={`slideLink-${i}`}
isActive={activeSlide === i}
onClick={e => {
setActiveSlide(i);
}}
>
{i + 1}
</SlideLink>
);
})}
<Wrapper
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
if ((scrollLeft / width) % 1 === 0) {
setActiveSlide(scrollLeft / width);
}
}}
ref={wrapperRef}
>
{children.map((child, i) => {
return (
<Slide
key={`slide-${i}`}
ref={i === activeSlide ? activeSlideRef : null}
>
{child}
</Slide>
);
})}
</Wrapper>
<button onClick={() => setActiveSlide(moveRight)}>NEXT</button>
</div>
);
};
export default BlogPostCardSlider;
It displays its children as Slides in a carousel. You can navigate the carousel by pressing the 'NEXT' or 'PREVIOUS' buttons. There is also a component that shows you what slide you're on (e.g.: 1 2 *3* 4 etc.). I call these the SlideLink(s). Whenever the activeSlide updates, the SlideLink will update and highlight you the new current Slide.
Lastly, you can also navigate by scrolling. And here's the problem:
Whenever somebody scrolls, I am checking what slide they're on by doing some calculations:
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
if ((scrollLeft / width) % 1 === 0) {
setActiveSlide(scrollLeft / width);
}
}}
...the result of this is that setActiveSlide is only called once the slide completely in view. This results in a laggy experience : the user has scrolled to the next slide, the next slide is in view, but the calculation is not completed yet (since it is only completed once the Slide is 100% in view), so the active SlideLink gets updated very late in the process.
How would I solve this? Is there some way to optimistically update this?
You can modify the calculations little bit so that when the slide is more than 50% in the view, set the active property to that one.
onScroll={e => {
let { width } = wrapperRef.current.getBoundingClientRect();
let { scrollLeft } = wrapperRef.current;
setActiveSlide(Math.round(scrollLeft / width) + 1);
}}
Example: Carousel with width: 800px, Total slides: 3. So, scrollLeft will vary from 0 to 1600
From 0-400: Active slide = Math.round(scrollLeft / width) = 0th index
(or first slide)
From 400-1200: Active slide = 1st index (or second
slide) since it is more in the view
From 1200-1600: Active slide =
2nd index (or third slide) since it is more in the view
Hope it helps. Revert for any doubts.
At first sight I thought this should be a quick fix. Turned out a bit more was necessary. I create a working example here:
https://codesandbox.io/s/dark-field-g78zb?fontsize=14&hidenavigation=1&theme=dark
The main point is that setActiveSide should not be called during the onScroll event, but rather when the user is done with scrolling. This is achieved in the example using the onIdle react hook.

Categories