Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | 1x 37x 37x 37x 37x 74x 37x 74x 37x 74x 37x 3x 3x 3x 3x 37x 29x 74x 2x 74x 1x 22x | "use client";
import { useState } from "react";
import { CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Briefcase, MapPin, Calendar } from "lucide-react";
import Link from "next/link";
interface Job {
documentID: string;
title: string;
description: string;
location: string;
postedAt: string;
experience: string;
deadline: string;
}
interface JobBoardClientProps {
initialJobs: Job[];
}
export default function JobBoardClient({ initialJobs }: JobBoardClientProps) {
const [searchTerm, setSearchTerm] = useState<string>("");
const [experienceFilter, setExperienceFilter] = useState<Set<string>>(new Set());
const [locationFilter, setLocationFilter] = useState<Set<string>>(new Set());
const experienceOptions = Array.from(
new Set(initialJobs.map((job) => job.experience).filter(Boolean))
);
const locationOptions = Array.from(
new Set(initialJobs.map((job) => job.location).filter(Boolean))
);
const filteredJobs = initialJobs.filter((job) => {
return (
(!searchTerm ||
job.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
job.description.toLowerCase().includes(searchTerm.toLowerCase())) &&
(experienceFilter.size === 0 || experienceFilter.has(job.experience)) &&
(locationFilter.size === 0 || locationFilter.has(job.location))
);
});
const toggleFilter = (
filterSet: Set<string>,
value: string,
setFilter: (s: Set<string>) => void
) => {
const newSet = new Set(filterSet);
Iif (newSet.has(value)) newSet.delete(value);
else newSet.add(value);
setFilter(newSet);
};
return (
<div className="container mx-auto p-6 mt-20 mb-20 gap-8 flex flex-col md:flex-row">
{/* Sidebar with Filters */}
<aside className="w-full md:w-1/4 lg:w-1/5 p-6 bg-white dark:bg-gray-900 rounded-2xl shadow-lg sticky top-24 h-fit">
<h2 className="font-heading text-2xl font-bold mb-6 text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700 pb-2">
Filters
</h2>
{/* Search Input */}
<div className="mb-6">
<input
type="text"
placeholder="Search jobs..."
className="w-full px-4 py-3 rounded-xl border border-gray-300 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
aria-label="Search jobs"
/>
</div>
{/* Experience Filter */}
<div className="mb-6">
<h3 className="font-heading text-lg font-semibold mb-3 text-gray-800 dark:text-gray-200">
Experience
</h3>
<div className="flex flex-col gap-3 max-h-48 overflow-y-auto pr-2">
{experienceOptions.map((experience) => (
<label
key={experience}
htmlFor={`experience-${experience}`}
className="inline-flex items-center cursor-pointer text-gray-700 dark:text-gray-300 select-none"
>
<input
id={`experience-${experience}`}
type="checkbox"
value={experience}
checked={experienceFilter.has(experience)}
onChange={() =>
toggleFilter(experienceFilter, experience, setExperienceFilter)
}
className="form-checkbox h-5 w-5 text-indigo-600 rounded focus:ring-indigo-500 dark:bg-gray-800 dark:border-gray-700"
/>
<span className="ml-3 font-sans">{experience}</span>
</label>
))}
</div>
</div>
{/* Location Filter */}
<div>
<h3 className="font-heading text-lg font-semibold mb-3 text-gray-800 dark:text-gray-200">
Location
</h3>
<div className="flex flex-col gap-3 max-h-48 overflow-y-auto pr-2">
{locationOptions.map((location) => (
<label
key={location}
htmlFor={`location-${location}`}
className="inline-flex items-center cursor-pointer text-gray-700 dark:text-gray-300 select-none"
>
<input
id={`location-${location}`}
type="checkbox"
value={location}
checked={locationFilter.has(location)}
onChange={() =>
toggleFilter(locationFilter, location, setLocationFilter)
}
className="form-checkbox h-5 w-5 text-indigo-600 rounded focus:ring-indigo-500 dark:bg-gray-800 dark:border-gray-700"
/>
<span className="ml-3 font-sans">{location}</span>
</label>
))}
</div>
</div>
</aside>
{/* Job Listings */}
<main className="flex-1">
{filteredJobs.length > 0 ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredJobs.map((job) => (
<Link
href={`/jobs/${job.documentID}`}
key={job.documentID}
className="group bg-white dark:bg-gray-900 rounded-2xl shadow-md hover:shadow-xl transition-shadow transform hover:-translate-y-1 hover:scale-[1.02] duration-300 cursor-pointer p-6 flex flex-col min-h-full"
aria-label={`View details for ${job.title}`}
>
<CardHeader className="p-0 mb-4">
<CardTitle className="font-heading text-xl font-extrabold text-gray-900 dark:text-white line-clamp-2">
{job.title}
</CardTitle>
</CardHeader>
<CardContent className="font-sans p-0 flex flex-col grow space-y-4 text-gray-700 dark:text-gray-300">
<p className="text-sm leading-relaxed line-clamp-3">{job.description}</p>
<div className="flex flex-wrap gap-4 text-sm">
<div className="flex items-center gap-1.5 text-indigo-600 dark:text-indigo-400 font-medium">
<MapPin className="h-5 w-5" />
<span>{job.location}</span>
</div>
<div className="flex items-center gap-1.5 text-gray-500 dark:text-gray-400">
<Calendar className="h-4 w-4" />
<span>Posted: {job.postedAt}</span>
</div>
<div className="flex items-center gap-1.5 text-gray-500 dark:text-gray-400">
<Briefcase className="h-4 w-4" />
<span>{job.experience} exp.</span>
</div>
</div>
</CardContent>
<footer className="mt-auto pt-6">
<div className="font-heading bg-red-50 dark:bg-red-900 text-red-700 dark:text-red-400 rounded-xl py-2 px-4 font-semibold text-sm max-w-full wrap-break-word">
Application Deadline: {job.deadline}
</div>
{/* Button appears only on hover */}
<div className="mt-5 opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<Button
className="font-heading w-full py-3 text-base font-semibold rounded-xl bg-linear-to-r from-indigo-600 to-blue-600 hover:from-blue-700 hover:to-indigo-700 text-white shadow-lg transition-transform transform group-hover:scale-105 focus:outline-none focus:ring-4 focus:ring-indigo-400"
>
View Details
</Button>
</div>
</footer>
</Link>
))}
</div>
) : (
<p className="text-center text-gray-500 dark:text-gray-400 text-lg mt-20">
No jobs found matching your criteria.
</p>
)}
</main>
</div>
);
}
|