Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions backend/src/jobs/job.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,28 @@ export class JobController {
async getUserJobs(
@Request() req: AuthRequest,
@Query() query: GetJobsQueryDto,
): Promise<{ message: string; data: Job[] }> {
const jobs = await this.jobService.getJobsByUserId(
): Promise<{
message: string;
data: Job[];
hasMore: boolean;
totalCount: number;
totalPages: number;
}> {
const result = await this.jobService.getJobsByUserId(
req.user.userId,
query.skip,
query.take,
);

// Calculate total pages (use nullish coalescing for default)
const totalPages = Math.ceil(result.totalCount / (query.take ?? 10));

return {
message: 'Jobs retrieved successfully',
data: jobs,
data: result.jobs,
hasMore: result.hasMore,
totalCount: result.totalCount,
totalPages,
};
}
}
24 changes: 20 additions & 4 deletions backend/src/jobs/job.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,28 @@ export class JobService {
userId: number,
skip: number = 0,
take: number = 10,
): Promise<Job[]> {
return this.jobRepository.find({
): Promise<{ jobs: Job[]; hasMore: boolean; totalCount: number }> {
// Get total count for this user
const totalCount = await this.jobRepository.count({
where: { userId },
order: { createdAt: 'DESC' },
});

// Fetch take+1 to determine if more exists
const jobs = await this.jobRepository.find({
where: { userId },
order: { createdAt: 'DESC', jobId: 'DESC' }, // Deterministic: tie-breaker with jobId
skip,
take,
take: take + 1,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

// If we got more than requested, there are more pages
const hasMore = jobs.length > take;

// Return only the requested amount
return {
jobs: jobs.slice(0, take),
hasMore,
totalCount,
};
}
}
4 changes: 2 additions & 2 deletions frontend/app/jobs/page.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Navbar from "@/components/layout/navbar";
import Footer from "@/components/layout/footer";
import JobForm from "@/components/job/job-form";
import JobsPageContent from "@/components/job/jobs-page-content";
import ProtectedRoute from "@/components/auth/protected-route";

export default function JobsPage() {
return (
<ProtectedRoute>
<div className="flex min-h-screen flex-col bg-gray-50">
<Navbar />
<JobForm />
<JobsPageContent />
<Footer />
</div>
</ProtectedRoute>
Expand Down
4 changes: 2 additions & 2 deletions frontend/components/dashboard/dashboard-widgets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ export default function DashboardWidgets() {
<div className="rounded-2xl border border-gray-200 bg-white p-6 shadow-sm">
<h2 className="text-lg font-semibold text-gray-900">Quick Actions</h2>
<p className="mt-2 text-sm text-gray-600 mb-4">
Analyze a new job posting to get recommendations.
View and manage all your job postings in one place.
</p>
<Link
href="/jobs"
className="inline-block rounded-lg bg-black px-4 py-2 text-sm font-medium text-white transition hover:opacity-90"
>
Submit Job Posting
Manage Jobs
</Link>
</div>
</section>
Expand Down
45 changes: 45 additions & 0 deletions frontend/components/job/job-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import { Job } from "@/types/job";

interface JobCardProps {
job: Job;
onView?: (jobId: number) => void;
onDelete?: (jobId: number) => void;
}

export default function JobCard({ job, onView, onDelete }: JobCardProps) {
const inputTypeBadgeColor = job.inputType === "TEXT" ? "bg-blue-100 text-blue-800" : "bg-green-100 text-green-800";

return (
<div className="bg-white rounded-lg shadow p-6 hover:shadow-lg transition">
<div className="flex justify-between items-start mb-3">
<h3 className="text-lg font-bold text-gray-900">{job.jobTitle || "Untitled Job"}</h3>
<span className={`text-xs ${inputTypeBadgeColor} px-3 py-1 rounded-full`}>
{job.inputType}
</span>
</div>
<p className="text-gray-700 font-semibold mb-2">{job.companyName || "No company"}</p>
<p className="text-gray-600 text-sm mb-4 line-clamp-2">
{job.inputType === "TEXT" ? job.jobText : job.jobLink}
</p>
<div className="flex justify-between items-center text-sm text-gray-500 mb-4">
<span>Created: {new Date(job.createdAt).toLocaleDateString()}</span>
</div>
<div className="flex gap-2">
<button
onClick={() => onView?.(job.jobId)}
className="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-900 py-2 px-4 rounded transition"
>
View
</button>
<button
onClick={() => onDelete?.(job.jobId)}
className="flex-1 bg-red-100 hover:bg-red-200 text-red-700 py-2 px-4 rounded transition"
>
Delete
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</div>
</div>
);
}
19 changes: 10 additions & 9 deletions frontend/components/job/job-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import InputField from "../ui/input-field";
import JobInputToggle from './job-input-toggle';
import JobSubmitButton from './job-submit-button';
import JobResultPreview from './job-result-preview';
import { createJob, InputType } from '@/lib/job-api';
import { createJob } from '@/lib/job-api';
import { InputType } from '@/types/job';

type JobFormData = {
jobId: number;
Expand All @@ -19,7 +20,7 @@ type JobFormData = {
};

export default function JobForm() {
const [inputType, setInputType] = useState<InputType>(InputType.TEXT);
const [inputType, setInputType] = useState<InputType>("TEXT");
const [companyName, setCompanyName] = useState('');
const [jobTitle, setJobTitle] = useState('');
const [jobText, setJobText] = useState('');
Expand All @@ -29,7 +30,7 @@ export default function JobForm() {
const [result, setResult] = useState<JobFormData | null>(null);

const conditionalResetField = (type: InputType) => {
if (type === InputType.TEXT) {
if (type === "TEXT") {
setJobLink("");
} else {
setJobText("");
Expand All @@ -44,7 +45,7 @@ export default function JobForm() {
};

const validateForm = (): string | null => {
if(inputType === InputType.TEXT) {
if(inputType === "TEXT") {
const trimmedText = jobText.trim()

if(!trimmedText) {
Expand All @@ -56,7 +57,7 @@ export default function JobForm() {
}
}

if(inputType === InputType.LINK) {
if(inputType === "LINK") {
const trimmedLink = jobLink.trim();

if(!trimmedLink) {
Expand Down Expand Up @@ -89,8 +90,8 @@ export default function JobForm() {
const payload = {
inputType,
jobTitle: jobTitle.trim() || undefined,
jobText: inputType === InputType.TEXT ? jobText.trim() : undefined,
jobLink: inputType === InputType.LINK ? jobLink.trim() : undefined,
jobText: inputType === "TEXT" ? jobText.trim() : undefined,
jobLink: inputType === "LINK" ? jobLink.trim() : undefined,
companyName: companyName.trim() || undefined,
}

Expand All @@ -101,7 +102,7 @@ export default function JobForm() {
setJobTitle("");
setJobText("");
setJobLink("");
setInputType(InputType.TEXT);
setInputType("TEXT");
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : "An error occurred while submitting the job";
setError(errorMessage);
Expand Down Expand Up @@ -142,7 +143,7 @@ export default function JobForm() {
placeholder="Optional"
/>

{inputType === InputType.TEXT ? (
{inputType === "TEXT" ? (
<div>
<label
htmlFor="jobText"
Expand Down
12 changes: 7 additions & 5 deletions frontend/components/job/job-input-toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
"use client";

import {InputType} from '@/lib/job-api';
import {InputType} from '@/types/job';

type Props = {
value: InputType;
onChange: (type: InputType) => void;
};

export default function JobInputToggle({ value, onChange }: Props) {
const isText = value === InputType.TEXT;
const isLink = value === InputType.LINK;
const isText = value === "TEXT";
const isLink = value === "LINK";

return (
<div className="w-full" role="group" aria-label="Job input type selection">
<div className="grid grid-cols-2 gap-2 rounded-xl bg-gray-100 p-1">
<button
type="button"
onClick={() => onChange(InputType.TEXT)}
onClick={() => onChange("TEXT")}
aria-pressed={isText}
className={`rounded-lg px-4 py-2 text-sm font-medium transition ${
isText
? "bg-white shadow text-black"
Expand All @@ -28,7 +29,8 @@ export default function JobInputToggle({ value, onChange }: Props) {

<button
type="button"
onClick={() => onChange(InputType.LINK)}
onClick={() => onChange("LINK")}
aria-pressed={isLink}
className={`rounded-lg px-4 py-2 text-sm font-medium transition ${
isLink
? "bg-white shadow text-black"
Expand Down
Empty file.
2 changes: 1 addition & 1 deletion frontend/components/job/job-result-preview.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { InputType } from "@/lib/job-api";
import { InputType } from "@/types/job";


type JobResult = {
Expand Down
80 changes: 80 additions & 0 deletions frontend/components/job/jobs-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"use client";

import JobCard from "./job-card";
import { Job } from "@/types/job";

interface JobsListProps {
jobs: Job[];
currentPage: number;
totalPages: number;
onPreviousPage: () => void;
onNextPage: () => void;
isLoading: boolean;
onView?: (jobId: number) => void;
onDelete?: (jobId: number) => void;
}

export default function JobsList({
jobs,
currentPage,
totalPages,
onPreviousPage,
onNextPage,
isLoading,
onView,
onDelete,
}: JobsListProps) {
if (isLoading) {
return (
<div className="flex justify-center items-center py-12">
<div className="text-lg text-gray-600">Loading jobs...</div>
</div>
);
}

if (jobs.length === 0) {
return (
<div className="flex justify-center items-center py-12">
<div className="text-center">
<p className="text-lg text-gray-600 mb-4">No jobs created yet</p>
<p className="text-sm text-gray-500">Create your first job to get started</p>
</div>
</div>
);
}

return (
<>
{/* Jobs Grid */}
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{jobs.map((job) => (
<JobCard
key={job.jobId}
job={job}
onView={onView}
onDelete={onDelete}
/>
))}
</div>

{/* Pagination */}
<div className="flex justify-center items-center gap-2 mt-12">
<button
onClick={onPreviousPage}
disabled={currentPage === 1}
className="px-4 py-2 bg-gray-700 text-white rounded hover:bg-gray-800 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<span className="px-4 py-2 text-gray-600">Page {currentPage} of {totalPages}</span>
<button
onClick={onNextPage}
disabled={currentPage === totalPages}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</>
);
}
Loading
Loading