Refetch query and cache evict on route change - reactjs

I have a problem regarding my GraphQL query cache. I'm trying to create pagination for this query. I have some music playlists and every playlist have its own page. On a playlist page I call a query to get the playlist songs (which I get as a JSON response). This is the merge function from my InMemoryCache:
getMoreSpotifyPlaylists: {
keyArgs: [],
merge(existing: PaginatedJsonResponse | undefined, incoming: PaginatedJsonResponse): PaginatedJsonResponse {
var existingJSON, incomingJSON
if (existing?.['jsonData']) {
existingJSON = JSON.parse(existing?.['jsonData'])
} else {
return incoming
}
incomingJSON = JSON.parse(incoming?.['jsonData'])
var finalJSON = {
...incomingJSON,
items: existingJSON?.items.concat(incomingJSON?.items)
}
return {
...incoming,
jsonData: JSON.stringify(finalJSON)
}
}
}
The problem is that when I change the playlist page, the first songs remain the songs from the other playlist. I've tried to evict the cache on page change but this didn't worked.
This is the playlist page:
const SpotifyPlaylistPage: NextPage = () => {
const apolloClient = useApolloClient();
const router = useRouter()
const [rerender, setRerender] = useState(false)
const [progress, setProgress] = useState(100)
var id = typeof router.query.id === "string" ? router.query.id : ""
const { t: menuTranslation } = useTranslation('menu')
const { t: commonTranslation } = useTranslation('common')
const { t: actionsTranslation } = useTranslation('actions')
const { data: playlistData, loading: playlistDataLoading } = useGetSpotifyPlaylistQuery({ variables: { id } })
const { data: spotifyPlaylistSongsData, loading: spotifyPlaylistSongsLoading, fetchMore: fetchMoreSpotifyPlaylistSongs, variables: spotifyPlaylistSongsVariables } = useGetSpotifyPlaylistSongsQuery({ variables: { id, offset: 0, limit: 5 } })
const [spotifyPlaylistSongsJSON, setSpotifyPlaylistSongsJSON] = useState(null)
const [newOffset, setNewOffset] = useState(5)
const [playlistJSON, setPlaylistJSON] = useState(null)
const [isPublicIconFilled, setIsPublicIconFilled] = useState(false)
const [isCollaborativeIconFilled, setIsCollaborativeIconFilled] = useState(false)
useEffect(() => {
const cache = new InMemoryCache()
if (cache.evict({ fieldName: "getMoreSpotifyPlaylists", broadcast: false })) {
console.log('evicted')
} else {
console.log("error")
}
setRerender(r => !r)
}, [router.query.id, id]);
useEffect(() => {
if (playlistData?.getSpotifyPlaylist?.jsonData) {
setPlaylistJSON(JSON.parse(playlistData?.getSpotifyPlaylist?.jsonData))
}
if (spotifyPlaylistSongsData?.getSpotifyPlaylistSongs?.jsonData) {
setSpotifyPlaylistSongsJSON(JSON.parse(spotifyPlaylistSongsData?.getSpotifyPlaylistSongs?.jsonData))
}
}, [apolloClient, playlistData?.getSpotifyPlaylist?.jsonData, playlistDataLoading, spotifyPlaylistSongsData?.getSpotifyPlaylistSongs?.jsonData])
var showContent = useRef(false)
if (!playlistDataLoading) {
if (id && playlistData?.getSpotifyPlaylist?.jsonData) {
showContent.current = true
} else {
router.push("/")
}
}
if (showContent.current) {
return (
<AppContainer routeType='public' key={id}>
<LaHead title={menuTranslation('home')} />
<div className='w-full h-96 top-0 left-0 absolute z-0'>
<Image alt="test" layout='fill' objectFit='cover' src={playlistJSON?.images[0]?.url} />
<div className='w-full h-full top-0 left-0 absolute bg-gradient-to-t from-la-mauve1 dark:from-la-martinique3 to-transparent'></div>
<div className='w-full h-full top-0 left-0 absolute bg-la-mauve1/80 dark:bg-la-martinique3/80' />
</div>
<div className='z-10'>
<NavbarWithSearchBar />
<Container size='large'>
<LaBreadcrumb breadcrumbElements={[{
href: "/",
name: "Home",
isActive: false
}, {
href: "#",
name: playlistJSON?.name,
isActive: true
}]} />
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-2 md:space-x-4'>
<Title>{playlistJSON?.name}</Title>
{playlistJSON?.public ?
<span className='flex cursor-pointer grow justify-center hover:text-la-seance2 hover:dark:text-la-seance1' data-tip={commonTranslation('public')} onMouseEnter={() => { setIsPublicIconFilled(true) }} onMouseLeave={() => { setIsPublicIconFilled(false) }}>
<ReactTooltip arrowColor="transparent" multiline place="top" className="la-tooltip" effect="solid" />
<UsersGroupIcon fillEffect={isPublicIconFilled} className='w-6 h-6' />
</span>
:
<span className='flex cursor-pointer grow justify-center hover:text-la-seance2 hover:dark:text-la-seance1' data-tip={commonTranslation('private')} onMouseEnter={() => { setIsPublicIconFilled(true) }} onMouseLeave={() => { setIsPublicIconFilled(false) }}>
<ReactTooltip arrowColor="transparent" multiline place="top" className="la-tooltip" effect="solid" />
<LockIcon fillEffect={isPublicIconFilled} className='w-6 h-6' />
</span>}
{playlistJSON?.collaborative ?
<span className='flex cursor-pointer grow justify-center hover:text-la-seance2 hover:dark:text-la-seance1' data-tip={commonTranslation('collaborative')} onMouseEnter={() => { setIsCollaborativeIconFilled(true) }} onMouseLeave={() => { setIsCollaborativeIconFilled(false) }}>
<ReactTooltip arrowColor="transparent" multiline place="top" className="la-tooltip" effect="solid" />
<UsersIcon fillEffect={isCollaborativeIconFilled} className='w-6 h-6' />
</span>
:
<span className='flex cursor-pointer grow justify-center hover:text-la-seance2 hover:dark:text-la-seance1' data-tip={commonTranslation('individual')} onMouseEnter={() => { setIsCollaborativeIconFilled(true) }} onMouseLeave={() => { setIsCollaborativeIconFilled(false) }}>
<ReactTooltip arrowColor="transparent" multiline place="top" className="la-tooltip" effect="solid" />
<UserIcon2 fillEffect={isCollaborativeIconFilled} className='w-6 h-6' />
</span>}
</div>
<Button onClick={async () => {
}} fullWidth={false} content={<RefreshIcon className='w-6 h-6' />} />
</div>
<div className='text-flex italic'>
{playlistJSON?.description}
</div>
{spotifyPlaylistSongsLoading ? <div className='flex w-full justify-center'>
<LaLoader />
</div> : spotifyPlaylistSongsJSON?.items.length > 0 ?
<>
<ThreeBoxesContainer>
{spotifyPlaylistSongsJSON?.items?.map((item: any) => <SongBox key={item.track.id} songData={item} />)}
{spotifyPlaylistSongsData && spotifyPlaylistSongsData.getSpotifyPlaylistSongs.hasMore ?
<Button onClick={async () => {
await fetchMoreSpotifyPlaylistSongs({ variables: { id, offset: newOffset, limit: spotifyPlaylistSongsVariables?.limit } })
setNewOffset(newOffset + spotifyPlaylistSongsVariables.limit)
}} content={commonTranslation('showmore')} /> : null}
</ThreeBoxesContainer>
</>
: <MessageBox type='warning' title={commonTranslation('error')} message={actionsTranslation('noPlaylists')} />}
</Container>
<BottomNavbar />
</div>
</AppContainer>
);
}
return (
<div><LoadingBar className="!bg-la-seance2 !dark:bg-la-seance1" progress={progress}
onLoaderFinished={() => setProgress(0)} /></div>
)
}

