DEV Community

Cover image for Hire+Plus! For Employees Here's how I built it (UI - Job)
AjeaS
AjeaS

Posted on

2 1

Hire+Plus! For Employees Here's how I built it (UI - Job)

Overview: All views and functionality related to the Job, all funcs called are coming from the jobReducer.


Job Route Page

inside routes > job > job-page.tsx
imports that assist with components to render, and functions to call. On mount, I fetch all jobs. I'm handling search and filter states using searchInput, and filteredData.

searchItems - filters jobs by search term and sets results to filteredData. If nothing was searched, filteredData is set to default jobs.

import { useEffect, useState } from 'react';
import BeatLoader from 'react-spinners/BeatLoader';
import { getPostedJobs } from '../../app/features/job/jobSlice';
import { JobData } from '../../app/features/job/jobTypes';
import { useAppDispatch, useAppSelector } from '../../app/hooks';
import Job from '../../components/job/job.component';

const JobsPage = () => {
    const dispatch = useAppDispatch();
    const { jobs, isLoading } = useAppSelector((state) => state.job);

    const [searchInput, setSearchInput] = useState<string>('');
    const [filteredData, setfilteredData] = useState<JobData[]>([]);

    useEffect(() => {
        dispatch(getPostedJobs());
    }, []);

    const searchItems = (value: string) => {
        setSearchInput(value);
        if (searchInput !== '') {
            const filtered = jobs.filter((item) => {
                return Object.values(item)
                    .join('')
                    .toLowerCase()
                    .includes(searchInput.toLowerCase());
            });
            setfilteredData(filtered);
        } else {
            setfilteredData(jobs);
        }
    };
    return ({/* removed for simplicity */});
};

export default JobsPage;

Enter fullscreen mode Exit fullscreen mode

UI

In a nutshell, it renders all jobs with a search bar. Searching by job title, and or location will show filtered results in Job component.

const JobsPage = () => {
{/* removed for simplicity */}
    return (
        <>
            {isLoading ? (
                <div className="text-center p-20">
                    <BeatLoader color="#ffffff" />
                </div>
            ) : (
                <>
                    <div className="flex justify-center pt-20">
                        <div className="mb-3 w-1/2">
                            <div className="input-group relative flex items-stretch w-full mb-4">
                                <input
                                    value={searchInput}
                                    onChange={(e) => searchItems(e.target.value)}
                                    type="search"
                                    className="form-control relative flex-auto min-w-0 block w-full px-5 py-3 text-base font-normal font-color secondary-bg-color bg-clip-padding border border-solid border-gray-300 rounded-full transition ease-in-out m-0 focus:text-slate-200 focus:secondary-bg-color focus:border-indigo-700 focus:outline-none"
                                    placeholder="Search for a job..."
                                    aria-label="Search"
                                    aria-describedby="button-addon2"
                                />
                            </div>
                        </div>
                    </div>
                    <section className="text-gray-600 body-font overflow-hidden">
                        <div className="container px-5 py-24 mx-auto">
                            <div className="-my-8 divide-y-2 divide-gray-700">
                                {searchInput.length
                                    ? filteredData.map((job, index) => {
                                            return <Job job={job} key={index} />;
                                      })
                                    : jobs.map((job, index) => {
                                            return <Job job={job} key={index} />;
                                      })}
                            </div>
                        </div>
                    </section>
                </>
            )}
        </>
    );
};

export default JobsPage;

Enter fullscreen mode Exit fullscreen mode

Screenshots

  1. when it's not filtered
  2. when it's filtered

Jobs unfiltered results

Job filtered results

inside routes > job > job-detail.tsx
imports that assist with functions to call. On mount, I fetch a single job by id I get from useParams. I parse the result and set it to local state jobData.

import { useEffect, useState } from 'react';
import { useParams } from 'react-router';
import BeatLoader from 'react-spinners/BeatLoader';
import { getPostedJobById } from '../../app/features/job/jobSlice';
import { JobData } from '../../app/features/job/jobTypes';
import { useAppDispatch, useAppSelector } from '../../app/hooks';

const JobDetail = () => {
    const { id } = useParams();
    const [jobData, setjobData] = useState<JobData>({} as JobData);
    const { isLoading } = useAppSelector((state) => state.job);
    const dispatch = useAppDispatch();

    useEffect(() => {
        dispatch(getPostedJobById(id))
            .unwrap()
            .then((val) => {
                setjobData(JSON.parse(val));
            });
    }, [dispatch, id]);

    return ( {/* removed for simplicity */} );
};

export default JobDetail;

Enter fullscreen mode Exit fullscreen mode

UI

In a nutshell, it renders the details of a job.

