How to avoid closing DropDown on MouseOver? - reactjs

I made Navigation component with dynamic menu items.
`
import React, { useState } from "react";
import NavMenuItems from "../data/NavMenuItems";
function NavBar() {
const [dropDown, setDropDown] = useState({});
const setDropDownOpen = (name) => {
setDropDown({[name]: true });
};
const setDropDownClose = (name) => {
setDropDown({[name]: false });
};
return (
<div className="flex flex-row my-2 mx-5">
{NavMenuItems.map((menu, index) => (
<>
<div key={menu.item} className="relative flex flex-col mx-1">
<div
className="bg-[#121C24] px-2 h-5 text-white text-sm hover:bg-green-700 hover:text-black hover:cursor-pointer "
onMouseEnter={() => setDropDownOpen(menu.item)}
onMouseLeave={() => setDropDownClose(menu.item)}
>
{menu.item}
</div>
{dropDown[menu.item] && (
<div className="bg-slate-200 absolute top-6 px-4 py-2"
onMouseEnter={() => setDropDownOpen(menu.item)}
onMouseLeave={() => setDropDownClose(menu.item)}
>
{menu.subitems.map((submenu, index) => (
<div key={index}>{submenu}</div>
))}
</div>
)}
</div>
</>
))}
</div>
);
}
export default NavBar;
NavMenuItems.js
`
const NavMenuItems = [
{
item: "Events",
subitems: ["Event1", "Event2", "Event2"],
},
{
item: "Reports",
subitems: ["Reports1", "Reports2", "Reports3"],
},
];
export default NavMenuItems
When i mouseover on tabs, its working fine. but when i move over dropdown sub menu, it closes and cant select anything in submenu items.
Can someone help with this?

Related

Nextjs navbar active class only becomes active on a second click

I'm trying to create header component for next.js/tailwindcss app. The nav active class only shows active when clicked on a second time. I'd like for it to be active upon the first click. Where am I going wrong? What element should I target with tailwindcss to reflect active state?
navlink.js file:
import Link from 'next/link';
const NavItem = ({ text, href, active }) => {
return (
<Link href={href}>
<a
className={`nav__item ${
active ? 'active underline underline-offset-8' : ''
}`}
>
{text}
</a>
</Link>
);
};
export default NavItem;
header.js :
import Image from 'next/image';
import React, { useState } from 'react';
import NavItem from './NavItem';
const MENU_LIST = [
{ text: 'About', href: '/about' },
{ text: 'My Work', href: '/MyWork' },
{ text: 'Blog', href: '/blog' },
{ text: 'Contact', href: '/contact' },
];
const Header = () => {
const [navActive, setNavActive] = useState(null);
const [activeIdx, setActiveIdx] = useState(-1);
return (
<header className="bg-white">
<nav className="max-w-5xl mx-auto border border-top-gray">
{/*containment div*/}
<div className="flex justify-between">
{/*Logo Container*/}
<div className="cursor-pointer">
<a href="/">
<Image
src="/../public/images/soulLogo.webp"
alt="site logo"
width={233}
height={144}
/>
</a>
</div>
{/*Link Container*/}
<div className="hidden md:flex">
<div
onClick={() => setNavActive(!navActive)}
className={`nav__menu-bar md:underline underline-offset-8 decoration-black `}
></div>
<div
className={`${
navActive
? 'active underline underline-offset-8 decoration-black'
: ''
} nav__menu-list flex items-center space-x-8 text-gray-700 tracking-wider `}
>
{MENU_LIST.map((menu, idx) => (
<div
onClick={() => {
setActiveIdx(idx);
setNavActive(false);
}}
key={menu.text}
>
<NavItem active={activeIdx === idx} {...menu} />
</div>
))}
</div>
</div>
{/*Mobile Menu button*/}
<div className="flex relative flex-col gap-y-2 cursor-pointer pt-14 pr-3 md:hidden">
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
<div className="w-24 h-1 bg-black shadow-gray-700 rounded"></div>
</div>
{/*Mobile Menu*/}
<
</div>
</nav>
</header>
);
};
export default Header;
I think we used the same code and I ran into the same problem.
I fixed it with with using next/router and comparing the paths inside the NavItem.js component.
navitem.js
import Link from "next/link";
import { useRouter } from 'next/router';
const NavItem = ({ href, text }) => {
const router = useRouter();
const currentRoute = router.pathname;
return (
<Link href={href} className={currentRoute === `${href}` ? 'active' : ''}> {text} </Link>
);
};
export default NavItem;
navbar.js
{PRIMARY_NAVIGATION_LIST.map((menu) => (
<div key={menu.text} >
<NavItem
href={menu.href}
text={menu.text}
/>
</div>
))}

