Compare commits

..

71 Commits

Author SHA1 Message Date
mohamedsalem401
806bc89e8c Open external links in a new tab 2024-02-29 13:52:36 +02: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
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
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
36 changed files with 1064 additions and 83 deletions

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.
You might be asking why we did that, there are three main reasons:

8
package-lock.json generated
View File

@@ -48297,7 +48297,7 @@
},
"packages/components": {
"name": "@portaljs/components",
"version": "0.4.0",
"version": "0.5.3",
"dependencies": {
"@githubocto/flat-ui": "^0.14.1",
"@heroicons/react": "^2.0.17",
@@ -48734,7 +48734,7 @@
},
"packages/core": {
"name": "@portaljs/core",
"version": "1.0.6",
"version": "1.0.8",
"license": "MIT",
"dependencies": {
"@docsearch/react": "^3.3.3",
@@ -48773,7 +48773,7 @@
},
"packages/remark-embed": {
"name": "@portaljs/remark-embed",
"version": "1.0.4",
"version": "1.0.5",
"license": "MIT",
"dependencies": {
"unist-util-visit": "^4.1.1"
@@ -48781,7 +48781,7 @@
},
"packages/remark-wiki-link": {
"name": "@portaljs/remark-wiki-link",
"version": "1.1.0",
"version": "1.1.2",
"license": "MIT",
"dependencies": {
"mdast-util-to-markdown": "^1.5.0",

View File

@@ -1,5 +1,73 @@
# @portaljs/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
### Minor Changes

View File

@@ -1,6 +1,6 @@
{
"name": "@portaljs/components",
"version": "0.4.0",
"version": "0.5.10",
"type": "module",
"description": "https://portaljs.org",
"keywords": [
@@ -29,6 +29,8 @@
"@githubocto/flat-ui": "^0.14.1",
"@heroicons/react": "^2.0.17",
"@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",
"ag-grid-react": "^30.0.4",
"chroma-js": "^2.4.2",
@@ -37,6 +39,7 @@
"next-mdx-remote": "^4.4.1",
"ol": "^7.4.0",
"papaparse": "^5.4.1",
"pdfjs-dist": "2.15.349",
"postcss-url": "^10.1.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
@@ -47,9 +50,6 @@
"vega": "5.25.0",
"vega-lite": "5.1.0",
"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"
},
"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

@@ -0,0 +1,14 @@
import { CSSProperties } from "react";
export interface IframeProps {
url: string;
style?: CSSProperties;
}
export function Iframe({
url, style
}: IframeProps) {
return (
<iframe src={url} style={style ?? { width: `100%`, height: `100%` }}></iframe>
);
}

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { CSSProperties, useEffect, useState } from 'react';
import LoadingSpinner from './LoadingSpinner';
import loadData from '../lib/loadData';
import chroma from 'chroma-js';
@@ -21,15 +21,19 @@ export type MapProps = {
ending: string;
};
tooltip?:
| {
propNames: string[];
}
| boolean;
| {
propNames: string[];
}
| boolean;
_id?: number;
}[];
title?: string;
center?: { latitude: number | undefined; longitude: number | undefined };
zoom?: number;
style?: CSSProperties;
autoZoomConfiguration?: {
layerName: string
}
};
export function Map({
@@ -44,6 +48,8 @@ export function Map({
center = { latitude: 45, longitude: 45 },
zoom = 2,
title = '',
style = {},
autoZoomConfiguration = undefined,
}: MapProps) {
const [isLoading, setIsLoading] = useState<boolean>(false);
const [layersData, setLayersData] = useState<any>([]);
@@ -96,6 +102,7 @@ export function Map({
zoom={zoom}
scrollWheelZoom={false}
className="h-80 w-full"
style={style ?? {}}
// @ts-ignore
whenReady={(map: any) => {
// Enable zoom using scroll wheel
@@ -104,17 +111,35 @@ export function Map({
// Create the title box
var info = new L.Control() as any;
info.onAdd = function () {
info.onAdd = function() {
this._div = L.DomUtil.create('div', 'info');
this.update();
return this._div;
};
info.update = function () {
info.update = function() {
this._div.innerHTML = `<h4 style="font-weight: 600; background: #f9f9f9; padding: 5px; border-radius: 5px; color: #464646;">${title}</h4>`;
};
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

View File

@@ -6,6 +6,8 @@ import {
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
PaginationState,
Table as ReactTable,
useReactTable,
} from '@tanstack/react-table';
@@ -25,12 +27,19 @@ import DebouncedInput from './DebouncedInput';
import loadData from '../lib/loadData';
import LoadingSpinner from './LoadingSpinner';
export type TableData = { cols: {key: string, name: string}[]; data: any[]; total: number };
export type TableProps = {
data?: Array<{ [key: string]: number | string }>;
cols?: Array<{ [key: string]: string }>;
csv?: string;
url?: string;
fullWidth?: boolean;
datastoreConfig?: {
dataStoreURI: string;
rowsPerPage?: number;
dataMapperFn: (data) => Promise<TableData> | TableData;
};
};
export const Table = ({
@@ -39,8 +48,28 @@ export const Table = ({
csv = '',
url = '',
fullWidth = false,
datastoreConfig,
}: TableProps) => {
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) {
const out = parseCsv(csv);
@@ -62,21 +91,56 @@ export const Table = ({
);
}, [data, cols]);
const [globalFilter, setGlobalFilter] = useState('');
let table: ReactTable<unknown>;
const table = useReactTable({
data,
columns: tableCols,
getCoreRowModel: getCoreRowModel(),
state: {
globalFilter,
},
globalFilterFn: globalFilterFn,
onGlobalFilterChange: setGlobalFilter,
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
if (datastoreConfig) {
useEffect(() => {
setIsLoading(true);
fetch(`${dataStoreURI}&limit=${rowsPerPage}&offset=0`)
.then((res) => res.json())
.then(async (res) => {
const { data, cols, total } = await dataMapperFn(res);
setData(data);
setCols(cols);
setTotalOfRows(Math.ceil(total / rowsPerPage));
pageMap.set(0, true);
})
.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(() => {
if (url) {
@@ -91,6 +155,70 @@ export const Table = ({
}
}, [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 ? (
<div className="w-full h-full min-h-[500px] flex items-center justify-center">
<LoadingSpinner />
@@ -99,7 +227,10 @@ export const Table = ({
<div className={`${fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full'}`}>
<DebouncedInput
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"
placeholder="Search all columns..."
/>
@@ -114,7 +245,10 @@ export const Table = ({
className: h.column.getCanSort()
? 'cursor-pointer select-none'
: '',
onClick: h.column.getToggleSortingHandler(),
onClick: (v) => {
setHasSorted(true);
h.column.getToggleSortingHandler()(v);
},
}}
>
{flexRender(h.column.columnDef.header, h.getContext())}
@@ -135,15 +269,28 @@ export const Table = ({
))}
</thead>
<tbody>
{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>
))}
{datastoreConfig && isLoadingPage ? (
<tr>
<td colSpan={cols.length} rowSpan={cols.length}>
<div className="w-full h-full flex items-center justify-center pt-6">
<LoadingSpinner />
</div>
</td>
</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>
</table>
<div className="flex gap-2 items-center justify-center mt-10">
@@ -151,7 +298,10 @@ export const Table = ({
className={`w-6 h-6 ${
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
}`}
onClick={() => table.setPageIndex(0)}
onClick={() => {
if (datastoreConfig) queryPaginatedData(0);
table.setPageIndex(0);
}}
disabled={!table.getCanPreviousPage()}
>
<ChevronDoubleLeftIcon />
@@ -160,7 +310,12 @@ export const Table = ({
className={`w-6 h-6 ${
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
}`}
onClick={() => table.previousPage()}
onClick={() => {
if (datastoreConfig) {
queryPaginatedData(table.getState().pagination.pageIndex - 1);
}
table.previousPage();
}}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeftIcon />
@@ -176,7 +331,11 @@ export const Table = ({
className={`w-6 h-6 ${
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
}`}
onClick={() => table.nextPage()}
onClick={() => {
if (datastoreConfig)
queryPaginatedData(table.getState().pagination.pageIndex + 1);
table.nextPage();
}}
disabled={!table.getCanNextPage()}
>
<ChevronRightIcon />
@@ -185,7 +344,11 @@ export const Table = ({
className={`w-6 h-6 ${
!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()}
>
<ChevronDoubleRightIcon />

View File

@@ -8,3 +8,5 @@ export * from './components/OpenLayers/OpenLayers';
export * from './components/Map';
export * from './components/PdfViewer';
export * from "./components/Excel";
export * from "./components/BucketViewer";
export * from "./components/Iframe";

View File

@@ -0,0 +1,97 @@
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

@@ -0,0 +1,31 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { Iframe, IframeProps } from '../src/components/Iframe';
const meta: Meta = {
title: 'Components/Iframe',
component: Iframe,
tags: ['autodocs'],
argTypes: {
url: {
description:
'Page to display inside of the component',
},
style: {
description:
'Style of the component',
},
},
};
export default meta;
type Story = StoryObj<IframeProps>;
export const Normal: Story = {
name: 'Iframe',
args: {
url: 'https://app.powerbi.com/view?r=eyJrIjoiYzBmN2Q2MzYtYzE3MS00ODkxLWE5OWMtZTQ2MjBlMDljMDk4IiwidCI6Ijk1M2IwZjgzLTFjZTYtNDVjMy04MmM5LTFkODQ3ZTM3MjMzOSIsImMiOjh9',
style: {width: `100%`, height: `100%`}
},
};

View File

@@ -21,6 +21,12 @@ const meta: Meta = {
zoom: {
description: 'Zoom level',
},
style: {
description: "Styles for the container"
},
autoZoomConfiguration: {
description: "Configuration to auto zoom in the specified layer data"
}
},
};
@@ -88,4 +94,32 @@ export const GeoJSONMultipleLayers: Story = {
center: { latitude: 45, longitude: 0 },
zoom: 2,
},
}
export const GeoJSONMultipleLayersWithAutoZoomInSpecifiedLayer: Story = {
name: 'GeoJSON polygons and points map with auto zoom in the points layer',
args: {
layers: [
{
data: 'https://opendata.arcgis.com/datasets/9c58741995174fbcb017cf46c8a42f4b_25.geojson',
name: 'Points',
tooltip: true,
},
{
data: '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

@@ -9,17 +9,22 @@ const meta: Meta = {
tags: ['autodocs'],
argTypes: {
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: {
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: {
description: "CSV data as string.",
description: 'CSV data as string.',
},
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 +34,7 @@ type Story = StoryObj<TableProps>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const FromColumnsAndData: Story = {
name: "Table from columns and data",
name: 'Table from columns and data',
args: {
data: [
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
@@ -49,21 +54,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 = {
name: "Table from raw CSV",
name: 'Table from raw CSV',
args: {
csv: `
Year,Temp Anomaly
1850,-0.418
2020,0.923
`
}
`,
},
};
export const FromURL: Story = {
name: "Table from URL",
name: 'Table from URL',
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

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

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 { AuthorConfig } from "./types";
export { Hero } from "./Hero";
export { Analytics, AnalyticsConfig } from "./analytics";

View File

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

View File

@@ -1,5 +1,11 @@
# @portaljs/remark-wiki-link
## 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
### Patch Changes

View File

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

View File

@@ -79,7 +79,7 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
data: { isEmbed, target, alias },
} = wikiLink;
// eslint-disable-next-line no-useless-escape
const wikiLinkWithHeadingPattern = /([\p{Letter}\d\s\/\.-_]*)(#.*)?/u;
const wikiLinkWithHeadingPattern = /^(.*?)(#.*)?$/u;
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern);
const possibleWikiLinkPermalinks = wikiLinkResolver(path);
@@ -110,13 +110,14 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
? "/" + possibleWikiLinkPermalinks[0]
: possibleWikiLinkPermalinks[0]) ||
"";
const isExternal = /^https?:\/\//.test(link);
wikiLink.data.exists = !!matchingPermalink;
wikiLink.data.permalink = link;
// remove leading # if the target is a heading on the same page
const displayName = alias || target.replace(/^#/, "");
const headingId = heading.replace(/\s+/, "-").toLowerCase();
const headingId = heading.replace(/\s+/g, "-").toLowerCase();
let classNames = wikiLinkClassName;
if (!matchingPermalink) {
classNames += " " + newClassName;
@@ -130,7 +131,7 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
wikiLink.data.hName = "a";
wikiLink.data.hProperties = {
className: classNames + " " + "transclusion",
href: hrefTemplate(link) + headingId,
href: hrefTemplate(link) + headingId
};
wikiLink.data.hChildren = [{ type: "text", value: displayName }];
@@ -163,7 +164,11 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
wikiLink.data.hProperties = {
className: classNames,
href: hrefTemplate(link) + headingId,
target: "_blank",
};
if(isExternal){
wikiLink.data.hProperties.target = "_blank"; // Open in a new tab
}
wikiLink.data.hChildren = [{ type: "text", value: displayName }];
}
}

View File

@@ -64,7 +64,7 @@ function html(opts: HtmlOptions = {}) {
const { target, alias } = wikiLink;
const isEmbed = token.isType === "embed";
// eslint-disable-next-line no-useless-escape
const wikiLinkWithHeadingPattern = /([\w\s\/\.-]*)(#.*)?/;
const wikiLinkWithHeadingPattern = /^(.*?)(#.*)?$/u;
const [, path, heading = ""] = target.match(wikiLinkWithHeadingPattern);
const possibleWikiLinkPermalinks = wikiLinkResolver(path);
@@ -96,10 +96,13 @@ function html(opts: HtmlOptions = {}) {
: possibleWikiLinkPermalinks[0]) ||
"";
const isExternal = /^https?:\/\//.test(link);
const openInNewTab = isExternal ? 'target="_blank"' : '';
// remove leading # if the target is a heading on the same page
const displayName = alias || target.replace(/^#/, "");
// replace spaces with dashes and lowercase headings
const headingId = heading.replace(/\s+/, "-").toLowerCase();
const headingId = heading.replace(/\s+/g, "-").toLowerCase();
let classNames = wikiLinkClassName;
if (!matchingPermalink) {
classNames += " " + newClassName;

View File

@@ -6,7 +6,7 @@ import { getPermalinks } from "../src/utils";
// const markdownFolder = path.join(__dirname, "/fixtures/content");
const markdownFolder = path.join(
".",
"/packages/remark-wiki-link/test/fixtures/content"
"test/fixtures/content"
);
describe("getPermalinks", () => {

View File

@@ -321,4 +321,24 @@ 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>`);
});
})
describe("External links", () => {
test("parses an external link that opens in a new tab", () => {
const serialized = micromark("[google](https://www.google.com/)", "ascii", {
extensions: [syntax()],
htmlExtensions: [html() as any],
});
expect(serialized).toBe(`<p><a href="https://www.google.com/" target="_blank">google</a></p>`);
});
})
});

View File

@@ -361,6 +361,32 @@ 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", () => {
test("doesn't parse a wiki link with two missing closing brackets", () => {
const processor = unified().use(markdown).use(wikiLinkPlugin);
@@ -560,3 +586,4 @@ describe("remark-wiki-link", () => {
});
});
});

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
authors: ['João Demenech', 'Luccas Mateus', 'Yoana Popova']
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

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
date: 2023-10-11
authors: ['Ola Rubaj']
filetype: blog
---
Hello, dear readers!

View File

@@ -2,6 +2,7 @@
title: What We Shipped in Jul-Aug 2023
authors: ['ola-rubaj']
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:

View File

@@ -23,8 +23,7 @@ const config = {
{ name: 'Guide', href: '/guide' },
{
name: 'Examples',
href: 'https://github.com/datopian/portaljs/tree/main/examples',
target: '_blank',
href: '/examples/'
},
{
name: 'Components',
@@ -70,7 +69,7 @@ const config = {
},
},
github: 'https://github.com/datopian/portaljs',
discord: 'https://discord.gg/EeyfGrGu4U',
discord: 'https://discord.gg/xfFDMPU9dC',
tableOfContents: true,
analytics: 'G-96GWZHMH57',
// editLinkShow: true,

View File

@@ -0,0 +1,5 @@
# Examples
For now, see the examples folder in github:
https://github.com/datopian/portaljs/tree/main/examples

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"]
}