Compare commits

..

1 Commits

Author SHA1 Message Date
Luccas Mateus
d267a60847 Openspending blog 2023-10-12 14:09:28 -03:00
36 changed files with 55 additions and 823 deletions

8
package-lock.json generated
View File

@@ -48297,7 +48297,7 @@
},
"packages/components": {
"name": "@portaljs/components",
"version": "0.5.3",
"version": "0.4.0",
"dependencies": {
"@githubocto/flat-ui": "^0.14.1",
"@heroicons/react": "^2.0.17",
@@ -48734,7 +48734,7 @@
},
"packages/core": {
"name": "@portaljs/core",
"version": "1.0.8",
"version": "1.0.6",
"license": "MIT",
"dependencies": {
"@docsearch/react": "^3.3.3",
@@ -48773,7 +48773,7 @@
},
"packages/remark-embed": {
"name": "@portaljs/remark-embed",
"version": "1.0.5",
"version": "1.0.4",
"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.2",
"version": "1.1.0",
"license": "MIT",
"dependencies": {
"mdast-util-to-markdown": "^1.5.0",

View File

@@ -1,43 +1,5 @@
# @portaljs/components
## 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.5.5",
"version": "0.4.0",
"type": "module",
"description": "https://portaljs.org",
"keywords": [
@@ -29,8 +29,6 @@
"@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",
@@ -39,7 +37,6 @@
"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",
@@ -50,6 +47,9 @@
"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

@@ -1,225 +0,0 @@
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;
suffix?: string;
className?: string;
downloadComponent?: ReactNode;
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,
downloadComponent,
suffix,
dataMapperFn,
className,
filterState,
paginationConfig,
onLoadTotalNumberOfItems
}: BucketViewerProps) {
suffix = suffix ?? '/';
downloadComponent = downloadComponent ?? <></>;
const [isLoading, setIsLoading] = useState<boolean>(false);
const [showDownloadComponentOnLine, setShowDownloadComponentOnLine] = useState(0);
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 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}
onMouseEnter={() => setShowDownloadComponentOnLine(i)}
onMouseLeave={() => setShowDownloadComponentOnLine(undefined)}
className={`${
className ??
'mb-2 border-b-[2px] border-b-[red] hover:cursor-pointer'
}`}
>
{
downloadComponent && showDownloadComponentOnLine === i
? downloadComponent
: <></>
}
<div>
<li>{data.fileName}</li>
{data.dateProps && data.dateProps.dateFormatter ? (
<li>{data.dateProps.dateFormatter(data.dateProps.date)}</li>
) : (
<></>
)}
</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

@@ -1,14 +0,0 @@
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 { CSSProperties, useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import LoadingSpinner from './LoadingSpinner';
import loadData from '../lib/loadData';
import chroma from 'chroma-js';
@@ -21,19 +21,15 @@ 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({
@@ -48,8 +44,6 @@ 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>([]);
@@ -102,7 +96,6 @@ export function Map({
zoom={zoom}
scrollWheelZoom={false}
className="h-80 w-full"
style={style ?? {}}
// @ts-ignore
whenReady={(map: any) => {
// Enable zoom using scroll wheel
@@ -111,35 +104,17 @@ 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

@@ -8,5 +8,3 @@ 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

@@ -1,103 +0,0 @@
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',
},
downloadComponent: {
description:
'Component to be displayed on hover of each bucket data',
},
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: '/',
downloadComponent: LoadingSpinner(),
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

@@ -1,31 +0,0 @@
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,12 +21,6 @@ const meta: Meta = {
zoom: {
description: 'Zoom level',
},
style: {
description: "Styles for the container"
},
autoZoomConfiguration: {
description: "Configuration to auto zoom in the specified layer data"
}
},
};
@@ -94,32 +88,4 @@ 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

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

View File

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

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

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

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

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

@@ -1,82 +0,0 @@
/* 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,4 +21,3 @@ 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,8 +7,6 @@ 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,
});
@@ -18,8 +16,6 @@ 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,11 +1,5 @@
# @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.2",
"version": "1.1.1",
"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 = /^(.*?)(#.*)?$/u;
const wikiLinkWithHeadingPattern = /([\p{Letter}\d\s\/\.-_]*)(#.*)?/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+/g, "-").toLowerCase();
const headingId = heading.replace(/\s+/, "-").toLowerCase();
let classNames = wikiLinkClassName;
if (!matchingPermalink) {
classNames += " " + newClassName;

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 = /^(.*?)(#.*)?$/u;
const wikiLinkWithHeadingPattern = /([\w\s\/\.-]*)(#.*)?/;
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+/g, "-").toLowerCase();
const headingId = heading.replace(/\s+/, "-").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(
".",
"test/fixtures/content"
"/packages/remark-wiki-link/test/fixtures/content"
);
describe("getPermalinks", () => {

View File

@@ -321,14 +321,4 @@ 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

@@ -361,32 +361,6 @@ 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);
@@ -586,4 +560,3 @@ describe("remark-wiki-link", () => {
});
});
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

View File

@@ -1,17 +1,15 @@
---
title: 'Adding Maps to PortalJS: Enhancing Geospatial Data Visualization with PortalJS'
title: 'Enhancing Geospatial Data Visualization with PortalJS'
date: 2023-07-18
authors: ['João Demenech', 'Luccas Mateus', 'Yoana Popova']
filetype: 'blog'
---
This post walks you though adding maps and geospatial visualizations to PortalJS.
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.
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.
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.
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.
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.
## Exploring Map Formats

View File

@@ -3,7 +3,6 @@ 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

@@ -0,0 +1,27 @@
---
title: The Openspending Revamp
date: 2023-10-12
authors: ['Luccas Mateus', 'João Demenech']
filetype: 'blog'
---
Following up [our previous blog post about the Open Spending revamp](), we now want to share the underlying technologies and tools we used to achieve our goals.
First of all, we used PortalJS, a JavaScript library that provides a set of reusable React components for mainly for building data portals with data visualization. This is the core technology underlying the Open Spending website, and can be seen in action, for example, on the CSV previews powered by the [FlatUI Component](https://storybook.portaljs.org/?path=/story/components-flatuitable--from-url) from PortalJS. For more information, visit the official website at https://portaljs.org/.
![](https://hackmd.io/_uploads/Sypq8irW6.png)
Following up on [our previous blog post about the Open Spending revamp](https://www.datopian.com/blog/the-open-spending-revamp), we now want to share the underlying technologies and tools we used to achieve our goals.
First, we used PortalJS, a JavaScript library that provides a set of reusable React components primarily for building data portals with data visualization. This is the core technology behind the Open Spending website and can be seen in action, for example, in the CSV previews powered by PortalJS' [FlatUI Component](https://storybook.portaljs.org/?path=/story/components-flatuitable--from-url). More information can be found on the official website at https://portaljs.org/.
![](https://hackmd.io/_uploads/rkUAvjBZp.png)
![](https://hackmd.io/_uploads/H1CjUjSWp.png)
Secondly, we stored the metadata as frictionless datapackages in the `os-data` GitHub organization. Frictionless datapackages are a simple format and standard for describing and packaging a collection of (in our case tabular) data. They are typically used to publish FAIR and open datasets. To learn more about frictionless datapackages, visit their documentation page at https://framework.frictionlessdata.io/.
Octokit, a GitHub API client for Node.js, was used to easily retrieve the metadata for the datasets from the repositories. You can find more information about Octokit on its GitHub repository: https://github.com/octokit/octokit.js, and you can find all the dataset repositories on https://github.com/os-data.
Finally, to store the data, we used Cloudflare R2, a cloud storage service that allows developers to store large amounts of blob data without the costly egress bandwidth fees associated with typical cloud storage services. We chose this product because it offers a generous free tier that we could use for our project. You can read more about Cloudflare R2 on their official announcement page at https://cloudflare.net/news/news-details/2021/Cloudflare-Announces-R2-Storage-Rapid-and-Reliable-S3-Compatible-Object-Storage-Designed-for-the-Edge/default.aspx.
You can check out the code for this project at https://github.com/datopian/portaljs/tree/main/examples/openspending. The website is live on [openspending.org](https://www.openspending.org/), you can file issues at https://github.com/datopian/portaljs/issues and get help by accessing our [Discord Channel](https://discord.com/invite/EeyfGrGu4U).

View File

@@ -2,7 +2,6 @@
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

@@ -1,34 +0,0 @@
---
title: 'The OpenSpending Revamp: Behind the Scenes'
date: 2023-10-13
authors: ['Luccas Mateus', 'João Demenech']
filetype: 'blog'
---
_This post was originally published on [the Datopian blog](http://datopian.com/blog/the-open-spending-revamp-behind-the-scenes)._
In our last article, we explored [the Open Spending revamp](https://www.datopian.com/blog/the-open-spending-revamp). Now, let's dive into the tech stack that makes it tick. We'll unpack how PortalJS, Cloudflare R2, Frictionless Data Packages, and Octokit come together to power this next-level data portal. From our Javascript framework PortalJS, that shapes the user experience, to Cloudflare R2, the robust storage solution that secures the data, we'll examine how each piece of technology contributes to the bigger picture. We'll also delve into the roles of Frictionless Data Packages for metadata management and Octokit for automating dataset metadata retrieval. Read on for the inside scoop!
## The Core: PortalJS
At the core of the revamped OpenSpending website is [PortalJS](https://portaljs.org), a JavaScript library that's a game-changer in building powerful data portals with data visualizations. What makes it so special? Well, it's packed with reusable React components that make our lives - and yours - a whole lot easier. Take, for example, our sleek CSV previews; they're brought to life by PortalJS' [FlatUI Component](https://storybook.portaljs.org/?path=/story/components-flatuitable--from-url). It helps transform raw numbers into visuals that you can easily understand and use. Curious to know more? Check out the [official PortalJS website](https://portaljs.org).
![Data visualization](/assets/blog/2023-10-13-the-open-spending-revamp-behind-the-scenes/data-visualization.png)
## Metadata: Frictionless Data Packages
Storing metadata might seem like a backstage operation, but it is pivotal. We chose Frictionless Data Packages, housed in the `os-data` GitHub organization as repositories, to serve this purpose. Frictionless Data Packages offer a simple but powerful format for cataloging and packaging a collection of data - in our scenario, that's primarily tabular data. These aren't merely storage bins - they align with FAIR principles, ensuring that the data is easily Findable, Accessible, Interoperable, and Reusable. This alignment positions them as an ideal solution for publishing datasets designed to be both openly accessible and highly usable. Learn more from their [official documentation](https://framework.frictionlessdata.io/).
## The Link: Octokit
Can you imagine having to manually gather metadata for each dataset from multiple GitHub repositories? Sounds tedious, right? Thats why we used Octokit, a GitHub API client for Node.js. This tool takes care of the heavy lifting, automating the metadata retrieval process for us. If you're intrigued by Octokit's capabilities, you can discover more in its [GitHub repository](https://github.com/octokit/octokit.js). To explore the datasets we've been working on, take a look at [OpenSpending Datasets](https://github.com/os-data).
## Storage: Cloudflare R2
When it comes to data storage, Cloudflare R2 emerges as our choice, defined by its blend of speed and reliability. This service empowers developers to securely store large amounts of blob data without the costly egress bandwidth fees associated with typical cloud storage services. For a comprehensive understanding of Cloudflare R2, their [blog post](https://cloudflare.net/news/news-details/2021/Cloudflare-Announces-R2-Storage-Rapid-and-Reliable-S3-Compatible-Object-Storage-Designed-for-the-Edge/default.aspx) serves as an excellent resource.
## In Closing
In closing, we invite you to explore the architecture and code that power this project. It's all openly accessible in our [GitHub repository](https://github.com/datopian/portaljs/tree/main/examples/openspending). Should you want to experience the end result firsthand, feel free to visit [openspending.org](https://www.openspending.org/). If you encounter any issues or have suggestions to improve the project, we welcome your contributions via our [GitHub issues page](https://github.com/datopian/portaljs/issues). For real-time assistance and to engage with our community, don't hesitate to join our [Discord Channel](https://discord.com/invite/EeyfGrGu4U). Thank you for taking the time to read about our work! We look forward to fostering a collaborative environment where knowledge is freely shared and continually enriched. ♥️
![FlatUiTable Code Snippet](/assets/blog/2023-10-13-the-open-spending-revamp-behind-the-scenes/code-example.png)

View File

@@ -23,7 +23,8 @@ const config = {
{ name: 'Guide', href: '/guide' },
{
name: 'Examples',
href: '/examples/'
href: 'https://github.com/datopian/portaljs/tree/main/examples',
target: '_blank',
},
{
name: 'Components',

View File

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