Skip to content

Commit

Permalink
Merge pull request #6 from caceresjuancruz/staging
Browse files Browse the repository at this point in the history
Staging
  • Loading branch information
caceresjuancruz committed Dec 9, 2023
2 parents e87dae3 + 3b2aa2b commit 2ca2745
Show file tree
Hide file tree
Showing 13 changed files with 566 additions and 42 deletions.
29 changes: 29 additions & 0 deletions app/(root)/jobs/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import LocalSearchbar from "@/components/shared/search/LocalSearchbar";
import { Skeleton } from "@/components/ui/skeleton";

const Loading = () => {
return (
<section>
<h1 className="h1-bold text-dark100_light900">Jobs</h1>

<div className="mb-12 mt-11 flex flex-wrap gap-5">
<LocalSearchbar
placeholder="Job Title, Company, or Keywords"
iconPosition="left"
iconSrc="/assets/icons/search.svg"
route="/jobs"
otherClasses="flex-1"
/>
<Skeleton className="h-14 w-28" />
</div>

<div className="flex flex-col gap-6">
{[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((item) => (
<Skeleton key={item} className="h-48 w-full rounded-xl" />
))}
</div>
</section>
);
};

export default Loading;
73 changes: 73 additions & 0 deletions app/(root)/jobs/page.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<h1 className="h1-bold text-dark100_light900">Jobs</h1>

<div className="flex">
<JobsFilter countriesList={countries} />
</div>

<section className="mt-12 flex flex-wrap gap-4">
{jobs && jobs.length > 0 ? (
jobs.map((job: Job, index: number) => (
<JobCard key={index} job={job} />
))
) : (
<NoResults
title="No jobs available"
description="There are no jobs available at the moment. Please check back later."
link="/"
linkTitle="Go home"
/>
)}
</section>
<div className="mt-10">
<Pagination pageNumber={page} isNext={jobs?.length === 10 || false} />
</div>
</>
);
}
8 changes: 7 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type { Metadata } from "next";
import "./globals.css";
import "../styles/prism.css";
import { ThemeProvider } from "@/context/ThemeProvider";
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/react";

const inter = Inter({
subsets: ["latin"],
Expand Down Expand Up @@ -42,7 +44,11 @@ export default function RootLayout({
},
}}
>
<ThemeProvider>{children}</ThemeProvider>
<ThemeProvider>
{children}
<SpeedInsights />
<Analytics />
</ThemeProvider>
</ClerkProvider>
</body>
</html>
Expand Down
136 changes: 136 additions & 0 deletions components/cards/JobCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="background-light900_dark200 light-border shadow-light100_darknone flex flex-col items-start gap-6 rounded-lg border p-6 sm:flex-row sm:p-8">
<div className="flex w-full justify-end sm:hidden">
<JobLocation
job_country={job_country}
job_state={job_state}
job_city={job_city}
/>
</div>

<div className="flex items-center gap-6">
{employer_logo ? (
<Link
href={employer_website ?? "/jobs"}
target="_blank"
className="background-light800_dark400 relative h-16 w-16 rounded-xl"
>
<Image
src={employer_logo}
alt="Company logo"
fill
className="h-full w-full object-contain p-2"
unoptimized
/>
</Link>
) : (
<Image
src="/assets/images/site-logo.svg"
alt="Default site logo"
width={64}
height={64}
className="rounded-[10px]"
/>
)}
</div>

<div className="w-full">
<div className="flex-between flex-wrap gap-2">
<p className="base-semibold text-dark200_light900">
{processJobTitle(job_title)}
</p>

<div className="hidden sm:flex">
<JobLocation
job_country={job_country}
job_state={job_state}
job_city={job_city}
/>
</div>
</div>

<p className="body-regular text-dark500_light700 mt-2 line-clamp-2">
{job_description?.slice(0, 200)}
</p>

<div className="flex-between mt-8 flex-wrap gap-6">
<div className="flex flex-wrap items-center gap-6">
<div className="flex items-center gap-2">
<Image
src="/assets/icons/clock.svg"
alt="clock"
width={20}
height={20}
unoptimized
/>
<p className="body-medium text-light-500">
{job_employment_type}
</p>
</div>

<div className="flex items-center gap-2">
<Image
src="/assets/icons/currency-dollar-circle.svg"
alt="dollar symbol"
width={20}
height={20}
unoptimized
/>
<p className="body-medium text-light-500">Not disclosed</p>
</div>
</div>

<Link
href={job_apply_link ?? "/jobs"}
className="flex items-center gap-2"
target="_blank"
>
<p className="body-semibold primary-text-gradient">View job</p>

<Image
src="/assets/icons/arrow-up-right.svg"
alt="arrow up right"
width={20}
height={20}
unoptimized
/>
</Link>
</div>
</div>
</div>
);
};

export default JobCard;
35 changes: 35 additions & 0 deletions components/jobs/JobLocation.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="background-light800_dark400 flex items-center justify-end gap-2 rounded-2xl px-3 py-1.5">
<Image
src={`https://flagsapi.com/${job_country}/flat/64.png`}
alt="Country flag"
width={16}
height={16}
unoptimized
className="rounded-full"
/>

<p className="body-medium text-dark400_light700">
{job_city && `${job_city}, `}
{job_state && `${job_state}, `}
{job_country && `${job_country}`}
</p>
</div>
);
};

export default JobLocation;
83 changes: 83 additions & 0 deletions components/jobs/JobsFilter.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="relative mt-11 flex w-full justify-between gap-5 max-sm:flex-col sm:items-center">
<LocalSearchbar
route={path}
iconPosition="left"
iconSrc="/assets/icons/search.svg"
placeholder="Job Title, Company, or Keywords"
otherClasses="flex-1 max-sm:w-full"
/>

<Select onValueChange={(value) => handleUpdateParams(value)}>
<SelectTrigger className="body-regular light-border-2 background-light800_dark300 text-dark500_light700 line-clamp-1 flex min-h-[56px] items-center gap-3 border p-4 focus:ring-0 focus:ring-offset-0 sm:max-w-[210px]">
<Image
src="/assets/icons/carbon-location.svg"
alt="location"
width={18}
height={18}
unoptimized
/>
<div className="line-clamp-1 flex-1 text-left">
<SelectValue placeholder="Select Location" />
</div>
</SelectTrigger>

<SelectContent className="body-semibold text-dark500_light700 light-border-2 max-h-[350px] max-w-[250px] border bg-light-900 dark:bg-dark-300">
<SelectGroup>
{countriesList && countriesList.length > 0 ? (
countriesList.map((country: Country) => (
<SelectItem
key={country.name.common}
value={country.name.common}
className="cursor-pointer px-4 py-3 hover:bg-light-800 dark:hover:bg-dark-400"
>
{country.name.common}
</SelectItem>
))
) : (
<SelectItem value="No results found">No results found</SelectItem>
)}
</SelectGroup>
</SelectContent>
</Select>
</div>
);
};

export default JobsFilter;
Loading

0 comments on commit 2ca2745

Please sign in to comment.