From d434a094d4dd7ebb9a59898406055864ca838206 Mon Sep 17 00:00:00 2001 From: Juan Cruz Caceres Date: Sat, 9 Dec 2023 12:48:54 -0300 Subject: [PATCH 1/4] implement jobs page --- app/(root)/jobs/loading.tsx | 29 ++++++ app/(root)/jobs/page.tsx | 73 +++++++++++++ components/cards/JobCard.tsx | 136 +++++++++++++++++++++++++ components/jobs/JobLocation.tsx | 35 +++++++ components/jobs/JobsFilter.tsx | 83 +++++++++++++++ components/shared/LeftSidebar.tsx | 4 +- components/shared/Pagination.tsx | 2 +- components/shared/navbar/MobileNav.tsx | 6 +- lib/actions/job.action.ts | 53 ++++++++++ lib/utils.ts | 30 ++++++ 10 files changed, 445 insertions(+), 6 deletions(-) create mode 100644 app/(root)/jobs/loading.tsx create mode 100644 app/(root)/jobs/page.tsx create mode 100644 components/cards/JobCard.tsx create mode 100644 components/jobs/JobLocation.tsx create mode 100644 components/jobs/JobsFilter.tsx create mode 100644 lib/actions/job.action.ts diff --git a/app/(root)/jobs/loading.tsx b/app/(root)/jobs/loading.tsx new file mode 100644 index 0000000..e9377db --- /dev/null +++ b/app/(root)/jobs/loading.tsx @@ -0,0 +1,29 @@ +import LocalSearchbar from "@/components/shared/search/LocalSearchbar"; +import { Skeleton } from "@/components/ui/skeleton"; + +const Loading = () => { + return ( +
+

Jobs

+ +
+ + +
+ +
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => ( + + ))} +
+
+ ); +}; + +export default Loading; diff --git a/app/(root)/jobs/page.tsx b/app/(root)/jobs/page.tsx new file mode 100644 index 0000000..5c4ef4b --- /dev/null +++ b/app/(root)/jobs/page.tsx @@ -0,0 +1,73 @@ +import UserCard from "@/components/cards/UserCard"; +import Filter from "@/components/shared/Filter"; +import NoResults from "@/components/shared/NoResults"; +import Pagination from "@/components/shared/Pagination"; +import LocalSearchbar from "@/components/shared/search/LocalSearchbar"; +import { UserFilters } from "@/constants/filters"; +import { getAllUsers } from "@/lib/actions/user.action"; +import { SearchParamsProps } from "@/types"; +import { Metadata } from "next"; + +import { + fetchCountries, + fetchJobs, + fetchLocation, +} from "@/lib/actions/job.action"; + +import { Job } from "@/types"; +import JobsFilter from "@/components/jobs/JobsFilter"; +import JobCard from "@/components/cards/JobCard"; + +interface JobsPageProps { + searchParams: { + q: string; + location: string; + page: string; + }; +} + +export const metadata: Metadata = { + title: "Jobs | Dev Overflow", +}; + +export default async function JobsPage({ searchParams }: JobsPageProps) { + const userLocation = await fetchLocation(); + + const jobs = await fetchJobs({ + query: + `${searchParams.q},${searchParams.location}` ?? + `Software Engineer in ${userLocation}`, + page: searchParams.page ?? 1, + }); + + const countries = await fetchCountries(); + const page = parseInt(searchParams.page ?? 1); + + return ( + <> +

Jobs

+ +
+ +
+ +
+ {jobs && jobs.length > 0 ? ( + jobs.map((job: Job, index: number) => ( + + )) + ) : ( + + )} +
+
+ +
+ + ); +} diff --git a/components/cards/JobCard.tsx b/components/cards/JobCard.tsx new file mode 100644 index 0000000..aef31a1 --- /dev/null +++ b/components/cards/JobCard.tsx @@ -0,0 +1,136 @@ +import RenderTag from "../shared/RenderTag"; +import { + formatAndDivideNumber, + getTimestamp, + processJobTitle, +} from "@/lib/utils"; +import Link from "next/link"; +import Metric from "../shared/Metric"; +import { toast } from "../ui/use-toast"; +import { title } from "process"; +import QuestionCard from "./QuestionCard"; +import { Job } from "@/types"; +import Image from "next/image"; +import JobLocation from "../jobs/JobLocation"; + +interface JobCardProps { + job: Job; +} + +const JobCard = ({ job }: JobCardProps) => { + const { + employer_logo, + employer_website, + job_employment_type, + job_title, + job_description, + job_apply_link, + job_city, + job_state, + job_country, + } = job; + + return ( +
+
+ +
+ +
+ {employer_logo ? ( + + Company logo + + ) : ( + Default site logo + )} +
+ +
+
+

+ {processJobTitle(job_title)} +

+ +
+ +
+
+ +

+ {job_description?.slice(0, 200)} +

+ +
+
+
+ clock +

+ {job_employment_type} +

+
+ +
+ dollar symbol +

Not disclosed

+
+
+ + +

View job

+ + arrow up right + +
+
+
+ ); +}; + +export default JobCard; diff --git a/components/jobs/JobLocation.tsx b/components/jobs/JobLocation.tsx new file mode 100644 index 0000000..16f9e59 --- /dev/null +++ b/components/jobs/JobLocation.tsx @@ -0,0 +1,35 @@ +import Image from "next/image"; +import React from "react"; + +interface JobLocationProps { + job_country?: string; + job_city?: string; + job_state?: string; +} + +const JobLocation = ({ + job_country, + job_city, + job_state, +}: JobLocationProps) => { + return ( +
+ Country flag + +

+ {job_city && `${job_city}, `} + {job_state && `${job_state}, `} + {job_country && `${job_country}`} +

+
+ ); +}; + +export default JobLocation; diff --git a/components/jobs/JobsFilter.tsx b/components/jobs/JobsFilter.tsx new file mode 100644 index 0000000..ddff24f --- /dev/null +++ b/components/jobs/JobsFilter.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { formUrlQuery } from "@/lib/utils"; +import { Country } from "@/types"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; +import React from "react"; +import LocalSearchbar from "../shared/search/LocalSearchbar"; +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import Image from "next/image"; + +interface JobsFilterProps { + countriesList: Country[]; +} + +const JobsFilter = ({ countriesList }: JobsFilterProps) => { + const router = useRouter(); + const path = usePathname(); + const searchParams = useSearchParams(); + + const handleUpdateParams = (value: string) => { + const newUrl = formUrlQuery({ + params: searchParams.toString(), + key: "location", + value, + }); + + router.push(newUrl, { scroll: false }); + }; + + return ( +
+ + + +
+ ); +}; + +export default JobsFilter; diff --git a/components/shared/LeftSidebar.tsx b/components/shared/LeftSidebar.tsx index de21b3f..923adac 100644 --- a/components/shared/LeftSidebar.tsx +++ b/components/shared/LeftSidebar.tsx @@ -64,7 +64,7 @@ const LeftSidebar = () => {
- @@ -88,7 +88,7 @@ const MobileNav = () => { - diff --git a/lib/actions/job.action.ts b/lib/actions/job.action.ts new file mode 100644 index 0000000..3ee1f57 --- /dev/null +++ b/lib/actions/job.action.ts @@ -0,0 +1,53 @@ +import { getErrorMessage } from "../utils"; +import { JobFilterParams } from "./shared.types"; + +export const fetchLocation = async () => { + try { + const response = await fetch("http://ip-api.com/json/?fields=country"); + const location = await response.json(); + + return location.country; + } catch (error) { + return { + message: getErrorMessage(error), + }; + } +}; + +export const fetchCountries = async () => { + try { + const response = await fetch("https://restcountries.com/v3.1/all"); + const result = await response.json(); + return result; + } catch (error) { + return { + message: getErrorMessage(error), + }; + } +}; + +export const fetchJobs = async (filters: JobFilterParams) => { + const { query, page } = filters; + + try { + const headers = { + "X-RapidAPI-Key": process.env.NEXT_PUBLIC_RAPID_API_KEY ?? "", + "X-RapidAPI-Host": "jsearch.p.rapidapi.com", + }; + + const response = await fetch( + `https://jsearch.p.rapidapi.com/search?query=${query}&page=${page}`, + { + headers, + } + ); + + const result = await response.json(); + + return result.data; + } catch (error) { + return { + message: getErrorMessage(error), + }; + } +}; diff --git a/lib/utils.ts b/lib/utils.ts index 8072ed1..8a76c2c 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -164,3 +164,33 @@ export const assignBadges = (params: BadgeParam) => { return badgeCounts; }; + +export function processJobTitle(title: string | undefined | null): string { + // Check if title is undefined or null + if (title === undefined || title === null) { + return "No Job Title"; + } + + // Split the title into words + const words = title.split(" "); + + // Filter out undefined or null and other unwanted words + const validWords = words.filter((word) => { + return ( + word !== undefined && + word !== null && + word.toLowerCase() !== "undefined" && + word.toLowerCase() !== "null" + ); + }); + + // If no valid words are left, return the general title + if (validWords.length === 0) { + return "No Job Title"; + } + + // Join the valid words to create the processed title + const processedTitle = validWords.join(" "); + + return processedTitle; +} From 1213325498fe2174ae9ee91fb5313ebc8cd6bbfc Mon Sep 17 00:00:00 2001 From: Juan Cruz Caceres Date: Sat, 9 Dec 2023 13:31:22 -0300 Subject: [PATCH 2/4] Fix styling issues in LeftSidebar and Pagination components --- components/shared/LeftSidebar.tsx | 2 ++ components/shared/Pagination.tsx | 2 +- components/shared/navbar/MobileNav.tsx | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/components/shared/LeftSidebar.tsx b/components/shared/LeftSidebar.tsx index 923adac..c16cedd 100644 --- a/components/shared/LeftSidebar.tsx +++ b/components/shared/LeftSidebar.tsx @@ -10,9 +10,11 @@ import Logo from "./navbar/Logo"; const LeftSidebar = () => { const { userId } = useAuth(); + const pathname = usePathname(); return ( + // eslint-disable-next-line tailwindcss/migration-from-tailwind-2
diff --git a/components/shared/Pagination.tsx b/components/shared/Pagination.tsx index 5f15767..6ec1436 100644 --- a/components/shared/Pagination.tsx +++ b/components/shared/Pagination.tsx @@ -30,7 +30,7 @@ const Pagination = ({ pageNumber, isNext }: PaginationProps) => { if (!isNext && pageNumber === 1) return null; return ( -
+