How can I display the button only once?

I'm passing all the user's data to the card component, but I want to remove the card when I click the button, without rendering the button more than one time. Is it possible to achieve that?
The cards are stacked on top of each other.
Thanks in advance!
This is where I'm getting the data and controlling the button click
const [user, setUser] = React.useState(null)
const [selectedUser, setSlectedUser] = React.useState(0)
const getUsers = async () => {
try{
const response = await axios.get('http://localhost:8000/users')
setUser(response.data)
console.log(response.data)
}
catch(err){
console.log(err)
}
}
useEffect(()=>{
getUsers()
}, [])
const handleCardClick = (userId) => {
setSlectedUser(userId)
}
const handleRemove = () => {
setUser(user.filter((user)=> user.userId !== selectedUser))
}
And this is where I'm rendering it.
<div>
{user && user.map(user => (
<div>
<Card
country={user.country}
name={user.name}
about={user.about}
photo={user.photo}
onClick={() => handleCardClick(user.userId)}/>
</div>
))}
<button className='btn-cards text-center' onClick={handleRemove}>DELETE</button>
</div>
This is the card component
import React from 'react'
const Card = ({name, about, photo, country}) => {
return (
//create a card to display the user's profile
<div className='bg-white rounded-3xl shadow-lg p-6 mb-4 card'>
<div className='flex flex-row justify-between'>
<div className='flex flex-col'>
<img className='rounded-2xl w-96 h-96 object-cover' src={photo} alt="" />
<h1 className='text-2xl'>{name}</h1>
<h1 className='text-xl'>{country}</h1>
<h2 className='text-xl'>{about}</h2>
</div>
</div>
</div>
)
}
export default Card
The state:
// In this stae var you will save the selected user ID
const [selectedUser, setSlectedUser] = useState(0)
The handlers:
const handleCardClick = (userId) => {
setSlectedUser(userId)
}
const handleRemove = () => {
setUser(user.filter((user)=> user.userId !== selectedUser))
}
The card item inside the list:
<Card
country={user.country}
name={user.name}
about={user.about}
photo={user.photo}
onClick={() => handleCardClick(user.userId)}/>
The button, in whatever place you like:
<button className='btn-cards text-center' onClick={handleRemove}>DELETE</button>
By the way your root 'div' in the list needs a key, I suggest to use the user's id: <div key={user.userId}>
Card component receiving the onClick method as a props:
const Card = ({name, about, photo, country, onClick}) => {
return (
//create a card to display the user's profile
<div className='bg-white rounded-3xl shadow-lg p-6 mb-4 card' onClick={onClick}>
<div className='flex flex-row justify-between'>
<div className='flex flex-col'>
<img className='rounded-2xl w-96 h-96 object-cover' src={photo} alt="" />
<h1 className='text-2xl'>{name}</h1>
<h1 className='text-xl'>{country}</h1>
<h2 className='text-xl'>{about}</h2>
</div>
</div>
</div>
)
}

Group Disclosures (Accordian) from Headless UI

