mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-02-07 15:44:23 +01:00
feat: add syncing filters state with URL
This commit is contained in:
83
package-lock.json
generated
83
package-lock.json
generated
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
50
src/features/Projects/pages/ExplorePage/helpers.ts
Normal file
50
src/features/Projects/pages/ExplorePage/helpers.ts
Normal 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]);
|
||||
|
||||
}
|
||||
30
src/utils/hooks/useQueryState.ts
Normal file
30
src/utils/hooks/useQueryState.ts
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user