feat: add syncing filters state with URL

This commit is contained in:
MTG2000
2022-10-24 16:57:18 +03:00
parent 5752f7fdbf
commit a70182aff7
6 changed files with 175 additions and 18 deletions

83
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{
"name": "makers-bolt-fun",
"name": "lightning-landscape",
"version": "0.1.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "makers-bolt-fun",
"name": "lightning-landscape",
"version": "0.1.0",
"dependencies": {
"@apollo/client": "^3.6.9",
@@ -70,6 +70,7 @@
"passport": "^0.6.0",
"passport-lnurl-auth": "^1.5.0",
"qrcode.react": "^3.0.2",
"qs": "^6.11.0",
"react": "^18.0.0",
"react-accessible-accordion": "^5.0.0",
"react-confetti": "^6.0.1",
@@ -19310,6 +19311,20 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/body-parser/node_modules/qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/body-scroll-lock": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz",
@@ -24983,6 +24998,20 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/express/node_modules/qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/express/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -31758,6 +31787,20 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"node_modules/lnurl/node_modules/qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"dependencies": {
"side-channel": "^1.0.4"
},
"engines": {
"node": ">=0.6"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/lnurl/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -61957,9 +62000,9 @@
}
},
"node_modules/qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"dependencies": {
"side-channel": "^1.0.4"
},
@@ -85911,6 +85954,14 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"requires": {
"side-channel": "^1.0.4"
}
}
}
},
@@ -90334,6 +90385,14 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"requires": {
"side-channel": "^1.0.4"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -95490,6 +95549,14 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="
},
"qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"requires": {
"side-channel": "^1.0.4"
}
},
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -118434,9 +118501,9 @@
"requires": {}
},
"qs": {
"version": "6.10.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz",
"integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==",
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
"requires": {
"side-channel": "^1.0.4"
}

View File

@@ -65,6 +65,7 @@
"passport": "^0.6.0",
"passport-lnurl-auth": "^1.5.0",
"qrcode.react": "^3.0.2",
"qs": "^6.11.0",
"react": "^18.0.0",
"react-accessible-accordion": "^5.0.0",
"react-confetti": "^6.0.1",

View File

@@ -4,7 +4,7 @@ import { useExplorePageQuery } from 'src/graphql';
import ProjectsGrid from './ProjectsGrid/ProjectsGrid';
import { Helmet } from "react-helmet";
import Categories, { Category } from '../../Components/Categories/Categories';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Header from './Header/Header';
import Button from 'src/Components/Button/Button';
import { useAppDispatch } from 'src/utils/hooks';
@@ -15,6 +15,7 @@ import { NetworkStatus } from '@apollo/client';
import { FiSliders } from 'react-icons/fi';
import { HiOutlineChevronDoubleDown } from 'react-icons/hi'
import { ProjectsFilters } from './Filters/FiltersModal';
import { getFiltersFromUrl, useUpdateUrlWithFilters } from './helpers';
const UPDATE_FILTERS_ACTION = createAction<Partial<ProjectsFilters>>('PROJECTS_FILTERS_UPDATED')({})
@@ -32,11 +33,15 @@ const PAGE_SIZE = 20;
export default function ExplorePage() {
const dispatch = useAppDispatch();
const [filters, setFilters] = useState<Partial<ProjectsFilters> | null>(null)
const [filters, setFilters] = useState<Partial<ProjectsFilters> | null>(getFiltersFromUrl)
const [selectedCategory, setSelectedCategory] = useState<Category | null>(null)
const projectsLength = useRef<number>(0);
const [canFetchMore, setCanFetchMore] = useState(true);
useUpdateUrlWithFilters(filters)
// useQueryState(removeEmptyFitlers(filters ?? {}));
const { queryFilters, hasSearchFilters } = useMemo(() => {
let filter: QueryFilter = {}
let hasSearchFilters = false;
@@ -155,8 +160,8 @@ export default function ExplorePage() {
<Header
category={selectedCategory}
/>
<div className="grid grid-cols-[1fr_auto] items-center gap-32">
<div className="min-w-0"><Categories filtersActive={hasSearchFilters} value={selectedCategory} onChange={v => selectCategoryTab(v)} /></div>
<div className="grid grid-cols-1 md:grid-cols-[1fr_auto] items-center gap-x-32 gap-y-16">
<div className="min-w-0 max-md:row-start-2"><Categories filtersActive={hasSearchFilters} value={selectedCategory} onChange={v => selectCategoryTab(v)} /></div>
<Button
className={`self-center ${hasSearchFilters ? "!font-bold !bg-primary-50 !text-primary-600 !border-2 !border-primary-400" : "!text-gray-600"}`}
variant='outline'
@@ -179,4 +184,4 @@ export default function ExplorePage() {
</div>
</>
)
}
}

View File

@@ -3,12 +3,13 @@ import { ModalCard, modalCardVariants } from 'src/Components/Modals/ModalsContai
import { motion } from 'framer-motion'
import { IoClose } from 'react-icons/io5'
import Button from 'src/Components/Button/Button'
import { useAppDispatch } from 'src/utils/hooks'
import { useAppDispatch, useMediaQuery } from 'src/utils/hooks'
import { PayloadAction } from '@reduxjs/toolkit'
import IconButton from 'src/Components/IconButton/IconButton'
import { useGetFiltersQuery } from 'src/graphql'
import Skeleton from 'react-loading-skeleton';
import { random } from 'src/utils/helperFunctions';
import { MEDIA_QUERIES } from 'src/utils/theme'
interface Props extends ModalCard {
initFilters?: Partial<ProjectsFilters>
@@ -49,6 +50,9 @@ export default function FiltersModal({ onClose, direction, initFilters, callback
setCategoriesFilter([id]);
}
const isMdScreen = useMediaQuery(MEDIA_QUERIES.isMedium)
const clickTag = (id: string) => {
if (tagsFilter.includes(id))
@@ -223,12 +227,12 @@ export default function FiltersModal({ onClose, direction, initFilters, callback
</div>
<div className="my-48"></div>
<div className='w-full bg-white content-container fixed bottom-0 left-0 py-24 border-t border-gray-200'>
<div className='w-full bg-white content-container fixed z-10 bottom-0 left-0 py-24 border-t border-gray-200'>
<div className="flex justify-between gap-16">
<Button onClick={clearFilters}>Clear all</Button>
<Button size={isMdScreen ? 'md' : 'sm'} onClick={clearFilters}>Clear <span className="hidden md:inline">all</span></Button>
<div className="flex gap-16">
<Button onClick={onClose}>Cancel</Button>
<Button color='primary' onClick={applyFilters}>Apply filters</Button>
<Button size={isMdScreen ? 'md' : 'sm'} onClick={onClose}>Cancel</Button>
<Button size={isMdScreen ? 'md' : 'sm'} color='primary' onClick={applyFilters}>Apply <span className="hidden md:inline">filters</span></Button>
</div>
</div>
</div>

View File

@@ -0,0 +1,50 @@
import { useQueryState } from 'src/utils/hooks/useQueryState';
import qs from "qs"
import { useLocation, useNavigate } from 'react-router-dom';
import { ProjectsFilters } from './Filters/FiltersModal';
import { useEffect } from 'react';
export function getFiltersFromUrl(): Partial<ProjectsFilters> {
const qsValues = qs.parse(window.location.search, { ignoreQueryPrefix: true }) as Partial<ProjectsFilters>;
return removeEmptyFitlers(qsValues)
}
export function removeEmptyFitlers(filters: Partial<ProjectsFilters>): Partial<ProjectsFilters> {
let res: Partial<ProjectsFilters> = {}
if (filters.yearFounded !== 'any')
res.yearFounded = filters.yearFounded;
if (filters.projectLicense !== 'any')
res.projectLicense = filters.projectLicense;
if (filters.projectStatus !== 'any')
res.projectStatus = filters.projectStatus;
if (filters.categoriesIds && filters.categoriesIds?.length > 0)
res.categoriesIds = filters.categoriesIds;
if (filters.tagsIds && filters.tagsIds?.length > 0)
res.tagsIds = filters.tagsIds;
return res;
}
export const useUpdateUrlWithFilters = (state?: Partial<ProjectsFilters> | null) => {
const location = useLocation()
const navigate = useNavigate()
useEffect(() => {
const queryString = qs.stringify(
removeEmptyFitlers(state ?? {}),
{ skipNulls: true }
)
navigate(`${location.pathname}?${queryString}`, { replace: true });
}, [location.pathname, location.search, navigate, state]);
}

View File

@@ -0,0 +1,30 @@
import { useCallback, useEffect } from "react"
import { useNavigate, useLocation } from "react-router-dom"
import qs from "qs"
export const useQueryState = (state?: Record<string, any> | null) => {
const location = useLocation()
const navigate = useNavigate()
useEffect(() => {
const existingQueries = qs.parse(location.search, {
ignoreQueryPrefix: true,
})
const queryString = qs.stringify(
{ ...existingQueries, ...(state ?? {}) },
{ skipNulls: true }
)
navigate(`${location.pathname}?${queryString}`, { replace: true });
}, [location.pathname, location.search, navigate, state]);
return qs.parse(location.search, { ignoreQueryPrefix: true })
}