Home > OS >  Job Listing Filtering React
Job Listing Filtering React

Time:09-06

I'm having trouble figuring out how to filter this job listing that I found in frontend mentor.

The filtering partially works. For example, if I add HTML and Senior tag, it filters it correctly. But the logic isn't right cause I test it for other tags and the result doesn't come out as expected.

I appreciate any help to get pointed at the right direction.

this is my App.js code

import './App.css';
import axios from 'axios';
import React, {useState,useEffect} from "react";

import Card from "./components/card/Card";
import Header from "./components/header/Header";
import FilterBar from "./components/FilterBar/FilterBar";

function App() {
    const [data, setData] = useState([]);
    const URL = 'http://localhost:3000/data.json';

    const [category, setCategory] = useState([]);
    console.log("category is "   category);

    useEffect(() => {
        axios.get(URL).then((response) => {
            setData(response.data);
            //console.log(data);
        });
    }, []);

    /*useEffect(() => {

    }, []);*/

    function addItem(item){
        console.log("addItem "   item);

        const duplicateItem = category.find((collection) => collection === item);

        if(!duplicateItem){
            setCategory([...category, item]);
        } else {
            alert(item   " is already in the list.");
        }
    }

    function removeAllItems(items, counter){
        if(counter > 0){
            setCategory(items);

        } else {
            alert("There are no items in the filter bar to remove.");
        }
    }

  return (
    <div className="App">
      <Header />
        {
            category.length > 0 ?
                <FilterBar removeAllItems={removeAllItems} languages={category} />
                :
                ""
        }
        {
            Object.keys(data).map((item, index) => (
                category.length < 1 ?
               <Card
                    key={index}
                    data_company_name={data[item].company}
                    data_logo={data[item].logo}
                    data_new={data[item].new}
                    data_featured={data[item].featured}
                    data_position={data[item].position}
                    data_languages={data[item].languages}
                    data_level={data[item].level}
                    data_role={data[item].role}
                    data_posted_at={data[item].postedAt}
                    data_contract={data[item].contract}
                    data_location={data[item].location}
                    data_tools={data[item].tools}
                    addItem={addItem}
                />
                : category.includes(data[item].level)
                || category.includes(data[item].role)
                || category.every((language) => data[item].languages.includes(language))
                || category.every((tool) => data[item].tools.includes(tool))

                 ?
                <Card
                    key={index}
                    data_company_name={data[item].company}
                    data_logo={data[item].logo}
                    data_new={data[item].new}
                    data_featured={data[item].featured}
                    data_position={data[item].position}
                    data_languages={data[item].languages}
                    data_level={data[item].level}
                    data_role={data[item].role}
                    data_posted_at={data[item].postedAt}
                    data_contract={data[item].contract}
                    data_location={data[item].location}
                    data_tools={data[item].tools}
                    addItem={addItem}
                />
                    : ""

            ))}
    </div>
  );
}

export default App;

This is the Card.js

import {StyledCard} from './Card.styled';

const Card = ({addItem, data_logo, data_company_name, data_new, data_featured, data_position, data_languages, data_level, data_role, data_contract, data_posted_at, data_location, data_tools}) => {

    const data = data_languages.map( (language, index) => {

        return <span onClick={() => addItem(language) } key={index} className={`role-tag-${index}`}>{language}</span>;

    });

    const tools_data = data_tools.map( (tool, index) => {
        return <span key={index} onClick={() => addItem(tool) } className={`data-tools`}>{tool}</span>
    });

    return (
        <>
            <div className={`card-container`}>
                <StyledCard>
                    <div className={`inside-card-wrapper`}>
                        <div className={`img`}>
                            <img src={data_logo} alt=""/>
                            <div className={`company-tag-wrapper`}>
                                <span className={`company-name`}>{data_company_name}</span>
                                {
                                    data_new === true ?
                                        <span className={`job-tag-new`}>NEW!</span>
                                        :
                                        ""
                                }
                                {
                                    data_featured === true ?
                                        <span className={`job-tag-featured`}>FEATURED</span>
                                        :
                                        ""
                                }

                                <div className={`role-title`}>
                                    <div><strong>{data_position}</strong></div>
                                    <div className={`role-tags`}>
                                        <span onClick={() => addItem(data_role)} className={`data-role`}>{data_role}</span>
                                        <span onClick={() => addItem(data_level) } className={`data-level`}>{data_level}</span>
                                        {data}
                                        {tools_data}
                                    </div>
                                </div>
                                <div className={`job-type-wrapper`}>
                                    <div className={`post`}>
                                        <span>{data_posted_at}</span>
                                        &bull;
                                        <span>{data_contract}</span>
                                        &bull;
                                        <span>{data_location}</span>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </div>
                </StyledCard>
            </div>
        </>
    );
};

