From 32be2e9625366aba99160ec361ea93ce38eec9ed Mon Sep 17 00:00:00 2001
From: Jozef Harag <32jojo32@gmail.com>
Date: Tue, 6 Oct 2020 16:05:23 +0200
Subject: [PATCH 1/6] Add data provider details page layout
---
modules/authors-box/index.jsx | 63 +++++++++
modules/authors-box/styles.module.css | 36 ++++++
modules/{repositories-map => map}/index.jsx | 2 -
modules/{repositories-map => map}/map.jsx | 43 +++---
.../styles.module.css | 0
modules/search-layout/index.js | 8 +-
modules/search-layout/main.jsx | 12 ++
modules/search-layout/results.jsx | 15 ---
.../{content.jsx => sidebar.jsx} | 6 +-
modules/search-layout/styles.module.css | 14 +-
pages/data-providers/[data-provider-id].jsx | 40 ++++++
store/data-provider.js | 36 ++++++
store/root.js | 6 +-
templates/data-provider/claim-card.jsx | 28 ++++
.../hooks/use-state-to-url-effect.js | 32 +++++
templates/data-provider/index.jsx | 34 +++++
templates/data-provider/map-card.jsx | 25 ++++
templates/data-provider/result.jsx | 37 ++++++
templates/data-provider/search.jsx | 55 ++++++++
templates/data-provider/styles.module.css | 122 ++++++++++++++++++
templates/data-providers/index.jsx | 35 +++--
templates/data-providers/result-card.jsx | 15 ++-
templates/data-providers/styles.module.css | 1 +
23 files changed, 589 insertions(+), 76 deletions(-)
create mode 100644 modules/authors-box/index.jsx
create mode 100644 modules/authors-box/styles.module.css
rename modules/{repositories-map => map}/index.jsx (80%)
rename modules/{repositories-map => map}/map.jsx (69%)
rename modules/{repositories-map => map}/styles.module.css (100%)
create mode 100644 modules/search-layout/main.jsx
delete mode 100644 modules/search-layout/results.jsx
rename modules/search-layout/{content.jsx => sidebar.jsx} (62%)
create mode 100644 pages/data-providers/[data-provider-id].jsx
create mode 100644 store/data-provider.js
create mode 100644 templates/data-provider/claim-card.jsx
create mode 100644 templates/data-provider/hooks/use-state-to-url-effect.js
create mode 100644 templates/data-provider/index.jsx
create mode 100644 templates/data-provider/map-card.jsx
create mode 100644 templates/data-provider/result.jsx
create mode 100644 templates/data-provider/search.jsx
create mode 100644 templates/data-provider/styles.module.css
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 (
+ <>
+ ,{' '}
+ ,{' '}
+
+ setIsExpanded(true)}
+ aria-hidden={isExpanded}
+ title="Show more authors"
+ >
+ + {authors.length - 3} MORE
+
+
+ {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/data-providers/[data-provider-id].jsx b/pages/data-providers/[data-provider-id].jsx
new file mode 100644
index 00000000..8ad3b780
--- /dev/null
+++ b/pages/data-providers/[data-provider-id].jsx
@@ -0,0 +1,40 @@
+import React from 'react'
+
+import DataProvider 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 outputs = await DataProvider.loadOutputs({
+ id,
+ })
+ const metadata = await DataProvider.loadMetadata({
+ id,
+ })
+
+ return {
+ props: {
+ initialState: {
+ dataProvider: {
+ outputs,
+ id: query['data-provider-id'],
+ metadata,
+ },
+ },
+ },
+ }
+}
+
+const DataProviderPage = observe(() => {
+ const { dataProvider } = useStore()
+ return (
+
+ )
+})
+
+export default DataProviderPage
diff --git a/store/data-provider.js b/store/data-provider.js
new file mode 100644
index 00000000..dc8dc69c
--- /dev/null
+++ b/store/data-provider.js
@@ -0,0 +1,36 @@
+import { observable } from 'mobx'
+
+import apiRequest from 'api'
+
+class DataProvider {
+ @observable outputs
+
+ @observable page
+
+ @observable metadata
+
+ constructor({ outputs, metadata, page }) {
+ this.outputs = outputs ?? []
+ this.metadata = metadata ?? []
+ this.page = page
+ }
+
+ static async loadOutputs({ id, page = 1 }) {
+ const params = {
+ from: (page - 1) * 20,
+ size: 20,
+ }
+
+ const { data } = await apiRequest(`/data-providers/${id}/works`, {
+ searchParams: params,
+ })
+ return data
+ }
+
+ static async loadMetadata({ id }) {
+ const { data } = await apiRequest(`/data-providers/${id}`)
+ return data
+ }
+}
+
+export default DataProvider
diff --git a/store/root.js b/store/root.js
index 1eca2f8b..0b3028ac 100644
--- a/store/root.js
+++ b/store/root.js
@@ -2,16 +2,20 @@ import { observable } from 'mobx'
import Claim from './claim'
import DataProviders from './data-providers'
+import DataProvider from './data-provider'
class Root {
claim = new Claim()
+ @observable dataProvider = null
+
@observable dataProviders = null
@observable statistics
- init({ dataProviders = {}, statistics = {} } = {}) {
+ init({ dataProviders = {}, statistics = {}, dataProvider = {} } = {}) {
this.dataProviders = new DataProviders(dataProviders)
+ this.dataProvider = new DataProvider(dataProvider)
this.statistics = statistics
}
}
diff --git a/templates/data-provider/claim-card.jsx b/templates/data-provider/claim-card.jsx
new file mode 100644
index 00000000..54f8f0c4
--- /dev/null
+++ b/templates/data-provider/claim-card.jsx
@@ -0,0 +1,28 @@
+import React from 'react'
+import { Card, Button } from '@oacore/design'
+import { classNames } from '@oacore/design/lib/utils'
+
+import styles from './styles.module.css'
+
+const ClaimCard = ({ name }) => (
+
+
+ Do you manage {name}? Access insider analytics, issue reports and
+ manage access to outputs from your repository in
+ the CORE Dashboard!
+
+ 👇
+
+
+
+
+ Gain access now
+
+
+ More details
+
+
+
+)
+
+export default ClaimCard
diff --git a/templates/data-provider/hooks/use-state-to-url-effect.js b/templates/data-provider/hooks/use-state-to-url-effect.js
new file mode 100644
index 00000000..0632da41
--- /dev/null
+++ b/templates/data-provider/hooks/use-state-to-url-effect.js
@@ -0,0 +1,32 @@
+import { useCallback, useEffect } from 'react'
+import { useRouter } from 'next/router'
+
+const useStateToUrlEffect = ({ query, size, showForm }) => {
+ const router = useRouter()
+ const setUrlParams = useCallback(
+ (params) => {
+ const url = {
+ pathname: '/data-providers',
+ query: {
+ ...params,
+ },
+ }
+ router.push(url, url, { shallow: true })
+ },
+ [router]
+ )
+
+ // reflect state to URL
+ useEffect(() => {
+ const params = {
+ query,
+ size,
+ }
+
+ if (showForm) params.action = 'add'
+
+ setUrlParams(params)
+ }, [query, size, showForm])
+}
+
+export default useStateToUrlEffect
diff --git a/templates/data-provider/index.jsx b/templates/data-provider/index.jsx
new file mode 100644
index 00000000..d5ff0ee5
--- /dev/null
+++ b/templates/data-provider/index.jsx
@@ -0,0 +1,34 @@
+import React from 'react'
+
+import Result from './result'
+import DataProviderOutputsSearch from './search'
+import ClaimCard from './claim-card'
+import MapCard from './map-card'
+import styles from './styles.module.css'
+
+import Search from 'modules/search-layout'
+
+const DataProviderTemplate = ({ outputs, metadata }) => (
+
+
+
+ {metadata.institution}
+ {metadata.name}
+
+
+ {}}
+ className={styles.search}
+ />
+ {outputs.map((o) => (
+
+ ))}
+
+
+
+
+
+
+)
+
+export default DataProviderTemplate
diff --git a/templates/data-provider/map-card.jsx b/templates/data-provider/map-card.jsx
new file mode 100644
index 00000000..1beb3ad6
--- /dev/null
+++ b/templates/data-provider/map-card.jsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import { Card } from '@oacore/design'
+import { classNames } from '@oacore/design/lib/utils'
+
+import styles from './styles.module.css'
+
+import Map from 'modules/map'
+
+const MapCard = ({ metadata }) => (
+
+
+ {metadata.name}
+
+)
+
+export default MapCard
diff --git a/templates/data-provider/result.jsx b/templates/data-provider/result.jsx
new file mode 100644
index 00000000..049819ac
--- /dev/null
+++ b/templates/data-provider/result.jsx
@@ -0,0 +1,37 @@
+import React from 'react'
+import { Link } from '@oacore/design'
+import { classNames } from '@oacore/design/lib/utils'
+
+import styles from './styles.module.css'
+
+import Authors from 'modules/authors-box'
+import Search from 'modules/search-layout'
+
+const Result = ({ id, title, author, publisher, year, abstract }) => (
+
+
+
+
Free PDF
+
+
+
+
+ {title}
+
+
+
+
+
+ {Boolean(publisher) && (
+ {publisher}
+ )}
+ {Boolean(year) && (
+ {year}
+ )}
+
+
{abstract}
+
+
+)
+
+export default Result
diff --git a/templates/data-provider/search.jsx b/templates/data-provider/search.jsx
new file mode 100644
index 00000000..2bd44e70
--- /dev/null
+++ b/templates/data-provider/search.jsx
@@ -0,0 +1,55 @@
+import React, { useCallback } from 'react'
+import { throttle } from 'throttle-debounce'
+import { Select } from '@oacore/design'
+
+const DataProviderOutputsSearch = ({
+ initQuery,
+ searchOutputs,
+ onQueryChanged,
+ className,
+}) => {
+ const [suggestions, setSuggestions] = React.useState([])
+ const [value, setValue] = React.useState(initQuery || '')
+
+ const search = useCallback(
+ throttle(500, false, (searchTerm) => {
+ if (!searchOutputs) return
+ setSuggestions(searchOutputs(searchTerm).slice(0, 10))
+ }),
+ []
+ )
+
+ const handleOnChange = (data) => {
+ onQueryChanged(data.value)
+ search(data.value)
+ }
+
+ const handleOnInput = (data) => {
+ setValue(data.value)
+
+ // if id doesn't exists it means user type own text
+ // and didn't use suggestion
+ if (!data.id) search(data.value)
+ }
+
+ return (
+
+ {suggestions.map((el) => (
+
+ {el.name}
+
+ ))}
+
+ )
+}
+
+export default DataProviderOutputsSearch
diff --git a/templates/data-provider/styles.module.css b/templates/data-provider/styles.module.css
new file mode 100644
index 00000000..9bcfa27e
--- /dev/null
+++ b/templates/data-provider/styles.module.css
@@ -0,0 +1,122 @@
+.layout {
+ z-index: 5;
+}
+
+.title-box {
+ grid-row: 1;
+ margin-bottom: 1rem;
+}
+
+.search {
+ grid-row: 2;
+}
+
+.title {
+ margin-top: 0;
+}
+
+.card:not(:last-child) {
+ margin-bottom: 1rem;
+}
+
+.claim-card {
+ box-shadow: none;
+}
+
+.claim-card-actions {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.claim-card-action {
+ margin-top: 1rem;
+ grid-row: 6;
+}
+
+.map-card {
+ grid-row: 5;
+}
+
+.claim-card-action:not(:last-child) {
+ margin-right: 0.5rem;
+}
+
+.card .map {
+ width: auto;
+ margin: calc(-1 * var(--card-padding-y)) calc(-1 * var(--card-padding-x))
+ var(--card-padding-x) calc(-1 * var(--card-padding-x));
+}
+
+.result {
+ --card-padding-x: 0.5rem;
+ --card-padding-y: 0.5rem;
+
+ display: flex;
+}
+
+.result-image {
+ position: relative;
+ display: flex;
+ flex: 0 0 100px;
+ flex-direction: column;
+ justify-content: center;
+ margin-right: 1rem;
+}
+
+.result-image > img {
+ width: 100%;
+ height: auto;
+}
+
+.result-content {
+ display: flex;
+ flex-direction: column;
+}
+
+/* stylelint-disable property-no-vendor-prefix, value-no-vendor-prefix */
+.result-title {
+ display: -webkit-box;
+ overflow: hidden;
+ font-weight: 500;
+ -webkit-line-clamp: 3;
+ -webkit-box-orient: vertical;
+}
+
+.result-abstract {
+ display: -webkit-box;
+ -webkit-line-clamp: 7;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+/* stylelint-enable property-no-vendor-prefix, value-no-vendor-prefix */
+.result-metadata {
+ display: inline-block;
+ padding-left: 0;
+ margin: 0;
+}
+
+.result-metadata-item:not(:first-child) {
+ position: relative;
+ padding-left: 1rem;
+ overflow: hidden;
+}
+
+.result-metadata-item:not(:first-child)::before {
+ position: absolute;
+ top: 0.75rem;
+ left: 0.5rem;
+ display: inline-block;
+ width: 0.25rem;
+ height: 0.25rem;
+ content: '';
+ background: var(--gray-500);
+ border-radius: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.free-pdf {
+ font-size: 0.75rem;
+ color: var(--white);
+ text-align: center;
+ background: var(--primary);
+}
diff --git a/templates/data-providers/index.jsx b/templates/data-providers/index.jsx
index b5ff0c45..937efc56 100644
--- a/templates/data-providers/index.jsx
+++ b/templates/data-providers/index.jsx
@@ -8,7 +8,22 @@ import AddDataProviderForm from './form'
import { formatNumber } from 'utils/format-number'
import Search from 'modules/search-layout'
-import RepositoriesMap from 'modules/repositories-map'
+import Map from 'modules/map'
+
+const filterAndMapDataProviders = (dataProviders) =>
+ dataProviders
+ .filter(
+ ({ name, dataProviderLocation }) =>
+ dataProviderLocation?.latitude != null &&
+ dataProviderLocation?.longitude != null &&
+ name
+ )
+ .map(({ id, name, dataProviderLocation }) => ({
+ name,
+ href: `https://core.ac.uk/search?q=repositories.id:(${id})`,
+ latitude: dataProviderLocation?.latitude,
+ longitude: dataProviderLocation?.longitude,
+ }))
const getCountryName = (code) => {
const countryName = countries[String(code).toUpperCase()]
@@ -31,17 +46,17 @@ const SearchResults = ({
if (!results.length) {
return (
-
+
No results found
{children}
-
+
)
}
return (
-
+
{results.slice(0, dataProvidersSize).map((el) => (
)}
{children}
-
+
)
}
@@ -140,12 +155,12 @@ const DataProvidersSearchTemplate = React.memo(
-
-
+
We aggregate research papers from data providers all over the world
@@ -170,7 +185,7 @@ const DataProvidersSearchTemplate = React.memo(
>
Become data provider
-
+
)
}
diff --git a/templates/data-providers/result-card.jsx b/templates/data-providers/result-card.jsx
index 1faf6e92..96723a39 100644
--- a/templates/data-providers/result-card.jsx
+++ b/templates/data-providers/result-card.jsx
@@ -1,5 +1,6 @@
import React from 'react'
-import { Icon, Link } from '@oacore/design'
+import { Icon, Link as UILink } from '@oacore/design'
+import Link from 'next/link'
import styles from './styles.module.css'
@@ -17,19 +18,19 @@ const ResultCard = ({
- {title}
+ {title}
-
+
{homePage}
-
+
{country && {country} }
diff --git a/templates/data-providers/styles.module.css b/templates/data-providers/styles.module.css
index b59c0647..b72b9aab 100644
--- a/templates/data-providers/styles.module.css
+++ b/templates/data-providers/styles.module.css
@@ -65,6 +65,7 @@
.map {
height: 30rem;
+ grid-row: 3;
}
@media (--screen-until-desktop) {
From fb90d7e6980af3fcd8dbd8441c303f7891572dcd Mon Sep 17 00:00:00 2001
From: Jozef Harag <32jojo32@gmail.com>
Date: Tue, 6 Oct 2020 16:05:23 +0200
Subject: [PATCH 2/6] Connect search to API
---
pages/data-providers/[data-provider-id].jsx | 30 +++++-
store/data-provider.js | 50 ++++++++--
templates/data-provider/hooks/index.js | 1 +
.../hooks/use-state-to-url-effect.js | 29 ++++--
templates/data-provider/index.jsx | 92 ++++++++++++++-----
templates/data-provider/result.jsx | 37 --------
templates/data-provider/search.jsx | 23 +++--
templates/data-provider/styles.module.css | 74 +--------------
8 files changed, 179 insertions(+), 157 deletions(-)
create mode 100644 templates/data-provider/hooks/index.js
delete mode 100644 templates/data-provider/result.jsx
diff --git a/pages/data-providers/[data-provider-id].jsx b/pages/data-providers/[data-provider-id].jsx
index 8ad3b780..6369af2f 100644
--- a/pages/data-providers/[data-provider-id].jsx
+++ b/pages/data-providers/[data-provider-id].jsx
@@ -1,14 +1,19 @@
import React from 'react'
+import { useStateToUrlEffect } from 'templates/data-provider/hooks'
import DataProvider 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 { q = '', from = 0, size = 10 } = query
- const outputs = await DataProvider.loadOutputs({
+ const outputs = await DataProvider.fetchOutputs({
id,
+ q,
+ from,
+ size,
})
const metadata = await DataProvider.loadMetadata({
id,
@@ -19,8 +24,11 @@ export async function getServerSideProps({ query }) {
initialState: {
dataProvider: {
outputs,
- id: query['data-provider-id'],
metadata,
+ id,
+ q,
+ from,
+ size,
},
},
},
@@ -29,10 +37,28 @@ export async function getServerSideProps({ query }) {
const DataProviderPage = observe(() => {
const { dataProvider } = useStore()
+
+ useStateToUrlEffect({
+ id: dataProvider.id,
+ q: dataProvider.query,
+ from: dataProvider.from,
+ size: dataProvider.size,
+ })
+
return (
{
+ dataProvider.query = q
+ }}
+ loadOutputs={dataProvider.loadOutputs}
+ from={dataProvider.from}
+ size={dataProvider.size}
+ loading={dataProvider.loading}
/>
)
})
diff --git a/store/data-provider.js b/store/data-provider.js
index dc8dc69c..bc301a4d 100644
--- a/store/data-provider.js
+++ b/store/data-provider.js
@@ -1,24 +1,37 @@
-import { observable } from 'mobx'
+import { observable, action } from 'mobx'
import apiRequest from 'api'
class DataProvider {
@observable outputs
- @observable page
-
@observable metadata
- constructor({ outputs, metadata, page }) {
+ @observable id
+
+ @observable query
+
+ @observable from
+
+ @observable size
+
+ @observable loading = false
+
+ constructor({ id, q, from, size, outputs, metadata }) {
+ this.id = id
+ this.from = from ?? 0
+ this.size = size ?? 10
+ this.query = q
this.outputs = outputs ?? []
this.metadata = metadata ?? []
- this.page = page
}
- static async loadOutputs({ id, page = 1 }) {
+ static async fetchOutputs({ id, from, size, q, ...restParams }) {
const params = {
- from: (page - 1) * 20,
- size: 20,
+ from,
+ size,
+ q,
+ ...restParams,
}
const { data } = await apiRequest(`/data-providers/${id}/works`, {
@@ -31,6 +44,27 @@ class DataProvider {
const { data } = await apiRequest(`/data-providers/${id}`)
return data
}
+
+ @action
+ loadOutputs = async () => {
+ // show loading placeholders only when search takes more than 500ms
+ const timeout = setTimeout(() => {
+ this.loading = true
+ }, 500)
+ this.loading = true
+ this.outputs = await DataProvider.fetchOutputs({
+ id: this.id,
+ from: this.from,
+ size: this.size,
+ q: this.query,
+ })
+ clearTimeout(timeout)
+ this.loading = false
+ }
+
+ @action
+ loadSuggestions = async (searchTerm) =>
+ DataProvider.fetchOutputs({ id: this.id, q: searchTerm, from: 0, size: 10 })
}
export default DataProvider
diff --git a/templates/data-provider/hooks/index.js b/templates/data-provider/hooks/index.js
new file mode 100644
index 00000000..4e47465f
--- /dev/null
+++ b/templates/data-provider/hooks/index.js
@@ -0,0 +1 @@
+export useStateToUrlEffect from './use-state-to-url-effect'
diff --git a/templates/data-provider/hooks/use-state-to-url-effect.js b/templates/data-provider/hooks/use-state-to-url-effect.js
index 0632da41..a4eef4e8 100644
--- a/templates/data-provider/hooks/use-state-to-url-effect.js
+++ b/templates/data-provider/hooks/use-state-to-url-effect.js
@@ -1,17 +1,29 @@
import { useCallback, useEffect } from 'react'
import { useRouter } from 'next/router'
-const useStateToUrlEffect = ({ query, size, showForm }) => {
+const useStateToUrlEffect = ({ id, q, from, size }) => {
const router = useRouter()
const setUrlParams = useCallback(
(params) => {
const url = {
- pathname: '/data-providers',
- query: {
- ...params,
+ href: {
+ pathname: '/data-providers/[data-provider-id]',
+ query: {
+ ...params,
+ },
+ },
+
+ as: {
+ pathname: `/data-providers/${id}`,
+ query: {
+ ...params,
+ },
+ },
+ options: {
+ shallow: true,
},
}
- router.push(url, url, { shallow: true })
+ router.push(url.href, url.as, url.options)
},
[router]
)
@@ -19,14 +31,13 @@ const useStateToUrlEffect = ({ query, size, showForm }) => {
// reflect state to URL
useEffect(() => {
const params = {
- query,
+ q,
+ from,
size,
}
- if (showForm) params.action = 'add'
-
setUrlParams(params)
- }, [query, size, showForm])
+ }, [q, from, size])
}
export default useStateToUrlEffect
diff --git a/templates/data-provider/index.jsx b/templates/data-provider/index.jsx
index d5ff0ee5..3c8d3093 100644
--- a/templates/data-provider/index.jsx
+++ b/templates/data-provider/index.jsx
@@ -1,6 +1,6 @@
import React from 'react'
+import { SearchResult } from '@oacore/design'
-import Result from './result'
import DataProviderOutputsSearch from './search'
import ClaimCard from './claim-card'
import MapCard from './map-card'
@@ -8,27 +8,75 @@ import styles from './styles.module.css'
import Search from 'modules/search-layout'
-const DataProviderTemplate = ({ outputs, metadata }) => (
-
-
-
- {metadata.institution}
- {metadata.name}
-
+const DataProviderTemplate = ({
+ outputs,
+ metadata,
+ query,
+ setQuery,
+ loadSuggestions,
+ loadOutputs,
+ loading,
+}) => {
+ const placeholders = Array.from(Array(10).keys())
- {}}
- className={styles.search}
- />
- {outputs.map((o) => (
-
- ))}
-
-
-
-
-
-
-)
+ return (
+
+
+
+ {metadata.institution}
+ {metadata.name}
+
+
+ {
+ setQuery(q)
+ loadOutputs()
+ }}
+ className={styles.search}
+ loadSuggestions={loadSuggestions}
+ />
+ {loading
+ ? placeholders.map((key) => (
+
+
+
+ ))
+ : outputs.map(({ id, abstract, title, author, year, link }) => {
+ const fullTextLink = link.find((l) => l.type === 'download')?.url
+ const metadataLink = link.find((l) => l.type === 'display')?.url
+ return (
+
+ {abstract}
+
+ )
+ })}
+ {outputs.length === 0 && (
+ No results found
+ )}
+
+
+
+
+
+
+ )
+}
export default DataProviderTemplate
diff --git a/templates/data-provider/result.jsx b/templates/data-provider/result.jsx
deleted file mode 100644
index 049819ac..00000000
--- a/templates/data-provider/result.jsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React from 'react'
-import { Link } from '@oacore/design'
-import { classNames } from '@oacore/design/lib/utils'
-
-import styles from './styles.module.css'
-
-import Authors from 'modules/authors-box'
-import Search from 'modules/search-layout'
-
-const Result = ({ id, title, author, publisher, year, abstract }) => (
-
-
-
-
Free PDF
-
-
-
-
- {title}
-
-
-
-
-
- {Boolean(publisher) && (
- {publisher}
- )}
- {Boolean(year) && (
- {year}
- )}
-
-
{abstract}
-
-
-)
-
-export default Result
diff --git a/templates/data-provider/search.jsx b/templates/data-provider/search.jsx
index 2bd44e70..3dde355a 100644
--- a/templates/data-provider/search.jsx
+++ b/templates/data-provider/search.jsx
@@ -1,25 +1,34 @@
-import React, { useCallback } from 'react'
+import React, { useCallback, useState } from 'react'
import { throttle } from 'throttle-debounce'
import { Select } from '@oacore/design'
const DataProviderOutputsSearch = ({
initQuery,
- searchOutputs,
+ loadSuggestions,
onQueryChanged,
className,
}) => {
const [suggestions, setSuggestions] = React.useState([])
const [value, setValue] = React.useState(initQuery || '')
+ const [isInitialised, setIsInitialised] = useState(false)
const search = useCallback(
- throttle(500, false, (searchTerm) => {
- if (!searchOutputs) return
- setSuggestions(searchOutputs(searchTerm).slice(0, 10))
+ throttle(500, false, async (searchTerm) => {
+ const newSuggestions = await loadSuggestions(searchTerm)
+ setSuggestions(newSuggestions)
}),
[]
)
const handleOnChange = (data) => {
+ // TODO: Consider this having in @oacore/design
+ // prevent firing callback in initialisation (we have the data prefetched
+ // on the server side already
+ if (!isInitialised) {
+ setIsInitialised(true)
+ return
+ }
+
onQueryChanged(data.value)
search(data.value)
}
@@ -44,8 +53,8 @@ const DataProviderOutputsSearch = ({
className={className}
>
{suggestions.map((el) => (
-
- {el.name}
+
+ {el.title}
))}
diff --git a/templates/data-provider/styles.module.css b/templates/data-provider/styles.module.css
index 9bcfa27e..af91a8d0 100644
--- a/templates/data-provider/styles.module.css
+++ b/templates/data-provider/styles.module.css
@@ -47,76 +47,6 @@
var(--card-padding-x) calc(-1 * var(--card-padding-x));
}
-.result {
- --card-padding-x: 0.5rem;
- --card-padding-y: 0.5rem;
-
- display: flex;
-}
-
-.result-image {
- position: relative;
- display: flex;
- flex: 0 0 100px;
- flex-direction: column;
- justify-content: center;
- margin-right: 1rem;
-}
-
-.result-image > img {
- width: 100%;
- height: auto;
-}
-
-.result-content {
- display: flex;
- flex-direction: column;
-}
-
-/* stylelint-disable property-no-vendor-prefix, value-no-vendor-prefix */
-.result-title {
- display: -webkit-box;
- overflow: hidden;
- font-weight: 500;
- -webkit-line-clamp: 3;
- -webkit-box-orient: vertical;
-}
-
-.result-abstract {
- display: -webkit-box;
- -webkit-line-clamp: 7;
- -webkit-box-orient: vertical;
- overflow: hidden;
-}
-/* stylelint-enable property-no-vendor-prefix, value-no-vendor-prefix */
-.result-metadata {
- display: inline-block;
- padding-left: 0;
- margin: 0;
-}
-
-.result-metadata-item:not(:first-child) {
- position: relative;
- padding-left: 1rem;
- overflow: hidden;
-}
-
-.result-metadata-item:not(:first-child)::before {
- position: absolute;
- top: 0.75rem;
- left: 0.5rem;
- display: inline-block;
- width: 0.25rem;
- height: 0.25rem;
- content: '';
- background: var(--gray-500);
- border-radius: 50%;
- transform: translate(-50%, -50%);
-}
-
-.free-pdf {
- font-size: 0.75rem;
- color: var(--white);
- text-align: center;
- background: var(--primary);
+.no-results-find {
+ margin-top: 1rem;
}
From 9824666d33c0adb77b1e4d7db57e4b7e9b62e89e Mon Sep 17 00:00:00 2001
From: Jozef Harag <32jojo32@gmail.com>
Date: Tue, 3 Nov 2020 09:24:58 +0100
Subject: [PATCH 3/6] Add pagination
---
design.config.js | 8 +-
hooks/use-sync-url-params-with-store/index.js | 82 +++++++----
main/hooks.js | 26 ----
pages/_app.jsx | 5 +-
pages/data-providers/[data-provider-id].jsx | 102 +++++++++----
pages/data-providers/index.jsx | 4 +-
store/data-provider.js | 87 +++++++----
store/data-providers.js | 15 +-
store/index.jsx | 23 +--
store/params.js | 65 ++-------
store/root.js | 12 +-
templates/data-provider/hooks/index.js | 1 -
.../hooks/use-state-to-url-effect.js | 43 ------
templates/data-provider/index.jsx | 33 ++++-
templates/data-provider/pagination.jsx | 136 ++++++++++++++++++
templates/data-provider/search.jsx | 26 +---
templates/data-provider/styles.module.css | 35 +++++
templates/data-providers/result-card.jsx | 6 +-
utils/validation.js | 54 +++++++
19 files changed, 503 insertions(+), 260 deletions(-)
delete mode 100644 main/hooks.js
delete mode 100644 templates/data-provider/hooks/index.js
delete mode 100644 templates/data-provider/hooks/use-state-to-url-effect.js
create mode 100644 templates/data-provider/pagination.jsx
create mode 100644 utils/validation.js
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/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
index 6369af2f..584dc075 100644
--- a/pages/data-providers/[data-provider-id].jsx
+++ b/pages/data-providers/[data-provider-id].jsx
@@ -1,64 +1,116 @@
-import React from 'react'
+import React, { useEffect, useRef } from 'react'
+import { useRouter } from 'next/router'
-import { useStateToUrlEffect } from 'templates/data-provider/hooks'
-import DataProvider from 'store/data-provider'
+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 { q = '', from = 0, size = 10 } = query
+ const params = convertAndValidate({
+ params: Object.fromEntries(
+ Object.entries(query).filter(([, v]) => v != null)
+ ),
+ schema,
+ })
+
+ const { q, from, size } = params
- const outputs = await DataProvider.fetchOutputs({
+ const outputsPromise = DataProvider.fetchOutputs({
id,
q,
from,
size,
})
- const metadata = await DataProvider.loadMetadata({
+ const metadataPromise = DataProvider.fetchMetadata({
id,
})
+ const [outputs, metadata] = await Promise.allSettled([
+ outputsPromise,
+ metadataPromise,
+ ])
+
return {
props: {
initialState: {
dataProvider: {
- outputs,
- metadata,
+ outputs: outputs.value,
+ metadata: metadata.value,
+ params,
id,
- q,
- from,
- size,
},
},
},
}
}
-const DataProviderPage = observe(() => {
- const { dataProvider } = useStore()
+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)
- useStateToUrlEffect({
- id: dataProvider.id,
- q: dataProvider.query,
- from: dataProvider.from,
- size: dataProvider.size,
- })
+ 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.query = q
+ dataProvider.params.q = q
}}
- loadOutputs={dataProvider.loadOutputs}
- from={dataProvider.from}
- size={dataProvider.size}
+ 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}
/>
)
})
diff --git a/pages/data-providers/index.jsx b/pages/data-providers/index.jsx
index f6e75b14..9598b912 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,
diff --git a/store/data-provider.js b/store/data-provider.js
index bc301a4d..c2d88227 100644
--- a/store/data-provider.js
+++ b/store/data-provider.js
@@ -1,29 +1,52 @@
import { observable, action } from 'mobx'
+import Params from './params'
+
import apiRequest from 'api'
-class DataProvider {
- @observable outputs
+export const schema = {
+ q: {
+ default: '',
+ convert: 'string',
+ validation: 'string',
+ },
+
+ from: {
+ default: 0,
+ validation: 'number',
+ convert: 'number',
+ },
+ size: {
+ default: 10,
+ validation: 'number',
+ convert: 'number',
+ },
+}
+class DataProvider {
@observable metadata
@observable id
- @observable query
+ @observable loading = false
- @observable from
+ @observable isLoadingMore = false
- @observable size
+ @observable params
- @observable loading = false
+ @observable outputsData
+
+ @observable outputs
- constructor({ id, q, from, size, outputs, metadata }) {
+ constructor({ id, outputs, metadata, params = {} }) {
this.id = id
- this.from = from ?? 0
- this.size = size ?? 10
- this.query = q
- this.outputs = outputs ?? []
- this.metadata = metadata ?? []
+ this.params = new Params(params, schema)
+
+ if (!outputs) this.loadOutputs()
+ else this.outputs = outputs ?? []
+
+ if (!metadata) this.loadMetadata()
+ else this.metadata = metadata
}
static async fetchOutputs({ id, from, size, q, ...restParams }) {
@@ -40,31 +63,37 @@ class DataProvider {
return data
}
- static async loadMetadata({ id }) {
+ static async fetchMetadata({ id }) {
const { data } = await apiRequest(`/data-providers/${id}`)
return data
}
@action
- loadOutputs = async () => {
- // show loading placeholders only when search takes more than 500ms
- const timeout = setTimeout(() => {
- this.loading = true
- }, 500)
- this.loading = true
- this.outputs = await DataProvider.fetchOutputs({
- id: this.id,
- from: this.from,
- size: this.size,
- q: this.query,
- })
- clearTimeout(timeout)
- this.loading = false
+ loadMetadata = async () => {
+ this.metadata = await DataProvider.fetchMetadata({ id: this.id })
}
@action
- loadSuggestions = async (searchTerm) =>
- DataProvider.fetchOutputs({ id: this.id, q: searchTerm, from: 0, size: 10 })
+ loadOutputs = async ({ loadMore = false } = {}) => {
+ this.loading = true
+ this.isLoadingMore = loadMore
+
+ this.outputs = (loadMore ? this.outputs : []).concat(
+ await DataProvider.fetchOutputs({
+ id: this.id,
+ from: loadMore
+ ? this.params.from + this.outputs.length
+ : this.params.from,
+ size: loadMore
+ ? this.params.size - this.outputs.length
+ : this.params.size,
+ q: this.params.q,
+ })
+ )
+
+ this.loading = false
+ this.isLoadingMore = false
+ }
}
export default DataProvider
diff --git a/store/data-providers.js b/store/data-providers.js
index d3ebb54a..b67aaffc 100644
--- a/store/data-providers.js
+++ b/store/data-providers.js
@@ -2,30 +2,31 @@ import { observable, computed } from 'mobx'
import Params from './params'
-const schema = {
- query: {
+export const schema = {
+ q: {
default: '',
- convert: (value) => value.toString(),
+ convert: 'string',
+ validation: 'string',
},
size: {
default: 10,
validation: 'number',
- convert: (value) => Number(value),
+ convert: 'number',
},
action: {
default: null,
- validation: (value) => ['ADD', null].includes(value),
+ validation: ['ADD', null],
},
}
class DataProviders {
@observable data
- @observable params
+ params
@computed
get results() {
- return this.search(this.params.query)
+ return this.search(this.params.q)
}
constructor({ data, params = {} }) {
diff --git a/store/index.jsx b/store/index.jsx
index abd4b9a5..2041a3a2 100644
--- a/store/index.jsx
+++ b/store/index.jsx
@@ -1,4 +1,4 @@
-import React, { createContext, useMemo, useContext } from 'react'
+import React, { createContext, useMemo, useContext, useRef } from 'react'
import { useStaticRendering, observer } from 'mobx-react-lite'
import RootStore from './root'
@@ -10,17 +10,14 @@ let store = null
useStaticRendering(isServer)
export const initStore = (initialData) => {
- const storeInner = store ?? new RootStore()
- storeInner.init(initialData)
-
// on the server-side a new instance is created for each page request
// as we don't want to mix between users/requests, etc.
- if (isServer) return storeInner
+ if (isServer) return new RootStore(initialData)
// Create the store once in the client
- if (!store) store = storeInner
+ if (!store) store = new RootStore(initialData)
- return storeInner
+ return store
}
const StoreContext = createContext({})
@@ -28,7 +25,17 @@ export const StoreProvider = ({ children, store: storeInner }) => (
{children}
)
-export const useStore = () => useContext(StoreContext)
+export const useStore = (initialState) => {
+ const isInitialized = useRef(false)
+ const storeContext = useContext(StoreContext)
+
+ if (isInitialized.current === false && initialState) {
+ storeContext.extend(initialState)
+ isInitialized.current = true
+ }
+
+ return storeContext
+}
export const useInitStore = (initialState) =>
useMemo(() => initStore(initialState), [])
diff --git a/store/params.js b/store/params.js
index ba0b9ac6..3ec47c9c 100644
--- a/store/params.js
+++ b/store/params.js
@@ -1,66 +1,27 @@
-import { observable } from 'mobx'
+import { action, extendObservable } from 'mobx'
-const validate = (keySchema, value) => {
- if (typeof keySchema.validation === 'function')
- return keySchema.validation(value)
-
- if (keySchema.validation === 'number') return Number.isInteger(value)
-
- return true
-}
-
-const convert = (keySchema, value) => {
- if (typeof keySchema.convert === 'function') return keySchema.convert(value)
-
- return value
-}
+import { convertAndValidate } from 'utils/validation'
class Params {
- @observable paramsMap = new Map()
-
schema
constructor(params, schema) {
- this.paramsSchema = params
this.schema = schema
- const proxyParams = new Proxy(this.paramsMap, {
- set: (object, key, value) => {
- if (key in this.schema) {
- const keySchema = this.schema[key]
- let keyValue = value
-
- try {
- keyValue = convert(keySchema, keyValue)
- if (!validate(keySchema, keyValue)) keyValue = keySchema.default
- } catch (error) {
- keyValue = keySchema.default
- }
-
- object.set(key, keyValue)
- return true
- }
+ extendObservable(this, convertAndValidate({ schema, params }))
+ }
- if (process.env.NODE_ENV === 'development') {
- console.error(
- `Trying to access non-existing property ${key} on Params Proxy`
- )
- }
- return false
- },
- get: (object, key) => {
- if (typeof object[key] === 'function')
- return (...args) => Reflect.apply(object[key], object, args)
+ @action
+ changeParams({ ...params }) {
+ const { schema, ...assignedParams } = this
- return object.get(key)
+ const newParams = convertAndValidate({
+ schema: this.schema,
+ params: {
+ ...assignedParams,
+ ...params,
},
})
-
- Object.entries(schema).forEach(([key, value]) => {
- if (key in params) proxyParams[key] = params[key]
- else proxyParams[key] = value.default
- })
-
- return proxyParams
+ Object.assign(this, newParams)
}
}
diff --git a/store/root.js b/store/root.js
index 0b3028ac..396837d9 100644
--- a/store/root.js
+++ b/store/root.js
@@ -13,11 +13,17 @@ class Root {
@observable statistics
- init({ dataProviders = {}, statistics = {}, dataProvider = {} } = {}) {
- this.dataProviders = new DataProviders(dataProviders)
- this.dataProvider = new DataProvider(dataProvider)
+ constructor({ statistics }) {
this.statistics = statistics
}
+
+ extend({ dataProviders, dataProvider }) {
+ if (!this.dataProviders && dataProviders != null)
+ this.dataProviders = new DataProviders(dataProviders)
+
+ if (!this.dataProvider && dataProvider != null)
+ this.dataProvider = new DataProvider(dataProvider)
+ }
}
export default Root
diff --git a/templates/data-provider/hooks/index.js b/templates/data-provider/hooks/index.js
deleted file mode 100644
index 4e47465f..00000000
--- a/templates/data-provider/hooks/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export useStateToUrlEffect from './use-state-to-url-effect'
diff --git a/templates/data-provider/hooks/use-state-to-url-effect.js b/templates/data-provider/hooks/use-state-to-url-effect.js
deleted file mode 100644
index a4eef4e8..00000000
--- a/templates/data-provider/hooks/use-state-to-url-effect.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { useCallback, useEffect } from 'react'
-import { useRouter } from 'next/router'
-
-const useStateToUrlEffect = ({ id, q, from, size }) => {
- const router = useRouter()
- const setUrlParams = useCallback(
- (params) => {
- const url = {
- href: {
- pathname: '/data-providers/[data-provider-id]',
- query: {
- ...params,
- },
- },
-
- as: {
- pathname: `/data-providers/${id}`,
- query: {
- ...params,
- },
- },
- options: {
- shallow: true,
- },
- }
- router.push(url.href, url.as, url.options)
- },
- [router]
- )
-
- // reflect state to URL
- useEffect(() => {
- const params = {
- q,
- from,
- size,
- }
-
- setUrlParams(params)
- }, [q, from, size])
-}
-
-export default useStateToUrlEffect
diff --git a/templates/data-provider/index.jsx b/templates/data-provider/index.jsx
index 3c8d3093..98d7ee64 100644
--- a/templates/data-provider/index.jsx
+++ b/templates/data-provider/index.jsx
@@ -5,17 +5,21 @@ import DataProviderOutputsSearch from './search'
import ClaimCard from './claim-card'
import MapCard from './map-card'
import styles from './styles.module.css'
+import Pagination from './pagination'
import Search from 'modules/search-layout'
const DataProviderTemplate = ({
+ basePath,
outputs,
metadata,
query,
setQuery,
- loadSuggestions,
- loadOutputs,
loading,
+ isLoadingMore,
+ from,
+ size,
+ total,
}) => {
const placeholders = Array.from(Array(10).keys())
@@ -31,15 +35,14 @@ const DataProviderTemplate = ({
initQuery={query}
onQueryChanged={(q) => {
setQuery(q)
- loadOutputs()
}}
className={styles.search}
- loadSuggestions={loadSuggestions}
/>
- {loading
+ {loading && !isLoadingMore
? placeholders.map((key) => (
@@ -57,7 +60,7 @@ const DataProviderTemplate = ({
data={{
title,
author,
- publicationDate: year,
+ publicationDate: String(year),
thumbnailUrl: `//core.ac.uk/image/${id}/medium`,
metadataLink,
fullTextLink,
@@ -67,9 +70,27 @@ const DataProviderTemplate = ({
)
})}
+ {isLoadingMore &&
+ placeholders.map((key) => (
+
+
+
+ ))}
{outputs.length === 0 && (
No results found
)}
+
diff --git a/templates/data-provider/pagination.jsx b/templates/data-provider/pagination.jsx
new file mode 100644
index 00000000..0979634a
--- /dev/null
+++ b/templates/data-provider/pagination.jsx
@@ -0,0 +1,136 @@
+import React from 'react'
+import { Button, Icon } from '@oacore/design'
+import Link from 'next/link'
+import { classNames } from '@oacore/design/lib/utils'
+
+import styles from './styles.module.css'
+
+import Search from 'modules/search-layout'
+
+const getPaginationState = ({ from, size, total }) => ({
+ loadMore: size < total,
+ previous: from > 0,
+ next: from + size < total,
+ first: from > 0,
+ last: from + size < total,
+})
+
+const Pagination = ({ basePath, from, size, total, ...restParams }) => {
+ const hrefBuilder = ({ next, previous, first, last, loadMore }) => {
+ const params = {
+ from,
+ size,
+ ...restParams,
+ }
+
+ if (!loadMore) params.size = 10
+ if (next) params.from = from + size
+ if (previous) params.from = Math.max(from - 10, 0)
+ if (first) params.from = 0
+ if (last) params.from = total - 10
+ if (loadMore) params.size = Math.min(size + 10, total)
+
+ const urlParams = new URLSearchParams(params)
+ urlParams.sort()
+
+ return `${basePath}?${urlParams.toString()}`
+ }
+
+ const paginationState = getPaginationState({ from, size, total })
+
+ return (
+
+
+
+
+ Load more
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default Pagination
diff --git a/templates/data-provider/search.jsx b/templates/data-provider/search.jsx
index 3dde355a..3c3b56e0 100644
--- a/templates/data-provider/search.jsx
+++ b/templates/data-provider/search.jsx
@@ -1,25 +1,14 @@
-import React, { useCallback, useState } from 'react'
-import { throttle } from 'throttle-debounce'
+import React, { useState } from 'react'
import { Select } from '@oacore/design'
const DataProviderOutputsSearch = ({
initQuery,
- loadSuggestions,
onQueryChanged,
className,
}) => {
- const [suggestions, setSuggestions] = React.useState([])
const [value, setValue] = React.useState(initQuery || '')
const [isInitialised, setIsInitialised] = useState(false)
- const search = useCallback(
- throttle(500, false, async (searchTerm) => {
- const newSuggestions = await loadSuggestions(searchTerm)
- setSuggestions(newSuggestions)
- }),
- []
- )
-
const handleOnChange = (data) => {
// TODO: Consider this having in @oacore/design
// prevent firing callback in initialisation (we have the data prefetched
@@ -30,15 +19,10 @@ const DataProviderOutputsSearch = ({
}
onQueryChanged(data.value)
- search(data.value)
}
const handleOnInput = (data) => {
setValue(data.value)
-
- // if id doesn't exists it means user type own text
- // and didn't use suggestion
- if (!data.id) search(data.value)
}
return (
@@ -51,13 +35,7 @@ const DataProviderOutputsSearch = ({
placeholder="Search over 11K research outputs in Open Research Online"
prependIcon="#magnify"
className={className}
- >
- {suggestions.map((el) => (
-
- {el.title}
-
- ))}
-
+ />
)
}
diff --git a/templates/data-provider/styles.module.css b/templates/data-provider/styles.module.css
index af91a8d0..86533922 100644
--- a/templates/data-provider/styles.module.css
+++ b/templates/data-provider/styles.module.css
@@ -50,3 +50,38 @@
.no-results-find {
margin-top: 1rem;
}
+
+.pagination {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 2rem;
+}
+
+.pagination .arrow-pagination {
+ display: flex;
+ align-items: center;
+}
+
+.pagination .arrow {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 2.5rem;
+ height: 2.5rem;
+ padding: 0;
+ color: var(--gray-700);
+ border: none;
+ border-radius: 2rem;
+}
+
+.result-stats {
+ margin-right: 1rem;
+}
+
+.pagination .disabled {
+ text-decoration: none;
+ pointer-events: none;
+ cursor: not-allowed;
+ opacity: 0.5;
+}
diff --git a/templates/data-providers/result-card.jsx b/templates/data-providers/result-card.jsx
index 96723a39..072ddff8 100644
--- a/templates/data-providers/result-card.jsx
+++ b/templates/data-providers/result-card.jsx
@@ -17,11 +17,7 @@ const ResultCard = ({
-
+
{title}
diff --git a/utils/validation.js b/utils/validation.js
new file mode 100644
index 00000000..918cc277
--- /dev/null
+++ b/utils/validation.js
@@ -0,0 +1,54 @@
+const validate = (keySchema, keyValue) => {
+ if (!keySchema) return true
+
+ if (Array.isArray(keySchema.validation))
+ return keySchema.validation.includes(keyValue)
+
+ if (keySchema.validation === 'number') return Number.isInteger(keyValue)
+ if (keySchema.validation === 'string') return typeof keyValue === 'string'
+
+ return true
+}
+
+const convert = (keySchema, keyValue) => {
+ if (!keySchema || keyValue === undefined) return keyValue
+
+ if (keySchema.convert === 'number') return Number(keyValue)
+
+ if (keySchema.convert === 'string') return keyValue.toString()
+
+ return keyValue
+}
+
+const processKey = (keySchema, keyValue) => {
+ const convertedValue = convert(keySchema, keyValue)
+ if (validate(keySchema, convertedValue)) return convertedValue
+
+ const defaultValue = keySchema.default
+ if (defaultValue === undefined) {
+ throw Error(
+ `Value for schema ${keySchema} is invalid. Default value is not specified`
+ )
+ } else return defaultValue
+}
+
+export const convertAndValidate = ({ schema, params, key = null }) => {
+ // request for validate specific key
+ if (key) return processKey(schema[key], params[key])
+
+ const schemaKeys = Object.keys(schema)
+
+ // for the simplicity and our use-case we take all keys in schema
+ // and process them, i.e. if params object contains any key
+ // which is not specified in schema it gets removed.
+ // At the same time if key we apply default values
+ // if key is in schema but not in params
+ return Object.fromEntries(
+ schemaKeys.map((schemaKey) => [
+ schemaKey,
+ processKey(schema[schemaKey], params[schemaKey]),
+ ])
+ )
+}
+
+export default convertAndValidate
From 660169101d5a4db9aa9d5e0ee41c5c372f7fedfb Mon Sep 17 00:00:00 2001
From: Jozef Harag <32jojo32@gmail.com>
Date: Wed, 11 Nov 2020 08:47:56 +0100
Subject: [PATCH 4/6] Temporarily hide claim card
---
templates/data-provider/index.jsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/templates/data-provider/index.jsx b/templates/data-provider/index.jsx
index 98d7ee64..1ead5233 100644
--- a/templates/data-provider/index.jsx
+++ b/templates/data-provider/index.jsx
@@ -2,7 +2,7 @@ import React from 'react'
import { SearchResult } from '@oacore/design'
import DataProviderOutputsSearch from './search'
-import ClaimCard from './claim-card'
+// import ClaimCard from './claim-card'
import MapCard from './map-card'
import styles from './styles.module.css'
import Pagination from './pagination'
@@ -94,7 +94,7 @@ const DataProviderTemplate = ({
-
+ {/* */}
)
From 5d7500c6250cb378b4e947e195da87be878d1a0c Mon Sep 17 00:00:00 2001
From: Jozef Harag <32jojo32@gmail.com>
Date: Mon, 16 Nov 2020 12:18:35 +0100
Subject: [PATCH 5/6] Fix params
---
pages/data-providers/index.jsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pages/data-providers/index.jsx b/pages/data-providers/index.jsx
index 9598b912..84f53c18 100644
--- a/pages/data-providers/index.jsx
+++ b/pages/data-providers/index.jsx
@@ -65,9 +65,9 @@ const SearchPage = observe(({ initialState }) => {
)
const setQuery = useCallback(
(q) => {
- dataProviders.params.query = q
+ dataProviders.params.q = q
},
- [dataProviders.params.query]
+ [dataProviders.params.q]
)
const handleUrlChange = useCallback(
(event) => {
From 2ea320748bbd06a22442bb1cbb9958f0e929a7b8 Mon Sep 17 00:00:00 2001
From: Jozef Harag <32jojo32@gmail.com>
Date: Mon, 16 Nov 2020 15:27:47 +0100
Subject: [PATCH 6/6] Add meta tags
---
pages/data-providers/[data-provider-id].jsx | 47 ++++++++++++++-------
pages/data-providers/index.jsx | 2 +-
2 files changed, 32 insertions(+), 17 deletions(-)
diff --git a/pages/data-providers/[data-provider-id].jsx b/pages/data-providers/[data-provider-id].jsx
index 584dc075..f931ab61 100644
--- a/pages/data-providers/[data-provider-id].jsx
+++ b/pages/data-providers/[data-provider-id].jsx
@@ -1,5 +1,6 @@
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'
@@ -96,22 +97,36 @@ const DataProviderPage = observe(({ initialState }) => {
])
return (
- {
- 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}
- />
+ <>
+
+
+ {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}
+ />
+ >
)
})
diff --git a/pages/data-providers/index.jsx b/pages/data-providers/index.jsx
index 84f53c18..13d24d14 100644
--- a/pages/data-providers/index.jsx
+++ b/pages/data-providers/index.jsx
@@ -82,7 +82,7 @@ const SearchPage = observe(({ initialState }) => {
{query ? `${query} - ` : ''}Data providers search
{/* eslint-disable react/no-danger */}