Skip to content

Commit

Permalink
feat: enhance filters in url behaviour
Browse files Browse the repository at this point in the history
Signed-off-by: Nishant Mittal <[email protected]>
  • Loading branch information
nishantwrp committed Dec 27, 2022
1 parent b0edda2 commit 5dfdf49
Show file tree
Hide file tree
Showing 8 changed files with 151 additions and 47 deletions.
29 changes: 21 additions & 8 deletions src/components/search.jsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,42 @@
import React, { useMemo, useState, useCallback, memo } from "react"
import React, { useMemo, useState, useCallback, useEffect, memo } from "react"
import { debounce } from "debounce"

import "./search.css"

import { Icon, Input } from "semantic-ui-react"
import { useAppDispatch, useAppSelector } from "../store"
import { getSearch, setSearch } from "../store/search"
import { EventBus } from "../utils/events"

const Search = () => {
const search = useAppSelector(getSearch)
const dispatch = useAppDispatch()
const [searchText, setSearchText] = useState(search)

const dispatchSetSearch = useCallback(value => {
dispatch(setSearch(value))
}, [])
const dispatchSetSearch = useCallback(
value => {
dispatch(setSearch(value))
},
[dispatch]
)

const debouncedDispatchSetSearch = useMemo(
() => debounce(dispatchSetSearch, 200),
[]
[dispatchSetSearch]
)

const handleChange = useCallback(
({ target: { value } }) => {
setSearchText(value)
debouncedDispatchSetSearch(value)
},
[debouncedDispatchSetSearch]
)

const handleChange = useCallback(({ target: { value } }) => {
setSearchText(value)
debouncedDispatchSetSearch(value)
useEffect(() => {
EventBus.subscribe("updateSearch", query => {
setSearchText(query)
})
}, [])

return (
Expand Down
2 changes: 1 addition & 1 deletion src/components/sidebar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ const Sidebar = ({ config, showFilters }) => {

const clearAllFilters = useCallback(() => {
dispatch(clearFilters())
}, [])
}, [dispatch])

return (
<div className="sidebar-sidebar" style={sidebarStyle}>
Expand Down
37 changes: 23 additions & 14 deletions src/pages/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useEffect, useMemo } from "react"
import Fuse from "fuse.js"
import { graphql } from "gatsby"
import { useBreakpoint } from "gatsby-plugin-breakpoints"
import { useLocation } from "@reach/router"

import "./index.css"

Expand All @@ -10,10 +11,12 @@ import OrgCard from "../components/org-card"
import SEO from "../components/seo"
import Notification from "../components/notification"
import { Grid } from "semantic-ui-react"
import { useAppSelector } from "../store"
import { useAppSelector, useAppDispatch } from "../store"
import { getSearch } from "../store/search"
import { getFilters } from "../store/filters"
import { setSearchParams } from "../utils/searchParams"
import { getSearchParam } from "../utils/searchParams"
import { EventBus } from "../utils/events"
import { urlChanged } from "../store/actions"

const getOrganizations = data => {
return data.allOrganization.edges.map(orgNode => {
Expand Down Expand Up @@ -110,30 +113,36 @@ const getFilteredOrganizations = (organizations, searchQuery, filters) => {
}

const IndexPage = ({ data }) => {
const dispatch = useAppDispatch()
const searchQuery = useAppSelector(getSearch)
const filters = useAppSelector(getFilters)
const allOrganizations = useMemo(() => getOrganizations(data), [])
const location = useLocation()
const allOrganizations = useMemo(() => getOrganizations(data), [data])
const filteredOrganizations = getFilteredOrganizations(
allOrganizations,
searchQuery,
filters
)

useEffect(() => {
// Update the search params in the url if filters or search query
// change.
const searchParams = {}
// This executes when there's an update in the url. (Example: User pressed back)
// This will not execute when setSearchParams is used because
// it uses JS history api. This is the desired behaviour so that this function
// doesn't run when the filters or search is being modified in the app itself.

if (searchQuery) {
searchParams["search"] = searchQuery
const updatedSearchQuery = getSearchParam("search") || ""
const updatedFilters = JSON.parse(getSearchParam("filters")) || {
years: [],
categories: [],
technologies: [],
topics: [],
}

if (Object.values(filters).filter(arr => arr.length !== 0).length !== 0) {
searchParams["filters"] = JSON.stringify(filters)
}

setSearchParams(searchParams)
}, [searchQuery, filters])
dispatch(
urlChanged({ search: updatedSearchQuery, filters: updatedFilters })
)
EventBus.emit("updateSearch", updatedSearchQuery)
}, [location, dispatch])

const metaDescription =
"View and analyse the info of the organizations participating in Google Summer of Code and filter them by various parameters."
Expand Down
5 changes: 5 additions & 0 deletions src/store/actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createAction } from "@reduxjs/toolkit"

// Actions that are common to multiple slices.

export const urlChanged = createAction("urlChanged")
41 changes: 34 additions & 7 deletions src/store/filters.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,66 @@
import { createSlice } from "@reduxjs/toolkit"
import { getSearchParam } from "../utils/searchParams";
import {
getSearchParam,
removeSearchParam,
setSearchParams,
} from "../utils/searchParams"
import { urlChanged } from "./actions"

const updateFiltersInUrl = filters => {
if (Object.values(filters).filter(arr => arr.length !== 0).length === 0) {
removeSearchParam("filters")
} else {
setSearchParams({
filters: JSON.stringify(filters),
})
}
}

const filtersSlice = createSlice({
name: "filters",
initialState: () => {
return JSON.parse(getSearchParam("filters")) || {
years: [],
categories: [],
technologies: [],
topics: [],
};
return (
JSON.parse(getSearchParam("filters")) || {
years: [],
categories: [],
technologies: [],
topics: [],
}
)
},
reducers: {
addFilter: (state, action) => {
const { name, val } = action.payload
state[name] = [...state[name], val]
updateFiltersInUrl(state)
},
removeFilter: (state, action) => {
const { name, val } = action.payload
state[name] = state[name].filter(v => v !== val)
updateFiltersInUrl(state)
},
setFilters: (state, action) => {
const { years, categories, technologies, topics } = action.payload
state.years = years
state.categories = categories
state.technologies = technologies
state.topics = topics
updateFiltersInUrl(state)
},
clearFilters: state => {
state.years = []
state.categories = []
state.technologies = []
state.topics = []
updateFiltersInUrl(state)
},
},
extraReducers: builder => {
builder.addCase(urlChanged, (state, action) => {
const { filters } = action.payload
return { ...filters }
})
},
})

export const getFilters = state => {
Expand Down
28 changes: 26 additions & 2 deletions src/store/search.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,42 @@
import { createSlice } from "@reduxjs/toolkit"
import { getSearchParam } from "../utils/searchParams"
import {
getSearchParam,
removeSearchParam,
setSearchParams,
} from "../utils/searchParams"
import { urlChanged } from "./actions"

const updateSearchInUrl = query => {
if (query) {
setSearchParams({
search: query,
})
} else {
removeSearchParam("search")
}
}

const searchSlice = createSlice({
name: "search",
initialState: () => {
return {
value: getSearchParam("search") || ""
value: getSearchParam("search") || "",
}
},
reducers: {
setSearch: (state, action) => {
state.value = action.payload
updateSearchInUrl(action.payload)
},
},
extraReducers: builder => {
builder.addCase(urlChanged, (state, action) => {
const { search } = action.payload
return {
value: search,
}
})
},
})

export const getSearch = state => {
Expand Down
13 changes: 13 additions & 0 deletions src/utils/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class EventBus {
static _subscriberMap = {}

static emit(eventName, payload) {
if (!this._subscriberMap[eventName]) return
this._subscriberMap[eventName].forEach(fn => fn(payload))
}

static subscribe(eventName, callback) {
if (!this._subscriberMap[eventName]) this._subscriberMap[eventName] = []
this._subscriberMap[eventName].push(callback)
}
}
43 changes: 28 additions & 15 deletions src/utils/searchParams.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,42 @@
* accessed elsewhere.
* @param {{ [param: string]: string }} params
*/
export const setSearchParams = (params) => {
if (typeof window !== 'undefined' && window) {
const url = new URL(window.location);
url.search = "";
export const setSearchParams = params => {
if (typeof window !== "undefined" && window) {
const url = new URL(window.location)
for (const [param, value] of Object.entries(params)) {
url.searchParams.set(param, value);
};
window.history.pushState({}, "", url);
url.searchParams.set(param, value)
}
window.history.pushState({}, "", url)
} else {
console.error("error setting search params. window is not defined.");
console.error("error setting search params. window is not defined.")
}
};
}

/**
* Gets the value of the search param in the url. Returns null if doesn't exist.
* @param {string} param
* @returns {string | null}
*/
export const getSearchParam = (param) => {
if (typeof window !== 'undefined' && window) {
return new URL(window.location).searchParams.get(param);
export const getSearchParam = param => {
if (typeof window !== "undefined" && window) {
return new URL(window.location).searchParams.get(param)
} else {
console.error("error getting search params. window is not defined.");
return null;
console.error("error getting search params. window is not defined.")
return null
}
};
}

/**
* Removes a search param from the url.
* @param {string} param
*/
export const removeSearchParam = param => {
if (typeof window !== "undefined" && window) {
const url = new URL(window.location)
url.searchParams.delete(param)
window.history.pushState({}, "", url)
} else {
console.error("error removing search param. window is not defined.")
}
}

0 comments on commit 5dfdf49

Please sign in to comment.