Compare commits
35 Commits
feat/site/
...
@portaljs/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96904aef0d | ||
|
|
92a549d6a9 | ||
|
|
1a5bbd4346 | ||
|
|
4985576183 | ||
|
|
7049917ef7 | ||
|
|
dd03a493be | ||
|
|
e5b0a85e48 | ||
|
|
a93b13f448 | ||
|
|
8a4ec39d25 | ||
|
|
38bf06f031 | ||
|
|
8560f165fd | ||
|
|
b13e3ade3c | ||
|
|
1394f02038 | ||
|
|
e687779fa6 | ||
|
|
2ec143707d | ||
|
|
4ddfc1126a | ||
|
|
f23d7965f2 | ||
|
|
97e4775894 | ||
|
|
3c14ce8af7 | ||
|
|
61c750b7e1 | ||
|
|
b55ec5126c | ||
|
|
712f4a3b0f | ||
|
|
03960c8bac | ||
|
|
73c7eaf145 | ||
|
|
542f2ede9e | ||
|
|
f17c2ed1d0 | ||
|
|
f1d7e68077 | ||
|
|
1663b09a86 | ||
|
|
b940c82d93 | ||
|
|
492593dedb | ||
|
|
4ae22c7411 | ||
|
|
85bb6cb98c | ||
|
|
737f880036 | ||
|
|
1a9d64e0cf | ||
|
|
3366086d87 |
8
package-lock.json
generated
8
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -1,5 +1,31 @@
|
||||
# @portaljs/components
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@portaljs/components",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.3",
|
||||
"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": {
|
||||
|
||||
81
packages/components/src/components/BucketViewer.tsx
Normal file
81
packages/components/src/components/BucketViewer.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
export interface BucketViewerProps {
|
||||
domain: string;
|
||||
suffix?: string;
|
||||
className?: string;
|
||||
dataMapperFn: (rawData: Response) => Promise<BucketViewerData[]>;
|
||||
}
|
||||
|
||||
export interface BucketViewerData {
|
||||
fileName: string;
|
||||
downloadFileUri: string;
|
||||
dateProps?: {
|
||||
date: Date;
|
||||
dateFormatter: (date: Date) => string;
|
||||
};
|
||||
}
|
||||
|
||||
export function BucketViewer({
|
||||
domain,
|
||||
suffix,
|
||||
dataMapperFn,
|
||||
className,
|
||||
}: BucketViewerProps) {
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [bucketFiles, setBucketFiles] = useState<BucketViewerData[]>([]);
|
||||
suffix = suffix ?? '/';
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true);
|
||||
fetch(`${domain}${suffix}`)
|
||||
.then((res) => dataMapperFn(res))
|
||||
.then((data) => setBucketFiles(data))
|
||||
.finally(() => setIsLoading(false));
|
||||
}, [domain, suffix]);
|
||||
return isLoading ? (
|
||||
<div className="w-full flex items-center justify-center h-[300px]">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
) : bucketFiles ? (
|
||||
<>
|
||||
{...bucketFiles?.map((data, i) => (
|
||||
<ul
|
||||
onClick={() => {
|
||||
const anchorId = `download_anchor_${i}`;
|
||||
const a: HTMLAnchorElement =
|
||||
(document.getElementById(anchorId) as HTMLAnchorElement | null) ??
|
||||
document.createElement('a');
|
||||
a.id = anchorId;
|
||||
if (a.download) a.click();
|
||||
else {
|
||||
setIsLoading(true);
|
||||
fetch(data.downloadFileUri)
|
||||
.then((res) => res.blob())
|
||||
.then((res) => {
|
||||
a.href = URL.createObjectURL(res);
|
||||
a.download = res.name ?? data.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}
|
||||
}}
|
||||
key={i}
|
||||
className={`${
|
||||
className ??
|
||||
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
|
||||
}`}
|
||||
>
|
||||
<li>{data.fileName}</li>
|
||||
{data.dateProps ? (
|
||||
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</ul>
|
||||
))}
|
||||
</>
|
||||
) : null;
|
||||
}
|
||||
14
packages/components/src/components/Iframe.tsx
Normal file
14
packages/components/src/components/Iframe.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
46
packages/components/stories/BucketViewer.stories.ts
Normal file
46
packages/components/stories/BucketViewer.stories.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import { BucketViewer, BucketViewerProps } from '../src/components/BucketViewer';
|
||||
|
||||
// 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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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()
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
};
|
||||
31
packages/components/stories/Iframe.stories.ts
Normal file
31
packages/components/stories/Iframe.stories.ts
Normal 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%`}
|
||||
},
|
||||
};
|
||||
@@ -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'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
36
packages/core/src/ui/analytics/GoogleAnalytics.tsx
Normal file
36
packages/core/src/ui/analytics/GoogleAnalytics.tsx
Normal 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,
|
||||
})
|
||||
}
|
||||
41
packages/core/src/ui/analytics/Plausible.tsx
Normal file
41
packages/core/src/ui/analytics/Plausible.tsx
Normal 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)
|
||||
}
|
||||
25
packages/core/src/ui/analytics/Posthog.tsx
Normal file
25
packages/core/src/ui/analytics/Posthog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
packages/core/src/ui/analytics/SimpleAnalytics.tsx
Normal file
29
packages/core/src/ui/analytics/SimpleAnalytics.tsx
Normal 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)
|
||||
}
|
||||
}
|
||||
20
packages/core/src/ui/analytics/Umami.tsx
Normal file
20
packages/core/src/ui/analytics/Umami.tsx
Normal 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
|
||||
/>
|
||||
)
|
||||
}
|
||||
82
packages/core/src/ui/analytics/index.tsx
Normal file
82
packages/core/src/ui/analytics/index.tsx
Normal 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,
|
||||
};
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
@@ -116,7 +116,7 @@ function fromMarkdown(opts: FromMarkdownOptions = {}) {
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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);
|
||||
@@ -99,7 +99,7 @@ function html(opts: HtmlOptions = {}) {
|
||||
// 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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -321,4 +321,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>`);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -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", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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!
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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',
|
||||
|
||||
5
site/content/examples/index.md
Normal file
5
site/content/examples/index.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Examples
|
||||
|
||||
For now, see the examples folder in github:
|
||||
|
||||
https://github.com/datopian/portaljs/tree/main/examples
|
||||
Reference in New Issue
Block a user