Improve vendors list
This commit is contained in:
138
frontend/pages/vendors/index.js
vendored
138
frontend/pages/vendors/index.js
vendored
@@ -1,7 +1,9 @@
|
|||||||
import { Link } from '@chakra-ui/next-js'
|
import { Link } from '@chakra-ui/next-js'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
AlertDescription,
|
||||||
AlertIcon,
|
AlertIcon,
|
||||||
|
AlertTitle,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
@@ -12,8 +14,17 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
IconButton,
|
IconButton,
|
||||||
Image,
|
Image,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
InputRightElement,
|
||||||
LinkBox,
|
LinkBox,
|
||||||
LinkOverlay,
|
LinkOverlay,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuDivider,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Spinner,
|
Spinner,
|
||||||
@@ -23,18 +34,42 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Tr,
|
Tr,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import { useLocalStorageValue } from '@react-hookz/web'
|
import { useDebouncedCallback, useLocalStorageValue } from '@react-hookz/web'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
import { TbLayoutGrid, TbLayoutList } from 'react-icons/tb'
|
import { TbCheck, TbFilter, TbFilterEdit, TbLayoutGrid, TbLayoutList, TbMoodSad, TbSearch, TbX } from 'react-icons/tb'
|
||||||
import useSWR from 'swr'
|
import useSWR from 'swr'
|
||||||
import Pagination from '~/components/pagination'
|
import Pagination from '~/components/pagination'
|
||||||
|
|
||||||
const PER_PAGE = 12
|
const PER_PAGE = 12
|
||||||
|
|
||||||
export default function VendorsPage() {
|
export const getStaticProps = async () => {
|
||||||
|
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/categories`)
|
||||||
|
url.searchParams.append('fields[]', 'slug')
|
||||||
|
url.searchParams.append('fields[]', 'name')
|
||||||
|
url.searchParams.append('fields[]', 'subcategories.slug')
|
||||||
|
url.searchParams.append('fields[]', 'subcategories.name')
|
||||||
|
url.searchParams.append('sort', 'name')
|
||||||
|
url.searchParams.append('limit', -1)
|
||||||
|
url.searchParams.append(
|
||||||
|
'filter',
|
||||||
|
JSON.stringify({
|
||||||
|
parent_id: {
|
||||||
|
_null: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await fetch(url.toString())
|
||||||
|
|
||||||
|
const { data: categories } = await res.json()
|
||||||
|
|
||||||
|
return { props: { categories } }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function VendorsPage({ categories }) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const {
|
const {
|
||||||
query: { page = 1 },
|
query: { page = 1, category = '', q: search = '' },
|
||||||
} = router
|
} = router
|
||||||
|
|
||||||
const { value: perPage, set: setPerPage } = useLocalStorageValue('perPage', {
|
const { value: perPage, set: setPerPage } = useLocalStorageValue('perPage', {
|
||||||
@@ -47,14 +82,43 @@ export default function VendorsPage() {
|
|||||||
initializeWithValue: false,
|
initializeWithValue: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data, error, isLoading, isValidating } = useSWR(['vendors', page, perPage], async () => {
|
const setSearch = useDebouncedCallback(
|
||||||
// await new Promise((r) => setTimeout(r, 2000))
|
(q) => {
|
||||||
|
router.replace({ query: { ...router.query, q } })
|
||||||
|
},
|
||||||
|
[router],
|
||||||
|
500
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data, error, isLoading, isValidating } = useSWR(['vendors', page, perPage, search, category], async () => {
|
||||||
|
if (!perPage) return
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 2000))
|
||||||
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/vendors`)
|
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/vendors`)
|
||||||
url.searchParams.append('fields[]', '*')
|
url.searchParams.append('fields[]', '*')
|
||||||
url.searchParams.append('limit', perPage)
|
url.searchParams.append('limit', perPage)
|
||||||
url.searchParams.append('page', page)
|
url.searchParams.append('page', page)
|
||||||
url.searchParams.append('sort', 'name')
|
url.searchParams.append('sort', 'name')
|
||||||
url.searchParams.append('meta[]', 'filter_count')
|
url.searchParams.append('meta[]', 'filter_count')
|
||||||
|
|
||||||
|
if (search !== '' || category !== '') {
|
||||||
|
url.searchParams.append(
|
||||||
|
'filter',
|
||||||
|
JSON.stringify({
|
||||||
|
_and: [
|
||||||
|
category !== '' ? { categories: { categories_id: { slug: { _eq: category } } } } : {},
|
||||||
|
search !== ''
|
||||||
|
? {
|
||||||
|
_or: ['name', 'description', 'long_description', 'city'].map((s) => ({
|
||||||
|
[s]: { _contains: search },
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const res = await fetch(url.toString())
|
const res = await fetch(url.toString())
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
throw new Error('Oops')
|
throw new Error('Oops')
|
||||||
@@ -64,17 +128,54 @@ export default function VendorsPage() {
|
|||||||
|
|
||||||
const { meta, data: vendors } = data || { meta: {}, data: [] }
|
const { meta, data: vendors } = data || { meta: {}, data: [] }
|
||||||
|
|
||||||
|
const lastPage = Math.max(1, Math.ceil(meta.filter_count / perPage))
|
||||||
|
if (!isNaN(lastPage) && page > lastPage) {
|
||||||
|
router.replace({ query: { ...router.query, page: lastPage } })
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<Flex alignItems="center" justifyContent="space-between">
|
<Flex alignItems="center" justifyContent="space-between" direction={['column', 'row']} mb="5" gap="5">
|
||||||
<Heading size="lg" my={8}>
|
<Heading size="lg" my={8}>
|
||||||
Vendors
|
Vendors
|
||||||
</Heading>
|
</Heading>
|
||||||
<Box>{isValidating && <Spinner />}</Box>
|
<Box>{isValidating && <Spinner />}</Box>
|
||||||
|
<HStack gap="5">
|
||||||
|
<InputGroup>
|
||||||
|
<InputLeftElement>
|
||||||
|
<TbSearch />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input onChange={(ev) => setSearch(ev.target.value.toLowerCase())} />
|
||||||
|
<InputRightElement
|
||||||
|
onClick={(ev) => {
|
||||||
|
ev.currentTarget.parentNode.childNodes[1].value = ''
|
||||||
|
setSearch('')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{search !== '' && <TbX />}
|
||||||
|
</InputRightElement>
|
||||||
|
</InputGroup>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={IconButton} icon={category === '' ? <TbFilter /> : <TbFilterEdit />} />
|
||||||
|
<MenuList>
|
||||||
|
{categories.map((cat) => (
|
||||||
|
<Link key={cat.slug} href={{ query: { ...router.query, category: cat.slug } }}>
|
||||||
|
<MenuItem icon={category === cat.slug && <TbCheck />} isDisabled={category === cat.slug}>
|
||||||
|
{cat.name}
|
||||||
|
</MenuItem>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<MenuDivider />
|
||||||
|
<Link href={{ query: { ...router.query, category: '' } }}>
|
||||||
|
<MenuItem>Clear</MenuItem>
|
||||||
|
</Link>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
<IconButton icon={<TbLayoutGrid />} onClick={() => setLayout('GRID')} isDisabled={layout === 'GRID'} />
|
<IconButton icon={<TbLayoutGrid />} onClick={() => setLayout('GRID')} isDisabled={layout === 'GRID'} />
|
||||||
<IconButton icon={<TbLayoutList />} onClick={() => setLayout('LIST')} isDisabled={layout === 'LIST'} />
|
<IconButton icon={<TbLayoutList />} onClick={() => setLayout('LIST')} isDisabled={layout === 'LIST'} />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
</HStack>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
@@ -176,9 +277,22 @@ export default function VendorsPage() {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Pagination page={Number(page)} itemsPerPage={perPage} totalItems={meta.filter_count} />
|
{meta.filter_count === 0 && (
|
||||||
|
<Box>
|
||||||
|
<Alert status="info" flexDirection="column" justifyContent="center" rounded="md" paddingY="10">
|
||||||
|
<TbMoodSad style={{ width: '2rem', height: '2rem' }} />
|
||||||
|
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||||
|
No results found.
|
||||||
|
</AlertTitle>
|
||||||
|
<AlertDescription maxWidth="sm">Try refining your search term and filters …</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<HStack>
|
{meta.filter_count > perPage && (
|
||||||
|
<>
|
||||||
|
<Pagination page={Number(page)} itemsPerPage={perPage} totalItems={meta.filter_count} />
|
||||||
|
<HStack mt="5" justifyContent="center">
|
||||||
<Text>Results per page:</Text>
|
<Text>Results per page:</Text>
|
||||||
<ButtonGroup isAttached>
|
<ButtonGroup isAttached>
|
||||||
{[12, 24, 48].map((n) => (
|
{[12, 24, 48].map((n) => (
|
||||||
@@ -188,6 +302,8 @@ export default function VendorsPage() {
|
|||||||
))}
|
))}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user