[monorepo][lg] - move over examples

This commit is contained in:
Luccas Mateus de Medeiros Gomes 2023-04-12 09:13:17 -03:00
parent d45b5a26d5
commit d466023926
122 changed files with 24606 additions and 6570 deletions

1
examples/ckan/.env Normal file
View File

@ -0,0 +1 @@
DMS=https://demo.dev.datopian.com/

View File

@ -10,10 +10,7 @@
{ {
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": { "rules": {
"@next/next/no-html-link-for-pages": [ "@next/next/no-html-link-for-pages": ["error", "packages/ckan/pages"]
"error",
"apps/data-literate/pages"
]
} }
}, },
{ {

306
examples/ckan/README.md Normal file
View 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 doesnt 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/

View 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();
});

View File

@ -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>
`;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,5 @@
import Table from './Table';
import ErrorMessage from './Error';
import CustomLink from './CustomLink';
export { Table, ErrorMessage, CustomLink };

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,7 @@
import Template from './HeroTemplate';
const Hero: React.FC = () => {
return <Template />;
};
export default Hero;

View 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>
);
}

View 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;

View 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;

View 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;

View 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;

View 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">&rarr;</span>{' '}
</a>
</div>
</div>
</div>
</div>
</div>
);
};
export default About;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View File

@ -0,0 +1,8 @@
module.exports = {
process() {
return 'module.exports = {};';
},
getCacheKey() {
return 'cssTransform';
},
};

View File

@ -0,0 +1,3 @@
{
"baseUrl": "http://localhost:3000"
}

View 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"
}

View 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);
});
});
});

View 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)'
// );
// });
});

View 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
};

View 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) => { ... })

View 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')

View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": "../node_modules",
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress"]
},
"include": [
"**/*.ts"
]
}

View 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
}
}
`;

View 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',
},
};

View File

@ -1,11 +1,11 @@
/* eslint-disable */ /* eslint-disable */
export default { export default {
displayName: 'data-literate', displayName: 'ckan',
preset: '../../jest.preset.js', preset: '../../jest.preset.js',
transform: { transform: {
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest', '^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }], '^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }],
}, },
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
coverageDirectory: '../../coverage/examples/data-literate', coverageDirectory: '../../coverage/examples/ckan',
}; };

View 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;
}

View 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."
}

View 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."
}

View 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');
};

View 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);

View 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;

View 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;

View 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;

View 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;

View 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>
);
}
}

View 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;

View 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;

View File

@ -0,0 +1,2 @@
.page {
}

View 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;

View 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;

View 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;

View 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));
}
}

View File

@ -1,7 +1,7 @@
{ {
"name": "data-literate", "name": "ckan",
"$schema": "../../node_modules/nx/schemas/project-schema.json", "$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "examples/data-literate", "sourceRoot": "examples/ckan",
"projectType": "application", "projectType": "application",
"targets": { "targets": {
"build": { "build": {
@ -9,12 +9,12 @@
"outputs": ["{options.outputPath}"], "outputs": ["{options.outputPath}"],
"defaultConfiguration": "production", "defaultConfiguration": "production",
"options": { "options": {
"root": "examples/data-literate", "root": "examples/ckan",
"outputPath": "dist/examples/data-literate" "outputPath": "dist/examples/ckan"
}, },
"configurations": { "configurations": {
"development": { "development": {
"outputPath": "examples/data-literate" "outputPath": "examples/ckan"
}, },
"production": {} "production": {}
} }
@ -23,16 +23,16 @@
"executor": "@nrwl/next:server", "executor": "@nrwl/next:server",
"defaultConfiguration": "development", "defaultConfiguration": "development",
"options": { "options": {
"buildTarget": "data-literate:build", "buildTarget": "ckan:build",
"dev": true "dev": true
}, },
"configurations": { "configurations": {
"development": { "development": {
"buildTarget": "data-literate:build:development", "buildTarget": "ckan:build:development",
"dev": true "dev": true
}, },
"production": { "production": {
"buildTarget": "data-literate:build:production", "buildTarget": "ckan:build:production",
"dev": false "dev": false
} }
} }
@ -40,14 +40,14 @@
"export": { "export": {
"executor": "@nrwl/next:export", "executor": "@nrwl/next:export",
"options": { "options": {
"buildTarget": "data-literate:build:production" "buildTarget": "ckan:build:production"
} }
}, },
"test": { "test": {
"executor": "@nrwl/jest:jest", "executor": "@nrwl/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"], "outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": { "options": {
"jestConfig": "examples/data-literate/jest.config.ts", "jestConfig": "examples/ckan/jest.config.ts",
"passWithNoTests": true "passWithNoTests": true
}, },
"configurations": { "configurations": {
@ -61,7 +61,7 @@
"executor": "@nrwl/linter:eslint", "executor": "@nrwl/linter:eslint",
"outputs": ["{options.outputFile}"], "outputs": ["{options.outputFile}"],
"options": { "options": {
"lintFilePatterns": ["examples/data-literate/**/*.{ts,tsx,js,jsx}"] "lintFilePatterns": ["examples/ckan/**/*.{ts,tsx,js,jsx}"]
} }
} }
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 85 KiB

View 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

View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -6,7 +6,7 @@ module.exports = {
content: [ content: [
join( join(
__dirname, __dirname,
'{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html,js,jsx}' '{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}'
), ),
...createGlobPatternsForDependencies(__dirname), ...createGlobPatternsForDependencies(__dirname),
], ],

View 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',
};

View 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,
};

View File

@ -0,0 +1,6 @@
import { extend } from './utils';
import base from './base';
export default extend(base, {
// Custom styles for primary theme
});

View 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 };
};

View 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, '&#x27;');
newDatapackage.displayResources[idx].dataExplorers.push(dataExplorer);
});
});
return newDatapackage;
};

