Compare commits

...

14 Commits

Author SHA1 Message Date
deme
1e27410f50 [examples/openspending,home][m]: add min date and max date facets 2023-05-17 20:50:45 -03:00
deme
8cb3cd4ddb [examples/openspending,home][xl]: removes datasets table, implement dataset cards grid, implement country facet 2023-05-17 18:43:19 -03:00
Luccas Mateus de Medeiros Gomes
902e5e07a0 [examples/openspending][m] - added loader + fetching from datapackage
- Also added an indexing example
2023-05-17 14:57:50 -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
84 changed files with 2494 additions and 272 deletions

View File

@@ -1,38 +0,0 @@
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"
/>
</Head>
<body>
<header className="max-w-5xl mx-auto mt-8 w-full">
<div className="border-b-2 pb-2.5 mx-2 border-zinc-800">
<h1>
<span className="sr-only">FiveThirtyEight</span>
<a
className="flex gap-x-2 items-center"
href="http://fivethirtyeight.com"
>
<img
width="197"
height="25"
alt="FiveThirtyEight"
src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MjEgNTMuNzYiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojMDEwMTAxO308L3N0eWxlPjwvZGVmcz48dGl0bGU+QXJ0Ym9hcmQgOTU8L3RpdGxlPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTAgMGgyNXY4SDl2MTBoMTV2OEg5djE3SDBWMHpNMzEgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdIMzF6bTUtMzZoOHY4aC04ek0xNzkgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdoLTE3em01LTM2aDh2OGgtOHpNMzE2IDM2aDVWMThoLTV2LThoMTN2MjZoNHY3aC0xN3ptNS0zNmg4djhoLTh6TTU0IDI3VjEwaDh2MTVsNCA5Ljk4aDFMNzEgMjVWMTBoOHYxN2wtNyAxNkg2MWwtNy0xNnpNMTExIDQzSDk3LjQyQzg5LjIzIDQzIDg1IDM5LjE5IDg1IDMxLjE3VjIyYzAtNy41NyA0LjMtMTMgMTMtMTMgOS4zMyAwIDEzIDUuMDcgMTMgMTR2N0g5NHYxLjc0YzAgMi42MiAxIDQuMjYgMy40MiA0LjI2SDExMXpNOTQgMjNoOHYtMS41NWMwLTIuNjItMS4wNi01LjQ1LTQuMTMtNS40NS0yLjc5IDAtMy44NyAyLjItMy44NyA1LjQ1ek0xMjUgOGgtMTBWMGgyOXY4aC0xMHYzNWgtOVY4ek0yMDIgNDNWMTBoOHY0YzEuMTQtMi40NSAzLjc1LTQgNy4yMi00SDIyMHY4aC02Yy0yLjg0IDAtNCAuOTQtNCAzLjlWNDN6TTI0NSA0M2gtNC44NEMyMzMuMDUgNDMgMjMwIDM5LjMxIDIzMCAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0gyNDV6TTQyMSA0M2gtNC44NEM0MDkuMDUgNDMgNDA2IDM5LjMxIDQwNiAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0g0MjF6TTI1NC4yNiA1My43Nmw0LjYxLTkuNUwyNTEgMjdWMTBoOHYxNWw0IDEwaDFsNC0xMFYxMGg4djE3bC0xMi4zIDI2Ljc2aC05LjQ0ek0yODQgMGgyNXY4aC0xNnY5aDE1djhoLTE1djEwaDE2djhoLTI1VjB6TTMzNyA0OHYtMmgxNi4xYzIgMCAyLjktLjE4IDIuOS0xLjI3di0uMzRjMC0xLjA4LS45MS0xLjM5LTIuOS0xLjM5SDM0MHYtNWw1LTVjLTUuMjktMS40OC04LTUuNDMtOC0xMXYtMWMwLTcuNTYgNC40NC0xMiAxNC0xMmEyMS45MyAyMS45MyAwIDAgMSA1Ljk1IDFMMzYxIDRsNSAzLTQgNmMxLjM3IDEuOTMgMyA0LjkzIDMgOHYxYzAgNy0zLjMgMTAuNjYtMTIgMTFsLTMgNGg2YzUuOTIgMCA5IDIuNjIgOSA3LjY4di4xMWMwIDUuMDYtMi43MSA4LjIxLTguNjIgOC4yMWgtMTNjLTQuMjkgMC02LjM4LTEuODQtNi4zOC01em0xOS0yNXYtM2MwLTMuMy0xLjMzLTQtNS00cy01IC43LTUgNHYzYzAgMy4zIDEuMzkgNCA1IDRzNS0uNyA1LTR6TTM4MCA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuNC00IDctNCA2LjI2IDAgOSAzLjA4IDkgMTAuNzZWNDNoLThWMjJjMC0zLjEzLTEuMDctNS00LTVzLTQgMS44Ny00IDV6TTE1NyA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuOTEtNCA3LjQ5LTQgNi4yNiAwIDguNTEgMy4xMyA4LjUxIDEwLjgxVjQzaC04VjIxYzAtMy4xMy0xLjA3LTQuNDQtNC00LjQ0cy00IDIuMjYtNCA1LjM5eiIvPjwvc3ZnPg=="
/>{' '}
by PortalJS
</a>
</h1>
</div>
</header>
<Main />
<NextScript />
</body>
</Html>
);
}

View File

@@ -1,4 +1,10 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 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 ## Getting Started

View File

