Compare commits
105 Commits
luccas_exp
...
part-3-tut
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bb567e88d | ||
|
|
77da0d5693 | ||
|
|
b52aa38249 | ||
|
|
72405162a1 | ||
|
|
982733737d | ||
|
|
ea5802a908 | ||
|
|
229a7b5324 | ||
|
|
014c4c043d | ||
|
|
ed3a26cd6d | ||
|
|
026059184a | ||
|
|
a041d69282 | ||
|
|
016f3e20e9 | ||
|
|
169a92d313 | ||
|
|
14abd5b768 | ||
|
|
4aaabba229 | ||
|
|
cc43597130 | ||
|
|
d9a6ea4ef1 | ||
|
|
f6b94ee254 | ||
|
|
04b05c0896 | ||
|
|
5b4d2d1990 | ||
|
|
b7e2e8e6b8 | ||
|
|
b6100546e3 | ||
|
|
58ca032d3f | ||
|
|
4b5329a93e | ||
|
|
298b59d291 | ||
|
|
41e7f8ad8d | ||
|
|
e354009e79 | ||
|
|
ad209c8f21 | ||
|
|
b49abb3b39 | ||
|
|
6d04e2d8c3 | ||
|
|
8038662160 | ||
|
|
5a70118545 | ||
|
|
8743f0d572 | ||
|
|
48908b0842 | ||
|
|
74a4f9a8ed | ||
|
|
907015461a | ||
|
|
7450302440 | ||
|
|
926ae16c35 | ||
|
|
63ab0c4d3c | ||
|
|
a31b2e8fa3 | ||
|
|
5305cc4c2f | ||
|
|
e8bf4daf5f | ||
|
|
267267ac11 | ||
|
|
1770deb960 | ||
|
|
7002b5669c | ||
|
|
bfc124473d | ||
|
|
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 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -40,3 +40,11 @@ Thumbs.db
|
||||
|
||||
# Next.js
|
||||
.next
|
||||
|
||||
# Env
|
||||
.env
|
||||
**/.env
|
||||
|
||||
# MarkdownDB
|
||||
*.db
|
||||
**/*.db
|
||||
|
||||
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">
|
||||
🌀 Portal.JS
|
||||
<br />
|
||||
@@ -9,821 +7,48 @@ Rapidly build rich data portals using a modern frontend framework
|
||||
* [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)
|
||||
* [Docs](#Docs)
|
||||
* [Community](#Community)
|
||||
* [Appendix](#Appendix)
|
||||
* [What happened to Recline?](#What-happened-to-Recline?)
|
||||
|
||||
# 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
|
||||
|
||||
- 🗺️ 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.
|
||||
- 🗺️ 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 NextJS and Apollo.
|
||||
- 📝 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.
|
||||
- 🚀 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
|
||||
- 🏗 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)
|
||||
|
||||
# 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](#Component-List).
|
||||
#### **Check out the [Portal.JS website](https://portaljs.org/) for a gallery of live portals**
|
||||
|
||||
___
|
||||
|
||||
# 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)
|
||||
* [A portal with a CKAN backend](#Build-a-CKAN-powered-dataset-portal)
|
||||
https://portaljs.org/docs
|
||||
|
||||
> 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
|
||||
|
||||
## 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
|
||||
|
||||

|
||||
|
||||
---
|
||||
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/EeyfGrGu4U).
|
||||
|
||||
# Appendix
|
||||
|
||||
|
||||
3
examples/alan-turing-portal/.eslintrc.json
Normal file
3
examples/alan-turing-portal/.eslintrc.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
35
examples/alan-turing-portal/.gitignore
vendored
Normal file
35
examples/alan-turing-portal/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# generated files
|
||||
/public/rss/
|
||||
129
examples/alan-turing-portal/LICENSE.md
Normal file
129
examples/alan-turing-portal/LICENSE.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Tailwind UI License
|
||||
|
||||
## Personal License
|
||||
|
||||
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
|
||||
|
||||
The license grants permission to **one individual** (the Licensee) to access and use the Components and Templates.
|
||||
|
||||
You **can**:
|
||||
|
||||
- Use the Components and Templates to create unlimited End Products.
|
||||
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
|
||||
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
|
||||
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
|
||||
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
|
||||
|
||||
You **cannot**:
|
||||
|
||||
- Use the Components and Templates to create End Products that are designed to allow an End User to build their own End Products using the Components and Templates or derivatives of the Components and Templates.
|
||||
- Re-distribute the Components and Templates or derivatives of the Components and Templates separately from an End Product, neither in code or as design assets.
|
||||
- Share your access to the Components and Templates with any other individuals.
|
||||
- Use the Components and Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
|
||||
|
||||
### Example usage
|
||||
|
||||
Examples of usage **allowed** by the license:
|
||||
|
||||
- Creating a personal website by yourself.
|
||||
- Creating a website or web application for a client that will be owned by that client.
|
||||
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
|
||||
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
|
||||
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components (like a conference organization app that uses the components for its UI for example) that is free and open source, where the source code is publicly available.
|
||||
|
||||
Examples of usage **not allowed** by the license:
|
||||
|
||||
- Creating a repository of your favorite Tailwind UI components or templates (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
|
||||
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
|
||||
- Create a Figma or Sketch UI kit based on the Tailwind UI component designs.
|
||||
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
|
||||
- Creating a theme, template, or project starter kit using the components or templates and making it available either for sale or for free.
|
||||
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
|
||||
|
||||
In simple terms, use Tailwind UI for anything you like as long as it doesn't compete with Tailwind UI.
|
||||
|
||||
### Personal License Definitions
|
||||
|
||||
Licensee is the individual who has purchased a Personal License.
|
||||
|
||||
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
|
||||
|
||||
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
|
||||
|
||||
End User is a user of an End Product.
|
||||
|
||||
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
|
||||
|
||||
## Team License
|
||||
|
||||
Tailwind Labs Inc. grants you an on-going, non-exclusive license to use the Components and Templates.
|
||||
|
||||
The license grants permission for **up to 25 Employees and Contractors of the Licensee** to access and use the Components and Templates.
|
||||
|
||||
You **can**:
|
||||
|
||||
- Use the Components and Templates to create unlimited End Products.
|
||||
- Modify the Components and Templates to create derivative components and templates. Those components and templates are subject to this license.
|
||||
- Use the Components and Templates to create unlimited End Products for unlimited Clients.
|
||||
- Use the Components and Templates to create End Products where the End Product is sold to End Users.
|
||||
- Use the Components and Templates to create End Products that are open source and freely available to End Users.
|
||||
|
||||
You **cannot**:
|
||||
|
||||
- Use the Components or Templates to create End Products that are designed to allow an End User to build their own End Products using the Components or Templates or derivatives of the Components or Templates.
|
||||
- Re-distribute the Components or Templates or derivatives of the Components or Templates separately from an End Product.
|
||||
- Use the Components or Templates to create End Products that are the property of any individual or entity other than the Licensee or Clients of the Licensee.
|
||||
- Use the Components or Templates to produce anything that may be deemed by Tailwind Labs Inc, in their sole and absolute discretion, to be competitive or in conflict with the business of Tailwind Labs Inc.
|
||||
|
||||
### Example usage
|
||||
|
||||
Examples of usage **allowed** by the license:
|
||||
|
||||
- Creating a website for your company.
|
||||
- Creating a website or web application for a client that will be owned by that client.
|
||||
- Creating a commercial SaaS application (like an invoicing app for example) where end users have to pay a fee to use the application.
|
||||
- Creating a commercial self-hosted web application that is sold to end users for a one-time fee.
|
||||
- Creating a web application where the primary purpose is clearly not to simply re-distribute the components or templates (like a conference organization app that uses the components or a template for its UI for example) that is free and open source, where the source code is publicly available.
|
||||
|
||||
Examples of use **not allowed** by the license:
|
||||
|
||||
- Creating a repository of your favorite Tailwind UI components or template (or derivatives based on Tailwind UI components or templates) and publishing it publicly.
|
||||
- Creating a React or Vue version of Tailwind UI and making it available either for sale or for free.
|
||||
- Creating a "website builder" project where end users can build their own websites using components or templates included with or derived from Tailwind UI.
|
||||
- Creating a theme or template using the components or templates and making it available either for sale or for free.
|
||||
- Creating an admin panel tool (like [Laravel Nova](https://nova.laravel.com/) or [ActiveAdmin](https://activeadmin.info/)) that is made available either for sale or for free.
|
||||
- Creating any End Product that is not the sole property of either your company or a client of your company. For example your employees/contractors can't use your company Tailwind UI license to build their own websites or side projects.
|
||||
|
||||
### Team License Definitions
|
||||
|
||||
Licensee is the business entity who has purchased a Team License.
|
||||
|
||||
Components and Templates are the source code and design assets made available to the Licensee after purchasing a Tailwind UI license.
|
||||
|
||||
End Product is any artifact produced that incorporates the Components or Templates or derivatives of the Components or Templates.
|
||||
|
||||
End User is a user of an End Product.
|
||||
|
||||
Employee is a full-time or part-time employee of the Licensee.
|
||||
|
||||
Contractor is an individual or business entity contracted to perform services for the Licensee.
|
||||
|
||||
Client is an individual or entity receiving custom professional services directly from the Licensee, produced specifically for that individual or entity. Customers of software-as-a-service products are not considered clients for the purpose of this document.
|
||||
|
||||
## Enforcement
|
||||
|
||||
If you are found to be in violation of the license, access to your Tailwind UI account will be terminated, and a refund may be issued at our discretion. When license violation is blatant and malicious (such as intentionally redistributing the Components or Templates through private warez channels), no refund will be issued.
|
||||
|
||||
The copyright of the Components and Templates is owned by Tailwind Labs Inc. You are granted only the permissions described in this license; all other rights are reserved. Tailwind Labs Inc. reserves the right to pursue legal remedies for any unauthorized use of the Components or Templates outside the scope of this license.
|
||||
|
||||
## Liability
|
||||
|
||||
Tailwind Labs Inc.’s liability to you for costs, damages, or other losses arising from your use of the Components or Templates — including third-party claims against you — is limited to a refund of your license fee. Tailwind Labs Inc. may not be held liable for any consequential damages related to your use of the Components or Templates.
|
||||
|
||||
This Agreement is governed by the laws of the Province of Ontario and the applicable laws of Canada. Legal proceedings related to this Agreement may only be brought in the courts of Ontario. You agree to service of process at the e-mail address on your original order.
|
||||
|
||||
## Questions?
|
||||
|
||||
Unsure which license you need, or unsure if your use case is covered by our licenses?
|
||||
|
||||
Email us at [support@tailwindui.com](mailto:support@tailwindui.com) with your questions.
|
||||
27
examples/alan-turing-portal/README.md
Normal file
27
examples/alan-turing-portal/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Intro
|
||||
|
||||
This page catalogues datasets annotated for hate speech, online abuse, and offensive language. They may be useful for e.g. training a natural language processing system to detect this language.
|
||||
|
||||
Its built on top of [PortalJS](https://portaljs.org/), it allows you to publish datasets, lists of offensive keywords and static pages, all of those are stored as markdown files inside the `content` folder.
|
||||
|
||||
- .md files inside `content/datasets/` will appear on the dataset list section of the homepage and be searchable as well as having a individual page in `datasets/<file name>`
|
||||
- .md files inside `content/keywords/` will appear on the list of offensive keywords section of the homepage as well as having a individual page in `keywords/<file name>`
|
||||
- .md files inside `content/` will be converted to static pages in the url `/<file name>` eg: `content/about.md` becomes `/about`
|
||||
|
||||
This is also a Next.JS project so you can use the following steps to run the website locally.
|
||||
|
||||
## Getting started
|
||||
|
||||
To get started first install the npm dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Next, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Finally, open [http://localhost:3000](http://localhost:3000) in your browser to view the website.
|
||||
94
examples/alan-turing-portal/components/Card.jsx
Normal file
94
examples/alan-turing-portal/components/Card.jsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import Link from 'next/link'
|
||||
import clsx from 'clsx'
|
||||
|
||||
function ChevronRightIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Card({ as: Component = 'div', className, children }) {
|
||||
return (
|
||||
<Component
|
||||
className={clsx(className, 'group relative flex flex-col items-start')}
|
||||
>
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Link = function CardLink({ children, ...props }) {
|
||||
return (
|
||||
<>
|
||||
<div className="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
||||
<Link {...props}>
|
||||
<span className="absolute -inset-x-4 -inset-y-6 z-20 sm:-inset-x-6 sm:rounded-2xl" />
|
||||
<span className="relative z-10">{children}</span>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Title = function CardTitle({ as: Component = 'h2', href, children }) {
|
||||
return (
|
||||
<Component className="text-base font-semibold tracking-tight text-zinc-800 dark:text-zinc-100">
|
||||
{href ? <Card.Link href={href}>{children}</Card.Link> : children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Description = function CardDescription({ children }) {
|
||||
return (
|
||||
<p className="z-10 mt-2 text-sm text-zinc-600 dark:text-zinc-400">
|
||||
{children}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Cta = function CardCta({ children }) {
|
||||
return (
|
||||
<div
|
||||
aria-hidden="true"
|
||||
className="relative z-10 mt-4 flex items-center text-sm font-medium text-teal-500"
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-1 h-4 w-4 stroke-current" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Card.Eyebrow = function CardEyebrow({
|
||||
as: Component = 'p',
|
||||
decorate = false,
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}) {
|
||||
return (
|
||||
<Component
|
||||
className={clsx(
|
||||
className,
|
||||
'relative z-10 order-first mb-3 flex items-center text-sm text-zinc-400 dark:text-zinc-500',
|
||||
decorate && 'pl-3.5'
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{decorate && (
|
||||
<span
|
||||
className="absolute inset-y-0 left-0 flex items-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span className="h-4 w-0.5 rounded-full bg-zinc-200 dark:bg-zinc-500" />
|
||||
</span>
|
||||
)}
|
||||
{children}
|
||||
</Component>
|
||||
)
|
||||
}
|
||||
42
examples/alan-turing-portal/components/Container.jsx
Normal file
42
examples/alan-turing-portal/components/Container.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { forwardRef } from 'react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
const OuterContainer = forwardRef(function OuterContainer(
|
||||
{ className, children, ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div ref={ref} className={clsx('sm:px-8', className)} {...props}>
|
||||
<div className="mx-auto max-w-7xl lg:px-8">{children}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const InnerContainer = forwardRef(function InnerContainer(
|
||||
{ className, children, ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx('relative px-4 sm:px-8 lg:px-12', className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto max-w-2xl lg:max-w-5xl">{children}</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export const Container = forwardRef(function Container(
|
||||
{ children, ...props },
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<OuterContainer ref={ref} {...props}>
|
||||
<InnerContainer>{children}</InnerContainer>
|
||||
</OuterContainer>
|
||||
)
|
||||
})
|
||||
|
||||
Container.Outer = OuterContainer
|
||||
Container.Inner = InnerContainer
|
||||
36
examples/alan-turing-portal/components/Footer.jsx
Normal file
36
examples/alan-turing-portal/components/Footer.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
import { Container } from '../components/Container'
|
||||
|
||||
function NavLink({ href, children }) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="transition hover:text-teal-500 dark:hover:text-teal-400"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="mt-32">
|
||||
<Container.Outer>
|
||||
<div className="border-t border-zinc-100 pb-16 pt-10 dark:border-zinc-700/40">
|
||||
<Container.Inner>
|
||||
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||
<p className="text-sm font-medium text-zinc-800 dark:text-zinc-200">
|
||||
Built with <a href='https://portaljs.org'>PortalJS 🌀</a>
|
||||
</p>
|
||||
<p className="text-sm text-zinc-400 dark:text-zinc-500">
|
||||
© {new Date().getFullYear()} Leon Derczynski. All rights
|
||||
reserved.
|
||||
</p>
|
||||
</div>
|
||||
</Container.Inner>
|
||||
</div>
|
||||
</Container.Outer>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
109
examples/alan-turing-portal/components/Header.jsx
Normal file
109
examples/alan-turing-portal/components/Header.jsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useRef } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { Container } from '../components/Container'
|
||||
|
||||
function SunIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<path d="M8 12.25A4.25 4.25 0 0 1 12.25 8v0a4.25 4.25 0 0 1 4.25 4.25v0a4.25 4.25 0 0 1-4.25 4.25v0A4.25 4.25 0 0 1 8 12.25v0Z" />
|
||||
<path
|
||||
d="M12.25 3v1.5M21.5 12.25H20M18.791 18.791l-1.06-1.06M18.791 5.709l-1.06 1.06M12.25 20v1.5M4.5 12.25H3M6.77 6.77 5.709 5.709M6.77 17.73l-1.061 1.061"
|
||||
fill="none"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function MoonIcon(props) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
|
||||
<path
|
||||
d="M17.25 16.22a6.937 6.937 0 0 1-9.47-9.47 7.451 7.451 0 1 0 9.47 9.47ZM12.75 7C17 7 17 2.75 17 2.75S17 7 21.25 7C17 7 17 11.25 17 11.25S17 7 12.75 7Z"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ModeToggle() {
|
||||
function disableTransitionsTemporarily() {
|
||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||
window.setTimeout(() => {
|
||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function toggleMode() {
|
||||
disableTransitionsTemporarily()
|
||||
|
||||
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
let isSystemDarkMode = darkModeMediaQuery.matches
|
||||
let isDarkMode = document.documentElement.classList.toggle('dark')
|
||||
|
||||
if (isDarkMode === isSystemDarkMode) {
|
||||
delete window.localStorage.isDarkMode
|
||||
} else {
|
||||
window.localStorage.isDarkMode = isDarkMode
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Toggle dark mode"
|
||||
className="group rounded-full bg-white/90 px-3 py-2 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur transition dark:bg-zinc-800/90 dark:ring-white/10 dark:hover:ring-white/20"
|
||||
onClick={toggleMode}
|
||||
>
|
||||
<SunIcon className="h-6 w-6 fill-zinc-100 stroke-zinc-500 transition group-hover:fill-zinc-200 group-hover:stroke-zinc-700 dark:hidden [@media(prefers-color-scheme:dark)]:fill-teal-50 [@media(prefers-color-scheme:dark)]:stroke-teal-500 [@media(prefers-color-scheme:dark)]:group-hover:fill-teal-50 [@media(prefers-color-scheme:dark)]:group-hover:stroke-teal-600" />
|
||||
<MoonIcon className="hidden h-6 w-6 fill-zinc-700 stroke-zinc-500 transition dark:block [@media(prefers-color-scheme:dark)]:group-hover:stroke-zinc-400 [@media_not_(prefers-color-scheme:dark)]:fill-teal-400/10 [@media_not_(prefers-color-scheme:dark)]:stroke-teal-500" />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
let isHomePage = useRouter().pathname === '/'
|
||||
|
||||
let headerRef = useRef()
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className="pointer-events-none relative z-50 flex flex-col"
|
||||
style={{
|
||||
height: 'var(--header-height)',
|
||||
marginBottom: 'var(--header-mb)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={headerRef}
|
||||
className="top-0 z-10 h-16 pt-6"
|
||||
style={{ position: 'var(--header-position)' }}
|
||||
>
|
||||
<Container
|
||||
className="top-[var(--header-top,theme(spacing.6))] w-full"
|
||||
style={{ position: 'var(--header-inner-position)' }}
|
||||
>
|
||||
<div className="relative flex gap-4">
|
||||
<div className="flex justify-end md:flex-1">
|
||||
<div className="pointer-events-auto">
|
||||
<ModeToggle />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
</header>
|
||||
{isHomePage && <div style={{ height: 'var(--content-offset)' }} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
5
examples/alan-turing-portal/content/about.md
Normal file
5
examples/alan-turing-portal/content/about.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: About
|
||||
---
|
||||
|
||||
This is an about page, left here as an example
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: AbuseEval v1.0
|
||||
link-to-publication: http://www.lrec-conf.org/proceedings/lrec2020/pdf/2020.lrec-1.760.pdf
|
||||
link-to-data: https://github.com/tommasoc80/AbuseEval
|
||||
task-description: Explicitness annotation of offensive and abusive content
|
||||
details-of-task: "Enriched versions of the OffensEval/OLID dataset with the distinction of explicit/implicit offensive messages and the new dimension for abusive messages. Labels for offensive language: EXPLICIT, IMPLICT, NOT; Labels for abusive language: EXPLICIT, IMPLICT, NOTABU"
|
||||
size-of-dataset: 14100
|
||||
percentage-abusive: 20.75
|
||||
language: English
|
||||
level-of-annotation: ["Tweets"]
|
||||
platform: ["Twitter"]
|
||||
medium: ["Text"]
|
||||
reference: "Caselli, T., Basile, V., Jelena, M., Inga, K., and Michael, G. 2020. \"I feel offended, don’t be abusive! implicit/explicit messages in offensive and abusive language\". The 12th Language Resources and Evaluation Conference (pp. 6193-6202). European Language Resources Association."
|
||||
---
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
title: "Abusive Language Detection on Arabic Social Media (Al Jazeera)"
|
||||
link-to-publication: https://www.aclweb.org/anthology/W17-3008
|
||||
link-to-data: http://alt.qcri.org/~hmubarak/offensive/AJCommentsClassification-CF.xlsx
|
||||
task-description: Ternary (Obscene, Offensive but not obscene, Clean)
|
||||
details-of-task: Incivility
|
||||
size-of-dataset: 32000
|
||||
percentage-abusive: 0.81
|
||||
language: Arabic
|
||||
level-of-annotation: ["Posts"]
|
||||
platform: ["AlJazeera"]
|
||||
medium: ["Text"]
|
||||
reference: "Mubarak, H., Darwish, K. and Magdy, W., 2017. Abusive Language Detection on Arabic Social Media. In: Proceedings of the First Workshop on Abusive Language Online. Vancouver, Canada: Association for Computational Linguistics, pp.52-56."
|
||||
---
|
||||
|
||||
SOMETHING TEST
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: "CoRAL: a Context-aware Croatian Abusive Language Dataset"
|
||||
link-to-publication: https://aclanthology.org/2022.findings-aacl.21/
|
||||
link-to-data: https://github.com/shekharRavi/CoRAL-dataset-Findings-of-the-ACL-AACL-IJCNLP-2022
|
||||
task-description: Multi-class based on context dependency categories (CDC)
|
||||
details-of-task: Detectioning CDC from abusive comments
|
||||
size-of-dataset: 2240
|
||||
percentage-abusive: 100
|
||||
language: "Croatian"
|
||||
level-of-annotation: ["Posts"]
|
||||
platform: ["Posts"]
|
||||
medium: ["Newspaper Comments"]
|
||||
reference: "Ravi Shekhar, Mladen Karan and Matthew Purver (2022). CoRAL: a Context-aware Croatian Abusive Language Dataset. Findings of the ACL: AACL-IJCNLP."
|
||||
---
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Detecting Abusive Albanian
|
||||
link-to-publication: https://arxiv.org/abs/2107.13592
|
||||
link-to-data: https://doi.org/10.6084/m9.figshare.19333298.v1
|
||||
task-description: Hierarchical (offensive/not; untargeted/targeted; person/group/other)
|
||||
details-of-task: Detect and categorise abusive language in social media data
|
||||
size-of-dataset: 11874
|
||||
percentage-abusive: 13.2
|
||||
language: Albanian
|
||||
level-of-annotation: ["Posts"]
|
||||
platform: ["Instagram", "Youtube"]
|
||||
medium: ["Text"]
|
||||
reference: "Nurce, E., Keci, J., Derczynski, L., 2021. Detecting Abusive Albanian. arXiv:2107.13592"
|
||||
---
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
title: "Hate Speech Detection in the Bengali language: A Dataset and its Baseline Evaluation"
|
||||
link-to-publication: https://arxiv.org/pdf/2012.09686.pdf
|
||||
link-to-data: https://www.kaggle.com/naurosromim/bengali-hate-speech-dataset
|
||||
task-description: Binary (hateful, not)
|
||||
details-of-task: "Several categories: sports, entertainment, crime, religion, politics, celebrity and meme"
|
||||
size-of-dataset: 30000
|
||||
percentage-abusive: 0.33
|
||||
language: Bengali
|
||||
level-of-annotation: ["Posts"]
|
||||
platform: ["Youtube", "Facebook"]
|
||||
medium: ["Text"]
|
||||
reference: "Romim, N., Ahmed, M., Talukder, H., & Islam, M. S. (2021). Hate speech detection in the bengali language: A dataset and its baseline evaluation. In Proceedings of International Joint Conference on Advances in Computational Intelligence (pp. 457-468). Springer, Singapore."
|
||||
---
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Large-Scale Hate Speech Detection with Cross-Domain Transfer
|
||||
link-to-publication: https://aclanthology.org/2022.lrec-1.238/
|
||||
link-to-data: https://github.com/avaapm/hatespeech
|
||||
task-description: Three-class (Hate speech, Offensive language, None)
|
||||
details-of-task: Hate speech detection on social media (Twitter) including 5 target groups (gender, race, religion, politics, sports)
|
||||
size-of-dataset: "100k English (27593 hate, 30747 offensive, 41660 none)"
|
||||
percentage-abusive: 58.3
|
||||
language: English
|
||||
level-of-annotation: ["Posts"]
|
||||
platform: ["Twitter"]
|
||||
medium: ["Text", "Image"]
|
||||
reference: "Cagri Toraman, Furkan Şahinuç, Eyup Yilmaz. 2022. Large-Scale Hate Speech Detection with Cross-Domain Transfer. In Proceedings of the Thirteenth Language Resources and Evaluation Conference, pages 2215–2225, Marseille, France. European Language Resources Association."
|
||||
---
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: "Let-Mi: An Arabic Levantine Twitter Dataset for Misogynistic Language"
|
||||
link-to-publication: https://arxiv.org/abs/2103.10195
|
||||
link-to-data: https://drive.google.com/file/d/1mM2vnjsy7QfUmdVUpKqHRJjZyQobhTrW/view
|
||||
task-description: Binary (misogyny/none) and Multi-class (none, discredit, derailing, dominance, stereotyping & objectification, threat of violence, sexual harassment, damning)
|
||||
details-of-task: Introducing an Arabic Levantine Twitter dataset for Misogynistic language
|
||||
size-of-dataset: 6603
|
||||
percentage-abusive: 48.76
|
||||
language: Arabic
|
||||
level-of-annotation: ["Posts"]
|
||||
platform: ["Twitter"]
|
||||
medium: ["Text", "Images"]
|
||||
reference: "Hala Mulki and Bilal Ghanem. 2021. Let-Mi: An Arabic Levantine Twitter Dataset for Misogynistic Language. In Proceedings of the Sixth Arabic Natural Language Processing Workshop, pages 154–163, Kyiv, Ukraine (Virtual). Association for Computational Linguistics"
|
||||
---
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Measuring Hate Speech
|
||||
link-to-publication: https://arxiv.org/abs/2009.10277
|
||||
link-to-data: https://huggingface.co/datasets/ucberkeley-dlab/measuring-hate-speech
|
||||
task-description: 10 ordinal labels (sentiment, (dis)respect, insult, humiliation, inferior status, violence, dehumanization, genocide, attack/defense, hate speech), which are debiased and aggregated into a continuous hate speech severity score (hate_speech_score) that includes a region for counterspeech & supportive speeech. Includes 8 target identity groups (race/ethnicity, religion, national origin/citizenship, gender, sexual orientation, age, disability, political ideology) and 42 identity subgroups.
|
||||
details-of-task: Hate speech measurement on social media in English
|
||||
size-of-dataset: "39,565 comments annotated by 7,912 annotators on 10 ordinal labels, for 1,355,560 total labels."
|
||||
percentage-abusive: 25
|
||||
language: English
|
||||
level-of-annotation: ["Social media comment"]
|
||||
platform: ["Twitter", "Reddit", "Youtube"]
|
||||
medium: ["Text"]
|
||||
reference: "Kennedy, C. J., Bacon, G., Sahn, A., & von Vacano, C. (2020). Constructing interval variables via faceted Rasch measurement and multitask deep learning: a hate speech application. arXiv preprint arXiv:2009.10277."
|
||||
---
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: Offensive Language and Hate Speech Detection for Danish
|
||||
link-to-publication: http://www.derczynski.com/papers/danish_hsd.pdf
|
||||
link-to-data: https://figshare.com/articles/Danish_Hate_Speech_Abusive_Language_data/12220805
|
||||
task-description: "Branching structure of tasks: Binary (Offensive, Not), Within Offensive (Target, Not), Within Target (Individual, Group, Other)"
|
||||
details-of-task: Group-directed + Person-directed
|
||||
size-of-dataset: 3600
|
||||
percentage-abusive: 0.12
|
||||
language: Danish
|
||||
level-of-annotation: ["Posts"]
|
||||
platform: ["Twitter", "Reddit", "Newspaper comments"]
|
||||
medium: ["Text"]
|
||||
reference: "Sigurbergsson, G. and Derczynski, L., 2019. Offensive Language and Hate Speech Detection for Danish. ArXiv."
|
||||
---
|
||||
34
examples/alan-turing-portal/content/index.md
Normal file
34
examples/alan-turing-portal/content/index.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: Hate Speech Dataset Catalogue
|
||||
---
|
||||
|
||||
This page catalogues datasets annotated for hate speech, online abuse, and offensive language. They may be useful for e.g. training a natural language processing system to detect this language.
|
||||
|
||||
The list is maintained by Leon Derczynski, Bertie Vidgen, Hannah Rose Kirk, Pica Johansson, Yi-Ling Chung, Mads Guldborg Kjeldgaard Kongsbak, Laila Sprejer, and Philine Zeinert.
|
||||
|
||||
We provide a list of datasets and keywords. If you would like to contribute to our catalogue or add your dataset, please see the instructions for contributing.
|
||||
|
||||
If you use these resources, please cite (and read!) our paper: Directions in Abusive Language Training Data: Garbage In, Garbage Out. And if you would like to find other resources for researching online hate, visit The Alan Turing Institute’s Online Hate Research Hub or read The Alan Turing Institute’s Reading List on Online Hate and Abuse Research.
|
||||
|
||||
If you’re looking for a good paper on online hate training datasets (beyond our paper, of course!) then have a look at ‘Resources and benchmark corpora for hate speech detection: a systematic review’ by Poletto et al. in Language Resources and Evaluation.
|
||||
|
||||
## How to contribute
|
||||
|
||||
We accept entries to our catalogue based on pull requests to the content folder. The dataset must be avaliable for download to be included in the list. If you want to add an entry, follow these steps!
|
||||
|
||||
Please send just one dataset addition/edit at a time - edit it in, then save. This will make everyone’s life easier (including yours!)
|
||||
|
||||
- Go to the repo url file and click the "Add file" dropdown and then click on "Create new file".
|
||||

|
||||
|
||||
- In the following page type `content/datasets/<name-of-the-file>.md`. if you want to add an entry to the datasets catalog or `content/keywords/<name-of-the-file>.md` if you want to add an entry to the lists of abusive keywords, if you want to just add an static page you can leave in the root of `content` it will automatically get assigned an url eg: `/content/about.md` becomes the `/about` page
|
||||

|
||||
|
||||
- Copy the contents of `templates/dataset.md` or `templates/keywords.md` respectively to the camp below, filling out the fields with the correct data format
|
||||

|
||||
|
||||
- Click on "Commit changes", on the popup make sure you give some brief detail on the proposed change. and then click on Propose changes
|
||||
<img src='https://i.imgur.com/BxuxKEJ.png' style={{ maxWidth: '50%', margin: '0 auto' }}/>
|
||||
|
||||
- Submit the pull request on the next page when prompted.
|
||||
|
||||
10
examples/alan-turing-portal/content/keywords/hurtlex.md
Normal file
10
examples/alan-turing-portal/content/keywords/hurtlex.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
title: Hurtlex
|
||||
description: HurtLex is a lexicon of offensive, aggressive, and hateful words in over 50 languages. The words are divided into 17 categories, plus a macro-category indicating whether there is stereotype involved.
|
||||
data-link: https://github.com/valeriobasile/hurtlex
|
||||
reference: http://ceur-ws.org/Vol-2253/paper49.pdf, Proc. CLiC-it 2018
|
||||
---
|
||||
|
||||
## Markdown TEST
|
||||
|
||||
Some text
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: SexHateLex is a Chinese lexicon of hateful and sexist words.
|
||||
data-link: https://doi.org/10.5281/zenodo.4773875
|
||||
reference: http://ceur-ws.org/Vol-2253/paper49.pdf, Journal of OSNEM, Vol.27, 2022, 100182, ISSN 2468-6964.
|
||||
---
|
||||
11
examples/alan-turing-portal/jsconfig.json
Normal file
11
examples/alan-turing-portal/jsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/components/*": ["components/*"],
|
||||
"@/pages/*": ["pages/*"],
|
||||
"@/lib/*": ["lib/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8
examples/alan-turing-portal/lib/formatDate.js
Normal file
8
examples/alan-turing-portal/lib/formatDate.js
Normal file
@@ -0,0 +1,8 @@
|
||||
export function formatDate(dateString) {
|
||||
return new Date(`${dateString}T00:00:00Z`).toLocaleDateString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric',
|
||||
timeZone: 'UTC',
|
||||
})
|
||||
}
|
||||
98
examples/alan-turing-portal/lib/markdown.js
Normal file
98
examples/alan-turing-portal/lib/markdown.js
Normal file
@@ -0,0 +1,98 @@
|
||||
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 } = matter(source);
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
mdxSource: mdxSource,
|
||||
frontMatter: data,
|
||||
};
|
||||
};
|
||||
|
||||
export default parse;
|
||||
14
examples/alan-turing-portal/lib/mddb.ts
Normal file
14
examples/alan-turing-portal/lib/mddb.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { MarkdownDB } from "@flowershow/markdowndb";
|
||||
|
||||
const dbPath = "markdown.db";
|
||||
|
||||
const client = new MarkdownDB({
|
||||
client: "sqlite3",
|
||||
connection: {
|
||||
filename: dbPath,
|
||||
},
|
||||
});
|
||||
|
||||
const clientPromise = client.init();
|
||||
|
||||
export default clientPromise;
|
||||
BIN
examples/alan-turing-portal/markdown.db
Normal file
BIN
examples/alan-turing-portal/markdown.db
Normal file
Binary file not shown.
5
examples/alan-turing-portal/next-env.d.ts
vendored
Normal file
5
examples/alan-turing-portal/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.
|
||||
3
examples/alan-turing-portal/next.config.js
Normal file
3
examples/alan-turing-portal/next.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
swcMinify: true,
|
||||
};
|
||||
22977
examples/alan-turing-portal/package-lock.json
generated
Normal file
22977
examples/alan-turing-portal/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
73
examples/alan-turing-portal/package.json
Normal file
73
examples/alan-turing-portal/package.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "tailwindui-template",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"prebuild": "npm run mddb",
|
||||
"mddb": "mddb ./content"
|
||||
},
|
||||
"browserslist": "defaults, not ie <= 11",
|
||||
"dependencies": {
|
||||
"@flowershow/core": "^0.4.10",
|
||||
"@flowershow/markdowndb": "^0.1.1",
|
||||
"@flowershow/remark-callouts": "^1.0.0",
|
||||
"@flowershow/remark-embed": "^1.0.0",
|
||||
"@flowershow/remark-wiki-link": "^1.1.2",
|
||||
"@headlessui/react": "^1.7.13",
|
||||
"@heroicons/react": "^2.0.17",
|
||||
"@mapbox/rehype-prism": "^0.8.0",
|
||||
"@mdx-js/loader": "^2.1.5",
|
||||
"@mdx-js/react": "^2.1.5",
|
||||
"@next/mdx": "^13.0.2",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.4",
|
||||
"@tanstack/react-table": "^8.8.5",
|
||||
"@types/node": "18.16.0",
|
||||
"@types/react": "18.2.0",
|
||||
"@types/react-dom": "18.2.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"clsx": "^1.2.1",
|
||||
"eslint": "8.39.0",
|
||||
"eslint-config-next": "13.3.1",
|
||||
"fast-glob": "^3.2.11",
|
||||
"feed": "^4.2.2",
|
||||
"flexsearch": "^0.7.31",
|
||||
"focus-visible": "^5.2.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hastscript": "^7.2.0",
|
||||
"mdx-mermaid": "^2.0.0-rc7",
|
||||
"mermaid": "^10.1.0",
|
||||
"next": "13.2.1",
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"next-router-mock": "^0.9.3",
|
||||
"next-superjson-plugin": "^0.5.7",
|
||||
"papaparse": "^5.4.1",
|
||||
"postcss-focus-visible": "^6.0.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-markdown": "^8.0.7",
|
||||
"react-vega": "^7.6.0",
|
||||
"rehype-autolink-headings": "^6.1.1",
|
||||
"rehype-katex": "^6.0.3",
|
||||
"rehype-prism-plus": "^1.5.1",
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"remark-smartypants": "^2.0.0",
|
||||
"remark-toc": "^8.0.1",
|
||||
"superjson": "^1.12.3",
|
||||
"tailwindcss": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-next": "13.0.2",
|
||||
"prettier": "^2.8.7",
|
||||
"prettier-plugin-tailwindcss": "^0.2.6"
|
||||
}
|
||||
}
|
||||
105
examples/alan-turing-portal/pages/[...slug].jsx
Normal file
105
examples/alan-turing-portal/pages/[...slug].jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Container } from '../components/Container'
|
||||
import clientPromise from '../lib/mddb'
|
||||
import { promises as fs } from 'fs';
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import { Card } from '../components/Card'
|
||||
import Head from 'next/head'
|
||||
import parse from '../lib/markdown'
|
||||
import { Mermaid } from '@flowershow/core';
|
||||
|
||||
export const getStaticProps = async ({ params }) => {
|
||||
const urlPath = params.slug ? params.slug.join('/') : ''
|
||||
|
||||
const mddb = await clientPromise
|
||||
const dbFile = await mddb.getFileByUrl(urlPath)
|
||||
|
||||
const source = await fs.readFile(dbFile.file_path,'utf-8')
|
||||
let mdxSource = await parse(source, '.mdx')
|
||||
|
||||
return {
|
||||
props: {
|
||||
mdxSource,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const mddb = await clientPromise
|
||||
const allDocuments = await mddb.getFiles({ extensions: ['md', 'mdx'] })
|
||||
|
||||
const paths = allDocuments.filter(document => document.url_path !== '/').map((page) => {
|
||||
const parts = page.url_path.split('/')
|
||||
return { params: { slug: parts } }
|
||||
})
|
||||
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
}
|
||||
}
|
||||
|
||||
const isValidUrl = (urlString) => {
|
||||
try {
|
||||
return Boolean(new URL(urlString))
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const Meta = ({keyValuePairs}) => {
|
||||
const prettifyMetaValue = (value) => value.replaceAll('-',' ').charAt(0).toUpperCase() + value.replaceAll('-',' ').slice(1);
|
||||
return (
|
||||
<>
|
||||
{keyValuePairs.map((entry) => {
|
||||
return isValidUrl(entry[1]) ? (
|
||||
<Card.Description>
|
||||
<span className="font-semibold">
|
||||
{prettifyMetaValue(entry[0])}: {' '}
|
||||
</span>
|
||||
<a
|
||||
className="text-ellipsis underline transition hover:text-teal-400 dark:hover:text-teal-900"
|
||||
href={entry[1]}
|
||||
>
|
||||
{entry[1]}
|
||||
</a>
|
||||
</Card.Description>
|
||||
) : (
|
||||
<Card.Description>
|
||||
<span className="font-semibold">{prettifyMetaValue(entry[0])}: </span>
|
||||
{Array.isArray(entry[1]) ? entry[1].join(', ') : entry[1]}
|
||||
</Card.Description>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default function DRDPage({ mdxSource }) {
|
||||
const meta = mdxSource.frontMatter
|
||||
const keyValuePairs = Object.entries(meta).filter(
|
||||
(entry) => entry[0] !== 'title'
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{meta.title}</title>
|
||||
</Head>
|
||||
<Container className="mt-16 lg:mt-32">
|
||||
<article>
|
||||
<header className="flex flex-col">
|
||||
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
{meta.title}
|
||||
</h1>
|
||||
<Card as="article">
|
||||
<Meta keyValuePairs={keyValuePairs} />
|
||||
</Card>
|
||||
</header>
|
||||
<div className="prose dark:prose-invert">
|
||||
<MDXRemote {...mdxSource.mdxSource} components={{mermaid: Mermaid}} />
|
||||
</div>
|
||||
</article>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
38
examples/alan-turing-portal/pages/_app.jsx
Normal file
38
examples/alan-turing-portal/pages/_app.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
import { Footer } from '../components/Footer'
|
||||
import { Header } from '../components/Header'
|
||||
|
||||
import '../styles/tailwind.css'
|
||||
import 'focus-visible'
|
||||
|
||||
function usePrevious(value) {
|
||||
let ref = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
ref.current = value
|
||||
}, [value])
|
||||
|
||||
return ref.current
|
||||
}
|
||||
|
||||
export default function App({ Component, pageProps, router }) {
|
||||
let previousPathname = usePrevious(router.pathname)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="fixed inset-0 flex justify-center sm:px-8">
|
||||
<div className="flex w-full max-w-7xl lg:px-8">
|
||||
<div className="w-full bg-white ring-1 ring-zinc-100 dark:bg-zinc-900 dark:ring-zinc-300/20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Header />
|
||||
<main>
|
||||
<Component previousPathname={previousPathname} {...pageProps} />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
60
examples/alan-turing-portal/pages/_document.jsx
Normal file
60
examples/alan-turing-portal/pages/_document.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Head, Html, Main, NextScript } from 'next/document'
|
||||
|
||||
const modeScript = `
|
||||
let darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
|
||||
updateMode()
|
||||
darkModeMediaQuery.addEventListener('change', updateModeWithoutTransitions)
|
||||
window.addEventListener('storage', updateModeWithoutTransitions)
|
||||
|
||||
function updateMode() {
|
||||
let isSystemDarkMode = darkModeMediaQuery.matches
|
||||
let isDarkMode = window.localStorage.isDarkMode === 'true' || (!('isDarkMode' in window.localStorage) && isSystemDarkMode)
|
||||
|
||||
if (isDarkMode) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
|
||||
if (isDarkMode === isSystemDarkMode) {
|
||||
delete window.localStorage.isDarkMode
|
||||
}
|
||||
}
|
||||
|
||||
function disableTransitionsTemporarily() {
|
||||
document.documentElement.classList.add('[&_*]:!transition-none')
|
||||
window.setTimeout(() => {
|
||||
document.documentElement.classList.remove('[&_*]:!transition-none')
|
||||
}, 0)
|
||||
}
|
||||
|
||||
function updateModeWithoutTransitions() {
|
||||
disableTransitionsTemporarily()
|
||||
updateMode()
|
||||
}
|
||||
`
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html className="h-full antialiased" lang="en">
|
||||
<Head>
|
||||
<script dangerouslySetInnerHTML={{ __html: modeScript }} />
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/rss+xml"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/rss/feed.xml`}
|
||||
/>
|
||||
<link
|
||||
rel="alternate"
|
||||
type="application/feed+json"
|
||||
href={`${process.env.NEXT_PUBLIC_SITE_URL}/rss/feed.json`}
|
||||
/>
|
||||
</Head>
|
||||
<body className="flex h-full flex-col bg-zinc-50 dark:bg-black">
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
}
|
||||
281
examples/alan-turing-portal/pages/index.jsx
Normal file
281
examples/alan-turing-portal/pages/index.jsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import Head from 'next/head'
|
||||
import fs from 'fs'
|
||||
|
||||
import { Card } from '../components/Card'
|
||||
import { Container } from '../components/Container'
|
||||
import clientPromise from '../lib/mddb'
|
||||
import { Index } from 'flexsearch'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import Link from 'next/link'
|
||||
import { serialize } from 'next-mdx-remote/serialize'
|
||||
import { MDXRemote } from 'next-mdx-remote'
|
||||
|
||||
function DatasetCard({ dataset }) {
|
||||
return (
|
||||
<Card as="article">
|
||||
<Card.Title><Link href={dataset.url}>{dataset.title}</Link></Card.Title>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Link to publication: </span>{' '}
|
||||
<a
|
||||
className="text-ellipsis underline transition hover:text-teal-400 dark:hover:text-teal-900"
|
||||
href={dataset['link-to-publication']}
|
||||
>
|
||||
{dataset['link-to-publication']}
|
||||
</a>
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Link to data: </span>
|
||||
<a
|
||||
className="text-ellipsis underline transition hover:text-teal-600 dark:hover:text-teal-900"
|
||||
href={dataset['link-to-data']}
|
||||
>
|
||||
{dataset['link-to-data']}
|
||||
</a>
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Task Description: </span>
|
||||
{dataset['task-description']}
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Details of Task: </span>{' '}
|
||||
{dataset['details-of-task']}
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Size of Dataset: </span>{' '}
|
||||
{dataset['size-of-dataset']}
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Percentage Abusive: </span>
|
||||
{dataset['percentage-abusive']}%
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Language: </span>
|
||||
{dataset['language']}
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Level of Annotation: </span>
|
||||
{dataset['level-of-annotation'].join(', ')}
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Platform: </span>
|
||||
{dataset['platform'].join(', ')}
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Medium: </span>
|
||||
{dataset['medium'].join(', ')}
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Reference: </span>
|
||||
{dataset['reference']}
|
||||
</Card.Description>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function ListOfAbusiveKeywordsCard({ list }) {
|
||||
return (
|
||||
<Card as="article">
|
||||
<Card.Title><Link href={list.url}>{list.title}</Link></Card.Title>
|
||||
{list.description && (
|
||||
<Card.Description>
|
||||
<span className="font-semibold">List Description: </span>{' '}
|
||||
{list.description}
|
||||
</Card.Description>
|
||||
)}
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Data Link: </span>
|
||||
<a
|
||||
className="text-ellipsis underline transition hover:text-teal-600 dark:hover:text-teal-900"
|
||||
href={list['data-link']}
|
||||
>
|
||||
{list['data-link']}
|
||||
</a>
|
||||
</Card.Description>
|
||||
<Card.Description>
|
||||
<span className="font-semibold">Reference: </span>
|
||||
<a
|
||||
className="text-ellipsis underline transition hover:text-teal-600 dark:hover:text-teal-900"
|
||||
href={list.reference}
|
||||
>
|
||||
{list.reference}
|
||||
</a>
|
||||
</Card.Description>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Home({
|
||||
datasets,
|
||||
indexText,
|
||||
listsOfKeywords,
|
||||
availableLanguages,
|
||||
availablePlatforms,
|
||||
}) {
|
||||
const index = new Index()
|
||||
datasets.forEach((dataset) =>
|
||||
index.add(
|
||||
dataset.id,
|
||||
`${dataset.title} ${dataset['task-description']} ${dataset['details-of-task']} ${dataset['reference']}`
|
||||
)
|
||||
)
|
||||
const { register, watch, handleSubmit, reset } = useForm({
|
||||
defaultValues: {
|
||||
searchTerm: '',
|
||||
lang: '',
|
||||
platform: '',
|
||||
},
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Hate Speech Dataset Catalogue</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Catalog of abusive language data (PLoS 2020)"
|
||||
/>
|
||||
</Head>
|
||||
<Container className="mt-9">
|
||||
<div className="max-w-2xl">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
{indexText.frontmatter.title}
|
||||
</h1>
|
||||
<article className="mt-6 index-text flex flex-col gap-y-2 text-base text-zinc-600 dark:text-zinc-400 prose dark:prose-invert">
|
||||
<MDXRemote {...indexText} />
|
||||
</article>
|
||||
</div>
|
||||
</Container>
|
||||
<Container className="mt-24 md:mt-28">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-y-8 lg:max-w-none">
|
||||
<h2 className="text-xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
Datasets
|
||||
</h2>
|
||||
<form onSubmit={handleSubmit(() => reset())} className="rounded-2xl border border-zinc-100 px-4 py-6 dark:border-zinc-700/40 sm:p-6">
|
||||
<p className="mt-2 text-lg font-semibold text-zinc-600 dark:text-zinc-100">
|
||||
Search for datasets
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
<input
|
||||
placeholder="Search here"
|
||||
aria-label="Hate speech on Twitter"
|
||||
{...register('searchTerm')}
|
||||
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] shadow-md shadow-zinc-800/5 placeholder:text-zinc-600 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-200 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
|
||||
/>
|
||||
<select
|
||||
placeholder="Language"
|
||||
defaultValue=""
|
||||
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] text-zinc-600 shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
|
||||
{...register('lang')}
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Filter by language
|
||||
</option>
|
||||
{availableLanguages.map((lang) => (
|
||||
<option
|
||||
key={lang}
|
||||
className="dark:bg-white dark:text-black"
|
||||
value={lang}
|
||||
>
|
||||
{lang}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
placeholder="Platform"
|
||||
defaultValue=""
|
||||
className="min-w-0 flex-auto appearance-none rounded-md border border-zinc-900/10 bg-white px-3 py-[calc(theme(spacing.2)-1px)] text-zinc-600 shadow-md shadow-zinc-800/5 placeholder:text-zinc-400 focus:border-teal-500 focus:outline-none focus:ring-4 focus:ring-teal-500/10 dark:border-zinc-700 dark:bg-zinc-700/[0.15] dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-teal-400 dark:focus:ring-teal-400/10 sm:text-sm"
|
||||
{...register('platform')}
|
||||
>
|
||||
<option value="" disabled hidden>
|
||||
Filter by platform
|
||||
</option>
|
||||
{availablePlatforms.map((platform) => (
|
||||
<option
|
||||
key={platform}
|
||||
className="dark:bg-white dark:text-black"
|
||||
value={platform}
|
||||
>
|
||||
{platform}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button type='submit' className='inline-flex items-center gap-2 justify-center rounded-md py-2 px-3 text-sm outline-offset-2 transition active:transition-none bg-zinc-800 font-semibold text-zinc-100 hover:bg-zinc-700 active:bg-zinc-800 active:text-zinc-100/70 dark:bg-zinc-700 dark:hover:bg-zinc-600 dark:active:bg-zinc-700 dark:active:text-zinc-100/70 flex-none'>Clear filters</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className="flex flex-col gap-16">
|
||||
{datasets
|
||||
.filter((dataset) =>
|
||||
watch().searchTerm && watch().searchTerm !== ''
|
||||
? index.search(watch().searchTerm).includes(dataset.id)
|
||||
: true
|
||||
)
|
||||
.filter((dataset) =>
|
||||
watch().lang && watch().lang !== ''
|
||||
? dataset.language === watch().lang
|
||||
: true
|
||||
)
|
||||
.filter((dataset) =>
|
||||
watch().platform && watch().platform !== ''
|
||||
? dataset.platform.includes(watch().platform)
|
||||
: true
|
||||
)
|
||||
.map((dataset) => (
|
||||
<DatasetCard key={dataset.title} dataset={dataset} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
<Container className="mt-16">
|
||||
<h2 className="text-xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
Lists of Abusive Keywords
|
||||
</h2>
|
||||
<div className="mt-3 flex flex-col gap-16">
|
||||
{listsOfKeywords.map((list) => (
|
||||
<ListOfAbusiveKeywordsCard key={list.title} list={list} />
|
||||
))}
|
||||
</div>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const mddb = await clientPromise
|
||||
const datasetPages = await mddb.getFiles({
|
||||
folder: 'datasets',
|
||||
extensions: ['md', 'mdx'],
|
||||
})
|
||||
const datasets = datasetPages.map((page) => ({
|
||||
...page.metadata,
|
||||
id: page._id,
|
||||
url: page.url_path,
|
||||
}))
|
||||
const listsOfKeywordsPages = await mddb.getFiles({
|
||||
folder: 'keywords',
|
||||
extensions: ['md', 'mdx'],
|
||||
})
|
||||
const listsOfKeywords = listsOfKeywordsPages.map((page) => ({
|
||||
...page.metadata,
|
||||
id: page._id,
|
||||
url: page.url_path,
|
||||
}))
|
||||
|
||||
const index = await mddb.getFileByUrl('/')
|
||||
let indexSource = fs.readFileSync(index.file_path, { encoding: 'utf-8' })
|
||||
indexSource = await serialize(indexSource, { parseFrontmatter: true })
|
||||
|
||||
const availableLanguages = [
|
||||
...new Set(datasets.map((dataset) => dataset.language)),
|
||||
]
|
||||
const availablePlatforms = [
|
||||
...new Set(datasets.map((dataset) => dataset.platform).flat()),
|
||||
]
|
||||
return {
|
||||
props: {
|
||||
datasets,
|
||||
listsOfKeywords,
|
||||
indexText: indexSource,
|
||||
availableLanguages,
|
||||
availablePlatforms,
|
||||
},
|
||||
}
|
||||
}
|
||||
9
examples/alan-turing-portal/postcss.config.js
Normal file
9
examples/alan-turing-portal/postcss.config.js
Normal file
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
'postcss-focus-visible': {
|
||||
replaceWith: '[data-focus-visible-added]',
|
||||
},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
5
examples/alan-turing-portal/prettier.config.js
Normal file
5
examples/alan-turing-portal/prettier.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
plugins: [require('prettier-plugin-tailwindcss')],
|
||||
}
|
||||
BIN
examples/alan-turing-portal/public/favicon.ico
Normal file
BIN
examples/alan-turing-portal/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 566 B |
47
examples/alan-turing-portal/styles/prism.css
Normal file
47
examples/alan-turing-portal/styles/prism.css
Normal file
@@ -0,0 +1,47 @@
|
||||
pre[class*='language-'] {
|
||||
color: theme('colors.zinc.100');
|
||||
}
|
||||
|
||||
.token.tag,
|
||||
.token.class-name,
|
||||
.token.selector,
|
||||
.token.selector .class,
|
||||
.token.selector.class,
|
||||
.token.function {
|
||||
color: theme('colors.pink.400');
|
||||
}
|
||||
|
||||
.token.attr-name,
|
||||
.token.keyword,
|
||||
.token.rule,
|
||||
.token.pseudo-class,
|
||||
.token.important {
|
||||
color: theme('colors.zinc.300');
|
||||
}
|
||||
|
||||
.token.module {
|
||||
color: theme('colors.pink.400');
|
||||
}
|
||||
|
||||
.token.attr-value,
|
||||
.token.class,
|
||||
.token.string,
|
||||
.token.property {
|
||||
color: theme('colors.teal.300');
|
||||
}
|
||||
|
||||
.token.punctuation,
|
||||
.token.attr-equals {
|
||||
color: theme('colors.zinc.500');
|
||||
}
|
||||
|
||||
.token.unit,
|
||||
.language-css .token.function {
|
||||
color: theme('colors.sky.200');
|
||||
}
|
||||
|
||||
.token.comment,
|
||||
.token.operator,
|
||||
.token.combinator {
|
||||
color: theme('colors.zinc.400');
|
||||
}
|
||||
13
examples/alan-turing-portal/styles/tailwind.css
Normal file
13
examples/alan-turing-portal/styles/tailwind.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@import 'tailwindcss/base';
|
||||
@import 'tailwindcss/components';
|
||||
@import './prism.css';
|
||||
@import 'tailwindcss/utilities';
|
||||
|
||||
.index-text ul,
|
||||
.index-text p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.index-text h2 {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
307
examples/alan-turing-portal/tailwind.config.js
Normal file
307
examples/alan-turing-portal/tailwind.config.js
Normal file
@@ -0,0 +1,307 @@
|
||||
/** @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}",
|
||||
],
|
||||
darkMode: 'class',
|
||||
plugins: [require('@tailwindcss/typography'), require('@tailwindcss/forms')],
|
||||
theme: {
|
||||
fontSize: {
|
||||
xs: ['0.8125rem', { lineHeight: '1.5rem' }],
|
||||
sm: ['0.875rem', { lineHeight: '1.5rem' }],
|
||||
base: ['1rem', { lineHeight: '1.75rem' }],
|
||||
lg: ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
xl: ['1.25rem', { lineHeight: '2rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem' }],
|
||||
'3xl': ['1.875rem', { lineHeight: '2.25rem' }],
|
||||
'4xl': ['2rem', { lineHeight: '2.5rem' }],
|
||||
'5xl': ['3rem', { lineHeight: '3.5rem' }],
|
||||
'6xl': ['3.75rem', { lineHeight: '1' }],
|
||||
'7xl': ['4.5rem', { lineHeight: '1' }],
|
||||
'8xl': ['6rem', { lineHeight: '1' }],
|
||||
'9xl': ['8rem', { lineHeight: '1' }],
|
||||
},
|
||||
typography: (theme) => ({
|
||||
invert: {
|
||||
css: {
|
||||
'--tw-prose-body': 'var(--tw-prose-invert-body)',
|
||||
'--tw-prose-headings': 'var(--tw-prose-invert-headings)',
|
||||
'--tw-prose-links': 'var(--tw-prose-invert-links)',
|
||||
'--tw-prose-links-hover': 'var(--tw-prose-invert-links-hover)',
|
||||
'--tw-prose-underline': 'var(--tw-prose-invert-underline)',
|
||||
'--tw-prose-underline-hover':
|
||||
'var(--tw-prose-invert-underline-hover)',
|
||||
'--tw-prose-bold': 'var(--tw-prose-invert-bold)',
|
||||
'--tw-prose-counters': 'var(--tw-prose-invert-counters)',
|
||||
'--tw-prose-bullets': 'var(--tw-prose-invert-bullets)',
|
||||
'--tw-prose-hr': 'var(--tw-prose-invert-hr)',
|
||||
'--tw-prose-quote-borders': 'var(--tw-prose-invert-quote-borders)',
|
||||
'--tw-prose-captions': 'var(--tw-prose-invert-captions)',
|
||||
'--tw-prose-code': 'var(--tw-prose-invert-code)',
|
||||
'--tw-prose-code-bg': 'var(--tw-prose-invert-code-bg)',
|
||||
'--tw-prose-pre-code': 'var(--tw-prose-invert-pre-code)',
|
||||
'--tw-prose-pre-bg': 'var(--tw-prose-invert-pre-bg)',
|
||||
'--tw-prose-pre-border': 'var(--tw-prose-invert-pre-border)',
|
||||
'--tw-prose-th-borders': 'var(--tw-prose-invert-th-borders)',
|
||||
'--tw-prose-td-borders': 'var(--tw-prose-invert-td-borders)',
|
||||
},
|
||||
},
|
||||
DEFAULT: {
|
||||
css: {
|
||||
'--tw-prose-body': theme('colors.zinc.600'),
|
||||
'--tw-prose-headings': theme('colors.zinc.900'),
|
||||
'--tw-prose-links': theme('colors.teal.500'),
|
||||
'--tw-prose-links-hover': theme('colors.teal.600'),
|
||||
'--tw-prose-underline': theme('colors.teal.500 / 0.2'),
|
||||
'--tw-prose-underline-hover': theme('colors.teal.500'),
|
||||
'--tw-prose-bold': theme('colors.zinc.900'),
|
||||
'--tw-prose-counters': theme('colors.zinc.900'),
|
||||
'--tw-prose-bullets': theme('colors.zinc.900'),
|
||||
'--tw-prose-hr': theme('colors.zinc.100'),
|
||||
'--tw-prose-quote-borders': theme('colors.zinc.200'),
|
||||
'--tw-prose-captions': theme('colors.zinc.400'),
|
||||
'--tw-prose-code': theme('colors.zinc.700'),
|
||||
'--tw-prose-code-bg': theme('colors.zinc.300 / 0.2'),
|
||||
'--tw-prose-pre-code': theme('colors.zinc.100'),
|
||||
'--tw-prose-pre-bg': theme('colors.zinc.900'),
|
||||
'--tw-prose-pre-border': 'transparent',
|
||||
'--tw-prose-th-borders': theme('colors.zinc.200'),
|
||||
'--tw-prose-td-borders': theme('colors.zinc.100'),
|
||||
|
||||
'--tw-prose-invert-body': theme('colors.zinc.400'),
|
||||
'--tw-prose-invert-headings': theme('colors.zinc.200'),
|
||||
'--tw-prose-invert-links': theme('colors.teal.400'),
|
||||
'--tw-prose-invert-links-hover': theme('colors.teal.400'),
|
||||
'--tw-prose-invert-underline': theme('colors.teal.400 / 0.3'),
|
||||
'--tw-prose-invert-underline-hover': theme('colors.teal.400'),
|
||||
'--tw-prose-invert-bold': theme('colors.zinc.200'),
|
||||
'--tw-prose-invert-counters': theme('colors.zinc.200'),
|
||||
'--tw-prose-invert-bullets': theme('colors.zinc.200'),
|
||||
'--tw-prose-invert-hr': theme('colors.zinc.700 / 0.4'),
|
||||
'--tw-prose-invert-quote-borders': theme('colors.zinc.500'),
|
||||
'--tw-prose-invert-captions': theme('colors.zinc.500'),
|
||||
'--tw-prose-invert-code': theme('colors.zinc.300'),
|
||||
'--tw-prose-invert-code-bg': theme('colors.zinc.200 / 0.05'),
|
||||
'--tw-prose-invert-pre-code': theme('colors.zinc.100'),
|
||||
'--tw-prose-invert-pre-bg': 'rgb(0 0 0 / 0.4)',
|
||||
'--tw-prose-invert-pre-border': theme('colors.zinc.200 / 0.1'),
|
||||
'--tw-prose-invert-th-borders': theme('colors.zinc.700'),
|
||||
'--tw-prose-invert-td-borders': theme('colors.zinc.800'),
|
||||
|
||||
// Base
|
||||
color: 'var(--tw-prose-body)',
|
||||
lineHeight: theme('lineHeight.7'),
|
||||
'> *': {
|
||||
marginTop: theme('spacing.10'),
|
||||
marginBottom: theme('spacing.10'),
|
||||
},
|
||||
p: {
|
||||
marginTop: theme('spacing.7'),
|
||||
marginBottom: theme('spacing.7'),
|
||||
},
|
||||
|
||||
// Headings
|
||||
'h2, h3': {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
},
|
||||
h2: {
|
||||
fontSize: theme('fontSize.xl')[0],
|
||||
lineHeight: theme('lineHeight.7'),
|
||||
marginTop: theme('spacing.20'),
|
||||
marginBottom: theme('spacing.4'),
|
||||
},
|
||||
h3: {
|
||||
fontSize: theme('fontSize.base')[0],
|
||||
lineHeight: theme('lineHeight.7'),
|
||||
marginTop: theme('spacing.16'),
|
||||
marginBottom: theme('spacing.4'),
|
||||
},
|
||||
':is(h2, h3) + *': {
|
||||
marginTop: 0,
|
||||
},
|
||||
|
||||
// Images
|
||||
img: {
|
||||
borderRadius: theme('borderRadius.3xl'),
|
||||
},
|
||||
|
||||
// Inline elements
|
||||
a: {
|
||||
color: 'var(--tw-prose-links)',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
textDecoration: 'underline',
|
||||
textDecorationColor: 'var(--tw-prose-underline)',
|
||||
transitionProperty: 'color, text-decoration-color',
|
||||
transitionDuration: theme('transitionDuration.150'),
|
||||
transitionTimingFunction: theme('transitionTimingFunction.in-out'),
|
||||
},
|
||||
'a:hover': {
|
||||
color: 'var(--tw-prose-links-hover)',
|
||||
textDecorationColor: 'var(--tw-prose-underline-hover)',
|
||||
},
|
||||
strong: {
|
||||
color: 'var(--tw-prose-bold)',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
},
|
||||
code: {
|
||||
display: 'inline-block',
|
||||
color: 'var(--tw-prose-code)',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
backgroundColor: 'var(--tw-prose-code-bg)',
|
||||
borderRadius: theme('borderRadius.lg'),
|
||||
paddingLeft: theme('spacing.1'),
|
||||
paddingRight: theme('spacing.1'),
|
||||
},
|
||||
'a code': {
|
||||
color: 'inherit',
|
||||
},
|
||||
':is(h2, h3) code': {
|
||||
fontWeight: theme('fontWeight.bold'),
|
||||
},
|
||||
|
||||
// Quotes
|
||||
blockquote: {
|
||||
paddingLeft: theme('spacing.6'),
|
||||
borderLeftWidth: theme('borderWidth.2'),
|
||||
borderLeftColor: 'var(--tw-prose-quote-borders)',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
|
||||
// Figures
|
||||
figcaption: {
|
||||
color: 'var(--tw-prose-captions)',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
lineHeight: theme('lineHeight.6'),
|
||||
marginTop: theme('spacing.3'),
|
||||
},
|
||||
'figcaption > p': {
|
||||
margin: 0,
|
||||
},
|
||||
|
||||
// Lists
|
||||
ul: {
|
||||
listStyleType: 'disc',
|
||||
},
|
||||
ol: {
|
||||
listStyleType: 'decimal',
|
||||
},
|
||||
'ul, ol': {
|
||||
paddingLeft: theme('spacing.6'),
|
||||
},
|
||||
li: {
|
||||
marginTop: theme('spacing.6'),
|
||||
marginBottom: theme('spacing.6'),
|
||||
paddingLeft: theme('spacing[3.5]'),
|
||||
},
|
||||
'li::marker': {
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
},
|
||||
'ol > li::marker': {
|
||||
color: 'var(--tw-prose-counters)',
|
||||
},
|
||||
'ul > li::marker': {
|
||||
color: 'var(--tw-prose-bullets)',
|
||||
},
|
||||
'li :is(ol, ul)': {
|
||||
marginTop: theme('spacing.4'),
|
||||
marginBottom: theme('spacing.4'),
|
||||
},
|
||||
'li :is(li, p)': {
|
||||
marginTop: theme('spacing.3'),
|
||||
marginBottom: theme('spacing.3'),
|
||||
},
|
||||
|
||||
// Code blocks
|
||||
pre: {
|
||||
color: 'var(--tw-prose-pre-code)',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
fontWeight: theme('fontWeight.medium'),
|
||||
backgroundColor: 'var(--tw-prose-pre-bg)',
|
||||
borderRadius: theme('borderRadius.3xl'),
|
||||
padding: theme('spacing.8'),
|
||||
overflowX: 'auto',
|
||||
border: '1px solid',
|
||||
borderColor: 'var(--tw-prose-pre-border)',
|
||||
},
|
||||
'pre code': {
|
||||
display: 'inline',
|
||||
color: 'inherit',
|
||||
fontSize: 'inherit',
|
||||
fontWeight: 'inherit',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: 0,
|
||||
padding: 0,
|
||||
},
|
||||
|
||||
// Horizontal rules
|
||||
hr: {
|
||||
marginTop: theme('spacing.20'),
|
||||
marginBottom: theme('spacing.20'),
|
||||
borderTopWidth: '1px',
|
||||
borderColor: 'var(--tw-prose-hr)',
|
||||
'@screen lg': {
|
||||
marginLeft: `calc(${theme('spacing.12')} * -1)`,
|
||||
marginRight: `calc(${theme('spacing.12')} * -1)`,
|
||||
},
|
||||
},
|
||||
|
||||
// Tables
|
||||
table: {
|
||||
width: '100%',
|
||||
tableLayout: 'auto',
|
||||
textAlign: 'left',
|
||||
fontSize: theme('fontSize.sm')[0],
|
||||
},
|
||||
thead: {
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'var(--tw-prose-th-borders)',
|
||||
},
|
||||
'thead th': {
|
||||
color: 'var(--tw-prose-headings)',
|
||||
fontWeight: theme('fontWeight.semibold'),
|
||||
verticalAlign: 'bottom',
|
||||
paddingBottom: theme('spacing.2'),
|
||||
},
|
||||
'thead th:not(:first-child)': {
|
||||
paddingLeft: theme('spacing.2'),
|
||||
},
|
||||
'thead th:not(:last-child)': {
|
||||
paddingRight: theme('spacing.2'),
|
||||
},
|
||||
'tbody tr': {
|
||||
borderBottomWidth: '1px',
|
||||
borderBottomColor: 'var(--tw-prose-td-borders)',
|
||||
},
|
||||
'tbody tr:last-child': {
|
||||
borderBottomWidth: 0,
|
||||
},
|
||||
'tbody td': {
|
||||
verticalAlign: 'baseline',
|
||||
},
|
||||
tfoot: {
|
||||
borderTopWidth: '1px',
|
||||
borderTopColor: 'var(--tw-prose-th-borders)',
|
||||
},
|
||||
'tfoot td': {
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
':is(tbody, tfoot) td': {
|
||||
paddingTop: theme('spacing.2'),
|
||||
paddingBottom: theme('spacing.2'),
|
||||
},
|
||||
':is(tbody, tfoot) td:not(:first-child)': {
|
||||
paddingLeft: theme('spacing.2'),
|
||||
},
|
||||
':is(tbody, tfoot) td:not(:last-child)': {
|
||||
paddingRight: theme('spacing.2'),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
14
examples/alan-turing-portal/templates/dataset.md
Normal file
14
examples/alan-turing-portal/templates/dataset.md
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
title: string
|
||||
link-to-publication: url
|
||||
link-to-data: url
|
||||
task-description: string
|
||||
details-of-task: string
|
||||
size-of-dataset: number
|
||||
percentage-abusive: number
|
||||
language: string
|
||||
level-of-annotation: list eg: ["Posts", "Comments", ...]
|
||||
platform: list eg: ["Youtube", "Facebook", ...]
|
||||
medium: list eg: ["Text", "Emojis", "Images", ...]
|
||||
reference: string
|
||||
---
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
title: string
|
||||
data-link: url
|
||||
reference: string
|
||||
---
|
||||
28
examples/alan-turing-portal/tsconfig.json
Normal file
28
examples/alan-turing-portal/tsconfig.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@@ -1,262 +1,8 @@
|
||||
<h1 align="center">
|
||||
# 🌀 PortalJS example with CKAN and Apollo
|
||||
|
||||
🌀 Portal.JS<br/>
|
||||
The javascript framework for<br/>
|
||||
data portals
|
||||
**🚩 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)**
|
||||
|
||||
</h1>
|
||||
|
||||
🌀 `Portal` is a framework for rapidly building rich data portal frontends using a modern frontend approach (javascript, React, SSR).
|
||||
|
||||
`Portal` assumes a "decoupled" approach where the frontend is a separate service from the backend and interacts with backend(s) via an API. It can be used with any backend and has out of the box support for [CKAN][]. `portal` is built in Javascript and React on top of the popular [Next.js][] framework.
|
||||
|
||||
[ckan]: https://ckan.org/
|
||||
[next.js]: https://nextjs.com/
|
||||
|
||||
Live DEMO: https://catalog-portal-js.vercel.app
|
||||
|
||||
## Features
|
||||
|
||||
- 🗺️ Unified sites: present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. wordpress) with a common internal API.
|
||||
- 👩💻 Developer friendly: built with familiar frontend tech Javascript, React etc
|
||||
- 🔋 Batteries included: Full set of portal components out of the box e.g. catalog search, dataset showcase, blog etc.
|
||||
- 🎨 Easy to theme and customize: installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.
|
||||
- 🧱 Extensible: quickly extend and develop/import your own React components
|
||||
- 📝 Well documented: full set of documentation plus the documentation of NextJS and Apollo.
|
||||
|
||||
### For developers
|
||||
|
||||
- 🏗 Build with modern, familiar frontend tech such as Javascript and React.
|
||||
- 🚀 NextJS framework: so everything in NextJS for free React, SSR, static site generation, huge number of examples and integrations etc.
|
||||
- SSR => unlimited number of pages, SEO etc whilst still using React.
|
||||
- Static Site Generation (SSG) (good for small sites) => ultra-simple deployment, great performance and lighthouse scores etc
|
||||
- 📋 Typescript support
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Setup
|
||||
|
||||
Install a recent version of Node. You'll need Node 10.13 or later.
|
||||
|
||||
### Create a Portal app
|
||||
|
||||
To create a Portal app, open your terminal, cd into the directory you'd like to create the app in, and run the following command:
|
||||
|
||||
```console
|
||||
npm init portal-app my-data-portal
|
||||
```
|
||||
|
||||
> NB: Under the hood, this uses the tool called create-next-app, which bootstraps a Next.js app for you. It uses this template through the --example flag.
|
||||
>
|
||||
> If it doesn’t work, please open an issue.
|
||||
|
||||
## Guide
|
||||
|
||||
### Styling 🎨
|
||||
|
||||
We use Tailwind as a CSS framework. Take a look at `/styles/index.css` to see what we're importing from Tailwind bundle. You can also configure Tailwind using `tailwind.config.js` file.
|
||||
|
||||
Have a look at Next.js support of CSS and ways of writing CSS:
|
||||
|
||||
https://nextjs.org/docs/basic-features/built-in-css-support
|
||||
|
||||
### Backend
|
||||
|
||||
So far the app is running with mocked data behind. You can connect CMS and DMS backends easily via environment variables:
|
||||
|
||||
```console
|
||||
$ export DMS=http://ckan:5000
|
||||
$ export CMS=http://myblog.wordpress.com
|
||||
```
|
||||
|
||||
> Note that we don't yet have implementations for the following CKAN features:
|
||||
>
|
||||
> - Activities
|
||||
> - Auth
|
||||
> - Groups
|
||||
> - Facets
|
||||
|
||||
### Routes
|
||||
|
||||
These are the default routes set up in the "starter" app.
|
||||
|
||||
- Home `/`
|
||||
- Search `/search`
|
||||
- Dataset `/@org/dataset`
|
||||
- Resource `/@org/dataset/r/resource`
|
||||
- Organization `/@org`
|
||||
- Collection (aka group in CKAN) (?) - suggest to merge into org
|
||||
- Static pages, eg, `/about` etc. from CMS or can do it without external CMS, e.g., in Next.js
|
||||
|
||||
### New Routes
|
||||
|
||||
TODO
|
||||
|
||||
### Data fetching
|
||||
|
||||
We use Apollo client which allows us to query data with GraphQL. We have setup CKAN API for the demo (it uses demo.ckan.org as DMS):
|
||||
|
||||
http://portal.datopian1.now.sh/
|
||||
|
||||
Note that we don't have Apollo Server but we connect CKAN API using [`apollo-link-rest`](https://www.apollographql.com/docs/link/links/rest/) module. You can see how it works in [lib/apolloClient.ts](https://github.com/datopian/portal/blob/master/lib/apolloClient.ts) and then have a look at [pages/\_app.tsx](https://github.com/datopian/portal/blob/master/pages/_app.tsx).
|
||||
|
||||
For development/debugging purposes, we suggest installing the Chrome extension - https://chrome.google.com/webstore/detail/apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm.
|
||||
|
||||
#### i18n configuration
|
||||
|
||||
Portal.js is configured by default to support both `English` and `French` subpath for language translation. But for subsequent users, this following steps can be used to configure i18n for other languages;
|
||||
|
||||
1. Update `next.config.js`, to add more languages to the i18n locales
|
||||
|
||||
```js
|
||||
i18n: {
|
||||
locales: ['en', 'fr', 'nl-NL'], // add more language to the list
|
||||
defaultLocale: 'en', // set the default language to use
|
||||
},
|
||||
```
|
||||
|
||||
2. Create a folder for the language in `locales` --> `locales/en-Us`
|
||||
|
||||
3. In the language folder, different namespace files (json) can be created for each translation. For the `index.js` use-case, I named it `common.json`
|
||||
|
||||
```json
|
||||
// locales/en/common.json
|
||||
{
|
||||
"title" : "Portal js in English",
|
||||
}
|
||||
|
||||
// locales/fr/common.json
|
||||
{
|
||||
"title" : "Portal js in French",
|
||||
}
|
||||
```
|
||||
|
||||
4. To use on pages using Server-side Props.
|
||||
|
||||
```js
|
||||
import { loadNamespaces } from './_app';
|
||||
import useTranslation from 'next-translate/useTranslation';
|
||||
|
||||
const Home: React.FC = ()=> {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div>{t(`common:title`)}</div> // we use common and title base on the common.json data
|
||||
);
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
||||
........ ........
|
||||
return {
|
||||
props : {
|
||||
_ns: await loadNamespaces(['common'], locale),
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
5. Go to the browser and view the changes using language subpath like this `http://localhost:3000` and `http://localhost:3000/fr`. **Note** The subpath also activate chrome language Translator
|
||||
|
||||
#### Pre-fetch data in the server-side
|
||||
|
||||
When visiting a dataset page, you may want to fetch the dataset metadata in the server-side. To do so, you can use `getServerSideProps` function from NextJS:
|
||||
|
||||
```javascript
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { initializeApollo } from '../lib/apolloClient';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
const QUERY = gql`
|
||||
query dataset($id: String) {
|
||||
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
|
||||
result
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
...
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const apolloClient = initializeApollo();
|
||||
|
||||
await apolloClient.query({
|
||||
query: QUERY,
|
||||
variables: {
|
||||
id: 'my-dataset'
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
initialApolloState: apolloClient.cache.extract(),
|
||||
},
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
This would fetch the data from DMS and save it in the Apollo cache so that we can query it again from the components.
|
||||
|
||||
#### Access data from a component
|
||||
|
||||
Consider situation when rendering a component for org info on the dataset page. We already have pre-fetched dataset metadata that includes `organization` property with attributes such as `name`, `title` etc. We can now query only organization part for our `Org` component:
|
||||
|
||||
```javascript
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const GET_ORG_QUERY = gql`
|
||||
query dataset($id: String) {
|
||||
dataset(id: $id) @rest(type: "Response", path: "package_show?{args}") {
|
||||
result {
|
||||
organization {
|
||||
name
|
||||
title
|
||||
image_url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function Org({ variables }) {
|
||||
const { loading, error, data } = useQuery(
|
||||
GET_ORG_QUERY,
|
||||
{
|
||||
variables: { id: 'my-dataset' }
|
||||
}
|
||||
);
|
||||
|
||||
...
|
||||
|
||||
const { organization } = data.dataset.result;
|
||||
|
||||
return (
|
||||
<>
|
||||
{organization ? (
|
||||
<>
|
||||
<img
|
||||
src={
|
||||
organization.image_url
|
||||
}
|
||||
className="h-5 w-5 mr-2 inline-block"
|
||||
/>
|
||||
<Link href={`/@${organization.name}`}>
|
||||
<a className="font-semibold text-primary underline">
|
||||
{organization.title || organization.name}
|
||||
</a>
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### Add a new data source
|
||||
|
||||
TODO
|
||||
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)
|
||||
|
||||
## Developers
|
||||
|
||||
@@ -303,4 +49,5 @@ yarn run e2e
|
||||
|
||||
### 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 */
|
||||
export default {
|
||||
displayName: 'simple-example',
|
||||
displayName: 'ckan-example',
|
||||
preset: '../../jest.preset.js',
|
||||
transform: {
|
||||
'^(?!.*\\.(js|jsx|ts|tsx|css|json)$)': '@nrwl/react/plugins/jest',
|
||||
'^.+\\.[tj]sx?$': ['babel-jest', { presets: ['@nrwl/next/babel'] }],
|
||||
},
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
|
||||
coverageDirectory: '../../coverage/examples/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",
|
||||
"sourceRoot": "examples/simple-example",
|
||||
"sourceRoot": "examples/ckan-example",
|
||||
"projectType": "application",
|
||||
"targets": {
|
||||
"build": {
|
||||
@@ -9,12 +9,12 @@
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"root": "examples/simple-example",
|
||||
"outputPath": "dist/examples/simple-example"
|
||||
"root": "examples/ckan-example",
|
||||
"outputPath": "dist/examples/ckan-example"
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"outputPath": "examples/simple-example"
|
||||
"outputPath": "examples/ckan-example"
|
||||
},
|
||||
"production": {}
|
||||
}
|
||||
@@ -23,16 +23,16 @@
|
||||
"executor": "@nrwl/next:server",
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"buildTarget": "simple-example:build",
|
||||
"buildTarget": "ckan-example:build",
|
||||
"dev": true
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "simple-example:build:development",
|
||||
"buildTarget": "ckan-example:build:development",
|
||||
"dev": true
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "simple-example:build:production",
|
||||
"buildTarget": "ckan-example:build:production",
|
||||
"dev": false
|
||||
}
|
||||
}
|
||||
@@ -40,14 +40,14 @@
|
||||
"export": {
|
||||
"executor": "@nrwl/next:export",
|
||||
"options": {
|
||||
"buildTarget": "simple-example:build:production"
|
||||
"buildTarget": "ckan-example:build:production"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nrwl/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "examples/simple-example/jest.config.ts",
|
||||
"jestConfig": "examples/ckan-example/jest.config.ts",
|
||||
"passWithNoTests": true
|
||||
},
|
||||
"configurations": {
|
||||
@@ -61,7 +61,7 @@
|
||||
"executor": "@nrwl/linter:eslint",
|
||||
"outputs": ["{options.outputFile}"],
|
||||
"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"],
|
||||
"jsx": "react"
|
||||
},
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
},
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
36
examples/learn-example/.gitignore
vendored
Normal file
36
examples/learn-example/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# 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*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
1
examples/learn-example/README.md
Normal file
1
examples/learn-example/README.md
Normal file
@@ -0,0 +1 @@
|
||||
PortalJS Learn Example - https://portaljs.org/docs
|
||||
40
examples/learn-example/components/Catalog.tsx
Normal file
40
examples/learn-example/components/Catalog.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Index } from 'flexsearch';
|
||||
import { useState } from 'react';
|
||||
import DebouncedInput from './DebouncedInput';
|
||||
|
||||
export default function Catalog({ datasets }: { datasets: any[] }) {
|
||||
const [indexFilter, setIndexFilter] = useState('');
|
||||
const index = new Index({ tokenize: "full"});
|
||||
datasets.forEach((dataset) =>
|
||||
index.add(
|
||||
dataset._id,
|
||||
Object.entries(dataset.metadata).reduce(
|
||||
(acc, curr) => acc + ' ' + curr.toString(),
|
||||
''
|
||||
) + ' ' + dataset.url_path
|
||||
)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<DebouncedInput
|
||||
value={indexFilter ?? ''}
|
||||
onChange={(value) => setIndexFilter(String(value))}
|
||||
className="p-2 text-sm shadow border border-block"
|
||||
placeholder="Search all datasets..."
|
||||
/>
|
||||
<ul>
|
||||
{datasets
|
||||
.filter((dataset) =>
|
||||
indexFilter !== ''
|
||||
? index.search(indexFilter).includes(dataset._id)
|
||||
: true
|
||||
)
|
||||
.map((dataset) => (
|
||||
<li key={dataset.id}>
|
||||
<a href={dataset.url_path}>{dataset.metadata.title ? dataset.metadata.title : dataset.url_path}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
);
|
||||
}
|
||||
22
examples/learn-example/components/DRD.tsx
Normal file
22
examples/learn-example/components/DRD.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { MDXRemote } from 'next-mdx-remote';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { Mermaid } from '@flowershow/core';
|
||||
|
||||
// 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')),
|
||||
Catalog: dynamic(() => import('./Catalog')),
|
||||
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 }: { source: any }) {
|
||||
return <MDXRemote {...source} components={components} />;
|
||||
}
|
||||
@@ -30,4 +30,3 @@ const DebouncedInput = ({
|
||||
};
|
||||
|
||||
export default DebouncedInput;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { VegaLite } from "react-vega";
|
||||
import VegaLite from "./VegaLite";
|
||||
|
||||
export default function LineChart({
|
||||
data = [],
|
||||
fullWidth = false,
|
||||
title = "",
|
||||
xAxis = "x",
|
||||
yAxis = "y",
|
||||
}) {
|
||||
var tmp = data;
|
||||
if (Array.isArray(data)) {
|
||||
@@ -15,10 +17,10 @@ export default function LineChart({
|
||||
const spec = {
|
||||
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
||||
title,
|
||||
width: "container" as "container",
|
||||
width: "container",
|
||||
height: 300,
|
||||
mark: {
|
||||
type: "line" as "line",
|
||||
type: "line",
|
||||
color: "black",
|
||||
strokeWidth: 1,
|
||||
tooltip: true,
|
||||
@@ -28,22 +30,26 @@ export default function LineChart({
|
||||
},
|
||||
selection: {
|
||||
grid: {
|
||||
type: "interval" as "interval",
|
||||
type: "interval",
|
||||
bind: "scales",
|
||||
},
|
||||
},
|
||||
encoding: {
|
||||
x: {
|
||||
field: "x",
|
||||
field: xAxis,
|
||||
timeUnit: "year",
|
||||
type: "temporal" as "temporal",
|
||||
type: "temporal",
|
||||
},
|
||||
y: {
|
||||
field: "y",
|
||||
type: "quantitative" as "temporal",
|
||||
field: yAxis,
|
||||
type: "quantitative",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return <VegaLite data={vegaData} spec={spec} />;
|
||||
if (typeof data === 'string') {
|
||||
spec.data = { "url": data } as any
|
||||
return <VegaLite fullWidth={fullWidth} spec={spec} />;
|
||||
}
|
||||
|
||||
return <VegaLite fullWidth={fullWidth} data={vegaData} spec={spec} />;
|
||||
}
|
||||
@@ -20,15 +20,16 @@ import {
|
||||
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import loadUrlProxied from "../../lib/loadUrlProxied";
|
||||
import parseCsv from "../../lib/parseCsv";
|
||||
import parseCsv from "../lib/parseCsv";
|
||||
import DebouncedInput from "./DebouncedInput";
|
||||
import loadData from "../lib/loadData";
|
||||
|
||||
const Table = ({
|
||||
data: ogData = [],
|
||||
cols: ogCols = [],
|
||||
csv = "",
|
||||
url = "",
|
||||
fullWidth = false,
|
||||
}) => {
|
||||
if (csv) {
|
||||
const out = parseCsv(csv);
|
||||
@@ -68,7 +69,7 @@ const Table = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (url) {
|
||||
loadUrlProxied(url).then((data) => {
|
||||
loadData(url).then((data) => {
|
||||
const { rows, fields } = parseCsv(data);
|
||||
setData(rows);
|
||||
setCols(fields);
|
||||
@@ -77,7 +78,7 @@ const Table = ({
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={`${fullWidth ? "w-[90vw] ml-[calc(50%-45vw)]" : "w-full"}`}>
|
||||
<DebouncedInput
|
||||
value={globalFilter ?? ""}
|
||||
onChange={(value) => setGlobalFilter(String(value))}
|
||||
@@ -1,4 +1,6 @@
|
||||
// Wrapper for the Vega component
|
||||
import { Vega as VegaOg } from "react-vega";
|
||||
|
||||
export default function Vega(props) {
|
||||
return <VegaOg className="w-full" {...props} />;
|
||||
return <VegaOg {...props} />;
|
||||
}
|
||||
9
examples/learn-example/components/VegaLite.tsx
Normal file
9
examples/learn-example/components/VegaLite.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
// Wrapper for the Vega Lite component
|
||||
import { VegaLite as VegaLiteOg } from "react-vega";
|
||||
import applyFullWidthDirective from "../lib/applyFullWidthDirective";
|
||||
|
||||
export default function VegaLite(props) {
|
||||
const Component = applyFullWidthDirective({ Component: VegaLiteOg });
|
||||
|
||||
return <Component {...props} />;
|
||||
}
|
||||
7
examples/learn-example/content/index.md
Normal file
7
examples/learn-example/content/index.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# My Dataset
|
||||
|
||||
Built with PortalJS
|
||||
|
||||
## Table
|
||||
|
||||
<Table url="data.csv" />
|
||||
21
examples/learn-example/lib/applyFullWidthDirective.tsx
Normal file
21
examples/learn-example/lib/applyFullWidthDirective.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
export default function applyFullWidthDirective({
|
||||
Component,
|
||||
defaultWFull = true,
|
||||
}) {
|
||||
return (props) => {
|
||||
const newProps = { ...props };
|
||||
|
||||
let newClassName = newProps.className || "";
|
||||
if (newProps.fullWidth === true) {
|
||||
newClassName += " w-[90vw] ml-[calc(50%-45vw)] max-w-none";
|
||||
} else if (defaultWFull) {
|
||||
// So that charts and tables will have the
|
||||
// same width as the text content, but images
|
||||
// can have its width set using the width prop
|
||||
newClassName += " w-full";
|
||||
}
|
||||
newProps.className = newClassName;
|
||||
|
||||
return <Component {...newProps} />;
|
||||
};
|
||||
}
|
||||
5
examples/learn-example/lib/loadData.tsx
Normal file
5
examples/learn-example/lib/loadData.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
export default async function loadData(url: string) {
|
||||
const response = await fetch(url)
|
||||
const data = await response.text()
|
||||
return data
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import { serialize } from "next-mdx-remote/serialize";
|
||||
* @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 parse = async function (source, format, scope) {
|
||||
const { content, data, excerpt } = matter(source, {
|
||||
excerpt: (file, options) => {
|
||||
// Generate an excerpt for the file
|
||||
@@ -91,7 +91,7 @@ const parse = async function (source, format) {
|
||||
],
|
||||
format,
|
||||
},
|
||||
scope: data,
|
||||
scope: { ...scope, ...data},
|
||||
}
|
||||
);
|
||||
|
||||
14
examples/learn-example/lib/mddb.ts
Normal file
14
examples/learn-example/lib/mddb.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { MarkdownDB } from "@flowershow/markdowndb";
|
||||
|
||||
const dbPath = "markdown.db";
|
||||
|
||||
const client = new MarkdownDB({
|
||||
client: "sqlite3",
|
||||
connection: {
|
||||
filename: dbPath,
|
||||
},
|
||||
});
|
||||
|
||||
const clientPromise = client.init();
|
||||
|
||||
export default clientPromise;
|
||||
11917
examples/learn-example/package-lock.json
generated
Normal file
11917
examples/learn-example/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
examples/learn-example/package.json
Normal file
55
examples/learn-example/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "basic-example",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "npm run build && next export -o out",
|
||||
"prebuild": "npm run mddb",
|
||||
"mddb": "mddb ./content"
|
||||
},
|
||||
"dependencies": {
|
||||
"@flowershow/core": "^0.4.10",
|
||||
"@flowershow/markdowndb": "^0.1.1",
|
||||
"@flowershow/remark-callouts": "^1.0.0",
|
||||
"@flowershow/remark-embed": "^1.0.0",
|
||||
"@flowershow/remark-wiki-link": "^1.1.2",
|
||||
"@heroicons/react": "^2.0.17",
|
||||
"@opentelemetry/api": "^1.4.0",
|
||||
"@tanstack/react-table": "^8.8.5",
|
||||
"@types/flexsearch": "^0.7.3",
|
||||
"@types/node": "18.16.0",
|
||||
"@types/react": "18.2.0",
|
||||
"@types/react-dom": "18.2.0",
|
||||
"eslint": "8.39.0",
|
||||
"eslint-config-next": "13.3.1",
|
||||
"flexsearch": "^0.7.31",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hastscript": "^7.2.0",
|
||||
"mdx-mermaid": "2.0.0-rc7",
|
||||
"next": "13.2.1",
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-vega": "^7.6.0",
|
||||
"rehype-autolink-headings": "^6.1.1",
|
||||
"rehype-katex": "^6.0.3",
|
||||
"rehype-prism-plus": "^1.5.1",
|
||||
"rehype-slug": "^5.1.0",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"remark-smartypants": "^2.0.0",
|
||||
"remark-toc": "^8.0.1",
|
||||
"typescript": "5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.23",
|
||||
"tailwindcss": "^3.3.1"
|
||||
}
|
||||
}
|
||||
80
examples/learn-example/pages/[[...path]].tsx
Normal file
80
examples/learn-example/pages/[[...path]].tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { existsSync, promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import parse from '../lib/markdown';
|
||||
import DRD from '../components/DRD';
|
||||
import clientPromise from '../lib/mddb';
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const contentDir = path.join(process.cwd(), '/content/');
|
||||
const contentFolders = await fs.readdir(contentDir, 'utf8');
|
||||
const paths = contentFolders.map((folder: string) =>
|
||||
folder === 'index.md'
|
||||
? { params: { path: [] } }
|
||||
: { params: { path: [folder] } }
|
||||
);
|
||||
return {
|
||||
paths,
|
||||
fallback: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const getStaticProps = async (context) => {
|
||||
let pathToFile = 'index.md';
|
||||
if (context.params.path) {
|
||||
pathToFile = context.params.path.join('/') + '/index.md';
|
||||
}
|
||||
|
||||
let datasets = [];
|
||||
const mddbFileExists = existsSync('markdown.db');
|
||||
if (mddbFileExists) {
|
||||
const mddb = await clientPromise;
|
||||
const datasetsFiles = await mddb.getFiles({
|
||||
extensions: ['md', 'mdx'],
|
||||
});
|
||||
datasets = datasetsFiles
|
||||
.filter((dataset) => dataset.url_path !== '/')
|
||||
.map((dataset) => ({
|
||||
_id: dataset._id,
|
||||
url_path: dataset.url_path,
|
||||
file_path: dataset.file_path,
|
||||
metadata: dataset.metadata,
|
||||
}));
|
||||
}
|
||||
|
||||
const indexFile = path.join(process.cwd(), '/content/' + pathToFile);
|
||||
const readme = await fs.readFile(indexFile, 'utf8');
|
||||
|
||||
let { mdxSource, frontMatter } = await parse(readme, '.mdx', { datasets });
|
||||
|
||||
return {
|
||||
props: {
|
||||
mdxSource,
|
||||
frontMatter,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default function DatasetPage({ mdxSource, frontMatter }) {
|
||||
return (
|
||||
<div className="prose dark:prose-invert mx-auto">
|
||||
<header>
|
||||
<div className="mb-6">
|
||||
<>
|
||||
<h1>{frontMatter.title}</h1>
|
||||
{frontMatter.author && (
|
||||
<div className="-mt-6">
|
||||
<p className="opacity-60 pl-1">{frontMatter.author}</p>
|
||||
</div>
|
||||
)}
|
||||
{frontMatter.description && (
|
||||
<p className="description">{frontMatter.description}</p>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<DRD source={mdxSource} />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
6
examples/learn-example/pages/_app.tsx
Normal file
6
examples/learn-example/pages/_app.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import '../styles/globals.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />
|
||||
}
|
||||
19
examples/learn-example/pages/_document.tsx
Normal file
19
examples/learn-example/pages/_document.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import Document, { Html, Main, Head, NextScript } from 'next/document';
|
||||
|
||||
class MyDocument extends Document {
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Head>
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
</Head>
|
||||
<body className='bg-white dark:bg-gray-900'>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MyDocument;
|
||||
6
examples/learn-example/postcss.config.js
Normal file
6
examples/learn-example/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
6
examples/learn-example/public/data.csv
Normal file
6
examples/learn-example/public/data.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
Year,Rating
|
||||
2008,86
|
||||
2009,96
|
||||
2010,100
|
||||
2011,100
|
||||
2012,97
|
||||
|
BIN
examples/learn-example/public/favicon.png
Normal file
BIN
examples/learn-example/public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -1,5 +1,16 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "@flowershow/remark-callouts/styles.css";
|
||||
|
||||
.w-5 {
|
||||
width: 1.25rem
|
||||
}
|
||||
|
||||
.h-5 {
|
||||
height: 1.25rem
|
||||
}
|
||||
|
||||
/* mathjax */
|
||||
.math-inline > mjx-container > svg {
|
||||
display: inline;
|
||||
@@ -65,3 +76,30 @@ html {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
}
|
||||
body {
|
||||
color: white;
|
||||
background: black;
|
||||
}
|
||||
}
|
||||
12
examples/learn-example/tailwind.config.js
Normal file
12
examples/learn-example/tailwind.config.js
Normal file
@@ -0,0 +1,12 @@
|
||||
/** @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/learn-example/tsconfig.json
Normal file
20
examples/learn-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", "middleware.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -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';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user