Related

React select from Radix-UI conflicts with default value, how to solve it?

I have this component where I render 3 different selectors depending on the data I pass. The first selector once clicked, gets the data of the second one (without selection), the same for the second, and so on. What I am trying to achieve is that, if I select the first dropdown && the second one has only one item in the list I get that value will be pre-selected without letting the user select it by himself.
This is the parent component where I pass the default value to each selector:
const CreateBatchTemplateDialog: FC<Props> = ({ open, onClose }) => {
const [localInstruments, setLocalInstruments] = useState<LocalInstrument[]>();
const [corridors, setCorridors] = useState<Corridor[]>();
const [corridorId, setCorridorId] = useState<string>();
const [productSchemas, setProductSchemas] = useState<ProductSchema[]>();
const [dowloaded, setDowloaded] = useState<boolean>(false);
const { data: localInstrumentsData } = useGetLocalInstrumentsQuery();
const downloadFile = useDownloadFile();
const [getProductSchemas] = useLazyGetProductSchemasQuery();
const fetchingProductSchemaData = async () => {
try {
const { data: productSchema } = await getProductSchemas(corridorId);
setProductSchemas(productSchema.data);
} catch (error: any) {
console.error(error);
}
};
useEffect(() => {
if (corridorId) {
fetchingProductSchemaData();
}
}, [corridorId]);
useEffect(() => {
if (localInstrumentsData?.data) {
setLocalInstruments(localInstrumentsData.data);
}
setProductSchemas([]);
}, [localInstrumentsData]);
const {
handleSubmit,
setError,
formState,
setValue,
reset: resetForm,
getValues
} = useForm<Inputs>({
mode: 'onSubmit',
resolver: yupResolver(schema)
});
const localInstrumentsOptions = useMemo(() => {
setProductSchemas([]);
return Array.isArray(localInstruments)
? localInstruments.map((i) => ({ label: i.name, value: i.name }))
: [];
}, [localInstruments]);
const corridorsOptions = useMemo<SelectChoice[]>(() => {
setProductSchemas([]);
return Array.isArray(corridors)
? corridors.map((i) => ({
label: i.label,
value: i.id.toString()
}))
: [];
}, [corridors, localInstruments]);
const productSchemasOptions = useMemo(() => {
return Array.isArray(productSchemas)
? productSchemas.map((i) => ({
label: i.label,
value: i.id.toString()
}))
: [];
}, [productSchemas, corridors, localInstruments]);
const onLocalInstrumentChange = (value: string) => {
setDowloaded(false);
setProductSchemas([]);
setCorridorId(undefined);
const corridorData = localInstruments?.find((i) => i.name === value);
setCorridors(corridorData ? corridorData.products : []);
setValue('localInstrument', value, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});
setValue('corridor', '', {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});
};
const onCorridorChange = (value: string) => {
setDowloaded(false);
setCorridorId(value);
setProductSchemas([]);
setValue('corridor', value, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});
setValue('productSchema', '', {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});
};
const onProductSchemaChange = (value: string) => {
setDowloaded(false);
setValue('productSchema', value, {
shouldDirty: true,
shouldTouch: true,
shouldValidate: true
});
};
const onSubmit: FormEventHandler<HTMLFormElement> = async (event: any) => {
try {
await handleSubmit(async (data) => {
downloadFile(`payouts/schemas/${data.productSchema}/csv_template`);
setDowloaded(true);
})(event);
} catch (error: any) {
setTimeout(
() =>
setFormErrors({
error,
setError,
fields: Object.keys(schema.fields)
}),
50
);
}
};
const onDialogClose = (value: boolean) => {
resetForm();
onClose(value);
setLocalInstruments([]);
setCorridors([]);
setCorridorId(undefined);
setProductSchemas([]);
setDowloaded(false);
};
return (
<Dialog open={open} onOpenChange={onDialogClose}>
<DialogPortal>
<DialogOverlay className="fixed inset-0 z-50 bg-inpay-green-700/90">
<DialogContent
onClick={(e) => e.stopPropagation()}
className="fixed top-[50%] left-[50%] translate-y-[-50%] translate-x-[-50%] rounded-lg shadow-[0_0_6px_#B7BCBA]"
>
<DialogTitle className="flex h-12 items-center justify-between rounded-t-lg border-b border-b-inpay-pumice bg-white px-6">
<DownloadIcon className="w-8 stroke-1 group-hover:stroke-1.5" />
<span className="pr-12">Create batch template</span>
<DialogClose
aria-label="Close"
className="outline-none focus:outline-none"
>
<XIcon className="-m-1 h-8 w-8 outline-none focus:outline-none" />
</DialogClose>
</DialogTitle>
<div className="flex justify-center rounded-b-lg bg-inpay-black-haze-100 p-6">
<div className="w-72">
<form onSubmit={onSubmit} autoComplete="off">
<div className="mb-4 flex flex-col">
<label className="mb-0.5 pl-2 text-xs">Format</label>
<CsvSelector />
</div>
<div className="mb-4 flex flex-col">
<label className="mb-0.5 pl-2 text-xs">Transfer type</label>
<Selector
selectChoices={localInstrumentsOptions}
onChange={onLocalInstrumentChange}
name="localInstruments"
disabled={
!localInstrumentsOptions ||
localInstrumentsOptions.length === 0
}
defaultValue={
localInstrumentsOptions?.length === 1
? localInstrumentsOptions[0].value
: undefined
}
/>
</div>
<div className="mb-4 flex flex-col">
<label className="mb-0.5 pl-2 text-xs">Corridor</label>
<Selector
selectChoices={corridorsOptions}
onChange={onCorridorChange}
name="corridor"
disabled={
!corridorsOptions || corridorsOptions.length === 0
}
value={getValues('corridor')}
defaultValue={
corridorsOptions?.length === 1
? corridorsOptions[0].label
: undefined
}
/>
{console.log(
corridorsOptions?.length === 1
? corridorsOptions[0].label
: undefined
)}
</div>
<div className="mb-10 flex flex-col">
<label className="mb-0.5 pl-2 text-xs">Data required</label>
<Selector
selectChoices={productSchemasOptions}
onChange={onProductSchemaChange}
name="productSchema"
disabled={
!productSchemasOptions ||
productSchemasOptions.length === 0
}
value={getValues('productSchema')}
defaultValue={
productSchemasOptions?.length === 1
? productSchemasOptions[0].label
: undefined
}
/>
</div>
{dowloaded === false ? (
<button
type="submit"
className="inpay-button w-full"
disabled={!formState.isValid}
>
Download CSV template
</button>
) : (
<button
type="submit"
className="inpay-button w-full"
onClick={() => onClose(false)}
>
CSV dowloaded, close window
</button>
)}
{formState.errors.base && (
<div
role="alert"
className="mt-2 rounded-2xl bg-rose-200 p-3 text-sm text-inpay-red-600"
style={{
filter: 'drop-shadow(0 0 1px rgb(0 0 0 / 0.16))'
}}
>
{formState.errors.base.message}
</div>
)}
</form>
</div>
</div>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</Dialog>
);
};
This is the child component where I render the Selector, the issue is that even though the default value (string) and the matchingValue (object) are logged in the useEffect, matched and passed to the setCurrentItem state, the selector won't know when to use the defaultValue. So the preselection never works for some reasons. I am a bit confused if it is a limit of the library or if it is some mistake a made. Here below the selector:
import { FC, useEffect, useState } from 'react';
import {
Select,
SelectContent,
SelectItem,
SelectItemText,
SelectPortal,
SelectScrollDownButton,
SelectScrollUpButton,
SelectTrigger,
SelectValue,
SelectViewport
} from '#radix-ui/react-select';
import { ChevronUpIcon, ChevronDownIcon } from '#heroicons/react/outline';
import cn from 'classnames';
import { twMerge } from 'tailwind-merge';
interface SelectorClassNames {
loadingClassName?: string;
formButtonClassName?: string;
selectedItemClassName?: string;
selectContentClassName?: string;
selectItemListClassName?: string;
}
export interface SelectChoice {
value: string;
label: string;
}
interface Props {
defaultValue?: string;
onChange?: (value: string) => void;
classNames?: SelectorClassNames;
selectChoices: SelectChoice[];
name?: string;
disabled?: boolean;
value?: string;
}
const Selector: FC<Props> = ({
defaultValue,
onChange,
classNames,
selectChoices,
name,
disabled,
value
}) => {
const [currentItem, setCurrentItem] = useState<SelectChoice | undefined>(
undefined
);
const [availableList, setAvailableList] = useState<SelectChoice[]>([]);
const [opened, onOpenChange] = useState<boolean>(false);
const selectTriggerProps = {
className: twMerge(
cn(
'group flex items-center h-12 select-none rounded-lg bg-white px-4 text-left shadow-plain',
classNames?.formButtonClassName,
{
'group hover:shadow-plain-lg cursor-pointer': !disabled,
'stroke-inpay-black-haze-700 stroke-1.5 group hover:none text-inpay-black-haze-700':
disabled,
'bg-inpay-gray-100': disabled && currentItem?.value
}
)
)
};
const selectedItemProps = {
className: twMerge(
cn(
'group flex h-12 cursor-pointer items-center px-4 rounded-t-lg outline-offset-[-1px] hover:bg-inpay-green-200 hover:text-inpay-green-700 border-none focus:outline-none',
classNames?.selectedItemClassName,
{
'px-4': disabled
}
)
)
};
const selectItemListProps = {
className: twMerge(
cn(
'group h-12 flex items-center cursor-pointer px-4 outline-offset-[-1px] first:rounded-t-lg last:rounded-b-lg hover:bg-inpay-green-200 hover:text-inpay-green-700 border-none focus:outline-none',
classNames?.selectItemListClassName
)
)
};
const selectContentProps = {
className: twMerge(
cn(
`${
currentItem
? ' relative z-50 select-none overflow-hidden rounded-lg bg-white shadow-plain-lg w-72'
: 'relative z-50 select-none overflow-hidden rounded-lg bg-white shadow-plain-lg w-72'
}`,
classNames?.selectContentClassName
)
)
};
const onValueChange = (value: string) => {
if (selectChoices) {
const matchingValue = selectChoices.find(
(element) => element.value == value
);
if (matchingValue) {
setCurrentItem({ ...matchingValue });
setAvailableList(
selectChoices.filter((i) => i.value != matchingValue.value)
);
onChange && onChange(value);
}
}
};
useEffect(() => {
if (defaultValue) {
console.log(selectChoices, 'selectChoices'); // gets logged correctly
const newValue = selectChoices.find((i) => i.label === defaultValue);
console.log(defaultValue, 'defaultValue'); // gets logged correctly
if (!newValue) {
console.error('can not find new value');
}
setCurrentItem(newValue);
console.log(newValue, 'new value'); // gets logged correctly
setAvailableList(selectChoices.filter((i) => i.value != defaultValue));
}
}, [defaultValue, selectChoices]);
useEffect(() => {
setAvailableList(selectChoices);
setCurrentItem(undefined);
}, [selectChoices]);
return (
<Select
onValueChange={onValueChange}
onOpenChange={onOpenChange}
name={name}
disabled={disabled}
defaultValue={defaultValue}
value={value}
>
<SelectTrigger {...selectTriggerProps}>
<SelectValue
placeholder={
<div className="flex">
<div className="flex-1"></div>
<ChevronDownIcon
className={cn('w-1/12 stroke-inpay-black-haze-700 stroke-1.5', {
'group-hover:stroke-inpay-black-500': !disabled
})}
/>
</div>
}
/>
</SelectTrigger>
<SelectPortal>
<SelectContent {...selectContentProps}>
<SelectScrollUpButton className="flex justify-center">
<ChevronUpIcon className="h-10 w-6 stroke-1.5" />
</SelectScrollUpButton>
<SelectViewport>
{currentItem && (
<SelectItem
key={currentItem.value}
value={currentItem.value}
{...selectedItemProps}
>
<SelectItemText>
<div className="flex">
<div className="flex-1">
<div>{currentItem.label}</div>
</div>
{opened ? (
<ChevronUpIcon className="w-1/12 stroke-inpay-black-500 stroke-1.5" />
) : (
<ChevronDownIcon className="w-1/12 stroke-inpay-black-haze-700 stroke-1.5 group-hover:stroke-inpay-black-500" />
)}
</div>
</SelectItemText>
</SelectItem>
)}
{availableList.map((item, index) => (
<SelectItem
key={item?.value}
value={item.value}
{...selectItemListProps}
>
<SelectItemText>
<div className="flex">
<div className="flex-1">{item.label}</div>
{index == 0 && !currentItem && (
<ChevronUpIcon className="w-1/12 stroke-inpay-black-500 stroke-1.5" />
)}
</div>
</SelectItemText>
</SelectItem>
))}
</SelectViewport>
<SelectScrollDownButton className="flex justify-center">
<ChevronDownIcon className="h-10 w-6 stroke-1.5" />
</SelectScrollDownButton>
</SelectContent>
</SelectPortal>
</Select>
);
};
export default Selector;
Any clues ?

