Compare commits
64 Commits
luccas_exp
...
refactor_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fb2aefa23 | ||
|
|
975887c127 | ||
|
|
cea928af7d | ||
|
|
3d63bc2e2a | ||
|
|
1a1a485927 | ||
|
|
6e90f1897b | ||
|
|
8292aa567b | ||
|
|
37fb13f52c | ||
|
|
2e6c87062f | ||
|
|
a89dfaae38 | ||
|
|
a9940a41fe | ||
|
|
07d903e454 | ||
|
|
996568c0f9 | ||
|
|
cceb1b011e | ||
|
|
7684a89f55 | ||
|
|
6b2b5f5e87 | ||
|
|
279426dcaf | ||
|
|
f688dd855c | ||
|
|
ebb1bc09c4 | ||
|
|
ae833febdc | ||
|
|
064b234442 | ||
|
|
061a5dd171 | ||
|
|
800e868f6a | ||
|
|
b4ec63e1e0 | ||
|
|
2fe5cafc40 | ||
|
|
22b916ea37 | ||
|
|
23a0420fcb | ||
|
|
7039564187 | ||
|
|
b38ea26f82 | ||
|
|
110360ccae | ||
|
|
b0e80c610f | ||
|
|
cea6cd9186 | ||
|
|
ee38b125bf | ||
|
|
99af8ce9b8 | ||
|
|
65e2a8be4c | ||
|
|
92316d4680 | ||
|
|
7f62550c7a | ||
|
|
f0cf5728b2 | ||
|
|
96480f2017 | ||
|
|
809028cc4a | ||
|
|
c0d35fe530 | ||
|
|
17e7434c97 | ||
|
|
23da1d94c6 | ||
|
|
8d567288f3 | ||
|
|
1482f437cd | ||
|
|
4d7a0f7e38 | ||
|
|
0161df99f2 | ||
|
|
80c0d1db63 | ||
|
|
9cf6ccc884 | ||
|
|
3a3ac5ce4d | ||
|
|
342eabbb3d | ||
|
|
0694c4764f | ||
|
|
234bbcec49 | ||
|
|
2dbfbbd552 | ||
|
|
ac70edc8dd | ||
|
|
8c8674c4ef | ||
|
|
e26ee8ea1e | ||
|
|
dce8b97a76 | ||
|
|
6e53942125 | ||
|
|
83bffccf52 | ||
|
|
97f97fb767 | ||
|
|
158dc12da2 | ||
|
|
0d835cca32 | ||
|
|
cb51660cbb |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -40,3 +40,7 @@ Thumbs.db
|
|||||||
|
|
||||||
# Next.js
|
# Next.js
|
||||||
.next
|
.next
|
||||||
|
|
||||||
|
# Env
|
||||||
|
.env
|
||||||
|
**/.env
|
||||||
|
|||||||
815
README.md
815
README.md
@@ -1,5 +1,3 @@
|
|||||||
> :warning: **This documentation is outdated**: In the coming months this repo has been and will continue to experience a major revamping, this is all in the effort of modernizing and expanding the framework, with that said, not everything shown in the documentation below is going to still be aplicable so thread carefully
|
|
||||||
|
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
🌀 Portal.JS
|
🌀 Portal.JS
|
||||||
<br />
|
<br />
|
||||||
@@ -9,821 +7,48 @@ Rapidly build rich data portals using a modern frontend framework
|
|||||||
* [What is Portal.JS ?](#What-is-Portal.JS)
|
* [What is Portal.JS ?](#What-is-Portal.JS)
|
||||||
* [Features](#Features)
|
* [Features](#Features)
|
||||||
* [For developers](#For-developers)
|
* [For developers](#For-developers)
|
||||||
* [Installation and setup](#Installation-and-setup)
|
* [Docs](#Docs)
|
||||||
* [Getting Started](#Getting-Started)
|
* [Community](#Community)
|
||||||
* [Tutorial](#Tutorial)
|
|
||||||
* [Build a single Frictionless dataset portal](#Build-a-single-Frictionless-dataset-portal)
|
|
||||||
* [Build a CKAN powered dataset portal](#Build-a-CKAN-powered-dataset-portal)
|
|
||||||
* [Architecture / Reference](#Architecture--Reference)
|
|
||||||
* [Component List](#Component-List)
|
|
||||||
* [UI Components](#UI-Components)
|
|
||||||
* [Dataset Components](#Dataset-Components)
|
|
||||||
* [View Components](#View-Components)
|
|
||||||
* [Search Components](#Search-Components)
|
|
||||||
* [Blog Components](#Blog-Components)
|
|
||||||
* [Misc Components](#Misc-Components)
|
|
||||||
* [Concepts and Terms](#Concepts-and-Terms)
|
|
||||||
* [Dataset](#Dataset)
|
|
||||||
* [Resource](#Resource)
|
|
||||||
* [View Spec](#view-spec)
|
|
||||||
* [Appendix](#Appendix)
|
* [Appendix](#Appendix)
|
||||||
* [What happened to Recline?](#What-happened-to-Recline?)
|
* [What happened to Recline?](#What-happened-to-Recline?)
|
||||||
|
|
||||||
# What is Portal.JS
|
# What is Portal.JS
|
||||||
🌀 `portal.js` is a framework for rapidly building rich data portal frontends using a modern frontend approach. `portal.js` can be used to present a single dataset or build a full-scale data catalog/portal.
|
|
||||||
|
|
||||||
`portal.js` is built in Javascript and React on top of the popular [Next.js](https://nextjs.com/) framework. `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](https://ckan.org/).
|
🌀 Portal.JS is a framework for rapidly building rich data portal frontends using a modern frontend approach. Portal.JS can be used to present a single dataset or build a full-scale data catalog/portal.
|
||||||
|
|
||||||
|
Built in JavaScript and React on top of the popular [Next.js](https://nextjs.com/) framework. Portal.JS 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](https://ckan.org/).
|
||||||
|
|
||||||
## Features
|
## 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.
|
- 🗺️ 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
|
- 👩💻 Developer friendly: built with familiar frontend tech (JavaScript, React, Next.js).
|
||||||
- 🔋 Batteries included: Full set of portal components out of the box e.g. catalog search, dataset showcase, blog 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.
|
- 🎨 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
|
- 🧱 Extensible: quickly extend and develop/import your own React components
|
||||||
- 📝 Well documented: full set of documentation plus the documentation of NextJS and Apollo.
|
- 📝 Well documented: full set of documentation plus the documentation of Next.js and Apollo.
|
||||||
|
|
||||||
### For developers
|
### For developers
|
||||||
|
|
||||||
- 🏗 Build with modern, familiar frontend tech such as Javascript and React.
|
- 🏗 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.
|
- 🚀 Next.js framework: so everything in Next.js for free: Server Side Rendering, Static Site Generation, huge number of examples and integrations, etc.
|
||||||
- SSR => unlimited number of pages, SEO etc whilst still using React.
|
- Server Side Rendering (SSR) => Unlimited number of pages, SEO and more whilst still using React.
|
||||||
- Static Site Generation (SSG) (good for small sites) => ultra-simple deployment, great performance and lighthouse scores etc
|
- Static Site Generation (SSG) => Ultra-simple deployment, great performance, great lighthouse scores and more (good for small sites)
|
||||||
|
|
||||||
# Installation and setup
|
#### **Check out the [Portal.JS website](https://portaljs.org/) for a gallery of live portals**
|
||||||
Before installation, ensure your system satisfies the following requirements:
|
|
||||||
|
|
||||||
- Node.js 10.13 or later
|
|
||||||
- Nextjs 10.0.3
|
|
||||||
- MacOS, Windows (including WSL), and Linux are supported
|
|
||||||
|
|
||||||
> Note: We also recommend instead of npm using `yarn` instead of `npm`.
|
|
||||||
>
|
|
||||||
Portal.js is built with React on top of Nextjs framework, so for a quick setup, you can bootstrap a Nextjs app and install portal.js as demonstrated in the code below:
|
|
||||||
|
|
||||||
```bash=
|
|
||||||
## Create a react app
|
|
||||||
npx create-next-app
|
|
||||||
# or
|
|
||||||
yarn create next-app
|
|
||||||
```
|
|
||||||
After the installation is complete, follow the instructions to start the development server. Try editing pages/index.js and see the result on your browser.
|
|
||||||
|
|
||||||
> For more information on how to use create-next-app, you can review the [create-next-app](https://nextjs.org/docs/api-reference/create-next-app) documentation.
|
|
||||||
|
|
||||||
Once you have Nextjs created, you can install portal.js:
|
|
||||||
|
|
||||||
```bash=
|
|
||||||
yarn add https://github.com/datopian/portal.js.git
|
|
||||||
```
|
|
||||||
|
|
||||||
You're now ready to use portal.js in your next app. To test portal.js, open your `index.js` file in the pages folder. By default you should have some autogenerated code in the `index.js` file:
|
|
||||||
|
|
||||||
|
|
||||||
Which outputs a page with the following content:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Now, we are going to do some clean up and add a table component. In the `index.js` file, import a [Table]() component from portal as shown below:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import Head from 'next/head'
|
|
||||||
import { Table } from 'portal' //import Table component
|
|
||||||
import styles from '../styles/Home.module.css'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ field: 'id', headerName: 'ID' },
|
|
||||||
{ field: 'firstName', headerName: 'First name' },
|
|
||||||
{ field: 'lastName', headerName: 'Last name' },
|
|
||||||
{ field: 'age', headerName: 'Age' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const rows = [
|
|
||||||
{ 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Head>
|
|
||||||
<title>Create Portal App</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<h1 className={styles.title}>
|
|
||||||
Welcome to <a href="https://nextjs.org">Portal.JS</a>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Use table component */}
|
|
||||||
<Table data={rows} columns={columns} />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, your page should look like the following:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> **Note**: You can learn more about individual portal components, as well as their prop types in the [components reference](#Component-List).
|
|
||||||
|
|
||||||
___
|
___
|
||||||
|
|
||||||
# Getting Started
|
# Docs
|
||||||
|
|
||||||
If you're new to Portal.js we recommend that you start with the step-by-step guide below. You can also check out the following examples of projects built with portal.js.
|
Access the Portal.JS documentation at:
|
||||||
|
|
||||||
* [A portal for a single Frictionless dataset](#Build-a-single-Frictionless-dataset-portal)
|
https://portaljs.org/docs
|
||||||
* [A portal with a CKAN backend](#Build-a-CKAN-powered-dataset-portal)
|
|
||||||
|
|
||||||
> The [`examples` directory](https://github.com/datopian/portal.js/tree/main/examples) is regularly updated with different portal examples.
|
- [Examples](https://portaljs.org/docs#examples)
|
||||||
|
|
||||||
If you have questions about anything related to Portal.js, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portal.js/discussions).
|
# Community
|
||||||
___
|
|
||||||
|
|
||||||
# Tutorial
|
If you have questions about anything related to Portal.JS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portal.js/discussions) or on our [Discord server](https://discord.gg/An7Bu5x8).
|
||||||
|
|
||||||
## Build a single Frictionless dataset portal
|
|
||||||
This tutorial will guide you through building a portal for a single Frictionless dataset.
|
|
||||||
|
|
||||||
[Here’s](https://portal-js.vercel.app/) an example of the final result.
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
The dataset should be a Frictionless Dataset i.e. it should have a [datapackage.json](https://specs.frictionlessdata.io/data-package/).
|
|
||||||
|
|
||||||
Create a frictionless dataset portal app from the template:
|
|
||||||
```
|
|
||||||
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
|
|
||||||
```
|
|
||||||
Go into your portal's directory and set the path to your dataset directory that contains the `datapackage.json`:
|
|
||||||
```
|
|
||||||
cd <your-portal>
|
|
||||||
export PORTAL_DATASET_PATH=<path/to/your/dataset>
|
|
||||||
```
|
|
||||||
Start the server:
|
|
||||||
```
|
|
||||||
yarn dev
|
|
||||||
```
|
|
||||||
Visit the Page to view your dataset portal.
|
|
||||||
|
|
||||||
## Build a CKAN powered dataset portal
|
|
||||||
|
|
||||||
See [the CKAN Portal.JS example](./examples/ckan).
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
|
|
||||||
# Architecture / Reference
|
|
||||||
|
|
||||||
## Component List
|
|
||||||
|
|
||||||
Portal.js supports many components that can help you build amazing data portals similar to [this](https://catalog-portal-js.vercel.app/) and [this](https://portal-js.vercel.app/).
|
|
||||||
|
|
||||||
In this section, we'll cover all supported components in depth, and help you understand their use as well as the expected properties.
|
|
||||||
|
|
||||||
Components are grouped under the following sections:
|
|
||||||
* [UI](https://github.com/datopian/portal.js/tree/main/src/components/ui): Components like Nav bar, Footer, e.t.c
|
|
||||||
* [Dataset](https://github.com/datopian/portal.js/tree/main/src/components/dataset): Components used for displaying a Frictionless dataset and resources
|
|
||||||
* [Search](https://github.com/datopian/portal.js/tree/main/src/components/search): Components used for building a search interface for datasets
|
|
||||||
* [Blog](https://github.com/datopian/portal.js/tree/main/src/components/blog): Components for building a simple blog for datasets
|
|
||||||
* [Views](https://github.com/datopian/portal.js/tree/main/src/components/views): Components like charts, tables, maps for generating data views
|
|
||||||
* [Misc](https://github.com/datopian/portal.js/tree/main/src/components/misc): Miscellaneous components like errors, custom links, etc used for extra design.
|
|
||||||
|
|
||||||
### UI Components
|
|
||||||
|
|
||||||
In the UI we group all components that can be used for building generic page sections. These are components for building sections like the Navigation bar, Footer, Side pane, Recent datasets, e.t.c.
|
|
||||||
|
|
||||||
#### [Nav Component](https://github.com/datopian/portal.js/blob/main/src/components/ui/Nav.js)
|
|
||||||
|
|
||||||
To build a navigation bar, you can use the `Nav` component as demonstrated below:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Nav } from 'portal'
|
|
||||||
|
|
||||||
export default function Home(){
|
|
||||||
|
|
||||||
const navMenu = [{ title: 'Blog', path: '/blog' },
|
|
||||||
{ title: 'Search', path: '/search' }]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Nav logo="/images/logo.png" navMenu={navMenu}/>
|
|
||||||
...
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Nav Component Prop Types
|
|
||||||
|
|
||||||
Nav component accepts two properties:
|
|
||||||
* **logo**: A string to an image path. Can be relative or absolute.
|
|
||||||
* **navMenu**: An array of objects with title and path. E.g : [{ title: 'Blog', path: '/blog' },{ title: 'Search', path: '/search' }]
|
|
||||||
|
|
||||||
|
|
||||||
#### [Recent Component](https://github.com/datopian/portal.js/blob/main/src/components/ui/Recent.js)
|
|
||||||
|
|
||||||
The `Recent` component is used to display a list of recent [datasets](#Dataset) in the home page. This useful if you want to display the most recent dataset users have interacted with in your home page.
|
|
||||||
To build a recent dataset section, you can use the `Recent` component as demonstrated below:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Recent } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const datasets = [
|
|
||||||
{
|
|
||||||
organization: {
|
|
||||||
name: "Org1",
|
|
||||||
title: "This is the first org",
|
|
||||||
description: "A description of the organization 1"
|
|
||||||
},
|
|
||||||
title: "Data package title",
|
|
||||||
name: "dataset1",
|
|
||||||
description: "description of data package",
|
|
||||||
resources: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
organization: {
|
|
||||||
name: "Org2",
|
|
||||||
title: "This is the second org",
|
|
||||||
description: "A description of the organization 2"
|
|
||||||
},
|
|
||||||
title: "Data package title",
|
|
||||||
name: "dataset2",
|
|
||||||
description: "description of data package",
|
|
||||||
resources: [],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Use Recent component */}
|
|
||||||
<Recent datasets={datasets} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: The `Recent` component is hyperlinked with the dataset name of the organization and the dataset name in the following format:
|
|
||||||
|
|
||||||
> `/@<org name>/<dataset name>`
|
|
||||||
|
|
||||||
For instance, using the example dataset above, the first component will be link to page:
|
|
||||||
|
|
||||||
> `/@org1/dataset1`
|
|
||||||
|
|
||||||
and the second will be linked to:
|
|
||||||
|
|
||||||
> `/@org2/dataset2`
|
|
||||||
|
|
||||||
This is useful to know when generating dynamic pages for each dataset.
|
|
||||||
|
|
||||||
#### Recent Component Prop Types
|
|
||||||
|
|
||||||
The `Recent` component accepts the following properties:
|
|
||||||
* **datasets**: An array of [datasets](#Dataset)
|
|
||||||
|
|
||||||
### Dataset Components
|
|
||||||
|
|
||||||
The dataset component groups together components that can be used for building a dataset UI. These includes components for displaying info about a dataset, resources in a dataset as well as dataset ReadMe.
|
|
||||||
|
|
||||||
#### [KeyInfo Component](https://github.com/datopian/portal.js/blob/main/src/components/dataset/KeyInfo.js)
|
|
||||||
|
|
||||||
The `KeyInfo` components displays key properties like the number of resources, size, format, licences of in a dataset in tabular form. See example in the `Key Info` section [here](https://portal-js.vercel.app/). To use it, you can import the `KeyInfo` component as demonstrated below:
|
|
||||||
```javascript
|
|
||||||
import { KeyInfo } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const datapackage = {
|
|
||||||
"name": "finance-vix",
|
|
||||||
"title": "VIX - CBOE Volatility Index",
|
|
||||||
"homepage": "http://www.cboe.com/micro/VIX/",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"license": "PDDL-1.0",
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"title": "CBOE VIX Page",
|
|
||||||
"name": "CBOE VIX Page",
|
|
||||||
"web": "http://www.cboe.com/micro/vix/historical.aspx"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"resources": [
|
|
||||||
{
|
|
||||||
"name": "vix-daily",
|
|
||||||
"path": "vix-daily.csv",
|
|
||||||
"format": "csv",
|
|
||||||
"size": 20982,
|
|
||||||
"mediatype": "text/csv",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Use KeyInfo component */}
|
|
||||||
<KeyInfo descriptor={datapackage} resources={datapackage.resources} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### KeyInfo Component Prop Types
|
|
||||||
|
|
||||||
KeyInfo component accepts two properties:
|
|
||||||
* **descriptor**: A [Frictionless data package descriptor](https://specs.frictionlessdata.io/data-package/#descriptor)
|
|
||||||
* **resources**: An [Frictionless data package resource](https://specs.frictionlessdata.io/data-resource/#introduction)
|
|
||||||
|
|
||||||
|
|
||||||
#### [ResourceInfo Component](https://github.com/datopian/portal.js/blob/main/src/components/dataset/ResourceInfo.js)
|
|
||||||
|
|
||||||
The `ResourceInfo` components displays key properties like the name, size, format, modification dates, as well as a download link in a resource object. See an example of a `ResourceInfo` component in the `Data Files` section [here](https://portal-js.vercel.app/).
|
|
||||||
|
|
||||||
You can import and use the `ResourceInfo` component as demonstrated below:
|
|
||||||
```javascript
|
|
||||||
import { ResourceInfo } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const resources = [
|
|
||||||
{
|
|
||||||
"name": "vix-daily",
|
|
||||||
"path": "vix-daily.csv",
|
|
||||||
"format": "csv",
|
|
||||||
"size": 20982,
|
|
||||||
"mediatype": "text/csv",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "vix-daily 2",
|
|
||||||
"path": "vix-daily2.csv",
|
|
||||||
"format": "csv",
|
|
||||||
"size": 2082,
|
|
||||||
"mediatype": "text/csv",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Use Recent component */}
|
|
||||||
<ResourceInfo resources={resources} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ResourceInfo Component Prop Types
|
|
||||||
|
|
||||||
ResourceInfo component accepts a single property:
|
|
||||||
* **resources**: An [Frictionless data package resource](https://specs.frictionlessdata.io/data-resource/#introduction)
|
|
||||||
|
|
||||||
|
|
||||||
#### [ReadMe Component](https://github.com/datopian/portal.js/blob/main/src/components/dataset/Readme.js)
|
|
||||||
|
|
||||||
The `ReadMe` component is used for displaying a compiled dataset Readme in a readable format. See example in the `README` section [here](https://portal-js.vercel.app/).
|
|
||||||
|
|
||||||
> Note: By compiled ReadMe, we mean ReadMe that has been converted to plain string using a package like [remark](https://www.npmjs.com/package/remark).
|
|
||||||
|
|
||||||
You can import and use the `ReadMe` component as demonstrated below:
|
|
||||||
```javascript
|
|
||||||
import { ReadMe } from 'portal'
|
|
||||||
import remark from 'remark'
|
|
||||||
import html from 'remark-html'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
|
|
||||||
const readMeMarkdown = `
|
|
||||||
CBOE Volatility Index (VIX) time-series dataset including daily open, close,
|
|
||||||
high and low. The CBOE Volatility Index (VIX) is a key measure of market
|
|
||||||
expectations of near-term volatility conveyed by S&P 500 stock index option
|
|
||||||
prices introduced in 1993.
|
|
||||||
|
|
||||||
## Data
|
|
||||||
|
|
||||||
From the [VIX FAQ][faq]:
|
|
||||||
|
|
||||||
> In 1993, the Chicago Board Options Exchange® (CBOE®) introduced the CBOE
|
|
||||||
> Volatility Index®, VIX®, and it quickly became the benchmark for stock market
|
|
||||||
> volatility. It is widely followed and has been cited in hundreds of news
|
|
||||||
> articles in the Wall Street Journal, Barron's and other leading financial
|
|
||||||
> publications. Since volatility often signifies financial turmoil, VIX is
|
|
||||||
> often referred to as the "investor fear gauge".
|
|
||||||
|
|
||||||
[faq]: http://www.cboe.com/micro/vix/faq.aspx
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
No obvious statement on [historical data page][historical]. Given size and
|
|
||||||
factual nature of the data and its source from a US company would imagine this
|
|
||||||
was public domain and as such have licensed the Data Package under the Public
|
|
||||||
Domain Dedication and License (PDDL).
|
|
||||||
|
|
||||||
[historical]: http://www.cboe.com/micro/vix/historical.aspx
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [readMe, setreadMe] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function processReadMe() {
|
|
||||||
const processed = await remark()
|
|
||||||
.use(html)
|
|
||||||
.process(readMeMarkdown)
|
|
||||||
setreadMe(processed.toString())
|
|
||||||
}
|
|
||||||
processReadMe()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ReadMe readme={readMe} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ReadMe Component Prop Types
|
|
||||||
|
|
||||||
The `ReadMe` component accepts a single property:
|
|
||||||
* **readme**: A string of a compiled ReadMe in html format.
|
|
||||||
|
|
||||||
### [View Components](https://github.com/datopian/portal.js/tree/main/src/components/views)
|
|
||||||
|
|
||||||
View components is a set of components that can be used for displaying dataset views like charts, tables, maps, e.t.c.
|
|
||||||
|
|
||||||
#### [Chart Component](https://github.com/datopian/portal.js/blob/main/src/components/views/Chart.js)
|
|
||||||
|
|
||||||
The `Chart` components exposes different chart components like Plotly Chart, Vega charts, which can be used for showing graphs. See example in the `Graph` section [here](https://portal-js.vercel.app/).
|
|
||||||
To use a chart component, you need to compile and pass a view spec as props to the chart component.
|
|
||||||
Each Chart type have their specific spec, as explained in this [doc](https://specs.frictionlessdata.io/views/#graph-spec).
|
|
||||||
|
|
||||||
In the example below, we assume there's a compiled Plotly spec:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { PlotlyChart } from 'portal'
|
|
||||||
|
|
||||||
export default function Home({plotlySpec}) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
< div >
|
|
||||||
<PlotlyChart spec={plotlySpec} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
> Note: You can compile views using the [datapackage-render](https://github.com/datopian/datapackage-views-js) library, as demonstrated in [this example](https://github.com/datopian/portal.js/blob/main/examples/dataset-frictionless/lib/utils.js).
|
|
||||||
|
|
||||||
|
|
||||||
#### Chart Component Prop Types
|
|
||||||
|
|
||||||
KeyInfo component accepts two properties:
|
|
||||||
* **spec**: A compiled view spec depending on the chart type.
|
|
||||||
|
|
||||||
#### [Table Component](https://github.com/datopian/portal.js/blob/main/examples/dataset-frictionless/components/Table.js)
|
|
||||||
|
|
||||||
The `Table` component is used for displaying dataset resources as a tabular grid. See example in the `Data Preview` section [here](https://portal-js.vercel.app/).
|
|
||||||
To use a Table component, you have to pass an array of data and columns as demonstrated below:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Table } from 'portal' //import Table component
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ field: 'id', headerName: 'ID' },
|
|
||||||
{ field: 'firstName', headerName: 'First name' },
|
|
||||||
{ field: 'lastName', headerName: 'Last name' },
|
|
||||||
{ field: 'age', headerName: 'Age' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table data={data} columns={columns} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
> Note: Under the hood, Table component uses the [DataGrid Material UI table](https://material-ui.com/components/data-grid/), and as such all supported params in data and columns are supported.
|
|
||||||
|
|
||||||
|
|
||||||
#### Table Component Prop Types
|
|
||||||
|
|
||||||
Table component accepts two properties:
|
|
||||||
* **data**: An array of column names with properties: e.g [{field: "col1", headerName: "col1"}, {field: "col2", headerName: "col2"}]
|
|
||||||
* **columns**: An array of data objects e.g. [ {col1: 1, col2: 2}, {col1: 5, col2: 7} ]
|
|
||||||
|
|
||||||
|
|
||||||
### [Search Components](https://github.com/datopian/portal.js/tree/main/src/components/search)
|
|
||||||
|
|
||||||
Search components groups together components that can be used for creating a search interface. This includes search forms, search item as well as search result list.
|
|
||||||
|
|
||||||
#### [Form Component](https://github.com/datopian/portal.js/blob/main/src/components/search/Form.js)
|
|
||||||
|
|
||||||
The search`Form` component is a simple search input and submit button. See example of a search form [here](https://catalog-portal-js.vercel.app/search).
|
|
||||||
|
|
||||||
The search `form` requires a submit handler (`handleSubmit`). This handler function receives the search term, and handles actual search.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `Form` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Form } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const handleSearchSubmit = (searchQuery) => {
|
|
||||||
// Write your custom code to perform search in db
|
|
||||||
console.log(searchQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
handleSubmit={handleSearchSubmit} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Form Component Prop Types
|
|
||||||
|
|
||||||
The `Form` component accepts a single property:
|
|
||||||
* **handleSubmit**: A function that receives the search text, and can be customize to perform the actual search.
|
|
||||||
|
|
||||||
#### [Item Component](https://github.com/datopian/portal.js/blob/main/src/components/search/Item.js)
|
|
||||||
|
|
||||||
The search`Item` component can be used to display a single search result.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `Item` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Item } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const datapackage = {
|
|
||||||
"name": "finance-vix",
|
|
||||||
"title": "VIX - CBOE Volatility Index",
|
|
||||||
"homepage": "http://www.cboe.com/micro/VIX/",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "This is a test organization description",
|
|
||||||
"resources": [
|
|
||||||
{
|
|
||||||
"name": "vix-daily",
|
|
||||||
"path": "vix-daily.csv",
|
|
||||||
"format": "csv",
|
|
||||||
"size": 20982,
|
|
||||||
"mediatype": "text/csv",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Item dataset={datapackage} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Item Component Prop Types
|
|
||||||
|
|
||||||
The `Item` component accepts a single property:
|
|
||||||
* **dataset**: A [Frictionless data package descriptor](https://specs.frictionlessdata.io/data-package/#descriptor)
|
|
||||||
|
|
||||||
|
|
||||||
#### [ItemTotal Component](https://github.com/datopian/portal.js/blob/main/src/components/search/Item.js)
|
|
||||||
|
|
||||||
The search`ItemTotal` is a simple component for displaying the total search result
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `ItemTotal` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { ItemTotal } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
//do some custom search to get results
|
|
||||||
const search = (text) => {
|
|
||||||
return [{ name: "data1" }, { name: "data2" }]
|
|
||||||
}
|
|
||||||
//get the total result count
|
|
||||||
const searchTotal = search("some text").length
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemTotal count={searchTotal} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ItemTotal Component Prop Types
|
|
||||||
|
|
||||||
The `ItemTotal` component accepts a single property:
|
|
||||||
* **count**: An integer of the total number of results.
|
|
||||||
|
|
||||||
|
|
||||||
### [Blog Components](https://github.com/datopian/portal.js/tree/main/src/components/blog)
|
|
||||||
|
|
||||||
These are group of components for building a portal blog. See example of portal blog [here](https://catalog-portal-js.vercel.app/blog)
|
|
||||||
|
|
||||||
#### [PostList Components](https://github.com/datopian/portal.js/tree/main/src/components/misc)
|
|
||||||
|
|
||||||
The `PostList` component is used to display a list of blog posts with the title and a short excerpts from the content.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `PostList` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { PostList } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const posts = [
|
|
||||||
{ title: "Blog post 1", excerpt: "This is the first blog excerpts in this list." },
|
|
||||||
{ title: "Blog post 2", excerpt: "This is the second blog excerpts in this list." },
|
|
||||||
{ title: "Blog post 3", excerpt: "This is the third blog excerpts in this list." },
|
|
||||||
]
|
|
||||||
return (
|
|
||||||
<PostList posts={posts} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PostList Component Prop Types
|
|
||||||
|
|
||||||
The `PostList` component accepts a single property:
|
|
||||||
* **posts**: An array of post list objects with the following properties:
|
|
||||||
```javascript
|
|
||||||
[
|
|
||||||
{
|
|
||||||
title: "The title of the blog post",
|
|
||||||
excerpt: "A short excerpt from the post content",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### [Post Components](https://github.com/datopian/portal.js/tree/main/src/components/misc)
|
|
||||||
|
|
||||||
The `Post` component is used to display a blog post. See an example of a blog post [here](https://catalog-portal-js.vercel.app/blog/nyt-pa-platformen-opdateringsfrekvens-og-andres-data)
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `Post` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Post } from 'portal'
|
|
||||||
import * as dayjs from 'dayjs' //For converting UTC time to relative format
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const post = {
|
|
||||||
title: "This is a sample blog post",
|
|
||||||
content: `<h1>A simple header</h1>
|
|
||||||
The PostList component is used to display a list of blog posts
|
|
||||||
with the title and a short excerpts from the content.
|
|
||||||
In the example below, we demonstrate how to use the PostList component.`,
|
|
||||||
createdAt: dayjs().to(dayjs(1620649596902)),
|
|
||||||
featuredImage: "https://pixabay.com/get/ge9a766d1f7b5fe0eccbf0f439501a2cf2b191997290e7ab15e6a402574acc2fdba48a82d278dca3547030e0202b7906d_640.jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Post post={post} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Post Component Prop Types
|
|
||||||
|
|
||||||
The `Post` component accepts a single property:
|
|
||||||
* **post**: An object with the following properties:
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
title: <The title of the blog post>
|
|
||||||
content: <The body of the blog post. Can be plain text or html>
|
|
||||||
createdAt: <The utc date when the post was last modified>
|
|
||||||
featuredImage: < Url/relative url to post cover image>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### [Misc Components](https://github.com/datopian/portal.js/tree/main/src/components/misc)
|
|
||||||
|
|
||||||
These are group of miscellaneous/extra components for extending your portal. They include components like Errors, custom links, etc.
|
|
||||||
|
|
||||||
#### [Error Component](https://github.com/datopian/portal.js/blob/main/src/components/misc/Error.js)
|
|
||||||
|
|
||||||
The `Error` component is used to display a custom error message.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `Error` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Error } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Error message="An error occured when loading the file!" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Error Component Prop Types
|
|
||||||
|
|
||||||
The `Error` component accepts a single property:
|
|
||||||
* **message**: A string with the error message to display.
|
|
||||||
|
|
||||||
|
|
||||||
#### [Custom Component](https://github.com/datopian/portal.js/blob/main/src/components/misc/Error.js)
|
|
||||||
|
|
||||||
The `CustomLink` component is used to create a link with a consistent style to other portal components.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `CustomLink` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { CustomLink } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomLink url="/blog" title="Goto Blog" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### CustomLink Component Prop Types
|
|
||||||
|
|
||||||
The `CustomLink` component accepts the following properties:
|
|
||||||
|
|
||||||
* **url**: A string. The relative or absolute url of the link.
|
|
||||||
* **title**: A string. The title of the link
|
|
||||||
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
## Concepts and Terms
|
|
||||||
In this section, we explain some of the terms and concepts used throughtout the portal.js documentation.
|
|
||||||
> Some of these concepts are part of official specs, and when appropriate, we'll link to the sources where you can get more details.
|
|
||||||
### Dataset
|
|
||||||
A dataset extends the [Frictionless data package](https://specs.frictionlessdata.io/data-package/#metadata) to add an extra organization property. The organization property describes the organization the dataset belongs to, and it should have the following properties:
|
|
||||||
```javascript
|
|
||||||
organization = {
|
|
||||||
name: "some org name",
|
|
||||||
title: "Some optional org title",
|
|
||||||
description: "A description of the organization"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
An example of dataset with organization properties is given below:
|
|
||||||
```javascript
|
|
||||||
datasets = [{
|
|
||||||
organization: {
|
|
||||||
name: "some org name",
|
|
||||||
title: "Some optional org title",
|
|
||||||
description: "A description of the organization"
|
|
||||||
},
|
|
||||||
title: "Data package title",
|
|
||||||
name: "Data package name",
|
|
||||||
description: "description of data package",
|
|
||||||
resources: [...],
|
|
||||||
licences: [...],
|
|
||||||
sources: [...]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource
|
|
||||||
TODO
|
|
||||||
|
|
||||||
### view spec
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Deploying portal build to github pages
|
|
||||||
|
|
||||||
[Deploying single frictionless dataset to Github](https://portaljs.org/publish)
|
|
||||||
|
|
||||||
## Showcases
|
|
||||||
|
|
||||||
### Single Dataset with Default Theme
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
# Appendix
|
# Appendix
|
||||||
|
|
||||||
|
|||||||
@@ -1,262 +1,8 @@
|
|||||||
<h1 align="center">
|
# 🌀 PortalJS example with CKAN and Apollo
|
||||||
|
|
||||||
🌀 Portal.JS<br/>
|
**🚩 UPDATE April 2023: This example is now deprecated - though still works!. Please use the [new CKAN examples](https://github.com/datopian/portaljs/tree/main/examples)**
|
||||||
The javascript framework for<br/>
|
|
||||||
data portals
|
|
||||||
|
|
||||||
</h1>
|
This example shows how you can build a full data portal using a CKAN Backend with a Next.JS Frontend powered by Apollo, a full fledged guide is available as a [blog post](https://portaljs.org/blog/example-ckan-2021)
|
||||||
|
|
||||||
🌀 `Portal` is a framework for rapidly building rich data portal frontends using a modern frontend approach (javascript, React, SSR).
|
|
||||||
|
|
||||||
`Portal` assumes a "decoupled" approach where the frontend is a separate service from the backend and interacts with backend(s) via an API. It can be used with any backend and has out of the box support for [CKAN][]. `portal` is built in Javascript and React on top of the popular [Next.js][] framework.
|
|
||||||
|
|
||||||
[ckan]: https://ckan.org/
|
|
||||||
[next.js]: https://nextjs.com/
|
|
||||||
|
|
||||||
Live DEMO: https://catalog-portal-js.vercel.app
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- 🗺️ Unified sites: present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. wordpress) with a common internal API.
|
|
||||||
- 👩💻 Developer friendly: built with familiar frontend tech Javascript, React etc
|
|
||||||
- 🔋 Batteries included: Full set of portal components out of the box e.g. catalog search, dataset showcase, blog etc.
|
|
||||||
- 🎨 Easy to theme and customize: installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.
|
|
||||||
- 🧱 Extensible: quickly extend and develop/import your own React components
|
|
||||||
- 📝 Well documented: full set of documentation plus the documentation of NextJS and Apollo.
|
|
||||||
|
|
||||||
### For developers
|
|
||||||
|
|
||||||
- 🏗 Build with modern, familiar frontend tech such as Javascript and React.
|
|
||||||
- 🚀 NextJS framework: so everything in NextJS for free React, SSR, static site generation, huge number of examples and integrations etc.
|
|
||||||
- SSR => unlimited number of pages, SEO etc whilst still using React.
|
|
||||||
- Static Site Generation (SSG) (good for small sites) => ultra-simple deployment, great performance and lighthouse scores etc
|
|
||||||
- 📋 Typescript support
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
Install a recent version of Node. You'll need Node 10.13 or later.
|
|
||||||
|
|
||||||
### Create a Portal app
|
|
||||||
|
|
||||||
To create a Portal app, open your terminal, cd into the directory you'd like to create the app in, and run the following command:
|
|
||||||
|
|
||||||
```console
|
|
||||||
npm init portal-app my-data-portal
|
|
||||||
```
|
|
||||||
|
|
||||||
> NB: Under the hood, this uses the tool called create-next-app, which bootstraps a Next.js app for you. It uses this template through the --example flag.
|
|
||||||
>
|
|
||||||
> If it doesn’t work, please open an issue.
|
|
||||||
|
|
||||||
## Guide
|
|
||||||
|
|
||||||
### Styling 🎨
|
|
||||||
|
|
||||||
We use Tailwind as a CSS framework. Take a look at `/styles/index.css` to see what we're importing from Tailwind bundle. You can also configure Tailwind using `tailwind.config.js` file.
|
|
||||||
|
|
||||||
Have a look at Next.js support of CSS and ways of writing CSS:
|
|
||||||
|
|
||||||
https://nextjs.org/docs/basic-features/built-in-css-support
|
|
||||||
|
|
||||||
### Backend
|
|
||||||
|
|
||||||
So far the app is running with mocked data behind. You can connect CMS and DMS backends easily via environment variables:
|
|
||||||
|
|
||||||
```console
|
|
||||||
$ export DMS=http://ckan:5000
|
|
||||||
$ export CMS=http://myblog.wordpress.com
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note that we don't yet have implementations for the following CKAN features:
|
|
||||||
>
|
|
||||||
> - Activities
|
|
||||||
> - Auth
|
|
||||||
> - Groups
|
|
||||||
> - Facets
|
|
||||||
|
|
||||||
### Routes
|
|
||||||
|
|
||||||
These are the default routes set up in the "starter" app.
|
|
||||||
|
|
||||||
- Home `/`
|
|
||||||
- Search `/search`
|
|
||||||
- Dataset `/@org/dataset`
|
|
||||||
- Resource `/@org/dataset/r/resource`
|
|
||||||
- Organization `/@org`
|
|
||||||
- Collection (aka group in CKAN) (?) - suggest to merge into org
|
|
||||||
- Static pages, eg, `/about` etc. from CMS or can do it without external CMS, e.g., in Next.js
|
|
||||||
|
|
||||||
### New Routes
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
### Data fetching
|
|
||||||
|
|
||||||
We use Apollo client which allows us to query data with GraphQL. We have setup CKAN API for the demo (it uses demo.ckan.org as DMS):
|
|
||||||
|
|
||||||
http://portal.datopian1.now.sh/
|
|
||||||
|
|
||||||
Note that we don't have Apollo Server but we connect CKAN API using [`apollo-link-rest`](https://www.apollographql.com/docs/link/links/rest/) module. You can see how it works in [lib/apolloClient.ts](https://github.com/datopian/portal/blob/master/lib/apolloClient.ts) and then have a look at [pages/\_app.tsx](https://github.com/datopian/portal/blob/master/pages/_app.tsx).
|
|
||||||
|
|
||||||
For development/debugging purposes, we suggest installing the Chrome extension - https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm.
|
|
||||||
|
|
||||||
#### i18n configuration
|
|
||||||
|
|
||||||
Portal.js is configured by default to support both `English` and `French` subpath for language translation. But for subsequent users, this following steps can be used to configure i18n for other languages;
|
|
||||||
|
|
||||||
1. Update `next.config.js`, to add more languages to the i18n locales
|
|
||||||
|
|
||||||
```js
|
|
||||||
i18n: {
|
|
||||||
locales: ['en', 'fr', 'nl-NL'], // add more language to the list
|
|
||||||
defaultLocale: 'en', // set the default language to use
|
|
||||||
},
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Create a folder for the language in `locales` --> `locales/en-Us`
|
|
||||||
|
|
||||||
3. In the language folder, different namespace files (json) can be created for each translation. For the `index.js` use-case, I named it `common.json`
|
|
||||||
|
|
||||||
```json
|
|
||||||
// locales/en/common.json
|
|
||||||
{
|
|
||||||
"title" : "Portal js in English",
|
|
||||||
}
|
|
||||||
|
|
||||||
// locales/fr/common.json
|
|
||||||
{
|
|
||||||
"title" : "Portal js in French",
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
4. To use on pages using Server-side Props.
|
|
||||||
|
|
||||||
```js
|
|
||||||
import { loadNamespaces } from './_app';
|
|
||||||
import useTranslation from 'next-translate/useTranslation';
|
|
||||||
|
|
||||||
const Home: React.FC = ()=> {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
return (
|
|
||||||
<div>{t(`common:title`)}</div> // we use common and title base on the common.json data
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
|
||||||
........ ........
|
|
||||||
return {
|
|
||||||
props : {
|
|
||||||
_ns: await loadNamespaces(['common'], locale),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Go to the browser and view the changes using language subpath like this `http://localhost:3000` and `http://localhost:3000/fr`. **Note** The subpath also activate chrome language Translator
|
|
||||||
|
|
||||||
#### Pre-fetch data in the server-side
|
|
||||||
|
|
||||||
When visiting a dataset page, you may want to fetch the dataset metadata in the server-side. To do so, you can use `getServerSideProps` function from NextJS:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { GetServerSideProps } from 'next';
|
|
||||||
import { initializeApollo } from '../lib/apolloClient';
|
|
||||||
import gql from 'graphql-tag';
|
|
||||||
|
|
||||||
const QUERY = gql`
|
|
||||||
query dataset($id: String) {
|
|
||||||
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
|
||||||
const apolloClient = initializeApollo();
|
|
||||||
|
|
||||||
await apolloClient.query({
|
|
||||||
query: QUERY,
|
|
||||||
variables: {
|
|
||||||
id: 'my-dataset'
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
initialApolloState: apolloClient.cache.extract(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
This would fetch the data from DMS and save it in the Apollo cache so that we can query it again from the components.
|
|
||||||
|
|
||||||
#### Access data from a component
|
|
||||||
|
|
||||||
Consider situation when rendering a component for org info on the dataset page. We already have pre-fetched dataset metadata that includes `organization` property with attributes such as `name`, `title` etc. We can now query only organization part for our `Org` component:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { useQuery } from '@apollo/react-hooks';
|
|
||||||
import gql from 'graphql-tag';
|
|
||||||
|
|
||||||
export const GET_ORG_QUERY = gql`
|
|
||||||
query dataset($id: String) {
|
|
||||||
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
|
|
||||||
result {
|
|
||||||
organization {
|
|
||||||
name
|
|
||||||
title
|
|
||||||
image_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default function Org({ variables }) {
|
|
||||||
const { loading, error, data } = useQuery(
|
|
||||||
GET_ORG_QUERY,
|
|
||||||
{
|
|
||||||
variables: { id: 'my-dataset' }
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
...
|
|
||||||
|
|
||||||
const { organization } = data.dataset.result;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{organization ? (
|
|
||||||
<>
|
|
||||||
<img
|
|
||||||
src={
|
|
||||||
organization.image_url
|
|
||||||
}
|
|
||||||
className="h-5 w-5 mr-2 inline-block"
|
|
||||||
/>
|
|
||||||
<Link href={`/@${organization.name}`}>
|
|
||||||
<a className="font-semibold text-primary underline">
|
|
||||||
{organization.title || organization.name}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Add a new data source
|
|
||||||
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## Developers
|
## Developers
|
||||||
|
|
||||||
@@ -303,4 +49,5 @@ yarn run e2e
|
|||||||
|
|
||||||
### Key Pages
|
### Key Pages
|
||||||
|
|
||||||
See https://tech.datopian.com/frontend/
|
See https://datahub.io/docs/dms/frontend/
|
||||||
|
|
||||||
|
|||||||
1
examples/ckan-example/.env
Normal file
1
examples/ckan-example/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DMS=https://demo.dev.datopian.com
|
||||||
33
examples/ckan-example/.eslintrc.json
Normal file
33
examples/ckan-example/.eslintrc.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"next",
|
||||||
|
"next/core-web-vitals",
|
||||||
|
"../../.eslintrc.json"
|
||||||
|
],
|
||||||
|
"ignorePatterns": ["!**/*", ".next/**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {
|
||||||
|
"@next/next/no-html-link-for-pages": [
|
||||||
|
"error",
|
||||||
|
"examples/ckan-example/pages"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@next/next/no-html-link-for-pages": "off"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
}
|
||||||
|
}
|
||||||
46
examples/ckan-example/README.md
Normal file
46
examples/ckan-example/README.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
This is a repo intended to serve as an example of a data catalog that get its data from a CKAN Instance.
|
||||||
|
|
||||||
|
```
|
||||||
|
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/ckan-example
|
||||||
|
cd <app-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
- This project uses CKAN as a backend, so you need to point the project to the CKAN Url desired, you can do so by setting up the `DMS` env variable in your terminal or adding a `.env` file with the following content:
|
||||||
|
|
||||||
|
```
|
||||||
|
DMS=<ckan url>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run the app using:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Congratulations, you now have something similar to this running on `http://localhost:4200`
|
||||||
|

|
||||||
|
If yo go to any one of those pages by clicking on `More info` you will see something similar to this
|
||||||
|

|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fckan-example&env=DMS&envDescription=URL%20For%20the%20CKAN%20Backend%20Ex%3A%20https%3A%2F%2Fdemo.dev.datopian.com)
|
||||||
|
|
||||||
|
By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own github/gitlab/bitbucket account and automatically deploy everything.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Extra commands
|
||||||
|
|
||||||
|
You can also build the project for production with
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
And run using the production build like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
6
examples/ckan-example/index.d.ts
vendored
Normal file
6
examples/ckan-example/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
declare module '*.svg' {
|
||||||
|
const content: any;
|
||||||
|
export const ReactComponent: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export default {
|
export default {
|
||||||
displayName: 'simple-example',
|
displayName: 'ckan-example',
|
||||||
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/simple-example',
|
coverageDirectory: '../../coverage/examples/ckan-example',
|
||||||
};
|
};
|
||||||
5
examples/ckan-example/next-env.d.ts
vendored
Normal file
5
examples/ckan-example/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||||
17
examples/ckan-example/next.config.js
Normal file
17
examples/ckan-example/next.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const nextConfig = {
|
||||||
|
publicRuntimeConfig: {
|
||||||
|
DMS: process.env.DMS ? process.env.DMS : '',
|
||||||
|
},
|
||||||
|
async rewrites() {
|
||||||
|
return {
|
||||||
|
beforeFiles: [
|
||||||
|
{
|
||||||
|
source: '/@:org/:project*',
|
||||||
|
destination: '/@org/:org/:project*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
5842
examples/ckan-example/package-lock.json
generated
Normal file
5842
examples/ckan-example/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
examples/ckan-example/package.json
Normal file
33
examples/ckan-example/package.json
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "my-app",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@heroicons/react": "^2.0.17",
|
||||||
|
"@types/node": "18.16.0",
|
||||||
|
"@types/react": "18.0.38",
|
||||||
|
"@types/react-dom": "18.0.11",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"eslint-config-next": "13.3.1",
|
||||||
|
"next": "13.3.1",
|
||||||
|
"next-seo": "^6.0.0",
|
||||||
|
"octokit": "^2.0.14",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-markdown": "^8.0.7",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"typescript": "5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"tailwindcss": "^3.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
188
examples/ckan-example/pages/@org/[org]/[dataset]/index.tsx
Normal file
188
examples/ckan-example/pages/@org/[org]/[dataset]/index.tsx
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { GetServerSideProps } from 'next';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
CalendarIcon,
|
||||||
|
CloudArrowUpIcon,
|
||||||
|
FolderOpenIcon,
|
||||||
|
LockClosedIcon,
|
||||||
|
MapPinIcon,
|
||||||
|
PaperClipIcon,
|
||||||
|
ServerIcon,
|
||||||
|
UserIcon,
|
||||||
|
} from '@heroicons/react/20/solid';
|
||||||
|
|
||||||
|
const dms = getConfig().publicRuntimeConfig.DMS;
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
|
const { dataset } = context.query;
|
||||||
|
const response = await fetch(
|
||||||
|
`${dms}/api/3/action/package_show?id=${dataset}`
|
||||||
|
);
|
||||||
|
const _dataset = await response.json();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
dataset: _dataset.result,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const positions = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: 'Back End Developer',
|
||||||
|
type: 'Full-time',
|
||||||
|
location: 'Remote',
|
||||||
|
department: 'Engineering',
|
||||||
|
closeDate: '2020-01-07',
|
||||||
|
closeDateFull: 'January 7, 2020',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: 'Front End Developer',
|
||||||
|
type: 'Full-time',
|
||||||
|
location: 'Remote',
|
||||||
|
department: 'Engineering',
|
||||||
|
closeDate: '2020-01-07',
|
||||||
|
closeDateFull: 'January 7, 2020',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: 'User Interface Designer',
|
||||||
|
type: 'Full-time',
|
||||||
|
location: 'Remote',
|
||||||
|
department: 'Design',
|
||||||
|
closeDate: '2020-01-14',
|
||||||
|
closeDateFull: 'January 14, 2020',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function DatasetPage({ dataset }) {
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden bg-white py-24 sm:py-32">
|
||||||
|
<div className="mx-auto max-w-7xl px-6 lg:px-8">
|
||||||
|
<div className="mx-auto grid max-w-2xl grid-cols-1 gap-x-8 gap-y-16 sm:gap-y-20 lg:mx-0 lg:max-w-none lg:grid-cols-2">
|
||||||
|
<div className="lg:pr-8 lg:pt-4">
|
||||||
|
<div className="lg:max-w-lg">
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-indigo-600">
|
||||||
|
{dataset.organization.title
|
||||||
|
? dataset.organization.title
|
||||||
|
: dataset.organization.name}
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-3xl font-bold tracking-tight text-gray-900 sm:text-4xl">
|
||||||
|
{dataset.title ? dataset.title : dataset.name}
|
||||||
|
</p>
|
||||||
|
<p className="mt-6 leading-8 text-gray-600">
|
||||||
|
{dataset.notes ? dataset.notes : 'No description'}
|
||||||
|
</p>
|
||||||
|
<div className="mt-6 border-t border-gray-100">
|
||||||
|
<dl className="divide-y divide-gray-100">
|
||||||
|
{dataset.tags.length > 0 && (
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Tags
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.tags.map((tag) => tag.display_name).join(', ')}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{dataset.url && (
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Url
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.url}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Created
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{formatter.format(new Date(dataset.metadata_created))}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Modified
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{formatter.format(new Date(dataset.metadata_modified))}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:pr-8 lg:pt-4">
|
||||||
|
<h2 className="text-base font-semibold leading-7 text-indigo-600">
|
||||||
|
Resources
|
||||||
|
</h2>
|
||||||
|
<div className="overflow-hidden bg-white shadow sm:rounded-md mt-2">
|
||||||
|
<ul role="list" className="divide-y divide-gray-200">
|
||||||
|
{dataset.resources.map((resource) => (
|
||||||
|
<li key={resource.id}>
|
||||||
|
<a href={resource.url} className="block hover:bg-gray-50">
|
||||||
|
<div className="px-4 py-4 sm:px-6">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className="truncate text-sm font-medium text-indigo-600">
|
||||||
|
{resource.name}
|
||||||
|
</p>
|
||||||
|
{resource.datastore_active && (
|
||||||
|
<div className="ml-2 flex flex-shrink-0">
|
||||||
|
<p className="inline-flex rounded-full bg-green-100 px-2 text-xs font-semibold leading-5 text-green-800">
|
||||||
|
Datastore active
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 sm:flex sm:justify-between">
|
||||||
|
<div className="sm:flex">
|
||||||
|
<p className="flex items-center text-sm text-gray-500">
|
||||||
|
<FolderOpenIcon
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
{resource.format}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 flex items-center text-sm text-gray-500 sm:mt-0">
|
||||||
|
<CalendarIcon
|
||||||
|
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
Last modified:{' '}
|
||||||
|
<time dateTime={resource.metadata_modified}>
|
||||||
|
{formatter.format(
|
||||||
|
new Date(resource.metadata_modified)
|
||||||
|
)}
|
||||||
|
</time>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
examples/ckan-example/pages/_app.tsx
Normal file
18
examples/ckan-example/pages/_app.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { AppProps } from 'next/app';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
function CustomApp({ Component, pageProps }: AppProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>Welcome to ckan-example!</title>
|
||||||
|
</Head>
|
||||||
|
<main className="app">
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomApp;
|
||||||
114
examples/ckan-example/pages/index.tsx
Normal file
114
examples/ckan-example/pages/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import getConfig from 'next/config';
|
||||||
|
import styles from './index.module.css';
|
||||||
|
|
||||||
|
const dms = getConfig().publicRuntimeConfig.DMS
|
||||||
|
|
||||||
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export async function getServerSideProps() {
|
||||||
|
const response = await fetch(`${dms}/api/3/action/package_search`)
|
||||||
|
const datasets = await response.json()
|
||||||
|
const datasetsWithDetails = await Promise.all(datasets.result.results.map(async (dataset) => {
|
||||||
|
const response = await fetch(`${dms}/api/3/action/package_show?id=` + dataset.name)
|
||||||
|
const json = await response.json()
|
||||||
|
return json.result
|
||||||
|
}))
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
datasets: datasetsWithDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Index({ datasets }) {
|
||||||
|
return (
|
||||||
|
<div className="bg-white">
|
||||||
|
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
||||||
|
<h2 className="text-2xl font-bold leading-10 tracking-tight text-indigo-500">
|
||||||
|
My Datasets
|
||||||
|
</h2>
|
||||||
|
<p className="mt-6 max-w-2xl text-base leading-7 text-gray-600">
|
||||||
|
Here is a list of all my datasets for easy access and sharing, they
|
||||||
|
are all available in the following{' '}
|
||||||
|
<a
|
||||||
|
href="#"
|
||||||
|
className="font-semibold text-indigo-600 hover:text-indigo-500"
|
||||||
|
>
|
||||||
|
CKAN Instance
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<div className="mt-20">
|
||||||
|
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
|
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Title
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Description
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Last updated
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="relative py-3.5 pl-3 pr-4 sm:pr-0"
|
||||||
|
></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{datasets.map((dataset) => (
|
||||||
|
<tr>
|
||||||
|
<td className="px-3 py-4 text-sm text-gray-500">
|
||||||
|
{dataset.title}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-4 text-sm text-gray-500">
|
||||||
|
{dataset.notes}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
{formatter.format(
|
||||||
|
new Date(dataset.metadata_modified)
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||||
|
<a
|
||||||
|
href={`/@${dataset.organization.name}/${dataset.name}`}
|
||||||
|
className="text-indigo-600 hover:text-indigo-900"
|
||||||
|
>
|
||||||
|
More info
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
||||||
3
examples/ckan-example/pages/styles.css
Normal file
3
examples/ckan-example/pages/styles.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
6
examples/ckan-example/postcss.config.js
Normal file
6
examples/ckan-example/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "simple-example",
|
"name": "ckan-example",
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||||
"sourceRoot": "examples/simple-example",
|
"sourceRoot": "examples/ckan-example",
|
||||||
"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/simple-example",
|
"root": "examples/ckan-example",
|
||||||
"outputPath": "dist/examples/simple-example"
|
"outputPath": "dist/examples/ckan-example"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
"outputPath": "examples/simple-example"
|
"outputPath": "examples/ckan-example"
|
||||||
},
|
},
|
||||||
"production": {}
|
"production": {}
|
||||||
}
|
}
|
||||||
@@ -23,16 +23,16 @@
|
|||||||
"executor": "@nrwl/next:server",
|
"executor": "@nrwl/next:server",
|
||||||
"defaultConfiguration": "development",
|
"defaultConfiguration": "development",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "simple-example:build",
|
"buildTarget": "ckan-example:build",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "simple-example:build:development",
|
"buildTarget": "ckan-example:build:development",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "simple-example:build:production",
|
"buildTarget": "ckan-example:build:production",
|
||||||
"dev": false
|
"dev": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,14 +40,14 @@
|
|||||||
"export": {
|
"export": {
|
||||||
"executor": "@nrwl/next:export",
|
"executor": "@nrwl/next:export",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "simple-example:build:production"
|
"buildTarget": "ckan-example:build:production"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "@nrwl/jest:jest",
|
"executor": "@nrwl/jest:jest",
|
||||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||||
"options": {
|
"options": {
|
||||||
"jestConfig": "examples/simple-example/jest.config.ts",
|
"jestConfig": "examples/ckan-example/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/simple-example/**/*.{ts,tsx,js,jsx}"]
|
"lintFilePatterns": ["examples/ckan-example/**/*.{ts,tsx,js,jsx}"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0
examples/ckan-example/public/.gitkeep
Normal file
0
examples/ckan-example/public/.gitkeep
Normal file
15
examples/ckan-example/tailwind.config.js
Normal file
15
examples/ckan-example/tailwind.config.js
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
require('@tailwindcss/typography')
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
20
examples/ckan-example/tsconfig.json
Normal file
20
examples/ckan-example/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@@ -6,9 +6,6 @@
|
|||||||
"types": ["jest", "node"],
|
"types": ["jest", "node"],
|
||||||
"jsx": "react"
|
"jsx": "react"
|
||||||
},
|
},
|
||||||
"paths": {
|
|
||||||
"@/*": ["./*"]
|
|
||||||
},
|
|
||||||
"include": [
|
"include": [
|
||||||
"jest.config.ts",
|
"jest.config.ts",
|
||||||
"src/**/*.test.ts",
|
"src/**/*.test.ts",
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": ["plugin:cypress/recommended", "../../.eslintrc.json"],
|
|
||||||
"ignorePatterns": ["!**/*"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
|
||||||
"rules": {}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
import { defineConfig } from 'cypress';
|
|
||||||
import { nxE2EPreset } from '@nrwl/cypress/plugins/cypress-preset';
|
|
||||||
|
|
||||||
export default defineConfig({
|
|
||||||
e2e: nxE2EPreset(__dirname),
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "simple-example-e2e",
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"sourceRoot": "examples/simple-example-e2e/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"targets": {
|
|
||||||
"e2e": {
|
|
||||||
"executor": "@nrwl/cypress:cypress",
|
|
||||||
"options": {
|
|
||||||
"cypressConfig": "examples/simple-example-e2e/cypress.config.ts",
|
|
||||||
"devServerTarget": "simple-example:serve:development",
|
|
||||||
"testingType": "e2e"
|
|
||||||
},
|
|
||||||
"configurations": {
|
|
||||||
"production": {
|
|
||||||
"devServerTarget": "simple-example:serve:production"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"lint": {
|
|
||||||
"executor": "@nrwl/linter:eslint",
|
|
||||||
"outputs": ["{options.outputFile}"],
|
|
||||||
"options": {
|
|
||||||
"lintFilePatterns": ["examples/simple-example-e2e/**/*.{js,ts}"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"tags": [],
|
|
||||||
"implicitDependencies": ["simple-example"]
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { getGreeting } from '../support/app.po';
|
|
||||||
|
|
||||||
describe('simple-example', () => {
|
|
||||||
beforeEach(() => cy.visit('/'));
|
|
||||||
|
|
||||||
it('should display welcome message', () => {
|
|
||||||
// Custom command example, see `../support/commands.ts` file
|
|
||||||
cy.login('my-email@something.com', 'myPassword');
|
|
||||||
|
|
||||||
// Function helper example, see `../support/app.po.ts` file
|
|
||||||
getGreeting().contains('Welcome simple-example');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "Using fixtures to represent data",
|
|
||||||
"email": "hello@cypress.io"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const getGreeting = () => cy.get('h1');
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// ***********************************************
|
|
||||||
// This example commands.js shows you how to
|
|
||||||
// create various custom commands and overwrite
|
|
||||||
// existing commands.
|
|
||||||
//
|
|
||||||
// For more comprehensive examples of custom
|
|
||||||
// commands please read more here:
|
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
||||||
declare namespace Cypress {
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
interface Chainable<Subject> {
|
|
||||||
login(email: string, password: string): void;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// -- This is a parent command --
|
|
||||||
Cypress.Commands.add('login', (email, password) => {
|
|
||||||
console.log('Custom command example: Login', email, password);
|
|
||||||
});
|
|
||||||
//
|
|
||||||
// -- This is a child command --
|
|
||||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This is a dual command --
|
|
||||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// -- This will overwrite an existing command --
|
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// ***********************************************************
|
|
||||||
// This example support/index.js is processed and
|
|
||||||
// loaded automatically before your test files.
|
|
||||||
//
|
|
||||||
// This is a great place to put global configuration and
|
|
||||||
// behavior that modifies Cypress.
|
|
||||||
//
|
|
||||||
// You can change the location of this file or turn off
|
|
||||||
// automatically serving support files with the
|
|
||||||
// 'supportFile' configuration option.
|
|
||||||
//
|
|
||||||
// You can read more here:
|
|
||||||
// https://on.cypress.io/configuration
|
|
||||||
// ***********************************************************
|
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
|
||||||
import './commands';
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"sourceMap": false,
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"allowJs": true,
|
|
||||||
"types": ["cypress", "node"]
|
|
||||||
},
|
|
||||||
"include": ["src/**/*.ts", "src/**/*.js", "cypress.config.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:@nrwl/nx/react-typescript",
|
|
||||||
"next",
|
"next",
|
||||||
"next/core-web-vitals",
|
"next/core-web-vitals"
|
||||||
"../../.eslintrc.json"
|
|
||||||
],
|
],
|
||||||
"ignorePatterns": ["!**/*", ".next/**/*"],
|
"ignorePatterns": ["!**/*", ".next/**/*"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
|
|||||||
75
examples/simple-example/README.md
Normal file
75
examples/simple-example/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
This is a repo intended to serve as a simple example of a data catalog that get its data from a series of github repos, you can init an example just like this one by.
|
||||||
|
|
||||||
|
- Creating a new project with `create-next-app` like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/simple-example
|
||||||
|
cd <app-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
- This project uses the github api, which for anonymous users will cap at 50 requests per hour, so you might want to get a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and add it to a `.env` file inside the folder like so
|
||||||
|
|
||||||
|
```
|
||||||
|
GITHUB_PAT=<github token>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Edit the file `datasets.json` to your liking, some examples can be found inside this [repo](https://github.com/datasets)
|
||||||
|
- Run the app using:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Congratulations, you now have something similar to this running on `http://localhost:3000`
|
||||||
|

|
||||||
|
If yo go to any one of those pages by clicking on `More info` you will see something similar to this
|
||||||
|

|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fsimple-example)
|
||||||
|
|
||||||
|
By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own github/gitlab/bitbucket account and automatically deploy everything.
|
||||||
|
|
||||||
|
|
||||||
|
## Structure of `datasets.json`
|
||||||
|
|
||||||
|
The `datasets.json` file is simply a list of datasets, below you can see a minimal example of a dataset
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"owner": "fivethirtyeight",
|
||||||
|
"repo": "data",
|
||||||
|
"branch": "master",
|
||||||
|
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
|
||||||
|
"readme": "nba-raptor/README.md"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It has
|
||||||
|
|
||||||
|
- A `owner` which is going to be the github repo owner
|
||||||
|
- A `repo` which is going to be the github repo name
|
||||||
|
- A `branch` which is going to be the branch to which we need to get the files and the readme
|
||||||
|
- A list of `files` which is going to be a list of paths with files that you want to show to the world
|
||||||
|
- A `readme` which is going to be the path to your data description, it can also be a subpath eg: `example/README.md`
|
||||||
|
|
||||||
|
You can also add
|
||||||
|
|
||||||
|
- A `description` which is useful if you have more than one dataset for each repo, if not provided we are just going to use the repo description
|
||||||
|
- A `Name` which is useful if you want to give your dataset a nice name, if not provided we are going to use the junction of the `owner` the `repo` + the path of the README, in the exaple above it will be `fivethirtyeight/data/nba-raptor`
|
||||||
|
|
||||||
|
## Extra commands
|
||||||
|
|
||||||
|
You can also build the project for production with
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
And run using the production build like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import FrictionlessViewFactory from "./drd/FrictionlessView";
|
|
||||||
import Table from "./drd/Table";
|
|
||||||
|
|
||||||
/* eslint import/no-default-export: off */
|
|
||||||
function DatapackageLayout({ children, project, excerpt }) {
|
|
||||||
const { metadata } = project;
|
|
||||||
|
|
||||||
const title = metadata.title;
|
|
||||||
const resources = metadata.resources;
|
|
||||||
const views = metadata.views;
|
|
||||||
|
|
||||||
const FrictionlessView = FrictionlessViewFactory({ views, resources });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article className="docs prose text-primary dark:text-primary-dark dark:prose-invert prose-headings:font-headings prose-a:break-words mx-auto p-6">
|
|
||||||
<header>
|
|
||||||
{title && <h1 className="mb-4">{title}</h1>}
|
|
||||||
<a
|
|
||||||
className="font-semibold mb-4"
|
|
||||||
target="_blank"
|
|
||||||
href={project.github_repo}
|
|
||||||
>
|
|
||||||
@{project.owner} / {project.name}
|
|
||||||
</a>
|
|
||||||
{excerpt && <p className="text-md">{excerpt}</p>}
|
|
||||||
</header>
|
|
||||||
<section className="mt-10">
|
|
||||||
{views.map((view, i) => {
|
|
||||||
return (
|
|
||||||
<div key={`visualization-${i}`}>
|
|
||||||
<FrictionlessView viewId={i} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
<section className="mt-10">
|
|
||||||
<h2>Data files</h2>
|
|
||||||
<table className="table-auto">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>File</th>
|
|
||||||
<th>Title</th>
|
|
||||||
<th>Format</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{resources.map((r) => {
|
|
||||||
return (
|
|
||||||
<tr key={`resources-list-${r.name}`}>
|
|
||||||
<td>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href={`https://github.com/${project.owner}/${project.name}/blob/main/${r.path}`}
|
|
||||||
>
|
|
||||||
{r.path}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>{r.title}</td>
|
|
||||||
<td>{r.format.toUpperCase()}</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{resources.slice(0, 5).map((resource) => {
|
|
||||||
return (
|
|
||||||
<div key={`resource-preview-${resource.name}`} className="mt-10">
|
|
||||||
<h3>{resource.title || resource.name || resource.path}</h3>
|
|
||||||
<Table url={resource.path} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</section>
|
|
||||||
<hr />
|
|
||||||
<section>
|
|
||||||
<h2>Read me</h2>
|
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function MDLayout({ children, layout, ...props }) {
|
|
||||||
return <DatapackageLayout project={props.project} excerpt={props.excerpt}>{children}</DatapackageLayout>;
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { MDXRemote } from "next-mdx-remote";
|
|
||||||
import dynamic from "next/dynamic";
|
|
||||||
import { Mermaid } from "@flowershow/core";
|
|
||||||
|
|
||||||
import FrictionlessViewFactory from "./FrictionlessView";
|
|
||||||
|
|
||||||
// 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: dynamic(() => import("./Table")),
|
|
||||||
mermaid: Mermaid,
|
|
||||||
// Excel: dynamic(() => import('../components/Excel')),
|
|
||||||
// TODO: try and make these dynamic ...
|
|
||||||
Vega: dynamic(() => import("./Vega")),
|
|
||||||
VegaLite: dynamic(() => import("./VegaLite")),
|
|
||||||
LineChart: dynamic(() => import("./LineChart")),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
export default function DRD({
|
|
||||||
source,
|
|
||||||
frictionless = {
|
|
||||||
views: [],
|
|
||||||
resources: [],
|
|
||||||
},
|
|
||||||
}: {
|
|
||||||
source: any;
|
|
||||||
frictionless?: any;
|
|
||||||
}) {
|
|
||||||
// dynamic() can't be used inside of React rendering
|
|
||||||
// as it needs to be marked in the top level of the
|
|
||||||
// module for preloading to work
|
|
||||||
components.FrictionlessView = FrictionlessViewFactory({
|
|
||||||
views: frictionless.views,
|
|
||||||
resources: frictionless.resources,
|
|
||||||
});
|
|
||||||
|
|
||||||
return <MDXRemote {...source} components={components} />;
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
const DebouncedInput = ({
|
|
||||||
value: initialValue,
|
|
||||||
onChange,
|
|
||||||
debounce = 500,
|
|
||||||
...props
|
|
||||||
}) => {
|
|
||||||
const [value, setValue] = useState(initialValue);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setValue(initialValue);
|
|
||||||
}, [initialValue]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
onChange(value);
|
|
||||||
}, debounce);
|
|
||||||
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<input
|
|
||||||
{...props}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => setValue(e.target.value)}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DebouncedInput;
|
|
||||||
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
// FrictionlessView is a factory because we have to
|
|
||||||
// set the views and resources lists before using it
|
|
||||||
|
|
||||||
import { convertSimpleToVegaLite } from "../../lib/viewSpecConversion";
|
|
||||||
import VegaLite from "./VegaLite";
|
|
||||||
|
|
||||||
export default function FrictionlessViewFactory({
|
|
||||||
views = [],
|
|
||||||
resources = [],
|
|
||||||
}): ({
|
|
||||||
viewId,
|
|
||||||
fullWidth,
|
|
||||||
}: {
|
|
||||||
viewId: number;
|
|
||||||
fullWidth?: boolean;
|
|
||||||
}) => JSX.Element {
|
|
||||||
return ({ viewId, fullWidth = false }) => {
|
|
||||||
if (!(viewId in views)) {
|
|
||||||
console.error(`View ${viewId} not found`);
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
const view = views[viewId];
|
|
||||||
|
|
||||||
let resource;
|
|
||||||
if (resources.length > 1) {
|
|
||||||
resource = resources.find((r) => r.name === view.resourceName);
|
|
||||||
} else {
|
|
||||||
resource = resources[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
console.error(`Resource not found for view id ${viewId}`);
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let vegaSpec;
|
|
||||||
switch (view.specType) {
|
|
||||||
case "simple":
|
|
||||||
vegaSpec = convertSimpleToVegaLite(view, resource);
|
|
||||||
break;
|
|
||||||
// ... other conversions
|
|
||||||
}
|
|
||||||
|
|
||||||
vegaSpec.data = { url: resource.path };
|
|
||||||
|
|
||||||
return (
|
|
||||||
<VegaLite
|
|
||||||
fullWidth={fullWidth}
|
|
||||||
spec={vegaSpec}
|
|
||||||
actions={{ editor: false }}
|
|
||||||
downloadFileName={resource.name}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import { VegaLite } from "react-vega";
|
|
||||||
|
|
||||||
export default function LineChart({
|
|
||||||
data = [],
|
|
||||||
fullWidth = false,
|
|
||||||
title = "",
|
|
||||||
}) {
|
|
||||||
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",
|
|
||||||
title,
|
|
||||||
width: "container" as "container",
|
|
||||||
height: 300,
|
|
||||||
mark: {
|
|
||||||
type: "line" as "line",
|
|
||||||
color: "black",
|
|
||||||
strokeWidth: 1,
|
|
||||||
tooltip: true,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name: "table",
|
|
||||||
},
|
|
||||||
selection: {
|
|
||||||
grid: {
|
|
||||||
type: "interval" as "interval",
|
|
||||||
bind: "scales",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
encoding: {
|
|
||||||
x: {
|
|
||||||
field: "x",
|
|
||||||
timeUnit: "year",
|
|
||||||
type: "temporal" as "temporal",
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
field: "y",
|
|
||||||
type: "quantitative" as "temporal",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return <VegaLite data={vegaData} spec={spec} />;
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
import {
|
|
||||||
createColumnHelper,
|
|
||||||
FilterFn,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
getFilteredRowModel,
|
|
||||||
getPaginationRowModel,
|
|
||||||
getSortedRowModel,
|
|
||||||
useReactTable,
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ArrowDownIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
ChevronDoubleLeftIcon,
|
|
||||||
ChevronDoubleRightIcon,
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
|
||||||
|
|
||||||
import loadUrlProxied from "../../lib/loadUrlProxied";
|
|
||||||
import parseCsv from "../../lib/parseCsv";
|
|
||||||
import DebouncedInput from "./DebouncedInput";
|
|
||||||
|
|
||||||
const Table = ({
|
|
||||||
data: ogData = [],
|
|
||||||
cols: ogCols = [],
|
|
||||||
csv = "",
|
|
||||||
url = "",
|
|
||||||
}) => {
|
|
||||||
if (csv) {
|
|
||||||
const out = parseCsv(csv);
|
|
||||||
ogData = out.rows;
|
|
||||||
ogCols = out.fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [data, setData] = React.useState(ogData);
|
|
||||||
const [cols, setCols] = React.useState(ogCols);
|
|
||||||
const [error, setError] = React.useState(""); // TODO: add error handling
|
|
||||||
|
|
||||||
const tableCols = useMemo(() => {
|
|
||||||
const columnHelper = createColumnHelper();
|
|
||||||
return cols.map((c) =>
|
|
||||||
columnHelper.accessor(c.key, {
|
|
||||||
header: () => c.name,
|
|
||||||
cell: (info) => info.getValue(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}, [data, cols]);
|
|
||||||
|
|
||||||
const [globalFilter, setGlobalFilter] = useState("");
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data,
|
|
||||||
columns: tableCols,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
state: {
|
|
||||||
globalFilter,
|
|
||||||
},
|
|
||||||
globalFilterFn: globalFilterFn,
|
|
||||||
onGlobalFilterChange: setGlobalFilter,
|
|
||||||
getFilteredRowModel: getFilteredRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
getSortedRowModel: getSortedRowModel(),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (url) {
|
|
||||||
loadUrlProxied(url).then((data) => {
|
|
||||||
const { rows, fields } = parseCsv(data);
|
|
||||||
setData(rows);
|
|
||||||
setCols(fields);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [url]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DebouncedInput
|
|
||||||
value={globalFilter ?? ""}
|
|
||||||
onChange={(value) => setGlobalFilter(String(value))}
|
|
||||||
className="p-2 text-sm shadow border border-block"
|
|
||||||
placeholder="Search all columns..."
|
|
||||||
/>
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
{table.getHeaderGroups().map((hg) => (
|
|
||||||
<tr key={hg.id}>
|
|
||||||
{hg.headers.map((h) => (
|
|
||||||
<th key={h.id}>
|
|
||||||
<div
|
|
||||||
{...{
|
|
||||||
className: h.column.getCanSort()
|
|
||||||
? "cursor-pointer select-none"
|
|
||||||
: "",
|
|
||||||
onClick: h.column.getToggleSortingHandler(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{flexRender(h.column.columnDef.header, h.getContext())}
|
|
||||||
{{
|
|
||||||
asc: (
|
|
||||||
<ArrowUpIcon className="inline-block ml-2 h-4 w-4" />
|
|
||||||
),
|
|
||||||
desc: (
|
|
||||||
<ArrowDownIcon className="inline-block ml-2 h-4 w-4" />
|
|
||||||
),
|
|
||||||
}[h.column.getIsSorted() as string] ?? (
|
|
||||||
<div className="inline-block ml-2 h-4 w-4" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{table.getRowModel().rows.map((r) => (
|
|
||||||
<tr key={r.id}>
|
|
||||||
{r.getVisibleCells().map((c) => (
|
|
||||||
<td key={c.id}>
|
|
||||||
{flexRender(c.column.columnDef.cell, c.getContext())}
|
|
||||||
</td>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<div className="flex gap-2 items-center justify-center">
|
|
||||||
<button
|
|
||||||
className={`w-6 h-6 ${
|
|
||||||
!table.getCanPreviousPage() ? "opacity-25" : "opacity-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => table.setPageIndex(0)}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<ChevronDoubleLeftIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`w-6 h-6 ${
|
|
||||||
!table.getCanPreviousPage() ? "opacity-25" : "opacity-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => table.previousPage()}
|
|
||||||
disabled={!table.getCanPreviousPage()}
|
|
||||||
>
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
</button>
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<div>Page</div>
|
|
||||||
<strong>
|
|
||||||
{table.getState().pagination.pageIndex + 1} of{" "}
|
|
||||||
{table.getPageCount()}
|
|
||||||
</strong>
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
className={`w-6 h-6 ${
|
|
||||||
!table.getCanNextPage() ? "opacity-25" : "opacity-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => table.nextPage()}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className={`w-6 h-6 ${
|
|
||||||
!table.getCanNextPage() ? "opacity-25" : "opacity-100"
|
|
||||||
}`}
|
|
||||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
|
||||||
disabled={!table.getCanNextPage()}
|
|
||||||
>
|
|
||||||
<ChevronDoubleRightIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const globalFilterFn: FilterFn<any> = (row, columnId, filterValue: string) => {
|
|
||||||
const search = filterValue.toLowerCase();
|
|
||||||
|
|
||||||
let value = row.getValue(columnId) as string;
|
|
||||||
if (typeof value === "number") value = String(value);
|
|
||||||
|
|
||||||
return value?.toLowerCase().includes(search);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Table;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { Vega as VegaOg } from "react-vega";
|
|
||||||
export default function Vega(props) {
|
|
||||||
return <VegaOg {...props} />;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import { VegaLite as VegaOg } from "react-vega";
|
|
||||||
export default function Vega(props) {
|
|
||||||
return <VegaOg {...props} />;
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,44 @@
|
|||||||
[
|
[
|
||||||
{ "owner": "datasets", "repo": "oil-prices"},
|
{
|
||||||
{ "owner": "datasets", "repo": "investor-flow-of-funds-us"},
|
"owner": "datasets",
|
||||||
{ "owner": "datasets", "repo": "browser-stats"},
|
"branch": "main",
|
||||||
{ "owner": "datasets", "repo": "glacier-mass-balance"},
|
"repo": "oil-prices",
|
||||||
{ "owner": "datasets", "repo": "bond-yields-us-10y"}
|
"files": [
|
||||||
|
"data/brent-daily.csv",
|
||||||
|
"data/brent-monthly.csv",
|
||||||
|
"data/brent-weekly.csv",
|
||||||
|
"data/brent-year.csv",
|
||||||
|
"data/wti-daily.csv",
|
||||||
|
"data/wti-monthly.csv",
|
||||||
|
"data/wti-weekly.csv",
|
||||||
|
"data/wti-year.csv"
|
||||||
|
],
|
||||||
|
"readme": "README.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "datasets",
|
||||||
|
"branch": "main",
|
||||||
|
"repo": "investor-flow-of-funds-us",
|
||||||
|
"files": [
|
||||||
|
"data/monthly.csv",
|
||||||
|
"data/weekly.csv"
|
||||||
|
],
|
||||||
|
"readme": "README.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "fivethirtyeight",
|
||||||
|
"repo": "data",
|
||||||
|
"branch": "master",
|
||||||
|
"description": "Data about bad drivers",
|
||||||
|
"name": "Bad Drivers",
|
||||||
|
"files": ["bad-drivers/bad-drivers.csv"],
|
||||||
|
"readme": "bad-drivers/README.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "fivethirtyeight",
|
||||||
|
"repo": "data",
|
||||||
|
"branch": "master",
|
||||||
|
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
|
||||||
|
"readme": "nba-raptor/README.md"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export default function loadUrlProxied(url: string) {
|
|
||||||
// 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 = "/api/proxy";
|
|
||||||
url = PROXY_URL + "?url=" + encodeURIComponent(url);
|
|
||||||
}
|
|
||||||
return axios.get(url).then((res) => res.data);
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import matter from "gray-matter";
|
|
||||||
import mdxmermaid from "mdx-mermaid";
|
|
||||||
import { h } from "hastscript";
|
|
||||||
import remarkCallouts from "@flowershow/remark-callouts";
|
|
||||||
import remarkEmbed from "@flowershow/remark-embed";
|
|
||||||
import remarkGfm from "remark-gfm";
|
|
||||||
import remarkMath from "remark-math";
|
|
||||||
import remarkSmartypants from "remark-smartypants";
|
|
||||||
import remarkToc from "remark-toc";
|
|
||||||
import remarkWikiLink from "@flowershow/remark-wiki-link";
|
|
||||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
||||||
import rehypeKatex from "rehype-katex";
|
|
||||||
import rehypeSlug from "rehype-slug";
|
|
||||||
import rehypePrismPlus from "rehype-prism-plus";
|
|
||||||
|
|
||||||
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
|
|
||||||
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
|
||||||
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
|
||||||
*/
|
|
||||||
const parse = async function (source, format) {
|
|
||||||
const { content, data, excerpt } = matter(source, {
|
|
||||||
excerpt: (file, options) => {
|
|
||||||
// Generate an excerpt for the file
|
|
||||||
file.excerpt = file.content.split("\n\n")[0];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mdxSource = await serialize(
|
|
||||||
{ value: content, path: format },
|
|
||||||
{
|
|
||||||
// Optionally pass remark/rehype plugins
|
|
||||||
mdxOptions: {
|
|
||||||
remarkPlugins: [
|
|
||||||
remarkEmbed,
|
|
||||||
remarkGfm,
|
|
||||||
[remarkSmartypants, { quotes: false, dashes: "oldschool" }],
|
|
||||||
remarkMath,
|
|
||||||
remarkCallouts,
|
|
||||||
remarkWikiLink,
|
|
||||||
[
|
|
||||||
remarkToc,
|
|
||||||
{
|
|
||||||
heading: "Table of contents",
|
|
||||||
tight: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[mdxmermaid, {}],
|
|
||||||
],
|
|
||||||
rehypePlugins: [
|
|
||||||
rehypeSlug,
|
|
||||||
[
|
|
||||||
rehypeAutolinkHeadings,
|
|
||||||
{
|
|
||||||
properties: { className: 'heading-link' },
|
|
||||||
test(element) {
|
|
||||||
return (
|
|
||||||
["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) &&
|
|
||||||
element.properties?.id !== "table-of-contents" &&
|
|
||||||
element.properties?.className !== "blockquote-heading"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
content() {
|
|
||||||
return [
|
|
||||||
h(
|
|
||||||
"svg",
|
|
||||||
{
|
|
||||||
xmlns: "http:www.w3.org/2000/svg",
|
|
||||||
fill: "#ab2b65",
|
|
||||||
viewBox: "0 0 20 20",
|
|
||||||
className: "w-5 h-5",
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h("path", {
|
|
||||||
fillRule: "evenodd",
|
|
||||||
clipRule: "evenodd",
|
|
||||||
d: "M9.493 2.853a.75.75 0 00-1.486-.205L7.545 6H4.198a.75.75 0 000 1.5h3.14l-.69 5H3.302a.75.75 0 000 1.5h3.14l-.435 3.148a.75.75 0 001.486.205L7.955 14h2.986l-.434 3.148a.75.75 0 001.486.205L12.456 14h3.346a.75.75 0 000-1.5h-3.14l.69-5h3.346a.75.75 0 000-1.5h-3.14l.435-3.147a.75.75 0 00-1.486-.205L12.045 6H9.059l.434-3.147zM8.852 7.5l-.69 5h2.986l.69-5H8.852z",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[rehypeKatex, { output: "mathml" }],
|
|
||||||
[rehypePrismPlus, { ignoreMissing: true }],
|
|
||||||
],
|
|
||||||
format,
|
|
||||||
},
|
|
||||||
scope: data,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
mdxSource: mdxSource,
|
|
||||||
frontMatter: data,
|
|
||||||
excerpt,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parse;
|
|
||||||
147
examples/simple-example/lib/octokit.ts
Normal file
147
examples/simple-example/lib/octokit.ts
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
|
export interface GithubProject {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
branch: string;
|
||||||
|
files: string[];
|
||||||
|
readme: string;
|
||||||
|
description?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectReadme(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
readme: string,
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const response = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: readme,
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
const data = response.data as { content?: string };
|
||||||
|
const fileContent = data.content ? data.content : '';
|
||||||
|
if (fileContent === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const decodedContent = Buffer.from(fileContent, 'base64').toString();
|
||||||
|
return decodedContent;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLastUpdated(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
readme: string,
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const response = await octokit.rest.repos.listCommits({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: readme,
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
return response.data[0].commit.committer.date;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function getProjectMetadata(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const response = await octokit.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRepoContents(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
files: string[],
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const contents = [];
|
||||||
|
for (const path of files) {
|
||||||
|
const response = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: branch,
|
||||||
|
path: path,
|
||||||
|
});
|
||||||
|
const data = response.data as { download_url?: string, name: string, size: number };
|
||||||
|
contents.push({ download_url: data.download_url, name: data.name, size: data.size});
|
||||||
|
}
|
||||||
|
return contents;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProject(project: GithubProject, github_pat?: string) {
|
||||||
|
const projectMetadata = await getProjectMetadata(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
if (!projectMetadata) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const projectReadme = await getProjectReadme(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
project.branch,
|
||||||
|
project.readme,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
if (!projectReadme) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const projectData = await getRepoContents(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
project.branch,
|
||||||
|
project.files,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
if (!projectData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const projectBase = project.readme.split('/').length > 1
|
||||||
|
? project.readme.split('/').slice(0, -1).join('/')
|
||||||
|
: '/'
|
||||||
|
const last_updated = await getLastUpdated(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
project.branch,
|
||||||
|
projectBase,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
return { ...projectMetadata, files: projectData, readmeContent: projectReadme, last_updated, base_path: projectBase };
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import papa from "papaparse";
|
|
||||||
|
|
||||||
const 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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parseCsv;
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
import * as crypto from "crypto";
|
|
||||||
import axios from "axios";
|
|
||||||
import { Octokit } from "octokit"
|
|
||||||
|
|
||||||
export default class Project {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
owner: string;
|
|
||||||
github_repo: string;
|
|
||||||
readme: string;
|
|
||||||
metadata: any;
|
|
||||||
repo_metadata: any;
|
|
||||||
|
|
||||||
constructor(owner: string, name: string) {
|
|
||||||
this.name = name;
|
|
||||||
this.owner = owner;
|
|
||||||
this.github_repo = `https://github.com/${owner}/${name}`;
|
|
||||||
|
|
||||||
// TODO: using the GitHub repo to set the id is not a good idea
|
|
||||||
// since repos can be renamed and then we are going to end up with
|
|
||||||
// a duplicate
|
|
||||||
const encodedGHRepo = Buffer.from(this.github_repo, "utf-8").toString();
|
|
||||||
this.id = crypto.createHash("sha1").update(encodedGHRepo).digest("hex");
|
|
||||||
}
|
|
||||||
|
|
||||||
initFromGitHub = async () => {
|
|
||||||
const octokit = new Octokit()
|
|
||||||
// TODO: what if the repo doesn't exist?
|
|
||||||
await this.getFileContent("README.md")
|
|
||||||
.then((content) => (this.readme = content))
|
|
||||||
.catch((e) => (this.readme = null));
|
|
||||||
|
|
||||||
await this.getFileContent("datapackage.json")
|
|
||||||
.then((content) => (this.metadata = content))
|
|
||||||
.catch((e) => (this.metadata = {}));
|
|
||||||
|
|
||||||
const github_metadata = await octokit.rest.repos.get({ owner: this.owner, repo: this.name })
|
|
||||||
this.repo_metadata = github_metadata.data ? github_metadata.data : null
|
|
||||||
};
|
|
||||||
|
|
||||||
getFileContent = (path, branch = "main") => {
|
|
||||||
return axios
|
|
||||||
.get(
|
|
||||||
`https://raw.githubusercontent.com/${this.owner}/${this.name}/${branch}/${path}`
|
|
||||||
)
|
|
||||||
.then((res) => res.data);
|
|
||||||
};
|
|
||||||
|
|
||||||
serialize() {
|
|
||||||
return JSON.parse(JSON.stringify(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getFromGitHub(owner: string, name: string) {
|
|
||||||
const project = new Project(owner, name);
|
|
||||||
await project.initFromGitHub();
|
|
||||||
|
|
||||||
return project;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
export function convertSimpleToVegaLite(view, resource) {
|
|
||||||
const x = resource.schema.fields.find((f) => f.name === view.spec.group);
|
|
||||||
const y = resource.schema.fields.find((f) => f.name === view.spec.series[0]);
|
|
||||||
|
|
||||||
const xType = inferVegaType(x.type);
|
|
||||||
const yType = inferVegaType(y.type);
|
|
||||||
|
|
||||||
let vegaLiteSpec = {
|
|
||||||
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
|
||||||
mark: {
|
|
||||||
type: view.spec.type,
|
|
||||||
color: "black",
|
|
||||||
strokeWidth: 1,
|
|
||||||
tooltip: true,
|
|
||||||
},
|
|
||||||
title: view.title,
|
|
||||||
width: "container",
|
|
||||||
height: 300,
|
|
||||||
selection: {
|
|
||||||
grid: {
|
|
||||||
type: "interval",
|
|
||||||
bind: "scales",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
encoding: {
|
|
||||||
x: {
|
|
||||||
field: x.name,
|
|
||||||
type: xType,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
field: y.name,
|
|
||||||
type: yType,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return vegaLiteSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
const inferVegaType = (fieldType) => {
|
|
||||||
switch (fieldType) {
|
|
||||||
case "date":
|
|
||||||
return "Temporal";
|
|
||||||
case "number":
|
|
||||||
return "Quantitative";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,23 +1,7 @@
|
|||||||
// 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 = {
|
const nextConfig = {
|
||||||
async rewrites() {
|
async rewrites() {
|
||||||
return {
|
return {
|
||||||
beforeFiles: [
|
beforeFiles: [
|
||||||
{
|
|
||||||
source: "/@org/:org/:project/:file(\.\+\\\.\.\+\$)",
|
|
||||||
destination:
|
|
||||||
'/api/proxy?url=https://raw.githubusercontent.com/:org/:project/main/:file',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
source: "/@:org/:project/:file(\.\+\\\.\.\+\$)",
|
|
||||||
destination:
|
|
||||||
'/api/proxy?url=https://raw.githubusercontent.com/:org/:project/main/:file',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
source: '/@:org/:project*',
|
source: '/@:org/:project*',
|
||||||
destination: '/@org/:org/:project*',
|
destination: '/@org/:org/:project*',
|
||||||
@@ -25,11 +9,9 @@ const nextConfig = {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
nx: {
|
serverRuntimeConfig: {
|
||||||
// Set this to true if you would like to use SVGR
|
github_pat: process.env.GITHUB_PAT ? process.env.GITHUB_PAT : null,
|
||||||
// See: https://github.com/gregberge/svgr
|
|
||||||
svgr: true,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = withNx(nextConfig);
|
module.exports = nextConfig;
|
||||||
|
|||||||
5833
examples/simple-example/package-lock.json
generated
Normal file
5833
examples/simple-example/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
examples/simple-example/package.json
Normal file
32
examples/simple-example/package.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "my-app",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "18.16.0",
|
||||||
|
"@types/react": "18.0.38",
|
||||||
|
"@types/react-dom": "18.0.11",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"eslint-config-next": "13.3.1",
|
||||||
|
"next": "13.3.1",
|
||||||
|
"next-seo": "^6.0.0",
|
||||||
|
"octokit": "^2.0.14",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-markdown": "^8.0.7",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"typescript": "5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"tailwindcss": "^3.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
120
examples/simple-example/pages/@org/[org]/[...path].tsx
Normal file
120
examples/simple-example/pages/@org/[org]/[...path].tsx
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
|
import { NextSeo } from 'next-seo';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import { getProject, GithubProject } from '../../../lib/octokit';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function ProjectPage({ project }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NextSeo title={`PortalJS - @${project.repo_config.owner}/${project.repo_config.repo}${project.base_path !== '/' ? '/' + project.base_path : ''}`} />
|
||||||
|
<main className="prose mx-auto my-8">
|
||||||
|
<Link href='/'>Back to homepage</Link>
|
||||||
|
<h1 className="mb-0">Data</h1>
|
||||||
|
<div className="inline-block min-w-full py-2 align-middle">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{project.files.map((file) => (
|
||||||
|
<tr key={file.download_url}>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
<a href={file.download_url}>{file.name}</a>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
{file.size} Bytes
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Readme</h1>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{project.readmeContent}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates `/posts/1` and `/posts/2`
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const jsonDirectory = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
'datasets.json'
|
||||||
|
);
|
||||||
|
const repos = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
|
|
||||||
|
return {
|
||||||
|
paths: JSON.parse(repos).map((repo) => {
|
||||||
|
const projectPath =
|
||||||
|
repo.readme.split('/').length > 1
|
||||||
|
? repo.readme.split('/').slice(0, -1)
|
||||||
|
: null;
|
||||||
|
let path = [repo.repo];
|
||||||
|
if (projectPath) {
|
||||||
|
projectPath.forEach((element) => {
|
||||||
|
path.push(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
params: { org: repo.owner, path },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
fallback: false, // can also be true or 'blocking'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticProps({ params }) {
|
||||||
|
const jsonDirectory = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
'datasets.json'
|
||||||
|
);
|
||||||
|
const reposFile = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
|
const repos: GithubProject[] = JSON.parse(reposFile);
|
||||||
|
const repo = repos.find((_repo) => {
|
||||||
|
const projectPath =
|
||||||
|
_repo.readme.split('/').length > 1
|
||||||
|
? _repo.readme.split('/').slice(0, -1)
|
||||||
|
: null;
|
||||||
|
let path = [_repo.repo];
|
||||||
|
if (projectPath) {
|
||||||
|
projectPath.forEach((element) => {
|
||||||
|
path.push(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
_repo.owner == params.org &&
|
||||||
|
JSON.stringify(path) === JSON.stringify(params.path)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||||
|
const project = await getProject(repo, github_pat);
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
project: { ...project, repo_config: repo },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import Head from 'next/head';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
import DRD from '../../../../components/drd/DRD';
|
|
||||||
import parse from '../../../../lib/markdown';
|
|
||||||
import Project from '../../../../lib/project';
|
|
||||||
import { NextSeo } from 'next-seo';
|
|
||||||
import MDLayout from 'examples/simple-example/components/MDLayout';
|
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
function CollectionsLayout({ children, ...frontMatter }) {
|
|
||||||
const { title, date, description } = frontMatter;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<article className="docs prose text-primary dark:text-primary-dark dark:prose-invert prose-headings:font-headings prose-a:break-words mx-auto p-6">
|
|
||||||
<header>
|
|
||||||
<div className="mb-6">
|
|
||||||
{date && (
|
|
||||||
<p className="text-sm text-zinc-400 dark:text-zinc-500">
|
|
||||||
<time dateTime={date}>{date}</time>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{title && <h1 className="mb-2">{title}</h1>}
|
|
||||||
{description && <p className="text-xl mt-0">{description}</p>}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<section>{children}</section>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProjectPage({
|
|
||||||
mdxSource,
|
|
||||||
frontMatter,
|
|
||||||
excerpt,
|
|
||||||
project,
|
|
||||||
}) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NextSeo title={`PortalJS - @${project.owner}/${project.name}`} />
|
|
||||||
<Head>
|
|
||||||
{/*
|
|
||||||
On index files, add trailling slash to the base path
|
|
||||||
see notes: https://github.com/datopian/datahub-next/issues/69
|
|
||||||
*/}
|
|
||||||
<base href={router.asPath.split('#')[0] + '/'} />
|
|
||||||
</Head>
|
|
||||||
<main>
|
|
||||||
<MDLayout
|
|
||||||
layout={frontMatter.layout}
|
|
||||||
excerpt={excerpt}
|
|
||||||
project={project}
|
|
||||||
{...frontMatter}
|
|
||||||
>
|
|
||||||
<DRD
|
|
||||||
source={mdxSource}
|
|
||||||
frictionless={{
|
|
||||||
views: project.metadata?.views,
|
|
||||||
resources: project.metadata?.resources,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</MDLayout>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generates `/posts/1` and `/posts/2`
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const jsonDirectory = path.join(process.cwd(), '/examples/simple-example/datasets.json');
|
|
||||||
const repos = await fs.readFile(jsonDirectory, 'utf8');
|
|
||||||
|
|
||||||
return {
|
|
||||||
paths: JSON.parse(repos).map(repo => ({ params: { org: repo.owner, project: repo.repo}})),
|
|
||||||
fallback: false, // can also be true or 'blocking'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getStaticProps({ params }) {
|
|
||||||
const { org: orgName, project: projectName } = params;
|
|
||||||
|
|
||||||
const project = await Project.getFromGitHub(orgName, projectName);
|
|
||||||
|
|
||||||
// Defaults to README
|
|
||||||
let content = project.readme;
|
|
||||||
|
|
||||||
if (content === null) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let { mdxSource, frontMatter, excerpt } = await parse(content, '.mdx');
|
|
||||||
|
|
||||||
if (project.metadata?.resources) {
|
|
||||||
frontMatter.layout = 'datapackage';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
mdxSource,
|
|
||||||
frontMatter,
|
|
||||||
excerpt,
|
|
||||||
project: project.serialize(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AppProps } from 'next/app';
|
import { AppProps } from 'next/app';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import './styles.css';
|
import './styles.css';
|
||||||
import "../styles/global.css";
|
|
||||||
|
|
||||||
function CustomApp({ Component, pageProps }: AppProps) {
|
function CustomApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export default function handler(req, res) {
|
|
||||||
if (!req.query.url) {
|
|
||||||
res.status(200).send({
|
|
||||||
error: true,
|
|
||||||
info: "No url to proxy in query string i.e. ?url=...",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
axios({
|
|
||||||
method: "get",
|
|
||||||
url: req.query.url,
|
|
||||||
responseType: "stream",
|
|
||||||
})
|
|
||||||
.then((resp) => {
|
|
||||||
resp.data.pipe(res);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
res.status(400).send({
|
|
||||||
error: true,
|
|
||||||
info: err.message,
|
|
||||||
detailed: err,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,35 +1,20 @@
|
|||||||
import parse from '../lib/markdown';
|
|
||||||
import Project from '../lib/project';
|
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import Link from 'next/link';
|
import { getProject } from '../lib/octokit';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const jsonDirectory = path.join(
|
const jsonDirectory = path.join(
|
||||||
process.cwd(),
|
process.cwd(),
|
||||||
'/examples/simple-example/datasets.json'
|
'/datasets.json'
|
||||||
);
|
);
|
||||||
const repos = await fs.readFile(jsonDirectory, 'utf8');
|
const repos = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
|
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||||
|
|
||||||
const projects = await Promise.all(
|
const projects = await Promise.all(
|
||||||
JSON.parse(repos).map(async (repo) => {
|
(JSON.parse(repos)).map(async (repo) => {
|
||||||
const project = await Project.getFromGitHub(repo.owner, repo.repo);
|
const project = await getProject(repo, github_pat);
|
||||||
|
return { ...project, repo_config: repo };
|
||||||
// Defaults to README
|
|
||||||
const content = project.readme ? project.readme : '';
|
|
||||||
|
|
||||||
let { mdxSource, frontMatter, excerpt } = await parse(content, '.mdx');
|
|
||||||
|
|
||||||
if (project.metadata?.resources) {
|
|
||||||
frontMatter.layout = 'datapackage';
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
mdxSource,
|
|
||||||
frontMatter,
|
|
||||||
excerpt,
|
|
||||||
project: project.serialize(),
|
|
||||||
};
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -53,37 +38,13 @@ export function Datasets({ projects }) {
|
|||||||
return (
|
return (
|
||||||
<div className="bg-white">
|
<div className="bg-white">
|
||||||
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
||||||
<h2 className="text-2xl font-bold leading-10 tracking-tight text-indigo-500">
|
<h2 className="text-2xl font-bold leading-10 tracking-tight">
|
||||||
My Datasets
|
My Datasets
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-6 max-w-2xl text-base leading-7 text-gray-600">
|
<p className="mt-6 max-w-2xl text-base leading-7 text-gray-600">
|
||||||
Here is a list of all my datasets for easy access and sharing
|
Here is a list of all my datasets for easy access and sharing
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-20">
|
<div className="mt-20">
|
||||||
{/*
|
|
||||||
<dl className="space-y-16 sm:grid sm:grid-cols-2 sm:gap-x-6 sm:gap-y-16 sm:space-y-0 lg:grid-cols-3 lg:gap-x-10">
|
|
||||||
{projects.map((project) => (
|
|
||||||
<div>
|
|
||||||
<dt className="text-base font-semibold leading-7 text-gray-900">
|
|
||||||
<Link
|
|
||||||
href={`@${project.project.owner}/${project.project.name}`}
|
|
||||||
>
|
|
||||||
{project.project.owner}/{project.project.name}
|
|
||||||
</Link>
|
|
||||||
</dt>
|
|
||||||
<dt className="text-base font-semibold leading-7 text-indigo-600">
|
|
||||||
<a
|
|
||||||
href={`https://github.com/${project.project.owner}/${project.project.name}`}
|
|
||||||
>
|
|
||||||
Github repo
|
|
||||||
</a>
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-2 text-base leading-7 text-gray-600">
|
|
||||||
{project.excerpt !== '' ? project.excerpt : 'No description'}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</dl> */}
|
|
||||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
<table className="min-w-full divide-y divide-gray-300">
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
@@ -93,7 +54,13 @@ export function Datasets({ projects }) {
|
|||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
>
|
>
|
||||||
Dataset name
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Repo
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
@@ -115,24 +82,26 @@ export function Datasets({ projects }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<tr>
|
<tr key={project.id}>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
<a href={project.project.repo_metadata.html_url}>
|
{project.repo_config.name
|
||||||
{project.project.owner}/{project.project.name}
|
? project.repo_config.name
|
||||||
</a>
|
: project.full_name + (project.base_path === '/' ? '' : '/' + project.base_path)}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
<a href={project.html_url}>{project.full_name}</a>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4 text-sm text-gray-500">
|
<td className="px-3 py-4 text-sm text-gray-500">
|
||||||
{project.project.repo_metadata.description}
|
{project.repo_config.description
|
||||||
|
? project.repo_config.description
|
||||||
|
: project.description}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
{formatter.format(
|
{formatter.format(new Date(project.last_updated))}
|
||||||
new Date(project.project.repo_metadata.updated_at)
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||||
<a
|
<a
|
||||||
href={`/@${project.project.owner}/${project.project.name}`}
|
href={`/@${project.repo_config.owner}/${project.repo_config.repo}/${project.base_path === '/' ? '' : project.base_path}`}
|
||||||
className="text-indigo-600 hover:text-indigo-900"
|
|
||||||
>
|
>
|
||||||
More info
|
More info
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
const { join } = require('path');
|
|
||||||
|
|
||||||
// Note: If you use library-specific PostCSS/Tailwind configuration then you should remove the `postcssConfig` build
|
|
||||||
// option from your application's configuration (i.e. project.json).
|
|
||||||
//
|
|
||||||
// See: https://nx.dev/guides/using-tailwind-css-in-react#step-4:-applying-configuration-to-libraries
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {
|
tailwindcss: {},
|
||||||
config: join(__dirname, 'tailwind.config.js'),
|
|
||||||
},
|
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
@import "@flowershow/remark-callouts/styles.css";
|
|
||||||
|
|
||||||
/* mathjax */
|
|
||||||
.math-inline > mjx-container > svg {
|
|
||||||
display: inline;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* smooth scrolling in modern browsers */
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tooltip fade-out clip */
|
|
||||||
.tooltip-body::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 3.6rem; /* multiple of $line-height used on the tooltip body (defined in tooltipBodyStyle) */
|
|
||||||
height: 1.2rem; /* ($top + $height)/$line-height is the number of lines we want to clip tooltip text at*/
|
|
||||||
width: 10rem;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
rgba(255, 255, 255, 0),
|
|
||||||
rgba(255, 255, 255, 1) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h2, h3, h4, h5, h6):not(.blogitem-title) {
|
|
||||||
margin-left: -2rem !important;
|
|
||||||
padding-left: 2rem !important;
|
|
||||||
scroll-margin-top: 4.5rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading-link {
|
|
||||||
padding: 1px;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
margin: auto 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #1e293b;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .heading-link {
|
|
||||||
/* border: 1px solid #ab2b65; */
|
|
||||||
/* background: none; */
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h2, h3, h4, h5, h6):not(.blogitem-title):hover .heading-link {
|
|
||||||
opacity: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading-link svg {
|
|
||||||
transform: scale(0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
.heading-link {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
const { createGlobPatternsForDependencies } = require('@nrwl/react/tailwind');
|
|
||||||
const { join } = require('path');
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: [
|
content: [
|
||||||
"node_modules/@flowershow/core/dist/*.js",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"node_modules/@flowershow/core/*.js",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
join(
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
__dirname,
|
|
||||||
'{src,pages,components}/**/*!(*.stories|*.spec).{ts,tsx,html}'
|
|
||||||
),
|
|
||||||
...createGlobPatternsForDependencies(__dirname),
|
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
require('@tailwindcss/typography'),
|
require('@tailwindcss/typography')
|
||||||
],
|
],
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,50 +1,20 @@
|
|||||||
{
|
{
|
||||||
"extends": "../../tsconfig.base.json",
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"jsx": "preserve",
|
"target": "es5",
|
||||||
"allowJs": true,
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"esModuleInterop": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": false,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"incremental": true,
|
|
||||||
"types": [
|
|
||||||
"jest",
|
|
||||||
"node"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"target": "es2020",
|
|
||||||
"lib": [
|
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false,
|
"strict": false,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"incremental": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"include": [
|
"incremental": true
|
||||||
"**/*.ts",
|
},
|
||||||
"**/*.tsx",
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
"**/*.js",
|
"exclude": ["node_modules"]
|
||||||
"**/*.jsx",
|
|
||||||
"next-env.d.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules",
|
|
||||||
"jest.config.ts",
|
|
||||||
"src/**/*.spec.ts",
|
|
||||||
"src/**/*.test.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
65
package-lock.json
generated
65
package-lock.json
generated
@@ -44,6 +44,7 @@
|
|||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-markdown": "^8.0.7",
|
||||||
"react-next-github-btn": "^1.2.1",
|
"react-next-github-btn": "^1.2.1",
|
||||||
"react-plotly.js": "^2.6.0",
|
"react-plotly.js": "^2.6.0",
|
||||||
"react-plotlyjs": "^0.4.4",
|
"react-plotlyjs": "^0.4.4",
|
||||||
@@ -29695,6 +29696,41 @@
|
|||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/react-markdown": {
|
||||||
|
"version": "8.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz",
|
||||||
|
"integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/hast": "^2.0.0",
|
||||||
|
"@types/prop-types": "^15.0.0",
|
||||||
|
"@types/unist": "^2.0.0",
|
||||||
|
"comma-separated-tokens": "^2.0.0",
|
||||||
|
"hast-util-whitespace": "^2.0.0",
|
||||||
|
"prop-types": "^15.0.0",
|
||||||
|
"property-information": "^6.0.0",
|
||||||
|
"react-is": "^18.0.0",
|
||||||
|
"remark-parse": "^10.0.0",
|
||||||
|
"remark-rehype": "^10.0.0",
|
||||||
|
"space-separated-tokens": "^2.0.0",
|
||||||
|
"style-to-object": "^0.4.0",
|
||||||
|
"unified": "^10.0.0",
|
||||||
|
"unist-util-visit": "^4.0.0",
|
||||||
|
"vfile": "^5.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/unified"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=16",
|
||||||
|
"react": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-markdown/node_modules/react-is": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
||||||
|
},
|
||||||
"node_modules/react-next-github-btn": {
|
"node_modules/react-next-github-btn": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-next-github-btn/-/react-next-github-btn-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-next-github-btn/-/react-next-github-btn-1.2.1.tgz",
|
||||||
@@ -57219,6 +57255,35 @@
|
|||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"react-markdown": {
|
||||||
|
"version": "8.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-8.0.7.tgz",
|
||||||
|
"integrity": "sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==",
|
||||||
|
"requires": {
|
||||||
|
"@types/hast": "^2.0.0",
|
||||||
|
"@types/prop-types": "^15.0.0",
|
||||||
|
"@types/unist": "^2.0.0",
|
||||||
|
"comma-separated-tokens": "^2.0.0",
|
||||||
|
"hast-util-whitespace": "^2.0.0",
|
||||||
|
"prop-types": "^15.0.0",
|
||||||
|
"property-information": "^6.0.0",
|
||||||
|
"react-is": "^18.0.0",
|
||||||
|
"remark-parse": "^10.0.0",
|
||||||
|
"remark-rehype": "^10.0.0",
|
||||||
|
"space-separated-tokens": "^2.0.0",
|
||||||
|
"style-to-object": "^0.4.0",
|
||||||
|
"unified": "^10.0.0",
|
||||||
|
"unist-util-visit": "^4.0.0",
|
||||||
|
"vfile": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": {
|
||||||
|
"version": "18.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-next-github-btn": {
|
"react-next-github-btn": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-next-github-btn/-/react-next-github-btn-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-next-github-btn/-/react-next-github-btn-1.2.1.tgz",
|
||||||
|
|||||||
60
package.json
60
package.json
@@ -4,62 +4,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {},
|
"scripts": {},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"@apollo/client": "^3.7.11",
|
|
||||||
"@apollo/react-hooks": "^4.0.0",
|
|
||||||
"@emotion/react": "^11.10.6",
|
|
||||||
"@emotion/styled": "^11.10.6",
|
|
||||||
"@flowershow/core": "^0.4.9",
|
|
||||||
"@flowershow/markdowndb": "^0.1.0",
|
|
||||||
"@flowershow/remark-callouts": "^1.0.0",
|
|
||||||
"@flowershow/remark-embed": "^1.0.0",
|
|
||||||
"@flowershow/remark-wiki-link": "^1.0.1",
|
|
||||||
"@headlessui/react": "^1.7.13",
|
|
||||||
"@heroicons/react": "^2.0.17",
|
|
||||||
"@mui/icons-material": "^5.11.16",
|
|
||||||
"@mui/material": "^5.11.16",
|
|
||||||
"@mui/x-data-grid": "^6.1.0",
|
|
||||||
"@opentelemetry/api": "^1.4.0",
|
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"@tanstack/react-table": "^8.8.5",
|
|
||||||
"apollo-cache-inmemory": "^1.6.6",
|
|
||||||
"apollo-link": "^1.2.14",
|
|
||||||
"apollo-link-rest": "^0.9.0",
|
|
||||||
"filesize": "^10.0.7",
|
|
||||||
"gray-matter": "^4.0.3",
|
|
||||||
"html-react-parser": "^3.0.15",
|
|
||||||
"markdown-it": "^13.0.1",
|
|
||||||
"next": "^13.2.1",
|
|
||||||
"next-mdx-remote": "^4.4.1",
|
|
||||||
"next-seo": "^6.0.0",
|
|
||||||
"next-translate": "^2.0.5",
|
|
||||||
"nock": "^13.3.0",
|
|
||||||
"octokit": "^2.0.14",
|
|
||||||
"papaparse": "^5.4.1",
|
|
||||||
"plotly.js-basic-dist": "^2.20.0",
|
|
||||||
"prop-types": "^15.8.1",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-next-github-btn": "^1.2.1",
|
|
||||||
"react-plotly.js": "^2.6.0",
|
|
||||||
"react-plotlyjs": "^0.4.4",
|
|
||||||
"react-vega": "^7.6.0",
|
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
|
||||||
"rehype-katex": "^6.0.2",
|
|
||||||
"rehype-prism-plus": "^1.5.1",
|
|
||||||
"rehype-slug": "^5.1.0",
|
|
||||||
"remark-footnotes": "^4.0.1",
|
|
||||||
"remark-gfm": "^3.0.1",
|
|
||||||
"remark-math": "^5.1.1",
|
|
||||||
"remark-slug": "^7.0.1",
|
|
||||||
"remark-smartypants": "^2.0.0",
|
|
||||||
"remark-toc": "^8.0.1",
|
|
||||||
"slugify": "^1.6.6",
|
|
||||||
"timeago.js": "^4.0.2",
|
|
||||||
"tslib": "^2.3.0",
|
|
||||||
"vega": "^5.24.0",
|
|
||||||
"xlsx": "^0.18.5"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/preset-react": "^7.14.5",
|
"@babel/preset-react": "^7.14.5",
|
||||||
"@nrwl/cypress": "15.9.2",
|
"@nrwl/cypress": "15.9.2",
|
||||||
@@ -83,7 +28,6 @@
|
|||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
"@typescript-eslint/eslint-plugin": "^5.36.1",
|
||||||
"@typescript-eslint/parser": "^5.36.1",
|
"@typescript-eslint/parser": "^5.36.1",
|
||||||
"autoprefixer": "10.4.13",
|
|
||||||
"babel-jest": "^29.4.1",
|
"babel-jest": "^29.4.1",
|
||||||
"cypress": "^12.2.0",
|
"cypress": "^12.2.0",
|
||||||
"eslint": "~8.15.0",
|
"eslint": "~8.15.0",
|
||||||
@@ -97,11 +41,9 @@
|
|||||||
"jest": "^29.4.1",
|
"jest": "^29.4.1",
|
||||||
"jest-environment-jsdom": "^29.4.1",
|
"jest-environment-jsdom": "^29.4.1",
|
||||||
"nx": "15.9.2",
|
"nx": "15.9.2",
|
||||||
"postcss": "8.4.21",
|
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"react-test-renderer": "18.2.0",
|
"react-test-renderer": "18.2.0",
|
||||||
"swc-loader": "0.1.15",
|
"swc-loader": "0.1.15",
|
||||||
"tailwindcss": "3.2.7",
|
|
||||||
"ts-jest": "^29.0.5",
|
"ts-jest": "^29.0.5",
|
||||||
"ts-node": "10.9.1",
|
"ts-node": "10.9.1",
|
||||||
"typescript": "~4.9.5"
|
"typescript": "~4.9.5"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
This the Portal.JS website.
|
This the PortalJS website.
|
||||||
|
|
||||||
It is built on [Next.js](https://nextjs.org/).
|
It is built on [Next.js](https://nextjs.org/).
|
||||||
|
|
||||||
|
|||||||
26
site/components/ButtonLink.tsx
Normal file
26
site/components/ButtonLink.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function ButtonLink({
|
||||||
|
style = 'primary',
|
||||||
|
className = '',
|
||||||
|
href = '',
|
||||||
|
children,
|
||||||
|
}) {
|
||||||
|
let styleClassName = '';
|
||||||
|
|
||||||
|
if (style == 'primary') {
|
||||||
|
styleClassName = 'text-primary bg-blue-400 hover:bg-blue-300';
|
||||||
|
} else if (style == 'secondary') {
|
||||||
|
styleClassName =
|
||||||
|
'text-secondary border !border-secondary hover:text-primary hover:bg-blue-300';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
className={`inline-block h-12 px-6 py-3 border border-transparent text-base font-medium rounded-md focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300/50 active:bg-sky-500 ${styleClassName} ${className}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
99
site/components/Community.tsx
Normal file
99
site/components/Community.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import Container from './Container';
|
||||||
|
import DiscordIcon from './icons/DiscordIcon';
|
||||||
|
import EmailIcon from './icons/EmailIcon';
|
||||||
|
import GitHubIcon from './icons/GitHubIcon';
|
||||||
|
|
||||||
|
import { siteConfig } from '@/config/siteConfig';
|
||||||
|
import { getContributorsCount, getRepoInfo } from '@/lib/getGitHubData';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const Stat = ({ title, value, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<span className="text-6xl font-bold text-secondary">{value}</span>
|
||||||
|
<p className="text-lg font-medium">{title}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const IconButton = ({ Icon, text, href, ...props }) => {
|
||||||
|
return (
|
||||||
|
<div {...props}>
|
||||||
|
<a
|
||||||
|
className="rounded border border-secondary px-5 py-3 text-primary dark:text-primary-dark flex items-center hover:bg-secondary hover:text-primary dark:hover:text-primary transition-all duration-200"
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
<Icon className="w-6 h-6 mr-2" />
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Community() {
|
||||||
|
const [repoInfo, setRepoInfo] = useState<any>();
|
||||||
|
const [contributorsCount, setContributorsCount] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This runs on client side and it's unlikely that users
|
||||||
|
// will exceed the GitHub API usage limit, but added a
|
||||||
|
// handling for that just in case.
|
||||||
|
|
||||||
|
getRepoInfo().then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
res.info.then((data) => setRepoInfo(data));
|
||||||
|
} else {
|
||||||
|
// If the request fail e.g API usage limit, use
|
||||||
|
// a placeholder
|
||||||
|
setRepoInfo({ stargazers_count: '+2k' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
getContributorsCount().then((res) => {
|
||||||
|
if (res.success) {
|
||||||
|
setContributorsCount(res.count);
|
||||||
|
} else {
|
||||||
|
setContributorsCount('+70');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-3xl font-bold text-primary dark:text-primary-dark ">
|
||||||
|
Community
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg mt-8 ">
|
||||||
|
We are growing. Get in touch or become a contributor!
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center mt-12">
|
||||||
|
<Stat
|
||||||
|
title="Stars on GitHub"
|
||||||
|
value={repoInfo?.stargazers_count}
|
||||||
|
className="mr-10"
|
||||||
|
/>
|
||||||
|
<Stat title="Contributors" value={contributorsCount} />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap justify-center mt-12">
|
||||||
|
<IconButton
|
||||||
|
Icon={GitHubIcon}
|
||||||
|
text="Star PortalJS on GitHub"
|
||||||
|
className="sm:mr-4 mb-4 w-full sm:w-auto"
|
||||||
|
href={siteConfig.github}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
Icon={DiscordIcon}
|
||||||
|
text="Join the Discord server"
|
||||||
|
className="sm:mr-4 mb-4 w-full sm:w-auto"
|
||||||
|
href={siteConfig.discord}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
Icon={EmailIcon}
|
||||||
|
text="Subscribe to the PortalJS newsletter"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
href="#hero"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
site/components/Container.tsx
Normal file
7
site/components/Container.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function Container({ children }) {
|
||||||
|
return (
|
||||||
|
<div className="lg:max-w-8xl mx-auto px-4 lg:px-8 xl:px-12 mb-32">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import Container from './Container';
|
||||||
|
|
||||||
const features: { title: string; description: string; icon: string }[] = [
|
const features: { title: string; description: string; icon: string }[] = [
|
||||||
{
|
{
|
||||||
title: 'Unified sites',
|
title: 'Unified sites',
|
||||||
@@ -37,10 +39,12 @@ const features: { title: string; description: string; icon: string }[] = [
|
|||||||
|
|
||||||
export default function Features() {
|
export default function Features() {
|
||||||
return (
|
return (
|
||||||
<div className="lg:max-w-8xl mx-auto px-4 lg:px-8 xl:px-12">
|
<Container>
|
||||||
<h2 className="text-3xl font-bold">How Portal.JS works?</h2>
|
<h2 className="text-3xl font-bold text-primary dark:text-primary-dark">
|
||||||
|
How PortalJS works?
|
||||||
|
</h2>
|
||||||
<p className="text-lg mt-8">
|
<p className="text-lg mt-8">
|
||||||
Portal.JS is built in JavaScript and React on top of the popular Next.js
|
PortalJS is built in JavaScript and React on top of the popular Next.js
|
||||||
framework, assuming a "decoupled" approach where the frontend is a
|
framework, assuming a "decoupled" approach where the frontend is a
|
||||||
separate service from the backend and interacts with backend(s) via an
|
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
|
API. It can be used with any backend and has out of the box support for
|
||||||
@@ -55,7 +59,7 @@ export default function Features() {
|
|||||||
<div className="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--quick-links-hover-bg,theme(colors.sky.50)),var(--quick-links-hover-bg,theme(colors.sky.50)))_padding-box,linear-gradient(to_top,theme(colors.blue.300),theme(colors.blue.400),theme(colors.blue.500))_border-box] group-hover:opacity-100 dark:[--quick-links-hover-bg:theme(colors.slate.800)]" />
|
<div className="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--quick-links-hover-bg,theme(colors.sky.50)),var(--quick-links-hover-bg,theme(colors.sky.50)))_padding-box,linear-gradient(to_top,theme(colors.blue.300),theme(colors.blue.400),theme(colors.blue.500))_border-box] group-hover:opacity-100 dark:[--quick-links-hover-bg:theme(colors.slate.800)]" />
|
||||||
<div className="relative overflow-hidden rounded-xl p-6">
|
<div className="relative overflow-hidden rounded-xl p-6">
|
||||||
<img src={feature.icon} alt="" className="h-24 w-auto" />
|
<img src={feature.icon} alt="" className="h-24 w-auto" />
|
||||||
<h2 className="mt-4 font-display text-base text-slate-900 dark:text-white">
|
<h2 className="mt-4 font-display text-base text-primary dark:text-primary-dark">
|
||||||
<span className="absolute -inset-px rounded-xl" />
|
<span className="absolute -inset-px rounded-xl" />
|
||||||
{feature.title}
|
{feature.title}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -66,6 +70,6 @@ export default function Features() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
66
site/components/Gallery.tsx
Normal file
66
site/components/Gallery.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import Container from './Container';
|
||||||
|
import GalleryItem from './GalleryItem';
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
title: 'Open Data Northern Ireland',
|
||||||
|
href: 'https://www.opendatani.gov.uk/',
|
||||||
|
image: '/images/showcases/odni.png',
|
||||||
|
description: 'Government Open Data Portal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Birmingham City Observatory',
|
||||||
|
href: 'https://www.cityobservatory.birmingham.gov.uk/',
|
||||||
|
image: '/images/showcases/birmingham.png',
|
||||||
|
description: 'Government Open Data Portal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'UAE Open Data',
|
||||||
|
href: 'https://opendata.fcsc.gov.ae/',
|
||||||
|
image: '/images/showcases/uae.png',
|
||||||
|
description: 'Government Open Data Portal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Brazil Open Data',
|
||||||
|
href: 'https://dados.gov.br/',
|
||||||
|
image: '/images/showcases/brazil.png',
|
||||||
|
description: 'Government Open Data Portal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Datahub Open Data',
|
||||||
|
href: 'https://opendata.datahub.io/',
|
||||||
|
image: '/images/showcases/datahub.png',
|
||||||
|
description: 'Demo Data Portal by DataHub',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Example: Simple Data Catalog',
|
||||||
|
href: 'https://example.portaljs.org/',
|
||||||
|
image: '/images/showcases/example-simple-catalog.png',
|
||||||
|
description: 'Simple data catalog',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Example: Portal with CKAN',
|
||||||
|
href: 'https://ckan-example.portaljs.org/',
|
||||||
|
image: '/images/showcases/example-ckan.png',
|
||||||
|
description: 'Simple portal with data coming from CKAN',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Gallery() {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<h2
|
||||||
|
className="text-3xl font-bold text-primary dark:text-primary-dark"
|
||||||
|
id="gallery"
|
||||||
|
>
|
||||||
|
Gallery
|
||||||
|
</h2>
|
||||||
|
<p className="text-lg mt-8">Discover what's being powered by PortalJS</p>
|
||||||
|
<div className="not-prose my-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{items.map((item) => {
|
||||||
|
return <GalleryItem item={item} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
site/components/GalleryItem.tsx
Normal file
24
site/components/GalleryItem.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export default function GalleryItem({ item }) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className="rounded overflow-hidden group relative border-1 shadow-lg"
|
||||||
|
target="_blank"
|
||||||
|
href={item.href}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-cover bg-no-repeat bg-top aspect-video w-full group-hover:blur-sm group-hover:scale-105 transition-all duration-200"
|
||||||
|
style={{ backgroundImage: `url(${item.image})` }}
|
||||||
|
>
|
||||||
|
<div className="w-full h-full bg-black opacity-0 group-hover:opacity-50 transition-all duration-200"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100 absolute top-0 bottom-0 right-0 left-0 transition-all duration-200 px-2 flex items-center justify-center">
|
||||||
|
<div className="text-center text-primary-dark">
|
||||||
|
<span className="text-xl font-semibold">{item.title}</span>
|
||||||
|
<p className="text-base font-medium">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import Highlight, { defaultProps } from 'prism-react-renderer';
|
import Highlight, { defaultProps } from 'prism-react-renderer';
|
||||||
import { Fragment, useRef } from 'react';
|
import { Fragment, useRef } from 'react';
|
||||||
|
import ButtonLink from './ButtonLink';
|
||||||
|
import NewsletterForm from './NewsletterForm';
|
||||||
|
|
||||||
const codeLanguage = 'javascript';
|
const codeLanguage = 'javascript';
|
||||||
const code = `export default {
|
const code = `export default {
|
||||||
@@ -31,7 +33,10 @@ export function Hero() {
|
|||||||
const el = useRef(null);
|
const el = useRef(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden -mb-32 mt-[-4.5rem] pb-32 pt-[4.5rem] lg:mt-[-4.75rem] lg:pt-[4.75rem]">
|
<div
|
||||||
|
className="overflow-hidden -mb-32 mt-[-4.5rem] pb-32 pt-[4.5rem] lg:mt-[-4.75rem] lg:pt-[4.75rem]"
|
||||||
|
id="hero"
|
||||||
|
>
|
||||||
<div className="py-16 sm:px-2 lg:relative lg:py-20 lg:px-0">
|
<div className="py-16 sm:px-2 lg:relative lg:py-20 lg:px-0">
|
||||||
{/* Commented code on line 37, 39 and 113 will reenable the two columns hero */}
|
{/* Commented code on line 37, 39 and 113 will reenable the two columns hero */}
|
||||||
{/* <div className="mx-auto grid max-w-2xl grid-cols-1 items-center gap-y-16 gap-x-8 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12"> */}
|
{/* <div className="mx-auto grid max-w-2xl grid-cols-1 items-center gap-y-16 gap-x-8 px-4 lg:max-w-8xl lg:grid-cols-2 lg:px-8 xl:gap-x-16 xl:px-12"> */}
|
||||||
@@ -44,55 +49,19 @@ export function Hero() {
|
|||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-4 text-xl tracking-tight text-slate-400">
|
<p className="mt-4 text-xl tracking-tight text-slate-400">
|
||||||
Portal.JS is a framework for rapidly building rich data portal
|
Rapidly build rich data portals using a modern frontend framework.
|
||||||
frontends using a modern frontend approach. It can be used to
|
|
||||||
present a single dataset or build a full-scale data
|
|
||||||
catalog/portal.
|
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-8 sm:mx-auto sm:text-center lg:text-left lg:mx-0">
|
|
||||||
<p className="text-base font-medium text-slate-400 dark:text-slate-400">
|
<ButtonLink className="mt-8" href="/docs">
|
||||||
Sign up to get notified about updates
|
Get started
|
||||||
</p>
|
</ButtonLink>
|
||||||
<form
|
|
||||||
method="POST"
|
<ButtonLink className="ml-3" href="#gallery" style="secondary">
|
||||||
name="get-updates"
|
Gallery
|
||||||
data-netlify="true"
|
</ButtonLink>
|
||||||
action="/subscribed"
|
|
||||||
className="mt-3 sm:flex"
|
<div className="md:max-w-md mx-auto">
|
||||||
>
|
<NewsletterForm />
|
||||||
<label htmlFor="name" className="sr-only">
|
|
||||||
Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
placeholder="Your name"
|
|
||||||
className="block w-full sm:flex-auto sm:w-32 px-2 py-3 text-base rounded-md bg-slate-200 dark:bg-slate-800 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-400 focus:ring-offset-gray-900"
|
|
||||||
/>
|
|
||||||
<label htmlFor="email" className="sr-only">
|
|
||||||
Email address
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
placeholder="Your email"
|
|
||||||
className="block w-full mt-3 sm:flex-auto sm:w-64 sm:mt-0 sm:ml-3 px-2 py-3 text-base rounded-md bg-slate-200 dark:bg-slate-800 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-400 focus:ring-offset-gray-900"
|
|
||||||
/>
|
|
||||||
<input type="hidden" name="form-name" value="get-updates" />
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="flex-none mt-3 px-6 py-3 border border-transparent text-base font-medium rounded-md text-slate-900 bg-blue-400 hover:bg-blue-300 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300/50 active:bg-sky-500 sm:mt-0 sm:ml-3"
|
|
||||||
>
|
|
||||||
Notify me
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
{/* <p className="mt-3 text-sm text-slate-400 dark:text-slate-300 sm:mt-4">
|
|
||||||
We are actively trialling and developing Flowershow. If you'd
|
|
||||||
like to get notified about our progress and important updates,
|
|
||||||
please sign up.
|
|
||||||
</p> */}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="my-10 text-l tracking-wide">
|
<p className="my-10 text-l tracking-wide">
|
||||||
<span>A project of</span>
|
<span>A project of</span>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { siteConfig } from '@/config/siteConfig';
|
import { siteConfig } from '@/config/siteConfig';
|
||||||
import { NextSeo } from 'next-seo';
|
import { NextSeo } from 'next-seo';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ export default function Layout({
|
|||||||
tableOfContents?;
|
tableOfContents?;
|
||||||
}) {
|
}) {
|
||||||
// const { toc } = children.props;
|
// const { toc } = children.props;
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
const currentSection = useTableOfContents(tableOfContents);
|
const currentSection = useTableOfContents(tableOfContents);
|
||||||
|
|
||||||
@@ -87,17 +89,21 @@ export default function Layout({
|
|||||||
>
|
>
|
||||||
Built by{' '}
|
Built by{' '}
|
||||||
<img
|
<img
|
||||||
src="/datopian-logo.png"
|
src={
|
||||||
|
theme === 'dark'
|
||||||
|
? '/images/datopian-light-logotype.svg'
|
||||||
|
: '/images/datopian-dark-logotype.svg'
|
||||||
|
}
|
||||||
alt="Datopian Logo"
|
alt="Datopian Logo"
|
||||||
className="h-6 ml-2"
|
className="h-6 ml-2"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
</footer>
|
||||||
{/** TABLE OF CONTENTS */}
|
{/** TABLE OF CONTENTS */}
|
||||||
{tableOfContents.length > 0 && (siteConfig.tableOfContents) && (
|
{tableOfContents.length > 0 && siteConfig.tableOfContents && (
|
||||||
<div className="hidden xl:fixed xl:right-0 xl:top-[4.5rem] xl:block xl:w-1/5 xl:h-[calc(100vh-4.5rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6 xl:mb-16">
|
<div className="hidden xl:fixed xl:right-0 xl:top-[4.5rem] xl:block xl:w-1/5 xl:h-[calc(100vh-4.5rem)] xl:flex-none xl:overflow-y-auto xl:py-16 xl:pr-6 xl:mb-16">
|
||||||
<nav aria-labelledby="on-this-page-title" className="w-56">
|
<nav aria-labelledby="on-this-page-title" className="w-56">
|
||||||
<h2 className="font-display text-md font-medium text-slate-900 dark:text-white">
|
<h2 className="font-display text-md font-medium text-primary dark:text-primary-dark">
|
||||||
On this page
|
On this page
|
||||||
</h2>
|
</h2>
|
||||||
<ol className="mt-4 space-y-3 text-sm">
|
<ol className="mt-4 space-y-3 text-sm">
|
||||||
@@ -108,7 +114,7 @@ export default function Layout({
|
|||||||
href={`#${section.id}`}
|
href={`#${section.id}`}
|
||||||
className={
|
className={
|
||||||
isActive(section)
|
isActive(section)
|
||||||
? 'text-sky-500'
|
? 'text-secondary'
|
||||||
: 'font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
|
: 'font-normal text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export default function MDXPage({ source, frontMatter }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="prose mx-auto prose-a:text-primary dark:prose-a:text-primary-dark prose-strong:text-primary dark:prose-strong:text-primary-dark prose-code:text-primary dark:prose-code:text-primary-dark prose-headings:text-primary dark:prose-headings:text-primary-dark prose text-primary dark:text-primary-dark prose-headings:font-headings dark:prose-invert prose-a:break-words">
|
<div className="prose mx-auto prose-a:text-primary dark:prose-a:text-primary-dark prose-strong:text-primary dark:prose-strong:text-primary-dark prose-headings:text-primary dark:prose-headings:text-primary-dark text-primary dark:text-primary-dark prose-headings:font-headings dark:prose-invert prose-a:break-words">
|
||||||
<header>
|
<header>
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
{/* Default layout */}
|
{/* Default layout */}
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ export default function NavItem({ item }) {
|
|||||||
{Object.prototype.hasOwnProperty.call(item, "href") ? (
|
{Object.prototype.hasOwnProperty.call(item, "href") ? (
|
||||||
<Link
|
<Link
|
||||||
href={item.href}
|
href={item.href}
|
||||||
className="text-slate-500 inline-flex items-center mr-2 px-1 pt-1 text-sm font-medium hover:text-slate-600"
|
className="text-slate-600 dark:text-slate-400 inline-flex items-center mr-2 px-1 pt-1 text-sm font-medium hover:text-slate-500"
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-slate-500 inline-flex items-center mr-2 px-1 pt-1 text-sm font-medium hover:text-slate-600 fill-slate-500 hover:fill-slate-600">
|
<div className="text-slate-600 dark:text-slate-400 inline-flex items-center mr-2 px-1 pt-1 text-sm font-medium hover:text-slate-500 fill-slate-500 hover:fill-slate-600">
|
||||||
{item.name}
|
{item.name}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
127
site/components/NewsletterForm.tsx
Normal file
127
site/components/NewsletterForm.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import Script from 'next/script';
|
||||||
|
|
||||||
|
export default function NewsletterForm() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://sibforms.com/forms/end-form/build/sib-styles.css"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
id="sib-form-container"
|
||||||
|
className="mt-8 sm:mx-auto sm:text-center lg:text-left lg:mx-0"
|
||||||
|
>
|
||||||
|
<p className="text-base font-medium text-slate-400 dark:text-slate-400">
|
||||||
|
Sign up to get notified about updates
|
||||||
|
</p>
|
||||||
|
<div id="sib-container" className="!bg-transparent !p-0 !pb-5">
|
||||||
|
<form
|
||||||
|
id="sib-form"
|
||||||
|
method="POST"
|
||||||
|
action="https://0613d040.sibforms.com/serve/MUIEAGd31Nknuf7_fodoNJ3t0B71KWpbFfnzgk_VewvONuLQG8JO3qOotew23kQT3HpoJQUG_vzcjOTjn29B6GpXxPbSml_XWwHgG2mWq-jhrjfqpHCcPoOY_ge-rN2vDFWYZ80l242DTYGDRRWtTusdAYIk2oyf-nhJyOqQrUzTnXlAlKc7SxWgynSQ1GHr3jU5s57h6986IoK4"
|
||||||
|
data-type="subscription"
|
||||||
|
className="mt-3 sm:flex"
|
||||||
|
>
|
||||||
|
<div className="sib-input sib-form-block !p-0 block w-full sm:flex-auto sm:w-64 mt-3 sm:mt-0">
|
||||||
|
<div className="form__entry entry_block w-full">
|
||||||
|
<label htmlFor="email" className="sr-only entry__label">
|
||||||
|
Email address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="EMAIL"
|
||||||
|
name="EMAIL"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
placeholder="Your email"
|
||||||
|
className="input entry__field !w-full px-2 py-3 text-base rounded-md bg-slate-200 dark:bg-slate-800 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-400 focus:ring-offset-gray-900"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<label className="entry__error entry__error--primary px-2 text-red-400 text-sm"></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="form-name" value="get-updates" />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="sib-form-block__button sib-form-block__button-with-loader h-12 flex-none mt-3 px-6 py-3 border border-transparent text-base font-medium rounded-md text-slate-900 bg-blue-400 hover:bg-blue-300 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-300/50 active:bg-sky-500 sm:mt-0 sm:ml-3"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="icon clickable__icon progress-indicator__icon sib-hide-loader-icon hidden"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
>
|
||||||
|
<path d="M460.116 373.846l-20.823-12.022c-5.541-3.199-7.54-10.159-4.663-15.874 30.137-59.886 28.343-131.652-5.386-189.946-33.641-58.394-94.896-95.833-161.827-99.676C261.028 55.961 256 50.751 256 44.352V20.309c0-6.904 5.808-12.337 12.703-11.982 83.556 4.306 160.163 50.864 202.11 123.677 42.063 72.696 44.079 162.316 6.031 236.832-3.14 6.148-10.75 8.461-16.728 5.01z" />
|
||||||
|
</svg>
|
||||||
|
Notify Me
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="email_address_check"
|
||||||
|
value=""
|
||||||
|
className="input--hidden"
|
||||||
|
/>
|
||||||
|
<input type="hidden" name="locale" value="en" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="error-message" className="sib-form-message-panel !border-none">
|
||||||
|
<div className="sib-form-message-panel__text sib-form-message-panel__text--center !text-red-400 justify-center">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
className="sib-icon sib-notification__icon"
|
||||||
|
>
|
||||||
|
<path d="M256 40c118.621 0 216 96.075 216 216 0 119.291-96.61 216-216 216-119.244 0-216-96.562-216-216 0-119.203 96.602-216 216-216m0-32C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm-11.49 120h22.979c6.823 0 12.274 5.682 11.99 12.5l-7 168c-.268 6.428-5.556 11.5-11.99 11.5h-8.979c-6.433 0-11.722-5.073-11.99-11.5l-7-168c-.283-6.818 5.167-12.5 11.99-12.5zM256 340c-15.464 0-28 12.536-28 28s12.536 28 28 28 28-12.536 28-28-12.536-28-28-28z" />
|
||||||
|
</svg>
|
||||||
|
<span className="sib-form-message-panel__inner-text !text-md">
|
||||||
|
Your subscription could not be saved. Please try again.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
id="success-message"
|
||||||
|
className="sib-form-message-panel !border-none"
|
||||||
|
>
|
||||||
|
<div className="sib-form-message-panel__text sib-form-message-panel__text--center text-green-400 justify-center">
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
className="sib-icon sib-notification__icon"
|
||||||
|
>
|
||||||
|
<path d="M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 464c-118.664 0-216-96.055-216-216 0-118.663 96.055-216 216-216 118.664 0 216 96.055 216 216 0 118.663-96.055 216-216 216zm141.63-274.961L217.15 376.071c-4.705 4.667-12.303 4.637-16.97-.068l-85.878-86.572c-4.667-4.705-4.637-12.303.068-16.97l8.52-8.451c4.705-4.667 12.303-4.637 16.97.068l68.976 69.533 163.441-162.13c4.705-4.667 12.303-4.637 16.97.068l8.451 8.52c4.668 4.705 4.637 12.303-.068 16.97z" />
|
||||||
|
</svg>
|
||||||
|
<span className="sib-form-message-panel__inner-text !text-md !text-green-500">
|
||||||
|
Your subscription has been successful.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Script
|
||||||
|
id="newsletter-form-validation-message"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
window.REQUIRED_CODE_ERROR_MESSAGE = 'Please choose a country code';
|
||||||
|
window.LOCALE = 'en';
|
||||||
|
window.EMAIL_INVALID_MESSAGE = window.SMS_INVALID_MESSAGE = "The information provided is invalid. Please review the field format and try again.";
|
||||||
|
|
||||||
|
window.REQUIRED_ERROR_MESSAGE = "This field cannot be left blank. ";
|
||||||
|
|
||||||
|
window.GENERIC_INVALID_MESSAGE = "The information provided is invalid. Please review the field format and try again.";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.translation = {
|
||||||
|
common: {
|
||||||
|
selectedList: '{quantity} list selected',
|
||||||
|
selectedLists: '{quantity} lists selected'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var AUTOHIDE = Boolean(0);
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="newsletter-submit-form"
|
||||||
|
src="https://sibforms.com/forms/end-form/build/main.js"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
site/components/icons/DiscordIcon.tsx
Normal file
14
site/components/icons/DiscordIcon.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default function DiscordIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308-.42.582-.863.818-1.329a.05.05 0 0 0-.01-.059.051.051 0 0 0-.018-.011 8.875 8.875 0 0 1-1.248-.595.05.05 0 0 1-.02-.066.051.051 0 0 1 .015-.019c.084-.063.168-.129.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.052.052 0 0 1 .053.007c.08.066.164.132.248.195a.051.051 0 0 1-.004.085 8.254 8.254 0 0 1-1.249.594.05.05 0 0 0-.03.03.052.052 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.235 13.235 0 0 0 4.001-2.02.049.049 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.034.034 0 0 0-.02-.019Zm-8.198 7.307c-.789 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612Zm5.316 0c-.788 0-1.438-.724-1.438-1.612 0-.889.637-1.613 1.438-1.613.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
site/components/icons/EmailIcon.tsx
Normal file
14
site/components/icons/EmailIcon.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
export default function EmailIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 2150 2150"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M1920 428.266v1189.54l-464.16-580.146-88.203 70.585 468.679 585.904H83.684l468.679-585.904-88.202-70.585L0 1617.805V428.265l959.944 832.441L1920 428.266ZM1919.932 226v52.627l-959.943 832.44L.045 278.628V226h1919.887Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
site/components/icons/GitHubIcon.tsx
Normal file
7
site/components/icons/GitHubIcon.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default function GitHubIcon(props) {
|
||||||
|
return (
|
||||||
|
<svg aria-hidden="true" viewBox="0 0 16 16" fill="currentColor" {...props}>
|
||||||
|
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
site/content/assets/examples/frictionless-dataset-demo.gif
Normal file
BIN
site/content/assets/examples/frictionless-dataset-demo.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.6 MiB |
@@ -1,14 +1,20 @@
|
|||||||
Live DEMOs:
|
---
|
||||||
|
title: "PortalJS example 1: Create a full-featured custom data portal frontend for CKAN with PortalJS"
|
||||||
|
authors: ['Luccas Mateus']
|
||||||
|
date: 2021-04-20
|
||||||
|
---
|
||||||
|
|
||||||
- https://catalog-portal-js.vercel.app
|
We have created a full data portal demo using PortalJS all backed by a CKAN instance storing data and metadata, you can see below a screenshot of the homepage and of an individual dataset page.
|
||||||
- https://ckan-enterprise-frontend.vercel.app/
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
## Create a Portal app for CKAN
|
## Create a Portal app for CKAN
|
||||||
|
|
||||||
To create a Portal app, run the following command in your terminal:
|
To create a Portal app, run the following command in your terminal:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
npx create-next-app -e https://github.com/datopian/portal.js/tree/main/examples/ckan
|
npx create-next-app -e https://github.com/datopian/portaljs/tree/main/examples/ckan
|
||||||
```
|
```
|
||||||
|
|
||||||
> NB: Under the hood, this uses the tool called create-next-app, which bootstraps an app for you based on our CKAN example.
|
> NB: Under the hood, this uses the tool called create-next-app, which bootstraps an app for you based on our CKAN example.
|
||||||
@@ -69,7 +75,7 @@ For development/debugging purposes, we suggest installing the Chrome extension -
|
|||||||
|
|
||||||
### I18n configuration
|
### 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;
|
PortalJS 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
|
1. Update `next.config.js`, to add more languages to the i18n locales
|
||||||
|
|
||||||
27
site/content/blog/example-ckan.md
Normal file
27
site/content/blog/example-ckan.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
title: "Example: data catalog with data coming from CKAN"
|
||||||
|
authors: ['Luccas Mateus']
|
||||||
|
date: 2023-04-20
|
||||||
|
---
|
||||||
|
|
||||||
|
PortalJS is an open source project that aims to simplify the creation of web-based data portals, making it easy for users to create and share data-driven applications.
|
||||||
|
|
||||||
|
The ckan-example added to PortalJS is intended to provide users with an easy way to set up a data catalog that can be used to display and share data stores behind a CKAN Backend. With this example, users can quickly set up a web-based portal that allows them to showcase their data and make it accessible to others, all this being done just by adding a simple env variable pointing to a CKAN Deployment.
|
||||||
|
|
||||||
|
To get a feel of the project, users can check the [live deployment](https://ckan-example.portaljs.org).
|
||||||
|
|
||||||
|
Below are some screenshots:
|
||||||
|
|
||||||
|
### Front page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Individual dataset page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Documentation](/docs/example-ckan)
|
||||||
|
- [Repo](https://github.com/datopian/portaljs/tree/main/examples/ckan-example)
|
||||||
|
- [Live Demo](https://ckan-example.portaljs.org)
|
||||||
28
site/content/blog/example-data-catalog.md
Normal file
28
site/content/blog/example-data-catalog.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
title: "Example: simple data catalog"
|
||||||
|
authors: ['Luccas Mateus']
|
||||||
|
date: 2023-04-20
|
||||||
|
---
|
||||||
|
|
||||||
|
PortalJS is an open source project that aims to simplify the creation of web-based data portals, making it easy for users to create and share data-driven applications.
|
||||||
|
|
||||||
|
The simple-example added to PortalJS is intended to provide users with an easy way to set up a data catalog that can be used to display and share data stores stored in GitHub repositories. With this example, users can quickly set up a web-based portal that allows them to showcase their data and make it accessible to others, all this being done thru the configuration of a simple `datasets.json` file.
|
||||||
|
|
||||||
|
To get a feel of the project, users can check the [live deployment](https://example.portaljs.org).
|
||||||
|
|
||||||
|
Below are some screenshots:
|
||||||
|
|
||||||
|
### Front page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Individual dataset page
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Documentation](/docs/example-data-catalog)
|
||||||
|
- [Repo](https://github.com/datopian/portaljs/tree/main/examples/simple-example)
|
||||||
|
- [Live Demo](https://example.portaljs.org)
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
const config = {
|
const config = {
|
||||||
title:
|
title:
|
||||||
"Portal.JS",
|
"PortalJS",
|
||||||
description:
|
description:
|
||||||
"Portal.JS is a framework for rapidly building rich data portal frontends using a modern frontend approach. portal.js can be used to present a single dataset or build a full-scale data catalog/portal.",
|
"PortalJS is a framework for rapidly building rich data portal frontends using a modern frontend approach. PortalJS can be used to present a single dataset or build a full-scale data catalog/portal.",
|
||||||
theme: {
|
theme: {
|
||||||
default: "dark",
|
default: "dark",
|
||||||
toggleIcon: "/images/theme-button.svg",
|
toggleIcon: "/images/theme-button.svg",
|
||||||
@@ -12,34 +12,33 @@ const config = {
|
|||||||
authorUrl: "https://datopian.com/",
|
authorUrl: "https://datopian.com/",
|
||||||
navbarTitle: {
|
navbarTitle: {
|
||||||
// logo: "/images/logo.svg",
|
// logo: "/images/logo.svg",
|
||||||
text: "🌀 Portal.JS",
|
text: "🌀 PortalJS",
|
||||||
// version: "Alpha",
|
// version: "Alpha",
|
||||||
},
|
},
|
||||||
navLinks: [
|
navLinks: [
|
||||||
{ name: "Docs", href: "/docs" },
|
{ name: "Docs", href: "/docs" },
|
||||||
{ name: "Components", href: "/docs/components" },
|
// { name: "Components", href: "/docs/components" },
|
||||||
{ name: "Learn", href: "/learn" },
|
{ name: "Blog", href: "/blog" },
|
||||||
{ name: "Gallery", href: "/gallery" },
|
// { name: "Gallery", href: "/gallery" },
|
||||||
{ name: "Data Literate", href: "/data-literate" },
|
// { name: "Data Literate", href: "/data-literate" },
|
||||||
{ name: "DL Demo", href: "/data-literate/demo" },
|
// { name: "DL Demo", href: "/data-literate/demo" },
|
||||||
{ name: "Excel Viewer", href: "/excel-viewer" },
|
// { name: "Excel Viewer", href: "/excel-viewer" },
|
||||||
{ name: "GitHub", href: "https://github.com/datopian/portal.js" },
|
|
||||||
],
|
],
|
||||||
footerLinks: [],
|
footerLinks: [],
|
||||||
nextSeo: {
|
nextSeo: {
|
||||||
openGraph: {
|
openGraph: {
|
||||||
type: "website",
|
type: "website",
|
||||||
title:
|
title:
|
||||||
"Portal.JS - Rapidly build rich data portals using a modern frontend framework",
|
"PortalJS - rapidly build rich data portals using a modern frontend framework.",
|
||||||
description:
|
description:
|
||||||
"Portal.JS is a framework for rapidly building rich data portal frontends using a modern frontend approach. portal.js can be used to present a single dataset or build a full-scale data catalog/portal.",
|
"PortalJS is a framework for rapidly building rich data portal frontends using a modern frontend approach. PortalJS can be used to present a single dataset or build a full-scale data catalog and portal.",
|
||||||
locale: "en_US",
|
locale: "en_US",
|
||||||
images: [
|
images: [
|
||||||
{
|
{
|
||||||
url: "https://datahub.io/static/img/opendata/product.png", // TODO
|
url: "/homepage-screenshot.png", // TODO
|
||||||
alt: "Portal.JS - Rapidly build rich data portals using a modern frontend framework",
|
alt: "PortalJS - rapidly build rich data portals using a modern frontend framework.",
|
||||||
width: 1200,
|
width: 1280,
|
||||||
height: 627,
|
height: 720,
|
||||||
type: "image/jpg",
|
type: "image/jpg",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ You can see the raw source of this page here: https://raw.githubusercontent.com/
|
|||||||
We can have github-flavored markdown including markdown tables, auto-linked links and checklists:
|
We can have github-flavored markdown including markdown tables, auto-linked links and checklists:
|
||||||
|
|
||||||
```
|
```
|
||||||
https://github.com/datopian/portal.js
|
https://github.com/datopian/portaljs
|
||||||
|
|
||||||
| a | b |
|
| a | b |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -23,7 +23,7 @@ https://github.com/datopian/portal.js
|
|||||||
* [ ] a second thing to do
|
* [ ] a second thing to do
|
||||||
```
|
```
|
||||||
|
|
||||||
https://github.com/datopian/portal.js
|
https://github.com/datopian/portaljs
|
||||||
|
|
||||||
| a | b |
|
| a | b |
|
||||||
|---|---|
|
|---|---|
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
# 🌀 Portal.JS: The JavaScript framework for data portals
|
|
||||||
|
|
||||||
🌀 `portal.js` is a framework for rapidly building rich data portal frontends using a modern frontend approach. `portal.js` can be used to present a single dataset or build a full-scale data catalog/portal.
|
|
||||||
|
|
||||||
`portal.js` is built in Javascript and React on top of the popular [Next.js](https://nextjs.com/) framework. `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](https://ckan.org/).
|
|
||||||
|
|
||||||
## 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
|
|
||||||
|
|
||||||
## Installation and setup
|
|
||||||
|
|
||||||
Before installation, ensure your system satisfies the following requirements:
|
|
||||||
|
|
||||||
- Node.js 10.13 or later
|
|
||||||
- Nextjs 10.0.3
|
|
||||||
- MacOS, Windows (including WSL), and Linux are supported
|
|
||||||
|
|
||||||
> Note: We also recommend instead of npm using `yarn` instead of `npm`.
|
|
||||||
>
|
|
||||||
Portal.js is built with React on top of Nextjs framework, so for a quick setup, you can bootstrap a Nextjs app and install portal.js as demonstrated in the code below:
|
|
||||||
|
|
||||||
```bash=
|
|
||||||
## Create a react app
|
|
||||||
npx create-next-app
|
|
||||||
# or
|
|
||||||
yarn create next-app
|
|
||||||
```
|
|
||||||
After the installation is complete, follow the instructions to start the development server. Try editing pages/index.js and see the result on your browser.
|
|
||||||
|
|
||||||
> For more information on how to use create-next-app, you can review the [create-next-app](https://nextjs.org/docs/api-reference/create-next-app) documentation.
|
|
||||||
|
|
||||||
Once you have Nextjs created, you can install portal.js:
|
|
||||||
|
|
||||||
```bash=
|
|
||||||
yarn add https://github.com/datopian/portal.js.git
|
|
||||||
```
|
|
||||||
|
|
||||||
You're now ready to use portal.js in your next app. To test portal.js, open your `index.js` file in the pages folder. By default you should have some autogenerated code in the `index.js` file:
|
|
||||||
|
|
||||||
|
|
||||||
Which outputs a page with the following content:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Now, we are going to do some clean up and add a table component. In the `index.js` file, import a [Table]() component from portal as shown below:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import Head from 'next/head'
|
|
||||||
import { Table } from 'portal' //import Table component
|
|
||||||
import styles from '../styles/Home.module.css'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ field: 'id', headerName: 'ID' },
|
|
||||||
{ field: 'firstName', headerName: 'First name' },
|
|
||||||
{ field: 'lastName', headerName: 'Last name' },
|
|
||||||
{ field: 'age', headerName: 'Age' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const rows = [
|
|
||||||
{ 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles.container}>
|
|
||||||
<Head>
|
|
||||||
<title>Create Portal App</title>
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
|
|
||||||
<h1 className={styles.title}>
|
|
||||||
Welcome to <a href="https://nextjs.org">Portal.JS</a>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
{/* Use table component */}
|
|
||||||
<Table data={rows} columns={columns} />
|
|
||||||
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Now, your page should look like the following:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
> **Note**: You can learn more about individual portal components, as well as their prop types in the [components reference](/docs/components).
|
|
||||||
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
You can check out the following examples built with Portal.js.
|
|
||||||
|
|
||||||
* [A portal for a single Frictionless dataset](/learn/ckan)
|
|
||||||
* [A portal with a CKAN backend](/learn/single-frictionless-dataset)
|
|
||||||
|
|
||||||
> The [`examples` directory](https://github.com/datopian/portal.js/tree/main/examples) is regularly updated with different portal examples.
|
|
||||||
|
|
||||||
You can also look at the full list of the available components that are provided by Portal.JS in [Components](/docs/components).
|
|
||||||
|
|
||||||
|
|
||||||
## Reference Information
|
|
||||||
|
|
||||||
* [Full list of the available components that are provided by Portal.JS](/docs/components)
|
|
||||||
* [Reference](/docs/references)
|
|
||||||
|
|
||||||
|
|
||||||
## Getting Help
|
|
||||||
|
|
||||||
If you have questions about anything related to Portal.js, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portal.js/discussions).
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,589 +0,0 @@
|
|||||||
# Components Reference
|
|
||||||
|
|
||||||
Portal.js supports many components that can help you build amazing data portals similar to [this](https://catalog-portal-js.vercel.app/) and [this](https://portal-js.vercel.app/).
|
|
||||||
|
|
||||||
In this section, we'll cover all supported components in depth, and help you understand their use as well as the expected properties.
|
|
||||||
|
|
||||||
Components are grouped under the following sections:
|
|
||||||
* [UI](https://github.com/datopian/portal.js/tree/main/src/components/ui): Components like Nav bar, Footer, e.t.c
|
|
||||||
* [Dataset](https://github.com/datopian/portal.js/tree/main/src/components/dataset): Components used for displaying a Frictionless dataset and resources
|
|
||||||
* [Search](https://github.com/datopian/portal.js/tree/main/src/components/search): Components used for building a search interface for datasets
|
|
||||||
* [Blog](https://github.com/datopian/portal.js/tree/main/src/components/blog): Components for building a simple blog for datasets
|
|
||||||
* [Views](https://github.com/datopian/portal.js/tree/main/src/components/views): Components like charts, tables, maps for generating data views
|
|
||||||
* [Misc](https://github.com/datopian/portal.js/tree/main/src/components/misc): Miscellaneos components like errors, custom links, etc used for extra design.
|
|
||||||
|
|
||||||
### UI Components
|
|
||||||
|
|
||||||
In the UI we group all components that can be used for building generic page sections. These are components for building sections like the Navigation bar, Footer, Side pane, Recent datasets, e.t.c.
|
|
||||||
|
|
||||||
#### [Nav Component](https://github.com/datopian/portal.js/blob/main/src/components/ui/Nav.js)
|
|
||||||
|
|
||||||
To build a navigation bar, you can use the `Nav` component as demonstrated below:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Nav } from 'portal'
|
|
||||||
|
|
||||||
export default function Home(){
|
|
||||||
|
|
||||||
const navMenu = [{ title: 'Blog', path: '/blog' },
|
|
||||||
{ title: 'Search', path: '/search' }]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Nav logo="/images/logo.png" navMenu={navMenu}/>
|
|
||||||
...
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Nav Component Prop Types
|
|
||||||
|
|
||||||
Nav component accepts two properties:
|
|
||||||
* **logo**: A string to an image path. Can be relative or absolute.
|
|
||||||
* **navMenu**: An array of objects with title and path. E.g : {"[{ title: 'Blog', path: '/blog' },{ title: 'Search', path: '/search' }]"}
|
|
||||||
|
|
||||||
|
|
||||||
#### [Recent Component](https://github.com/datopian/portal.js/blob/main/src/components/ui/Recent.js)
|
|
||||||
|
|
||||||
The `Recent` component is used to display a list of recent [datasets](#Dataset) in the home page. This useful if you want to display the most recent dataset users have interacted with in your home page.
|
|
||||||
To build a recent dataset section, you can use the `Recent` component as demonstrated below:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Recent } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const datasets = [
|
|
||||||
{
|
|
||||||
organization: {
|
|
||||||
name: "Org1",
|
|
||||||
title: "This is the first org",
|
|
||||||
description: "A description of the organization 1"
|
|
||||||
},
|
|
||||||
title: "Data package title",
|
|
||||||
name: "dataset1",
|
|
||||||
description: "description of data package",
|
|
||||||
resources: [],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
organization: {
|
|
||||||
name: "Org2",
|
|
||||||
title: "This is the second org",
|
|
||||||
description: "A description of the organization 2"
|
|
||||||
},
|
|
||||||
title: "Data package title",
|
|
||||||
name: "dataset2",
|
|
||||||
description: "description of data package",
|
|
||||||
resources: [],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Use Recent component */}
|
|
||||||
<Recent datasets={datasets} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: The `Recent` component is hyperlinked with the dataset name of the organization and the dataset name in the following format:
|
|
||||||
|
|
||||||
> `/@<org name>/<dataset name>`
|
|
||||||
|
|
||||||
For instance, using the example dataset above, the first component will be link to page:
|
|
||||||
|
|
||||||
> `/@org1/dataset1`
|
|
||||||
|
|
||||||
and the second will be linked to:
|
|
||||||
|
|
||||||
> `/@org2/dataset2`
|
|
||||||
|
|
||||||
This is useful to know when generating dynamic pages for each dataset.
|
|
||||||
|
|
||||||
#### Recent Component Prop Types
|
|
||||||
|
|
||||||
The `Recent` component accepts the following properties:
|
|
||||||
* **datasets**: An array of [datasets](#Dataset)
|
|
||||||
|
|
||||||
### Dataset Components
|
|
||||||
|
|
||||||
The dataset component groups together components that can be used for building a dataset UI. These includes components for displaying info about a dataset, resources in a dataset as well as dataset ReadMe.
|
|
||||||
|
|
||||||
#### [KeyInfo Component](https://github.com/datopian/portal.js/blob/main/src/components/dataset/KeyInfo.js)
|
|
||||||
|
|
||||||
The `KeyInfo` components displays key properties like the number of resources, size, format, licences of in a dataset in tabular form. See example in the `Key Info` section [here](https://portal-js.vercel.app/). To use it, you can import the `KeyInfo` component as demonstrated below:
|
|
||||||
```javascript
|
|
||||||
import { KeyInfo } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const datapackage = {
|
|
||||||
"name": "finance-vix",
|
|
||||||
"title": "VIX - CBOE Volatility Index",
|
|
||||||
"homepage": "http://www.cboe.com/micro/VIX/",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"license": "PDDL-1.0",
|
|
||||||
"sources": [
|
|
||||||
{
|
|
||||||
"title": "CBOE VIX Page",
|
|
||||||
"name": "CBOE VIX Page",
|
|
||||||
"web": "http://www.cboe.com/micro/vix/historical.aspx"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"resources": [
|
|
||||||
{
|
|
||||||
"name": "vix-daily",
|
|
||||||
"path": "vix-daily.csv",
|
|
||||||
"format": "csv",
|
|
||||||
"size": 20982,
|
|
||||||
"mediatype": "text/csv",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Use KeyInfo component */}
|
|
||||||
<KeyInfo descriptor={datapackage} resources={datapackage.resources} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### KeyInfo Component Prop Types
|
|
||||||
|
|
||||||
KeyInfo component accepts two properties:
|
|
||||||
* **descriptor**: A [Frictionless data package descriptor](https://specs.frictionlessdata.io/data-package/#descriptor)
|
|
||||||
* **resources**: An [Frictionless data package resource](https://specs.frictionlessdata.io/data-resource/#introduction)
|
|
||||||
|
|
||||||
|
|
||||||
#### [ResourceInfo Component](https://github.com/datopian/portal.js/blob/main/src/components/dataset/ResourceInfo.js)
|
|
||||||
|
|
||||||
The `ResourceInfo` components displays key properties like the name, size, format, modification dates, as well as a download link in a resource object. See an example of a `ResourceInfo` component in the `Data Files` section [here](https://portal-js.vercel.app/).
|
|
||||||
|
|
||||||
You can import and use the`ResourceInfo` component as demonstrated below:
|
|
||||||
```javascript
|
|
||||||
import { ResourceInfo } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const resources = [
|
|
||||||
{
|
|
||||||
"name": "vix-daily",
|
|
||||||
"path": "vix-daily.csv",
|
|
||||||
"format": "csv",
|
|
||||||
"size": 20982,
|
|
||||||
"mediatype": "text/csv",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "vix-daily 2",
|
|
||||||
"path": "vix-daily2.csv",
|
|
||||||
"format": "csv",
|
|
||||||
"size": 2082,
|
|
||||||
"mediatype": "text/csv",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
{/* Use Recent component */}
|
|
||||||
<ResourceInfo resources={resources} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ResourceInfo Component Prop Types
|
|
||||||
|
|
||||||
ResourceInfo component accepts a single property:
|
|
||||||
* **resources**: An [Frictionless data package resource](https://specs.frictionlessdata.io/data-resource/#introduction)
|
|
||||||
|
|
||||||
|
|
||||||
#### [ReadMe Component](https://github.com/datopian/portal.js/blob/main/src/components/dataset/Readme.js)
|
|
||||||
|
|
||||||
The `ReadMe` component is used for displaying a compiled dataset Readme in a readable format. See example in the `README` section [here](https://portal-js.vercel.app/).
|
|
||||||
|
|
||||||
> Note: By compiled ReadMe, we mean ReadMe that has been converted to plain string using a package like [remark](https://www.npmjs.com/package/remark).
|
|
||||||
|
|
||||||
You can import and use the`ReadMe` component as demonstrated below:
|
|
||||||
```javascript
|
|
||||||
import { ReadMe } from 'portal'
|
|
||||||
import remark from 'remark'
|
|
||||||
import html from 'remark-html'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
|
|
||||||
|
|
||||||
const readMeMarkdown = `
|
|
||||||
CBOE Volatility Index (VIX) time-series dataset including daily open, close,
|
|
||||||
high and low. The CBOE Volatility Index (VIX) is a key measure of market
|
|
||||||
expectations of near-term volatility conveyed by S&P 500 stock index option
|
|
||||||
prices introduced in 1993.
|
|
||||||
|
|
||||||
## Data
|
|
||||||
|
|
||||||
From the [VIX FAQ][faq]:
|
|
||||||
|
|
||||||
> In 1993, the Chicago Board Options Exchange® (CBOE®) introduced the CBOE
|
|
||||||
> Volatility Index®, VIX®, and it quickly became the benchmark for stock market
|
|
||||||
> volatility. It is widely followed and has been cited in hundreds of news
|
|
||||||
> articles in the Wall Street Journal, Barron's and other leading financial
|
|
||||||
> publications. Since volatility often signifies financial turmoil, VIX is
|
|
||||||
> often referred to as the "investor fear gauge".
|
|
||||||
|
|
||||||
[faq]: http://www.cboe.com/micro/vix/faq.aspx
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
No obvious statement on [historical data page][historical]. Given size and
|
|
||||||
factual nature of the data and its source from a US company would imagine this
|
|
||||||
was public domain and as such have licensed the Data Package under the Public
|
|
||||||
Domain Dedication and License (PDDL).
|
|
||||||
|
|
||||||
[historical]: http://www.cboe.com/micro/vix/historical.aspx
|
|
||||||
`
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const [readMe, setreadMe] = useState("")
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function processReadMe() {
|
|
||||||
const processed = await remark()
|
|
||||||
.use(html)
|
|
||||||
.process(readMeMarkdown)
|
|
||||||
setreadMe(processed.toString())
|
|
||||||
}
|
|
||||||
processReadMe()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ReadMe readme={readMe} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ReadMe Component Prop Types
|
|
||||||
|
|
||||||
The `ReadMe` component accepts a single property:
|
|
||||||
* **readme**: A string of a compiled ReadMe in html format.
|
|
||||||
|
|
||||||
### [View Components](https://github.com/datopian/portal.js/tree/main/src/components/views)
|
|
||||||
|
|
||||||
View components is a set of components that can be used for displaying dataset views like charts, tables, maps, e.t.c.
|
|
||||||
|
|
||||||
#### [Chart Component](https://github.com/datopian/portal.js/blob/main/src/components/views/Chart.js)
|
|
||||||
|
|
||||||
The `Chart` components exposes different chart components like Plotly Chart, Vega charts, which can be used for showing graphs. See example in the `Graph` section [here](https://portal-js.vercel.app/).
|
|
||||||
To use a chart component, you need to compile and pass a view spec as props to the chart component.
|
|
||||||
Each Chart type have their specific spec, as explained in this [doc](https://specs.frictionlessdata.io/views/#graph-spec).
|
|
||||||
|
|
||||||
In the example below, we assume there's a compiled Plotly spec:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { PlotlyChart } from 'portal'
|
|
||||||
|
|
||||||
export default function Home({plotlySpec}) {
|
|
||||||
|
|
||||||
return (
|
|
||||||
< div >
|
|
||||||
<PlotlyChart spec={plotlySpec} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
> Note: You can compile views using the [datapackage-render](https://github.com/datopian/datapackage-views-js) library, as demonstrated in [this example](https://github.com/datopian/portal.js/blob/main/examples/dataset-frictionless/lib/utils.js).
|
|
||||||
|
|
||||||
|
|
||||||
#### Chart Component Prop Types
|
|
||||||
|
|
||||||
KeyInfo component accepts two properties:
|
|
||||||
* **spec**: A compiled view spec depending on the chart type.
|
|
||||||
|
|
||||||
#### [Table Component](https://github.com/datopian/portal.js/blob/main/examples/dataset-frictionless/components/Table.js)
|
|
||||||
|
|
||||||
The `Table` component is used for displaying dataset resources as a tabular grid. See example in the `Data Preview` section [here](https://portal-js.vercel.app/).
|
|
||||||
To use a Table component, you have to pass an array of data and columns as demonstrated below:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Table } from 'portal' //import Table component
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{ field: 'id', headerName: 'ID' },
|
|
||||||
{ field: 'firstName', headerName: 'First name' },
|
|
||||||
{ field: 'lastName', headerName: 'Last name' },
|
|
||||||
{ field: 'age', headerName: 'Age' }
|
|
||||||
];
|
|
||||||
|
|
||||||
const 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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Table data={data} columns={columns} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
> Note: Under the hood, Table component uses the [DataGrid Material UI table](https://material-ui.com/components/data-grid/), and as such all supported params in data and columns are supported.
|
|
||||||
|
|
||||||
|
|
||||||
#### Table Component Prop Types
|
|
||||||
|
|
||||||
Table component accepts two properties:
|
|
||||||
* **data**: An array of column names with properties: e.g {'[{field: "col1", headerName: "col1"}, {field: "col2", headerName: "col2"}]'}
|
|
||||||
* **columns**: An array of data objects e.g. {'[ {col1: 1, col2: 2}, {col1: 5, col2: 7} ]'}
|
|
||||||
|
|
||||||
|
|
||||||
### [Search Components](https://github.com/datopian/portal.js/tree/main/src/components/search)
|
|
||||||
|
|
||||||
Search components groups together components that can be used for creating a search interface. This includes search forms, search item as well as search result list.
|
|
||||||
|
|
||||||
#### [Form Component](https://github.com/datopian/portal.js/blob/main/src/components/search/Form.js)
|
|
||||||
|
|
||||||
The search`Form` component is a simple search input and submit button. See example of a search form [here](https://catalog-portal-js.vercel.app/search).
|
|
||||||
|
|
||||||
The search `form` requires a submit handler (`handleSubmit`). This handler function receives the search term, and handles actual search.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `Form` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Form } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const handleSearchSubmit = (searchQuery) => {
|
|
||||||
// Write your custom code to perform search in db
|
|
||||||
console.log(searchQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form
|
|
||||||
handleSubmit={handleSearchSubmit} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Form Component Prop Types
|
|
||||||
|
|
||||||
The `Form` component accepts a single property:
|
|
||||||
* **handleSubmit**: A function that receives the search text, and can be customize to perform the actual search.
|
|
||||||
|
|
||||||
#### [Item Component](https://github.com/datopian/portal.js/blob/main/src/components/search/Item.js)
|
|
||||||
|
|
||||||
The search`Item` component can be used to display a single search result.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `Item` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Item } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
const datapackage = {
|
|
||||||
"name": "finance-vix",
|
|
||||||
"title": "VIX - CBOE Volatility Index",
|
|
||||||
"homepage": "http://www.cboe.com/micro/VIX/",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"description": "This is a test organization description",
|
|
||||||
"resources": [
|
|
||||||
{
|
|
||||||
"name": "vix-daily",
|
|
||||||
"path": "vix-daily.csv",
|
|
||||||
"format": "csv",
|
|
||||||
"size": 20982,
|
|
||||||
"mediatype": "text/csv",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Item dataset={datapackage} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Item Component Prop Types
|
|
||||||
|
|
||||||
The `Item` component accepts a single property:
|
|
||||||
* **dataset**: A [Frictionless data package descriptor](https://specs.frictionlessdata.io/data-package/#descriptor)
|
|
||||||
|
|
||||||
|
|
||||||
#### [ItemTotal Component](https://github.com/datopian/portal.js/blob/main/src/components/search/Item.js)
|
|
||||||
|
|
||||||
The search`ItemTotal` is a simple component for displaying the total search result
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `ItemTotal` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { ItemTotal } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
//do some custom search to get results
|
|
||||||
const search = (text) => {
|
|
||||||
return [{ name: "data1" }, { name: "data2" }]
|
|
||||||
}
|
|
||||||
//get the total result count
|
|
||||||
const searchTotal = search("some text").length
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ItemTotal count={searchTotal} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ItemTotal Component Prop Types
|
|
||||||
|
|
||||||
The `ItemTotal` component accepts a single property:
|
|
||||||
* **count**: An integer of the total number of results.
|
|
||||||
|
|
||||||
|
|
||||||
### [Blog Components](https://github.com/datopian/portal.js/tree/main/src/components/blog)
|
|
||||||
|
|
||||||
These are group of components for building a portal blog. See example of portal blog [here](https://catalog-portal-js.vercel.app/blog)
|
|
||||||
|
|
||||||
#### [PostList Components](https://github.com/datopian/portal.js/tree/main/src/components/misc)
|
|
||||||
|
|
||||||
The `PostList` component is used to display a list of blog posts with the title and a short excerpts from the content.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `PostList` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { PostList } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const posts = [
|
|
||||||
{ title: "Blog post 1", excerpt: "This is the first blog excerpts in this list." },
|
|
||||||
{ title: "Blog post 2", excerpt: "This is the second blog excerpts in this list." },
|
|
||||||
{ title: "Blog post 3", excerpt: "This is the third blog excerpts in this list." },
|
|
||||||
]
|
|
||||||
return (
|
|
||||||
<PostList posts={posts} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### PostList Component Prop Types
|
|
||||||
|
|
||||||
The `PostList` component accepts a single property:
|
|
||||||
* **posts**: An array of post list objects with the following properties:
|
|
||||||
```javascript
|
|
||||||
[
|
|
||||||
{
|
|
||||||
title: "The title of the blog post",
|
|
||||||
excerpt: "A short excerpt from the post content",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
#### [Post Components](https://github.com/datopian/portal.js/tree/main/src/components/misc)
|
|
||||||
|
|
||||||
The `Post` component is used to display a blog post. See an example of a blog post [here](https://catalog-portal-js.vercel.app/blog/nyt-pa-platformen-opdateringsfrekvens-og-andres-data)
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `Post` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Post } from 'portal'
|
|
||||||
import * as dayjs from 'dayjs' //For converting UTC time to relative format
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
||||||
|
|
||||||
dayjs.extend(relativeTime)
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
const post = {
|
|
||||||
title: "This is a sample blog post",
|
|
||||||
content: `<h1>A simple header</h1>
|
|
||||||
The PostList component is used to display a list of blog posts
|
|
||||||
with the title and a short excerpts from the content.
|
|
||||||
In the example below, we demonstrate how to use the PostList component.`,
|
|
||||||
createdAt: dayjs().to(dayjs(1620649596902)),
|
|
||||||
featuredImage: "https://pixabay.com/get/ge9a766d1f7b5fe0eccbf0f439501a2cf2b191997290e7ab15e6a402574acc2fdba48a82d278dca3547030e0202b7906d_640.jpg"
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Post post={post} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Post Component Prop Types
|
|
||||||
|
|
||||||
The `Post` component accepts a single property:
|
|
||||||
* **post**: An object with the following properties:
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
title: <The title of the blog post>
|
|
||||||
content: <The body of the blog post. Can be plain text or html>
|
|
||||||
createdAt: <The utc date when the post was last modified>
|
|
||||||
featuredImage: < Url/relative url to post cover image>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### [Misc Components](https://github.com/datopian/portal.js/tree/main/src/components/misc)
|
|
||||||
|
|
||||||
These are group of miscellaneous/extra components for extending your portal. They include components like Errors, custom links, etc.
|
|
||||||
|
|
||||||
#### [Error Component](https://github.com/datopian/portal.js/blob/main/src/components/misc/Error.js)
|
|
||||||
|
|
||||||
The `Error` component is used to display a custom error message.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `Error` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { Error } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Error message="An error occured when loading the file!" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Error Component Prop Types
|
|
||||||
|
|
||||||
The `Error` component accepts a single property:
|
|
||||||
* **message**: A string with the error message to display.
|
|
||||||
|
|
||||||
|
|
||||||
#### [Custom Component](https://github.com/datopian/portal.js/blob/main/src/components/misc/Error.js)
|
|
||||||
|
|
||||||
The `CustomLink` component is used to create a link with a consistent style to other portal components.
|
|
||||||
|
|
||||||
In the example below, we demonstrate how to use the `CustomLink` component.
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import { CustomLink } from 'portal'
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
|
|
||||||
return (
|
|
||||||
<CustomLink url="/blog" title="Goto Blog" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### CustomLink Component Prop Types
|
|
||||||
|
|
||||||
The `CustomLink` component accepts the following properties:
|
|
||||||
|
|
||||||
* **url**: A string. The relative or absolute url of the link.
|
|
||||||
* **title**: A string. The title of the link
|
|
||||||
|
|
||||||
54
site/content/docs/example-ckan.md
Normal file
54
site/content/docs/example-ckan.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
title: "Example: data catalog with data coming from CKAN"
|
||||||
|
authors: ['Luccas Mateus']
|
||||||
|
date: 2023-04-20
|
||||||
|
---
|
||||||
|
|
||||||
|
**See the repo:** https://github.com/datopian/portaljs/tree/main/examples/ckan-example
|
||||||
|
|
||||||
|
This is a repo intended to serve as an example of a data catalog that get its data from a CKAN Instance.
|
||||||
|
|
||||||
|
```
|
||||||
|
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/ckan-example
|
||||||
|
cd <app-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
- This project uses CKAN as a backend, so you need to point the project to the CKAN Url desired, you can do so by setting up the `DMS` env variable in your terminal or adding a `.env` file with the following content:
|
||||||
|
|
||||||
|
```
|
||||||
|
DMS=<ckan url>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Run the app using:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Congratulations, you now have something similar to this running on `http://localhost:4200`
|
||||||
|

|
||||||
|
If yo go to any one of those pages by clicking on `More info` you will see something similar to this
|
||||||
|

|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fckan-example&env=DMS&envDescription=URL%20For%20the%20CKAN%20Backend%20Ex%3A%20https%3A%2F%2Fdemo.dev.datopian.com)
|
||||||
|
|
||||||
|
By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own github/gitlab/bitbucket account and automatically deploy everything.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Extra commands
|
||||||
|
|
||||||
|
You can also build the project for production with
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
And run using the production build like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
81
site/content/docs/example-data-catalog.md
Normal file
81
site/content/docs/example-data-catalog.md
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: "Example: simple data catalog"
|
||||||
|
authors: ['Luccas Mateus']
|
||||||
|
date: 2023-04-20
|
||||||
|
---
|
||||||
|
|
||||||
|
**See the repo:** https://github.com/datopian/portaljs/tree/main/examples/simple-example
|
||||||
|
|
||||||
|
- Creating a new project with `create-next-app` like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/simple-example
|
||||||
|
cd <app-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
- This project uses the github api, which for anonymous users will cap at 50 requests per hour, so you might want to get a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and add it to a `.env` file inside the folder like so
|
||||||
|
|
||||||
|
```
|
||||||
|
GITHUB_PAT=<github token>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Edit the file `datasets.json` to your liking, some examples can be found inside this [repo](https://github.com/datasets)
|
||||||
|
- Run the app using:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Congratulations, you now have something similar to this running on `http://localhost:3000`
|
||||||
|

|
||||||
|
If yo go to any one of those pages by clicking on `More info` you will see something similar to this
|
||||||
|

|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fsimple-example)
|
||||||
|
|
||||||
|
By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own github/gitlab/bitbucket account and automatically deploy everything.
|
||||||
|
|
||||||
|
|
||||||
|
## Structure of `datasets.json`
|
||||||
|
|
||||||
|
The `datasets.json` file is simply a list of datasets, below you can see a minimal example of a dataset
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"owner": "fivethirtyeight",
|
||||||
|
"repo": "data",
|
||||||
|
"branch": "master",
|
||||||
|
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
|
||||||
|
"readme": "nba-raptor/README.md"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It has
|
||||||
|
|
||||||
|
- A `owner` which is going to be the github repo owner
|
||||||
|
- A `repo` which is going to be the github repo name
|
||||||
|
- A `branch` which is going to be the branch to which we need to get the files and the readme
|
||||||
|
- A list of `files` which is going to be a list of paths with files that you want to show to the world
|
||||||
|
- A `readme` which is going to be the path to your data description, it can also be a subpath eg: `example/README.md`
|
||||||
|
|
||||||
|
You can also add
|
||||||
|
|
||||||
|
- A `description` which is useful if you have more than one dataset for each repo, if not provided we are just going to use the repo description
|
||||||
|
- A `Name` which is useful if you want to give your dataset a nice name, if not provided we are going to use the junction of the `owner` the `repo` + the path of the README, in the exaple above it will be `fivethirtyeight/data/nba-raptor`
|
||||||
|
|
||||||
|
## Extra commands
|
||||||
|
|
||||||
|
You can also build the project for production with
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
And run using the production build like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
48
site/content/docs/example-frictionless-dataset.md.bkp
Normal file
48
site/content/docs/example-frictionless-dataset.md.bkp
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
title: "Example: showcase for a single Frictionless dataset"
|
||||||
|
authors: ['Luccas Mateus']
|
||||||
|
date: 2023-04-20
|
||||||
|
filetype: blog
|
||||||
|
---
|
||||||
|
|
||||||
|
**See the repo:** https://github.com/datopian/portaljs/tree/main/examples/dataset-frictionless
|
||||||
|
|
||||||
|
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/portaljs/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/examples/frictionless-dataset-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:
|
||||||
|
|
||||||
|

|
||||||
38
site/content/docs/index.md
Normal file
38
site/content/docs/index.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 🌀 PortalJS: The JavaScript framework for data portals
|
||||||
|
|
||||||
|
🌀 PortalJS is a framework for rapidly building rich data portal frontends using a modern frontend approach. PortalJS can be used to present a single dataset or build a full-scale data catalog/portal.
|
||||||
|
|
||||||
|
Built in JavaScript and React on top of the popular [Next.js](https://nextjs.com/) framework. PortalJS 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](https://ckan.org/).
|
||||||
|
|
||||||
|
## 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, Next.js).
|
||||||
|
- 🔋 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 Next.js and Apollo.
|
||||||
|
|
||||||
|
### For developers
|
||||||
|
|
||||||
|
- 🏗 Build with modern, familiar frontend tech such as JavaScript and React.
|
||||||
|
- 🚀 Next.js framework: so everything in Next.js for free: Server Side Rendering, Static Site Generation, huge number of examples and integrations, etc.
|
||||||
|
- Server Side Rendering (SSR) => Unlimited number of pages, SEO and more whilst still using React.
|
||||||
|
- Static Site Generation (SSG) => Ultra-simple deployment, great performance, great lighthouse scores and more (good for small sites)
|
||||||
|
|
||||||
|
## Tutorials and guides
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
You can check out the following examples built with PortalJS.
|
||||||
|
|
||||||
|
* [Data catalog with data coming from CKAN](/docs/example-ckan)
|
||||||
|
* [Simple data catalog](/docs/example-data-catalog)
|
||||||
|
|
||||||
|
> The [`examples` directory](https://github.com/datopian/portaljs/tree/main/examples) is regularly updated with different portal examples.
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you have questions about anything related to PortalJS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portaljs/discussions) or on our [Discord server](https://discord.gg/An7Bu5x8).
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Concepts and Terms
|
# Concepts and Terms
|
||||||
|
|
||||||
In this section, we explain some of the terms and concepts used throughtout the portal.js documentation.
|
In this section, we explain some of the terms and concepts used throughtout the PortalJS documentation.
|
||||||
|
|
||||||
> Some of these concepts are part of official specs, and when appropriate, we'll link to the sources where you can get more details.
|
> Some of these concepts are part of official specs, and when appropriate, we'll link to the sources where you can get more details.
|
||||||
|
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
# Getting Started
|
|
||||||
|
|
||||||
It's no secret that creating data portals and data-driven applications can be quite complex nowadays. Fortunately, there are some projects available which simplify things and help you build platforms faster.
|
|
||||||
|
|
||||||
[CKAN](https://ckan.org/), [Jupyter](https://jupyter.org/) and other tools are very good examples of that.
|
|
||||||
|
|
||||||
Even still, there's a high learning curve before you can build a proper application. That's because you need to learn about Python, templating, data loading and so on. If you'd like to integrate content or rich visualizations things are even more complex.
|
|
||||||
|
|
||||||
**So, we need something simple but customizable.**
|
|
||||||
|
|
||||||
Think about how apps are created as a frontend developer. You create some files, write some code, load some data and then simply deploy it. We don't have to worry about Docker, Kubernetes, data storage, Postgres etc.
|
|
||||||
|
|
||||||
That's exactly what we do with Portal.js. Built in pure Javascript and React on top of the awesome Next.js framework. Here are some the cool features Portal.js brings to the table:
|
|
||||||
|
|
||||||
- 🗺️ 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)
|
|
||||||
- 👩💻 Developer friendly: built with familiar frontend tech Javascript, React etc
|
|
||||||
- 🔋 Batteries included: Full set of presentation and portal components out of the box e.g. data tables, graphs, maps plus 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.
|
|
||||||
- 🚀 Built on 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
|
|
||||||
|
|
||||||
Sounds great, right? Let's give it a try.
|
|
||||||
|
|
||||||
> This tutorial assumes basic knowledge of JavaScript, React and Nextjs. If you are not familiar with React or Nextjs, it is advisable to learn them first. We provide some links below to get you started:
|
|
||||||
>
|
|
||||||
> * [Learn NextJS](https://nextjs.org/docs/getting-started)
|
|
||||||
> * [Getting started with React](https://reactjs.org/docs/getting-started.html#learn-react)
|
|
||||||
|
|
||||||
## Create a Portal.JS app
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
First, let’s make sure that your development environment is ready.
|
|
||||||
|
|
||||||
* If you don’t have Node.js installed, [install it from here](https://nodejs.org/en/). You’ll need Node.js version 10.13 or later.
|
|
||||||
* You’ll be using your own text editor and terminal app for this tutorial.
|
|
||||||
|
|
||||||
If you are on Windows, we recommend downloading Git for Windows and use Git Bash that comes with it, which supports the UNIX-specific commands in this tutorial. Windows Subsystem for Linux (WSL) is another option.
|
|
||||||
|
|
||||||
### Create a Portal.js App
|
|
||||||
|
|
||||||
To create a Portal.js app, open your terminal, cd into the directory you’d like to create the app in, and run the following command:
|
|
||||||
|
|
||||||
```
|
|
||||||
npx create-next-app portaljs-dataset --use-npm --example "https://github.com/datopian/portal.js/tree/main/examples/default"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run the development server
|
|
||||||
|
|
||||||
You now have a new directory called portaljs-dataset. Let’s cd into it:
|
|
||||||
|
|
||||||
```
|
|
||||||
cd portaljs-dataset
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, run the following command:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
This starts your Portal.js app’s "development server" (more on this later) on port 3000.
|
|
||||||
|
|
||||||
Let’s check to see if it’s working. Open http://localhost:3000 from your browser and you should see the following page:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Edit the page
|
|
||||||
|
|
||||||
Portal.js app is a Next.js/React.js based project. To edit the page follow these steps:
|
|
||||||
|
|
||||||
1. Open the project in your text editor.
|
|
||||||
2. Go to `/pages/index.js` file.
|
|
||||||
3. Find the `h2` tag with text that says **"Yay, the portal is open 🌀"** and change it to **"Hello World!"**.
|
|
||||||
4. Save the file.
|
|
||||||
|
|
||||||
Once you've changed the file and saved it, the page on `localhost:3000` should update:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
We won't dive into details of how to edit the pages as our focus is presenting data. To learn more about how to use Next.js and/or React, please visit the following sites:
|
|
||||||
|
|
||||||
* [Learn NextJS](https://nextjs.org/docs/getting-started)
|
|
||||||
* [Getting started with React](https://reactjs.org/docs/getting-started.html#learn-react)
|
|
||||||
|
|
||||||
## Next steps
|
|
||||||
|
|
||||||
* [Presenting a dataset]()
|
|
||||||
* [Putting your portal online]()
|
|
||||||
* Deploy to GitHub Pages - [learn/deploy-to-gh-pages](/learn/deploy-to-gh-pages).
|
|
||||||
* Learn how to build a portal for a single frictionless dataset - [learn/single-frictionless-dataset](/learn/single-frictionless-dataset).
|
|
||||||
* Learn how to use Portal.js as a frontend for CKAN - [learn/ckan](/learn/ckan).
|
|
||||||
@@ -1,251 +0,0 @@
|
|||||||
# Deploying data on Github using Portal.js and Github pages
|
|
||||||
---
|
|
||||||
**Use Case:**
|
|
||||||
---
|
|
||||||
You have some data in a Github repo and you'd like to deploy it online using "portal" so that it is easy for others to view, explore and use.
|
|
||||||
---
|
|
||||||
Here we show how you can use portal.js plus github actions to deploy your dataset in minutes and keep it updated as you make changes.
|
|
||||||
|
|
||||||
The example focuses on the case of a [Frictionless dataset][fd] but it works for any dataset type supported by portal.js.
|
|
||||||
|
|
||||||
We provide three options on how to do this and recommend using the first one unless you really want to get hands on:
|
|
||||||
|
|
||||||
* Deploying datasets automatically by setting up a github actions script.
|
|
||||||
* Deploying datasets from a local bash script with portal code commits
|
|
||||||
* Deploying datasets from a local bash script without portal code commits
|
|
||||||
|
|
||||||
[fd]: https://frictionlessdata.io/data-packages/
|
|
||||||
|
|
||||||
## Deploy datasets automatically by setting up a github actions script
|
|
||||||
|
|
||||||
The github actions below will automatically build and deploy a single page, Frictionless dataset to `gh-pages` branch. Follow the steps below to achieve this:
|
|
||||||
|
|
||||||
1. Create a secret so we can automatically commit to gh-pages branch (see below)
|
|
||||||
2. Set up the github action to build portal to your dataset and deploy it (see below)
|
|
||||||
3. Wait for your page to build and then setup github pages (see below)
|
|
||||||
4. View the results: visit `https://<your github username>/github.io/<dataset repo name>/`
|
|
||||||
|
|
||||||
### Step 1
|
|
||||||
|
|
||||||
In the dataset repository you want to deploy, create a github secret with the name `PORTAL_REPO_NAME` and the value should be the name of the repository.
|
|
||||||
|
|
||||||
See steps on creating a secret [here](https://docs.github.com/en/actions/reference/encrypted-secrets)
|
|
||||||
|
|
||||||
<img src="/scripts/assets/secrets.png" />
|
|
||||||
|
|
||||||
### Step 2
|
|
||||||
|
|
||||||
In the dataset repository you want deploy create a `.github/workflow` directory and add a `main.yml` file with the following content (you can also view/download this [action file here](scripts/actions/single-dataset-ssg.yml):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
name: github pages
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
run:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@v2.1.2
|
|
||||||
with:
|
|
||||||
node-version: '12.x'
|
|
||||||
|
|
||||||
- name: Build datasets
|
|
||||||
env:
|
|
||||||
PORTAL_REPO_NAME: ${{ secrets.PORTAL_REPO_NAME }}
|
|
||||||
run: |
|
|
||||||
curl https://raw.githubusercontent.com/datopian/portal.js/main/site/public/scripts/single-dataset-no-commit.sh > portal.sh
|
|
||||||
git config --local user.email "$(git log --format='%ae' HEAD^!)"
|
|
||||||
git config --local user.name "$(git log --format='%an' HEAD^!)"
|
|
||||||
source ./portal.sh
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, commit and push your code.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add .
|
|
||||||
git commit -m "Build dataset page"
|
|
||||||
git push
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3
|
|
||||||
|
|
||||||
Wait for a while as your page builds, and once you see the green check mark, navigate to your repository's github `pages` in settings, set the `source` to `gh-pages` and folder to `/root`:
|
|
||||||
|
|
||||||
<img src='/scripts/assets/sdnocommit.png' />
|
|
||||||
|
|
||||||
|
|
||||||
## Deploy single dataset without commiting portal.js code
|
|
||||||
|
|
||||||
Users who want to deploy datasets from a local bash script without saving/commiting the portal.js code, can use the script shown below.
|
|
||||||
|
|
||||||
Using this script means you do not have access to the portal.js code used to generate the dataset page, and as such cannot modify/extend it.
|
|
||||||
|
|
||||||
This script creates and commit only the build/output files to the gh-pages branch. Follow the steps below to achieve this.
|
|
||||||
|
|
||||||
### Step 1
|
|
||||||
|
|
||||||
Clone/Pull the dataset repository you want deploy. For example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/datasets/finance-vix
|
|
||||||
cd finance-vix
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2
|
|
||||||
|
|
||||||
In a terminal, export an env variable with the name of your dataset github repo. For example if deploying https://github.com/datasets/finance-vix, then export the name as:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export PORTAL_REPO_NAME=finance-vix
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3
|
|
||||||
|
|
||||||
In the dataset repository's root folder, create a file called `portal.sh` and paste the following [content](/scripts/single-dataset-no-commit.sh):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
git checkout -b gh-pages
|
|
||||||
git rm -r --cached .
|
|
||||||
rm -rf portal
|
|
||||||
mkdir -p portal
|
|
||||||
npx create-next-app portal -e https://github.com/datopian/portal.js/tree/main/examples/dataset-frictionless
|
|
||||||
mkdir portal/public/dataset
|
|
||||||
|
|
||||||
cp -a ./data portal/public/dataset
|
|
||||||
cp -a ./datapackage.json portal/public/dataset
|
|
||||||
cp -a ./README.md portal/public/dataset
|
|
||||||
|
|
||||||
PORTAL_DATASET_PATH=$PWD"/portal/public/dataset"
|
|
||||||
export PORTAL_DATASET_PATH
|
|
||||||
|
|
||||||
cd portal
|
|
||||||
assetPrefix='"/'$PORTAL_REPO_NAME'/"'
|
|
||||||
basePath='"/'$PORTAL_REPO_NAME'"'
|
|
||||||
echo 'module.exports = {assetPrefix:' ${assetPrefix}', basePath: '${basePath}' }' > next.config.js ## This ensures css and public folder works
|
|
||||||
yarn export
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
cp -R -a portal/out/* ./
|
|
||||||
touch .nojekyll
|
|
||||||
git add $PWD'/_next' $PWD'/index.html' $PWD'/dataset' $PWD'/404.html' $PWD'/.nojekyll' $PWD'/favicon.ico'
|
|
||||||
git commit -m "Build new dataset page"
|
|
||||||
git push origin gh-pages
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4
|
|
||||||
|
|
||||||
Run the bash script in a terminal with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
source portal.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note: Use `source` instead of `bash` so that the script can work well with environment variables.
|
|
||||||
|
|
||||||
### Step 5
|
|
||||||
|
|
||||||
Go to your repository's github `pages` in setting and set the Branch to gh-pages and folder to root:
|
|
||||||
|
|
||||||
<img src='/scripts/assets/sdnocommit.png' />
|
|
||||||
|
|
||||||
### Step 6
|
|
||||||
|
|
||||||
Open your deployed site at `https://<your github username>/github.io/<dataset repo name>`
|
|
||||||
|
|
||||||
|
|
||||||
## Deploy single dataset with portal commit
|
|
||||||
|
|
||||||
Users who want access to the portal.js code used for generating the dataset page can use the script shown in the following section.
|
|
||||||
|
|
||||||
This script creates and commits the portal.js code to the root branch and also adds an automated script to deploy to gh-page. Follow the steps below to use this script.
|
|
||||||
|
|
||||||
### Step 1
|
|
||||||
|
|
||||||
Create a Github Personal Access Token (PAT). See steps [here](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token)
|
|
||||||
|
|
||||||
### Step 2
|
|
||||||
|
|
||||||
In the dataset repository you want to deploy, create a github secret with the name `PORTAL_NEXT_TOKEN`. The value should be the PAT created in step 1. See steps on creating a secret [here](https://docs.github.com/en/actions/reference/encrypted-secrets)
|
|
||||||
|
|
||||||
> Note: Without the PAT and the secret configured, the automatic build will fail.
|
|
||||||
|
|
||||||
### Step 3
|
|
||||||
|
|
||||||
Clone/Pull the dataset repository you want deploy. For example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/datasets/finance-vix
|
|
||||||
cd finance-vix
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4
|
|
||||||
|
|
||||||
In your computer's terminal/command prompt, export an environment variable with the name of your dataset's github repo.
|
|
||||||
|
|
||||||
For example if you want to deploy the dataset at https://github.com/datasets/finance-vix, then export the name using the command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export PORTAL_REPO_NAME=finance-vix
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 5
|
|
||||||
|
|
||||||
Create a file called `portal.sh` and paste the following [content](/scripts/single-dataset-commit.sh):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
#!/bin/bash
|
|
||||||
rm -rf portal
|
|
||||||
mkdir -p portal
|
|
||||||
npx create-next-app portal -e https://github.com/datopian/portal.js/tree/main/examples/dataset-frictionless
|
|
||||||
mkdir portal/public/dataset
|
|
||||||
|
|
||||||
cp -a ./data portal/public/dataset
|
|
||||||
cp -a ./datapackage.json portal/public/dataset
|
|
||||||
cp -a ./README.md portal/public/dataset
|
|
||||||
|
|
||||||
PORTAL_DATASET_PATH=$PWD"/portal/public/dataset"
|
|
||||||
export PORTAL_DATASET_PATH
|
|
||||||
|
|
||||||
mkdir -p .github && mkdir -p .github/workflows && touch .github/workflows/main.yml
|
|
||||||
curl https://raw.githubusercontent.com/datopian/portal.js/main/site/public/scripts/gh-page-builder-action.yml > .github/workflows/main.yml
|
|
||||||
|
|
||||||
cd portal
|
|
||||||
assetPrefix='"/'$PORTAL_REPO_NAME'/"'
|
|
||||||
basePath='"/'$PORTAL_REPO_NAME'"'
|
|
||||||
echo 'module.exports = {assetPrefix:' ${assetPrefix}', basePath: '${basePath}' }' > next.config.js ## This ensures css and public folder works
|
|
||||||
|
|
||||||
cd ..
|
|
||||||
git add .
|
|
||||||
git commit -m "Add dataset build feature"
|
|
||||||
git push
|
|
||||||
echo "Portal generated, please push your code to github"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 6
|
|
||||||
|
|
||||||
Run the bash script with:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
source portal.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
> Note: Use `source` instead of `bash` so that the script can work well with environment variables.
|
|
||||||
|
|
||||||
### Step 7
|
|
||||||
|
|
||||||
Go to your repository's github `pages` in setting and set the Branch to gh-pages and folder to root:
|
|
||||||
|
|
||||||
<img src='/scripts/assets/sdnocommit.png' />
|
|
||||||
|
|
||||||
### Step 8
|
|
||||||
|
|
||||||
Open your deployed site at `https://<your github username>/github.io/<dataset repo name>`
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
Live Demo:
|
|
||||||
- https://portal-js.vercel.app/
|
|
||||||
|
|
||||||
## Create a single frictionless dataset portal
|
|
||||||
|
|
||||||
The dataset should be a frictionless dataset i.e. it should have a [datapackage.json](https://specs.frictionlessdata.io/data-package/)
|
|
||||||
|
|
||||||
|
|
||||||
Create a frictionless dataset portal app from the default template by executing the following command in your terminal:
|
|
||||||
```
|
|
||||||
$ 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
|
|
||||||
|
|
||||||
Next, connect the frictionless dataset to `your-portal` by declaring the path to the directory level that contains the `datapackage.json` via an environment variable by executing the following command in your terminal:
|
|
||||||
```
|
|
||||||
$ cd your-portal
|
|
||||||
$ export PORTAL_DATASET_PATH=path/to/your/dataset
|
|
||||||
```
|
|
||||||
In `your-portal` directory, run the command below in your terminal to start the portal:
|
|
||||||
```
|
|
||||||
$ yarn dev
|
|
||||||
```
|
|
||||||
Open the page in your browser via the localhost url(usually http://localhost:3000) returned in the terminal to see your frictionless dataset portal.
|
|
||||||
|
|
||||||
### Styling 🎨
|
|
||||||
|
|
||||||
We use Tailwind as a CSS framework. Take a look at `/styles/tailwind.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
|
|
||||||
5
site/content/people/anuveyatsu.md
Normal file
5
site/content/people/anuveyatsu.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
id: anuveyatsu
|
||||||
|
name: Anuar Ustayev
|
||||||
|
avatar: https://avatars.githubusercontent.com/anuveyatsu
|
||||||
|
---
|
||||||
5
site/content/people/joao-demenech.md
Normal file
5
site/content/people/joao-demenech.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
id: joaodemenech
|
||||||
|
name: João Demenech
|
||||||
|
avatar: https://avatars.githubusercontent.com/demenech
|
||||||
|
---
|
||||||
5
site/content/people/luccas-mateus.md
Normal file
5
site/content/people/luccas-mateus.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
id: luccasmateus
|
||||||
|
name: Luccas Mateus
|
||||||
|
avatar: https://avatars.githubusercontent.com/luccasmmg
|
||||||
|
---
|
||||||
5
site/content/people/mikanebu.md
Normal file
5
site/content/people/mikanebu.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
id: mikanebu
|
||||||
|
name: Meiran Zhiyenbayev
|
||||||
|
avatar: https://avatars.githubusercontent.com/mikanebu
|
||||||
|
---
|
||||||
5
site/content/people/popovayoana.md
Normal file
5
site/content/people/popovayoana.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
id: popovayoana
|
||||||
|
name: Yoana Popova
|
||||||
|
avatar: https://avatars.githubusercontent.com/popovayoana
|
||||||
|
---
|
||||||
5
site/content/people/rufus-pollock.md
Normal file
5
site/content/people/rufus-pollock.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
id: rufuspollock
|
||||||
|
name: Rufus Pollock
|
||||||
|
avatar: https://avatars.githubusercontent.com/rufuspollock
|
||||||
|
---
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user