diff --git a/package.json b/package.json index 6e88ac71..b1e35907 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", + "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-radio-group": "^1.2.0", "@radix-ui/react-scroll-area": "^1.1.0", "@radix-ui/react-select": "^2.1.1", @@ -35,6 +36,7 @@ "bcryptjs": "^2.4.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "dayjs": "^1.11.13", "lucide-react": "^0.426.0", "next": "14.2.5", "next-auth": "^4.24.7", diff --git a/public/companies.png b/public/companies.png new file mode 100644 index 00000000..f67631ef Binary files /dev/null and b/public/companies.png differ diff --git a/public/main.png b/public/main.png new file mode 100644 index 00000000..d89d59fb Binary files /dev/null and b/public/main.png differ diff --git a/src/actions/job.action.ts b/src/actions/job.action.ts index 0d9cb930..7d5ae5dd 100644 --- a/src/actions/job.action.ts +++ b/src/actions/job.action.ts @@ -133,7 +133,10 @@ export const getJobById = withServerActionAsyncCatcher< }).serialize(); }); -export const jobFilterQuery = async (queries: JobQuerySchemaType) => { +export const jobFilterQuery = async ( + queries: JobQuerySchemaType, + baseUrl: string +) => { const { page, sortby, location, salaryrange, search, workmode } = JobQuerySchema.parse(queries); const searchParams = new URLSearchParams({ @@ -144,5 +147,5 @@ export const jobFilterQuery = async (queries: JobQuerySchemaType) => { location?.map((location) => searchParams.append('location', location)); salaryrange?.map((range) => searchParams.append('salaryrange', range)); workmode?.map((mode) => searchParams.append('workmode', mode)); - redirect(`/jobs?${searchParams.toString()}`); + redirect(`${baseUrl}?${searchParams.toString()}`); }; diff --git a/src/app/(auth)/signin/page.tsx b/src/app/(auth)/signin/page.tsx index 9902f626..83ad0731 100644 --- a/src/app/(auth)/signin/page.tsx +++ b/src/app/(auth)/signin/page.tsx @@ -3,12 +3,14 @@ import { FormContainer } from '@/layouts/form-container'; const LoginPage = () => { return ( - - - +
+ + + +
); }; diff --git a/src/app/jobs/page.tsx b/src/app/jobs/page.tsx index 3d2cd097..c0e6996f 100644 --- a/src/app/jobs/page.tsx +++ b/src/app/jobs/page.tsx @@ -1,15 +1,16 @@ import AllJobs from '@/components/all-jobs'; import Loader from '@/components/loader'; +import APP_PATHS from '@/config/path.config'; import JobFilters from '@/layouts/job-filters'; import JobsHeader from '@/layouts/jobs-header'; import { JobQuerySchemaType } from '@/lib/validators/jobs.validator'; import { Suspense } from 'react'; const page = async ({ searchParams }: { searchParams: JobQuerySchemaType }) => { return ( -
- +
+
- + diff --git a/src/app/page.tsx b/src/app/page.tsx index 38a57dc6..d207eec6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,14 +1,26 @@ import BackgroundSvg from '@/components/BackgroundSvg'; +import HalfCircleGradient from '@/components/HalfCircleGradient'; import HeroSection from '@/components/hero-section'; +import { JobLanding } from '@/components/job-landing'; +import { JobQuerySchemaType } from '@/lib/validators/jobs.validator'; -const HomePage = async () => { +const HomePage = async ({ + searchParams, +}: { + searchParams: JobQuerySchemaType; +}) => { return ( - <> +
-
+ +
- +
+ +
+ +
); }; diff --git a/src/components/BackgroundSvg.tsx b/src/components/BackgroundSvg.tsx index 5d79ebb8..c492b6c3 100644 --- a/src/components/BackgroundSvg.tsx +++ b/src/components/BackgroundSvg.tsx @@ -1,72 +1,220 @@ export default function BackgroundSvg() { return ( - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {/* + + + */} + + + + + + + + + + + + ); diff --git a/src/components/HalfCircleGradient.tsx b/src/components/HalfCircleGradient.tsx new file mode 100644 index 00000000..17281c11 --- /dev/null +++ b/src/components/HalfCircleGradient.tsx @@ -0,0 +1,17 @@ +import { cn } from '@/lib/utils'; + +export default function HalfCircleGradient({ position }: { position: string }) { + return ( +
+ ); +} diff --git a/src/components/all-jobs.tsx b/src/components/all-jobs.tsx index 352bcc1f..10842d8c 100644 --- a/src/components/all-jobs.tsx +++ b/src/components/all-jobs.tsx @@ -10,6 +10,7 @@ import { PaginationPages } from './ui/paginator'; import Icon from './ui/icon'; import { formatSalary } from '@/lib/utils'; import Link from 'next/link'; +import APP_PATHS from '@/config/path.config'; type PaginatorProps = { searchParams: JobQuerySchemaType; }; @@ -22,7 +23,7 @@ const AllJobs = async ({ searchParams }: PaginatorProps) => { const totalPages = Math.ceil((jobs.additional?.totalJobs || 0) / JOBS_PER_PAGE) || DEFAULT_PAGE; - const currentPage = searchParams.page || DEFAULT_PAGE; + const currentPage = parseInt(searchParams.page?.toString()) || DEFAULT_PAGE; return (
{jobs.additional?.jobs.map((job) => { @@ -63,6 +64,7 @@ const AllJobs = async ({ searchParams }: PaginatorProps) => { ) : null} @@ -70,6 +72,7 @@ const AllJobs = async ({ searchParams }: PaginatorProps) => { searchParams={searchParams} currentPage={currentPage} totalPages={totalPages} + baseUrl={APP_PATHS.JOBS} /> {totalPages ? ( @@ -77,6 +80,7 @@ const AllJobs = async ({ searchParams }: PaginatorProps) => { searchParams={searchParams} currentPage={currentPage} totalPages={totalPages} + baseUrl={APP_PATHS.JOBS} /> ) : null} diff --git a/src/components/hero-section.tsx b/src/components/hero-section.tsx index d50271ee..5f216a17 100644 --- a/src/components/hero-section.tsx +++ b/src/components/hero-section.tsx @@ -1,49 +1,50 @@ -import APP_PATHS from '@/config/path.config'; import { GITHUB_REPO } from '@/lib/constant/app.constant'; import Link from 'next/link'; -import { Button } from './ui/button'; import Icon from './ui/icon'; -import { JobLanding } from './job-landing'; +import Image from 'next/image'; const HeroSection = () => { return ( -
-
- - -

Star us on Github

- -
-
-

- Find the Right{' '} - - Opportunity - - , Hire the Perfect Talent -

-
-
-

- India's most rapidly growing developer community -

+ <> +
+
+ + +

Star us on Github

+ +
+
+

+ {/* Find the Right{' '} + + Opportunity + + , Hire the Perfect Talent */} + Find Your Perfect Job Today! +

+
+
+

+ Discover a thoughtfully selected collection of job opportunities + chosen by our dedicated team of experts. +

+
-
-
- -
- -
+
+ companies +
+ + ); }; diff --git a/src/components/job-landing.tsx b/src/components/job-landing.tsx index b105eced..6df56fc6 100644 --- a/src/components/job-landing.tsx +++ b/src/components/job-landing.tsx @@ -1,55 +1,138 @@ import { getAllJobs } from '@/actions/job.action'; import { formatSalary } from '@/lib/utils'; import Link from 'next/link'; -import Icon from './ui/icon'; -import { JobType } from '@/types/jobs.types'; -import { DEFAULT_PAGE } from '@/config/app.config'; - -export const JobLanding = async () => { - const jobs = await getAllJobs({ - sortby: 'postedat_desc', - page: DEFAULT_PAGE, - limit: 9, - }); - if (!jobs.status) { - return; - } - const allJobs = jobs.additional?.jobs || []; +import { DEFAULT_PAGE, JOBS_PER_PAGE } from '@/config/app.config'; +import JobsHeader from '@/layouts/jobs-header'; +import { Suspense } from 'react'; +import { Loader } from 'lucide-react'; +import { JobQuerySchemaType } from '@/lib/validators/jobs.validator'; +import { Pagination, PaginationContent, PaginationItem } from './ui/pagination'; +import { + PaginationNextButton, + PaginationPreviousButton, +} from './pagination-client'; +import { PaginationPages } from './ui/paginator'; +import APP_PATHS from '@/config/path.config'; + +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +dayjs.extend(relativeTime); + +export const calculateTimeSincePosted = (postedAt: Date): string => { + return dayjs(postedAt).fromNow(); +}; + +export const JobLanding = async ({ + searchParams, +}: { + searchParams: JobQuerySchemaType; +}) => { return ( -
- {allJobs.map((job) => ( - - ))} +
+
+ + + +
+ } + > + + +
); }; -const JobCard = ({ job }: { job: JobType }) => { +type PaginatorProps = { + searchParams: JobQuerySchemaType; +}; + +const JobCard = async ({ searchParams }: PaginatorProps) => { + const jobs = await getAllJobs(searchParams); + if (!jobs.status) { + return
Error {jobs.message}
; + } + + const totalPages = + Math.ceil((jobs.additional?.totalJobs || 0) / JOBS_PER_PAGE) || + DEFAULT_PAGE; + const currentPage = parseInt(searchParams.page?.toString()) || DEFAULT_PAGE; return ( - -
-
-

{job.title}

-

{job.companyName}

-
-
- - - {job.workMode} - - - {job.minSalary && } - {job.minSalary && job.maxSalary - ? `${formatSalary(job.minSalary)}-${formatSalary(job.maxSalary)}` - : 'Not disclosed'} - -
-

- - {job.description} -

-
- +
+ {jobs.additional?.jobs.map((job) => { + return ( + +
+
+ {/* todo:replace with original jobImage */} +
+ {/* job image */} +
+
+
+

+ {job.title} - {job.companyName} +

+

+ {calculateTimeSincePosted(job.postedAt)} • {job.workMode} +

+
+ +
+ + {/* {job.minSalary && } */} + {job.minSalary && job.maxSalary + ? `$${formatSalary(job.maxSalary)}` + : 'NotDisclosed'} + +

+ per annum +

+
+
+ + ); + })} + + + {totalPages ? ( + + + + ) : null} + + {totalPages ? ( + + + + ) : null} + + +
); }; diff --git a/src/components/pagination-client.tsx b/src/components/pagination-client.tsx index 55b090c7..c90f56c5 100644 --- a/src/components/pagination-client.tsx +++ b/src/components/pagination-client.tsx @@ -7,21 +7,26 @@ const PAGE_INCREMENT = 1; const PaginationPreviousButton = ({ searchParams, currentPage, + baseUrl, }: { searchParams: JobQuerySchemaType; currentPage: number; + baseUrl: string; }) => { return ( - jobFilterQuery({ - ...searchParams, - page: currentPage - PAGE_INCREMENT, - }) + jobFilterQuery( + { + ...searchParams, + page: currentPage - PAGE_INCREMENT, + }, + baseUrl + ) } aria-disabled={currentPage - PAGE_INCREMENT < PAGE_INCREMENT} role="button" - className="aria-disabled:pointer-events-none" + className="aria-disabled:pointer-events-none dark:bg-neutral-900 rounded-full bg-neutral-100" /> ); }; @@ -29,22 +34,27 @@ const PaginationNextButton = ({ searchParams, currentPage, totalPages, + baseUrl, }: { searchParams: JobQuerySchemaType; currentPage: number; totalPages: number; + baseUrl: string; }) => { return ( - jobFilterQuery({ - ...searchParams, - page: currentPage + PAGE_INCREMENT, - }) + jobFilterQuery( + { + ...searchParams, + page: currentPage + PAGE_INCREMENT, + }, + baseUrl + ) } aria-disabled={currentPage > totalPages - PAGE_INCREMENT} - className="aria-disabled:pointer-events-none" + className="aria-disabled:pointer-events-none dark:bg-neutral-900 rounded-full bg-neutral-100" /> ); }; diff --git a/src/components/ui/paginator.tsx b/src/components/ui/paginator.tsx index 0731bbb6..b2aa6446 100644 --- a/src/components/ui/paginator.tsx +++ b/src/components/ui/paginator.tsx @@ -6,18 +6,21 @@ import { PaginationItem, PaginationLink, } from './pagination'; +import { cn } from '@/lib/utils'; export const PaginationPages = ({ currentPage, totalPages, searchParams, + baseUrl, }: { currentPage: number; totalPages: number; searchParams: JobQuerySchemaType; + baseUrl: string; }) => { function paginationHandler(page: number) { - jobFilterQuery({ ...searchParams, page: page }); + jobFilterQuery({ ...searchParams, page: page }, baseUrl); } const pages: JSX.Element[] = []; if (totalPages <= 5) { @@ -26,8 +29,11 @@ export const PaginationPages = ({ paginationHandler(i)} - isActive={i === currentPage} role="button" + className={cn('rounded-full dark:bg-neutral-900 bg-neutral-100', { + 'bg-neutral-900 text-white dark:bg-neutral-100 dark:text-black ': + i === currentPage, + })} > {i} @@ -40,8 +46,12 @@ export const PaginationPages = ({ paginationHandler(i)} - isActive={i === currentPage} + // isActive={i === currentPage} role="button" + className={cn('rounded-full dark:bg-neutral-900 bg-neutral-100', { + 'bg-neutral-900 text-white dark:bg-neutral-100 dark:text-black ': + i === currentPage, + })} > {i} @@ -63,7 +73,11 @@ export const PaginationPages = ({ paginationHandler(i)} - isActive={i === currentPage} + // isActive={i === currentPage} + className={cn('rounded-full dark:bg-neutral-900 bg-neutral-100', { + 'bg-neutral-900 text-white dark:bg-neutral-100 dark:text-black ': + i === currentPage, + })} > {i} diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover.tsx new file mode 100644 index 00000000..f224d5ed --- /dev/null +++ b/src/components/ui/popover.tsx @@ -0,0 +1,31 @@ +'use client'; + +import * as React from 'react'; +import * as PopoverPrimitive from '@radix-ui/react-popover'; + +import { cn } from '@/lib/utils'; + +const Popover = PopoverPrimitive.Root; + +const PopoverTrigger = PopoverPrimitive.Trigger; + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => ( + + + +)); +PopoverContent.displayName = PopoverPrimitive.Content.displayName; + +export { Popover, PopoverTrigger, PopoverContent }; diff --git a/src/layouts/footer.tsx b/src/layouts/footer.tsx index a8852f6c..89be9544 100644 --- a/src/layouts/footer.tsx +++ b/src/layouts/footer.tsx @@ -1,30 +1,77 @@ import Icon from '@/components/ui/icon'; -import { socials } from '@/lib/constant/app.constant'; +import { socials, footerLinks } from '@/lib/constant/app.constant'; +import Image from 'next/image'; import Link from 'next/link'; const Footer = () => { return ( -