with react DND only one element accepts the drag reference

with react DND only one element accepts the drag reference i fetch data with firebase and put them in an array then map through the items and generate a card but only the last element accepts the drag ref
this is the place where I'm trying to implement it
import { useDrag } from "react-dnd";
const db = getFirestore(app);
export default function Projects({ currentUid }) {
const [projects, setProjects] = useState([]);
const [doneProjects, setDoneProjects] = useState([]);
const [tasks, setTasks] = useState([]);
const [dropDown, setDropDown] = useState(false);
const [key, setKey] = useState();
const [documentId, setDocumentId] = useState();
const [progressPercentage, setProgressPercentage] = useState(0);
const [{ isDragging }, dragRef] = useDrag(
() => ({
type: itemTypes.CARD,
item: { type: itemTypes.CARD },
collect: (monitor) => ({
isDragging: monitor.isDragging(),
})
}),
[]
)
useEffect(() => {
// amay xwarawa bo aw projecta naya ka tawaw bwn
// const docRef3 = query(collection(db, "assignedProjects", currentUid, "projects"),where("isDone", "==", true));
const getProjects = async () => {
const docRef3 = query(
collection(db, "assignedProjects", currentUid, "projects"),
where("isDone", "==", true)
);
const docDoneSnap = await getDocs(docRef3);
onSnapshot(docRef3, (docDoneSnap) => {
const projectsDoneArr = [];
docDoneSnap.forEach((doc) => {
projectsDoneArr.push(doc.data());
});
setDoneProjects(projectsDoneArr);
});
const docRef = query(
collection(db, "assignedProjects", currentUid, "projects"),
where("isDone", "==", false)
);
// const docSnap = await getDocs(docRef);
// const projectsArr = [];
// docSnap.forEach((doc) => {
// projectsArr.push(
// {
// projects:doc.data(),
// projectId:doc.id
// }
// );
// documentId = doc.id;
// // console.log(doc.data());
// });
onSnapshot(docRef, (docSnap) => {
const projectsArr = [];
docSnap.forEach((doc) => {
projectsArr.push(
{
projects:doc.data(),
projectId:doc.id
}
);
setDocumentId(doc.id);
// console.log(doc.data());
});
setProjects(projectsArr);
});
console.log(documentId)
const taskRef = query(
collection(db, "assignedProjects", currentUid, "tasks"),
where("projectId", "==", documentId)
);
const taskSnap = await getDocs(taskRef);
const tasksArr = taskSnap.docs.map((doc) => doc.data());
setTasks(tasksArr);
};
getProjects();
}, []);
console.log(tasks)
useEffect(() => {
const getProgressPercentage = () => {
if (projects.length === 0) {
setProgressPercentage(100);
return;
}
const totalProjects = projects.length;
const totalDoneProjects = doneProjects.length;
const totalTasks = tasks.length;
const totalDoneTasks = tasks.filter((task) => task.isDone === true)
.length;
const totalProgress = totalDoneProjects + totalDoneTasks;
const total = totalProjects + totalTasks;
const percentage = Math.round((totalProgress / total) * 100);
setProgressPercentage(percentage);
};
getProgressPercentage();
}, [projects, doneProjects, tasks]);
function markProjectDone(project) {
const projectRef = collection(
db,
"assignedProjects",
currentUid,
"projects"
);
const docRef = doc(projectRef, project.projectId);
updateDoc(docRef, {
isDone: true,
});
}
const tasksToShow=tasks.map((task) => (
<div className={""}>
<p className="h-fit px-10 my-2 py-2 bg-slate-200 dark:bg-[#252525] rounded-2xl text-xl text-start">
{task.task1}
</p>
<p className="h-fit px-10 my-2 py-2 bg-slate-200 dark:bg-[#252525] rounded-2xl text-xl text-start">
{task.task2}
</p>
<p className="h-fit px-10 my-2 py-2 bg-slate-200 dark:bg-[#252525] rounded-2xl text-xl text-start">
{task.task3}
</p>
</div>
))
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.75, ease: "easeOut" }}
className="w-full min-h-full items-center font-Main text-[#121212] dark:text-[#ffffff] flex justify-between"
>
<div className="flex flex-col gap-20 h-full justify-around">
{/* <Progress value={50} label="Completed" className="bg-cyan-500" /> */}
{/* {isNaN(progressPercentage)?"Loading ...":<div className='h-5 w-full bg-gray-300'>
<div
style={{ width: `${progressPercentage}%`}}
className={`h-full ${
progressPercentage < 70 ? 'bg-red-600' : 'bg-green-600'}`}>
{progressPercentage}%
</div>
</div>} */}
<h1 className="text-6xl">Projects in progress</h1>
{projects.map((project) => (
<div ref={dragRef} key={project.id} className=" text-[#121212] dark:text-[#ffffff] flex flex-col gap-2" draggable={true} >
<div className="bg-slate-200 dark:bg-[#252525] w-[35rem] items-center h-32 rounded-2xl py-2 flex justify-between px-10" >
<div className="flex flex-col items-start justify-start gap-2" >
<h1 className="text-4xl text-[#018786] dark:text-[#03dac6] ">
{project.projects.title}
</h1>
<p className="text-lg text-[#12121247] dark:text-[#ffffff70]">
Due date :{" "}
<span className="text-[#121212c5] dark:text-[#ffffffb5] ">
{project.projects.dueDate}
</span>
</p>
</div>
<div className="text-5xl">
<button
onClick={() => {
setKey(project.title);
setDropDown(dropDown === false ? true : false);
}}
className="mx-5 dark:text-[#bb86fc] text-[#6200ee]"
>
{dropDown && key === project.title ? (
<ion-icon name="chevron-up-circle-outline"></ion-icon>
) : (
<ion-icon name="chevron-down-circle-outline"></ion-icon>
)}
</button>
<button className="text-[#018786] dark:text-[#03dac6]" onClick={()=>{
markProjectDone(project)
}}>
<ion-icon name="checkmark-circle-outline"></ion-icon>
</button>
</div>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.75, ease: "easeOut" }}
className={`h-fit transition-all ${
dropDown && key === project.title ? "" : "hidden"
} w-full py-2`}
>
<div
className={`h-fit px-10 py-2 dark:bg-[#3700b3] bg-[#6200ee] rounded-2xl w-full flex flex-col justify-start`}
>
<div className="h-fit w-full ">
<p className="text-xl flex text-[#ffffffc1]">
Description :{" "}
<span className=" text-[#ffffff] ">
<p>{project.projects.description}</p>
</span>
</p>
</div>
<p>{project.projects.status}</p>
</div>
{tasksToShow}
</motion.div>
</div>
))}
</div>
<div className="flex flex-col gap-20 h-full justify-around">
<h1 className="text-6xl">Done Projects</h1>
{doneProjects.map((project) => (
<div className="">
<div className=" bg-[#0187878e] dark:bg-[#03dac579] w-[35rem] items-center h-32 rounded-2xl py-2 flex justify-between px-10">
<div className="flex flex-col items-start justify-start gap-2">
<h1 className="text-4xl text-[#121212]">{project.title}</h1>
<p className="text-lg text-[#12121247] ">
Due date :{" "}
<span className="text-[#121212c5] ">{project.dueDate}</span>
</p>
</div>
</div>
</div>
))}
</div>
</motion.div>
);
}
and this is the index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
import {DndProvider} from "react-dnd";
import {HTML5Backend} from "react-dnd-html5-backend";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<DndProvider backend={HTML5Backend}>
<App />
</DndProvider>
</BrowserRouter>
</React.StrictMode>
);
I tried putting the reference in different divs but still the same.