View File

@ -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
```

View File

@ -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>
)
}

View File

@ -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
}

View File

@ -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>
</>
)
}

View File

@ -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 } />
)
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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' />

View File

@ -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')

View File

@ -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

View File

@ -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))

View File

@ -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);

View File

@ -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,
}
}

View File

@ -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
})
})
}

View File

@ -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
1 Time Anomaly (deg C) Lower confidence limit (2.5%) Upper confidence limit (97.5%)
2 1850 -0.41765878 -0.589203 -0.24611452
3 1851 -0.2333498 -0.41186792 -0.054831687
4 1852 -0.22939907 -0.40938243 -0.04941572
5 1853 -0.27035445 -0.43000934 -0.110699534
6 1854 -0.29163003 -0.43282393 -0.15043613
7 1855 -0.2969512 -0.43935776 -0.15454465
8 1856 -0.32035372 -0.46809322 -0.1726142
9 1857 -0.46723005 -0.61632216 -0.31813794
10 1858 -0.3887657 -0.53688604 -0.24064532
11 1859 -0.28119546 -0.42384982 -0.13854107
12 1860 -0.39016518 -0.5389766 -0.24135375
13 1861 -0.42927712 -0.5972301 -0.26132414
14 1862 -0.53639776 -0.7037096 -0.36908585
15 1863 -0.3443432 -0.5341645 -0.1545219
16 1864 -0.4654367 -0.6480974 -0.282776
17 1865 -0.33258784 -0.5246526 -0.14052312
18 1866 -0.34126064 -0.52183825 -0.16068307
19 1867 -0.35696334 -0.55306214 -0.16086453
20 1868 -0.35196072 -0.52965826 -0.17426313
21 1869 -0.31657043 -0.47642276 -0.15671812
22 1870 -0.32789087 -0.46867347 -0.18710826
23 1871 -0.3685807 -0.5141493 -0.22301209
24 1872 -0.32804197 -0.4630833 -0.19300064
25 1873 -0.34133235 -0.4725396 -0.21012507
26 1874 -0.3732512 -0.5071426 -0.2393598
27 1875 -0.37562594 -0.514041 -0.23721085
28 1876 -0.42410994 -0.56287116 -0.28534868
29 1877 -0.101108834 -0.22982001 0.027602348
30 1878 -0.011315193 -0.13121258 0.10858219
31 1879 -0.30363432 -0.43406433 -0.1732043
32 1880 -0.31583208 -0.44015095 -0.19151321
33 1881 -0.23224552 -0.35793498 -0.10655605
34 1882 -0.29553008 -0.4201501 -0.17091006
35 1883 -0.3464744 -0.4608177 -0.23213111
36 1884 -0.49232006 -0.6026686 -0.38197154
37 1885 -0.47112358 -0.5830682 -0.35917896
38 1886 -0.42090362 -0.5225382 -0.31926903
39 1887 -0.49878576 -0.61655986 -0.3810117
40 1888 -0.37937889 -0.49332377 -0.265434
41 1889 -0.24989556 -0.37222093 -0.12757017
42 1890 -0.50685817 -0.6324095 -0.3813068
43 1891 -0.40131494 -0.5373699 -0.26525995
44 1892 -0.5075585 -0.64432853 -0.3707885
45 1893 -0.49461925 -0.6315314 -0.35770702
46 1894 -0.48376393 -0.6255681 -0.34195974
47 1895 -0.4487516 -0.58202064 -0.3154826
48 1896 -0.28400728 -0.4174015 -0.15061308
49 1897 -0.25980017 -0.39852425 -0.12107607
50 1898 -0.48579213 -0.6176492 -0.35393503
51 1899 -0.35543364 -0.48639694 -0.22447036
52 1900 -0.23447904 -0.3669676 -0.10199049
53 1901 -0.29342857 -0.42967388 -0.15718324
54 1902 -0.43898427 -0.5754281 -0.30254042
55 1903 -0.5333264 -0.66081935 -0.40583345
56 1904 -0.5975614 -0.7288325 -0.46629035
57 1905 -0.40775132 -0.5350291 -0.28047356
58 1906 -0.3191393 -0.45052385 -0.18775477
59 1907 -0.5041577 -0.6262818 -0.38203365
60 1908 -0.5138707 -0.63748026 -0.3902612
61 1909 -0.5357649 -0.6526296 -0.41890016
62 1910 -0.5310242 -0.6556868 -0.40636164
63 1911 -0.5392051 -0.66223973 -0.4161705
64 1912 -0.47567302 -0.5893311 -0.36201498
65 1913 -0.46715254 -0.5893755 -0.34492958
66 1914 -0.2625924 -0.38276345 -0.1424214
67 1915 -0.19184391 -0.32196194 -0.06172589
68 1916 -0.42020997 -0.5588941 -0.28152588
69 1917 -0.54301953 -0.6921192 -0.3939199
70 1918 -0.42458433 -0.58198184 -0.26718682
71 1919 -0.32551822 -0.48145813 -0.1695783
72 1920 -0.2985808 -0.44860035 -0.14856121
73 1921 -0.24067703 -0.38175339 -0.09960067
74 1922 -0.33922812 -0.46610323 -0.21235302
75 1923 -0.31793055 -0.444173 -0.1916881
76 1924 -0.3120622 -0.4388317 -0.18529275
77 1925 -0.28242525 -0.4147755 -0.15007503
78 1926 -0.12283547 -0.25264767 0.006976739
79 1927 -0.22940508 -0.35135695 -0.10745319
80 1928 -0.20676155 -0.33881804 -0.074705064
81 1929 -0.39275664 -0.52656746 -0.25894582
82 1930 -0.1768054 -0.29041144 -0.06319936
83 1931 -0.10339768 -0.2126916 0.0058962475
84 1932 -0.14546166 -0.25195515 -0.0389682
85 1933 -0.32234442 -0.4271004 -0.21758842
86 1934 -0.17433685 -0.27400395 -0.07466974
87 1935 -0.20605922 -0.30349734 -0.10862111
88 1936 -0.16952093 -0.26351926 -0.07552261
89 1937 -0.01919893 -0.11975875 0.08136089
90 1938 -0.012200732 -0.11030374 0.08590227
91 1939 -0.040797167 -0.14670466 0.065110326
92 1940 0.07593584 -0.04194966 0.19382134
93 1941 0.038129337 -0.16225387 0.23851255
94 1942 0.0014060909 -0.1952124 0.19802457
95 1943 0.0064140745 -0.19959097 0.21241911
96 1944 0.14410514 -0.054494828 0.3427051
97 1945 0.043088365 -0.15728289 0.24345961
98 1946 -0.1188128 -0.2659574 0.028331792
99 1947 -0.091205545 -0.23179041 0.04937931
100 1948 -0.12466127 -0.25913337 0.009810844
101 1949 -0.14380224 -0.2540775 -0.033526987
102 1950 -0.22662179 -0.33265698 -0.12058662
103 1951 -0.06115397 -0.15035024 0.028042298
104 1952 0.015354565 -0.08293597 0.11364509
105 1953 0.07763074 -0.020529618 0.1757911
106 1954 -0.11675021 -0.20850271 -0.024997713
107 1955 -0.19730993 -0.28442997 -0.1101899
108 1956 -0.2631656 -0.33912563 -0.18720557
109 1957 -0.035334926 -0.10056862 0.029898768
110 1958 -0.017632553 -0.083074555 0.04780945
111 1959 -0.048004825 -0.11036375 0.0143540995
112 1960 -0.115487024 -0.17416587 -0.056808177
113 1961 -0.019997388 -0.07078052 0.030785747
114 1962 -0.06405444 -0.11731443 -0.010794453
115 1963 -0.03680589 -0.09057008 0.016958294
116 1964 -0.30586675 -0.34949213 -0.26224136
117 1965 -0.2043879 -0.25357357 -0.15520222
118 1966 -0.14888458 -0.19839221 -0.09937696
119 1967 -0.11751631 -0.16062479 -0.07440783
120 1968 -0.1686323 -0.21325313 -0.124011464
121 1969 -0.031366713 -0.07186544 0.009132013
122 1970 -0.08510657 -0.12608096 -0.04413217
123 1971 -0.20593274 -0.24450706 -0.16735843
124 1972 -0.0938271 -0.13171694 -0.05593726
125 1973 0.04993336 0.013468528 0.086398184
126 1974 -0.17253734 -0.21022376 -0.1348509
127 1975 -0.11075424 -0.15130512 -0.07020335
128 1976 -0.21586166 -0.25588378 -0.17583954
129 1977 0.10308852 0.060056705 0.14612034
130 1978 0.0052557723 -0.034576867 0.04508841
131 1979 0.09085813 0.062358618 0.119357646
132 1980 0.19607207 0.162804 0.22934014
133 1981 0.25001204 0.21939126 0.28063282
134 1982 0.034263328 -0.005104665 0.07363132
135 1983 0.22383861 0.18807402 0.2596032
136 1984 0.04800471 0.011560736 0.08444869
137 1985 0.04972978 0.015663471 0.08379609
138 1986 0.09568697 0.064408 0.12696595
139 1987 0.2430264 0.21218552 0.27386728
140 1988 0.28215173 0.2470353 0.31726816
141 1989 0.17925027 0.14449838 0.21400215
142 1990 0.36056247 0.32455227 0.39657268
143 1991 0.33889654 0.30403617 0.3737569
144 1992 0.124896795 0.09088206 0.15891153
145 1993 0.16565846 0.12817313 0.2031438
146 1994 0.23354977 0.19841294 0.2686866
147 1995 0.37686616 0.34365577 0.41007656
148 1996 0.2766894 0.24318004 0.31019878
149 1997 0.4223085 0.39009082 0.4545262
150 1998 0.57731646 0.54304415 0.6115888
151 1999 0.32448497 0.29283476 0.35613516
152 2000 0.3310848 0.29822788 0.36394167
153 2001 0.48928034 0.4580683 0.5204924
154 2002 0.5434665 0.51278186 0.57415116
155 2003 0.5441702 0.5112426 0.5770977
156 2004 0.46737072 0.43433833 0.5004031
157 2005 0.60686255 0.5757053 0.6380198
158 2006 0.5725527 0.541973 0.60313237
159 2007 0.5917013 0.56135315 0.6220495
160 2008 0.46564984 0.43265733 0.49864236
161 2009 0.5967817 0.56525564 0.6283077
162 2010 0.68037146 0.649076 0.7116669
163 2011 0.53769773 0.5060012 0.5693943
164 2012 0.5776071 0.5448553 0.6103589
165 2013 0.6235754 0.5884838 0.6586669
166 2014 0.67287165 0.63890487 0.7068384
167 2015 0.82511437 0.79128706 0.8589417
168 2016 0.93292713 0.90176356 0.96409065
169 2017 0.84517425 0.81477475 0.87557375
170 2018 0.762654 0.731052 0.79425603
171 2019 0.8910726 0.85678726 0.92535794
172 2020 0.9227938 0.8882121 0.9573755
173 2021 0.6640137 0.5372486 0.79077876

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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;
}

View File

@ -0,0 +1,5 @@
{
"presets": ["next/babel"]
}

View 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

View 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:
![](https://i.imgur.com/KSEtNF1.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 MiB

View 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