diff --git a/design.config.js b/design.config.js index 6831227c..1b06facd 100644 --- a/design.config.js +++ b/design.config.js @@ -1,6 +1,12 @@ const path = require('path') -const icons = ['office-building'] +const icons = [ + 'office-building', + 'chevron-left', + 'chevron-right', + 'chevron-double-left', + 'chevron-double-right', +] const iconsRoot = path.join( path.dirname(require.resolve('@mdi/svg/package.json')), diff --git a/hooks/use-sync-url-params-with-store/index.js b/hooks/use-sync-url-params-with-store/index.js index d05919b8..fd815b42 100644 --- a/hooks/use-sync-url-params-with-store/index.js +++ b/hooks/use-sync-url-params-with-store/index.js @@ -1,38 +1,64 @@ -import { useEffect, useCallback } from 'react' +import { useEffect } from 'react' import { useRouter } from 'next/router' -export const useSyncUrlParamsWithStore = (params) => { +import useDebouncedEffect from 'hooks/use-debounced-effect' + +export const useSyncUrlParamsWithStore = (paramsInstance) => { const router = useRouter() const setUrlParams = () => { - const newParams = Array.from(params.entries()).filter( + const { schema, ...params } = paramsInstance + + const newParams = Object.entries(params).filter( ([, value]) => value != null ) - const query = Object.fromEntries(newParams) - router.push( - { pathname: router.pathname, query }, - { - // asPath contains url params so we need to get rid of them - // and replace them with the new ones. - pathname: new URL(router.route, window.location.origin).pathname, - query, - }, - { shallow: true } + const { pathname: pathnameUrl } = new URL( + router.asPath, + window.location.origin + ) + const { pathname: pathnameAs } = new URL( + router.pathname, + window.location.origin ) + + const urlParams = new URLSearchParams(newParams) + + urlParams.sort() + + if ( + `${window.location.pathname}${window.location.search}` !== + `${pathnameUrl}?${urlParams.toString()}` + ) { + const updateParams = + window.location.search === '' ? router.replace : router.push + + updateParams( + { + pathname: `${pathnameAs}`, + query: { ...router.query, ...urlParams }, + }, + `${pathnameUrl}?${urlParams.toString()}`, + + { shallow: true } + ) + } } - const handleRouteChange = useCallback( - (nextUrl) => { - const { searchParams } = new URL(nextUrl, window.location.origin) - Array.from(searchParams.entries()).forEach(([key, value]) => { + const handleRouteChange = (nextUrl) => { + const { schema, ...params } = paramsInstance + const { searchParams } = new URL(nextUrl, window.location.origin) + + const newParams = Array.from(searchParams.entries()).filter( + ([key, value]) => // We want to compare like it because URLSearch params doesn't // automatically convert param (e.g. size) to number. // eslint-disable-next-line eqeqeq - if (params.has(key) && params[key] != value) params[key] = value - }) - }, - [params] - ) + params[key] != value + ) + + if (newParams.length) + paramsInstance.changeParams(Object.fromEntries(newParams)) + } useEffect(() => { router.events.on('routeChangeComplete', handleRouteChange) @@ -42,9 +68,15 @@ export const useSyncUrlParamsWithStore = (params) => { }, []) // whenever any param changes in store reflect it to the URL - useEffect(() => { - setUrlParams() - }, Array.from(params.values())) + useDebouncedEffect( + () => { + setUrlParams() + }, + Object.keys(paramsInstance) + .sort() + .filter((key) => key !== 'schema') + .map((key) => paramsInstance[key]) + ) } export default useSyncUrlParamsWithStore diff --git a/main/hooks.js b/main/hooks.js deleted file mode 100644 index 11df8d62..00000000 --- a/main/hooks.js +++ /dev/null @@ -1,26 +0,0 @@ -import { join } from 'path' - -import { useEffect } from 'react' -import { useRouter } from 'next/router' - -const useRedirect = ( - routePath, - actualPath = routePath, - { replace = true } = {} -) => { - const router = useRouter() - - useEffect(() => { - // Do not need to redirect - if (!routePath) return - - const pathname = join(router.pathname, routePath) - const asPath = join(router.asPath, actualPath) - if (replace) router.replace(pathname, asPath) - else router.push(pathname, asPath) - }, []) -} - -// Keeping default way to import hooks -// eslint-disable-next-line import/prefer-default-export -export { useRedirect } diff --git a/modules/authors-box/index.jsx b/modules/authors-box/index.jsx new file mode 100644 index 00000000..03f4ac5e --- /dev/null +++ b/modules/authors-box/index.jsx @@ -0,0 +1,63 @@ +import React, { useState } from 'react' +import { Button, Link } from '@oacore/design' +import { classNames } from '@oacore/design/lib/utils' + +import styles from './styles.module.css' + +const AuthorLink = ({ name }) => ( + + {name.replace(',', ' ')} + +) + +const Authors = ({ authors }) => { + const [isExpanded, setIsExpanded] = useState(false) + if (authors.length <= 3) { + return authors.map((author, index) => ( + <> + + {index < authors.length - 1 ? ', ' : ''} + + )) + } + + return ( + <> + ,{' '} + ,{' '} + + + + {authors.slice(2, -1).map((author) => ( + <> + ,{' '} + + ))} + + + + + ) +} + +export default Authors diff --git a/modules/authors-box/styles.module.css b/modules/authors-box/styles.module.css new file mode 100644 index 00000000..dfab0f71 --- /dev/null +++ b/modules/authors-box/styles.module.css @@ -0,0 +1,36 @@ +.author-link { + color: var(--gray-800); + text-decoration: underline; +} + +.show-more { + position: relative; + padding: 0 0.25rem; + margin: 0 0.25rem; + color: var(--gray-700); + text-transform: uppercase; + white-space: nowrap; + background: var(--gray-100); +} + +.show-more::after { + position: absolute; + right: -0.05rem; + content: ','; +} + +.more-authors { + display: none; +} + +.more-authors-box { + display: contents; +} + +.more-authors-expanded .show-more { + display: none; +} + +.more-authors-expanded .more-authors { + display: inline-block; +} diff --git a/modules/repositories-map/index.jsx b/modules/map/index.jsx similarity index 80% rename from modules/repositories-map/index.jsx rename to modules/map/index.jsx index 2b7e75d4..88089baf 100644 --- a/modules/repositories-map/index.jsx +++ b/modules/map/index.jsx @@ -1,13 +1,11 @@ import React from 'react' import dynamic from 'next/dynamic' -import { ProgressSpinner } from '@oacore/design' import { classNames } from '@oacore/design/lib/utils' import styles from './styles.module.css' const Map = dynamic(() => import('./map'), { ssr: false, - loading: () => , }) const RepositoriesMap = React.memo( diff --git a/modules/repositories-map/map.jsx b/modules/map/map.jsx similarity index 69% rename from modules/repositories-map/map.jsx rename to modules/map/map.jsx index 9f0ff839..6cad1d6c 100644 --- a/modules/repositories-map/map.jsx +++ b/modules/map/map.jsx @@ -16,7 +16,7 @@ const markerIcon = L.icon({ popupAnchor: [0, -32], }) -const CustomMap = ({ dataProviders }) => { +const CustomMap = ({ locations }) => { const mapContainerRef = useRef(null) const map = useRef(null) @@ -34,7 +34,10 @@ const CustomMap = ({ dataProviders }) => { ) map.current = L.map(mapContainerRef.current, { - center: centerPosition, + center: + locations.length > 1 + ? centerPosition + : new L.LatLng(locations[0].latitude, locations[0].longitude), zoom: 2, maxBounds: [ [-90, -180], @@ -52,40 +55,30 @@ const CustomMap = ({ dataProviders }) => { icon: markerIcon, }) - dataProviders - .filter( - ({ name, dataProviderLocation }) => - dataProviderLocation != null && - dataProviderLocation.latitude != null && - dataProviderLocation.longitude != null && - name - ) - .forEach(({ id, name, dataProviderLocation }) => { - const marker = L.marker( - new L.LatLng( - dataProviderLocation.latitude, - dataProviderLocation.longitude - ), - { - title: name, - icon: markerIcon, - } - ) + locations.forEach(({ name, href, latitude, longitude }) => { + const marker = L.marker(new L.LatLng(latitude, longitude), { + title: name, + icon: markerIcon, + }) + + if (href) { marker.bindPopup( ` ${name} ` ) - markers.addLayer(marker) - }) + } else marker.bindPopup(name) + + markers.addLayer(marker) + }) map.current.addLayer(markers) return () => map.current.removeLayer(markers) - }, [dataProviders]) + }, [locations]) return
} diff --git a/modules/repositories-map/styles.module.css b/modules/map/styles.module.css similarity index 100% rename from modules/repositories-map/styles.module.css rename to modules/map/styles.module.css diff --git a/modules/search-layout/index.js b/modules/search-layout/index.js index 9a63c20c..2b357f49 100644 --- a/modules/search-layout/index.js +++ b/modules/search-layout/index.js @@ -1,12 +1,12 @@ -import Content from './content' +import Sidebar from './sidebar' import Result from './result' -import Results from './results' +import Main from './main' import Search from './search' import ResultStats from './result-stats' -Search.Content = Content +Search.Sidebar = Sidebar Search.Result = Result -Search.Results = Results +Search.Main = Main Search.ResultStats = ResultStats export default Search diff --git a/modules/search-layout/main.jsx b/modules/search-layout/main.jsx new file mode 100644 index 00000000..6ff8475d --- /dev/null +++ b/modules/search-layout/main.jsx @@ -0,0 +1,12 @@ +import React from 'react' +import { classNames } from '@oacore/design/lib/utils' + +import styles from './styles.module.css' + +const Main = ({ children, className, tag: Tag = 'div', ...restProps }) => ( + + {children} + +) + +export default Main diff --git a/modules/search-layout/results.jsx b/modules/search-layout/results.jsx deleted file mode 100644 index c7a2be11..00000000 --- a/modules/search-layout/results.jsx +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react' -import { classNames } from '@oacore/design/lib/utils' - -import styles from './styles.module.css' - -const Results = ({ children, className, tag: Tag = 'div', ...restProps }) => ( - - {children} - -) - -export default Results diff --git a/modules/search-layout/content.jsx b/modules/search-layout/sidebar.jsx similarity index 62% rename from modules/search-layout/content.jsx rename to modules/search-layout/sidebar.jsx index dbfb9196..ec6317cc 100644 --- a/modules/search-layout/content.jsx +++ b/modules/search-layout/sidebar.jsx @@ -4,14 +4,14 @@ import { Card } from '@oacore/design' import styles from './styles.module.css' -const Content = ({ children, className, tag = 'div', ...restProps }) => ( +const Sidebar = ({ children, className, tag = 'div', ...restProps }) => ( {children} ) -export default Content +export default Sidebar diff --git a/modules/search-layout/styles.module.css b/modules/search-layout/styles.module.css index 828e56c7..e5b15b74 100644 --- a/modules/search-layout/styles.module.css +++ b/modules/search-layout/styles.module.css @@ -5,14 +5,14 @@ grid-auto-rows: auto; } -.search-results { +.main { display: flex; flex-direction: column; grid-column: 1; align-items: stretch; } -.content { +.sidebar { padding-top: 0; border: none; box-shadow: none; @@ -50,20 +50,16 @@ grid-auto-rows: auto; } - .search-results { + .main { display: contents; } - .content { + .sidebar { + display: contents; grid-column: 1; - grid-row: 2; padding-top: 1rem; } - .result-stats ~ .content { - grid-row: 3; - } - .result { margin: 0; } diff --git a/pages/_app.jsx b/pages/_app.jsx index 8660de65..c5173b7d 100644 --- a/pages/_app.jsx +++ b/pages/_app.jsx @@ -36,10 +36,9 @@ class App extends NextApp { render() { const { Component, pageProps, statistics } = this.props - const { initialState, ...restPageProps } = pageProps || {} return ( -
- +
+
) } diff --git a/pages/data-providers/[data-provider-id].jsx b/pages/data-providers/[data-provider-id].jsx new file mode 100644 index 00000000..f931ab61 --- /dev/null +++ b/pages/data-providers/[data-provider-id].jsx @@ -0,0 +1,133 @@ +import React, { useEffect, useRef } from 'react' +import { useRouter } from 'next/router' +import Head from 'next/head' + +import { useSyncUrlParamsWithStore } from 'hooks/use-sync-url-params-with-store' +import { convertAndValidate } from 'utils/validation' +import DataProvider, { schema } from 'store/data-provider' +import { useStore, observe } from 'store' +import DataProviderTemplate from 'templates/data-provider' + +export async function getServerSideProps({ query }) { + const id = query['data-provider-id'] + const params = convertAndValidate({ + params: Object.fromEntries( + Object.entries(query).filter(([, v]) => v != null) + ), + schema, + }) + + const { q, from, size } = params + + const outputsPromise = DataProvider.fetchOutputs({ + id, + q, + from, + size, + }) + const metadataPromise = DataProvider.fetchMetadata({ + id, + }) + + const [outputs, metadata] = await Promise.allSettled([ + outputsPromise, + metadataPromise, + ]) + + return { + props: { + initialState: { + dataProvider: { + outputs: outputs.value, + metadata: metadata.value, + params, + id, + }, + }, + }, + } +} + +const DataProviderPage = observe(({ initialState }) => { + const router = useRouter() + const store = useStore( + initialState ?? { + initialState: { + dataProvider: { + id: router.query['data-provider-id'], + }, + }, + } + ) + const { dataProvider } = store + const previousQuery = useRef(dataProvider.params.q) + const previousFrom = useRef(dataProvider.params.from) + const previousSize = useRef(dataProvider.params.size) + + useSyncUrlParamsWithStore(dataProvider.params) + + useEffect(() => { + // whenever query changes go back to first page + if (previousQuery.current !== dataProvider.params.q) { + dataProvider.params.changeParams({ + size: 10, + from: 0, + }) + } + + const isLoadMore = + previousQuery.current === dataProvider.params.q && + previousFrom.current === dataProvider.params.from && + previousSize.current !== dataProvider.params.size + + const hasChanged = + previousQuery.current !== dataProvider.params.q || + previousFrom.current !== dataProvider.params.from || + previousSize.current !== dataProvider.params.size + + previousQuery.current = dataProvider.params.q + previousFrom.current = dataProvider.params.from + previousSize.current = dataProvider.params.size + + if (hasChanged) dataProvider.loadOutputs({ loadMore: isLoadMore }) + }, [ + dataProvider.params.size, + dataProvider.params.from, + dataProvider.params.q, + ]) + + return ( + <> + + + {dataProvider.params.q + ? `Search "${dataProvider.params.q}" in ${dataProvider.metadata.name}` + : `${dataProvider.metadata.name}`} + Data providers search + + + + { + dataProvider.params.q = q + }} + from={dataProvider.params.from} + size={dataProvider.params.size} + // TODO: Get proper value from API + total={50000} + loading={dataProvider.loading} + basePath={`/data-providers/${dataProvider.id}`} + isLoadingMore={dataProvider.isLoadingMore} + /> + + ) +}) + +export default DataProviderPage diff --git a/pages/data-providers/index.jsx b/pages/data-providers/index.jsx index f6e75b14..13d24d14 100644 --- a/pages/data-providers/index.jsx +++ b/pages/data-providers/index.jsx @@ -35,8 +35,8 @@ export async function getServerSideProps({ query }) { } } -const SearchPage = observe(() => { - const { dataProviders, statistics } = useStore() +const SearchPage = observe(({ initialState }) => { + const { dataProviders, statistics } = useStore(initialState) const { params: { action, query, size }, results, @@ -65,9 +65,9 @@ const SearchPage = observe(() => { ) const setQuery = useCallback( (q) => { - dataProviders.params.query = q + dataProviders.params.q = q }, - [dataProviders.params.query] + [dataProviders.params.q] ) const handleUrlChange = useCallback( (event) => { @@ -82,7 +82,7 @@ const SearchPage = observe(() => { {query ? `${query} - ` : ''}Data providers search {/* eslint-disable react/no-danger */}