I've just started using Headless UI. I'm trying to use the Disclosure component from Headless UI to render my job experiences.
Basically, I need "n" number of Disclosures which will be dynamically rendered and whenever one Disclosure is opened the others should close.
I am able to render the Disclosures dynamically, and they all have their individual states. (opening/closing a disclosure doesn't affect the other Disclosure).
All I want to do is to have only one disclosure open at a time. Opening another Disclosure should close all the remaining Disclosures.
I have gone through their docs but couldn't find a way to manage multiple Disclosure states together.
Here is my code:
import React, { useContext } from "react";
import { GlobalContext } from "../data/GlobalContext";
import { Tab, Disclosure } from "#headlessui/react";
import ReactMarkdown from "react-markdown";
const Experience = () => {
const { data } = useContext(GlobalContext);
const expData = data.pageContent.find(
(content) => content.__component === "page-content.experience-page-content"
);
return (
<div className="container h-screen">
<div className="flex h-full flex-col items-center justify-center">
<h3 className="">{expData.pageTitle}</h3>
<div className="flex min-h-[600px] flex-col">
{expData.jobs.map((job, i) => (
<Disclosure key={job.companyName} defaultOpen={i === 0}>
<Disclosure.Button
key={job.companyName + "_tab"}
className="px-4 py-3 dark:text-dark-primary"
>
{job.companyName}
</Disclosure.Button>
<Disclosure.Panel key={job.companyName + "_panel"}>
<p className="">
<span className="">{job.designation}</span>
<span className="">{" # "}</span>
<span className="">{job.companyName}</span>
</p>
<p className="">{job.range}</p>
<ReactMarkdown className="">
{job.workDescription}
</ReactMarkdown>
</Disclosure.Panel>
</Disclosure>
))}
</div>
</div>
</div>
);
};
export default Experience;
It would be really helpful if someone could help me with this.
Thanks.
Ok, I stole from various sources and managed to hack it. I haven't tested it for accessibility but it has some interesting things because it deviates a little bit (rather usefully if you ask me) from the React mental model.
The tldr is that you will need to trigger clicks on the other elements imperatively via ref.current?.click()
Here are the steps:
1) Create the refs:
Here we can't use hooks since you can't call hooks inside loops or conditionals, we use React.createRef<HTMLButtonElement>() instead
const refs = React.useMemo(() => {
return (
items.map(() => {
return React.createRef<HTMLButtonElement>();
}) ?? []
);
}, [items]);
2) Add the corresponding ref to the Disclosure.Button component
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button ref={refs[idx]}>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
3) Use data attributes (for making it easy on yourself)
this one is gonna be specially useful for the next step
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button
ref={refs[idx]}
data-id={item.id}
data-open={open}
>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
4) define your handleClosingOthers function (an onClick handler)
Basically here we get all the buttons that aren't the one that the user is clicking, verifying if they are open and if they are, clicking programmatically on them to close them.
function handleClosingOthers(id: string) {
const otherRefs = refs.filter((ref) => {
return ref.current?.getAttribute("data-id") !== id;
});
otherRefs.forEach((ref) => {
const isOpen = ref.current?.getAttribute("data-open") === "true";
if (isOpen) {
ref.current?.click();
}
});
}
5) finally we add that function to the onClick handler
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({open}) => (
<>
{/* other relevant stuff */}
<Disclosure.Button
ref={refs[idx]}
data-id={item.id}
data-open={open}
onClick={() => handleClosingOthers(item.id)}
>
Button
</Disclosure.Button>
<Disclosure.Panel>
{/* more stuff */}
</Disclosure.Panel>
</>
)}
</Disclosure>)
)}
<template>
<div class="mx-auto w-full max-w-md space-y-3 rounded-2xl bg-white p-2">
<Disclosure v-for="(i, idx) in 3" :key="i" v-slot="{ open, close }">
<DisclosureButton
:ref="el => (disclosure[idx] = close)"
class="flex w-full justify-between rounded-lg bg-purple-100 px-4 py-2 text-left text-sm font-medium text-purple-900 hover:bg-purple-200 focus:outline-none focus-visible:ring focus-visible:ring-purple-500 focus-visible:ring-opacity-75"
#click="hideOther(idx)"
>
<span> What is your refund policy? {{ open }} </span>
</DisclosureButton>
<DisclosurePanel class="px-4 pt-4 pb-2 text-sm text-gray-500">
If you're unhappy with your purchase for any reason, email us within 90 days and we'll
refund you in full, no questions asked.
</DisclosurePanel>
</Disclosure>
</div>
</template>
<script setup>
import { Disclosure, DisclosureButton, DisclosurePanel } from '#headlessui/vue'
const disclosure = ref([])
const hideOther = id => {
disclosure.value.filter((d, i) => i !== id).forEach(c => c())
}
</script>
here how I did it in Vue.
I've used this approach:
function Akkordion({ items }) {
const buttonRefs = useRef([]);
const openedRef = useRef(null);
const clickRecent = (index) => {
const clickedButton = buttonRefs.current[index];
if (clickedButton === openedRef.current) {
openedRef.current = null;
return;
}
if (Boolean(openedRef.current?.getAttribute("data-value"))) {
openedRef.current?.click();
}
openedRef.current = clickedButton;
};
return (
<div>
{items.map((item, idx) => (
<Disclosure key={item.id}>
{({ open }) => (
<div>
<Disclosure.Button as="div">
<button
data-value={open}
ref={(ref) => {
buttonRefs.current[idx] = ref;
}}
onClick={() => clickRecent(idx)}
>
{item.label}
</button>
</Disclosure.Button>
<Disclosure.Panel
>
{item.content}
</Disclosure.Panel>
</div>
)}
</Disclosure>
))}
</div>
);
}

