Compare commits
417 Commits
19-markdow
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 47b4ffece8 | |||
|
|
816db6858c | ||
|
|
0381f2fccf | ||
|
|
62dbc35d3b | ||
|
|
12f0d0d732 | ||
|
|
d80d1f5012 | ||
|
|
af5b6b7a29 | ||
|
|
8487175f01 | ||
|
|
6551576700 | ||
|
|
4fccb2945f | ||
|
|
a9025e5cbe | ||
|
|
ad5a176e85 | ||
|
|
eeb480e8cf | ||
|
|
30fcb256b2 | ||
|
|
a4f8c0ed76 | ||
|
|
829f3b1f13 | ||
|
|
836b143a31 | ||
|
|
be38086794 | ||
|
|
63d9e3b754 | ||
|
|
f86f0541eb | ||
|
|
64bc212384 | ||
|
|
1e7daf353d | ||
|
|
cc69dabf80 | ||
|
|
a5d87712e0 | ||
|
|
86834fd1a6 | ||
|
|
8a661b1617 | ||
|
|
1baebc3f3c | ||
|
|
bbac4954f5 | ||
|
|
be6b184884 | ||
|
|
64103d6488 | ||
|
|
8e3496782c | ||
|
|
e034503399 | ||
|
|
93ae498ec2 | ||
|
|
97e43fdcba | ||
|
|
32f29024f8 | ||
|
|
134f72948c | ||
|
|
c1f2c526a8 | ||
|
|
8feb87739d | ||
|
|
3a07267e44 | ||
|
|
3f19ca16ed | ||
|
|
5deabac5fe | ||
|
|
96901150c6 | ||
|
|
9ff25ed7c4 | ||
|
|
8f884fceab | ||
|
|
7094eded50 | ||
|
|
30e7c6379f | ||
|
|
feada58932 | ||
|
|
31406d48e3 | ||
|
|
d6bf344ca3 | ||
|
|
d1a5138c6e | ||
|
|
a6047a9341 | ||
|
|
a4e60540ae | ||
|
|
e4c456c237 | ||
|
|
ce9ebbf41e | ||
|
|
a8fb176bcc | ||
|
|
2ac82367c5 | ||
|
|
85de6f7878 | ||
|
|
539fffeb55 | ||
|
|
0d276535bd | ||
|
|
38dd7103a3 | ||
|
|
48cd812a48 | ||
|
|
7bba10714d | ||
|
|
de2c1e5b48 | ||
|
|
57952e0817 | ||
|
|
df9664624f | ||
|
|
2ea185b710 | ||
|
|
b859d48f17 | ||
|
|
3d73ac422e | ||
|
|
059ffe4e34 | ||
|
|
0aed7dce77 | ||
|
|
c202d6cfc4 | ||
|
|
d9c20528c5 | ||
|
|
b7ee5a1869 | ||
|
|
4b5d549190 | ||
|
|
e6f0ab4ec8 | ||
|
|
22038fbd4f | ||
|
|
8b292a9bf2 | ||
|
|
cda3d335f1 | ||
|
|
fe97cc87f4 | ||
|
|
88f6199d18 | ||
|
|
852cf60abc | ||
|
|
704be0d5a7 | ||
|
|
fb3598fa49 | ||
|
|
d898b5a833 | ||
|
|
3aac4dabf9 | ||
|
|
a044f56e3c | ||
|
|
1b58c311eb | ||
|
|
ed9ac2c263 | ||
|
|
42c72e5afd | ||
|
|
9e1a324fa1 | ||
|
|
90178af8f2 | ||
|
|
00e61e104c | ||
|
|
f7f03fddca | ||
|
|
0891dfde2d | ||
|
|
c904e3731b | ||
|
|
86a2945ee6 | ||
|
|
09daa98b28 | ||
|
|
b511c9f71b | ||
|
|
464cda6db8 | ||
|
|
2bbf313489 | ||
|
|
c26b76368d | ||
|
|
af11f0cfd5 | ||
|
|
9ae2b31113 | ||
|
|
2bffd130c8 | ||
|
|
058d23678a | ||
|
|
540a08934c | ||
|
|
7d010cfee4 | ||
|
|
dd79da1c6b | ||
|
|
a58e2b81f7 | ||
|
|
6d7acd27ed | ||
|
|
7c30842c7d | ||
|
|
35ca1d6dfd | ||
|
|
a7e90b64af | ||
|
|
26dcffc279 | ||
|
|
d18e3dd486 | ||
|
|
8d7059acb4 | ||
|
|
09d5324d4e | ||
|
|
cf24042a91 | ||
|
|
2c45da679b | ||
|
|
0a476101e7 | ||
|
|
1343a7a6f7 | ||
|
|
27c99adde8 | ||
|
|
1a8e7ac06e | ||
|
|
4355efe0c4 | ||
|
|
96904aef0d | ||
|
|
92a549d6a9 | ||
|
|
1a5bbd4346 | ||
|
|
4985576183 | ||
|
|
7049917ef7 | ||
|
|
dd03a493be | ||
|
|
e5b0a85e48 | ||
|
|
a93b13f448 | ||
|
|
9e73410b17 | ||
|
|
8a4ec39d25 | ||
|
|
38bf06f031 | ||
|
|
8560f165fd | ||
|
|
b13e3ade3c | ||
|
|
1394f02038 | ||
|
|
e687779fa6 | ||
|
|
2ec143707d | ||
|
|
4ddfc1126a | ||
|
|
f23d7965f2 | ||
|
|
97e4775894 | ||
|
|
3c14ce8af7 | ||
|
|
61c750b7e1 | ||
|
|
b55ec5126c | ||
|
|
712f4a3b0f | ||
|
|
03960c8bac | ||
|
|
73c7eaf145 | ||
|
|
542f2ede9e | ||
|
|
f17c2ed1d0 | ||
|
|
f1d7e68077 | ||
|
|
1663b09a86 | ||
|
|
b940c82d93 | ||
|
|
492593dedb | ||
|
|
4ae22c7411 | ||
|
|
85bb6cb98c | ||
|
|
737f880036 | ||
|
|
1a9d64e0cf | ||
|
|
3366086d87 | ||
|
|
b12e725467 | ||
|
|
578a52a101 | ||
|
|
48a9243b21 | ||
|
|
6b3afa878b | ||
|
|
c9a39ec421 | ||
|
|
63ad514f9e | ||
|
|
e4624c35cb | ||
|
|
975aaed743 | ||
|
|
b8b6dd662d | ||
|
|
98db406793 | ||
|
|
9ea045d16a | ||
|
|
6acef2be56 | ||
|
|
19d40db62d | ||
|
|
c63551a54e | ||
|
|
c5e17810af | ||
|
|
c16970fbce | ||
|
|
93d35e3bcd | ||
|
|
8f4c134fd8 | ||
|
|
9482483b51 | ||
|
|
8d74fd9844 | ||
|
|
3ae685253b | ||
|
|
5f2f0653e9 | ||
|
|
56cb6e7912 | ||
|
|
71716ab018 | ||
|
|
06d39779ce | ||
|
|
aec67de35c | ||
|
|
68fbf2cda6 | ||
|
|
83fd7727ba | ||
|
|
083d3178cd | ||
|
|
3200dc5ade | ||
|
|
32dce434eb | ||
|
|
37ef29d9a2 | ||
|
|
98d62532c5 | ||
|
|
50122cd0cb | ||
|
|
0156e72dd3 | ||
|
|
91217f3256 | ||
|
|
11f9253709 | ||
|
|
c09c78b015 | ||
|
|
4a1ccd2f8d | ||
|
|
728d5b1465 | ||
|
|
a43d4a3b86 | ||
|
|
4bc7ce5ce7 | ||
|
|
8c5c6a2112 | ||
|
|
8e896138c6 | ||
|
|
b2b4fbdf12 | ||
|
|
099f3c5204 | ||
|
|
17ad9558e1 | ||
|
|
88ccee6f0a | ||
|
|
6418dbb7e2 | ||
|
|
84cc6cf82b | ||
|
|
df395e2b70 | ||
|
|
ea5dade346 | ||
|
|
8027026399 | ||
|
|
af7812f689 | ||
|
|
6a36e65b27 | ||
|
|
38aa62fcef | ||
|
|
ed9b575b4e | ||
|
|
3efba6578d | ||
|
|
8327f4efc0 | ||
|
|
d6a12e3111 | ||
|
|
9fc834c16d | ||
|
|
1a7371f9c5 | ||
|
|
c5ae365a20 | ||
|
|
30f7de04c7 | ||
|
|
989d0987c6 | ||
|
|
e1014025f0 | ||
|
|
7fc69b7ce8 | ||
|
|
d88a23c922 | ||
|
|
d367deaea3 | ||
|
|
3e9eadcc69 | ||
|
|
da226ef205 | ||
|
|
a37a31f89a | ||
|
|
03c27df800 | ||
|
|
d198130038 | ||
|
|
06209877ea | ||
|
|
822a3ce5ec | ||
|
|
1f06c67d13 | ||
|
|
9dea140859 | ||
|
|
d5899b22ab | ||
|
|
dc895ed277 | ||
|
|
7315df8a86 | ||
|
|
349f5bea66 | ||
|
|
6aef860a81 | ||
|
|
e908cb9344 | ||
|
|
1a22e54d5b | ||
|
|
172b4b71d4 | ||
|
|
3873852567 | ||
|
|
5e349855a2 | ||
|
|
40bd9e0311 | ||
|
|
b437b58d06 | ||
|
|
c3137ba1cb | ||
|
|
2e13c1b738 | ||
|
|
122870a23e | ||
|
|
4e282e0d86 | ||
|
|
6020f76adb | ||
|
|
f3c2a2ffa7 | ||
|
|
11659a838b | ||
|
|
58b7b4e753 | ||
|
|
7cf8c31e53 | ||
|
|
df000b9e8f | ||
|
|
77e9f58899 | ||
|
|
0737aaafb2 | ||
|
|
d798f402f6 | ||
|
|
80c6221a05 | ||
|
|
f04b86dda4 | ||
|
|
0fd3ee9912 | ||
|
|
cb0b9b1f14 | ||
|
|
9ee4376abf | ||
|
|
5a0ddd91ce | ||
|
|
d097bc765b | ||
|
|
b283fc1e99 | ||
|
|
0511e00d83 | ||
|
|
c8afa775d4 | ||
|
|
7ba9b5157e | ||
|
|
6c2a1ea125 | ||
|
|
343faf72cf | ||
|
|
1eb3f7367b | ||
|
|
8cdf54397f | ||
|
|
fb94cb9ce9 | ||
|
|
4595cd2231 | ||
|
|
aa2c8aac04 | ||
|
|
f2e5459297 | ||
|
|
e111adfe73 | ||
|
|
492c21ca4e | ||
|
|
0581357df8 | ||
|
|
15ceeec035 | ||
|
|
1caabcf6b4 | ||
|
|
b548dfd113 | ||
|
|
c5ee257d48 | ||
|
|
8d83f3a900 | ||
|
|
add2f6d0f3 | ||
|
|
dfab6aa318 | ||
|
|
f96fb562fe | ||
|
|
c4bf5bd054 | ||
|
|
c706575ae4 | ||
|
|
ed8de380a9 | ||
|
|
33521916d6 | ||
|
|
04206457a4 | ||
|
|
8a5acb7012 | ||
|
|
32493a2014 | ||
|
|
b34220cac7 | ||
|
|
44b37e27d9 | ||
|
|
6d3e571151 | ||
|
|
0e997f71e5 | ||
|
|
4a41d517ee | ||
|
|
c79b69ffe6 | ||
|
|
2ad6551a44 | ||
|
|
5de9888c02 | ||
|
|
5a517d714a | ||
|
|
746c77de11 | ||
|
|
9e256b9bf1 | ||
|
|
4bfcd4373b | ||
|
|
6649f78459 | ||
|
|
6f0da8c3a3 | ||
|
|
5b1238cc27 | ||
|
|
17803f1f5d | ||
|
|
81f50bb9a2 | ||
|
|
f1aee6a93e | ||
|
|
053005d784 | ||
|
|
2f5dd4d0f7 | ||
|
|
fb7ce8723a | ||
|
|
7636c3d26c | ||
|
|
6bf6c8faf4 | ||
|
|
095eba606e | ||
|
|
1097b5077d | ||
|
|
aa365cbb0d | ||
|
|
038427874a | ||
|
|
bdfdb2e6a5 | ||
|
|
95b3fc03d3 | ||
|
|
6aeadd71de | ||
|
|
affca05058 | ||
|
|
f54d238795 | ||
|
|
e82e2ae021 | ||
|
|
c3246ee7f8 | ||
|
|
40d80d2282 | ||
|
|
e0e720338f | ||
|
|
4f8b1b1e96 | ||
|
|
362afcc133 | ||
|
|
c165b3cc44 | ||
|
|
261a2a081e | ||
|
|
d27857f490 | ||
|
|
b3ba263bd8 | ||
|
|
cb774d0ad0 | ||
|
|
b48f71ecef | ||
|
|
07b3235647 | ||
|
|
d0c2ee1e71 | ||
|
|
bc180189cb | ||
|
|
39c862627d | ||
|
|
b7158a5be6 | ||
|
|
ee87c4f623 | ||
|
|
4141af0e82 | ||
|
|
7d36d22671 | ||
|
|
eab2d65113 | ||
|
|
51d0a7692e | ||
|
|
cdd90ac384 | ||
|
|
dcf6400304 | ||
|
|
247b2412d6 | ||
|
|
1ad9b85e02 | ||
|
|
af134cac8b | ||
|
|
0b8c56bcac | ||
|
|
20c64222c1 | ||
|
|
683159da02 | ||
|
|
c0681fdc7f | ||
|
|
ec1910e016 | ||
|
|
fc70f6ec66 | ||
|
|
8e4428e2f8 | ||
|
|
1292350aac | ||
|
|
e4da3ed672 | ||
|
|
959fe5a588 | ||
|
|
7a46a6732b | ||
|
|
688db3e6a6 | ||
|
|
fa642d8914 | ||
|
|
c80b283201 | ||
|
|
95fd8e72df | ||
|
|
e50c76090c | ||
|
|
70012d7c03 | ||
|
|
024c06d9cd | ||
|
|
35668c069e | ||
|
|
2500779499 | ||
|
|
fc8eb95dbc | ||
|
|
45089419da | ||
|
|
837a2d3d7a | ||
|
|
37e36539ec | ||
|
|
14974edcbf | ||
|
|
cb7d801968 | ||
|
|
8fd9f00cfa | ||
|
|
7696f43ff9 | ||
|
|
c4f447668a | ||
|
|
b9cf1bad44 | ||
|
|
a3788c7d19 | ||
|
|
3a7d166c59 | ||
|
|
68fa745066 | ||
|
|
cc50ba6223 | ||
|
|
3b9147bdbe | ||
|
|
9af90ec906 | ||
|
|
efa8732e55 | ||
|
|
360af236e8 | ||
|
|
f5541b5098 | ||
|
|
0f1d44ea61 | ||
|
|
de4c666f80 | ||
|
|
b1845dd2c9 | ||
|
|
7849873582 | ||
|
|
58b4c1918f | ||
|
|
a2dd2dfbff | ||
|
|
f22d8dc80c | ||
|
|
4dffc7aaa6 | ||
|
|
920146352d | ||
|
|
eeb40c8689 | ||
|
|
699401238e | ||
|
|
92ebac4a50 | ||
|
|
5a6cf37c9e | ||
|
|
b8f0a9e432 | ||
|
|
c82bfdd847 | ||
|
|
eac0a22aa8 | ||
|
|
91c76c213c | ||
|
|
622428a015 | ||
|
|
bedc9a8d33 |
8
.changeset/README.md
Normal file
8
.changeset/README.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Changesets
|
||||||
|
|
||||||
|
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
|
||||||
|
with multi-package repos, or single-package repos to help you version and publish your code. You can
|
||||||
|
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
|
||||||
|
|
||||||
|
We have a quick list of common questions to get you started engaging with this project in
|
||||||
|
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
|
||||||
14
.changeset/config.json
Normal file
14
.changeset/config.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json",
|
||||||
|
"changelog": [
|
||||||
|
"@changesets/changelog-github",
|
||||||
|
{ "repo": "datopian/portaljs" }
|
||||||
|
],
|
||||||
|
"commit": false,
|
||||||
|
"fixed": [],
|
||||||
|
"linked": [],
|
||||||
|
"access": "restricted",
|
||||||
|
"baseBranch": "main",
|
||||||
|
"updateInternalDependencies": "patch",
|
||||||
|
"ignore": []
|
||||||
|
}
|
||||||
39
.github/workflows/release.yml
vendored
Normal file
39
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency: release-${{ github.ref }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
name: Release
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16.x
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Create Release Pull Request or Publish to npm
|
||||||
|
id: changesets
|
||||||
|
uses: changesets/action@v1
|
||||||
|
with:
|
||||||
|
publish: npm run release
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
||||||
|
# - name: Send a Discord notification if a publish happens
|
||||||
|
# if: steps.changesets.outputs.published == 'true'
|
||||||
|
# uses: Ilshidur/action-discord@0.3.2
|
||||||
|
# with:
|
||||||
|
# args: 'The project {{ EVENT_PAYLOAD.repository.full_name }} has been deployed.'
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -4,6 +4,7 @@
|
|||||||
dist
|
dist
|
||||||
tmp
|
tmp
|
||||||
/out-tsc
|
/out-tsc
|
||||||
|
**/*.tgz
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|||||||
8
.vscode/extensions.json
vendored
8
.vscode/extensions.json
vendored
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"nrwl.angular-console",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"firsttris.vscode-jest-runner",
|
|
||||||
"dbaeumer.vscode-eslint"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -4,7 +4,7 @@ title: Developer docs for contributors
|
|||||||
|
|
||||||
## Our repository
|
## Our repository
|
||||||
|
|
||||||
https://github.com/datopian/portaljs
|
https://github.com/datopian/datahub
|
||||||
|
|
||||||
Structure:
|
Structure:
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ Structure:
|
|||||||
|
|
||||||
## How to contribute
|
## How to contribute
|
||||||
|
|
||||||
You can start by checking our [issues board](https://github.com/datopian/portaljs/issues).
|
You can start by checking our [issues board](https://github.com/datopian/datahub/issues).
|
||||||
|
|
||||||
If you'd like to work on one of the issues you can:
|
If you'd like to work on one of the issues you can:
|
||||||
|
|
||||||
@ -26,15 +26,16 @@ If you'd like to work on one of the issues you can:
|
|||||||
3. Clone the forked repository to your machine.
|
3. Clone the forked repository to your machine.
|
||||||
4. Create a feature branch (e.g. `50-update-readme`, where `50` is the number of the related issue).
|
4. Create a feature branch (e.g. `50-update-readme`, where `50` is the number of the related issue).
|
||||||
5. Commit your changes to the feature branch.
|
5. Commit your changes to the feature branch.
|
||||||
6. Push the feature branch to your forked repository.
|
6. Add changeset file describing the changes. (See section below)
|
||||||
7. Create a Pull Request against the original repository.
|
7. Push the feature branch to your forked repository.
|
||||||
|
8. Create a Pull Request against the original repository.
|
||||||
- add a short description of the changes included in the PR
|
- add a short description of the changes included in the PR
|
||||||
8. Address review comments if requested by our demanding reviewers 😜.
|
9. Address review comments if requested by our demanding reviewers 😜.
|
||||||
|
|
||||||
If you have an idea for improvement, and it doesn't have a corresponding issue yet, simply submit a new one.
|
If you have an idea for improvement, and it doesn't have a corresponding issue yet, simply submit a new one.
|
||||||
|
|
||||||
> [!note]
|
> [!note]
|
||||||
> Join our [Discord channel](https://discord.gg/rTxfCutu) do discuss existing issues and to ask for help.
|
> Join our [Discord channel](https://discord.gg/KZSf3FG4EZ) do discuss existing issues and to ask for help.
|
||||||
|
|
||||||
## Nx
|
## Nx
|
||||||
|
|
||||||
@ -62,6 +63,7 @@ or you can use just:
|
|||||||
nx <target> <project>
|
nx <target> <project>
|
||||||
# e.g. npx nx serve ckan
|
# e.g. npx nx serve ckan
|
||||||
```
|
```
|
||||||
|
|
||||||
if you have the `nx` binary installed globally in your machine
|
if you have the `nx` binary installed globally in your machine
|
||||||
|
|
||||||
#### Running multiple tasks
|
#### Running multiple tasks
|
||||||
@ -174,3 +176,23 @@ To learn more see this [offical docs page](https://nx.dev/reference/nx-json).
|
|||||||
Each project also has it's own configuration file - `project.json`, where you can define and configure it's targets (and more).
|
Each project also has it's own configuration file - `project.json`, where you can define and configure it's targets (and more).
|
||||||
|
|
||||||
To learn more see this [offical docs page](https://nx.dev/reference/project-configuration).
|
To learn more see this [offical docs page](https://nx.dev/reference/project-configuration).
|
||||||
|
|
||||||
|
## Changesets and publishing packages
|
||||||
|
|
||||||
|
> This monorepo is set up with changesets versioning tool. See their [github repository](https://github.com/changesets/changesets) to learn more.
|
||||||
|
|
||||||
|
### What are Changesets?
|
||||||
|
|
||||||
|
Changesets are files that describe the intention of a contributor to bump a version of the package according to their changes. Changeset file holds two key bits of information: a version type (following semver), and change information to be added to a changelog.
|
||||||
|
|
||||||
|
### Adding changesets
|
||||||
|
|
||||||
|
In the root directory of the repo, run:
|
||||||
|
|
||||||
|
```
|
||||||
|
npx changeset
|
||||||
|
```
|
||||||
|
|
||||||
|
Select the package that has been changed, the semver version that should be bumped with it and a description of your changes. Please make sure to add the most accurate but also concise information.
|
||||||
|
|
||||||
|
To learn about semantic versioning standards see [this semver doc page](https://semver.org/).
|
||||||
|
|||||||
76
README.md
76
README.md
@ -1,31 +1,51 @@
|
|||||||
<h1 align="center">
|
<p align="center">
|
||||||
🌀 Portal.JS
|
Bugs, issues and suggestions re PortalJS framework
|
||||||
<br />
|
<br />
|
||||||
Rapidly build rich data portals using a modern frontend framework
|
<br /><a href="https://discord.gg/xfFDMPU9dC"><img src="https://dcbadge.vercel.app/api/server/xfFDMPU9dC" /></a>
|
||||||
</h1>
|
</p>
|
||||||
|
|
||||||
* [What is Portal.JS ?](#What-is-Portal.JS)
|
## PortalJS framework
|
||||||
* [Features](#Features)
|
|
||||||
* [For developers](#For-developers)
|
|
||||||
* [Docs](#Docs)
|
|
||||||
* [Community](#Community)
|
|
||||||
* [Appendix](#Appendix)
|
|
||||||
* [What happened to Recline?](#What-happened-to-Recline?)
|
|
||||||
|
|
||||||
# What is Portal.JS
|
This repo and issue tracker are for
|
||||||
|
|
||||||
🌀 Portal.JS is a framework for rapidly building rich data portal frontends using a modern frontend approach. Portal.JS can be used to present a single dataset or build a full-scale data catalog/portal.
|
- PortalJS 🌀 - https://www.portaljs.com/
|
||||||
|
- DataHub Cloud ☁️ - https://datahub.io/
|
||||||
|
|
||||||
Built in JavaScript and React on top of the popular [Next.js](https://nextjs.com/) framework. Portal.JS assumes a "decoupled" approach where the frontend is a separate service from the backend and interacts with backend(s) via an API. It can be used with any backend and has out of the box support for [CKAN](https://ckan.org/).
|
### Issues
|
||||||
|
|
||||||
## Features
|
Found a bug: 👉 https://github.com/datopian/portaljs/issues/new
|
||||||
|
|
||||||
|
### Discussions
|
||||||
|
|
||||||
|
Got a suggestion, a question, want some support or just want to shoot the breeze 🙂
|
||||||
|
|
||||||
|
Head to the discussion forum: 👉 https://github.com/datopian/portaljs/discussions
|
||||||
|
|
||||||
|
### Chat on Discord
|
||||||
|
|
||||||
|
If you would prefer to get help via live chat check out our discord 👉
|
||||||
|
|
||||||
|
[Discord](https://discord.gg/xfFDMPU9dC)
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
|
||||||
|
- For PortalJS go to https://www.portaljs.com/opensource
|
||||||
|
- For DataHub Cloud – https://datahub.io/docs
|
||||||
|
|
||||||
|
## PortalJS Cloud 🌀
|
||||||
|
|
||||||
|
PortalJS Cloud 🌀 is a platform for rapidly creating rich data portal and publishing systems using a modern frontend approach. PortalJS Cloud can be used to publish a single dataset or build a full-scale data catalog/portal.
|
||||||
|
|
||||||
|
PortalJS Cloud is built in JavaScript and React on top of the popular [Next.js](https://nextjs.org) framework. PortalJS Cloud assumes a "decoupled" approach where the frontend is a separate service from the backend and interacts with backend(s) via an API. It can be used with any backend and has out of the box support for [CKAN](https://ckan.org/), GitHub, Frictionless Data Packages and more.
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
- 🗺️ Unified sites: present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. Wordpress) with a common internal API.
|
- 🗺️ Unified sites: present data and content in one seamless site, pulling datasets from a DMS (e.g. CKAN) and content from a CMS (e.g. Wordpress) with a common internal API.
|
||||||
- 👩💻 Developer friendly: built with familiar frontend tech (JavaScript, React, Next.js).
|
- 👩💻 Developer friendly: built with familiar frontend tech (JavaScript, React, Next.js).
|
||||||
- 🔋 Batteries included: full set of portal components out of the box e.g. catalog search, dataset showcase, blog, etc.
|
- 🔋 Batteries included: full set of portal components out of the box e.g. catalog search, dataset showcase, blog, etc.
|
||||||
- 🎨 Easy to theme and customize: installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.
|
- 🎨 Easy to theme and customize: installable themes, use standard CSS and React+CSS tooling. Add new routes quickly.
|
||||||
- 🧱 Extensible: quickly extend and develop/import your own React components
|
- 🧱 Extensible: quickly extend and develop/import your own React components
|
||||||
- 📝 Well documented: full set of documentation plus the documentation of Next.js and Apollo.
|
- 📝 Well documented: full set of documentation plus the documentation of Next.js.
|
||||||
|
|
||||||
### For developers
|
### For developers
|
||||||
|
|
||||||
@ -33,25 +53,3 @@ Built in JavaScript and React on top of the popular [Next.js](https://nextjs.com
|
|||||||
- 🚀 Next.js framework: so everything in Next.js for free: Server Side Rendering, Static Site Generation, huge number of examples and integrations, etc.
|
- 🚀 Next.js framework: so everything in Next.js for free: Server Side Rendering, Static Site Generation, huge number of examples and integrations, etc.
|
||||||
- Server Side Rendering (SSR) => Unlimited number of pages, SEO and more whilst still using React.
|
- Server Side Rendering (SSR) => Unlimited number of pages, SEO and more whilst still using React.
|
||||||
- Static Site Generation (SSG) => Ultra-simple deployment, great performance, great lighthouse scores and more (good for small sites)
|
- Static Site Generation (SSG) => Ultra-simple deployment, great performance, great lighthouse scores and more (good for small sites)
|
||||||
|
|
||||||
#### **Check out the [Portal.JS website](https://portaljs.org/) for a gallery of live portals**
|
|
||||||
|
|
||||||
___
|
|
||||||
|
|
||||||
# Docs
|
|
||||||
|
|
||||||
Access the Portal.JS documentation at:
|
|
||||||
|
|
||||||
https://portaljs.org/docs
|
|
||||||
|
|
||||||
- [Examples](https://portaljs.org/docs#examples)
|
|
||||||
|
|
||||||
# Community
|
|
||||||
|
|
||||||
If you have questions about anything related to Portal.JS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portal.js/discussions) or on our [Discord server](https://discord.gg/EeyfGrGu4U).
|
|
||||||
|
|
||||||
# Appendix
|
|
||||||
|
|
||||||
## What happened to Recline?
|
|
||||||
|
|
||||||
Portal.JS used to be Recline(JS). If you are looking for the old Recline codebase it still exists: see the [`recline` branch](https://github.com/datopian/portal.js/tree/recline). If you want context for the rename see [this issue](https://github.com/datopian/portal.js/issues/520).
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
**🚩 UPDATE April 2023: This example is now deprecated - though still works!. Please use the [new CKAN examples](https://github.com/datopian/portaljs/tree/main/examples)**
|
**🚩 UPDATE April 2023: This example is now deprecated - though still works!. Please use the [new CKAN examples](https://github.com/datopian/portaljs/tree/main/examples)**
|
||||||
|
|
||||||
This example shows how you can build a full data portal using a CKAN Backend with a Next.JS Frontend powered by Apollo, a full fledged guide is available as a [blog post](https://portaljs.org/blog/example-ckan-2021)
|
This example shows how you can build a full data portal using a CKAN Backend with a Next.JS Frontend powered by Apollo, a full fledged guide is available as a [blog post](https://portaljs.com/blog/example-ckan-2021)
|
||||||
|
|
||||||
## Developers
|
## Developers
|
||||||
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
DMS=https://demo.dev.datopian.com
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
This is a repo intended to serve as an example of a data catalog that get its data from a CKAN Instance.
|
This is a repo intended to serve as an example of a data catalog that get its data from a CKAN Instance.
|
||||||
|
|
||||||
```
|
```
|
||||||
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/ckan-example
|
npx create-next-app <app-name> --example https://github.com/datopian/datahub/tree/main/examples/ckan-ssg
|
||||||
cd <app-name>
|
cd <app-name>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -19,7 +19,7 @@ npm run dev
|
|||||||
|
|
||||||
Congratulations, you now have something similar to this running on `http://localhost:4200`
|
Congratulations, you now have something similar to this running on `http://localhost:4200`
|
||||||

|

|
||||||
If yo go to any one of those pages by clicking on `More info` you will see something similar to this
|
If you go to any one of those pages by clicking on `More info` you will see something similar to this
|
||||||

|

|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
File diff suppressed because it is too large
Load Diff
@ -10,6 +10,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.0.17",
|
"@heroicons/react": "^2.0.17",
|
||||||
|
"@portaljs/ckan": "^0.0.2",
|
||||||
|
"@portaljs/remark-wiki-link": "^1.0.4",
|
||||||
"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",
|
||||||
@ -20,14 +22,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"autoprefixer": "^10.4.14",
|
|
||||||
"postcss": "^8.4.23",
|
|
||||||
"tailwindcss": "^3.3.1",
|
|
||||||
"eslint": "8.39.0",
|
|
||||||
"eslint-config-next": "13.3.1",
|
|
||||||
"typescript": "5.0.4",
|
|
||||||
"@types/node": "18.16.0",
|
"@types/node": "18.16.0",
|
||||||
"@types/react": "18.0.38",
|
"@types/react": "18.0.38",
|
||||||
"@types/react-dom": "18.0.11"
|
"@types/react-dom": "18.0.11",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"eslint": "8.39.0",
|
||||||
|
"eslint-config-next": "13.3.1",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"tailwindcss": "^3.3.1",
|
||||||
|
"typescript": "5.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11,8 +11,9 @@ import {
|
|||||||
ServerIcon,
|
ServerIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from '@heroicons/react/20/solid';
|
} from '@heroicons/react/20/solid';
|
||||||
|
import { CKAN } from '@portaljs/ckan';
|
||||||
|
|
||||||
const dms = getConfig().publicRuntimeConfig.DMS;
|
const backend_url = getConfig().publicRuntimeConfig.DMS;
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -25,14 +26,12 @@ const formatter = new Intl.DateTimeFormat('en-US', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
|
const ckan = new CKAN(backend_url)
|
||||||
const { dataset } = context.query;
|
const { dataset } = context.query;
|
||||||
const response = await fetch(
|
const _dataset = await ckan.getDatasetDetails(dataset as string)
|
||||||
`${dms}/api/3/action/package_show?id=${dataset}`
|
|
||||||
);
|
|
||||||
const _dataset = await response.json();
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
dataset: _dataset.result,
|
dataset: _dataset,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -1,7 +1,8 @@
|
|||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
|
import { CKAN } from '@portaljs/ckan';
|
||||||
|
|
||||||
const dms = getConfig().publicRuntimeConfig.DMS
|
const backend_url = getConfig().publicRuntimeConfig.DMS
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@ -15,12 +16,11 @@ const formatter = new Intl.DateTimeFormat('en-US', {
|
|||||||
|
|
||||||
|
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
const response = await fetch(`${dms}/api/3/action/package_search`)
|
const ckan = new CKAN(backend_url)
|
||||||
const datasets = await response.json()
|
const { datasets } = await ckan.packageSearch({ limit: 1000, offset: 0, groups:[], orgs: [], tags: []})
|
||||||
const datasetsWithDetails = await Promise.all(datasets.result.results.map(async (dataset) => {
|
const datasetsWithDetails = await Promise.all(datasets.map(async (dataset) => {
|
||||||
const response = await fetch(`${dms}/api/3/action/package_show?id=` + dataset.name)
|
const _dataset = await ckan.getDatasetDetails(dataset.name)
|
||||||
const json = await response.json()
|
return _dataset
|
||||||
return json.result
|
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -79,7 +79,7 @@ export function Index({ datasets }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{datasets.map((dataset) => (
|
{datasets.map((dataset) => (
|
||||||
<tr>
|
<tr key={dataset.name}>
|
||||||
<td className="px-3 py-4 text-sm text-gray-500">
|
<td className="px-3 py-4 text-sm text-gray-500">
|
||||||
{dataset.title}
|
{dataset.title}
|
||||||
</td>
|
</td>
|
||||||
3
examples/ckan/.eslintrc.json
Normal file
3
examples/ckan/.eslintrc.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
||||||
35
examples/ckan/.gitignore
vendored
Normal file
35
examples/ckan/.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
|
||||||
38
examples/ckan/README.md
Normal file
38
examples/ckan/README.md
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
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).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
||||||
|
|
||||||
|
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { MDXRemote } from 'next-mdx-remote';
|
import { MDXRemote } from 'next-mdx-remote';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { Mermaid } from '@flowershow/core';
|
import { Mermaid } from '@portaljs/core';
|
||||||
|
|
||||||
// Custom components/renderers to pass to MDX.
|
// Custom components/renderers to pass to MDX.
|
||||||
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
||||||
@ -9,6 +9,7 @@ import { Mermaid } from '@flowershow/core';
|
|||||||
const components = {
|
const components = {
|
||||||
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
|
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
|
||||||
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.Catalog)),
|
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.Catalog)),
|
||||||
|
FlatUiTable: dynamic(() => import('@portaljs/components').then(mod => mod.FlatUiTable)),
|
||||||
mermaid: Mermaid,
|
mermaid: Mermaid,
|
||||||
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
|
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
|
||||||
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
|
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
|
||||||
3
examples/ckan/content/test.md
Normal file
3
examples/ckan/content/test.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Test
|
||||||
|
|
||||||
|
Test Data Rich Stories
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import matter from "gray-matter";
|
import matter from "gray-matter";
|
||||||
import mdxmermaid from "mdx-mermaid";
|
import mdxmermaid from "mdx-mermaid";
|
||||||
import { h } from "hastscript";
|
import { h } from "hastscript";
|
||||||
import remarkCallouts from "@flowershow/remark-callouts";
|
import remarkCallouts from "@portaljs/remark-callouts";
|
||||||
import remarkEmbed from "@flowershow/remark-embed";
|
import remarkEmbed from "@portaljs/remark-embed";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import remarkMath from "remark-math";
|
import remarkMath from "remark-math";
|
||||||
import remarkSmartypants from "remark-smartypants";
|
import remarkSmartypants from "remark-smartypants";
|
||||||
import remarkToc from "remark-toc";
|
import remarkToc from "remark-toc";
|
||||||
import remarkWikiLink from "@flowershow/remark-wiki-link";
|
import remarkWikiLink from "@portaljs/remark-wiki-link";
|
||||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||||
import rehypeKatex from "rehype-katex";
|
import rehypeKatex from "rehype-katex";
|
||||||
import rehypeSlug from "rehype-slug";
|
import rehypeSlug from "rehype-slug";
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { MarkdownDB } from "@flowershow/markdowndb";
|
import { MarkdownDB } from "mddb";
|
||||||
|
|
||||||
const dbPath = "markdown.db";
|
const dbPath = "markdown.db";
|
||||||
|
|
||||||
11
examples/ckan/next.config.js
Normal file
11
examples/ckan/next.config.js
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
publicRuntimeConfig: {
|
||||||
|
DMS: process.env.DMS
|
||||||
|
? process.env.DMS.replace(/\/?$/, '')
|
||||||
|
: 'https://demo.dev.datopian.com/',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = nextConfig;
|
||||||
13799
examples/ckan/package-lock.json
generated
Normal file
13799
examples/ckan/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
48
examples/ckan/package.json
Normal file
48
examples/ckan/package.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"name": "ckan",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"prebuild": "npm run mddb",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint",
|
||||||
|
"mddb": "mddb ./content"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@githubocto/flat-ui": "^0.14.1",
|
||||||
|
"@heroicons/react": "^2.0.18",
|
||||||
|
"@portaljs/ckan": "^0.0.2",
|
||||||
|
"@portaljs/components": "0.1.6",
|
||||||
|
"@portaljs/core": "^1.0.5",
|
||||||
|
"@portaljs/remark-callouts": "^1.0.5",
|
||||||
|
"@portaljs/remark-embed": "^1.0.4",
|
||||||
|
"@portaljs/remark-wiki-link": "^1.0.4",
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@types/node": "20.2.3",
|
||||||
|
"@types/react": "18.2.6",
|
||||||
|
"@types/react-dom": "18.2.4",
|
||||||
|
"autoprefixer": "10.4.14",
|
||||||
|
"eslint": "8.41.0",
|
||||||
|
"eslint-config-next": "13.4.3",
|
||||||
|
"isomorphic-unfetch": "^4.0.2",
|
||||||
|
"mddb": "^0.1.9",
|
||||||
|
"next": "13.4.3",
|
||||||
|
"next-mdx-remote": "^4.4.1",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
|
"postcss": "8.4.23",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-query": "^3.39.3",
|
||||||
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
|
"rehype-katex": "^6.0.3",
|
||||||
|
"rehype-prism-plus": "^1.5.1",
|
||||||
|
"rehype-slug": "^5.1.0",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
|
"remark-smartypants": "^2.0.0",
|
||||||
|
"remark-toc": "^8.0.1",
|
||||||
|
"tailwindcss": "3.3.2",
|
||||||
|
"typescript": "5.0.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
179
examples/ckan/pages/[org]/[dataset]/index.tsx
Normal file
179
examples/ckan/pages/[org]/[dataset]/index.tsx
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
import Head from "next/head";
|
||||||
|
import { CKAN, Dataset } from "@portaljs/ckan";
|
||||||
|
import {
|
||||||
|
ChevronRightIcon,
|
||||||
|
HomeIcon,
|
||||||
|
PaperClipIcon,
|
||||||
|
} from "@heroicons/react/20/solid";
|
||||||
|
import Link from "next/link";
|
||||||
|
import getConfig from "next/config";
|
||||||
|
|
||||||
|
const backend_url = getConfig().publicRuntimeConfig.DMS
|
||||||
|
|
||||||
|
export const getServerSideProps = async (context: any) => {
|
||||||
|
try {
|
||||||
|
const datasetName = context.params?.dataset;
|
||||||
|
if (!datasetName) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const ckan = new CKAN(backend_url);
|
||||||
|
const dataset = await ckan.getDatasetDetails(datasetName as string);
|
||||||
|
if (!dataset) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
props: { dataset },
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DatasetPage({
|
||||||
|
dataset,
|
||||||
|
}: {
|
||||||
|
dataset: Dataset;
|
||||||
|
}): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{`${dataset.title || dataset.name} - Dataset`}</title>
|
||||||
|
<meta name="description" content="Generated by create next app" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
</Head>
|
||||||
|
<main className="flex min-h-screen flex-col items-center justify-between p-24 bg-zinc-900">
|
||||||
|
<div className="bg-white p-8 my-4 rounded-lg">
|
||||||
|
<nav className="flex px-4 py-8" aria-label="Breadcrumb">
|
||||||
|
<ol role="list" className="flex items-center space-x-4">
|
||||||
|
<li>
|
||||||
|
<div>
|
||||||
|
<Link href="/" className="text-gray-400 hover:text-gray-500">
|
||||||
|
<HomeIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span className="sr-only">Home</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<ChevronRightIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
|
||||||
|
aria-current={"page"}
|
||||||
|
>
|
||||||
|
{dataset.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
{dataset && (
|
||||||
|
<div>
|
||||||
|
<div className="px-4 sm:px-0">
|
||||||
|
<h3 className="text-base font-semibold leading-7 text-gray-900">
|
||||||
|
{dataset.title || dataset.name}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 max-w-2xl text-sm leading-6 text-gray-500">
|
||||||
|
Dataset details
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="mt-6 border-t border-gray-100">
|
||||||
|
<dl className="divide-y divide-gray-100">
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Title
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.title}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
{dataset.tags && dataset.tags.length > 0 && (
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Tags
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.tags.map((tag) => tag.display_name).join(", ")}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{dataset.tags && dataset.tags.length > 0 && (
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
URL
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.url}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
{dataset.notes && (
|
||||||
|
<>
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Description
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
||||||
|
{dataset.notes}
|
||||||
|
</dd>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt className="text-sm font-medium leading-6 text-gray-900">
|
||||||
|
Files
|
||||||
|
</dt>
|
||||||
|
<dd className="mt-2 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className="divide-y divide-gray-100 rounded-md border border-gray-200"
|
||||||
|
>
|
||||||
|
{dataset.resources.map((resource) => (
|
||||||
|
<li key={resource.id} className="flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6">
|
||||||
|
<div className="flex w-0 flex-1 items-center">
|
||||||
|
<PaperClipIcon
|
||||||
|
className="h-5 w-5 flex-shrink-0 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
<div className="ml-4 flex min-w-0 flex-1 gap-2">
|
||||||
|
<span className="truncate font-medium">
|
||||||
|
{resource.name || resource.id}
|
||||||
|
</span>
|
||||||
|
<span className="flex-shrink-0 text-gray-400">
|
||||||
|
{resource.size}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4 flex-shrink-0">
|
||||||
|
<a
|
||||||
|
href={resource.url}
|
||||||
|
className="font-medium hover:text-indigo-500 mr-4"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
examples/ckan/pages/_app.tsx
Normal file
7
examples/ckan/pages/_app.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import '@/styles/globals.css'
|
||||||
|
import '@portaljs/ckan/styles.css'
|
||||||
|
import type { AppProps } from 'next/app'
|
||||||
|
|
||||||
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
|
return <Component {...pageProps} />
|
||||||
|
}
|
||||||
13
examples/ckan/pages/_document.tsx
Normal file
13
examples/ckan/pages/_document.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
|
|
||||||
|
export default function Document() {
|
||||||
|
return (
|
||||||
|
<Html lang="en">
|
||||||
|
<Head />
|
||||||
|
<body>
|
||||||
|
<Main />
|
||||||
|
<NextScript />
|
||||||
|
</body>
|
||||||
|
</Html>
|
||||||
|
)
|
||||||
|
}
|
||||||
18
examples/ckan/pages/api/cors.ts
Normal file
18
examples/ckan/pages/api/cors.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import fetch from 'isomorphic-unfetch';
|
||||||
|
|
||||||
|
const Cors = async (req: any, res: any) => {
|
||||||
|
const { url } = req.query;
|
||||||
|
try {
|
||||||
|
const resProxy = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Range: 'bytes=0-5132288',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const data = await resProxy.text();
|
||||||
|
return res.status(200).send(data);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(400).send(error.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Cors;
|
||||||
64
examples/ckan/pages/index.tsx
Normal file
64
examples/ckan/pages/index.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
CKAN,
|
||||||
|
DatasetSearchForm,
|
||||||
|
ListOfDatasets,
|
||||||
|
PackageSearchOptions,
|
||||||
|
Organization,
|
||||||
|
Group,
|
||||||
|
} from '@portaljs/ckan';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const backend_url = getConfig().publicRuntimeConfig.DMS;
|
||||||
|
|
||||||
|
export async function getServerSideProps() {
|
||||||
|
const ckan = new CKAN(backend_url);
|
||||||
|
const groups = await ckan.getGroupsWithDetails();
|
||||||
|
const orgs = await ckan.getOrgsWithDetails();
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
groups,
|
||||||
|
orgs,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home({
|
||||||
|
orgs,
|
||||||
|
groups,
|
||||||
|
}: {
|
||||||
|
orgs: Organization[];
|
||||||
|
groups: Group[];
|
||||||
|
}) {
|
||||||
|
const ckan = new CKAN(backend_url);
|
||||||
|
const [options, setOptions] = useState<PackageSearchOptions>({
|
||||||
|
offset: 0,
|
||||||
|
limit: 5,
|
||||||
|
tags: [],
|
||||||
|
groups: [],
|
||||||
|
orgs: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<main className="py-12 bg-zinc-900">
|
||||||
|
<DatasetSearchForm
|
||||||
|
options={options}
|
||||||
|
setOptions={setOptions}
|
||||||
|
groups={groups}
|
||||||
|
orgs={orgs}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="bg-white p-8 mx-auto my-4 rounded-lg"
|
||||||
|
style={{ width: 'min(1100px, 95vw)' }}
|
||||||
|
>
|
||||||
|
<ListOfDatasets
|
||||||
|
options={options}
|
||||||
|
setOptions={setOptions}
|
||||||
|
ckan={ckan}
|
||||||
|
/>{' '}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
examples/ckan/pages/stories/[[...path]].tsx
Normal file
129
examples/ckan/pages/stories/[[...path]].tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
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';
|
||||||
|
import getConfig from 'next/config';
|
||||||
|
import { CKAN } from '@portaljs/ckan';
|
||||||
|
|
||||||
|
export const getStaticPaths = async () => {
|
||||||
|
const contentDir = path.join(process.cwd(), '/content/');
|
||||||
|
const contentFolders = await fs.readdir(contentDir, 'utf8');
|
||||||
|
const paths = contentFolders.map((folder: string) => ({
|
||||||
|
params: { path: [folder.split('.')[0]] },
|
||||||
|
}));
|
||||||
|
return {
|
||||||
|
paths,
|
||||||
|
fallback: false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const backend_url = getConfig().publicRuntimeConfig.DMS;
|
||||||
|
|
||||||
|
export const getStaticProps = async (context) => {
|
||||||
|
const mddb = await clientPromise;
|
||||||
|
const storyFile = await mddb.getFileByUrl(context.params.path);
|
||||||
|
const md = await fs.readFile(
|
||||||
|
`${process.cwd()}/${storyFile.file_path}`,
|
||||||
|
'utf8'
|
||||||
|
);
|
||||||
|
|
||||||
|
const ckan = new CKAN(backend_url);
|
||||||
|
const datasets = storyFile.metadata.datasets ? await Promise.all(
|
||||||
|
storyFile.metadata.datasets.map(
|
||||||
|
async (datasetName: string) => await ckan.getDatasetDetails(datasetName)
|
||||||
|
)
|
||||||
|
) : [];
|
||||||
|
const orgs = storyFile.metadata.orgs ? await Promise.all(
|
||||||
|
storyFile.metadata.orgs.map(
|
||||||
|
async (orgName: string) => await ckan.getOrgDetails(orgName)
|
||||||
|
)
|
||||||
|
) : [];
|
||||||
|
|
||||||
|
let { mdxSource, frontMatter } = await parse(md, '.mdx', { datasets, orgs });
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
mdxSource,
|
||||||
|
frontMatter: JSON.stringify(frontMatter),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DatasetPage({ mdxSource, frontMatter }) {
|
||||||
|
frontMatter = JSON.parse(frontMatter);
|
||||||
|
return (
|
||||||
|
<main className="flex min-h-screen flex-col justify-between p-16 bg-zinc-900">
|
||||||
|
<div className="bg-white p-8 my-4 rounded-lg">
|
||||||
|
<div className="prose 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>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
examples/ckan/public/favicon.ico
Normal file
BIN
examples/ckan/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
1
examples/ckan/public/next.svg
Normal file
1
examples/ckan/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 |
1
examples/ckan/public/vercel.svg
Normal file
1
examples/ckan/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 |
71
examples/ckan/styles/globals.css
Normal file
71
examples/ckan/styles/globals.css
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@import "@portaljs/remark-callouts/styles.css";
|
||||||
|
|
||||||
|
/* mathjax */
|
||||||
|
.math-inline > mjx-container > svg {
|
||||||
|
display: inline;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* smooth scrolling in modern browsers */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tooltip fade-out clip */
|
||||||
|
.tooltip-body::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 3.6rem; /* multiple of $line-height used on the tooltip body (defined in tooltipBodyStyle) */
|
||||||
|
height: 1.2rem; /* ($top + $height)/$line-height is the number of lines we want to clip tooltip text at*/
|
||||||
|
width: 10rem;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(255, 255, 255, 0),
|
||||||
|
rgba(255, 255, 255, 1) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h2, h3, h4, h5, h6):not(.blogitem-title) {
|
||||||
|
margin-left: -2rem !important;
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
scroll-margin-top: 4.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-link {
|
||||||
|
padding: 1px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin: auto 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #1e293b;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .heading-link {
|
||||||
|
/* border: 1px solid #ab2b65; */
|
||||||
|
/* background: none; */
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h2, h3, h4, h5, h6):not(.blogitem-title):hover .heading-link {
|
||||||
|
opacity: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-link svg {
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
.heading-link {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
examples/ckan/tailwind.config.js
Normal file
18
examples/ckan/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/ckan/tsconfig.json
Normal file
23
examples/ckan/tsconfig.json
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es5",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": false,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
This example creates a portal/showcase for a single dataset. The dataset should be a [Frictionless dataset (data package)][fd] i.e. there should be a `datapackage.json`.
|
This example creates a portal/showcase for a single dataset. The dataset should be a [Frictionless dataset (data package)][fd] i.e. there should be a `datapackage.json`.
|
||||||
|
|
||||||
[fd]: https://frictionlessdata.io/data-packages/
|
[fd]: https://specs.frictionlessdata.io/data-package/
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
|
# PortalJS Demo replicating the FiveThirtyEight data portal
|
||||||
|
|
||||||
|
## 👉 https://fivethirtyeight.portaljs.org 👈
|
||||||
|
|
||||||
|
Here's a blog post we wrote about it: https://www.datopian.com/blog/fivethirtyeight-replica
|
||||||
|
|
||||||
This is a replica of the awesome data.fivethirtyeight.com using PortalJS.
|
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:
|
You might be asking why we did that, there are three main reasons:
|
||||||
|
|||||||
99
examples/fivethirtyeight/components/Layout.tsx
Normal file
99
examples/fivethirtyeight/components/Layout.tsx
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { XMarkIcon } from '@heroicons/react/20/solid';
|
||||||
|
import { Transition } from '@headlessui/react';
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
const [isShowing, setShow] = useState(true);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Transition
|
||||||
|
show={isShowing}
|
||||||
|
enter="transition-opacity duration-75"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="transition-opacity duration-150"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-x-6 bg-[#3c3c3c] px-6 py-2.5 sm:px-3.5 sm:before:flex-1">
|
||||||
|
<p className="text-sm leading-6 text-white">
|
||||||
|
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#readme"
|
||||||
|
>
|
||||||
|
Read more here
|
||||||
|
</a>{' '}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-1 justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShow(false)}
|
||||||
|
className="-m-3 p-3 focus-visible:outline-offset-[-4px]"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Dismiss</span>
|
||||||
|
<XMarkIcon className="h-5 w-5 text-white" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
<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.com"
|
||||||
|
>
|
||||||
|
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.com"
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
{children}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,33 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"url": "https://github.com/fivethirtyeight/data/tree/master/polls",
|
|
||||||
"name": "polls",
|
|
||||||
"displayName": "<span class=\"lastword\">polls</span>",
|
|
||||||
"articles": [
|
|
||||||
{
|
|
||||||
"date": "2023-05-11T14:35:40.000Z",
|
|
||||||
"title": "Latest Polls",
|
|
||||||
"url": "https://projects.fivethirtyeight.com/polls/"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"files": [
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/president_primary_polls.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/president_primary_polls_historical.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/president_polls.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/president_polls_historical.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/senate_polls.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/senate_polls_historical.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/house_polls.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/house_polls_historical.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/governor_polls.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/governor_polls_historical.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/president_approval_polls.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/generic_ballot_polls.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/polls-page/data/generic_ballot_polls_historical.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/2020-primary-data/pres_primary_avgs_2020.csv",
|
|
||||||
"https://projects.fivethirtyeight.com/2020-general-data/presidential_poll_averages_2020.csv"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "https://github.com/fivethirtyeight/data/tree/master/congress-generic-ballot",
|
"url": "https://github.com/fivethirtyeight/data/tree/master/congress-generic-ballot",
|
||||||
"name": "congress-generic-ballot",
|
"name": "congress-generic-ballot",
|
||||||
@ -195,6 +166,35 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fivethirtyeight/data/tree/master/polls",
|
||||||
|
"name": "polls",
|
||||||
|
"displayName": "<span class=\"lastword\">polls</span>",
|
||||||
|
"articles": [
|
||||||
|
{
|
||||||
|
"date": "2023-05-11T14:35:40.000Z",
|
||||||
|
"title": "Latest Polls",
|
||||||
|
"url": "https://projects.fivethirtyeight.com/polls/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"files": [
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/president_primary_polls.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/president_primary_polls_historical.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/president_polls.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/president_polls_historical.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/senate_polls.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/senate_polls_historical.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/house_polls.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/house_polls_historical.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/governor_polls.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/governor_polls_historical.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/president_approval_polls.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/generic_ballot_polls.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/polls-page/data/generic_ballot_polls_historical.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/2020-primary-data/pres_primary_avgs_2020.csv",
|
||||||
|
"https://projects.fivethirtyeight.com/2020-general-data/presidential_poll_averages_2020.csv"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://github.com/fivethirtyeight/data/tree/master/nfl-elo",
|
"url": "https://github.com/fivethirtyeight/data/tree/master/nfl-elo",
|
||||||
"name": "nfl-elo",
|
"name": "nfl-elo",
|
||||||
@ -1169,18 +1169,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"url": "https://github.com/fivethirtyeight/data/tree/master/undefeated-boxers",
|
|
||||||
"name": "undefeated-boxers",
|
|
||||||
"displayName": "undefeated-<span class=\"lastword\">boxers</span>",
|
|
||||||
"articles": [
|
|
||||||
{
|
|
||||||
"date": "2017-08-18T18:47:32.000Z",
|
|
||||||
"title": "Mayweather Is Defined By The Zero Next To His Name",
|
|
||||||
"url": "https://fivethirtyeight.com/features/mayweather-is-defined-by-the-zero-next-to-his-name/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"url": "https://github.com/fivethirtyeight/data/tree/master/chess-transfers",
|
"url": "https://github.com/fivethirtyeight/data/tree/master/chess-transfers",
|
||||||
"name": "chess-transfers",
|
"name": "chess-transfers",
|
||||||
@ -2139,6 +2127,18 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fivethirtyeight/data/tree/master/undefeated-boxers",
|
||||||
|
"name": "undefeated-boxers",
|
||||||
|
"displayName": "undefeated-<span class=\"lastword\">boxers</span>",
|
||||||
|
"articles": [
|
||||||
|
{
|
||||||
|
"date": "2017-08-18T18:47:32.000Z",
|
||||||
|
"title": "Mayweather Is Defined By The Zero Next To His Name",
|
||||||
|
"url": "https://fivethirtyeight.com/features/mayweather-is-defined-by-the-zero-next-to-his-name/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"url": "https://github.com/fivethirtyeight/data/tree/master/march-madness-predictions",
|
"url": "https://github.com/fivethirtyeight/data/tree/master/march-madness-predictions",
|
||||||
"name": "march-madness-predictions",
|
"name": "march-madness-predictions",
|
||||||
|
|||||||
6984
examples/fivethirtyeight/package-lock.json
generated
6984
examples/fivethirtyeight/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -9,7 +9,11 @@
|
|||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@portaljs/components": "^0.1.0",
|
"@headlessui/react": "^1.7.14",
|
||||||
|
"@heroicons/react": "^2.0.18",
|
||||||
|
"@portaljs/components": "^0.1.8",
|
||||||
|
"@portaljs/core": "^1.0.5",
|
||||||
|
"@portaljs/remark-wiki-link": "^1.0.4",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@types/node": "20.1.1",
|
"@types/node": "20.1.1",
|
||||||
"@types/react": "18.2.6",
|
"@types/react": "18.2.6",
|
||||||
@ -26,12 +30,15 @@
|
|||||||
"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": "^14.0.3",
|
||||||
"remark-code-frontmatter": "^1.0.0",
|
"remark-code-frontmatter": "^1.0.0",
|
||||||
|
"remark-excerpt": "^1.0.0-beta.1",
|
||||||
"remark-extract-frontmatter": "^3.2.0",
|
"remark-extract-frontmatter": "^3.2.0",
|
||||||
"remark-frontmatter": "^4.0.1",
|
"remark-frontmatter": "^4.0.1",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"tailwindcss": "3.3.2",
|
"tailwindcss": "3.3.2",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
|
"to-vfile": "^7.2.4",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,49 @@
|
|||||||
import '@/styles/globals.css'
|
import '@/styles/globals.css';
|
||||||
import '@portaljs/components/styles.css'
|
import '@portaljs/components/styles.css';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { pageview } from '@portaljs/core';
|
||||||
|
import Script from 'next/script';
|
||||||
|
import Head from 'next/head';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
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) {
|
||||||
return <Component {...pageProps} />
|
const router = useRouter();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleRouteChange = (url: any) => {
|
||||||
|
pageview(url);
|
||||||
|
};
|
||||||
|
router.events.on('routeChangeComplete', handleRouteChange);
|
||||||
|
return () => {
|
||||||
|
router.events.off('routeChangeComplete', handleRouteChange);
|
||||||
|
};
|
||||||
|
}, [router.events]);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Head>
|
||||||
|
<link rel="shortcut icon" href="/squared_logo.png" />
|
||||||
|
</Head>
|
||||||
|
<Script
|
||||||
|
strategy="afterInteractive"
|
||||||
|
src="https://www.googletagmanager.com/gtag/js?id=G-3N9SXTC7GS"
|
||||||
|
/>
|
||||||
|
<Script
|
||||||
|
id="gtag-init"
|
||||||
|
strategy="afterInteractive"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag(){dataLayer.push(arguments);}
|
||||||
|
gtag('js', new Date());
|
||||||
|
gtag('config', 'G-3N9SXTC7GS', {
|
||||||
|
page_path: window.location.pathname,
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,78 +19,9 @@ export default function Document() {
|
|||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<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 />
|
<Main />
|
||||||
<NextScript />
|
|
||||||
</body>
|
</body>
|
||||||
|
<NextScript />
|
||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,10 +7,19 @@ import remarkGfm from 'remark-gfm';
|
|||||||
import extract from 'remark-extract-frontmatter';
|
import extract from 'remark-extract-frontmatter';
|
||||||
import { Dataset } from '..';
|
import { Dataset } from '..';
|
||||||
import { GetStaticProps } from 'next';
|
import { GetStaticProps } from 'next';
|
||||||
import { Table } from '@portaljs/components';
|
import { FlatUiTable } from '@portaljs/components';
|
||||||
import Breadcrumbs from '@/components/Breadcrumbs';
|
import Breadcrumbs from '@/components/Breadcrumbs';
|
||||||
import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
|
import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
|
||||||
import remarkFrontmatter from 'remark-frontmatter';
|
import remarkFrontmatter from 'remark-frontmatter';
|
||||||
|
import Layout from '@/components/Layout';
|
||||||
|
import { format } from 'timeago.js';
|
||||||
|
|
||||||
|
// Request a weekday along with a long date
|
||||||
|
const options = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
} as const;
|
||||||
|
|
||||||
export default function DatasetPage({
|
export default function DatasetPage({
|
||||||
dataset,
|
dataset,
|
||||||
@ -22,54 +31,28 @@ export default function DatasetPage({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NextSeo title={`${dataset.name} page`} />
|
<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">
|
<Layout>
|
||||||
|
<main className="max-w-5xl px-2 prose mx-auto my-8 pb-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 prose-a:no-underline">
|
||||||
<Breadcrumbs links={[{ title: dataset.name, href: '' }]} />
|
<Breadcrumbs links={[{ title: dataset.name, href: '' }]} />
|
||||||
<h1 className="uppercase mb-0 mt-16">{dataset.name}</h1>
|
<h1 className="uppercase mb-0 mt-16">{dataset.name}</h1>
|
||||||
<p className="mb-8">
|
<table className="w-full my-10 mb-8 hidden md:table">
|
||||||
<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">
|
<thead className="border-b-4 pb-2 border-zinc-900">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th className="uppercase text-left font-normal text-xs pb-3">
|
||||||
className="uppercase text-left font-light text-xs pb-3"
|
related content
|
||||||
scope="col"
|
</th>
|
||||||
>
|
<th className="uppercase text-left font-normal text-xs pb-3">
|
||||||
Name
|
last updated
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody>
|
||||||
{dataset.files?.map((file) => (
|
<DesktopItem key={dataset.name} dataset={dataset} />
|
||||||
<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>
|
</tbody>
|
||||||
</table>
|
</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 && (
|
{dataset.readme && (
|
||||||
<>
|
<>
|
||||||
<h2 className="uppercase font-black">Readme</h2>
|
|
||||||
{dataset.readme && (
|
{dataset.readme && (
|
||||||
<ReactMarkdown
|
<ReactMarkdown
|
||||||
remarkPlugins={[
|
remarkPlugins={[
|
||||||
@ -83,7 +66,111 @@ export default function DatasetPage({
|
|||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<th
|
||||||
|
className="uppercase text-left font-light text-xs pb-3"
|
||||||
|
scope="col"
|
||||||
|
>
|
||||||
|
Download
|
||||||
|
</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.split('/').slice(-1)}`}>
|
||||||
|
{file.split('/').slice(-1)}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap py-4 text-sm text-gray-500">
|
||||||
|
<a href={file}>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
className="w-8 h-8 text-blue-400 hover:text-blue-300 transition mt-1 ml-3"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{dataset.files && dataset.files.length > 0 && (
|
||||||
|
<>
|
||||||
|
<h2 className="mb-0 mt-8">Data Previews</h2>
|
||||||
|
{dataset.files?.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file}
|
||||||
|
id={file.split('/').slice(-1).join('')}
|
||||||
|
className="preview-table my-8"
|
||||||
|
>
|
||||||
|
<h3>{file.split('/').slice(-1)}</h3>
|
||||||
|
<FlatUiTable url={file} />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
</Layout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
|
<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 text-end">
|
||||||
|
{index === 0 && (
|
||||||
|
<a
|
||||||
|
className="ml-auto 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>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -101,6 +188,7 @@ export async function getStaticPaths() {
|
|||||||
fallback: false, // can also be true or 'blocking'
|
fallback: false, // can also be true or 'blocking'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// change href base check datahub-next
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||||
const datasetsFile = path.join(process.cwd(), 'datasets.json');
|
const datasetsFile = path.join(process.cwd(), 'datasets.json');
|
||||||
@ -110,15 +198,20 @@ export const getStaticProps: GetStaticProps = async ({ params }) => {
|
|||||||
(_dataset) => _dataset.name === params?.datasetName
|
(_dataset) => _dataset.name === params?.datasetName
|
||||||
);
|
);
|
||||||
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||||
const readmes = await Promise.all(['/README.md', '/readme.md', '/Readme.md'].map(async (readme) => await getProjectReadme(
|
const readmes = await Promise.all(
|
||||||
|
['/README.md', '/readme.md', '/Readme.md'].map(
|
||||||
|
async (readme) =>
|
||||||
|
await getProjectReadme(
|
||||||
'fivethirtyeight',
|
'fivethirtyeight',
|
||||||
'data',
|
'data',
|
||||||
'master',
|
'master',
|
||||||
dataset?.name + readme,
|
dataset?.name + readme,
|
||||||
github_pat
|
github_pat
|
||||||
)));
|
)
|
||||||
const readme = readmes.find(item => item !== null)
|
)
|
||||||
if (!readme) console.log('Readme not found for ' + dataset?.name)
|
);
|
||||||
|
const readme = readmes.find((item) => item !== null);
|
||||||
|
if (!readme) console.log('Readme not found for ' + dataset?.name);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
dataset: {
|
dataset: {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { format } from 'timeago.js';
|
|||||||
import { promises as fs } from 'fs';
|
import { promises as fs } from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { NextSeo } from 'next-seo';
|
import { NextSeo } from 'next-seo';
|
||||||
|
import Layout from '@/components/Layout';
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
@ -51,21 +52,12 @@ export function MobileItem({ dataset }: { dataset: Dataset }) {
|
|||||||
>
|
>
|
||||||
info
|
info
|
||||||
</a>
|
</a>
|
||||||
{/*
|
<a
|
||||||
<button>
|
className="ml-2 border border-[#3c3c3c] px-[25px] py-2.5 text-sm transition bg-[#3c3c3c] text-white hover:bg-zinc-900"
|
||||||
<svg
|
href={`/datasets/${dataset.name}`}
|
||||||
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
|
explore
|
||||||
fillRule="evenodd"
|
</a>
|
||||||
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -97,6 +89,16 @@ export function DesktopItem({ dataset }: { dataset: Dataset }) {
|
|||||||
? new Date(article.date).toLocaleString('en-US', options)
|
? new Date(article.date).toLocaleString('en-US', options)
|
||||||
: format(article.date)}
|
: format(article.date)}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{index === 0 && (
|
||||||
|
<a
|
||||||
|
className="ml-2 border border-[#3c3c3c] px-[25px] py-2.5 text-sm transition bg-[#3c3c3c] text-white hover:bg-zinc-900"
|
||||||
|
href={`/datasets/${dataset.name}`}
|
||||||
|
>
|
||||||
|
explore
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
<td className="py-8">
|
<td className="py-8">
|
||||||
{index === 0 && (
|
{index === 0 && (
|
||||||
<a
|
<a
|
||||||
@ -107,23 +109,6 @@ export function DesktopItem({ dataset }: { dataset: Dataset }) {
|
|||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</td>
|
</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>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
@ -143,6 +128,7 @@ export default function Home({ datasets }: { datasets: Dataset[] }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NextSeo title="FiveThirtyEight tribute by PortalJS" />
|
<NextSeo title="FiveThirtyEight tribute by PortalJS" />
|
||||||
|
<Layout>
|
||||||
<main
|
<main
|
||||||
className={`flex min-h-screen flex-col items-center max-w-5xl mx-auto pt-20 px-2.5 ${inter.className}`}
|
className={`flex min-h-screen flex-col items-center max-w-5xl mx-auto pt-20 px-2.5 ${inter.className}`}
|
||||||
>
|
>
|
||||||
@ -206,6 +192,7 @@ export default function Home({ datasets }: { datasets: Dataset[] }) {
|
|||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
|
</Layout>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,9 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
.preview-table > div {
|
.preview-table > div {
|
||||||
overflow-x: scroll;
|
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.prose h1 {
|
||||||
|
font-size: 1.5em !important;
|
||||||
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ A `datasets.json` file is used to specify which datasets are going to be part of
|
|||||||
|
|
||||||
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.
|
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.
|
You can read more about it on the [Data catalog with data on GitHub](https://portaljs.com/docs/examples/github-backed-catalog) blog post.
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { MDXRemote } from 'next-mdx-remote';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { Mermaid } from '@portaljs/core';
|
||||||
|
|
||||||
|
// Custom components/renderers to pass to MDX.
|
||||||
|
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
||||||
|
// to handle import statements. Instead, you must include components in scope
|
||||||
|
// here.
|
||||||
|
const components = {
|
||||||
|
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
|
||||||
|
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.Catalog)),
|
||||||
|
FlatUiTable: dynamic(() => import('@portaljs/components').then(mod => mod.FlatUiTable)),
|
||||||
|
mermaid: Mermaid,
|
||||||
|
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
|
||||||
|
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
|
||||||
|
LineChart: dynamic(() => import('@portaljs/components').then(mod => mod.LineChart)),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export default function DRD({ source }: { source: any }) {
|
||||||
|
return <MDXRemote {...source} components={components} />;
|
||||||
|
}
|
||||||
@ -15,6 +15,13 @@
|
|||||||
],
|
],
|
||||||
"readme": "README.md"
|
"readme": "README.md"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"owner": "luccasmmg",
|
||||||
|
"branch": "main",
|
||||||
|
"repo": "test-data-repo-1",
|
||||||
|
"files": ["data_1.csv", "data_2.csv"],
|
||||||
|
"readme": "README.md"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"owner": "datasets",
|
"owner": "datasets",
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
|
|||||||
105
examples/github-backed-catalog/lib/markdown.js
Normal file
105
examples/github-backed-catalog/lib/markdown.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import matter from "gray-matter";
|
||||||
|
import mdxmermaid from "mdx-mermaid";
|
||||||
|
import { h } from "hastscript";
|
||||||
|
import remarkCallouts from "@portaljs/remark-callouts";
|
||||||
|
import remarkEmbed from "@portaljs/remark-embed";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import remarkSmartypants from "remark-smartypants";
|
||||||
|
import remarkToc from "remark-toc";
|
||||||
|
import remarkWikiLink from "@portaljs/remark-wiki-link";
|
||||||
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import rehypeSlug from "rehype-slug";
|
||||||
|
import rehypePrismPlus from "rehype-prism-plus";
|
||||||
|
|
||||||
|
import { serialize } from "next-mdx-remote/serialize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a markdown or MDX file to an MDX source form + front matter data
|
||||||
|
*
|
||||||
|
* @source: the contents of a markdown or mdx file
|
||||||
|
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
||||||
|
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
||||||
|
*/
|
||||||
|
const parse = async function (source, format, scope) {
|
||||||
|
const { content, data, excerpt } = matter(source, {
|
||||||
|
excerpt: (file, options) => {
|
||||||
|
// Generate an excerpt for the file
|
||||||
|
file.excerpt = file.content.split("\n\n")[0];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mdxSource = await serialize(
|
||||||
|
{ value: content, path: format },
|
||||||
|
{
|
||||||
|
// Optionally pass remark/rehype plugins
|
||||||
|
mdxOptions: {
|
||||||
|
remarkPlugins: [
|
||||||
|
remarkEmbed,
|
||||||
|
remarkGfm,
|
||||||
|
[remarkSmartypants, { quotes: false, dashes: "oldschool" }],
|
||||||
|
remarkMath,
|
||||||
|
remarkCallouts,
|
||||||
|
remarkWikiLink,
|
||||||
|
[
|
||||||
|
remarkToc,
|
||||||
|
{
|
||||||
|
heading: "Table of contents",
|
||||||
|
tight: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[mdxmermaid, {}],
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
{
|
||||||
|
properties: { className: 'heading-link' },
|
||||||
|
test(element) {
|
||||||
|
return (
|
||||||
|
["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) &&
|
||||||
|
element.properties?.id !== "table-of-contents" &&
|
||||||
|
element.properties?.className !== "blockquote-heading"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
content() {
|
||||||
|
return [
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
xmlns: "http:www.w3.org/2000/svg",
|
||||||
|
fill: "#ab2b65",
|
||||||
|
viewBox: "0 0 20 20",
|
||||||
|
className: "w-5 h-5",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("path", {
|
||||||
|
fillRule: "evenodd",
|
||||||
|
clipRule: "evenodd",
|
||||||
|
d: "M9.493 2.853a.75.75 0 00-1.486-.205L7.545 6H4.198a.75.75 0 000 1.5h3.14l-.69 5H3.302a.75.75 0 000 1.5h3.14l-.435 3.148a.75.75 0 001.486.205L7.955 14h2.986l-.434 3.148a.75.75 0 001.486.205L12.456 14h3.346a.75.75 0 000-1.5h-3.14l.69-5h3.346a.75.75 0 000-1.5h-3.14l.435-3.147a.75.75 0 00-1.486-.205L12.045 6H9.059l.434-3.147zM8.852 7.5l-.69 5h2.986l.69-5H8.852z",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[rehypeKatex, { output: "mathml" }],
|
||||||
|
[rehypePrismPlus, { ignoreMissing: true }],
|
||||||
|
],
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
scope,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mdxSource: mdxSource,
|
||||||
|
frontMatter: data,
|
||||||
|
excerpt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parse;
|
||||||
14
examples/github-backed-catalog/lib/mddb.ts
Normal file
14
examples/github-backed-catalog/lib/mddb.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MarkdownDB } from "mddb";
|
||||||
|
|
||||||
|
const dbPath = "markdown.db";
|
||||||
|
|
||||||
|
const client = new MarkdownDB({
|
||||||
|
client: "sqlite3",
|
||||||
|
connection: {
|
||||||
|
filename: dbPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientPromise = client.init();
|
||||||
|
|
||||||
|
export default clientPromise;
|
||||||
@ -39,6 +39,32 @@ export async function getProjectReadme(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
return JSON.parse(decodedContent);
|
||||||
|
} catch (error) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getLastUpdated(
|
export async function getLastUpdated(
|
||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
@ -162,11 +188,20 @@ export async function getProject(project: GithubProject, github_pat?: string) {
|
|||||||
projectBase,
|
projectBase,
|
||||||
github_pat
|
github_pat
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const projectDatapackage = await getProjectDatapackage(
|
||||||
|
project.owner,
|
||||||
|
project.repo,
|
||||||
|
project.branch,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...projectMetadata,
|
...projectMetadata,
|
||||||
files: projectData,
|
files: projectData,
|
||||||
readmeContent: projectReadme,
|
readmeContent: projectReadme,
|
||||||
last_updated,
|
last_updated,
|
||||||
base_path: projectBase,
|
base_path: projectBase,
|
||||||
|
datapackage: projectDatapackage
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
7442
examples/github-backed-catalog/package-lock.json
generated
7442
examples/github-backed-catalog/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,19 +10,33 @@
|
|||||||
"prettier": "prettier --write ."
|
"prettier": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@portaljs/components": "^0.1.6",
|
||||||
|
"@portaljs/core": "^1.0.5",
|
||||||
|
"@portaljs/remark-callouts": "^1.0.5",
|
||||||
|
"@portaljs/remark-embed": "^1.0.4",
|
||||||
|
"@portaljs/remark-wiki-link": "^1.0.4",
|
||||||
"@types/node": "18.16.0",
|
"@types/node": "18.16.0",
|
||||||
"@types/react": "18.0.38",
|
"@types/react": "18.0.38",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"eslint": "8.39.0",
|
"eslint": "8.39.0",
|
||||||
"eslint-config-next": "13.3.1",
|
"eslint-config-next": "13.3.1",
|
||||||
"next": "13.3.1",
|
"mddb": "^0.1.9",
|
||||||
|
"next": "13.4.3",
|
||||||
|
"next-mdx-remote": "^4.4.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",
|
||||||
"react-timeago": "^7.1.0",
|
"react-timeago": "^7.1.0",
|
||||||
|
"rehype-autolink-headings": "^6.1.1",
|
||||||
|
"rehype-katex": "^6.0.3",
|
||||||
|
"rehype-prism-plus": "^1.5.1",
|
||||||
|
"rehype-slug": "^5.1.0",
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
|
"remark-math": "^5.1.1",
|
||||||
|
"remark-smartypants": "^2.0.0",
|
||||||
|
"remark-toc": "^8.0.1",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ 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 Breadcrumbs from "../../../components/_shared/Breadcrumbs";
|
import Breadcrumbs from "../../../components/_shared/Breadcrumbs";
|
||||||
|
import parse from '../../../lib/markdown';
|
||||||
|
import DataRichDocument from '../../../components/DataRichDocument'
|
||||||
|
|
||||||
export default function ProjectPage({ project }) {
|
export default function ProjectPage({ project }) {
|
||||||
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`;
|
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`;
|
||||||
@ -64,9 +66,7 @@ export default function ProjectPage({ project }) {
|
|||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h2 className="uppercase font-black">Readme</h2>
|
<h2 className="uppercase font-black">Readme</h2>
|
||||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
<DataRichDocument source={project.mdxSource} />
|
||||||
{project.readmeContent}
|
|
||||||
</ReactMarkdown>
|
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -119,9 +119,10 @@ export async function getStaticProps({ params }) {
|
|||||||
});
|
});
|
||||||
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||||
const project = await getProject(repo, github_pat);
|
const project = await getProject(repo, github_pat);
|
||||||
|
let { mdxSource, frontMatter } = await parse(project.readmeContent, '.mdx', { project });
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
project: { ...project, repo_config: repo },
|
project: { ...project, repo_config: repo, mdxSource },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,7 +40,7 @@ export function Datasets({ projects }) {
|
|||||||
<Link
|
<Link
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="underline"
|
className="underline"
|
||||||
href="https://portaljs.org/"
|
href="https://portaljs.com/"
|
||||||
>
|
>
|
||||||
🌀 PortalJS
|
🌀 PortalJS
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@ -78,3 +78,72 @@ pre {
|
|||||||
color: rgba(55, 65, 81, 1);
|
color: rgba(55, 65, 81, 1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@import "@portaljs/remark-callouts/styles.css";
|
||||||
|
|
||||||
|
/* mathjax */
|
||||||
|
.math-inline > mjx-container > svg {
|
||||||
|
display: inline;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* smooth scrolling in modern browsers */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tooltip fade-out clip */
|
||||||
|
.tooltip-body::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 3.6rem; /* multiple of $line-height used on the tooltip body (defined in tooltipBodyStyle) */
|
||||||
|
height: 1.2rem; /* ($top + $height)/$line-height is the number of lines we want to clip tooltip text at*/
|
||||||
|
width: 10rem;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(255, 255, 255, 0),
|
||||||
|
rgba(255, 255, 255, 1) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h2, h3, h4, h5, h6):not(.blogitem-title) {
|
||||||
|
margin-left: -2rem !important;
|
||||||
|
padding-left: 2rem !important;
|
||||||
|
scroll-margin-top: 4.5rem;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-link {
|
||||||
|
padding: 1px;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
margin: auto 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #1e293b;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.light .heading-link {
|
||||||
|
/* border: 1px solid #ab2b65; */
|
||||||
|
/* background: none; */
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(h2, h3, h4, h5, h6):not(.blogitem-title):hover .heading-link {
|
||||||
|
opacity: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heading-link svg {
|
||||||
|
transform: scale(0.75);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 640px) {
|
||||||
|
.heading-link {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
PortalJS Learn Example - https://portaljs.org/docs
|
|
||||||
1
examples/learn/README.md
Normal file
1
examples/learn/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
PortalJS Learn Example - https://portaljs.com/docs
|
||||||
21
examples/learn/components/DataRichDocument.tsx
Normal file
21
examples/learn/components/DataRichDocument.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { MDXRemote } from 'next-mdx-remote';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { Mermaid } from '@portaljs/core';
|
||||||
|
|
||||||
|
// Custom components/renderers to pass to MDX.
|
||||||
|
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
||||||
|
// to handle import statements. Instead, you must include components in scope
|
||||||
|
// here.
|
||||||
|
const components = {
|
||||||
|
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
|
||||||
|
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.Catalog)),
|
||||||
|
mermaid: Mermaid,
|
||||||
|
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
|
||||||
|
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
|
||||||
|
LineChart: dynamic(() => import('@portaljs/components').then(mod => mod.LineChart)),
|
||||||
|
FlatUiTable: dynamic(() => import('@portaljs/components').then(mod => mod.FlatUiTable)),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export default function DRD({ source }: { source: any }) {
|
||||||
|
return <MDXRemote {...source} components={components} />;
|
||||||
|
}
|
||||||
105
examples/learn/lib/markdown.js
Normal file
105
examples/learn/lib/markdown.js
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import matter from "gray-matter";
|
||||||
|
import mdxmermaid from "mdx-mermaid";
|
||||||
|
import { h } from "hastscript";
|
||||||
|
import remarkCallouts from "@portaljs/remark-callouts";
|
||||||
|
import remarkEmbed from "@portaljs/remark-embed";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
import remarkMath from "remark-math";
|
||||||
|
import remarkSmartypants from "remark-smartypants";
|
||||||
|
import remarkToc from "remark-toc";
|
||||||
|
import remarkWikiLink from "@portaljs/remark-wiki-link";
|
||||||
|
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||||
|
import rehypeKatex from "rehype-katex";
|
||||||
|
import rehypeSlug from "rehype-slug";
|
||||||
|
import rehypePrismPlus from "rehype-prism-plus";
|
||||||
|
|
||||||
|
import { serialize } from "next-mdx-remote/serialize";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a markdown or MDX file to an MDX source form + front matter data
|
||||||
|
*
|
||||||
|
* @source: the contents of a markdown or mdx file
|
||||||
|
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
||||||
|
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
||||||
|
*/
|
||||||
|
const parse = async function (source, format, scope) {
|
||||||
|
const { content, data, excerpt } = matter(source, {
|
||||||
|
excerpt: (file, options) => {
|
||||||
|
// Generate an excerpt for the file
|
||||||
|
file.excerpt = file.content.split("\n\n")[0];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mdxSource = await serialize(
|
||||||
|
{ value: content, path: format },
|
||||||
|
{
|
||||||
|
// Optionally pass remark/rehype plugins
|
||||||
|
mdxOptions: {
|
||||||
|
remarkPlugins: [
|
||||||
|
remarkEmbed,
|
||||||
|
remarkGfm,
|
||||||
|
[remarkSmartypants, { quotes: false, dashes: "oldschool" }],
|
||||||
|
remarkMath,
|
||||||
|
remarkCallouts,
|
||||||
|
remarkWikiLink,
|
||||||
|
[
|
||||||
|
remarkToc,
|
||||||
|
{
|
||||||
|
heading: "Table of contents",
|
||||||
|
tight: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[mdxmermaid, {}],
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
rehypeSlug,
|
||||||
|
[
|
||||||
|
rehypeAutolinkHeadings,
|
||||||
|
{
|
||||||
|
properties: { className: 'heading-link' },
|
||||||
|
test(element) {
|
||||||
|
return (
|
||||||
|
["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) &&
|
||||||
|
element.properties?.id !== "table-of-contents" &&
|
||||||
|
element.properties?.className !== "blockquote-heading"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
content() {
|
||||||
|
return [
|
||||||
|
h(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
xmlns: "http:www.w3.org/2000/svg",
|
||||||
|
fill: "#ab2b65",
|
||||||
|
viewBox: "0 0 20 20",
|
||||||
|
className: "w-5 h-5",
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("path", {
|
||||||
|
fillRule: "evenodd",
|
||||||
|
clipRule: "evenodd",
|
||||||
|
d: "M9.493 2.853a.75.75 0 00-1.486-.205L7.545 6H4.198a.75.75 0 000 1.5h3.14l-.69 5H3.302a.75.75 0 000 1.5h3.14l-.435 3.148a.75.75 0 001.486.205L7.955 14h2.986l-.434 3.148a.75.75 0 001.486.205L12.456 14h3.346a.75.75 0 000-1.5h-3.14l.69-5h3.346a.75.75 0 000-1.5h-3.14l.435-3.147a.75.75 0 00-1.486-.205L12.045 6H9.059l.434-3.147zM8.852 7.5l-.69 5h2.986l.69-5H8.852z",
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[rehypeKatex, { output: "mathml" }],
|
||||||
|
[rehypePrismPlus, { ignoreMissing: true }],
|
||||||
|
],
|
||||||
|
format,
|
||||||
|
},
|
||||||
|
scope,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
mdxSource: mdxSource,
|
||||||
|
frontMatter: data,
|
||||||
|
excerpt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default parse;
|
||||||
14
examples/learn/lib/mddb.ts
Normal file
14
examples/learn/lib/mddb.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { MarkdownDB } from "mddb";
|
||||||
|
|
||||||
|
const dbPath = "markdown.db";
|
||||||
|
|
||||||
|
const client = new MarkdownDB({
|
||||||
|
client: "sqlite3",
|
||||||
|
connection: {
|
||||||
|
filename: dbPath,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const clientPromise = client.init();
|
||||||
|
|
||||||
|
export default clientPromise;
|
||||||
5
examples/learn/next.config.js
Normal file
5
examples/learn/next.config.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const nextConfig = {
|
||||||
|
swcMinify: false
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
||||||
File diff suppressed because it is too large
Load Diff
@ -12,25 +12,27 @@
|
|||||||
"mddb": "mddb ./content"
|
"mddb": "mddb ./content"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@flowershow/core": "^0.4.10",
|
"@githubocto/flat-ui": "^0.14.1",
|
||||||
"@flowershow/markdowndb": "^0.1.1",
|
|
||||||
"@flowershow/remark-callouts": "^1.0.0",
|
|
||||||
"@flowershow/remark-embed": "^1.0.0",
|
|
||||||
"@flowershow/remark-wiki-link": "^1.1.2",
|
|
||||||
"@heroicons/react": "^2.0.17",
|
"@heroicons/react": "^2.0.17",
|
||||||
"@opentelemetry/api": "^1.4.0",
|
"@opentelemetry/api": "^1.4.0",
|
||||||
"@portaljs/components": "^0.1.0",
|
"@portaljs/components": "^0.1.8",
|
||||||
|
"@portaljs/core": "^1.0.5",
|
||||||
|
"@portaljs/remark-callouts": "^1.0.5",
|
||||||
|
"@portaljs/remark-embed": "^1.0.4",
|
||||||
|
"@portaljs/remark-wiki-link": "^1.0.4",
|
||||||
"@tanstack/react-table": "^8.8.5",
|
"@tanstack/react-table": "^8.8.5",
|
||||||
"flexsearch": "0.7.21",
|
"flexsearch": "0.7.21",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"hastscript": "^7.2.0",
|
"hastscript": "^7.2.0",
|
||||||
|
"mddb": "^0.1.9",
|
||||||
"mdx-mermaid": "2.0.0-rc7",
|
"mdx-mermaid": "2.0.0-rc7",
|
||||||
"next": "13.2.1",
|
"next": "13.2.1",
|
||||||
"next-mdx-remote": "^4.4.1",
|
"next-mdx-remote": "^4.4.1",
|
||||||
"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-hook-form": "^7.43.9",
|
||||||
|
"react-query": "^3.39.3",
|
||||||
"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",
|
||||||
@ -40,7 +42,9 @@
|
|||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"remark-smartypants": "^2.0.0",
|
"remark-smartypants": "^2.0.0",
|
||||||
"remark-toc": "^8.0.1",
|
"remark-toc": "^8.0.1",
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4",
|
||||||
|
"vega": "5.25.0",
|
||||||
|
"vega-lite": "5.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
@ -81,7 +81,7 @@ export default function DatasetPage({ mdxSource, frontMatter }) {
|
|||||||
<p className="my-0">
|
<p className="my-0">
|
||||||
<span className="font-semibold">Modified: </span>
|
<span className="font-semibold">Modified: </span>
|
||||||
<span className="description my-0">
|
<span className="description my-0">
|
||||||
{new Date(frontMatter.modified).toLocaleDateString()}
|
{new Date(frontMatter.modified).toLocaleDateString("en-US")}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
6
examples/learn/postcss.config.js
Normal file
6
examples/learn/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@ -1,7 +1,7 @@
|
|||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
@import "@flowershow/remark-callouts/styles.css";
|
@import "@portaljs/remark-callouts/styles.css";
|
||||||
|
|
||||||
.w-5 {
|
.w-5 {
|
||||||
width: 1.25rem
|
width: 1.25rem
|
||||||
@ -6,7 +6,7 @@ A `datasets.json` file is used to specify which datasets are going to be part of
|
|||||||
|
|
||||||
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.
|
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.
|
You can read more about it on the [Data catalog with data on GitHub](https://portaljs.com/docs/examples/github-backed-catalog) blog post.
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { expect, test } from 'vitest';
|
|||||||
import { getAllProjectsFromOrg, getProjectDataPackage } from '../lib/project';
|
import { getAllProjectsFromOrg, getProjectDataPackage } from '../lib/project';
|
||||||
import { loadDataPackage } from '../lib/loader';
|
import { loadDataPackage } from '../lib/loader';
|
||||||
import { getProjectMetadata } from '../lib/project';
|
import { getProjectMetadata } from '../lib/project';
|
||||||
import { getCsv, parseCsv } from '../components/Table';
|
import { validate } from 'datapackage';
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'Test OS-Data',
|
'Test OS-Data',
|
||||||
@ -12,8 +12,24 @@ test(
|
|||||||
'main',
|
'main',
|
||||||
process.env.VITE_GITHUB_PAT
|
process.env.VITE_GITHUB_PAT
|
||||||
);
|
);
|
||||||
if (repos.failed.length > 0) console.log(repos.failed);
|
if (repos.failed.length > 0)
|
||||||
expect(repos.failed.length).toBe(0);
|
console.log('Failed to get datapackage on', repos.failed);
|
||||||
|
let failedDatapackages = await Promise.all(
|
||||||
|
repos.results.map(async (item) => {
|
||||||
|
try {
|
||||||
|
const { valid, errors } = await validate(item.datapackage);
|
||||||
|
return errors.length > 0 ? item.repo.name : null;
|
||||||
|
} catch {
|
||||||
|
return item.repo.name;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
failedDatapackages = failedDatapackages.filter((item) => item !== null);
|
||||||
|
if (failedDatapackages.length > 0) {
|
||||||
|
console.log('Failed to validate datapackage on ', failedDatapackages);
|
||||||
|
} else {
|
||||||
|
console.log('No invalid packages');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ timeout: 100000 }
|
{ timeout: 100000 }
|
||||||
);
|
);
|
||||||
@ -27,7 +43,22 @@ test(
|
|||||||
process.env.VITE_GITHUB_PAT
|
process.env.VITE_GITHUB_PAT
|
||||||
);
|
);
|
||||||
if (repos.failed.length > 0) console.log(repos.failed);
|
if (repos.failed.length > 0) console.log(repos.failed);
|
||||||
expect(repos.failed.length).toBe(0);
|
let failedDatapackages = await Promise.all(
|
||||||
|
repos.results.map(async (item) => {
|
||||||
|
try {
|
||||||
|
const { valid, errors } = await validate(item.datapackage);
|
||||||
|
return errors.length > 0 ? item.repo.name : null;
|
||||||
|
} catch {
|
||||||
|
return item.repo.name;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
failedDatapackages = failedDatapackages.filter((item) => item !== null);
|
||||||
|
if (failedDatapackages.length > 0) {
|
||||||
|
console.log('Failed to validate datapackage on ', failedDatapackages);
|
||||||
|
} else {
|
||||||
|
console.log('No invalid packages');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{ timeout: 100000 }
|
{ timeout: 100000 }
|
||||||
);
|
);
|
||||||
@ -83,56 +114,3 @@ test(
|
|||||||
},
|
},
|
||||||
{ timeout: 100000 }
|
{ timeout: 100000 }
|
||||||
);
|
);
|
||||||
|
|
||||||
test(
|
|
||||||
'Test getting one section of csv from R2',
|
|
||||||
async () => {
|
|
||||||
const rawCsv = await getCsv(
|
|
||||||
'https://storage.openspending.org/state-of-minas-gerais-brazil-planned-budget/__os_imported__br-mg-ppagloc.csv'
|
|
||||||
);
|
|
||||||
const parsedCsv = await parseCsv(rawCsv);
|
|
||||||
expect(parsedCsv.errors.length).toBe(1);
|
|
||||||
expect(parsedCsv.data.length).toBe(10165);
|
|
||||||
expect(parsedCsv.meta.fields).toStrictEqual([
|
|
||||||
'function_name',
|
|
||||||
'function_label',
|
|
||||||
'product_name',
|
|
||||||
'product_label',
|
|
||||||
'area_name',
|
|
||||||
'area_label',
|
|
||||||
'subaction_name',
|
|
||||||
'subaction_label',
|
|
||||||
'region_label_map',
|
|
||||||
'region_reg_map',
|
|
||||||
'region_name',
|
|
||||||
'region_label',
|
|
||||||
'municipality_map_id',
|
|
||||||
'municipality_name',
|
|
||||||
'municipality_map_code',
|
|
||||||
'municipality_label',
|
|
||||||
'municipality_map_name_simple',
|
|
||||||
'municipality_map_name',
|
|
||||||
'cofog1_label_en',
|
|
||||||
'cofog1_name',
|
|
||||||
'cofog1_label',
|
|
||||||
'amount',
|
|
||||||
'subprogramme_name',
|
|
||||||
'subprogramme_label',
|
|
||||||
'time_name',
|
|
||||||
'time_year',
|
|
||||||
'time_month',
|
|
||||||
'time_day',
|
|
||||||
'time_week',
|
|
||||||
'time_yearmonth',
|
|
||||||
'time_quarter',
|
|
||||||
'time',
|
|
||||||
'action_name',
|
|
||||||
'action_label',
|
|
||||||
'subfunction_name',
|
|
||||||
'subfunction_label',
|
|
||||||
'programme_name',
|
|
||||||
'programme_label',
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
{ timeout: 100000 }
|
|
||||||
);
|
|
||||||
|
|||||||
29
examples/openspending/components/DataRichDocument.tsx
Normal file
29
examples/openspending/components/DataRichDocument.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { MDXRemote } from 'next-mdx-remote';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
import { Mermaid } from '@portaljs/core';
|
||||||
|
|
||||||
|
// Custom components/renderers to pass to MDX.
|
||||||
|
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
||||||
|
// to handle import statements. Instead, you must include components in scope
|
||||||
|
// here.
|
||||||
|
const components = {
|
||||||
|
Table: dynamic(() => import('@portaljs/components').then((mod) => mod.Table)),
|
||||||
|
Catalog: dynamic(() =>
|
||||||
|
import('@portaljs/components').then((mod) => mod.Catalog)
|
||||||
|
),
|
||||||
|
mermaid: Mermaid,
|
||||||
|
Vega: dynamic(() => import('@portaljs/components').then((mod) => mod.Vega)),
|
||||||
|
VegaLite: dynamic(() =>
|
||||||
|
import('@portaljs/components').then((mod) => mod.VegaLite)
|
||||||
|
),
|
||||||
|
LineChart: dynamic(() =>
|
||||||
|
import('@portaljs/components').then((mod) => mod.LineChart)
|
||||||
|
),
|
||||||
|
FlatUiTable: dynamic(() =>
|
||||||
|
import('@portaljs/components').then((mod) => mod.FlatUiTable)
|
||||||
|
),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
export default function DRD({ source }: { source: any }) {
|
||||||
|
return <MDXRemote {...source} components={components} />;
|
||||||
|
}
|
||||||
@ -45,12 +45,12 @@ export default function DatasetCard({ dataset }: { dataset: Project }) {
|
|||||||
<dt className="text-gray-500">Fiscal Period</dt>
|
<dt className="text-gray-500">Fiscal Period</dt>
|
||||||
<dd className="text-gray-700">
|
<dd className="text-gray-700">
|
||||||
{dataset.fiscalPeriod?.start &&
|
{dataset.fiscalPeriod?.start &&
|
||||||
new Date(dataset.fiscalPeriod.start).getFullYear()}
|
new Date(dataset.fiscalPeriod.start).getUTCFullYear()}
|
||||||
{dataset.fiscalPeriod?.end &&
|
{dataset.fiscalPeriod?.end &&
|
||||||
dataset.fiscalPeriod?.start !== dataset.fiscalPeriod?.end && (
|
dataset.fiscalPeriod?.start !== dataset.fiscalPeriod?.end && (
|
||||||
<>
|
<>
|
||||||
{' - '}
|
{' - '}
|
||||||
{new Date(dataset.fiscalPeriod.end).getFullYear()}
|
{new Date(dataset.fiscalPeriod.end).getUTCFullYear()}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</dd>
|
</dd>
|
||||||
|
|||||||
@ -13,9 +13,13 @@ import { useState } from 'react';
|
|||||||
export default function DatasetsSearch({
|
export default function DatasetsSearch({
|
||||||
datasets,
|
datasets,
|
||||||
availableCountries,
|
availableCountries,
|
||||||
|
minPeriod,
|
||||||
|
maxPeriod,
|
||||||
}: {
|
}: {
|
||||||
datasets: Project[];
|
datasets: Project[];
|
||||||
availableCountries;
|
availableCountries;
|
||||||
|
minPeriod: string;
|
||||||
|
maxPeriod: string;
|
||||||
}) {
|
}) {
|
||||||
const itemsPerPage = 6;
|
const itemsPerPage = 6;
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
@ -49,20 +53,35 @@ export default function DatasetsSearch({
|
|||||||
? dataset.countryCode === watch().country
|
? dataset.countryCode === watch().country
|
||||||
: true
|
: true
|
||||||
)
|
)
|
||||||
// TODO: Does that really makes sense?
|
.filter((dataset) => {
|
||||||
// What if the fiscalPeriod is 2015-2017 and inputs are
|
const filterMinDate = watch().minDate;
|
||||||
// set to 2015-2016. It's going to be filtered out but
|
const filterMaxDate = watch().maxDate;
|
||||||
// it shouldn't.
|
|
||||||
.filter((dataset) =>
|
const datasetMinDate = dataset.fiscalPeriod?.start;
|
||||||
watch().minDate && watch().minDate !== ''
|
const datasetMaxDate = dataset.fiscalPeriod?.end;
|
||||||
? dataset.fiscalPeriod?.start >= watch().minDate
|
|
||||||
: true
|
let datasetStartOverlaps = false;
|
||||||
)
|
if (datasetMinDate) {
|
||||||
.filter((dataset) =>
|
datasetStartOverlaps =
|
||||||
watch().maxDate && watch().maxDate !== ''
|
datasetMinDate >= filterMinDate && datasetMinDate <= filterMaxDate;
|
||||||
? dataset.fiscalPeriod?.end <= watch().maxDate
|
}
|
||||||
: true
|
|
||||||
);
|
let datasetEndOverlaps = false;
|
||||||
|
if (datasetMaxDate) {
|
||||||
|
datasetEndOverlaps =
|
||||||
|
datasetMaxDate >= filterMinDate && datasetMaxDate <= filterMaxDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterMinDate && filterMaxDate) {
|
||||||
|
return datasetStartOverlaps || datasetEndOverlaps;
|
||||||
|
} else if (filterMinDate) {
|
||||||
|
return datasetMinDate >= filterMinDate;
|
||||||
|
} else if (filterMaxDate) {
|
||||||
|
return datasetMinDate <= filterMaxDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const paginatedDatasets = filteredDatasets.slice(
|
const paginatedDatasets = filteredDatasets.slice(
|
||||||
(page - 1) * itemsPerPage,
|
(page - 1) * itemsPerPage,
|
||||||
@ -111,22 +130,32 @@ export default function DatasetsSearch({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/6">
|
<div className="sm:basis-1/6">
|
||||||
<label className="text-sm text-gray-600 font-medium">Min. date</label>
|
<label className="text-sm text-gray-600 font-medium">
|
||||||
|
Fiscal Period Start
|
||||||
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
aria-label="Min. date"
|
aria-label="Min. date"
|
||||||
type="date"
|
type="text"
|
||||||
|
placeholder={minPeriod}
|
||||||
|
onFocus={(e) => (e.target.type = 'date')}
|
||||||
|
onBlur={(e) => (e.target.type = 'text')}
|
||||||
{...register('minDate', { onChange: () => setPage(1) })}
|
{...register('minDate', { onChange: () => setPage(1) })}
|
||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/6">
|
<div className="sm:basis-1/6">
|
||||||
<label className="text-sm text-gray-600 font-medium">Max. date</label>
|
<label className="text-sm text-gray-600 font-medium">
|
||||||
|
Fiscal Period End
|
||||||
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
aria-label="Max. date"
|
aria-label="Max. date"
|
||||||
type="date"
|
type="text"
|
||||||
|
placeholder={maxPeriod}
|
||||||
|
onFocus={(e) => (e.target.type = 'date')}
|
||||||
|
onBlur={(e) => (e.target.type = 'text')}
|
||||||
{...register('maxDate', { onChange: () => setPage(1) })}
|
{...register('maxDate', { onChange: () => setPage(1) })}
|
||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
@ -196,9 +225,9 @@ const CloseIcon = () => {
|
|||||||
id="Vector"
|
id="Vector"
|
||||||
d="M18 18L12 12M12 12L6 6M12 12L18 6M12 12L6 18"
|
d="M18 18L12 12M12 12L6 6M12 12L18 6M12 12L6 18"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
strokeWidth="2"
|
||||||
stroke-linecap="round"
|
strokeLinecap="round"
|
||||||
stroke-linejoin="round"
|
strokeLinejoin="round"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
import {
|
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
||||||
QueryClient,
|
|
||||||
QueryClientProvider,
|
|
||||||
useQuery,
|
|
||||||
useQueryClient,
|
|
||||||
} from 'react-query';
|
|
||||||
import Papa from 'papaparse';
|
import Papa from 'papaparse';
|
||||||
import { Grid } from '@githubocto/flat-ui';
|
import { Grid } from '@githubocto/flat-ui';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
export async function getCsv(url: string) {
|
export async function getCsv(url: string, corsProxy?: string, range?: string) {
|
||||||
|
if (corsProxy) {
|
||||||
|
url = corsProxy + url
|
||||||
|
}
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
Range: 'bytes=0-5132288',
|
Range: range ? `bytes=0-${range}` : 'bytes=0-512000',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
@ -62,24 +60,45 @@ const Spinning = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Table: React.FC<{ url: string }> = ({ url }) => {
|
export interface FlatUiTableProps {
|
||||||
|
url?: string;
|
||||||
|
data?: { [key: string]: number | string }[];
|
||||||
|
rawCsv?: string;
|
||||||
|
range?: string;
|
||||||
|
corsProxy?: string;
|
||||||
|
}
|
||||||
|
export const FlatUiTable: React.FC<FlatUiTableProps> = ({
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
rawCsv,
|
||||||
|
corsProxy,
|
||||||
|
range
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
// Provide the client to your App
|
// Provide the client to your App
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<TableInner url={url} />
|
<TableInner range={range} corsProxy={corsProxy} url={url} data={data} rawCsv={rawCsv} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TableInner: React.FC<{ url: string }> = ({ url }) => {
|
const TableInner: React.FC<FlatUiTableProps> = ({ url, data, rawCsv, corsProxy, range }) => {
|
||||||
|
if (data) {
|
||||||
|
return (
|
||||||
|
<div className="w-full" style={{height: '500px'}}>
|
||||||
|
<Grid data={data} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
|
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
|
||||||
['dataCsv', url],
|
['dataCsv', url],
|
||||||
() => getCsv(url)
|
() => getCsv(url as string, corsProxy, range),
|
||||||
|
{ enabled: !!url }
|
||||||
);
|
);
|
||||||
const { data: parsedData, isLoading: isParsing } = useQuery(
|
const { data: parsedData, isLoading: isParsing } = useQuery(
|
||||||
['dataPreview', csvString],
|
['dataPreview', csvString],
|
||||||
() => parseCsv(csvString),
|
() => parseCsv(rawCsv ? rawCsv as string : csvString as string),
|
||||||
{ enabled: !!csvString }
|
{ enabled: rawCsv ? true : !!csvString }
|
||||||
);
|
);
|
||||||
if (isParsing || isDownloadingCSV)
|
if (isParsing || isDownloadingCSV)
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@ -87,8 +106,10 @@ const TableInner: React.FC<{ url: string }> = ({ url }) => {
|
|||||||
</div>;
|
</div>;
|
||||||
if (parsedData)
|
if (parsedData)
|
||||||
return (
|
return (
|
||||||
<div className="h-[500px] overflow-scroll">
|
<div className="w-full" style={{height: '500px'}}>
|
||||||
<Grid data={parsedData.data} />
|
<Grid data={parsedData.data} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
return <Spinning />
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -5,6 +5,9 @@ import Link from 'next/link';
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Bars3Icon } from '@heroicons/react/24/outline';
|
import { Bars3Icon } from '@heroicons/react/24/outline';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Fragment } from 'react';
|
||||||
|
import { Menu, Transition } from '@headlessui/react';
|
||||||
|
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
||||||
@ -16,42 +19,85 @@ export function Header() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{
|
|
||||||
title: 'Home',
|
|
||||||
href: '/',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Datasets',
|
title: 'Datasets',
|
||||||
href: '/#datasets',
|
href: '/#datasets',
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// title: "Community",
|
title: 'Data Stories',
|
||||||
// href: "https://community.openspending.org/"
|
href: '/stories',
|
||||||
// }
|
},
|
||||||
|
{
|
||||||
|
title: 'Blog',
|
||||||
|
href: '/blog',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'About',
|
||||||
|
href: '/about',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Fiscal Data Package',
|
||||||
|
href: '/about/fiscaldatapackage/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Tools',
|
||||||
|
href: '/about/tools/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Funders',
|
||||||
|
href: '/about/funders/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Presentations',
|
||||||
|
href: '/about/presentations/',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Contributing',
|
||||||
|
href: '/contributing',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Help',
|
||||||
|
href: '/help',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Resources',
|
||||||
|
href: '/resources',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
title: 'Follow the money',
|
||||||
|
href: '/resources/journo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Map of Spending Projects',
|
||||||
|
href: '/resources/map-of-spending-projects/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Working Group On Open Spending Data',
|
||||||
|
href: '/resources/wg/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'UK Departamental Spending',
|
||||||
|
href: '/resources/gb-spending',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="relative z-50 pb-11 lg:pt-11">
|
<header className="relative z-50 pb-11 lg:pt-11">
|
||||||
<Container className="flex flex-wrap items-center justify-between lg:flex-nowrap mt-10 lg:mt-0">
|
<Container className="flex flex-wrap justify-between lg:flex-nowrap mt-10 lg:mt-0">
|
||||||
<Link href="/" className="lg:mt-0 lg:grow lg:basis-0 flex items-center">
|
<Link href="/" className="lg:mt-0 lg:grow lg:basis-0 flex items-center">
|
||||||
<Image src={logo} alt="OpenSpending" className="h-12 w-auto" />
|
<Image src={logo} alt="OpenSpending" className="h-12 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
<ul className="hidden list-none sm:flex gap-x-5 text-base font-medium">
|
<ul className="hidden list-none sm:flex gap-x-5 text-base font-medium">
|
||||||
{navLinks.map((link, i) => (
|
{navLinks.map((link, i) => (
|
||||||
<li key={`nav-link-${i}`}>
|
<li key={`nav-link-${i}`}>
|
||||||
<Link
|
<Dropdown navItem={link} />
|
||||||
className={`text-emerald-900 hover:text-emerald-600 ${
|
|
||||||
isActive(link) ? 'text-emerald-600' : ''
|
|
||||||
}`}
|
|
||||||
href={link.href}
|
|
||||||
scroll={false}
|
|
||||||
>
|
|
||||||
{link.title}
|
|
||||||
</Link>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="hidden xl:block xl:grow"></div>
|
|
||||||
<div className="sm:hidden sm:mt-10 lg:mt-0 lg:grow lg:basis-0 lg:justify-end">
|
<div className="sm:hidden sm:mt-10 lg:mt-0 lg:grow lg:basis-0 lg:justify-end">
|
||||||
<button onClick={() => setMenuOpen(!menuOpen)}>
|
<button onClick={() => setMenuOpen(!menuOpen)}>
|
||||||
<Bars3Icon className="w-8 h-8" />
|
<Bars3Icon className="w-8 h-8" />
|
||||||
@ -80,3 +126,77 @@ export function Header() {
|
|||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function classNames(...classes) {
|
||||||
|
return classes.filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dropdown({ navItem }: { navItem: any }) {
|
||||||
|
const [showDropDown, setShowDropDown] = useState(false);
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
{({ open }) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Menu.Button
|
||||||
|
onMouseEnter={() => setShowDropDown(true)}
|
||||||
|
onMouseLeave={() => setShowDropDown(false)}
|
||||||
|
className="text-emerald-900 hover:text-emerald-600 inline-flex w-full justify-center gap-x-1.5 px-3 py-2 text-sm font-semibold"
|
||||||
|
>
|
||||||
|
<Link href={navItem.href}>{navItem.title}</Link>
|
||||||
|
{navItem.children && (
|
||||||
|
<ChevronDownIcon
|
||||||
|
className="-mr-1 h-5 w-5 text-gray-400"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{navItem.children && (
|
||||||
|
<Transition
|
||||||
|
as={Fragment}
|
||||||
|
show={showDropDown}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Menu.Items
|
||||||
|
static
|
||||||
|
onMouseEnter={() => setShowDropDown(true)}
|
||||||
|
onMouseLeave={() => setShowDropDown(false)}
|
||||||
|
className="absolute right-0 z-10 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||||
|
>
|
||||||
|
<div className="py-1">
|
||||||
|
{navItem.children.map((item) => (
|
||||||
|
<Menu.Item key={item.href}>
|
||||||
|
{({ active }) => (
|
||||||
|
<a
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className={classNames(
|
||||||
|
active
|
||||||
|
? 'bg-gray-100 text-emerald-900 hover:text-emerald-600'
|
||||||
|
: 'text-gray-700',
|
||||||
|
'block px-4 py-2 text-sm'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export function Hero({ countriesCount, datasetsCount, filesCount }) {
|
|||||||
<Button href="#datasets" className="mt-10">
|
<Button href="#datasets" className="mt-10">
|
||||||
Search datasets
|
Search datasets
|
||||||
</Button>
|
</Button>
|
||||||
<dl className="mt-10 grid grid-cols-2 gap-x-10 gap-y-6 sm:mt-16 sm:gap-x-16 sm:gap-y-10 sm:text-center lg:auto-cols-auto lg:grid-flow-col lg:grid-cols-none lg:justify-start lg:text-left">
|
<dl className="mt-10 grid grid-cols-1 sm:grid-cols-3 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">
|
||||||
{[
|
{[
|
||||||
// Added the plus sign because some datasets do not
|
// Added the plus sign because some datasets do not
|
||||||
// contain defined countries
|
// contain defined countries
|
||||||
@ -36,11 +36,19 @@ export function Hero({ countriesCount, datasetsCount, filesCount }) {
|
|||||||
['Files', filesCount],
|
['Files', filesCount],
|
||||||
].map(([name, value]) => (
|
].map(([name, value]) => (
|
||||||
<div key={name}>
|
<div key={name}>
|
||||||
|
<div className='flex gap-x-2 items-center sm:hidden' key={name}>
|
||||||
|
<dd className="mt-0.5 text-2xl font-semibold tracking-tight text-emerald-900">
|
||||||
|
{value}
|
||||||
|
</dd>
|
||||||
|
<dt className="font-mono text-sm text-emerald-600">{name}</dt>
|
||||||
|
</div>
|
||||||
|
<div className='hidden sm:block' key={name}>
|
||||||
<dt className="font-mono text-sm text-emerald-600">{name}</dt>
|
<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">
|
<dd className="mt-0.5 text-2xl font-semibold tracking-tight text-emerald-900">
|
||||||
{value}
|
{value}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user