Rename frontend to legacy-frontend
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
|
||||
# The URL where your API can be reached on the web.
|
||||
NEXT_PUBLIC_DIRECTUS_API_URL="https://admin.pca-pijac.dev.civokram.com"
|
||||
NEXT_PUBLIC_GLOBALS_ID="4f8d9e66-ec95-4bdd-a5e7-34df8ea68a45"
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
# The URL where your API can be reached on the web.
|
||||
NEXT_PUBLIC_DIRECTUS_API_URL=https://admin.pca-pijac.dev.civokram.com
|
||||
NEXT_PUBLIC_GLOBALS_ID=4f8d9e66-ec95-4bdd-a5e7-34df8ea68a45
|
||||
35
frontend/.gitignore
vendored
35
frontend/.gitignore
vendored
@@ -1,35 +0,0 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -1,40 +0,0 @@
|
||||
import { Link } from '@chakra-ui/next-js'
|
||||
import { Box, Flex, Stack, Text, useColorModeValue } from '@chakra-ui/react'
|
||||
|
||||
export default function Footer({ menu, globals }) {
|
||||
return (
|
||||
<Box bg={useColorModeValue('gray.100', 'gray.900')} px={4} mt="8">
|
||||
<Flex h={16} alignItems={'center'} justifyContent="space-between">
|
||||
<FooterMenu items={menu.items} />
|
||||
<Text>{globals.copyright}</Text>
|
||||
</Flex>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const FooterMenu = ({ items }) => {
|
||||
const linkColor = useColorModeValue('gray.600', 'gray.200')
|
||||
const linkHoverColor = useColorModeValue('gray.800', 'white')
|
||||
|
||||
return (
|
||||
<Stack direction={'row'} spacing={4} pl="4">
|
||||
{items.map((navItem) => (
|
||||
<Box key={navItem.label}>
|
||||
<Link
|
||||
p={2}
|
||||
href={navItem.url ?? '#'}
|
||||
fontSize={'sm'}
|
||||
fontWeight={500}
|
||||
color={linkColor}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
color: linkHoverColor,
|
||||
}}
|
||||
>
|
||||
{navItem.label}
|
||||
</Link>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { MoonIcon, SunIcon } from '@chakra-ui/icons'
|
||||
import { Box, Button, Flex, Heading, Spacer, Stack, useColorMode, useColorModeValue } from '@chakra-ui/react'
|
||||
import { Link } from '@chakra-ui/next-js'
|
||||
|
||||
export default function Header({ menu, globals }) {
|
||||
const { colorMode, toggleColorMode } = useColorMode()
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box bg={useColorModeValue('gray.100', 'gray.900')} px={4}>
|
||||
<Flex h={16} alignItems={'center'}>
|
||||
<Heading size="md">{globals.site_name}</Heading>
|
||||
|
||||
<MainMenu items={menu.items} />
|
||||
<Spacer />
|
||||
|
||||
<Flex alignItems={'center'}>
|
||||
<Stack direction={'row'} spacing={7}>
|
||||
<Button onClick={toggleColorMode}>{colorMode === 'light' ? <MoonIcon /> : <SunIcon />}</Button>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const MainMenu = ({ items }) => {
|
||||
const linkColor = useColorModeValue('gray.600', 'gray.200')
|
||||
const linkHoverColor = useColorModeValue('gray.800', 'white')
|
||||
|
||||
return (
|
||||
<Stack direction={'row'} spacing={4} pl="4">
|
||||
{items.map((navItem) => (
|
||||
<Box key={navItem.label}>
|
||||
<Link
|
||||
p={2}
|
||||
href={navItem.url ?? '#'}
|
||||
fontSize={'sm'}
|
||||
fontWeight={500}
|
||||
color={linkColor}
|
||||
_hover={{
|
||||
textDecoration: 'none',
|
||||
color: linkHoverColor,
|
||||
}}
|
||||
>
|
||||
{navItem.label}
|
||||
</Link>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Container, Box } from '@chakra-ui/react'
|
||||
import Footer from '~/components/footer'
|
||||
import Header from '~/components/header'
|
||||
|
||||
export default function Layout({ globals, menus, children }) {
|
||||
return (
|
||||
<Container maxW={'8xl'}>
|
||||
<Header menu={menus.find((m) => m.id === 'MAIN_MENU')} globals={globals} />
|
||||
<Box marginY="6">{children}</Box>
|
||||
<Footer menu={menus.find((m) => m.id === 'FOOTER_MENU')} globals={globals} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Flex, Link, Select, Text } from '@chakra-ui/react'
|
||||
import clsx from 'clsx'
|
||||
import NextLink from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Fragment } from 'react'
|
||||
|
||||
export default function Pagination({ page = 1, itemsPerPage = 12, totalItems = 0, limit = 3 }) {
|
||||
const router = useRouter()
|
||||
|
||||
const totalPages = Math.ceil(totalItems / itemsPerPage)
|
||||
|
||||
if (totalPages === 1) return <></>
|
||||
|
||||
const links = new Array(totalPages)
|
||||
.fill(true)
|
||||
.map((_v, i) => (i < limit || i >= totalPages - limit || (i >= page - limit && i < page + limit - 1)) && i + 1)
|
||||
.filter((i) => i !== false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Flex justifyContent="space-between" direction={['column', 'row']} mt={6} mb={6} gap={3}>
|
||||
<Link
|
||||
//
|
||||
as={NextLink}
|
||||
href={{ query: { ...router.query, page: Math.max(1, page - 1) } }}
|
||||
variant="pagination"
|
||||
>
|
||||
← Previous
|
||||
</Link>
|
||||
<Flex gap={1} display={['none', 'none', 'flex']}>
|
||||
{links.map((v, i) => (
|
||||
<Fragment key={i}>
|
||||
{v > 1 && v - 1 !== links[i - 1] && (
|
||||
<Text marginX={3} lineHeight={10}>
|
||||
…
|
||||
</Text>
|
||||
)}
|
||||
<Link
|
||||
//
|
||||
key={v}
|
||||
as={NextLink}
|
||||
prefetch={false}
|
||||
href={{ query: { ...router.query, page: v } }}
|
||||
className={clsx({
|
||||
current: v === page,
|
||||
})}
|
||||
variant="pagination"
|
||||
>
|
||||
{v}
|
||||
</Link>
|
||||
</Fragment>
|
||||
))}
|
||||
</Flex>
|
||||
<Select
|
||||
value={page}
|
||||
onChange={(v) => router.push({ query: { ...router.query, page: v.target.value } })}
|
||||
display={['flex', 'flex', 'none']}
|
||||
textAlign="center"
|
||||
>
|
||||
{new Array(totalPages).fill(true).map((v, i) => (
|
||||
<option value={i + 1} key={i}>
|
||||
{i + 1}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
<Link
|
||||
//
|
||||
as={NextLink}
|
||||
href={{ query: { ...router.query, page: Math.min(totalPages, page + 1) } }}
|
||||
variant="pagination"
|
||||
textAlign="right"
|
||||
>
|
||||
Next →
|
||||
</Link>
|
||||
</Flex>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"~/*": ["./*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {}
|
||||
|
||||
module.exports = nextConfig
|
||||
@@ -1,38 +0,0 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22",
|
||||
"pnpm": ">=10"
|
||||
},
|
||||
"packageManager": "pnpm@10.33.2",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/icons": "2.0.19",
|
||||
"@chakra-ui/next-js": "2.1.4",
|
||||
"@chakra-ui/react": "2.7.1",
|
||||
"@emotion/react": "11.11.1",
|
||||
"@emotion/styled": "11.11.0",
|
||||
"@react-hookz/web": "23.1.0",
|
||||
"clsx": "1.2.1",
|
||||
"iso-3166": "4.2.0",
|
||||
"next": "13.4.9",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-icons": "4.10.1",
|
||||
"sass": "1.63.6",
|
||||
"swr": "2.2.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"sharp",
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from '@chakra-ui/react'
|
||||
import { TbMoodSad } from 'react-icons/tb'
|
||||
|
||||
export default function Custom404() {
|
||||
return (
|
||||
<Alert status="info" flexDirection="column" justifyContent="center" rounded="md" paddingY="10">
|
||||
<TbMoodSad style={{ width: '2rem', height: '2rem' }} />
|
||||
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||
404: Page Not Found
|
||||
</AlertTitle>
|
||||
<AlertDescription maxWidth="sm">Try going back or something …</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Alert, AlertDescription, AlertTitle } from '@chakra-ui/react'
|
||||
import { TbMoodSad } from 'react-icons/tb'
|
||||
|
||||
export default function Custom500() {
|
||||
return (
|
||||
<Alert status="info" flexDirection="column" justifyContent="center" rounded="md" paddingY="10">
|
||||
<TbMoodSad style={{ width: '2rem', height: '2rem' }} />
|
||||
<AlertTitle mt={4} mb={1} fontSize="lg">
|
||||
500: Server-side error occurred
|
||||
</AlertTitle>
|
||||
<AlertDescription maxWidth="sm">Something happened that was totally unexpected …</AlertDescription>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Box, Heading } from '@chakra-ui/react'
|
||||
import Head from 'next/head'
|
||||
|
||||
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()
|
||||
|
||||
return {
|
||||
paths: pages.map((p) => ({ params: { slug: p.slug } })),
|
||||
fallback: false, // false or "blocking"
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticProps = async ({ params: { slug } }) => {
|
||||
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/pages`)
|
||||
url.searchParams.append('fields[]', 'title')
|
||||
url.searchParams.append('fields[]', 'content')
|
||||
url.searchParams.append('limit', 1)
|
||||
url.searchParams.append('filter', JSON.stringify({ slug: { _eq: slug } }))
|
||||
const res = await fetch(url.toString())
|
||||
|
||||
const {
|
||||
data: [page],
|
||||
} = await res.json()
|
||||
|
||||
return { props: { page } }
|
||||
}
|
||||
|
||||
export default function Page({ globals, page }) {
|
||||
return (
|
||||
<Box className="page">
|
||||
<Head>{page.title && <title>{[page.title, globals.meta_title].join(' — ')}</title>}</Head>
|
||||
<Heading>{page.title}</Heading>
|
||||
<div dangerouslySetInnerHTML={{ __html: page.content }}></div>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { ChakraProvider } from '@chakra-ui/react'
|
||||
import App from 'next/app'
|
||||
import Head from 'next/head'
|
||||
import Layout from '~/components/layout'
|
||||
import '~/src/style.scss'
|
||||
import theme from '~/src/theme'
|
||||
|
||||
export default function MyApp({ Component, pageProps, globals, menus }) {
|
||||
return (
|
||||
<ChakraProvider theme={theme}>
|
||||
<Head>
|
||||
<title>{globals.meta_title}</title>
|
||||
<meta name="description" content={globals.meta_description} />
|
||||
</Head>
|
||||
<Layout globals={globals} menus={menus}>
|
||||
<Component {...pageProps} globals={globals} />
|
||||
</Layout>
|
||||
</ChakraProvider>
|
||||
)
|
||||
}
|
||||
|
||||
MyApp.getInitialProps = async (context) => {
|
||||
const pageProps = await App.getInitialProps(context)
|
||||
|
||||
let url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/globals`)
|
||||
url.searchParams.append('fields[]', '*')
|
||||
url.searchParams.append('limit', 1)
|
||||
const resG = await fetch(url.toString())
|
||||
let { data: globals } = await resG.json()
|
||||
|
||||
url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/menus`)
|
||||
url.searchParams.append('fields[]', '*')
|
||||
url.searchParams.append('fields[]', 'menus_menu_items.sort')
|
||||
url.searchParams.append('fields[]', 'menus_menu_items.menu_items_id.label')
|
||||
url.searchParams.append('fields[]', 'menus_menu_items.menu_items_id.url')
|
||||
url.searchParams.append('fields[]', 'menus_menu_items.menu_items_id.sort')
|
||||
url.searchParams.append('limit', -1)
|
||||
const resM = await fetch(url.toString())
|
||||
const { data: menus } = await resM.json()
|
||||
|
||||
return {
|
||||
...pageProps,
|
||||
globals,
|
||||
menus: menus.map((m) => ({
|
||||
id: m.id,
|
||||
items: m.menus_menu_items.sort((a, b) => a.sort - b.sort).map((mm) => mm.menu_items_id),
|
||||
})),
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
export default function Error(props) {
|
||||
return (
|
||||
<>
|
||||
<h2>Oops, there is an error!</h2>
|
||||
<pre>{JSON.stringify(props, null, 2)}</pre>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import Page, { getStaticProps as gsp } from '~/pages/[slug]'
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
return gsp({ params: { slug: 'home' } })
|
||||
}
|
||||
|
||||
export default function Home(props) {
|
||||
return <Page {...props} />
|
||||
}
|
||||
164
frontend/pages/vendors/[slug]/index.js
vendored
164
frontend/pages/vendors/[slug]/index.js
vendored
@@ -1,164 +0,0 @@
|
||||
import { Link } from '@chakra-ui/next-js'
|
||||
import { Box, Button, Flex, Heading, Image, List, ListIcon, ListItem, SimpleGrid, Text } from '@chakra-ui/react'
|
||||
import { iso31661 } from 'iso-3166'
|
||||
import Head from 'next/head'
|
||||
import { FaFacebookSquare, FaLinkedin, FaTwitterSquare } from 'react-icons/fa'
|
||||
import { TbWorldWww } from 'react-icons/tb'
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/vendors`)
|
||||
url.searchParams.append('fields[]', 'slug')
|
||||
url.searchParams.append('limit', -1)
|
||||
const res = await fetch(url.toString())
|
||||
|
||||
const { data: vendors } = await res.json()
|
||||
|
||||
return {
|
||||
paths: vendors.map((v) => ({ params: { slug: v.slug } })),
|
||||
fallback: false, // false or "blocking"
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticProps = async ({ params: { slug } }) => {
|
||||
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/vendors`)
|
||||
url.searchParams.append('fields[]', '*')
|
||||
url.searchParams.append('fields[]', 'categories.categories_id.slug')
|
||||
url.searchParams.append('fields[]', 'categories.categories_id.name')
|
||||
url.searchParams.append('fields[]', 'categories.categories_id.parent_id')
|
||||
url.searchParams.append('fields[]', 'categories.categories_id.subcategories.slug')
|
||||
url.searchParams.append('fields[]', 'categories.categories_id.subcategories.name')
|
||||
url.searchParams.append('limit', 1)
|
||||
url.searchParams.append('filter', JSON.stringify({ slug: { _eq: slug } }))
|
||||
|
||||
const res = await fetch(url.toString())
|
||||
|
||||
const {
|
||||
data: [vendor],
|
||||
} = await res.json()
|
||||
|
||||
return {
|
||||
props: { vendor: { ...vendor, country: iso31661.find((iso) => iso.alpha3 === vendor.country)?.name || '' } },
|
||||
}
|
||||
}
|
||||
|
||||
export default function VendorPage({ globals, vendor }) {
|
||||
return (
|
||||
<Box className="vendor">
|
||||
<Head>
|
||||
<title>{[vendor.name, globals.meta_title].join(' — ')}</title>
|
||||
</Head>
|
||||
<Flex justifyContent="space-between" direction={['column', 'column', 'row']}>
|
||||
<Heading marginY="6">{vendor.name}</Heading>
|
||||
<Image
|
||||
rounded={4}
|
||||
objectFit="contain"
|
||||
src={`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/assets/${vendor.logo}?key=logo-page`}
|
||||
alt={vendor.name}
|
||||
width="350px"
|
||||
height="150px"
|
||||
bg="#fff"
|
||||
/>
|
||||
</Flex>
|
||||
<Box dangerouslySetInnerHTML={{ __html: vendor.long_description }}></Box>
|
||||
|
||||
<SimpleGrid columns={[1, 2, 3]} spacing={10}>
|
||||
<Box>
|
||||
<Heading size="md" mt="6" mb="1">
|
||||
Address
|
||||
</Heading>
|
||||
<Text>
|
||||
{vendor.address_line_1 && (
|
||||
<>
|
||||
{vendor.address_line_1}
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{vendor.address_line_2 && (
|
||||
<>
|
||||
{vendor.address_line_2}
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{vendor.city && (
|
||||
<>
|
||||
{vendor.city}
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{vendor.state && (
|
||||
<>
|
||||
{vendor.state}
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{vendor.country && <>{vendor.country}</>}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
{(vendor.website || vendor.facebook || vendor.linkedin || vendor.twitter) && (
|
||||
<>
|
||||
<Heading size="md" mt="6" mb="1">
|
||||
Social
|
||||
</Heading>
|
||||
<List>
|
||||
{vendor.website && (
|
||||
<ListItem>
|
||||
<ListIcon as={TbWorldWww} />
|
||||
{vendor.website}
|
||||
</ListItem>
|
||||
)}
|
||||
{vendor.linkedin && (
|
||||
<ListItem>
|
||||
<ListIcon as={FaLinkedin} />
|
||||
{vendor.linkedin}
|
||||
</ListItem>
|
||||
)}
|
||||
{vendor.twitter && (
|
||||
<ListItem>
|
||||
<ListIcon as={FaTwitterSquare} />
|
||||
{vendor.twitter}
|
||||
</ListItem>
|
||||
)}
|
||||
{vendor.facebook && (
|
||||
<ListItem>
|
||||
<ListIcon as={FaFacebookSquare} />
|
||||
{vendor.facebook}
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
{vendor.categories.length > 0 && (
|
||||
<>
|
||||
<Heading size="md" mt="6" mb="1">
|
||||
Categories
|
||||
</Heading>
|
||||
<List spacing="3">
|
||||
{vendor.categories.map((cat) => (
|
||||
<ListItem key={cat.categories_id.slug}>
|
||||
<Button
|
||||
as={Link}
|
||||
fontSize="11"
|
||||
href={{
|
||||
pathname: '/vendors',
|
||||
query: {
|
||||
category: cat.categories_id.slug,
|
||||
},
|
||||
}}
|
||||
paddingX="0.5rem"
|
||||
height="1.5rem"
|
||||
>
|
||||
{cat.categories_id.name}
|
||||
</Button>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
310
frontend/pages/vendors/index.js
vendored
310
frontend/pages/vendors/index.js
vendored
@@ -1,310 +0,0 @@
|
||||
import { Link } from '@chakra-ui/next-js'
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
AlertIcon,
|
||||
AlertTitle,
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardBody,
|
||||
Flex,
|
||||
HStack,
|
||||
Heading,
|
||||
IconButton,
|
||||
Image,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputLeftElement,
|
||||
InputRightElement,
|
||||
LinkBox,
|
||||
LinkOverlay,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuDivider,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
Spinner,
|
||||
Table,
|
||||
Tbody,
|
||||
Td,
|
||||
Text,
|
||||
Tr,
|
||||
} from '@chakra-ui/react'
|
||||
import { useDebouncedCallback, useLocalStorageValue } from '@react-hookz/web'
|
||||
import Head from 'next/head'
|
||||
import { useRouter } from 'next/router'
|
||||
import { TbCheck, TbFilter, TbFilterEdit, TbLayoutGrid, TbLayoutList, TbMoodSad, TbSearch, TbX } from 'react-icons/tb'
|
||||
import useSWR from 'swr'
|
||||
import Pagination from '~/components/pagination'
|
||||
|
||||
const PER_PAGE = 12
|
||||
|
||||
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({ globals, categories }) {
|
||||
const router = useRouter()
|
||||
const {
|
||||
query: { page = 1, category = '', q: search = '' },
|
||||
} = router
|
||||
|
||||
const { value: perPage, set: setPerPage } = useLocalStorageValue('perPage', {
|
||||
defaultValue: PER_PAGE,
|
||||
initializeWithValue: false,
|
||||
})
|
||||
|
||||
const { value: layout, set: setLayout } = useLocalStorageValue('layout', {
|
||||
defaultValue: 'GRID',
|
||||
initializeWithValue: false,
|
||||
})
|
||||
|
||||
const setSearch = useDebouncedCallback(
|
||||
(q) => {
|
||||
router.replace({ query: { ...router.query, q } })
|
||||
},
|
||||
[router],
|
||||
500
|
||||
)
|
||||
|
||||
const { data, error, isLoading, isValidating } = useSWR(['vendors', page, perPage, search, category], async () => {
|
||||
if (!perPage) return
|
||||
|
||||
const url = new URL(`${process.env.NEXT_PUBLIC_DIRECTUS_API_URL}/items/vendors`)
|
||||
url.searchParams.append('fields[]', '*')
|
||||
url.searchParams.append('limit', perPage)
|
||||
url.searchParams.append('page', page)
|
||||
url.searchParams.append('sort', 'name')
|
||||
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())
|
||||
if (!res.ok) {
|
||||
throw new Error('Oops')
|
||||
}
|
||||
return res.json()
|
||||
})
|
||||
|
||||
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 (
|
||||
<Box className="vendors">
|
||||
<Head>
|
||||
<title>{['Vendors', globals.meta_title].join(' — ')}</title>
|
||||
</Head>
|
||||
<Flex alignItems="center" justifyContent="space-between" direction={['column', 'row']} mb="5" gap="5">
|
||||
<Heading size="lg">Vendors</Heading>
|
||||
<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} prefetch={false} 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>
|
||||
<IconButton icon={<TbLayoutGrid />} onClick={() => setLayout('GRID')} isDisabled={layout === 'GRID'} />
|
||||
<IconButton icon={<TbLayoutList />} onClick={() => setLayout('LIST')} isDisabled={layout === 'LIST'} />
|
||||
</ButtonGroup>
|
||||
</HStack>
|
||||
</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}>
|
||||
{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>
|
||||
))}
|
||||
{!isLoading &&
|
||||
vendors.map((v) => (
|
||||
<LinkBox as={Card} rounded={4} key={v.id}>
|
||||
<Image
|
||||
roundedTop={4}
|
||||
objectFit="contain"
|
||||
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(' '))} …
|
||||
</Text>
|
||||
</LinkOverlay>
|
||||
</CardBody>
|
||||
</LinkBox>
|
||||
))}
|
||||
</SimpleGrid>
|
||||
)}
|
||||
|
||||
{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(' '))} …
|
||||
</Text>
|
||||
</Td>
|
||||
</Tr>
|
||||
</Link>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{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>
|
||||
)}
|
||||
|
||||
{meta.filter_count > perPage && (
|
||||
<>
|
||||
<Pagination page={Number(page)} itemsPerPage={perPage} totalItems={meta.filter_count} />
|
||||
<HStack mt="5" justifyContent="center">
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
2488
frontend/pnpm-lock.yaml
generated
2488
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
.page {
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5 {
|
||||
font-family: var(--chakra-fonts-heading);
|
||||
font-weight: var(--chakra-fontWeights-bold);
|
||||
margin-top: var(--chakra-space-6);
|
||||
margin-bottom: var(--chakra-space-1);
|
||||
}
|
||||
|
||||
|
||||
ul {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.vendor {
|
||||
|
||||
h5 {
|
||||
font-size: var(--chakra-fontSizes-xl);
|
||||
font-family: var(--chakra-fonts-heading);
|
||||
font-weight: var(--chakra-fontWeights-bold);
|
||||
margin-top: var(--chakra-space-6);
|
||||
margin-bottom: var(--chakra-space-1);
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { extendTheme } from '@chakra-ui/react'
|
||||
import { defineStyle, defineStyleConfig } from '@chakra-ui/react'
|
||||
|
||||
const pagination = defineStyle({
|
||||
whiteSpace: 'nowrap',
|
||||
bg: 'transparent',
|
||||
gap: 3,
|
||||
paddingInlineStart: 3,
|
||||
paddingInlineEnd: 3,
|
||||
lineHeight: 10,
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'inherit',
|
||||
width: '100%',
|
||||
_hover: {
|
||||
textDecor: 'none',
|
||||
},
|
||||
'&.current': {
|
||||
bg: 'var(--chakra-colors-gray-300)',
|
||||
border: '1px solid transparent',
|
||||
},
|
||||
_dark: {
|
||||
bg: 'var(--chakra-colors-gray-900)',
|
||||
'&.current': {
|
||||
bg: 'var(--chakra-colors-gray-700)',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const linkTheme = defineStyleConfig({
|
||||
variants: { pagination },
|
||||
})
|
||||
|
||||
const theme = extendTheme({
|
||||
components: {
|
||||
Link: linkTheme,
|
||||
},
|
||||
})
|
||||
|
||||
export default theme
|
||||
Reference in New Issue
Block a user