export default Card;

This is the FilterBar.js file

import {StyledFilterBar} from "./FilterBar.styled";

const FilterBar = ({removeAllItems, languages}) => {

    //console.log("FilterBar.js "   items);

    let counter = 0;

    const data = languages.map( (item, index) => {
        counter = languages.length;
        return <div key={index}>{item}&nbsp;</div>
    });

    return (
        <div>
            <StyledFilterBar>
                <div className={`filter-options`}>
                    <div className={`left-side`}>
                        {data}
                    </div>
                    <div className={`right-side`}>
                        <div onClick={() => removeAllItems([], counter) } className={`clear`}>clear</div>
                    </div>
                </div>
            </StyledFilterBar>
        </div>
    );
};

export default FilterBar;

This is the json file that contains the data

[
  {
    "id": 1,
    "company": "Photosnap",
    "logo": "./img/photosnap.svg",
    "new": true,
    "featured": true,
    "position": "Senior Frontend Developer",
    "role": "Frontend",
    "level": "Senior",
    "postedAt": "1d ago",
    "contract": "Full Time",
    "location": "USA Only",
    "languages": ["HTML", "CSS", "JavaScript"],
    "tools": []
  },
  {
    "id": 2,
    "company": "Manage",
    "logo": "./img/manage.svg",
    "new": true,
    "featured": true,
    "position": "Fullstack Developer",
    "role": "Fullstack",
    "level": "Midweight",
    "postedAt": "1d ago",
    "contract": "Part Time",
    "location": "Remote",
    "languages": ["Python"],
    "tools": ["React"]
  },
  {
    "id": 3,
    "company": "Account",
    "logo": "./img/account.svg",
    "new": true,
    "featured": false,
    "position": "Junior Frontend Developer",
    "role": "Frontend",
    "level": "Junior",
    "postedAt": "2d ago",
    "contract": "Part Time",
    "location": "USA Only",
    "languages": ["JavaScript"],
    "tools": ["React", "Sass"]
  },
  {
    "id": 4,
    "company": "MyHome",
    "logo": "./img/myhome.svg",
    "new": false,
    "featured": false,
    "position": "Junior Frontend Developer",
    "role": "Frontend",
    "level": "Junior",
    "postedAt": "5d ago",
    "contract": "Contract",
    "location": "USA Only",
    "languages": ["CSS", "JavaScript"],
    "tools": []
  },
  {
    "id": 5,
    "company": "Loop Studios",
    "logo": "./img/loop-studios.svg",
    "new": false,
    "featured": false,
    "position": "Software Engineer",
    "role": "Fullstack",
    "level": "Midweight",
    "postedAt": "1w ago",
    "contract": "Full Time",
    "location": "Worldwide",
    "languages": ["JavaScript"],
    "tools": ["Ruby", "Sass"]
  },
  {
    "id": 6,
    "company": "FaceIt",
    "logo": "./img/faceit.svg",
    "new": false,
    "featured": false,
    "position": "Junior Backend Developer",
    "role": "Backend",
    "level": "Junior",
    "postedAt": "2w ago",
    "contract": "Full Time",
    "location": "UK Only",
    "languages": ["Ruby"],
    "tools": ["RoR"]
  },
  {
    "id": 7,
    "company": "Shortly",
    "logo": "./img/shortly.svg",
    "new": false,
    "featured": false,
    "position": "Junior Developer",
    "role": "Frontend",
    "level": "Junior",
    "postedAt": "2w ago",
    "contract": "Full Time",
    "location": "Worldwide",
    "languages": ["HTML", "JavaScript"],
    "tools": ["Sass"]
  },
  {
    "id": 8,
    "company": "Insure",
    "logo": "./img/insure.svg",
    "new": false,
    "featured": false,
    "position": "Junior Frontend Developer",
    "role": "Frontend",
    "level": "Junior",
    "postedAt": "2w ago",
    "contract": "Full Time",
    "location": "USA Only",
    "languages": ["JavaScript"],
    "tools": ["Vue", "Sass"]
  },
  {
    "id": 9,
    "company": "Eyecam Co.",
    "logo": "./img/eyecam-co.svg",
    "new": false,
    "featured": false,
    "position": "Full Stack Engineer",
    "role": "Fullstack",
    "level": "Midweight",
    "postedAt": "3w ago",
    "contract": "Full Time",
    "location": "Worldwide",
    "languages": ["JavaScript", "Python"],
    "tools": ["Django"]
  },
  {
    "id": 10,
    "company": "The Air Filter Company",
    "logo": "./img/the-air-filter-company.svg",
    "new": false,
    "featured": false,
    "position": "Front-end Dev",
    "role": "Frontend",
    "level": "Junior",
    "postedAt": "1mo ago",
    "contract": "Part Time",
    "location": "Worldwide",
    "languages": ["JavaScript"],
    "tools": ["React", "Sass"]
  }
]