Reactjs setInterval with scroll only trigger once

I have auto scroll function and scroll will start when user click the function as follow.
It is scroll 50px to y axis once when user click play button. But it is only scroll once even thought I have added interval. Interval is working because I saw the "scrolling" console.log is increasing. But scroll is not scrolling again.
May I know why scroll is not move again?
import React, { useState, useEffect, useRef } from "react";
import { useParams, NavLink } from "react-router-dom";
import { useQuery } from "#apollo/client";
import { getTrack, getTrackVariable } from "../../gql/track";
import ChordSheetJS from "chordsheetjs";
import {
YoutubeIcon,
FacebookIcon,
PlayIcon,
PauseIcon,
} from "../../assets/icons/svg_icons";
import { SettingIcon } from "../../assets/icons/svg_icons";
import paths from "../../routes/paths";
import { FacebookShareButton } from "react-share";
import GoTop from "../../components/go_top";
const TrackPage = () => {
const intervalId = useRef(null);
const { trackId } = useParams();
const [track, setTrack] = useState();
const [collapse, setCollapse] = useState(true);
const [play, setPlay] = useState(false);
const [speed, setSpeed] = useState(1);
const { loading, error, data } = useQuery(getTrack, {
variables: getTrackVariable(trackId),
});
const trackRef = useRef();
useEffect(() => {
if (!loading && !error) {
setTrack(data?.track);
}
}, [loading, error, data]);
const getChordSheet = (value) => {
const parser = new ChordSheetJS.ChordProParser();
const song = parser.parse(value);
const formatter = new ChordSheetJS.HtmlTableFormatter();
const chordSheet = formatter.format(song);
return chordSheet;
};
const handleError = (e) => {
e.target.onerror = null;
e.target.src = Monk;
};
const handleMenuCollapse = (e) => {
e.preventDefault();
setCollapse(!collapse);
};
const handleSpeedUp = () => {
setSpeed(speed + 1);
};
const handleSpeedDown = () => {
setSpeed(speed - 1);
};
const handleScroll = () => {
setPlay(!play);
if (play) {
console.log("stop");
clearInterval(intervalId.current);
} else {
let delayInMs = 100;
const onScrollStep = () => {
document.getElementById("content").scroll(0,50);
console.log("srolling")
};
intervalId.current = setInterval(onScrollStep, delayInMs);
console.log("play");
}
};
return (
<>
<div id="setting">
{/** the big div */}
<div
className={` w-36 h-56 bg-primary absolute top-[calc((100vh-384px)/2)] ${
collapse ? "hidden" : "right-0"
} " bg-primary rounded-b-lg items-center justify-center`}
>
<div>
<div className="items-center justify-center mt-5">
<div className="flex text-xs items-center justify-center ">
<span className=" text-sm text-white">Scroll</span>
</div>
<div className="flex text-xs pt-0 mt-0 items-center justify-center ">
<button
className="px-2 btn-sm flex w-20 items-center bg-transparent hover:bg-accent border text-white font-semibold hover:text-white border-white hover:border-transparent rounded "
onClick={handleScroll}
>
{play ? (
<PauseIcon className="text-white mr-2" />
) : (
<PlayIcon className="text-white mr-2" />
)}
{play ? <span>Pause</span> : <span>Play</span>}
</button>
</div>
<div className="flex text-xs items-center justify-center mt-2">
<button
className="w-auto bg-transparent mr-2 hover:bg-accent text-white font-semibold hover:text-white py-1 px-2 border border-white hover:border-transparent rounded"
onClick={handleSpeedDown}
>
-1
</button>
<button
className="w-auto bg-transparent ml-2 hover:bg-accent text-white font-semibold hover:text-white py-1 px-2 border border-white hover:border-transparent rounded"
onClick={handleSpeedUp}
>
+1
</button>
</div>
</div>
</div>
</div>
{/** the icon div */}
<div
className={`flex w-12 absolute top-[calc((100vh-384px)/2)] h-12 bg-primary
${collapse ? "animate-pulse right-0" : "right-36"}
cursor-pointer bg-primary rounded-l-lg items-center justify-center`}
onClick={handleMenuCollapse}
>
{/* <div className="w-5 h-5 bg-white rounded-full " /> */}
<SettingIcon />
</div>
</div>
<div id="track" ref={trackRef}>
<div className="flex flex-col w-full py-1 my-1 items-center bg-gray-50">
<div className="relative my-6 mx-auto md:min-w-[60%] max-h-full">
{track ? (
<div className="w-full">
<pre
className="px-5 textarea"
dangerouslySetInnerHTML={{
__html: getChordSheet(track.lyric),
}}
/>
</div>
) : (
<div></div>
)}
</div>
</div>
</div>
</>
);
};
export default TrackPage;
app.jsx
import React, { useState, useEffect } from "react";
import Header from "./components/header";
import SideMenu from "./components/side_menu";
import AppRoutes from "./routes";
import withUser from "./hocs/with_user";
import { isMobile } from "react-device-detect";
import { useLocation } from "react-router-dom";
import { AuthProvider, setAccessToken } from "./auth/auth_provider";
import { Toaster } from "react-hot-toast";
import AppContext from "./components/app_context";
import "./i18n";
import "./App.css";
function App(props) {
const [collapse, setCollapse] = useState(isMobile);
const [sideBarFull] = useState(true);
const location = useLocation();
const IsNormalPage = () => {
const blankPages = ["/login"];
for (let i = 0; i < blankPages.length; i++) {
if (location.pathname.startsWith(blankPages[i])) return
false;
}
return true;
};
useEffect(() => {
if (props.user) setAccessToken(props.user.t);
}, []);
const PageHeader = () => {
return (
<div className="h-[72px] w-full flex items-center align-middle justify-center bg-neutral shadow">
<div className="w-full text-center">
<Header />
</div>
</div>
);
};
return (
<AuthProvider user={props.user}>
<AppContext.Provider
value={{
collapse: collapse,
setCollapse: setCollapse,
}}
>
<div className="relative w-full min-h-screen h-full">
<div className="flex flex-row min-h-screen">
<div className="w-auto z-0 ">
<div className="flex-1 w-full max-h-screen mx-auto text-lg h-full shadow-lg bg-white overflow-y-auto">
{IsNormalPage() && <SideMenu showFullMenu={sideBarFull} />}
</div>
</div>
<div className="w-full max-h-screen flex flex-col z-10">
{IsNormalPage() && <PageHeader />}
<div id="content" className="flex-1 w-full max-h-screen mx-auto text-lg h-full shadow-lg bg-white overflow-y-auto">
<Toaster />
<AppRoutes />
</div>
</div>
</div>
</div>
</AppContext.Provider>
</AuthProvider>
);
}
export default withUser(App);
I think because you are toggling the play state in your component
setPlay(!play);
Are you trying to scroll to a specific div or just scroll for 50 px in the direction of y-axis? there are two approaches, you can use window or Refs.
an example using the refs to scroll to a specific node in the dom
const ScrollDemo = () => {
const myRef = useRef(null)
const executeScroll = () => { myRef.current.scrollIntoView()}
return (
<div>
<div ref={myRef}>Element to scroll to</div>
<button onClick={executeScroll}> Click to scroll </button>
<div/>
)
}
or if you just want to just scroll 50 pixel in the direction of y-axis
const scrollToTop = () => {
window.scrollTo(0,50);
};
return (
<button onClick={scrollToTop}>
Go down 50 px!
</button>
);
window.scrollTo is only working with html body. document.getElementById is only working overflow div.
useEffect(() => {
if (play) {
const onScrollStep = () => {
var e = document.getElementById("content");
if (e.scrollHeight - e.scrollTop === e.clientHeight) {
clearInterval(intervalId.current);
setPlay(!play);
return;
}
e.scroll(0, e.scrollTop + speed);
};
intervalId.current = setInterval(onScrollStep, delayInMs);
} else {
clearInterval(intervalId.current);
}
},[play, speed])

