Compare commits

...

89 Commits

Author SHA1 Message Date
olayway
11144758ca [site/content/howto][xs]: list of guides in index page 2023-05-18 20:09:27 +02:00
olayway
f443dab92c [site/content][s]: add drd guide 2023-05-18 19:50:03 +02:00
olayway
d9be7d0f5b [site/content/assets][s]: ctreate howto subdir 2023-05-18 19:25:18 +02:00
olayway
7c21993b92 [site/content][s]: add markdown guide 2023-05-18 19:25:18 +02:00
olayway
3f06aa3f01 [site/content][s]: add comments guide 2023-05-18 15:03:26 +02:00
olayway
d191178ecf [site/content][s]: add blog guide 2023-05-18 15:03:26 +02:00
olayway
a0eb06ee19 [site/content][s]: add sitemap guide 2023-05-18 14:45:27 +02:00
olayway
539fd6c77e [site/content][xs]: add seo guide 2023-05-18 14:44:50 +02:00
olayway
8fa930b3f4 [content/howto][s]: add analytics guide 2023-05-18 14:43:51 +02:00
olayway
3be88f1cdb [content/howto][s]: create howto index page 2023-05-17 20:49:35 +02:00
olayway
d0ee06e8d0 [.gitignore][xs]: ignore .obsidian 2023-05-17 20:49:08 +02:00
Luccas Mateus
4e91e88f2b [examples/github-backed-catalog] - improvements after issue #871 (#887) 2023-05-17 11:10:18 -03:00
Luccas Mateus
ebcb93c996 [examples/turing][xs] - point out that markdown gets rendered 2023-05-16 07:22:45 -03:00
Luccas Mateus
1fc2499c71 [examples/538][xs] - change banner position + text + change README (#877)
* [examples/538][xs] - change banner position + text + change README

* [examples/538] - change banner background

* [examples/538][m] - changes after ola comments

* [example/538] - fix typo
2023-05-15 12:37:19 -03:00
Luccas Mateus
1af24ef57e 538 banner (#873)
* [example/538] - banner

* [example/538] - title on head
2023-05-12 14:50:31 -03:00
Luccas Mateus de Medeiros Gomes
698c06efda [site][xs] - fix logo on dark mode 2023-05-12 14:25:02 -03:00
Luccas Mateus de Medeiros Gomes
8792f295b0 [examples/turing][xs] - fix header 2023-05-12 08:26:02 -03:00
Luccas Mateus de Medeiros Gomes
3e6d01c4c7 [examples/538][xs] - go back to v01(only index page) 2023-05-11 19:17:41 -03:00
Luccas Mateus de Medeiros Gomes
7c943c1b31 [example/turing][sm] - forgot to add github on desktop 2023-05-11 17:16:56 -03:00
Luccas Mateus de Medeiros Gomes
7197a6686e [examples/turing][sm] - change view on github 2023-05-11 17:11:20 -03:00
Luccas Mateus de Medeiros Gomes
7822440f0d [examples/turing] - rename it to turing 2023-05-11 16:13:09 -03:00
Luccas Mateus de Medeiros Gomes
82773b5e8a [examples/538] - fix build 2023-05-11 13:28:33 -03:00
Luccas Mateus
1cfc4db528 [examples/538][m] - little fixes and renaming (#870) 2023-05-11 13:15:18 -03:00
João Demenech
336ff819dc OpenSpending Data Portal (#868)
* [#856,openspending][xl]: initial commit

* [examples/openspending][xs] - remove console.logs

---------

Co-authored-by: Luccas Mateus de Medeiros Gomes <luccasmmg@gmail.com>
2023-05-10 18:20:47 -03:00
Luccas Mateus
f610c953e7 [example/538] - individual pages (#865)
* [example/538] - individual pages

* [examples/538][sm] - force inclusion of classes

* [examples/538] - changes requested by demenech
2023-05-10 18:13:16 -03:00
João Demenech
3f350f8fcd [#810, github-backed example][xl]: improve looks, improve README, rename from simple-example to github-backed (#864) 2023-05-09 19:19:36 -03:00
Luccas Mateus de Medeiros Gomes
714faf9986 [examples/538][sm] - bug fixes + favicon 2023-05-09 15:06:26 -03:00
João Demenech
a954575397 Website v0.4 (#860)
* [#858,site][xl]: add Examples to the Navbar, rename gallery to showcases, remove examples from showcases, move github stars to the navbar, add view on github button to the hero section, reduce padding on buttons, add RHS image to the hero

* [#858,site][xl]: make sidebar consistent on all pages

* [site][xs]: fix ts error on GitHub button component

* [site][xs]: fix external links on navbar needing two clicks to open

* [site, hero][xs]: align RHS image to the top
2023-05-09 14:39:23 -03:00
João Demenech
ca13e7b9c3 Merge pull request #862 from datopian/dependabot/npm_and_yarn/examples/alan-turing-portal/webpack-5.82.0
Bump webpack from 5.74.0 to 5.82.0 in /examples/alan-turing-portal
2023-05-09 14:34:23 -03:00
João Demenech
f12e007ce4 Merge pull request #817 from datopian/dependabot/npm_and_yarn/examples/alan-turing-portal/http-cache-semantics-4.1.1
Bump http-cache-semantics from 4.1.0 to 4.1.1 in /examples/alan-turing-portal
2023-05-09 14:26:48 -03:00
dependabot[bot]
2edf488fe7 Bump webpack from 5.74.0 to 5.82.0 in /examples/alan-turing-portal
Bumps [webpack](https://github.com/webpack/webpack) from 5.74.0 to 5.82.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.74.0...v5.82.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-09 17:26:16 +00:00
João Demenech
ce395b4c49 Merge pull request #816 from datopian/dependabot/npm_and_yarn/examples/alan-turing-portal/json5-1.0.2
Bump json5 from 1.0.1 to 1.0.2 in /examples/alan-turing-portal
2023-05-09 13:59:41 -03:00
João Demenech
51828b85f1 Merge pull request #838 from datopian/dependabot/npm_and_yarn/packages/components/d3-color-and-vega-3.1.0
Bump d3-color and vega in /packages/components
2023-05-09 13:57:35 -03:00
Luccas Mateus
d2e9c54c13 [examples/fivethirtyeight][lg] - first commmit of 538 Example (#861) 2023-05-08 20:43:28 -03:00
João Demenech
6705bc1e2d merge: implement tutorial improvements based on feedback
**Issue:** https://github.com/datopian/portaljs/issues/839

## Changes

- Update info about required Node version
- Remove mention to automatic reload from docs
2023-05-08 17:20:36 -03:00
deme
7dfde0935e [#839, docs][xs]: remove mention to automtic reload 2023-05-08 17:17:45 -03:00
deme
3f76bea895 [#839,docs][xs]: change Node version to either 16 or 18, fix small typo 2023-05-08 16:30:04 -03:00
João Demenech
f17efce02e Merge pull request #857 from datopian/alan-turing-fixes
[alan-turing][sm] - fixes requested
2023-05-08 15:40:23 -03:00
Luccas Mateus de Medeiros Gomes
61b96c20ed [alan-turing][xs] - fix links 2023-05-08 14:36:59 -03:00
Luccas Mateus de Medeiros Gomes
4cadc50e46 [alan-turing][m] - additional fixes 2023-05-08 13:25:34 -03:00
Rufus Pollock
684f473e62 Update Gallery.tsx 2023-05-08 17:59:13 +02:00
Rufus Pollock
b963cf2cbb [ex/turing/README][xs]: quick proofing. 2023-05-08 16:08:03 +02:00
Luccas Mateus de Medeiros Gomes
43ac5cfb47 [alan-turing][sm] - fix typo 2023-05-08 08:56:17 -03:00
Luccas Mateus de Medeiros Gomes
f6b8ef2190 [alan-turing][sm] - fixes requested 2023-05-08 08:44:22 -03:00
Anuar Ustayev (aka Anu)
e5c89308d1 Merge pull request #852 from datopian/components-tutorial
[docs][m] - components api section
2023-05-07 11:45:56 +06:00
Anuar Ustayev (aka Anu)
8b51123290 [docs][s]: update and rename components-api.md to components.md 2023-05-07 11:10:26 +06:00
Anuar Ustayev (aka Anu)
53b64b81c9 [docs][xs]: bring back prev link to prev tutorial as was removed by mistake. 2023-05-07 11:06:53 +06:00
Anuar Ustayev (aka Anu)
9fe08fcd1b [docs][xs]: no need to link to the next tutorial as it's irrelevant to getting started series. 2023-05-07 11:05:21 +06:00
Anuar Ustayev (aka Anu)
7150150db0 [sidebar][xs]: moved the components page to higher level. 2023-05-07 11:04:34 +06:00
Luccas Mateus de Medeiros Gomes
5cc312b55b [docs][m] - components api section 2023-05-06 14:31:48 -03:00
João Demenech
5c8431bf39 Fix code blocks not being displayed properly on light mode (#851)
* [#803,blogs][m]: fix code blocks not being displayed properly on light mode

* [docs][m] - fix problems with merge

---------

Co-authored-by: Luccas Mateus <Luccasmmg@gmail.com>
2023-05-06 13:36:49 -03:00
dependabot[bot]
0a1ede10e8 Bump d3-color and vega in /packages/components
Bumps [d3-color](https://github.com/d3/d3-color) to 3.1.0 and updates ancestor dependency [vega](https://github.com/vega/vega). These dependencies need to be updated together.


Updates `d3-color` from 2.0.0 to 3.1.0
- [Release notes](https://github.com/d3/d3-color/releases)
- [Commits](https://github.com/d3/d3-color/compare/v2.0.0...v3.1.0)

Updates `vega` from 5.20.2 to 5.25.0
- [Release notes](https://github.com/vega/vega/releases)
- [Commits](https://github.com/vega/vega/compare/v5.20.2...v5.25.0)

---
updated-dependencies:
- dependency-name: d3-color
  dependency-type: indirect
- dependency-name: vega
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-06 04:35:59 +00:00
Anuar Ustayev (aka Anu)
45c07f829a Merge pull request #850 from datopian/feature/lhs-navigation
LHS Navigation
2023-05-06 10:35:00 +06:00
deme
53ea7957c0 [blogs][xs]: fix issue with date field 2023-05-05 16:30:44 -03:00
deme
0c65a145c8 [#809,docs][l]: fix Vercel build, add examples do sidebar, fix blogs list 2023-05-05 16:17:28 -03:00
deme
91caeff6c3 [#809,docs][xl]: add LHS sidebar to docs 2023-05-05 15:33:17 -03:00
deme
0f65e253da Merge branch 'main' of github.com:datopian/portal.js into feature/lhs-navigation 2023-05-05 10:32:55 -03:00
Luccas Mateus de Medeiros Gomes
c390a21611 [docs][sm] - fix typo 2023-05-05 09:54:16 -03:00
Luccas Mateus de Medeiros Gomes
dac7d03d05 [learn-example][sm] - remove console.log and fix typo 2023-05-05 09:12:31 -03:00
Luccas Mateus de Medeiros Gomes
89ba260b70 [docs][m] - tutorial part 4 2023-05-05 08:56:09 -03:00
Luccas Mateus de Medeiros Gomes
ce847746d2 [docs][sm] - use "groups" instead of title for facets 2023-05-05 08:54:19 -03:00
deme
5328492575 [#809,docs,navigation][xl]: initial commit 2023-05-04 22:34:17 -03:00
João Demenech
e52e789314 Merge pull request #849 from datopian/tutorial-part-4
[learn-example][m] - add extra metadata fields
2023-05-04 22:31:52 -03:00
Luccas Mateus de Medeiros Gomes
0e8cac7d50 [learn-example][m] - add extra metadata fields 2023-05-04 20:54:17 -03:00
João Demenech
2e30c76a3d [#842,package.json][xs]: move eslint and @types deps to dependencies (#848) 2023-05-04 19:59:12 -03:00
João Demenech
edb2354945 merge: change version to 0.1.0, publish new version which adds (#847)
## Changes:

- @portaljs/components version bumped from 0.0.3 to 0.1.0
- New version adds the Catalog component
2023-05-04 13:56:17 -03:00
João Demenech
5834a4a470 Website Misc Improvements (#836)
* [#803,website][s]: remove gallery button from hero, add gallery link to navbar, make docs listed on /blog be displayed as blog posts

* [#803,analytics,website][xs]: implement GA
2023-05-04 13:43:13 -03:00
João Demenech
90b93e6819 [#819,xlsx][m]: remove data-literate, excel and everything related to the xlsx dependency (#845) 2023-05-04 13:42:13 -03:00
Luccas Mateus de Medeiros Gomes
ad52721a38 [components][m] - move catalog to @portaljs/components 2023-05-04 11:14:39 -03:00
Luccas Mateus de Medeiros Gomes
cf2a93abfd [docs][sm] - change layout of docs 2023-05-04 09:47:02 -03:00
Luccas Mateus de Medeiros Gomes
8afb30c96b [docs][sm] - add filters section to docs 2023-05-04 08:22:01 -03:00
Luccas Mateus
94a3c2a5f0 [learn-example][m] - add facets to catalog component (#841) 2023-05-04 07:39:59 -03:00
João Demenech
a0620f9255 merge: components package preparation, replace components on learn-example (#835)
* [#812,package][xl]: package preparation, replace components on learn-example

* [#812,package][xs]: upgrade portaljs/components version on learn-example

* [package][xs]: add deboundebinput back to lean-example
2023-05-03 19:27:51 -03:00
Luccas Mateus de Medeiros Gomes
e5513f59a6 [learn-example][sm] - fix build 2023-05-03 11:45:57 -03:00
dependabot[bot]
d73bcc77f3 Bump http-cache-semantics in /examples/alan-turing-portal
Bumps [http-cache-semantics](https://github.com/kornelski/http-cache-semantics) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/kornelski/http-cache-semantics/releases)
- [Commits](https://github.com/kornelski/http-cache-semantics/compare/v4.1.0...v4.1.1)

---
updated-dependencies:
- dependency-name: http-cache-semantics
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-03 14:31:28 +00:00
Luccas Mateus
1782f23b84 Part 3 tutorial - Creating an index page (#834)
* [learn-example][m] - code section for the tutorial part 3

* [learn-example][sm] - dont panic when no markdown.db file found

* [docs][m] - creating an inedx page
2023-05-03 11:30:39 -03:00
João Demenech
72405162a1 merge: fix storybook build
* [package][xs]: remove parameter that was not being used

* [package][xs]: remove import that's breaking the build

* [package][xs]: trying to fix build error on Vercel
2023-05-02 17:31:48 -03:00
João Demenech
982733737d merge: Components package initial setup + Components extraction
**Issue:** https://github.com/datopian/portaljs/issues/812

## Changes

- Renamed old package to "components-old"
- Created a Vite project based on https://dev.to/nicolaserny/create-a-react-component-library-with-vite-and-typescript-1ih9 and  https://zach.codes/build-your-own-flexible-component-library-using-tsdx-typescript-tailwind-css-headless-ui/
- Implemented tailwind on it
- Extracted components
  - LineChart
  - Table
  - Vega
  - VegaLite
- Created stories for the extracted components
2023-05-02 16:41:28 -03:00
deme
ea5802a908 [#812,package][xl]: changed project to Vite, created stories for LineChart, Table, Vega and VegaLite 2023-05-02 16:37:22 -03:00
Luccas Mateus
229a7b5324 [alan-turing][m] - fix markdown (#831) 2023-05-02 15:31:45 -03:00
Luccas Mateus
014c4c043d [alan-turing][m] - small tweaks (#830) 2023-05-02 12:53:10 -03:00
Luccas Mateus de Medeiros Gomes
ed3a26cd6d [alan-turing][sm] - fix build 2023-05-01 21:40:46 -03:00
Luccas Mateus
026059184a [alan-turing][m] - individual pages (#828) 2023-05-01 21:06:52 -03:00
João Demenech
a041d69282 merge: tutorial VI
**Issue:** https://github.com/datopian/portaljs/issues/821

## Changes

- Added `npm run export` command to `learn-example`
- Added "Deploying your PortalJS app" section to `/docs`
  - Deploy to Vercel
  - One-Click Deploy (to Vercel)
  - Deploy to  Cloudflare
2023-05-01 19:09:18 -03:00
deme
016f3e20e9 [#812,package][xl]: add Table component and story for it 2023-05-01 18:56:22 -03:00
deme
169a92d313 [#812,package][xl]: initial versioning of the package 2023-05-01 15:53:42 -03:00
deme
14abd5b768 [tutorial][xs]: fix typo 2023-05-01 15:02:59 -03:00
deme
4aaabba229 [#821,tutorial][m]: add export npm command to example-learn, add tutorial VI to /docs 2023-05-01 14:51:02 -03:00
dependabot[bot]
9c25c71286 Bump json5 from 1.0.1 to 1.0.2 in /examples/alan-turing-portal
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-04-30 02:38:06 +00:00
256 changed files with 54233 additions and 6466 deletions

1
.gitignore vendored
View File

@@ -16,6 +16,7 @@ node_modules
*.launch
.settings/
*.sublime-workspace
.obsidian
# IDE - VSCode
.vscode/*

View File

@@ -1,19 +0,0 @@
## Getting started
To get started with this template, 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.
## License
This site template is a commercial product and is licensed under the [Tailwind UI license](https://tailwindui.com/license).

View File

@@ -1,109 +0,0 @@
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)' }} />}
</>
)
}

View File

@@ -1,9 +0,0 @@
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 Institutes Online Hate Research Hub or read The Alan Turing Institutes Reading List on Online Hate and Abuse Research.
If youre 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.

View File

@@ -1,182 +0,0 @@
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 ReactMarkdown from 'react-markdown'
import { Index } from 'flexsearch'
import { useForm } from 'react-hook-form'
function DatasetCard({ dataset }) {
return (
<Card as="article">
<Card.Title>{dataset.title}</Card.Title>
<Card.Description>
<span className="font-semibold">Link to publication: </span>{' '}
<a
className="underline transition hover:text-teal-400 dark:hover:text-teal-900 text-ellipsis"
href={dataset['link-to-publication']}
>
{dataset['link-to-publication']}
</a>
</Card.Description>
<Card.Description>
<span className="font-semibold">Link to data: </span>
<a
className="underline transition hover:text-teal-600 dark:hover:text-teal-900 text-ellipsis"
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>
)
}
export default function Home({ datasets, indexText, 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 } = 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">
Hate Speech Dataset Catalogue
</h1>
<article className="mt-6 flex flex-col gap-y-2 text-base text-zinc-600 dark:text-zinc-400">
<ReactMarkdown>{indexText}</ReactMarkdown>
</article>
</div>
</Container>
<Container className="mt-24 md:mt-28">
<div className="mx-auto grid max-w-xl grid-cols-1 gap-y-8 lg:max-w-none">
<form className="rounded-2xl border border-zinc-100 px-4 py-6 sm:p-6 dark:border-zinc-700/40">
<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 sm:flex-row gap-3">
<input
placeholder="Search here"
aria-label="Hate speech on Twitter"
required
{...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 text-zinc-600 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-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 text-zinc-600 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-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>
</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>
</>
)
}
export async function getStaticProps() {
const mddb = await clientPromise
const allPages = await mddb.getFiles({ extensions: ['md', 'mdx'] })
const datasets = allPages
.filter((page) => page.url_path !== '/')
.map((page) => ({ ...page.metadata, id: page._id }))
const index = allPages.filter((page) => page.url_path === '/')[0]
const source = fs.readFileSync(index.file_path, { encoding: 'utf-8' })
const availableLanguages = [... new Set(datasets.map((dataset) => dataset.language))]
const availablePlatforms = [... new Set(datasets.map((dataset) => dataset.platform).flat())]
return {
props: {
indexText: source,
datasets,
availableLanguages,
availablePlatforms,
},
}
}

View File

@@ -10,24 +10,24 @@
},
"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"
"remark-gfm": "^3.0.1"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.1"
"tailwindcss": "^3.3.1",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
"typescript": "5.0.4",
"@types/node": "18.16.0",
"@types/react": "18.0.38",
"@types/react-dom": "18.0.11"
}
}

35
examples/fivethirtyeight/.gitignore vendored Normal file
View 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*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,44 @@
This is a replica of the awesome data.fivethirtyeight.com using PortalJS.
You might be asking why we did that, there are three main reasons:
- The website has a great UI, with multiple datasets being displayed elegantly and with simplicity.
- PortalJS allows us to add more functionality to it e.g dataset previews and search functionality.
- The project follows our same principles of open sourcing and free data, with every dataset being publicly available on Github.
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -0,0 +1,23 @@
import Link from "next/link";
function HomeIcon({ className = "" }) {
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M 12 2 A 1 1 0 0 0 11.289062 2.296875 L 1.203125 11.097656 A 0.5 0.5 0 0 0 1 11.5 A 0.5 0.5 0 0 0 1.5 12 L 4 12 L 4 20 C 4 20.552 4.448 21 5 21 L 9 21 C 9.552 21 10 20.552 10 20 L 10 14 L 14 14 L 14 20 C 14 20.552 14.448 21 15 21 L 19 21 C 19.552 21 20 20.552 20 20 L 20 12 L 22.5 12 A 0.5 0.5 0 0 0 23 11.5 A 0.5 0.5 0 0 0 22.796875 11.097656 L 12.716797 2.3027344 A 1 1 0 0 0 12.710938 2.296875 A 1 1 0 0 0 12 2 z"/></svg></div>
}
export default function Breadcrumbs({ links }: { links: { title: string, href?: string, target?: string }[] }) {
const current = links.at(-1);
return <div className="flex items-center uppercase font-black text-xs">
<Link className="flex items-center" href='/'><HomeIcon /></Link>
{/* {links.length > 1 && links.slice(0, -1).map((link) => {
return <>
<span className="mx-4">/</span>
<Link href={link.href}>{link.title}</Link>
</>
})} */}
<span className="mx-4">/</span>
<span>{current?.title}</span>
</div >
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,38 @@
import { Octokit } from 'octokit';
export interface GithubProject {
owner: string;
repo: string;
branch: string;
files: string[];
readme: string;
description?: string;
name?: string;
}
export async function getProjectReadme(
owner: string,
repo: string,
branch: string,
readme: string,
github_pat?: string
) {
const octokit = new Octokit({ auth: github_pat });
try {
const response = await octokit.rest.repos.getContent({
owner,
repo,
path: readme,
ref: branch,
});
const data = response.data as { content?: string };
const fileContent = data.content ? data.content : '';
if (fileContent === '') {
return null;
}
const decodedContent = Buffer.from(fileContent, 'base64').toString();
return decodedContent;
} catch (error) {
return null;
}
}

View File

@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
serverRuntimeConfig: {
github_pat: process.env.GITHUB_PAT ? process.env.GITHUB_PAT : null,
},
}
module.exports = nextConfig

6878
examples/fivethirtyeight/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
{
"name": "fiverthirtyeight-example",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@portaljs/components": "^0.1.0",
"@tailwindcss/typography": "^0.5.9",
"@types/node": "20.1.1",
"@types/react": "18.2.6",
"@types/react-dom": "18.2.4",
"autoprefixer": "10.4.14",
"eslint": "8.40.0",
"eslint-config-next": "13.4.1",
"flexsearch": "^0.7.31",
"next": "13.4.1",
"next-mdx-remote": "^4.4.1",
"next-seo": "^6.0.0",
"octokit": "^2.0.14",
"postcss": "8.4.23",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"remark-code-frontmatter": "^1.0.0",
"remark-extract-frontmatter": "^3.2.0",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"tailwindcss": "3.3.2",
"timeago.js": "^4.0.2",
"typescript": "5.0.4"
}
}

View File

@@ -0,0 +1,8 @@
import '@/styles/globals.css'
import '@portaljs/components/styles.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
}

View File

@@ -0,0 +1,96 @@
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html lang="en">
<Head>
<link
rel="icon"
type="image/x-icon"
href="https://projects.fivethirtyeight.com/shared/favicon.ico"
/>
<meta
property="og:image"
content="https://portaljs-fivethirtyeight.vercel.app/share_image.png"
/>
<meta
property="twitter:image"
content="https://portaljs-fivethirtyeight.vercel.app/share_image.png"
/>
</Head>
<body>
<div className="px-2 max-w-5xl mx-auto pb-2">
<div className="mt-2 px-2 bg-[#3c3c3c] text-white">
<div className="p-2 text-center">
This is a replica to the awesome{' '}
<a
className="hover:underline font-bold"
href="https://data.fivethirtyeight.com"
>
data.fivethirtyeight.com
</a>{' '}
website.{' '}
<a
className="hover:underline font-bold"
href="https://github.com/datopian/portaljs/tree/main/examples/fivethirtyeight"
>
Read more here
</a>{' '}
</div>
</div>
</div>
<header className="max-w-5xl mx-auto mt-8 w-full">
<div className="border-b-2 pb-2.5 mx-2 border-zinc-800 flex justify-between">
<h1 className="flex gap-x-1 items-end">
<span className="sr-only">FiveThirtyEight</span>
<img
width="197"
height="25"
alt="FiveThirtyEight"
src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MjEgNTMuNzYiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojMDEwMTAxO308L3N0eWxlPjwvZGVmcz48dGl0bGU+QXJ0Ym9hcmQgOTU8L3RpdGxlPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTAgMGgyNXY4SDl2MTBoMTV2OEg5djE3SDBWMHpNMzEgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdIMzF6bTUtMzZoOHY4aC04ek0xNzkgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdoLTE3em01LTM2aDh2OGgtOHpNMzE2IDM2aDVWMThoLTV2LThoMTN2MjZoNHY3aC0xN3ptNS0zNmg4djhoLTh6TTU0IDI3VjEwaDh2MTVsNCA5Ljk4aDFMNzEgMjVWMTBoOHYxN2wtNyAxNkg2MWwtNy0xNnpNMTExIDQzSDk3LjQyQzg5LjIzIDQzIDg1IDM5LjE5IDg1IDMxLjE3VjIyYzAtNy41NyA0LjMtMTMgMTMtMTMgOS4zMyAwIDEzIDUuMDcgMTMgMTR2N0g5NHYxLjc0YzAgMi42MiAxIDQuMjYgMy40MiA0LjI2SDExMXpNOTQgMjNoOHYtMS41NWMwLTIuNjItMS4wNi01LjQ1LTQuMTMtNS40NS0yLjc5IDAtMy44NyAyLjItMy44NyA1LjQ1ek0xMjUgOGgtMTBWMGgyOXY4aC0xMHYzNWgtOVY4ek0yMDIgNDNWMTBoOHY0YzEuMTQtMi40NSAzLjc1LTQgNy4yMi00SDIyMHY4aC02Yy0yLjg0IDAtNCAuOTQtNCAzLjlWNDN6TTI0NSA0M2gtNC44NEMyMzMuMDUgNDMgMjMwIDM5LjMxIDIzMCAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0gyNDV6TTQyMSA0M2gtNC44NEM0MDkuMDUgNDMgNDA2IDM5LjMxIDQwNiAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0g0MjF6TTI1NC4yNiA1My43Nmw0LjYxLTkuNUwyNTEgMjdWMTBoOHYxNWw0IDEwaDFsNC0xMFYxMGg4djE3bC0xMi4zIDI2Ljc2aC05LjQ0ek0yODQgMGgyNXY4aC0xNnY5aDE1djhoLTE1djEwaDE2djhoLTI1VjB6TTMzNyA0OHYtMmgxNi4xYzIgMCAyLjktLjE4IDIuOS0xLjI3di0uMzRjMC0xLjA4LS45MS0xLjM5LTIuOS0xLjM5SDM0MHYtNWw1LTVjLTUuMjktMS40OC04LTUuNDMtOC0xMXYtMWMwLTcuNTYgNC40NC0xMiAxNC0xMmEyMS45MyAyMS45MyAwIDAgMSA1Ljk1IDFMMzYxIDRsNSAzLTQgNmMxLjM3IDEuOTMgMyA0LjkzIDMgOHYxYzAgNy0zLjMgMTAuNjYtMTIgMTFsLTMgNGg2YzUuOTIgMCA5IDIuNjIgOSA3LjY4di4xMWMwIDUuMDYtMi43MSA4LjIxLTguNjIgOC4yMWgtMTNjLTQuMjkgMC02LjM4LTEuODQtNi4zOC01em0xOS0yNXYtM2MwLTMuMy0xLjMzLTQtNS00cy01IC43LTUgNHYzYzAgMy4zIDEuMzkgNCA1IDRzNS0uNyA1LTR6TTM4MCA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuNC00IDctNCA2LjI2IDAgOSAzLjA4IDkgMTAuNzZWNDNoLThWMjJjMC0zLjEzLTEuMDctNS00LTVzLTQgMS44Ny00IDV6TTE1NyA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuOTEtNCA3LjQ5LTQgNi4yNiAwIDguNTEgMy4xMyA4LjUxIDEwLjgxVjQzaC04VjIxYzAtMy4xMy0xLjA3LTQuNDQtNC00LjQ0cy00IDIuMjYtNCA1LjM5eiIvPjwvc3ZnPg=="
/>{' '}
<span className="-mb-0.5 text-[#3c3c3c]">replica</span>
</h1>
<div className="md:flex items-center gap-x-3 text-[#3c3c3c] -mb-1 hidden">
<a
className="hover:opacity-75 transition"
href="https://portaljs.org"
>
Built with 🌀PortalJS
</a>
<hr className="h-[80%] border border-[#3c3c3c] opacity-75 my-2"></hr>
<a
className="hover:opacity-75 transition"
href="https://github.com/datopian/portaljs/tree/main/examples/fivethirtyeight"
>
Github
</a>
</div>
</div>
<div className="mx-2 py-1.5 text-[14px] text-[#3c3c3c] md:hidden">
<ul className="flex gap-x-4">
<li>
<a
className="hover:opacity-75 transition"
href="https://portaljs.org"
>
PortalJS
</a>
</li>
<li>
<a
className="hover:opacity-75 transition"
href="https://github.com/datopian/portaljs/tree/main/examples/fivethirtyeight"
>
View on Github
</a>
</li>
</ul>
</div>
</header>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -0,0 +1,131 @@
import { NextSeo } from 'next-seo';
import { promises as fs } from 'fs';
import path from 'path';
import getConfig from 'next/config';
import { getProjectReadme, GithubProject } from '@/lib/octokit';
import remarkGfm from 'remark-gfm';
import extract from 'remark-extract-frontmatter';
import { Dataset } from '..';
import { GetStaticProps } from 'next';
import { Table } from '@portaljs/components';
import Breadcrumbs from '@/components/Breadcrumbs';
import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
import remarkFrontmatter from 'remark-frontmatter';
export default function DatasetPage({
dataset,
}: {
dataset: Dataset & {
readme: string | null;
};
}) {
return (
<>
<NextSeo title={`${dataset.name} page`} />
<main className="max-w-5xl px-2 prose mx-auto my-8 prose-thead:border-b-4 prose-table:max-w-5xl prose-table:overflow-scroll prose-thead:overflow-scroll prose-tbody:overflow-scroll prose-thead:pb-2 prose-thead:border-zinc-900 prose-th:uppercase prose-th:text-left prose-th:font-light prose-th:text-xs">
<Breadcrumbs links={[{ title: dataset.name, href: '' }]} />
<h1 className="uppercase mb-0 mt-16">{dataset.name}</h1>
<p className="mb-8">
<span className="font-semibold">Repository:</span>{' '}
<a target="_blank" href={dataset.url}>
{dataset.url}
</a>
</p>
<h2 className="mb-0 mt-10">FILES</h2>
<div className="inline-block min-w-full py-2 align-middle">
<table className="min-w-full divide-y divide-gray-300">
<thead className="border-b-4 pb-2 border-zinc-900">
<tr>
<th
className="uppercase text-left font-light text-xs pb-3"
scope="col"
>
Name
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{dataset.files?.map((file) => (
<tr key={file}>
<td className="whitespace-nowrap text-left py-4 text-sm text-gray-500">
<a href={file}>{file.split('/').slice(-1)}</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
{dataset.files && dataset.files.length > 0 && (
<>
<h2 className="mb-0 mt-10">DATA PREVIEWS</h2>
{dataset.files?.map((file) => (
<div key={file} className="preview-table my-8">
<h3>{file.split('/').slice(-1)}</h3>
<Table url={file} />
</div>
))}
</>
)}
{dataset.readme && (
<>
<h2 className="uppercase font-black">Readme</h2>
{dataset.readme && (
<ReactMarkdown
remarkPlugins={[
remarkFrontmatter,
remarkGfm,
[extract, { remove: true }],
]}
>
{dataset.readme}
</ReactMarkdown>
)}
</>
)}
</main>
</>
);
}
export async function getStaticPaths() {
const datasetsFile = path.join(process.cwd(), 'datasets.json');
const datasets = await fs.readFile(datasetsFile, 'utf8');
return {
paths: JSON.parse(datasets).map((dataset: Dataset) => {
return {
params: { datasetName: dataset.name },
};
}),
fallback: false, // can also be true or 'blocking'
};
}
export const getStaticProps: GetStaticProps = async ({ params }) => {
const datasetsFile = path.join(process.cwd(), 'datasets.json');
const datasetsString = await fs.readFile(datasetsFile, 'utf8');
const datasets: Dataset[] = JSON.parse(datasetsString);
const dataset: Dataset | undefined = datasets.find(
(_dataset) => _dataset.name === params?.datasetName
);
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const readmes = await Promise.all(['/README.md', '/readme.md', '/Readme.md'].map(async (readme) => await getProjectReadme(
'fivethirtyeight',
'data',
'master',
dataset?.name + readme,
github_pat
)));
const readme = readmes.find(item => item !== null)
if (!readme) console.log('Readme not found for ' + dataset?.name)
return {
props: {
dataset: {
...dataset,
readme,
files: dataset && dataset.files ? dataset.files : null,
},
},
};
};

View File

@@ -0,0 +1,211 @@
import Image from 'next/image';
import { Inter } from 'next/font/google';
import { format } from 'timeago.js';
import { promises as fs } from 'fs';
import path from 'path';
import { NextSeo } from 'next-seo';
const inter = Inter({ subsets: ['latin'] });
export interface Article {
date: string;
title: string;
url: string;
}
export interface Dataset {
url: string;
name: string;
displayName: string;
articles: Article[];
files?: string[];
}
// Request a weekday along with a long date
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
} as const;
export function MobileItem({ dataset }: { dataset: Dataset }) {
return (
<div className="flex gap-x-2 pb-2 py-4 items-center justify-between border-b border-zinc-600">
<div className="flex flex-col">
<span className="font-mono font-light">{dataset.name}</span>
{dataset.articles.map((article) => (
<div key={article.title} className="py-1 flex flex-col">
<span className="font-bold hover:underline">{article.title}</span>
<span className="font-light text-base">
{format(article.date).includes('years')
? new Date(article.date).toLocaleString('en-US', options)
: format(article.date)}
</span>{' '}
</div>
))}
</div>
<div className="flex flex-col justify-start">
<a
className="ml-2 border border-zinc-900 font-light px-4 py-1 text-sm transition hover:bg-zinc-900 hover:text-white"
href={dataset.url}
>
info
</a>
{/*
<button>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-12 h-12 text-blue-400 hover:text-blue-300 transition mt-1"
>
<path
fillRule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-.53 14.03a.75.75 0 001.06 0l3-3a.75.75 0 10-1.06-1.06l-1.72 1.72V8.25a.75.75 0 00-1.5 0v5.69l-1.72-1.72a.75.75 0 00-1.06 1.06l3 3z"
clipRule="evenodd"
/>
</svg>
</button> */}
</div>
</div>
);
}
export function DesktopItem({ dataset }: { dataset: Dataset }) {
return (
<>
{dataset.articles.map((article, index) => (
<tr
key={article.url}
className={`${
index === dataset.articles.length - 1 ? 'border-b' : ''
} border-zinc-400`}
>
<td className="py-8 font-light font-mono text-[13px] text-zinc-700">
{index === 0 ? dataset.name : ''}
</td>
<td>
<a
className="py-8 font-bold hover:underline pr-2"
href={article.url}
>
{article.title}
</a>
</td>
<td className="py-8 font-light text-[14px] min-w-[138px] font-mono text-[#999]">
{format(article.date).includes('years')
? new Date(article.date).toLocaleString('en-US', options)
: format(article.date)}
</td>
<td className="py-8">
{index === 0 && (
<a
className="ml-2 border border-zinc-900 font-light px-[25px] py-2.5 text-sm transition hover:bg-zinc-900 hover:text-white"
href={dataset.url}
>
info
</a>
)}
</td>
{/*
<td>
<button>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
className="w-12 h-12 text-blue-400 hover:text-blue-300 transition mt-1"
>
<path
fillRule="evenodd"
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-.53 14.03a.75.75 0 001.06 0l3-3a.75.75 0 10-1.06-1.06l-1.72 1.72V8.25a.75.75 0 00-1.5 0v5.69l-1.72-1.72a.75.75 0 00-1.06 1.06l3 3z"
clipRule="evenodd"
/>
</svg>
</button>
</td>*/}
</tr>
))}
</>
);
}
export async function getStaticProps() {
const jsonDirectory = path.join(process.cwd(), '/datasets.json');
const datasetString = await fs.readFile(jsonDirectory, 'utf8');
const datasets = JSON.parse(datasetString);
return {
props: { datasets },
};
}
export default function Home({ datasets }: { datasets: Dataset[] }) {
return (
<>
<NextSeo title="FiveThirtyEight tribute by PortalJS" />
<main
className={`flex min-h-screen flex-col items-center max-w-5xl mx-auto pt-20 px-2.5 ${inter.className}`}
>
<div>
<h1 className="text-[40px] font-bold text-zinc-800 text-center">
Our Data
</h1>
<p className="max-w-[600px] text-[17px] text-center text-[#6d6f71]">
Were sharing the data and code behind some of our articles and
graphics. We hope youll use it to check our work and to create
stories and visualizations of&nbsp;your&nbsp;own.
</p>
</div>
<article className="w-full px-2 md:hidden py-4">
{datasets.map((dataset) => (
<MobileItem key={dataset.name} dataset={dataset} />
))}
</article>
<table className="w-full mt-10 mb-4 hidden md:table">
<thead className="border-b-4 pb-2 border-zinc-900">
<tr>
<th className="uppercase text-left font-normal text-xs pb-3">
data set
</th>
<th className="uppercase text-left font-normal text-xs pb-3">
related content
</th>
<th className="uppercase text-left font-normal text-xs pb-3">
last updated
</th>
</tr>
</thead>
<tbody>
{datasets.map((dataset) => (
<DesktopItem key={dataset.name} dataset={dataset} />
))}
</tbody>
</table>
<p className="text-[13px] py-8">
Unless otherwise noted, our data sets are available under the{' '}
<a
className="text-blue-400 hover:underline"
href="http://creativecommons.org/licenses/by/4.0/"
>
Creative Commons Attribution 4.0 International license
</a>
, and the code is available under the{' '}
<a
className="text-blue-400 hover:underline"
href="http://opensource.org/licenses/MIT"
>
MIT license
</a>
. If you find this information useful, please{' '}
<a
className="text-blue-400 hover:underline"
href="mailto:data@fivethirtyeight.com"
>
let us know
</a>
.
</p>
</main>
</>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,8 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.preview-table > div {
overflow-x: scroll;
overflow-y: hidden;
}

View File

@@ -0,0 +1,18 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
'./components/**/*.{js,ts,jsx,tsx,mdx}',
'./app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [require('@tailwindcss/typography')],
};

View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,29 @@
{
"extends": ["next", "next/core-web-vitals"],
"ignorePatterns": ["!**/*", ".next/**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {
"@next/next/no-html-link-for-pages": [
"error",
"examples/simple-example/pages"
]
}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
],
"rules": {
"@next/next/no-html-link-for-pages": "off"
},
"env": {
"jest": true
}
}

View File

@@ -0,0 +1,7 @@
node_modules
**/.next/**
**/_next/**
**/dist/**
**/__tmp__/**
lerna.json
.github

View File

@@ -0,0 +1 @@
{}

View File

@@ -0,0 +1,101 @@
# A data catalog with data on GitHub
This example showcases a simple data catalog that get its data from a list of GitHub repos that serve as datasets.
A `datasets.json` file is used to specify which datasets are going to be part of the data catalog.
The application contains an index page, which lists all the datasets specified in the `datasets.json` file, and users can see more information about each dataset, such as the list of data files in it and the README, by clicking the "info" button on the list.
You can read more about it on the [Data catalog with data on GitHub](https://portaljs.org/docs/examples/github-backed-catalog) blog post.
## Demo
https://example.portaljs.org/
## Deploy your own
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fgithub-backed-catalog)
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.
## How to use
### Install
Execute `create-next-app` to bootstrap the example:
```
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/github-backed-catalog
cd <app-name>
```
### Set environment variables
This project uses the GitHub API, which for anonymous users will cap at 50 requests per hour, so you might want to get a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and add it to a `.env` file inside the folder like so
```
GITHUB_PAT=<github token>
```
### Change datasets
You can change the datasets that will be displayed in the data catalog by editing the file `datasets.json`. Some examples can be found inside [this repo](https://github.com/datasets).
### Run in development mode
Run the app using:
```
npm run dev
```
Open http://localhost:3000 from your browser. You should see something similar to this:
![](https://i.imgur.com/jAljJ9C.png)
If click on the `info` button for a dataset you will see a page similar to this:
![](https://i.imgur.com/AoJd4O0.png)
## Notes
### Structure of `datasets.json`
The `datasets.json` file is simply a list of datasets, below you can see a minimal example of a dataset:
```json
{
"owner": "fivethirtyeight",
"repo": "data",
"branch": "master",
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
"readme": "nba-raptor/README.md"
}
```
It has:
- A `owner` which is going to be the github repo owner
- A `repo` which is going to be the github repo name
- A `branch` which is going to be the branch to which we need to get the files and the readme
- A list of `files` which is going to be a list of paths with files that you want to show to the world
- A `readme` which is going to be the path to your data description, it can also be a subpath eg: `example/README.md`
You can also add:
- A `description` which is useful if you have more than one dataset for each repo, if not provided we are just going to use the repo description
- A `Name` which is useful if you want to give your dataset a nice name, if not provided we are going to use the junction of the `owner` the `repo` + the path of the README, in the exaple above it will be `fivethirtyeight/data/nba-raptor`
### Extra commands
You can also build the project for production with:
```
npm run build
```
And run the production build with:
```
npm run start
```

View File

@@ -0,0 +1,28 @@
import Link from "next/link";
import HomeIcon from "../icons/HomeIcon";
export default function Breadcrumbs({
links,
}: {
links: { title: string; href?: string; target?: string }[];
}) {
const current = links.at(-1);
return (
<div className="flex items-center uppercase font-black text-xs">
<Link className="flex items-center" href="/">
<HomeIcon />
</Link>
{/* {links.length > 1 && links.slice(0, -1).map((link) => {
return <>
<span className="mx-4">/</span>
<Link href={link.href}>{link.title}</Link>
</>
})} */}
<span className="mx-4">/</span>
<span>{current.title}</span>
</div>
);
}

View File

@@ -0,0 +1,13 @@
export default function ExternalLinkIcon({ className = "" }) {
return (
<div className={`inline-block w-4 ${className}`}>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 64 64"
fill="currentColor"
>
<path d="M 40 10 C 38.896 10 38 10.896 38 12 C 38 13.104 38.896 14 40 14 L 47.171875 14 L 30.585938 30.585938 C 29.804938 31.366938 29.804938 32.633063 30.585938 33.414062 C 30.976938 33.805063 31.488 34 32 34 C 32.512 34 33.023063 33.805062 33.414062 33.414062 L 50 16.828125 L 50 24 C 50 25.104 50.896 26 52 26 C 53.104 26 54 25.104 54 24 L 54 12 C 54 10.896 53.104 10 52 10 L 40 10 z M 18 12 C 14.691 12 12 14.691 12 18 L 12 46 C 12 49.309 14.691 52 18 52 L 46 52 C 49.309 52 52 49.309 52 46 L 52 34 C 52 32.896 51.104 32 50 32 C 48.896 32 48 32.896 48 34 L 48 46 C 48 47.103 47.103 48 46 48 L 18 48 C 16.897 48 16 47.103 16 46 L 16 18 C 16 16.897 16.897 16 18 16 L 30 16 C 31.104 16 32 15.104 32 14 C 32 12.896 31.104 12 30 12 L 18 12 z" />
</svg>
</div>
);
}

View File

@@ -0,0 +1,10 @@
export default function HomeIcon({ className = "" }) {
return (
<div className={`inline-block w-4 ${className}`}>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
{" "}
<path d="M 12 2 A 1 1 0 0 0 11.289062 2.296875 L 1.203125 11.097656 A 0.5 0.5 0 0 0 1 11.5 A 0.5 0.5 0 0 0 1.5 12 L 4 12 L 4 20 C 4 20.552 4.448 21 5 21 L 9 21 C 9.552 21 10 20.552 10 20 L 10 14 L 14 14 L 14 20 C 14 20.552 14.448 21 15 21 L 19 21 C 19.552 21 20 20.552 20 20 L 20 12 L 22.5 12 A 0.5 0.5 0 0 0 23 11.5 A 0.5 0.5 0 0 0 22.796875 11.097656 L 12.716797 2.3027344 A 1 1 0 0 0 12.710938 2.296875 A 1 1 0 0 0 12 2 z" />
</svg>
</div>
);
}

View File

@@ -19,10 +19,7 @@
"owner": "datasets",
"branch": "main",
"repo": "investor-flow-of-funds-us",
"files": [
"data/monthly.csv",
"data/weekly.csv"
],
"files": ["data/monthly.csv", "data/weekly.csv"],
"readme": "README.md"
},
{
@@ -38,7 +35,10 @@
"owner": "fivethirtyeight",
"repo": "data",
"branch": "master",
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
"files": [
"nba-raptor/historical_RAPTOR_by_player.csv",
"nba-raptor/historical_RAPTOR_by_team.csv"
],
"readme": "nba-raptor/README.md"
}
]

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

View File

@@ -0,0 +1,172 @@
import { Octokit } from "octokit";
export interface GithubProject {
owner: string;
repo: string;
branch: string;
files: string[];
readme: string;
description?: string;
name?: string;
}
export async function getProjectReadme(
owner: string,
repo: string,
branch: string,
readme: string,
github_pat?: string
) {
const octokit = new Octokit({ auth: github_pat });
try {
const response = await octokit.rest.repos.getContent({
owner,
repo,
path: readme,
ref: branch,
});
const data = response.data as { content?: string };
const fileContent = data.content ? data.content : "";
if (fileContent === "") {
return null;
}
const decodedContent = Buffer.from(fileContent, "base64").toString();
return decodedContent;
} catch (error) {
throw new Error(
"Couldn't get project readme please make sure that you are pointing to a valid repo and that the repo in question contains a README.md"
);
}
}
export async function getLastUpdated(
owner: string,
repo: string,
branch: string,
readme: string,
github_pat?: string
) {
const octokit = new Octokit({ auth: github_pat });
try {
const response = await octokit.rest.repos.listCommits({
owner,
repo,
ref: branch,
});
return response.data[0].commit.committer.date;
} catch (error) {
throw new Error(
"Couldn't get project list of commits please make sure that you are pointing to a valid repo"
);
}
}
export async function getProjectMetadata(
owner: string,
repo: string,
github_pat?: string
) {
const octokit = new Octokit({ auth: github_pat });
try {
const response = await octokit.rest.repos.get({
owner,
repo,
});
return response.data;
} catch (error) {
throw new Error(
"Couldn't get project metadata please make sure that you are pointing to a valid repo"
);
}
}
export async function getRepoContents(
owner: string,
repo: string,
branch: string,
files: string[],
github_pat?: string
) {
const octokit = new Octokit({ auth: github_pat });
try {
const contents = [];
for (const path of files) {
const response = await octokit.rest.repos.getContent({
owner,
repo,
ref: branch,
path: path,
});
const data = response.data as {
download_url?: string;
name: string;
size: number;
};
contents.push({
download_url: data.download_url,
name: data.name,
size: data.size,
});
}
return contents;
} catch (error) {
if (
error.message ===
'This endpoint can only return blobs smaller than 100 MB in size. The requested blob is too large to fetch via the API, but you can always clone the repository via Git to obtain it.: {"resource":"Blob","field":"data","code":"too_large"}'
) {
throw new Error(
`The requested files ${files.join(
", "
)} are too big making it impossible to fetch via Github API`
);
}
throw new Error(
"Couldn't get project contents please make sure that you are pointing to a valid repo"
);
}
}
export async function getProject(project: GithubProject, github_pat?: string) {
const projectMetadata = await getProjectMetadata(
project.owner,
project.repo,
github_pat
);
if (!projectMetadata) {
return null;
}
const projectReadme = await getProjectReadme(
project.owner,
project.repo,
project.branch,
project.readme,
github_pat
);
let projectData = [];
if (project.files) {
projectData = await getRepoContents(
project.owner,
project.repo,
project.branch,
project.files,
github_pat
);
}
const projectBase =
project.readme && project.readme.split("/").length > 1
? project.readme.split("/").slice(0, -1).join("/")
: "/";
const last_updated = await getLastUpdated(
project.owner,
project.repo,
project.branch,
projectBase,
github_pat
);
return {
...projectMetadata,
files: projectData,
readmeContent: projectReadme,
last_updated,
base_path: projectBase,
};
}

View File

@@ -0,0 +1,17 @@
const nextConfig = {
async rewrites() {
return {
beforeFiles: [
{
source: "/@:org/:project*",
destination: "/@org/:org/:project*",
},
],
};
},
serverRuntimeConfig: {
github_pat: process.env.GITHUB_PAT ? process.env.GITHUB_PAT : null,
},
};
module.exports = nextConfig;

View File

@@ -19,6 +19,7 @@
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"react-timeago": "^7.1.0",
"remark-gfm": "^3.0.1",
"typescript": "5.0.4"
},
@@ -26,6 +27,7 @@
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"prettier": "2.8.8",
"tailwindcss": "^3.3.1"
}
},
@@ -4676,6 +4678,21 @@
"node": ">= 0.8.0"
}
},
"node_modules/prettier": {
"version": "2.8.8",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
"dev": true,
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -4797,6 +4814,14 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
},
"node_modules/react-timeago": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-7.1.0.tgz",
"integrity": "sha512-rouF7MiEm55fH791Y8cg+VobIJgx8gtNJ+gjr86R4ZqO1WKPkXiXjdT/lRzrvEkUzsxT1exHqV2V+Zdi114H3A==",
"peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View File

@@ -0,0 +1,35 @@
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"prettier": "prettier --write ."
},
"dependencies": {
"@types/node": "18.16.0",
"@types/react": "18.0.38",
"@types/react-dom": "18.0.11",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
"next": "13.3.1",
"next-seo": "^6.0.0",
"octokit": "^2.0.14",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"react-timeago": "^7.1.0",
"remark-gfm": "^3.0.1",
"typescript": "5.0.4"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.23",
"prettier": "2.8.8",
"tailwindcss": "^3.3.1"
}
}

View File

@@ -0,0 +1,127 @@
import { NextSeo } from "next-seo";
import { promises as fs } from "fs";
import path from "path";
import getConfig from "next/config";
import { getProject, GithubProject } from "../../../lib/octokit";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import Breadcrumbs from "../../../components/_shared/Breadcrumbs";
export default function ProjectPage({ project }) {
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`;
return (
<>
<NextSeo
title={`${repoId}${
project.base_path !== "/" ? "/" + project.base_path : ""
} - GitHub Datasets`}
/>
<main className="prose mx-auto my-8">
<Breadcrumbs links={[{ title: repoId, href: "" }]} />
<h1 className="mb-0 mt-16">{project.repo_config.name || repoId}</h1>
<p className="mb-8">
<span className="font-semibold">Repository:</span>{" "}
<a target="_blank" href={project.html_url}>
{project.html_url}
</a>
</p>
<h2 className="mb-0 mt-10">Files</h2>
<div className="inline-block min-w-full py-2 align-middle">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Size
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{project.files.map((file) => (
<tr key={file.download_url}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<a href={file.download_url}>{file.name}</a>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{file.size} Bytes
</td>
</tr>
))}
</tbody>
</table>
</div>
<hr />
<h2 className="uppercase font-black">Readme</h2>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{project.readmeContent}
</ReactMarkdown>
</main>
</>
);
}
// Generates `/posts/1` and `/posts/2`
export async function getStaticPaths() {
const jsonDirectory = path.join(process.cwd(), "datasets.json");
const repos = await fs.readFile(jsonDirectory, "utf8");
return {
paths: JSON.parse(repos).map((repo) => {
const projectPath =
repo.readme && repo.readme.split("/").length > 1
? repo.readme.split("/").slice(0, -1)
: null;
let path = [repo.repo];
if (projectPath) {
projectPath.forEach((element) => {
path.push(element);
});
}
return {
params: { org: repo.owner, path },
};
}),
fallback: false, // can also be true or 'blocking'
};
}
export async function getStaticProps({ params }) {
const jsonDirectory = path.join(process.cwd(), "datasets.json");
const reposFile = await fs.readFile(jsonDirectory, "utf8");
const repos: GithubProject[] = JSON.parse(reposFile);
const repo = repos.find((_repo) => {
const projectPath =
_repo.readme && _repo.readme.split("/").length > 1
? _repo.readme.split("/").slice(0, -1)
: null;
let path = [_repo.repo];
if (projectPath) {
projectPath.forEach((element) => {
path.push(element);
});
}
return (
_repo.owner == params.org &&
JSON.stringify(path) === JSON.stringify(params.path)
);
});
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const project = await getProject(repo, github_pat);
return {
props: {
project: { ...project, repo_config: repo },
},
};
}

View 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>GitHub Datasets</title>
</Head>
<main className="app">
<Component {...pageProps} />
</main>
</>
);
}
export default CustomApp;

View File

@@ -0,0 +1,139 @@
import { promises as fs } from "fs";
import path from "path";
import { getProject } from "../lib/octokit";
import getConfig from "next/config";
import ExternalLinkIcon from "../components/icons/ExternalLinkIcon";
import TimeAgo from "react-timeago";
import Link from "next/link";
import { NextSeo } from "next-seo";
export async function getStaticProps() {
const jsonDirectory = path.join(process.cwd(), "/datasets.json");
const repos = await fs.readFile(jsonDirectory, "utf8");
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const projects = await Promise.all(
JSON.parse(repos).map(async (repo) => {
const project = await getProject(repo, github_pat);
return { ...project, repo_config: repo };
})
);
return {
props: {
projects,
},
};
}
export function Datasets({ projects }) {
return (
<>
<NextSeo title="GitHub Datasets" />
<div className="bg-white min-h-screen">
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
<div className="text-center">
<h2 className="text-3xl font-bold leading-10 tracking-tight">
GitHub Datasets
</h2>
<p className="mt-3 mx-auto max-w-2xl text-base leading-7 text-gray-500">
Data catalog with datasets hosted on GitHub by{" "}
<Link
target="_blank"
className="underline"
href="https://portaljs.org/"
>
🌀 PortalJS
</Link>
</p>
</div>
<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"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Repository
</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">
{projects.map((project) => (
<tr key={project.id}>
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
{project.repo_config.name
? project.repo_config.name
: project.full_name +
(project.base_path === "/"
? ""
: "/" + project.base_path)}
</td>
<td className="whitespace-nowrap px-3 py-6 text-sm group text-gray-500 hover:text-gray-900 transition-all duration-250">
<a
href={project.html_url}
target="_blank"
className="flex items-center"
>
@{project.full_name}{" "}
<ExternalLinkIcon className="ml-1" />
</a>
</td>
<td className="px-3 py-4 text-sm text-gray-500">
{project.repo_config.description
? project.repo_config.description
: project.description}
</td>
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
<TimeAgo date={new Date(project.last_updated)} />
</td>
<td className="relative whitespace-nowrap py-6 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<a
href={`/@${project.repo_config.owner}/${
project.repo_config.repo
}/${
project.base_path === "/" ? "" : project.base_path
}`}
className="border border-gray-900 text-gray-900 px-4 py-2 transition-all hover:bg-gray-900 hover:text-white"
>
info
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</>
);
}
export default Datasets;

View File

@@ -0,0 +1,80 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
}
svg {
display: block;
vertical-align: middle;
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
overflow: scroll;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

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

View File

@@ -7,13 +7,12 @@ import { Mermaid } from '@flowershow/core';
// to handle import statements. Instead, you must include components in scope
// here.
const components = {
Table: dynamic(() => import('./Table')),
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.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')),
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
LineChart: dynamic(() => import('@portaljs/components').then(mod => mod.LineChart)),
} as any;
export default function DRD({ source }: { source: any }) {

View File

@@ -1,55 +0,0 @@
import VegaLite from "./VegaLite";
export default function LineChart({
data = [],
fullWidth = false,
title = "",
xAxis = "x",
yAxis = "y",
}) {
var tmp = data;
if (Array.isArray(data)) {
tmp = data.map((r, i) => {
return { x: r[0], y: r[1] };
});
}
const vegaData = { table: tmp };
const spec = {
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
title,
width: "container",
height: 300,
mark: {
type: "line",
color: "black",
strokeWidth: 1,
tooltip: true,
},
data: {
name: "table",
},
selection: {
grid: {
type: "interval",
bind: "scales",
},
},
encoding: {
x: {
field: xAxis,
timeUnit: "year",
type: "temporal",
},
y: {
field: yAxis,
type: "quantitative",
},
},
};
if (typeof data === 'string') {
spec.data = { "url": data } as any
return <VegaLite fullWidth={fullWidth} spec={spec} />;
}
return <VegaLite fullWidth={fullWidth} data={vegaData} spec={spec} />;
}

View File

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

View File

@@ -1,16 +0,0 @@
import papa from "papaparse";
const parseCsv = (csv) => {
csv = csv.trim();
const rawdata = papa.parse(csv, { header: true });
const cols = rawdata.meta.fields.map((r, i) => {
return { key: r, name: r };
});
return {
rows: rawdata.data,
fields: cols,
};
};
export default parseCsv;

File diff suppressed because it is too large Load Diff

View File

@@ -6,21 +6,22 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"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",
"@portaljs/components": "^0.1.0",
"@tanstack/react-table": "^8.8.5",
"@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.21",
"gray-matter": "^4.0.3",
"hastscript": "^7.2.0",
"mdx-mermaid": "2.0.0-rc7",
@@ -29,6 +30,7 @@
"papaparse": "^5.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-vega": "^7.6.0",
"rehype-autolink-headings": "^6.1.1",
"rehype-katex": "^6.0.3",
@@ -42,7 +44,13 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/flexsearch": "^0.7.3",
"@types/node": "18.16.0",
"@types/react": "18.2.0",
"@types/react-dom": "18.2.0",
"autoprefixer": "^10.4.14",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
"postcss": "^8.4.23",
"tailwindcss": "^3.3.1"
}

View File

@@ -1,7 +1,9 @@
import { promises as fs } from 'fs';
import { existsSync, promises as fs } from 'fs';
import path from 'path';
import parse from '../lib/markdown';
import DRD from '../components/DRD';
import DataRichDocument from '../components/DataRichDocument';
import clientPromise from '../lib/mddb';
export const getStaticPaths = async () => {
const contentDir = path.join(process.cwd(), '/content/');
@@ -23,37 +25,101 @@ export const getStaticProps = async (context) => {
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');
let { mdxSource, frontMatter } = await parse(readme, '.mdx', { datasets });
return {
props: {
mdxSource,
frontMatter,
frontMatter: JSON.stringify(frontMatter),
},
};
};
export default function DatasetPage({ mdxSource, frontMatter }) {
frontMatter = JSON.parse(frontMatter);
return (
<div className="prose dark:prose-invert mx-auto">
<div className="prose dark:prose-invert mx-auto py-8">
<header>
<div className="mb-6">
<>
<h1>{frontMatter.title}</h1>
<h1 className="mb-2">{frontMatter.title}</h1>
{frontMatter.author && (
<div className="-mt-6">
<p className="opacity-60 pl-1">{frontMatter.author}</p>
</div>
<p className="my-0">
<span className="font-semibold">Author: </span>
<span className="my-0">{frontMatter.author}</span>
</p>
)}
{frontMatter.description && (
<p className="description">{frontMatter.description}</p>
<p className="my-0">
<span className="font-semibold">Description: </span>
<span className="description my-0">
{frontMatter.description}
</span>
</p>
)}
{frontMatter.modified && (
<p className="my-0">
<span className="font-semibold">Modified: </span>
<span className="description my-0">
{new Date(frontMatter.modified).toLocaleDateString()}
</span>
</p>
)}
{frontMatter.files && (
<section className="py-6">
<h2 className="mt-0">Data files</h2>
<table className="table-auto">
<thead>
<tr>
<th>File</th>
<th>Format</th>
</tr>
</thead>
<tbody>
{frontMatter.files.map((f) => {
const fileName = f.split('/').slice(-1);
return (
<tr key={`resources-list-${f}`}>
<td>
<a target="_blank" href={f}>
{fileName}
</a>
</td>
<td>
{fileName[0].split('.').slice(-1)[0].toUpperCase()}
</td>
</tr>
);
})}
</tbody>
</table>
</section>
)}
</>
</div>
</header>
<main>
<DRD source={mdxSource} />
<DataRichDocument source={mdxSource} />
</main>
</div>
);

View File

@@ -1,4 +1,6 @@
import '../styles/globals.css'
import '@portaljs/components/styles.css'
import type { AppProps } from 'next/app'
export default function App({ Component, pageProps }: AppProps) {

View File

@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es6",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,

35
examples/openspending/.gitignore vendored Normal file
View 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*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View File

@@ -0,0 +1,102 @@
# A data catalog with data on GitHub
This example showcases a simple data catalog that get its data from a list of GitHub repos that serve as datasets.
A `datasets.json` file is used to specify which datasets are going to be part of the data catalog.
The application contains an index page, which lists all the datasets specified in the `datasets.json` file, and users can see more information about each dataset, such as the list of data files in it and the README, by clicking the "info" button on the list.
You can read more about it on the [Data catalog with data on GitHub](https://portaljs.org/docs/examples/github-backed-catalog) blog post.
## Demo
https://example.portaljs.org/
## Deploy your own
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fgithub-backed-catalog)
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.
## How to use
### Install
Execute `create-next-app` to bootstrap the example:
```
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/github-backed-catalog
cd <app-name>
```
### Set environment variables
This project uses the GitHub API, which for anonymous users will cap at 50 requests per hour, so you might want to get a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and add it to a `.env` file inside the folder like so
```
GITHUB_PAT=<github token>
```
### Change datasets
You can change the datasets that will be displayed in the data catalog by editing the file `datasets.json`. Some examples can be found inside [this repo](https://github.com/datasets).
### Run in development mode
Run the app using:
```
npm run dev
```
Open http://localhost:3000 from your browser. You should see something similar to this:
![](https://i.imgur.com/jAljJ9C.png)
If click on the `info` button for a dataset you will see a page similar to this:
![](https://i.imgur.com/AoJd4O0.png)
## Notes
### Structure of `datasets.json`
The `datasets.json` file is simply a list of datasets, below you can see a minimal example of a dataset:
```json
{
"owner": "fivethirtyeight",
"repo": "data",
"branch": "master",
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
"readme": "nba-raptor/README.md"
}
```
It has:
- A `owner` which is going to be the github repo owner
- A `repo` which is going to be the github repo name
- A `branch` which is going to be the branch to which we need to get the files and the readme
- A list of `files` which is going to be a list of paths with files that you want to show to the world
- A `readme` which is going to be the path to your data description, it can also be a subpath eg: `example/README.md`
You can also add:
- A `description` which is useful if you have more than one dataset for each repo, if not provided we are just going to use the repo description
- A `Name` which is useful if you want to give your dataset a nice name, if not provided we are going to use the junction of the `owner` the `repo` + the path of the README, in the exaple above it will be `fivethirtyeight/data/nba-raptor`
### Extra commands
You can also build the project for production with:
```
npm run build
```
And run the production build with:
```
npm run start
```

View File

@@ -0,0 +1,15 @@
import Link from 'next/link'
import clsx from 'clsx'
export function Button({ href, className = "", ...props }) {
className = clsx(
'inline-flex justify-center rounded-2xl bg-emerald-600 p-4 text-base font-semibold text-white hover:bg-emerald-500 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-500 active:text-white/70',
className
)
return href ? (
<Link href={href} className={className} {...props} />
) : (
<button className={className} {...props} />
)
}

View File

@@ -0,0 +1,10 @@
import clsx from 'clsx'
export function Container({ className = "", ...props }) {
return (
<div
className={clsx('mx-auto max-w-7xl px-4 sm:px-6 lg:px-8', className)}
{...props}
/>
)
}

View File

@@ -0,0 +1,54 @@
import Image from 'next/image'
import { Button } from './Button'
import { Container } from './Container'
import logo from "../public/logo.svg"
import Link from 'next/link'
import { useRouter } from 'next/router'
export function Header() {
const router = useRouter();
const isActive = (navLink) => {
return router.asPath.split("?")[0] == navLink.href;
}
const navLinks = [
{
title: "Home",
href: "/#header"
},
{
title: "Datasets",
href: "/#datasets"
},
{
title: "Community",
href: "https://community.openspending.org/"
}
]
return (
<header className="z-50 pb-5 lg:pt-11 sticky top-0 backdrop-blur" id="header">
<Container className="flex flex-wrap items-center justify-center sm:justify-between lg:flex-nowrap">
<div className="mt-10 lg:mt-0 lg:grow lg:basis-0 flex items-center">
<Image src={logo} alt="OpenSpending" className="h-12 w-auto" />
</div>
<ul className='list-none flex gap-x-5 text-base font-medium'>
{navLinks.map((link, i) => (
<li key={`nav-link-${i}`}>
<Link
className={`text-emerald-900 hover:text-emerald-600 ${isActive(link) ? "text-emerald-600" : ""}`}
href={link.href}
scroll={false}
>
{link.title}
</Link>
</li>))}
</ul>
<div className="hidden sm:mt-10 sm:flex lg:mt-0 lg:grow lg:basis-0 lg:justify-end">
<Button href="#">View on GitHub</Button>
</div>
</Container>
</header >
)
}

View File

@@ -0,0 +1,47 @@
import { Button } from './Button'
import { Container } from './Container'
export function Hero() {
return (
<div className="relative pb-20 pt-10 sm:py-40">
<div className="absolute inset-x-0 -bottom-14 -top-48 overflow-hidden bg-green-50 bg-opacity-50">
<div className="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-white" />
<div className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-white" />
</div>
<Container className="relative">
<div className="mx-auto max-w-2xl lg:max-w-4xl lg:px-12">
<h1 className="font-display text-5xl font-bold tracking-tighter text-emerald-600 sm:text-7xl">
It's our money!
</h1>
<div className="mt-6 space-y-6 font-display text-2xl tracking-tight text-emerald-900">
<p>
By understanding how governments spend money in our name can we have a say
in how that money will affect our own lives. The journey starts here.
</p>
<p>
OpenSpending is a free, open and global platform to search, visualise and analyse
fiscal data in the public sphere.
</p>
</div>
<Button href="#" className="mt-10 w-full sm:hidden">
View on GitHub
</Button>
<dl className="mt-10 grid grid-cols-2 gap-x-10 gap-y-6 sm:mt-16 sm:gap-x-16 sm:gap-y-10 sm:text-center lg:auto-cols-auto lg:grid-flow-col lg:grid-cols-none lg:justify-start lg:text-left">
{[
['Countries', '75'],
['Datasets', '2091'],
['Files', '9230'],
].map(([name, value]) => (
<div key={name}>
<dt className="font-mono text-sm text-emerald-600">{name}</dt>
<dd className="mt-0.5 text-2xl font-semibold tracking-tight text-emerald-900">
{value}
</dd>
</div>
))}
</dl>
</div>
</Container>
</div>
)
}

View File

@@ -0,0 +1,20 @@
import Link from "next/link";
import HomeIcon from "../icons/HomeIcon";
export default function Breadcrumbs({ links }: { links: { title: string, href?: string, target?: string }[] }) {
const current = links.at(-1);
return <div className="flex items-center uppercase font-black text-xs">
<Link className="flex items-center" href='/'><HomeIcon /></Link>
{/* {links.length > 1 && links.slice(0, -1).map((link) => {
return <>
<span className="mx-4">/</span>
<Link href={link.href}>{link.title}</Link>
</>
})} */}
<span className="mx-4">/</span>
<span>{current.title}</span>
</div >
}

View File

@@ -0,0 +1,3 @@
export default function ExternalLinkIcon({ className = "" }) {
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="currentColor"><path d="M 40 10 C 38.896 10 38 10.896 38 12 C 38 13.104 38.896 14 40 14 L 47.171875 14 L 30.585938 30.585938 C 29.804938 31.366938 29.804938 32.633063 30.585938 33.414062 C 30.976938 33.805063 31.488 34 32 34 C 32.512 34 33.023063 33.805062 33.414062 33.414062 L 50 16.828125 L 50 24 C 50 25.104 50.896 26 52 26 C 53.104 26 54 25.104 54 24 L 54 12 C 54 10.896 53.104 10 52 10 L 40 10 z M 18 12 C 14.691 12 12 14.691 12 18 L 12 46 C 12 49.309 14.691 52 18 52 L 46 52 C 49.309 52 52 49.309 52 46 L 52 34 C 52 32.896 51.104 32 50 32 C 48.896 32 48 32.896 48 34 L 48 46 C 48 47.103 47.103 48 46 48 L 18 48 C 16.897 48 16 47.103 16 46 L 16 18 C 16 16.897 16.897 16 18 16 L 30 16 C 31.104 16 32 15.104 32 14 C 32 12.896 31.104 12 30 12 L 18 12 z"/></svg></div>
}

View File

@@ -0,0 +1,3 @@
export default function HomeIcon({ className = "" }) {
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M 12 2 A 1 1 0 0 0 11.289062 2.296875 L 1.203125 11.097656 A 0.5 0.5 0 0 0 1 11.5 A 0.5 0.5 0 0 0 1.5 12 L 4 12 L 4 20 C 4 20.552 4.448 21 5 21 L 9 21 C 9.552 21 10 20.552 10 20 L 10 14 L 14 14 L 14 20 C 14 20.552 14.448 21 15 21 L 19 21 C 19.552 21 20 20.552 20 20 L 20 12 L 22.5 12 A 0.5 0.5 0 0 0 23 11.5 A 0.5 0.5 0 0 0 22.796875 11.097656 L 12.716797 2.3027344 A 1 1 0 0 0 12.710938 2.296875 A 1 1 0 0 0 12 2 z"/></svg></div>
}

View File

@@ -0,0 +1,25 @@
[
{
"owner": "os-data",
"branch": "main",
"repo": "mongolia-budget-2016-2017",
"files": [
"data/mongolia-2017.csv",
"data/mongolia-2017__2017.csv"
]
},
{
"owner": "os-data",
"branch": "main",
"repo": "gb-country-regional-analysis",
"files": [
"data/cofog.csv",
"data/cofog_dejargonise.csv",
"data/cra.csv",
"data/departments.csv",
"data/nuts_pop.csv",
"data/pogs.csv"
],
"readme": "README.md"
}
]

View File

@@ -94,8 +94,16 @@ export async function getRepoContents(
ref: branch,
path: path,
});
const data = response.data as { download_url?: string, name: string, size: number };
contents.push({ download_url: data.download_url, name: data.name, size: data.size});
const data = response.data as {
download_url?: string;
name: string;
size: number;
};
contents.push({
download_url: data.download_url,
name: data.name,
size: data.size,
});
}
return contents;
} catch (error) {
@@ -120,9 +128,7 @@ export async function getProject(project: GithubProject, github_pat?: string) {
project.readme,
github_pat
);
if (!projectReadme) {
return null;
}
const projectData = await getRepoContents(
project.owner,
project.repo,
@@ -133,15 +139,26 @@ export async function getProject(project: GithubProject, github_pat?: string) {
if (!projectData) {
return null;
}
const projectBase = project.readme.split('/').length > 1
? project.readme.split('/').slice(0, -1).join('/')
: '/'
const last_updated = await getLastUpdated(
project.owner,
project.repo,
project.branch,
projectBase,
github_pat
);
return { ...projectMetadata, files: projectData, readmeContent: projectReadme, last_updated, base_path: projectBase };
let projectBase = "", last_updated = "";
if (projectReadme) {
projectBase =
project.readme.split('/').length > 1
? project.readme.split('/').slice(0, -1).join('/')
: '/';
last_updated = await getLastUpdated(
project.owner,
project.repo,
project.branch,
projectBase,
github_pat
);
}
return {
...projectMetadata,
files: projectData,
readmeContent: projectReadme,
last_updated,
base_path: projectBase,
};
}

5866
examples/openspending/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -12,14 +12,17 @@
"@types/node": "18.16.0",
"@types/react": "18.0.38",
"@types/react-dom": "18.0.11",
"clsx": "^1.2.1",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
"next": "13.3.1",
"next-seo": "^6.0.0",
"octokit": "^2.0.14",
"prettier": "^2.8.8",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-markdown": "^8.0.7",
"react-timeago": "^7.1.0",
"remark-gfm": "^3.0.1",
"typescript": "5.0.4"
},

View File

@@ -1,6 +1,3 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
import { NextSeo } from 'next-seo';
import { promises as fs } from 'fs';
import path from 'path';
@@ -8,15 +5,20 @@ import getConfig from 'next/config';
import { getProject, GithubProject } from '../../../lib/octokit';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import Link from 'next/link';
import Breadcrumbs from '../../../components/_shared/Breadcrumbs';
export default function ProjectPage({ project }) {
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`
return (
<>
<NextSeo title={`PortalJS - @${project.repo_config.owner}/${project.repo_config.repo}${project.base_path !== '/' ? '/' + project.base_path : ''}`} />
<NextSeo title={`${repoId}${project.base_path !== '/' ? '/' + project.base_path : ''} - GitHub Datasets`} />
<main className="prose mx-auto my-8">
<Link href='/'>Back to homepage</Link>
<h1 className="mb-0">Data</h1>
<Breadcrumbs links={[{ title: repoId, href: "" }]} />
<h1 className="mb-0 mt-16">{project.repo_config.name || repoId}</h1>
<p className='mb-8'><span className='font-semibold'>Repository:</span> <a target="_blank" href={project.html_url}>{project.html_url}</a></p>
<h2 className="mb-0 mt-10">Files</h2>
<div className="inline-block min-w-full py-2 align-middle">
<table className="min-w-full divide-y divide-gray-300">
<thead>
@@ -50,10 +52,14 @@ export default function ProjectPage({ project }) {
</table>
</div>
<h1>Readme</h1>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{project.readmeContent}
</ReactMarkdown>
{project.readmeContent && <>
<hr />
<h2 className='uppercase font-black'>Readme</h2>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{project.readmeContent}
</ReactMarkdown>
</>}
</main>
</>
);
@@ -70,7 +76,7 @@ export async function getStaticPaths() {
return {
paths: JSON.parse(repos).map((repo) => {
const projectPath =
repo.readme.split('/').length > 1
repo.readme && repo.readme.split('/').length > 1
? repo.readme.split('/').slice(0, -1)
: null;
let path = [repo.repo];
@@ -96,7 +102,7 @@ export async function getStaticProps({ params }) {
const repos: GithubProject[] = JSON.parse(reposFile);
const repo = repos.find((_repo) => {
const projectPath =
_repo.readme.split('/').length > 1
_repo.readme && _repo.readme.split('/').length > 1
? _repo.readme.split('/').slice(0, -1)
: null;
let path = [_repo.repo];

View File

@@ -6,7 +6,7 @@ function CustomApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>Welcome to simple-example!</title>
<title>GitHub Datasets</title>
</Head>
<main className="app">
<Component {...pageProps} />

View File

@@ -0,0 +1,130 @@
import { promises as fs } from 'fs';
import path from 'path';
import { getProject } from '../lib/octokit';
import getConfig from 'next/config';
import ExternalLinkIcon from '../components/icons/ExternalLinkIcon';
import TimeAgo from 'react-timeago';
import Link from 'next/link';
import { Hero } from '../components/Hero';
import { Header } from '../components/Header';
import { Container } from '../components/Container';
export async function getStaticProps() {
const jsonDirectory = path.join(
process.cwd(),
'/datasets.json'
);
const repos = await fs.readFile(jsonDirectory, 'utf8');
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const projects = await Promise.all(
(JSON.parse(repos)).map(async (repo) => {
const project = await getProject(repo, github_pat);
return { ...project, repo_config: repo };
})
);
return {
props: {
projects,
},
};
}
export function Datasets({ projects }) {
return (
<div className="bg-white min-h-screen">
<Header />
<Hero />
<section
className="py-20 sm:py-32"
>
<Container>
<div className="mx-auto max-w-2xl lg:mx-0">
<h2
id="datasets"
className="font-display text-4xl font-medium tracking-tighter text-emerald-600 sm:text-5xl"
>
Datasets
</h2>
<p className="mt-4 font-display text-2xl tracking-tight text-emerald-900">
Find spending data about countries all around the world.
</p>
</div>
<div className="mt-5">
<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"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Repository
</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">
{projects.map((project) => (
<tr key={project.id}>
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
{project.repo_config.name
? project.repo_config.name
: project.full_name + (project.base_path === '/' ? '' : '/' + project.base_path)}
</td>
<td className="whitespace-nowrap px-3 py-6 text-sm group text-gray-500 hover:text-gray-900 transition-all duration-250">
<a href={project.html_url} target="_blank" className='flex items-center'>@{project.full_name} <ExternalLinkIcon className='ml-1' /></a>
</td>
<td className="px-3 py-4 text-sm text-gray-500">
{project.repo_config.description
? project.repo_config.description
: project.description}
</td>
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
<TimeAgo date={new Date(project.last_updated)} />
</td>
<td className="relative whitespace-nowrap py-6 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<a
href={`/@${project.repo_config.owner}/${project.repo_config.repo}/${project.base_path === '/' ? '' : project.base_path}`}
className='border border-gray-900 text-gray-900 px-4 py-2 transition-all hover:bg-gray-900 hover:text-white'
>
info
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</Container>
</section>
</div>
);
}
export default Datasets;

View File

@@ -0,0 +1,80 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
}
svg {
display: block;
vertical-align: middle;
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
overflow: scroll;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}

View File

@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.6 KiB

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

View File

@@ -1,75 +0,0 @@
This is a repo intended to serve as a simple example of a data catalog that get its data from a series of github repos, you can init an example just like this one by.
- Creating a new project with `create-next-app` like so:
```
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/simple-example
cd <app-name>
```
- This project uses the github api, which for anonymous users will cap at 50 requests per hour, so you might want to get a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and add it to a `.env` file inside the folder like so
```
GITHUB_PAT=<github token>
```
- Edit the file `datasets.json` to your liking, some examples can be found inside this [repo](https://github.com/datasets)
- Run the app using:
```
npm run dev
```
Congratulations, you now have something similar to this running on `http://localhost:3000`
![](https://i.imgur.com/jAljJ9C.png)
If yo go to any one of those pages by clicking on `More info` you will see something similar to this
![](https://i.imgur.com/AoJd4O0.png)
## Deployment
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fsimple-example)
By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own github/gitlab/bitbucket account and automatically deploy everything.
## Structure of `datasets.json`
The `datasets.json` file is simply a list of datasets, below you can see a minimal example of a dataset
```json
{
"owner": "fivethirtyeight",
"repo": "data",
"branch": "master",
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
"readme": "nba-raptor/README.md"
}
```
It has
- A `owner` which is going to be the github repo owner
- A `repo` which is going to be the github repo name
- A `branch` which is going to be the branch to which we need to get the files and the readme
- A list of `files` which is going to be a list of paths with files that you want to show to the world
- A `readme` which is going to be the path to your data description, it can also be a subpath eg: `example/README.md`
You can also add
- A `description` which is useful if you have more than one dataset for each repo, if not provided we are just going to use the repo description
- A `Name` which is useful if you want to give your dataset a nice name, if not provided we are going to use the junction of the `owner` the `repo` + the path of the README, in the exaple above it will be `fivethirtyeight/data/nba-raptor`
## Extra commands
You can also build the project for production with
```
npm run build
```
And run using the production build like so:
```
npm run start
```

View File

@@ -1,121 +0,0 @@
import { promises as fs } from 'fs';
import path from 'path';
import { getProject } from '../lib/octokit';
import getConfig from 'next/config';
export async function getStaticProps() {
const jsonDirectory = path.join(
process.cwd(),
'/datasets.json'
);
const repos = await fs.readFile(jsonDirectory, 'utf8');
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const projects = await Promise.all(
(JSON.parse(repos)).map(async (repo) => {
const project = await getProject(repo, github_pat);
return { ...project, repo_config: repo };
})
);
return {
props: {
projects,
},
};
}
const formatter = new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZone: 'UTC',
});
export function Datasets({ projects }) {
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">
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
</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"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Repo
</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">
{projects.map((project) => (
<tr key={project.id}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{project.repo_config.name
? project.repo_config.name
: project.full_name + (project.base_path === '/' ? '' : '/' + project.base_path)}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<a href={project.html_url}>{project.full_name}</a>
</td>
<td className="px-3 py-4 text-sm text-gray-500">
{project.repo_config.description
? project.repo_config.description
: project.description}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{formatter.format(new Date(project.last_updated))}
</td>
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
<a
href={`/@${project.repo_config.owner}/${project.repo_config.repo}/${project.base_path === '/' ? '' : project.base_path}`}
>
More info
</a>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
}
export default Datasets;

View File

@@ -1,403 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
-webkit-text-size-adjust: 100%;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
line-height: 1.5;
tab-size: 4;
scroll-behavior: smooth;
}
body {
font-family: inherit;
line-height: inherit;
margin: 0;
}
h1,
h2,
p,
pre {
margin: 0;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: currentColor;
}
h1,
h2 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
text-decoration: inherit;
}
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
}
svg {
display: block;
vertical-align: middle;
shape-rendering: auto;
text-rendering: optimizeLegibility;
}
pre {
background-color: rgba(55, 65, 81, 1);
border-radius: 0.25rem;
color: rgba(229, 231, 235, 1);
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
Liberation Mono, Courier New, monospace;
overflow: scroll;
padding: 0.5rem 0.75rem;
}
.shadow {
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
}
.rounded {
border-radius: 1.5rem;
}
.wrapper {
width: 100%;
}
.container {
margin-left: auto;
margin-right: auto;
max-width: 768px;
padding-bottom: 3rem;
padding-left: 1rem;
padding-right: 1rem;
color: rgba(55, 65, 81, 1);
width: 100%;
}
#welcome {
margin-top: 2.5rem;
}
#welcome h1 {
font-size: 3rem;
font-weight: 500;
letter-spacing: -0.025em;
line-height: 1;
}
#welcome span {
display: block;
font-size: 1.875rem;
font-weight: 300;
line-height: 2.25rem;
margin-bottom: 0.5rem;
}
#hero {
align-items: center;
background-color: hsla(214, 62%, 21%, 1);
border: none;
box-sizing: border-box;
color: rgba(55, 65, 81, 1);
display: grid;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#hero .text-container {
color: rgba(255, 255, 255, 1);
padding: 3rem 2rem;
}
#hero .text-container h2 {
font-size: 1.5rem;
line-height: 2rem;
position: relative;
}
#hero .text-container h2 svg {
color: hsla(162, 47%, 50%, 1);
height: 2rem;
left: -0.25rem;
position: absolute;
top: 0;
width: 2rem;
}
#hero .text-container h2 span {
margin-left: 2.5rem;
}
#hero .text-container a {
background-color: rgba(255, 255, 255, 1);
border-radius: 0.75rem;
color: rgba(55, 65, 81, 1);
display: inline-block;
margin-top: 1.5rem;
padding: 1rem 2rem;
text-decoration: inherit;
}
#hero .logo-container {
display: none;
justify-content: center;
padding-left: 2rem;
padding-right: 2rem;
}
#hero .logo-container svg {
color: rgba(255, 255, 255, 1);
width: 66.666667%;
}
#middle-content {
align-items: flex-start;
display: grid;
gap: 4rem;
grid-template-columns: 1fr;
margin-top: 3.5rem;
}
#learning-materials {
padding: 2.5rem 2rem;
}
#learning-materials h2 {
font-weight: 500;
font-size: 1.25rem;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.list-item-link {
align-items: center;
border-radius: 0.75rem;
display: flex;
margin-top: 1rem;
padding: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 100%;
}
.list-item-link svg:first-child {
margin-right: 1rem;
height: 1.5rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1.5rem;
}
.list-item-link > span {
flex-grow: 1;
font-weight: 400;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link > span > span {
color: rgba(107, 114, 128, 1);
display: block;
flex-grow: 1;
font-size: 0.75rem;
font-weight: 300;
line-height: 1rem;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.list-item-link svg:last-child {
height: 1rem;
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
width: 1rem;
}
.list-item-link:hover {
color: rgba(255, 255, 255, 1);
background-color: hsla(162, 47%, 50%, 1);
}
.list-item-link:hover > span {
}
.list-item-link:hover > span > span {
color: rgba(243, 244, 246, 1);
}
.list-item-link:hover svg:last-child {
transform: translateX(0.25rem);
}
#other-links {
}
.button-pill {
padding: 1.5rem 2rem;
transition-duration: 300ms;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
align-items: center;
display: flex;
}
.button-pill svg {
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
flex-shrink: 0;
width: 3rem;
}
.button-pill > span {
letter-spacing: -0.025em;
font-weight: 400;
font-size: 1.125rem;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
.button-pill span span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
.button-pill:hover svg,
.button-pill:hover {
color: rgba(255, 255, 255, 1) !important;
}
#nx-console:hover {
background-color: rgba(0, 122, 204, 1);
}
#nx-console svg {
color: rgba(0, 122, 204, 1);
}
#nx-repo:hover {
background-color: rgba(24, 23, 23, 1);
}
#nx-repo svg {
color: rgba(24, 23, 23, 1);
}
#nx-cloud {
margin-bottom: 2rem;
margin-top: 2rem;
padding: 2.5rem 2rem;
}
#nx-cloud > div {
align-items: center;
display: flex;
}
#nx-cloud > div svg {
border-radius: 0.375rem;
flex-shrink: 0;
width: 3rem;
}
#nx-cloud > div h2 {
font-size: 1.125rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#nx-cloud > div h2 span {
display: block;
font-size: 0.875rem;
font-weight: 300;
line-height: 1.25rem;
}
#nx-cloud p {
font-size: 1rem;
line-height: 1.5rem;
margin-top: 1rem;
}
#nx-cloud pre {
margin-top: 1rem;
}
#nx-cloud a {
color: rgba(107, 114, 128, 1);
display: block;
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 1.5rem;
text-align: right;
}
#nx-cloud a:hover {
text-decoration: underline;
}
#commands {
padding: 2.5rem 2rem;
margin-top: 3.5rem;
}
#commands h2 {
font-size: 1.25rem;
font-weight: 400;
letter-spacing: -0.025em;
line-height: 1.75rem;
padding-left: 1rem;
padding-right: 1rem;
}
#commands p {
font-size: 1rem;
font-weight: 300;
line-height: 1.5rem;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
details {
align-items: center;
display: flex;
margin-top: 1rem;
padding-left: 1rem;
padding-right: 1rem;
width: 100%;
}
details pre > span {
color: rgba(181, 181, 181, 1);
display: block;
}
summary {
border-radius: 0.5rem;
display: flex;
font-weight: 400;
padding: 0.5rem;
cursor: pointer;
transition-property: background-color, border-color, color, fill, stroke,
opacity, box-shadow, transform, filter, backdrop-filter,
-webkit-backdrop-filter;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
summary:hover {
background-color: rgba(243, 244, 246, 1);
}
summary svg {
height: 1.5rem;
margin-right: 1rem;
width: 1.5rem;
}
#love {
color: rgba(107, 114, 128, 1);
font-size: 0.875rem;
line-height: 1.25rem;
margin-top: 3.5rem;
opacity: 0.6;
text-align: center;
}
#love svg {
color: rgba(252, 165, 165, 1);
width: 1.25rem;
height: 1.25rem;
display: inline;
margin-top: -0.25rem;
}
@media screen and (min-width: 768px) {
#hero {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
#hero .logo-container {
display: flex;
}
#middle-content {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}

View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

25
examples/turing/README.md Normal file
View File

@@ -0,0 +1,25 @@
This demo data portal is designed for https://hatespeechdata.com. It catalogs datasets annotated for hate speech, online abuse, and offensive language which are useful for training a natural language processing system to detect this online abuse.
The site is built on top of [PortalJS](https://portaljs.org/). It catalogs datasets and lists of offensive keywords. It also includes static pages. All of these 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.

View File

@@ -21,7 +21,7 @@ export function Footer() {
<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">
hatespeechdata maintained by <a href='https://github.com/leondz'>leondz</a>
Built with <a href='https://portaljs.org'>PortalJS 🌀</a>
</p>
<p className="text-sm text-zinc-400 dark:text-zinc-500">
&copy; {new Date().getFullYear()} Leon Derczynski. All rights

View File

@@ -0,0 +1,265 @@
import { Fragment, useEffect, useRef } from 'react'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Popover, Transition } from '@headlessui/react'
import clsx from 'clsx'
import { Container } from '../components/Container'
function CloseIcon(props) {
return (
<svg viewBox="0 0 24 24" aria-hidden="true" {...props}>
<path
d="m17.25 6.75-10.5 10.5M6.75 6.75l10.5 10.5"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
function ChevronDownIcon(props) {
return (
<svg viewBox="0 0 8 6" aria-hidden="true" {...props}>
<path
d="M1.75 1.75 4 4.25l2.25-2.5"
fill="none"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}
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 GithubIcon(props) {
return (
<svg
viewBox="0 0 24 24"
aria-hidden="true"
className="h-6 w-6 fill-slate-900 dark:fill-zinc-200"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M12 2C6.477 2 2 6.463 2 11.97c0 4.404 2.865 8.14 6.839 9.458.5.092.682-.216.682-.48 0-.236-.008-.864-.013-1.695-2.782.602-3.369-1.337-3.369-1.337-.454-1.151-1.11-1.458-1.11-1.458-.908-.618.069-.606.069-.606 1.003.07 1.531 1.027 1.531 1.027.892 1.524 2.341 1.084 2.91.828.092-.643.35-1.083.636-1.332-2.22-.251-4.555-1.107-4.555-4.927 0-1.088.39-1.979 1.029-2.675-.103-.252-.446-1.266.098-2.638 0 0 .84-.268 2.75 1.022A9.607 9.607 0 0 1 12 6.82c.85.004 1.705.114 2.504.336 1.909-1.29 2.747-1.022 2.747-1.022.546 1.372.202 2.386.1 2.638.64.696 1.028 1.587 1.028 2.675 0 3.83-2.339 4.673-4.566 4.92.359.307.678.915.678 1.846 0 1.332-.012 2.407-.012 2.734 0 .267.18.577.688.48 3.97-1.32 6.833-5.054 6.833-9.458C22 6.463 17.522 2 12 2Z"
></path>
</svg>
)
}
function MobileNavItem({ href, children }) {
return (
<li>
<Popover.Button
as={Link}
href={href}
className="flex items-center gap-x-2 py-2"
>
{children}
</Popover.Button>
</li>
)
}
function MobileNavigation(props) {
return (
<Popover {...props}>
<Popover.Button className="group flex items-center rounded-full bg-white/90 px-4 py-2 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10 dark:hover:ring-white/20">
Menu
<ChevronDownIcon className="ml-3 h-auto w-2 stroke-zinc-500 group-hover:stroke-zinc-700 dark:group-hover:stroke-zinc-400" />
</Popover.Button>
<Transition.Root>
<Transition.Child
as={Fragment}
enter="duration-150 ease-out"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="duration-150 ease-in"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Popover.Overlay className="fixed inset-0 z-50 bg-zinc-800/40 backdrop-blur-sm dark:bg-black/80" />
</Transition.Child>
<Transition.Child
as={Fragment}
enter="duration-150 ease-out"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="duration-150 ease-in"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Popover.Panel
focus
className="fixed inset-x-4 top-8 z-50 origin-top rounded-3xl bg-white p-8 ring-1 ring-zinc-900/5 dark:bg-zinc-900 dark:ring-zinc-800"
>
<div className="flex flex-row-reverse items-center justify-between">
<Popover.Button aria-label="Close menu" className="-m-1 p-1">
<CloseIcon className="h-6 w-6 text-zinc-500 dark:text-zinc-400" />
</Popover.Button>
<h2 className="text-sm font-medium text-zinc-600 dark:text-zinc-400">
Navigation
</h2>
</div>
<nav className="mt-6">
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800 dark:divide-zinc-100/5 dark:text-zinc-300">
<MobileNavItem href="https://github.com/datopian/portaljs/tree/main/examples/turing">
View on Github <GithubIcon />
</MobileNavItem>
</ul>
</nav>
</Popover.Panel>
</Transition.Child>
</Transition.Root>
</Popover>
)
}
function NavItem({ href, children }) {
let isActive = useRouter().pathname === href
return (
<li>
<Link
href={href}
className={clsx(
'relative flex items-center gap-x-2 px-3 py-2 transition',
isActive
? 'text-teal-500 dark:text-teal-400'
: 'hover:text-teal-500 dark:hover:text-teal-400'
)}
>
{children}
{isActive && (
<span className="absolute inset-x-1 -bottom-px h-px bg-gradient-to-r from-teal-500/0 via-teal-500/40 to-teal-500/0 dark:from-teal-400/0 dark:via-teal-400/40 dark:to-teal-400/0" />
)}
</Link>
</li>
)
}
function DesktopNavigation(props) {
return (
<nav {...props}>
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
<NavItem href="https://github.com/datopian/portaljs/tree/main/examples/turing">
View on Github <GithubIcon />
</NavItem>
</ul>
</nav>
)
}
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>
)
}
function clamp(number, a, b) {
let min = Math.min(a, b)
let max = Math.max(a, b)
return Math.min(Math.max(number, min), max)
}
export function Header() {
return (
<>
<header
className="pointer-events-none relative z-50 flex flex-col"
style={{
height: 'var(--header-height)',
marginBottom: 'var(--header-mb)',
}}
>
<div
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 flex-1">
<MobileNavigation className="pointer-events-auto md:hidden" />
<DesktopNavigation className="pointer-events-auto hidden md:block" />
</div>
<div className="flex justify-end md:flex-1">
<div className="pointer-events-auto">
<ModeToggle />
</div>
</div>
</div>
</Container>
</div>
</header>
</>
)
}

View File

@@ -0,0 +1,5 @@
---
title: About
---
This is an about page, left here as an example

View File

@@ -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, dont be abusive! implicit/explicit messages in offensive and abusive language\". The 12th Language Resources and Evaluation Conference (pp. 6193-6202). European Language Resources Association."
---

View File

@@ -12,3 +12,5 @@ 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

View File

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

Some files were not shown because too many files have changed in this diff Show More