Compare commits
103 Commits
basic-exam
...
start-of-f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a7be6e402 | ||
|
|
fa05e374c9 | ||
|
|
f52832624f | ||
|
|
f2e7f157b9 | ||
|
|
ddc954d6bb | ||
|
|
7c62a8c93f | ||
|
|
64cc1355bb | ||
|
|
8945e7dd85 | ||
|
|
92f6c5eb47 | ||
|
|
ff8157bf57 | ||
|
|
ba3efc9ec7 | ||
|
|
87c46aba04 | ||
|
|
964eb5b3ee | ||
|
|
f93d4aa6bd | ||
|
|
902e5e07a0 | ||
|
|
ebcb93c996 | ||
|
|
1fc2499c71 | ||
|
|
1af24ef57e | ||
|
|
698c06efda | ||
|
|
8792f295b0 | ||
|
|
3e6d01c4c7 | ||
|
|
7c943c1b31 | ||
|
|
7197a6686e | ||
|
|
7822440f0d | ||
|
|
82773b5e8a | ||
|
|
1cfc4db528 | ||
|
|
336ff819dc | ||
|
|
f610c953e7 | ||
|
|
3f350f8fcd | ||
|
|
714faf9986 | ||
|
|
a954575397 | ||
|
|
ca13e7b9c3 | ||
|
|
f12e007ce4 | ||
|
|
2edf488fe7 | ||
|
|
ce395b4c49 | ||
|
|
51828b85f1 | ||
|
|
d2e9c54c13 | ||
|
|
6705bc1e2d | ||
|
|
7dfde0935e | ||
|
|
3f76bea895 | ||
|
|
f17efce02e | ||
|
|
61b96c20ed | ||
|
|
4cadc50e46 | ||
|
|
684f473e62 | ||
|
|
b963cf2cbb | ||
|
|
43ac5cfb47 | ||
|
|
f6b8ef2190 | ||
|
|
e5c89308d1 | ||
|
|
8b51123290 | ||
|
|
53b64b81c9 | ||
|
|
9fe08fcd1b | ||
|
|
7150150db0 | ||
|
|
5cc312b55b | ||
|
|
5c8431bf39 | ||
|
|
0a1ede10e8 | ||
|
|
45c07f829a | ||
|
|
53ea7957c0 | ||
|
|
0c65a145c8 | ||
|
|
91caeff6c3 | ||
|
|
0f65e253da | ||
|
|
c390a21611 | ||
|
|
dac7d03d05 | ||
|
|
89ba260b70 | ||
|
|
ce847746d2 | ||
|
|
5328492575 | ||
|
|
e52e789314 | ||
|
|
0e8cac7d50 | ||
|
|
2e30c76a3d | ||
|
|
edb2354945 | ||
|
|
5834a4a470 | ||
|
|
90b93e6819 | ||
|
|
ad52721a38 | ||
|
|
cf2a93abfd | ||
|
|
8afb30c96b | ||
|
|
94a3c2a5f0 | ||
|
|
a0620f9255 | ||
|
|
e5513f59a6 | ||
|
|
d73bcc77f3 | ||
|
|
1782f23b84 | ||
|
|
72405162a1 | ||
|
|
982733737d | ||
|
|
ea5802a908 | ||
|
|
229a7b5324 | ||
|
|
014c4c043d | ||
|
|
ed3a26cd6d | ||
|
|
026059184a | ||
|
|
a041d69282 | ||
|
|
016f3e20e9 | ||
|
|
169a92d313 | ||
|
|
14abd5b768 | ||
|
|
4aaabba229 | ||
|
|
cc43597130 | ||
|
|
d9a6ea4ef1 | ||
|
|
9c25c71286 | ||
|
|
f6b94ee254 | ||
|
|
04b05c0896 | ||
|
|
5b4d2d1990 | ||
|
|
b7e2e8e6b8 | ||
|
|
b6100546e3 | ||
|
|
58ca032d3f | ||
|
|
4b5329a93e | ||
|
|
298b59d291 | ||
|
|
41e7f8ad8d |
4
.gitignore
vendored
@@ -44,3 +44,7 @@ Thumbs.db
|
|||||||
# Env
|
# Env
|
||||||
.env
|
.env
|
||||||
**/.env
|
**/.env
|
||||||
|
|
||||||
|
# MarkdownDB
|
||||||
|
*.db
|
||||||
|
**/*.db
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
import VegaLite from "./VegaLite";
|
|
||||||
|
|
||||||
export default function LineChart({
|
|
||||||
data = [],
|
|
||||||
fullWidth = false,
|
|
||||||
title = "",
|
|
||||||
}) {
|
|
||||||
var tmp = data;
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
tmp = data.map((r, i) => {
|
|
||||||
return { x: r[0], y: r[1] };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const vegaData = { table: tmp };
|
|
||||||
const spec = {
|
|
||||||
$schema: "https://vega.github.io/schema/vega-lite/v5.json",
|
|
||||||
title,
|
|
||||||
width: 500,
|
|
||||||
height: 300,
|
|
||||||
mark: {
|
|
||||||
type: "line",
|
|
||||||
color: "black",
|
|
||||||
strokeWidth: 1,
|
|
||||||
tooltip: true,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
name: "table",
|
|
||||||
},
|
|
||||||
selection: {
|
|
||||||
grid: {
|
|
||||||
type: "interval",
|
|
||||||
bind: "scales",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
encoding: {
|
|
||||||
x: {
|
|
||||||
field: "x",
|
|
||||||
timeUnit: "year",
|
|
||||||
type: "temporal",
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
field: "y",
|
|
||||||
type: "quantitative",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return <VegaLite fullWidth={fullWidth} data={vegaData} spec={spec} />;
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Wrapper for the Vega Lite component
|
|
||||||
import { VegaLite as VegaLiteOg } from "react-vega";
|
|
||||||
|
|
||||||
export default function VegaLite(props) {
|
|
||||||
return <VegaLiteOg {...props} />;
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# Data
|
|
||||||
|
|
||||||
This is the README.md this project.
|
|
||||||
|
|
||||||
## Table
|
|
||||||
|
|
||||||
<Table url="data_1.csv" />
|
|
||||||
|
|
||||||
## Vega Lite Line Chart from URL
|
|
||||||
|
|
||||||
<VegaLite spec={ { "$schema": "https://vega.github.io/schema/vega-lite/v5.json", "data": {"url": "data_2.csv"}, "width": 550, "height": 250, "mark": "line", "encoding": { "x": {"field": "Time", "type": "temporal"}, "y": {"field": "Anomaly (deg C)", "type": "quantitative"}, "tooltip": {"field": "Anomaly (deg C)", "type": "quantitative"} } } } />
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import papa from "papaparse";
|
|
||||||
|
|
||||||
const parseCsv = (csv) => {
|
|
||||||
csv = csv.trim();
|
|
||||||
const rawdata = papa.parse(csv, { header: true });
|
|
||||||
const cols = rawdata.meta.fields.map((r, i) => {
|
|
||||||
return { key: r, name: r };
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
rows: rawdata.data,
|
|
||||||
fields: cols,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parseCsv;
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { GetStaticProps } from 'next';
|
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import parse from '../lib/markdown';
|
|
||||||
import DRD from '../components/DRD';
|
|
||||||
|
|
||||||
export const getServerSideProps = async (context) => {
|
|
||||||
const indexFile = path.join(process.cwd(), '/content/' + context.params.path.join('/') + '/index.md');
|
|
||||||
const readme = await fs.readFile(indexFile, 'utf8');
|
|
||||||
let { mdxSource, frontMatter } = await parse(readme, '.mdx');
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
mdxSource,
|
|
||||||
frontMatter,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DatasetPage({ mdxSource, frontMatter }) {
|
|
||||||
return (
|
|
||||||
<div className="prose mx-auto">
|
|
||||||
<header>
|
|
||||||
<div className="mb-6">
|
|
||||||
<>
|
|
||||||
<h1>{frontMatter.title}</h1>
|
|
||||||
{frontMatter.author && (
|
|
||||||
<div className="-mt-6">
|
|
||||||
<p className="opacity-60 pl-1">{frontMatter.author}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{frontMatter.description && (
|
|
||||||
<p className="description">{frontMatter.description}</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<DRD source={mdxSource} />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
export default async function handler(
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse<string>
|
|
||||||
) {
|
|
||||||
const contentDir = path.join(process.cwd(), '/content');
|
|
||||||
const datasets = await fs.readdir(contentDir);
|
|
||||||
const query = req.query;
|
|
||||||
const { fileName } = query;
|
|
||||||
const dataFile = path.join(
|
|
||||||
process.cwd(),
|
|
||||||
'/content/' + datasets[0] + '/' + fileName
|
|
||||||
);
|
|
||||||
const data = await fs.readFile(dataFile, 'utf8');
|
|
||||||
res.status(200).send(data)
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
import { GetStaticProps } from 'next';
|
|
||||||
import { promises as fs } from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import parse from '../lib/markdown';
|
|
||||||
import DRD from '../components/DRD';
|
|
||||||
|
|
||||||
export const getStaticProps = async (context) => {
|
|
||||||
const indexFile = path.join(process.cwd(), '/content/index.md');
|
|
||||||
const readme = await fs.readFile(indexFile, 'utf8');
|
|
||||||
let { mdxSource, frontMatter } = await parse(readme, '.mdx');
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
mdxSource,
|
|
||||||
frontMatter,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DatasetPage({ mdxSource, frontMatter }) {
|
|
||||||
return (
|
|
||||||
<div className="prose mx-auto">
|
|
||||||
<header>
|
|
||||||
<div className="mb-6">
|
|
||||||
<>
|
|
||||||
<h1>{frontMatter.title}</h1>
|
|
||||||
{frontMatter.author && (
|
|
||||||
<div className="-mt-6">
|
|
||||||
<p className="opacity-60 pl-1">{frontMatter.author}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{frontMatter.description && (
|
|
||||||
<p className="description">{frontMatter.description}</p>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<DRD source={mdxSource} />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
Year,Temp Anomaly
|
|
||||||
1850,-0.418
|
|
||||||
2020,0.923
|
|
||||||
|
@@ -1,173 +0,0 @@
|
|||||||
Time,Anomaly (deg C),Lower confidence limit (2.5%),Upper confidence limit (97.5%)
|
|
||||||
1850,-0.41765878,-0.589203,-0.24611452
|
|
||||||
1851,-0.2333498,-0.41186792,-0.054831687
|
|
||||||
1852,-0.22939907,-0.40938243,-0.04941572
|
|
||||||
1853,-0.27035445,-0.43000934,-0.110699534
|
|
||||||
1854,-0.29163003,-0.43282393,-0.15043613
|
|
||||||
1855,-0.2969512,-0.43935776,-0.15454465
|
|
||||||
1856,-0.32035372,-0.46809322,-0.1726142
|
|
||||||
1857,-0.46723005,-0.61632216,-0.31813794
|
|
||||||
1858,-0.3887657,-0.53688604,-0.24064532
|
|
||||||
1859,-0.28119546,-0.42384982,-0.13854107
|
|
||||||
1860,-0.39016518,-0.5389766,-0.24135375
|
|
||||||
1861,-0.42927712,-0.5972301,-0.26132414
|
|
||||||
1862,-0.53639776,-0.7037096,-0.36908585
|
|
||||||
1863,-0.3443432,-0.5341645,-0.1545219
|
|
||||||
1864,-0.4654367,-0.6480974,-0.282776
|
|
||||||
1865,-0.33258784,-0.5246526,-0.14052312
|
|
||||||
1866,-0.34126064,-0.52183825,-0.16068307
|
|
||||||
1867,-0.35696334,-0.55306214,-0.16086453
|
|
||||||
1868,-0.35196072,-0.52965826,-0.17426313
|
|
||||||
1869,-0.31657043,-0.47642276,-0.15671812
|
|
||||||
1870,-0.32789087,-0.46867347,-0.18710826
|
|
||||||
1871,-0.3685807,-0.5141493,-0.22301209
|
|
||||||
1872,-0.32804197,-0.4630833,-0.19300064
|
|
||||||
1873,-0.34133235,-0.4725396,-0.21012507
|
|
||||||
1874,-0.3732512,-0.5071426,-0.2393598
|
|
||||||
1875,-0.37562594,-0.514041,-0.23721085
|
|
||||||
1876,-0.42410994,-0.56287116,-0.28534868
|
|
||||||
1877,-0.101108834,-0.22982001,0.027602348
|
|
||||||
1878,-0.011315193,-0.13121258,0.10858219
|
|
||||||
1879,-0.30363432,-0.43406433,-0.1732043
|
|
||||||
1880,-0.31583208,-0.44015095,-0.19151321
|
|
||||||
1881,-0.23224552,-0.35793498,-0.10655605
|
|
||||||
1882,-0.29553008,-0.4201501,-0.17091006
|
|
||||||
1883,-0.3464744,-0.4608177,-0.23213111
|
|
||||||
1884,-0.49232006,-0.6026686,-0.38197154
|
|
||||||
1885,-0.47112358,-0.5830682,-0.35917896
|
|
||||||
1886,-0.42090362,-0.5225382,-0.31926903
|
|
||||||
1887,-0.49878576,-0.61655986,-0.3810117
|
|
||||||
1888,-0.37937889,-0.49332377,-0.265434
|
|
||||||
1889,-0.24989556,-0.37222093,-0.12757017
|
|
||||||
1890,-0.50685817,-0.6324095,-0.3813068
|
|
||||||
1891,-0.40131494,-0.5373699,-0.26525995
|
|
||||||
1892,-0.5075585,-0.64432853,-0.3707885
|
|
||||||
1893,-0.49461925,-0.6315314,-0.35770702
|
|
||||||
1894,-0.48376393,-0.6255681,-0.34195974
|
|
||||||
1895,-0.4487516,-0.58202064,-0.3154826
|
|
||||||
1896,-0.28400728,-0.4174015,-0.15061308
|
|
||||||
1897,-0.25980017,-0.39852425,-0.12107607
|
|
||||||
1898,-0.48579213,-0.6176492,-0.35393503
|
|
||||||
1899,-0.35543364,-0.48639694,-0.22447036
|
|
||||||
1900,-0.23447904,-0.3669676,-0.10199049
|
|
||||||
1901,-0.29342857,-0.42967388,-0.15718324
|
|
||||||
1902,-0.43898427,-0.5754281,-0.30254042
|
|
||||||
1903,-0.5333264,-0.66081935,-0.40583345
|
|
||||||
1904,-0.5975614,-0.7288325,-0.46629035
|
|
||||||
1905,-0.40775132,-0.5350291,-0.28047356
|
|
||||||
1906,-0.3191393,-0.45052385,-0.18775477
|
|
||||||
1907,-0.5041577,-0.6262818,-0.38203365
|
|
||||||
1908,-0.5138707,-0.63748026,-0.3902612
|
|
||||||
1909,-0.5357649,-0.6526296,-0.41890016
|
|
||||||
1910,-0.5310242,-0.6556868,-0.40636164
|
|
||||||
1911,-0.5392051,-0.66223973,-0.4161705
|
|
||||||
1912,-0.47567302,-0.5893311,-0.36201498
|
|
||||||
1913,-0.46715254,-0.5893755,-0.34492958
|
|
||||||
1914,-0.2625924,-0.38276345,-0.1424214
|
|
||||||
1915,-0.19184391,-0.32196194,-0.06172589
|
|
||||||
1916,-0.42020997,-0.5588941,-0.28152588
|
|
||||||
1917,-0.54301953,-0.6921192,-0.3939199
|
|
||||||
1918,-0.42458433,-0.58198184,-0.26718682
|
|
||||||
1919,-0.32551822,-0.48145813,-0.1695783
|
|
||||||
1920,-0.2985808,-0.44860035,-0.14856121
|
|
||||||
1921,-0.24067703,-0.38175339,-0.09960067
|
|
||||||
1922,-0.33922812,-0.46610323,-0.21235302
|
|
||||||
1923,-0.31793055,-0.444173,-0.1916881
|
|
||||||
1924,-0.3120622,-0.4388317,-0.18529275
|
|
||||||
1925,-0.28242525,-0.4147755,-0.15007503
|
|
||||||
1926,-0.12283547,-0.25264767,0.006976739
|
|
||||||
1927,-0.22940508,-0.35135695,-0.10745319
|
|
||||||
1928,-0.20676155,-0.33881804,-0.074705064
|
|
||||||
1929,-0.39275664,-0.52656746,-0.25894582
|
|
||||||
1930,-0.1768054,-0.29041144,-0.06319936
|
|
||||||
1931,-0.10339768,-0.2126916,0.0058962475
|
|
||||||
1932,-0.14546166,-0.25195515,-0.0389682
|
|
||||||
1933,-0.32234442,-0.4271004,-0.21758842
|
|
||||||
1934,-0.17433685,-0.27400395,-0.07466974
|
|
||||||
1935,-0.20605922,-0.30349734,-0.10862111
|
|
||||||
1936,-0.16952093,-0.26351926,-0.07552261
|
|
||||||
1937,-0.01919893,-0.11975875,0.08136089
|
|
||||||
1938,-0.012200732,-0.11030374,0.08590227
|
|
||||||
1939,-0.040797167,-0.14670466,0.065110326
|
|
||||||
1940,0.07593584,-0.04194966,0.19382134
|
|
||||||
1941,0.038129337,-0.16225387,0.23851255
|
|
||||||
1942,0.0014060909,-0.1952124,0.19802457
|
|
||||||
1943,0.0064140745,-0.19959097,0.21241911
|
|
||||||
1944,0.14410514,-0.054494828,0.3427051
|
|
||||||
1945,0.043088365,-0.15728289,0.24345961
|
|
||||||
1946,-0.1188128,-0.2659574,0.028331792
|
|
||||||
1947,-0.091205545,-0.23179041,0.04937931
|
|
||||||
1948,-0.12466127,-0.25913337,0.009810844
|
|
||||||
1949,-0.14380224,-0.2540775,-0.033526987
|
|
||||||
1950,-0.22662179,-0.33265698,-0.12058662
|
|
||||||
1951,-0.06115397,-0.15035024,0.028042298
|
|
||||||
1952,0.015354565,-0.08293597,0.11364509
|
|
||||||
1953,0.07763074,-0.020529618,0.1757911
|
|
||||||
1954,-0.11675021,-0.20850271,-0.024997713
|
|
||||||
1955,-0.19730993,-0.28442997,-0.1101899
|
|
||||||
1956,-0.2631656,-0.33912563,-0.18720557
|
|
||||||
1957,-0.035334926,-0.10056862,0.029898768
|
|
||||||
1958,-0.017632553,-0.083074555,0.04780945
|
|
||||||
1959,-0.048004825,-0.11036375,0.0143540995
|
|
||||||
1960,-0.115487024,-0.17416587,-0.056808177
|
|
||||||
1961,-0.019997388,-0.07078052,0.030785747
|
|
||||||
1962,-0.06405444,-0.11731443,-0.010794453
|
|
||||||
1963,-0.03680589,-0.09057008,0.016958294
|
|
||||||
1964,-0.30586675,-0.34949213,-0.26224136
|
|
||||||
1965,-0.2043879,-0.25357357,-0.15520222
|
|
||||||
1966,-0.14888458,-0.19839221,-0.09937696
|
|
||||||
1967,-0.11751631,-0.16062479,-0.07440783
|
|
||||||
1968,-0.1686323,-0.21325313,-0.124011464
|
|
||||||
1969,-0.031366713,-0.07186544,0.009132013
|
|
||||||
1970,-0.08510657,-0.12608096,-0.04413217
|
|
||||||
1971,-0.20593274,-0.24450706,-0.16735843
|
|
||||||
1972,-0.0938271,-0.13171694,-0.05593726
|
|
||||||
1973,0.04993336,0.013468528,0.086398184
|
|
||||||
1974,-0.17253734,-0.21022376,-0.1348509
|
|
||||||
1975,-0.11075424,-0.15130512,-0.07020335
|
|
||||||
1976,-0.21586166,-0.25588378,-0.17583954
|
|
||||||
1977,0.10308852,0.060056705,0.14612034
|
|
||||||
1978,0.0052557723,-0.034576867,0.04508841
|
|
||||||
1979,0.09085813,0.062358618,0.119357646
|
|
||||||
1980,0.19607207,0.162804,0.22934014
|
|
||||||
1981,0.25001204,0.21939126,0.28063282
|
|
||||||
1982,0.034263328,-0.005104665,0.07363132
|
|
||||||
1983,0.22383861,0.18807402,0.2596032
|
|
||||||
1984,0.04800471,0.011560736,0.08444869
|
|
||||||
1985,0.04972978,0.015663471,0.08379609
|
|
||||||
1986,0.09568697,0.064408,0.12696595
|
|
||||||
1987,0.2430264,0.21218552,0.27386728
|
|
||||||
1988,0.28215173,0.2470353,0.31726816
|
|
||||||
1989,0.17925027,0.14449838,0.21400215
|
|
||||||
1990,0.36056247,0.32455227,0.39657268
|
|
||||||
1991,0.33889654,0.30403617,0.3737569
|
|
||||||
1992,0.124896795,0.09088206,0.15891153
|
|
||||||
1993,0.16565846,0.12817313,0.2031438
|
|
||||||
1994,0.23354977,0.19841294,0.2686866
|
|
||||||
1995,0.37686616,0.34365577,0.41007656
|
|
||||||
1996,0.2766894,0.24318004,0.31019878
|
|
||||||
1997,0.4223085,0.39009082,0.4545262
|
|
||||||
1998,0.57731646,0.54304415,0.6115888
|
|
||||||
1999,0.32448497,0.29283476,0.35613516
|
|
||||||
2000,0.3310848,0.29822788,0.36394167
|
|
||||||
2001,0.48928034,0.4580683,0.5204924
|
|
||||||
2002,0.5434665,0.51278186,0.57415116
|
|
||||||
2003,0.5441702,0.5112426,0.5770977
|
|
||||||
2004,0.46737072,0.43433833,0.5004031
|
|
||||||
2005,0.60686255,0.5757053,0.6380198
|
|
||||||
2006,0.5725527,0.541973,0.60313237
|
|
||||||
2007,0.5917013,0.56135315,0.6220495
|
|
||||||
2008,0.46564984,0.43265733,0.49864236
|
|
||||||
2009,0.5967817,0.56525564,0.6283077
|
|
||||||
2010,0.68037146,0.649076,0.7116669
|
|
||||||
2011,0.53769773,0.5060012,0.5693943
|
|
||||||
2012,0.5776071,0.5448553,0.6103589
|
|
||||||
2013,0.6235754,0.5884838,0.6586669
|
|
||||||
2014,0.67287165,0.63890487,0.7068384
|
|
||||||
2015,0.82511437,0.79128706,0.8589417
|
|
||||||
2016,0.93292713,0.90176356,0.96409065
|
|
||||||
2017,0.84517425,0.81477475,0.87557375
|
|
||||||
2018,0.762654,0.731052,0.79425603
|
|
||||||
2019,0.8910726,0.85678726,0.92535794
|
|
||||||
2020,0.9227938,0.8882121,0.9573755
|
|
||||||
2021,0.6640137,0.5372486,0.79077876
|
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,129 +0,0 @@
|
|||||||
.container {
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main {
|
|
||||||
min-height: 100vh;
|
|
||||||
padding: 4rem 0;
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
display: flex;
|
|
||||||
flex: 1;
|
|
||||||
padding: 2rem 0;
|
|
||||||
border-top: 1px solid #eaeaea;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer a {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title a {
|
|
||||||
color: #0070f3;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title a:hover,
|
|
||||||
.title a:focus,
|
|
||||||
.title a:active {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1.15;
|
|
||||||
font-size: 4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title,
|
|
||||||
.description {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.description {
|
|
||||||
margin: 4rem 0;
|
|
||||||
line-height: 1.5;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.code {
|
|
||||||
background: #fafafa;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0.75rem;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
|
||||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
max-width: 800px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
margin: 1rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
text-align: left;
|
|
||||||
color: inherit;
|
|
||||||
text-decoration: none;
|
|
||||||
border: 1px solid #eaeaea;
|
|
||||||
border-radius: 10px;
|
|
||||||
transition: color 0.15s ease, border-color 0.15s ease;
|
|
||||||
max-width: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card:hover,
|
|
||||||
.card:focus,
|
|
||||||
.card:active {
|
|
||||||
color: #0070f3;
|
|
||||||
border-color: #0070f3;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card h2 {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 1em;
|
|
||||||
margin-left: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.grid {
|
|
||||||
width: 100%;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
.card,
|
|
||||||
.footer {
|
|
||||||
border-color: #222;
|
|
||||||
}
|
|
||||||
.code {
|
|
||||||
background: #111;
|
|
||||||
}
|
|
||||||
.logo img {
|
|
||||||
filter: invert(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -10,24 +10,24 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.0.17",
|
"@heroicons/react": "^2.0.17",
|
||||||
"@types/node": "18.16.0",
|
|
||||||
"@types/react": "18.0.38",
|
|
||||||
"@types/react-dom": "18.0.11",
|
|
||||||
"eslint": "8.39.0",
|
|
||||||
"eslint-config-next": "13.3.1",
|
|
||||||
"next": "13.3.1",
|
"next": "13.3.1",
|
||||||
"next-seo": "^6.0.0",
|
"next-seo": "^6.0.0",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.14",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1"
|
||||||
"typescript": "5.0.4"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"eslint-config-next": "13.3.1",
|
||||||
|
"typescript": "5.0.4",
|
||||||
|
"@types/node": "18.16.0",
|
||||||
|
"@types/react": "18.0.38",
|
||||||
|
"@types/react-dom": "18.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
examples/fivethirtyeight/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -8,6 +14,8 @@ First, run the development server:
|
|||||||
npm run dev
|
npm run dev
|
||||||
# or
|
# or
|
||||||
yarn dev
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
@@ -18,6 +26,8 @@ You can start editing the page by modifying `pages/index.tsx`. The page auto-upd
|
|||||||
|
|
||||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
## Learn More
|
## Learn More
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
To learn more about Next.js, take a look at the following resources:
|
||||||
23
examples/fivethirtyeight/components/Breadcrumbs.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
function HomeIcon({ className = "" }) {
|
||||||
|
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M 12 2 A 1 1 0 0 0 11.289062 2.296875 L 1.203125 11.097656 A 0.5 0.5 0 0 0 1 11.5 A 0.5 0.5 0 0 0 1.5 12 L 4 12 L 4 20 C 4 20.552 4.448 21 5 21 L 9 21 C 9.552 21 10 20.552 10 20 L 10 14 L 14 14 L 14 20 C 14 20.552 14.448 21 15 21 L 19 21 C 19.552 21 20 20.552 20 20 L 20 12 L 22.5 12 A 0.5 0.5 0 0 0 23 11.5 A 0.5 0.5 0 0 0 22.796875 11.097656 L 12.716797 2.3027344 A 1 1 0 0 0 12.710938 2.296875 A 1 1 0 0 0 12 2 z"/></svg></div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Breadcrumbs({ links }: { links: { title: string, href?: string, target?: string }[] }) {
|
||||||
|
const current = links.at(-1);
|
||||||
|
|
||||||
|
return <div className="flex items-center uppercase font-black text-xs">
|
||||||
|
<Link className="flex items-center" href='/'><HomeIcon /></Link>
|
||||||
|
|
||||||
|
{/* {links.length > 1 && links.slice(0, -1).map((link) => {
|
||||||
|
return <>
|
||||||
|
<span className="mx-4">/</span>
|
||||||
|
<Link href={link.href}>{link.title}</Link>
|
||||||
|
</>
|
||||||
|
})} */}
|
||||||
|
|
||||||
|
<span className="mx-4">/</span>
|
||||||
|
<span>{current?.title}</span>
|
||||||
|
</div >
|
||||||
|
}
|
||||||
2154
examples/fivethirtyeight/datasets.json
Normal file
38
examples/fivethirtyeight/lib/octokit.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
|
export interface GithubProject {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
branch: string;
|
||||||
|
files: string[];
|
||||||
|
readme: string;
|
||||||
|
description?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectReadme(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
readme: string,
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const response = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: readme,
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
const data = response.data as { content?: string };
|
||||||
|
const fileContent = data.content ? data.content : '';
|
||||||
|
if (fileContent === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const decodedContent = Buffer.from(fileContent, 'base64').toString();
|
||||||
|
return decodedContent;
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
swcMinify: true,
|
serverRuntimeConfig: {
|
||||||
|
github_pat: process.env.GITHUB_PAT ? process.env.GITHUB_PAT : null,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = nextConfig
|
module.exports = nextConfig
|
||||||
6878
examples/fivethirtyeight/package-lock.json
generated
Normal file
37
examples/fivethirtyeight/package.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "fiverthirtyeight-example",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@portaljs/components": "^0.1.0",
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@types/node": "20.1.1",
|
||||||
|
"@types/react": "18.2.6",
|
||||||
|
"@types/react-dom": "18.2.4",
|
||||||
|
"autoprefixer": "10.4.14",
|
||||||
|
"eslint": "8.40.0",
|
||||||
|
"eslint-config-next": "13.4.1",
|
||||||
|
"flexsearch": "^0.7.31",
|
||||||
|
"next": "13.4.1",
|
||||||
|
"next-mdx-remote": "^4.4.1",
|
||||||
|
"next-seo": "^6.0.0",
|
||||||
|
"octokit": "^2.0.14",
|
||||||
|
"postcss": "8.4.23",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-markdown": "^8.0.7",
|
||||||
|
"remark-code-frontmatter": "^1.0.0",
|
||||||
|
"remark-extract-frontmatter": "^3.2.0",
|
||||||
|
"remark-frontmatter": "^4.0.1",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"tailwindcss": "3.3.2",
|
||||||
|
"timeago.js": "^4.0.2",
|
||||||
|
"typescript": "5.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
8
examples/fivethirtyeight/pages/_app.tsx
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import '@/styles/globals.css'
|
||||||
|
import '@portaljs/components/styles.css'
|
||||||
|
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
return <Component {...pageProps} />
|
||||||
|
}
|
||||||
96
examples/fivethirtyeight/pages/_document.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
examples/fivethirtyeight/pages/datasets/[datasetName].tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { NextSeo } from 'next-seo';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import { getProjectReadme, GithubProject } from '@/lib/octokit';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import extract from 'remark-extract-frontmatter';
|
||||||
|
import { Dataset } from '..';
|
||||||
|
import { GetStaticProps } from 'next';
|
||||||
|
import { Table } from '@portaljs/components';
|
||||||
|
import Breadcrumbs from '@/components/Breadcrumbs';
|
||||||
|
import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
|
||||||
|
import remarkFrontmatter from 'remark-frontmatter';
|
||||||
|
|
||||||
|
export default function DatasetPage({
|
||||||
|
dataset,
|
||||||
|
}: {
|
||||||
|
dataset: Dataset & {
|
||||||
|
readme: string | null;
|
||||||
|
};
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NextSeo title={`${dataset.name} page`} />
|
||||||
|
<main className="max-w-5xl px-2 prose mx-auto my-8 prose-thead:border-b-4 prose-table:max-w-5xl prose-table:overflow-scroll prose-thead:overflow-scroll prose-tbody:overflow-scroll prose-thead:pb-2 prose-thead:border-zinc-900 prose-th:uppercase prose-th:text-left prose-th:font-light prose-th:text-xs">
|
||||||
|
<Breadcrumbs links={[{ title: dataset.name, href: '' }]} />
|
||||||
|
<h1 className="uppercase mb-0 mt-16">{dataset.name}</h1>
|
||||||
|
<p className="mb-8">
|
||||||
|
<span className="font-semibold">Repository:</span>{' '}
|
||||||
|
<a target="_blank" href={dataset.url}>
|
||||||
|
{dataset.url}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2 className="mb-0 mt-10">FILES</h2>
|
||||||
|
<div className="inline-block min-w-full py-2 align-middle">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead className="border-b-4 pb-2 border-zinc-900">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
className="uppercase text-left font-light text-xs pb-3"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{dataset.files?.map((file) => (
|
||||||
|
<tr key={file}>
|
||||||
|
<td className="whitespace-nowrap text-left py-4 text-sm text-gray-500">
|
||||||
|
<a href={file}>{file.split('/').slice(-1)}</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{dataset.files && dataset.files.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2 className="mb-0 mt-10">DATA PREVIEWS</h2>
|
||||||
|
{dataset.files?.map((file) => (
|
||||||
|
<div key={file} className="preview-table my-8">
|
||||||
|
<h3>{file.split('/').slice(-1)}</h3>
|
||||||
|
<Table url={file} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{dataset.readme && (
|
||||||
|
<>
|
||||||
|
<h2 className="uppercase font-black">Readme</h2>
|
||||||
|
{dataset.readme && (
|
||||||
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[
|
||||||
|
remarkFrontmatter,
|
||||||
|
remarkGfm,
|
||||||
|
[extract, { remove: true }],
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{dataset.readme}
|
||||||
|
</ReactMarkdown>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const datasetsFile = path.join(process.cwd(), 'datasets.json');
|
||||||
|
const datasets = await fs.readFile(datasetsFile, 'utf8');
|
||||||
|
|
||||||
|
return {
|
||||||
|
paths: JSON.parse(datasets).map((dataset: Dataset) => {
|
||||||
|
return {
|
||||||
|
params: { datasetName: dataset.name },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
fallback: false, // can also be true or 'blocking'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||||
|
const datasetsFile = path.join(process.cwd(), 'datasets.json');
|
||||||
|
const datasetsString = await fs.readFile(datasetsFile, 'utf8');
|
||||||
|
const datasets: Dataset[] = JSON.parse(datasetsString);
|
||||||
|
const dataset: Dataset | undefined = datasets.find(
|
||||||
|
(_dataset) => _dataset.name === params?.datasetName
|
||||||
|
);
|
||||||
|
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||||
|
const readmes = await Promise.all(['/README.md', '/readme.md', '/Readme.md'].map(async (readme) => await getProjectReadme(
|
||||||
|
'fivethirtyeight',
|
||||||
|
'data',
|
||||||
|
'master',
|
||||||
|
dataset?.name + readme,
|
||||||
|
github_pat
|
||||||
|
)));
|
||||||
|
const readme = readmes.find(item => item !== null)
|
||||||
|
if (!readme) console.log('Readme not found for ' + dataset?.name)
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
dataset: {
|
||||||
|
...dataset,
|
||||||
|
readme,
|
||||||
|
files: dataset && dataset.files ? dataset.files : null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
211
examples/fivethirtyeight/pages/index.tsx
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import Image from 'next/image';
|
||||||
|
import { Inter } from 'next/font/google';
|
||||||
|
import { format } from 'timeago.js';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { NextSeo } from 'next-seo';
|
||||||
|
|
||||||
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
date: string;
|
||||||
|
title: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Dataset {
|
||||||
|
url: string;
|
||||||
|
name: string;
|
||||||
|
displayName: string;
|
||||||
|
articles: Article[];
|
||||||
|
files?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a weekday along with a long date
|
||||||
|
const options = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export function MobileItem({ dataset }: { dataset: Dataset }) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-x-2 pb-2 py-4 items-center justify-between border-b border-zinc-600">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-mono font-light">{dataset.name}</span>
|
||||||
|
{dataset.articles.map((article) => (
|
||||||
|
<div key={article.title} className="py-1 flex flex-col">
|
||||||
|
<span className="font-bold hover:underline">{article.title}</span>
|
||||||
|
<span className="font-light text-base">
|
||||||
|
{format(article.date).includes('years')
|
||||||
|
? new Date(article.date).toLocaleString('en-US', options)
|
||||||
|
: format(article.date)}
|
||||||
|
</span>{' '}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col justify-start">
|
||||||
|
<a
|
||||||
|
className="ml-2 border border-zinc-900 font-light px-4 py-1 text-sm transition hover:bg-zinc-900 hover:text-white"
|
||||||
|
href={dataset.url}
|
||||||
|
>
|
||||||
|
info
|
||||||
|
</a>
|
||||||
|
{/*
|
||||||
|
<button>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="w-12 h-12 text-blue-400 hover:text-blue-300 transition mt-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-.53 14.03a.75.75 0 001.06 0l3-3a.75.75 0 10-1.06-1.06l-1.72 1.72V8.25a.75.75 0 00-1.5 0v5.69l-1.72-1.72a.75.75 0 00-1.06 1.06l3 3z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DesktopItem({ dataset }: { dataset: Dataset }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{dataset.articles.map((article, index) => (
|
||||||
|
<tr
|
||||||
|
key={article.url}
|
||||||
|
className={`${
|
||||||
|
index === dataset.articles.length - 1 ? 'border-b' : ''
|
||||||
|
} border-zinc-400`}
|
||||||
|
>
|
||||||
|
<td className="py-8 font-light font-mono text-[13px] text-zinc-700">
|
||||||
|
{index === 0 ? dataset.name : ''}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
className="py-8 font-bold hover:underline pr-2"
|
||||||
|
href={article.url}
|
||||||
|
>
|
||||||
|
{article.title}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td className="py-8 font-light text-[14px] min-w-[138px] font-mono text-[#999]">
|
||||||
|
{format(article.date).includes('years')
|
||||||
|
? new Date(article.date).toLocaleString('en-US', options)
|
||||||
|
: format(article.date)}
|
||||||
|
</td>
|
||||||
|
<td className="py-8">
|
||||||
|
{index === 0 && (
|
||||||
|
<a
|
||||||
|
className="ml-2 border border-zinc-900 font-light px-[25px] py-2.5 text-sm transition hover:bg-zinc-900 hover:text-white"
|
||||||
|
href={dataset.url}
|
||||||
|
>
|
||||||
|
info
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
{/*
|
||||||
|
<td>
|
||||||
|
<button>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="w-12 h-12 text-blue-400 hover:text-blue-300 transition mt-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-.53 14.03a.75.75 0 001.06 0l3-3a.75.75 0 10-1.06-1.06l-1.72 1.72V8.25a.75.75 0 00-1.5 0v5.69l-1.72-1.72a.75.75 0 00-1.06 1.06l3 3z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>*/}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticProps() {
|
||||||
|
const jsonDirectory = path.join(process.cwd(), '/datasets.json');
|
||||||
|
const datasetString = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
|
const datasets = JSON.parse(datasetString);
|
||||||
|
return {
|
||||||
|
props: { datasets },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home({ datasets }: { datasets: Dataset[] }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NextSeo title="FiveThirtyEight tribute by PortalJS" />
|
||||||
|
<main
|
||||||
|
className={`flex min-h-screen flex-col items-center max-w-5xl mx-auto pt-20 px-2.5 ${inter.className}`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-[40px] font-bold text-zinc-800 text-center">
|
||||||
|
Our Data
|
||||||
|
</h1>
|
||||||
|
<p className="max-w-[600px] text-[17px] text-center text-[#6d6f71]">
|
||||||
|
We’re sharing the data and code behind some of our articles and
|
||||||
|
graphics. We hope you’ll use it to check our work and to create
|
||||||
|
stories and visualizations of your own.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<article className="w-full px-2 md:hidden py-4">
|
||||||
|
{datasets.map((dataset) => (
|
||||||
|
<MobileItem key={dataset.name} dataset={dataset} />
|
||||||
|
))}
|
||||||
|
</article>
|
||||||
|
<table className="w-full mt-10 mb-4 hidden md:table">
|
||||||
|
<thead className="border-b-4 pb-2 border-zinc-900">
|
||||||
|
<tr>
|
||||||
|
<th className="uppercase text-left font-normal text-xs pb-3">
|
||||||
|
data set
|
||||||
|
</th>
|
||||||
|
<th className="uppercase text-left font-normal text-xs pb-3">
|
||||||
|
related content
|
||||||
|
</th>
|
||||||
|
<th className="uppercase text-left font-normal text-xs pb-3">
|
||||||
|
last updated
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{datasets.map((dataset) => (
|
||||||
|
<DesktopItem key={dataset.name} dataset={dataset} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<p className="text-[13px] py-8">
|
||||||
|
Unless otherwise noted, our data sets are available under the{' '}
|
||||||
|
<a
|
||||||
|
className="text-blue-400 hover:underline"
|
||||||
|
href="http://creativecommons.org/licenses/by/4.0/"
|
||||||
|
>
|
||||||
|
Creative Commons Attribution 4.0 International license
|
||||||
|
</a>
|
||||||
|
, and the code is available under the{' '}
|
||||||
|
<a
|
||||||
|
className="text-blue-400 hover:underline"
|
||||||
|
href="http://opensource.org/licenses/MIT"
|
||||||
|
>
|
||||||
|
MIT license
|
||||||
|
</a>
|
||||||
|
. If you find this information useful, please{' '}
|
||||||
|
<a
|
||||||
|
className="text-blue-400 hover:underline"
|
||||||
|
href="mailto:data@fivethirtyeight.com"
|
||||||
|
>
|
||||||
|
let us know
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
1
examples/fivethirtyeight/public/next.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
examples/fivethirtyeight/public/share_image.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
1
examples/fivethirtyeight/public/vercel.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
||||||
|
After Width: | Height: | Size: 629 B |
8
examples/fivethirtyeight/styles/globals.css
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.preview-table > div {
|
||||||
|
overflow-x: scroll;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
18
examples/fivethirtyeight/tailwind.config.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
backgroundImage: {
|
||||||
|
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
||||||
|
'gradient-conic':
|
||||||
|
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('@tailwindcss/typography')],
|
||||||
|
};
|
||||||
23
examples/fivethirtyeight/tsconfig.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
102
examples/github-backed-catalog/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# A data catalog with data on GitHub
|
||||||
|
|
||||||
|
This example showcases a simple data catalog that get its data from a list of GitHub repos that serve as datasets.
|
||||||
|
|
||||||
|
A `datasets.json` file is used to specify which datasets are going to be part of the data catalog.
|
||||||
|
|
||||||
|
The application contains an index page, which lists all the datasets specified in the `datasets.json` file, and users can see more information about each dataset, such as the list of data files in it and the README, by clicking the "info" button on the list.
|
||||||
|
|
||||||
|
You can read more about it on the [Data catalog with data on GitHub](https://portaljs.org/docs/examples/github-backed-catalog) blog post.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
https://example.portaljs.org/
|
||||||
|
|
||||||
|
## Deploy your own
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fgithub-backed-catalog)
|
||||||
|
|
||||||
|
By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own GitHub/GitLab/Bitbucket account and automatically deploy everything.
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
Execute `create-next-app` to bootstrap the example:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/github-backed-catalog
|
||||||
|
cd <app-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set environment variables
|
||||||
|
|
||||||
|
This project uses the GitHub API, which for anonymous users will cap at 50 requests per hour, so you might want to get a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and add it to a `.env` file inside the folder like so
|
||||||
|
|
||||||
|
```
|
||||||
|
GITHUB_PAT=<github token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change datasets
|
||||||
|
|
||||||
|
You can change the datasets that will be displayed in the data catalog by editing the file `datasets.json`. Some examples can be found inside [this repo](https://github.com/datasets).
|
||||||
|
|
||||||
|
### Run in development mode
|
||||||
|
|
||||||
|
Run the app using:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost:3000 from your browser. You should see something similar to this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If click on the `info` button for a dataset you will see a page similar to this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### Structure of `datasets.json`
|
||||||
|
|
||||||
|
The `datasets.json` file is simply a list of datasets, below you can see a minimal example of a dataset:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"owner": "fivethirtyeight",
|
||||||
|
"repo": "data",
|
||||||
|
"branch": "master",
|
||||||
|
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
|
||||||
|
"readme": "nba-raptor/README.md"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It has:
|
||||||
|
|
||||||
|
- A `owner` which is going to be the github repo owner
|
||||||
|
- A `repo` which is going to be the github repo name
|
||||||
|
- A `branch` which is going to be the branch to which we need to get the files and the readme
|
||||||
|
- A list of `files` which is going to be a list of paths with files that you want to show to the world
|
||||||
|
- A `readme` which is going to be the path to your data description, it can also be a subpath eg: `example/README.md`
|
||||||
|
|
||||||
|
You can also add:
|
||||||
|
|
||||||
|
- A `description` which is useful if you have more than one dataset for each repo, if not provided we are just going to use the repo description
|
||||||
|
- A `Name` which is useful if you want to give your dataset a nice name, if not provided we are going to use the junction of the `owner` the `repo` + the path of the README, in the exaple above it will be `fivethirtyeight/data/nba-raptor`
|
||||||
|
|
||||||
|
### Extra commands
|
||||||
|
|
||||||
|
You can also build the project for production with:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
And run the production build with:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import HomeIcon from "../icons/HomeIcon";
|
||||||
|
|
||||||
|
export default function Breadcrumbs({ links }: { links: { title: string, href?: string, target?: string }[] }) {
|
||||||
|
const current = links.at(-1);
|
||||||
|
|
||||||
|
return <div className="flex items-center uppercase font-black text-xs">
|
||||||
|
<Link className="flex items-center" href='/'><HomeIcon /></Link>
|
||||||
|
|
||||||
|
{/* {links.length > 1 && links.slice(0, -1).map((link) => {
|
||||||
|
return <>
|
||||||
|
<span className="mx-4">/</span>
|
||||||
|
<Link href={link.href}>{link.title}</Link>
|
||||||
|
</>
|
||||||
|
})} */}
|
||||||
|
|
||||||
|
<span className="mx-4">/</span>
|
||||||
|
<span>{current.title}</span>
|
||||||
|
</div >
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function ExternalLinkIcon({ className = "" }) {
|
||||||
|
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="currentColor"><path d="M 40 10 C 38.896 10 38 10.896 38 12 C 38 13.104 38.896 14 40 14 L 47.171875 14 L 30.585938 30.585938 C 29.804938 31.366938 29.804938 32.633063 30.585938 33.414062 C 30.976938 33.805063 31.488 34 32 34 C 32.512 34 33.023063 33.805062 33.414062 33.414062 L 50 16.828125 L 50 24 C 50 25.104 50.896 26 52 26 C 53.104 26 54 25.104 54 24 L 54 12 C 54 10.896 53.104 10 52 10 L 40 10 z M 18 12 C 14.691 12 12 14.691 12 18 L 12 46 C 12 49.309 14.691 52 18 52 L 46 52 C 49.309 52 52 49.309 52 46 L 52 34 C 52 32.896 51.104 32 50 32 C 48.896 32 48 32.896 48 34 L 48 46 C 48 47.103 47.103 48 46 48 L 18 48 C 16.897 48 16 47.103 16 46 L 16 18 C 16 16.897 16.897 16 18 16 L 30 16 C 31.104 16 32 15.104 32 14 C 32 12.896 31.104 12 30 12 L 18 12 z"/></svg></div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function HomeIcon({ className = "" }) {
|
||||||
|
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M 12 2 A 1 1 0 0 0 11.289062 2.296875 L 1.203125 11.097656 A 0.5 0.5 0 0 0 1 11.5 A 0.5 0.5 0 0 0 1.5 12 L 4 12 L 4 20 C 4 20.552 4.448 21 5 21 L 9 21 C 9.552 21 10 20.552 10 20 L 10 14 L 14 14 L 14 20 C 14 20.552 14.448 21 15 21 L 19 21 C 19.552 21 20 20.552 20 20 L 20 12 L 22.5 12 A 0.5 0.5 0 0 0 23 11.5 A 0.5 0.5 0 0 0 22.796875 11.097656 L 12.716797 2.3027344 A 1 1 0 0 0 12.710938 2.296875 A 1 1 0 0 0 12 2 z"/></svg></div>
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
|
"react-timeago": "^7.1.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
},
|
},
|
||||||
@@ -4797,6 +4798,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-timeago": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-timeago/-/react-timeago-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-rouF7MiEm55fH791Y8cg+VobIJgx8gtNJ+gjr86R4ZqO1WKPkXiXjdT/lRzrvEkUzsxT1exHqV2V+Zdi114H3A==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
|
"react-timeago": "^7.1.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
},
|
},
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
import Head from 'next/head';
|
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
|
|
||||||
import { NextSeo } from 'next-seo';
|
import { NextSeo } from 'next-seo';
|
||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
@@ -8,15 +5,20 @@ import getConfig from 'next/config';
|
|||||||
import { getProject, GithubProject } from '../../../lib/octokit';
|
import { getProject, GithubProject } from '../../../lib/octokit';
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
import Link from 'next/link';
|
import Breadcrumbs from '../../../components/_shared/Breadcrumbs';
|
||||||
|
|
||||||
export default function ProjectPage({ project }) {
|
export default function ProjectPage({ project }) {
|
||||||
|
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NextSeo title={`PortalJS - @${project.repo_config.owner}/${project.repo_config.repo}${project.base_path !== '/' ? '/' + project.base_path : ''}`} />
|
<NextSeo title={`${repoId}${project.base_path !== '/' ? '/' + project.base_path : ''} - GitHub Datasets`} />
|
||||||
<main className="prose mx-auto my-8">
|
<main className="prose mx-auto my-8">
|
||||||
<Link href='/'>Back to homepage</Link>
|
<Breadcrumbs links={[{ title: repoId, href: "" }]} />
|
||||||
<h1 className="mb-0">Data</h1>
|
<h1 className="mb-0 mt-16">{project.repo_config.name || repoId}</h1>
|
||||||
|
<p className='mb-8'><span className='font-semibold'>Repository:</span> <a target="_blank" href={project.html_url}>{project.html_url}</a></p>
|
||||||
|
|
||||||
|
<h2 className="mb-0 mt-10">Files</h2>
|
||||||
<div className="inline-block min-w-full py-2 align-middle">
|
<div className="inline-block min-w-full py-2 align-middle">
|
||||||
<table className="min-w-full divide-y divide-gray-300">
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -50,7 +52,9 @@ export default function ProjectPage({ project }) {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1>Readme</h1>
|
<hr />
|
||||||
|
|
||||||
|
<h2 className='uppercase font-black'>Readme</h2>
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
{project.readmeContent}
|
{project.readmeContent}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
@@ -6,7 +6,7 @@ function CustomApp({ Component, pageProps }: AppProps) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head>
|
<Head>
|
||||||
<title>Welcome to simple-example!</title>
|
<title>GitHub Datasets</title>
|
||||||
</Head>
|
</Head>
|
||||||
<main className="app">
|
<main className="app">
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
@@ -2,6 +2,9 @@ import { promises as fs } from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getProject } from '../lib/octokit';
|
import { getProject } from '../lib/octokit';
|
||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
|
import ExternalLinkIcon from '../components/icons/ExternalLinkIcon';
|
||||||
|
import TimeAgo from 'react-timeago';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const jsonDirectory = path.join(
|
const jsonDirectory = path.join(
|
||||||
@@ -24,26 +27,18 @@ export async function getStaticProps() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
second: 'numeric',
|
|
||||||
timeZone: 'UTC',
|
|
||||||
});
|
|
||||||
|
|
||||||
export function Datasets({ projects }) {
|
export function Datasets({ projects }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white">
|
<div className="bg-white min-h-screen">
|
||||||
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
||||||
<h2 className="text-2xl font-bold leading-10 tracking-tight">
|
<div className='text-center'>
|
||||||
My Datasets
|
<h2 className="text-3xl font-bold leading-10 tracking-tight">
|
||||||
</h2>
|
GitHub Datasets
|
||||||
<p className="mt-6 max-w-2xl text-base leading-7 text-gray-600">
|
</h2>
|
||||||
Here is a list of all my datasets for easy access and sharing
|
<p className="mt-3 mx-auto max-w-2xl text-base leading-7 text-gray-500">
|
||||||
</p>
|
Data catalog with datasets hosted on GitHub by <Link target="_blank" className='underline' href="https://portaljs.org/">🌀 PortalJS</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="mt-20">
|
<div className="mt-20">
|
||||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<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">
|
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
@@ -60,7 +55,7 @@ export function Datasets({ projects }) {
|
|||||||
scope="col"
|
scope="col"
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
>
|
>
|
||||||
Repo
|
Repository
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
@@ -83,27 +78,28 @@ export function Datasets({ projects }) {
|
|||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<tr key={project.id}>
|
<tr key={project.id}>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
|
||||||
{project.repo_config.name
|
{project.repo_config.name
|
||||||
? project.repo_config.name
|
? project.repo_config.name
|
||||||
: project.full_name + (project.base_path === '/' ? '' : '/' + project.base_path)}
|
: project.full_name + (project.base_path === '/' ? '' : '/' + project.base_path)}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<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}>{project.full_name}</a>
|
<a href={project.html_url} target="_blank" className='flex items-center'>@{project.full_name} <ExternalLinkIcon className='ml-1' /></a>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4 text-sm text-gray-500">
|
<td className="px-3 py-4 text-sm text-gray-500">
|
||||||
{project.repo_config.description
|
{project.repo_config.description
|
||||||
? project.repo_config.description
|
? project.repo_config.description
|
||||||
: project.description}
|
: project.description}
|
||||||
</td>
|
</td>
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
|
||||||
{formatter.format(new Date(project.last_updated))}
|
<TimeAgo date={new Date(project.last_updated)} />
|
||||||
</td>
|
</td>
|
||||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
<td className="relative whitespace-nowrap py-6 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||||
<a
|
<a
|
||||||
href={`/@${project.repo_config.owner}/${project.repo_config.repo}/${project.base_path === '/' ? '' : project.base_path}`}
|
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'
|
||||||
>
|
>
|
||||||
More info
|
info
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
80
examples/github-backed-catalog/pages/styles.css
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
|
Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
|
||||||
|
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
line-height: 1.5;
|
||||||
|
tab-size: 4;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: currentColor;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
Liberation Mono, Courier New, monospace;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
vertical-align: middle;
|
||||||
|
shape-rendering: auto;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: rgba(55, 65, 81, 1);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: rgba(229, 231, 235, 1);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
Liberation Mono, Courier New, monospace;
|
||||||
|
overflow: scroll;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.rounded {
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 768px;
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
color: rgba(55, 65, 81, 1);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
1
examples/learn-example/README.md
Normal file
@@ -0,0 +1 @@
|
|||||||
|
PortalJS Learn Example - https://portaljs.org/docs
|
||||||
@@ -7,13 +7,12 @@ import { Mermaid } from '@flowershow/core';
|
|||||||
// to handle import statements. Instead, you must include components in scope
|
// to handle import statements. Instead, you must include components in scope
|
||||||
// here.
|
// here.
|
||||||
const components = {
|
const components = {
|
||||||
Table: dynamic(() => import('./Table')),
|
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
|
||||||
|
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.Catalog)),
|
||||||
mermaid: Mermaid,
|
mermaid: Mermaid,
|
||||||
// Excel: dynamic(() => import('../components/Excel')),
|
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
|
||||||
// TODO: try and make these dynamic ...
|
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
|
||||||
Vega: dynamic(() => import('./Vega')),
|
LineChart: dynamic(() => import('@portaljs/components').then(mod => mod.LineChart)),
|
||||||
VegaLite: dynamic(() => import('./VegaLite')),
|
|
||||||
LineChart: dynamic(() => import('./LineChart')),
|
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
export default function DRD({ source }: { source: any }) {
|
export default function DRD({ source }: { source: any }) {
|
||||||
@@ -4,5 +4,4 @@ Built with PortalJS
|
|||||||
|
|
||||||
## Table
|
## Table
|
||||||
|
|
||||||
<Table url="data_1.csv" />
|
<Table url="data.csv" />
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ import { serialize } from "next-mdx-remote/serialize";
|
|||||||
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
||||||
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
||||||
*/
|
*/
|
||||||
const parse = async function (source, format) {
|
const parse = async function (source, format, scope) {
|
||||||
const { content, data, excerpt } = matter(source, {
|
const { content, data, excerpt } = matter(source, {
|
||||||
excerpt: (file, options) => {
|
excerpt: (file, options) => {
|
||||||
// Generate an excerpt for the file
|
// Generate an excerpt for the file
|
||||||
@@ -91,7 +91,7 @@ const parse = async function (source, format) {
|
|||||||
],
|
],
|
||||||
format,
|
format,
|
||||||
},
|
},
|
||||||
scope: data,
|
scope,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
14
examples/learn-example/lib/mddb.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { MarkdownDB } from "@flowershow/markdowndb";
|
||||||
|
|
||||||
|
const dbPath = "markdown.db";
|
||||||
|
|
||||||
|
const client = new MarkdownDB({
|
||||||
|
client: "sqlite3",
|
||||||
|
connection: {
|
||||||
|
filename: dbPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientPromise = client.init();
|
||||||
|
|
||||||
|
export default clientPromise;
|
||||||
@@ -6,21 +6,22 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint",
|
||||||
|
"export": "npm run build && next export -o out",
|
||||||
|
"prebuild": "npm run mddb",
|
||||||
|
"mddb": "mddb ./content"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@flowershow/core": "^0.4.10",
|
"@flowershow/core": "^0.4.10",
|
||||||
|
"@flowershow/markdowndb": "^0.1.1",
|
||||||
"@flowershow/remark-callouts": "^1.0.0",
|
"@flowershow/remark-callouts": "^1.0.0",
|
||||||
"@flowershow/remark-embed": "^1.0.0",
|
"@flowershow/remark-embed": "^1.0.0",
|
||||||
"@flowershow/remark-wiki-link": "^1.1.2",
|
"@flowershow/remark-wiki-link": "^1.1.2",
|
||||||
"@heroicons/react": "^2.0.17",
|
"@heroicons/react": "^2.0.17",
|
||||||
"@opentelemetry/api": "^1.4.0",
|
"@opentelemetry/api": "^1.4.0",
|
||||||
|
"@portaljs/components": "^0.1.0",
|
||||||
"@tanstack/react-table": "^8.8.5",
|
"@tanstack/react-table": "^8.8.5",
|
||||||
"@types/node": "18.16.0",
|
"flexsearch": "0.7.21",
|
||||||
"@types/react": "18.2.0",
|
|
||||||
"@types/react-dom": "18.2.0",
|
|
||||||
"eslint": "8.39.0",
|
|
||||||
"eslint-config-next": "13.3.1",
|
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hastscript": "^7.2.0",
|
"hastscript": "^7.2.0",
|
||||||
"mdx-mermaid": "2.0.0-rc7",
|
"mdx-mermaid": "2.0.0-rc7",
|
||||||
@@ -29,6 +30,7 @@
|
|||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"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-vega": "^7.6.0",
|
"react-vega": "^7.6.0",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
"rehype-katex": "^6.0.3",
|
"rehype-katex": "^6.0.3",
|
||||||
@@ -42,7 +44,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@types/flexsearch": "^0.7.3",
|
||||||
|
"@types/node": "18.16.0",
|
||||||
|
"@types/react": "18.2.0",
|
||||||
|
"@types/react-dom": "18.2.0",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"eslint-config-next": "13.3.1",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"tailwindcss": "^3.3.1"
|
"tailwindcss": "^3.3.1"
|
||||||
}
|
}
|
||||||
126
examples/learn-example/pages/[[...path]].tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { existsSync, promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import parse from '../lib/markdown';
|
||||||
|
|
||||||
|
import DataRichDocument from '../components/DataRichDocument';
|
||||||
|
import clientPromise from '../lib/mddb';
|
||||||
|
|
||||||
|
export const getStaticPaths = async () => {
|
||||||
|
const contentDir = path.join(process.cwd(), '/content/');
|
||||||
|
const contentFolders = await fs.readdir(contentDir, 'utf8');
|
||||||
|
const paths = contentFolders.map((folder: string) =>
|
||||||
|
folder === 'index.md'
|
||||||
|
? { params: { path: [] } }
|
||||||
|
: { params: { path: [folder] } }
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
fallback: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStaticProps = async (context) => {
|
||||||
|
let pathToFile = 'index.md';
|
||||||
|
if (context.params.path) {
|
||||||
|
pathToFile = context.params.path.join('/') + '/index.md';
|
||||||
|
}
|
||||||
|
|
||||||
|
let datasets = [];
|
||||||
|
const mddbFileExists = existsSync('markdown.db');
|
||||||
|
if (mddbFileExists) {
|
||||||
|
const mddb = await clientPromise;
|
||||||
|
const datasetsFiles = await mddb.getFiles({
|
||||||
|
extensions: ['md', 'mdx'],
|
||||||
|
});
|
||||||
|
datasets = datasetsFiles
|
||||||
|
.filter((dataset) => dataset.url_path !== '/')
|
||||||
|
.map((dataset) => ({
|
||||||
|
_id: dataset._id,
|
||||||
|
url_path: dataset.url_path,
|
||||||
|
file_path: dataset.file_path,
|
||||||
|
metadata: dataset.metadata,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexFile = path.join(process.cwd(), '/content/' + pathToFile);
|
||||||
|
const readme = await fs.readFile(indexFile, 'utf8');
|
||||||
|
|
||||||
|
let { mdxSource, frontMatter } = await parse(readme, '.mdx', { datasets });
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
mdxSource,
|
||||||
|
frontMatter: JSON.stringify(frontMatter),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DatasetPage({ mdxSource, frontMatter }) {
|
||||||
|
frontMatter = JSON.parse(frontMatter);
|
||||||
|
return (
|
||||||
|
<div className="prose dark:prose-invert mx-auto py-8">
|
||||||
|
<header>
|
||||||
|
<div className="mb-6">
|
||||||
|
<>
|
||||||
|
<h1 className="mb-2">{frontMatter.title}</h1>
|
||||||
|
{frontMatter.author && (
|
||||||
|
<p className="my-0">
|
||||||
|
<span className="font-semibold">Author: </span>
|
||||||
|
<span className="my-0">{frontMatter.author}</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{frontMatter.description && (
|
||||||
|
<p className="my-0">
|
||||||
|
<span className="font-semibold">Description: </span>
|
||||||
|
<span className="description my-0">
|
||||||
|
{frontMatter.description}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{frontMatter.modified && (
|
||||||
|
<p className="my-0">
|
||||||
|
<span className="font-semibold">Modified: </span>
|
||||||
|
<span className="description my-0">
|
||||||
|
{new Date(frontMatter.modified).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{frontMatter.files && (
|
||||||
|
<section className="py-6">
|
||||||
|
<h2 className="mt-0">Data files</h2>
|
||||||
|
<table className="table-auto">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>File</th>
|
||||||
|
<th>Format</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{frontMatter.files.map((f) => {
|
||||||
|
const fileName = f.split('/').slice(-1);
|
||||||
|
return (
|
||||||
|
<tr key={`resources-list-${f}`}>
|
||||||
|
<td>
|
||||||
|
<a target="_blank" href={f}>
|
||||||
|
{fileName}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{fileName[0].split('.').slice(-1)[0].toUpperCase()}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
<DataRichDocument source={mdxSource} />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import '../styles/globals.css'
|
import '../styles/globals.css'
|
||||||
|
import '@portaljs/components/styles.css'
|
||||||
|
|
||||||
import type { AppProps } from 'next/app'
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
19
examples/learn-example/pages/_document.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import Document, { Html, Main, Head, NextScript } from 'next/document';
|
||||||
|
|
||||||
|
class MyDocument extends Document {
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head>
|
||||||
|
<link rel="icon" href="/favicon.png" />
|
||||||
|
</Head>
|
||||||
|
<body className='bg-white dark:bg-gray-900'>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyDocument;
|
||||||
6
examples/learn-example/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
6
examples/learn-example/public/data.csv
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
Year,Rating
|
||||||
|
2008,86
|
||||||
|
2009,96
|
||||||
|
2010,100
|
||||||
|
2011,100
|
||||||
|
2012,97
|
||||||
|
BIN
examples/learn-example/public/favicon.png
Normal file
|
After Width: | Height: | Size: 37 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es6",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
32
examples/openspending/.eslintrc.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"next",
|
||||||
|
"next/core-web-vitals"
|
||||||
|
],
|
||||||
|
"ignorePatterns": ["!**/*", ".next/**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
|
||||||
|
"rules": {
|
||||||
|
"@next/next/no-html-link-for-pages": [
|
||||||
|
"error",
|
||||||
|
"examples/simple-example/pages"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.ts", "*.tsx"],
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.js", "*.jsx"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@next/next/no-html-link-for-pages": "off"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"jest": true
|
||||||
|
}
|
||||||
|
}
|
||||||
35
examples/openspending/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
102
examples/openspending/README.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# A data catalog with data on GitHub
|
||||||
|
|
||||||
|
This example showcases a simple data catalog that get its data from a list of GitHub repos that serve as datasets.
|
||||||
|
|
||||||
|
A `datasets.json` file is used to specify which datasets are going to be part of the data catalog.
|
||||||
|
|
||||||
|
The application contains an index page, which lists all the datasets specified in the `datasets.json` file, and users can see more information about each dataset, such as the list of data files in it and the README, by clicking the "info" button on the list.
|
||||||
|
|
||||||
|
You can read more about it on the [Data catalog with data on GitHub](https://portaljs.org/docs/examples/github-backed-catalog) blog post.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
https://example.portaljs.org/
|
||||||
|
|
||||||
|
## Deploy your own
|
||||||
|
|
||||||
|
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fgithub-backed-catalog)
|
||||||
|
|
||||||
|
By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own GitHub/GitLab/Bitbucket account and automatically deploy everything.
|
||||||
|
|
||||||
|
## How to use
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
Execute `create-next-app` to bootstrap the example:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/github-backed-catalog
|
||||||
|
cd <app-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set environment variables
|
||||||
|
|
||||||
|
This project uses the GitHub API, which for anonymous users will cap at 50 requests per hour, so you might want to get a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and add it to a `.env` file inside the folder like so
|
||||||
|
|
||||||
|
```
|
||||||
|
GITHUB_PAT=<github token>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change datasets
|
||||||
|
|
||||||
|
You can change the datasets that will be displayed in the data catalog by editing the file `datasets.json`. Some examples can be found inside [this repo](https://github.com/datasets).
|
||||||
|
|
||||||
|
### Run in development mode
|
||||||
|
|
||||||
|
Run the app using:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open http://localhost:3000 from your browser. You should see something similar to this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
If click on the `info` button for a dataset you will see a page similar to this:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
### Structure of `datasets.json`
|
||||||
|
|
||||||
|
The `datasets.json` file is simply a list of datasets, below you can see a minimal example of a dataset:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"owner": "fivethirtyeight",
|
||||||
|
"repo": "data",
|
||||||
|
"branch": "master",
|
||||||
|
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
|
||||||
|
"readme": "nba-raptor/README.md"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It has:
|
||||||
|
|
||||||
|
- A `owner` which is going to be the github repo owner
|
||||||
|
- A `repo` which is going to be the github repo name
|
||||||
|
- A `branch` which is going to be the branch to which we need to get the files and the readme
|
||||||
|
- A list of `files` which is going to be a list of paths with files that you want to show to the world
|
||||||
|
- A `readme` which is going to be the path to your data description, it can also be a subpath eg: `example/README.md`
|
||||||
|
|
||||||
|
You can also add:
|
||||||
|
|
||||||
|
- A `description` which is useful if you have more than one dataset for each repo, if not provided we are just going to use the repo description
|
||||||
|
- A `Name` which is useful if you want to give your dataset a nice name, if not provided we are going to use the junction of the `owner` the `repo` + the path of the README, in the exaple above it will be `fivethirtyeight/data/nba-raptor`
|
||||||
|
|
||||||
|
### Extra commands
|
||||||
|
|
||||||
|
You can also build the project for production with:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
And run the production build with:
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start
|
||||||
|
```
|
||||||
|
|
||||||
45
examples/openspending/__tests__/os-data.test.ts
Normal 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})
|
||||||
|
|
||||||
15
examples/openspending/components/Button.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
export function Button({ href, className = '', ...props }) {
|
||||||
|
className = clsx(
|
||||||
|
'inline-flex justify-center rounded-2xl bg-emerald-600 p-4 text-base font-semibold text-white hover:bg-emerald-500 focus:outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-emerald-500 active:text-white/70',
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
return href ? (
|
||||||
|
<Link scroll={false} href={href} className={className} {...props} />
|
||||||
|
) : (
|
||||||
|
<button className={className} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
10
examples/openspending/components/Container.tsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import clsx from 'clsx'
|
||||||
|
|
||||||
|
export function Container({ className = "", ...props }) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx('mx-auto max-w-7xl px-4 sm:px-6 lg:px-8', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
76
examples/openspending/components/DatasetCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
examples/openspending/components/DatasetsGrid.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
163
examples/openspending/components/DatasetsSearch.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
53
examples/openspending/components/Header.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
import { Button } from './Button'
|
||||||
|
import { Container } from './Container'
|
||||||
|
import logo from "../public/logo.svg"
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const isActive = (navLink) => {
|
||||||
|
return router.asPath.split("?")[0] == navLink.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navLinks = [
|
||||||
|
{
|
||||||
|
title: "Home",
|
||||||
|
href: "/#header"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Datasets",
|
||||||
|
href: "/#datasets"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Community",
|
||||||
|
href: "https://community.openspending.org/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="z-50 pb-5 lg:pt-11 sticky top-0 backdrop-blur" id="header">
|
||||||
|
<Container className="flex flex-wrap items-center justify-center sm:justify-between lg:flex-nowrap">
|
||||||
|
<div className="mt-10 lg:mt-0 lg:grow lg:basis-0 flex items-center">
|
||||||
|
<Image src={logo} alt="OpenSpending" className="h-12 w-auto" />
|
||||||
|
</div>
|
||||||
|
<ul className='list-none flex gap-x-5 text-base font-medium'>
|
||||||
|
{navLinks.map((link, i) => (
|
||||||
|
<li key={`nav-link-${i}`}>
|
||||||
|
<Link
|
||||||
|
className={`text-emerald-900 hover:text-emerald-600 ${isActive(link) ? "text-emerald-600" : ""}`}
|
||||||
|
href={link.href}
|
||||||
|
scroll={false}
|
||||||
|
>
|
||||||
|
{link.title}
|
||||||
|
</Link>
|
||||||
|
</li>))}
|
||||||
|
</ul>
|
||||||
|
<div className="hidden sm:mt-10 sm:flex lg:mt-0 lg:grow lg:basis-0 lg:justify-end">
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</header >
|
||||||
|
)
|
||||||
|
}
|
||||||
47
examples/openspending/components/Hero.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Button } from './Button'
|
||||||
|
import { Container } from './Container'
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<div className="relative pb-20 pt-10 sm:py-40">
|
||||||
|
<div className="absolute inset-x-0 -bottom-14 -top-48 overflow-hidden bg-green-50 bg-opacity-50">
|
||||||
|
<div className="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-white" />
|
||||||
|
<div className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-white" />
|
||||||
|
</div>
|
||||||
|
<Container className="relative">
|
||||||
|
<div className="mx-auto max-w-2xl lg:max-w-4xl lg:px-12">
|
||||||
|
<h1 className="font-display text-5xl font-bold tracking-tighter text-emerald-600 sm:text-7xl">
|
||||||
|
It's our money!
|
||||||
|
</h1>
|
||||||
|
<div className="mt-6 space-y-6 font-display text-2xl tracking-tight text-emerald-900">
|
||||||
|
<p>
|
||||||
|
By understanding how governments spend money in our name can we have a say
|
||||||
|
in how that money will affect our own lives. The journey starts here.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
OpenSpending is a free, open and global platform to search, visualise and analyse
|
||||||
|
fiscal data in the public sphere.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button href="#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">
|
||||||
|
{[
|
||||||
|
['Countries', '75'],
|
||||||
|
['Datasets', '2091'],
|
||||||
|
['Files', '9230'],
|
||||||
|
].map(([name, value]) => (
|
||||||
|
<div key={name}>
|
||||||
|
<dt className="font-mono text-sm text-emerald-600">{name}</dt>
|
||||||
|
<dd className="mt-0.5 text-2xl font-semibold tracking-tight text-emerald-900">
|
||||||
|
{value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
0
examples/openspending/components/Table.tsx
Normal file
20
examples/openspending/components/_shared/Breadcrumbs.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import HomeIcon from "../icons/HomeIcon";
|
||||||
|
|
||||||
|
export default function Breadcrumbs({ links }: { links: { title: string, href?: string, target?: string }[] }) {
|
||||||
|
const current = links.at(-1);
|
||||||
|
|
||||||
|
return <div className="flex items-center uppercase font-black text-xs">
|
||||||
|
<Link className="flex items-center" href='/'><HomeIcon /></Link>
|
||||||
|
|
||||||
|
{/* {links.length > 1 && links.slice(0, -1).map((link) => {
|
||||||
|
return <>
|
||||||
|
<span className="mx-4">/</span>
|
||||||
|
<Link href={link.href}>{link.title}</Link>
|
||||||
|
</>
|
||||||
|
})} */}
|
||||||
|
|
||||||
|
<span className="mx-4">/</span>
|
||||||
|
<span>{current.title}</span>
|
||||||
|
</div >
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export default function ExternalLinkIcon({ className = "" }) {
|
||||||
|
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="currentColor"><path d="M 40 10 C 38.896 10 38 10.896 38 12 C 38 13.104 38.896 14 40 14 L 47.171875 14 L 30.585938 30.585938 C 29.804938 31.366938 29.804938 32.633063 30.585938 33.414062 C 30.976938 33.805063 31.488 34 32 34 C 32.512 34 33.023063 33.805062 33.414062 33.414062 L 50 16.828125 L 50 24 C 50 25.104 50.896 26 52 26 C 53.104 26 54 25.104 54 24 L 54 12 C 54 10.896 53.104 10 52 10 L 40 10 z M 18 12 C 14.691 12 12 14.691 12 18 L 12 46 C 12 49.309 14.691 52 18 52 L 46 52 C 49.309 52 52 49.309 52 46 L 52 34 C 52 32.896 51.104 32 50 32 C 48.896 32 48 32.896 48 34 L 48 46 C 48 47.103 47.103 48 46 48 L 18 48 C 16.897 48 16 47.103 16 46 L 16 18 C 16 16.897 16.897 16 18 16 L 30 16 C 31.104 16 32 15.104 32 14 C 32 12.896 31.104 12 30 12 L 18 12 z"/></svg></div>
|
||||||
|
}
|
||||||
3
examples/openspending/components/icons/HomeIcon.tsx
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default function HomeIcon({ className = "" }) {
|
||||||
|
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M 12 2 A 1 1 0 0 0 11.289062 2.296875 L 1.203125 11.097656 A 0.5 0.5 0 0 0 1 11.5 A 0.5 0.5 0 0 0 1.5 12 L 4 12 L 4 20 C 4 20.552 4.448 21 5 21 L 9 21 C 9.552 21 10 20.552 10 20 L 10 14 L 14 14 L 14 20 C 14 20.552 14.448 21 15 21 L 19 21 C 19.552 21 20 20.552 20 20 L 20 12 L 22.5 12 A 0.5 0.5 0 0 0 23 11.5 A 0.5 0.5 0 0 0 22.796875 11.097656 L 12.716797 2.3027344 A 1 1 0 0 0 12.710938 2.296875 A 1 1 0 0 0 12 2 z"/></svg></div>
|
||||||
|
}
|
||||||
27
examples/openspending/datasets.json
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"owner": "os-data",
|
||||||
|
"branch": "main",
|
||||||
|
"name": "mongolia-budget-2016-2017"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"owner": "os-data",
|
||||||
|
"branch": "main",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
6
examples/openspending/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
declare module '*.svg' {
|
||||||
|
const content: any;
|
||||||
|
export const ReactComponent: any;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
288
examples/openspending/lib/datapackage.interface.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
34
examples/openspending/lib/loader.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
192
examples/openspending/lib/octokit.ts
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
|
export interface GithubProject {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
branch: string;
|
||||||
|
files: string[];
|
||||||
|
readme: string;
|
||||||
|
description?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProjectReadme(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
readme: string,
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const response = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: readme,
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
const data = response.data as { content?: string };
|
||||||
|
const fileContent = data.content ? data.content : '';
|
||||||
|
if (fileContent === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const decodedContent = Buffer.from(fileContent, 'base64').toString();
|
||||||
|
return decodedContent;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLastUpdated(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
readme: string,
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const response = await octokit.rest.repos.listCommits({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
path: readme,
|
||||||
|
ref: branch,
|
||||||
|
});
|
||||||
|
return response.data[0].commit.committer.date;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export async function getProjectMetadata(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const response = await octokit.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRepoContents(
|
||||||
|
owner: string,
|
||||||
|
repo: string,
|
||||||
|
branch: string,
|
||||||
|
files: string[],
|
||||||
|
github_pat?: string
|
||||||
|
) {
|
||||||
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
|
try {
|
||||||
|
const contents = [];
|
||||||
|
for (const path of files) {
|
||||||
|
const response = await octokit.rest.repos.getContent({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
ref: branch,
|
||||||
|
path: path,
|
||||||
|
});
|
||||||
|
const data = response.data as {
|
||||||
|
download_url?: string;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
contents.push({
|
||||||
|
download_url: data.download_url,
|
||||||
|
name: data.name,
|
||||||
|
size: data.size,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return contents;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProject(project: GithubProject, github_pat?: string) {
|
||||||
|
const projectMetadata = await getProjectMetadata(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
if (!projectMetadata) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const projectReadme = await getProjectReadme(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
project.branch,
|
||||||
|
project.readme,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectData = await getRepoContents(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
project.branch,
|
||||||
|
project.files,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
if (!projectData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let projectBase = '',
|
||||||
|
last_updated = '';
|
||||||
|
if (projectReadme) {
|
||||||
|
projectBase =
|
||||||
|
project.readme.split('/').length > 1
|
||||||
|
? project.readme.split('/').slice(0, -1).join('/')
|
||||||
|
: '/';
|
||||||
|
last_updated = await getLastUpdated(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
project.branch,
|
||||||
|
projectBase,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...projectMetadata,
|
||||||
|
files: projectData,
|
||||||
|
readmeContent: projectReadme,
|
||||||
|
last_updated,
|
||||||
|
base_path: projectBase,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
examples/openspending/lib/project.interface.ts
Normal 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;
|
||||||
|
}
|
||||||
17
examples/openspending/next.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
const nextConfig = {
|
||||||
|
async rewrites() {
|
||||||
|
return {
|
||||||
|
beforeFiles: [
|
||||||
|
{
|
||||||
|
source: '/@:org/:project*',
|
||||||
|
destination: '/@org/:org/:project*',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
serverRuntimeConfig: {
|
||||||
|
github_pat: process.env.GITHUB_PAT ? process.env.GITHUB_PAT : null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
7361
examples/openspending/package-lock.json
generated
Normal file
42
examples/openspending/package.json
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"name": "my-app",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"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",
|
||||||
|
"typescript": "5.0.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"tailwindcss": "^3.3.1",
|
||||||
|
"vitest": "^0.31.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
126
examples/openspending/pages/@org/[org]/[...path].tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { NextSeo } from 'next-seo';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import { getProject, GithubProject } from '../../../lib/octokit';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import remarkGfm from 'remark-gfm';
|
||||||
|
import Breadcrumbs from '../../../components/_shared/Breadcrumbs';
|
||||||
|
|
||||||
|
export default function ProjectPage({ project }) {
|
||||||
|
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NextSeo title={`${repoId}${project.base_path !== '/' ? '/' + project.base_path : ''} - GitHub Datasets`} />
|
||||||
|
<main className="prose mx-auto my-8">
|
||||||
|
<Breadcrumbs links={[{ title: repoId, href: "" }]} />
|
||||||
|
<h1 className="mb-0 mt-16">{project.repo_config.name || repoId}</h1>
|
||||||
|
<p className='mb-8'><span className='font-semibold'>Repository:</span> <a target="_blank" href={project.html_url}>{project.html_url}</a></p>
|
||||||
|
|
||||||
|
<h2 className="mb-0 mt-10">Files</h2>
|
||||||
|
<div className="inline-block min-w-full py-2 align-middle">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
|
>
|
||||||
|
Size
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200">
|
||||||
|
{project.files?.map((file) => (
|
||||||
|
<tr key={file.download_url}>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
<a href={file.download_url}>{file.name}</a>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
|
||||||
|
{file.size} Bytes
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{project.readmeContent && <>
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h2 className='uppercase font-black'>Readme</h2>
|
||||||
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{project.readmeContent}
|
||||||
|
</ReactMarkdown>
|
||||||
|
</>}
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates `/posts/1` and `/posts/2`
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const jsonDirectory = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
'datasets.json'
|
||||||
|
);
|
||||||
|
const repos = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
|
|
||||||
|
return {
|
||||||
|
paths: JSON.parse(repos).map((repo) => {
|
||||||
|
const projectPath =
|
||||||
|
repo.readme && repo.readme.split('/').length > 1
|
||||||
|
? repo.readme.split('/').slice(0, -1)
|
||||||
|
: null;
|
||||||
|
let path = [repo.name];
|
||||||
|
if (projectPath) {
|
||||||
|
projectPath.forEach((element) => {
|
||||||
|
path.push(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
params: { org: repo.owner, path },
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
fallback: false, // can also be true or 'blocking'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getStaticProps({ params }) {
|
||||||
|
const jsonDirectory = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
'datasets.json'
|
||||||
|
);
|
||||||
|
const reposFile = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
|
const repos: GithubProject[] = JSON.parse(reposFile);
|
||||||
|
const repo = repos.find((_repo) => {
|
||||||
|
const projectPath =
|
||||||
|
_repo.readme && _repo.readme.split('/').length > 1
|
||||||
|
? _repo.readme.split('/').slice(0, -1)
|
||||||
|
: null;
|
||||||
|
let path = [_repo.name];
|
||||||
|
if (projectPath) {
|
||||||
|
projectPath.forEach((element) => {
|
||||||
|
path.push(element);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
_repo.owner == params.org &&
|
||||||
|
JSON.stringify(path) === JSON.stringify(params.path)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||||
|
const project = await getProject(repo, github_pat);
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
project: { ...project, repo_config: repo },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
18
examples/openspending/pages/_app.tsx
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { AppProps } from 'next/app';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
function CustomApp({ Component, pageProps }: AppProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>GitHub Datasets</title>
|
||||||
|
</Head>
|
||||||
|
<main className="app">
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomApp;
|
||||||
85
examples/openspending/pages/index.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import {
|
||||||
|
GithubProject,
|
||||||
|
getProjectDataPackage,
|
||||||
|
getProjectMetadata,
|
||||||
|
} from '../lib/octokit';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import ExternalLinkIcon from '../components/icons/ExternalLinkIcon';
|
||||||
|
import TimeAgo from 'react-timeago';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Hero } from '../components/Hero';
|
||||||
|
import { Header } from '../components/Header';
|
||||||
|
import { Container } from '../components/Container';
|
||||||
|
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 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
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
datapackage,
|
||||||
|
repo,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const projects = datapackages.map(
|
||||||
|
(item: { datapackage: FiscalDataPackage & { repo: string }; repo: any }) =>
|
||||||
|
loadDataPackage(item.datapackage, item.repo)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
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">
|
||||||
|
<Container>
|
||||||
|
<div className="mx-auto max-w-2xl lg:mx-0">
|
||||||
|
<h2
|
||||||
|
id="datasets"
|
||||||
|
className="font-display text-4xl font-medium tracking-tighter text-emerald-600 sm:text-5xl"
|
||||||
|
>
|
||||||
|
Datasets
|
||||||
|
</h2>
|
||||||
|
<p className="mt-4 font-display text-2xl tracking-tight text-emerald-900">
|
||||||
|
Find spending data about countries all around the world.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-10">
|
||||||
|
<DatasetsSearch datasets={projects} />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Datasets;
|
||||||
80
examples/openspending/pages/styles.css
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
html {
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
||||||
|
Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif,
|
||||||
|
Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
|
||||||
|
line-height: 1.5;
|
||||||
|
tab-size: 4;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: inherit;
|
||||||
|
line-height: inherit;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
p,
|
||||||
|
pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
*,
|
||||||
|
::before,
|
||||||
|
::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-width: 0;
|
||||||
|
border-style: solid;
|
||||||
|
border-color: currentColor;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2 {
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
Liberation Mono, Courier New, monospace;
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
display: block;
|
||||||
|
vertical-align: middle;
|
||||||
|
shape-rendering: auto;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background-color: rgba(55, 65, 81, 1);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
color: rgba(229, 231, 235, 1);
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
|
||||||
|
Liberation Mono, Courier New, monospace;
|
||||||
|
overflow: scroll;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow {
|
||||||
|
box-shadow: 0 0 #0000, 0 0 #0000, 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
.rounded {
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
max-width: 768px;
|
||||||
|
padding-bottom: 3rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
color: rgba(55, 65, 81, 1);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
6
examples/openspending/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||