wavesurfer not displaying waveform

I'm trying to use wavesurfer.js to display a waveform for audio files in a chat app i'm building using react . The audio plays fine but the wave is not displayed.
How do i display the waveform?
export const Wave = ({ url }) => {
const [isPlaying, setIsPlaying] = useState(false);
const waveformRef = useRef(null);
const trackRef = useRef(null);
useEffect(() => {
waveformRef.current = WaveSurfer.create({
container: "#waveform",
waveColor: "#FDCCE3",
progressColor: "#FA67AB",
backend: "MediaElement",
height: 80,
normalize: true,
responsive: true,
cursorColor: "transparent"
});
waveformRef.current.on("finish", () => {
setIsPlaying(false);
});
waveformRef.current.load(trackRef.current);
}, [url]);
const handlePlay = () => {
setIsPlaying(!isPlaying);
waveformRef.current.playPause();
};
return (
<div className="flex items-center h-24 bg-transparent gap-8">
<button
className="border-none outline-none cursor-pointer"
onClick={handlePlay}
>
<div className="bg-[#FA67AB] rounded-full p-2">
{!isPlaying ? (
<PlayArrowIcon htmlColor="white" />
) : (
<PauseIcon htmlColor="white" />
)}
</div>
</button>
<div ref={waveformRef} id="waveform"></div>
<audio id="track" src={url} ref={trackRef} />
</div>
);
};