const JobDetail = () => {
    {/* removed for simplicity */}
    return (
        <>
            {isLoading ? (
                <BeatLoader />
            ) : (
                <>
                    <section style={{ backgroundColor: '#252731' }}>
                        <div className="md:px-12 lg:px-24 max-w-7xl relative items-center w-full px-5 py-20 mx-auto">
                            <div className="mx-auto flex flex-col w-full max-w-lg text-center">
                                <p className="mb-5 font-medium text-3xl text-white">
                                    {jobData.position}
                                </p>
                                <div>
                                    <span className="font-color">
                                        {jobData.location} - {jobData.salary}
                                    </span>
                                </div>
                            </div>
                        </div>
                    </section>
                    <section>
                        <div className="divide-y divide-gray-700">
                            <section className="text-gray-600 body-font mt-2">
                                <div className="container px-5 py-20 mx-auto">
                                    <div className="flex flex-col w-full mx-auto">
                                        <div className="w-full mx-auto">
                                            <h2 className="xs:text-3xl text-2xl my-5 font-bold">
                                                Job Description
                                            </h2>
                                            <div>
                                                <p className="font-color">{jobData.description}</p>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </section>
                            <section className="text-gray-600 body-font mt-2">
                                <div className="container px-5 py-20 mx-auto">
                                    <div className="flex flex-col w-full mx-auto">
                                        <div className="w-full mx-auto">
                                            <h2 className="xs:text-3xl text-2xl my-5 font-bold">
                                                Job-Type
                                            </h2>
                                            <div>
                                                <p className="font-color">{jobData.jobType}</p>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                            </section>
                        </div>
                    </section>
                </>
            )}
        </>
    );
};

export default JobDetail;

Enter fullscreen mode Exit fullscreen mode

Screenshot

Job detail


Job Component

inside components > job > job.component.tsx
I created a helper func truncateString for text that's too long. If text reaches max length it shows ...

I get the job data from props, since I'm using typescript I defined what the data type of the prop should be, in my case JobData.

import React from 'react';
import { Link } from 'react-router-dom';
import { JobData } from '../../app/features/job/jobTypes';
import { truncateString } from '../../utils/truncateString';

interface JobProps {
     job: JobData;
}
Enter fullscreen mode Exit fullscreen mode

UI

Job card with links to view details, apply for the job, and or view the company that posted it.

const Job: React.FC<JobProps> = ({ job }) => {
    return (
        <div className="py-8 flex flex-wrap md:flex-nowrap">
            <div className="md:w-64 md:mb-0 mb-6 flex-shrink-0 flex flex-col">
                <span className="font-semibold title-font text-indigo-500">
                    {job.companyName.toUpperCase()}
                </span>
                <span className="mt-1 font-color text-sm">{job.datePosted}</span>
            </div>
            <div className="md:flex-grow">
                <h2 className="text-2xl font-medium text-white title-font mb-2">
                    {job.position}{' '}
                    <span className="text-indigo-500 text-sm">
                        ({job.location}) - ({job.jobType})
                    </span>
                </h2>
                <p className="leading-relaxed font-color max-w-3xl">
                    {truncateString(job.description, 250)}
                </p>
                <a
                    href={job.applyUrl}
                    className="text-indigo-500 inline-flex items-center mt-4 mr-4"
                >
                    APPLY NOW
                    <svg
                        className="w-4 h-4 ml-2"
                        viewBox="0 0 24 24"
                        stroke="currentColor"
                        strokeWidth="2"
                        fill="none"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                    >
                        <path d="M5 12h14"></path>
                        <path d="M12 5l7 7-7 7"></path>
                    </svg>
                </a>
                <Link
                    to={`job/${job.id}`}
                    className="text-indigo-500 inline-flex items-center mt-4 mr-3"
                >
                    VIEW JOB
                    <svg
                        className="w-4 h-4 ml-2"
                        viewBox="0 0 24 24"
                        stroke="currentColor"
                        strokeWidth="2"
                        fill="none"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                    >
                        <path d="M5 12h14"></path>
                        <path d="M12 5l7 7-7 7"></path>
                    </svg>
                </Link>
                <Link
                    to={`company/${job.id}`}
                    className="text-indigo-500 inline-flex items-center mt-4"
                >
                    VIEW COMPANY
                    <svg
                        className="w-4 h-4 ml-2"
                        viewBox="0 0 24 24"
                        stroke="currentColor"
                        strokeWidth="2"
                        fill="none"
                        strokeLinecap="round"
                        strokeLinejoin="round"
                    >
                        <path d="M5 12h14"></path>
                        <path d="M12 5l7 7-7 7"></path>
                    </svg>
                </Link>
            </div>
        </div>
    );
};

export default Job;

Enter fullscreen mode Exit fullscreen mode

That's all for the UI/Job portion of the project, stay tuned!

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more