SWR, improved vendors list

This commit is contained in:
2023-07-08 02:25:54 +04:00
parent 6090e97fa2
commit e487ad224c
6 changed files with 223 additions and 322 deletions

View File

@@ -12,13 +12,14 @@
"@chakra-ui/icons": "2.0.19",
"@chakra-ui/next-js": "2.1.4",
"@chakra-ui/react": "2.7.1",
"@directus/sdk": "10.3.3",
"@emotion/react": "11.11.1",
"@emotion/styled": "11.11.0",
"@tanstack/react-query": "4.29.19",
"@react-hookz/web": "23.1.0",
"clsx": "1.2.1",
"next": "13.4.8",
"react": "18.2.0",
"react-dom": "18.2.0"
"react-dom": "18.2.0",
"react-icons": "4.10.1",
"swr": "2.2.0"
}
}

View File

@@ -1,6 +1,7 @@
export const getStaticPaths = async () => {
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/pages`)
url.searchParams.append('fields[]', 'slug')
url.searchParams.append('limit', -1)
const res = await fetch(url.toString())
const { data: pages } = await res.json()

View File

@@ -1,18 +1,13 @@
import { ChakraProvider } from '@chakra-ui/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import Layout from '~/components/layout'
import theme from '~/src/theme'
const queryClient = new QueryClient()
export default function MyApp({ Component, pageProps }) {
return (
<QueryClientProvider client={queryClient}>
<ChakraProvider theme={theme}>
<Layout>
<Component {...pageProps} />
</Layout>
</ChakraProvider>
</QueryClientProvider>
)
}

View File

@@ -1,7 +1,32 @@
import { Link } from '@chakra-ui/next-js'
import { Card, CardBody, Heading, Image, LinkBox, LinkOverlay, SimpleGrid, Skeleton } from '@chakra-ui/react'
import { useQuery } from '@tanstack/react-query'
import {
Alert,
AlertIcon,
Box,
Button,
ButtonGroup,
Card,
CardBody,
Flex,
HStack,
Heading,
IconButton,
Image,
LinkBox,
LinkOverlay,
SimpleGrid,
Skeleton,
Spinner,
Table,
Tbody,
Td,
Text,
Tr,
} from '@chakra-ui/react'
import { useLocalStorageValue } from '@react-hookz/web'
import { useRouter } from 'next/router'
import { TbLayoutGrid, TbLayoutList } from 'react-icons/tb'
import useSWR from 'swr'
import Pagination from '~/components/pagination'
const PER_PAGE = 12
@@ -12,65 +37,157 @@ export default function VendorsPage() {
query: { page = 1 },
} = router
const { data, isFetching } = useQuery({
queryKey: ['vendors', page],
queryFn: async (...props2) => {
const { value: perPage, set: setPerPage } = useLocalStorageValue('perPage', {
defaultValue: PER_PAGE,
initializeWithValue: false,
})
const { value: layout, set: setLayout } = useLocalStorageValue('layout', {
defaultValue: 'GRID',
initializeWithValue: false,
})
const { data, error, isLoading, isValidating } = useSWR(['vendors', page, perPage], async () => {
// await new Promise((r) => setTimeout(r, 2000))
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/vendors`)
url.searchParams.append('fields[]', '*')
url.searchParams.append('limit', PER_PAGE)
url.searchParams.append('limit', perPage)
url.searchParams.append('page', page)
url.searchParams.append('sort', 'name')
url.searchParams.append('meta[]', 'filter_count')
const res = await fetch(url.toString())
if (!res.ok) {
throw new Error('Oops')
}
return res.json()
},
keepPreviousData: true,
cacheTime: 1000 * 60 * 30,
staleTime: 1000 * 60 * 30,
})
const { meta, data: vendors } = data || { meta: {}, data: [] }
return (
<div>
<Flex alignItems="center" justifyContent="space-between">
<Heading size="lg" my={8}>
Vendors
</Heading>
<Box>{isValidating && <Spinner />}</Box>
<ButtonGroup isAttached>
<IconButton icon={<TbLayoutGrid />} onClick={() => setLayout('GRID')} isDisabled={layout === 'GRID'} />
<IconButton icon={<TbLayoutList />} onClick={() => setLayout('LIST')} isDisabled={layout === 'LIST'} />
</ButtonGroup>
</Flex>
{error && (
<Alert status="error" marginY={3}>
<AlertIcon />
There was an error processing your request
</Alert>
)}
{layout === 'GRID' && (
<SimpleGrid columns={[1, 2, 3, 4]} spacing={3}>
{isFetching &&
new Array(PER_PAGE).fill(true).map((v, k) => (
{isLoading &&
new Array(perPage).fill(true).map((v, k) => (
<Card key={k}>
<Skeleton h="40" />
<CardBody>
<Skeleton h="6" mb="2" />
<Skeleton h="4" mb="1" />
<Skeleton h="4" />
</CardBody>
</Card>
))}
{!isFetching &&
{!isLoading &&
vendors.map((v) => (
<LinkBox as={Card} rounded={4} key={v.id}>
<Image
roundedTop={4}
objectFit="cover"
src="https://images.unsplash.com/photo-1531403009284-440f080d1e12?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=250&q=80"
src={`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/assets/${v.logo}?key=logo-card`}
alt={v.name}
width="250"
height="150"
style={{ objectFit: 'contain', background: '#fff' }}
/>
<CardBody>
<LinkOverlay as={Link} href={`/vendors/${v.slug}`} prefetch={false}>
<Text mb="2" fontWeight="900">
{v.name}
</Text>
<Text fontSize="sm">
{v?.description?.substring(0, v?.description?.substring(0, 80).lastIndexOf(' '))} &hellip;
</Text>
</LinkOverlay>
</CardBody>
</LinkBox>
))}
</SimpleGrid>
)}
<Pagination page={Number(page)} itemsPerPage={PER_PAGE} totalItems={meta.filter_count} />
{layout === 'LIST' && (
<>
{isLoading && (
<Table>
<Tbody>
{new Array(perPage).fill(true).map((v, k) => (
<Tr key={k}>
<Td w="10%">
<Skeleton h="12" w="12" />
</Td>
<Td w="30%">
<Skeleton h="6" />
</Td>
<Td w="60%">
<Skeleton h="6" />
</Td>
</Tr>
))}
</Tbody>
</Table>
)}
{!isLoading && (
<Table>
<Tbody>
{vendors.map((v) => (
<Link key={v.id} href={`/vendors/${v.slug}`} prefetch={false} display="contents">
<Tr verticalAlign="middle">
<Td>
<Image
rounded="md"
src={`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/assets/${v.logo}?key=logo-card`}
alt={v.name}
width="12"
height="12"
style={{ objectFit: 'contain', background: '#fff' }}
/>
</Td>
<Td>{v.name}</Td>
<Td>
<Text fontSize="sm">
{v?.description?.substring(0, v?.description?.substring(0, 80).lastIndexOf(' '))} &hellip;
</Text>
</Td>
</Tr>
</Link>
))}
</Tbody>
</Table>
)}
</>
)}
<Pagination page={Number(page)} itemsPerPage={perPage} totalItems={meta.filter_count} />
<HStack>
<Text>Results per page:</Text>
<ButtonGroup isAttached>
{[12, 24, 48].map((n) => (
<Button key={n} onClick={() => setPerPage(n)} isDisabled={perPage === n}>
{n}
</Button>
))}
</ButtonGroup>
</HStack>
</div>
)
}

137
frontend/pnpm-lock.yaml generated
View File

@@ -14,18 +14,15 @@ dependencies:
'@chakra-ui/react':
specifier: 2.7.1
version: 2.7.1(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(framer-motion@6.5.1)(react-dom@18.2.0)(react@18.2.0)
'@directus/sdk':
specifier: 10.3.3
version: 10.3.3
'@emotion/react':
specifier: 11.11.1
version: 11.11.1(react@18.2.0)
'@emotion/styled':
specifier: 11.11.0
version: 11.11.0(@emotion/react@11.11.1)(react@18.2.0)
'@tanstack/react-query':
specifier: 4.29.19
version: 4.29.19(react-dom@18.2.0)(react@18.2.0)
'@react-hookz/web':
specifier: 23.1.0
version: 23.1.0(react-dom@18.2.0)(react@18.2.0)
clsx:
specifier: 1.2.1
version: 1.2.1
@@ -38,6 +35,12 @@ dependencies:
react-dom:
specifier: 18.2.0
version: 18.2.0(react@18.2.0)
react-icons:
specifier: 4.10.1
version: 4.10.1(react@18.2.0)
swr:
specifier: 2.2.0
version: 2.2.0(react@18.2.0)
packages:
@@ -1203,14 +1206,6 @@ packages:
react: 18.2.0
dev: false
/@directus/sdk@10.3.3:
resolution: {integrity: sha512-58gw+QjkuIr0lJFRx5HwSp1ewAf7rjfV++eJqAmC13p7vif9wfJEcWcJwqXstYdvtJVUg+nB4O/CE0OBEtp5HQ==}
dependencies:
axios: 0.27.2
transitivePeerDependencies:
- debug
dev: false
/@emotion/babel-plugin@11.11.0:
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
dependencies:
@@ -1474,34 +1469,31 @@ packages:
resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
dev: false
/@react-hookz/deep-equal@1.0.4:
resolution: {integrity: sha512-N56fTrAPUDz/R423pag+n6TXWbvlBZDtTehaGFjK0InmN+V2OFWLE/WmORhmn6Ce7dlwH5+tQN1LJFw3ngTJVg==}
dev: false
/@react-hookz/web@23.1.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-fvbURdsa1ukttbLR1ASE/XmqXP09vZ1PiCYppYeR1sNMzCrdkG0iBnjxniFSVjJ8gIw2fRs6nqMTbeBz2uAkuA==}
peerDependencies:
js-cookie: ^3.0.5
react: ^16.8 || ^17 || ^18
react-dom: ^16.8 || ^17 || ^18
peerDependenciesMeta:
js-cookie:
optional: true
dependencies:
'@react-hookz/deep-equal': 1.0.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@swc/helpers@0.5.1:
resolution: {integrity: sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==}
dependencies:
tslib: 2.6.0
dev: false
/@tanstack/query-core@4.29.19:
resolution: {integrity: sha512-uPe1DukeIpIHpQi6UzIgBcXsjjsDaLnc7hF+zLBKnaUlh7jFE/A+P8t4cU4VzKPMFB/C970n/9SxtpO5hmIRgw==}
dev: false
/@tanstack/react-query@4.29.19(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-XiTIOHHQ5Cw1WUlHaD4fmVUMhoWjuNJlAeJGq7eM4BraI5z7y8WkZO+NR8PSuRnQGblpuVdjClQbDFtwxTtTUw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
react-native: '*'
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
dependencies:
'@tanstack/query-core': 4.29.19
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/@types/lodash.mergewith@4.6.7:
resolution: {integrity: sha512-3m+lkO5CLRRYU0fhGRp7zbsGi6+BZj0uTVSwvcKU+nSlhjA9/QRNfuSGnD2mX6hQA7ZbmcCkzk5h4ZYGOtk14A==}
dependencies:
@@ -1538,19 +1530,6 @@ packages:
tslib: 2.5.3
dev: false
/asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
dev: false
/axios@0.27.2:
resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==}
dependencies:
follow-redirects: 1.15.2
form-data: 4.0.0
transitivePeerDependencies:
- debug
dev: false
/babel-plugin-macros@3.1.0:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
@@ -1608,13 +1587,6 @@ packages:
resolution: {integrity: sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==}
dev: false
/combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
dependencies:
delayed-stream: 1.0.0
dev: false
/compute-scroll-into-view@1.0.20:
resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
dev: false
@@ -1650,11 +1622,6 @@ packages:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
dev: false
/delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
dev: false
/detect-node-es@1.1.0:
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
dev: false
@@ -1686,25 +1653,6 @@ packages:
tslib: 2.5.3
dev: false
/follow-redirects@1.15.2:
resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
dev: false
/form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
dev: false
/framer-motion@6.5.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw==}
peerDependencies:
@@ -1821,18 +1769,6 @@ packages:
js-tokens: 4.0.0
dev: false
/mime-db@1.52.0:
resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
engines: {node: '>= 0.6'}
dev: false
/mime-types@2.1.35:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'}
dependencies:
mime-db: 1.52.0
dev: false
/nanoid@3.3.6:
resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1984,6 +1920,14 @@ packages:
use-sidecar: 1.1.2(react@18.2.0)
dev: false
/react-icons@4.10.1(react@18.2.0):
resolution: {integrity: sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==}
peerDependencies:
react: '*'
dependencies:
react: 18.2.0
dev: false
/react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
dev: false
@@ -2123,6 +2067,15 @@ packages:
engines: {node: '>= 0.4'}
dev: false
/swr@2.2.0(react@18.2.0):
resolution: {integrity: sha512-AjqHOv2lAhkuUdIiBu9xbuettzAzWXmCEcLONNKJRba87WAefz8Ca9d6ds/SzrPc235n1IxWYdhJ2zF3MNUaoQ==}
peerDependencies:
react: ^16.11.0 || ^17.0.0 || ^18.0.0
dependencies:
react: 18.2.0
use-sync-external-store: 1.2.0(react@18.2.0)
dev: false
/tiny-invariant@1.3.1:
resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==}
dev: false

View File

@@ -1,166 +0,0 @@
import directus from '~/lib/directus'
import all from '~/public/data.json'
function sluggify(str = '') {
return (
str
.toLocaleLowerCase()
.normalize('NFKC')
.trim()
//
.split('.')
.join(' ')
.split('(')
.join('')
.split(')')
.join('')
.split('|')
.join(' ')
.trim()
.split(',')
.join('')
.split(' ')
.join('-')
.split('-/-')
.join('-')
.split(' &amp')
.join('')
.split('&')
.join('and')
.split('/')
.join('-')
.replace(/\-\-+/, '-')
.trim()
)
}
function sanitize(str = '') {
return (
str
//
.split(' &amp')
.join('')
.split(' / ')
.join('/')
.split('/')
.join(' / ')
.trim()
)
}
async function importCategories() {
all.results.map((one) => {
one.raw.marketplaceexhibitorcategory?.map(async (cat) => {
const name = sanitize(cat)
const slug = sluggify(name)
try {
await directus.items('categories').createOne({
slug,
name,
})
} catch (error) {}
})
})
}
async function importSubcategories() {
all.results.map((one) => {
one.raw.marketplaceexhibitorsubcategory?.map(async (cat) => {
const name = sanitize(cat)
const slug = sluggify(name)
try {
await directus.items('categories').createOne({
slug,
name,
})
} catch (error) {}
})
})
}
async function importCategoryStructure() {
const { data: categories } = await directus.items('categories').readByQuery({ limit: -1 })
all.results.map((one) => {
one.raw.marketplaceexhibitorcategorysubcategory?.map(async (cat) => {
const [parentTitle, catTitle] = cat.split('|')
const catSlug = sluggify(catTitle)
const parentSlug = sluggify(parentTitle)
if (parentSlug) {
try {
const category = categories.find((c) => catSlug === c.slug)
const parent = categories.find((c) => parentSlug === c.slug)
await directus.items('categories').updateOne(category.id, {
parent_id: parent.id,
})
} catch (error) {}
}
})
})
}
async function importVendors() {
const { data: categories } = await directus.items('categories').readByQuery({ limit: -1 })
return Promise.all(
all.results.map(async (one) => {
const name = sanitize(one.raw.name)
let slug = sluggify(name)
const {
data: [existingVendor],
} = await directus.items('vendors').readByQuery({
fields: ['slug'],
limit: -1,
filter: {
slug: {
_eq: slug,
},
},
})
let i = 1
while (existingVendor?.slug === slug) {
slug = sluggify(name) + '-' + i++
}
const categoryIds = new Set(
one.raw.marketplaceexhibitorcategory
?.map((t) => sluggify(sanitize(t)))
.map((s) => categories.filter((t) => !t.parent_id).find((t) => s === t.slug)?.id)
.filter((s) => !!s)
)
const vendor = {
slug,
name,
status: 'published',
description: one.raw?.description,
address_line_1: one.raw?.addressline1,
address_line_2: one.raw?.addressline2,
city: one.raw?.city,
state: one.raw?.state,
country: one.raw?.country,
website: one.raw?.website,
}
try {
const res = await directus.items('vendors').createOne(vendor)
categoryIds.forEach(async (cid) => {
await directus.items('vendors_categories').createOne({ vendors_id: res.id, categories_id: cid })
})
} catch (error) {}
})
)
}
export default function Home() {
// importCategories()
// importSubcategories()
// importCategoryStructure()
// importVendors()
return (
<main>
<h1>Import {new Date().toISOString()}</h1>
{/* <pre>{JSON.stringify(res, null, 2)} </pre> */}
</main>
)
}