I am trying to improve my skills in React here and I wanted to build my portfolio, so
I decided to have custom slider instead of using a library, so I use the following code:
const ProjectWrapper = styled.div`
.container {
transform: translateX(-${(props) => props.activeIndex * 200}px);
transition: transform 0.3s;
display: flex;
flex-direction: column;
margin: 20px;
border: 1px solid gray;
border-radius: 5px;
padding: 20px;
text-align: justify;
color: white;
text-decoration: none;
height: 450px;
}
`;
export default function Portfolio() {
const [activeIndex, setActiveIndex] = useState(0);
const updateIndex = (newIndex) => {
if (newIndex < 0) {
newIndex = projects.count - 1;
} else if (newIndex >= projects.count) {
newIndex = 0;
}
setActiveIndex(newIndex);
};
return (
<div>
<Arrow>
<ProjectWrapper activeIndex={activeIndex}>
{projects.map((el, idx) => {
return (
<a key={idx} className="container" href={el.url}>
<div>
<div className="img">
<img src={el.image} alt={el.title} />
</div>
<div className="row">
<h3 className="title">
{el.title}
<a target="_blank" href={el.github}>
{el.github === "" ? (
""
) : (
<i className="fa-brands fa-github fa-lg"></i>
)}
</a>
</h3>
<div className="desc">
<p>{el.description}</p>
</div>
<p>
<b>Technologies:</b> {el.resources}
</p>
</div>
</div>
</a>
);
})}
</ProjectWrapper>
<button
onClick={() => {
updateIndex(activeIndex - 1);
}}
>
{" "}
<i class="fa-solid fa-angle-left"></i>
</button>
<button
onClick={() => {
updateIndex(activeIndex + 1);
}}
>
<i class="fa-solid fa-angle-right"></i>
</button>
</Arrow>
</div>
);
}
It is working pretty fine except for two issues:
After showing the last card I want arrow for next to not be clickable;
After going next, and then clicking back to the very beginning, the arror for next is not clickable anymore. After refreshing the page, I am able again to go to the next cards.
Anyone any idea what can I improve on my code here?
To disable the "Next" button, add a conditional disabled flag to it. This example will disable the button when the state is equal to the total number of projects:
<button
onClick={() => {
updateIndex(activeIndex + 1);
}}
disabled={activeIndex === (projects.count - 1)}
/>
For your second problem, your function is currently setting the index to 0 once you reach the last slide. So you are on slide 5/5, but your function is setting the index to 0:
else if (newIndex >= projects.count) {
newIndex = 0;
}
This is muddling the logic, so I would recommend removing updateIndex() entirely and writing your buttons like so:
<button
onClick={() => setActiveIndex((prevIndex) => prevIndex + 1)}
>
<i class="fa-solid fa-angle-right"></i>
</button>
Or - 1 for the back button.
Related
I am trying to do a horizontal slider with buttons. So i have 6 cards and 6 dot buttons. As i click for example in the second button with id of 1 it should slide to the second image. But fore some reason the scroll event is not working.
This is my css:
#media only screen and (max-width: 810px){
.fake-magicwall ul{
overflow: hidden;
overflow-x: auto;
flex-wrap: nowrap;
}
.dot{
width: 15px;
height: 15px;
border-radius: 50%;
margin: 10px;
}
}
And this is my component:
import React from 'react'
import './Work.css'
import Cta from '../atoms/Cta'
import Headphone from '../assets/images/headphones3.png'
import Calendar from '../assets/images/calendar.png'
import DevConnector from '../assets/images/dev.png'
import Bulls from '../assets/images/bulls2.png'
import Expenses from '../assets/images/reactExpenses.png'
import SixFlags from '../assets/images/sixflags.png'
import useDots from '../hooks/useDots';
const images = [Calendar, Headphone, SixFlags, DevConnector, Bulls, Expenses];
const Work = () => {
const [activeIndex, setActiveIndex] = useDots(images.length);
function handleDotClick(index) {
console.log("Clicked dot with index:", index);
setActiveIndex(index);
const targetCard = document.querySelector(`[data-id="${index}"]`);
console.log("Target card:", targetCard);
if (targetCard) {
const magicWall = document.getElementById('home-magicwall');
console.log("Magic wall:", magicWall);
console.log("Target card offsetLeft:", targetCard.offsetLeft);
console.log("Magic wall offsetLeft:", magicWall.offsetLeft);
magicWall.scroll({
left: targetCard.offsetLeft - magicWall.offsetLeft,
behavior: 'smooth'
});
}
}
return (
<div className="main-container">
<section id="section-work">
<div id="header">
<h2>My Work</h2>
</div>
<div className="text-zone-2">
<div>
<p>
A small gallery of recent projects chosen by me. I've done them all together with amazing people from
companies around the globe. It's only a drop in the ocean compared to the entire list. Interested to see
some more? Visit my work page.
<br />
</p>
</div>
<div className="btn-container">
<Cta className="btn" link="https://github.com/mateoghidini1998" content="See More" />
</div>
</div>
<div className="fake-big-2">Work</div>
</section>
<div className="dots">
{images.map((image, index) => (
<button
key={index}
className={`dot ${index === activeIndex ? 'active' : ''}`}
onClick={() => handleDotClick(index)}
></button>
))}
</div>
<div id="home-magicwall" className="fake-magicwall">
<ul>
{images.map((image, index) => (
<li key={index} className={`magic-wall_item ${index === activeIndex ? 'active' : ''}`}>
<img src={image} alt="image" />
<div className="hover-content"></div>
<a href="/" className="colorbox" data-id={index}></a>
</li>
))}
</ul>
</div>
</div>
);
};
export default Work;
This is my custom hook:
import { useState } from 'react';
function useDots(images) {
const [activeIndex, setActiveIndex] = useState(0);
return [activeIndex, setActiveIndex];
}
export default useDots;
I did some console log and the index of the dot i am clicking is correct.
The data-id on the <a> tags is correct.
I also get Target card offsetLeft: 133
and Magic wall offsetLeft: 0 Every time i click in a button
Any reason why it is not scrolling?
You have applied the overflow-x: auto to the ul element inside of the home-magicwall div and not the home-magicwall div itself so that technically isn't scrollable. Try the ul instead:
if (targetCard) {
const magicWall = document.querySelector('.home-magicwall ul');
console.log("Magic wall:", magicWall);
console.log("Target card offsetLeft:", targetCard.offsetLeft);
console.log("Magic wall offsetLeft:", magicWall.offsetLeft);
magicWall.scroll({
left: targetCard.offsetLeft - magicWall.offsetLeft,
behavior: 'smooth'
});
}
I just want to add a fade in animation to next index. i found a package called "react transition group" but all tutorials were based on class components or redux I didn't understand anything.
const AboutTestimonials = () => {
const [index, setIndex] = useState<any>(0);
const [data] = useState(AddTestimonial);
const current = data[index];
return (
<div className="testimonials__container">
<div className="testimonials__description">
<h3>TESTIMONIALS</h3>
<p>{current.quote}</p>
<h5 className="author__testimonials">{current.postedby}</h5>
<h6 className="job__testimonials">{current.profession}</h6>
</div>
<div className="testimonials__pagination">
<Image
src={leftarrow}
alt="arrow"
onClick={() => setIndex(index > 0 ? index - 1 : index)}
className="pagination__arrows"
/>
<p>{index + 1} / 5</p>
<Image
src={rightarrow}
alt="arrow"
onClick={() => setIndex(index < 4 ? index + 1 : index)}
className="pagination__arrows"
/>
</div>
SwitchTransition waits for the old child to exit then render the new child as mentioned in the react transition group website.
there are two modes:
in-out
out-in
the important factor is changing the key prop of the child component.child component could be CSSTransition or Transition.if you want the transition changes simultaneously you can use the TransitionGroup.
side note: if you want to use the AddTestimonial in your component and you don't want to change the state (I noticed there is no second argument for setting the data), there is no need to declare a useState.it is much better to set AddTestimonial as a prop on AboutTestimonials component
import { CSSTransition, SwitchTransition } from 'react-transition-group';
const AboutTestimonials = () => {
const [index, setIndex] = useState<any>(0);
const [data] = useState(AddTestimonial);
const current = data[index];
return (
<div className="testimonials__container">
<div className="testimonials__description">
<h3>TESTIMONIALS</h3>
<SwitchTransition mode={'out-in'} >
<CSSTransition
key={index}
timeout={300}
classNames="fade"
>
<>
<p>{current.quote}</p>
<h5 className="author__testimonials">{current.postedby}</h5>
<h6 className="job__testimonials">{current.profession}</h6>
</>
</CSSTransition>
</SwitchTransition>
</div>
<div className="testimonials__pagination">
<Image
src={leftarrow}
alt="arrow"
onClick={() => setIndex(index > 0 ? index - 1 : index)}
className="pagination__arrows"
/>
<p>{index + 1} / 5</p>
<Image
src={rightarrow}
alt="arrow"
onClick={() => setIndex(index < 4 ? index + 1 : index)}
className="pagination__arrows"
/>
</div>
)}
css:
.fade-enter {
opacity: 0;
}
.fade-enter-active {
opacity: 1;
transition: opacity 300ms;
}
.fade-exit {
opacity: 1;
}
.fade-exit-active {
opacity: 0;
transition: opacity 300ms;
}
I have 3 buttons in a group and a form. When I select a button it stays active which is correct behaviour. However if I click "submit" on the form, the previously selected button is not active anymore. How do I keep the button active even after submitting the form? I'm using NextJS and css styles.
const [position, setPosition] = useState("");
const categoryOptions = ["Left", "Middle", "Right"];
return (
<div className={styles.card}>
{categoryOptions.map((category) => (
<button
type="button"
className={styles.buttonGroup}
onClick={() => setPosition(category)}
>
{category}
</button>
))}
<form className={styles.form} onSubmit={someFunction}>
<label htmlFor="amount">Amount</label>
<input
id={props.id}
name="amount"
type="number"
required
/>
<button className={styles.button} type="submit">
Submit amount
</button>
</form>
</div>
)
here is the css for buttons
.buttonGroup {
color: #000000;
border-radius: 0px;
border: 0px;
min-width: 80px;
min-height: 30px;
}
.buttonGroup:hover {
color: red;
}
.buttonGroup:focus,
.buttonGroup:active {
color: red;
font-weight: bold;
}
You can call useRef to keep your active element, and then call focus to regain your previous button focus state. For example
const [position, setPosition] = useState("");
const categoryOptions = ["Left", "Middle", "Right"];
const currentCategoryRef = useRef()
return (
<div className={styles.card}>
{categoryOptions.map((category) => (
<button
type="button"
className={styles.buttonGroup}
onClick={(event) => {
setPosition(category)
currentCategoryRef.current = event.target //set current active button
}}
>
{category}
</button>
))}
<form className={styles.form} onSubmit={someFunction}>
<label htmlFor="amount">Amount</label>
<input
id={props.id}
name="amount"
type="number"
required
/>
<button className={styles.button} type="submit" onClick={() => {
setTimeout(() => currentCategoryRef.current && currentCategoryRef.current.focus()) //focus back to the previous button in categories
}}>
Submit amount
</button>
</form>
</div>
)
I create a function when I click outside of the sidebar it will hide it and I also have a button that toggles show and hide the sidebar. But when I combined both of them together, the button did not work properly, it only show the sidebar but can't close it, only when I click outside it will close the sidebar
Click OutSide to close function:
const ref = useRef(null);
useEffect(() => {
document.addEventListener("mousedown", Clickout);
return () => {
document.removeEventListener("mousedown", Clickout);
};
}, []);
const Clickout = (eve) => {
if (ref.current && !ref.current.contains(eve.target)) {
setShow(false);
}
};
My Return:
return (
<header>
<div className="head">
<div className="logo">
<img src={logo} alt="logo" />
</div>
<button
className="burger"
onClick={() => {
setShow(!showMenu);
console.log("here");
}}
>
<div className={`${showMenu ? "change" : ""} bur1 `}></div>
<div className={`${showMenu ? "change" : ""} bur2 `}></div>
<div className={`${showMenu ? "change" : ""} bur3 `}></div>
</button>
</div>
<nav className={showMenu ? "active" : ""} ref={ref}>
<ul>
{navItem.map((item) => {
const { id, url, text } = item;
return (
<li key={id}>
<a href={url}>{text}</a>
</li>
);
})}
</ul>
</nav>
</header>
);
};
Nav bar CSS:
nav {
position: fixed;
right: -100%;
top: 0;
width: 60%;
height: 100vh;
text-align: center;
padding-top: 15vh;
transition: 0.8s ease;
background-color: blue;
}
nav.active {
right: 0;
transition: 0.5s;
}
Thank you.
you can use another state for manage button onclick when menu is open:
const [disableBtn, setDisableBtn] = useState(false);
and in Clickout function manage it:
const Clickout = (eve) => {
if (showMenu && ref.current && !ref.current.contains(eve.target)) {
setShow(false);
setDisableBtn(true)
} else {
setDisableBtn(false)
}
};
and in button for onclick use condition:
if (!disableBtn) setShow(true);
Updating state this way setShow(!showMenu) does not immediately update the state.Rather it schedules the update(You can read the docs). When your setState depends on your previous state (in this case showMenu depends on previous state) use this technique: (prev) => setState(!prev) instead. So, simply updating your onClick will solve the issue.
<button className="burger"
onClick={() => {
(prevShowMenu) => setShow(!prevShowMenu)
}}>
(Let me know in the comments if this was helpful)
I a week new in learning react coming from an angular background. I have the following unordered list in React.
const QueueManage: React.FC = () => {
const { queue, setQueue, loading, error } = useGetQueue();
const [btnState, setBtnState] = useState(state);
const enterIconLoading = (event: React.MouseEvent<HTMLElement, MouseEvent>) => {
const item = '';
const btn = '';
console.log(item, btn);
setBtnState({ loading: true, iconLoading: true, item: item, btnType: btn });
};
<ul className="listCont">
{queue.map(queueItem => (
<li className="col-12" key={queueItem.id}>
<div className="row">
<div className="listName col-3">
<p>{queueItem.user.firstName} {queueItem.user.lastName}</p>
</div>
<div className="listName col-5">
<div className="row">
<div className="col-3">
<Button type="primary" loading={btnState.loading} onClick={enterIconLoading}>
Assign
</Button>
</div>
<div className="col-3">
<Button type="primary" loading={btnState.loading} onClick={enterIconLoading}>
Absent
</Button>
</div>
<div className="col-3">
<Button type="primary" loading={btnState.loading} onClick={enterIconLoading}>
Done
</Button>
</div>
<div className="col-3">
<Button type="primary" loading={btnState.loading} onClick={enterIconLoading}>
Cancel
</Button>
</div>
</div>
</div>
</div>
</li>
)
)}
</ul>
}
For each list item, the list item will have for buttons, namely Assign, Absent, Done, Cancel. My goal is to identify which button was clicked and for which list item so that I can apply a loader for that specific button. Can any one please assist me with an explanation of how I can achieve this in my code
Here is a visual representation of the list that i get
https://i.imgur.com/kxcpxOo.png
At the moment went i click one button, all buttons are applied a spinner like below:
Your assistance and explanation is highly appreciated.
The Reactful approach involved splitting the li into a separate component. This will help keep each item's state separate. Let's call that QueueItem.
const QueueItem = ({ user }) => {
const [loading, setLoading] = useState(false)
function onClickAssign() {
setLoading(true)
// do something
setLoading(false)
}
function onClickAbsent() {
setLoading(true)
// do something
setLoading(false)
}
function onClickDone() {
setLoading(true)
// do something
setLoading(false)
}
function onClickCancel() {
setLoading(true)
// do something
setLoading(false)
}
return (
<li className='col-12'>
<div className='row'>
<div className='listName col-3'>
<p>
{user.firstName} {user.lastName}
</p>
</div>
<div className='listName col-5'>
<div className='row'>
<div className='col-3'>
<Button type='primary' loading={loading} onClick={onClickAssign}>
Assign
</Button>
</div>
<div className='col-3'>
<Button type='primary' loading={loading} onClick={onClickAbsent}>
Absent
</Button>
</div>
<div className='col-3'>
<Button type='primary' loading={loading} onClick={onClickDone}>
Done
</Button>
</div>
<div className='col-3'>
<Button type='primary' loading={loading} onClick={onClickCancel}>
Cancel
</Button>
</div>
</div>
</div>
</div>
</li>
)
}
Here I've also split out each button's onClick into a separate callback since they are well defined and probably have unique behaviours. Another approach mentioned above in a comment is
function onClickButton(action) {
...
}
<Button type='primary' loading={loading} onClick={() => onClickButton('cancel')}>
Cancel
</Button>
This follows the action / reducer pattern which might be applicable here instead of state (useState)
Move the buttons or the whole li to a component and let each list manage it's state.
// Get a hook function
const {useState} = React;
//pass the index of li as prop
const Buttons = ({ listId }) => {
const [clicked, setClickedButton] = useState(0);
return (
<div>
<button
className={clicked === 1 && "Button"}
onClick={() => setClickedButton(1)}
>
Assign
</button>
<button className={clicked === 2 && "Button"} onClick={() => setClickedButton(2)}>Absent</button>
<button className={clicked === 3 && "Button"} onClick={() => setClickedButton(3)}>Done</button>
<button className={clicked === 4 && "Button"} onClick={() => setClickedButton(4)}>Cancel</button>
</div>
);
};
// Render it
ReactDOM.render(
<Buttons />,
document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<style>
.Button {
background-color: #4CAF50; /* Green */
border: none;
color: white;
padding: 15px 32px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
}
</style>
<div id="react"></div>
In addition to the previous answer it's worth adding that making simple components (in our case buttons) stateful is often considered a bad practice as it gets harder to track all the state changes, and to use state from different buttons together (e.g. if you want to disable all 4 buttons in a row after any of them is pressed)
Take a look at the following implementation, where entire buttons state is contained within parent component
enum ButtonType {
ASSIGN, ABSENT, DONE, CANCEL
}
// this component is stateless and will render a button
const ActionButton = ({ label, loading, onClick }) =>
<Button type="primary" loading={loading} onClick={onClick}>
{label}
</Button>
/* inside the QueueManage component */
const [buttonsState, setButtonsState] = useState({})
const updateButton = (itemId: string, buttonType: ButtonType) => {
setButtonsState({
...buttonsState,
[itemId]: {
...(buttonsState[itemId] || {}),
[buttonType]: {
...(buttonsState[itemId]?.[buttonType] || {}),
loading: true,
}
}
})
}
const isButtonLoading = (itemId: string, buttonType: ButtonType) => {
return buttonsState[itemId]?.[buttonType]?.loading
}
return (
<ul className="listCont">
{queue.map(queueItem => (
<li className="col-12" key={queueItem.id}>
<div className="row">
<div className="listName col-3">
<p>{queueItem.user.firstName} {queueItem.user.lastName}</p>
</div>
<div className="listName col-5">
<div className="row">
<div className="col-3">
<ActionButton
label={'Assign'}
onClick={() => updateButton(queueItem.id, ButtonType.ASSIGN)}
loading={isButtonLoading(queueItem.id, ButtonType.ASSIGN)}
/>
</div>
<div className="col-3">
<ActionButton
label={'Absent'}
onClick={() => updateButton(queueItem.id, ButtonType.ABSENT)}
loading={isButtonLoading(queueItem.id, ButtonType.ABSENT)}
/>
</div>
<div className="col-3">
<ActionButton
label={'Done'}
onClick={() => updateButton(queueItem.id, ButtonType.DONE)}
loading={isButtonLoading(queueItem.id, ButtonType.DONE)}
/>
</div>
<div className="col-3">
<ActionButton
label={'Cancel'}
onClick={() => updateButton(queueItem.id, ButtonType.CANCEL)}
loading={isButtonLoading(queueItem.id, ButtonType.CANCEL)}
/>
</div>
</div>
</div>
</div>
</li>
)
)}
</ul>
)
The goal here is to keep buttons loading state in parent component and manage it from here. buttonsState is a multilevel object like
{
'23': {
[ButtonType.ASSIGN]: { loading: false },
[ButtonType.ABSENT]: { loading: false },
[ButtonType.DONE]: { loading: false },
[ButtonType.CANCEL]: { loading: false },
},
...
}
where keys are ids of queueItems and values describe the state of the 4 buttons for that item. It is usually preferred to use useReducer instead of nested spreading in updateButton but it is good to start with