feat(student): added eligible and non eligible tabs
This commit is contained in:
@@ -43,28 +43,33 @@ export type Job = InferSelectModel<typeof jobs> & {
|
||||
export type Resume = typeof resumes.$inferSelect;
|
||||
|
||||
export default function JobsPage({
|
||||
jobs,
|
||||
eligibleJobs,
|
||||
ineligibleJobs,
|
||||
resumes,
|
||||
studentId,
|
||||
appliedJobIds = [],
|
||||
}: {
|
||||
jobs: Job[];
|
||||
eligibleJobs: Job[];
|
||||
ineligibleJobs: Job[];
|
||||
resumes: Resume[];
|
||||
studentId: number;
|
||||
appliedJobIds?: number[];
|
||||
}) {
|
||||
const [filteredJobs, setFilteredJobs] = useState<Job[]>([]);
|
||||
const [activeTab, setActiveTab] = useState<'eligible' | 'ineligible'>('eligible');
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [locationFilter, setLocationFilter] = useState('all');
|
||||
const [jobTypeFilter, setJobTypeFilter] = useState('all');
|
||||
const [showLoadMore, setShowLoadMore] = useState(false);
|
||||
const allJobs = [...eligibleJobs, ...ineligibleJobs];
|
||||
|
||||
useEffect(() => {
|
||||
filterJobs();
|
||||
}, [jobs, searchTerm, locationFilter, jobTypeFilter]);
|
||||
}, [eligibleJobs, ineligibleJobs, activeTab, searchTerm, locationFilter, jobTypeFilter]);
|
||||
|
||||
const filterJobs = () => {
|
||||
let filtered = [...jobs];
|
||||
let base = activeTab === 'eligible' ? eligibleJobs : ineligibleJobs;
|
||||
let filtered = [...base];
|
||||
|
||||
// Search filter
|
||||
if (searchTerm) {
|
||||
@@ -180,7 +185,7 @@ export default function JobsPage({
|
||||
Clear Filters
|
||||
</Button>
|
||||
<span className="text-sm text-gray-500">
|
||||
{filteredJobs.length} of {jobs.length} jobs
|
||||
{filteredJobs.length} of {allJobs.length} jobs
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -194,7 +199,7 @@ export default function JobsPage({
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Total Jobs</p>
|
||||
<p className="text-3xl font-bold text-gray-800">{jobs.length}</p>
|
||||
<p className="text-3xl font-bold text-gray-800">{allJobs.length}</p>
|
||||
</div>
|
||||
<Briefcase className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
@@ -207,7 +212,7 @@ export default function JobsPage({
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Active Companies</p>
|
||||
<p className="text-3xl font-bold text-gray-800">
|
||||
{new Set(jobs.map((job) => job.company.name)).size}
|
||||
{new Set(allJobs.map((job) => job.company.name)).size}
|
||||
</p>
|
||||
</div>
|
||||
<Building2 className="w-8 h-8 text-green-600" />
|
||||
@@ -221,7 +226,7 @@ export default function JobsPage({
|
||||
<div>
|
||||
<p className="text-sm font-medium text-gray-600">Remote Jobs</p>
|
||||
<p className="text-3xl font-bold text-gray-800">
|
||||
{jobs.filter((job) => job.location.toLowerCase().includes('remote')).length}
|
||||
{allJobs.filter((job) => job.location.toLowerCase().includes('remote')).length}
|
||||
</p>
|
||||
</div>
|
||||
<Users className="w-8 h-8 text-purple-600" />
|
||||
@@ -242,6 +247,42 @@ export default function JobsPage({
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Card className="bg-white shadow-sm mb-6">
|
||||
<CardContent className="p-6">
|
||||
<div className="text-center mb-3">
|
||||
<h2 className="text-lg font-semibold text-gray-800">Select a category to view jobs</h2>
|
||||
<p className="text-sm text-gray-500">Switch between eligible and not eligible jobs based on your profile</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<div className="inline-flex rounded-full border border-gray-200 bg-gray-50 p-1">
|
||||
<Button
|
||||
variant={activeTab === 'eligible' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className={`rounded-full px-5 ${activeTab === 'eligible' ? '' : 'text-gray-700'}`}
|
||||
onClick={() => setActiveTab('eligible')}
|
||||
>
|
||||
Eligible
|
||||
<span className={`ml-2 inline-flex items-center justify-center rounded-full text-xs px-2 py-0.5 ${activeTab === 'eligible' ? 'bg-white text-gray-900' : 'bg-gray-200 text-gray-700'}`}>
|
||||
{eligibleJobs.length}
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={activeTab === 'ineligible' ? 'default' : 'ghost'}
|
||||
size="sm"
|
||||
className={`rounded-full px-5 ${activeTab === 'ineligible' ? '' : 'text-gray-700'}`}
|
||||
onClick={() => setActiveTab('ineligible')}
|
||||
>
|
||||
Not Eligible
|
||||
<span className={`ml-2 inline-flex items-center justify-center rounded-full text-xs px-2 py-0.5 ${activeTab === 'ineligible' ? 'bg-white text-gray-900' : 'bg-gray-200 text-gray-700'}`}>
|
||||
{ineligibleJobs.length}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Jobs Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{displayedJobs.map((job) => (
|
||||
@@ -298,13 +339,24 @@ export default function JobsPage({
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<JobApplicationModal
|
||||
job={{ ...job, minCGPA: Number(job.minCGPA) }}
|
||||
studentId={studentId} // Mock student ID - in real app this would come from auth
|
||||
resumes={resumes}
|
||||
/>
|
||||
{activeTab === 'eligible' ? (
|
||||
<JobApplicationModal
|
||||
job={{ ...job, minCGPA: Number(job.minCGPA) }}
|
||||
studentId={studentId}
|
||||
resumes={resumes}
|
||||
isApplied={appliedJobIds.includes(job.id)}
|
||||
/>
|
||||
) : (
|
||||
<Button size="sm" variant="outline" disabled>
|
||||
Not Eligible
|
||||
</Button>
|
||||
)}
|
||||
{job.link && (
|
||||
<Button size="sm" variant="outline">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => window.open(job.link as string, '_blank')}
|
||||
>
|
||||
<ExternalLink className="w-4 h-4 mr-2" />
|
||||
View Details
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import JobsClient from './JobClient';
|
||||
import { auth } from '@/auth';
|
||||
import { db, resumes } from '@workspace/db';
|
||||
import { db, resumes, students } from '@workspace/db';
|
||||
import { eq } from '@workspace/db/drizzle';
|
||||
import { getStudentApplicationJobIds } from '../actions';
|
||||
|
||||
@@ -18,5 +18,49 @@ export default async function JobsPage() {
|
||||
const { success, appliedJobIds } = await getStudentApplicationJobIds(studentId);
|
||||
const studentAppliedJobIds = success ? appliedJobIds : [];
|
||||
|
||||
return <JobsClient jobs={jobs} resumes={reusmes} studentId={studentId} appliedJobIds={studentAppliedJobIds} />;
|
||||
// Fetch student with grades for eligibility computation
|
||||
const student = await db.query.students.findFirst({
|
||||
where: eq(students.id, studentId),
|
||||
with: { grades: true },
|
||||
});
|
||||
|
||||
const studentSSC = Number((student as any)?.ssc ?? 0);
|
||||
const studentHSC = Number((student as any)?.hsc ?? 0);
|
||||
const grades = (student?.grades || []).map((g) => ({
|
||||
sem: g.sem,
|
||||
sgpi: Number(g.sgpi),
|
||||
isKT: Boolean(g.isKT),
|
||||
deadKT: Boolean(g.deadKT),
|
||||
}));
|
||||
const hasLiveKT = grades.some((g) => g.isKT);
|
||||
const hasDeadKT = grades.some((g) => g.deadKT);
|
||||
const avgCGPA = grades.length > 0 ? Number((grades.reduce((a, b) => a + (b.sgpi || 0), 0) / grades.length).toFixed(2)) : 0;
|
||||
|
||||
const isEligible = (job: typeof jobs[number]) => {
|
||||
const minCGPA = Number(job.minCGPA || 0);
|
||||
const minSSC = Number(job.minSSC || 0);
|
||||
const minHSC = Number(job.minHSC || 0);
|
||||
const allowDeadKT = Boolean(job.allowDeadKT);
|
||||
const allowLiveKT = Boolean(job.allowLiveKT);
|
||||
|
||||
if (avgCGPA < minCGPA) return false;
|
||||
if (studentSSC < minSSC) return false;
|
||||
if (studentHSC < minHSC) return false;
|
||||
if (!allowLiveKT && hasLiveKT) return false;
|
||||
if (!allowDeadKT && hasDeadKT) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const eligibleJobs = jobs.filter(isEligible);
|
||||
const ineligibleJobs = jobs.filter((j) => !isEligible(j));
|
||||
|
||||
return (
|
||||
<JobsClient
|
||||
eligibleJobs={eligibleJobs as any}
|
||||
ineligibleJobs={ineligibleJobs as any}
|
||||
resumes={reusmes}
|
||||
studentId={studentId}
|
||||
appliedJobIds={studentAppliedJobIds}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,18 +69,30 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
|
||||
}
|
||||
};
|
||||
|
||||
const isDeadlinePassed = new Date() > job.applicationDeadline;
|
||||
const isDeadlinePassed = new Date() > new Date(job.applicationDeadline as any);
|
||||
const cannotApplyReason = isApplied
|
||||
? 'You have already applied to this job'
|
||||
: resumes.length === 0
|
||||
? 'No resumes found. Please upload a resume first.'
|
||||
: isDeadlinePassed
|
||||
? 'Application deadline has passed'
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
disabled={isDeadlinePassed}
|
||||
>
|
||||
Apply Now
|
||||
</Button>
|
||||
<div className="flex flex-col items-start">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
disabled={Boolean(cannotApplyReason)}
|
||||
>
|
||||
{isApplied ? 'Applied' : 'Apply Now'}
|
||||
</Button>
|
||||
{cannotApplyReason && (
|
||||
<span className="mt-1 text-xs text-red-600">{cannotApplyReason}</span>
|
||||
)}
|
||||
</div>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
@@ -180,7 +192,7 @@ export default function JobApplicationModal({ job, studentId, resumes, isApplied
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
onClick={handleApply}
|
||||
disabled={isApplying || resumes.length === 0}
|
||||
disabled={isApplying || resumes.length === 0 || isApplied}
|
||||
className="flex-1 bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isApplying ? 'Submitting...' : 'Submit Application'}
|
||||
|
||||
Reference in New Issue
Block a user