Rename frontend to legacy-frontend

This commit is contained in:
2026-04-23 21:03:23 +04:00
parent a3ec5b78c6
commit 63ac2df4b5
21 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
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 &hellip;</AlertDescription>
</Alert>
)
}

View File

@@ -0,0 +1,14 @@
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 &hellip;</AlertDescription>
</Alert>
)
}

View File

@@ -0,0 +1,41 @@
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>
)
}

View File

@@ -0,0 +1,49 @@
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),
})),
}
}

View File

@@ -0,0 +1,8 @@
export default function Error(props) {
return (
<>
<h2>Oops, there is an error!</h2>
<pre>{JSON.stringify(props, null, 2)}</pre>
</>
)
}

View File

@@ -0,0 +1,9 @@
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} />
}

View File

@@ -0,0 +1,164 @@
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
legacy-frontend/pages/vendors/index.js vendored Normal file
View File

@@ -0,0 +1,310 @@
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(' '))} &hellip;
</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(' '))} &hellip;
</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 &hellip;</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>
)
}