I have a problem where I am trying to click on a certain div being returned by my backend and update it. I am trying to have it setup so that you click edit and the blogTitle and blogBody become input bars where you type in the new title and new body. In my code you can see an update that works and one that does not. The problem with the one that does not is that when I hit edit all blogBody's and blogTitle's become input bars instead of just one.
This 5 second of the issue video might make more sense than my explanation.
Now in my code this mainly deals with the following:
<div style={{display : 'flex', flexDirection: 'column-reverse'}}>
{ getBasic.miscData.map(({blogBody, blogTitle, _id}, i) => {
return(
<div key={i} >
<p>{i}</p>
{prompt ? <p>{blogBody}</p> : <input onChange={handleChange} name='newerValue' value={Item.newerValue} placeholder='new title'></input>}
{prompt ? <p>{blogTitle}</p> : <input onChange={handleChange} name='newerValue2' value={Item.newerValue2} placeholder='new title'></input>}
<p>{blogTitle}</p>
<p>{_id}</p>
<button onClick={deleteOne} value={_id}>Delete</button>
{prompt ? <button onClick={() => { setPrompt(!prompt)}} >Edit</button> : <button onClick={() => { setPrompt(!prompt)}} >Submit</button>}
</div>
Here is the full code:
import './App.css';
import axios from 'axios';
import React, { useState, useEffect } from 'react'
function App() {
const [getBasic, setGetBasic] = useState()
const [Item, setItem] = useState(
{
title: '',
description: '',
id: '',
newValue: '',
newValue2: '',
newerValue: '',
newerValue2: '',
previousValue: ''
}
)
const [prompt, setPrompt] = useState(true)
async function fetchData() {
await axios.get('http://localhost:3001/api')
.then(result => setGetBasic(result.data))
}
function handleChange(event) {
let {name, value} = event.target;
setItem(prevInput => {
return(
{
...prevInput,
[name]: value,
}
);
});
// console.log(Item);
}
//delete
function deleteOne(event) {
event.preventDefault();
const aValue = event.target.value;
const deleteItem = {
// id: Item.id
id: aValue
}
console.log(deleteItem)
axios.post('http://localhost:3001/api/delete', deleteItem);
window.location.reload();
// console.log(deleteItem)
}
function updateOne(event) {
event.preventDefault();
const updatedItem = {
previousValue: Item.previousValue,
newValue: Item.newValue,
newValue2: Item.newValue2
}
console.log(updatedItem)
axios.put('http://localhost:3001/api/update', updatedItem);
window.location.reload();
// console.log(deleteItem)
}
useEffect(() => {
fetchData()
}, [])
if (getBasic) {
return (
<div className="App">
<br />
{/* A version of update that works */}
<input onChange={handleChange} name="previousValue" value={Item.previousValue} placeholder="old value id" />
<input onChange={handleChange} name='newValue' value={Item.newValue} placeholder='new title'></input>
<input onChange={handleChange} name='newValue2' value={Item.newValue2} placeholder='new body'></input>
<button onClick={updateOne} >update</button>
{/* A version of update that does not work*/}
<div style={{display : 'flex', flexDirection: 'column-reverse'}}>
{ getBasic.miscData.map(({blogBody, blogTitle, _id}, i) => {
return(
<div key={i} >
<p>{i}</p>
{prompt ? <p>{blogBody}</p> : <input onChange={handleChange} name='newerValue' value={Item.newerValue} placeholder='new title'></input>}
{prompt ? <p>{blogTitle}</p> : <input onChange={handleChange} name='newerValue2' value={Item.newerValue2} placeholder='new title'></input>}
<p>{blogTitle}</p>
<p>{_id}</p>
<button onClick={deleteOne} value={_id}>Delete</button>
{prompt ? <button onClick={() => { setPrompt(!prompt)}} >Edit</button> : <button onClick={() => { setPrompt(!prompt)}} >Submit</button>}
</div>
)})}
</div>
</div>
)} else {
return (
<div>
<h1>Loading</h1>
</div>
)
};
}
export default App;
Now I understand why it is doing this, I just cannot think through the logic of getting my edit button to only edit one at a time. Also I understand the update will not work once I fix this, I am trying to solve this problem of editing 1 at a time first before setting it up how the other update one is.
Edit: so far I received one dislike and no comments I re read through this and realized it lacks a bit of focus thus made the code simpler:
import './App.css';
import axios from 'axios';
import React, { useState, useEffect } from 'react'
function App() {
const [getBasic, setGetBasic] = useState()
const [Item, setItem] = useState(
{
title: '',
description: '',
id: '',
newerValue: '',
newerValue2: '',
previousValue: ''
}
)
const [prompt, setPrompt] = useState(true)
async function fetchData() {
await axios.get('http://localhost:3001/api')
.then(result => setGetBasic(result.data))
console.log(getBasic)
}
useEffect(() => {
fetchData()
}, [])
if (getBasic) {
return (
<div className="App">
<div style={{display : 'flex', flexDirection: 'column-reverse'}}>
{ getBasic.miscData.map(({blogBody, blogTitle, _id}, i) => {
return(
<div key={i} >
<p>{i}</p>
{prompt ? <p>{blogBody}</p> : <input ></input>}
{prompt ? <p>{blogTitle}</p> : <input ></input>}
<p>{blogTitle}</p>
<p>{_id}</p>
{prompt ? <button onClick={() => { setPrompt(!prompt)}} >Edit</button> : <button onClick={() => { setPrompt(!prompt)}} >Submit</button>}
</div>
)})}
</div>
</div>
)} else {
return (
<div>
<h1>Loading</h1>
</div>
)
};
}
export default App;
This is all thats needed to work with to solve the issue at hand which is edit one at a time instead of all of them. Leaving the op for reference.
Have a component for each element so they each have their own unique prompt
For example,
//Element.js
import React from "react";
export default function Element({
i,
blogTitle,
handleChange,
_id,
deleteOne,
blogBody,
Item
}) {
const [prompt, setPrompt] = React.useState(true);
return (
<div>
<p>{i}</p>
{prompt ? (
<p>{blogBody}</p>
) : (
<input
onChange={handleChange}
name="newerValue"
value={Item.newerValue}
placeholder="new title"
></input>
)}
{prompt ? (
<p>{blogTitle}</p>
) : (
<input
onChange={handleChange}
name="newerValue2"
value={Item.newerValue2}
placeholder="new title"
></input>
)}
<p>{blogTitle}</p>
<p>{_id}</p>
<button onClick={deleteOne} value={_id}>
Delete
</button>
{prompt ? (
<button
onClick={() => {
setPrompt(!prompt);
}}
>
Edit
</button>
) : (
<button
onClick={() => {
setPrompt(!prompt);
}}
>
Submit
</button>
)}
</div>
);
}
//App.js
Now in the body return of your main file you could just do this
First import import Element from "./Element"; Then add the new component with specified props
{getBasic.miscData.map(({ blogBody, blogTitle, _id }, i) => {
return (
<Element
i={i}
blogTitle={blogTitle}
handleChange={handleChange}
_id={_id}
deleteOne={deleteOne}
blogBody={blogBody}
Item={Item}
/>
);
})}
Related
I have a form and song state where I am trying to attach a new field "appleMusicId", however whenever I do this, it resets the timeDescription and sceneDescription values.
I've been trying to fix for hours and am worried I'm overlooking something simple.
As an example when I first hit Submit I get this for the console.log values
{
"episodeId": 5,
"spotifyId": "3NanY0K4okhIQzL33U5Ad8",
"name": "Boy's a liar",
"artistName": "PinkPantheress",
"timeDescription": 12,
"sceneDescription": "First song at the party."
}
If I then click the "Add Apple ID" button, the values become this on onSubmit and the time and scene removed.
{
"episodeId": 5,
"spotifyId": "3NanY0K4okhIQzL33U5Ad8",
"name": "Boy's a liar",
"artistName": "PinkPantheress",
"appleMusicId": "dummyId"
}
Component
import { TextField } from "#/components/TextField";
import { Button } from "#chakra-ui/react";
import { Formik, Form } from "formik";
import { validate } from "graphql";
import { useState } from "react";
const initialValues = {
name: "",
artistName: "",
episodeId: undefined,
movieId: undefined,
spotifyId: "",
appleMusicId: "",
sceneDescription: "",
timeDescription: undefined,
};
const AddSong = () => {
const [song, setSong] = useState(initialValues);
const submit = (values: any) => {
console.log(values, "values");
}
const addAppleId = () => {
setSong((v) => {
return {
...v,
appleMusicId: "dummyId",
};
});
};
return (
<Formik
initialValues={song}
onSubmit={(values) => submit(values)}
validationSchema={validate}
enableReinitialize={true}
>
<Form>
<TextField name={"name"} label={"Name"} type="text" />
<TextField name={"artistName"} label={"Artist Name"} type="text" />
<TextField
name={"timeDescription"}
label={"Time Description"}
type="number"
placeholder="How many minutes in, was this song played?"
/>
<TextField
name={"sceneDescription"}
label={"Scene Description"}
type="text"
/>
<Button onClick={addAppleId}>Test Add Apple Id</Button>
<Button isLoading={isLoading} type={"submit"}>
Create
</Button>
</Form>
</Formik>
)
};
TextField (if you need to see it)
export const TextField = ({ label, value, ...props }: TextFieldProps) => {
const [field, meta] = useField(props);
return (
<FormControl
isInvalid={
meta.touched && (meta.error && meta.error?.length > 0 ? true : false)
}
mb={3}
>
<FormLabel
aria-label={field.name}
htmlFor={field.name}
mb={0}
textColor={useColorModeValue('gray.700', 'gray.100')}
>
{label}
</FormLabel>
<Input
{...field}
{...props}
value={value ? value : undefined}
autoComplete="off"
borderRadius={0}
paddingLeft={2}
border={"2px solid"}
borderColor={"gray.200"}
_invalid={{ borderColor: "error-red" }}
/>
<ErrorMessage
name={field.name}
component="span"
className="text-sm pt-2 text-red-error"
/>
<Text pt={1} fontSize={'sm'} color={useColorModeValue('gray.400', 'gray.600')}>{props.footerText}</Text>
</FormControl>
);
};
Hi Bro I have send you proposal on UW Plathform . Please accept it will reolve issue
Also It would be grat if you check out my proposal.
Best
Asadbek Savronov!
It seems like the issue is happening because you are updating the song state outside of the Formik component and therefore the form values are not updating.
To fix the issue, you need to update the form values within the Formik component. You can do this by passing the setFieldValue prop to your "Add Apple ID" button and updating the form values directly within the Formik component.
Possible Solution:
const AddSong = () => {
return (
<Formik
initialValues={initialValues}
onSubmit={(values) => console.log(values)}
validationSchema={validate}
enableReinitialize={true}
>
{({ setFieldValue }) => (
<Form>
<TextField name={"name"} label={"Name"} type="text" />
<TextField name={"artistName"} label={"Artist Name"} type="text" />
<TextField
name={"timeDescription"}
label={"Time Description"}
type="number"
placeholder="How many minutes in, was this song played?"
/>
<TextField
name={"sceneDescription"}
label={"Scene Description"}
type="text"
/>
<Button onClick={() => setFieldValue("appleMusicId", "dummyId")}>
Test Add Apple Id
</Button>
<Button isLoading={isLoading} type={"submit"}>
Create
</Button>
</Form>
)}
</Formik>
);
};
no issue in your code.
I have tried to create my project and run it.
import { Formik, Form, useField, ErrorMessage, } from "formik";
// import { validate } from "graphql";
import { useState } from "react";
import {FormControl, FormLabel, Input} from '#material-ui/core'
import {useColorModeValue, Button, Text} from '#chakra-ui/react'
const TextField = ({ label, value, ...props }) => {
const [field, meta] = useField(props);
return (
<FormControl
isInvalid={
meta.touched && (meta.error && meta.error?.length > 0 ? true : false)
}
mb={3}
>
<FormLabel
aria-label={field.name}
htmlFor={field.name}
mb={0}
textColor={useColorModeValue('gray.700', 'gray.100')}
>
{label}
</FormLabel>
<Input
{...field}
{...props}
value={value ? value : undefined}
autoComplete="off"
borderRadius={0}
paddingLeft={2}
border={"2px solid"}
borderColor={"gray.200"}
_invalid={{ borderColor: "error-red" }}
/>
<ErrorMessage
name={field.name}
component="span"
className="text-sm pt-2 text-red-error"
/>
<Text pt={1} fontSize={'sm'} color={useColorModeValue('gray.400', 'gray.600')}>{props.footerText}</Text>
</FormControl>
);
};
const initialValues = {
name: "111111111",
artistName: "11111111",
appleMusicId: "",
sceneDescription: "12",
timeDescription: 1,
};
export const AddSong = () => {
const [song, setSong] = useState(initialValues);
const [isLoading, setIsLoading] = useState(true)
const submit = (values) => {
console.log(values, "values");
}
const addAppleId = () => {
setSong((v) => {
return {
...v,
appleMusicId: "dummyId",
};
});
};
return (
<Formik
initialValues={song}
onSubmit={(values) => submit(values)}
// validationSchema={validate}
enableReinitialize={true}
>
<Form>
<TextField name={"name"} label={"Name"} type="text" />
<TextField name={"artistName"} label={"Artist Name"} type="text" />
<TextField
name={"timeDescription"}
label={"Time Description"}
type="number"
placeholder="How many minutes in, was this song played?"
/>
<TextField
name={"sceneDescription"}
label={"Scene Description"}
type="text"
/>
<Button onClick={addAppleId}>Test Add Apple Id</Button>
<Button type={"submit"}>
Create
</Button>
</Form>
</Formik>
)
};
I am getting log like this:
{name: '111111111', artistName: '11111111', appleMusicId: '', sceneDescription: '12', timeDescription: 1}
and after adding dummy apple id:
{name: '111111111', artistName: '11111111', appleMusicId: 'dummyId', sceneDescription: '12', timeDescription: 1}
I am trying to make my list of stuff draggable, because it will be used to reorder a set of items retrieved from an API. All these items will have a prop named number, and with the drag and drop, I should change the order and the number in the visualization.
I followed along with some tutorials, the documentation, and many StackOverflow questions, but I have not been able to fix the problem. Each time I try to grab an element, in the console appears a warning message with the following content:
react_devtools_backend.js:4012
react-beautiful-dndUnable to find draggable with id: 1👷
This is a development-only message. It will be removed in production builds.
Here is the code I am working on:
Categories.tsx file with all the logic:
// Global imports
import {DragDropContext,Droppable, DropResult} from "react-beautiful-dnd";
// Local imports
// Context
import { menuContext } from "../menu";
// Components
import CategoryItem from "./cateogory";
// Types
import Category from "../../types/category";
function Categories(){
const {categories, setCategories} = useContext(menuContext);
/*functions and logic*/
const onDragEnd = (result:DropResult) => {
const {destination, source} = result;
if (!destination) return;
const items = Array.from(categories);
const [newOrder] = items.splice(source.index, 1);
items.splice(destination.index, 0, newOrder);
setCategories(items);
}
return (
<div className="greyBox">
<header className="header_menu">
<p>All your categories</p>
<div className="searchContainer">
<AiOutlineSearch className="searchIcon" />
<input type={"text"} placeholder={"Search"} className="searchBar" value={search}
onChange={(e) => handleModification(e)} />
</div>
</header>
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="categories">
{(provided) =>
<div {...provided.droppableProps}
ref={provided.innerRef}
className="categoriesContainer" >
{filteredCategories.map((category: Category, idx: number) => {
return (
<CategoryItem key={category.id} idx={idx} category={category}/>
)
})}
</div>}
</Droppable>
</DragDropContext>
<button onClick={(e) => confirmChanges(e)} className="submitBTN bottomButton">Confirm changes</button>
</div>
)
}
export default Categories;
Category.tsx File into I have the single Item:
// Global imports
import React, { useContext, useState } from "react";
import { Draggable } from "react-beautiful-dnd";
// Local imports
// Context
import { menuContext } from "../menu";
// Types
import Category from "../../types/category";
function CategoryItem(props:{category:Category, idx:number}) {
const {categories, setCategories} = useContext(menuContext);
const {selectedCategory, setSelectedCategory} = useContext(menuContext);
/*Functions and various small things*/
let category = categories.find((category:Category) => category.id === props.category.id);
return (
<Draggable key={props.category.id} draggableId={props.category.id.toString()} index={props.idx}>
{(provided, snapshot) => (
<div className={selectedCategory === category.id ? "category activeCategory":"category"}
onClick={() => handleCategoryClick()}
style={{backgroundColor: category.isActive ? "" : "#FF7B7B"}}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div className="alwaysActiveCategory" style={{alignItems: props.category.id === selectedCategory ? "center" : "normal"}}>
<MdOutlineDragIndicator className="dragIcon" />
<p style={{display: props.category.id === selectedCategory ? "none" : "block" }}>{category.name}</p>
</div>
<aside style={{display: selectedCategory === category.id ? "flex": "none"}}>
<form className="modifyCategory" onClick={(e) => e.stopPropagation()} onSubmit={e => updateCategory(e)}>
<label htmlFor="categoryName">Category Name</label>
<p id="categoryProblem3">The name must be given</p>
<input value={name} onChange={(e) => changeName(e)} type="text" name="categoryName" id="categoryName" />
<label htmlFor="categoryDescription">Category Description</label>
<p id="categoryProblem4">The description must be given</p>
<textarea value={description} onChange={(e) => changeDescription(e)} name="categoryDescription" id="categoryDescription"
style={{maxWidth: "85%"}} />
<button>Update Category</button>
<div className="manageCategory">
<button onClick={(e) => hideCategory(e) }> <BiHide className="hideIcon"
style={{backgroundColor: categories.find((category: Category) => category.id === props.category.id).isActive ? "#FF7B7B" : "#76CB8E"}} />
{category.isActive ? "Hide Category" : "Show Category"}
</button>
<button onClick={(e) => deleteCategory(e)}> <BsTrash className="deleteIcon" /> Delete Category</button>
</div>
</form>
</aside>
</div>
)}
</Draggable>
)
}
export default CategoryItem;
They are pretty long, I tried to take away everything useless.
I don't know where the problem is in this script. It seems all fine to me, but there are some problems otherwise it wouldn't broke each time. If someone knows where my mistakes are, please tell me. Thank you a lot
I am trying to create a custom component that iterates over an array of data and display for each item an input field type radio with its according label, it works but for some reason the data is displayed three times, instead of just once for each field and I cant figure out why, I just want to render just three inputs, why does this behavior occur, am I missing something? Here is my code:
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Tooltip } from '#progress/kendo-react-tooltip';
import { Form, Field, FormElement } from '#progress/kendo-react-form';
import { RadioGroup, RadioButton } from '#progress/kendo-react-inputs';
import { Error } from '#progress/kendo-react-labels';
import { Input } from '#progress/kendo-react-inputs';
const emailRegex = new RegExp(/\S+#\S+\.\S+/);
const data = [
{
label: 'Female',
value: 'female',
},
{
label: 'Male',
value: 'male',
},
{
label: 'Other',
value: 'other',
},
];
const emailValidator = (value) =>
emailRegex.test(value) ? '' : 'Please enter a valid email.';
const EmailInput = (fieldRenderProps) => {
const { validationMessage, visited, ...others } = fieldRenderProps;
return (
<div>
<Input {...others} />
{visited && validationMessage && <Error>{validationMessage}</Error>}
</div>
);
};
const Component = () => {
return (
<>
{data.map((item, index) => {
return (
<p>
<input
name="group1"
type="radio"
value={item.value}
label={item.label}
key={index}
/>
<label>{item.label}</label>
</p>
);
})}
</>
);
};
const App = () => {
const handleSubmit = (dataItem) => alert(JSON.stringify(dataItem, null, 2));
const tooltip = React.useRef(null);
return (
<Form
onSubmit={handleSubmit}
render={(formRenderProps) => (
<FormElement
style={{
maxWidth: 650,
}}
>
<fieldset className={'k-form-fieldset'}>
<legend className={'k-form-legend'}>
Please fill in the fields:
</legend>
<div className="mb-3">
<Field
name={'firstName'}
component={Input}
label={'First name'}
/>
</div>
<div className="mb-3">
<Field name={'lastName'} component={Input} label={'Last name'} />
</div>
<div className="mb-3">
<Field
name={'email'}
type={'email'}
component={EmailInput}
label={'Email'}
validator={emailValidator}
/>
</div>
</fieldset>
<div
onMouseOver={(event) =>
tooltip.current && tooltip.current.handleMouseOver(event)
}
onMouseOut={(event) =>
tooltip.current && tooltip.current.handleMouseOut(event)
}
>
<RadioGroup data={data} item={Component} />
<Tooltip
ref={tooltip}
anchorElement="target"
position="right"
openDelay={300}
parentTitle={true}
/>
</div>
<div className="k-form-buttons">
<button
type={'submit'}
className="k-button k-button-md k-rounded-md k-button-solid k-button-solid-base"
disabled={!formRenderProps.allowSubmit}
>
Submit
</button>
</div>
</FormElement>
)}
/>
);
};
ReactDOM.render(<App />, document.querySelector('my-app'));
and here is an example:
https://stackblitz.com/edit/react-rfb1ux-jpqvwy?file=app/main.jsx
<RadioGroup data={[1]} item={Component} />
RadioGroup assumes that Component element is to be rendered over data array ... so it renders like this -->
data.map(val=><Component />)
here size of data array is 3 .
or
<Component />
Radio Group will do the mapping for you. Try changing Component to:
const Component = (props) => {
return (
<p>
<input
name="group1"
type="radio"
value={props.children.props.value}
label={props.children.props.label}
key={props.children.props.label}
/>
<label>{props.children.props.label}</label>
</p>
);
};
This will then map out the items for you.
seems like this works,
const Component = ({ children: { props } }) => {
const { value, label } = { ...props };
return (
<p>
<input name="group1" type="radio" value={value} label={label} />
<label>{label}</label>
</p>
);
};
I am building a simple todo-esk feature where if a user clicks the edit icon, only that item is editable. I implement this currently with a useState hook, const [editingMemberName, setEditingMemberName] = useState(false), but when I call a function, editMemberName all instances of items show an input field. This is not the experience I am going for.
Here are some screen shots that should make this more clear:
As you can see, I have two cards, but when I click the tool icon, both input boxes are displayed.
Here is the code:
const [editingMemberName, setEditingMemberName] = useState(false)
const [memberName, setMemberName] = useState('')
const handleChangeName = (e) => {
setMemberName(e.target.value)
}
// Update member name
const editMemberName = async (e) => {
setEditingMemberName(true)
}
const memberItems = members.map((member) => {
return (
<div
key={member.id}
>
<div className="flex items-center gap-4 w-full">
{editingMemberName ? (
<input
type="text"
placeholder="Johnny Appleseed"
onChange={handleChangeName}
/>
) : (
<>
<div>
{member.name}
</div>
<h3>{member.name}</h3>
</>
)}
</div>
<div>
{editingMemberName ? (
<button
onClick={() => updateMemberName(member.id)}
>
<CgCheckO size=".75em" />
</button>
) : (
<button
onClick={() => editMemberName(member.id)}
>
<FiTool size=".75em" />
</button>
)}
</div>
</div>
)
I've realized that editingMemberName hook operates on all instances, but I am not sure how to only target a single item.
Note: you can assume that the members array has a unique id for each item.
members: [
{
name: "Johnny",
id: 123
},
{
name: "George",
id: 456
}
]
That's because you are referring the boolean to all the boxes and not an individual element, use
const [editingMemberName, setEditingMemberName] = useState(members.map(e => false))
Something along the lines
const editMemberName = async (memberID, index) => {
let new_editing_members_state = members.map(e => false)
new_editing_members_state[index] = true
setEditingMemberName(new_editing_members_state)
}
const memberItems = members.map((member, index) => {
return (
<div
key={member.id}
>
<div className="flex items-center gap-4 w-full">
{editingMemberName ? (
<input
type="text"
placeholder="Johnny Appleseed"
onChange={handleChangeName}
/>
) : (
<>
<div>
{member.name}
</div>
<h3>{member.name}</h3>
</>
)}
</div>
<div>
{editingMemberName[index] ? (
<button
onClick={() => updateMemberName(member.id)}
>
<CgCheckO size=".75em" />
</button>
) : (
<button
onClick={() => editMemberName(member.id,index)}
>
<FiTool size=".75em" />
</button>
)}
</div>
</div>
)
I am currently making a project over the database I created using Mock API. I created a button, created addToFavorites function. When the button was clicked, I wanted the selected product's information to go to the favorites, but I couldn't. I would be glad if you could help me on how to do this.
(Favorites.js empty now. I got angry and deleted all the codes because I couldn't.)
(
Recipes.js
import React, { useState, useEffect } from "react"
import axios from "axios"
import "./_recipe.scss"
import Card from "../Card"
function Recipes() {
const [recipes, setRecipes] = useState([])
const [favorites, setFavorites] = useState([])
useEffect(() => {
axios
.get("https://5fccb170603c0c0016487102.mockapi.io/api/recipes")
.then((res) => {
setRecipes(res.data)
})
.catch((err) => {
console.log(err)
})
}, [])
const addToFavorites = (recipes) => {
setFavorites([...favorites, recipes])
console.log("its work?")
}
return (
<div className="recipe">
<Card recipes={recipes} addToFavorites={addToFavorites} />
</div>
)
}
export default Recipes
Card.js
import React, { useState } from "react"
import { Link } from "react-router-dom"
import { BsClock, BsBook, BsPerson } from "react-icons/bs"
function Card({ recipes, addToFavorites }) {
const [searchTerm, setSearchTerm] = useState("")
return (
<>
<div className="recipe__search">
<input
type="text"
onChange={(event) => {
setSearchTerm(event.target.value)
}}
/>
</div>
<div className="recipe__list">
{recipes
.filter((recipes) => {
if (searchTerm === "") {
return recipes
} else if (
recipes.title.toLowerCase().includes(searchTerm.toLowerCase())
) {
return recipes
}
})
.map((recipe) => {
return (
<div key={recipe.id} className="recipe__card">
<img src={recipe.image} alt="foods" width={350} height={230} />
<h1 className="recipe__card__title">{recipe.title}</h1>
<h3 className="recipe__card__info">
<p className="recipe__card__info--icon">
<BsClock /> {recipe.time} <BsBook />{" "}
{recipe.ingredientsCount} <BsPerson />
{recipe.servings}
</p>
</h3>
<h3 className="recipe__card__desc">
{recipe.description.length < 100
? `${recipe.description}`
: `${recipe.description.substring(0, 120)}...`}
</h3>
<button type="button" className="recipe__card__cta">
<Link
to={{
pathname: `/recipes/${recipe.id}`,
state: { recipe }
}}
>
View Recipes
</Link>
</button>
<button onClick={() => addToFavorites(recipes)}>
Add to favorites
</button>
</div>
)
})}
</div>
</>
)
}
export default Card
Final Output:
I have implemented the addToFavorite() and removeFavorite() functionality, you can reuse it the way you want.
I have to do bit of modification to the code to demonstrate its working, but underlying functionality of addToFavorite() and removeFavotie() works exactly the way it should:
Here is the Card.js where these both functions are implemented:
import React, { useState } from "react";
import { BsClock, BsBook, BsPerson } from "react-icons/bs";
function Card({ recipes }) {
const [searchTerm, setSearchTerm] = useState("");
const [favorite, setFavorite] = useState([]); // <= this state holds the id's of all favorite reciepies
// following function handles the operation of adding fav recipes's id's
const addToFavorite = id => {
if (!favorite.includes(id)) setFavorite(favorite.concat(id));
console.log(id);
};
// this one does the exact opposite, it removes the favorite recipe id's
const removeFavorite = id => {
let index = favorite.indexOf(id);
console.log(index);
let temp = [...favorite.slice(0, index), ...favorite.slice(index + 1)];
setFavorite(temp);
};
// this variable holds the list of favorite recipes, we will use it to render all the fav ecipes
let findfavorite = recipes.filter(recipe => favorite.includes(recipe.id));
// filtered list of recipes
let filtered = recipes.filter(recipe => {
if (searchTerm === "") {
return recipe;
} else if (recipe.title.toLowerCase().includes(searchTerm.toLowerCase())) {
return recipe;
}
});
return (
<div className="main">
<div className="recipe__search">
<input
type="text"
onChange={event => {
setSearchTerm(event.target.value);
}}
/>
</div>
<div className="recipe-container">
<div className="recipe__list">
<h2>all recipes</h2>
{filtered.map(recipe => {
return (
<div key={recipe.id} className="recipe__card">
<img src={recipe.image} alt="foods" width={50} height={50} />
<h2 className="recipe__card__title">{recipe.title}</h2>
<h4 className="recipe__card__info">
<p>
<BsClock /> {recipe.time} <BsBook />{" "}
{recipe.ingredientsCount} <BsPerson />
{recipe.servings}
</p>
</h4>
<h4 className="recipe__card__desc">
{recipe.description.length < 100
? `${recipe.description}`
: `${recipe.description.substring(0, 120)}...`}
</h4>
<button onClick={() => addToFavorite(recipe.id)}>
add to favorite
</button>
</div>
);
})}
</div>
<div className="favorite__list">
<h2>favorite recipes</h2>
{findfavorite.map(recipe => {
return (
<div key={recipe.id} className="recipe__card">
<img src={recipe.image} alt="foods" width={50} height={50} />
<h2 className="recipe__card__title">{recipe.title}</h2>
<h4 className="recipe__card__info">
<p className="recipe__card__info--icon">
<BsClock /> {recipe.time} <BsBook />{" "}
{recipe.ingredientsCount} <BsPerson />
{recipe.servings}
</p>
</h4>
<h4 className="recipe__card__desc">
{recipe.description.length < 100
? `${recipe.description}`
: `${recipe.description.substring(0, 120)}...`}
</h4>
<button onClick={() => removeFavorite(recipe.id)}>
remove favorite
</button>
</div>
);
})}
</div>
</div>
</div>
);
}
export default Card;
Here is the live working app : stackblitz
You can get the previous favourites recipes and add the new ones.
const addToFavorites = (recipes) => {
setFavorites(prevFavourites => [...prevFavourites, recipes])
console.log("its work?")
}