Merged in feat/SW-1555-jobylon-feed-filter (pull request #1494)
Feat/SW-1555 jobylon feed filter * feat(SW-1555): Added jobylon feed component * feat(SW-1555): Added filter functionality for Jobylon feed Approved-by: Matilda Landström
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
.filterForm {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
"use client"
|
||||
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Select from "@/components/TempDesignSystem/Select"
|
||||
|
||||
import styles from "./filter.module.css"
|
||||
|
||||
import type { Key } from "react-aria-components"
|
||||
|
||||
import type { JobylonFilterItem } from "@/types/components/blocks/jobylonFeed"
|
||||
import { JobylonFilterKey } from "@/types/enums/jobylonFeed"
|
||||
|
||||
interface FilterProps {
|
||||
onFilterChange: (filter: JobylonFilterKey, value: Key) => void
|
||||
countryFilters: JobylonFilterItem[]
|
||||
cityFilters: JobylonFilterItem[]
|
||||
departmentFilters: JobylonFilterItem[]
|
||||
categoryFilters: JobylonFilterItem[]
|
||||
}
|
||||
|
||||
export default function Filter({
|
||||
onFilterChange,
|
||||
countryFilters,
|
||||
cityFilters,
|
||||
departmentFilters,
|
||||
categoryFilters,
|
||||
}: FilterProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
return (
|
||||
<form className={styles.filterForm}>
|
||||
<Select
|
||||
items={countryFilters}
|
||||
defaultSelectedKey={""}
|
||||
label={intl.formatMessage({ id: "Country" })}
|
||||
aria-label={intl.formatMessage({ id: "Country" })}
|
||||
name="country"
|
||||
onSelect={(value) => onFilterChange(JobylonFilterKey.country, value)}
|
||||
/>
|
||||
<Select
|
||||
items={cityFilters}
|
||||
defaultSelectedKey={""}
|
||||
label={intl.formatMessage({
|
||||
id: "Location (shown in local language)",
|
||||
})}
|
||||
aria-label={intl.formatMessage({
|
||||
id: "Location (shown in local language)",
|
||||
})}
|
||||
name="city"
|
||||
onSelect={(value) => onFilterChange(JobylonFilterKey.city, value)}
|
||||
/>
|
||||
<Select
|
||||
items={departmentFilters}
|
||||
defaultSelectedKey={""}
|
||||
label={intl.formatMessage({ id: "Hotel or office" })}
|
||||
aria-label={intl.formatMessage({ id: "Hotel or office" })}
|
||||
name="department"
|
||||
onSelect={(value) => onFilterChange(JobylonFilterKey.department, value)}
|
||||
/>
|
||||
<Select
|
||||
items={categoryFilters}
|
||||
defaultSelectedKey={""}
|
||||
label={intl.formatMessage({ id: "Category" })}
|
||||
aria-label={intl.formatMessage({ id: "Category" })}
|
||||
name="category"
|
||||
onSelect={(value) => onFilterChange(JobylonFilterKey.category, value)}
|
||||
/>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
"use client"
|
||||
|
||||
import { useReducer } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
|
||||
|
||||
import JobylonCard from "../JobylonCard"
|
||||
import Filter from "./Filter"
|
||||
import { init, reducer } from "./reducer"
|
||||
|
||||
import styles from "./jobList.module.css"
|
||||
|
||||
import type { Key } from "react-aria-components"
|
||||
|
||||
import { ActionType } from "@/types/components/blocks/jobylonFeed"
|
||||
import type { JobylonFilterKey } from "@/types/enums/jobylonFeed"
|
||||
import type { JobylonItem } from "@/types/trpc/routers/jobylon"
|
||||
|
||||
interface JobListProps {
|
||||
allJobs: JobylonItem[]
|
||||
}
|
||||
|
||||
export default function JobList({ allJobs }: JobListProps) {
|
||||
const intl = useIntl()
|
||||
const [state, dispatch] = useReducer(reducer, { allJobs }, init)
|
||||
|
||||
function handleFilterChange(filter: JobylonFilterKey, value: Key) {
|
||||
const payload = { filter, value }
|
||||
dispatch({ type: ActionType.UPDATE_FILTER, payload })
|
||||
}
|
||||
|
||||
const countryFilters = [
|
||||
{ label: intl.formatMessage({ id: "All countries" }), value: "" },
|
||||
...state.countryFilters,
|
||||
]
|
||||
const cityFilters = [
|
||||
{ label: intl.formatMessage({ id: "All locations" }), value: "" },
|
||||
...state.cityFilters,
|
||||
]
|
||||
const departmentFilters = [
|
||||
{ label: intl.formatMessage({ id: "All hotels and offices" }), value: "" },
|
||||
...state.departmentFilters,
|
||||
]
|
||||
const categoryFilters = [
|
||||
{ label: intl.formatMessage({ id: "All categories" }), value: "" },
|
||||
...state.categoryFilters,
|
||||
]
|
||||
|
||||
return (
|
||||
<div className={styles.jobList}>
|
||||
<Filter
|
||||
onFilterChange={handleFilterChange}
|
||||
countryFilters={countryFilters}
|
||||
cityFilters={cityFilters}
|
||||
departmentFilters={departmentFilters}
|
||||
categoryFilters={categoryFilters}
|
||||
/>
|
||||
<Subtitle type="two">
|
||||
{intl.formatMessage(
|
||||
{
|
||||
id: "{count, plural, one {{count} Result} other {{count} Results}}",
|
||||
},
|
||||
{ count: state.jobs.length }
|
||||
)}
|
||||
</Subtitle>
|
||||
<ul className={styles.list}>
|
||||
{state.jobs.map((job) => (
|
||||
<li key={job.id}>
|
||||
<JobylonCard job={job} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.jobList {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
|
||||
.list {
|
||||
list-style: none;
|
||||
display: grid;
|
||||
gap: var(--Spacing-x2);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { getFilteredJobs, getFiltersFromJobs } from "./utils"
|
||||
|
||||
import {
|
||||
type Action,
|
||||
ActionType,
|
||||
type InitState,
|
||||
type State,
|
||||
} from "@/types/components/blocks/jobylonFeed"
|
||||
|
||||
export function init(initState: InitState): State {
|
||||
const { country, city, department, category } = getFiltersFromJobs(
|
||||
initState.allJobs
|
||||
)
|
||||
|
||||
return {
|
||||
allJobs: initState.allJobs,
|
||||
jobs: initState.allJobs,
|
||||
countryFilters: country,
|
||||
cityFilters: city,
|
||||
departmentFilters: department,
|
||||
categoryFilters: category,
|
||||
chosenCategoryFilter: "",
|
||||
chosenCityFilter: "",
|
||||
chosenCountryFilter: "",
|
||||
chosenDepartmentFilter: "",
|
||||
}
|
||||
}
|
||||
|
||||
export function reducer(state: State, action: Action) {
|
||||
const type = action.type
|
||||
switch (type) {
|
||||
case ActionType.UPDATE_FILTER: {
|
||||
const filters = {
|
||||
country: state.chosenCountryFilter,
|
||||
city: state.chosenCityFilter,
|
||||
department: state.chosenDepartmentFilter,
|
||||
category: state.chosenCategoryFilter,
|
||||
[action.payload.filter]: action.payload.value,
|
||||
}
|
||||
const jobs = getFilteredJobs(state.allJobs, filters)
|
||||
const { country, city, department, category } = getFiltersFromJobs(jobs)
|
||||
|
||||
return {
|
||||
...state,
|
||||
chosenCountryFilter: filters.country,
|
||||
chosenCityFilter: filters.city,
|
||||
chosenDepartmentFilter: filters.department,
|
||||
chosenCategoryFilter: filters.category,
|
||||
countryFilters: country,
|
||||
cityFilters: city,
|
||||
departmentFilters: department,
|
||||
categoryFilters: category,
|
||||
jobs,
|
||||
}
|
||||
}
|
||||
default:
|
||||
const unhandledActionType: never = type
|
||||
console.info(`Unhandled type: ${unhandledActionType}`)
|
||||
return state
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import type { Key } from "react-aria-components"
|
||||
|
||||
import type { JobylonFilters } from "@/types/components/blocks/jobylonFeed"
|
||||
import { JobylonFilterKey } from "@/types/enums/jobylonFeed"
|
||||
import type { JobylonItem } from "@/types/trpc/routers/jobylon"
|
||||
|
||||
export function getFiltersFromJobs(jobs: JobylonItem[]) {
|
||||
const filters = jobs.reduce<JobylonFilters>(
|
||||
(acc, job) => {
|
||||
if (job.locations.length) {
|
||||
job.locations.forEach((location) => {
|
||||
if (location.country && location.countryShort) {
|
||||
if (
|
||||
!acc[JobylonFilterKey.country].find(
|
||||
(f) => f.value === location.countryShort
|
||||
)
|
||||
) {
|
||||
acc[JobylonFilterKey.country].push({
|
||||
value: location.countryShort,
|
||||
label: location.country,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (location.city) {
|
||||
if (
|
||||
!acc[JobylonFilterKey.city].find((f) => f.value === location.city)
|
||||
) {
|
||||
acc[JobylonFilterKey.city].push({
|
||||
value: location.city,
|
||||
label: location.city,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
if (job.departments.length) {
|
||||
job.departments.forEach((department) => {
|
||||
if (
|
||||
!acc[JobylonFilterKey.department].find(
|
||||
(f) => f.value === department.id
|
||||
)
|
||||
) {
|
||||
acc[JobylonFilterKey.department].push({
|
||||
value: department.id,
|
||||
label: department.name,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
if (job.categories.length) {
|
||||
job.categories.forEach((category) => {
|
||||
if (
|
||||
!acc[JobylonFilterKey.category].find(
|
||||
(f) => f.value === category.text
|
||||
)
|
||||
) {
|
||||
acc[JobylonFilterKey.category].push({
|
||||
value: category.text,
|
||||
label: category.text,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{
|
||||
[JobylonFilterKey.country]: [],
|
||||
[JobylonFilterKey.category]: [],
|
||||
[JobylonFilterKey.city]: [],
|
||||
[JobylonFilterKey.department]: [],
|
||||
}
|
||||
)
|
||||
|
||||
// Sort each filter array by label
|
||||
Object.keys(filters).forEach((key) => {
|
||||
const typedKey = key as JobylonFilterKey
|
||||
filters[typedKey].sort((a, b) => a.label.localeCompare(b.label))
|
||||
})
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
export function getFilteredJobs(
|
||||
jobs: JobylonItem[],
|
||||
filters: Record<JobylonFilterKey, Key>
|
||||
) {
|
||||
const countryFilter = filters[JobylonFilterKey.country]
|
||||
const cityFilter = filters[JobylonFilterKey.city]
|
||||
const categoryFilter = filters[JobylonFilterKey.category]
|
||||
const departmentFilter = filters[JobylonFilterKey.department]
|
||||
|
||||
return jobs.filter((job) => {
|
||||
const countryMatch = countryFilter
|
||||
? job.locations.some(
|
||||
(location) => location.countryShort === countryFilter
|
||||
)
|
||||
: true
|
||||
const cityMatch = cityFilter
|
||||
? job.locations.some((location) => location.city === cityFilter)
|
||||
: true
|
||||
const departmentMatch = filters[JobylonFilterKey.department]
|
||||
? job.departments.some((department) => department.id === departmentFilter)
|
||||
: true
|
||||
const categoryMatch = filters[JobylonFilterKey.category]
|
||||
? job.categories.some((category) => category.text === categoryFilter)
|
||||
: true
|
||||
return countryMatch && cityMatch && departmentMatch && categoryMatch
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user