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
67 changed files with 2382 additions and 380 deletions

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

View File

@@ -211,18 +211,6 @@
"https://projects.fivethirtyeight.com/nfl-api/nfl_elo_latest.csv"
]
},
{
"url": "https://github.com/fivethirtyeight/checking-our-work-data",
"name": "checking-our-work-data",
"displayName": "checking-our-work-<span class=\"lastword\">data</span>",
"articles": [
{
"date": "2023-02-02T16:30:00.000Z",
"title": "How Good Are FiveThirtyEight Forecasts?",
"url": "https://projects.fivethirtyeight.com/checking-our-work/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/world-cup-2022",
"name": "world-cup-2022",
@@ -239,18 +227,6 @@
"https://projects.fivethirtyeight.com/soccer-api/international/2022/wc_forecasts.csv"
]
},
{
"url": "https://github.com/fivethirtyeight/covid-19-polls",
"name": "covid-19-polls",
"displayName": "covid-19-<span class=\"lastword\">polls</span>",
"articles": [
{
"date": "2022-11-29T20:20:08.000Z",
"title": "How Americans View Bidens Response To The Coronavirus Crisis",
"url": "https://projects.fivethirtyeight.com/coronavirus-polls/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/election-deniers",
"name": "election-deniers",
@@ -371,18 +347,6 @@
}
]
},
{
"url": "https://github.com/fivethirtyeight/nfl-elo-game",
"name": "nfl-elo-game",
"displayName": "nfl-elo-<span class=\"lastword\">game</span>",
"articles": [
{
"date": "2022-02-14T03:03:26.000Z",
"title": "Can You Beat FiveThirtyEights NFL Forecasts?",
"url": "https://projects.fivethirtyeight.com/2021-nfl-forecasting-game/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/redlining",
"name": "redlining",
@@ -407,42 +371,6 @@
}
]
},
{
"url": "https://github.com/fivethirtyeight/negro-leagues-player-ratings",
"name": "negro-leagues-player-ratings",
"displayName": "negro-leagues-player-<span class=\"lastword\">ratings</span>",
"articles": [
{
"date": "2021-02-25T11:00:00.000Z",
"title": "The Negro League Stars That MLB Kept Out — And Is Finally Recognizing",
"url": "https://projects.fivethirtyeight.com/negro-leagues-mlb/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/police-settlements",
"name": "police-settlements",
"displayName": "police-<span class=\"lastword\">settlements</span>",
"articles": [
{
"date": "2021-02-22T11:00:49.000Z",
"title": "Cities Spend Millions On Police Misconduct Every Year. Heres Why Its So Difficult to Hold Departments Accountable.",
"url": "https://fivethirtyeight.com/features/police-misconduct-costs-cities-millions-every-year-but-thats-where-the-accountability-ends/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/superbowl-ads",
"name": "superbowl-ads",
"displayName": "superbowl-<span class=\"lastword\">ads</span>",
"articles": [
{
"date": "2021-02-04T16:11:00.000Z",
"title": "According To Super Bowl Ads, Americans Love America, Animals And Sex",
"url": "https://projects.fivethirtyeight.com/super-bowl-ads/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/trump-approval-ratings",
"name": "trump-approval-ratings",
@@ -475,18 +403,6 @@
"https://projects.fivethirtyeight.com/congress-tracker-data/csv/vote_predictions.csv"
]
},
{
"url": "https://github.com/fivethirtyeight/election-results",
"name": "election-results",
"displayName": "election-<span class=\"lastword\">results</span>",
"articles": [
{
"date": "2020-11-03T05:33:43.000Z",
"title": "2020 Election Forecast",
"url": "https://projects.fivethirtyeight.com/2020-election-forecast/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/election-forecasts-2020",
"name": "election-forecasts-2020",
@@ -557,18 +473,6 @@
}
]
},
{
"url": "https://github.com/fivethirtyeight/WNBA-stats",
"name": "WNBA-stats",
"displayName": "WNBA-<span class=\"lastword\">stats</span>",
"articles": [
{
"date": "2020-05-27T19:06:43.000Z",
"title": "Its Time To Give Basketballs Other GOAT Her Due",
"url": "https://fivethirtyeight.com/features/its-time-to-give-basketballs-other-goat-her-due/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/covid-geography",
"name": "covid-geography",
@@ -608,18 +512,6 @@
"https://projects.fivethirtyeight.com/endorsements-2020-data/endorsements-2020.csv"
]
},
{
"url": "https://github.com/fivethirtyeight/nba-player-advanced-metrics",
"name": "nba-player-advanced-metrics",
"displayName": "nba-player-advanced-<span class=\"lastword\">metrics</span>",
"articles": [
{
"date": "2020-03-09T15:27:37.000Z",
"title": "Luka Dončić And The Mavs Are Pushing The Limits Of Offensive Efficiency",
"url": "https://fivethirtyeight.com/features/luka-doncic-and-the-mavs-are-pushing-the-limits-of-offensive-efficiency/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/impeachment-polls",
"name": "impeachment-polls",
@@ -711,18 +603,6 @@
}
]
},
{
"url": "https://github.com/fivethirtyeight/candidate-emails",
"name": "candidate-emails",
"displayName": "candidate-<span class=\"lastword\">emails</span>",
"articles": [
{
"date": "2019-07-11T15:33:15.000Z",
"title": "What Our Inbox Tells Us About How Democrats Are Tackling Trump",
"url": "https://fivethirtyeight.com/features/which-democrats-are-campaigning-on-trump/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/nba-draymond",
"name": "nba-draymond",
@@ -763,18 +643,6 @@
}
]
},
{
"url": "https://github.com/fivethirtyeight/twitter-overlap",
"name": "twitter-overlap",
"displayName": "twitter-<span class=\"lastword\">overlap</span>",
"articles": [
{
"date": "2019-06-12T15:24:07.000Z",
"title": "Which 2020 Candidates Have The Most In Common … On Twitter?",
"url": "https://fivethirtyeight.com/features/which-2020-candidates-have-the-most-in-common-on-twitter/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/trump-lawsuits",
"name": "trump-lawsuits",
@@ -957,18 +825,6 @@
"https://projects.fivethirtyeight.com/congress-model-2018/governor_state_forecast.csv"
]
},
{
"url": "https://github.com/fivethirtyeight/actblue-analysis",
"name": "actblue-analysis",
"displayName": "actblue-<span class=\"lastword\">analysis</span>",
"articles": [
{
"date": "2018-10-25T17:31:42.000Z",
"title": "How ActBlue Is Trying To Turn Small Donations Into A Blue Wave",
"url": "https://fivethirtyeight.com/features/how-actblue-is-trying-to-turn-small-donations-into-a-blue-wave"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/primary-candidates-2018",
"name": "primary-candidates-2018",
@@ -1179,18 +1035,6 @@
}
]
},
{
"url": "https://github.com/fivethirtyeight/redistricting-atlas-data",
"name": "redistricting-atlas-data",
"displayName": "redistricting-atlas-<span class=\"lastword\">data</span>",
"articles": [
{
"date": "2018-01-25T11:00:00.000Z",
"title": "The Atlas Of Redistricting",
"url": "https://projects.fivethirtyeight.com/redistricting-maps/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/next-bechdel",
"name": "next-bechdel",
@@ -1620,37 +1464,6 @@
}
]
},
{
"url": "https://github.com/fivethirtyeight/uber-tlc-foil-response",
"name": "uber-tlc-foil-response",
"displayName": "uber-tlc-foil-<span class=\"lastword\">response</span>",
"articles": [
{
"date": "2015-12-09T16:19:40.000Z",
"title": "Is Uber Making NYC Rush-Hour Traffic Worse?",
"rowspan": 4,
"url": "https://fivethirtyeight.com/features/is-uber-making-nyc-rush-hour-traffic-worse/"
},
{
"date": "2015-10-13T20:44:12.000Z",
"title": "Uber Is Taking Millions Of Manhattan Rides Away From Taxis",
"rowspan": 0,
"url": "https://fivethirtyeight.com/features/uber-is-taking-millions-of-manhattan-rides-away-from-taxis/"
},
{
"date": "2015-08-28T10:30:36.000Z",
"title": "Public Transit Should Be Ubers New Best Friend",
"rowspan": 0,
"url": "https://fivethirtyeight.com/features/public-transit-should-be-ubers-new-best-friend/"
},
{
"date": "2015-08-10T18:06:17.000Z",
"title": "Uber Is Serving New Yorks Outer Boroughs More Than Taxis Are",
"rowspan": 0,
"url": "https://fivethirtyeight.com/features/uber-is-serving-new-yorks-outer-boroughs-more-than-taxis-are/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/tarantino",
"name": "tarantino",
@@ -2169,18 +1982,6 @@
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/comma-survey-data",
"name": "comma-survey-data",
"displayName": "comma-survey-<span class=\"lastword\">data</span>",
"articles": [
{
"date": "2014-06-17T16:28:55.000Z",
"title": "Elitist, Superfluous, Or Popular? We Polled Americans on the Oxford Comma",
"url": "http://fivethirtyeight.com/features/elitist-superfluous-or-popular-we-polled-americans-on-the-oxford-comma/"
}
]
},
{
"url": "https://github.com/fivethirtyeight/data/tree/master/world-cup-predictions",
"name": "world-cup-predictions",
@@ -2350,4 +2151,4 @@
}
]
}
]
]

View File

@@ -9,33 +9,81 @@ export default function Document() {
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>
<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>
<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="flex gap-x-2 items-center"
href="http://fivethirtyeight.com"
className="hover:underline font-bold"
href="https://data.fivethirtyeight.com"
>
<img
width="197"
height="25"
alt="FiveThirtyEight"
src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MjEgNTMuNzYiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojMDEwMTAxO308L3N0eWxlPjwvZGVmcz48dGl0bGU+QXJ0Ym9hcmQgOTU8L3RpdGxlPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTAgMGgyNXY4SDl2MTBoMTV2OEg5djE3SDBWMHpNMzEgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdIMzF6bTUtMzZoOHY4aC04ek0xNzkgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdoLTE3em01LTM2aDh2OGgtOHpNMzE2IDM2aDVWMThoLTV2LThoMTN2MjZoNHY3aC0xN3ptNS0zNmg4djhoLTh6TTU0IDI3VjEwaDh2MTVsNCA5Ljk4aDFMNzEgMjVWMTBoOHYxN2wtNyAxNkg2MWwtNy0xNnpNMTExIDQzSDk3LjQyQzg5LjIzIDQzIDg1IDM5LjE5IDg1IDMxLjE3VjIyYzAtNy41NyA0LjMtMTMgMTMtMTMgOS4zMyAwIDEzIDUuMDcgMTMgMTR2N0g5NHYxLjc0YzAgMi42MiAxIDQuMjYgMy40MiA0LjI2SDExMXpNOTQgMjNoOHYtMS41NWMwLTIuNjItMS4wNi01LjQ1LTQuMTMtNS40NS0yLjc5IDAtMy44NyAyLjItMy44NyA1LjQ1ek0xMjUgOGgtMTBWMGgyOXY4aC0xMHYzNWgtOVY4ek0yMDIgNDNWMTBoOHY0YzEuMTQtMi40NSAzLjc1LTQgNy4yMi00SDIyMHY4aC02Yy0yLjg0IDAtNCAuOTQtNCAzLjlWNDN6TTI0NSA0M2gtNC44NEMyMzMuMDUgNDMgMjMwIDM5LjMxIDIzMCAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0gyNDV6TTQyMSA0M2gtNC44NEM0MDkuMDUgNDMgNDA2IDM5LjMxIDQwNiAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0g0MjF6TTI1NC4yNiA1My43Nmw0LjYxLTkuNUwyNTEgMjdWMTBoOHYxNWw0IDEwaDFsNC0xMFYxMGg4djE3bC0xMi4zIDI2Ljc2aC05LjQ0ek0yODQgMGgyNXY4aC0xNnY5aDE1djhoLTE1djEwaDE2djhoLTI1VjB6TTMzNyA0OHYtMmgxNi4xYzIgMCAyLjktLjE4IDIuOS0xLjI3di0uMzRjMC0xLjA4LS45MS0xLjM5LTIuOS0xLjM5SDM0MHYtNWw1LTVjLTUuMjktMS40OC04LTUuNDMtOC0xMXYtMWMwLTcuNTYgNC40NC0xMiAxNC0xMmEyMS45MyAyMS45MyAwIDAgMSA1Ljk1IDFMMzYxIDRsNSAzLTQgNmMxLjM3IDEuOTMgMyA0LjkzIDMgOHYxYzAgNy0zLjMgMTAuNjYtMTIgMTFsLTMgNGg2YzUuOTIgMCA5IDIuNjIgOSA3LjY4di4xMWMwIDUuMDYtMi43MSA4LjIxLTguNjIgOC4yMWgtMTNjLTQuMjkgMC02LjM4LTEuODQtNi4zOC01em0xOS0yNXYtM2MwLTMuMy0xLjMzLTQtNS00cy01IC43LTUgNHYzYzAgMy4zIDEuMzkgNCA1IDRzNS0uNyA1LTR6TTM4MCA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuNC00IDctNCA2LjI2IDAgOSAzLjA4IDkgMTAuNzZWNDNoLThWMjJjMC0zLjEzLTEuMDctNS00LTVzLTQgMS44Ny00IDV6TTE1NyA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuOTEtNCA3LjQ5LTQgNi4yNiAwIDguNTEgMy4xMyA4LjUxIDEwLjgxVjQzaC04VjIxYzAtMy4xMy0xLjA3LTQuNDQtNC00LjQ0cy00IDIuMjYtNCA1LjM5eiIvPjwvc3ZnPg=="
/>{' '}
by PortalJS
</a>
</h1>
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 className="mx-2 py-1.5 text-[14px] text-[#3c3c3c]">
<ul className='flex gap-x-4'>
</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>
<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>
<a
className="hover:opacity-75 transition"
href="https://github.com/datopian/portaljs/tree/main/examples/fivethirtyeight"
>
View on Github
</a>
</li>
</ul>
</div>

View File

@@ -3,6 +3,7 @@ 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'] });
@@ -31,11 +32,7 @@ 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">
<a className="underline" href={dataset.url} target="_blank">
{dataset.name}
</a>
</span>
<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>
@@ -50,9 +47,9 @@ export function MobileItem({ dataset }: { dataset: Dataset }) {
<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={`/datasets/${dataset.name}`}
href={dataset.url}
>
explore
info
</a>
{/*
<button>
@@ -84,10 +81,8 @@ export function DesktopItem({ dataset }: { dataset: Dataset }) {
index === dataset.articles.length - 1 ? 'border-b' : ''
} border-zinc-400`}
>
<td className="py-8 font-light font-mono text-[14px] text-zinc-700">
<a className="underline" href={dataset.url} target="_blank">
{index === 0 ? dataset.name : ''}
</a>
<td className="py-8 font-light font-mono text-[13px] text-zinc-700">
{index === 0 ? dataset.name : ''}
</td>
<td>
<a
@@ -106,9 +101,9 @@ export function DesktopItem({ dataset }: { dataset: Dataset }) {
{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={`/datasets/${dataset.name}`}
href={dataset.url}
>
explore
info
</a>
)}
</td>
@@ -147,6 +142,7 @@ export async function getStaticProps() {
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}`}
>

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

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

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

View File

@@ -2,24 +2,26 @@
{
"owner": "os-data",
"branch": "main",
"repo": "mongolia-budget-2016-2017",
"files": [
"data/mongolia-2017.csv",
"data/mongolia-2017__2017.csv"
]
"name": "mongolia-budget-2016-2017"
},
{
"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"
"name": "gb-country-regional-analysis"
},
{
"owner": "os-data",
"branch": "main",
"name": "berlin-berlin"
},
{
"owner": "os-data",
"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;
}
let projectBase = "", last_updated = "";
let projectBase = '',
last_updated = '';
if (projectReadme) {
projectBase =
project.readme.split('/').length > 1
@@ -162,3 +163,30 @@ export async function getProject(project: GithubProject, github_pat?: string) {
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",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"test": "vitest"
},
"dependencies": {
"@octokit/plugin-throttling": "^5.2.2",
"@types/flexsearch": "^0.7.3",
"@types/node": "18.16.0",
"@types/react": "18.0.38",
"@types/react-dom": "18.0.11",
"@vitejs/plugin-react": "^4.0.0",
"clsx": "^1.2.1",
"eslint": "8.39.0",
"eslint-config-next": "13.3.1",
"flexsearch": "0.7.21",
"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-hook-form": "^7.43.9",
"react-markdown": "^8.0.7",
"react-timeago": "^7.1.0",
"remark-gfm": "^3.0.1",
@@ -30,6 +36,7 @@
"@tailwindcss/typography": "^0.5.9",
"autoprefixer": "^10.4.14",
"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>
</thead>
<tbody className="divide-y divide-gray-200">
{project.files.map((file) => (
{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>
@@ -79,7 +79,7 @@ export async function getStaticPaths() {
repo.readme && repo.readme.split('/').length > 1
? repo.readme.split('/').slice(0, -1)
: null;
let path = [repo.repo];
let path = [repo.name];
if (projectPath) {
projectPath.forEach((element) => {
path.push(element);
@@ -105,7 +105,7 @@ export async function getStaticProps({ params }) {
_repo.readme && _repo.readme.split('/').length > 1
? _repo.readme.split('/').slice(0, -1)
: null;
let path = [_repo.repo];
let path = [_repo.name];
if (projectPath) {
projectPath.forEach((element) => {
path.push(element);

View File

@@ -1,6 +1,10 @@
import { promises as fs } from 'fs';
import path from 'path';
import { getProject } from '../lib/octokit';
import {
GithubProject,
getProjectDataPackage,
getProjectMetadata,
} from '../lib/octokit';
import getConfig from 'next/config';
import ExternalLinkIcon from '../components/icons/ExternalLinkIcon';
import TimeAgo from 'react-timeago';
@@ -8,37 +12,55 @@ import Link from 'next/link';
import { Hero } from '../components/Hero';
import { Header } from '../components/Header';
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() {
const jsonDirectory = path.join(
process.cwd(),
'/datasets.json'
);
const jsonDirectory = path.join(process.cwd(), '/datasets.json');
const repos = await fs.readFile(jsonDirectory, 'utf8');
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(
(JSON.parse(repos)).map(async (repo) => {
const project = await getProject(repo, github_pat);
return { ...project, repo_config: repo };
return {
datapackage,
repo,
};
})
);
const projects = datapackages.map(
(item: { datapackage: FiscalDataPackage & { repo: string }; repo: any }) =>
loadDataPackage(item.datapackage, item.repo)
);
return {
props: {
projects,
projects: JSON.stringify(projects),
},
};
}
export function Datasets({ projects }) {
projects = JSON.parse(projects);
return (
<div className="bg-white min-h-screen">
<Header />
<Hero />
<section
className="py-20 sm:py-32"
>
<section className="py-20 sm:py-32">
<Container>
<div className="mx-auto max-w-2xl lg:mx-0">
<h2
@@ -51,75 +73,8 @@ export function Datasets({ projects }) {
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 className="mt-10">
<DatasetsSearch datasets={projects} />
</div>
</Container>
</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>
<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/leondz/hatespeechdata">
<MobileNavItem href="https://github.com/datopian/portaljs/tree/main/examples/turing">
View on Github <GithubIcon />
</MobileNavItem>
</ul>
@@ -179,7 +179,7 @@ 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/leondz/hatespeechdata">
<NavItem href="https://github.com/datopian/portaljs/tree/main/examples/turing">
View on Github <GithubIcon />
</NavItem>
</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
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)

View File

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