[monorepo][lg] - start of monorepo
This commit is contained in:
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"presets": ["next/babel"]
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaFeatures: { jsx: true },
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:jsx-a11y/recommended',
|
||||
// Prettier plugin and recommended rules
|
||||
'prettier/@typescript-eslint',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
rules: {
|
||||
// Include .prettierrc.js rules
|
||||
'prettier/prettier': ['error', {}, { usePrettierrc: true }],
|
||||
'react/prop-types': 'off',
|
||||
'react/react-in-jsx-scope': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/ban-ts-ignore': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'jsx-a11y/label-has-associated-control': [
|
||||
'error',
|
||||
{
|
||||
labelComponents: [],
|
||||
labelAttributes: [],
|
||||
controlComponents: [],
|
||||
assert: 'either',
|
||||
depth: 25,
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
};
|
||||
1
examples/ckan/.gitignore
vendored
1
examples/ckan/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
.vercel
|
||||
@@ -1,24 +0,0 @@
|
||||
node_modules/
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
/out/
|
||||
|
||||
# yarn
|
||||
yarn-error.log
|
||||
yarn.lock
|
||||
|
||||
# sass
|
||||
.sass-cache
|
||||
|
||||
# misc
|
||||
sandbox/*
|
||||
.env
|
||||
.staging.env
|
||||
.nyc_output/*
|
||||
.DS_Store
|
||||
|
||||
public/
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
semi: true,
|
||||
trailingComma: 'es5',
|
||||
singleQuote: true,
|
||||
printWidth: 79,
|
||||
tabWidth: 2,
|
||||
useTabs: false,
|
||||
};
|
||||
@@ -1,306 +0,0 @@
|
||||
<h1 align="center">
|
||||
|
||||
🌀 Portal.JS<br/>
|
||||
The javascript framework for<br/>
|
||||
data portals
|
||||
|
||||
</h1>
|
||||
|
||||
🌀 `Portal` is a framework for rapidly building rich data portal frontends using a modern frontend approach (javascript, React, SSR).
|
||||
|
||||
`Portal` assumes a "decoupled" approach where the frontend is a separate service from the backend and interacts with backend(s) via an API. It can be used with any backend and has out of the box support for [CKAN][]. `portal` is built in Javascript and React on top of the popular [Next.js][] framework.
|
||||
|
||||
[ckan]: https://ckan.org/
|
||||
[next.js]: https://nextjs.com/
|
||||
|
||||
Live DEMO: https://catalog-portal-js.vercel.app
|
||||
|
||||
## Features
|
||||
|
||||
- 🗺️ Unified sites: present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. wordpress) with a common internal API.
|
||||
- 👩💻 Developer friendly: built with familiar frontend tech Javascript, React etc
|
||||
- 🔋 Batteries included: Full set of portal components out of the box e.g. catalog search, dataset showcase, blog etc.
|
||||
- 🎨 Easy to theme and customize: installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.
|
||||
- 🧱 Extensible: quickly extend and develop/import your own React components
|
||||
- 📝 Well documented: full set of documentation plus the documentation of NextJS and Apollo.
|
||||
|
||||
### For developers
|
||||
|
||||
- 🏗 Build with modern, familiar frontend tech such as Javascript and React.
|
||||
- 🚀 NextJS framework: so everything in NextJS for free React, SSR, static site generation, huge number of examples and integrations etc.
|
||||
- SSR => unlimited number of pages, SEO etc whilst still using React.
|
||||
- Static Site Generation (SSG) (good for small sites) => ultra-simple deployment, great performance and lighthouse scores etc
|
||||
- 📋 Typescript support
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Setup
|
||||
|
||||
Install a recent version of Node. You'll need Node 10.13 or later.
|
||||
|
||||
### Create a Portal app
|
||||
|
||||
To create a Portal app, open your terminal, cd into the directory you'd like to create the app in, and run the following command:
|
||||
|
||||
```console
|
||||
npm init portal-app my-data-portal
|
||||
```
|
||||
|
||||
> NB: Under the hood, this uses the tool called create-next-app, which bootstraps a Next.js app for you. It uses this template through the --example flag.
|
||||
>
|
||||
> If it doesn’t work, please open an issue.
|
||||
|
||||
## Guide
|
||||
|
||||
### Styling 🎨
|
||||
|
||||
We use Tailwind as a CSS framework. Take a look at `/styles/index.css` to see what we're importing from Tailwind bundle. You can also configure Tailwind using `tailwind.config.js` file.
|
||||
|
||||
Have a look at Next.js support of CSS and ways of writing CSS:
|
||||
|
||||
https://nextjs.org/docs/basic-features/built-in-css-support
|
||||
|
||||
### Backend
|
||||
|
||||
So far the app is running with mocked data behind. You can connect CMS and DMS backends easily via environment variables:
|
||||
|
||||
```console
|
||||
$ export DMS=http://ckan:5000
|
||||
$ export CMS=http://myblog.wordpress.com
|
||||
```
|
||||
|
||||
> Note that we don't yet have implementations for the following CKAN features:
|
||||
>
|
||||
> - Activities
|
||||
> - Auth
|
||||
> - Groups
|
||||
> - Facets
|
||||
|
||||
### Routes
|
||||
|
||||
These are the default routes set up in the "starter" app.
|
||||
|
||||
- Home `/`
|
||||
- Search `/search`
|
||||
- Dataset `/@org/dataset`
|
||||
- Resource `/@org/dataset/r/resource`
|
||||
- Organization `/@org`
|
||||
- Collection (aka group in CKAN) (?) - suggest to merge into org
|
||||
- Static pages, eg, `/about` etc. from CMS or can do it without external CMS, e.g., in Next.js
|
||||
|
||||
### New Routes
|
||||
|
||||
TODO
|
||||
|
||||
### Data fetching
|
||||
|
||||
We use Apollo client which allows us to query data with GraphQL. We have setup CKAN API for the demo (it uses demo.ckan.org as DMS):
|
||||
|
||||
http://portal.datopian1.now.sh/
|
||||
|
||||
Note that we don't have Apollo Server but we connect CKAN API using [`apollo-link-rest`](https://www.apollographql.com/docs/link/links/rest/) module. You can see how it works in [lib/apolloClient.ts](https://github.com/datopian/portal/blob/master/lib/apolloClient.ts) and then have a look at [pages/\_app.tsx](https://github.com/datopian/portal/blob/master/pages/_app.tsx).
|
||||
|
||||
For development/debugging purposes, we suggest installing the Chrome extension - https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm.
|
||||
|
||||
#### i18n configuration
|
||||
|
||||
Portal.js is configured by default to support both `English` and `French` subpath for language translation. But for subsequent users, this following steps can be used to configure i18n for other languages;
|
||||
|
||||
1. Update `next.config.js`, to add more languages to the i18n locales
|
||||
|
||||
```js
|
||||
i18n: {
|
||||
locales: ['en', 'fr', 'nl-NL'], // add more language to the list
|
||||
defaultLocale: 'en', // set the default language to use
|
||||
},
|
||||
```
|
||||
|
||||
2. Create a folder for the language in `locales` --> `locales/en-Us`
|
||||
|
||||
3. In the language folder, different namespace files (json) can be created for each translation. For the `index.js` use-case, I named it `common.json`
|
||||
|
||||
```json
|
||||
// locales/en/common.json
|
||||
{
|
||||
"title" : "Portal js in English",
|
||||
}
|
||||
|
||||
// locales/fr/common.json
|
||||
{
|
||||
"title" : "Portal js in French",
|
||||
}
|
||||
```
|
||||
|
||||
4. To use on pages using Server-side Props.
|
||||
|
||||
```js
|
||||
import { loadNamespaces } from './_app';
|
||||
import useTranslation from 'next-translate/useTranslation';
|
||||
|
||||
const Home: React.FC = ()=> {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>{t(`common:title`)}</div> // we use common and title base on the common.json data
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
||||
........ ........
|
||||
return {
|
||||
props : {
|
||||
_ns: await loadNamespaces(['common'], locale),
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
5. Go to the browser and view the changes using language subpath like this `http://localhost:3000` and `http://localhost:3000/fr`. **Note** The subpath also activate chrome language Translator
|
||||
|
||||
#### Pre-fetch data in the server-side
|
||||
|
||||
When visiting a dataset page, you may want to fetch the dataset metadata in the server-side. To do so, you can use `getServerSideProps` function from NextJS:
|
||||
|
||||
```javascript
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { initializeApollo } from '../lib/apolloClient';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
const QUERY = gql`
|
||||
query dataset($id: String) {
|
||||
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
|
||||
result
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
...
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
await apolloClient.query({
|
||||
query: QUERY,
|
||||
variables: {
|
||||
id: 'my-dataset'
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
This would fetch the data from DMS and save it in the Apollo cache so that we can query it again from the components.
|
||||
|
||||
#### Access data from a component
|
||||
|
||||
Consider situation when rendering a component for org info on the dataset page. We already have pre-fetched dataset metadata that includes `organization` property with attributes such as `name`, `title` etc. We can now query only organization part for our `Org` component:
|
||||
|
||||
```javascript
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_ORG_QUERY = gql`
|
||||
query dataset($id: String) {
|
||||
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
|
||||
result {
|
||||
organization {
|
||||
name
|
||||
title
|
||||
image_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Org({ variables }) {
|
||||
const { loading, error, data } = useQuery(
|
||||
GET_ORG_QUERY,
|
||||
{
|
||||
variables: { id: 'my-dataset' }
|
||||
}
|
||||
);
|
||||
|
||||
...
|
||||
|
||||
const { organization } = data.dataset.result;
|
||||
|
||||
return (
|
||||
<>
|
||||
{organization ? (
|
||||
<>
|
||||
<img
|
||||
src={
|
||||
organization.image_url
|
||||
}
|
||||
className="h-5 w-5 mr-2 inline-block"
|
||||
/>
|
||||
<Link href={`/@${organization.name}`}>
|
||||
<a className="font-semibold text-primary underline">
|
||||
{organization.title || organization.name}
|
||||
</a>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Add a new data source
|
||||
|
||||
TODO
|
||||
|
||||
## Developers
|
||||
|
||||
### Boot the local instance
|
||||
|
||||
Install the dependencies:
|
||||
|
||||
```bash
|
||||
yarn # or npm i
|
||||
```
|
||||
|
||||
Boot the demo portal:
|
||||
|
||||
```console
|
||||
$ yarn dev # or npm run dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) to see the home page 🎉
|
||||
|
||||
You can start editing the page by modifying `/pages/index.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
### Tests
|
||||
|
||||
We use Jest for running tests:
|
||||
|
||||
```bash
|
||||
yarn test # or npm run test
|
||||
|
||||
# turn on watching
|
||||
yarn test --watch
|
||||
```
|
||||
|
||||
We use Cypress tests as well
|
||||
|
||||
```
|
||||
yarn run e2e
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
- Language: Javascript
|
||||
- Framework: NextJS - https://nextjs.org/
|
||||
- Data layer API: GraphQL using Apollo. So controllers access data using GraphQL “gatsby like”
|
||||
|
||||
### Key Pages
|
||||
|
||||
See https://tech.datopian.com/frontend/
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import Form from '../../../components/search/Form';
|
||||
|
||||
const useRouter = jest.spyOn(require('next/router'), 'useRouter');
|
||||
|
||||
test('📸 of Form component with empty', () => {
|
||||
useRouter.mockImplementationOnce(() => ({
|
||||
query: { search: '', sort: '' },
|
||||
}));
|
||||
|
||||
const { container } = render(<Form />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('📸 of Form component with query', () => {
|
||||
useRouter.mockImplementationOnce(() => ({
|
||||
query: { search: 'gdp', sort: '' },
|
||||
}));
|
||||
|
||||
const { container } = render(<Form />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`📸 of Form component with empty 1`] = `
|
||||
<div>
|
||||
<form
|
||||
class="items-center"
|
||||
>
|
||||
<input
|
||||
aria-label="Search"
|
||||
class="inline-block w-1/2 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
id="search2"
|
||||
name="search"
|
||||
placeholder="GDP data..."
|
||||
type="search"
|
||||
/>
|
||||
<button
|
||||
class="inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
|
||||
type="button"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`📸 of Form component with query 1`] = `
|
||||
<div>
|
||||
<form
|
||||
class="items-center"
|
||||
>
|
||||
<input
|
||||
aria-label="Search"
|
||||
class="inline-block w-1/2 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
id="search2"
|
||||
name="search"
|
||||
placeholder="GDP data..."
|
||||
type="search"
|
||||
/>
|
||||
<button
|
||||
class="inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
|
||||
type="button"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,15 +0,0 @@
|
||||
type LinkProps = {
|
||||
url: string;
|
||||
format: any;
|
||||
};
|
||||
|
||||
const CustomLink: React.FC<LinkProps> = ({ url, format }: LinkProps) => (
|
||||
<a
|
||||
href={url}
|
||||
className="bg-white hover:bg-gray-200 border text-black font-semibold py-2 px-4 rounded"
|
||||
>
|
||||
{format}
|
||||
</a>
|
||||
);
|
||||
|
||||
export default CustomLink;
|
||||
@@ -1,17 +0,0 @@
|
||||
const ErrorMessage: React.FC<{ message: any }> = ({ message }) => {
|
||||
return (
|
||||
<aside>
|
||||
{message}
|
||||
<style jsx>{`
|
||||
aside {
|
||||
padding: 1.5em;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
background-color: red;
|
||||
}
|
||||
`}</style>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default ErrorMessage;
|
||||
@@ -1,53 +0,0 @@
|
||||
interface TableProps {
|
||||
columns: Array<any>;
|
||||
data: Array<any>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Table: React.FC<TableProps> = ({ columns, data, className }) => {
|
||||
return (
|
||||
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||
<div className="py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8">
|
||||
<div className="shadow overflow-hidden border-b border-gray-200 sm:rounded-lg">
|
||||
<table
|
||||
className={`min-w-full divide-y divide-gray-200 ${className}`}
|
||||
>
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
{columns.map(({ key, name }) => (
|
||||
<th
|
||||
key={key}
|
||||
scope="col"
|
||||
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||||
>
|
||||
{name}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{data.map((item) => (
|
||||
<tr key={item.id}>
|
||||
{columns.map(({ key, render }) => (
|
||||
<td
|
||||
key={key}
|
||||
className="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900"
|
||||
>
|
||||
{(render &&
|
||||
typeof render === 'function' &&
|
||||
render(item)) ||
|
||||
item[key] ||
|
||||
''}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Table;
|
||||
@@ -1,5 +0,0 @@
|
||||
import Table from './Table';
|
||||
import ErrorMessage from './Error';
|
||||
import CustomLink from './CustomLink';
|
||||
|
||||
export { Table, ErrorMessage, CustomLink };
|
||||
@@ -1,83 +0,0 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import * as timeago from 'timeago.js';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_DATASET_QUERY } from '../../graphql/queries';
|
||||
|
||||
const About: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_DATASET_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading dataset." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.dataset;
|
||||
|
||||
const stats = [
|
||||
{ name: 'Files', stat: result.resources.length },
|
||||
{ name: 'Size', stat: result.size || 'N/A' },
|
||||
{
|
||||
name: 'Formats',
|
||||
stat: result.resources.map((item) => item.format).join(', '),
|
||||
},
|
||||
{
|
||||
name: 'Created',
|
||||
stat: result.created && timeago.format(result.created),
|
||||
},
|
||||
{
|
||||
name: 'Updated',
|
||||
stat: result.updated && timeago.format(result.updated),
|
||||
},
|
||||
{
|
||||
name: 'Licenses',
|
||||
stat: result.licenses?.length
|
||||
? result.licenses.map((item, index) => (
|
||||
<a
|
||||
className="text-yellow-600"
|
||||
href={item.path || '#'}
|
||||
title={item.title || ''}
|
||||
key={index}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
))
|
||||
: 'N/A',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-5 border-b border-gray-200">
|
||||
<h1 className="text-3xl leading-6 font-medium text-gray-900">
|
||||
{result.title || result.name}
|
||||
</h1>
|
||||
<p className="mt-2 max-w-4xl text-sm text-gray-500">
|
||||
{result.description || 'This dataset does not have a description.'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="px-4 py-5 bg-white shadow rounded-lg overflow-hidden sm:p-6"
|
||||
>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-1 text-3xl font-semibold text-gray-900">
|
||||
{item.stat}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_DATASET_QUERY } from '../../graphql/queries';
|
||||
import { Org } from 'portal';
|
||||
|
||||
const OrgInfo: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_DATASET_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading dataset." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { organization } = data.dataset.result;
|
||||
|
||||
return <Org organization={organization} />;
|
||||
};
|
||||
|
||||
export default OrgInfo;
|
||||
@@ -1,76 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
/* eslint-disable react/display-name */
|
||||
import Link from 'next/link';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import * as timeago from 'timeago.js';
|
||||
import { Table, ErrorMessage } from '../_shared';
|
||||
import { GET_DATASET_QUERY } from '../../graphql/queries';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'File',
|
||||
key: 'file',
|
||||
render: ({ name: resName, title, parentName }) => (
|
||||
<Link href={`${parentName}/r/${resName}`}>
|
||||
<a className="underline">{title || resName}</a>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'Format',
|
||||
key: 'format',
|
||||
},
|
||||
{
|
||||
name: 'Created',
|
||||
key: 'created',
|
||||
render: ({ created }) => timeago.format(created),
|
||||
},
|
||||
{
|
||||
name: 'Updated',
|
||||
key: 'updated',
|
||||
render: ({ updated }) => timeago.format(updated),
|
||||
},
|
||||
{
|
||||
name: 'Link',
|
||||
key: 'link',
|
||||
render: ({ name: resName, parentName }) => (
|
||||
<Link href={`${parentName}/r/${resName}`}>
|
||||
<a className="underline">Preview</a>
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const Resources: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_DATASET_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading dataset." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.dataset;
|
||||
|
||||
return (
|
||||
<div className="mt-12">
|
||||
<div className="pb-5 border-b border-gray-200">
|
||||
<h1 className="text-2xl leading-6 font-medium text-gray-900">
|
||||
Data files
|
||||
</h1>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
data={result.resources.map((resource) => ({
|
||||
...resource,
|
||||
parentName: result.name,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Resources;
|
||||
@@ -1,61 +0,0 @@
|
||||
const Footer: React.FC = () => {
|
||||
const navigation = {
|
||||
main: [
|
||||
{ name: 'Blog', href: '/blog' },
|
||||
{ name: 'Search', href: '/search' },
|
||||
{ name: 'Docs', href: '/docs' },
|
||||
],
|
||||
social: [
|
||||
{
|
||||
name: 'GitHub',
|
||||
href: 'https://github.com/datopian/portal.js',
|
||||
// eslint-disable-next-line
|
||||
icon: (props) => (
|
||||
<svg fill="currentColor" viewBox="0 0 24 24" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
<footer className="bg-white">
|
||||
<div className="max-w-7xl mx-auto py-12 px-4 overflow-hidden sm:px-6 lg:px-8">
|
||||
<nav
|
||||
className="-mx-5 -my-2 flex flex-wrap justify-center"
|
||||
aria-label="Footer"
|
||||
>
|
||||
{navigation.main.map((item) => (
|
||||
<div key={item.name} className="px-5 py-2">
|
||||
<a
|
||||
href={item.href}
|
||||
className="text-base text-gray-500 hover:text-gray-900"
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<div className="mt-8 flex justify-center space-x-6">
|
||||
{navigation.social.map((item) => (
|
||||
<a
|
||||
key={item.name}
|
||||
href={item.href}
|
||||
className="text-gray-400 hover:text-gray-500"
|
||||
>
|
||||
<span className="sr-only">{item.name}</span>
|
||||
<item.icon className="h-6 w-6" aria-hidden="true" />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -1,7 +0,0 @@
|
||||
import Template from './HeroTemplate';
|
||||
|
||||
const Hero: React.FC = () => {
|
||||
return <Template />;
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
@@ -1,130 +0,0 @@
|
||||
import useTranslation from 'next-translate/useTranslation';
|
||||
import SearchForm from '../search/Form';
|
||||
|
||||
export default function Example() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="relative bg-white overflow-hidden">
|
||||
<div
|
||||
className="hidden lg:block lg:absolute lg:inset-0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
className="absolute top-0 left-1/2 transform translate-x-64 -translate-y-8"
|
||||
width={640}
|
||||
height={784}
|
||||
fill="none"
|
||||
viewBox="0 0 640 784"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="9ebea6f4-a1f5-4d96-8c4e-4c2abf658047"
|
||||
x={118}
|
||||
y={0}
|
||||
width={20}
|
||||
height={20}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={4}
|
||||
height={4}
|
||||
className="text-gray-200"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
y={72}
|
||||
width={640}
|
||||
height={640}
|
||||
className="text-gray-50"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x={118}
|
||||
width={404}
|
||||
height={784}
|
||||
fill="url(#9ebea6f4-a1f5-4d96-8c4e-4c2abf658047)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div className="relative pt-6 pb-16 sm:pb-24 lg:pb-32">
|
||||
<main className="mt-16 mx-auto max-w-7xl px-4 sm:mt-24 sm:px-6 lg:mt-32">
|
||||
<div className="lg:grid lg:grid-cols-12 lg:gap-8">
|
||||
<div className="sm:text-center md:max-w-2xl md:mx-auto lg:col-span-6 lg:text-left">
|
||||
<h1>
|
||||
<span className="block text-sm font-semibold uppercase tracking-wide text-gray-500 sm:text-base lg:text-sm xl:text-base">
|
||||
Quality Data ready to Integrate
|
||||
</span>
|
||||
<span className="mt-1 block text-4xl tracking-tight font-extrabold sm:text-5xl xl:text-6xl">
|
||||
<span className="block text-gray-900">Find and Share</span>
|
||||
<span className="block text-indigo-600">Quality Data</span>
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mt-3 text-base text-gray-500 sm:mt-5 sm:text-xl lg:text-lg xl:text-xl">
|
||||
{t(`common:description`)}
|
||||
</p>
|
||||
<div className="mt-8 sm:max-w-lg sm:mx-auto sm:text-center lg:text-left lg:mx-0">
|
||||
<SearchForm />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-12 relative sm:max-w-lg sm:mx-auto lg:mt-0 lg:max-w-none lg:mx-0 lg:col-span-6 lg:flex lg:items-center">
|
||||
<svg
|
||||
className="absolute top-0 left-1/2 transform -translate-x-1/2 -translate-y-8 scale-75 origin-top sm:scale-100 lg:hidden"
|
||||
width={640}
|
||||
height={784}
|
||||
fill="none"
|
||||
viewBox="0 0 640 784"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="4f4f415c-a0e9-44c2-9601-6ded5a34a13e"
|
||||
x={118}
|
||||
y={0}
|
||||
width={20}
|
||||
height={20}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={4}
|
||||
height={4}
|
||||
className="text-gray-200"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
y={72}
|
||||
width={640}
|
||||
height={640}
|
||||
className="text-gray-50"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<rect
|
||||
x={118}
|
||||
width={404}
|
||||
height={784}
|
||||
fill="url(#4f4f415c-a0e9-44c2-9601-6ded5a34a13e)"
|
||||
/>
|
||||
</svg>
|
||||
<div className="relative mx-auto w-full rounded-lg shadow-lg lg:max-w-md">
|
||||
<img
|
||||
className="w-full"
|
||||
src="/images/banner.svg"
|
||||
alt="banner_img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import Template from './NavTemplate';
|
||||
|
||||
const NavBar: React.FC = () => {
|
||||
const navMenu = [
|
||||
{ title: 'Blog', path: '/blog' },
|
||||
{ title: 'Search', path: '/search' },
|
||||
{ title: 'Docs', path: 'http://tech.datopian.com/frontend/' },
|
||||
{ title: 'GitHub', path: 'https://github.com/datopian/portal.js' },
|
||||
];
|
||||
|
||||
return <Template menu={navMenu} logo={'/images/logo.svg'} />;
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
@@ -1,118 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import { SearchIcon } from '@heroicons/react/solid';
|
||||
import { MenuIcon, XIcon } from '@heroicons/react/outline';
|
||||
|
||||
const NavBar: React.FC<{ menu: any; logo: string }> = ({ menu, logo }) => {
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
router.push({
|
||||
pathname: '/search',
|
||||
query: { q: searchQuery },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Disclosure as="nav" className="bg-white shadow">
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-4 lg:px-8">
|
||||
<div className="flex justify-between h-16">
|
||||
<div className="flex px-2 lg:px-0">
|
||||
<div className="flex-shrink-0 flex items-center">
|
||||
<a href="/">
|
||||
<img
|
||||
className="block lg:hidden h-8 w-auto"
|
||||
src={logo}
|
||||
alt="Portal.js"
|
||||
/>
|
||||
<img
|
||||
className="hidden lg:block h-8 w-auto"
|
||||
src={logo}
|
||||
alt="Portal.js"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="hidden lg:ml-6 lg:flex lg:space-x-8">
|
||||
{/* Current: "border-indigo-500 text-gray-900", Default: "border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700" */}
|
||||
{menu.map((item, index) => (
|
||||
<a
|
||||
key={'menu-link' + index}
|
||||
href={item.path}
|
||||
className="border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center px-2 lg:ml-6 lg:justify-end">
|
||||
<div className="max-w-lg w-full lg:max-w-xs">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
Search
|
||||
</label>
|
||||
<div className="relative">
|
||||
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
|
||||
<SearchIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={(e) => handleSubmit(e)}
|
||||
className="items-center"
|
||||
>
|
||||
<input
|
||||
id="search"
|
||||
type="search"
|
||||
name="search"
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
}}
|
||||
placeholder="Search"
|
||||
aria-label="Search"
|
||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center lg:hidden">
|
||||
{/* Mobile menu button */}
|
||||
<Disclosure.Button className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500">
|
||||
<span className="sr-only">Open main menu</span>
|
||||
{open ? (
|
||||
<XIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<MenuIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
)}
|
||||
</Disclosure.Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Disclosure.Panel className="lg:hidden">
|
||||
<div className="pt-2 pb-3 space-y-1">
|
||||
{/* Current: "bg-indigo-50 border-indigo-500 text-indigo-700", Default: "border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800" */}
|
||||
{menu.map((item, index) => (
|
||||
<a
|
||||
key={'mobile-menu-link' + index}
|
||||
href={item.path}
|
||||
className="border-transparent text-gray-600 hover:bg-gray-50 hover:border-gray-300 hover:text-gray-800 block pl-3 pr-4 py-2 border-l-4 text-base font-medium"
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</Disclosure.Panel>
|
||||
</>
|
||||
)}
|
||||
</Disclosure>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
@@ -1,96 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import {
|
||||
CollectionIcon,
|
||||
PresentationChartBarIcon,
|
||||
} from '@heroicons/react/solid';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { SEARCH_QUERY } from '../../graphql/queries';
|
||||
|
||||
const RecentDataset: React.FC = () => {
|
||||
const { loading, error, data } = useQuery(SEARCH_QUERY, {
|
||||
variables: {
|
||||
sort: 'metadata_created desc',
|
||||
rows: 3,
|
||||
},
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading search results." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.search;
|
||||
|
||||
return (
|
||||
<section className="my-10 p-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
Recent Datasets
|
||||
</h3>
|
||||
<ul className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{result.results.map((dataset) => (
|
||||
<li
|
||||
key={dataset.id}
|
||||
className="col-span-1 bg-white rounded-lg shadow divide-y divide-gray-200"
|
||||
>
|
||||
<div className="w-full flex items-center justify-between p-6 space-x-6">
|
||||
<div className="flex-1 truncate">
|
||||
<div className="flex items-center space-x-3">
|
||||
<h3 className="text-gray-900 text-sm font-medium truncate">
|
||||
{dataset.title || dataset.name}
|
||||
</h3>
|
||||
<span className="flex-shrink-0 inline-block px-2 py-0.5 text-green-800 text-xs font-medium bg-green-100 rounded-full">
|
||||
dataset
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-gray-500 text-sm truncate">
|
||||
{dataset.organization.title || dataset.organization.name}
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
className="w-10 h-10 bg-gray-300 rounded-full flex-shrink-0"
|
||||
src={
|
||||
dataset.organization.image ||
|
||||
'https://datahub.io/static/img/datahub-cube-edited.svg'
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="-mt-px flex divide-x divide-gray-200">
|
||||
<div className="w-0 flex-1 flex">
|
||||
<a
|
||||
href={`/@${dataset.organization.name}`}
|
||||
className="relative -mr-px w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-gray-700 font-medium border border-transparent rounded-bl-lg hover:text-gray-500"
|
||||
>
|
||||
<CollectionIcon
|
||||
className="w-5 h-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="ml-3">Organization</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="-ml-px w-0 flex-1 flex">
|
||||
<a
|
||||
href={`/@${dataset.organization.name}/${dataset.name}`}
|
||||
className="relative w-0 flex-1 inline-flex items-center justify-center py-4 text-sm text-gray-700 font-medium border border-transparent rounded-br-lg hover:text-gray-500"
|
||||
>
|
||||
<PresentationChartBarIcon
|
||||
className="w-5 h-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="ml-3">Preview</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecentDataset;
|
||||
@@ -1,54 +0,0 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_STATS_QUERY } from '../../graphql/queries';
|
||||
|
||||
const Stats: React.FC = () => {
|
||||
const { loading, error, data } = useQuery(GET_STATS_QUERY, {
|
||||
variables: {},
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading search results." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const stats = [
|
||||
{ name: 'Datasets', stat: data.datasets.result.count },
|
||||
{
|
||||
name: 'Organizations',
|
||||
stat: data.orgs.result ? data.orgs.result.length : 0,
|
||||
},
|
||||
{
|
||||
name: 'Groups',
|
||||
stat: data.groups.result ? data.groups.result.length : 0,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h3 className="text-lg leading-6 font-medium text-gray-900">
|
||||
DataHub Stats
|
||||
</h3>
|
||||
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="px-4 py-5 bg-white shadow rounded-lg overflow-hidden sm:p-6"
|
||||
>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-1 text-3xl font-semibold text-gray-900">
|
||||
{item.stat}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Stats;
|
||||
@@ -1,154 +0,0 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import * as timeago from 'timeago.js';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_ORG_QUERY } from '../../graphql/queries';
|
||||
|
||||
const About: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_ORG_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading dataset." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.org;
|
||||
|
||||
return (
|
||||
<div className="relative bg-white py-16 sm:py-4">
|
||||
<div className="lg:mx-auto lg:max-w-7xl lg:px-8 lg:grid lg:grid-cols-2 lg:gap-24 lg:items-start">
|
||||
<div className="relative sm:py-16 lg:py-0">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="hidden sm:block lg:absolute lg:inset-y-0 lg:right-0 lg:w-screen"
|
||||
>
|
||||
<div className="absolute inset-y-0 right-1/2 w-full bg-gray-50 rounded-r-3xl lg:right-72" />
|
||||
<svg
|
||||
className="absolute top-8 left-1/2 -ml-3 lg:-right-8 lg:left-auto lg:top-12"
|
||||
width={404}
|
||||
height={392}
|
||||
fill="none"
|
||||
viewBox="0 0 404 392"
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id="02f20b47-fd69-4224-a62a-4c9de5c763f7"
|
||||
x={0}
|
||||
y={0}
|
||||
width={20}
|
||||
height={20}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<rect
|
||||
x={0}
|
||||
y={0}
|
||||
width={4}
|
||||
height={4}
|
||||
className="text-gray-200"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
<rect
|
||||
width={404}
|
||||
height={392}
|
||||
fill="url(#02f20b47-fd69-4224-a62a-4c9de5c763f7)"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="relative mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:px-0 lg:max-w-none lg:py-20">
|
||||
{/* Testimonial card*/}
|
||||
<div className="relative pt-64 pb-10 rounded-2xl shadow-xl overflow-hidden">
|
||||
<img
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
src={
|
||||
result.image ||
|
||||
'https://datahub.io/static/img/datahub-cube-edited.svg'
|
||||
}
|
||||
alt={result.title || result.name}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-indigo-500 mix-blend-multiply" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-indigo-600 via-indigo-600 opacity-90" />
|
||||
<div className="relative px-8">
|
||||
<blockquote className="mt-8">
|
||||
<div className="relative text-lg font-medium text-white md:flex-grow">
|
||||
<p className="relative">
|
||||
{result.description ||
|
||||
"This organization doesn't have a description."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer className="mt-4">
|
||||
<p className="text-base font-semibold text-indigo-200">
|
||||
{result.title}
|
||||
</p>
|
||||
</footer>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-md px-4 sm:max-w-3xl sm:px-6 lg:px-0">
|
||||
{/* Content area */}
|
||||
<div className="pt-12 sm:pt-16 lg:pt-20">
|
||||
<h1 className="text-3xl text-gray-900 font-extrabold tracking-tight sm:text-4xl">
|
||||
{result.title || result.name}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Stats section */}
|
||||
<div className="mt-10">
|
||||
<dl className="grid grid-cols-2 gap-x-4 gap-y-8">
|
||||
<div className="border-t-2 border-gray-100 pt-6">
|
||||
<dt className="text-base font-medium text-gray-500">
|
||||
Datasets
|
||||
</dt>
|
||||
<dd className="text-3xl font-extrabold tracking-tight text-gray-900">
|
||||
{result.total}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="border-t-2 border-gray-100 pt-6">
|
||||
<dt className="text-base font-medium text-gray-500">Users</dt>
|
||||
<dd className="text-3xl font-extrabold tracking-tight text-gray-900">
|
||||
{result.users && result.users.length}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="border-t-2 border-gray-100 pt-6">
|
||||
<dt className="text-base font-medium text-gray-500">
|
||||
Followers
|
||||
</dt>
|
||||
<dd className="text-3xl font-extrabold tracking-tight text-gray-900">
|
||||
{result.followers}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="border-t-2 border-gray-100 pt-6">
|
||||
<dt className="text-base font-medium text-gray-500">
|
||||
Created
|
||||
</dt>
|
||||
<dd className="text-3xl font-extrabold tracking-tight text-gray-900">
|
||||
{timeago.format(result.created)}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<div className="mt-10">
|
||||
<a
|
||||
href={`/search?fq=organization:${result.name}`}
|
||||
className="text-base font-medium text-indigo-600"
|
||||
>
|
||||
{' '}
|
||||
Datasets by {result.title || result.name}{' '}
|
||||
<span aria-hidden="true">→</span>{' '}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -1,78 +0,0 @@
|
||||
/* eslint-disable react/display-name */
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import * as timeago from 'timeago.js';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_DATASET_QUERY } from '../../graphql/queries';
|
||||
|
||||
const About: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_DATASET_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading dataset." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.dataset;
|
||||
const resource = result.resources.find(
|
||||
(item) => item.name === variables.resource
|
||||
);
|
||||
|
||||
const stats = [
|
||||
{ name: 'File', stat: resource.title || resource.name },
|
||||
{ name: 'Description', stat: resource.description || 'N/A' },
|
||||
{ name: 'Size', stat: resource.size || 'N/A' },
|
||||
{
|
||||
name: 'Created',
|
||||
stat: resource.created && timeago.format(resource.created),
|
||||
},
|
||||
{
|
||||
name: 'Updated',
|
||||
stat: resource.updated && timeago.format(resource.updated),
|
||||
},
|
||||
{ name: 'Download', stat: resource.path, link: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="pb-5 border-b border-gray-200">
|
||||
<h1 className="text-3xl leading-6 font-medium text-gray-900">
|
||||
{result.title || result.name}
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mb-5">
|
||||
<dl className="mt-5 grid grid-cols-1 gap-5 sm:grid-cols-3">
|
||||
{stats.map((item) => (
|
||||
<div
|
||||
key={item.name}
|
||||
className="px-4 py-5 bg-white shadow rounded-lg overflow-hidden sm:p-6"
|
||||
>
|
||||
<dt className="text-sm font-medium text-gray-500 truncate">
|
||||
{item.name}
|
||||
</dt>
|
||||
<dd className="mt-1 text-3xl font-semibold text-gray-900">
|
||||
{item.link ? (
|
||||
<a
|
||||
href={item.stat}
|
||||
className="underline"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{resource.format || 'Click'}
|
||||
</a>
|
||||
) : (
|
||||
item.stat
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default About;
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { PlotlyChart, Table } from 'portal';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_DATASTORE_DATA } from '../../graphql/queries';
|
||||
|
||||
const Preview: React.FC<{ view: any }> = ({ view }) => {
|
||||
const variables = {
|
||||
resource_id: view.resources,
|
||||
};
|
||||
const { loading, error, data } = useQuery(GET_DATASTORE_DATA, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading dataset." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.datastore;
|
||||
|
||||
// Assuming for now it is always a table view
|
||||
const columns = result.fields.map((field) => ({
|
||||
field: field.id,
|
||||
headerName: field.id,
|
||||
}));
|
||||
|
||||
return <Table columns={columns} data={result.records} />;
|
||||
};
|
||||
|
||||
export default Preview;
|
||||
@@ -1,24 +0,0 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import Preview from './Preview';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_RESOURCE_VIEWS } from '../../graphql/queries';
|
||||
|
||||
const View: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_RESOURCE_VIEWS, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading dataset." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.views;
|
||||
const previews = result.map((view) => <Preview view={view} key={view.id} />);
|
||||
|
||||
return <>{previews}</>;
|
||||
};
|
||||
|
||||
export default View;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
const SearchForm: React.FC = () => {
|
||||
const router = useRouter();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
router.push({
|
||||
pathname: '/search',
|
||||
query: { q: searchQuery },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => handleSubmit(e)} className="items-center">
|
||||
<input
|
||||
id="search2"
|
||||
type="search"
|
||||
name="search"
|
||||
onChange={(e) => {
|
||||
setSearchQuery(e.target.value);
|
||||
}}
|
||||
placeholder="GDP data..."
|
||||
aria-label="Search"
|
||||
className="inline-block w-1/2 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSubmit(false)}
|
||||
type="button"
|
||||
className="inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchForm;
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import Link from 'next/link';
|
||||
import * as timeago from 'timeago.js';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { SEARCH_QUERY } from '../../graphql/queries';
|
||||
|
||||
const List: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(SEARCH_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading search results." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
const { result } = data.search;
|
||||
return (
|
||||
<ul className="divide-y divide-gray-200">
|
||||
{result.results.map((dataset, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="relative bg-white py-5 px-4 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600"
|
||||
>
|
||||
<div className="flex justify-between space-x-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link
|
||||
href={`/@${
|
||||
dataset.organization ? dataset.organization.name : 'dataset'
|
||||
}/${dataset.name}`}
|
||||
>
|
||||
{/*
|
||||
Linting fails because href is required for anchor tags and it
|
||||
ignores NextJS's Link API. Please, see this issue for details:
|
||||
https://github.com/vercel/next.js/issues/5533
|
||||
*/}
|
||||
{/* eslint-disable-next-line */}
|
||||
<a className="block focus:outline-none">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{dataset.title}
|
||||
</p>
|
||||
<p className="text-sm text-gray-500 truncate">
|
||||
{dataset.organization
|
||||
? dataset.organization.title
|
||||
: 'dataset'}{' '}
|
||||
/ {dataset.name}
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<time
|
||||
dateTime={dataset.metadata_modified}
|
||||
className="flex-shrink-0 whitespace-nowrap text-sm text-gray-500"
|
||||
>
|
||||
Updated {timeago.format(dataset.metadata_modified)}
|
||||
</time>
|
||||
</div>
|
||||
<div className="mt-1">
|
||||
<p className="line-clamp-2 text-sm text-gray-600">
|
||||
{dataset.description ||
|
||||
"This dataset doesn't have a description."}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
};
|
||||
|
||||
export default List;
|
||||
@@ -1,23 +0,0 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_TOTAL_COUNT_QUERY } from '../../graphql/queries';
|
||||
import { ItemTotal } from 'portal';
|
||||
|
||||
const Total: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_TOTAL_COUNT_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading search results." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.search;
|
||||
|
||||
return <ItemTotal count={result.count} />;
|
||||
};
|
||||
|
||||
export default Total;
|
||||
@@ -1,39 +0,0 @@
|
||||
import parse from 'html-react-parser';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_POSTS_QUERY } from '../../graphql/queries';
|
||||
|
||||
const List: React.FC = () => {
|
||||
const { loading, error, data } = useQuery(GET_POSTS_QUERY, {
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading search results." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { posts, found } = data.posts;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
|
||||
{found} posts found
|
||||
</h1>
|
||||
{posts.map((post, index) => (
|
||||
<div key={index}>
|
||||
<a
|
||||
href={`/blog/${post.slug}`}
|
||||
className="text-2xl font-semibold text-primary my-6 inline-block"
|
||||
>
|
||||
{parse(post.title)}
|
||||
</a>
|
||||
<p>{parse(post.excerpt)}</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default List;
|
||||
@@ -1,32 +0,0 @@
|
||||
import parse from 'html-react-parser';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_PAGE_QUERY } from '../../graphql/queries';
|
||||
|
||||
const Page: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_PAGE_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading search results." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { title, content, modified, featured_image } = data.page;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mb-6">Edited: {modified}</p>
|
||||
<img src={featured_image} className="mb-6" alt="featured_img" />
|
||||
<div>{parse(content)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,32 +0,0 @@
|
||||
import parse from 'html-react-parser';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_PAGE_QUERY } from '../../graphql/queries';
|
||||
|
||||
const Post: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { loading, error, data } = useQuery(GET_PAGE_QUERY, {
|
||||
variables,
|
||||
// Setting this value to true will make the component rerender when
|
||||
// the "networkStatus" changes, so we are able to know if it is fetching
|
||||
// more data
|
||||
notifyOnNetworkStatusChange: true,
|
||||
});
|
||||
|
||||
if (error) return <ErrorMessage message="Error loading search results." />;
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { title, content, modified, featured_image } = data.page;
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
|
||||
{title}
|
||||
</h1>
|
||||
<p className="mb-6">Edited: {modified}</p>
|
||||
<img src={featured_image} className="mb-6" alt="featured_img" />
|
||||
<div>{parse(content)}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Post;
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
process() {
|
||||
return 'module.exports = {};';
|
||||
},
|
||||
getCacheKey() {
|
||||
return 'cssTransform';
|
||||
},
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000"
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
describe('Test Home Page', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('renders the hero title', () => {
|
||||
cy.contains('Find and Share');
|
||||
});
|
||||
|
||||
it('checks that a search form exists', () => {
|
||||
cy.get('form').contains('Search');
|
||||
});
|
||||
|
||||
// it('submits the search form', () => {
|
||||
// cy.get('form').find('[type="text"]').type('gdp');
|
||||
// cy.get('form').submit();
|
||||
// cy.url().should('include', '/search?q=gdp&sort=');
|
||||
// cy.get('.text-3xl').and('contain.text', '1 results found');
|
||||
// });
|
||||
|
||||
it('shows the recent datasets', () => {
|
||||
cy.contains('Recent Datasets');
|
||||
});
|
||||
|
||||
it('returns the expected number of recent datasets', () => {
|
||||
cy.get('.recent')
|
||||
.find('div')
|
||||
.should(($div) => {
|
||||
expect($div).to.have.length.of.at.least(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
describe('Test Search Page', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit('/search');
|
||||
});
|
||||
|
||||
it('has a search form', () => {
|
||||
cy.contains('form');
|
||||
cy.contains('Search');
|
||||
});
|
||||
|
||||
// it('should return a search result', () => {
|
||||
// cy.get('form').find('[type="text"]').type('gdp');
|
||||
// cy.get('form').submit();
|
||||
// cy.url().should('include', 'search?q=gdp&sort=');
|
||||
// cy.get('.text-3xl').should('have.text', '1 results found');
|
||||
// cy.get('.text-xl > .text-primary').should(
|
||||
// 'have.text',
|
||||
// 'Country, Regional and World GDP (Gross Domestic Product)'
|
||||
// );
|
||||
// });
|
||||
});
|
||||
@@ -1,21 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||
@@ -1,20 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_RESOURCE_VIEWS = gql`
|
||||
query views($id: String) {
|
||||
views(id: $id) @rest(type: "Response", path: "resource_view_list?{args}") {
|
||||
result @type(name: "View") {
|
||||
id
|
||||
title
|
||||
description
|
||||
resources: resource_id
|
||||
viewType: view_type
|
||||
series
|
||||
group
|
||||
type: graph_type
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_DATASTORE_DATA = gql`
|
||||
query datastore($resource_id: String) {
|
||||
datastore(resource_id: $resource_id)
|
||||
@rest(type: "Response", path: "datastore_search?{args}") {
|
||||
result {
|
||||
count: total
|
||||
fields
|
||||
records
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_ORG_QUERY = gql`
|
||||
query org($id: String) {
|
||||
org(id: $id) @rest(type: "Response", path: "organization_show?{args}") {
|
||||
result {
|
||||
name
|
||||
title
|
||||
description
|
||||
image: image_url
|
||||
created
|
||||
total: package_count
|
||||
users
|
||||
followers: num_followers
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_DATASET_QUERY = gql`
|
||||
query dataset($id: String) {
|
||||
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
|
||||
result {
|
||||
name
|
||||
title
|
||||
size
|
||||
created: metadata_created
|
||||
updated: metadata_modified
|
||||
resources {
|
||||
id
|
||||
name
|
||||
title
|
||||
description
|
||||
path: url
|
||||
format
|
||||
created
|
||||
updated: last_modified
|
||||
size
|
||||
}
|
||||
organization {
|
||||
name
|
||||
title
|
||||
image: image_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const SEARCH_QUERY = gql`
|
||||
query search($q: String, $sort: String, $rows: Int, $start: Int) {
|
||||
search(q: $q, sort: $sort, rows: $rows, start: $start)
|
||||
@rest(type: "Search", path: "package_search?{args}") {
|
||||
result {
|
||||
count
|
||||
results {
|
||||
name
|
||||
title
|
||||
updated: metadata_modified
|
||||
organization {
|
||||
name
|
||||
title
|
||||
description
|
||||
image: image_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_TOTAL_COUNT_QUERY = gql`
|
||||
query search($q: String, $sort: String) {
|
||||
search(q: $q, sort: $sort)
|
||||
@rest(type: "Search", path: "package_search?{args}") {
|
||||
result {
|
||||
count
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_STATS_QUERY = gql`
|
||||
query stats {
|
||||
datasets @rest(type: "Search", path: "package_search?rows=0") {
|
||||
result {
|
||||
count
|
||||
}
|
||||
}
|
||||
orgs @rest(type: "Search", path: "organization_list") {
|
||||
result
|
||||
}
|
||||
groups @rest(type: "Search", path: "group_list") {
|
||||
result
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_POSTS_QUERY = gql`
|
||||
query posts {
|
||||
posts @rest(type: "Posts", path: "", endpoint: "wordpress-posts") {
|
||||
found
|
||||
posts
|
||||
meta
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_PAGE_QUERY = gql`
|
||||
query page($slug: String) {
|
||||
page(slug: $slug)
|
||||
@rest(type: "Page", path: "{args.slug}", endpoint: "wordpress") {
|
||||
title
|
||||
content
|
||||
excerpt
|
||||
slug
|
||||
date
|
||||
modified
|
||||
featured_image
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const GET_POST_QUERY = gql`
|
||||
query post($slug: String) {
|
||||
post(slug: $slug)
|
||||
@rest(type: "Post", path: "{args.slug}", endpoint: "wordpress") {
|
||||
title
|
||||
content
|
||||
excerpt
|
||||
slug
|
||||
date
|
||||
modified
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,29 +0,0 @@
|
||||
module.exports = {
|
||||
collectCoverageFrom: [
|
||||
'**/*.{js,jsx,ts,tsx}',
|
||||
'!**/*.d.ts',
|
||||
'!**/node_modules/**',
|
||||
'!**/config/**',
|
||||
'!**/coverage/**',
|
||||
'!**/**.config.js**',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'/.next/',
|
||||
'/jest.config.js/',
|
||||
'/tailwind.config.js/',
|
||||
'<rootDir>/postcss.config.js',
|
||||
],
|
||||
transform: {
|
||||
'^.+\\.(js|jsx|ts|tsx)$': 'babel-jest',
|
||||
'^.+\\.css$': '<rootDir>/config/jest/cssTransform.js',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'^.+\\.module\\.(css|sass|scss)$',
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'^.+\\.module\\.(css|sass|scss)$': 'identity-obj-proxy',
|
||||
},
|
||||
};
|
||||
@@ -1,92 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import getConfig from 'next/config';
|
||||
import { ApolloClient } from 'apollo-client';
|
||||
import {
|
||||
InMemoryCache,
|
||||
NormalizedCache,
|
||||
NormalizedCacheObject,
|
||||
} from 'apollo-cache-inmemory';
|
||||
import { RestLink } from 'apollo-link-rest';
|
||||
|
||||
let apolloClient:
|
||||
| ApolloClient<NormalizedCache>
|
||||
| ApolloClient<NormalizedCacheObject>;
|
||||
|
||||
const restLink = new RestLink({
|
||||
uri: getConfig().publicRuntimeConfig.DMS + '/api/3/action/',
|
||||
endpoints: {
|
||||
wordpress: `https://public-api.wordpress.com/rest/v1.1/sites/${
|
||||
getConfig().publicRuntimeConfig.CMS
|
||||
}/posts/slug:`,
|
||||
'wordpress-posts': `https://public-api.wordpress.com/rest/v1.1/sites/${
|
||||
getConfig().publicRuntimeConfig.CMS
|
||||
}/posts/`,
|
||||
},
|
||||
typePatcher: {
|
||||
Search: (data: any): any => {
|
||||
if (data.result != null) {
|
||||
data.result.__typename = 'SearchResponse';
|
||||
|
||||
if (data.result.results != null) {
|
||||
data.result.results = data.result.results.map((item) => {
|
||||
if (item.organization != null) {
|
||||
item.organization.__typename = 'Organization';
|
||||
}
|
||||
return { __typename: 'Package', ...item };
|
||||
});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
Response: (data: any): any => {
|
||||
if (data.result != null) {
|
||||
data.result.__typename = 'Package';
|
||||
if (data.result.organization != null) {
|
||||
data.result.organization.__typename = 'Organization';
|
||||
}
|
||||
|
||||
if (data.result.resources != null) {
|
||||
data.result.resources = data.result.resources.map((item) => {
|
||||
return { __typename: 'Resource', ...item };
|
||||
});
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
function createApolloClient() {
|
||||
return new ApolloClient({
|
||||
link: restLink,
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
}
|
||||
|
||||
export function initializeApollo(
|
||||
initialState = null
|
||||
): ApolloClient<NormalizedCache> | ApolloClient<NormalizedCacheObject> {
|
||||
const _apolloClient:
|
||||
| ApolloClient<NormalizedCache>
|
||||
| ApolloClient<NormalizedCacheObject> =
|
||||
apolloClient ?? createApolloClient();
|
||||
|
||||
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
|
||||
// gets hydrated here
|
||||
if (initialState) {
|
||||
_apolloClient.cache.restore(initialState);
|
||||
}
|
||||
// For SSG and SSR always create a new Apollo Client
|
||||
if (typeof window === 'undefined') return _apolloClient;
|
||||
// Create the Apollo Client once in the client
|
||||
if (!apolloClient) apolloClient = _apolloClient;
|
||||
|
||||
return _apolloClient;
|
||||
}
|
||||
|
||||
export function useApollo(
|
||||
initialState = null
|
||||
): ApolloClient<NormalizedCache> | ApolloClient<NormalizedCacheObject> {
|
||||
const store = useMemo(() => initializeApollo(initialState), [initialState]);
|
||||
return store;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"title": "Portal in English",
|
||||
"description": "At Datahub, we have over thousands of datasets for free and a Premium Data Service for additional or customised data with guaranteed updates."
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"title": "Portal in french",
|
||||
"description": "Chez Datahub, nous avons plus de milliers d'ensembles de données gratuitement et un service de données Premium pour des données supplémentaires ou personnalisées avec des mises à jour garanties."
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
const nock = require('nock');
|
||||
|
||||
const gdp = {
|
||||
name: 'gdp',
|
||||
title: 'Country, Regional and World GDP (Gross Domestic Product)',
|
||||
notes:
|
||||
'Country, regional and world GDP in current US Dollars ($). Regional means collections of countries e.g. Europe & Central Asia. Data is sourced from the World Bank and turned into a standard normalized CSV.',
|
||||
resources: [
|
||||
{
|
||||
name: 'gdp',
|
||||
id: 'gdp',
|
||||
title: 'GDP data',
|
||||
format: 'csv',
|
||||
created: '2019-03-07T12:00:36.273495',
|
||||
last_modified: '2020-05-07T12:00:36.273495',
|
||||
datastore_active: false,
|
||||
url: 'http://mock.filestore/gdp.csv',
|
||||
},
|
||||
],
|
||||
organization: {
|
||||
title: 'World Bank',
|
||||
name: 'world-bank',
|
||||
description:
|
||||
'The World Bank is an international financial institution that provides loans and grants to the governments of poorer countries for the purpose of pursuing capital projects.',
|
||||
created: '2019-03-07T11:51:13.758844',
|
||||
image_url:
|
||||
'https://github.com/datahq/frontend/raw/master/public/img/avatars/world-bank.jpg',
|
||||
},
|
||||
metadata_created: '2019-03-07T11:56:19.696257',
|
||||
metadata_modified: '2019-03-07T12:03:58.817280',
|
||||
size: '',
|
||||
};
|
||||
|
||||
const population = {
|
||||
name: 'population',
|
||||
title: 'World population data',
|
||||
notes:
|
||||
'Population figures for countries, regions (e.g. Asia) and the world. Data comes originally from World Bank and has been converted into standard CSV.',
|
||||
resources: [
|
||||
{
|
||||
name: 'population',
|
||||
id: 'population',
|
||||
title: 'Population data',
|
||||
format: 'csv',
|
||||
created: '2019-03-07T12:00:36.273495',
|
||||
last_modified: '2020-05-07T12:00:36.273495',
|
||||
datastore_active: true,
|
||||
url: 'http://mock.filestore/population.csv',
|
||||
},
|
||||
],
|
||||
organization: {
|
||||
title: 'World Bank',
|
||||
name: 'world-bank',
|
||||
description:
|
||||
'The World Bank is an international financial institution that provides loans and grants to the governments of poorer countries for the purpose of pursuing capital projects.',
|
||||
created: '2019-03-07T11:51:13.758844',
|
||||
image_url:
|
||||
'https://github.com/datahq/frontend/raw/master/public/img/avatars/world-bank.jpg',
|
||||
},
|
||||
};
|
||||
|
||||
module.exports.initMocks = function () {
|
||||
// Uncomment this line if you want to record API calls
|
||||
// nock.recorder.rec()
|
||||
|
||||
// "package_search" mocks
|
||||
nock('http://mock.ckan/api/3/action', { encodedQueryParams: true })
|
||||
.persist()
|
||||
// 1. Call without query.
|
||||
.get('/package_search?')
|
||||
.reply(200, {
|
||||
success: true,
|
||||
result: {
|
||||
count: 2,
|
||||
sort: 'score desc, metadata_modified desc',
|
||||
facets: {},
|
||||
results: [gdp, population],
|
||||
search_facets: {},
|
||||
},
|
||||
})
|
||||
// 2. Call with `q=gdp` query.
|
||||
.get('/package_search?q=gdp')
|
||||
.reply(200, {
|
||||
success: true,
|
||||
result: {
|
||||
count: 1,
|
||||
sort: 'score desc, metadata_modified desc',
|
||||
facets: {},
|
||||
results: [gdp],
|
||||
search_facets: {},
|
||||
},
|
||||
})
|
||||
// 3. Call for recent packages.
|
||||
.get('/package_search?sort=metadata_created%20desc&rows=3')
|
||||
.reply(200, {
|
||||
success: true,
|
||||
result: {
|
||||
count: 2,
|
||||
sort: 'metadata_created desc',
|
||||
facets: {},
|
||||
results: [gdp, population],
|
||||
search_facets: {},
|
||||
},
|
||||
});
|
||||
|
||||
// "package_show" mocks
|
||||
nock('http://mock.ckan/api/3/action', { encodedQueryParams: true })
|
||||
.persist()
|
||||
.get('/package_show?id=gdp')
|
||||
.reply(200, {
|
||||
success: true,
|
||||
result: gdp,
|
||||
})
|
||||
.get('/package_show?id=population')
|
||||
.reply(200, {
|
||||
success: true,
|
||||
result: population,
|
||||
});
|
||||
|
||||
// "datastore_search" mocks
|
||||
nock('http://mock.ckan/api/3/action', { encodedQueryParams: true })
|
||||
.persist()
|
||||
.get('/datastore_search?resource_id=population')
|
||||
.reply(200, {
|
||||
success: true,
|
||||
result: {
|
||||
records: [
|
||||
{
|
||||
'Country Code': 'ARB',
|
||||
'Country Name': 'Arab World',
|
||||
Value: 92197753,
|
||||
Year: 1960,
|
||||
},
|
||||
{
|
||||
'Country Code': 'ARB',
|
||||
'Country Name': 'Arab World',
|
||||
Value: 94724510,
|
||||
Year: 1961,
|
||||
},
|
||||
{
|
||||
'Country Code': 'ARB',
|
||||
'Country Name': 'Arab World',
|
||||
Value: 97334442,
|
||||
Year: 1962,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// Filestore mocks
|
||||
nock('http://mock.filestore', { encodedQueryParams: true })
|
||||
.persist()
|
||||
.get('/gdp.csv')
|
||||
.reply(200, 'a,b,c\n1,2,3\n4,5,6\n');
|
||||
};
|
||||
6
examples/ckan/next-env.d.ts
vendored
6
examples/ckan/next-env.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/types/global" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
@@ -1,48 +0,0 @@
|
||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
||||
|
||||
module.exports = (phase, { defaultConfig }) => {
|
||||
const dms = process.env.DMS;
|
||||
const cms = process.env.CMS;
|
||||
if (phase === PHASE_DEVELOPMENT_SERVER) {
|
||||
if (dms) {
|
||||
console.log('\nYou are running the app in dev mode 🌀');
|
||||
console.log(
|
||||
'Did you know that you can use mocked CKAN API? Just unset your `DMS` env var.'
|
||||
);
|
||||
console.log('Happy coding ☀️\n');
|
||||
} else {
|
||||
const mocks = require('./mocks');
|
||||
mocks.initMocks();
|
||||
console.log(
|
||||
'\nYou have not defined any DMS API so we are activating the mocks ⚠️'
|
||||
);
|
||||
console.log(
|
||||
'If you wish to run against your CKAN API, you can set `DMS` env var.'
|
||||
);
|
||||
console.log(
|
||||
'For example, to run against demo ckan site: `DMS=https://demo.ckan.org`\n'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
i18n: {
|
||||
locales: ['en', 'fr', 'nl-NL'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
DMS: dms ? dms.replace(/\/?$/, '') : 'http://mock.ckan',
|
||||
CMS: cms ? cms.replace(/\/?$/, '') : 'oddk.home.blog',
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
i18n: {
|
||||
locales: ['en', 'fr', 'nl-NL'],
|
||||
defaultLocale: 'en',
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
DMS: dms ? dms.replace(/\/?$/, '') : 'https://demo.dev.datopian.com',
|
||||
CMS: cms ? cms.replace(/\/?$/, '') : 'oddk.home.blog',
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,95 +0,0 @@
|
||||
{
|
||||
"name": "portal-main",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/datopian/portal#readme",
|
||||
"directories": {
|
||||
"test": "__tests__"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:ci": "cypress run --config video=false",
|
||||
"e2e": "cypress run",
|
||||
"format": "prettier --single-quote --write .",
|
||||
"pre-commit": "yarn lint:fix && prettier --single-quote --write",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"lint:fix": "yarn lint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.0.2",
|
||||
"@apollo/react-hooks": "^3.1.5",
|
||||
"@fullhuman/postcss-purgecss": "^2.3.0",
|
||||
"@headlessui/react": "^1.4.0",
|
||||
"@heroicons/react": "^1.0.3",
|
||||
"apollo-cache-inmemory": "^1.6.6",
|
||||
"apollo-client": "^2.6.10",
|
||||
"apollo-link": "^1.2.14",
|
||||
"apollo-link-rest": "0.7.3",
|
||||
"apollo-server-testing": "^2.16.0",
|
||||
"bytes": "^3.1.0",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-15": "^1.4.1",
|
||||
"graphql": "^15.1.0",
|
||||
"graphql-anywhere": "^4.2.7",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"html-react-parser": "^0.13.0",
|
||||
"markdown-it": "^11.0.0",
|
||||
"next": "^11.1.3",
|
||||
"next-translate": "^0.20.2",
|
||||
"portal": "https://github.com/datopian/portal.js",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router": "^5.2.0",
|
||||
"slugify": "^1.4.0",
|
||||
"timeago.js": "^4.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.3.3",
|
||||
"@tailwindcss/typography": "^0.4.1",
|
||||
"@testing-library/jest-dom": "^5.8.0",
|
||||
"@testing-library/react": "^10.0.4",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/react": "^16.9.35",
|
||||
"@typescript-eslint/eslint-plugin": "^3.8.0",
|
||||
"@typescript-eslint/parser": "^3.8.0",
|
||||
"autoprefixer": "^10.3.1",
|
||||
"babel-jest": "^26.0.1",
|
||||
"babel-plugin-graphql-tag": "^2.5.0",
|
||||
"cypress": "^6.6.0",
|
||||
"eslint": "^7.6.0",
|
||||
"eslint-config-prettier": "^6.11.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.3.1",
|
||||
"eslint-plugin-prettier": "^3.1.4",
|
||||
"eslint-plugin-react": "^7.20.5",
|
||||
"eslint-plugin-react-hooks": "^4.0.8",
|
||||
"husky": ">=4",
|
||||
"jest": "^26.1.0",
|
||||
"lerna": "^3.22.1",
|
||||
"lint-staged": ">=10",
|
||||
"nock": "^12.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss": "^8.3.6",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-preset-env": "6.7.0",
|
||||
"prettier": "^2.0.5",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"tailwindcss": "2.0.2",
|
||||
"typescript": "^4.3.2"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx,css,html,md}": "yarn pre-commit"
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import Head from 'next/head';
|
||||
import { initializeApollo } from '../../../lib/apolloClient';
|
||||
import Nav from '../../../components/home/Nav';
|
||||
import About from '../../../components/dataset/About';
|
||||
import Org from '../../../components/dataset/Org';
|
||||
import Resources from '../../../components/dataset/Resources';
|
||||
import Footer from '../../../components/home/Footer';
|
||||
import { GET_DATASET_QUERY } from '../../../graphql/queries';
|
||||
|
||||
const Dataset: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { data, loading } = useQuery(GET_DATASET_QUERY, { variables });
|
||||
|
||||
if (loading) return <div>Loading</div>;
|
||||
const { result } = data.dataset;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Portal | {result.title || result.name}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className="p-8">
|
||||
<About variables={variables} />
|
||||
<Resources variables={variables} />
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const apolloClient = initializeApollo();
|
||||
const variables = {
|
||||
id: context.query.dataset,
|
||||
};
|
||||
|
||||
await apolloClient.query({
|
||||
query: GET_DATASET_QUERY,
|
||||
variables,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
variables,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Dataset;
|
||||
@@ -1,56 +0,0 @@
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import Head from 'next/head';
|
||||
import { initializeApollo } from '../../../../../lib/apolloClient';
|
||||
import Nav from '../../../../../components/home/Nav';
|
||||
import About from '../../../../../components/resource/About';
|
||||
import View from '../../../../../components/resource/View';
|
||||
import Footer from '../../../../../components/home/Footer';
|
||||
import { GET_DATASET_QUERY } from '../../../../../graphql/queries';
|
||||
|
||||
const Resource: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { data, loading } = useQuery(GET_DATASET_QUERY, { variables });
|
||||
|
||||
if (loading) return <div>Loading</div>;
|
||||
const result = data.dataset.result;
|
||||
// Find right resource
|
||||
const resource = result.resources.find(
|
||||
(item) => item.name === variables.resource
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Portal | {resource.title || resource.name}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className="p-6">
|
||||
<About variables={variables} />
|
||||
<View variables={{ id: resource.id }} />
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const apolloClient = initializeApollo();
|
||||
const variables = {
|
||||
id: context.query.dataset,
|
||||
resource: context.query.resource,
|
||||
};
|
||||
|
||||
await apolloClient.query({
|
||||
query: GET_DATASET_QUERY,
|
||||
variables,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
variables,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Resource;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import Head from 'next/head';
|
||||
import { initializeApollo } from '../../lib/apolloClient';
|
||||
import Nav from '../../components/home/Nav';
|
||||
import About from '../../components/org/About';
|
||||
import Footer from '../../components/home/Footer';
|
||||
import { GET_ORG_QUERY } from '../../graphql/queries';
|
||||
|
||||
const Org: React.FC<{ variables: any }> = ({ variables }) => {
|
||||
const { data, loading } = useQuery(GET_ORG_QUERY, { variables });
|
||||
|
||||
if (loading) return <div>Loading</div>;
|
||||
|
||||
const { result } = data.org;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Portal | {result.title || result.name}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Nav />
|
||||
<About variables={variables} />
|
||||
<Footer />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const apolloClient = initializeApollo();
|
||||
const variables = {
|
||||
id: (context.query.org as string).replace('@', ''),
|
||||
};
|
||||
|
||||
await apolloClient.query({
|
||||
query: GET_ORG_QUERY,
|
||||
variables,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
variables,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Org;
|
||||
@@ -1,55 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ApolloProvider } from '@apollo/react-hooks';
|
||||
import { useApollo } from '../lib/apolloClient';
|
||||
import { DEFAULT_THEME } from '../themes';
|
||||
import { applyTheme } from '../themes/utils';
|
||||
import I18nProvider from 'next-translate/I18nProvider';
|
||||
import { useRouter } from 'next/router';
|
||||
import '../styles/globals.css';
|
||||
|
||||
interface I8nObject {
|
||||
[property: string]: any;
|
||||
}
|
||||
|
||||
export async function loadNamespaces(
|
||||
namespaces: string[],
|
||||
lang: string
|
||||
): Promise<I8nObject> {
|
||||
const res = {};
|
||||
for (const ns of namespaces) {
|
||||
res[ns] = await import(`../locales/${lang}/${ns}.json`).then(
|
||||
(m) => m.default
|
||||
);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
type Props = {
|
||||
Component: any;
|
||||
pageProps: any;
|
||||
};
|
||||
|
||||
const MyApp: React.FC<Props> = ({ Component, pageProps }) => {
|
||||
const apolloClient = useApollo(pageProps.initialApolloState);
|
||||
const [theme] = useState(DEFAULT_THEME); // setTheme
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* We can switch theme.
|
||||
* e.g. setTheme('primary');
|
||||
* */
|
||||
|
||||
applyTheme(theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<I18nProvider lang={router.locale} namespaces={pageProps._ns}>
|
||||
<ApolloProvider client={apolloClient}>
|
||||
<Component {...pageProps} />
|
||||
</ApolloProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyApp;
|
||||
@@ -1,34 +0,0 @@
|
||||
import Document, { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
const GA_TRACKING_ID = 'G-NX72GYFHFS';
|
||||
export default class CustomDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<script
|
||||
async
|
||||
src="https://www.googletagmanager.com/gtag/js?id=G-NX72GYFHFS"
|
||||
/>
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', '${GA_TRACKING_ID}',{
|
||||
page_path: window.location.pathname,
|
||||
});
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
</body>
|
||||
<NextScript />
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { initializeApollo } from '../../../lib/apolloClient';
|
||||
import Nav from '../../../components/home/Nav';
|
||||
import Post from '../../../components/static/Post';
|
||||
import { GET_POST_QUERY } from '../../../graphql/queries';
|
||||
|
||||
type Props = {
|
||||
variables: any;
|
||||
};
|
||||
|
||||
const PostItem: React.FC<Props> = ({ variables }) => (
|
||||
<>
|
||||
<Head>
|
||||
<title>Portal | {variables.slug}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className="p-6">
|
||||
<Post variables={variables} />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const variables = {
|
||||
slug: context.query.post,
|
||||
};
|
||||
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
await apolloClient.query({
|
||||
query: GET_POST_QUERY,
|
||||
variables,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
variables,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default PostItem;
|
||||
@@ -1,35 +0,0 @@
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { initializeApollo } from '../../lib/apolloClient';
|
||||
import Nav from '../../components/home/Nav';
|
||||
import List from '../../components/static/List';
|
||||
import { GET_POSTS_QUERY } from '../../graphql/queries';
|
||||
|
||||
const PostList: React.FC = () => (
|
||||
<>
|
||||
<Head>
|
||||
<title>Portal | Blog</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className="p-6">
|
||||
<List />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async () => {
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
await apolloClient.query({
|
||||
query: GET_POSTS_QUERY,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default PostList;
|
||||
@@ -1,59 +0,0 @@
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { initializeApollo } from '../lib/apolloClient';
|
||||
import RecentDataset from '../components/home/Recent';
|
||||
import { SEARCH_QUERY } from '../graphql/queries';
|
||||
import { loadNamespaces } from './_app';
|
||||
import useTranslation from 'next-translate/useTranslation';
|
||||
import NavBar from '../components/home/Nav';
|
||||
import Hero from '../components/home/Hero';
|
||||
import Footer from '../components/home/Footer';
|
||||
import Stats from '../components/home/Stats';
|
||||
|
||||
const Home: React.FC<{ locale: any; locales: any }> = ({
|
||||
locale,
|
||||
locales,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<>
|
||||
<div className="container mx-auto">
|
||||
<Head>
|
||||
<title>{t(`common:title`)}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<NavBar />
|
||||
<Hero />
|
||||
<Stats />
|
||||
<RecentDataset />
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({
|
||||
locale,
|
||||
locales,
|
||||
}) => {
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
await apolloClient.query({
|
||||
query: SEARCH_QUERY,
|
||||
variables: {
|
||||
sort: 'metadata_created desc',
|
||||
rows: 3,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
_ns: await loadNamespaces(['common'], locale),
|
||||
locale,
|
||||
locales,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Home;
|
||||
@@ -1,45 +0,0 @@
|
||||
import { GetServerSideProps } from 'next';
|
||||
import Head from 'next/head';
|
||||
import { initializeApollo } from '../../../lib/apolloClient';
|
||||
import Nav from '../../../components/home/Nav';
|
||||
import Page from '../../../components/static/Page';
|
||||
import { GET_PAGE_QUERY } from '../../../graphql/queries';
|
||||
|
||||
type Props = {
|
||||
variables: any;
|
||||
};
|
||||
|
||||
const PageItem: React.FC<Props> = ({ variables }) => (
|
||||
<>
|
||||
<Head>
|
||||
<title>Portal | {variables.slug}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className="p-6">
|
||||
<Page variables={variables} />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const variables = {
|
||||
slug: context.query.page,
|
||||
};
|
||||
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
await apolloClient.query({
|
||||
query: GET_PAGE_QUERY,
|
||||
variables,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
variables,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default PageItem;
|
||||
@@ -1,49 +0,0 @@
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { initializeApollo } from '../lib/apolloClient';
|
||||
import utils from '../utils';
|
||||
import Head from 'next/head';
|
||||
import Nav from '../components/home/Nav';
|
||||
import Form from '../components/search/Form';
|
||||
import Total from '../components/search/Total';
|
||||
import List from '../components/search/List';
|
||||
import { SEARCH_QUERY } from '../graphql/queries';
|
||||
|
||||
type Props = {
|
||||
variables: any;
|
||||
};
|
||||
|
||||
const Search: React.FC<Props> = ({ variables }) => (
|
||||
<>
|
||||
<Head>
|
||||
<title>Portal | Search</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Nav />
|
||||
<main className="p-6">
|
||||
<Form />
|
||||
<Total variables={variables} />
|
||||
<List variables={variables} />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const query = context.query || {};
|
||||
const variables = utils.convertToCkanSearchQuery(query);
|
||||
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
await apolloClient.query({
|
||||
query: SEARCH_QUERY,
|
||||
variables,
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
variables,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default Search;
|
||||
@@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'postcss-preset-env',
|
||||
'postcss-import',
|
||||
'tailwindcss',
|
||||
'autoprefixer',
|
||||
],
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 318 B |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 85 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g transform="translate(-395.882 247.118)"><rect width="64" height="64" rx="2" transform="translate(395.882 -247.118)" fill="#030303"/><text transform="translate(408.882 -221.118)" fill="#fff" font-size="20" font-family="AndaleMono, Andale Mono"><tspan x="0" y="0">POR</tspan><tspan x="0" y="22">TAL</tspan></text></g></svg>
|
||||
|
Before Width: | Height: | Size: 408 B |
@@ -1 +0,0 @@
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -1,11 +0,0 @@
|
||||
const defaultTheme = require('tailwindcss/defaultTheme');
|
||||
|
||||
module.exports = {
|
||||
mode: 'jit',
|
||||
purge: false,
|
||||
darkMode: false, // or 'media' or 'class'
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [require('@tailwindcss/forms')],
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
export default {
|
||||
primary: '#896A00',
|
||||
secondary: '#254E70',
|
||||
black: '#0C0C0C',
|
||||
positive: '#0C0C0C',
|
||||
textPrimary: '#896A00',
|
||||
backgroundPrimary: '#FAEEC5',
|
||||
|
||||
// Define font size variables
|
||||
fontSmall: '18px',
|
||||
fontMedium: '30px',
|
||||
fontLarge: '45px',
|
||||
};
|
||||
@@ -1,11 +0,0 @@
|
||||
import base from './base';
|
||||
import { IThemes } from './utils';
|
||||
|
||||
/**
|
||||
* The default theme to load
|
||||
*/
|
||||
export const DEFAULT_THEME = 'base';
|
||||
|
||||
export const themes: IThemes = {
|
||||
base,
|
||||
};
|
||||
@@ -1,6 +0,0 @@
|
||||
import { extend } from './utils';
|
||||
import base from './base';
|
||||
|
||||
export default extend(base, {
|
||||
// Custom styles for primary theme
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import { themes } from './index';
|
||||
|
||||
export interface ITheme {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface IThemes {
|
||||
[key: string]: ITheme;
|
||||
}
|
||||
|
||||
export interface IMappedTheme {
|
||||
[key: string]: string | null;
|
||||
}
|
||||
|
||||
export const mapTheme = (variables: ITheme): IMappedTheme => {
|
||||
return {
|
||||
'--color-primary': variables.primary || '',
|
||||
'--color-secondary': variables.secondary || '',
|
||||
'--color-positive': variables.positive || '',
|
||||
'--color-negative': variables.negative || '',
|
||||
'--color-text-primary': variables.textPrimary || '',
|
||||
'--background-primary': variables.backgroundPrimary || '',
|
||||
'--background-sec': variables.backgroundSecondary || '',
|
||||
'--font-size-small': variables.fontSmall || '18px',
|
||||
'--font-size-medium': variables.fontMedium || '30px',
|
||||
'--font-size-large': variables.fontLarge || '45px',
|
||||
};
|
||||
};
|
||||
|
||||
export const applyTheme = (theme: string): void => {
|
||||
const themeObject: IMappedTheme = mapTheme(themes[theme]);
|
||||
if (!themeObject) return;
|
||||
|
||||
const root = document.documentElement;
|
||||
|
||||
Object.keys(themeObject).forEach((property) => {
|
||||
if (property === 'name') {
|
||||
return;
|
||||
}
|
||||
|
||||
root.style.setProperty(property, themeObject[property]);
|
||||
});
|
||||
};
|
||||
|
||||
export const extend = (extending: ITheme, newTheme: ITheme): ITheme => {
|
||||
return { ...extending, ...newTheme };
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"setupTests.js",
|
||||
"jest.config.js",
|
||||
"config/jest/cssTransform.js"
|
||||
]
|
||||
}
|
||||
@@ -1,541 +0,0 @@
|
||||
const { URL } = require('url');
|
||||
const bytes = require('bytes');
|
||||
const slugify = require('slugify');
|
||||
const config = require('../next.config.js');
|
||||
|
||||
module.exports.ckanToDataPackage = function (descriptor) {
|
||||
// Make a copy
|
||||
const datapackage = JSON.parse(JSON.stringify(descriptor));
|
||||
|
||||
// Lowercase name
|
||||
datapackage.name = datapackage.name.toLowerCase();
|
||||
|
||||
// Rename notes => description
|
||||
if (datapackage.notes) {
|
||||
datapackage.description = datapackage.notes;
|
||||
delete datapackage.notes;
|
||||
}
|
||||
|
||||
// Rename ckan_url => homepage
|
||||
if (datapackage.ckan_url) {
|
||||
datapackage.homepage = datapackage.ckan_url;
|
||||
delete datapackage.ckan_url;
|
||||
}
|
||||
|
||||
// Parse license
|
||||
const license = {};
|
||||
if (datapackage.license_id) {
|
||||
license.type = datapackage.license_id;
|
||||
delete datapackage.license_id;
|
||||
}
|
||||
if (datapackage.license_title) {
|
||||
license.title = datapackage.license_title;
|
||||
delete datapackage.license_title;
|
||||
}
|
||||
if (datapackage.license_url) {
|
||||
license.url = datapackage.license_url;
|
||||
delete datapackage.license_url;
|
||||
}
|
||||
if (Object.keys(license).length > 0) {
|
||||
datapackage.license = license;
|
||||
}
|
||||
|
||||
// Parse author and sources
|
||||
const source = {};
|
||||
if (datapackage.author) {
|
||||
source.name = datapackage.author;
|
||||
delete datapackage.author;
|
||||
}
|
||||
if (datapackage.author_email) {
|
||||
source.email = datapackage.author_email;
|
||||
delete datapackage.author_email;
|
||||
}
|
||||
if (datapackage.url) {
|
||||
source.web = datapackage.url;
|
||||
delete datapackage.url;
|
||||
}
|
||||
if (Object.keys(source).length > 0) {
|
||||
datapackage.sources = [source];
|
||||
}
|
||||
|
||||
// Parse maintainer
|
||||
const author = {};
|
||||
if (datapackage.maintainer) {
|
||||
author.name = datapackage.maintainer;
|
||||
delete datapackage.maintainer;
|
||||
}
|
||||
if (datapackage.maintainer_email) {
|
||||
author.email = datapackage.maintainer_email;
|
||||
delete datapackage.maintainer_email;
|
||||
}
|
||||
if (Object.keys(author).length > 0) {
|
||||
datapackage.author = author;
|
||||
}
|
||||
|
||||
// Parse tags
|
||||
if (datapackage.tags) {
|
||||
datapackage.keywords = [];
|
||||
datapackage.tags.forEach((tag) => {
|
||||
datapackage.keywords.push(tag.name);
|
||||
});
|
||||
delete datapackage.tags;
|
||||
}
|
||||
|
||||
// Parse extras
|
||||
// TODO
|
||||
|
||||
// Resources
|
||||
datapackage.resources = datapackage.resources.map((resource) => {
|
||||
if (resource.name) {
|
||||
resource.title = resource.title || resource.name;
|
||||
resource.name = resource.name.toLowerCase().replace(/ /g, '_');
|
||||
} else {
|
||||
resource.name = resource.id;
|
||||
}
|
||||
|
||||
if (resource.url) {
|
||||
resource.path = resource.url;
|
||||
delete resource.url;
|
||||
}
|
||||
|
||||
if (!resource.schema) {
|
||||
// If 'fields' property exists use it as schema fields
|
||||
if (resource.fields) {
|
||||
if (typeof resource.fields === 'string') {
|
||||
try {
|
||||
resource.fields = JSON.parse(resource.fields);
|
||||
} catch (e) {
|
||||
console.log('Could not parse resource.fields');
|
||||
}
|
||||
}
|
||||
resource.schema = { fields: resource.fields };
|
||||
delete resource.fields;
|
||||
}
|
||||
}
|
||||
|
||||
return resource;
|
||||
});
|
||||
|
||||
return datapackage;
|
||||
};
|
||||
|
||||
/*
|
||||
At the moment, we're considering only following examples of CKAN view:
|
||||
1. recline_view => Data Explorer with Table view, Chart Builder, Map Builder
|
||||
and Query Builder.
|
||||
2. geojson_view => Leaflet map
|
||||
3. pdf_view => our PDF viewer
|
||||
4. recline_grid_view => our Table viewer
|
||||
5. recline_graph_view => our Simple graph
|
||||
6. recline_map_view => our Leaflet map
|
||||
7. image_view => not supported at the moment
|
||||
8. text_view => not supported at the moment
|
||||
9. webpage_view => not supported at the moment
|
||||
*/
|
||||
module.exports.ckanViewToDataPackageView = (ckanView) => {
|
||||
const viewTypeToSpecType = {
|
||||
recline_view: 'dataExplorer', // from datastore data
|
||||
recline_grid_view: 'table',
|
||||
recline_graph_view: 'simple',
|
||||
recline_map_view: 'tabularmap',
|
||||
geojson_view: 'map',
|
||||
pdf_view: 'document',
|
||||
image_view: 'web',
|
||||
webpage_view: 'web',
|
||||
};
|
||||
const dataPackageView = JSON.parse(JSON.stringify(ckanView));
|
||||
dataPackageView.specType =
|
||||
viewTypeToSpecType[ckanView.view_type] ||
|
||||
dataPackageView.specType ||
|
||||
'unsupported';
|
||||
|
||||
if (dataPackageView.specType === 'dataExplorer') {
|
||||
dataPackageView.spec = {
|
||||
widgets: [
|
||||
{ specType: 'table' },
|
||||
{ specType: 'simple' },
|
||||
{ specType: 'tabularmap' },
|
||||
],
|
||||
};
|
||||
} else if (dataPackageView.specType === 'simple') {
|
||||
const graphTypeConvert = {
|
||||
lines: 'line',
|
||||
'lines-and-points': 'lines-and-points',
|
||||
points: 'points',
|
||||
bars: 'horizontal-bar',
|
||||
columns: 'bar',
|
||||
};
|
||||
dataPackageView.spec = {
|
||||
group: ckanView.group,
|
||||
series: Array.isArray(ckanView.series)
|
||||
? ckanView.series
|
||||
: [ckanView.series],
|
||||
type: graphTypeConvert[ckanView.graph_type] || 'line',
|
||||
};
|
||||
} else if (dataPackageView.specType === 'tabularmap') {
|
||||
if (ckanView.map_field_type === 'geojson') {
|
||||
dataPackageView.spec = {
|
||||
geomField: ckanView.geojson_field,
|
||||
};
|
||||
} else {
|
||||
dataPackageView.spec = {
|
||||
lonField: ckanView.longitude_field,
|
||||
latField: ckanView.latitude_field,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return dataPackageView;
|
||||
};
|
||||
|
||||
/*
|
||||
Takes single field descriptor from datastore data dictionary and coverts into
|
||||
tableschema field descriptor.
|
||||
*/
|
||||
module.exports.dataStoreDataDictionaryToTableSchema = (dataDictionary) => {
|
||||
const internalDataStoreFields = ['_id', '_full_text', '_count'];
|
||||
if (internalDataStoreFields.includes(dataDictionary.id)) {
|
||||
return null;
|
||||
}
|
||||
const dataDictionaryType2TableSchemaType = {
|
||||
text: 'string',
|
||||
int: 'integer',
|
||||
float: 'number',
|
||||
date: 'date',
|
||||
time: 'time',
|
||||
timestamp: 'datetime',
|
||||
bool: 'boolean',
|
||||
json: 'object',
|
||||
};
|
||||
const field = {
|
||||
name: dataDictionary.id,
|
||||
type: dataDictionaryType2TableSchemaType[dataDictionary.type] || 'any',
|
||||
};
|
||||
if (dataDictionary.info) {
|
||||
const constraintsAttributes = [
|
||||
'required',
|
||||
'unique',
|
||||
'minLength',
|
||||
'maxLength',
|
||||
'minimum',
|
||||
'maximum',
|
||||
'pattern',
|
||||
'enum',
|
||||
];
|
||||
field.constraints = {};
|
||||
Object.keys(dataDictionary.info).forEach((key) => {
|
||||
if (constraintsAttributes.includes(key)) {
|
||||
field.constraints[key] = dataDictionary.info[key];
|
||||
} else {
|
||||
field[key] = dataDictionary.info[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
return field;
|
||||
};
|
||||
|
||||
module.exports.convertToStandardCollection = (descriptor) => {
|
||||
const standard = {
|
||||
name: '',
|
||||
title: '',
|
||||
summary: '',
|
||||
image: '',
|
||||
count: null,
|
||||
};
|
||||
|
||||
standard.name = descriptor.name;
|
||||
standard.title = descriptor.title || descriptor.display_name;
|
||||
standard.summary = descriptor.description || '';
|
||||
standard.image = descriptor.image_display_url || descriptor.image_url;
|
||||
standard.count = descriptor.package_count || 0;
|
||||
standard.extras = descriptor.extras || [];
|
||||
standard.groups = descriptor.groups || [];
|
||||
|
||||
return standard;
|
||||
};
|
||||
|
||||
module.exports.convertToCkanSearchQuery = (query) => {
|
||||
const ckanQuery = {
|
||||
q: '',
|
||||
fq: '',
|
||||
rows: '',
|
||||
start: '',
|
||||
sort: '',
|
||||
'facet.field': [
|
||||
'organization',
|
||||
'groups',
|
||||
'tags',
|
||||
'res_format',
|
||||
'license_id',
|
||||
],
|
||||
'facet.limit': 5,
|
||||
'facet.mincount': 0,
|
||||
};
|
||||
// Split by space but ignore spaces within double quotes:
|
||||
if (query.q) {
|
||||
query.q.match(/(?:[^\s"]+|"[^"]*")+/g).forEach((part) => {
|
||||
if (part.includes(':')) {
|
||||
ckanQuery.fq += part + ' ';
|
||||
} else {
|
||||
ckanQuery.q += part + ' ';
|
||||
}
|
||||
});
|
||||
ckanQuery.fq = ckanQuery.fq.trim();
|
||||
ckanQuery.q = ckanQuery.q.trim();
|
||||
}
|
||||
|
||||
if (query.fq) {
|
||||
ckanQuery.fq = ckanQuery.fq ? ckanQuery.fq + ' ' + query.fq : query.fq;
|
||||
}
|
||||
|
||||
// standard 'size' => ckan 'rows'
|
||||
ckanQuery.rows = query.size || '';
|
||||
|
||||
// standard 'from' => ckan 'start'
|
||||
ckanQuery.start = query.from || '';
|
||||
|
||||
// standard 'sort' => ckan 'sort'
|
||||
const sortQueries = [];
|
||||
if (query.sort && query.sort.constructor == Object) {
|
||||
for (let [key, value] of Object.entries(query.sort)) {
|
||||
sortQueries.push(`${key} ${value}`);
|
||||
}
|
||||
ckanQuery.sort = sortQueries.join(',');
|
||||
} else if (query.sort && query.sort.constructor == String) {
|
||||
ckanQuery.sort = query.sort.replace(':', ' ');
|
||||
} else if (query.sort && query.sort.constructor == Array) {
|
||||
query.sort.forEach((sort) => {
|
||||
sortQueries.push(sort.replace(':', ' '));
|
||||
});
|
||||
ckanQuery.sort = sortQueries.join(',');
|
||||
}
|
||||
|
||||
// Facets
|
||||
ckanQuery['facet.field'] = query['facet.field'] || ckanQuery['facet.field'];
|
||||
ckanQuery['facet.limit'] = query['facet.limit'] || ckanQuery['facet.limit'];
|
||||
ckanQuery['facet.mincount'] =
|
||||
query['facet.mincount'] || ckanQuery['facet.mincount'];
|
||||
ckanQuery['facet.field'] = query['facet.field'] || ckanQuery['facet.field'];
|
||||
|
||||
// Remove attributes with empty string, null or undefined values
|
||||
Object.keys(ckanQuery).forEach(
|
||||
(key) => !ckanQuery[key] && delete ckanQuery[key]
|
||||
);
|
||||
|
||||
return ckanQuery;
|
||||
};
|
||||
|
||||
module.exports.pagination = (c, m) => {
|
||||
let current = c,
|
||||
last = m,
|
||||
delta = 2,
|
||||
left = current - delta,
|
||||
right = current + delta + 1,
|
||||
range = [],
|
||||
rangeWithDots = [],
|
||||
l;
|
||||
|
||||
range.push(1);
|
||||
for (let i = c - delta; i <= c + delta; i++) {
|
||||
if (i >= left && i < right && i < m && i > 1) {
|
||||
range.push(i);
|
||||
}
|
||||
}
|
||||
range.push(m);
|
||||
|
||||
for (let i of range) {
|
||||
if (l) {
|
||||
if (i - l === 2) {
|
||||
rangeWithDots.push(l + 1);
|
||||
} else if (i - l !== 1) {
|
||||
rangeWithDots.push('...');
|
||||
}
|
||||
}
|
||||
rangeWithDots.push(i);
|
||||
l = i;
|
||||
}
|
||||
return rangeWithDots;
|
||||
};
|
||||
|
||||
module.exports.processMarkdown = require('markdown-it')({
|
||||
html: true,
|
||||
linkify: true,
|
||||
typographer: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* Process data package attributes prior to display to users.
|
||||
* Process markdown
|
||||
* Convert bytes to human readable format
|
||||
* etc.
|
||||
**/
|
||||
module.exports.processDataPackage = function (datapackage) {
|
||||
const newDatapackage = JSON.parse(JSON.stringify(datapackage));
|
||||
if (newDatapackage.description) {
|
||||
newDatapackage.descriptionHtml = module.exports.processMarkdown.render(
|
||||
newDatapackage.description
|
||||
);
|
||||
}
|
||||
|
||||
if (newDatapackage.readme) {
|
||||
newDatapackage.readmeHtml = module.exports.processMarkdown.render(
|
||||
newDatapackage.readme
|
||||
);
|
||||
}
|
||||
|
||||
newDatapackage.formats = newDatapackage.formats || [];
|
||||
// Per each resource:
|
||||
newDatapackage.resources.forEach((resource) => {
|
||||
if (resource.description) {
|
||||
resource.descriptionHtml = module.exports.processMarkdown.render(
|
||||
resource.description
|
||||
);
|
||||
}
|
||||
// Normalize format (lowercase)
|
||||
if (resource.format) {
|
||||
resource.format = resource.format.toLowerCase();
|
||||
newDatapackage.formats.push(resource.format);
|
||||
}
|
||||
|
||||
// Convert bytes into human-readable format:
|
||||
if (resource.size) {
|
||||
resource.sizeFormatted = bytes(resource.size, { decimalPlaces: 0 });
|
||||
}
|
||||
});
|
||||
|
||||
return newDatapackage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create 'displayResources' property which has:
|
||||
* resource: Object containing resource descriptor
|
||||
* api: API URL for the resource if available, e.g., Datastore
|
||||
* proxy: path via proxy for the resource if available
|
||||
* cc_proxy: path via CKAN Classic proxy if available
|
||||
* slug: slugified name of a resource
|
||||
**/
|
||||
module.exports.prepareResourcesForDisplay = function (datapackage) {
|
||||
const newDatapackage = JSON.parse(JSON.stringify(datapackage));
|
||||
newDatapackage.displayResources = [];
|
||||
newDatapackage.resources.forEach((resource, index) => {
|
||||
const api = resource.datastore_active
|
||||
? config.get('API_URL') +
|
||||
'datastore_search?resource_id=' +
|
||||
resource.id +
|
||||
'&sort=_id asc'
|
||||
: null;
|
||||
// Use proxy path if datastore/filestore proxies are given:
|
||||
let proxy, cc_proxy;
|
||||
try {
|
||||
const resourceUrl = new URL(resource.path);
|
||||
if (
|
||||
resourceUrl.host === config.get('PROXY_DATASTORE') &&
|
||||
resource.format !== 'pdf'
|
||||
) {
|
||||
proxy = '/proxy/datastore' + resourceUrl.pathname + resourceUrl.search;
|
||||
}
|
||||
if (
|
||||
resourceUrl.host === config.get('PROXY_FILESTORE') &&
|
||||
resource.format !== 'pdf'
|
||||
) {
|
||||
proxy = '/proxy/filestore' + resourceUrl.pathname + resourceUrl.search;
|
||||
}
|
||||
// Store a CKAN Classic proxy path
|
||||
// https://github.com/ckan/ckan/blob/master/ckanext/resourceproxy/plugin.py#L59
|
||||
const apiUrlObject = new URL(config.get('API_URL'));
|
||||
cc_proxy =
|
||||
apiUrlObject.origin +
|
||||
`/dataset/${datapackage.id}/resource/${resource.id}/proxy`;
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
const displayResource = {
|
||||
resource,
|
||||
api, // URI for getting the resource via API, e.g., Datastore. Useful when you want to fetch only 100 rows or similar.
|
||||
proxy, // alternative for path in case there is CORS issue
|
||||
cc_proxy,
|
||||
slug: slugify(resource.name) + '-' + index, // Used for anchor links
|
||||
};
|
||||
newDatapackage.displayResources.push(displayResource);
|
||||
});
|
||||
return newDatapackage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare 'views' property which is used by 'datapackage-views-js' library to
|
||||
* render visualizations such as tables, graphs and maps.
|
||||
**/
|
||||
module.exports.prepareViews = function (datapackage) {
|
||||
const newDatapackage = JSON.parse(JSON.stringify(datapackage));
|
||||
newDatapackage.views = newDatapackage.views || [];
|
||||
newDatapackage.resources.forEach((resource) => {
|
||||
const resourceViews =
|
||||
resource.views &&
|
||||
resource.views.map((view) => {
|
||||
view.resources = [resource.name];
|
||||
return view;
|
||||
});
|
||||
|
||||
newDatapackage.views = newDatapackage.views.concat(resourceViews);
|
||||
});
|
||||
|
||||
return newDatapackage;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create 'dataExplorers' property which is used by 'data-explorer' library to
|
||||
* render data explorer widgets.
|
||||
**/
|
||||
module.exports.prepareDataExplorers = function (datapackage) {
|
||||
const newDatapackage = JSON.parse(JSON.stringify(datapackage));
|
||||
newDatapackage.displayResources.forEach((displayResource, idx) => {
|
||||
newDatapackage.displayResources[idx].dataExplorers = [];
|
||||
displayResource.resource.views &&
|
||||
displayResource.resource.views.forEach((view) => {
|
||||
const widgets = [];
|
||||
if (view.specType === 'dataExplorer') {
|
||||
view.spec.widgets.forEach((widget, index) => {
|
||||
const widgetNames = {
|
||||
table: 'Table',
|
||||
simple: 'Chart',
|
||||
tabularmap: 'Map',
|
||||
};
|
||||
widget = {
|
||||
name: widgetNames[widget.specType] || 'Widget-' + index,
|
||||
active: index === 0 ? true : false,
|
||||
datapackage: {
|
||||
views: [
|
||||
{
|
||||
id: view.id,
|
||||
specType: widget.specType,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
widgets.push(widget);
|
||||
});
|
||||
} else {
|
||||
const widget = {
|
||||
name: view.title || '',
|
||||
active: true,
|
||||
datapackage: {
|
||||
views: [view],
|
||||
},
|
||||
};
|
||||
widgets.push(widget);
|
||||
}
|
||||
|
||||
displayResource.resource.api =
|
||||
displayResource.resource.api || displayResource.api;
|
||||
const dataExplorer = JSON.stringify({
|
||||
widgets,
|
||||
datapackage: {
|
||||
resources: [displayResource.resource],
|
||||
},
|
||||
}).replace(/'/g, ''');
|
||||
newDatapackage.displayResources[idx].dataExplorers.push(dataExplorer);
|
||||
});
|
||||
});
|
||||
|
||||
return newDatapackage;
|
||||
};
|
||||
18868
examples/ckan/yarn.lock
18868
examples/ckan/yarn.lock
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user