[monorepo][lg] - move over examples
This commit is contained in:
parent
d45b5a26d5
commit
d466023926
1
examples/ckan/.env
Normal file
1
examples/ckan/.env
Normal file
@ -0,0 +1 @@
|
||||
DMS=https://demo.dev.datopian.com/
|
||||
@ -10,10 +10,7 @@
|
||||
{
|
||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||
"rules": {
|
||||
"@next/next/no-html-link-for-pages": [
|
||||
"error",
|
||||
"apps/data-literate/pages"
|
||||
]
|
||||
"@next/next/no-html-link-for-pages": ["error", "packages/ckan/pages"]
|
||||
}
|
||||
},
|
||||
{
|
||||
306
examples/ckan/README.md
Normal file
306
examples/ckan/README.md
Normal file
@ -0,0 +1,306 @@
|
||||
<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/
|
||||
23
examples/ckan/__tests__/components/search/Form.test.tsx
Normal file
23
examples/ckan/__tests__/components/search/Form.test.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
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();
|
||||
});
|
||||
@ -0,0 +1,47 @@
|
||||
// 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>
|
||||
`;
|
||||
15
examples/ckan/components/_shared/CustomLink.tsx
Normal file
15
examples/ckan/components/_shared/CustomLink.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
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;
|
||||
17
examples/ckan/components/_shared/Error.tsx
Normal file
17
examples/ckan/components/_shared/Error.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
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;
|
||||
53
examples/ckan/components/_shared/Table.tsx
Normal file
53
examples/ckan/components/_shared/Table.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
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;
|
||||
5
examples/ckan/components/_shared/index.ts
Normal file
5
examples/ckan/components/_shared/index.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import Table from './Table';
|
||||
import ErrorMessage from './Error';
|
||||
import CustomLink from './CustomLink';
|
||||
|
||||
export { Table, ErrorMessage, CustomLink };
|
||||
83
examples/ckan/components/dataset/About.tsx
Normal file
83
examples/ckan/components/dataset/About.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
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;
|
||||
23
examples/ckan/components/dataset/Org.tsx
Normal file
23
examples/ckan/components/dataset/Org.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_DATASET_QUERY } from '../../graphql/queries';
|
||||
import { Org } from '@portaljs/portaljs-components';
|
||||
|
||||
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;
|
||||
76
examples/ckan/components/dataset/Resources.tsx
Normal file
76
examples/ckan/components/dataset/Resources.tsx
Normal file
@ -0,0 +1,76 @@
|
||||
/* 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 className="underline" href={`${parentName}/r/${resName}`}>
|
||||
{title || resName}
|
||||
</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 className="underline" href={`${parentName}/r/${resName}`}>
|
||||
Preview
|
||||
</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;
|
||||
61
examples/ckan/components/home/Footer.tsx
Normal file
61
examples/ckan/components/home/Footer.tsx
Normal file
@ -0,0 +1,61 @@
|
||||
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/portaljs',
|
||||
// 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;
|
||||
7
examples/ckan/components/home/Hero.tsx
Normal file
7
examples/ckan/components/home/Hero.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import Template from './HeroTemplate';
|
||||
|
||||
const Hero: React.FC = () => {
|
||||
return <Template />;
|
||||
};
|
||||
|
||||
export default Hero;
|
||||
130
examples/ckan/components/home/HeroTemplate.tsx
Normal file
130
examples/ckan/components/home/HeroTemplate.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
14
examples/ckan/components/home/Nav.tsx
Normal file
14
examples/ckan/components/home/Nav.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
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/portaljs' },
|
||||
];
|
||||
|
||||
return <Template menu={navMenu} logo={'/images/logo.svg'} />;
|
||||
};
|
||||
|
||||
export default NavBar;
|
||||
117
examples/ckan/components/home/NavTemplate.tsx
Normal file
117
examples/ckan/components/home/NavTemplate.tsx
Normal file
@ -0,0 +1,117 @@
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { Disclosure } from '@headlessui/react';
|
||||
import { Bars3Icon, MagnifyingGlassIcon, XMarkIcon } from '@heroicons/react/24/solid';
|
||||
|
||||
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">
|
||||
<MagnifyingGlassIcon
|
||||
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 ? (
|
||||
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
|
||||
) : (
|
||||
<Bars3Icon 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;
|
||||
96
examples/ckan/components/home/Recent.tsx
Normal file
96
examples/ckan/components/home/Recent.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
/* eslint-disable jsx-a11y/anchor-is-valid */
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import {
|
||||
PresentationChartBarIcon,
|
||||
RectangleStackIcon,
|
||||
} from '@heroicons/react/24/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"
|
||||
>
|
||||
<RectangleStackIcon
|
||||
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;
|
||||
54
examples/ckan/components/home/Stats.tsx
Normal file
54
examples/ckan/components/home/Stats.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
/* 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;
|
||||
154
examples/ckan/components/org/About.tsx
Normal file
154
examples/ckan/components/org/About.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
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;
|
||||
78
examples/ckan/components/resource/About.tsx
Normal file
78
examples/ckan/components/resource/About.tsx
Normal file
@ -0,0 +1,78 @@
|
||||
/* 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;
|
||||
32
examples/ckan/components/resource/Preview.tsx
Normal file
32
examples/ckan/components/resource/Preview.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { PlotlyChart, Table } from '@portaljs/portaljs-components';
|
||||
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} height="300px" width="100%"/>;
|
||||
};
|
||||
|
||||
export default Preview;
|
||||
24
examples/ckan/components/resource/View.tsx
Normal file
24
examples/ckan/components/resource/View.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
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;
|
||||
42
examples/ckan/components/search/Form.tsx
Normal file
42
examples/ckan/components/search/Form.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
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;
|
||||
64
examples/ckan/components/search/List.tsx
Normal file
64
examples/ckan/components/search/List.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
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 className="block focus:outline-none"
|
||||
href={`/@${
|
||||
dataset.organization ? dataset.organization.name : 'dataset'
|
||||
}/${dataset.name}`}
|
||||
>
|
||||
<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>
|
||||
</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;
|
||||
23
examples/ckan/components/search/Total.tsx
Normal file
23
examples/ckan/components/search/Total.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import { ErrorMessage } from '../_shared';
|
||||
import { GET_TOTAL_COUNT_QUERY } from '../../graphql/queries';
|
||||
import { ItemTotal } from '@portaljs/portaljs-components';
|
||||
|
||||
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;
|
||||
39
examples/ckan/components/static/List.tsx
Normal file
39
examples/ckan/components/static/List.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
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;
|
||||
32
examples/ckan/components/static/Page.tsx
Normal file
32
examples/ckan/components/static/Page.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
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;
|
||||
32
examples/ckan/components/static/Post.tsx
Normal file
32
examples/ckan/components/static/Post.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
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;
|
||||
8
examples/ckan/config/jest/cssTransform.js
Normal file
8
examples/ckan/config/jest/cssTransform.js
Normal file
@ -0,0 +1,8 @@
|
||||
module.exports = {
|
||||
process() {
|
||||
return 'module.exports = {};';
|
||||
},
|
||||
getCacheKey() {
|
||||
return 'cssTransform';
|
||||
},
|
||||
};
|
||||
3
examples/ckan/cypress.json
Normal file
3
examples/ckan/cypress.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3000"
|
||||
}
|
||||
5
examples/ckan/cypress/fixtures/example.json
Normal file
5
examples/ckan/cypress/fixtures/example.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
32
examples/ckan/cypress/integration/pages/homepage-spec.js
Normal file
32
examples/ckan/cypress/integration/pages/homepage-spec.js
Normal file
@ -0,0 +1,32 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
21
examples/ckan/cypress/integration/pages/search-spec.js
Normal file
21
examples/ckan/cypress/integration/pages/search-spec.js
Normal file
@ -0,0 +1,21 @@
|
||||
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)'
|
||||
// );
|
||||
// });
|
||||
});
|
||||
21
examples/ckan/cypress/plugins/index.js
Normal file
21
examples/ckan/cypress/plugins/index.js
Normal file
@ -0,0 +1,21 @@
|
||||
/// <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
|
||||
};
|
||||
25
examples/ckan/cypress/support/commands.js
Normal file
25
examples/ckan/cypress/support/commands.js
Normal file
@ -0,0 +1,25 @@
|
||||
// ***********************************************
|
||||
// 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) => { ... })
|
||||
20
examples/ckan/cypress/support/index.js
Normal file
20
examples/ckan/cypress/support/index.js
Normal file
@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// 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')
|
||||
12
examples/ckan/cypress/tsconfig.json
Normal file
12
examples/ckan/cypress/tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
166
examples/ckan/graphql/queries.ts
Normal file
166
examples/ckan/graphql/queries.ts
Normal file
@ -0,0 +1,166 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
`;
|
||||
29
examples/ckan/jest.config.js
Normal file
29
examples/ckan/jest.config.js
Normal file
@ -0,0 +1,29 @@
|
||||
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,11 +1,11 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'data-literate',
|
||||
displayName: 'ckan',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/examples/data-literate',
|
||||
coverageDirectory: '../../coverage/examples/ckan',
|
||||
};
|
||||
93
examples/ckan/lib/apolloClient.ts
Normal file
93
examples/ckan/lib/apolloClient.ts
Normal file
@ -0,0 +1,93 @@
|
||||
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';
|
||||
import { ApolloLink } from '@apollo/client';
|
||||
|
||||
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 as any,
|
||||
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;
|
||||
}
|
||||
4
examples/ckan/locales/en/common.json
Normal file
4
examples/ckan/locales/en/common.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
4
examples/ckan/locales/fr/common.json
Normal file
4
examples/ckan/locales/fr/common.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"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."
|
||||
}
|
||||
155
examples/ckan/mocks/index.js
Normal file
155
examples/ckan/mocks/index.js
Normal file
@ -0,0 +1,155 @@
|
||||
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');
|
||||
};
|
||||
65
examples/ckan/next.config.js
Normal file
65
examples/ckan/next.config.js
Normal file
@ -0,0 +1,65 @@
|
||||
//@ts-check
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { withNx } = require('@nrwl/next/plugins/with-nx');
|
||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
||||
|
||||
const _nextConfig = (phase) => {
|
||||
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',
|
||||
},
|
||||
};
|
||||
};
|
||||
/**
|
||||
* @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
|
||||
**/
|
||||
const nextConfig = {
|
||||
..._nextConfig(),
|
||||
nx: {
|
||||
// Set this to true if you would like to use SVGR
|
||||
// See: https://github.com/gregberge/svgr
|
||||
svgr: false,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = withNx(nextConfig);
|
||||
53
examples/ckan/pages/[org]/[dataset]/index.tsx
Normal file
53
examples/ckan/pages/[org]/[dataset]/index.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
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;
|
||||
56
examples/ckan/pages/[org]/[dataset]/r/[resource]/index.tsx
Normal file
56
examples/ckan/pages/[org]/[dataset]/r/[resource]/index.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
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;
|
||||
49
examples/ckan/pages/[org]/index.tsx
Normal file
49
examples/ckan/pages/[org]/index.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
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;
|
||||
57
examples/ckan/pages/_app.tsx
Normal file
57
examples/ckan/pages/_app.tsx
Normal file
@ -0,0 +1,57 @@
|
||||
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';
|
||||
import ApolloClient from '@apollo/client';
|
||||
import { NormalizedCache, NormalizedCacheObject } from 'apollo-cache-inmemory';
|
||||
|
||||
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 as any}>
|
||||
<Component {...pageProps} />
|
||||
</ApolloProvider>
|
||||
</I18nProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default MyApp;
|
||||
34
examples/ckan/pages/_document.tsx
Normal file
34
examples/ckan/pages/_document.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
45
examples/ckan/pages/blog/[post]/index.tsx
Normal file
45
examples/ckan/pages/blog/[post]/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
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;
|
||||
35
examples/ckan/pages/blog/index.tsx
Normal file
35
examples/ckan/pages/blog/index.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
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;
|
||||
2
examples/ckan/pages/index.module.css
Normal file
2
examples/ckan/pages/index.module.css
Normal file
@ -0,0 +1,2 @@
|
||||
.page {
|
||||
}
|
||||
59
examples/ckan/pages/index.tsx
Normal file
59
examples/ckan/pages/index.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
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;
|
||||
45
examples/ckan/pages/p/[page]/index.tsx
Normal file
45
examples/ckan/pages/p/[page]/index.tsx
Normal file
@ -0,0 +1,45 @@
|
||||
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;
|
||||
49
examples/ckan/pages/search.tsx
Normal file
49
examples/ckan/pages/search.tsx
Normal file
@ -0,0 +1,49 @@
|
||||
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;
|
||||
403
examples/ckan/pages/styles.css
Normal file
403
examples/ckan/pages/styles.css
Normal file
@ -0,0 +1,403 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
html {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||
Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
|
||||
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||
line-height: 1.5;
|
||||
tab-size: 4;
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
body {
|
||||
font-family: inherit;
|
||||
line-height: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
p,
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: currentColor;
|
||||
}
|
||||
h1,
|
||||
h2 {
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
}
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
vertical-align: middle;
|
||||
shape-rendering: auto;
|
||||
text-rendering: optimizeLegibility;
|
||||
}
|
||||
pre {
|
||||
background-color: rgba(55, 65, 81, 1);
|
||||
border-radius: 0.25rem;
|
||||
color: rgba(229, 231, 235, 1);
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||
Liberation Mono, Courier New, monospace;
|
||||
overflow: scroll;
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.rounded {
|
||||
border-radius: 1.5rem;
|
||||
}
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
max-width: 768px;
|
||||
padding-bottom: 3rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
width: 100%;
|
||||
}
|
||||
#welcome {
|
||||
margin-top: 2.5rem;
|
||||
}
|
||||
#welcome h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1;
|
||||
}
|
||||
#welcome span {
|
||||
display: block;
|
||||
font-size: 1.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 2.25rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
#hero {
|
||||
align-items: center;
|
||||
background-color: hsla(214, 62%, 21%, 1);
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#hero .text-container {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
padding: 3rem 2rem;
|
||||
}
|
||||
#hero .text-container h2 {
|
||||
font-size: 1.5rem;
|
||||
line-height: 2rem;
|
||||
position: relative;
|
||||
}
|
||||
#hero .text-container h2 svg {
|
||||
color: hsla(162, 47%, 50%, 1);
|
||||
height: 2rem;
|
||||
left: -0.25rem;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 2rem;
|
||||
}
|
||||
#hero .text-container h2 span {
|
||||
margin-left: 2.5rem;
|
||||
}
|
||||
#hero .text-container a {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border-radius: 0.75rem;
|
||||
color: rgba(55, 65, 81, 1);
|
||||
display: inline-block;
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem 2rem;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: none;
|
||||
justify-content: center;
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
}
|
||||
#hero .logo-container svg {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
width: 66.666667%;
|
||||
}
|
||||
#middle-content {
|
||||
align-items: flex-start;
|
||||
display: grid;
|
||||
gap: 4rem;
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#learning-materials {
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#learning-materials h2 {
|
||||
font-weight: 500;
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.list-item-link {
|
||||
align-items: center;
|
||||
border-radius: 0.75rem;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 100%;
|
||||
}
|
||||
.list-item-link svg:first-child {
|
||||
margin-right: 1rem;
|
||||
height: 1.5rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1.5rem;
|
||||
}
|
||||
.list-item-link > span {
|
||||
flex-grow: 1;
|
||||
font-weight: 400;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link > span > span {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
flex-grow: 1;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 300;
|
||||
line-height: 1rem;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
.list-item-link svg:last-child {
|
||||
height: 1rem;
|
||||
transition-property: all;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
width: 1rem;
|
||||
}
|
||||
.list-item-link:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
background-color: hsla(162, 47%, 50%, 1);
|
||||
}
|
||||
.list-item-link:hover > span {
|
||||
}
|
||||
.list-item-link:hover > span > span {
|
||||
color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
.list-item-link:hover svg:last-child {
|
||||
transform: translateX(0.25rem);
|
||||
}
|
||||
#other-links {
|
||||
}
|
||||
.button-pill {
|
||||
padding: 1.5rem 2rem;
|
||||
transition-duration: 300ms;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
.button-pill svg {
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
.button-pill > span {
|
||||
letter-spacing: -0.025em;
|
||||
font-weight: 400;
|
||||
font-size: 1.125rem;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.button-pill span span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
.button-pill:hover svg,
|
||||
.button-pill:hover {
|
||||
color: rgba(255, 255, 255, 1) !important;
|
||||
}
|
||||
#nx-console:hover {
|
||||
background-color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-console svg {
|
||||
color: rgba(0, 122, 204, 1);
|
||||
}
|
||||
#nx-repo:hover {
|
||||
background-color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
#nx-repo svg {
|
||||
color: rgba(24, 23, 23, 1);
|
||||
}
|
||||
#nx-cloud {
|
||||
margin-bottom: 2rem;
|
||||
margin-top: 2rem;
|
||||
padding: 2.5rem 2rem;
|
||||
}
|
||||
#nx-cloud > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
#nx-cloud > div svg {
|
||||
border-radius: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
width: 3rem;
|
||||
}
|
||||
#nx-cloud > div h2 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#nx-cloud > div h2 span {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
#nx-cloud p {
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud pre {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
#nx-cloud a {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 1.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
#nx-cloud a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
#commands {
|
||||
padding: 2.5rem 2rem;
|
||||
margin-top: 3.5rem;
|
||||
}
|
||||
#commands h2 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.025em;
|
||||
line-height: 1.75rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
#commands p {
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
line-height: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
details {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
margin-top: 1rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
details pre > span {
|
||||
color: rgba(181, 181, 181, 1);
|
||||
display: block;
|
||||
}
|
||||
summary {
|
||||
border-radius: 0.5rem;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
transition-property: background-color, border-color, color, fill, stroke,
|
||||
opacity, box-shadow, transform, filter, backdrop-filter,
|
||||
-webkit-backdrop-filter;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
transition-duration: 150ms;
|
||||
}
|
||||
summary:hover {
|
||||
background-color: rgba(243, 244, 246, 1);
|
||||
}
|
||||
summary svg {
|
||||
height: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
width: 1.5rem;
|
||||
}
|
||||
#love {
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.25rem;
|
||||
margin-top: 3.5rem;
|
||||
opacity: 0.6;
|
||||
text-align: center;
|
||||
}
|
||||
#love svg {
|
||||
color: rgba(252, 165, 165, 1);
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
display: inline;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
@media screen and (min-width: 768px) {
|
||||
#hero {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
#hero .logo-container {
|
||||
display: flex;
|
||||
}
|
||||
#middle-content {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "data-literate",
|
||||
"name": "ckan",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "examples/data-literate",
|
||||
"sourceRoot": "examples/ckan",
|
||||
"projectType": "application",
|
||||
"targets": {
|
||||
"build": {
|
||||
@ -9,12 +9,12 @@
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"root": "examples/data-literate",
|
||||
"outputPath": "dist/examples/data-literate"
|
||||
"root": "examples/ckan",
|
||||
"outputPath": "dist/examples/ckan"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"outputPath": "examples/data-literate"
|
||||
"outputPath": "examples/ckan"
|
||||
},
|
||||
"production": {}
|
||||
}
|
||||
@ -23,16 +23,16 @@
|
||||
"executor": "@nrwl/next:server",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "data-literate:build",
|
||||
"buildTarget": "ckan:build",
|
||||
"dev": true
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "data-literate:build:development",
|
||||
"buildTarget": "ckan:build:development",
|
||||
"dev": true
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "data-literate:build:production",
|
||||
"buildTarget": "ckan:build:production",
|
||||
"dev": false
|
||||
}
|
||||
}
|
||||
@ -40,14 +40,14 @@
|
||||
"export": {
|
||||
"executor": "@nrwl/next:export",
|
||||
"options": {
|
||||
"buildTarget": "data-literate:build:production"
|
||||
"buildTarget": "ckan:build:production"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nrwl/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "examples/data-literate/jest.config.ts",
|
||||
"jestConfig": "examples/ckan/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
@ -61,7 +61,7 @@
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"options": {
|
||||
"lintFilePatterns": ["examples/data-literate/**/*.{ts,tsx,js,jsx}"]
|
||||
"lintFilePatterns": ["examples/ckan/**/*.{ts,tsx,js,jsx}"]
|
||||
}
|
||||
}
|
||||
},
|
||||
BIN
examples/ckan/public/favicon.ico
Normal file
BIN
examples/ckan/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
1
examples/ckan/public/images/banner.svg
Normal file
1
examples/ckan/public/images/banner.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 85 KiB |
1
examples/ckan/public/images/logo.svg
Normal file
1
examples/ckan/public/images/logo.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 408 B |
3
examples/ckan/styles/globals.css
Normal file
3
examples/ckan/styles/globals.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@ -6,7 +6,7 @@ module.exports = {
|
||||
content: [
|
||||
join(
|
||||
__dirname,
|
||||
'{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html,js,jsx}'
|
||||
'{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}'
|
||||
),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
13
examples/ckan/themes/base.ts
Normal file
13
examples/ckan/themes/base.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export default {
|
||||
primary: '#896A00',
|
||||
secondary: '#254E70',
|
||||
black: '#0C0C0C',
|
||||
positive: '#0C0C0C',
|
||||
textPrimary: '#896A00',
|
||||
backgroundPrimary: '#FAEEC5',
|
||||
|
||||
// Define font size variables
|
||||
fontSmall: '18px',
|
||||
fontMedium: '30px',
|
||||
fontLarge: '45px',
|
||||
};
|
||||
11
examples/ckan/themes/index.ts
Normal file
11
examples/ckan/themes/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import base from './base';
|
||||
import { IThemes } from './utils';
|
||||
|
||||
/**
|
||||
* The default theme to load
|
||||
*/
|
||||
export const DEFAULT_THEME = 'base';
|
||||
|
||||
export const themes: IThemes = {
|
||||
base,
|
||||
};
|
||||
6
examples/ckan/themes/primary.ts
Normal file
6
examples/ckan/themes/primary.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { extend } from './utils';
|
||||
import base from './base';
|
||||
|
||||
export default extend(base, {
|
||||
// Custom styles for primary theme
|
||||
});
|
||||
47
examples/ckan/themes/utils.ts
Normal file
47
examples/ckan/themes/utils.ts
Normal file
@ -0,0 +1,47 @@
|
||||
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 };
|
||||
};
|
||||
541
examples/ckan/utils/index.js
Normal file
541
examples/ckan/utils/index.js
Normal file
@ -0,0 +1,541 @@
|
||||
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;
|
||||
};
|
||||
@ -1,60 +0,0 @@
|
||||
This example renders markdown + CSV into an elegant web page. These type of data setup we term [data literate][]
|
||||
|
||||
[data literate]: https://portaljs.org/data-literate
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
npx create-next-app -e https://github.com/datopian/portal.js/tree/main/examples/data-literate
|
||||
# choose a name for your portal when prompted e.g. your-portal or go with default my-app
|
||||
|
||||
# then run it
|
||||
cd your-portal
|
||||
yarn #install packages
|
||||
yarn dev # start app in dev mode
|
||||
```
|
||||
|
||||
You should see the demo portal running with the example dataset provided in `http://localhost:3000/demo`
|
||||
|
||||
For the moment there is no root path and each markdown file will have it's own path (route) for the generated html code.
|
||||
|
||||
TODO
|
||||
### Use your own dataset
|
||||
|
||||
You can try it out with your own data literate setups:
|
||||
|
||||
In the directory of your portal do:
|
||||
|
||||
```bash
|
||||
export PORTAL_DATASET_PATH=/path/to/my/dataset
|
||||
```
|
||||
|
||||
Then restart the dev server:
|
||||
|
||||
```
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Check the portal page and it should have updated e.g. like:
|
||||
|
||||
TODO
|
||||
|
||||
### Static Export
|
||||
|
||||
Build the export:
|
||||
|
||||
```
|
||||
yarn build
|
||||
```
|
||||
|
||||
Results will be in `out/` subfolder.
|
||||
|
||||
To test you will need to run a local webserver in the folder (just opening the relevant file in your browser won't work):
|
||||
|
||||
Here we do this with another (non nodejs based) server to show that the static site works. Python3 as a really useful simple http server that one can use here:
|
||||
|
||||
```
|
||||
cd out
|
||||
python3 -m http.server
|
||||
```
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
import Layout from '../components/Layout'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import Head from 'next/head'
|
||||
import TableGrid from './TableGrid'
|
||||
import Table from '../components/Table'
|
||||
import Excel from '../components/Excel'
|
||||
import LineChart from '../components/LineChart'
|
||||
import { Vega, VegaLite } from 'react-vega'
|
||||
|
||||
// Custom components/renderers to pass to MDX.
|
||||
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
||||
// to handle import statements. Instead, you must include components in scope
|
||||
// here.
|
||||
const components = {
|
||||
Table,
|
||||
TableGrid,
|
||||
Excel,
|
||||
Vega,
|
||||
VegaLite,
|
||||
LineChart,
|
||||
Head,
|
||||
}
|
||||
|
||||
export default function DataLiterate({ children, source, frontMatter }) {
|
||||
return (
|
||||
<Layout title={frontMatter.title}>
|
||||
<div className="prose mx-auto">
|
||||
<header>
|
||||
<div className="mb-6">
|
||||
<h1>{frontMatter.title}</h1>
|
||||
{frontMatter.author && (
|
||||
<div className="-mt-6"><p className="opacity-60 pl-1">{frontMatter.author}</p></div>
|
||||
)}
|
||||
{frontMatter.description && (
|
||||
<p className="description">{frontMatter.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<MDXRemote {...source} components={components} />
|
||||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import XLSX from 'xlsx'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import Table from './Table'
|
||||
|
||||
export default function Excel ({ src='' }) {
|
||||
const [data, setData] = React.useState([])
|
||||
const [cols, setCols] = React.useState([])
|
||||
const [workbook, setWorkbook] = React.useState(null)
|
||||
const [error, setError] = React.useState('')
|
||||
const [hasMounted, setHasMounted] = React.useState(0)
|
||||
|
||||
// so this is here so we re-render this in the browser
|
||||
// and not just when we build the page statically in nextjs
|
||||
useEffect(() => {
|
||||
if (hasMounted==0) {
|
||||
handleUrl(src)
|
||||
}
|
||||
setHasMounted(1)
|
||||
})
|
||||
|
||||
function handleUrl(url) {
|
||||
// if url is external may have CORS issue so we proxy it ...
|
||||
if (url.startsWith('http')) {
|
||||
const PROXY_URL = window.location.origin + '/api/proxy'
|
||||
url = PROXY_URL + '?url=' + encodeURIComponent(url)
|
||||
}
|
||||
axios.get(url, {
|
||||
responseType: 'arraybuffer'
|
||||
}).then((res) => {
|
||||
let out = new Uint8Array(res.data)
|
||||
let workbook = XLSX.read(out, {type: "array"})
|
||||
// Get first worksheet
|
||||
const wsname = workbook.SheetNames[0]
|
||||
const ws = workbook.Sheets[wsname]
|
||||
// Convert array of arrays
|
||||
const datatmp = XLSX.utils.sheet_to_json(ws, {header:1})
|
||||
const colstmp = make_cols(ws['!ref'])
|
||||
setData(datatmp)
|
||||
setCols(colstmp)
|
||||
setWorkbook(workbook)
|
||||
}).catch((e) => {
|
||||
setError(e.message)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{error &&
|
||||
<div>
|
||||
There was an error loading the excel file at {src}:
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
}
|
||||
{workbook &&
|
||||
<ul>
|
||||
{workbook.SheetNames.map((value, index) => {
|
||||
return <li key={index}>{value}</li>
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
<Table data={data} cols={cols} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/* generate an array of column objects */
|
||||
const make_cols = refstr => {
|
||||
let o = [], C = XLSX.utils.decode_range(refstr).e.c + 1
|
||||
for(var i = 0; i < C; ++i) o[i] = {name:XLSX.utils.encode_col(i), key:i}
|
||||
return o
|
||||
}
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import Head from 'next/head'
|
||||
|
||||
export default function Layout({ children, title = 'Home' }) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Portal.JS - {title}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="initial-scale=1.0, width=device-width" />
|
||||
</Head>
|
||||
<div className="mx-auto p-6">
|
||||
{children}
|
||||
</div>
|
||||
<footer className="flex items-center justify-center w-full h-24 border-t">
|
||||
<a
|
||||
className="flex items-center justify-center"
|
||||
href="https://datopian.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Built by{' '}
|
||||
<img src="/datopian-logo.png" alt="Datopian Logo" className="h-6 ml-2" />
|
||||
</a>
|
||||
</footer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
import { Vega, VegaLite } from 'react-vega'
|
||||
|
||||
export default function LineChart( { data=[] }) {
|
||||
var tmp = data
|
||||
if (Array.isArray(data)) {
|
||||
tmp = data.map((r,i) => {
|
||||
return { x: r[0], y: r[1] }
|
||||
})
|
||||
}
|
||||
const vegaData = { "table": tmp }
|
||||
const spec = {
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"mark": "line",
|
||||
"data": {
|
||||
"name": "table"
|
||||
},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "x",
|
||||
"timeUnit": "year",
|
||||
"type": "temporal"
|
||||
},
|
||||
"y": {
|
||||
"field": "y",
|
||||
"type": "quantitative"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<VegaLite data={ vegaData } spec={ spec } />
|
||||
)
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
const papa = require("papaparse")
|
||||
|
||||
/*
|
||||
Simple HTML Table
|
||||
usage: <OutTable data={data} cols={cols} />
|
||||
data:Array<Array<any> >;
|
||||
cols:Array<{name:string, key:number|string}>;
|
||||
*/
|
||||
export default function Table({ data=[], cols=[], csv='', url='' }) {
|
||||
if (csv) {
|
||||
const out = parseCsv(csv)
|
||||
data = out.rows
|
||||
cols = out.cols
|
||||
}
|
||||
|
||||
const [ourdata, setData] = React.useState(data)
|
||||
const [ourcols, setCols] = React.useState(cols)
|
||||
const [error, setError] = React.useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
loadUrl(url)
|
||||
}
|
||||
}, [url])
|
||||
|
||||
function loadUrl(path) {
|
||||
// HACK: duplicate of Excel code - maybe refactor
|
||||
// if url is external may have CORS issue so we proxy it ...
|
||||
if (url.startsWith('http')) {
|
||||
const PROXY_URL = window.location.origin + '/api/proxy'
|
||||
url = PROXY_URL + '?url=' + encodeURIComponent(url)
|
||||
}
|
||||
axios.get(url).then((res) => {
|
||||
const { rows, fields } = parseCsv(res.data)
|
||||
setData(rows)
|
||||
setCols(fields)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SimpleTable data={ourdata} cols={ourcols} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
Simple HTML Table
|
||||
usage: <OutTable data={data} cols={cols} />
|
||||
data:Array<Array<any> >;
|
||||
cols:Array<{name:string, key:number|string}>;
|
||||
*/
|
||||
function SimpleTable({ data=[], cols=[] }) {
|
||||
return (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>{cols.map((c) => <th key={c.key}>{c.name}</th>)}</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((r,i) => <tr key={i}>
|
||||
{cols.map(c => <td key={c.key}>{ r[c.key] }</td>)}
|
||||
</tr>)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function parseCsv(csv) {
|
||||
csv = csv.trim()
|
||||
const rawdata = papa.parse(csv, {header: true})
|
||||
const cols = rawdata.meta.fields.map((r,i) => {
|
||||
return { key: r, name: r }
|
||||
})
|
||||
return {
|
||||
rows: rawdata.data,
|
||||
fields: cols
|
||||
}
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import React, { useEffect } from 'react'
|
||||
import { Table } from '@portaljs/portaljs-components'
|
||||
|
||||
const papa = require("papaparse")
|
||||
|
||||
/*
|
||||
Portaljs Table Grid
|
||||
usage: <TableGrid url="" data={data} cols={cols} csv="" />
|
||||
*/
|
||||
export default function TableGrid({ data = [], cols = [], csv = '', url = '' }) {
|
||||
|
||||
if (csv) {
|
||||
const out = parseCsv(csv)
|
||||
data = prepareRowsForPortalJsTable(out.rows)
|
||||
cols = out.fields
|
||||
}
|
||||
|
||||
if (cols) {
|
||||
cols = prepareColsForPortalJsTable(cols)
|
||||
}
|
||||
|
||||
const [ourdata, setData] = React.useState(data)
|
||||
const [ourcols, setCols] = React.useState(cols)
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
loadUrl(url)
|
||||
}
|
||||
}, [url])
|
||||
|
||||
function loadUrl(path) {
|
||||
// HACK: duplicate of Excel code - maybe refactor
|
||||
// if url is external may have CORS issue so we proxy it ...
|
||||
if (url.startsWith('http')) {
|
||||
const PROXY_URL = window.location.origin + '/api/proxy'
|
||||
url = PROXY_URL + '?url=' + encodeURIComponent(url)
|
||||
}
|
||||
axios.get(url).then((res) => {
|
||||
const { rows, fields } = parseCsv(res.data)
|
||||
setData(rows)
|
||||
setCols(prepareColsForPortalJsTable(fields))
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<Table columns={ourcols} data={ourdata} height={"400px"} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function prepareColsForPortalJsTable(cols) {
|
||||
return cols.map((col) => {
|
||||
return {
|
||||
field: col.key,
|
||||
headerName: col.name,
|
||||
flex: true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function prepareRowsForPortalJsTable(rows) {
|
||||
return rows.map((r) => {
|
||||
return {
|
||||
...r,
|
||||
id: r.id || r.key
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function parseCsv(csv) {
|
||||
csv = csv.trim()
|
||||
const rawdata = papa.parse(csv, { header: true })
|
||||
const cols = rawdata.meta.fields.map((r, i) => {
|
||||
return { key: r, name: r }
|
||||
})
|
||||
return {
|
||||
rows: rawdata.data,
|
||||
fields: cols
|
||||
}
|
||||
}
|
||||
@ -1,331 +0,0 @@
|
||||
---
|
||||
title: Demo
|
||||
---
|
||||
|
||||
This demos and documents Data Literate features live.
|
||||
|
||||
You can see the raw source of this page here: https://raw.githubusercontent.com/datopian/data-literate/main/content/demo.mdx
|
||||
|
||||
## Table of Contents
|
||||
|
||||
## GFM
|
||||
|
||||
We can have github-flavored markdown including markdown tables, auto-linked links and checklists:
|
||||
|
||||
```
|
||||
https://github.com/datopian/portal.js
|
||||
|
||||
| a | b |
|
||||
|---|---|
|
||||
| 1 | 2 |
|
||||
|
||||
* [x] one thing to do
|
||||
* [ ] a second thing to do
|
||||
```
|
||||
|
||||
https://github.com/datopian/portal.js
|
||||
|
||||
| a | b |
|
||||
|---|---|
|
||||
| 1 | 2 |
|
||||
|
||||
* [x] one thing to do
|
||||
* [ ] a second thing to do
|
||||
|
||||
## Footnotes
|
||||
|
||||
```
|
||||
here is a footnote reference[^1]
|
||||
|
||||
[^1]: a very interesting footnote.
|
||||
```
|
||||
|
||||
here is a footnote reference[^1]
|
||||
|
||||
[^1]: a very interesting footnote.
|
||||
|
||||
|
||||
## Frontmatter
|
||||
|
||||
Posts can have frontmatter like:
|
||||
|
||||
```
|
||||
---
|
||||
title: Hello World
|
||||
author: Rufus Pollock
|
||||
---
|
||||
```
|
||||
|
||||
The title and description are pulled from the MDX file and processed using `gray-matter`. Additionally, links are rendered using a custom component passed to `next-mdx-remote`.
|
||||
|
||||
## A Table of Contents
|
||||
|
||||
You can create a table of contents by having a markdown heading named `Table of Contents`. You can see an example at the start of this post.
|
||||
|
||||
|
||||
## A Table
|
||||
|
||||
You can create tables ...
|
||||
|
||||
```
|
||||
<Table cols={[
|
||||
{ key: 'id', name: 'ID' },
|
||||
{ key: 'firstName', name: 'First name' },
|
||||
{ key: 'lastName', name: 'Last name' },
|
||||
{ key: 'age', name: 'Age' }
|
||||
]} data={[
|
||||
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
|
||||
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
|
||||
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
|
||||
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
|
||||
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
|
||||
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
<Table cols={[
|
||||
{ key: 'id', name: 'ID' },
|
||||
{ key: 'firstName', name: 'First name' },
|
||||
{ key: 'lastName', name: 'Last name' },
|
||||
{ key: 'age', name: 'Age' }
|
||||
]} data={[
|
||||
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
|
||||
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
|
||||
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
|
||||
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
|
||||
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
|
||||
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
|
||||
]}
|
||||
/>
|
||||
|
||||
### Table from Raw CSV
|
||||
|
||||
You can also pass raw CSV as the content ...
|
||||
|
||||
```
|
||||
<Table csv={`
|
||||
Year,Temp Anomaly
|
||||
1850,-0.418
|
||||
2020,0.923
|
||||
`} />
|
||||
```
|
||||
|
||||
<Table csv={`
|
||||
Year,Temp Anomaly,
|
||||
1850,-0.418
|
||||
2020,0.923
|
||||
`} />
|
||||
|
||||
### Table from a URL
|
||||
|
||||
<Table url='/_files/HadCRUT.5.0.1.0.analysis.summary_series.global.annual.csv' />
|
||||
|
||||
```
|
||||
<Table url='/_files/HadCRUT.5.0.1.0.analysis.summary_series.global.annual.csv' />
|
||||
```
|
||||
___
|
||||
|
||||
You can also create a Table Grid, with more advance features
|
||||
|
||||
```
|
||||
<TableGrid cols={[
|
||||
{ key: 'id', name: 'ID' },
|
||||
{ key: 'firstName', name: 'First name' },
|
||||
{ key: 'lastName', name: 'Last name' },
|
||||
{ key: 'age', name: 'Age' }
|
||||
]} data={[
|
||||
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
|
||||
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
|
||||
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
|
||||
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
|
||||
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
|
||||
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
<TableGrid cols={[
|
||||
{ key: 'id', name: 'ID' },
|
||||
{ key: 'firstName', name: 'First name' },
|
||||
{ key: 'lastName', name: 'Last name' },
|
||||
{ key: 'age', name: 'Age' }
|
||||
]} data={[
|
||||
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
|
||||
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
|
||||
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
|
||||
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
|
||||
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
|
||||
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
|
||||
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
|
||||
]}
|
||||
/>
|
||||
|
||||
### Table Grid from Raw CSV
|
||||
|
||||
You can also pass raw CSV as the content ...
|
||||
|
||||
```
|
||||
<TableGrid csv={`
|
||||
Year,Temp Anomaly
|
||||
1850,-0.418
|
||||
2020,0.923
|
||||
`} />
|
||||
```
|
||||
|
||||
<TableGrid csv={`
|
||||
Year,Temp Anomaly,
|
||||
1850,-0.418
|
||||
2020,0.923
|
||||
`} />
|
||||
|
||||
### Table Grid from a URL
|
||||
|
||||
```
|
||||
<TableGrid url='/_files/HadCRUT.5.0.1.0.analysis.summary_series.global.annual.csv' />
|
||||
```
|
||||
|
||||
<TableGrid url='/_files/HadCRUT.5.0.1.0.analysis.summary_series.global.annual.csv' />
|
||||
|
||||
## Charts
|
||||
|
||||
You can create charts using a simple syntax.
|
||||
|
||||
### Line Chart
|
||||
|
||||
<LineChart data={
|
||||
[
|
||||
["1850",-0.41765878],
|
||||
["1851",-0.2333498],
|
||||
["1852",-0.22939907],
|
||||
["1853",-0.27035445],
|
||||
["1854",-0.29163003]
|
||||
]
|
||||
}
|
||||
/>
|
||||
|
||||
```
|
||||
<LineChart data={
|
||||
[
|
||||
["1850",-0.41765878],
|
||||
["1851",-0.2333498],
|
||||
["1852",-0.22939907],
|
||||
["1853",-0.27035445],
|
||||
["1854",-0.29163003]
|
||||
]
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
NB: we have quoted years as otherwise not interpreted as dates but as integers ...
|
||||
|
||||
|
||||
### Vega and Vega Lite
|
||||
|
||||
You can using vega or vega-lite. Here's an example using vega-lite:
|
||||
|
||||
<VegaLite data={ { "table": [
|
||||
{
|
||||
"y": -0.418,
|
||||
"x": 1850
|
||||
},
|
||||
{
|
||||
"y": 0.923,
|
||||
"x": 2020
|
||||
}
|
||||
]
|
||||
}
|
||||
} spec={
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
|
||||
"mark": "bar",
|
||||
"data": {
|
||||
"name": "table"
|
||||
},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "x",
|
||||
"type": "ordinal"
|
||||
},
|
||||
"y": {
|
||||
"field": "y",
|
||||
"type": "quantitative"
|
||||
}
|
||||
}
|
||||
}
|
||||
} />
|
||||
|
||||
|
||||
```jsx
|
||||
<VegaLite data={ { "table": [
|
||||
{
|
||||
"y": -0.418,
|
||||
"x": 1850
|
||||
},
|
||||
{
|
||||
"y": 0.923,
|
||||
"x": 2020
|
||||
}
|
||||
]
|
||||
}
|
||||
} spec={
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
|
||||
"mark": "bar",
|
||||
"data": {
|
||||
"name": "table"
|
||||
},
|
||||
"encoding": {
|
||||
"x": {
|
||||
"field": "x",
|
||||
"type": "ordinal"
|
||||
},
|
||||
"y": {
|
||||
"field": "y",
|
||||
"type": "quantitative"
|
||||
}
|
||||
}
|
||||
}
|
||||
} />
|
||||
|
||||
```
|
||||
|
||||
#### Line Chart from URL with Tooltip
|
||||
|
||||
https://vega.github.io/vega-lite/examples/interactive_multi_line_pivot_tooltip.html
|
||||
|
||||
<VegaLite spec={
|
||||
{
|
||||
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
"data": {"url": "/_files/HadCRUT.5.0.1.0.analysis.summary_series.global.annual.csv"},
|
||||
"width": 600,
|
||||
"height": 250,
|
||||
"mark": "line",
|
||||
"encoding": {
|
||||
"x": {"field": "Time", "type": "temporal"},
|
||||
"y": {"field": "Anomaly (deg C)", "type": "quantitative"},
|
||||
"tooltip": {"field": "Anomaly (deg C)", "type": "quantitative"}
|
||||
}
|
||||
}
|
||||
} />
|
||||
|
||||
## Display Excel Files
|
||||
|
||||
Local file ...
|
||||
|
||||
```
|
||||
<Excel src='/_files/eight-centuries-of-global-real-interest-rates-r-g-and-the-suprasecular-decline-1311-2018-data.xlsx' />
|
||||
```
|
||||
|
||||
<Excel src='/_files/eight-centuries-of-global-real-interest-rates-r-g-and-the-suprasecular-decline-1311-2018-data.xlsx' />
|
||||
|
||||
Remote files work too (even without CORS) thanks to proxying:
|
||||
|
||||
```
|
||||
<Excel src='https://github.com/datasets/awesome-data/files/6604635/eight-centuries-of-global-real-interest-rates-r-g-and-the-suprasecular-decline-1311-2018-data.xlsx' />
|
||||
```
|
||||
|
||||
<Excel src='https://github.com/datasets/awesome-data/files/6604635/eight-centuries-of-global-real-interest-rates-r-g-and-the-suprasecular-decline-1311-2018-data.xlsx' />
|
||||
@ -1,18 +0,0 @@
|
||||
const path = require('path')
|
||||
const fs = require('fs');
|
||||
|
||||
const srcPath = process.argv[2]
|
||||
const destForMarkdown = './content'
|
||||
const destForData = './public'
|
||||
|
||||
const readme = path.join(srcPath, 'README.md')
|
||||
const readmeDest = path.join(destForMarkdown, 'README.md')
|
||||
|
||||
fs.copyFileSync(readme, readmeDest)
|
||||
|
||||
const data = path.join(srcPath, 'data.csv')
|
||||
const dataDest = path.join(destForData, 'data.csv')
|
||||
|
||||
fs.copyFileSync(data, dataDest)
|
||||
|
||||
console.log('Done')
|
||||
@ -1,33 +0,0 @@
|
||||
import matter from 'gray-matter'
|
||||
import toc from 'remark-toc'
|
||||
import slug from 'remark-slug'
|
||||
import gfm from 'remark-gfm'
|
||||
import footnotes from 'remark-footnotes'
|
||||
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
|
||||
/**
|
||||
* Parse a markdown or MDX file to an MDX source form + front matter data
|
||||
*
|
||||
* @source: the contents of a markdown or mdx file
|
||||
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
||||
*/
|
||||
const parse = async function(source) {
|
||||
const { content, data } = matter(source)
|
||||
|
||||
const mdxSource = await serialize(content, {
|
||||
// Optionally pass remark/rehype plugins
|
||||
mdxOptions: {
|
||||
remarkPlugins: [gfm, toc, slug, footnotes],
|
||||
rehypePlugins: [],
|
||||
},
|
||||
scope: data,
|
||||
})
|
||||
|
||||
return {
|
||||
mdxSource: mdxSource,
|
||||
frontMatter: data
|
||||
}
|
||||
}
|
||||
|
||||
export default parse
|
||||
@ -1,23 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import glob from 'glob'
|
||||
import path from 'path'
|
||||
|
||||
// POSTS_PATH is useful when you want to get the path to a specific file
|
||||
export const POSTS_PATH = path.join(process.cwd(), '/examples/data-literate/content')
|
||||
|
||||
const walkSync = (dir, filelist = []) => {
|
||||
fs.readdirSync(dir).forEach(file => {
|
||||
|
||||
filelist = fs.statSync(path.join(dir, file)).isDirectory()
|
||||
? walkSync(path.join(dir, file), filelist)
|
||||
: filelist.concat(path.join(dir, file))
|
||||
|
||||
})
|
||||
return filelist
|
||||
}
|
||||
|
||||
// postFilePaths is the list of all mdx files inside the POSTS_PATH directory
|
||||
export const postFilePaths = walkSync(POSTS_PATH)
|
||||
.map((file) => { return file.slice(POSTS_PATH.length) })
|
||||
// Only include md(x) files
|
||||
.filter((path) => /\.mdx?$/.test(path))
|
||||
@ -1,17 +0,0 @@
|
||||
//@ts-check
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { withNx } = require('@nrwl/next/plugins/with-nx');
|
||||
|
||||
/**
|
||||
* @type {import('@nrwl/next/plugins/with-nx').WithNxOptions}
|
||||
**/
|
||||
const nextConfig = {
|
||||
nx: {
|
||||
// Set this to true if you would like to use SVGR
|
||||
// See: https://github.com/gregberge/svgr
|
||||
svgr: false,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = withNx(nextConfig);
|
||||
@ -1,48 +0,0 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
import parse from '../lib/markdown.js'
|
||||
|
||||
import DataLiterate from '../components/DataLiterate'
|
||||
|
||||
import { postFilePaths, POSTS_PATH } from '../lib/mdxUtils'
|
||||
|
||||
|
||||
export default function PostPage({ source, frontMatter }) {
|
||||
return (
|
||||
<DataLiterate source={source} frontMatter={frontMatter} />
|
||||
)
|
||||
}
|
||||
|
||||
export const getStaticProps = async ({ params }) => {
|
||||
const mdxPath = path.join(POSTS_PATH, `${params.slug.join('/')}.mdx`)
|
||||
const postFilePath = fs.existsSync(mdxPath) ? mdxPath : mdxPath.slice(0, -1)
|
||||
const source = fs.readFileSync(postFilePath)
|
||||
|
||||
const { mdxSource, frontMatter } = await parse(source)
|
||||
|
||||
return {
|
||||
props: {
|
||||
source: mdxSource,
|
||||
frontMatter: frontMatter,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
var paths = postFilePaths
|
||||
// Remove file extensions for page paths
|
||||
.map((path) => path.replace(/\.mdx?$/, ''))
|
||||
|
||||
// Map the path into the static paths object required by Next.js
|
||||
paths = paths.map((slug) => {
|
||||
// /demo => [demo]
|
||||
const parts = slug.slice(1).split('/')
|
||||
return { params: { slug: parts } }
|
||||
})
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import axios from 'axios'
|
||||
|
||||
export default function handler(req, res) {
|
||||
if (!req.query.url) {
|
||||
res.status(200).send({
|
||||
error: true,
|
||||
info: 'No url to proxy in query string i.e. ?url=...'
|
||||
})
|
||||
return
|
||||
}
|
||||
axios({
|
||||
method: 'get',
|
||||
url: req.query.url,
|
||||
responseType:'stream'
|
||||
})
|
||||
.then(resp => {
|
||||
resp.data.pipe(res)
|
||||
})
|
||||
.catch(err => {
|
||||
res.status(400).send({
|
||||
error: true,
|
||||
info: err.message,
|
||||
detailed: err
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -1,173 +0,0 @@
|
||||
Time,Anomaly (deg C),Lower confidence limit (2.5%),Upper confidence limit (97.5%)
|
||||
1850,-0.41765878,-0.589203,-0.24611452
|
||||
1851,-0.2333498,-0.41186792,-0.054831687
|
||||
1852,-0.22939907,-0.40938243,-0.04941572
|
||||
1853,-0.27035445,-0.43000934,-0.110699534
|
||||
1854,-0.29163003,-0.43282393,-0.15043613
|
||||
1855,-0.2969512,-0.43935776,-0.15454465
|
||||
1856,-0.32035372,-0.46809322,-0.1726142
|
||||
1857,-0.46723005,-0.61632216,-0.31813794
|
||||
1858,-0.3887657,-0.53688604,-0.24064532
|
||||
1859,-0.28119546,-0.42384982,-0.13854107
|
||||
1860,-0.39016518,-0.5389766,-0.24135375
|
||||
1861,-0.42927712,-0.5972301,-0.26132414
|
||||
1862,-0.53639776,-0.7037096,-0.36908585
|
||||
1863,-0.3443432,-0.5341645,-0.1545219
|
||||
1864,-0.4654367,-0.6480974,-0.282776
|
||||
1865,-0.33258784,-0.5246526,-0.14052312
|
||||
1866,-0.34126064,-0.52183825,-0.16068307
|
||||
1867,-0.35696334,-0.55306214,-0.16086453
|
||||
1868,-0.35196072,-0.52965826,-0.17426313
|
||||
1869,-0.31657043,-0.47642276,-0.15671812
|
||||
1870,-0.32789087,-0.46867347,-0.18710826
|
||||
1871,-0.3685807,-0.5141493,-0.22301209
|
||||
1872,-0.32804197,-0.4630833,-0.19300064
|
||||
1873,-0.34133235,-0.4725396,-0.21012507
|
||||
1874,-0.3732512,-0.5071426,-0.2393598
|
||||
1875,-0.37562594,-0.514041,-0.23721085
|
||||
1876,-0.42410994,-0.56287116,-0.28534868
|
||||
1877,-0.101108834,-0.22982001,0.027602348
|
||||
1878,-0.011315193,-0.13121258,0.10858219
|
||||
1879,-0.30363432,-0.43406433,-0.1732043
|
||||
1880,-0.31583208,-0.44015095,-0.19151321
|
||||
1881,-0.23224552,-0.35793498,-0.10655605
|
||||
1882,-0.29553008,-0.4201501,-0.17091006
|
||||
1883,-0.3464744,-0.4608177,-0.23213111
|
||||
1884,-0.49232006,-0.6026686,-0.38197154
|
||||
1885,-0.47112358,-0.5830682,-0.35917896
|
||||
1886,-0.42090362,-0.5225382,-0.31926903
|
||||
1887,-0.49878576,-0.61655986,-0.3810117
|
||||
1888,-0.37937889,-0.49332377,-0.265434
|
||||
1889,-0.24989556,-0.37222093,-0.12757017
|
||||
1890,-0.50685817,-0.6324095,-0.3813068
|
||||
1891,-0.40131494,-0.5373699,-0.26525995
|
||||
1892,-0.5075585,-0.64432853,-0.3707885
|
||||
1893,-0.49461925,-0.6315314,-0.35770702
|
||||
1894,-0.48376393,-0.6255681,-0.34195974
|
||||
1895,-0.4487516,-0.58202064,-0.3154826
|
||||
1896,-0.28400728,-0.4174015,-0.15061308
|
||||
1897,-0.25980017,-0.39852425,-0.12107607
|
||||
1898,-0.48579213,-0.6176492,-0.35393503
|
||||
1899,-0.35543364,-0.48639694,-0.22447036
|
||||
1900,-0.23447904,-0.3669676,-0.10199049
|
||||
1901,-0.29342857,-0.42967388,-0.15718324
|
||||
1902,-0.43898427,-0.5754281,-0.30254042
|
||||
1903,-0.5333264,-0.66081935,-0.40583345
|
||||
1904,-0.5975614,-0.7288325,-0.46629035
|
||||
1905,-0.40775132,-0.5350291,-0.28047356
|
||||
1906,-0.3191393,-0.45052385,-0.18775477
|
||||
1907,-0.5041577,-0.6262818,-0.38203365
|
||||
1908,-0.5138707,-0.63748026,-0.3902612
|
||||
1909,-0.5357649,-0.6526296,-0.41890016
|
||||
1910,-0.5310242,-0.6556868,-0.40636164
|
||||
1911,-0.5392051,-0.66223973,-0.4161705
|
||||
1912,-0.47567302,-0.5893311,-0.36201498
|
||||
1913,-0.46715254,-0.5893755,-0.34492958
|
||||
1914,-0.2625924,-0.38276345,-0.1424214
|
||||
1915,-0.19184391,-0.32196194,-0.06172589
|
||||
1916,-0.42020997,-0.5588941,-0.28152588
|
||||
1917,-0.54301953,-0.6921192,-0.3939199
|
||||
1918,-0.42458433,-0.58198184,-0.26718682
|
||||
1919,-0.32551822,-0.48145813,-0.1695783
|
||||
1920,-0.2985808,-0.44860035,-0.14856121
|
||||
1921,-0.24067703,-0.38175339,-0.09960067
|
||||
1922,-0.33922812,-0.46610323,-0.21235302
|
||||
1923,-0.31793055,-0.444173,-0.1916881
|
||||
1924,-0.3120622,-0.4388317,-0.18529275
|
||||
1925,-0.28242525,-0.4147755,-0.15007503
|
||||
1926,-0.12283547,-0.25264767,0.006976739
|
||||
1927,-0.22940508,-0.35135695,-0.10745319
|
||||
1928,-0.20676155,-0.33881804,-0.074705064
|
||||
1929,-0.39275664,-0.52656746,-0.25894582
|
||||
1930,-0.1768054,-0.29041144,-0.06319936
|
||||
1931,-0.10339768,-0.2126916,0.0058962475
|
||||
1932,-0.14546166,-0.25195515,-0.0389682
|
||||
1933,-0.32234442,-0.4271004,-0.21758842
|
||||
1934,-0.17433685,-0.27400395,-0.07466974
|
||||
1935,-0.20605922,-0.30349734,-0.10862111
|
||||
1936,-0.16952093,-0.26351926,-0.07552261
|
||||
1937,-0.01919893,-0.11975875,0.08136089
|
||||
1938,-0.012200732,-0.11030374,0.08590227
|
||||
1939,-0.040797167,-0.14670466,0.065110326
|
||||
1940,0.07593584,-0.04194966,0.19382134
|
||||
1941,0.038129337,-0.16225387,0.23851255
|
||||
1942,0.0014060909,-0.1952124,0.19802457
|
||||
1943,0.0064140745,-0.19959097,0.21241911
|
||||
1944,0.14410514,-0.054494828,0.3427051
|
||||
1945,0.043088365,-0.15728289,0.24345961
|
||||
1946,-0.1188128,-0.2659574,0.028331792
|
||||
1947,-0.091205545,-0.23179041,0.04937931
|
||||
1948,-0.12466127,-0.25913337,0.009810844
|
||||
1949,-0.14380224,-0.2540775,-0.033526987
|
||||
1950,-0.22662179,-0.33265698,-0.12058662
|
||||
1951,-0.06115397,-0.15035024,0.028042298
|
||||
1952,0.015354565,-0.08293597,0.11364509
|
||||
1953,0.07763074,-0.020529618,0.1757911
|
||||
1954,-0.11675021,-0.20850271,-0.024997713
|
||||
1955,-0.19730993,-0.28442997,-0.1101899
|
||||
1956,-0.2631656,-0.33912563,-0.18720557
|
||||
1957,-0.035334926,-0.10056862,0.029898768
|
||||
1958,-0.017632553,-0.083074555,0.04780945
|
||||
1959,-0.048004825,-0.11036375,0.0143540995
|
||||
1960,-0.115487024,-0.17416587,-0.056808177
|
||||
1961,-0.019997388,-0.07078052,0.030785747
|
||||
1962,-0.06405444,-0.11731443,-0.010794453
|
||||
1963,-0.03680589,-0.09057008,0.016958294
|
||||
1964,-0.30586675,-0.34949213,-0.26224136
|
||||
1965,-0.2043879,-0.25357357,-0.15520222
|
||||
1966,-0.14888458,-0.19839221,-0.09937696
|
||||
1967,-0.11751631,-0.16062479,-0.07440783
|
||||
1968,-0.1686323,-0.21325313,-0.124011464
|
||||
1969,-0.031366713,-0.07186544,0.009132013
|
||||
1970,-0.08510657,-0.12608096,-0.04413217
|
||||
1971,-0.20593274,-0.24450706,-0.16735843
|
||||
1972,-0.0938271,-0.13171694,-0.05593726
|
||||
1973,0.04993336,0.013468528,0.086398184
|
||||
1974,-0.17253734,-0.21022376,-0.1348509
|
||||
1975,-0.11075424,-0.15130512,-0.07020335
|
||||
1976,-0.21586166,-0.25588378,-0.17583954
|
||||
1977,0.10308852,0.060056705,0.14612034
|
||||
1978,0.0052557723,-0.034576867,0.04508841
|
||||
1979,0.09085813,0.062358618,0.119357646
|
||||
1980,0.19607207,0.162804,0.22934014
|
||||
1981,0.25001204,0.21939126,0.28063282
|
||||
1982,0.034263328,-0.005104665,0.07363132
|
||||
1983,0.22383861,0.18807402,0.2596032
|
||||
1984,0.04800471,0.011560736,0.08444869
|
||||
1985,0.04972978,0.015663471,0.08379609
|
||||
1986,0.09568697,0.064408,0.12696595
|
||||
1987,0.2430264,0.21218552,0.27386728
|
||||
1988,0.28215173,0.2470353,0.31726816
|
||||
1989,0.17925027,0.14449838,0.21400215
|
||||
1990,0.36056247,0.32455227,0.39657268
|
||||
1991,0.33889654,0.30403617,0.3737569
|
||||
1992,0.124896795,0.09088206,0.15891153
|
||||
1993,0.16565846,0.12817313,0.2031438
|
||||
1994,0.23354977,0.19841294,0.2686866
|
||||
1995,0.37686616,0.34365577,0.41007656
|
||||
1996,0.2766894,0.24318004,0.31019878
|
||||
1997,0.4223085,0.39009082,0.4545262
|
||||
1998,0.57731646,0.54304415,0.6115888
|
||||
1999,0.32448497,0.29283476,0.35613516
|
||||
2000,0.3310848,0.29822788,0.36394167
|
||||
2001,0.48928034,0.4580683,0.5204924
|
||||
2002,0.5434665,0.51278186,0.57415116
|
||||
2003,0.5441702,0.5112426,0.5770977
|
||||
2004,0.46737072,0.43433833,0.5004031
|
||||
2005,0.60686255,0.5757053,0.6380198
|
||||
2006,0.5725527,0.541973,0.60313237
|
||||
2007,0.5917013,0.56135315,0.6220495
|
||||
2008,0.46564984,0.43265733,0.49864236
|
||||
2009,0.5967817,0.56525564,0.6283077
|
||||
2010,0.68037146,0.649076,0.7116669
|
||||
2011,0.53769773,0.5060012,0.5693943
|
||||
2012,0.5776071,0.5448553,0.6103589
|
||||
2013,0.6235754,0.5884838,0.6586669
|
||||
2014,0.67287165,0.63890487,0.7068384
|
||||
2015,0.82511437,0.79128706,0.8589417
|
||||
2016,0.93292713,0.90176356,0.96409065
|
||||
2017,0.84517425,0.81477475,0.87557375
|
||||
2018,0.762654,0.731052,0.79425603
|
||||
2019,0.8910726,0.85678726,0.92535794
|
||||
2020,0.9227938,0.8882121,0.9573755
|
||||
2021,0.6640137,0.5372486,0.79077876
|
||||
|
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@ -1,16 +0,0 @@
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
5
examples/dataset-frictionless/.babelrc
Normal file
5
examples/dataset-frictionless/.babelrc
Normal file
@ -0,0 +1,5 @@
|
||||
|
||||
|
||||
{
|
||||
"presets": ["next/babel"]
|
||||
}
|
||||
30
examples/dataset-frictionless/.gitignore
vendored
Normal file
30
examples/dataset-frictionless/.gitignore
vendored
Normal file
@ -0,0 +1,30 @@
|
||||
# dependencies
|
||||
node_modules/
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
coverage
|
||||
cypress/videos
|
||||
|
||||
# next.js
|
||||
.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
39
examples/dataset-frictionless/README.md
Normal file
39
examples/dataset-frictionless/README.md
Normal file
@ -0,0 +1,39 @@
|
||||
This example creates a portal/showcase for a single dataset. The dataset should be a [Frictionless dataset (data package)][fd] i.e. there should be a `datapackage.json`.
|
||||
|
||||
[fd]: https://frictionlessdata.io/data-packages/
|
||||
|
||||
## How to use
|
||||
|
||||
```bash
|
||||
npx create-next-app -e https://github.com/datopian/portal.js/tree/main/examples/dataset-frictionless
|
||||
# choose a name for your portal when prompted e.g. your-portal or go with default my-app
|
||||
|
||||
# then run it
|
||||
cd your-portal
|
||||
yarn #install packages
|
||||
yarn dev #start app in dev mode
|
||||
```
|
||||
|
||||
You should see the demo portal running with the example dataset provided:
|
||||
|
||||
<img src="./assets/demo.gif" />
|
||||
|
||||
### Use your own dataset
|
||||
|
||||
You can try it out with other [Frictionless datasets](https://datahub.io/search).
|
||||
|
||||
In the directory of your portal do:
|
||||
|
||||
```bash
|
||||
export PORTAL_DATASET_PATH=/path/to/my/dataset
|
||||
```
|
||||
|
||||
Then restart the dev server:
|
||||
|
||||
```
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Check the portal page and it should have updated e.g. like:
|
||||
|
||||

|
||||
BIN
examples/dataset-frictionless/assets/demo.gif
Normal file
BIN
examples/dataset-frictionless/assets/demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 MiB |
70
examples/dataset-frictionless/lib/dataset.js
Normal file
70
examples/dataset-frictionless/lib/dataset.js
Normal file
@ -0,0 +1,70 @@
|
||||
import remark from 'remark'
|
||||
import html from 'remark-html'
|
||||
import { Dataset } from 'frictionless.js'
|
||||
import toArray from 'stream-to-array'
|
||||
|
||||
|
||||
export async function getDataset(directory) {
|
||||
try {
|
||||
|
||||
if (!directory) {
|
||||
throw new Error('No directory provided.')
|
||||
}
|
||||
|
||||
const f11sDataset = await Dataset.load(directory)
|
||||
const descriptor = f11sDataset.descriptor
|
||||
|
||||
const resources = await Promise.all(f11sDataset.resources.map(async (resource) => {
|
||||
let _tmp = resource.descriptor
|
||||
let rowStream = await resource.rows({ keyed: true })
|
||||
_tmp.sample = await toArray(rowStream)
|
||||
_tmp.size = resource.size
|
||||
return _tmp
|
||||
}))
|
||||
|
||||
const readme = descriptor.readme || ""
|
||||
|
||||
const processed = await remark()
|
||||
.use(html)
|
||||
.process(readme)
|
||||
|
||||
const readmeHtml = processed.toString()
|
||||
|
||||
|
||||
const dataset = {
|
||||
readme: readme,
|
||||
readmeHtml: readmeHtml,
|
||||
descriptor: descriptor,
|
||||
resources: resources,
|
||||
hasError: false,
|
||||
errorMsg: ""
|
||||
}
|
||||
return dataset
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return {
|
||||
hasError: true,
|
||||
errorMsg: errorMessageMappings[err.message] || err.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const errorMessageMappings = {
|
||||
"No datapackage.json at destination.":
|
||||
`
|
||||
<div>
|
||||
<p style="color:red;"><b>No datapackage.json file in the data directory!</b></p>
|
||||
<p >You need to add a datapackage.json file describing your dataset to the root folder.</p>
|
||||
<p>For more information, see <a style="color:blue;" href="https://specs.frictionlessdata.io/tabular-data-package/#example">the documentation</a></p>
|
||||
</div>
|
||||
`,
|
||||
"No directory provided.":
|
||||
`
|
||||
<div>
|
||||
<p style="color:red;"><b>No data directory found!</b></p>
|
||||
<p >You need to provide a data directory with CSV data, datapackage.json, and optionally a ReadMe file.</p>
|
||||
<p>For more information, see <a style="color:blue;" href="https://specs.frictionlessdata.io/tabular-data-package/#example">the documentation</a></p>
|
||||
</div>
|
||||
`,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user