Skip to content

Commit 2395404

Browse files
final code freeze
1 parent a5d12a2 commit 2395404

File tree

16 files changed

+285
-34
lines changed

16 files changed

+285
-34
lines changed

apps/backend/main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def get_db():
2727
db.close()
2828

2929
# --- Job Endpoints ---
30-
@app.post('/jobs/', response_model=schemas.Job)
30+
@app.post('/jobs', response_model=schemas.Job)
3131
def create_job(job: schemas.JobBase, db: Session = Depends(get_db)):
3232
return controller.create_job(db, job)
3333

@@ -38,9 +38,9 @@ def get_job(job_id: UUID, db: Session = Depends(get_db)):
3838
raise HTTPException(status_code=404, detail='Job not found')
3939
return db_job
4040

41-
@app.get('/jobs/', response_model=list[schemas.Job])
42-
def get_jobs(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
43-
return controller.get_jobs(db, skip, limit)
41+
@app.get('/jobs', response_model=list[schemas.Job])
42+
def get_jobs(skip: int = 0, limit: int = 100, job_title: str | None = None, db: Session = Depends(get_db)):
43+
return controller.get_jobs(db, skip, limit, job_title)
4444

4545
@app.put('/jobs/{job_id}', response_model=schemas.Job)
4646
def update_job(job_id: UUID, job_update: schemas.JobBase, db: Session = Depends(get_db)):

apps/backend/src/controller.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,11 @@ def create_job(db: Session, job: JobBase) -> Job:
3333
def get_job(db: Session, job_id: UUID) -> Job | None:
3434
return db.query(Job).filter(Job.job_id == job_id).first()
3535

36-
def get_jobs(db: Session, skip: int = 0, limit: int = 100):
37-
return db.query(Job).offset(skip).limit(limit).all()
36+
def get_jobs(db: Session, skip: int, limit: int, job_title: str | None = None):
37+
query = db.query(Job)
38+
if job_title:
39+
query = query.filter(Job.job_title.ilike(f"%{job_title}%"))
40+
return query.offset(skip).limit(limit).all()
3841

3942
def update_job(db: Session, job_id: UUID, job_update: JobBase) -> Job | None:
4043
db_job = db.query(Job).filter(Job.job_id == job_id).first()

apps/browser-extension/src/lib/dataservice.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,21 @@ export default {
8181
});
8282
},
8383

