16
loading...
This website collects cookies to deliver better user experience
npx create-react-app tag-text-search
cd tag-text-search
yarn start
src/sideBar/sideBar.js
import SearchBar from './searchBar';
import TagSection from './tagSection';
import styles from '../../styles/sideBar.module.css';
export default function SideBar() {
return (
<div className={styles.stickyContainer}>
<div className={styles.container}>
<SearchBar />
<TagSection />
</div>
</div>
);
}
sideBar.module.css
in a moment. But let's first finish off the SerachBar
and the TagSection
src/styles/sideBar.module.css
.stickyContainer {
position: -webkit-sticky;
position: sticky;
top: 0px;
height: 0;
z-index: 20;
width: 30%;
max-width: 30%;
}
.container {
position: absolute;
z-index: 20;
display: inline-block;
height: 100vh;
left: 20px;
padding: 1rem;
top: 0;
background: var(--baseLight);
border-radius: 2px;
z-index: 10;
transition: 0.8s;
}
.sb__container {
background: var(--base);
border-radius: 2px;
transition: 0.4s;
margin: 2rem 0;
display: flex;
justify-content: center;
align-items: center;
position: relative;
border: 2px solid transparent;
width: 100%;
}
.sb__container:focus-within {
background: var(--baseLight);
border: 2px solid var(--primary);
}
.sb__container svg {
margin-left: 0.5rem;
padding-top: 0.2rem;
}
.sb__input {
font-family: Antonio;
background: var(--base);
outline: none;
color: var(--primary);
font-size: 1rem;
letter-spacing: 0.5px;
width: 100%;
padding: 5px;
border: none;
transition: 0.4s;
}
.sb__input:focus-within {
background: var(--baseLight);
}
.sb__input::placeholder {
color: var(--primary);
}
.ts__flexContainer {
position: relative;
display: flex;
justify-content: center;
width: 100%;
flex-direction: column;
}
.ts__heading {
text-transform: capitalize;
font-size: 1.7rem;
}
.ts_selectedTagContainer {
margin-bottom: 1.5rem;
margin-top: 0.3rem;
}
.ts__unselectedTagContainer {
overflow-y: auto;
height: 100%;
margin-top: 0.3rem;
display: flex;
flex-direction: column;
align-items: flex-start;
}
.ts__selectedTag {
cursor: pointer;
margin: 0.2rem 0;
margin-right: 0.4rem;
position: relative;
color: black;
border: 2px solid var(--primary);
padding: 5px 15px;
overflow: hidden;
transition: 1s all ease;
background: transparent;
font-family: Antonio;
font-weight: bold;
letter-spacing: 0.04em;
border-radius: 2px;
outline: none;
background: var(--primary);
font-size: 1rem;
}
.ts__unselectedTag {
cursor: pointer;
margin: 0.5rem 0;
position: relative;
color: var(--primary);
border: 2px solid var(--primary);
padding: 5px 15px;
overflow: hidden;
transition: 1s all ease;
background: transparent;
font-family: Antonio;
font-weight: bold;
letter-spacing: 0.04em;
border-radius: 2px;
outline: none;
background: var(--base);
font-size: 1rem;
}
SideBar
, the SearchBar
, and the TagSection
.src/index.css
:root {
--base: #e3e3e3;
--baseLight: #c8c7c7;
--primary: #f1762d;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
SearchBar
. It is pretty straightforward as opposed to the TagSection
src/components/sideBar/serachBar.js
import { useContext } from 'react';
import { SearchContext } from '../../store/search-context';
import styles from '../../styles/sideBar.module.css';
const SearchBar = () => {
const { query, searchHandler } = useContext(SearchContext);
return (
<div className={styles.sb__container}>
<svg
width='16'
height='16'
viewBox='0 0 16 16'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
d='M10.9167 9.66667H10.2583L10.025 9.44166C10.8417 8.49166 11.3333 7.25833 11.3333 5.91667C11.3333 2.925 8.90833 0.5 5.91667 0.5C2.925 0.5 0.5 2.925 0.5 5.91667C0.5 8.90833 2.925 11.3333 5.91667 11.3333C7.25833 11.3333 8.49166 10.8417 9.44166 10.025L9.66667 10.2583V10.9167L13.8333 15.075L15.075 13.8333L10.9167 9.66667ZM5.91667 9.66667C3.84167 9.66667 2.16667 7.99167 2.16667 5.91667C2.16667 3.84167 3.84167 2.16667 5.91667 2.16667C7.99167 2.16667 9.66667 3.84167 9.66667 5.91667C9.66667 7.99167 7.99167 9.66667 5.91667 9.66667Z'
fill='#F1762D'
/>
</svg>
<input
className={styles.sb__input}
placeholder='Search'
type='text'
onChange={e => searchHandler(e.target.value)}
value={query}
/>
<span>
<i></i>
</span>
</div>
);
};
export default SearchBar;
SearchContext
. So let's get on with it.state
and the setState
function in the Context and now you can essentially 'get the state' or 'update the state' from any of the components wrapped in the Context (whole application mostly :))src/store/search-context.js
import { useState, createContext } from 'react';
export const SearchContext = createContext({
query: '',
searchHandler: () => {},
});
const SearchContextProvider = props => {
const [query, setQuery] = useState('');
const searchHandler = query => {
setQuery(query.toLowerCase());
};
return (
<SearchContext.Provider
value={{ query: query, searchHandler: searchHandler }}
>
{props.children}
</SearchContext.Provider>
);
};
export default SearchContextProvider;
searchBar.js
and serach-context.js
simultaneously.onChange
function on the input in the serachBar.js
is the state defined in the context, i.e. query
, and the function that is updating the state is also coming from the context, i.e. searchHandler
.index.js
in src/store
and export a component named Context
.src/store/index.js
import SearchContextProvider from './search-context';
export default function Context({ children }) {
return (
<SearchContextProvider>
{children}
</SearchContextProvider>
);
}
filter-context.js
for tag filtering functionality)src/App.js
import SideBar from './components/sideBar/sideBar';
import Context from './store';
function App() {
return (
<Context>
<SideBar />
</Context>
);
}
export default App;
selectTagHandler
and unselectTagHandler
. The selectTagHandler
puts the selected tag into the 'Selected Tags' list and removes it from the 'Unselected Tags' list, and the unselectTagsHandler
does exactly the opposite.fillter-contenxt
(that we'll be creating shortly) with the 'Selected Tags' list for us to access later and use to filter articles. But more on that later. Okay, enough formulation! Let's jump right into creating the Tag SectionTAGS
array.src/data/data.js
export const TAGS = [
'react',
'books-notes',
'web-dev',
'design',
'javascript',
'reactnative',
'mobile-dev',
];
src/components/sideBar/tagSection.js
import { useState } from 'react';
import { TAGS } from '../../data/data';
import styles from '../../styles/sideBar.module.css';
export default function TagSection() {
// 1.
const [unselectedTags, setUnselectedTags] = useState(TAGS);
const [selectedTags, setSelectedTags] = useState([]);
// 2.
const selectTagHandler = tag => {
const updatedSelectedTags = selectedTags.concat(tag);
setSelectedTags(updatedSelectedTags);
setUnselectedTags(
unselectedTags.filter(unselectedTag => unselectedTag !== tag)
);
};
// 3.
const unselectTagHandler = tag => {
setUnselectedTags(unselectedTags.concat(tag));
const updatedSelectedTags = selectedTags.filter(
selectedTag => selectedTag !== tag
);
setSelectedTags(updatedSelectedTags);
};
const selectedTagsList = selectedTags.map(tag => (
<button
className={styles.ts__selectedTag}
key={tag}
onClick={() => unselectTagHandler(tag)}
>
#{tag}
</button>
));
const unselectedTagsList = unselectedTags.map(tag => (
<button
key={tag}
className={styles.ts__unselectedTag}
onClick={() => selectTagHandler(tag)}
>
#{tag}
</button>
));
return (
<div className={styles.ts__flexContainer}>
<div className={styles.ts__flexContainer}>
<h2 className={styles.ts__heading}>You Selected</h2>
<div className={styles.ts_selectedTagContainer}>{selectedTagsList}</div>
</div>
<div className={styles.ts__flexContainer}>
<h2 className={styles.ts__heading}>To Select From</h2>
<div className={styles.ts__unselectedTagContainer}>
{unselectedTagsList}
</div>
</div>
</div>
);
}
unselectedTags
and selectedTags
. You'll notice that the unselectedTags
is prepopulated with the TAGS
array just like we planned, and the selectedTags
is empty.selectTagHandler
takes an argument tag
and uses that to populate selectedTags
array using the .concat method that comes with the JS Arrays. Now we also need to remove the selected tag from the unselectedTags
array and for that, we use the .filter method, again, that comes with the JS Arrays.unselectTagHandler
does exactly the opposite of the function we just discussed, i.e. selectTagHandler
. Takes an argument, named tag
- updates the unselectedTags
array with it - remove it out from the selectedTags
array.TagSection
component. But, in order to filter out articles based on the selected tags we need access to the selectedTags
array. To achieve this we create the filter-context in which we basically keep a copy of selectedTags
array. So when we update the selectedTags
array in the TagSection
we also update the state in our filter context.src/store/filter-context.js
import { useState, createContext } from 'react';
export const FilterContext = createContext({
tags: [],
tagSelector: () => {},
});
const FilterContextProvider = props => {
const [tags, setTags] = useState([]);
const tagSelector = tagsList => {
setTags(tagsList);
};
return (
<FilterContext.Provider value={{ tags: tags, tagSelector: tagSelector }}>
{props.children}
</FilterContext.Provider>
);
};
export default FilterContextProvider;
tagSelector
function, is used to update the state tags
. This is the state that we'll be using later in another component to filter out articles.src/store/index.js
import SearchContextProvider from './search-context';
import FilterContextProvider from './filter-context';
export default function Context({ children }) {
return (
<SearchContextProvider>
<FilterContextProvider>{children}</FilterContextProvider>
</SearchContextProvider>
);
}
TagSection
component.src/components/sideBar/tagSection.js
and I'll explain the added lines just below it.import { useState, useContext } from 'react';
import { TAGS } from '../../data/data';
import { FilterContext } from '../../store/filter-context';
import styles from '../../styles/sideBar.module.css';
export default function TagSection() {
// 1.
const filterContext = useContext(FilterContext);
const [unselectedTags, setUnselectedTags] = useState(TAGS);
const [selectedTags, setSelectedTags] = useState([]);
const selectTagHandler = tag => {
const updatedSelectedTags = selectedTags.concat(tag);
setSelectedTags(updatedSelectedTags);
setUnselectedTags(
unselectedTags.filter(unselectedTag => unselectedTag !== tag)
);
// 2.
filterContext.tagSelector(updatedSelectedTags);
};
const unselectTagHandler = tag => {
setUnselectedTags(unselectedTags.concat(tag));
const updatedSelectedTags = selectedTags.filter(
selectedTag => selectedTag !== tag
);
setSelectedTags(updatedSelectedTags);
// 3.
filterContext.tagSelector(updatedSelectedTags);
};
const selectedTagsList = selectedTags.map(tag => (
<button
className={styles.ts__selectedTag}
key={tag}
onClick={() => unselectTagHandler(tag)}
>
#{tag}
</button>
));
const unselectedTagsList = unselectedTags.map(tag => (
<button
key={tag}
className={styles.ts__unselectedTag}
onClick={() => selectTagHandler(tag)}
>
#{tag}
</button>
));
return (
<div className={styles.ts__flexContainer}>
<div className={styles.ts__flexContainer}>
<h2 className={styles.ts__heading}>You Selected</h2>
<div className={styles.ts_selectedTagContainer}>{selectedTagsList}</div>
</div>
<div className={styles.ts__flexContainer}>
<h2 className={styles.ts__heading}>To Select From</h2>
<div className={styles.ts__unselectedTagContainer}>
{unselectedTagsList}
</div>
</div>
</div>
);
}
FilterContext
in a constant named filterContext
.filterContext.tagSelector
function that we passed to setSeletectTags
array. Hence this makes a copy of the array selectedTags
in the filter-context
, namely tags
array.src/styles/mainContent.module.css
.container {
width: 70%;
margin-left: auto;
}
.thumbnailContainer {
padding: 2rem 2rem;
background-color: gray;
border-radius: 10px;
margin: 2rem;
display: inline-block;
width: 200px;
}
ARTICLES
array. Paste the code below in src/data/data.js
export const ARTICLES = [
{
id: '1',
title: 'Using React Context API Like a Pro',
body: 'If you\'ve been hearing the term "Context API" and feel totally confused about it (like me, some days ago) or you have no clue what this even means, look no further! I\'ve got you covered (for the most part, I believe)',
tags: ['react', 'web-dev', 'javascript'],
},
{
id: '2',
title: 'Never Split the difference - Book Notes',
body: 'If you\'ve been hearing the term "Context API" and feel totally confused about it (like me, some days ago) or you have no clue what this even means, look no further! I\'ve got you covered (for the most part, I believe)',
tags: ['books-notes'],
},
{
id: '3',
title: 'Design in React',
body: 'If you\'ve been hearing the term "Context API" and feel totally confused about it (like me, some days ago) or you have no clue what this even means, look no further! I\'ve got you covered (for the most part, I believe)',
tags: ['design', 'react', 'javascript', 'web-dev'],
},
{
id: '4',
title: 'Design in React Native',
body: 'If you\'ve been hearing the term "Context API" and feel totally confused about it (like me, some days ago) or you have no clue what this even means, look no further! I\'ve got you covered (for the most part, I believe)',
tags: ['design', 'react', 'reactnative', 'javascript', 'mobile-dev'],
},
];
export const TAGS = [
'react',
'books-notes',
'web-dev',
'design',
'javascript',
'reactnative',
'mobile-dev',
];
src/components/mainContent.js
import { useContext } from 'react';
import { SearchContext } from '../store/search-context';
import { FilterContext } from '../store/filter-context';
import styles from '../styles/mainContent.module.css';
export default function MainContent({ articles }) {
const searchContext = useContext(SearchContext);
const { tags } = useContext(FilterContext);
let filteredArticles = null;
if (articles) {
// 1. Tag Search
filteredArticles = articles.filter(article => {
if (tags.length === 0) {
return article;
}
if (tags.some(val => article.tags.includes(val))) {
return article;
} else {
return null;
}
});
// 2. Text Search
filteredArticles = filteredArticles.filter(article => {
if (article.title.toLowerCase().includes(searchContext.query)) {
return article;
} else {
return null;
}
});
}
return (
<div className={styles.container}>
{filteredArticles !== null &&
filteredArticles.map(article => {
return (
<div className={styles.thumbnailContainer} key={article.id}>
<h2>{article.title}</h2>
</div>
);
})}
</div>
);
}
query
state and tags
state.tags.length === 0
, in other words, if no tag is selected → return all the articles. if tags.some(val => article.tags.includes(val))
, i.e. if any of the tags selected are there in the tags array associated with the article object → return that article otherwise → return nothing.(article.title.toLowerCase().includes(searchContext.query))
, in other words, if a part of the title of the article matches with the search query (no matter the casing) → return that article otherwise → return nothing.App.js
, pass the articles and finish the remaining coffee (at least for me :)).App.js
import SideBar from './components/sideBar/sideBar';
import Context from './store';
import MainContent from './components/mainContent';
import { ARTICLES } from './data/data';
function App() {
return (
<Context>
<SideBar />
<MainContent articles={ARTICLES} />
</Context>
);
}
export default App;