Enhance job search with sorting, pagination, and UI improvements#5
Merged
Conversation
khaledfahmy123
approved these changes
May 30, 2026
There was a problem hiding this comment.
Pull request overview
This PR adds a URL-synchronized job search/filtering experience for candidate job listings, with API helpers, Pinia search state, sidebar filters, active filter chips, and home page search routing.
Changes:
- Added search state management and URL sync for filters, sorting, and job results.
- Reworked candidate jobs UI with filters, active chips, sorting, pagination, and application actions.
- Added API support types/helpers for filtered job search, categories, and locations.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
src/api/jobs.ts |
Adds filter types, paginated response typing, search endpoint, and locations endpoint. |
src/components/search/ActiveFilters.vue |
Displays removable active filter chips and clear-all action. |
src/components/search/FilterSection.vue |
Adds a collapsible filter section component. |
src/components/search/FilterSidebar.vue |
Adds advanced keyword/location/work-type/date/category filter controls. |
src/composables/useJobSearch.ts |
Synchronizes search store state with URL query parameters and loads results. |
src/router/index.ts |
Reformats and clarifies candidate/employer job route metadata. |
src/stores/search.ts |
Adds Pinia store for search filters, options, jobs, pagination, and loading state. |
src/views/CandidateJobsView.vue |
Replaces basic candidate jobs list with filterable, paginated search UI. |
src/views/HomeView.vue |
Adds search form state and routes home searches to the jobs page. |
src/views/search/JobSearchView.vue |
Adds a separate job search page implementation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+19
to
+30
| defineProps<{ | ||
| title: string; | ||
| icon: string; | ||
| open: boolean; | ||
| }>(); | ||
|
|
||
| const emit = defineEmits<{ | ||
| "update:open": [value: boolean]; | ||
| }>(); | ||
|
|
||
| function toggle() { | ||
| emit("update:open", !open); |
Comment on lines
+256
to
+272
| import { ref, computed } from "vue"; | ||
| import { useRouter } from "vue-router"; | ||
| import AppLayout from "@/layouts/AppLayout.vue"; | ||
| import FilterSidebar from "@/components/search/FilterSidebar.vue"; | ||
| import ActiveFilters from "@/components/search/ActiveFilters.vue"; | ||
| import { useJobSearch } from "@/composables/useJobSearch"; | ||
| import { useApplicationsStore } from "@/stores/applications"; | ||
| import type { JobListing } from "@/api/jobs"; | ||
|
|
||
| const router = useRouter(); | ||
| const applicationsStore = useApplicationsStore(); | ||
| const sidebarOpen = ref(true); | ||
| const { store, goToPage } = useJobSearch(); | ||
|
|
||
| function goToDetail(jobId: number) { | ||
| router.push({ name: "job-detail", params: { jobId } }); | ||
| } |
Comment on lines
+32
to
+43
| function initFromUrl() { | ||
| const q = route.query; | ||
| store.query = (q.q as string) ?? ""; | ||
| store.sort = q.sort === "deadline" ? "deadline" : "newest"; | ||
| store.filters.location = (q.location as string) ?? ""; | ||
| store.filters.work_type = ((q.work_type as string) ?? | ||
| "") as typeof store.filters.work_type; | ||
| store.filters.experience_level = ((q.experience_level as string) ?? | ||
| "") as typeof store.filters.experience_level; | ||
| store.filters.category = (q.category as string) ?? ""; | ||
| store.filters.date_from = (q.date_from as string) ?? ""; | ||
| } |
Comment on lines
+55
to
+58
| function goToPage(page: number) { | ||
| syncToUrl(); | ||
| store.loadJobs(page); | ||
| window.scrollTo({ top: 0, behavior: "smooth" }); |
Comment on lines
+222
to
+223
| <template v-for="item in visiblePages" :key="item"> | ||
| <span v-if="item === '...'" class="page-ellipsis">…</span> |
Comment on lines
+1
to
+3
| <template> | ||
| <AppLayout> | ||
| <div class="search-page"> |
Comment on lines
+22
to
+31
| <label class="filter-label">Keywords</label> | ||
| <div class="icon-field"> | ||
| <span class="material-symbols-outlined icon-field__icon">search</span> | ||
| <input | ||
| v-model="store.query" | ||
| type="text" | ||
| placeholder="e.g. Vue, Laravel, React" | ||
| class="icon-field__input" | ||
| autocomplete="off" | ||
| /> |
Comment on lines
+37
to
+49
| <label class="filter-label">Location</label> | ||
| <div class="icon-field"> | ||
| <span class="material-symbols-outlined icon-field__icon" | ||
| >location_on</span | ||
| > | ||
| <input | ||
| v-model="store.filters.location" | ||
| type="text" | ||
| placeholder="City, State, or Remote" | ||
| class="icon-field__input" | ||
| list="loc-suggestions" | ||
| autocomplete="off" | ||
| /> |
Comment on lines
+58
to
+62
| <span | ||
| v-if="store.activeFilterCount > 0 || store.query" | ||
| class="filter-badge" | ||
| >{{ store.activeFilterCount + (store.query ? 1 : 0) }}</span | ||
| > |
Comment on lines
+78
to
+82
| <span | ||
| v-if="store.activeFilterCount > 0 || store.query" | ||
| class="filter-badge" | ||
| >{{ store.activeFilterCount + (store.query ? 1 : 0) }}</span | ||
| > |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request introduces a comprehensive search and filtering system for job listings, enabling candidates to filter jobs by multiple criteria (such as location, work type, experience level, category, and date) and have their selections reflected in the URL for shareable/searchable links. It adds a Pinia store for managing search state, new composables for syncing state with the router, API enhancements to support filtering, and new UI components for filter display and interaction. The home page search now routes directly to the filtered jobs page.
Search and Filtering System:
src/stores/search.ts) to manage job search filters, query, sorting, and results, including loading job listings and filter options from the API.src/composables/useJobSearch.ts) that synchronizes the search/filter state with URL query parameters, supporting shareable and restorable filter states.src/api/jobs.ts) to support advanced filtering, including new filter types, a locations endpoint, and paginated responses.UI Components for Filters:
ActiveFilters.vueto display current filters as removable chips and provide a "Clear all" option.FilterSection.vuefor collapsible filter groups in the sidebar, improving filter organization and UX.Routing and Home Page Integration:
HomeView.vue) to include a search form that routes users to the filterable jobs page with the selected query and location as URL parameters. [1] [2] [3]