Group Disclosures (Accordian) from Headless UI - reactjs

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>
);
}

Related

DndKit transition delay on drop, useSortable

When I drop an element to resort, there's a slight transition issue where the items goes back to its' original position & then transitions. Below I added a link to video & code.
Really, I don't want any transition delay, etc. I feel like it could be the remapping of the array, but i've tested it by optimizing as much as possible & still getting the same issue. It works fine without the transition property on there, but would like it to make things feel smooth.
Link to video --> Link to video
import { useSortable } from '#dnd-kit/sortable';
import { CSS } from '#dnd-kit/utilities';
import React from 'react';
import Icon from '../../../../../../../common/components/icon/Icon';
interface Props {
id: number;
children: React.ReactNode;
}
const SortableItem = ({ id, children }: Props) => {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id: id });
return (
<div
ref={setNodeRef}
style={{
transform: CSS.Transform.toString(transform),
transition,
}}>
<div className='grid grid-cols-[60px_auto] items-center w-full h-full relative'>
{/* Make this the icon grabble */}
<div
className='flex items-center w-full h-full cursor-grab'
data-grab={true}
{...attributes}
{...listeners}>
<Icon name='ArrowsUpDownLeftRight' width={20} height={20} />
</div>
{children}
</div>
</div>
);
};
export default SortableItem;
'use client';
import React, { useState } from 'react';
import { ListenService } from '../SmartLinkClient';
import Icon from '../../../../../../common/components/icon/Icon';
import Input from '../../../../../../common/components/input/Input';
import ToggleSwitch from '../../../../../../common/components/toggle-switch/ToggleSwitch';
import Hr from '../../../../../../common/components/hr/Hr';
import { DndContext, PointerSensor, useSensor, useSensors, closestCenter } from '#dnd-kit/core';
import { SortableContext, arrayMove, verticalListSortingStrategy } from '#dnd-kit/sortable';
import SortableItem from './(edit-services-partials)/SortableItem';
interface Props {
servicesConfig: ListenService[];
handleServiceShowToggle: (e: any, elementToChange: ListenService) => void;
handleServiceDragEnd: (active: any, over: any) => void;
}
const EditServices = ({ servicesConfig, handleServiceShowToggle, handleServiceDragEnd }: Props) => {
const sensors = useSensors(useSensor(PointerSensor));
return (
<div className='select-services'>
<div className='flex flex-col gap-y-4'>
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={({ active, over }) => {
handleServiceDragEnd(active, over);
}}>
<SortableContext
items={servicesConfig.map((service: ListenService) => service.id)}
strategy={verticalListSortingStrategy}>
{/* Components that use the useSortable hook */}
{servicesConfig.map((service: ListenService, i: number) => (
<SortableItem key={i} id={service.id}>
<div className='grid grid-cols-[minmax(160px,180px)_minmax(200px,auto)_100px] items-center'>
<div className='grid grid-cols-[24px_auto] items-center gap-x-3 dark:text-stone-100 text-stone-800'>
<Icon name={service.iconName} width={24} height={24} color={service.color} />
<span className='text-[16px]'>{service.name}</span>
</div>
<Input
placeholder='We could not find a valid URL, but you can enter your own.'
type={'url'}
/>
<div className='justify-self-end -mt-2 flex flex-row items-center gap-x-2'>
<span className='text-[11px] text-stone-600 dark:text-stone-300'>Show</span>
<ToggleSwitch
toggled={service.show}
handleToggled={(e: any) => {
handleServiceShowToggle(e, service);
}}
/>
</div>
</div>
{i !== servicesConfig.length - 1 && (
<span className='col-span-2'>
<Hr />
</span>
)}
</SortableItem>
))}
</SortableContext>
</DndContext>
</div>
</div>
);
};
export default EditServices;
I ended up figuring it out. The key on the <SortableItem /> component must be the same as the id on the <SortableItem /> component. Weird, but makes sense.
Previous:
ex) <SortableItem key={i} id={service.id} />
Current (Solution):
ex) <SortableItem key={service.id} id={service.id} />

How to avoid closing DropDown on MouseOver?

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?

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>
)
}

Prevent mapped components in conditional from reloading?

I have a screen with two tabs/buttons "Recent" and "Top". When switching between the two, the individual items always reload the data which is unnecessary and causes shifting. I haven't been able to solve this with useMemo or anything similar.
How can I make the components not load their data again when switching between tabs:
Example editable in Sandbox: codesandbox
Edit: I realized I can use CSS to hide the non-active tab or I can load the data at a higher level to prevent the reload - but at the component level, is it possible to stop a reload of the data?
import * as React from "react";
import "./styles.css";
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
const Item = ({ title }) => {
const [itemLoading, setItemLoading] = React.useState(true);
async function loadItem() {
await delay((Math.random() * (3 - 1) + 1) * 1000);
setItemLoading(false);
}
React.useEffect(() => {
loadItem();
}, []);
if (itemLoading) {
return (
<div className="w-full text-center py-4 border bg-white rounded shadow-lg">
item is loading ...
</div>
);
} else {
return (
<div className="w-full text-center py-4 border bg-white rounded shadow-lg">
item {title}
</div>
);
}
};
export default function App() {
const [tab, setTab] = React.useState("Recent");
const recentItems = ["1", "2", "3", "4", "5", "6", "7", "9"];
const topItems = ["99", "98", "97", "96", "95", "94", "93"];
return (
<div className="w-screen h-screen bg-gray-100">
<h1 className="text-2xl text-center py-4">Current Tab: {tab}</h1>
<section className="w-full h-24 grid grid-cols-2">
<div
className="cursor-pointer text-center bg-red-200 pt-8"
onClick={() => {
setTab("Recent");
}}
>
Recent
</div>
<div
className="cursor-pointer text-center bg-yellow-200 pt-8"
onClick={() => {
setTab("Top");
}}
>
Top
</div>
</section>
<section>
{tab === "Recent"
? recentItems.map((item) => <Item key={item} title={item} />)
: topItems.map((item) => <Item key={item} title={item} />)}
</section>
</div>
);
}
Every time tab state changes, array.map is invoked and creates a new array of react components:
{tab === "Recent"
? recentItems.map((item) => <Item key={item} title={item} />)
: topItems.map((item) => <Item key={item} title={item} />)}
And everytime a new component is created, the code you've provided tries to fetch data with async call and ReactDOM re-renders the table. Data fetching part should be seperate from component if speed is a constraint.
You can achive that by either moving the data state up to upper component(in your case App) or handle it seperately elsewhere. First option is recommended.
The way I solved this without moving the data loading further up, is to set the height and width of the tabs to 0, and mark them as hidden (unless it it the current tab). This got rid of all double loading and cumulative layout shift without modify lots of code.
(tailwind)
const hiddenClass = "w-0 h-0 hidden"
<div className={currentTab === "Recent" ? undefined : hiddenClass} >
...recent items
</div>
<div className={currentTab === "Popular" ? undefined : hiddenClass} >
...popular items
</div>

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