Keep track of pagination and go back most recent last page number in react

I am working on an ecommerce, where I am using material UI pagination component for implementing pagination. Here is new requirement arises. I need to add functionality in pagination: if user click on let's say respectively 3,7,11,13 if they click on browser back button they will go back to 11 then 7 then 3 and lastly 1. How do I do that?
I am using react, react router dom.
Here is pagination structure:
FYI, this is url and API structure:
URL and API structure
import React, { useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import ProductList from "../../components/common/ProductList/ProductList";
import {
GET_PRODUCTS_BY_BRAND,
GET_PRODUCTS_BY_CATEGORY,
GET_PRODUCTS_BY_SUBCATEGORY,
GET_PRODUCTS_BY_VENDOR,
} from "../../requests/HomePageApi";
import { Pagination } from "#material-ui/lab";
import "./ShopPage.scss";
const ShopPage = () => {
const { type, slug, subcategory } = useParams();
const [loading, setLoading] = useState(true);
// const [error, setError] = useState(false);
const [brands, setBrands] = useState([]);
const [colors, setColors] = useState([]);
const [sizes, setSizes] = useState([]);
const [products, setProducts] = useState(null);
const [filteredProducts, setFilteredProducts] = useState(null);
const [page, setPage] = React.useState(0);
const [count, setCount] = React.useState(1);
const [limit, setLimit] = React.useState(60);
const [total, setTotal] = React.useState(60);
const [sideFilter, setSideFilter] = useState(false);
const [vandor, setvandor] = useState({
vendorImg: "",
vendorName: "",
vendorSlug: "",
});
const [filter, setFilter] = useState({
// brands: "",
color: "",
size: "",
price: "",
});
const closeSideFilter = () => {
setSideFilter(false);
};
const getProducts = async (slug, qParams) => {
try {
let res;
if (type === "category") {
subcategory
? (res = await GET_PRODUCTS_BY_SUBCATEGORY(
slug,
subcategory,
qParams
))
: (res = await GET_PRODUCTS_BY_CATEGORY(slug, qParams));
}
if (type === "brand") res = await GET_PRODUCTS_BY_BRAND(slug, qParams);
if (type === "store") res = await GET_PRODUCTS_BY_VENDOR(slug, qParams);
if (res) setLoading(false);
if (res && res.products && res.products.length > 0) {
setProducts(res.products);
setFilteredProducts(res.products);
setTotal(res.total);
setCount(Math.ceil(res.total / limit));
if (type === "brand") {
setvandor({
vendorImg: `/assets/images/brand/${res.products[0].brand_logo}`,
vendorName: res.products[0].brand_name,
vendorSlug: res.products[0].brand_slug,
});
} else if (type === "store") {
setvandor({
vendorImg: `/assets/images/brand/${res.products[0].brand_logo}`,
vendorName: res.products[0].shop_name,
vendorSlug: res.products[0].vendorSlug,
});
}
if (res.colors) {
const uniqueColors = [...new Set(res.colors)];
setColors(uniqueColors);
}
if (res.sizes) {
const uniqueSizes = [...new Set(res.sizes)];
setSizes(uniqueSizes);
}
// if (res.brands) setBrands(res.brands);
}
} catch (error) {
console.log(error);
}
};
// console.log({ filteredProducts, filter, page, count, limit, total });
React.useMemo(() => {
let qParams = {
page: page,
limit: limit,
size: filter.size,
color: filter.color,
// brands: filter.brands,
price: filter.price.length ? `${filter.price[0]},${filter.price[1]}` : "",
};
if (slug) {
getProducts(slug, qParams);
}
}, [slug, page, limit, filter, count]);
React.useEffect(() => {
setPage(0);
}, [filter]);
const changeLimit = (limit) => {
setPage(0);
setLimit(limit);
};
const handleChange = (event, value) => {
// window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
setPage(value - 1);
};
const slugTitle = (slug) => slug.split("-").join(" ");
return (
<FadeTransition>
{/* {loading && (
<div className="section-big-py-space ratio_asos py-5">
<div className="custom-container">
<Skeleton type="ShopPage" />
</div>
</div>
)} */}
{!loading && products === null && (
<div className="section-big-py-space ratio_asos py-5">
<div className="custom-container">
<h3 style={{ color: "#32375A", textAlign: "center" }}>
Sorry, No Product Found!
</h3>
</div>
</div>
)}
{products && (
<div className="title-slug-section">
<h2 class="title-slug">{slug && slugTitle(slug)}</h2>
</div>
)}
{products && (
<section className="section-big-py-space ratio_asos">
{/* {type !== "category" && (
<div className="merchant-page-header">
<div className="custom-container">
<div
className="shadow-sm bg-white rounded p-3 mb-5 d-flex align-items-center w-100"
style={{ minHeight: "132px" }}
>
<div className="row align-items-center w-100">
<div className="col-lg-6">
<div className="row align-items-center">
{vandor && vandor.vendorImg && (
<div className="col-auto">
<Image
src={vandor.vendorImg}
alt={vandor.vendorName}
className="img-fluid merchant-img"
/>
</div>
)}
<div className="col-auto mt-lg-0 mt-2">
<h3 className="mb-0"> {vandor.vendorName} </h3>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)} */}
<div className="collection-wrapper">
<div className="custom-container">
<div className="row">
<div className="col-sm-3 collection-filter category-page-side">
{/* <SidebarFilter
type={type}
brands={brands}
colors={colors}
sizes={sizes}
onChange={(data) => setFilter(data)}
/> */}
<InnerCategory />
{products && (
<RowSlider title="New Products" products={products} />
)}
</div>
<div className="collection-content col-lg-9">
<div className="page-main-content">
<div className="collection-product-wrapper">
<div className="row">
<div className="col-xl-12">
{/* <Button
variant='contained'
className='bg-dark text-light d-lg-none mb-3 mt-2 w-100'
onClick={() => setSideFilter(true)}
>
<span className='filter-btn '>
<i
className='fa fa-filter'
aria-hidden='true'
></i>
Filter
</span>
</Button> */}
</div>
</div>
<MainFilter
type={type}
// brands={brands}
colors={colors}
sizes={sizes}
page={page}
limit={limit}
onCountChange={(c) => changeLimit(c)}
onChange={(data) => setFilter(data)}
/>
{/* <TopFilter
onCountChange={(x) => changeLimit(x)}
total={total}
page={page}
limit={limit}
setSideFilter={setSideFilter}
/> */}
{filteredProducts && (
<ProductList products={filteredProducts} />
)}
{count > 1 && (
<div className="d-flex justify-content-center mt-4">
<Pagination
count={count}
page={page + 1}
onChange={handleChange}
shape="rounded"
/>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</div>
</section>
)}
{!loading && products?.length === 0 && (
<div className="merchant-page-header">
<div className="custom-container pt-5">
<div
className="shadow-sm bg-white rounded p-3 mb-5 d-flex align-items-center justify-content-center w-100"
style={{ minHeight: "132px" }}
>
<h3 className="mb-0">No Products found!</h3>
</div>
</div>
</div>
)}
<Drawer
open={sideFilter}
className="add-to-cart"
onClose={() => setSideFilter(false)}
transitionDuration={400}
style={{ paddingLeft: "15px" }}
>
<SidebarFilter
onClose={closeSideFilter}
type={type}
// brands={brands}
colors={colors}
sizes={sizes}
onChange={(data) => setFilter(data)}
/>
</Drawer>
</FadeTransition>
);
};
export default ShopPage;
Usually you would like to have URL to represent selected page, so you could refresh the page and still be on the same page. Or share the exact page via copy-paste of URL. Especially on e-commerce sites. So I would recommend to sync selected page with URL.
While it's so common scenario, i have couple hooks for that. First of all - URL hook, which should work with your reactjs app setup.
https://www.npmjs.com/package/hook-use-url
and then couple more hooks to not worry about pagination details inside component:
usePage.js:
import useUrl from "hook-use-url";
export default function usePage() {
const url = useUrl();
const page = url.get({ variable: "page" })
? parseInt(url.get({ variable: "page" }), 10)
: 1;
const setPage = (value) => {
url.multipleActions({
setPairs: [{ variable: "page", value: value }],
});
};
return [page, setPage];
}
and usePerPage.js:
import useUrl from "hook-use-url";
export default function usePerPage() {
const url = useUrl();
const perPage = url.get({ variable: "per-page" })
? parseInt(url.get({ variable: "per-page" }), 10)
: 25;
const setPerPage = (value) => {
url.multipleActions({
setPairs: [
{ variable: "page", value: 1 },
{ variable: "per-page", value },
],
});
};
return [perPage, setPerPage];
}
Inside components you can use these like so:
(Take a note that which page is 1st depends on your backend API, in my case 1st page is always 1 and not 0, but mui.com component starts from 0 that's why there is -1 and +1).
function MyComp(){
const [page, setPage] = usePage();
const [perPage, setPerPage] = usePerPage();
// ....
return (
<TablePagination
count={totalRows}
page={page - 1}
onPageChange={(e, newPage) => {
setPage(newPage + 1);
}}
rowsPerPage={perPage}
onRowsPerPageChange={(e) => {
setPerPage(e.target.value);
}}
/>
)
}

DraftJS blockstyle (header-one) not getting triggered

I have used draftJS in my project as a note taker. The inline styles are working perfectly fine but the blockType aren't.
Used the RichUtils to toggleInlineStyles and toggleBlockType.
Main DraftJS.jsx file :
class PageContainer extends React.Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty(),
alignment: "align-left",
};
this.setDomEditorRef = ref => (this.domEditor = ref);
this.focus = () => this.domEditor.focus();
this.onChange = editorState => {
this.setState({
...this.state,
editorState,
});
};
}
handleKeyCommand = command => {
const newState = RichUtils.handleKeyCommand(
this.state.editorState,
command,
);
if (newState) {
this.onChange(newState);
return "handled";
}
return "not-handled";
};
blockStyleFn = () => {
return this.state.alignment;
};
handleAlignment = alignment => {
this.setState({
...this.state,
alignment,
});
};
toggleInlineStyle = inlineStyle => {
this.onChange(
RichUtils.toggleInlineStyle(this.state.editorState, inlineStyle),
);
};
toggleBlockType = blockType => {
this.onChange(RichUtils.toggleBlockType(this.state.editorState, blockType));
};
render() {
return (
<div className="flex items-center justify-center h-screen ">
<div className="overflow-hidden shadow-md min-w-[422px] rounded-2xl ">
<div
onClick={this.focus}
className="editors min-h-[210px] break-words text-base p-6 bg-white border-b-[1px] border-primary-400">
<Editor
blockStyleFn={this.blockStyleFn}
className="text-secondary-400"
editorState={this.state.editorState}
handleKeyCommand={this.handleKeyCommand}
onChange={this.onChange}
placeholder="Take notes here..."
spellCheck={true}
ref={this.setDomEditorRef}
/>
</div>
<div className="flex justify-between py-4 px-6 ">
<div className="flex flex-1 justify-between">
<InlineStyleControls
editorState={this.state.editorState}
onToggleInLineStyle={this.toggleInlineStyle}
onToggleBlock={this.toggleBlockType}
focus={this.focus}
/>
</div>
<VerticalBorder className="mx-7" />
<AlignTextControls
handleAlignment={this.handleAlignment}
focus={this.focus}
currentAlignment={this.state.alignment}
/>
</div>
</div>
</div>
);
}
}
InLineStyleControls.jsx :
const InlineStyleControls = props => {
const INLINE_STYLES = [
{
label: "Bold",
style: "BOLD",
icon: bold => <BoldIcon bold={bold} />,
},
{
label: "Italic",
style: "ITALIC",
icon: bold => <ItalicIcon bold={bold} />,
},
{
label: "Underline",
style: "UNDERLINE",
icon: bold => <UnderlineIcon bold={bold} />,
},
{
label: "H1",
style: "header-one",
icon: bold => <TextIcon bold={bold} />,
},
];
let currentStyle = props.editorState.getCurrentInlineStyle();
const selection = props.editorState.getSelection();
const blockType = props.editorState
.getCurrentContent()
.getBlockForKey(selection.getStartKey())
.getType();
return (
<div className="flex flex-1 justify-between">
{INLINE_STYLES.map(type => (
<StyleButton
key={type.label}
active={
type.label === "H1"
? type.style === blockType
: currentStyle.has(type.style)
}
icon={type.icon}
onToggle={type.label === "H1" ? props.onToggleBlock : props.onToggleInLineStyle}
style={type.style}
focus={props.focus}
/>
))}
<span className="cursor-pointer">
<TextColor />
</span>
</div>
);
};
StyleButton.jsx:
class StyleButton extends React.Component {
constructor() {
super();
this.onToggle = e => {
e.preventDefault();
this.props.onToggle(this.props.style);
};
}
render() {
return (
<span className={` cursor-pointer ${this.props.class} `} onMouseDown={this.onToggle} onMouseUp={this.props.focus} >
{this.props.icon(this.props.active ? 2 : 1)}
</span>
);
}
}
Problem : Everything is working fine except the "header-one" and I not able to figure out why.
I used the rich editor example to build this.
Edit : I found the problem. The problem was due to tailwind css. When I remove tailwind from my project everything is working fine. any idea on to how to make draftJS work with tailwind?

Resources