I want to filter over an array using react hooks. It should be a fairly straight forward task, but I am assuming it is something to do with hooks being updated asynchronously, although this could be wrong.
I am fairly stumped, but have included a code sandbox and my code below:
const teams_data = [
"tottenham",
"arsenal",
"man utd",
"liverpool",
"chelsea",
"west ham"
];
function App() {
const [teams, setTeams] = React.useState(teams_data);
const [search, setSearch] = React.useState("");
return (
<div className="App">
<input
onChange={e => {
const test = teams.filter(team => {
return team.toLowerCase().includes(e.target.value.toLowerCase());
});
console.log("test: ", test);
// uncomment line below and teams is logged as I want
setTeams(test);
setSearch(e.target.value);
}}
type="text"
value={search}
/>
{teams.map(team => (
<p>{team}</p>
))}
</div>
);
}
You need to filter the original data :
const test = teams_data.filter(team => {
return team.toLowerCase().includes(e.target.value.toLowerCase());
});
https://codesandbox.io/s/thirsty-austin-uqx8k
You just need to add another state for search results
const [data , setData] = useState(teams);
const [query, setQuery] = useState('')
const[res , setRes] = useState([]);
return (
<div className="App container">
<form onSubmit = {(e) => e.preventDefault()}>
<input type = "search" className = "srh" placeholder = "search about..."
onChange = {(e) => {
const test = data.filter(team => {
return (
team.toLowerCase().includes(e.target.value.toLowerCase())
)
})
setRes(test)
if(e.target.value === '') setRes([])
}}
/>
</form>
<div>
{
res.map((item , i) => (
<p key = {i}>{item}</p>
))
}
</div>
</div>
);
I've made custom hook.
It receives the array as a first param
the search variable as a second
and the property you want to filter by
I hope it's helpfull
export function useSearch(array: any[], search: string, field: string) {
const filteredArray = array.filter((entry) => {
if (search === "") return entry;
else if (
entry[field].toLocaleLowerCase().includes(search.toLocaleLowerCase())
)
return entry;
});
return {
filteredArray
};
}
Them apply the filtered array to your map function
import { useSearch } from "./useSearch";
import { useState } from "react";
const array = [
{
id: 1,
name: "Humberto Guenzo Yoshimoto"
},
{
id: 2,
name: "Diego Braga"
},
{
id: 3,
name: "Hudson Teixeira"
},
{
id: 4,
name: "Matheus Doimo"
}
];
type FilteredArrayTypes = {
id: number;
name: string;
};
export default function App() {
const [searchByFullName, setSearchByFullName] = useState("");
const { filteredArray } = useSearch(array, searchByFullName, "name");
return (
<div className="App">
<h1>Search list</h1>
<input
onChange={(e) => setSearchByFullName(e.target.value)}
type="text"
value={searchByFullName}
placeholder="search"
/>
{filteredArray.map((entry: FilteredArrayTypes) => {
return (
<ul>
<li>{entry.name}</li>
</ul>
);
})}
</div>
);
}
Here goes a sandbox with the code: here
Related
bit of a newbie at js here and I'm creating a search filter for my NFT marketplace platform.
I've successfully been able to create a basic input function to search for the name of the item uploaded but because the category of the item is an array, I'm finding difficulty creating checkbox inputs to filter the search results by category.
Here's the piece my code with the search by name function:
export function Results() {
const { fetchNFTs, setError, currentAccount } = useContext(
NFTMarketplaceContext
);
const [nfts, setNfts] = useState([]);
const [nftsCopy, setNftsCopy] = useState([]);
useEffect(() => {
try {
// if (currentAccount) {
fetchNFTs().then((items) => {
setNfts(items.reverse());
setNftsCopy(items);
console.log(nfts);
});
// }
} catch (error) {
setError("Please reload the browser", error);
}
}, []);
const onHandleSearch = (value) => {
const filteredNFTS = nfts.filter(({ name }) =>
name.toLowerCase().includes(value.toLowerCase())
);
if (filteredNFTS.length === 0) {
setNfts(nftsCopy);
} else {
setNfts(filteredNFTS);
}
};
const onClearSearch = () => {
if (nfts.length && nftsCopy.length) {
setNfts(nftsCopy);
}
};
return {
<SearchBar
onHandleSearch={onHandleSearch}
onClearSearch={onClearSearch}
/>
}
And my search bar:
import React, { useEffect, useState } from "react";
const SearchBar = ({ onHandleSearch, onClearSearch }) => {
const [search, setSearch] = useState("");
const [searchItem, setSearchItem] = useState(search);
useEffect(() => {
const timer = setTimeout(() => setSearch(searchItem), 1000);
return () => clearTimeout(timer);
}, [searchItem]);
useEffect(() => {
if (search) {
onHandleSearch(search);
} else {
onClearSearch();
}
}, [search]);
return (
<div className="SearchBar">
<div className="section-filters-bar-actions">
<form className="form">
<div className="form-item split">
<div className="form-input small">
<label for="items-search">Search Items</label>
<input type="text" id="items-search" style={{width:'370px'}}
onChange={(e) => setSearchItem(e.target.value)}
value={searchItem}/>
</div>
</div>
</form>
</div>
</div>
);
}
export default SearchBar;
And finally the array in the upload page / function:
const UloadNFT = ({ uploadToIPFS, createNFT }) => {
const [price, setPrice] = useState("");
const [active, setActive] = useState(0);
const [name, setName] = useState("");
const [website, setWebsite] = useState("");
const [description, setDescription] = useState("");
const [royalties, setRoyalties] = useState("");
const [fileSize, setFileSize] = useState("");
const [category, setCategory] = useState(0);
const [properties, setProperties] = useState("");
const [image, setImage] = useState(null);
const router = useRouter();
const categoryArry = [
{
category: "AI Generated",
},
{
category: "Artwork",
},
{
category: "Audio",
},
{
category: "Collectible",
},
{
category: "Customization",
},
{
category: "Digital Land",
},
{
category: "Gaming",
},
{
category: "Utility",
},
];
return (
<div className="UloadNFT" style={{height:'1350px'}}>
<div style={{display:'inline-grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '0rem'}}>
<div className={Style.upload_box} style={{backgroundColor:'var(--primary-white)',padding:'30px 30px 30px 30px'}}>
<div className={formStyle.Form_box_input}>
<label htmlFor="nft">Item Name</label>
<input
type="text"
placeholder="Name your token..."
className={formStyle.Form_box_input_userName}
onChange={(e) => setName(e.target.value)}
style={{border:'1px solid #202020', backgroundColor:'transparent'}}
/>
</div>
// category upload input area
<div className={formStyle.Form_box_input}>
<label htmlFor="name">Choose Category</label>
<p className={Style.upload_box_input_para}>
Choose a specific category for your token
</p>
<br></br>
<div className={Style.upload_box_slider_div}>
{categoryArry.map((el, i) => (
<div
className={`${Style.upload_box_slider} ${
active == i + 1 ? Style.active : ""
}`}
key={i + 1}
onClick={() => (setActive(i + 1), setCategory(el.category))}
>
<p style={{textAlign:'center'}}>{el.category} </p>
</div>
))}
</div>
</div>
<Button
btnName="Create Token"
handleClick={async () =>
createNFT(
name,
price,
image,
description,
router,
category,
// royalties,
// fileSize,
website
// properties
)
}
/>
</div>
</div>
I know it's a lot here since I'm passing the value through multiple functions but I've tried to narrow it down as much as possible. Also the smart contract is working perfectly fine and holds the category variable. I'm sure I just need to call it properly now but it's an array.
Please help!
I tried to replace { name } and name.toLowerCase() with category just to test it but it said 'category.toLowerCase() is undefined'.
Then I tried category.toString().toLowerCase() but it gave the same result.
So i have some dummy data and want to filter through array and display objects that have that specific status.
Currently when i click on some checkbox it filters objects with clicked status correctly but then from what i understand - filtered data gets saved to invoiceList state so unchecking it has zero sense, because next filtering is based on that filtered objects previously.
I also want to combine checked checkboxes so it filters objects with both e.g. "paid" and "pending" statutes.
How to do all of these filerings properly?
const {useState, useEffect} = React;
const DATA = [
{
name: "invoice1",
status: "paid"
},
{
name: "invoice2",
status: "paid"
},
{
name: "invoice3",
status: "pending"
},
{
name: "invoice4",
status: "draft"
}
];
const App = () => {
const [invoiceList, setInvoiceList] = useState([]);
useEffect(() => {
setInvoiceList(DATA);
}, []);
const statuses = ["draft", "pending", "paid"];
const [checkedState, setCheckedState] = useState(
new Array(statuses.length).fill(false)
);
const handleCheckboxChange = (position, status) => {
const updatedCheckedState = checkedState.map((item, index) =>
index === position ? !item : item
);
setCheckedState(updatedCheckedState);
//THIS
////////////////////////////////////////////
const filteredList = invoiceList.filter(
(invoice) => invoice.status === status
);
setInvoiceList(filteredList);
};
return (
<div>
<div>
{statuses.map((status, index) => (
<label key={index}>
<input
type="checkbox"
checked={checkedState[index]}
onChange={(e) => handleCheckboxChange(index, status)}
/>
<span>{status}</span>
</label>
))}
</div>
<ul>
{invoiceList.map((invoice,index) => {
return <li key={index}>{invoice.name}</li>;
})}
</ul>
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById("root")
).render(<App/>);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Instead of saving the state using setInvoiceList, you may filter the list using useMemo hook.
const {useState, useEffect, useMemo} = React;
const DATA = [
{
name: "invoice1",
status: "paid"
},
{
name: "invoice2",
status: "paid"
},
{
name: "invoice3",
status: "pending"
},
{
name: "invoice4",
status: "draft"
}
];
const App = () => {
const [invoiceList, setInvoiceList] = useState([]);
useEffect(() => {
setInvoiceList(DATA);
}, []);
const statuses = ["draft", "pending", "paid"];
// keep track of the selected/active status
const [activeStatus, setActiveStatus] = useState();
const filteredInvoices = useMemo(() => {
// if no active status, return all invoices
if (!activeStatus) {
return invoiceList;
}
// otherwise, filter invoices by active status
return invoiceList.filter(invoice => invoice.status === activeStatus);
},[activeStatus, invoiceList]);
return (
<div>
<div>
{statuses.map((status, index) => (
<label key={index}>
<input
type="checkbox"
checked={statuses[index] === activeStatus}
onChange={(e) => setActiveStatus(status === activeStatus ? undefined : status)}
/>
<span>{status}</span>
</label>
))}
</div>
<ul>
{filteredInvoices.map((invoice,index) => {
return <li key={index}>{invoice.name}</li>;
})}
</ul>
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById("root")
).render(<App/>);
For multiple status support, you can use the following
const {useState, useEffect, useMemo} = React;
const DATA = [
{
name: "invoice1",
status: "paid"
},
{
name: "invoice2",
status: "paid"
},
{
name: "invoice3",
status: "pending"
},
{
name: "invoice4",
status: "draft"
}
];
const App = () => {
const [invoiceList, setInvoiceList] = useState([]);
useEffect(() => {
setInvoiceList(DATA);
}, []);
const statuses = ["draft", "pending", "paid"];
// keep track of the selected/active statuses
const [activeStatuses, setActiveStatuses] = useState([]);
// toggle the status
const toggleStatus = (status) => {
if (activeStatuses.includes(status)) {
setActiveStatuses(activeStatuses.filter((s) => s !== status));
} else {
setActiveStatuses([...activeStatuses, status]);
}
};
// filter the invoices based on the active statuses
const filteredInvoices = useMemo(() => {
// if there are no active statuses, return the original list
if (activeStatuses.length === 0) {
return invoiceList;
}
// otherwise, filter the list
return invoiceList.filter(invoice => activeStatuses.includes(invoice.status));
},[activeStatuses, invoiceList]);
return (
<div>
<div>
{statuses.map((status, index) => (
<label key={index}>
<input
type="checkbox"
checked={activeStatuses.includes(status)}
onChange={(e) => toggleStatus(status)}
/>
<span>{status}</span>
</label>
))}
</div>
<ul>
{filteredInvoices.map((invoice,index) => {
return <li key={index}>{invoice.name}</li>;
})}
</ul>
</div>
);
}
const root = ReactDOM.createRoot(
document.getElementById("root")
).render(<App/>);
I'm working on a React Notes Application and my App.js contains all the necessary functions props which are passed down to several components.
As a result I'm doing prop drilling a lot where I'm passing down around 10-20 props/functions in the components where it isn't needed.
I tried using useContext Hook but I guess it doesn't work with callback functions in the value parameter.
App.js
const App = () => {
const [ notes, setNotes ] = useState([]);
const [ category, setCategory ] = useState(['Notes']);
const [ searchText, setSearchText ] = useState('');
const [ alert, setAlert ] = useState({
show:false,
msg:'',
type:''
});
const [isEditing, setIsEditing] = useState(false);
const [editId, setEditId] = useState(null);
useEffect(()=>{
keepTheme();
})
// retreive saved notes
useEffect(()=>{
const savedNotes = JSON.parse(localStorage.getItem('react-notes-data'));
if (savedNotes) {
setNotes(savedNotes)
}
}, []);
// save notes to local storage
useEffect(() => {
localStorage.setItem('react-notes-data', JSON.stringify(notes))
setNotesCopy([...notes]);
}, [notes]);
// save button will add new note
const addNote = (text) => {
const date = new Date();
const newNote = {
id: nanoid(),
text: text,
date: date.toLocaleDateString(),
category: category,
}
const newNotes = [...notes, newNote]
setNotes(newNotes);
}
const deleteNote = (id) => {
showAlert(true, 'Note deleted', 'warning');
const newNotes = notes.filter(note => note.id !== id);
setNotes(newNotes);
}
// hardcoded values for categories
const allCategories = ['Notes', 'Misc', 'Todo', 'Lecture Notes', 'Recipe'];
// copy notes for filtering through
const [notesCopy, setNotesCopy] = useState([...notes]);
const handleSidebar = (category) => {
setNotesCopy(category==='Notes'?[...notes]:
notes.filter(note=>note.category===category));
}
// function to call alert
const showAlert = (show=false, msg='', type='') => {
setAlert({show, msg, type});
}
return (
<div>
<div className="container">
<Sidebar
allCategories={allCategories}
handleSidebar={handleSidebar}
notesCopy={notesCopy}
key={notes.id}
/>
<Header notes={notes} alert={alert} removeAlert={showAlert} />
<Search handleSearchNote={setSearchText} />
<NotesList
notesCopy={notesCopy.filter(note=>
note.text.toLowerCase().includes(searchText) ||
note.category.toString().toLowerCase().includes(searchText)
)}
handleAddNote={addNote}
deleteNote={deleteNote}
category={category}
setCategory={setCategory}
allCategories={allCategories}
showAlert={showAlert}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
isEditing={isEditing}
setIsEditing={setIsEditing}
/>
</div>
</div>
)
}
NotesList.js
const NotesList = (
{ notesCopy, handleAddNote, deleteNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, notes, setNotes, editId, setEditId }
) => {
const [ noteText, setNoteText ] = useState('');
const textareaRef = useRef();
// function to set edit notes
const editItem = (id) => {
const specificItem = notes.find(note=>note.id === id);
setNoteText(specificItem.text);
setIsEditing(true);
setEditId(id);
textareaRef.current.focus();
}
return (
<div key={allCategories} className="notes-list">
{notesCopy.map(note => {
return (
<Note
key={note.id}
{...note}
deleteNote={deleteNote}
category={note.category}
isEditing={isEditing}
editId={editId}
editItem={editItem}
/>)
})}
<AddNote
handleAddNote={handleAddNote}
category={category}
setCategory={setCategory}
showHideClassName={showHideClassName}
allCategories={allCategories}
showAlert={showAlert}
isEditing={isEditing}
setIsEditing={setIsEditing}
notes={notes}
setNotes={setNotes}
editId={editId}
setEditId={setEditId}
noteText={noteText}
setNoteText={setNoteText}
textareaRef={textareaRef}
/>
</div>
)
}
AddNote.js
const AddNote = ({ notes, setNotes, handleAddNote, category, setCategory, showHideClassName, allCategories, showAlert, isEditing, setIsEditing, editId, setEditId, noteText, setNoteText, textareaRef }) => {
const [ show, setShow ] = useState(false);
const [ modalText, setModalText ] = useState('');
const charCount = 200;
const handleChange = (event) => {
if (charCount - event.target.value.length >= 0) {
setNoteText(event.target.value);
}
}
const handleSaveClick = () => {
if (noteText.trim().length === 0) {
setModalText('Text cannot be blank!');
setShow(true);
}
if (category === '') {
setModalText('Please select a label');
setShow(true);
}
if (noteText.trim().length > 0 && category!=='') {
showAlert(true, 'Note added', 'success');
handleAddNote(noteText);
setNoteText('');
setShow(false);
}
if (noteText.trim().length > 0 && category!=='' && isEditing) {
setNotes(notes.map(note=>{
if (note.id === editId) {
return ({...note, text:noteText, category:category})
}
return note
}));
setEditId(null);
setIsEditing(false);
showAlert(true, 'Note Changed', 'success');
}
}
const handleCategory = ( event ) => {
let { value } = event.target;
setCategory(value);
}
showHideClassName = show ? "modal display-block" : "modal display-none";
return (
<div className="note new">
<textarea
cols="10"
rows="8"
className='placeholder-dark'
placeholder="Type to add a note.."
onChange={handleChange}
value={noteText}
autoFocus
ref={textareaRef}
>
</textarea>
<div className="note-footer">
<small
className='remaining'
style={{color:(charCount - noteText.length == 0) && '#c60000'}}>
{charCount - noteText.length} remaining</small>
<div className='select'>
<select
name={category}
className="select"
onChange={(e)=>handleCategory(e)}
required
title='Select a label for your note'
defaultValue="Notes"
>
<option value="Notes" disabled selected>Categories</option>
{allCategories.map(item => {
return <option key={item} value={item}>{item}</option>
})}
</select>
</div>
<button className='save' onClick={handleSaveClick} title='Save note'>
<h4>{isEditing ? 'Edit':'Save'}</h4>
</button>
</div>
{/* Modal */}
<main>
<div className={showHideClassName}>
<section className="modal-main">
<p className='modal-text'>{modalText}</p>
<button type="button" className='modal-close-btn'
onClick={()=>setShow(false)}><p>Close</p>
</button>
</section>
</div>
</main>
</div>
)
}
I want the functions passed from App.js to NotesList.js to be in AddNote.js without them being passed in NotesList.js basically minimizing the props destructuring in NotesList.js
Context API does work with function. What you need to do is pass your function to Provider inside value :
<MyContext.Provider value={{notes: notesData, handler: myFunction}} >
For example:
// notes-context.js
import React, { useContext, createContext } from 'react';
const Context = createContext({});
export const NotesProvider = ({children}) => {
const [notes, setNote] = useState([]);
const addNote = setNote(...); // your logic
const removeNote = setNote(...); // your logic
return (
<Context.Provider value={{notes, addNote, removeNote}}>
{children}
</Context.Provider>
)
}
export const useNotes = () => useContext(Context);
Add Provider to your App.js like so:
// App.js
import NoteProvider from './notes-context';
export default App = () => {
return (
<NoteProvider>
<div>... Your App</div>
</NoteProvider>
)
}
Then call UseNote in your NoteList.js to use the function:
// NoteList.js
import {useNotes} from './note-context.js';
export const NoteList = () => {
const {notes, addNotes, removeNotes} = useNotes();
// do your stuff. You can now use functions addNotes and removeNotes without passing them down the props
}
I can't seem to filter two separate things in react.js
I am able to filter by 'hotel ratings using state' but when I tried to add another, it wouldn't work. In the console it says should have a unique key prop.
How can I filter both data sets? currently i'm filtering one data set with
.filter((h) => filter.ratings.includes(h.hotel.starRating))
when I tried creating something similar like this
(filter((room) => extra.occupancy.includes(room.occupancy.maxAdults))
it breaks the code, why is that ?
This is my code:
import { useEffect, useState } from "react";
import './App.css';
export default function App() {
const [hotelRooms, setHotelRooms] = useState([]);
const [filter, setFilter] = useState({ ratings: ["1", "2", "3", "4", "5"] });
const [extra, setExtra] = useState ({occupancy: ["1", "2", "3", "4", "5"] });
const fetchHotels = async () => {
const res = await fetch(
"https://obmng.dbm.guestline.net/api/hotels?collection-id=OBMNG"
);
const hotels = await res.json();
const hotelRooms = [];
for (const hotel of hotels) {
const res = await fetch(
`https://obmng.dbm.guestline.net/api/roomRates/OBMNG/${hotel.id}`
);
const info = await res.json();
hotelRooms.push({ hotel, rooms: info.rooms });
}
setHotelRooms(hotelRooms);
};
useEffect(() => {
fetchHotels();
}, []);
const handleRatingFilter = (e) => {
if (e.target.checked) {
// adding value
const temp = [...filter.ratings];
temp.push(e.target.value);
setFilter({ ...filter, ratings: temp });
} else {
// removing value
setFilter({
...filter,
ratings: [...filter.ratings.filter((v) => v !== e.target.value)]
});
}
};
const handleOccupancyExtra = (e) => {
if (e.target.checked) {
const perm = [...extra.occupancy];
perm.push(e.target.value);
setExtra({...extra, occupancy: perm});
} else {
setExtra ({
...extra,
occupancy: [...extra.occupancy.filter((d) => d !== e.target.value)]
});
}
}
return (
<div className="App">
<div>
{["1", "2", "3", "4", "5"].map((star) => (
<div key={"input-" + star}>
<input
id={"rated" + star}
value={star}
name="ratings"
type="checkbox"
checked={filter.ratings.includes(star)}
onChange={handleRatingFilter}
/>
<label htmlFor={"rated" + star}>Rated {star} star</label>
</div>
))}
</div>
<div>
{["1", "2", "3", "4", "5"].map((adults) => (
<div key={"adults" + adults}>
<input
id={"maximum" + adults}
value={adults}
name="extra"
type="checkbox"
checked={extra.occupancy.includes(adults)}
onChange={handleOccupancyExtra}
/>
<label htmlFor={"maximum" + adults}>adults {adults}</label>
</div>
))}
</div>
{hotelRooms
.filter((h) => filter.ratings.includes(h.hotel.starRating))
.map((h) => (
<div>
<h2> Name: {h.hotel.name}</h2>
<p> Description: {h.hotel.description}</p>
<p> Rating: {h.hotel.starRating}</p>
<p> Postcode: {h.hotel.postcode}</p>
<p> City: {h.hotel.town}</p>
<p style={{ fontWeight: "bold" }}>Rooms:</p>
{
h.rooms.map((room) => (
<div>
<h5>Occupancy</h5>
<div> adults: {room.occupancy.maxAdults}</div>
<div> Children: {room.occupancy.maxChildren}</div>
<div> Maximum guests: {room.occupancy.maxOverall}</div>
<div> Room type: {room.name}</div>
</div>
))}
</div>
))}
</div>
);
}
You can add multiple conditions inside filter.
Example:
const arr = [1,2,3,4,5,6,7,8,9,10]
const evenNums = arr.filter(num => num%2===0)
const biggerThan5 = arr.filter(num => num > 5)
const evenAndBiggerThan5 = arr.filter(num => num > 5 && num%2 === 0)
console.log("evenNums:", evenNums)
console.log("biggerThan5:", biggerThan5)
console.log("evenAndBiggerThan5:", evenAndBiggerThan5)
In your case
{hotelRooms
.filter((h) => filter.ratings.includes(h.hotel.starRating) && /* other condition (e.g. extra.occupancy.includes(...) */)
Codesandbox
This is a codesandbox based on my understanding of what you want to do.
How to implement two search terms in React? The first search term is name from the api, the second search term is the element's grandChild's innerHTML (the grandchild is an array of elements). The user should be able to filter the search by the two search terms together or individually.
The students array looks like this
[
{city: "NY"
email: "lily#hotmail.com"
firstName: "Lily",
lastName: "Yee",},
...
]
For each student, I have the functionality to add tags to them. The tags that are added to each student are html buttons. I have the function to search students by their names. I also need to implement a functionality to search students by those html buttons.
import React, {useState, useEffect} from 'react'
import StudentsContainer from './StudentsContainer';
import '../stylesheets/App.scss'
function App() {
const [students, setStudents] = useState([]);
const fetchStudents = () => {
fetch('./url')
.then(res => res.json())
.then(data => {
setStudents(data.students)
})
.catch(err => console.log(err))
}
useEffect(() => {
fetchStudents()
}, [])
return (
<div className='App'>
<StudentsContainer students={students} />
</div>
);
}
export default App;
import React, { useState } from "react";
import Student from "./Student";
import Input from "./Input";
import "../stylesheets/StudentsContainer.scss";
const StudentsContainer = ({ students }) => {
const [searchByName, setSearchByName] = useState("");
const [searchByTag, setSearchByTag] = useState("");
const [tags, setTags] = useState([]);
const addTagClick = (newTag) => {
setTags((prevTags) => [...prevTags, newTag]);
};
const renderStudentsByNameSearch = () => {
return students
.filter((student) => {
if (searchByName.length < 2) {
return student;
} else {
const fullName = student.firstName.concat(student.lastName);
return fullName
.toLowerCase()
.includes(searchByName.toLocaleLowerCase());
}
})
.map((student) => {
return (
<Student
tags={tags}
addTagClick={addTagClick}
key={student.id}
student={student}
/>
);
});
};
return (
<section className="students-container">
<Input
value={searchByName}
placeholder="Search by name"
onChange={({ target }) => setSearchByName(target.value)}
/>
<Input
placeholder="Search by tag"
onChange={({ target }) => setSearchByTag(target.value)}
/>
{renderStudentsByNameSearch()}
</section>
);
};
export default StudentsContainer;
import React, { useState } from "react";
import { generateId } from "../helper";
import Input from "./Input";
const AddTag = ({addTagClick, studentId}) => {
const [tag, setTag] = useState("");
const handleInputChange = ({ target }) => {
setTag(target.value);
};
const onSubmitClick = (e) => {
e.preventDefault();
const newTag = {
tag: tag,
id: generateId(),
studentId: studentId,
};
if(tag) {
addTagClick(newTag)
}
setTag("");
};
return (
<>
<form onSubmit={onSubmitClick}>
<Input
className="add-tag-input"
placeholder="Add a tag"
type="text"
value={tag}
onChange={handleInputChange}
/>
</form>
</>
);
};
export default AddTag;
import React, { useState } from "react";
import "../stylesheets/Student.scss";
import AddTag from "./AddTag";
import Tag from "./Tag";
const Student = ({ student, addTagClick, tags }) => {
tags = tags.filter(tag => tag.studentId === student.id)
const averageGrade =
student.grades.reduce((acc, grade) => {
return parseInt(acc) + parseInt(grade);
}) / student.grades.length;
const [isViewScores, setIsViewScores] = useState(false);
const viewScoreClick = () => {
setIsViewScores((prev) => !prev);
};
return (
<article className="student">
<figure>
<img src={student.pic} alt="student" />
</figure>
<aside>
<h2>
{student.firstName} {student.lastName}
</h2>
<ul>
<li>Email: {student.email}</li>
<li>Company: {student.company}</li>
<li>Skill: {student.skill}</li>
<li>
Average: {averageGrade}%
{isViewScores && (
<ul className="scores">
{student.grades.map((grade, index) => {
return (
<li key={index}>
Test {index + 1}: {grade}%
</li>
);
})}
</ul>
)}
</li>
</ul>
<div className="tags-container">
{tags.map((tag) => (
<Tag tag={tag} key={tag.id} />
))}
</div>
<AddTag studentId={student.id} addTagClick={addTagClick}/>
</aside>
<button onClick={viewScoreClick} className="view-scores-btn">
{isViewScores ? "-" : "+"}
</button>
</article>
);
};
export default Student;
I'm not entirely sure what are you asking. If you are simply asking how to filter an array of students with tag(s) and name, and you already know the UI part. This is how you may do it.
Since you never mention what your students array look like, I would assume the following:
[
{
"firstName": "",
"lastName": "",
"tags": [
"tag1",
"tag2"
]
}
]
const [searchByName, setSearchByName] = useState("");
const [searchByTags, setSearchByTags] = useState([]); // I don't think it is a good idea to let people to search by one and only one tag, I changed it multiple tags
const [students, setStudents] = useState([]); // all avalible students
// this is the list of student you should display in the search result
const filteredStudents = useMemo(() => students.filter(({ firstName, lastName, tags}) =>
searchByName === "" || (firstName + " " + lastName).includes(searchByName) // include this student if the search string is empty or the full name includes the search string
&& tags.length === 0 || searchByTags.every(tag => tags.includes(tag)) // and if the search tags are empty or student matches all the search tags
), [students, searchByName, searchByTags]);