Compare commits

...

143 Commits

Author SHA1 Message Date
github-actions[bot]
be38086794 Version Packages 2024-10-23 18:08:18 +02:00
Ola Rubaj
63d9e3b754 [feat,LineChart][s]: support for multiple series 2024-10-23 18:03:07 +02:00
Anuar Ustayev (aka Anu)
f86f0541eb Merge pull request #1332 from datopian/site/fix-showcases
[portaljs site][showcases][s] Merge examples into Showcases tab
2024-10-11 09:36:16 +05:00
Lucas Morais Bispo
64bc212384 Update README.md 2024-10-09 11:46:02 -03:00
Lucas Morais Bispo
1e7daf353d Add files via upload 2024-10-09 11:28:42 -03:00
lucasmbispo
cc69dabf80 [site][showcases] update examples 2024-10-03 21:04:06 -03:00
lucasmbispo
a5d87712e0 [site][showcases][s] Merge examples into Showcases tab 2024-10-01 11:07:33 -03:00
Rufus Pollock
86834fd1a6 Merge pull request #1317 from loleg/patch-1
Fix link to Next.js in README.md
2024-09-20 13:30:02 +02:00
Oleg Lavrovsky
8a661b1617 Fix link to Next.js in README.md 2024-09-20 11:23:06 +02:00
Rufus Pollock
1baebc3f3c Merge pull request #1200 from rzmk/patch-1
[#1181, examples/ckan-ssg][xs]: update example generation command
2024-07-05 19:13:43 +02:00
João Demenech
bbac4954f5 Merge pull request #1202 from datopian/changeset-release/main
Version Packages
2024-06-24 17:58:02 -03:00
github-actions[bot]
be6b184884 Version Packages 2024-06-24 20:47:23 +00:00
João Demenech
64103d6488 Merge pull request #1122 from datopian/feature/custom-tile-layer
Custom Tile Layer for Map Component
2024-06-24 17:44:19 -03:00
Demenech
8e3496782c version: add changeset 2024-06-24 17:42:49 -03:00
Mueez Khan
e034503399 [examples/ckan-ssg][xs]: update command to create project 2024-06-22 00:17:49 -04:00
William Lima
93ae498ec2 Code cleanup 2024-06-19 10:10:56 -01:00
William Lima
97e43fdcba add mapbox as default basemap 2024-06-18 22:37:20 -01:00
William Lima
32f29024f8 attr replace fix 2024-06-18 22:05:41 -01:00
William Lima
134f72948c Add TileLayer Presets configuration 2024-06-18 22:01:59 -01:00
Rufus Pollock
c1f2c526a8 [#1181,site][xs]: change portaljs to datahub in github repo references. 2024-06-10 19:31:43 +02:00
João Demenech
8feb87739d Merge pull request #1173 from datopian/changeset-release/main
Version Packages
2024-06-09 08:06:43 -03:00
github-actions[bot]
3a07267e44 Version Packages 2024-06-09 09:25:23 +00:00
Rufus Pollock
3f19ca16ed [#1118,docs/portaljs][s 2024-06-09 11:22:25 +02:00
João Demenech
5deabac5fe Merge pull request #1170 from datopian/fix/iframe-height
[components][iFrame] Change default height
2024-06-04 14:57:24 -03:00
lucasmbispo
96901150c6 [changesets] change major to patch 2024-06-04 09:38:47 -03:00
lucasmbispo
9ff25ed7c4 [components][iFrame] Change iFrame height 2024-06-04 09:38:12 -03:00
lucasmbispo
8f884fceab [components][iFrame] Change default height 2024-06-04 09:26:30 -03:00
Anuar Ustayev (aka Anu)
7094eded50 Merge pull request #1167 from datopian/fix/map-geojson
Fix: autoZoomConfiguration not working properly when the geojson parameter is passed
2024-06-04 14:06:45 +05:00
Rufus Pollock
30e7c6379f Merge pull request #1069 from marcchehab/patch-2 - Add SiteToc to MobileNav.
This PR adds the SiteToc to the MobileNav. It also fixes double type declarations in MobileNav by importing the interfaces from Nav. Adding SiteToc was then just a matter of uncommenting code that was there already.
2024-05-31 17:16:42 +02:00
Ronaldo Campos
feada58932 Fix: autoZoomConfiguration not working properly when the geojson parameter is passed 2024-05-31 11:37:01 -03:00
William Lima
31406d48e3 Update Map.tsx 2024-05-31 10:29:15 -01:00
Daniellappv
d6bf344ca3 Update CONTRIBUTING.md 2024-05-31 10:55:58 +03:00
William Lima
d1a5138c6e include configs on .env vars or pass through props 2024-05-22 11:48:20 -01:00
William Lima
a6047a9341 Implements Custom Tile Layer
#1121 adds default tile layer and allows user to pass a tile object to map
2024-05-13 12:51:28 -01:00
Ola Rubaj
a4e60540ae Merge pull request #1119 from datopian/remark-wiki-link-cleanup
## Changes

- remove unneeded tests
- do not remove "index" from the end of tile path in `getPermalinks` function
2024-05-09 02:20:45 +02:00
Ola Rubaj
e4c456c237 rm changeset file 2024-05-09 02:19:54 +02:00
Ola Rubaj
ce9ebbf41e add changeset file 2024-05-09 02:16:05 +02:00
Ola Rubaj
a8fb176bcc rm test for custom permarlink converter (irrelevant) 2024-05-09 02:12:44 +02:00
Ola Rubaj
2ac82367c5 do not remove "index" from the end of file
- should be treated as a regular file name
- it's up to the app how to interpret those paths/files later
2024-05-09 02:12:38 +02:00
Ola Rubaj
85de6f7878 replace inex.md with README.md in test fixtures 2024-05-09 02:09:52 +02:00
Ola Rubaj
539fffeb55 Merge pull request #1113 from datopian/changeset-release/main
Version Packages
2024-04-18 15:43:31 +02:00
github-actions[bot]
0d276535bd Version Packages 2024-04-18 13:42:23 +00:00
Ola Rubaj
38dd7103a3 Merge pull request #1103 from datopian/feat/portaljs-components-improvements
Components API and docs improvements
Related to: #1089
2024-04-17 16:16:27 +02:00
Ola Rubaj
48cd812a48 add changeset file 2024-04-17 16:14:00 +02:00
Ola Rubaj
7bba10714d refresh package-lock file 2024-04-17 16:13:47 +02:00
github-actions[bot]
de2c1e5b48 Version Packages (#1109)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-17 15:54:34 +02:00
mohamed yahia
57952e0817 [remark-wiki-link][m]: Add image size adjustment in remark-wiki-link (#1084)
* [remark-wiki-link][m]: Add image size adjustment in `remark-wiki-link`

* [remark-wiki-links][m]: Add image size feature to images
2024-04-15 18:39:27 +02:00
Demenech
df9664624f fix(LineChart): remove unused fillWidth prop 2024-04-09 17:45:12 -03:00
Demenech
2ea185b710 feat: Catalog component API and docs improvements 2024-04-09 17:41:01 -03:00
Demenech
b859d48f17 feat: Map component API and docs improvements 2024-04-09 17:30:45 -03:00
Demenech
3d73ac422e feat: Vega and Vega Lite components API and docs improvements 2024-04-09 17:13:05 -03:00
Demenech
059ffe4e34 feat: PlotlyLineChart component API and docs improvements 2024-04-09 17:08:50 -03:00
Demenech
0aed7dce77 feat: Plotly component docs improvements 2024-04-09 16:57:23 -03:00
Demenech
c202d6cfc4 feat: LineChart component API and docs improvements 2024-04-09 16:50:49 -03:00
Demenech
d9c20528c5 feat: PdfViewer component API and docs improvements 2024-04-09 16:20:01 -03:00
Demenech
b7ee5a1869 feat: Iframe component API and docs improvements 2024-04-09 16:07:12 -03:00
Demenech
4b5d549190 feat: comment out the Table component for now 2024-04-09 15:58:33 -03:00
Demenech
e6f0ab4ec8 feat: FlatUiTable component API and docs improvements 2024-04-09 15:54:03 -03:00
Demenech
22038fbd4f feat: Excel component API and docs improvements 2024-04-09 15:44:37 -03:00
Demenech
8b292a9bf2 feat: group stories in different categories 2024-04-09 15:36:48 -03:00
Demenech
cda3d335f1 feat: rename Plotly components stories so that they show up together on the storybook sidebar 2024-04-09 15:25:14 -03:00
Demenech
fe97cc87f4 fix: OpenLayers and BucketViewer were still showing up 2024-04-09 15:22:55 -03:00
Demenech
88f6199d18 feat: implement new Data interface + review PlotlyBarChart API and docs + hide BucketViewer and OpenLayers 2024-04-09 15:21:08 -03:00
Rufus Pollock
852cf60abc [README][s]: further refinements of info re DataHub. 2024-04-05 11:50:24 +02:00
Rufus Pollock
704be0d5a7 [README][s]: update portal.js to datahub nomenclature. 2024-04-05 11:28:37 +02:00
Rufus Pollock
fb3598fa49 Delete .vscode/extensions.json
No need for vscode stuff in repo.
2024-04-01 18:18:06 +02:00
Rufus Pollock
d898b5a833 Merge pull request #1065 from marcchehab/patch-1
Fix React warning about unique "key" prop
2024-03-29 14:56:38 +01:00
github-actions[bot]
3aac4dabf9 Version Packages (#1087)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-22 10:42:53 -03:00
Luccas Mateus
a044f56e3c Changeset 2024-03-22 10:32:11 -03:00
Luccas Mateus
1b58c311eb Plotly components 2024-03-22 10:31:30 -03:00
Rufus Pollock
ed9ac2c263 Delete tools/tsconfig.tools.json
Remove as tools is empty folder.
2024-02-28 16:49:24 +01:00
Rufus Pollock
42c72e5afd Delete tools/generators/.gitkeep
empty directory
2024-02-28 16:48:34 +01:00
Rufus Pollock
9e1a324fa1 [examples/fivethirtyeight][xs]: link to blog post we wrote about it. 2024-02-13 13:31:39 +01:00
Rufus Pollock
90178af8f2 [examples/fivethirtyeight][s]: add link to demo site to README. 2024-02-13 13:30:57 +01:00
Rufus Pollock
00e61e104c [site/config][xs]: change discord server to datahub. 2024-02-09 10:27:06 +01:00
Leonardo Yuri Farias
f7f03fddca Merge pull request #1085 from datopian/changeset-release/main
Version Packages
2024-01-31 13:20:53 -03:00
github-actions[bot]
0891dfde2d Version Packages 2024-01-31 09:31:04 +00:00
Anuar Ustayev (aka Anu)
c904e3731b Merge pull request #1083 from datopian/feature/table-with-integration-with-datastore-api
Created integration with datastore api for table component
2024-01-31 15:28:17 +06:00
Gutts-n
86a2945ee6 Created integration with datastore api for table component 2024-01-29 14:07:42 -03:00
Leonardo Yuri Farias
09daa98b28 Merge pull request #1082 from datopian/changeset-release/main
Version Packages
2024-01-25 16:49:22 -03:00
github-actions[bot]
b511c9f71b Version Packages 2024-01-25 19:48:44 +00:00
Leonardo Yuri Farias
464cda6db8 Merge pull request #1081 from datopian/fix/changed-the-download-behavior
Fixed error to remove anchor from document
2024-01-25 16:45:58 -03:00
Gutts-n
2bbf313489 Fixed error to remove anchor from document 2024-01-25 16:45:39 -03:00
Gutts-n
c26b76368d Fixed error to remove anchor from document 2024-01-25 16:43:47 -03:00
Leonardo Yuri Farias
af11f0cfd5 Merge pull request #1080 from datopian/changeset-release/main
Version Packages
2024-01-25 16:20:34 -03:00
github-actions[bot]
9ae2b31113 Version Packages 2024-01-25 19:19:58 +00:00
Leonardo Yuri Farias
2bffd130c8 Merge pull request #1079 from datopian/fix/changed-the-download-behavior
Changed behavior of the download data bucket viewer component
2024-01-25 16:17:14 -03:00
Gutts-n
058d23678a Added changeset to the PR 2024-01-25 16:16:49 -03:00
Gutts-n
540a08934c Changed behavior of the download data bucket viewer component 2024-01-25 16:10:22 -03:00
Leonardo Yuri Farias
7d010cfee4 Merge pull request #1078 from datopian/changeset-release/main
Version Packages
2024-01-24 17:23:14 -03:00
github-actions[bot]
dd79da1c6b Version Packages 2024-01-24 20:22:57 +00:00
Leonardo Yuri Farias
a58e2b81f7 Merge pull request #1077 from datopian/feature/download-loading-message
Created property to present a component while loading the download of the file and fixed download bug on pagination
2024-01-24 17:20:09 -03:00
Gutts-n
6d7acd27ed Created property to present a component while is loading the download of the file and fixed download bug on pagination 2024-01-24 17:15:14 -03:00
Leonardo Yuri Farias
7c30842c7d Merge pull request #1076 from datopian/changeset-release/main
Version Packages
2024-01-24 11:08:44 -03:00
github-actions[bot]
35ca1d6dfd Version Packages 2024-01-24 14:08:13 +00:00
Leonardo Yuri Farias
a7e90b64af Merge pull request #1075 from datopian/fix/download-button-presented-on-start-of-bucket-viewer
Fixed problem presenting the download component in the first load of …
2024-01-24 11:05:18 -03:00
Gutts-n
26dcffc279 Fixed problem presenting the download component in the first load of the bucket viewer 2024-01-24 11:03:08 -03:00
Leonardo Yuri Farias
d18e3dd486 Merge pull request #1074 from datopian/changeset-release/main
Version Packages
2024-01-23 16:54:19 -03:00
github-actions[bot]
8d7059acb4 Version Packages 2024-01-23 19:53:24 +00:00
Leonardo Yuri Farias
09d5324d4e Merge pull request #1073 from datopian/feature/search-and-pagination-for-bucket-viewer
Fixed bug on filter by startDate
2024-01-23 16:50:37 -03:00
Gutts-n
cf24042a91 Fixed bug on filter by startDate 2024-01-23 16:49:15 -03:00
Leonardo Yuri Farias
2c45da679b Merge pull request #1072 from datopian/changeset-release/main
Version Packages
2024-01-23 14:46:42 -03:00
github-actions[bot]
0a476101e7 Version Packages 2024-01-23 17:44:30 +00:00
Leonardo Yuri Farias
1343a7a6f7 Merge pull request #1071 from datopian/feature/search-and-pagination-for-bucket-viewer
Added pagination and filter properties for the BucketViewer component
2024-01-23 14:41:44 -03:00
Gutts-n
27c99adde8 Added pagination and filter properties for the BucketViewer component 2024-01-23 14:37:03 -03:00
luzmediach
1a8e7ac06e NavMobile to use Nav interfaces and add SiteToc to sidebar 2024-01-21 12:48:10 +01:00
marcchehab
4355efe0c4 Update Nav.tsx 2024-01-21 12:36:46 +01:00
mohamed yahia
96904aef0d Merge pull request #1068 from datopian/analytics
[core][m]: Add analytics component to the core packages
2024-01-18 23:53:27 +02:00
mohamed yahia
92a549d6a9 [core][m]: Add analytics component to the core packages 2024-01-18 23:51:01 +02:00
Leonardo Yuri Farias
1a5bbd4346 Merge pull request #1067 from datopian/changeset-release/main
Version Packages
2024-01-17 22:42:02 -03:00
github-actions[bot]
4985576183 Version Packages 2024-01-18 01:41:06 +00:00
Leonardo Yuri Farias
7049917ef7 Merge pull request #1066 from datopian/feature/iframe-component
Created Iframe component
2024-01-17 22:38:23 -03:00
Gutts-n
dd03a493be Created Iframe component 2024-01-17 22:32:56 -03:00
Gutts-n
e5b0a85e48 Created Iframe component 2024-01-17 21:54:22 -03:00
Gutts-n
a93b13f448 Component start 2024-01-17 21:08:44 -03:00
marcchehab
9e73410b17 Fix React warning about unique "key" prop
I always get a react warning: Warning: Each child in a list should have a unique "key" prop.

This fixed it and makes for warning-free development 😊
2024-01-04 14:14:49 +01:00
Leonardo Yuri Farias
8a4ec39d25 Merge pull request #1064 from datopian/changeset-release/main
Version Packages
2023-12-21 22:30:58 -03:00
github-actions[bot]
38bf06f031 Version Packages 2023-12-22 01:29:56 +00:00
Leonardo Yuri Farias
8560f165fd Merge pull request #1063 from datopian/feature/auto-zoom-in-map-componnet
Created auto zoom configuration for the map component
2023-12-21 22:27:14 -03:00
Leonardo Farias
b13e3ade3c Created auto zoom configuration for the map component 2023-12-21 22:23:42 -03:00
Leonardo Yuri Farias
1394f02038 Merge pull request #1062 from datopian/changeset-release/main
Version Packages
2023-12-19 22:25:59 -03:00
github-actions[bot]
e687779fa6 Version Packages 2023-12-20 01:22:46 +00:00
Leonardo Yuri Farias
2ec143707d Merge pull request #1061 from datopian/feature/style-in-map-component
Created the style property for the map component
2023-12-19 22:20:07 -03:00
Leonardo Farias
4ddfc1126a Created the style property for the map component 2023-12-19 22:16:57 -03:00
Leonardo Yuri Farias
f23d7965f2 Merge pull request #1056 from datopian/changeset-release/main
Version Packages
2023-11-26 17:56:27 -03:00
github-actions[bot]
97e4775894 Version Packages 2023-11-26 20:55:10 +00:00
Leonardo Yuri Farias
3c14ce8af7 Merge pull request #1057 from datopian/fix/exporting-bucket-viewer-component
Added the export of BucketViewer component
2023-11-26 17:52:29 -03:00
leonardo.farias
61c750b7e1 Added the export of BucketViewer component 2023-11-26 17:50:19 -03:00
Anuar Ustayev (aka Anu)
b55ec5126c Merge pull request #1055 from datopian/feature/bucket-viewer-component
feature: Created bucket viewer component
2023-11-24 12:17:28 +06:00
leonardo.farias
712f4a3b0f Finished the development of the BucketViewer component 2023-11-23 21:41:28 -03:00
leonardo.farias
03960c8bac feature: Created bucket viewer component 2023-11-20 23:50:04 -03:00
Leonardo Yuri Farias
73c7eaf145 Merge pull request #1050 from datopian/changeset-release/main
Version Packages
2023-11-02 19:57:46 -03:00
github-actions[bot]
542f2ede9e Version Packages 2023-11-02 22:43:32 +00:00
Leonardo Yuri Farias
f17c2ed1d0 Merge pull request #1040 from datopian/fix/broken-urls-with-dashes
Support wiki links with special characters and fix links to headings
2023-11-02 19:40:53 -03:00
Demenech
f1d7e68077 fix(site,blogs): author not showing up on two posts 2023-11-02 00:09:05 -03:00
leonardo.farias
1663b09a86 Adjusts in the regex used to replace spaces with dashes in the header of wiki links and adjusted the unit tests 2023-11-01 21:50:52 -03:00
Rufus Pollock
b940c82d93 [site/blog][s]: minor updates to enhancing-geospatial-data-visualization-with-portaljs.
- Add maps to title
- Proof intro paragraphs
2023-11-01 01:05:52 +01:00
Rufus Pollock
492593dedb [site/navbar][s]: link examples to new examples page rather than direct to github examples folder. 2023-11-01 00:25:07 +01:00
Rufus Pollock
4ae22c7411 [site/examples][s]: stub an /examples/ page. 2023-11-01 00:23:55 +01:00
leonardo.farias
85bb6cb98c Changed tests and created tests to verify the generated prefix in the HTML plugin 2023-10-27 22:35:16 -03:00
leonardo.farias
737f880036 Changed regex to permit any symbols other than # 2023-10-26 00:00:06 -03:00
leonardo.farias
1a9d64e0cf Fixing regex to not remove dashes - 2023-10-16 22:16:07 -03:00
João Demenech
3366086d87 Merge pull request #1038 from datopian/feat/site/open-spending-revamp
Blog: add new blog post about the OpenSpending revamp
2023-10-16 17:03:37 -03:00
89 changed files with 6945 additions and 911 deletions

View File

@@ -1,8 +0,0 @@
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
"dbaeumer.vscode-eslint"
]
}

View File

@@ -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:
@@ -35,7 +35,7 @@ If you'd like to work on one of the issues you can:
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

View File

@@ -1,31 +1,56 @@
<h1 align="center"> <h1 align="center">
🌀 Portal.JS <a href="https://datahub.io/">
<br /> <img alt="datahub" src="http://datahub.io/datahub-cube.svg" width="146">
Rapidly build rich data portals using a modern frontend framework </a>
</h1> </h1>
* [What is Portal.JS ?](#What-is-Portal.JS) <p align="center">
* [Features](#Features) Bugs, issues and suggestions re DataHub Cloud ☁️ and DataHub OpenSource 🌀
* [For developers](#For-developers) <br />
* [Docs](#Docs) <br /><a href="https://discord.gg/xfFDMPU9dC"><img src="https://dcbadge.vercel.app/api/server/xfFDMPU9dC" /></a>
* [Community](#Community) </p>
* [Appendix](#Appendix)
* [What happened to Recline?](#What-happened-to-Recline?)
# What is Portal.JS ## DataHub
🌀 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. This repo and issue tracker are for
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/). - DataHub Cloud ☁️ - https://datahub.io/
- DataHub 🌀 - https://datahub.io/opensource
## Features ### Issues
Found a bug: 👉 https://github.com/datopian/datahub/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/datahub/discussions
### Chat on Discord
If you would prefer to get help via live chat check out our discord 👉
[Discord](https://discord.gg/xfFDMPU9dC)
### Docs
https://datahub.io/docs
## DataHub OpenSource 🌀
DataHub 🌀 is a platform for rapidly creating rich data portal and publishing systems using a modern frontend approach. Datahub can be used to publish a single dataset or build a full-scale data catalog/portal.
DataHub is built in JavaScript and React on top of the popular [Next.js](https://nextjs.org) framework. DataHub 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 +58,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).

View File

@@ -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`
![](https://media.discordapp.net/attachments/1069718983604977754/1098252297726865408/image.png?width=853&height=461) ![](https://media.discordapp.net/attachments/1069718983604977754/1098252297726865408/image.png?width=853&height=461)
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
![](https://media.discordapp.net/attachments/1069718983604977754/1098252298074988595/image.png?width=853&height=461) ![](https://media.discordapp.net/attachments/1069718983604977754/1098252298074988595/image.png?width=853&height=461)
## Deployment ## Deployment

View File

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

View File

@@ -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:

3522
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,16 @@
import 'tailwindcss/tailwind.css' import 'tailwindcss/tailwind.css'
import '../src/index.css' import '../src/index.css'
import type { Preview } from '@storybook/react'; import type { Preview } from '@storybook/react';
window.process = {
...window.process,
env:{
...window.process?.env,
}
};
const preview: Preview = { const preview: Preview = {
parameters: { parameters: {
actions: { argTypesRegex: '^on[A-Z].*' }, actions: { argTypesRegex: '^on[A-Z].*' },

View File

@@ -1,5 +1,103 @@
# @portaljs/components # @portaljs/components
## 1.2.0
### Minor Changes
- [#1338](https://github.com/datopian/datahub/pull/1338) [`63d9e3b7`](https://github.com/datopian/datahub/commit/63d9e3b7543c38154e6989ef1cc1d694ae9fc4f8) Thanks [@olayway](https://github.com/olayway)! - Support for plotting multiple series in LineChart component.
## 1.1.0
### Minor Changes
- [#1122](https://github.com/datopian/datahub/pull/1122) [`8e349678`](https://github.com/datopian/datahub/commit/8e3496782c022b0653e07f217c6b315ba84e0e61) Thanks [@willy1989cv](https://github.com/willy1989cv)! - Map: allow users to choose a base layer setting
## 1.0.1
### Patch Changes
- [#1170](https://github.com/datopian/datahub/pull/1170) [`9ff25ed7`](https://github.com/datopian/datahub/commit/9ff25ed7c47c8c02cc078c64f76ae35d6754c508) Thanks [@lucasmbispo](https://github.com/lucasmbispo)! - iFrame component: change height
## 1.0.0
### Major Changes
- [#1103](https://github.com/datopian/datahub/pull/1103) [`48cd812a`](https://github.com/datopian/datahub/commit/48cd812a488a069a419d8ecc67f24f94d4d1d1d6) Thanks [@demenech](https://github.com/demenech)! - Components API tidying up and storybook docs improvements.
## 0.6.0
### Minor Changes
- [`a044f56e`](https://github.com/datopian/portaljs/commit/a044f56e3cbe0519ddf9d24d78b0bb7eac917e1c) Thanks [@luccasmmg](https://github.com/luccasmmg)! - Added plotly components
## 0.5.10
### Patch Changes
- [#1083](https://github.com/datopian/portaljs/pull/1083) [`86a2945e`](https://github.com/datopian/portaljs/commit/86a2945ee68dfcea0299984ca9cc9070d68fe1c2) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created integration with datastore api for table component
## 0.5.9
### Patch Changes
- [#1081](https://github.com/datopian/portaljs/pull/1081) [`2bbf3134`](https://github.com/datopian/portaljs/commit/2bbf3134896df3ecc66560bdf95bece143614c7b) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Fixed error to remove anchor from document
## 0.5.8
### Patch Changes
- [#1079](https://github.com/datopian/portaljs/pull/1079) [`058d2367`](https://github.com/datopian/portaljs/commit/058d23678a024890f8a6d909ded9fc8fc11cf145) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Changed the download behaviour of the bucket viewer component and removed loading component while downloading
## 0.5.7
### Patch Changes
- [#1077](https://github.com/datopian/portaljs/pull/1077) [`6d7acd27`](https://github.com/datopian/portaljs/commit/6d7acd27ed9299cbcc14eab906f2f0eb414656b8) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created property to present a component while is loading the download of the file and fixed download bug on pagination
## 0.5.6
### Patch Changes
- [#1075](https://github.com/datopian/portaljs/pull/1075) [`26dcffc2`](https://github.com/datopian/portaljs/commit/26dcffc279057f80a579134e862085ba042c06c3) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Fixed problem presenting the download component in the first load of the bucket viewer
## 0.5.5
### Patch Changes
- [#1073](https://github.com/datopian/portaljs/pull/1073) [`cf24042a`](https://github.com/datopian/portaljs/commit/cf24042a910567e98eeb75ade42ce0149bdb62d1) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Fixed filter by startDate error
## 0.5.4
### Patch Changes
- [#1071](https://github.com/datopian/portaljs/pull/1071) [`27c99add`](https://github.com/datopian/portaljs/commit/27c99adde8fa36ad2c2e03f227f93aa62454eefa) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Added pagination and filter properties for the BucketViewer component
## 0.5.3
### Patch Changes
- [#1066](https://github.com/datopian/portaljs/pull/1066) [`dd03a493`](https://github.com/datopian/portaljs/commit/dd03a493beca5459d1ef447b2df505609fc64e95) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created Iframe component
## 0.5.2
### Patch Changes
- [#1063](https://github.com/datopian/portaljs/pull/1063) [`b13e3ade`](https://github.com/datopian/portaljs/commit/b13e3ade3ccefe7dffe84f824bdedd3e512ce499) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created auto zoom configuration for the map component
## 0.5.1
### Patch Changes
- [#1061](https://github.com/datopian/portaljs/pull/1061) [`4ddfc112`](https://github.com/datopian/portaljs/commit/4ddfc1126a3f0b8137ea47a08a36c56b7373b8f6) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Created the style property in the Map component
## 0.5.0
### Minor Changes
- [#1055](https://github.com/datopian/portaljs/pull/1055) [`712f4a3b`](https://github.com/datopian/portaljs/commit/712f4a3b0f074e654879bb75059f51e06b422b32) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Creation of BucketViewer component to show the data of public buckets
- [#1057](https://github.com/datopian/portaljs/pull/1057) [`61c750b7`](https://github.com/datopian/portaljs/commit/61c750b7e11fe52bf04d25f192440ee1bb307404) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Exporting BucketViewer to be accessed out of the folder
## 0.4.0 ## 0.4.0
### Minor Changes ### Minor Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@portaljs/components", "name": "@portaljs/components",
"version": "0.4.0", "version": "1.2.0",
"type": "module", "type": "module",
"description": "https://portaljs.org", "description": "https://portaljs.org",
"keywords": [ "keywords": [
@@ -29,6 +29,8 @@
"@githubocto/flat-ui": "^0.14.1", "@githubocto/flat-ui": "^0.14.1",
"@heroicons/react": "^2.0.17", "@heroicons/react": "^2.0.17",
"@planet/maps": "^8.1.0", "@planet/maps": "^8.1.0",
"@react-pdf-viewer/core": "3.6.0",
"@react-pdf-viewer/default-layout": "3.6.0",
"@tanstack/react-table": "^8.8.5", "@tanstack/react-table": "^8.8.5",
"ag-grid-react": "^30.0.4", "ag-grid-react": "^30.0.4",
"chroma-js": "^2.4.2", "chroma-js": "^2.4.2",
@@ -37,19 +39,19 @@
"next-mdx-remote": "^4.4.1", "next-mdx-remote": "^4.4.1",
"ol": "^7.4.0", "ol": "^7.4.0",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pdfjs-dist": "2.15.349",
"plotly.js": "^2.30.1",
"postcss-url": "^10.1.3", "postcss-url": "^10.1.3",
"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-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
"react-plotly.js": "^2.6.0",
"react-query": "^3.39.3", "react-query": "^3.39.3",
"react-vega": "^7.6.0", "react-vega": "^7.6.0",
"vega": "5.25.0", "vega": "5.25.0",
"vega-lite": "5.1.0", "vega-lite": "5.1.0",
"vitest": "^0.31.4", "vitest": "^0.31.4",
"@react-pdf-viewer/core": "3.6.0",
"@react-pdf-viewer/default-layout": "3.6.0",
"pdfjs-dist": "2.15.349",
"xlsx": "^0.18.5" "xlsx": "^0.18.5"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,222 @@
import { CSSProperties, ReactNode, useEffect, useState } from 'react';
import LoadingSpinner from './LoadingSpinner';
export interface BucketViewerFilterSearchedDataEvent {
startDate?: Date;
endDate?: Date;
}
export interface BucketViewerProps {
onLoadTotalNumberOfItems?: (total: number) => void;
domain: string;
downloadConfig?: {
hoverOfTheFileComponent?: ReactNode;
};
suffix?: string;
className?: string;
paginationConfig?: BucketViewerPaginationConfig;
filterState?: BucketViewerFilterSearchedDataEvent;
dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>;
}
export interface BucketViewerPaginationConfig {
containerClassName?: string;
containerStyles?: CSSProperties;
itemsPerPage: number;
}
export interface BucketViewerData {
fileName: string;
downloadFileUri: string;
dateProps?: {
date: Date;
dateFormatter?: (date: Date) => string;
};
}
export function BucketViewer({
domain,
suffix,
dataMapperFn,
className,
filterState,
paginationConfig,
downloadConfig,
onLoadTotalNumberOfItems,
}: BucketViewerProps) {
suffix = suffix ?? '/';
const { hoverOfTheFileComponent } = downloadConfig ?? {};
const [isLoading, setIsLoading] = useState<boolean>(false);
const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] =
useState(-1);
const [currentPage, setCurrentPage] = useState<number>(0);
const [lastPage, setLastPage] = useState<number>(0);
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
const [paginatedData, setPaginatedData] = useState<BucketViewerData[]>([]);
const [filteredData, setFilteredData] = useState<BucketViewerData[]>([]);
useEffect(() => {
setIsLoading(true);
fetch(`${domain}${suffix}`)
.then((res) => dataMapperFn(res))
.then((data) => {
setBucketFiles(data);
setFilteredData(data);
})
.finally(() => setIsLoading(false));
}, [domain, suffix]);
useEffect(() => {
if (paginationConfig) {
const startIndex = paginationConfig
? currentPage * paginationConfig.itemsPerPage
: 0;
const endIndex = paginationConfig
? startIndex + paginationConfig.itemsPerPage
: 0;
setLastPage(
Math.ceil(filteredData.length / paginationConfig.itemsPerPage) - 1
);
setPaginatedData(filteredData.slice(startIndex, endIndex));
}
}, [currentPage, filteredData]);
useEffect(() => {
if (onLoadTotalNumberOfItems) onLoadTotalNumberOfItems(filteredData.length);
}, [filteredData]);
useEffect(() => {
if (!filterState) return;
if (filterState.startDate && filterState.endDate) {
setFilteredData(
bucketFiles.filter(({ dateProps }) =>
dateProps
? dateProps.date.getTime() >= filterState.startDate.getTime() &&
dateProps.date.getTime() <= filterState.endDate.getTime()
: true
)
);
} else if (filterState.startDate) {
setFilteredData(
bucketFiles.filter(({ dateProps }) =>
dateProps
? dateProps.date.getTime() >= filterState.startDate.getTime()
: true
)
);
} else if (filterState.endDate) {
setFilteredData(
bucketFiles.filter(({ dateProps }) =>
dateProps
? dateProps.date.getTime() <= filterState.endDate.getTime()
: true
)
);
} else {
setFilteredData(bucketFiles);
}
}, [filterState]);
return isLoading ? (
<div className="w-full flex items-center justify-center h-[300px]">
<LoadingSpinner />
</div>
) : bucketFiles ? (
<>
{...(paginationConfig && bucketFiles ? paginatedData : filteredData)?.map(
(data, i) => (
<ul
onClick={() => {
const a: HTMLAnchorElement = document.createElement('a');
a.href = data.downloadFileUri;
a.target = `_blank`;
a.download = data.fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}}
key={i}
onMouseEnter={() => setShowDownloadComponentOnLine(i)}
onMouseLeave={() => setShowDownloadComponentOnLine(undefined)}
className={`${
className ??
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
}`}
>
{hoverOfTheFileComponent && showDownloadComponentOnLine === i ? (
hoverOfTheFileComponent
) : (
<></>
)}
<div className="flex justify-between w-full items-center">
<div>
<li>{data.fileName}</li>
{data.dateProps && data.dateProps.dateFormatter ? (
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
) : (
<></>
)}
</div>
</div>
</ul>
)
)}
{paginationConfig ? (
<ul
className={
paginationConfig.containerClassName
? paginationConfig.containerClassName
: 'flex justify-end gap-x-[0.5rem] w-full'
}
style={paginationConfig.containerStyles ?? {}}
>
<li>
<button
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
disabled={currentPage === 0}
onClick={() => setCurrentPage(0)}
>
First
</button>
</li>
<li>
<button
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
onClick={() => setCurrentPage(currentPage - 1)}
disabled={currentPage === 0}
>
Previous
</button>
</li>
<label>{currentPage + 1}</label>
<li>
<button
onClick={() => setCurrentPage(currentPage + 1)}
disabled={currentPage >= lastPage}
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
>
Next
</button>
</li>
<li>
<button
onClick={() => setCurrentPage(lastPage)}
disabled={currentPage >= lastPage}
className="hover:cursor-pointer hover:disabled:cursor-not-allowed"
>
Last
</button>
</li>
</ul>
) : (
<></>
)}
</>
) : null;
}

View File

@@ -7,7 +7,12 @@ export function Catalog({
datasets, datasets,
facets, facets,
}: { }: {
datasets: any[]; datasets: {
_id: string | number;
metadata: { title: string; [k: string]: string | number };
url_path: string;
[k: string]: any;
}[];
facets: string[]; facets: string[];
}) { }) {
const [indexFilter, setIndexFilter] = useState(''); const [indexFilter, setIndexFilter] = useState('');
@@ -56,7 +61,7 @@ export function Catalog({
//Then check if the selectedValue for the given facet is included in the dataset metadata //Then check if the selectedValue for the given facet is included in the dataset metadata
.filter((dataset) => { .filter((dataset) => {
//Avoids a server rendering breakage //Avoids a server rendering breakage
if (!watch() || Object.keys(watch()).length === 0) return true if (!watch() || Object.keys(watch()).length === 0) return true;
//This will filter only the key pairs of the metadata values that were selected as facets //This will filter only the key pairs of the metadata values that were selected as facets
const datasetFacets = Object.entries(dataset.metadata).filter((entry) => const datasetFacets = Object.entries(dataset.metadata).filter((entry) =>
facets.includes(entry[0]) facets.includes(entry[0])
@@ -86,9 +91,7 @@ export function Catalog({
className="p-2 ml-1 text-sm shadow border border-block" className="p-2 ml-1 text-sm shadow border border-block"
{...register(elem[0] + '.selectedValue')} {...register(elem[0] + '.selectedValue')}
> >
<option value=""> <option value="">Filter by {elem[0]}</option>
Filter by {elem[0]}
</option>
{(elem[1] as { possibleValues: string[] }).possibleValues.map( {(elem[1] as { possibleValues: string[] }).possibleValues.map(
(val) => ( (val) => (
<option <option
@@ -102,10 +105,10 @@ export function Catalog({
)} )}
</select> </select>
))} ))}
<ul className='mb-5 pl-6 mt-5 list-disc'> <ul className="mb-5 pl-6 mt-5 list-disc">
{filteredDatasets.map((dataset) => ( {filteredDatasets.map((dataset) => (
<li className='py-2' key={dataset._id}> <li className="py-2" key={dataset._id}>
<a className='font-medium underline' href={dataset.url_path}> <a className="font-medium underline" href={dataset.url_path}>
{dataset.metadata.title {dataset.metadata.title
? dataset.metadata.title ? dataset.metadata.title
: dataset.url_path} : dataset.url_path}
@@ -116,4 +119,3 @@ export function Catalog({
</> </>
); );
} }

View File

@@ -4,12 +4,14 @@ import { read, utils } from 'xlsx';
import { AgGridReact } from 'ag-grid-react'; import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css'; import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css'; import 'ag-grid-community/styles/ag-theme-alpine.css';
import { Data } from '../types/properties';
export type ExcelProps = { export type ExcelProps = {
url: string; data: Required<Pick<Data, 'url'>>;
}; };
export function Excel({ url }: ExcelProps) { export function Excel({ data }: ExcelProps) {
const url = data.url;
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [activeSheetName, setActiveSheetName] = useState<string>(); const [activeSheetName, setActiveSheetName] = useState<string>();
const [workbook, setWorkbook] = useState<any>(); const [workbook, setWorkbook] = useState<any>();

View File

@@ -2,6 +2,7 @@ import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import Papa from 'papaparse'; import Papa from 'papaparse';
import { Grid } from '@githubocto/flat-ui'; import { Grid } from '@githubocto/flat-ui';
import LoadingSpinner from './LoadingSpinner'; import LoadingSpinner from './LoadingSpinner';
import { Data } from '../types/properties';
const queryClient = new QueryClient(); const queryClient = new QueryClient();
@@ -36,30 +37,25 @@ export async function parseCsv(file: string, parsingConfig): Promise<any> {
} }
export interface FlatUiTableProps { export interface FlatUiTableProps {
url?: string; data: Data;
data?: { [key: string]: number | string }[]; uniqueId?: number;
rawCsv?: string;
randomId?: number;
bytes: number; bytes: number;
parsingConfig: any; parsingConfig: any;
} }
export const FlatUiTable: React.FC<FlatUiTableProps> = ({ export const FlatUiTable: React.FC<FlatUiTableProps> = ({
url,
data, data,
rawCsv, uniqueId,
bytes = 5132288, bytes = 5132288,
parsingConfig = {}, parsingConfig = {},
}) => { }) => {
const randomId = Math.random(); uniqueId = uniqueId ?? Math.random();
return ( return (
// Provide the client to your App // Provide the client to your App
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<TableInner <TableInner
bytes={bytes} bytes={bytes}
url={url}
data={data} data={data}
rawCsv={rawCsv} uniqueId={uniqueId}
randomId={randomId}
parsingConfig={parsingConfig} parsingConfig={parsingConfig}
/> />
</QueryClientProvider> </QueryClientProvider>
@@ -67,33 +63,32 @@ export const FlatUiTable: React.FC<FlatUiTableProps> = ({
}; };
const TableInner: React.FC<FlatUiTableProps> = ({ const TableInner: React.FC<FlatUiTableProps> = ({
url,
data, data,
rawCsv, uniqueId,
randomId,
bytes, bytes,
parsingConfig, parsingConfig,
}) => { }) => {
if (data) { const url = data.url;
const csv = data.csv;
const values = data.values;
if (values) {
return ( return (
<div className="w-full" style={{ height: '500px' }}> <div className="w-full" style={{ height: '500px' }}>
<Grid data={data} /> <Grid data={values} />
</div> </div>
); );
} }
const { data: csvString, isLoading: isDownloadingCSV } = useQuery( const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
['dataCsv', url, randomId], ['dataCsv', url, uniqueId],
() => getCsv(url as string, bytes), () => getCsv(url as string, bytes),
{ enabled: !!url } { enabled: !!url }
); );
const { data: parsedData, isLoading: isParsing } = useQuery( const { data: parsedData, isLoading: isParsing } = useQuery(
['dataPreview', csvString, randomId], ['dataPreview', csvString, uniqueId],
() => () =>
parseCsv( parseCsv(csv ? (csv as string) : (csvString as string), parsingConfig),
rawCsv ? (rawCsv as string) : (csvString as string), { enabled: csv ? true : !!csvString }
parsingConfig
),
{ enabled: rawCsv ? true : !!csvString }
); );
if (isParsing || isDownloadingCSV) if (isParsing || isDownloadingCSV)
<div className="w-full flex justify-center items-center h-[500px]"> <div className="w-full flex justify-center items-center h-[500px]">

View File

@@ -0,0 +1,17 @@
import { CSSProperties } from 'react';
import { Data } from '../types/properties';
export interface IframeProps {
data: Required<Pick<Data, 'url'>>;
style?: CSSProperties;
}
export function Iframe({ data, style }: IframeProps) {
const url = data.url;
return (
<iframe
src={url}
style={style ?? { width: `100%`, height: `600px` }}
></iframe>
);
}

View File

@@ -2,35 +2,40 @@ import { useEffect, useState } from 'react';
import LoadingSpinner from './LoadingSpinner'; import LoadingSpinner from './LoadingSpinner';
import { VegaLite } from './VegaLite'; import { VegaLite } from './VegaLite';
import loadData from '../lib/loadData'; import loadData from '../lib/loadData';
import { Data } from '../types/properties';
type AxisType = 'quantitative' | 'temporal'; type AxisType = 'quantitative' | 'temporal';
type TimeUnit = 'year' | undefined; // or ... type TimeUnit = 'year' | undefined; // or ...
export type LineChartProps = { export type LineChartProps = {
data: Array<Array<string | number>> | string | { x: string; y: number }[]; data: Omit<Data, 'csv'>;
title?: string; title?: string;
xAxis?: string; xAxis: string;
xAxisType?: AxisType; xAxisType?: AxisType;
xAxisTimeUnit: TimeUnit; xAxisTimeUnit?: TimeUnit;
yAxis?: string; yAxis: string | string[];
yAxisType?: AxisType; yAxisType?: AxisType;
fullWidth?: boolean; fullWidth?: boolean;
symbol?: string;
}; };
export function LineChart({ export function LineChart({
data = [], data,
fullWidth = false,
title = '', title = '',
xAxis = 'x', xAxis,
xAxisType = 'temporal', xAxisType = 'temporal',
xAxisTimeUnit = 'year', // TODO: defaults to undefined would probably work better... keeping it as it's for compatibility purposes xAxisTimeUnit = 'year', // TODO: defaults to undefined would probably work better... keeping it as it's for compatibility purposes
yAxis = 'y', yAxis,
yAxisType = 'quantitative', yAxisType = 'quantitative',
symbol,
}: LineChartProps) { }: LineChartProps) {
const url = data.url;
const values = data.values;
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
// By default, assumes data is an Array... // By default, assumes data is an Array...
const [specData, setSpecData] = useState<any>({ name: 'table' }); const [specData, setSpecData] = useState<any>({ name: 'table' });
const isMultiYAxis = Array.isArray(yAxis);
const spec = { const spec = {
$schema: 'https://vega.github.io/schema/vega-lite/v5.json', $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
@@ -44,6 +49,11 @@ export function LineChart({
tooltip: true, tooltip: true,
}, },
data: specData, data: specData,
...(isMultiYAxis
? {
transform: [{ fold: yAxis, as: ['key', 'value'] }],
}
: {}),
selection: { selection: {
grid: { grid: {
type: 'interval', type: 'interval',
@@ -57,20 +67,35 @@ export function LineChart({
type: xAxisType, type: xAxisType,
}, },
y: { y: {
field: yAxis, field: isMultiYAxis ? 'value' : yAxis,
type: yAxisType, type: yAxisType,
}, },
...(symbol
? {
color: {
field: symbol,
type: 'nominal',
},
}
: {}),
...(isMultiYAxis
? {
color: {
field: 'key',
type: 'nominal',
},
}
: {}),
}, },
} as any; } as any;
useEffect(() => { useEffect(() => {
// If data is string, assume it's a URL if (url) {
if (typeof data === 'string') {
setIsLoading(true); setIsLoading(true);
// Manualy loading the data allows us to do other kinds // Manualy loading the data allows us to do other kinds
// of stuff later e.g. load a file partially // of stuff later e.g. load a file partially
loadData(data).then((res: any) => { loadData(url).then((res: any) => {
setSpecData({ values: res, format: { type: 'csv' } }); setSpecData({ values: res, format: { type: 'csv' } });
setIsLoading(false); setIsLoading(false);
}); });
@@ -78,12 +103,8 @@ export function LineChart({
}, []); }, []);
var vegaData = {}; var vegaData = {};
if (Array.isArray(data)) { if (values) {
var dataObj; vegaData = { table: values };
dataObj = data.map((r) => {
return { x: r[0], y: r[1] };
});
vegaData = { table: dataObj };
} }
return isLoading ? ( return isLoading ? (
@@ -91,6 +112,6 @@ export function LineChart({
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
) : ( ) : (
<VegaLite fullWidth={fullWidth} data={vegaData} spec={spec} /> <VegaLite data={vegaData} spec={spec} />
); );
} }

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'; import { CSSProperties, useEffect, useState } from 'react';
import LoadingSpinner from './LoadingSpinner'; import LoadingSpinner from './LoadingSpinner';
import loadData from '../lib/loadData'; import loadData from '../lib/loadData';
import chroma from 'chroma-js'; import chroma from 'chroma-js';
import { GeospatialData } from '../types/properties';
import { import {
MapContainer, MapContainer,
TileLayer, TileLayer,
@@ -11,10 +12,34 @@ import {
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import * as L from 'leaflet'; import * as L from 'leaflet';
import providers from '../lib/tileLayerPresets';
type VariantKeys<T> = T extends { variants: infer V }
? {
[K in keyof V]: K extends string
? `${K}` | `${K}.${VariantKeys<V[K]>}`
: never;
}[keyof V]
: never;
type ProviderVariantKeys<T> = {
[K in keyof T]: K extends string
? `${K}` | `${K}.${VariantKeys<T[K]>}`
: never;
}[keyof T];
type TileLayerPreset = ProviderVariantKeys<typeof providers> | 'custom';
interface TileLayerSettings extends L.TileLayerOptions {
url?: string;
variant?: string | any;
}
export type MapProps = { export type MapProps = {
tileLayerName: TileLayerPreset;
tileLayerOptions?: TileLayerSettings | undefined;
layers: { layers: {
data: string | GeoJSON.GeoJSON; data: GeospatialData;
name: string; name: string;
colorScale?: { colorScale?: {
starting: string; starting: string;
@@ -25,14 +50,29 @@ export type MapProps = {
propNames: string[]; propNames: string[];
} }
| boolean; | boolean;
_id?: number;
}[]; }[];
title?: string; title?: string;
center?: { latitude: number | undefined; longitude: number | undefined }; center?: { latitude: number | undefined; longitude: number | undefined };
zoom?: number; zoom?: number;
style?: CSSProperties;
autoZoomConfiguration?: {
layerName: string;
};
}; };
const tileLayerDefaultName = process?.env
.NEXT_PUBLIC_MAP_TILE_LAYER_NAME as TileLayerPreset;
const tileLayerDefaultOptions = Object.keys(process?.env)
.filter((key) => key.startsWith('NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_'))
.reduce((obj, key) => {
obj[key.split('NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_')[1]] = process.env[key];
return obj;
}, {}) as TileLayerSettings;
export function Map({ export function Map({
tileLayerName = tileLayerDefaultName || 'OpenStreetMap',
tileLayerOptions,
layers = [ layers = [
{ {
data: null, data: null,
@@ -44,23 +84,116 @@ export function Map({
center = { latitude: 45, longitude: 45 }, center = { latitude: 45, longitude: 45 },
zoom = 2, zoom = 2,
title = '', title = '',
style = {},
autoZoomConfiguration = undefined,
}: MapProps) { }: MapProps) {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [layersData, setLayersData] = useState<any>([]); const [layersData, setLayersData] = useState<any>([]);
/*
tileLayerDefaultOptions
extract all environment variables thats starts with NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_.
the variables names are the same as the TileLayer object properties:
- NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_url:
- NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_attribution
- NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_accessToken
- NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_id
- NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_ext
- NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_bounds
- NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_maxZoom
- NEXT_PUBLIC_MAP_TILE_LAYER_OPTION_minZoom
see TileLayerOptions inteface
*/
//tileLayerData prioritizes properties passed through component over those passed through .env variables
tileLayerOptions = Object.assign(tileLayerDefaultOptions, tileLayerOptions);
let provider = {
url: tileLayerOptions.url,
options: tileLayerOptions,
};
if (tileLayerName != 'custom') {
var parts = tileLayerName.split('.');
var providerName = parts[0];
var variantName: string = parts[1];
//make sure to declare a variant if url depends on a variant: assume first
if (providers[providerName].url?.includes('{variant}') && !variantName)
variantName = Object.keys(providers[providerName].variants)[0];
if (!providers[providerName]) {
throw 'No such provider (' + providerName + ')';
}
provider = {
url: providers[providerName].url,
options: providers[providerName].options,
};
// overwrite values in provider from variant.
if (variantName && 'variants' in providers[providerName]) {
if (!(variantName in providers[providerName].variants)) {
throw 'No such variant of ' + providerName + ' (' + variantName + ')';
}
var variant = providers[providerName].variants[variantName];
var variantOptions;
if (typeof variant === 'string') {
variantOptions = {
variant: variant,
};
} else {
variantOptions = variant.options;
}
provider = {
url: variant.url || provider.url,
options: L.Util.extend({}, provider.options, variantOptions),
};
}
var attributionReplacer = function (attr) {
if (attr.indexOf('{attribution.') === -1) {
return attr;
}
return attr.replace(
/\{attribution.(\w*)\}/g,
function (match: any, attributionName: string) {
match;
return attributionReplacer(
providers[attributionName].options.attribution
);
}
);
};
provider.options.attribution = attributionReplacer(
provider.options.attribution
);
}
var tileLayerData = L.Util.extend(
{
url: provider.url,
},
provider.options,
tileLayerOptions
);
useEffect(() => { useEffect(() => {
const loadDataPromises = layers.map(async (layer) => { const loadDataPromises = layers.map(async (layer) => {
const url = layer.data.url;
const geojson = layer.data.geojson;
let layerData: any; let layerData: any;
if (typeof layer.data === 'string') { if (url) {
// If "data" is string, assume it's a URL // If "data" is string, assume it's a URL
setIsLoading(true); setIsLoading(true);
layerData = await loadData(layer.data).then((res: any) => { layerData = await loadData(url).then((res: any) => {
return JSON.parse(res); return JSON.parse(res);
}); });
} else { } else {
// Else, expect raw GeoJSON // Else, expect raw GeoJSON
layerData = layer.data; layerData = geojson;
} }
if (layer.colorScale) { if (layer.colorScale) {
@@ -92,10 +225,12 @@ export function Map({
</div> </div>
) : ( ) : (
<MapContainer <MapContainer
key={layersData}
center={[center.latitude, center.longitude]} center={[center.latitude, center.longitude]}
zoom={zoom} zoom={zoom}
scrollWheelZoom={false} scrollWheelZoom={false}
className="h-80 w-full" className="h-80 w-full"
style={style ?? {}}
// @ts-ignore // @ts-ignore
whenReady={(map: any) => { whenReady={(map: any) => {
// Enable zoom using scroll wheel // Enable zoom using scroll wheel
@@ -115,12 +250,28 @@ export function Map({
}; };
if (title) info.addTo(map.target); if (title) info.addTo(map.target);
if (!autoZoomConfiguration) return;
let layerToZoomBounds = L.latLngBounds(L.latLng(0, 0), L.latLng(0, 0));
layers.forEach((layer) => {
if (layer.name === autoZoomConfiguration.layerName) {
const data = layersData.find(
(layerData) => layerData.name === layer.name
)?.data;
if (data) {
layerToZoomBounds = L.geoJSON(data).getBounds();
return;
}
}
});
map.target.fitBounds(layerToZoomBounds);
}} }}
> >
<TileLayer {tileLayerData.url && <TileLayer {...tileLayerData} />}
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LayersControl position="bottomright"> <LayersControl position="bottomright">
{layers.map((layer) => { {layers.map((layer) => {
const data = layersData.find( const data = layersData.find(

View File

@@ -1,22 +1,24 @@
// Core viewer // Core viewer
import { Viewer, Worker, SpecialZoomLevel } from '@react-pdf-viewer/core'; import { Viewer, Worker, SpecialZoomLevel } from '@react-pdf-viewer/core';
import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout'; import { defaultLayoutPlugin } from '@react-pdf-viewer/default-layout';
import { Data } from '../types/properties';
// Import styles // Import styles
import '@react-pdf-viewer/core/lib/styles/index.css'; import '@react-pdf-viewer/core/lib/styles/index.css';
import '@react-pdf-viewer/default-layout/lib/styles/index.css'; import '@react-pdf-viewer/default-layout/lib/styles/index.css';
export interface PdfViewerProps { export interface PdfViewerProps {
url: string; data: Required<Pick<Data, 'url'>>;
layout: boolean; layout: boolean;
parentClassName?: string; parentClassName?: string;
} }
export function PdfViewer({ export function PdfViewer({
url, data,
layout = false, layout = false,
parentClassName, parentClassName = 'h-screen',
}: PdfViewerProps) { }: PdfViewerProps) {
const url = data.url;
const defaultLayoutPluginInstance = defaultLayoutPlugin(); const defaultLayoutPluginInstance = defaultLayoutPlugin();
return ( return (
<Worker workerUrl="https://unpkg.com/pdfjs-dist@2.15.349/build/pdf.worker.js"> <Worker workerUrl="https://unpkg.com/pdfjs-dist@2.15.349/build/pdf.worker.js">

View File

@@ -0,0 +1,9 @@
import Plot, { PlotParams } from "react-plotly.js";
export const Plotly: React.FC<PlotParams> = (props) => {
return (
<div>
<Plot {...props} />
</div>
);
};

View File

@@ -0,0 +1,153 @@
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import { Plotly } from './Plotly';
import Papa, { ParseConfig } from 'papaparse';
import LoadingSpinner from './LoadingSpinner';
import { Data } from '../types/properties';
const queryClient = new QueryClient();
async function getCsv(url: string, bytes: number) {
const response = await fetch(url, {
headers: {
Range: `bytes=0-${bytes}`,
},
});
const data = await response.text();
return data;
}
async function parseCsv(
file: string,
parsingConfig: ParseConfig
): Promise<any> {
return new Promise((resolve, reject) => {
Papa.parse(file, {
...parsingConfig,
header: true,
dynamicTyping: true,
skipEmptyLines: true,
transform: (value: string): string => {
return value.trim();
},
complete: (results: any) => {
return resolve(results);
},
error: (error: any) => {
return reject(error);
},
});
});
}
export interface PlotlyBarChartProps {
data: Data;
uniqueId?: number;
bytes?: number;
parsingConfig?: ParseConfig;
xAxis: string;
yAxis: string;
// TODO: commented out because this doesn't work. I believe
// this would only make any difference on charts with multiple
// traces.
// lineLabel?: string;
title?: string;
}
export const PlotlyBarChart: React.FC<PlotlyBarChartProps> = ({
data,
bytes = 5132288,
parsingConfig = {},
xAxis,
yAxis,
// lineLabel,
title = '',
}) => {
const uniqueId = Math.random();
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<PlotlyBarChartInner
data={data}
uniqueId={uniqueId}
bytes={bytes}
parsingConfig={parsingConfig}
xAxis={xAxis}
yAxis={yAxis}
// lineLabel={lineLabel ?? yAxis}
title={title}
/>
</QueryClientProvider>
);
};
const PlotlyBarChartInner: React.FC<PlotlyBarChartProps> = ({
data,
uniqueId,
bytes,
parsingConfig,
xAxis,
yAxis,
// lineLabel,
title,
}) => {
if (data.values) {
return (
<div className="w-full" style={{ height: '500px' }}>
<Plotly
layout={{
title,
}}
data={[
{
x: data.values.map((d) => d[xAxis]),
y: data.values.map((d) => d[yAxis]),
type: 'bar',
// name: lineLabel,
},
]}
/>
</div>
);
}
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
['dataCsv', data.url, uniqueId],
() => getCsv(data.url as string, bytes ?? 5132288),
{ enabled: !!data.url }
);
const { data: parsedData, isLoading: isParsing } = useQuery(
['dataPreview', csvString, uniqueId],
() =>
parseCsv(
data.csv ? (data.csv as string) : (csvString as string),
parsingConfig ?? {}
),
{ enabled: data.csv ? true : !!csvString }
);
if (isParsing || isDownloadingCSV)
<div className="w-full flex justify-center items-center h-[500px]">
<LoadingSpinner />
</div>;
if (parsedData)
return (
<div className="w-full" style={{ height: '500px' }}>
<Plotly
layout={{
title,
}}
data={[
{
x: parsedData.data.map((d: any) => d[xAxis]),
y: parsedData.data.map((d: any) => d[yAxis]),
type: 'bar',
// name: lineLabel, TODO: commented out because this doesn't work
},
]}
/>
</div>
);
return (
<div className="w-full flex justify-center items-center h-[500px]">
<LoadingSpinner />
</div>
);
};

View File

@@ -0,0 +1,155 @@
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
import { Plotly } from './Plotly';
import Papa, { ParseConfig } from 'papaparse';
import LoadingSpinner from './LoadingSpinner';
import { Data } from '../types/properties';
const queryClient = new QueryClient();
async function getCsv(url: string, bytes: number) {
const response = await fetch(url, {
headers: {
Range: `bytes=0-${bytes}`,
},
});
const data = await response.text();
return data;
}
async function parseCsv(
file: string,
parsingConfig: ParseConfig
): Promise<any> {
return new Promise((resolve, reject) => {
Papa.parse(file, {
...parsingConfig,
header: true,
dynamicTyping: true,
skipEmptyLines: true,
transform: (value: string): string => {
return value.trim();
},
complete: (results: any) => {
return resolve(results);
},
error: (error: any) => {
return reject(error);
},
});
});
}
export interface PlotlyLineChartProps {
data: Data;
bytes?: number;
parsingConfig?: ParseConfig;
xAxis: string;
yAxis: string;
lineLabel?: string;
title?: string;
uniqueId?: number;
}
export const PlotlyLineChart: React.FC<PlotlyLineChartProps> = ({
data,
bytes = 5132288,
parsingConfig = {},
xAxis,
yAxis,
lineLabel,
title = '',
uniqueId,
}) => {
uniqueId = uniqueId ?? Math.random();
return (
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<LineChartInner
data={data}
uniqueId={uniqueId}
bytes={bytes}
parsingConfig={parsingConfig}
xAxis={xAxis}
yAxis={yAxis}
lineLabel={lineLabel ?? yAxis}
title={title}
/>
</QueryClientProvider>
);
};
const LineChartInner: React.FC<PlotlyLineChartProps> = ({
data,
uniqueId,
bytes,
parsingConfig,
xAxis,
yAxis,
lineLabel,
title,
}) => {
const values = data.values;
const url = data.url;
const csv = data.csv;
if (values) {
return (
<div className="w-full" style={{ height: '500px' }}>
<Plotly
layout={{
title,
}}
data={[
{
x: values.map((d) => d[xAxis]),
y: values.map((d) => d[yAxis]),
mode: 'lines',
name: lineLabel,
},
]}
/>
</div>
);
}
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
['dataCsv', url, uniqueId],
() => getCsv(url as string, bytes ?? 5132288),
{ enabled: !!url }
);
const { data: parsedData, isLoading: isParsing } = useQuery(
['dataPreview', csvString, uniqueId],
() =>
parseCsv(
csv ? (csv as string) : (csvString as string),
parsingConfig ?? {}
),
{ enabled: csv ? true : !!csvString }
);
if (isParsing || isDownloadingCSV)
<div className="w-full flex justify-center items-center h-[500px]">
<LoadingSpinner />
</div>;
if (parsedData)
return (
<div className="w-full" style={{ height: '500px' }}>
<Plotly
layout={{
title,
}}
data={[
{
x: parsedData.data.map((d: any) => d[xAxis]),
y: parsedData.data.map((d: any) => d[yAxis]),
mode: 'lines',
name: lineLabel,
},
]}
/>
</div>
);
return (
<div className="w-full flex justify-center items-center h-[500px]">
<LoadingSpinner />
</div>
);
};

View File

@@ -6,6 +6,8 @@ import {
getFilteredRowModel, getFilteredRowModel,
getPaginationRowModel, getPaginationRowModel,
getSortedRowModel, getSortedRowModel,
PaginationState,
Table as ReactTable,
useReactTable, useReactTable,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
@@ -25,12 +27,19 @@ import DebouncedInput from './DebouncedInput';
import loadData from '../lib/loadData'; import loadData from '../lib/loadData';
import LoadingSpinner from './LoadingSpinner'; import LoadingSpinner from './LoadingSpinner';
export type TableData = { cols: {key: string, name: string}[]; data: any[]; total: number };
export type TableProps = { export type TableProps = {
data?: Array<{ [key: string]: number | string }>; data?: Array<{ [key: string]: number | string }>;
cols?: Array<{ [key: string]: string }>; cols?: Array<{ [key: string]: string }>;
csv?: string; csv?: string;
url?: string; url?: string;
fullWidth?: boolean; fullWidth?: boolean;
datastoreConfig?: {
dataStoreURI: string;
rowsPerPage?: number;
dataMapperFn: (data) => Promise<TableData> | TableData;
};
}; };
export const Table = ({ export const Table = ({
@@ -39,8 +48,28 @@ export const Table = ({
csv = '', csv = '',
url = '', url = '',
fullWidth = false, fullWidth = false,
datastoreConfig,
}: TableProps) => { }: TableProps) => {
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [pageMap, setPageMap] = useState(new Map<number, boolean>());
const {
dataMapperFn,
dataStoreURI,
rowsPerPage = 10,
} = datastoreConfig ?? {};
const [globalFilter, setGlobalFilter] = useState('');
const [isLoadingPage, setIsLoadingPage] = useState<boolean>(false);
const [totalOfRows, setTotalOfRows] = useState<number>(0);
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: rowsPerPage,
});
const [lastIndex, setLastIndex] = useState(pageSize);
const [startIndex, setStartIndex] = useState(0);
const [hasSorted, setHasSorted] = useState(false);
if (csv) { if (csv) {
const out = parseCsv(csv); const out = parseCsv(csv);
@@ -62,21 +91,56 @@ export const Table = ({
); );
}, [data, cols]); }, [data, cols]);
const [globalFilter, setGlobalFilter] = useState(''); let table: ReactTable<unknown>;
const table = useReactTable({ if (datastoreConfig) {
data, useEffect(() => {
columns: tableCols, setIsLoading(true);
getCoreRowModel: getCoreRowModel(), fetch(`${dataStoreURI}&limit=${rowsPerPage}&offset=0`)
state: { .then((res) => res.json())
globalFilter, .then(async (res) => {
}, const { data, cols, total } = await dataMapperFn(res);
globalFilterFn: globalFilterFn, setData(data);
onGlobalFilterChange: setGlobalFilter, setCols(cols);
getFilteredRowModel: getFilteredRowModel(), setTotalOfRows(Math.ceil(total / rowsPerPage));
getPaginationRowModel: getPaginationRowModel(), pageMap.set(0, true);
getSortedRowModel: getSortedRowModel(), })
}); .finally(() => setIsLoading(false));
}, [dataStoreURI]);
table = useReactTable({
data,
pageCount: totalOfRows,
columns: tableCols,
getCoreRowModel: getCoreRowModel(),
state: {
pagination: { pageIndex, pageSize },
},
getFilteredRowModel: getFilteredRowModel(),
manualPagination: true,
onPaginationChange: setPagination,
getSortedRowModel: getSortedRowModel(),
});
useEffect(() => {
if (!hasSorted) return;
queryDataByText(globalFilter);
}, [table.getState().sorting]);
} else {
table = useReactTable({
data,
columns: tableCols,
getCoreRowModel: getCoreRowModel(),
state: {
globalFilter,
},
globalFilterFn: globalFilterFn,
onGlobalFilterChange: setGlobalFilter,
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
}
useEffect(() => { useEffect(() => {
if (url) { if (url) {
@@ -91,6 +155,70 @@ export const Table = ({
} }
}, [url]); }, [url]);
const queryDataByText = (filter) => {
setIsLoadingPage(true);
const sortedParam = getSortParam();
fetch(
`${dataStoreURI}&limit=${rowsPerPage}&offset=0&q=${filter}${sortedParam}`
)
.then((res) => res.json())
.then(async (res) => {
const { data, total = 0 } = await dataMapperFn(res);
setTotalOfRows(Math.ceil(total / rowsPerPage));
setData(data);
const newMap = new Map();
newMap.set(0, true);
setPageMap(newMap);
table.setPageIndex(0);
setStartIndex(0);
setLastIndex(pageSize);
})
.finally(() => setIsLoadingPage(false));
};
const getSortParam = () => {
const sort = table.getState().sorting;
return sort.length == 0
? ``
: '&sort=' +
sort
.map(
(x, i) =>
`${x.id}${
i === sort.length - 1 ? (x.desc ? ` desc` : ` asc`) : `,`
}`
)
.reduce((x1, x2) => x1 + x2);
};
const queryPaginatedData = (newPageIndex) => {
let newStartIndex = newPageIndex * pageSize;
setStartIndex(newStartIndex);
setLastIndex(newStartIndex + pageSize);
if (!pageMap.get(newPageIndex)) pageMap.set(newPageIndex, true);
else return;
const sortedParam = getSortParam();
setIsLoadingPage(true);
fetch(
`${dataStoreURI}&limit=${rowsPerPage}&offset=${
newStartIndex + pageSize
}&q=${globalFilter}${sortedParam}`
)
.then((res) => res.json())
.then(async (res) => {
const { data: responseData } = await dataMapperFn(res);
responseData.forEach((e) => {
data[newStartIndex] = e;
newStartIndex++;
});
setData([...data]);
})
.finally(() => setIsLoadingPage(false));
};
return isLoading ? ( return isLoading ? (
<div className="w-full h-full min-h-[500px] flex items-center justify-center"> <div className="w-full h-full min-h-[500px] flex items-center justify-center">
<LoadingSpinner /> <LoadingSpinner />
@@ -99,7 +227,10 @@ export const Table = ({
<div className={`${fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full'}`}> <div className={`${fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full'}`}>
<DebouncedInput <DebouncedInput
value={globalFilter ?? ''} value={globalFilter ?? ''}
onChange={(value: any) => setGlobalFilter(String(value))} onChange={(value: any) => {
if (datastoreConfig) queryDataByText(String(value));
setGlobalFilter(String(value));
}}
className="p-2 text-sm shadow border border-block" className="p-2 text-sm shadow border border-block"
placeholder="Search all columns..." placeholder="Search all columns..."
/> />
@@ -114,7 +245,10 @@ export const Table = ({
className: h.column.getCanSort() className: h.column.getCanSort()
? 'cursor-pointer select-none' ? 'cursor-pointer select-none'
: '', : '',
onClick: h.column.getToggleSortingHandler(), onClick: (v) => {
setHasSorted(true);
h.column.getToggleSortingHandler()(v);
},
}} }}
> >
{flexRender(h.column.columnDef.header, h.getContext())} {flexRender(h.column.columnDef.header, h.getContext())}
@@ -135,15 +269,28 @@ export const Table = ({
))} ))}
</thead> </thead>
<tbody> <tbody>
{table.getRowModel().rows.map((r) => ( {datastoreConfig && isLoadingPage ? (
<tr key={r.id} className="border-b border-b-slate-200"> <tr>
{r.getVisibleCells().map((c) => ( <td colSpan={cols.length} rowSpan={cols.length}>
<td key={c.id} className="py-2"> <div className="w-full h-full flex items-center justify-center pt-6">
{flexRender(c.column.columnDef.cell, c.getContext())} <LoadingSpinner />
</td> </div>
))} </td>
</tr> </tr>
))} ) : (
(datastoreConfig
? table.getRowModel().rows.slice(startIndex, lastIndex)
: table.getRowModel().rows
).map((r) => (
<tr key={r.id} className="border-b border-b-slate-200">
{r.getVisibleCells().map((c) => (
<td key={c.id} className="py-2">
{flexRender(c.column.columnDef.cell, c.getContext())}
</td>
))}
</tr>
))
)}
</tbody> </tbody>
</table> </table>
<div className="flex gap-2 items-center justify-center mt-10"> <div className="flex gap-2 items-center justify-center mt-10">
@@ -151,7 +298,10 @@ export const Table = ({
className={`w-6 h-6 ${ className={`w-6 h-6 ${
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100' !table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
}`} }`}
onClick={() => table.setPageIndex(0)} onClick={() => {
if (datastoreConfig) queryPaginatedData(0);
table.setPageIndex(0);
}}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
<ChevronDoubleLeftIcon /> <ChevronDoubleLeftIcon />
@@ -160,7 +310,12 @@ export const Table = ({
className={`w-6 h-6 ${ className={`w-6 h-6 ${
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100' !table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
}`} }`}
onClick={() => table.previousPage()} onClick={() => {
if (datastoreConfig) {
queryPaginatedData(table.getState().pagination.pageIndex - 1);
}
table.previousPage();
}}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage()}
> >
<ChevronLeftIcon /> <ChevronLeftIcon />
@@ -176,7 +331,11 @@ export const Table = ({
className={`w-6 h-6 ${ className={`w-6 h-6 ${
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100' !table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
}`} }`}
onClick={() => table.nextPage()} onClick={() => {
if (datastoreConfig)
queryPaginatedData(table.getState().pagination.pageIndex + 1);
table.nextPage();
}}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
<ChevronRightIcon /> <ChevronRightIcon />
@@ -185,7 +344,11 @@ export const Table = ({
className={`w-6 h-6 ${ className={`w-6 h-6 ${
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100' !table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
}`} }`}
onClick={() => table.setPageIndex(table.getPageCount() - 1)} onClick={() => {
const pageIndexToNavigate = table.getPageCount() - 1;
if (datastoreConfig) queryPaginatedData(pageIndexToNavigate);
table.setPageIndex(pageIndexToNavigate);
}}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage()}
> >
<ChevronDoubleRightIcon /> <ChevronDoubleRightIcon />

View File

@@ -1,6 +1,7 @@
// Wrapper for the Vega component // Wrapper for the Vega component
import { Vega as VegaOg } from "react-vega"; import { Vega as VegaOg } from "react-vega";
import { VegaProps } from "react-vega/lib/Vega";
export function Vega(props) { export function Vega(props: VegaProps) {
return <VegaOg {...props} />; return <VegaOg {...props} />;
} }

View File

@@ -1,8 +1,9 @@
// Wrapper for the Vega Lite component // Wrapper for the Vega Lite component
import { VegaLite as VegaLiteOg } from "react-vega"; import { VegaLite as VegaLiteOg } from 'react-vega';
import applyFullWidthDirective from "../lib/applyFullWidthDirective"; import { VegaLiteProps } from 'react-vega/lib/VegaLite';
import applyFullWidthDirective from '../lib/applyFullWidthDirective';
export function VegaLite(props) { export function VegaLite(props: VegaLiteProps) {
const Component = applyFullWidthDirective({ Component: VegaLiteOg }); const Component = applyFullWidthDirective({ Component: VegaLiteOg });
return <Component {...props} />; return <Component {...props} />;

View File

@@ -1,10 +1,17 @@
export * from './components/Table';
export * from './components/Catalog'; export * from './components/Catalog';
export * from './components/LineChart'; export * from './components/LineChart';
export * from './components/Vega'; export * from './components/Vega';
export * from './components/VegaLite'; export * from './components/VegaLite';
export * from './components/FlatUiTable'; export * from './components/FlatUiTable';
export * from './components/OpenLayers/OpenLayers';
export * from './components/Map'; export * from './components/Map';
export * from './components/PdfViewer'; export * from './components/PdfViewer';
export * from "./components/Excel"; export * from "./components/Excel";
export * from "./components/Iframe";
export * from "./components/Plotly";
export * from "./components/PlotlyLineChart";
export * from "./components/PlotlyBarChart";
// NOTE: components that are hidden for now
// TODO: deprecate those components?
// export * from './components/Table';
// export * from "./components/BucketViewer";
// export * from './components/OpenLayers/OpenLayers';

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,18 @@
/*
* All components should use this interface for
* its data property.
* Based on vega.
*
*/
type URL = string; // Just in case we want to transform it into an object with configurations
export interface Data {
url?: URL;
values?: { [key: string]: number | string }[];
csv?: string;
}
export interface GeospatialData {
url?: URL;
geojson?: GeoJSON.GeoJSON;
}

View File

@@ -0,0 +1,100 @@
// NOTE: this component was renamed with .bkp so that it's hidden
// from the Storybook app
import { type Meta, type StoryObj } from '@storybook/react';
import {
BucketViewer,
BucketViewerProps,
} from '../src/components/BucketViewer';
import LoadingSpinner from '../src/components/LoadingSpinner';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
title: 'Components/BucketViewer',
component: BucketViewer,
tags: ['autodocs'],
argTypes: {
domain: {
description: 'Bucket domain URI',
},
suffix: {
description: 'Suffix of bucket domain',
},
downloadConfig: {
description: `Bucket file download configuration`,
},
filterState: {
description: `State with values used to filter the bucket files`,
},
paginationConfig: {
description: `Configuration to show and stylise the pagination on the component`,
},
},
};
export default meta;
type Story = StoryObj<BucketViewerProps>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Normal: Story = {
name: 'Bucket viewer',
args: {
domain: 'https://ssen-smart-meter.datopian.workers.dev',
suffix: '/',
dataMapperFn: async (rawData: Response) => {
const result = await rawData.json();
return result.objects.map((e) => ({
downloadFileUri: e.downloadLink,
fileName: e.key.replace(/^(\w+\/)/g, ''),
dateProps: {
date: new Date(e.uploaded),
dateFormatter: (date) => date.toLocaleDateString(),
},
}));
},
},
};
export const WithPagination: Story = {
name: 'With pagination',
args: {
domain: 'https://ssen-smart-meter.datopian.workers.dev',
suffix: '/',
paginationConfig: {
itemsPerPage: 3,
},
dataMapperFn: async (rawData: Response) => {
const result = await rawData.json();
return result.objects.map((e) => ({
downloadFileUri: e.downloadLink,
fileName: e.key.replace(/^(\w+\/)/g, ''),
dateProps: {
date: new Date(e.uploaded),
dateFormatter: (date) => date.toLocaleDateString(),
},
}));
},
},
};
export const WithComponentOnHoverOfEachBucketFile: Story = {
name: 'With component on hover of each bucket file',
args: {
domain: 'https://ssen-smart-meter.datopian.workers.dev',
suffix: '/',
downloadConfig: { hoverOfTheFileComponent: `HOVER COMPONENT` },
dataMapperFn: async (rawData: Response) => {
const result = await rawData.json();
return result.objects.map((e) => ({
downloadFileUri: e.downloadLink,
fileName: e.key.replace(/^(\w+\/)/g, ''),
dateProps: {
date: new Date(e.uploaded),
dateFormatter: (date) => date.toLocaleDateString(),
},
}));
},
},
};

View File

@@ -10,11 +10,14 @@ const meta: Meta = {
argTypes: { argTypes: {
datasets: { datasets: {
description: description:
'Lists of datasets to be displayed in the list, will usually be automatically available', "Array of items to be displayed on the searchable list. Must have the following properties: \n\n \
`_id`: item's unique id \n\n \
`url_path`: href of the item \n\n \
`metadata`: object with a `title` property, that will be displayed as the title of the item, together with any other custom fields that might or not be faceted.",
}, },
facets: { facets: {
description: description:
'List of frontmatter fields that should be used as filters, needs to match exactly with the field name', "Array of strings, which are name of properties in the datasets' `metadata`, which are going to be faceted.",
}, },
}, },
}; };
@@ -31,99 +34,35 @@ export const WithoutFacets: Story = {
{ {
_id: '07026b22d49916754df1dc8ffb9ccd1c31878aae', _id: '07026b22d49916754df1dc8ffb9ccd1c31878aae',
url_path: 'dataset-4', url_path: 'dataset-4',
file_path: 'content/dataset-4/index.md',
metadata: { metadata: {
title: 'Detecting Abusive Albanian', title: 'Detecting Abusive Albanian',
'link-to-publication': 'https://arxiv.org/abs/2107.13592',
'link-to-data': 'https://doi.org/10.6084/m9.figshare.19333298.v1',
'task-description':
'Hierarchical (offensive/not; untargeted/targeted; person/group/other)',
'details-of-task':
'Detect and categorise abusive language in social media data',
'size-of-dataset': 11874,
'percentage-abusive': 13.2,
language: 'Albanian',
'level-of-annotation': ['Posts'],
platform: ['Instagram', 'Youtube'],
medium: ['Text'],
reference:
'Nurce, E., Keci, J., Derczynski, L., 2021. Detecting Abusive Albanian. arXiv:2107.13592',
}, },
}, },
{ {
_id: '42c86cf3c4fbbab11d91c2a7d6dcb8f750bc4e19', _id: '42c86cf3c4fbbab11d91c2a7d6dcb8f750bc4e19',
url_path: 'dataset-1', url_path: 'dataset-1',
file_path: 'content/dataset-1/index.md',
metadata: { metadata: {
title: 'AbuseEval v1.0', title: 'AbuseEval v1.0',
'link-to-publication':
'http://www.lrec-conf.org/proceedings/lrec2020/pdf/2020.lrec-1.760.pdf',
'link-to-data': 'https://github.com/tommasoc80/AbuseEval',
'task-description':
'Explicitness annotation of offensive and abusive content',
'details-of-task':
'Enriched versions of the OffensEval/OLID dataset with the distinction of explicit/implicit offensive messages and the new dimension for abusive messages. Labels for offensive language: EXPLICIT, IMPLICT, NOT; Labels for abusive language: EXPLICIT, IMPLICT, NOTABU',
'size-of-dataset': 14100,
'percentage-abusive': 20.75,
language: 'English',
'level-of-annotation': ['Tweets'],
platform: ['Twitter'],
medium: ['Text'],
reference:
'Caselli, T., Basile, V., Jelena, M., Inga, K., and Michael, G. 2020. "I feel offended, dont be abusive! implicit/explicit messages in offensive and abusive language". The 12th Language Resources and Evaluation Conference (pp. 6193-6202). European Language Resources Association.',
}, },
}, },
{ {
_id: '80001dd32a752421fdcc64e91fbd237dc31d6bb3', _id: '80001dd32a752421fdcc64e91fbd237dc31d6bb3',
url_path: 'dataset-2', url_path: 'dataset-2',
file_path: 'content/dataset-2/index.md',
metadata: { metadata: {
title: title:
'Abusive Language Detection on Arabic Social Media (Al Jazeera)', 'Abusive Language Detection on Arabic Social Media (Al Jazeera)',
'link-to-publication': 'https://www.aclweb.org/anthology/W17-3008',
'link-to-data':
'http://alt.qcri.org/~hmubarak/offensive/AJCommentsClassification-CF.xlsx',
'task-description':
'Ternary (Obscene, Offensive but not obscene, Clean)',
'details-of-task': 'Incivility',
'size-of-dataset': 32000,
'percentage-abusive': 0.81,
language: 'Arabic',
'level-of-annotation': ['Posts'],
platform: ['AlJazeera'],
medium: ['Text'],
reference:
'Mubarak, H., Darwish, K. and Magdy, W., 2017. Abusive Language Detection on Arabic Social Media. In: Proceedings of the First Workshop on Abusive Language Online. Vancouver, Canada: Association for Computational Linguistics, pp.52-56.',
}, },
}, },
{ {
_id: '96649d05d8193f4333b10015af76c6562971bd8c', _id: '96649d05d8193f4333b10015af76c6562971bd8c',
url_path: 'dataset-3', url_path: 'dataset-3',
file_path: 'content/dataset-3/index.md',
metadata: { metadata: {
title: 'CoRAL: a Context-aware Croatian Abusive Language Dataset', title: 'CoRAL: a Context-aware Croatian Abusive Language Dataset',
'link-to-publication':
'https://aclanthology.org/2022.findings-aacl.21/',
'link-to-data':
'https://github.com/shekharRavi/CoRAL-dataset-Findings-of-the-ACL-AACL-IJCNLP-2022',
'task-description':
'Multi-class based on context dependency categories (CDC)',
'details-of-task': 'Detectioning CDC from abusive comments',
'size-of-dataset': 2240,
'percentage-abusive': 100,
language: 'Croatian',
'level-of-annotation': ['Posts'],
platform: ['Posts'],
medium: ['Newspaper Comments'],
reference:
'Ravi Shekhar, Mladen Karan and Matthew Purver (2022). CoRAL: a Context-aware Croatian Abusive Language Dataset. Findings of the ACL: AACL-IJCNLP.',
}, },
}, },
], ],
}, },
}; };
;
export const WithFacets: Story = { export const WithFacets: Story = {
name: 'Catalog with facets', name: 'Catalog with facets',
args: { args: {
@@ -131,7 +70,6 @@ export const WithFacets: Story = {
{ {
_id: '07026b22d49916754df1dc8ffb9ccd1c31878aae', _id: '07026b22d49916754df1dc8ffb9ccd1c31878aae',
url_path: 'dataset-4', url_path: 'dataset-4',
file_path: 'content/dataset-4/index.md',
metadata: { metadata: {
title: 'Detecting Abusive Albanian', title: 'Detecting Abusive Albanian',
'link-to-publication': 'https://arxiv.org/abs/2107.13592', 'link-to-publication': 'https://arxiv.org/abs/2107.13592',
@@ -220,7 +158,6 @@ export const WithFacets: Story = {
}, },
}, },
], ],
facets: ['language', 'platform'] facets: ['language', 'platform'],
}, },
}; };
;

View File

@@ -4,13 +4,13 @@ import { Excel, ExcelProps } from '../src/components/Excel';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = { const meta: Meta = {
title: 'Components/Excel', title: 'Components/Tabular/Excel',
component: Excel, component: Excel,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
url: { data: {
description: description:
'Url of the file to be displayed e.g.: "https://url.to/data.csv"', 'Object with a `url` property pointing to the Excel file to be displayed, e.g.: `{ url: "https://url.to/data.csv" }`',
}, },
}, },
}; };
@@ -22,13 +22,17 @@ type Story = StoryObj<ExcelProps>;
export const SingleSheet: Story = { export const SingleSheet: Story = {
name: 'Excel file with just one sheet', name: 'Excel file with just one sheet',
args: { args: {
url: 'https://sheetjs.com/pres.xlsx', data: {
url: 'https://sheetjs.com/pres.xlsx',
},
}, },
}; };
export const MultipleSheet: Story = { export const MultipleSheet: Story = {
name: 'Excel file with multiple sheets', name: 'Excel file with multiple sheets',
args: { args: {
url: 'https://storage.portaljs.org/IC-Gantt-Chart-Project-Template-8857.xlsx', data: {
url: 'https://storage.portaljs.org/IC-Gantt-Chart-Project-Template-8857.xlsx',
},
}, },
}; };

View File

@@ -4,29 +4,31 @@ import { FlatUiTable, FlatUiTableProps } from '../src/components/FlatUiTable';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = { const meta: Meta = {
title: 'Components/FlatUiTable', title: 'Components/Tabular/FlatUiTable',
component: FlatUiTable, component: FlatUiTable,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
data: { data: {
description: description:
'Data to be displayed in the table, must be setup as an array of key value pairs', 'Data to be displayed. \n\n \
}, Must be an object with one of the following properties: `url`, `values` or `csv` \n\n \
csv: { `url`: URL pointing to a CSV file. \n\n \
description: 'CSV data as string.', `values`: array of objects. \n\n \
}, `csv`: string with valid CSV. \n\n \
url: { ',
description:
'Fetch the data from a CSV file remotely. only the first 5MB of data will be displayed',
}, },
bytes: { bytes: {
description: description:
'Fetch the data from a CSV file remotely. only the first <bytes> of data will be displayed', 'Fetch the data from a CSV file remotely. Only the first <bytes> of data will be displayed. Defaults to 5MB.',
}, },
parsingConfig: { parsingConfig: {
description: description:
'Configuration for parsing the CSV data. See https://www.papaparse.com/docs#config for more details', 'Configuration for parsing the CSV data. See https://www.papaparse.com/docs#config for more details',
}, },
uniqueId: {
description:
'Provide a unique ID to help with cache revalidation of the fetched data.',
},
}, },
}; };
@@ -36,34 +38,40 @@ type Story = StoryObj<FlatUiTableProps>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const FromColumnsAndData: Story = { export const FromColumnsAndData: Story = {
name: 'Table data', name: 'Table from array or objects',
args: { args: {
data: [ data: {
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 }, values: [
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 }, { id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 }, { id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 }, { id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 }, { id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 }, { id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 }, { id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
], { id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
],
},
}, },
}; };
export const FromRawCSV: Story = { export const FromRawCSV: Story = {
name: 'Table from raw CSV', name: 'Table from inline CSV',
args: { args: {
rawCsv: ` data: {
csv: `
Year,Temp Anomaly Year,Temp Anomaly
1850,-0.418 1850,-0.418
2020,0.923 2020,0.923
`, `,
},
}, },
}; };
export const FromURL: Story = { export const FromURL: Story = {
name: 'Table from URL', name: 'Table from URL',
args: { args: {
url: 'https://storage.openspending.org/alberta-budget/__os_imported__alberta_total.csv', data: {
url: 'https://storage.openspending.org/alberta-budget/__os_imported__alberta_total.csv',
},
}, },
}; };

View File

@@ -0,0 +1,33 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { Iframe, IframeProps } from '../src/components/Iframe';
const meta: Meta = {
title: 'Components/Embedding/Iframe',
component: Iframe,
tags: ['autodocs'],
argTypes: {
data: {
description:
'Object with a `url` property pointing to the page to be embeded.',
},
style: {
description:
'Style object of the component. See example at https://react.dev/learn#displaying-data. Defaults to `{ width: "100%", height: "100%" }`',
},
},
};
export default meta;
type Story = StoryObj<IframeProps>;
export const Normal: Story = {
name: 'Iframe',
args: {
data: {
url: 'https://app.powerbi.com/view?r=eyJrIjoiYzBmN2Q2MzYtYzE3MS00ODkxLWE5OWMtZTQ2MjBlMDljMDk4IiwidCI6Ijk1M2IwZjgzLTFjZTYtNDVjMy04MmM5LTFkODQ3ZTM3MjMzOSIsImMiOjh9',
},
style: { width: `100%`, height: `600px` },
},
};

View File

@@ -4,37 +4,40 @@ import { LineChart, LineChartProps } from '../src/components/LineChart';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = { const meta: Meta = {
title: 'Components/LineChart', title: 'Components/Charts/LineChart',
component: LineChart, component: LineChart,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
data: { data: {
description: description:
'Data to be displayed.\n\n E.g.: [["1990", 1], ["1991", 2]] \n\nOR\n\n "https://url.to/data.csv"', 'Data to be displayed. \n\n \
Must be an object with one of the following properties: `url` or `values` \n\n \
`url`: URL pointing to a CSV file. \n\n \
`values`: array of objects \n\n',
}, },
title: { title: {
description: 'Title to display on the chart. Optional.', description: 'Title to display on the chart.',
}, },
xAxis: { xAxis: {
description: description:
'Name of the X axis on the data. Required when the "data" parameter is an URL.', 'Name of the column header or object property that represents the X-axis on the data.',
}, },
xAxisType: { xAxisType: {
description: 'Type of the X axis', description: 'Type of the X-axis.',
}, },
xAxisTimeUnit: { xAxisTimeUnit: {
description: 'Time unit of the X axis (optional)', description: 'Time unit of the X-axis, in case its type is `temporal.`',
}, },
yAxis: { yAxis: {
description: description:
'Name of the Y axis on the data. Required when the "data" parameter is an URL.', 'Name of the column headers or object properties that represent the Y-axis on the data.',
}, },
yAxisType: { yAxisType: {
description: 'Type of the Y axis', description: 'Type of the Y-axis',
}, },
fullWidth: { symbol: {
description: description:
'Whether the component should be rendered as full bleed or not', 'Name of the column header or object property that represents a series for multiple series.',
}, },
}, },
}; };
@@ -47,21 +50,72 @@ type Story = StoryObj<LineChartProps>;
export const FromDataPoints: Story = { export const FromDataPoints: Story = {
name: 'Line chart from array of data points', name: 'Line chart from array of data points',
args: { args: {
data: [ data: {
['1850', -0.41765878], values: [
['1851', -0.2333498], { year: '1850', value: -0.41765878 },
['1852', -0.22939907], { year: '1851', value: -0.2333498 },
['1853', -0.27035445], { year: '1852', value: -0.22939907 },
['1854', -0.29163003], { year: '1853', value: -0.27035445 },
], { year: '1854', value: -0.29163003 },
],
},
xAxis: 'year',
yAxis: 'value',
},
};
export const MultiSeries: Story = {
name: 'Line chart with multiple series (specifying symbol)',
args: {
data: {
values: [
{ year: '1850', value: -0.41765878, z: 'A' },
{ year: '1851', value: -0.2333498, z: 'A' },
{ year: '1852', value: -0.22939907, z: 'A' },
{ year: '1853', value: -0.27035445, z: 'A' },
{ year: '1854', value: -0.29163003, z: 'A' },
{ year: '1850', value: -0.42993882, z: 'B' },
{ year: '1851', value: -0.30365549, z: 'B' },
{ year: '1852', value: -0.27905189, z: 'B' },
{ year: '1853', value: -0.22939704, z: 'B' },
{ year: '1854', value: -0.25688013, z: 'B' },
{ year: '1850', value: -0.4757164, z: 'C' },
{ year: '1851', value: -0.41971018, z: 'C' },
{ year: '1852', value: -0.40724799, z: 'C' },
{ year: '1853', value: -0.45049156, z: 'C' },
{ year: '1854', value: -0.41896583, z: 'C' },
],
},
xAxis: 'year',
yAxis: 'value',
symbol: 'z',
},
};
export const MultiColumns: Story = {
name: 'Line chart with multiple series (with multiple columns)',
args: {
data: {
values: [
{ year: '1850', A: -0.41765878, B: -0.42993882, C: -0.4757164 },
{ year: '1851', A: -0.2333498, B: -0.30365549, C: -0.41971018 },
{ year: '1852', A: -0.22939907, B: -0.27905189, C: -0.40724799 },
{ year: '1853', A: -0.27035445, B: -0.22939704, C: -0.45049156 },
{ year: '1854', A: -0.29163003, B: -0.25688013, C: -0.41896583 },
],
},
xAxis: 'year',
yAxis: ['A', 'B', 'C'],
}, },
}; };
export const FromURL: Story = { export const FromURL: Story = {
name: 'Line chart from URL', name: 'Line chart from URL',
args: { args: {
data: {
url: 'https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv',
},
title: 'Oil Price x Year', title: 'Oil Price x Year',
data: 'https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv',
xAxis: 'Date', xAxis: 'Date',
yAxis: 'Price', yAxis: 'Price',
}, },

View File

@@ -4,22 +4,33 @@ import { Map, MapProps } from '../src/components/Map';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = { const meta: Meta = {
title: 'Components/Map', title: 'Components/Geospatial/Map',
component: Map, component: Map,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
layers: { layers: {
description: description:
'Data to be displayed.\n\n GeoJSON Object \n\nOR\n\n URL to GeoJSON Object', 'Array of layers to be displayed on the map. Should be an object with: \n\n \
`data`: object with either a `url` property pointing to a GeoJSON file or a `geojson` property with a GeoJSON object. \n\n \
`name`: name of the layer. \n\n \
`colorscale`: object with a `starting` and `ending` colors that will be used to create a gradient and color the map. \n\n \
`tooltip`: `true` to show all available features on the tooltip, object with a `propNames` property as an array of strings to choose which features to display. \n\n',
}, },
title: { title: {
description: 'Title to display on the map. Optional.', description: 'Title to display on the map.',
}, },
center: { center: {
description: 'Initial coordinates of the center of the map', description: 'Initial coordinates of the center of the map',
}, },
zoom: { zoom: {
description: 'Zoom level', description: 'Initial zoom level',
},
style: {
description: "CSS styles to be applied to the map's container.",
},
autoZoomConfiguration: {
description:
"Pass a layer's name to automatically zoom to the bounding area of a layer.",
}, },
}, },
}; };
@@ -32,9 +43,15 @@ type Story = StoryObj<MapProps>;
export const GeoJSONPolygons: Story = { export const GeoJSONPolygons: Story = {
name: 'GeoJSON polygons map', name: 'GeoJSON polygons map',
args: { args: {
tileLayerName:'MapBox',
tileLayerOptions:{
accessToken : 'pk.eyJ1Ijoid2lsbHktcGFsbWFyZWpvIiwiYSI6ImNqNzk5NmRpNDFzb2cyeG9sc2luMHNjajUifQ.lkoVRFSI8hOLH4uJeOzwXw',
},
layers: [ layers: [
{ {
data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson', data: {
url: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
},
name: 'Polygons', name: 'Polygons',
tooltip: { propNames: ['name'] }, tooltip: { propNames: ['name'] },
colorScale: { colorScale: {
@@ -54,7 +71,9 @@ export const GeoJSONPoints: Story = {
args: { args: {
layers: [ layers: [
{ {
data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson', data: {
url: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
},
name: 'Points', name: 'Points',
tooltip: { propNames: ['Location'] }, tooltip: { propNames: ['Location'] },
}, },
@@ -70,12 +89,16 @@ export const GeoJSONMultipleLayers: Story = {
args: { args: {
layers: [ layers: [
{ {
data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson', data: {
url: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
},
name: 'Points', name: 'Points',
tooltip: true, tooltip: true,
}, },
{ {
data: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson', data: {
url: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
},
name: 'Polygons', name: 'Polygons',
tooltip: true, tooltip: true,
colorScale: { colorScale: {
@@ -89,3 +112,35 @@ export const GeoJSONMultipleLayers: Story = {
zoom: 2, zoom: 2,
}, },
}; };
export const GeoJSONMultipleLayersWithAutoZoomInSpecifiedLayer: Story = {
name: 'GeoJSON polygons and points map with auto zoom in the points layer',
args: {
layers: [
{
data: {
url: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
},
name: 'Points',
tooltip: true,
},
{
data: {
url: 'https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_geography_marine_polys.geojson',
},
name: 'Polygons',
tooltip: true,
colorScale: {
starting: '#ff0000',
ending: '#00ff00',
},
},
],
title: 'Polygons and points',
center: { latitude: 45, longitude: 0 },
zoom: 2,
autoZoomConfiguration: {
layerName: 'Points',
},
},
};

View File

@@ -1,3 +1,6 @@
// NOTE: this component was renamed with .bkp so that it's hidden
// from the Storybook app
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import React from 'react'; import React from 'react';
import OpenLayers from '../src/components/OpenLayers/OpenLayers'; import OpenLayers from '../src/components/OpenLayers/OpenLayers';

View File

@@ -3,19 +3,21 @@ import type { Meta, StoryObj } from '@storybook/react';
import { PdfViewer, PdfViewerProps } from '../src/components/PdfViewer'; import { PdfViewer, PdfViewerProps } from '../src/components/PdfViewer';
const meta: Meta = { const meta: Meta = {
title: 'Components/PdfViewer', title: 'Components/Embedding/PdfViewer',
component: PdfViewer, component: PdfViewer,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
url: { data: {
description: 'URL to PDF file', description:
'Object with a `url` property pointing to the PDF file to be displayed, e.g.: `{ url: "https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK" }`.',
}, },
parentClassName: { parentClassName: {
description: 'Classname for the parent div of the pdf viewer',
},
layour: {
description: description:
'Set to true if you want to have a layout with zoom level, page count, printing button etc', 'HTML classes to be applied to the container of the PDF viewer. [Tailwind](https://tailwindcss.com/) classes, such as `h-96` to define the height of the component, can be used on this field.',
},
layout: {
description:
'Set to `true` if you want to display a layout with zoom level, page count, printing button and other controls.',
defaultValue: false, defaultValue: false,
}, },
}, },
@@ -25,26 +27,23 @@ export default meta;
type Story = StoryObj<PdfViewerProps>; type Story = StoryObj<PdfViewerProps>;
export const PdfViewerStory: Story = { export const PdfViewerStoryWithoutControlsLayout: Story = {
name: 'PdfViewer', name: 'PDF Viewer without controls layout',
args: { args: {
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK', data: {
}, url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK',
}; },
parentClassName: 'h-96',
export const PdfViewerStoryWithLayout: Story = { },
name: 'PdfViewer with the default layout', };
args: {
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK', export const PdfViewerStoryWithControlsLayout: Story = {
layout: true, name: 'PdfViewer with controls layout',
}, args: {
}; data: {
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK',
export const PdfViewerStoryWithHeight: Story = { },
name: 'PdfViewer with a custom height', layout: true,
args: {
url: 'https://cdn.filestackcontent.com/wcrjf9qPTCKXV3hMXDwK',
parentClassName: 'h-96', parentClassName: 'h-96',
layout: true,
}, },
}; };

View File

@@ -0,0 +1,49 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Plotly } from '../src/components/Plotly';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
title: 'Components/Charts/Plotly',
component: Plotly,
tags: ['autodocs'],
argTypes: {
data: {
description:
"Plotly's `data` prop. You can find references on how to use these props at https://github.com/plotly/react-plotly.js/#basic-props.",
},
layout: {
description:
"Plotly's `layout` prop. You can find references on how to use these props at https://github.com/plotly/react-plotly.js/#basic-props.",
},
},
};
export default meta;
type Story = StoryObj<any>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Primary: Story = {
name: 'Line chart',
args: {
data: [
{
x: [1, 2, 3],
y: [2, 6, 3],
type: 'scatter',
mode: 'lines+markers',
marker: { color: 'red' },
},
],
layout: {
title: 'Chart built with Plotly',
xaxis: {
title: 'x Axis',
},
yaxis: {
title: 'y Axis',
},
},
},
};

View File

@@ -0,0 +1,102 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
PlotlyBarChart,
PlotlyBarChartProps,
} from '../src/components/PlotlyBarChart';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
title: 'Components/Charts/PlotlyBarChart',
component: PlotlyBarChart,
tags: ['autodocs'],
argTypes: {
data: {
description:
'Data to be displayed. \n\n \
Must be an object with one of the following properties: `url`, `values` or `csv` \n\n \
`url`: URL pointing to a CSV file. \n\n \
`values`: array of objects (check out [this example](/?path=/story/components-plotlybarchart--from-data-points)) \n\n \
`csv`: string with valid CSV (check out [this example](/?path=/story/components-plotlybarchart--from-inline-csv)) \n\n \
',
},
bytes: {
// TODO: likely this should be an extra option on the data parameter,
// specific to URLs
description:
"How many bytes to read from the url so that the entire file doesn's have to be fetched.",
},
parsingConfig: {
description:
'If using URL or CSV, this parsing config will be used to parse the data. Check https://www.papaparse.com/ for more info.',
},
title: {
description: 'Title to display on the chart.',
},
// TODO: commented out because this doesn't work
// lineLabel: {
// description:
// 'Label to display on the line, Optional, will use yAxis if not provided',
// },
xAxis: {
description:
'Name of the column header or object property that represents the X-axis on the data.',
},
yAxis: {
description:
'Name of the column header or object property that represents the Y-axis on the data.',
},
uniqueId: {
description: 'Provide a unique ID to help with cache revalidation of the fetched data.'
}
},
};
export default meta;
type Story = StoryObj<PlotlyBarChartProps>;
export const FromDataPoints: Story = {
name: 'Bar chart from array of data points',
args: {
data: {
values: [
{ year: '1850', temperature: -0.41765878 },
{ year: '1851', temperature: -0.2333498 },
{ year: '1852', temperature: -0.22939907 },
{ year: '1853', temperature: -0.27035445 },
{ year: '1854', temperature: -0.29163003 },
],
},
xAxis: 'year',
yAxis: 'temperature',
},
};
export const FromURL: Story = {
name: 'Bar chart from URL',
args: {
title: 'Apple Stock Prices',
data: {
url: 'https://raw.githubusercontent.com/plotly/datasets/master/finance-charts-apple.csv',
},
xAxis: 'Date',
yAxis: 'AAPL.Open',
},
};
export const FromInlineCSV: Story = {
name: 'Bar chart from inline CSV',
args: {
title: 'Apple Stock Prices',
data: {
csv: `Date,AAPL.Open,AAPL.High,AAPL.Low,AAPL.Close,AAPL.Volume,AAPL.Adjusted,dn,mavg,up,direction
2015-02-17,127.489998,128.880005,126.919998,127.830002,63152400,122.905254,106.7410523,117.9276669,129.1142814,Increasing
2015-02-18,127.629997,128.779999,127.449997,128.720001,44891700,123.760965,107.842423,118.9403335,130.0382439,Increasing
2015-02-19,128.479996,129.029999,128.330002,128.449997,37362400,123.501363,108.8942449,119.8891668,130.8840887,Decreasing
2015-02-20,128.619995,129.5,128.050003,129.5,48948400,124.510914,109.7854494,120.7635001,131.7415509,Increasing`,
},
xAxis: 'Date',
yAxis: 'AAPL.Open',
},
};

View File

@@ -0,0 +1,101 @@
import type { Meta, StoryObj } from '@storybook/react';
import {
PlotlyLineChart,
PlotlyLineChartProps,
} from '../src/components/PlotlyLineChart';
const meta: Meta = {
title: 'Components/Charts/PlotlyLineChart',
component: PlotlyLineChart,
tags: ['autodocs'],
argTypes: {
data: {
description:
'Data to be displayed. \n\n \
Must be an object with one of the following properties: `url`, `values` or `csv` \n\n \
`url`: URL pointing to a CSV file. \n\n \
`values`: array of objects. \n\n \
`csv`: string with valid CSV. \n\n \
',
},
bytes: {
// TODO: likely this should be an extra option on the data parameter,
// specific to URLs
description:
"How many bytes to read from the url so that the entire file doesn's have to be fetched.",
},
parsingConfig: {
description:
'If using URL or CSV, this parsing config will be used to parse the data. Check https://www.papaparse.com/ for more info',
},
title: {
description: 'Title to display on the chart.',
},
lineLabel: {
description:
'Label to display on the line, will use yAxis if not provided',
},
xAxis: {
description:
'Name of the column header or object property that represents the X-axis on the data.',
},
yAxis: {
description:
'Name of the column header or object property that represents the Y-axis on the data.',
},
uniqueId: {
description:
'Provide a unique ID to help with cache revalidation of the fetched data.',
},
},
};
export default meta;
type Story = StoryObj<PlotlyLineChartProps>;
export const FromDataPoints: Story = {
name: 'Line chart from array of data points',
args: {
data: {
values: [
{ year: '1850', temperature: -0.41765878 },
{ year: '1851', temperature: -0.2333498 },
{ year: '1852', temperature: -0.22939907 },
{ year: '1853', temperature: -0.27035445 },
{ year: '1854', temperature: -0.29163003 },
],
},
xAxis: 'year',
yAxis: 'temperature',
},
};
export const FromURL: Story = {
name: 'Line chart from URL',
args: {
title: 'Oil Price x Year',
data: {
url: 'https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv',
},
xAxis: 'Date',
yAxis: 'Price',
},
};
export const FromInlineCSV: Story = {
name: 'Bar chart from inline CSV',
args: {
title: 'Apple Stock Prices',
data: {
csv: `Date,AAPL.Open,AAPL.High,AAPL.Low,AAPL.Close,AAPL.Volume,AAPL.Adjusted,dn,mavg,up,direction
2015-02-17,127.489998,128.880005,126.919998,127.830002,63152400,122.905254,106.7410523,117.9276669,129.1142814,Increasing
2015-02-18,127.629997,128.779999,127.449997,128.720001,44891700,123.760965,107.842423,118.9403335,130.0382439,Increasing
2015-02-19,128.479996,129.029999,128.330002,128.449997,37362400,123.501363,108.8942449,119.8891668,130.8840887,Decreasing
2015-02-20,128.619995,129.5,128.050003,129.5,48948400,124.510914,109.7854494,120.7635001,131.7415509,Increasing`,
},
xAxis: 'Date',
yAxis: 'AAPL.Open',
},
};

View File

@@ -1,25 +1,33 @@
// NOTE: this component was renamed with .bkp so that it's hidden
// from the Storybook app
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { Table, TableProps } from '../src/components/Table'; import { Table, TableProps } from '../src/components/Table';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = { const meta: Meta = {
title: 'Components/Table', title: 'Components/Tabular/Table',
component: Table, component: Table,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
data: { data: {
description: "Data to be displayed in the table, must also set \"cols\" to work." description:
'Data to be displayed in the table, must also set "cols" to work.',
}, },
cols: { cols: {
description: "Columns to be displayed in the table, must also set \"data\" to work." description:
'Columns to be displayed in the table, must also set "data" to work.',
}, },
csv: { csv: {
description: "CSV data as string.", description: 'CSV data as string.',
}, },
url: { url: {
description: "Fetch the data from a CSV file remotely." description: 'Fetch the data from a CSV file remotely.',
} },
datastoreConfig: {
description: `Configuration to use CKAN's datastore API extension integrated with the component`,
},
}, },
}; };
@@ -29,7 +37,7 @@ type Story = StoryObj<TableProps>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const FromColumnsAndData: Story = { export const FromColumnsAndData: Story = {
name: "Table from columns and data", name: 'Table from columns and data',
args: { args: {
data: [ data: [
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 }, { id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
@@ -49,21 +57,40 @@ export const FromColumnsAndData: Story = {
}, },
}; };
export const WithDataStoreIntegration: Story = {
name: 'Table with datastore integration',
args: {
datastoreConfig: {
dataStoreURI: `https://www.civicdata.com/api/action/datastore_search?resource_id=46ec0807-31ff-497f-bfa0-f31c796cdee8`,
dataMapperFn: ({
result,
}: {
result: { fields: { id }[]; records: []; total: number };
}) => {
return {
data: result.records,
cols: result.fields.map((x) => ({ key: x.id, name: x.id })),
total: result.total,
};
},
},
},
};
export const FromRawCSV: Story = { export const FromRawCSV: Story = {
name: "Table from raw CSV", name: 'Table from raw CSV',
args: { args: {
csv: ` csv: `
Year,Temp Anomaly Year,Temp Anomaly
1850,-0.418 1850,-0.418
2020,0.923 2020,0.923
` `,
} },
}; };
export const FromURL: Story = { export const FromURL: Story = {
name: "Table from URL", name: 'Table from URL',
args: { args: {
url: "https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv" url: 'https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv',
} },
}; };

View File

@@ -4,9 +4,19 @@ import { Vega } from '../src/components/Vega';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = { const meta: Meta = {
title: 'Components/Vega', title: 'Components/Charts/Vega',
component: Vega, component: Vega,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: {
data: {
description:
"Vega's `data` prop. You can find references on how to use this prop at https://vega.github.io/vega/docs/data/",
},
spec: {
description:
"Vega's `spec` prop. You can find references on how to use this prop at https://vega.github.io/vega/docs/specification/",
},
},
}; };
export default meta; export default meta;
@@ -15,7 +25,7 @@ type Story = StoryObj<any>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Primary: Story = { export const Primary: Story = {
name: 'Chart built with Vega', name: 'Bar chart',
args: { args: {
data: { data: {
table: [ table: [

View File

@@ -4,7 +4,7 @@ import { VegaLite } from '../src/components/VegaLite';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = { const meta: Meta = {
title: 'Components/VegaLite', title: 'Components/Charts/VegaLite',
component: VegaLite, component: VegaLite,
tags: ['autodocs'], tags: ['autodocs'],
argTypes: { argTypes: {
@@ -25,7 +25,7 @@ type Story = StoryObj<any>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Primary: Story = { export const Primary: Story = {
name: 'Chart built with Vega Lite', name: 'Bar chart',
args: { args: {
data: { data: {
table: [ table: [

View File

@@ -1,6 +1,6 @@
{ {
"name": "@portaljs/core", "name": "@portaljs/core",
"version": "1.0.8", "version": "1.0.9",
"description": "Core Portal.JS components, configs and utils.", "description": "Core Portal.JS components, configs and utils.",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -53,7 +53,7 @@ export const Nav: React.FC<Props> = ({
<nav className="flex justify-between"> <nav className="flex justify-between">
{/* Mobile navigation */} {/* Mobile navigation */}
<div className="mr-2 sm:mr-4 flex lg:hidden"> <div className="mr-2 sm:mr-4 flex lg:hidden">
<NavMobile links={links}>{children}</NavMobile> <NavMobile {...{title, links, social, search, defaultTheme, themeToggleIcon}}>{children}</NavMobile>
</div> </div>
{/* Non-mobile navigation */} {/* Non-mobile navigation */}
<div className="flex flex-none items-center"> <div className="flex flex-none items-center">

View File

@@ -4,20 +4,16 @@ import { useRouter } from "next/router.js";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { SearchContext, SearchField } from "../Search"; import { SearchContext, SearchField } from "../Search";
import { MenuIcon, CloseIcon } from "../Icons"; import { MenuIcon, CloseIcon } from "../Icons";
import { NavLink, SearchProviderConfig } from "../types"; import type { NavConfig, ThemeConfig } from "./Nav";
interface Props extends React.PropsWithChildren { interface Props extends NavConfig, ThemeConfig, React.PropsWithChildren {}
author?: string;
links?: Array<NavLink>;
search?: SearchProviderConfig;
}
// TODO why mobile navigation only accepts author and regular nav accepts different things like title, logo, version // TODO: Search doesn't appear
export const NavMobile: React.FC<Props> = ({ export const NavMobile: React.FC<Props> = ({
children, children,
title,
links, links,
search, search,
author,
}) => { }) => {
const router = useRouter(); const router = useRouter();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@@ -77,8 +73,8 @@ export const NavMobile: React.FC<Props> = ({
legacyBehavior legacyBehavior
> >
{/* <Logomark className="h-9 w-9" /> */} {/* <Logomark className="h-9 w-9" /> */}
<div className="font-extrabold text-primary dark:text-primary-dark text-2xl ml-6"> <div className="font-extrabold text-primary dark:text-primary-dark text-lg ml-6">
{author} {title}
</div> </div>
</Link> </Link>
</div> </div>
@@ -106,9 +102,7 @@ export const NavMobile: React.FC<Props> = ({
))} ))}
</ul> </ul>
)} )}
{/* <div className="pt-6 border border-t-2"> <div className="pt-6">{children}</div>
{children}
</div> */}
</Dialog.Panel> </Dialog.Panel>
</Dialog> </Dialog>
</> </>

View File

@@ -46,8 +46,8 @@ export const SiteToc: React.FC<Props> = ({ currentPath, nav }) => {
return ( return (
<nav data-testid="lhs-sidebar" className="flex flex-col space-y-3 text-sm"> <nav data-testid="lhs-sidebar" className="flex flex-col space-y-3 text-sm">
{sortNavGroupChildren(nav).map((n) => ( {sortNavGroupChildren(nav).map((n, index) => (
<NavComponent item={n} isActive={false} /> <NavComponent key={index} item={n} isActive={false} />
))} ))}
</nav> </nav>
); );
@@ -96,8 +96,8 @@ const NavComponent: React.FC<{
leaveTo="transform scale-95 opacity-0" leaveTo="transform scale-95 opacity-0"
> >
<Disclosure.Panel className="flex flex-col space-y-3 pl-5 mt-3"> <Disclosure.Panel className="flex flex-col space-y-3 pl-5 mt-3">
{sortNavGroupChildren(item.children).map((subItem) => ( {sortNavGroupChildren(item.children).map((subItem, index) => (
<NavComponent item={subItem} isActive={false} /> <NavComponent key={index} item={subItem} isActive={false} />
))} ))}
</Disclosure.Panel> </Disclosure.Panel>
</Transition> </Transition>

View File

@@ -0,0 +1,36 @@
import Script from 'next/script.js'
export interface GoogleAnalyticsProps {
googleAnalyticsId: string
}
export const GA = ({ googleAnalyticsId }: GoogleAnalyticsProps) => {
return (
<>
<Script
strategy="afterInteractive"
src={`https://www.googletagmanager.com/gtag/js?id=${googleAnalyticsId}`}
/>
<Script strategy="afterInteractive" id="ga-script">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${googleAnalyticsId}');
`}
</Script>
</>
)
}
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const logEvent = (action, category, label, value) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.gtag?.('event', action, {
event_category: category,
event_label: label,
value: value,
})
}

View File

@@ -0,0 +1,41 @@
import Script from 'next/script.js'
export interface PlausibleProps {
plausibleDataDomain: string
dataApi?: string
src?: string
}
/**
* Plausible analytics component.
* To proxy the requests through your own domain, you can use the dataApi and src attribute.
* See [Plausible docs](https://plausible.io/docs/proxy/guides/nextjs#step-2-adjust-your-deployed-script)
* for more information.
*
*/
export const Plausible = ({
plausibleDataDomain,
dataApi = undefined,
src = 'https://plausible.io/js/plausible.js',
}: PlausibleProps) => {
return (
<>
<Script
strategy="lazyOnload"
data-domain={plausibleDataDomain}
data-api={dataApi}
src={src}
/>
<Script strategy="lazyOnload" id="plausible-script">
{`
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }
`}
</Script>
</>
)
}
// https://plausible.io/docs/custom-event-goals
export const logEvent = (eventName, ...rest) => {
return window.plausible?.(eventName, ...rest)
}

View File

@@ -0,0 +1,25 @@
import Script from 'next/script.js'
export interface PosthogProps {
posthogProjectApiKey: string
apiHost?: string
}
/**
* Posthog analytics component.
* See [Posthog docs](https://posthog.com/docs/libraries/js#option-1-add-javascript-snippet-to-your-html-badgerecommendedbadge) for more information.
*
*/
export const Posthog = ({
posthogProjectApiKey,
apiHost = 'https://app.posthog.com',
}: PosthogProps) => {
return (
<Script strategy="lazyOnload" id="posthog-script">
{`
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${posthogProjectApiKey}',{api_host:'${apiHost}'})
`}
</Script>
)
}

View File

@@ -0,0 +1,29 @@
import Script from 'next/script.js'
export interface SimpleAnalyticsProps {
src?: string
}
export const SimpleAnalytics = ({
src = 'https://scripts.simpleanalyticscdn.com/latest.js',
}: SimpleAnalyticsProps) => {
return (
<>
<Script strategy="lazyOnload" id="sa-script">
{`
window.sa_event=window.sa_event||function(){var a=[].slice.call(arguments);window.sa_event.q?window.sa_event.q.push(a):window.sa_event.q=[a]};
`}
</Script>
<Script strategy="lazyOnload" src={src} />
</>
)
}
// https://docs.simpleanalytics.com/events
export const logEvent = (eventName, callback) => {
if (callback) {
return window.sa_event?.(eventName, callback)
} else {
return window.sa_event?.(eventName)
}
}

View File

@@ -0,0 +1,20 @@
import Script from 'next/script.js'
export interface UmamiProps {
umamiWebsiteId: string
src?: string
}
export const Umami = ({
umamiWebsiteId,
src = 'https://analytics.umami.is/script.js',
}: UmamiProps) => {
return (
<Script
async
defer
data-website-id={umamiWebsiteId}
src={src} // Replace with your umami instance
/>
)
}

View File

@@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { GA, GoogleAnalyticsProps } from "./GoogleAnalytics";
import { Plausible, PlausibleProps } from "./Plausible";
import { SimpleAnalytics, SimpleAnalyticsProps } from "./SimpleAnalytics";
import { Umami, UmamiProps } from "./Umami";
import { Posthog, PosthogProps } from "./Posthog";
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
gtag?: (...args: any[]) => void;
plausible?: (...args: any[]) => void;
sa_event?: (...args: any[]) => void;
}
}
export interface AnalyticsConfig {
googleAnalytics?: GoogleAnalyticsProps;
plausibleAnalytics?: PlausibleProps;
umamiAnalytics?: UmamiProps;
posthogAnalytics?: PosthogProps;
simpleAnalytics?: SimpleAnalyticsProps;
}
/**
* @example
* const analytics: AnalyticsConfig = {
* plausibleDataDomain: '', // e.g. tailwind-nextjs-starter-blog.vercel.app
* simpleAnalytics: false, // true or false
* umamiWebsiteId: '', // e.g. 123e4567-e89b-12d3-a456-426614174000
* posthogProjectApiKey: '', // e.g. AhnJK8392ndPOav87as450xd
* googleAnalyticsId: '', // e.g. UA-000000-2 or G-XXXXXXX
* }
*/
export interface AnalyticsProps {
analyticsConfig: AnalyticsConfig;
}
const isProduction = true || process.env["NODE_ENV"] === "production";
/**
* Supports Plausible, Simple Analytics, Umami, Posthog or Google Analytics.
* All components default to the hosted service, but can be configured to use a self-hosted
* or proxied version of the script by providing the `src` / `apiHost` props.
*
* Note: If you want to use an analytics provider you have to add it to the
* content security policy in the `next.config.js` file.
* @param {AnalyticsProps} { analytics }
* @return {*}
*/
export const Analytics = ({ analyticsConfig }: AnalyticsProps) => {
return (
<>
{isProduction && analyticsConfig.plausibleAnalytics && (
<Plausible {...analyticsConfig.plausibleAnalytics} />
)}
{isProduction && analyticsConfig.simpleAnalytics && (
<SimpleAnalytics {...analyticsConfig.simpleAnalytics} />
)}
{isProduction && analyticsConfig.posthogAnalytics && (
<Posthog {...analyticsConfig.posthogAnalytics} />
)}
{isProduction && analyticsConfig.umamiAnalytics && (
<Umami {...analyticsConfig.umamiAnalytics} />
)}
{isProduction && analyticsConfig.googleAnalytics && (
<GA {...analyticsConfig.googleAnalytics} />
)}
</>
);
};
export { GA, Plausible, SimpleAnalytics, Umami, Posthog };
export type {
GoogleAnalyticsProps,
PlausibleProps,
UmamiProps,
PosthogProps,
SimpleAnalyticsProps,
};

View File

@@ -21,3 +21,4 @@ export { SiteToc, NavItem, NavGroup } from "./SiteToc";
export { Comments, CommentsConfig } from "./Comments"; export { Comments, CommentsConfig } from "./Comments";
export { AuthorConfig } from "./types"; export { AuthorConfig } from "./types";
export { Hero } from "./Hero"; export { Hero } from "./Hero";
export { Analytics, AnalyticsConfig } from "./analytics";

View File

@@ -7,6 +7,8 @@ export const pageview = ({
analyticsID: string; analyticsID: string;
}) => { }) => {
if (typeof window.gtag !== undefined) { if (typeof window.gtag !== undefined) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.gtag("config", analyticsID, { window.gtag("config", analyticsID, {
page_path: url, page_path: url,
}); });
@@ -16,6 +18,8 @@ export const pageview = ({
// https://developers.google.com/analytics/devguides/collection/gtagjs/events // https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const event = ({ action, category, label, value }) => { export const event = ({ action, category, label, value }) => {
if (typeof window.gtag !== undefined) { if (typeof window.gtag !== undefined) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
window.gtag("event", action, { window.gtag("event", action, {
event_category: category, event_category: category,
event_label: label, event_label: label,

View File

@@ -1,5 +1,17 @@
# @portaljs/remark-wiki-link # @portaljs/remark-wiki-link
## 1.2.0
### Minor Changes
- [#1084](https://github.com/datopian/datahub/pull/1084) [`57952e08`](https://github.com/datopian/datahub/commit/57952e0817770138881e7492dc9f43e9910b56a8) Thanks [@mohamedsalem401](https://github.com/mohamedsalem401)! - Add image resize feature
## 1.1.2
### Patch Changes
- [#1040](https://github.com/datopian/portaljs/pull/1040) [`85bb6cb9`](https://github.com/datopian/portaljs/commit/85bb6cb98c53bedc2add3d014927570b5dd1bbdf) Thanks [@Gutts-n](https://github.com/Gutts-n)! - Changed regex to permit any symbols other than #
## 1.1.1 ## 1.1.1
### Patch Changes ### Patch Changes

View File

@@ -1,6 +1,6 @@
{ {
"name": "@portaljs/remark-wiki-link", "name": "@portaljs/remark-wiki-link",
"version": "1.1.1", "version": "1.2.0",
"description": "Parse and render wiki-style links in markdown especially Obsidian style links.", "description": "Parse and render wiki-style links in markdown especially Obsidian style links.",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@@ -1,23 +1,23 @@
import { isSupportedFileFormat } from "./isSupportedFileFormat"; import { isSupportedFileFormat } from './isSupportedFileFormat';
const defaultWikiLinkResolver = (target: string) => { const defaultWikiLinkResolver = (target: string) => {
// for [[#heading]] links // for [[#heading]] links
if (!target) { if (!target) {
return []; return [];
} }
let permalink = target.replace(/\/index$/, ""); let permalink = target.replace(/\/index$/, '');
// TODO what to do with [[index]] link? // TODO what to do with [[index]] link?
if (permalink.length === 0) { if (permalink.length === 0) {
permalink = "/"; permalink = '/';
} }
return [permalink]; return [permalink];
}; };
export interface FromMarkdownOptions { export interface FromMarkdownOptions {
pathFormat?: pathFormat?:
| "raw" // default; use for regular relative or absolute paths | 'raw' // default; use for regular relative or absolute paths
| "obsidian-absolute" // use for Obsidian-style absolute paths (with no leading slash) | 'obsidian-absolute' // use for Obsidian-style absolute paths (with no leading slash)
| "obsidian-short"; // use for Obsidian-style shortened paths (shortest path possible) | 'obsidian-short'; // use for Obsidian-style shortened paths (shortest path possible)
permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against
wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks
newClassName?: string; // class name to add to links that don't have a matching permalink newClassName?: string; // class name to add to links that don't have a matching permalink
@@ -25,14 +25,23 @@ export interface FromMarkdownOptions {
hrefTemplate?: (permalink: string) => string; // function to generate the href attribute of a link hrefTemplate?: (permalink: string) => string; // function to generate the href attribute of a link
} }
export function getImageSize(size: string) {
// eslint-disable-next-line prefer-const
let [width, height] = size.split('x');
if (!height) height = width;
return { width, height };
}
// mdas-util-from-markdown extension // mdas-util-from-markdown extension
// https://github.com/syntax-tree/mdast-util-from-markdown#extension // https://github.com/syntax-tree/mdast-util-from-markdown#extension
function fromMarkdown(opts: FromMarkdownOptions = {}) { function fromMarkdown(opts: FromMarkdownOptions = {}) {
const pathFormat = opts.pathFormat || "raw"; const pathFormat = opts.pathFormat || 'raw';
const permalinks = opts.permalinks || []; const permalinks = opts.permalinks || [];
const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver; const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
const newClassName = opts.newClassName || "new"; const newClassName = opts.newClassName || 'new';
const wikiLinkClassName = opts.wikiLinkClassName || "internal"; const wikiLinkClassName = opts.wikiLinkClassName || 'internal';
const defaultHrefTemplate = (permalink: string) => permalink; const defaultHrefTemplate = (permalink: string) => permalink;
const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate; const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate;
@@ -44,9 +53,9 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
function enterWikiLink(token) { function enterWikiLink(token) {
this.enter( this.enter(
{ {
type: "wikiLink", type: 'wikiLink',
data: { data: {
isEmbed: token.isType === "embed", isEmbed: token.isType === 'embed',
target: null, // the target of the link, e.g. "Foo Bar#Heading" in "[[Foo Bar#Heading]]" target: null, // the target of the link, e.g. "Foo Bar#Heading" in "[[Foo Bar#Heading]]"
alias: null, // the alias of the link, e.g. "Foo" in "[[Foo Bar|Foo]]" alias: null, // the alias of the link, e.g. "Foo" in "[[Foo Bar|Foo]]"
permalink: null, // TODO shouldn't this be named just "link"? permalink: null, // TODO shouldn't this be named just "link"?
@@ -79,19 +88,19 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
data: { isEmbed, target, alias }, data: { isEmbed, target, alias },
} = wikiLink; } = wikiLink;
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const wikiLinkWithHeadingPattern = /([\p{Letter}\d\s\/\.-_]*)(#.*)?/u; const wikiLinkWithHeadingPattern = /^(.*?)(#.*)?$/u;
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern); const [, path, heading = ''] = target.match(wikiLinkWithHeadingPattern);
const possibleWikiLinkPermalinks = wikiLinkResolver(path); const possibleWikiLinkPermalinks = wikiLinkResolver(path);
const matchingPermalink = permalinks.find((e) => { const matchingPermalink = permalinks.find((e) => {
return possibleWikiLinkPermalinks.find((p) => { return possibleWikiLinkPermalinks.find((p) => {
if (pathFormat === "obsidian-short") { if (pathFormat === 'obsidian-short') {
if (e === p || e.endsWith(p)) { if (e === p || e.endsWith(p)) {
return true; return true;
} }
} else if (pathFormat === "obsidian-absolute") { } else if (pathFormat === 'obsidian-absolute') {
if (e === "/" + p) { if (e === '/' + p) {
return true; return true;
} }
} else { } else {
@@ -106,20 +115,19 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
// TODO this is ugly // TODO this is ugly
const link = const link =
matchingPermalink || matchingPermalink ||
(pathFormat === "obsidian-absolute" (pathFormat === 'obsidian-absolute'
? "/" + possibleWikiLinkPermalinks[0] ? '/' + possibleWikiLinkPermalinks[0]
: possibleWikiLinkPermalinks[0]) || : possibleWikiLinkPermalinks[0]) ||
""; '';
wikiLink.data.exists = !!matchingPermalink; wikiLink.data.exists = !!matchingPermalink;
wikiLink.data.permalink = link; wikiLink.data.permalink = link;
// remove leading # if the target is a heading on the same page // remove leading # if the target is a heading on the same page
const displayName = alias || target.replace(/^#/, ""); const displayName = alias || target.replace(/^#/, '');
const headingId = heading.replace(/\s+/, "-").toLowerCase(); const headingId = heading.replace(/\s+/g, '-').toLowerCase();
let classNames = wikiLinkClassName; let classNames = wikiLinkClassName;
if (!matchingPermalink) { if (!matchingPermalink) {
classNames += " " + newClassName; classNames += ' ' + newClassName;
} }
if (isEmbed) { if (isEmbed) {
@@ -127,44 +135,55 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
if (!isSupportedFormat) { if (!isSupportedFormat) {
// Temporarily render note transclusion as a regular wiki link // Temporarily render note transclusion as a regular wiki link
if (!format) { if (!format) {
wikiLink.data.hName = "a"; wikiLink.data.hName = 'a';
wikiLink.data.hProperties = { wikiLink.data.hProperties = {
className: classNames + " " + "transclusion", className: classNames + ' ' + 'transclusion',
href: hrefTemplate(link) + headingId, href: hrefTemplate(link) + headingId,
}; };
wikiLink.data.hChildren = [{ type: "text", value: displayName }]; wikiLink.data.hChildren = [{ type: 'text', value: displayName }];
} else { } else {
wikiLink.data.hName = "p"; wikiLink.data.hName = 'p';
wikiLink.data.hChildren = [ wikiLink.data.hChildren = [
{ {
type: "text", type: 'text',
value: `![[${target}]]`, value: `![[${target}]]`,
}, },
]; ];
} }
} else if (format === "pdf") { } else if (format === 'pdf') {
wikiLink.data.hName = "iframe"; wikiLink.data.hName = 'iframe';
wikiLink.data.hProperties = { wikiLink.data.hProperties = {
className: classNames, className: classNames,
width: "100%", width: '100%',
src: `${hrefTemplate(link)}#toolbar=0`, src: `${hrefTemplate(link)}#toolbar=0`,
}; };
} else { } else {
wikiLink.data.hName = "img"; const hasDimensions = alias && /^\d+(x\d+)?$/.test(alias);
// Take the target as alt text except if alt name was provided [[target|alt text]]
const altText = hasDimensions || !alias ? target : alias;
wikiLink.data.hName = 'img';
wikiLink.data.hProperties = { wikiLink.data.hProperties = {
className: classNames, className: classNames,
src: hrefTemplate(link), src: hrefTemplate(link),
alt: displayName, alt: altText
}; };
if (hasDimensions) {
const { width, height } = getImageSize(alias as string);
Object.assign(wikiLink.data.hProperties, {
width,
height,
});
}
} }
} else { } else {
wikiLink.data.hName = "a"; wikiLink.data.hName = 'a';
wikiLink.data.hProperties = { wikiLink.data.hProperties = {
className: classNames, className: classNames,
href: hrefTemplate(link) + headingId, href: hrefTemplate(link) + headingId,
}; };
wikiLink.data.hChildren = [{ type: "text", value: displayName }]; wikiLink.data.hChildren = [{ type: 'text', value: displayName }];
} }
} }

View File

@@ -1,23 +1,24 @@
import { isSupportedFileFormat } from "./isSupportedFileFormat"; import { getImageSize } from './fromMarkdown';
import { isSupportedFileFormat } from './isSupportedFileFormat';
const defaultWikiLinkResolver = (target: string) => { const defaultWikiLinkResolver = (target: string) => {
// for [[#heading]] links // for [[#heading]] links
if (!target) { if (!target) {
return []; return [];
} }
let permalink = target.replace(/\/index$/, ""); let permalink = target.replace(/\/index$/, '');
// TODO what to do with [[index]] link? // TODO what to do with [[index]] link?
if (permalink.length === 0) { if (permalink.length === 0) {
permalink = "/"; permalink = '/';
} }
return [permalink]; return [permalink];
}; };
export interface HtmlOptions { export interface HtmlOptions {
pathFormat?: pathFormat?:
| "raw" // default; use for regular relative or absolute paths | 'raw' // default; use for regular relative or absolute paths
| "obsidian-absolute" // use for Obsidian-style absolute paths (with no leading slash) | 'obsidian-absolute' // use for Obsidian-style absolute paths (with no leading slash)
| "obsidian-short"; // use for Obsidian-style shortened paths (shortest path possible) | 'obsidian-short'; // use for Obsidian-style shortened paths (shortest path possible)
permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against permalinks?: string[]; // list of permalinks to match possible permalinks of a wiki link against
wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks wikiLinkResolver?: (name: string) => string[]; // function to resolve wiki links to an array of possible permalinks
newClassName?: string; // class name to add to links that don't have a matching permalink newClassName?: string; // class name to add to links that don't have a matching permalink
@@ -28,11 +29,11 @@ export interface HtmlOptions {
// Micromark HtmlExtension // Micromark HtmlExtension
// https://github.com/micromark/micromark#htmlextension // https://github.com/micromark/micromark#htmlextension
function html(opts: HtmlOptions = {}) { function html(opts: HtmlOptions = {}) {
const pathFormat = opts.pathFormat || "raw"; const pathFormat = opts.pathFormat || 'raw';
const permalinks = opts.permalinks || []; const permalinks = opts.permalinks || [];
const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver; const wikiLinkResolver = opts.wikiLinkResolver || defaultWikiLinkResolver;
const newClassName = opts.newClassName || "new"; const newClassName = opts.newClassName || 'new';
const wikiLinkClassName = opts.wikiLinkClassName || "internal"; const wikiLinkClassName = opts.wikiLinkClassName || 'internal';
const defaultHrefTemplate = (permalink: string) => permalink; const defaultHrefTemplate = (permalink: string) => permalink;
const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate; const hrefTemplate = opts.hrefTemplate || defaultHrefTemplate;
@@ -41,21 +42,21 @@ function html(opts: HtmlOptions = {}) {
} }
function enterWikiLink() { function enterWikiLink() {
let stack = this.getData("wikiLinkStack"); let stack = this.getData('wikiLinkStack');
if (!stack) this.setData("wikiLinkStack", (stack = [])); if (!stack) this.setData('wikiLinkStack', (stack = []));
stack.push({}); stack.push({});
} }
function exitWikiLinkTarget(token) { function exitWikiLinkTarget(token) {
const target = this.sliceSerialize(token); const target = this.sliceSerialize(token);
const current = top(this.getData("wikiLinkStack")); const current = top(this.getData('wikiLinkStack'));
current.target = target; current.target = target;
} }
function exitWikiLinkAlias(token) { function exitWikiLinkAlias(token) {
const alias = this.sliceSerialize(token); const alias = this.sliceSerialize(token);
const current = top(this.getData("wikiLinkStack")); const current = top(this.getData('wikiLinkStack'));
current.alias = alias; current.alias = alias;
} }
@@ -64,7 +65,7 @@ function html(opts: HtmlOptions = {}) {
const { target, alias } = wikiLink; const { target, alias } = wikiLink;
const isEmbed = token.isType === "embed"; const isEmbed = token.isType === "embed";
// eslint-disable-next-line no-useless-escape // eslint-disable-next-line no-useless-escape
const wikiLinkWithHeadingPattern = /([\w\s\/\.-]*)(#.*)?/; const wikiLinkWithHeadingPattern = /^(.*?)(#.*)?$/u;
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern); const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern);
const possibleWikiLinkPermalinks = wikiLinkResolver(path); const possibleWikiLinkPermalinks = wikiLinkResolver(path);
@@ -99,7 +100,7 @@ function html(opts: HtmlOptions = {}) {
// remove leading # if the target is a heading on the same page // remove leading # if the target is a heading on the same page
const displayName = alias || target.replace(/^#/, ""); const displayName = alias || target.replace(/^#/, "");
// replace spaces with dashes and lowercase headings // replace spaces with dashes and lowercase headings
const headingId = heading.replace(/\s+/, "-").toLowerCase(); const headingId = heading.replace(/\s+/g, "-").toLowerCase();
let classNames = wikiLinkClassName; let classNames = wikiLinkClassName;
if (!matchingPermalink) { if (!matchingPermalink) {
classNames += " " + newClassName; classNames += " " + newClassName;
@@ -111,7 +112,9 @@ function html(opts: HtmlOptions = {}) {
// Temporarily render note transclusion as a regular wiki link // Temporarily render note transclusion as a regular wiki link
if (!format) { if (!format) {
this.tag( this.tag(
`<a href="${hrefTemplate(link + headingId)}" class="${classNames} transclusion">` `<a href="${hrefTemplate(
link + headingId
)}" class="${classNames} transclusion">`
); );
this.raw(displayName); this.raw(displayName);
this.tag("</a>"); this.tag("</a>");
@@ -125,11 +128,18 @@ function html(opts: HtmlOptions = {}) {
)}#toolbar=0" class="${classNames}" />` )}#toolbar=0" class="${classNames}" />`
); );
} else { } else {
this.tag( const hasDimensions = alias && /^\d+(x\d+)?$/.test(alias);
`<img src="${hrefTemplate( // Take the target as alt text except if alt name was provided [[target|alt text]]
link const altText = hasDimensions || !alias ? target : alias;
)}" alt="${displayName}" class="${classNames}" />` let imgAttributes = `src="${hrefTemplate(
); link
)}" alt="${altText}" class="${classNames}"`;
if (hasDimensions) {
const { width, height } = getImageSize(alias as string);
imgAttributes += ` width="${width}" height="${height}"`;
}
this.tag(`<img ${imgAttributes} />`);
} }
} else { } else {
this.tag( this.tag(

View File

@@ -38,6 +38,5 @@ const defaultPathToPermalinkFunc = (
.replace(markdownFolder, "") // make the permalink relative to the markdown folder .replace(markdownFolder, "") // make the permalink relative to the markdown folder
.replace(/\.(mdx|md)/, "") .replace(/\.(mdx|md)/, "")
.replace(/\\/g, "/") // replace windows backslash with forward slash .replace(/\\/g, "/") // replace windows backslash with forward slash
.replace(/\/index$/, ""); // remove index from the end of the permalink
return permalink.length > 0 ? permalink : "/"; // for home page return permalink.length > 0 ? permalink : "/"; // for home page
}; };

View File

@@ -1,23 +1,20 @@
import * as path from "path"; import * as path from "path";
// import * as url from "url";
import { getPermalinks } from "../src/utils"; import { getPermalinks } from "../src/utils";
// const __dirname = url.fileURLToPath(new URL(".", import.meta.url));
// const markdownFolder = path.join(__dirname, "/fixtures/content");
const markdownFolder = path.join( const markdownFolder = path.join(
".", ".",
"/packages/remark-wiki-link/test/fixtures/content" "test/fixtures/content"
); );
describe("getPermalinks", () => { describe("getPermalinks", () => {
test("should return an array of permalinks", () => { test("should return an array of permalinks", () => {
const expectedPermalinks = [ const expectedPermalinks = [
"/", // /index.md "/README",
"/abc", "/abc",
"/blog/first-post", "/blog/first-post",
"/blog/Second Post", "/blog/Second Post",
"/blog/third-post", "/blog/third-post",
"/blog", // /blog/index.md "/blog/README",
"/blog/tutorials/first-tutorial", "/blog/tutorials/first-tutorial",
"/assets/Pasted Image 123.png", "/assets/Pasted Image 123.png",
]; ];
@@ -28,35 +25,4 @@ describe("getPermalinks", () => {
expect(expectedPermalinks).toContain(permalink); expect(expectedPermalinks).toContain(permalink);
}); });
}); });
test("should return an array of permalinks with custom path -> permalink converter function", () => {
const expectedPermalinks = [
"/", // /index.md
"/abc",
"/blog/first-post",
"/blog/second-post",
"/blog/third-post",
"/blog", // /blog/index.md
"/blog/tutorials/first-tutorial",
"/assets/pasted-image-123.png",
];
const func = (filePath: string, markdownFolder: string) => {
const permalink = filePath
.replace(markdownFolder, "") // make the permalink relative to the markdown folder
.replace(/\.(mdx|md)/, "")
.replace(/\\/g, "/") // replace windows backslash with forward slash
.replace(/\/index$/, "") // remove index from the end of the permalink
.replace(/ /g, "-") // replace spaces with hyphens
.toLowerCase(); // convert to lowercase
return permalink.length > 0 ? permalink : "/"; // for home page
};
const permalinks = getPermalinks(markdownFolder, [/\.DS_Store/], func);
expect(permalinks).toHaveLength(expectedPermalinks.length);
permalinks.forEach((permalink) => {
expect(expectedPermalinks).toContain(permalink);
});
});
}); });

View File

@@ -48,7 +48,7 @@ describe("micromark-extension-wiki-link", () => {
html({ html({
permalinks: ["/some/folder/Wiki Link"], permalinks: ["/some/folder/Wiki Link"],
pathFormat: "obsidian-short", pathFormat: "obsidian-short",
}) as any // TODO type fix }) as any, // TODO type fix
], ],
}); });
expect(serialized).toBe( expect(serialized).toBe(
@@ -75,7 +75,7 @@ describe("micromark-extension-wiki-link", () => {
html({ html({
permalinks: ["/some/folder/Wiki Link"], permalinks: ["/some/folder/Wiki Link"],
pathFormat: "obsidian-absolute", pathFormat: "obsidian-absolute",
}) as any // TODO type fix }) as any, // TODO type fix
], ],
}); });
expect(serialized).toBe( expect(serialized).toBe(
@@ -97,10 +97,14 @@ describe("micromark-extension-wiki-link", () => {
}); });
test("parses a wiki link with heading and alias", () => { test("parses a wiki link with heading and alias", () => {
const serialized = micromark("[[Wiki Link#Some Heading|Alias]]", "ascii", { const serialized = micromark(
extensions: [syntax()], "[[Wiki Link#Some Heading|Alias]]",
htmlExtensions: [html() as any], // TODO type fix "ascii",
}); {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
}
);
// note: lowercased and hyphenated heading // note: lowercased and hyphenated heading
expect(serialized).toBe( expect(serialized).toBe(
'<p><a href="Wiki Link#some-heading" class="internal new">Alias</a></p>' '<p><a href="Wiki Link#some-heading" class="internal new">Alias</a></p>'
@@ -134,7 +138,7 @@ describe("micromark-extension-wiki-link", () => {
extensions: [syntax()], extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix htmlExtensions: [html() as any], // TODO type fix
}); });
expect(serialized).toBe("<p>![[My Image.xyz]]</p>"); expect(serialized).toBe('<p>![[My Image.xyz]]</p>');
}); });
test("parses and image ambed with a matching permalink", () => { test("parses and image ambed with a matching permalink", () => {
@@ -147,6 +151,28 @@ describe("micromark-extension-wiki-link", () => {
); );
}); });
// TODO: Fix alt attribute
test("Can identify the dimensions of the image if exists", () => {
const serialized = micromark("![[My Image.jpg|200]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html({ permalinks: ["My Image.jpg"] }) as any], // TODO type fix
});
expect(serialized).toBe(
'<p><img src="My Image.jpg" alt="My Image.jpg" class="internal" width="200" height="200" /></p>'
);
});
// TODO: Fix alt attribute
test("Can identify the dimensions of the image if exists", () => {
const serialized = micromark("![[My Image.jpg|200x200]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html({ permalinks: ["My Image.jpg"] }) as any], // TODO type fix
});
expect(serialized).toBe(
'<p><img src="My Image.jpg" alt="My Image.jpg" class="internal" width="200" height="200" /></p>'
);
});
test("parses an image embed with a matching permalink and Obsidian-style shortedned path", () => { test("parses an image embed with a matching permalink and Obsidian-style shortedned path", () => {
const serialized = micromark("![[My Image.jpg]]", { const serialized = micromark("![[My Image.jpg]]", {
extensions: [syntax()], extensions: [syntax()],
@@ -154,7 +180,7 @@ describe("micromark-extension-wiki-link", () => {
html({ html({
permalinks: ["/assets/My Image.jpg"], permalinks: ["/assets/My Image.jpg"],
pathFormat: "obsidian-short", pathFormat: "obsidian-short",
}) as any // TODO type fix }) as any, // TODO type fix
], ],
}); });
expect(serialized).toBe( expect(serialized).toBe(
@@ -189,7 +215,7 @@ describe("micromark-extension-wiki-link", () => {
extensions: [syntax()], extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix htmlExtensions: [html() as any], // TODO type fix
}); });
expect(serialized).toBe("<p>[[Wiki Link</p>"); expect(serialized).toBe('<p>[[Wiki Link</p>');
}); });
test("doesn't parse a wiki link with one missing closing bracket", () => { test("doesn't parse a wiki link with one missing closing bracket", () => {
@@ -197,7 +223,7 @@ describe("micromark-extension-wiki-link", () => {
extensions: [syntax()], extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix htmlExtensions: [html() as any], // TODO type fix
}); });
expect(serialized).toBe("<p>[[Wiki Link]</p>"); expect(serialized).toBe('<p>[[Wiki Link]</p>');
}); });
test("doesn't parse a wiki link with a missing opening bracket", () => { test("doesn't parse a wiki link with a missing opening bracket", () => {
@@ -205,7 +231,7 @@ describe("micromark-extension-wiki-link", () => {
extensions: [syntax()], extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix htmlExtensions: [html() as any], // TODO type fix
}); });
expect(serialized).toBe("<p>[Wiki Link]]</p>"); expect(serialized).toBe('<p>[Wiki Link]]</p>');
}); });
test("doesn't parse a wiki link in single brackets", () => { test("doesn't parse a wiki link in single brackets", () => {
@@ -213,7 +239,7 @@ describe("micromark-extension-wiki-link", () => {
extensions: [syntax()], extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix htmlExtensions: [html() as any], // TODO type fix
}); });
expect(serialized).toBe("<p>[Wiki Link]</p>"); expect(serialized).toBe('<p>[Wiki Link]</p>');
}); });
}); });
@@ -225,7 +251,7 @@ describe("micromark-extension-wiki-link", () => {
html({ html({
newClassName: "test-new", newClassName: "test-new",
wikiLinkClassName: "test-wiki-link", wikiLinkClassName: "test-wiki-link",
}) as any // TODO type fix }) as any, // TODO type fix
], ],
}); });
expect(serialized).toBe( expect(serialized).toBe(
@@ -251,7 +277,7 @@ describe("micromark-extension-wiki-link", () => {
wikiLinkResolver: (page) => [ wikiLinkResolver: (page) => [
page.replace(/\s+/, "-").toLowerCase(), page.replace(/\s+/, "-").toLowerCase(),
], ],
}) as any // TODO type fix }) as any, // TODO type fix
], ],
}); });
expect(serialized).toBe( expect(serialized).toBe(
@@ -260,56 +286,6 @@ describe("micromark-extension-wiki-link", () => {
}); });
}); });
test("parses wiki links to index files", () => {
const serialized = micromark("[[/some/folder/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/some/folder" class="internal new">/some/folder/index</a></p>'
);
});
describe("other", () => {
test("parses a wiki link to some index page in a folder with no matching permalink", () => {
const serialized = micromark("[[/some/folder/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/some/folder" class="internal new">/some/folder/index</a></p>'
);
});
test("parses a wiki link to some index page in a folder with a matching permalink", () => {
const serialized = micromark("[[/some/folder/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html({ permalinks: ["/some/folder"] }) as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/some/folder" class="internal">/some/folder/index</a></p>'
);
});
test("parses a wiki link to home index page with no matching permalink", () => {
const serialized = micromark("[[/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any], // TODO type fix
});
expect(serialized).toBe(
'<p><a href="/" class="internal new">/index</a></p>'
);
});
test("parses a wiki link to home index page with a matching permalink", () => {
const serialized = micromark("[[/index]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html({ permalinks: ["/"] }) as any], // TODO type fix
});
expect(serialized).toBe('<p><a href="/" class="internal">/index</a></p>');
});
});
describe("transclusions", () => { describe("transclusions", () => {
test("parsers a transclusion as a regular wiki link", () => { test("parsers a transclusion as a regular wiki link", () => {
const serialized = micromark("![[Some Page]]", "ascii", { const serialized = micromark("![[Some Page]]", "ascii", {
@@ -321,4 +297,14 @@ describe("micromark-extension-wiki-link", () => {
); );
}); });
}); });
describe("Links with special characters", () => {
test("parses a link with special characters and symbols", () => {
const serialized = micromark("[[li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#LI NK-W(i)th-àcèô íã_a(n)d_uNdErlinE!:ª%@'*º$ °~./\\]]", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any],
});
expect(serialized).toBe(`<p><a href="li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô-íã_a(n)d_underline!:ª%@'*º$-°~./\\" class="internal new">li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#LI NK-W(i)th-àcèô íã_a(n)d_uNdErlinE!:ª%@'*º$ °~./\\</a></p>`);
});
});
}); });

View File

@@ -246,6 +246,28 @@ describe("remark-wiki-link", () => {
expect(node.data?.hName).toEqual("img"); expect(node.data?.hName).toEqual("img");
expect((node.data?.hProperties as any).src).toEqual("My Image.png"); expect((node.data?.hProperties as any).src).toEqual("My Image.png");
expect((node.data?.hProperties as any).alt).toEqual("My Image.png"); expect((node.data?.hProperties as any).alt).toEqual("My Image.png");
expect((node.data?.hProperties as any).width).toBeUndefined();
expect((node.data?.hProperties as any).height).toBeUndefined();
});
});
test("Can identify the dimensions of the image if exists", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("![[My Image.png|132x612]]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).not.toEqual(null);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.isEmbed).toEqual(true);
expect(node.data?.target).toEqual("My Image.png");
expect(node.data?.permalink).toEqual("My Image.png");
expect(node.data?.hName).toEqual("img");
expect((node.data?.hProperties as any).src).toEqual("My Image.png");
expect((node.data?.hProperties as any).alt).toEqual("My Image.png");
expect((node.data?.hProperties as any).width).toBe("132");
expect((node.data?.hProperties as any).height).toBe("612");
}); });
}); });
@@ -361,6 +383,36 @@ describe("remark-wiki-link", () => {
}); });
}); });
describe("Links with special characters", () => {
test("parses a link with special characters and symbols", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse(
"[[li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\]]"
);
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).not.toEqual(null);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(false);
expect(node.data?.permalink).toEqual(
"li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\"
);
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual(
"li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô-íã_a(n)d_underline!:ª%@'*º$-°~./\\"
);
expect((node.data?.hChildren as any)[0].value).toEqual(
"li nk-w(i)th-àcèô íã_a(n)d_underline!:ª%@'*º$ °~./\\#li-nk-w(i)th-àcèô íã_a(n)D_UNDERLINE!:ª%@'*º$ °~./\\"
);
});
});
});
describe("invalid wiki links", () => { describe("invalid wiki links", () => {
test("doesn't parse a wiki link with two missing closing brackets", () => { test("doesn't parse a wiki link with two missing closing brackets", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin); const processor = unified().use(markdown).use(wikiLinkPlugin);
@@ -433,109 +485,6 @@ describe("remark-wiki-link", () => {
}); });
}); });
test("parses wiki links to index files", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[/some/folder/index]]");
ast = processor.runSync(ast);
expect(select("wikiLink", ast)).not.toEqual(null);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(false);
expect(node.data?.permalink).toEqual("/some/folder");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal new");
expect((node.data?.hProperties as any).href).toEqual("/some/folder");
expect((node.data?.hChildren as any)[0].value).toEqual(
"/some/folder/index"
);
});
});
describe("other", () => {
test("parses a wiki link to some index page in a folder with no matching permalink", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[/some/folder/index]]");
ast = processor.runSync(ast);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(false);
expect(node.data?.permalink).toEqual("/some/folder");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual("/some/folder");
expect((node.data?.hChildren as any)[0].value).toEqual(
"/some/folder/index"
);
});
});
test("parses a wiki link to some index page in a folder with a matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, { permalinks: ["/some/folder"] });
let ast = processor.parse("[[/some/folder/index]]");
ast = processor.runSync(ast);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("/some/folder");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal");
expect((node.data?.hProperties as any).href).toEqual("/some/folder");
expect((node.data?.hChildren as any)[0].value).toEqual(
"/some/folder/index"
);
});
});
test("parses a wiki link to home index page with no matching permalink", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
let ast = processor.parse("[[/index]]");
ast = processor.runSync(ast);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(false);
expect(node.data?.permalink).toEqual("/");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual(
"internal new"
);
expect((node.data?.hProperties as any).href).toEqual("/");
expect((node.data?.hChildren as any)[0].value).toEqual("/index");
});
});
test("parses a wiki link to home index page with a matching permalink", () => {
const processor = unified()
.use(markdown)
.use(wikiLinkPlugin, { permalinks: ["/"] });
let ast = processor.parse("[[/index]]");
ast = processor.runSync(ast);
visit(ast, "wikiLink", (node: Node) => {
expect(node.data?.exists).toEqual(true);
expect(node.data?.permalink).toEqual("/");
expect(node.data?.alias).toEqual(null);
expect(node.data?.hName).toEqual("a");
expect((node.data?.hProperties as any).className).toEqual("internal");
expect((node.data?.hProperties as any).href).toEqual("/");
expect((node.data?.hChildren as any)[0].value).toEqual("/index");
});
});
});
describe("transclusions", () => { describe("transclusions", () => {
test("replaces a transclusion with a regular wiki link", () => { test("replaces a transclusion with a regular wiki link", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin); const processor = unified().use(markdown).use(wikiLinkPlugin);

View File

@@ -22,11 +22,41 @@ const items = [
sourceUrl: 'https://github.com/FCSCOpendata/frontend', sourceUrl: 'https://github.com/FCSCOpendata/frontend',
}, },
{ {
title: 'Datahub Open Data', title: 'Frictionless Data',
href: 'https://opendata.datahub.io/', href: 'https://datahub.io/core/co2-ppm',
image: '/images/showcases/datahub.webp', repository: 'https://github.com/datopian/datahub/tree/main/examples/dataset-frictionless',
description: 'Demo Data Portal by DataHub', image: '/images/showcases/frictionless-capture.png',
description: 'Progressive open-source framework for building data infrastructure - data management, data integration, data flows, etc. It includes various data standards and provides software to work with data.',
}, },
{
title: "OpenSpending",
image: "/images/showcases/openspending.png",
href: "https://www.openspending.org",
repository: 'https://github.com/datopian/datahub/tree/main/examples/openspending',
description: "OpenSpending is a free, open and global platform to search, visualise and analyse fiscal data in the public sphere."
},
{
title: "FiveThirtyEight",
image: "/images/showcases/fivethirtyeight.png",
href: "https://fivethirtyeight.portaljs.org/",
repository: 'https://github.com/datopian/datahub/tree/main/examples/fivethirtyeight',
description: "This is a replica of data.fivethirtyeight.com using PortalJS."
},
{
title: "Github Datasets",
image: "/images/showcases/github-datasets.png",
href: "https://example.portaljs.org/",
repository: 'https://github.com/datopian/datahub/tree/main/examples/github-backed-catalog',
description: "A simple data catalog that get its data from a list of GitHub repos that serve as datasets."
},
{
title: "Hatespeech Data",
image: "/images/showcases/turing.png",
href: "https://hatespeechdata.com/",
repository: 'https://github.com/datopian/datahub/tree/main/examples/turing',
description: "Datasets annotated for hate speech, online abuse, and offensive language which are useful for training a natural language processing system to detect this online abuse."
},
]; ];
export default function Showcases() { export default function Showcases() {

View File

@@ -1,10 +1,6 @@
export default function ShowcasesItem({ item }) { export default function ShowcasesItem({ item }) {
return ( return (
<a <div className="rounded overflow-hidden group relative border-1 shadow-lg">
className="rounded overflow-hidden group relative border-1 shadow-lg"
target="_blank"
href={item.href}
>
<div <div
className="bg-cover bg-no-repeat bg-top aspect-video w-full group-hover:blur-sm group-hover:scale-105 transition-all duration-200" className="bg-cover bg-no-repeat bg-top aspect-video w-full group-hover:blur-sm group-hover:scale-105 transition-all duration-200"
style={{ backgroundImage: `url(${item.image})` }} style={{ backgroundImage: `url(${item.image})` }}
@@ -16,9 +12,48 @@ export default function ShowcasesItem({ item }) {
<div className="text-center text-primary-dark"> <div className="text-center text-primary-dark">
<span className="text-xl font-semibold">{item.title}</span> <span className="text-xl font-semibold">{item.title}</span>
<p className="text-base font-medium">{item.description}</p> <p className="text-base font-medium">{item.description}</p>
<div className="flex justify-center mt-2 gap-2 ">
{item.href && (
<a
target="_blank"
className=" text-white w-8 h-8 p-1 bg-primary rounded-full hover:scale-110 transition cursor-pointer z-50"
rel="noreferrer"
href={item.href}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 420 420"
stroke="white"
fill="none"
>
<path stroke-width="26" d="M209,15a195,195 0 1,0 2,0z" />
<path
stroke-width="18"
d="m210,15v390m195-195H15M59,90a260,260 0 0,0 302,0 m0,240 a260,260 0 0,0-302,0M195,20a250,250 0 0,0 0,382 m30,0 a250,250 0 0,0 0-382"
/>
</svg>
</a>
)}
{item.repository && (
<a
target="_blank"
rel="noreferrer"
className="w-8 h-8 bg-black rounded-full p-1 hover:scale-110 transition cursor-pointer z-50"
href={item.repository}
>
<svg
aria-hidden="true"
viewBox="0 0 16 16"
fill="currentColor"
>
<path d="M8 0C3.58 0 0 3.58 0 8C0 11.54 2.29 14.53 5.47 15.59C5.87 15.66 6.02 15.42 6.02 15.21C6.02 15.02 6.01 14.39 6.01 13.72C4 14.09 3.48 13.23 3.32 12.78C3.23 12.55 2.84 11.84 2.5 11.65C2.22 11.5 1.82 11.13 2.49 11.12C3.12 11.11 3.57 11.7 3.72 11.94C4.44 13.15 5.59 12.81 6.05 12.6C6.12 12.08 6.33 11.73 6.56 11.53C4.78 11.33 2.92 10.64 2.92 7.58C2.92 6.71 3.23 5.99 3.74 5.43C3.66 5.23 3.38 4.41 3.82 3.31C3.82 3.31 4.49 3.1 6.02 4.13C6.66 3.95 7.34 3.86 8.02 3.86C8.7 3.86 9.38 3.95 10.02 4.13C11.55 3.09 12.22 3.31 12.22 3.31C12.66 4.41 12.38 5.23 12.3 5.43C12.81 5.99 13.12 6.7 13.12 7.58C13.12 10.65 11.25 11.33 9.47 11.53C9.76 11.78 10.01 12.26 10.01 13.01C10.01 14.08 10 14.94 10 15.21C10 15.42 10.15 15.67 10.55 15.59C13.71 14.53 16 11.53 16 8C16 3.58 12.42 0 8 0Z" />
</svg>
</a>
)}
</div>
</div> </div>
</div> </div>
</div> </div>
</a> </div>
); );
} }

View File

@@ -1,15 +1,17 @@
--- ---
title: 'Enhancing Geospatial Data Visualization with PortalJS' title: 'Adding Maps to PortalJS: Enhancing Geospatial Data Visualization with PortalJS'
date: 2023-07-18 date: 2023-07-18
authors: ['João Demenech', 'Luccas Mateus', 'Yoana Popova'] authors: ['João Demenech', 'Luccas Mateus', 'Yoana Popova']
filetype: 'blog' filetype: 'blog'
--- ---
Are you keen on building rich and interactive data portals? Do you find value in the power and flexibility of JavaScript, Nextjs, and React? In that case, allow us to introduce you to [PortalJS](https://portaljs.org/), a state-of-the-art framework leveraging these technologies to help you build amazing data portals. This post walks you though adding maps and geospatial visualizations to PortalJS.
Perhaps you already understand that the effective data visualization lies in the adept utilization of various data components. Within [PortalJS](https://portaljs.org/), we take data visualization a step further. It's not just about displaying data - it's about telling a captivating story through the strategic orchestration of a diverse array of data components. Are you interested in building rich and interactive data portals? Do you find value in the power and flexibility of JavaScript, Nextjs, and React? If so, [PortalJS](https://portaljs.org/) is for you. It's a state-of-the-art framework leveraging these technologies to help you build rich data portals.
We are now eager to share our latest enhancement to [PortalJS](https://portaljs.org/): maps, a powerful tool for visualizing geospatial data. In this post, we will to take you on a tour of our experiments and progress in enhancing map functionalities on [PortalJS](https://portaljs.org/). Our journey into this innovative feature is still in its early stages, with new facets being unveiled and refined as we perfect our API. Still, this exciting development opens a new avenue for visualizing data, enhancing your ability to convey complex geospatial information with clarity and precision. Effective data visualization lies in the use of various data components. Within [PortalJS](https://portaljs.org/), we take data visualization a step further. It's not just about displaying data - it's about telling a story through combining a variety of data components.
In this post we will share our latest enhancement to PortalJS: maps, a powerful tool for visualizing geospatial data. In this post, we will to take you on a tour of our experiments and progress in enhancing map functionalities on PortalJS. The journey is still in its early stages, with new facets being unveiled and refined as we perfect our API.
## Exploring Map Formats ## Exploring Map Formats

View File

@@ -4,7 +4,7 @@ authors: ['Luccas Mateus']
date: 2021-04-20 date: 2021-04-20
--- ---
We have created a full data portal demo using PortalJS all backed by a CKAN instance storing data and metadata, you can see below a screenshot of the homepage and of an individual dataset page. We have created a full data portal demo using DataHub PortalJS all backed by a CKAN instance storing data and metadata, you can see below a screenshot of the homepage and of an individual dataset page.
![](https://i.imgur.com/ai0VLS4.png) ![](https://i.imgur.com/ai0VLS4.png)
![](https://i.imgur.com/3RhXOW4.png) ![](https://i.imgur.com/3RhXOW4.png)
@@ -14,7 +14,7 @@ We have created a full data portal demo using PortalJS all backed by a CKAN inst
To create a Portal app, run the following command in your terminal: To create a Portal app, run the following command in your terminal:
```console ```console
npx create-next-app -e https://github.com/datopian/portaljs/tree/main/examples/ckan npx create-next-app -e https://github.com/datopian/datahub/tree/main/examples/ckan
``` ```
> NB: Under the hood, this uses the tool called create-next-app, which bootstraps an app for you based on our CKAN example. > NB: Under the hood, this uses the tool called create-next-app, which bootstraps an app for you based on our CKAN example.

View File

@@ -3,6 +3,7 @@ title: 'Announcing MarkdownDB: an open source tool to create an SQL API to your
description: MarkdownDB - an open source library to transform markdown content into sql-queryable data. Build rich markdown-powered sites easily and reliably. New dedicated website at markdowndb.com description: MarkdownDB - an open source library to transform markdown content into sql-queryable data. Build rich markdown-powered sites easily and reliably. New dedicated website at markdowndb.com
date: 2023-10-11 date: 2023-10-11
authors: ['Ola Rubaj'] authors: ['Ola Rubaj']
filetype: blog
--- ---
Hello, dear readers! Hello, dear readers!

View File

@@ -2,6 +2,7 @@
title: What We Shipped in Jul-Aug 2023 title: What We Shipped in Jul-Aug 2023
authors: ['ola-rubaj'] authors: ['ola-rubaj']
date: 2023-09-2 date: 2023-09-2
filetype: blog
--- ---
Hey everyone! 👋 Summer has been in full swing, and while I've managed to catch some vacation vibes, I've also been deep into code. I'm super excited to share some of the latest updates and features we've rolled out over the past two months. Let's dive in: Hey everyone! 👋 Summer has been in full swing, and while I've managed to catch some vacation vibes, I've also been deep into code. I'm super excited to share some of the latest updates and features we've rolled out over the past two months. Let's dive in:

View File

@@ -1,7 +1,7 @@
const config = { const config = {
title: 'PortalJS - The JavaScript framework for data portals.', title: 'DataHub PortalJS - The JavaScript framework for data portals.',
description: description:
'PortalJS is a JavaScript framework for rapidly building rich data portal frontends using a modern frontend approach.', 'DataHub PortalJS is a JavaScript framework for rapidly building rich data portal frontends using a modern frontend approach.',
theme: { theme: {
default: 'dark', default: 'dark',
toggleIcon: '/images/theme-button.svg', toggleIcon: '/images/theme-button.svg',
@@ -11,20 +11,18 @@ const config = {
authorUrl: 'https://datopian.com/', authorUrl: 'https://datopian.com/',
navbarTitle: { navbarTitle: {
// logo: "/images/logo.svg", // logo: "/images/logo.svg",
text: '🌀 PortalJS', text: '🌀 DataHub PortalJS',
// version: "Alpha", // version: "Alpha",
}, },
navLinks: [ navLinks: [
{ name: 'Docs', href: '/docs' }, { name: 'Docs', href: '/docs' },
// { name: "Components", href: "/docs/components" }, // { name: "Components", href: "/docs/components" },
{ name: 'Blog', href: '/blog' }, { name: 'Blog', href: '/blog' },
{ name: 'Showcases', href: '/#showcases' },
{ name: 'Howtos', href: '/howtos' }, { name: 'Howtos', href: '/howtos' },
{ name: 'Guide', href: '/guide' }, { name: 'Guide', href: '/guide' },
{ {
name: 'Examples', name: 'Showcases',
href: 'https://github.com/datopian/portaljs/tree/main/examples', href: '/showcases/'
target: '_blank',
}, },
{ {
name: 'Components', name: 'Components',
@@ -69,8 +67,8 @@ const config = {
cardType: 'summary_large_image', cardType: 'summary_large_image',
}, },
}, },
github: 'https://github.com/datopian/portaljs', github: 'https://github.com/datopian/datahub',
discord: 'https://discord.gg/EeyfGrGu4U', discord: 'https://discord.gg/KrRzMKU',
tableOfContents: true, tableOfContents: true,
analytics: 'G-96GWZHMH57', analytics: 'G-96GWZHMH57',
// editLinkShow: true, // editLinkShow: true,

View File

@@ -26,7 +26,7 @@ Below are some screenshots:
- Create a new app with `create-next-app`: - Create a new app with `create-next-app`:
``` ```
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-example
cd <app-name> cd <app-name>
``` ```
@@ -49,7 +49,7 @@ If yo go to any one of those pages by clicking on `More info` you will see somet
## Deployment ## Deployment
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fckan-example&env=DMS&envDescription=URL%20For%20the%20CKAN%20Backend%20Ex%3A%20https%3A%2F%2Fdemo.dev.datopian.com) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fdatahub%2Ftree%2Fmain%2Fexamples%2Fckan-example&env=DMS&envDescription=URL%20For%20the%20CKAN%20Backend%20Ex%3A%20https%3A%2F%2Fdemo.dev.datopian.com)
By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own github/gitlab/bitbucket account and automatically deploy everything. By clicking on this button, you will be redirected to a page which will allow you to clone the content into your own github/gitlab/bitbucket account and automatically deploy everything.
@@ -70,6 +70,6 @@ npm run start
## Links ## Links
- [Repo](https://github.com/datopian/portaljs/tree/main/examples/ckan-example) - [Repo](https://github.com/datopian/datahub/tree/main/examples/ckan-example)
- [Live Demo](https://ckan-example.portaljs.org) - [Live Demo](https://ckan-example.portaljs.org)

View File

@@ -26,7 +26,7 @@ To get a feel of the project, check out the demo at [live deployment](https://ck
Navigate to the directory in which you want to create the project folder and run the following command: Navigate to the directory in which you want to create the project folder and run the following command:
``` ```
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/ckan npx create-next-app <app-name> --example https://github.com/datopian/datahub/tree/main/examples/ckan
cd <app-name> cd <app-name>
``` ```
@@ -56,7 +56,7 @@ If you navigate to any of the dataset pages by clicking on the dataset title you
## Deployment ## Deployment
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fckan&env=DMS&envDescription=URL%20For%20the%20CKAN%20Backend%20Ex%3A%20https%3A%2F%2Fdemo.dev.datopian.com) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fdatahub%2Ftree%2Fmain%2Fexamples%2Fckan&env=DMS&envDescription=URL%20For%20the%20CKAN%20Backend%20Ex%3A%20https%3A%2F%2Fdemo.dev.datopian.com)
By clicking on this button, you will be redirected to a page which allows you to clone the base project into your own GitHub/GitLab/BitBucket account and automatically deploy it. By clicking on this button, you will be redirected to a page which allows you to clone the base project into your own GitHub/GitLab/BitBucket account and automatically deploy it.
@@ -158,6 +158,6 @@ Thanks to TypeScript, you can get a list of all the API methods in `@portaljs/ck
## Links ## Links
- [Repo](https://github.com/datopian/portaljs/tree/main/examples/ckan) - [Repo](https://github.com/datopian/datahub/tree/main/examples/ckan)
- [Live Demo](http://ckan.portaljs.org/) - [Live Demo](http://ckan.portaljs.org/)

View File

@@ -1,48 +0,0 @@
---
title: "Example: showcase for a single Frictionless dataset"
authors: ['Luccas Mateus']
date: 2023-04-20
filetype: blog
---
**See the repo:** https://github.com/datopian/portaljs/tree/main/examples/dataset-frictionless
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/
## How to use
```bash
npx create-next-app -e https://github.com/datopian/portaljs/tree/main/examples/dataset-frictionless
# choose a name for your portal when prompted e.g. your-portal or go with default my-app
# then run it
cd your-portal
yarn #install packages
yarn dev #start app in dev mode
```
You should see the demo portal running with the example dataset provided:
<img src="/assets/examples/frictionless-dataset-demo.gif" />
### Use your own dataset
You can try it out with other [Frictionless datasets](https://datahub.io/search).
In the directory of your portal do:
```bash
export PORTAL_DATASET_PATH=/path/to/my/dataset
```
Then restart the dev server:
```
yarn dev
```
Check the portal page and it should have updated e.g. like:
![](https://i.imgur.com/KSEtNF1.png)

View File

@@ -33,7 +33,7 @@ Run the following commands:
```bash ```bash
npx create-next-app <app-name> --example https://github.com/datopian/portaljs/tree/main/examples/github-backed-catalog npx create-next-app <app-name> --example https://github.com/datopian/datahub/tree/main/examples/github-backed-catalog
cd <app-name> cd <app-name>
``` ```
@@ -61,7 +61,7 @@ Congratulations, your new app is now running at http://localhost:3000.
## Deployment ## Deployment
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fportaljs%2Ftree%2Fmain%2Fexamples%2Fgithub-backed-catalog) [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdatopian%2Fdatahub%2Ftree%2Fmain%2Fexamples%2Fgithub-backed-catalog)
By clicking on this button, you will be redirected to a page which will allow you to clone the example into your own GitHub/GitLab/BitBucket account and automatically deploy it. By clicking on this button, you will be redirected to a page which will allow you to clone the example into your own GitHub/GitLab/BitBucket account and automatically deploy it.
@@ -119,5 +119,5 @@ npm run start
## Links ## Links
- [Repo](https://github.com/datopian/portaljs/tree/main/examples/github-backed-catalog) - [Repo](https://github.com/datopian/datahub/tree/main/examples/github-backed-catalog)
- [Live Demo](https://example.portaljs.org) - [Live Demo](https://example.portaljs.org)

View File

@@ -3,9 +3,9 @@ title: Getting Started
description: 'Getting started guide and tutorial about data portal-building with PortalJS!' description: 'Getting started guide and tutorial about data portal-building with PortalJS!'
--- ---
Welcome to the PortalJS documentation! Welcome to the DataHub PortalJS documentation!
If you have questions about anything related to PortalJS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portaljs/discussions) or on [our chat channel on Discord](https://discord.gg/EeyfGrGu4U). If you have questions about anything related to PortalJS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/datahub/discussions) or on [our chat channel on Discord](https://discord.com/invite/KrRzMKU).
## Setup ## Setup
@@ -16,10 +16,10 @@ If you have questions about anything related to PortalJS, you're always welcome
### Create a PortalJS app ### Create a PortalJS app
To create a PortalJS app, open your terminal, cd into the directory youd like to create the app in, and run the following command: To create a DataHub PortalJS app, open your terminal, cd into the directory youd like to create the app in, and run the following command:
```bash ```bash
npx create-next-app my-data-portal --example https://github.com/datopian/portaljs/tree/main/examples/learn npx create-next-app my-data-portal --example https://github.com/datopian/datahub/tree/main/examples/learn
``` ```
> [!tip] > [!tip]

View File

@@ -11,5 +11,5 @@ description: Learn more about how you can achieve different data portal features
- [[howtos/drd|How to create data-rich documents with charts and tables?]] - [[howtos/drd|How to create data-rich documents with charts and tables?]]
- [[howtos/comments|How to add user comments?]] - [[howtos/comments|How to add user comments?]]
If you have questions about anything related to PortalJS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/portaljs/discussions) or on [our chat channel on Discord](https://discord.gg/EeyfGrGu4U). If you have questions about anything related to PortalJS, you're always welcome to ask our community on [GitHub Discussions](https://github.com/datopian/datahub/discussions) or on [our chat channel on Discord](https://discord.gg/EeyfGrGu4U).

View File

@@ -50,7 +50,7 @@ function MyApp({ Component, pageProps }) {
<DefaultSeo <DefaultSeo
defaultTitle={siteConfig.title} defaultTitle={siteConfig.title}
description={siteConfig.description} description={siteConfig.description}
titleTemplate="PortalJS - %s" titleTemplate="DataHub PortalJS - %s"
{...siteConfig.nextSeo} {...siteConfig.nextSeo}
/> />

View File

@@ -35,7 +35,7 @@ export default function Home({ sidebarTree }) {
sidebarTree={sidebarTree} sidebarTree={sidebarTree}
> >
<Features /> <Features />
<Showcases />
<Community /> <Community />
</Layout> </Layout>
</> </>

8
site/pages/showcases.tsx Normal file
View File

@@ -0,0 +1,8 @@
import Layout from "@/components/Layout";
import Showcases from "@/components/Showcases";
export default function ShowcasesList() {
return (
<Layout><Showcases/></Layout>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

View File

@@ -1,7 +1,7 @@
#!/bin/bash #!/bin/bash
rm -rf portal rm -rf portal
mkdir -p portal mkdir -p portal
npx create-next-app portal -e https://github.com/datopian/portaljs/tree/main/examples/dataset-frictionless npx create-next-app portal -e https://github.com/datopian/datahub/tree/main/examples/dataset-frictionless
mkdir portal/public/dataset mkdir portal/public/dataset
cp -a ./data portal/public/dataset cp -a ./data portal/public/dataset
@@ -12,7 +12,7 @@ PORTAL_DATASET_PATH=$PWD"/portal/public/dataset"
export PORTAL_DATASET_PATH export PORTAL_DATASET_PATH
mkdir -p .github && mkdir -p .github/workflows && touch .github/workflows/main.yml mkdir -p .github && mkdir -p .github/workflows && touch .github/workflows/main.yml
curl https://raw.githubusercontent.com/datopian/portaljs/main/site/public/scripts/gh-page-builder-action.yml > .github/workflows/main.yml curl https://raw.githubusercontent.com/datopian/datahub/main/site/public/scripts/gh-page-builder-action.yml > .github/workflows/main.yml
cd portal cd portal
assetPrefix='"/'$PORTAL_REPO_NAME'/"' assetPrefix='"/'$PORTAL_REPO_NAME'/"'

View File

@@ -3,7 +3,7 @@ git checkout -b gh-pages
git rm -r --cached . git rm -r --cached .
rm -rf portal rm -rf portal
mkdir -p portal mkdir -p portal
npx create-next-app portal -e https://github.com/datopian/portaljs/tree/main/examples/dataset-frictionless npx create-next-app portal -e https://github.com/datopian/datahub/tree/main/examples/dataset-frictionless
mkdir portal/public/dataset mkdir portal/public/dataset
cp -a ./data portal/public/dataset cp -a ./data portal/public/dataset

View File

@@ -1,12 +0,0 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../dist/out-tsc/tools",
"rootDir": ".",
"module": "commonjs",
"target": "es5",
"types": ["node"],
"importHelpers": false
},
"include": ["**/*.ts"]
}