@@ -1,46 +1,11 @@
[ [
{
"url": "https://github.com/fivethirtyeight/data/tree/master/nba-forecasts",
"name": "nba-forecasts",
"displayName": "nba-<span class=\"lastword\">forecasts</span>",
"articles": [
{
"date": "2023-05-08T22:33:43.000Z",
"title": "2022-23 NBA Predictions",
"url": "https://projects.fivethirtyeight.com/2023-nba-predictions/"
}
],
"files": [
"https://projects.fivethirtyeight.com/nba-model/nba_elo.csv",
"https://projects.fivethirtyeight.com/nba-model/nba_elo_latest.csv"
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/soccer-spi",
"name": "soccer-spi",
"displayName": "soccer-<span class=\"lastword\">spi</span>",
"articles": [
{
"date": "2023-05-08T22:17:18.000Z",
"title": "Club Soccer Predictions",
"url": "https://projects.fivethirtyeight.com/soccer-predictions/"
}
],
"files": [
"https://projects.fivethirtyeight.com/soccer-api/club/spi_matches.csv",
"https://projects.fivethirtyeight.com/soccer-api/club/spi_matches_latest.csv",
"https://projects.fivethirtyeight.com/soccer-api/club/spi_global_rankings.csv",
"https://projects.fivethirtyeight.com/soccer-api/international/spi_matches_intl.csv",
"https://projects.fivethirtyeight.com/soccer-api/international/spi_global_rankings_intl.csv"
]
},
{ {
"url": "https://github.com/fivethirtyeight/data/tree/master/polls", "url": "https://github.com/fivethirtyeight/data/tree/master/polls",
"name": "polls", "name": "polls",
"displayName": "<span class=\"lastword\">polls</span>", "displayName": "<span class=\"lastword\">polls</span>",
"articles": [ "articles": [
{ {
"date": "2023-05-08T20:36:59.000Z", "date": "2023-05-11T14:35:40.000Z",
"title": "Latest Polls", "title": "Latest Polls",
"url": "https://projects.fivethirtyeight.com/polls/" "url": "https://projects.fivethirtyeight.com/polls/"
} }
@@ -63,13 +28,45 @@
"https://projects.fivethirtyeight.com/2020-general-data/presidential_poll_averages_2020.csv" "https://projects.fivethirtyeight.com/2020-general-data/presidential_poll_averages_2020.csv"
] ]
}, },
{
"url": "https://github.com/fivethirtyeight/data/tree/master/congress-generic-ballot",
"name": "congress-generic-ballot",
"displayName": "congress-generic-<span class=\"lastword\">ballot</span>",
"articles": [
{
"date": "2023-05-11T14:35:40.000Z",
"title": "Do Voters Want Democrats Or Republicans In Congress?",
"url": "https://projects.fivethirtyeight.com/congress-generic-ballot-polls/"
}
],
"files": [
"https://projects.fivethirtyeight.com/generic-ballot-data/generic_polllist.csv",
"https://projects.fivethirtyeight.com/polls/data/generic_ballot_averages.csv"
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/nba-forecasts",
"name": "nba-forecasts",
"displayName": "nba-<span class=\"lastword\">forecasts</span>",
"articles": [
{
"date": "2023-05-11T11:15:46.000Z",
"title": "2022-23 NBA Predictions",
"url": "https://projects.fivethirtyeight.com/2023-nba-predictions/"
}
],
"files": [
"https://projects.fivethirtyeight.com/nba-model/nba_elo.csv",
"https://projects.fivethirtyeight.com/nba-model/nba_elo_latest.csv"
]
},
{ {
"url": "https://github.com/fivethirtyeight/data/tree/master/nba-raptor", "url": "https://github.com/fivethirtyeight/data/tree/master/nba-raptor",
"name": "nba-raptor", "name": "nba-raptor",
"displayName": "nba-<span class=\"lastword\">raptor</span>", "displayName": "nba-<span class=\"lastword\">raptor</span>",
"articles": [ "articles": [
{ {
"date": "2023-05-08T11:15:48.000Z", "date": "2023-05-11T11:13:20.000Z",
"title": "The Best NBA Players, According To RAPTOR", "title": "The Best NBA Players, According To RAPTOR",
"rowspan": 3, "rowspan": 3,
"url": "https://projects.fivethirtyeight.com/nba-player-ratings/" "url": "https://projects.fivethirtyeight.com/nba-player-ratings/"
@@ -92,13 +89,32 @@
"https://projects.fivethirtyeight.com/nba-model/2023/latest_RAPTOR_by_player.csv" "https://projects.fivethirtyeight.com/nba-model/2023/latest_RAPTOR_by_player.csv"
] ]
}, },
{
"url": "https://github.com/fivethirtyeight/data/tree/master/soccer-spi",
"name": "soccer-spi",
"displayName": "soccer-<span class=\"lastword\">spi</span>",
"articles": [
{
"date": "2023-05-11T05:25:51.000Z",
"title": "Club Soccer Predictions",
"url": "https://projects.fivethirtyeight.com/soccer-predictions/"
}
],
"files": [
"https://projects.fivethirtyeight.com/soccer-api/club/spi_matches.csv",
"https://projects.fivethirtyeight.com/soccer-api/club/spi_matches_latest.csv",
"https://projects.fivethirtyeight.com/soccer-api/club/spi_global_rankings.csv",
"https://projects.fivethirtyeight.com/soccer-api/international/spi_matches_intl.csv",
"https://projects.fivethirtyeight.com/soccer-api/international/spi_global_rankings_intl.csv"
]
},
{ {
"url": "https://github.com/fivethirtyeight/data/tree/master/nhl-forecasts", "url": "https://github.com/fivethirtyeight/data/tree/master/nhl-forecasts",
"name": "nhl-forecasts", "name": "nhl-forecasts",
"displayName": "nhl-<span class=\"lastword\">forecasts</span>", "displayName": "nhl-<span class=\"lastword\">forecasts</span>",
"articles": [ "articles": [
{ {
"date": "2023-05-08T04:18:20.000Z", "date": "2023-05-11T04:53:22.000Z",
"title": "2022-23 NHL Predictions", "title": "2022-23 NHL Predictions",
"url": "https://projects.fivethirtyeight.com/2023-nhl-predictions/" "url": "https://projects.fivethirtyeight.com/2023-nhl-predictions/"
} }
@@ -114,7 +130,7 @@
"displayName": "mlb-<span class=\"lastword\">elo</span>", "displayName": "mlb-<span class=\"lastword\">elo</span>",
"articles": [ "articles": [
{ {
"date": "2023-05-08T02:25:55.000Z", "date": "2023-05-11T02:35:49.000Z",
"title": "2023 MLB Predictions", "title": "2023 MLB Predictions",
"url": "https://projects.fivethirtyeight.com/2023-mlb-predictions/" "url": "https://projects.fivethirtyeight.com/2023-mlb-predictions/"
} }
@@ -124,22 +140,6 @@
"https://projects.fivethirtyeight.com/mlb-api/mlb_elo_latest.csv" "https://projects.fivethirtyeight.com/mlb-api/mlb_elo_latest.csv"
] ]
}, },
{
"url": "https://github.com/fivethirtyeight/data/tree/master/congress-generic-ballot",
"name": "congress-generic-ballot",
"displayName": "congress-generic-<span class=\"lastword\">ballot</span>",
"articles": [
{
"date": "2023-05-02T13:48:41.000Z",
"title": "Do Voters Want Democrats Or Republicans In Congress?",
"url": "https://projects.fivethirtyeight.com/congress-generic-ballot-polls/"
}
],
"files": [
"https://projects.fivethirtyeight.com/generic-ballot-data/generic_polllist.csv",
"https://projects.fivethirtyeight.com/polls/data/generic_ballot_averages.csv"
]
},
{ {
"url": "https://github.com/fivethirtyeight/data/tree/master/congress-demographics", "url": "https://github.com/fivethirtyeight/data/tree/master/congress-demographics",
"name": "congress-demographics", "name": "congress-demographics",

View File

@@ -16,6 +16,7 @@
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"eslint": "8.40.0", "eslint": "8.40.0",
"eslint-config-next": "13.4.1", "eslint-config-next": "13.4.1",
"flexsearch": "^0.7.31",
"next": "13.4.1", "next": "13.4.1",
"next-mdx-remote": "^4.4.1", "next-mdx-remote": "^4.4.1",
"next-seo": "^6.0.0", "next-seo": "^6.0.0",
@@ -2610,6 +2611,11 @@
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz",
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ=="
}, },
"node_modules/flexsearch": {
"version": "0.7.31",
"resolved": "https://registry.npmjs.org/flexsearch/-/flexsearch-0.7.31.tgz",
"integrity": "sha512-XGozTsMPYkm+6b5QL3Z9wQcJjNYxp0CYn3U1gO7dwD6PAqU1SVWZxI9CCg3z+ml3YfqdPnrBehaBrnH2AGKbNA=="
},
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",

View File

@@ -17,6 +17,7 @@
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"eslint": "8.40.0", "eslint": "8.40.0",
"eslint-config-next": "13.4.1", "eslint-config-next": "13.4.1",
"flexsearch": "^0.7.31",
"next": "13.4.1", "next": "13.4.1",
"next-mdx-remote": "^4.4.1", "next-mdx-remote": "^4.4.1",
"next-seo": "^6.0.0", "next-seo": "^6.0.0",

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

@@ -3,6 +3,7 @@ import { Inter } from 'next/font/google';
import { format } from 'timeago.js'; import { format } from 'timeago.js';
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { NextSeo } from 'next-seo';
const inter = Inter({ subsets: ['latin'] }); const inter = Inter({ subsets: ['latin'] });
@@ -20,34 +21,36 @@ export interface Dataset {
files?: string[]; 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 }) { export function MobileItem({ dataset }: { dataset: Dataset }) {
return ( return (
<div className="flex gap-x-2 pb-2 py-4 items-center justify-between border-b border-zinc-600"> <div className="flex gap-x-2 pb-2 py-4 items-center justify-between border-b border-zinc-600">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-light">{dataset.name}</span> <span className="font-mono font-light">{dataset.name}</span>
{dataset.articles.map((article) => ( {dataset.articles.map((article) => (
<div key={article.title} className="py-1 flex flex-col"> <div key={article.title} className="py-1 flex flex-col">
<span className="font-bold hover:underline">{article.title}</span> <span className="font-bold hover:underline">{article.title}</span>
<span className="font-light text-base"> <span className="font-light text-base">
{format(article.date)} {format(article.date).includes('years')
? new Date(article.date).toLocaleString('en-US', options)
: format(article.date)}
</span>{' '} </span>{' '}
</div> </div>
))} ))}
</div> </div>
<div className="flex flex-col justify-start"> <div className="flex flex-col justify-start">
<a <a
className="border border-zinc-900 font-light px-4 py-1 text-sm transition hover:bg-zinc-900 hover:text-white" 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} href={dataset.url}
target="_blank"
> >
info info
</a> </a>
<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={`/datasets/${dataset.name}`}
>
explore
</a>
{/* {/*
<button> <button>
<svg <svg
@@ -78,33 +81,29 @@ export function DesktopItem({ dataset }: { dataset: Dataset }) {
index === dataset.articles.length - 1 ? 'border-b' : '' index === dataset.articles.length - 1 ? 'border-b' : ''
} border-zinc-400`} } border-zinc-400`}
> >
<td className="py-8 font-light">{index === 0 ? dataset.name : ''}</td> <td className="py-8 font-light font-mono text-[13px] text-zinc-700">
{index === 0 ? dataset.name : ''}
</td>
<td> <td>
<a className="py-8 font-bold hover:underline" href={article.url}> <a
className="py-8 font-bold hover:underline pr-2"
href={article.url}
>
{article.title} {article.title}
</a> </a>
</td> </td>
<td className="py-8 font-light text-base min-w-[120px]"> <td className="py-8 font-light text-[14px] min-w-[138px] font-mono text-[#999]">
{format(article.date)} {format(article.date).includes('years')
</td> ? new Date(article.date).toLocaleString('en-US', options)
<td className="py-8"> : format(article.date)}
{index === 0 && (
<a
className="border border-zinc-900 font-light px-[25px] py-2.5 text-sm transition hover:bg-zinc-900 hover:text-white"
href={dataset.url}
target="_blank"
>
info
</a>
)}
</td> </td>
<td className="py-8"> <td className="py-8">
{index === 0 && ( {index === 0 && (
<a <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" 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={`/datasets/${dataset.name}`} href={dataset.url}
> >
explore info
</a> </a>
)} )}
</td> </td>
@@ -143,6 +142,7 @@ export async function getStaticProps() {
export default function Home({ datasets }: { datasets: Dataset[] }) { export default function Home({ datasets }: { datasets: Dataset[] }) {
return ( return (
<> <>
<NextSeo title="FiveThirtyEight tribute by PortalJS" />
<main <main
className={`flex min-h-screen flex-col items-center max-w-5xl mx-auto pt-20 px-2.5 ${inter.className}`} className={`flex min-h-screen flex-col items-center max-w-5xl mx-auto pt-20 px-2.5 ${inter.className}`}
> >
@@ -150,7 +150,7 @@ export default function Home({ datasets }: { datasets: Dataset[] }) {
<h1 className="text-[40px] font-bold text-zinc-800 text-center"> <h1 className="text-[40px] font-bold text-zinc-800 text-center">
Our Data Our Data
</h1> </h1>
<p className="max-w-2xl text-lg text-center text-zinc-700"> <p className="max-w-[600px] text-[17px] text-center text-[#6d6f71]">
Were sharing the data and code behind some of our articles and 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 graphics. We hope youll use it to check our work and to create
stories and visualizations of&nbsp;your&nbsp;own. stories and visualizations of&nbsp;your&nbsp;own.
@@ -164,13 +164,13 @@ export default function Home({ datasets }: { datasets: Dataset[] }) {
<table className="w-full mt-10 mb-4 hidden md:table"> <table className="w-full mt-10 mb-4 hidden md:table">
<thead className="border-b-4 pb-2 border-zinc-900"> <thead className="border-b-4 pb-2 border-zinc-900">
<tr> <tr>
<th className="uppercase text-left font-light text-xs pb-3"> <th className="uppercase text-left font-normal text-xs pb-3">
data set data set
</th> </th>
<th className="uppercase text-left font-light text-xs pb-3"> <th className="uppercase text-left font-normal text-xs pb-3">
related content related content
</th> </th>
<th className="uppercase text-left font-light text-xs pb-3"> <th className="uppercase text-left font-normal text-xs pb-3">
last updated last updated
</th> </th>
</tr> </tr>

View File

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

View File

@@ -0,0 +1,45 @@
import { Octokit } from 'octokit';
import { assert, expect, test } from 'vitest'
import { getProjectDataPackage } from '../lib/octokit';
export async function getAllDataPackagesFromOrg(
org: string,
branch?: string,
github_pat?: string
) {
const octokit = new Octokit({ auth: github_pat });
const repos = await octokit.rest.repos.listForOrg({ org, type: 'public', per_page: 100 });
let failedDataPackages = [];
const datapackages = await Promise.all(
repos.data.map(async (_repo) => {
const datapackage = await getProjectDataPackage(
org,
_repo.name,
branch ? branch : 'main',
github_pat
);
if (!datapackage) {
failedDataPackages.push(_repo.name)
return null
};
return {...datapackage, repo: _repo.name};
})
);
return {
datapackages: datapackages.filter((item) => item !== null),
failedDataPackages,
};
}
test('Test OS-Data', async () => {
const repos = await getAllDataPackagesFromOrg('os-data', 'main', process.env.VITE_GITHUB_PAT)
if (repos.failedDataPackages.length > 0) console.log(repos.failedDataPackages)
expect(repos.failedDataPackages.length).toBe(0)
}, {timeout: 100000})
test('Test Gift-Data', async () => {
const repos = await getAllDataPackagesFromOrg('gift-data', 'main', process.env.VITE_GITHUB_PAT)
if (repos.failedDataPackages.length > 0) console.log(repos.failedDataPackages)
expect(repos.failedDataPackages.length).toBe(0)
}, {timeout: 100000})

View File

@@ -1,15 +1,15 @@
import Link from 'next/link' import Link from 'next/link';
import clsx from 'clsx' import clsx from 'clsx';
export function Button({ href, className = "", ...props }) { export function Button({ href, className = '', ...props }) {
className = clsx( 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', '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 className
) );
return href ? ( return href ? (
<Link href={href} className={className} {...props} /> <Link scroll={false} href={href} className={className} {...props} />
) : ( ) : (
<button className={className} {...props} /> <button className={className} {...props} />
) );
} }

View File

@@ -0,0 +1,76 @@
import Link from 'next/link';
import { Project } from '../lib/project.interface';
import ExternalLinkIcon from './icons/ExternalLinkIcon';
export default function DatasetCard({ dataset }: { dataset: Project }) {
return (
<div
key={dataset.name}
className="overflow-hidden rounded-xl border border-gray-200"
>
<Link
href=""
className="flex items-center gap-x-4 border-b border-gray-900/5 bg-gray-50 p-6"
>
<img
src={dataset.owner.logo || '/assets/org-icon.svg'}
alt={dataset.owner.name}
className="h-12 w-12 flex-none rounded-lg bg-white object-cover ring-1 ring-gray-900/10 p-2"
/>
<div className="text-sm font-medium leading-6">
<div className="text-gray-900 line-clamp-1">{dataset.title}</div>
<div className="text-gray-500 line-clamp-1">
{dataset.owner.title}
</div>
</div>
</Link>
<dl className="-my-3 divide-y divide-gray-100 px-6 py-4 text-sm leading-6">
<div className="flex justify-between gap-x-4 py-3">
<dt className="text-gray-500">Name</dt>
<dd className="flex items-start gap-x-2">
<div className="font-medium text-gray-900 line-clamp-1">
{dataset.name}
</div>
</dd>
</div>
<div className="flex justify-between gap-x-4 py-3">
<dt className="text-gray-500">Country</dt>
<dd className="flex items-start gap-x-2">
<div className="font-medium text-gray-900">
{dataset.countryCode}
</div>
</dd>
</div>
<div className="flex justify-between gap-x-4 py-3">
<dt className="text-gray-500">Fiscal Period</dt>
<dd className="text-gray-700">
{dataset.fiscalPeriod?.start &&
new Date(dataset.fiscalPeriod.start).getFullYear()}
{dataset.fiscalPeriod?.end &&
dataset.fiscalPeriod?.start !== dataset.fiscalPeriod?.end && (
<>
{' - '}
{new Date(dataset.fiscalPeriod.end).getFullYear()}
</>
)}
</dd>
</div>
<div className="flex justify-between gap-x-4 py-3">
<dt className="text-gray-500">Metadata</dt>
<dd className="flex items-start gap-x-2">
<div className="font-medium text-gray-900">
<Link
// TODO: where do we get the info needed for this link?
href=""
target="_blank"
className="flex items-center hover:text-gray-700"
>
datapackage.json <ExternalLinkIcon className="ml-1" />
</Link>
</div>
</dd>
</div>
</dl>
</div>
);
}

View File

@@ -0,0 +1,19 @@
import { Project } from '../lib/project.interface';
import DatasetCard from './DatasetCard';
export default function DatasetsGrid({ datasets }: { datasets: Project[] }) {
return (
<ul
className="grid gap-x-6 gap-y-8 grid-cols-1 sm:grid-cols-2 md:grid-cols-3"
role="list"
>
{datasets.map((dataset, idx) => {
return (
<li key={`datasets-grid-item-${idx}`}>
<DatasetCard dataset={dataset} />
</li>
);
})}
</ul>
);
}

View File

@@ -0,0 +1,163 @@
import { useForm } from 'react-hook-form';
import DatasetsGrid from './DatasetsGrid';
import { Project } from '../lib/project.interface';
import { Index } from 'flexsearch';
export default function DatasetsSearch({ datasets }: { datasets: Project[] }) {
const index = new Index({ tokenize: 'full' });
datasets.forEach((dataset: Project) =>
index.add(
dataset.name,
`${dataset.repo} ${dataset.name} ${dataset.title} ${dataset.author} ${dataset.title} ${dataset.cityCode} ${dataset.fiscalPeriod?.start} ${dataset.fiscalPeriod?.end}`
)
);
const { register, watch, handleSubmit, reset, resetField } = useForm({
defaultValues: {
searchTerm: '',
country: '',
minDate: '',
maxDate: '',
},
});
const allCountries = datasets
.map((item) => item.countryCode)
.filter((v) => v) // Filters false values
.filter((v, i, a) => a.indexOf(v) === i) // Remove duplicates
// TODO: title should be the full name
.map((code) => ({ code, title: code }));
return (
<>
<div className="flex flex-col gap-3 sm:flex-row">
<div className="min-w-0 flex-auto">
<br />
<div className="relative">
<input
placeholder="Search datasets"
aria-label="Search datasets"
{...register('searchTerm')}
className="h-[3em] relative w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
/>
{watch().searchTerm !== '' && (
<button
onClick={() => resetField('searchTerm')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
>
<CloseIcon />
</button>
)}
</div>
</div>
<div className="sm:basis-1/6">
{/* TODO: nicer select e.g. headlessui example */}
<label className="text-sm text-gray-600 font-medium">Country</label>
<select
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
{...register('country')}
>
<option value="">All</option>
{allCountries.map((country) => {
return (
<option key={country.code} value={country.code}>
{country.title}
</option>
);
})}
</select>
</div>
<div className="sm:basis-1/6">
<label className="text-sm text-gray-600 font-medium">Min. date</label>
<div className="relative">
<input
aria-label="Min. date"
type="date"
{...register('minDate')}
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
/>
{watch().minDate !== '' && (
<button
onClick={() => resetField('minDate')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
>
<CloseIcon />
</button>
)}
</div>
</div>
<div className="sm:basis-1/6">
<label className="text-sm text-gray-600 font-medium">Max. date</label>
<div className="relative">
<input
aria-label="Max. date"
type="date"
{...register('maxDate')}
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
/>
{watch().maxDate !== '' && (
<button
onClick={() => resetField('maxDate')}
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
>
<CloseIcon />
</button>
)}
</div>
</div>
</div>
<div className="min-w-full mt-10 align-middle">
<DatasetsGrid
datasets={datasets
.filter((dataset: Project) =>
watch().searchTerm && watch().searchTerm !== ''
? index.search(watch().searchTerm).includes(dataset.name)
: true
)
.filter((dataset) =>
watch().country && watch().country !== ''
? dataset.countryCode === watch().country
: true
)
// TODO: Does that really makes sense?
// What if the fiscalPeriod is 2015-2017 and inputs are
// set to 2015-2016. It's going to be filtered out but
// it shouldn't.
.filter((dataset) =>
watch().minDate && watch().minDate !== ''
? dataset.fiscalPeriod?.start >= watch().minDate
: true
)
.filter((dataset) =>
watch().maxDate && watch().maxDate !== ''
? dataset.fiscalPeriod?.end <= watch().maxDate
: true
)}
/>
</div>
</>
);
}
const CloseIcon = () => {
return (
<svg
width={20}
height={20}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g id="Menu / Close_MD">
<path
id="Vector"
d="M18 18L12 12M12 12L6 6M12 12L18 6M12 12L6 18"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
</svg>
);
};

View File

@@ -46,7 +46,6 @@ export function Header() {
</li>))} </li>))}
</ul> </ul>
<div className="hidden sm:mt-10 sm:flex lg:mt-0 lg:grow lg:basis-0 lg:justify-end"> <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> </div>
</Container> </Container>
</header > </header >

View File

@@ -23,8 +23,8 @@ export function Hero() {
fiscal data in the public sphere. fiscal data in the public sphere.
</p> </p>
</div> </div>
<Button href="#" className="mt-10 w-full sm:hidden"> <Button href="#datasets" className="mt-10">
View on GitHub Search datasets
</Button> </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"> <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">
{[ {[

View File

@@ -2,24 +2,26 @@
{ {
"owner": "os-data", "owner": "os-data",
"branch": "main", "branch": "main",
"repo": "mongolia-budget-2016-2017", "name": "mongolia-budget-2016-2017"
"files": [
"data/mongolia-2017.csv",
"data/mongolia-2017__2017.csv"
]
}, },
{ {
"owner": "os-data", "owner": "os-data",
"branch": "main", "branch": "main",
"repo": "gb-country-regional-analysis", "name": "gb-country-regional-analysis"
"files": [ },
"data/cofog.csv", {
"data/cofog_dejargonise.csv", "owner": "os-data",
"data/cra.csv", "branch": "main",
"data/departments.csv", "name": "berlin-berlin"
"data/nuts_pop.csv", },
"data/pogs.csv" {
], "owner": "os-data",
"readme": "README.md" "branch": "main",
"name": "state-of-minas-gerais-brazil-planned-budget"
},
{
"owner": "os-data",
"branch": "main",
"name": "wesel"
} }
] ]

View File

@@ -0,0 +1,288 @@
/**
* Fiscal Data Package is a simple specification for data access and delivery of fiscal data.
*/
export type FiscalDataPackage = TabularDataPackage & {
countryCode?: ISO31661Alpha2CountryCode
regionCode?: string
cityCode?: string
author?: string
readme?: string
granularity?: GranularityOfResources
fiscalPeriod?: FiscalPeriodForTheBudget
[k: string]: unknown
}
/**
* The profile of this descriptor.
*/
export type Profile = "tabular-data-package"
/**
* An identifier string. Lower case characters with `.`, `_`, `-` and `/` are allowed.
*/
export type Name = string
/**
* A property reserved for globally unique identifiers. Examples of identifiers that are unique include UUIDs and DOIs.
*/
export type ID = string
/**
* A human-readable title.
*/
export type Title = string
/**
* A text description. Markdown is encouraged.
*/
export type Description = string
/**
* The home on the web that is related to this data package.
*/
export type HomePage = string
/**
* The datetime on which this descriptor was created.
*/
export type Created = string
/**
* The contributors to this descriptor.
*/
export type Contributors = [Contributor, ...Contributor[]]
/**
* A human-readable title.
*/
export type Title1 = string
/**
* A fully qualified URL, or a POSIX file path.
*/
export type Path = string
/**
* An email address.
*/
export type Email = string
/**
* An organizational affiliation for this contributor.
*/
export type Organization = string
/**
* A list of keywords that describe this package.
*/
export type Keywords = [string, ...string[]]
/**
* A image to represent this package.
*/
export type Image = string
/**
* The license(s) under which this package is published.
*/
export type Licenses = [License, ...License[]]
/**
* A license for this descriptor.
*/
export type License =
| {
[k: string]: unknown
}
| {
[k: string]: unknown
}
/**
* An `array` of Tabular Data Resource objects, each compliant with the [Tabular Data Resource](/tabular-data-resource/) specification.
*
/**
* A Tabular Data Resource.
*/
export interface TabularDataResource {
format?: string;
name: string;
description?: string;
title?: string;
schema?: Schema;
sample?: any[];
profile?: string;
key?: string;
path?: string;
size?: number;
}
export interface Field {
name: string;
type: FieldType;
}
export interface Schema {
fields: Field[];
}
export const OptionsFields = [
"any",
"array",
"boolean",
"date",
"datetime",
"duration",
"geojson",
"geopoint",
"integer",
"number",
"object",
"string",
"time",
"year",
"yearmonth",
] as const;
type FieldType = typeof OptionsFields[number];
/**
* A human-readable title.
*/
export type Title2 = string
/**
* A fully qualified URL, or a POSIX file path.
*/
export type Path1 = string
/**
* An email address.
*/
export type Email1 = string
/**
* The raw sources for this resource.
*/
export type Sources = Source[]
/**
* A keyword that represents the direction of the spend, either expenditure or revenue.
*/
export type DirectionOfTheSpending = "expenditure" | "revenue"
/**
* A keyword that represents the phase of the data, can be proposed for a budget proposal, approved for an approved budget, adjusted for modified budget or executed for the enacted budget
*/
export type BudgetPhase = "proposed" | "approved" | "adjusted" | "executed"
/**
* Either an array of strings corresponding to the name attributes in a set of field objects in the fields array or a single string corresponding to one of these names. The value of primaryKey indicates the primary key or primary keys for the dimension.
*/
export type PrimaryKey = string | [string, ...string[]]
/**
* Describes what kind of a dimension it is.
*/
export type DimensionType =
| "datetime"
| "entity"
| "classification"
| "activity"
| "fact"
| "location"
| "other"
/**
* The type of the classification.
*/
export type ClassificationType = "functional" | "administrative" | "economic"
/**
* A valid 2-digit ISO country code (ISO 3166-1 alpha-2), or, an array of valid ISO codes.
*/
export type ISO31661Alpha2CountryCode = string | [string, ...string[]]
/**
* A keyword that represents the type of spend data, eiter aggregated or transactional
*/
export type GranularityOfResources = "aggregated" | "transactional"
/**
* Tabular Data Package
*/
export interface TabularDataPackage {
profile: Profile
name?: Name
id?: ID
title?: Title
description?: Description
homepage?: HomePage
created?: Created
contributors?: Contributors
keywords?: Keywords
image?: Image
licenses?: Licenses
resources: TabularDataResource[]
sources?: Sources
[k: string]: unknown
}
/**
* A contributor to this descriptor.
*/
export interface Contributor {
title: Title1
path?: Path
email?: Email
organization?: Organization
role?: string
[k: string]: unknown
}
/**
* A source file.
*/
export interface Source {
title: Title2
path?: Path1
email?: Email1
[k: string]: unknown
}
/**
* Measures are numerical and correspond to financial amounts in the source data.
*/
export interface Measures {
[k: string]: Measure
}
/**
* Measure.
*
* This interface was referenced by `Measures`'s JSON-Schema definition
* via the `patternProperty` "^\w+".
*/
export interface Measure {
source: string
resource?: string
currency: string
factor?: number
direction?: DirectionOfTheSpending
phase?: BudgetPhase
[k: string]: unknown
}
/**
* Dimensions are groups of related fields. Dimensions cover all items other than the measure.
*/
export interface Dimensions {
[k: string]: Dimension
}
/**
* Dimension.
*
* This interface was referenced by `Dimensions`'s JSON-Schema definition
* via the `patternProperty` "^\w+".
*/
export interface Dimension {
attributes: Attributes
primaryKey: PrimaryKey
dimensionType?: DimensionType
classificationType?: ClassificationType
[k: string]: unknown
}
/**
* Attribute objects that make up the dimension
*/
export interface Attributes {
/**
* This interface was referenced by `Attributes`'s JSON-Schema definition
* via the `patternProperty` "^\w+".
*/
[k: string]: {
source: string
resource?: string
constant?: string | number
parent?: string
labelfor?: string
[k: string]: unknown
}
}
/**
* The fiscal period of the dataset
*/
export interface FiscalPeriodForTheBudget {
start: string
end?: string
[k: string]: unknown
}

View File

@@ -0,0 +1,34 @@
import { FiscalDataPackage } from './datapackage.interface';
import { Project } from './project.interface';
export function loadDataPackage(datapackage: FiscalDataPackage, repo): Project {
return {
name: datapackage.name,
title: datapackage.title,
owner: {
name: repo.owner.login,
logo: repo.owner.avatar_url,
// TODO: make this title work
title: repo.owner.login,
},
repo: { name: repo, full_name: repo.full_name },
files: datapackage.resources,
author: datapackage.author ? datapackage.author : null,
cityCode: datapackage.cityCode ? datapackage.cityCode : null,
countryCode: datapackage.countryCode
? (datapackage.countryCode as string)
: null,
fiscalPeriod: datapackage.fiscalPeriod
? {
start: datapackage.fiscalPeriod.start
? datapackage.fiscalPeriod.start
: null,
end: datapackage.fiscalPeriod.end
? datapackage.fiscalPeriod.end
: null,
}
: null,
readme: datapackage.readme ? datapackage.readme : '',
datapackage,
};
}

View File

@@ -140,7 +140,8 @@ export async function getProject(project: GithubProject, github_pat?: string) {
return null; return null;
} }
let projectBase = "", last_updated = ""; let projectBase = '',
last_updated = '';
if (projectReadme) { if (projectReadme) {
projectBase = projectBase =
project.readme.split('/').length > 1 project.readme.split('/').length > 1
@@ -162,3 +163,30 @@ export async function getProject(project: GithubProject, github_pat?: string) {
base_path: projectBase, base_path: projectBase,
}; };
} }
export async function getProjectDataPackage(
owner: string,
repo: string,
branch: string,
github_pat?: string
) {
const octokit = new Octokit({ auth: github_pat });
try {
const response = await octokit.rest.repos.getContent({
owner,
repo,
path: 'datapackage.json',
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();
const datapackage = JSON.parse(decodedContent);
return {...datapackage, repo };
} catch (error) {
return null;
}
}

View File

@@ -0,0 +1,21 @@
import {
FiscalDataPackage,
TabularDataResource,
} from './datapackage.interface';
export interface Project {
owner: { name: string; logo?: string; title?: string }; // Info about the owner of the data repo
repo: { name: string; full_name: string }; // Info about the the data repo
files: TabularDataResource[];
name: string;
title?: string;
author?: string;
cityCode?: string;
countryCode?: string;
fiscalPeriod?: {
start: string;
end: string;
};
readme?: string;
datapackage: FiscalDataPackage;
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,21 +6,27 @@
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"test": "vitest"
}, },
"dependencies": { "dependencies": {
"@octokit/plugin-throttling": "^5.2.2",
"@types/flexsearch": "^0.7.3",
"@types/node": "18.16.0", "@types/node": "18.16.0",
"@types/react": "18.0.38", "@types/react": "18.0.38",
"@types/react-dom": "18.0.11", "@types/react-dom": "18.0.11",
"@vitejs/plugin-react": "^4.0.0",
"clsx": "^1.2.1", "clsx": "^1.2.1",
"eslint": "8.39.0", "eslint": "8.39.0",
"eslint-config-next": "13.3.1", "eslint-config-next": "13.3.1",
"flexsearch": "0.7.21",
"next": "13.3.1", "next": "13.3.1",
"next-seo": "^6.0.0", "next-seo": "^6.0.0",
"octokit": "^2.0.14", "octokit": "^2.0.14",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
"react-timeago": "^7.1.0", "react-timeago": "^7.1.0",
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
@@ -30,6 +36,7 @@
"@tailwindcss/typography": "^0.5.9", "@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"tailwindcss": "^3.3.1" "tailwindcss": "^3.3.1",
"vitest": "^0.31.0"
} }
} }

View File

@@ -38,7 +38,7 @@ export default function ProjectPage({ project }) {
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200"> <tbody className="divide-y divide-gray-200">
{project.files.map((file) => ( {project.files?.map((file) => (
<tr key={file.download_url}> <tr key={file.download_url}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500"> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<a href={file.download_url}>{file.name}</a> <a href={file.download_url}>{file.name}</a>
@@ -79,7 +79,7 @@ export async function getStaticPaths() {
repo.readme && repo.readme.split('/').length > 1 repo.readme && repo.readme.split('/').length > 1
? repo.readme.split('/').slice(0, -1) ? repo.readme.split('/').slice(0, -1)
: null; : null;
let path = [repo.repo]; let path = [repo.name];
if (projectPath) { if (projectPath) {
projectPath.forEach((element) => { projectPath.forEach((element) => {
path.push(element); path.push(element);
@@ -105,7 +105,7 @@ export async function getStaticProps({ params }) {
_repo.readme && _repo.readme.split('/').length > 1 _repo.readme && _repo.readme.split('/').length > 1
? _repo.readme.split('/').slice(0, -1) ? _repo.readme.split('/').slice(0, -1)
: null; : null;
let path = [_repo.repo]; let path = [_repo.name];
if (projectPath) { if (projectPath) {
projectPath.forEach((element) => { projectPath.forEach((element) => {
path.push(element); path.push(element);

View File

@@ -1,6 +1,10 @@
import { promises as fs } from 'fs'; import { promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import { getProject } from '../lib/octokit'; import {
GithubProject,
getProjectDataPackage,
getProjectMetadata,
} from '../lib/octokit';
import getConfig from 'next/config'; import getConfig from 'next/config';
import ExternalLinkIcon from '../components/icons/ExternalLinkIcon'; import ExternalLinkIcon from '../components/icons/ExternalLinkIcon';
import TimeAgo from 'react-timeago'; import TimeAgo from 'react-timeago';
@@ -8,37 +12,55 @@ import Link from 'next/link';
import { Hero } from '../components/Hero'; import { Hero } from '../components/Hero';
import { Header } from '../components/Header'; import { Header } from '../components/Header';
import { Container } from '../components/Container'; import { Container } from '../components/Container';
import { FiscalDataPackage } from '../lib/datapackage.interface';
import { loadDataPackage } from '../lib/loader';
import DatasetsSearch from '../components/DatasetsSearch';
export async function getStaticProps() { export async function getStaticProps() {
const jsonDirectory = path.join( const jsonDirectory = path.join(process.cwd(), '/datasets.json');
process.cwd(),
'/datasets.json'
);
const repos = await fs.readFile(jsonDirectory, 'utf8'); const repos = await fs.readFile(jsonDirectory, 'utf8');
const github_pat = getConfig().serverRuntimeConfig.github_pat; const github_pat = getConfig().serverRuntimeConfig.github_pat;
const datapackages = await Promise.all(
JSON.parse(repos).map(async (_repo: GithubProject) => {
const datapackage = await getProjectDataPackage(
_repo.owner,
_repo.name,
'main',
github_pat
);
const repo = await getProjectMetadata(
_repo.owner,
_repo.name,
github_pat
);
const projects = await Promise.all( return {
(JSON.parse(repos)).map(async (repo) => { datapackage,
const project = await getProject(repo, github_pat); repo,
return { ...project, repo_config: repo }; };
}) })
); );
const projects = datapackages.map(
(item: { datapackage: FiscalDataPackage & { repo: string }; repo: any }) =>
loadDataPackage(item.datapackage, item.repo)
);
return { return {
props: { props: {
projects, projects: JSON.stringify(projects),
}, },
}; };
} }
export function Datasets({ projects }) { export function Datasets({ projects }) {
projects = JSON.parse(projects);
return ( return (
<div className="bg-white min-h-screen"> <div className="bg-white min-h-screen">
<Header /> <Header />
<Hero /> <Hero />
<section <section className="py-20 sm:py-32">
className="py-20 sm:py-32"
>
<Container> <Container>
<div className="mx-auto max-w-2xl lg:mx-0"> <div className="mx-auto max-w-2xl lg:mx-0">
<h2 <h2
@@ -51,75 +73,8 @@ export function Datasets({ projects }) {
Find spending data about countries all around the world. Find spending data about countries all around the world.
</p> </p>
</div> </div>
<div className="mt-5"> <div className="mt-10">
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8"> <DatasetsSearch datasets={projects} />
<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>
</Container> </Container>
</section> </section>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#000000" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="800px" viewBox="0 0 120 120" enable-background="new 0 0 120 120" xml:space="preserve">
<rect x="2" y="108.1" width="116" height="11.9"/>
<rect x="6.744" y="96.582" width="104.979" height="6.543"/>
<rect x="15.288" y="38.532" width="17.639" height="52.925"/>
<rect x="50.484" y="38.532" width="17.639" height="52.925"/>
<rect x="84.33" y="38.532" width="17.639" height="52.925"/>
<polygon points="0,26.96 60,0 120,26.96 119.946,33.912 0,34.01 "/>
</svg>

After

Width:  |  Height:  |  Size: 818 B

View File

@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
},
})

View File

@@ -140,7 +140,7 @@ function MobileNavigation(props) {
</div> </div>
<nav className="mt-6"> <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"> <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/leondz/hatespeechdata"> <MobileNavItem href="https://github.com/datopian/portaljs/tree/main/examples/turing">
View on Github <GithubIcon /> View on Github <GithubIcon />
</MobileNavItem> </MobileNavItem>
</ul> </ul>
@@ -179,7 +179,7 @@ function DesktopNavigation(props) {
return ( return (
<nav {...props}> <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"> <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/leondz/hatespeechdata"> <NavItem href="https://github.com/datopian/portaljs/tree/main/examples/turing">
View on Github <GithubIcon /> View on Github <GithubIcon />
</NavItem> </NavItem>
</ul> </ul>

View File

@@ -36,7 +36,7 @@ In the following page type `content/datasets/<name-of-the-file>.md`. if you want
### Fill in content ### Fill in content
Copy the contents of `templates/dataset.md` or `templates/keywords.md` respectively to the camp below, filling out the fields with the correct data format Copy the contents of `templates/dataset.md` or `templates/keywords.md` respectively to the camp below, filling out the fields with the correct data format. Everything below the second `---` will automatically get rendered into the page, so you may add any standard markdown fields e.g tables, headings, lists...
![](https://i.imgur.com/x6JIjhz.png) ![](https://i.imgur.com/x6JIjhz.png)

View File

@@ -6,7 +6,6 @@ import { Card } from '../components/Card'
import Head from 'next/head' import Head from 'next/head'
import parse from '../lib/markdown' import parse from '../lib/markdown'
import { Mermaid } from '@flowershow/core'; import { Mermaid } from '@flowershow/core';
import { Header } from '../components/Header';
export const getStaticProps = async ({ params }) => { export const getStaticProps = async ({ params }) => {
const urlPath = params.slug ? params.slug.join('/') : '' const urlPath = params.slug ? params.slug.join('/') : ''
@@ -82,15 +81,13 @@ export default function DRDPage({ mdxSource }) {
) )
return ( return (
<> <>
<Header />
<Head> <Head>
<title>{meta.title}</title> <title>{meta.title}</title>
</Head> </Head>
<Container className="mt-16 lg:mt-32 relative"> <Container className="mt-9 relative">
<Header />
<article> <article>
<header className="flex flex-col"> <header className="flex flex-col">
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl"> <h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
{meta.title} {meta.title}
</h1> </h1>
<Card as="article"> <Card as="article">

View File

Before

Width:  |  Height:  |  Size: 566 B

After

Width:  |  Height:  |  Size: 566 B

View File

@@ -161,13 +161,14 @@ export default function Layout({
> >
Built by{' '} Built by{' '}
<img <img
src={ src="/images/datopian-light-logotype.svg"
theme === 'dark'
? '/images/datopian-light-logotype.svg'
: '/images/datopian-dark-logotype.svg'
}
alt="Datopian Logo" alt="Datopian Logo"
className="h-6 ml-2" className="h-6 ml-2 hidden dark:block"
/>
<img
src="/images/datopian-dark-logotype.svg"
alt="Datopian Logo"
className="h-6 ml-2 dark:hidden"
/> />
</a> </a>
</footer> </footer>