Make only one post request with new state after clicking on one of multiple identical components

I have a small issue and do not have a good idea how to solve it. Hope you can help
I have created a simple Starrating component. You have five stars. If you click on one of the stars the state changes and so on ... (5 stars. Rating from 1 to 5 :D ). Just basic stuff.
The main problem is based on the fact that the Starrating component is a part of another component (AlbumList.js), which is rendered 5 times on the homepage (5 different pictures which you can rate)
(between there is another component AlbumCard.js which is holding the Starrating component but I assume that's not important.
Basically I have 5 components which are the same and each one of them has the Starrating Component.
My main goal is to click on one of the pictures, rate that and send the right state to my database.
The function which is sending the right rating to the database (rateAlbum), is invoked in useEffect, because only there I am able to send the new state to my database(
outside useEffect I only have access to the new state after rendering, I guess).
Unfortunately if I reload the page or just make one rate the function is invoked as many times as pictures there are (5 times)
How do I call the function just ones if I just rated one picture or just after the onClick on the right picture with the new state ?
Starrating.js
import React, { useEffect, useState } from 'react'
import { FaStar } from 'react-icons/fa'
import { rateAlbum } from '../../store/actions/userAlbumRatingAction'
function Starrating({ width }) {
const [rating, setRating] = useState(null)
const [hover, setHover] = useState(null)
const ratePicture = (rating) => {
setRating(rating)
// ratePicture() do not have the new state of rating
}
useEffect(() => {
rateAlbum({ // function which is making the axios call
//... not imporant information just the right IDs and so on
rating: rating,
})
}, [rating])
return (
<div className='flex h-full' style={{ width: width }}>
{[...Array(5)].map((star, i) => {
const ratinValue = i + 1
return (
<label key={ratinValue} className='flex items-center w-full'>
<input
className='hidden'
type='radio'
name='raiting'
value={ratinValue}
onClick={
() => ratePicture(ratinValue)
// () => setRating(ratinValue)
}
/>
<FaStar
className='md:m-1 w-full h-full delay-200 cursor-pointer'
color={ratinValue <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
onMouseEnter={() => setHover(ratinValue)}
onMouseLeave={() => setHover(null)}
/>
</label>
)
})}
</div>
)
}
export default Starrating
AlbumList.js (Starrting component is a part of the AlbumCard Component)
import { connect, useSelector } from 'react-redux'
import { fetchAlbum } from '../../store/actions/albumAction'
import AlbumCard from './AlbumCard'
import { setView } from '../../store/actions/uiAction'
import { useHistory } from 'react-router'
function AlbumList(props) {
const newreleases = useSelector((state) => state.newReleases.NewReleases)
const view = useSelector((state) => state.ui.view)
const searchAlbum = useSelector((state) => state.search.albums)
const history = useHistory()
const onAlbumCardClick = (dataId) => {
props.fetchAlbum(dataId)
history.push('/home/album')
}
return (
<section className='sm:flex sm:justify-between sm:flex-nowrap grid grid-cols-3'>
{view === 'noSearch' ? (
<>
{newreleases.slice(0, 5).map((data) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : view === 'search' ? (
<>
{searchAlbum.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : null}
</section>
)
}
const mapDispatch = { fetchAlbum, setView }
export default connect(null, mapDispatch)(AlbumList)
rateAlbum function
export const rateAlbum = (data) => {
axios.post('....', data)
}
AlbumCard.js ( not important, but has the Starrting component and Albumcard.js is part of
ALbumList.js)
import React from 'react'
import CardButtons from './CardButtons'
import Starrating from '../HelperComponents/Starrating'
function AlbumCard({ url, albumname, onClick }) {
return (
<>
<div className=' sm:m-2 sm:w-40 dark:bg-white w-24 m-1 rounded-lg shadow-md'>
<div onClick={onClick} id='hi' className='group relative rounded-lg'>
<img
className='md:w-72 block w-full h-full rounded-lg'
src={url}
alt=''
/>
<div className='group-hover:bg-opacity-60 group-hover:opacity-100 justify-evenly absolute top-0 flex items-center w-full h-full transition bg-black bg-opacity-0 rounded-md'>
<CardButtons />
</div>
</div>
<div className=' flex flex-col items-center justify-center pt-3 pb-3'>
<p className='dark:text-black font-body whitespace-nowrap flex justify-center w-11/12 mb-2 overflow-hidden text-xs text-black'>
{albumname}
</p>
<Starrating />
</div>
</div>
</>
)
}
export default AlbumCard
Startrating.js
import React, { useEffect, useState } from 'react'
import { FaStar } from 'react-icons/fa'
import { rateAlbum } from '../../store/actions/userAlbumRatingAction'
function Starrating({ width, onClickStar }) {
const [rating, setRating] = useState(null)
const [hover, setHover] = useState(null)
const ratePicture = (rating) => {
setRating(rating)
// ratePicture() do not have the new state of rating
}
useEffect(() => {
rateAlbum({ // function which is making the axios call
//... not imporant information just the right IDs and so on
rating: rating,
})
}, [rating])
return (
<div className='flex h-full' style={{ width: width }}>
{[...Array(5)].map((star, i) => {
const ratinValue = i + 1
return (
<label key={ratinValue} className='flex items-center w-full'>
<input
className='hidden'
type='radio'
name='raiting'
value={ratinValue}
onClick={
() => onClickStart(ratinValue)
// () => setRating(ratinValue)
}
/>
<FaStar
className='md:m-1 w-full h-full delay-200 cursor-pointer'
color={ratinValue <= (hover || rating) ? '#ffc107' : '#e4e5e9'}
onMouseEnter={() => setHover(ratinValue)}
onMouseLeave={() => setHover(null)}
/>
</label>
)
})}
</div>
)
}
export default Starrating
AlbumCard,js
import React from 'react'
import CardButtons from './CardButtons'
import Starrating from '../HelperComponents/Starrating'
function AlbumCard({ url, albumname, onClick, onClickStar }) {
return (
<>
<div className=' sm:m-2 sm:w-40 dark:bg-white w-24 m-1 rounded-lg shadow-md'>
<div onClick={onClick} id='hi' className='group relative rounded-lg'>
<img
className='md:w-72 block w-full h-full rounded-lg'
src={url}
alt=''
/>
<div className='group-hover:bg-opacity-60 group-hover:opacity-100 justify-evenly absolute top-0 flex items-center w-full h-full transition bg-black bg-opacity-0 rounded-md'>
<CardButtons />
</div>
</div>
<div className=' flex flex-col items-center justify-center pt-3 pb-3'>
<p className='dark:text-black font-body whitespace-nowrap flex justify-center w-11/12 mb-2 overflow-hidden text-xs text-black'>
{albumname}
</p>
<Starrating onClickStar={onClickStar} />
</div>
</div>
</>
)
}
export default AlbumCard
AlbumList.js
import { connect, useSelector } from 'react-redux'
import { fetchAlbum } from '../../store/actions/albumAction'
import AlbumCard from './AlbumCard'
import { setView } from '../../store/actions/uiAction'
import { useHistory } from 'react-router'
function AlbumList(props) {
const newreleases = useSelector((state) => state.newReleases.NewReleases)
const view = useSelector((state) => state.ui.view)
const searchAlbum = useSelector((state) => state.search.albums)
const [releases, setReleases] = useState([])
useEffect(() => {
setReleases(newreleases)
}, [newreleases])
const history = useHistory()
const setRating = (ratingValue, index) => {
let updatedReleases = [...releases]
updatedReleases[index].rating = ratingValue // i'm assuming you have //rating field
setReleases(updatedReleases)
// then send this new releases to the api
}
const onAlbumCardClick = (dataId) => {
props.fetchAlbum(dataId)
history.push('/home/album')
}
return (
<section className='sm:flex sm:justify-between sm:flex-nowrap grid grid-cols-3'>
{view === 'noSearch' ? (
<>
{newreleases.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
albumname={data.name}
onClickStar={(ratingValue) => setRating(ratingValue, index)}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : view === 'search' ? (
<>
{searchAlbum.slice(0, 5).map((data, index) => (
<AlbumCard
url={data.images[0].url}
key={data.id}
id={data.id}
onClickStar={(ratingValue) => setRating(ratingValue, index)}
albumname={data.name}
onClick={() => onAlbumCardClick(data.id)}
/>
))}
</>
) : null}
</section>
)
}
const mapDispatch = { fetchAlbum, setView }
export default connect(null, mapDispatch)(AlbumList)
I implement your StarRating component in an optimized & efficient manner.
import React, { useState } from "react";
import { FaStar } from "react-icons/fa";
import { rateAlbum } from "../../store/actions/userAlbumRatingAction";
function StarRating({ width }) {
const [rating, setRating] = useState(0);
const ratePicture = (rating) => {
setRating(++rating);
console.log("clicked me");
rateAlbum({
rating: ++rating,
}); // Dispatch the action to save the rating
};
return (
<div className="flex h-full" style={{ width: width, display: "flex" }}>
{[...Array(5)].map((star, i) => (
<div key={i} className="flex items-center w-full">
<FaStar
className="md:m-1 w-full h-full delay-200 cursor-pointer"
color={i < rating ? "#ffc107" : "#e4e5e9"}
onClick={() => ratePicture(i)}
/>
</div>
))}
</div>
);
}
export default StarRating;
Let me know if you already implement rendering the StarRating component with its album's default rate to include that too in this code.
I hope this will solve all the problems related to rating.

Resources