84+
async checkJobExists(jobTitle: string): Promise<boolean> {
85+
const url = `${API_BASE}/jobs?job_title=${encodeURIComponent(jobTitle)}`;
86+
const response = await fetch(url, {
87+
method: 'GET',
88+
headers: { 'Content-Type': 'application/json' }
89+
});
90+
91+
if (!response.ok) {
92+
const err = await response.json();
93+
console.error('Job check failed:', err);
94+
}
95+
96+
return response.json().length > 0; // returns true if 2xx status
97+
},
98+
8499
async createJob(jobData: JobData) {
85100
const url = `${API_BASE}/jobs`;
86101
const response = await fetch(url, {

apps/browser-extension/src/popup.vue

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const jobLocation = ref<string | null>(null); // New field for job location
1010
const jobTitle = ref(''); // placeholder, could be fetched
1111
const company = ref(''); // placeholder, could be fetched
1212
const jobStatus = ref('Saved');
13+
const alreadyExists = ref(false);
1314
const isSaveBtnLoading = ref(false);
1415
1516
const addMessageListener = (callback: any) => {
@@ -28,6 +29,7 @@ async function setupData() {
2829
2930
if (isLinkedInJob.value) {
3031
jobId.value = new URL(currentLocation).searchParams.get('currentJobId');
32+
alreadyExists.value = await dataservice.checkJobExists(jobId.value || '');
3133
jobLink.value = currentLocation;
3234
3335
addMessageListener((msg: any) => {
@@ -44,7 +46,7 @@ onMounted(async () => {
4446
await setupData();
4547
});
4648
47-
const addJob = async () => {
49+
const addOrUpdateJob = async () => {
4850
isSaveBtnLoading.value = true;
4951
const success = await dataservice.createJob({
5052
job_title: `${jobId.value}:${jobTitle.value}`,
@@ -71,7 +73,7 @@ const addJob = async () => {
7173
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100 text-center">Job Applica</h2>
7274

7375
<div v-if="isLinkedInJob">
74-
<form @submit.prevent="addJob" class="flex flex-col gap-3 w-full">
76+
<form @submit.prevent="addOrUpdateJob" class="flex flex-col gap-3 w-full">
7577
<!-- Job Title -->
7678
<div class="flex flex-col w-full">
7779
<label for="jobTitle" class="text-sm font-medium text-gray-700 dark:text-gray-200 mb-1">Job Title</label>
@@ -111,7 +113,7 @@ const addJob = async () => {
111113
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
112114
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"></path>
113115
</svg>
114-
<span>{{ isSaveBtnLoading ? 'Saving...' : 'Add Job' }}</span>
116+
<span>{{ isSaveBtnLoading ? 'Saving...' : alreadyExists ? 'Update Job' : 'Add Job' }}</span>
115117
</button>
116118
</form>
117119
</div>
@@ -121,7 +123,7 @@ const addJob = async () => {
121123
</div>
122124

123125
<!-- Dashboard Button -->
124-
<a href="http://localhost:5173" target="_blank"
126+
<a href="http://localhost:5173/applications" target="_blank"
125127
class="w-full block text-center px-4 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600 transition mt-2">
126128
Go to Dashboard
127129
</a>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<script setup lang="ts">
2+
</script>
3+
4+
<template>
5+
<div>
6+
Board view
7+
</div>
8+
</template>
9+
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<script setup lang="ts">
2+
import { DataTable, type ColumnDef } from '@/components/ui/data-table';
3+
import { ref, h } from 'vue';
4+
import type { JobData } from '@/lib/types';
5+
import { Checkbox } from '@/components/ui/checkbox';
6+
import DataTableHeader from '@/components/ui/data-table/DataTableHeader.vue';
7+
import type { Column } from '@tanstack/vue-table';
8+
import { Badge } from '@/components/ui/badge';
9+
10+
const props = defineProps<{
11+
jobs: JobData[]
12+
}>();
13+
14+
const emit = defineEmits<{
15+
(e: 'selection-change', selectedJobs: any): void
16+
}>();
17+
18+
interface IData {
19+
id: string
20+
21+
title: string
22+
company: string
23+
status: string
24+
}
25+
26+
const statusVariants: Record<string, string> = {
27+
applied: 'info', // Blue — represents action taken
28+
saved: 'secondary', // Gray — neutral, passive state
29+
rejected: 'danger', // Red — error or negative outcome
30+
interviewed: 'warning', // Yellow/Amber — pending or in-progress stage
31+
};
32+
33+
const columns: ColumnDef<IData>[] = [
34+
{
35+
accessorKey: 'id',
36+
header: ({ table }) => h(Checkbox, {
37+
checked: table.getIsAllPageRowsSelected(),
38+
'onUpdate:checked': val => {
39+
table.toggleAllPageRowsSelected(!!val);
40+
emit('selection-change', table.getSelectedRowModel().flatRows.map(row => row.original));
41+
},
42+
ariaLabel: 'Select All',
43+
class: 'translate-y-0.5',
44+
}),
45+
cell: ({ table, row }) => h(Checkbox, {
46+
checked: row.getIsSelected(),
47+
'onUpdate:checked': val => {
48+
row.toggleSelected(!!val);
49+
emit('selection-change', table.getSelectedRowModel().flatRows.map(row => row.original));
50+
},
51+
'ariaLabel': 'Select row',
52+
class: 'translate-y-0.5',
53+
enableSorting: false,
54+
enableHiding: false,
55+
})
56+
},
57+
{
58+
accessorKey: 'id',
59+
header: 'ID',
60+
enableSorting: false,
61+
},
62+
{
63+
accessorKey: 'title',
64+
header: ({ column }) => h(DataTableHeader, {
65+
column: column as Column<IData>,
66+
title: 'Job Title',
67+
'onUpdate:sort': (val) => {
68+
console.log(val)
69+
},
70+
})
71+
},
72+
{
73+
accessorKey: 'company',
74+
header: 'Company',
75+
enableSorting: false,
76+
},
77+
{
78+
accessorKey: 'status',
79+
header: 'Status',
80+
cell: ({ row }) => h('div', {
81+
class: 'max-w-[500px] truncate flex items-center',
82+
}, [
83+
h(Badge, {
84+
variant: (statusVariants[row.original.status] as any),
85+
class: 'mr-2',
86+
}, () => row.original.status),
87+
]),
88+
enableSorting: false,
89+
},
90+
{
91+
id: 'actions',
92+
},
93+
];
94+
95+
function transformAsList(jobs: JobData[]) {
96+
return jobs.map(job => ({
97+
id: job.job_id,
98+
title: job.job_title.split(':')[1].trim(),
99+
company: job.company,
100+
status: job.status
101+
}));
102+
}
103+
104+
</script>
105+
106+
<template>
107+
<div>
108+
<DataTable :columns="columns" :data="transformAsList(jobs)"></DataTable>
109+
</div>
110+
</template>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { default as TableApplications } from './TableApplications.vue';
2+
export { default as BoardApplications } from './BoardApplications.vue';

apps/frontend/src/components/ui/Breadcrumb.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
<script setup lang="ts">
22
import { RouterLink, useRoute } from 'vue-router';
3-
import { onUpdated } from 'vue';
43
import { computed } from 'vue';
54
65
const route = useRoute()
76
const currentRoute = computed(() => {
8-
return route.matched[1]
7+
return route.matched.length > 1 ? route.matched[route.matched.length - 1] : route.matched[0];
98
});
109
1110
</script>
@@ -15,7 +14,7 @@ const currentRoute = computed(() => {
1514
<div class="flex">
1615
<router-link :key="currentRoute.path" :to="currentRoute.path">
1716
<span class="text-sm flex">
18-
<p :class="route.path === currentRoute.path && 'text-primary font-semibold'">{{ currentRoute.meta.title }}</p>
17+
<p :class="route.path === currentRoute.path && 'text-primary font-semibold'">{{ currentRoute.meta.title.split('|')[1] }}</p>
1918
</span>
2019
</router-link>
2120
</div>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { JobData } from './types';
2+
3+
const API_BASE = 'http://localhost:8000';
4+
5+
export default {
6+
async getJobs(): Promise<JobData[]> {
7+
const response = await fetch(`${API_BASE}/jobs`);
8+
if (!response.ok) {
9+
console.error('Failed to fetch jobs');
10+
return [];
11+
}
12+
return response.json();
13+
},
14+
async deleteJob(jobId: number): Promise<void> {
15+
const response = await fetch(`${API_BASE}/jobs/${jobId}`, {
16+
method: 'DELETE',
17+
});
18+
if (!response.ok) {
19+
console.error(`Failed to delete job with ID ${jobId}`);
20+
}
21+
}
22+
};

apps/frontend/src/lib/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export interface JobData {
2+
job_id: string,
3+
job_title: string
4+
company?: string
5+
location?: string,
6+
status: string
7+
category?: string
8+
salary_range?: string
9+
required_skills?: string[]
10+
job_description?: string
11+
min_years_of_experience?: number
12+
max_years_of_experience?: number
13+
};

0 commit comments

Comments
 (0)