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 */}