CodePudding user response:

The problem is that you're combining all your "categories" into a single filter, but each category (language, tool, etc) has a different matching criteria. I think that the matching criteria you want is:

  • Any match for the job level or the job role
  • All match for tools and languages

But you can't do this without knowing the type of filter that you've got for each category.

So, there are a few ways you can solve that. One way would be to to separate out your filters by category. You could write a custom hook to do the heavy lifting:

const createEmptyFilter = () => ({
  all: new Set(),
  language: new Set(),
  tool: new Set(),
  role: new Set(),
  level: new Set()
});

const useFilter = () => {
  const [filter, setFilter] = useState(createEmptyFilter);

  const handleAddFilter = (type, value) => {
    if (!filter[type]) {
      alert(`Unknown filter category: '${type}'`);
      return;
    }

    setFilter((prev) => {
      const next = { ...prev };
      next.all = new Set(prev.all).add(value);
      next[type] = new Set(prev[type]).add(value);

      return next;
    });
  };

  const handleClearFilter = () => setFilter(createEmptyFilter());

  const matchesFilter = (item) => {
    const { language, tool, role, level } = filter;

    if (level.size && !level.has(item.level)) return false;
    if (role.size && !role.has(item.role)) return false;

    for (const l of language) {
      if (!item.languages.includes(l)) return false;
    }

    for (const t of tool) {
      if (!item.tools.includes(t)) return false;
    }

    return true;
  };

  return [
    [...filter.all.values()],
    handleAddFilter,
    handleClearFilter,
    matchesFilter
  ];
};

A couple of things to note:

  • The 'all' category is just to maintain the order of items that the user adds entries to the filter. You could compose this by combining the values from all the categories, but you'd lose the ordering.
  • Now that we know which category each filter belongs to, our filtering criteria can be more specific.

With the card, you'd have to add the category when you apply a filter, for example:

<span
  onClick={() => onAddFilter("role", data_role)}
  className={`data-role`}
>

Here's an updated sample.

  • Related