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:
Erik Tiekstra
2025-03-07 11:46:42 +00:00
parent c3be694874
commit 7fa86a2077
15 changed files with 424 additions and 52 deletions

View File

@@ -0,0 +1,4 @@
.filterForm {
display: grid;
gap: var(--Spacing-x1);
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -0,0 +1,10 @@
.jobList {
display: grid;
gap: var(--Spacing-x2);
}
.list {
list-style: none;
display: grid;
gap: var(--Spacing-x2);
}

View File

@@ -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
}
}

View File

@@ -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
})
}