Merge branch 'main' of github.com:datopian/portal.js into main

This commit is contained in:
Rufus Pollock 2021-05-19 16:19:01 +02:00
commit 2721727c75
24 changed files with 4441 additions and 99 deletions

795
README.md
View File

@ -1,17 +1,34 @@
<h1 align="center">
🌀 Portal.JS<br/>
The javascript framework for<br/>
data portals
<h1 align="center">🌀 Portal.JS
> The JavaScript framework for data portals
</h1>
🌀 `portal` is a framework for rapidly building rich data portal frontends using a modern frontend approach. `portal` can be used to present a single dataset or build a full-scale data catalog/portal.
* [What is Portal.JS ?](#What-is-Portal.JS)
* [Features](#Features)
* [For developers](#For-developers)
* [Installation and setup](#Installation-and-setup)
* [Getting Started](#Getting-Started)
* [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)
* [What happened to Recline?](#What-happened-to-Recline?)
`portal` is built in Javascript and React on top of the popular [Next.js][] 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][].
# 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.
[ckan]: https://ckan.org/
[next.js]: https://nextjs.com/
`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
@ -28,14 +45,758 @@ data portals
- 🚀 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
### Check out more of the examples
# Installation and setup
Before installation, ensure your system satisfies the following requirements:
The [`examples` directory](./examples) is regularly updated with different portal examples.
- Node.js 10.13 or later
- Nextjs 10.0.3
- MacOS, Windows (including WSL), and Linux are supported
* [A portal for a single Frictionless dataset](./examples/dataset-frictionless)
* [A portal with a CKAN backend](./examples/catalog)
> 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:
![](https://i.imgur.com/GVh0P6p.png)
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:
![](https://i.imgur.com/n0vSjY4.png)
> **Note**: You can learn more about individual portal components, as well as their prop types in the [components reference](#Component-List).
___
# Getting Started
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.
* [A portal for a single Frictionless dataset](#Build-a-single-Frictionless-dataset-portal)
* [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.
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).
___
# Tutorial
## Build a single Frictionless dataset portal
This tutorial will guide you through building a portal for a single Frictionless dataset.
In this tutorial, youll learn Portal.js basics by creating a very simple portal app. [Heres](https://portal-js.vercel.app/) an example of the final result.
Lets get started!
> 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)
### Setup
TODO
## Build a CKAN powered dataset portal
TODO
___
# 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): 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
___
## 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 officila 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
@ -49,6 +810,8 @@ The [`examples` directory](./examples) is regularly updated with different porta
---
# Appendix: What happened to Recline?
# Appendix
## What happened to Recline?
Portal.JS used to be Recline(JS). If you are looking for the old Recline codebase it still exists: see the [`recline` branch](https://github.com/datopian/portal.js/tree/recline). If you want context for the rename see [this issue](https://github.com/datopian/portal.js/issues/520).

139
dist/index.cjs.js vendored
View File

@ -393,27 +393,27 @@ Org.propTypes = {
/**
* Displays a blog post page
* @param {object} props
* {
* post = {
* title: <The title of the blog post>
* content: <The body of the blog post>
* modified: <The utc date when the post was last modified.
* featured_image: <Url/relative url to post cover image
* 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>
* }
* @returns
*/
var Post = function Post(_ref) {
var page = _ref.page;
var title = page.title,
content = page.content,
modified = page.modified,
featured_image = page.featured_image;
var post = _ref.post;
var title = post.title,
content = post.content,
createdAt = post.createdAt,
featuredImage = post.featuredImage;
return /*#__PURE__*/React__default['default'].createElement(React__default['default'].Fragment, null, /*#__PURE__*/React__default['default'].createElement("h1", {
className: "text-3xl font-semibold text-primary my-6 inline-block"
}, title), /*#__PURE__*/React__default['default'].createElement("p", {
className: "mb-6"
}, "Edited: ", modified), /*#__PURE__*/React__default['default'].createElement("img", {
src: featured_image,
}, "Posted: ", createdAt), /*#__PURE__*/React__default['default'].createElement("img", {
src: featuredImage,
className: "mb-6",
alt: "featured_img"
}), /*#__PURE__*/React__default['default'].createElement("div", null, parse__default['default'](content)));
@ -423,8 +423,8 @@ Post.propTypes = {
page: PropTypes__default['default'].shape({
title: PropTypes__default['default'].string.isRequired,
content: PropTypes__default['default'].string.isRequired,
modified: PropTypes__default['default'].string,
featured_image: PropTypes__default['default'].string
createdAt: PropTypes__default['default'].number,
featuredImage: PropTypes__default['default'].string
})
};
@ -442,9 +442,7 @@ Post.propTypes = {
var PostList = function PostList(_ref) {
var posts = _ref.posts;
return /*#__PURE__*/React__default['default'].createElement(React__default['default'].Fragment, null, /*#__PURE__*/React__default['default'].createElement("h1", {
className: "text-3xl font-semibold text-primary my-6 inline-block"
}, posts.length, " posts found"), posts.map(function (post, index) {
return /*#__PURE__*/React__default['default'].createElement(React__default['default'].Fragment, null, posts.map(function (post, index) {
return /*#__PURE__*/React__default['default'].createElement("div", {
key: index
}, /*#__PURE__*/React__default['default'].createElement("a", {
@ -601,9 +599,118 @@ Recent.propTypes = {
datasets: PropTypes__default['default'].array.isRequired
};
/**
* Search component form that can be customized with change and submit handlers
* @param {object} props
* {
* handleChange: A form input change event handler. This function is executed when the
* search input or order by input changes.
* handleSubmit: A form submit event handler. This function is executed when the
* search form is submitted.
* }
* @returns
*/
var Form = function Form(_ref) {
var handleSubmit = _ref.handleSubmit;
var _useState = React.useState(""),
_useState2 = _slicedToArray(_useState, 2),
searchQuery = _useState2[0],
setSearchQuery = _useState2[1];
return /*#__PURE__*/React__default['default'].createElement("form", {
onSubmit: function onSubmit(e) {
return e.preventDefault();
},
className: "items-center"
}, /*#__PURE__*/React__default['default'].createElement("div", {
className: "flex"
}, /*#__PURE__*/React__default['default'].createElement("input", {
type: "text",
name: "search#q",
value: searchQuery,
onChange: function onChange(e) {
setSearchQuery(e.target.value);
},
placeholder: "Search",
"aria-label": "Search",
className: "bg-white focus:outline-none focus:shadow-outline border border-gray-300 w-1/2 rounded-lg py-2 px-4 block appearance-none leading-normal"
}), /*#__PURE__*/React__default['default'].createElement("button", {
onClick: function onClick() {
return handleSubmit(searchQuery);
},
type: "button",
className: "inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
}, "Search")));
};
Form.propTypes = {
handleSubmit: PropTypes__default['default'].func.isRequired
};
/**
* Single item from a search result showing info about a dataset.
* @param {object} props data package with the following format:
* {
* organization: {name: <some name>, title: <some title> },
* title: <Data package title>
* name: <Data package name>
* description: <description of data package>
* notes: <Notes associated with the data package>
* }
* @returns React Component
*/
var Item = function Item(_ref) {
var dataset = _ref.dataset;
return /*#__PURE__*/React__default['default'].createElement("div", {
className: "mb-6"
}, /*#__PURE__*/React__default['default'].createElement("h3", {
className: "text-xl font-semibold"
}, /*#__PURE__*/React__default['default'].createElement(Link__default['default'], {
href: "/@".concat(dataset.organization ? dataset.organization.name : 'dataset', "/").concat(dataset.name)
}, /*#__PURE__*/React__default['default'].createElement("a", {
className: "text-primary"
}, dataset.title || dataset.name))), /*#__PURE__*/React__default['default'].createElement(Link__default['default'], {
href: "/@".concat(dataset.organization ? dataset.organization.name : 'dataset')
}, /*#__PURE__*/React__default['default'].createElement("a", {
className: "text-gray-500 block mt-1"
}, dataset.organization ? dataset.organization.title : 'dataset')), /*#__PURE__*/React__default['default'].createElement("div", {
className: "leading-relaxed mt-2"
}, dataset.description || dataset.notes));
};
Item.propTypes = {
dataset: PropTypes__default['default'].object.isRequired
};
/**
* Displays the total search result
* @param {object} props
* {
* count: The total number of search results
* }
* @returns React Component
*/
var Total = function Total(_ref) {
var count = _ref.count;
return /*#__PURE__*/React__default['default'].createElement("h1", {
className: "text-3xl font-semibold text-primary my-6 inline-block"
}, count, " results found");
};
Total.propTypes = {
count: PropTypes__default['default'].number.isRequired
};
exports.CustomLink = CustomLink;
exports.DataExplorer = DataExplorer;
exports.Error = ErrorMessage;
exports.Form = Form;
exports.Item = Item;
exports.ItemTotal = Total;
exports.KeyInfo = KeyInfo;
exports.Nav = Nav;
exports.Org = Org;

138
dist/index.esm.js vendored
View File

@ -360,27 +360,27 @@ Org.propTypes = {
/**
* Displays a blog post page
* @param {object} props
* {
* post = {
* title: <The title of the blog post>
* content: <The body of the blog post>
* modified: <The utc date when the post was last modified.
* featured_image: <Url/relative url to post cover image
* 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>
* }
* @returns
*/
var Post = function Post(_ref) {
var page = _ref.page;
var title = page.title,
content = page.content,
modified = page.modified,
featured_image = page.featured_image;
var post = _ref.post;
var title = post.title,
content = post.content,
createdAt = post.createdAt,
featuredImage = post.featuredImage;
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", {
className: "text-3xl font-semibold text-primary my-6 inline-block"
}, title), /*#__PURE__*/React.createElement("p", {
className: "mb-6"
}, "Edited: ", modified), /*#__PURE__*/React.createElement("img", {
src: featured_image,
}, "Posted: ", createdAt), /*#__PURE__*/React.createElement("img", {
src: featuredImage,
className: "mb-6",
alt: "featured_img"
}), /*#__PURE__*/React.createElement("div", null, parse(content)));
@ -390,8 +390,8 @@ Post.propTypes = {
page: PropTypes.shape({
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
modified: PropTypes.string,
featured_image: PropTypes.string
createdAt: PropTypes.number,
featuredImage: PropTypes.string
})
};
@ -409,9 +409,7 @@ Post.propTypes = {
var PostList = function PostList(_ref) {
var posts = _ref.posts;
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("h1", {
className: "text-3xl font-semibold text-primary my-6 inline-block"
}, posts.length, " posts found"), posts.map(function (post, index) {
return /*#__PURE__*/React.createElement(React.Fragment, null, posts.map(function (post, index) {
return /*#__PURE__*/React.createElement("div", {
key: index
}, /*#__PURE__*/React.createElement("a", {
@ -568,4 +566,110 @@ Recent.propTypes = {
datasets: PropTypes.array.isRequired
};
export { CustomLink, DataExplorer, ErrorMessage as Error, KeyInfo, Nav, Org, PlotlyChart, Post, PostList, ReadMe, Recent, ResourcesInfo as ResourceInfo, Table };
/**
* Search component form that can be customized with change and submit handlers
* @param {object} props
* {
* handleChange: A form input change event handler. This function is executed when the
* search input or order by input changes.
* handleSubmit: A form submit event handler. This function is executed when the
* search form is submitted.
* }
* @returns
*/
var Form = function Form(_ref) {
var handleSubmit = _ref.handleSubmit;
var _useState = useState(""),
_useState2 = _slicedToArray(_useState, 2),
searchQuery = _useState2[0],
setSearchQuery = _useState2[1];
return /*#__PURE__*/React.createElement("form", {
onSubmit: function onSubmit(e) {
return e.preventDefault();
},
className: "items-center"
}, /*#__PURE__*/React.createElement("div", {
className: "flex"
}, /*#__PURE__*/React.createElement("input", {
type: "text",
name: "search#q",
value: searchQuery,
onChange: function onChange(e) {
setSearchQuery(e.target.value);
},
placeholder: "Search",
"aria-label": "Search",
className: "bg-white focus:outline-none focus:shadow-outline border border-gray-300 w-1/2 rounded-lg py-2 px-4 block appearance-none leading-normal"
}), /*#__PURE__*/React.createElement("button", {
onClick: function onClick() {
return handleSubmit(searchQuery);
},
type: "button",
className: "inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
}, "Search")));
};
Form.propTypes = {
handleSubmit: PropTypes.func.isRequired
};
/**
* Single item from a search result showing info about a dataset.
* @param {object} props data package with the following format:
* {
* organization: {name: <some name>, title: <some title> },
* title: <Data package title>
* name: <Data package name>
* description: <description of data package>
* notes: <Notes associated with the data package>
* }
* @returns React Component
*/
var Item = function Item(_ref) {
var dataset = _ref.dataset;
return /*#__PURE__*/React.createElement("div", {
className: "mb-6"
}, /*#__PURE__*/React.createElement("h3", {
className: "text-xl font-semibold"
}, /*#__PURE__*/React.createElement(Link, {
href: "/@".concat(dataset.organization ? dataset.organization.name : 'dataset', "/").concat(dataset.name)
}, /*#__PURE__*/React.createElement("a", {
className: "text-primary"
}, dataset.title || dataset.name))), /*#__PURE__*/React.createElement(Link, {
href: "/@".concat(dataset.organization ? dataset.organization.name : 'dataset')
}, /*#__PURE__*/React.createElement("a", {
className: "text-gray-500 block mt-1"
}, dataset.organization ? dataset.organization.title : 'dataset')), /*#__PURE__*/React.createElement("div", {
className: "leading-relaxed mt-2"
}, dataset.description || dataset.notes));
};
Item.propTypes = {
dataset: PropTypes.object.isRequired
};
/**
* Displays the total search result
* @param {object} props
* {
* count: The total number of search results
* }
* @returns React Component
*/
var Total = function Total(_ref) {
var count = _ref.count;
return /*#__PURE__*/React.createElement("h1", {
className: "text-3xl font-semibold text-primary my-6 inline-block"
}, count, " results found");
};
Total.propTypes = {
count: PropTypes.number.isRequired
};
export { CustomLink, DataExplorer, ErrorMessage as Error, Form, Item, Total as ItemTotal, KeyInfo, Nav, Org, PlotlyChart, Post, PostList, ReadMe, Recent, ResourcesInfo as ResourceInfo, Table };

34
docs/.gitignore vendored Normal file
View File

@ -0,0 +1,34 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# vercel
.vercel

34
docs/README.md Normal file
View File

@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

23
docs/package.json Normal file
View File

@ -0,0 +1,23 @@
{
"name": "docs",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
},
"dependencies": {
"next": "10.2.0",
"react": "17.0.2",
"react-dom": "17.0.2",
"frictionless.js": "^0.13.4",
"remark": "^13.0.0",
"remark-html": "^13.0.1",
"tailwindcss": "^2.0.2",
"@tailwindcss/typography": "^0.4.0",
"autoprefixer": "^10.0.4",
"postcss": "^8.2.10"
}
}

8
docs/pages/_app.js Normal file
View File

@ -0,0 +1,8 @@
import '../styles/globals.css'
import '../styles/tailwind.css'
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />
}
export default MyApp

5
docs/pages/api/hello.js Normal file
View File

@ -0,0 +1,5 @@
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
export default (req, res) => {
res.status(200).json({ name: 'John Doe' })
}

65
docs/pages/index.js Normal file
View File

@ -0,0 +1,65 @@
import Head from 'next/head'
import styles from '../styles/Home.module.css'
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.js</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h3>Documentation &rarr;</h3>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h3>Learn &rarr;</h3>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/master/examples"
className={styles.card}
>
<h3>Examples &rarr;</h3>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h3>Deploy &rarr;</h3>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<img src="/vercel.svg" alt="Vercel Logo" className={styles.logo} />
</a>
</footer>
</div>
)
}

8
docs/postcss.config.js Normal file
View File

@ -0,0 +1,8 @@
// If you want to use other PostCSS plugins, see the following:
// https://tailwindcss.com/docs/using-with-preprocessors
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

BIN
docs/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

4
docs/public/vercel.svg Normal file
View File

@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

122
docs/styles/Home.module.css Normal file
View File

@ -0,0 +1,122 @@
.container {
min-height: 100vh;
padding: 0 0.5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.main {
padding: 5rem 0;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.footer {
width: 100%;
height: 100px;
border-top: 1px solid #eaeaea;
display: flex;
justify-content: center;
align-items: center;
}
.footer img {
margin-left: 0.5rem;
}
.footer a {
display: flex;
justify-content: center;
align-items: center;
}
.title a {
color: #0070f3;
text-decoration: none;
}
.title a:hover,
.title a:focus,
.title a:active {
text-decoration: underline;
}
.title {
margin: 0;
line-height: 1.15;
font-size: 4rem;
}
.title,
.description {
text-align: center;
}
.description {
line-height: 1.5;
font-size: 1.5rem;
}
.code {
background: #fafafa;
border-radius: 5px;
padding: 0.75rem;
font-size: 1.1rem;
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.grid {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
max-width: 800px;
margin-top: 3rem;
}
.card {
margin: 1rem;
flex-basis: 45%;
padding: 1.5rem;
text-align: left;
color: inherit;
text-decoration: none;
border: 1px solid #eaeaea;
border-radius: 10px;
transition: color 0.15s ease, border-color 0.15s ease;
}
.card:hover,
.card:focus,
.card:active {
color: #0070f3;
border-color: #0070f3;
}
.card h3 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
height: 1em;
}
@media (max-width: 600px) {
.grid {
width: 100%;
flex-direction: column;
}
}

16
docs/styles/globals.css Normal file
View File

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

3
docs/styles/tailwind.css Normal file
View File

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

22
docs/tailwind.config.js Normal file
View File

@ -0,0 +1,22 @@
const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
purge: ['./pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}'],
darkMode: false, // or 'media' or 'class'
theme: {
container: {
center: true,
},
extend: {
fontFamily: {
mono: ["Inconsolata", ...defaultTheme.fontFamily.mono]
}
},
},
variants: {
extend: {},
},
plugins: [
require('@tailwindcss/typography'),
],
}

3031
docs/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,7 +24,7 @@
"frictionless.js": "^0.13.4",
"next": "latest",
"plotly.js-basic-dist": "^1.58.4",
"postcss": "^8.1.10",
"postcss": "^8.2.10",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-plotly.js": "^2.5.1",

View File

@ -4666,10 +4666,10 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
nanoid@^3.1.16, nanoid@^3.1.20:
version "3.1.20"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
nanoid@^3.1.16, nanoid@^3.1.22:
version "3.1.23"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
nanomatch@^1.2.9:
version "1.2.13"
@ -5294,13 +5294,13 @@ postcss@^7.0.32:
source-map "^0.6.1"
supports-color "^6.1.0"
postcss@^8.1.10, postcss@^8.1.6, postcss@^8.2.1:
version "8.2.7"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.7.tgz#48ed8d88b4de10afa0dfd1c3f840aa57b55c4d47"
integrity sha512-DsVLH3xJzut+VT+rYr0mtvOtpTjSyqDwPf5EZWXcb0uAKfitGpTY9Ec+afi2+TgdN8rWS9Cs88UDYehKo/RvOw==
postcss@^8.1.6, postcss@^8.2.1, postcss@^8.2.10:
version "8.2.10"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b"
integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw==
dependencies:
colorette "^1.2.2"
nanoid "^3.1.20"
nanoid "^3.1.22"
source-map "^0.6.1"
prelude-ls@~1.1.2:

View File

@ -5,24 +5,24 @@ import parse from 'html-react-parser';
/**
* Displays a blog post page
* @param {object} props
* {
* post = {
* title: <The title of the blog post>
* content: <The body of the blog post>
* modified: <The utc date when the post was last modified.
* featured_image: <Url/relative url to post cover image
* 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>
* }
* @returns
*/
const Post = ({ page }) => {
const { title, content, modified, featured_image } = page;
const Post = ({ post }) => {
const { title, content, createdAt, featuredImage } = post;
return (
<>
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
{title}
</h1>
<p className="mb-6">Edited: {modified}</p>
<img src={featured_image} className="mb-6" alt="featured_img" />
<p className="mb-6">Posted: {createdAt}</p>
<img src={featuredImage} className="mb-6" alt="featured_img" />
<div>{parse(content)}</div>
</>
);
@ -33,8 +33,8 @@ Post.propTypes = {
page: PropTypes.shape({
title: PropTypes.string.isRequired,
content: PropTypes.string.isRequired,
modified: PropTypes.string,
featured_image: PropTypes.string,
createdAt: PropTypes.number,
featuredImage: PropTypes.string,
})
}

View File

@ -16,9 +16,6 @@ import parse from 'html-react-parser';
const PostList = ({ posts }) => {
return (
<>
<h1 className="text-3xl font-semibold text-primary my-6 inline-block">
{posts.length} posts found
</h1>
{posts.map((post, index) => (
<div key={index}>
<a

View File

@ -1,4 +1,4 @@
import React from 'react'
import React, { useState } from 'react'
import PropTypes from 'prop-types';
/**
@ -12,50 +12,33 @@ import PropTypes from 'prop-types';
* }
* @returns
*/
const Form = ({ handleChange, handleSubmit }) => {
const Form = ({ handleSubmit }) => {
const [searchQuery, setSearchQuery] = useState("")
return (
<form onSubmit={handleSubmit} className="items-center">
<form onSubmit={(e) => e.preventDefault()} className="items-center">
<div className="flex">
<input
type="text"
name="q"
value={q}
onChange={handleChange}
name="search#q"
value={searchQuery}
onChange={(e) => { setSearchQuery(e.target.value) }}
placeholder="Search"
aria-label="Search"
className="bg-white focus:outline-none focus:shadow-outline border border-gray-300 w-1/2 rounded-lg py-2 px-4 block appearance-none leading-normal"
/>
<button
onClick={handleSubmit}
onClick={() => handleSubmit(searchQuery)}
type="button"
className="inline-block text-sm px-4 py-3 mx-3 leading-none border rounded text-white bg-black border-black lg:mt-0"
>
Search
</button>
</div>
<div className="inline-block my-6 float-right">
<label htmlFor="field-order-by">Order by:</label>
<select
className="bg-white"
id="field-order-by"
name="sort"
onChange={handleChange}
onBlur={handleChange}
value={sort}
>
<option value="score:desc">Relevance</option>
<option value="title_string:asc">Name Ascending</option>
<option value="title_string:desc">Name Descending</option>
<option value="metadata_modified:desc">Last Modified</option>
<option value="views_recent:desc">Popular</option>
</select>
</div>
</form>
);
};
Form.propTypes = {
handleChange: PropTypes.func.isRequired,
handleSubmit: PropTypes.func.isRequired
}

View File

@ -21,6 +21,11 @@ import CustomLink from './components/misc/CustomLink'
import Nav from './components/ui/Nav'
import Recent from './components/ui/Recent'
//UI components
import Form from './components/search/Form'
import Item from './components/search/Item'
import ItemTotal from './components/search/Total'
export {
Table,
PlotlyChart,
@ -34,5 +39,8 @@ export {
PostList,
Org,
Error,
CustomLink
CustomLink,
Form,
Item,
ItemTotal
}

View File

@ -4557,6 +4557,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
dayjs@^1.10.4:
version "1.10.4"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.4.tgz#8e544a9b8683f61783f570980a8a80eaf54ab1e2"
integrity sha512-RI/Hh4kqRc1UKLOAf/T5zdMMX5DQIlDxwUe3wSyMMnEbGunnpENCdbUgM+dW7kXidZqCttBrmw7BhN4TMddkCw==
debug@2, debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"