Compare commits

..

10 Commits

Author SHA1 Message Date
Luccas Mateus de Medeiros Gomes
2bb567e88d [docs][m] - creating an inedx page 2023-05-03 11:30:11 -03:00
Luccas Mateus de Medeiros Gomes
77da0d5693 [learn-example][sm] - dont panic when no markdown.db file found 2023-05-03 10:17:12 -03:00
Luccas Mateus de Medeiros Gomes
b52aa38249 [learn-example][m] - code section for the tutorial part 3 2023-05-03 09:49:50 -03:00
João Demenech
72405162a1 merge: fix storybook build
* [package][xs]: remove parameter that was not being used

* [package][xs]: remove import that's breaking the build

* [package][xs]: trying to fix build error on Vercel
2023-05-02 17:31:48 -03:00
João Demenech
982733737d merge: Components package initial setup + Components extraction
**Issue:** https://github.com/datopian/portaljs/issues/812

## Changes

- Renamed old package to "components-old"
- Created a Vite project based on https://dev.to/nicolaserny/create-a-react-component-library-with-vite-and-typescript-1ih9 and  https://zach.codes/build-your-own-flexible-component-library-using-tsdx-typescript-tailwind-css-headless-ui/
- Implemented tailwind on it
- Extracted components
  - LineChart
  - Table
  - Vega
  - VegaLite
- Created stories for the extracted components
2023-05-02 16:41:28 -03:00
deme
ea5802a908 [#812,package][xl]: changed project to Vite, created stories for LineChart, Table, Vega and VegaLite 2023-05-02 16:37:22 -03:00
Luccas Mateus
229a7b5324 [alan-turing][m] - fix markdown (#831) 2023-05-02 15:31:45 -03:00
Luccas Mateus
014c4c043d [alan-turing][m] - small tweaks (#830) 2023-05-02 12:53:10 -03:00
deme
016f3e20e9 [#812,package][xl]: add Table component and story for it 2023-05-01 18:56:22 -03:00
deme
169a92d313 [#812,package][xl]: initial versioning of the package 2023-05-01 15:53:42 -03:00
73 changed files with 12170 additions and 273 deletions

View File

@@ -23,12 +23,7 @@ import { serialize } from "next-mdx-remote/serialize";
* @returns: { mdxSource: mdxSource, frontMatter: ...} * @returns: { mdxSource: mdxSource, frontMatter: ...}
*/ */
const parse = async function (source, format) { const parse = async function (source, format) {
const { content, data, excerpt } = matter(source, { const { content, data } = matter(source);
excerpt: (file, options) => {
// Generate an excerpt for the file
file.excerpt = file.content.split("\n\n")[0];
},
});
const mdxSource = await serialize( const mdxSource = await serialize(
{ value: content, path: format }, { value: content, path: format },
@@ -56,7 +51,7 @@ const parse = async function (source, format) {
[ [
rehypeAutolinkHeadings, rehypeAutolinkHeadings,
{ {
properties: { className: 'heading-link' }, properties: { className: "heading-link" },
test(element) { test(element) {
return ( return (
["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) && ["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) &&
@@ -91,14 +86,12 @@ const parse = async function (source, format) {
], ],
format, format,
}, },
scope: data,
} }
); );
return { return {
mdxSource: mdxSource, mdxSource: mdxSource,
frontMatter: data, frontMatter: data,
excerpt,
}; };
}; };

View File

@@ -36,7 +36,8 @@
"focus-visible": "^5.2.0", "focus-visible": "^5.2.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hastscript": "^7.2.0", "hastscript": "^7.2.0",
"mdx-mermaid": "2.0.0-rc7", "mdx-mermaid": "^2.0.0-rc7",
"mermaid": "^10.1.0",
"next": "13.2.1", "next": "13.2.1",
"next-mdx-remote": "^4.4.1", "next-mdx-remote": "^4.4.1",
"next-router-mock": "^0.9.3", "next-router-mock": "^0.9.3",
@@ -3338,7 +3339,6 @@
"version": "7.0.10", "version": "7.0.10",
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz",
"integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==",
"peer": true,
"dependencies": { "dependencies": {
"d3": "^7.8.2", "d3": "^7.8.2",
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
@@ -3353,8 +3353,7 @@
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.7", "version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
"peer": true
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.3.4", "version": "4.3.4",
@@ -3544,8 +3543,7 @@
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "2.4.5", "version": "2.4.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz",
"integrity": "sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==", "integrity": "sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA=="
"peer": true
}, },
"node_modules/dot-prop": { "node_modules/dot-prop": {
"version": "5.3.0", "version": "5.3.0",
@@ -7533,7 +7531,6 @@
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.1.0.tgz", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.1.0.tgz",
"integrity": "sha512-LYekSMNJygI1VnMizAPUddY95hZxOjwZxr7pODczILInO0dhQKuhXeu4sargtnuTwCilSuLS7Uiq/Qn7HTVrmA==", "integrity": "sha512-LYekSMNJygI1VnMizAPUddY95hZxOjwZxr7pODczILInO0dhQKuhXeu4sargtnuTwCilSuLS7Uiq/Qn7HTVrmA==",
"peer": true,
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "^6.0.0", "@braintree/sanitize-url": "^6.0.0",
"@khanacademy/simple-markdown": "^0.8.6", "@khanacademy/simple-markdown": "^0.8.6",
@@ -7558,7 +7555,6 @@
"version": "0.8.6", "version": "0.8.6",
"resolved": "https://registry.npmjs.org/@khanacademy/simple-markdown/-/simple-markdown-0.8.6.tgz", "resolved": "https://registry.npmjs.org/@khanacademy/simple-markdown/-/simple-markdown-0.8.6.tgz",
"integrity": "sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==", "integrity": "sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==",
"peer": true,
"dependencies": { "dependencies": {
"@types/react": ">=16.0.0" "@types/react": ">=16.0.0"
}, },
@@ -15739,7 +15735,6 @@
"version": "7.0.10", "version": "7.0.10",
"resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz", "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.10.tgz",
"integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==", "integrity": "sha512-qTCQmEhcynucuaZgY5/+ti3X/rnszKZhEQH/ZdWdtP1tA/y3VoHJzcVrO9pjjJCNpigfscAtoUB5ONcd2wNn0A==",
"peer": true,
"requires": { "requires": {
"d3": "^7.8.2", "d3": "^7.8.2",
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
@@ -15754,8 +15749,7 @@
"dayjs": { "dayjs": {
"version": "1.11.7", "version": "1.11.7",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.7.tgz",
"integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ==", "integrity": "sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ=="
"peer": true
}, },
"debug": { "debug": {
"version": "4.3.4", "version": "4.3.4",
@@ -15894,8 +15888,7 @@
"dompurify": { "dompurify": {
"version": "2.4.5", "version": "2.4.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.4.5.tgz",
"integrity": "sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA==", "integrity": "sha512-jggCCd+8Iqp4Tsz0nIvpcb22InKEBrGz5dw3EQJMs8HPJDsKbFIO3STYtAvCfDx26Muevn1MHVI0XxjgFfmiSA=="
"peer": true
}, },
"dot-prop": { "dot-prop": {
"version": "5.3.0", "version": "5.3.0",
@@ -18847,7 +18840,6 @@
"version": "10.1.0", "version": "10.1.0",
"resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.1.0.tgz", "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-10.1.0.tgz",
"integrity": "sha512-LYekSMNJygI1VnMizAPUddY95hZxOjwZxr7pODczILInO0dhQKuhXeu4sargtnuTwCilSuLS7Uiq/Qn7HTVrmA==", "integrity": "sha512-LYekSMNJygI1VnMizAPUddY95hZxOjwZxr7pODczILInO0dhQKuhXeu4sargtnuTwCilSuLS7Uiq/Qn7HTVrmA==",
"peer": true,
"requires": { "requires": {
"@braintree/sanitize-url": "^6.0.0", "@braintree/sanitize-url": "^6.0.0",
"@khanacademy/simple-markdown": "^0.8.6", "@khanacademy/simple-markdown": "^0.8.6",
@@ -18872,7 +18864,6 @@
"version": "0.8.6", "version": "0.8.6",
"resolved": "https://registry.npmjs.org/@khanacademy/simple-markdown/-/simple-markdown-0.8.6.tgz", "resolved": "https://registry.npmjs.org/@khanacademy/simple-markdown/-/simple-markdown-0.8.6.tgz",
"integrity": "sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==", "integrity": "sha512-mAUlR9lchzfqunR89pFvNI51jQKsMpJeWYsYWw0DQcUXczn/T/V6510utgvm7X0N3zN87j1SvuKk8cMbl9IAFw==",
"peer": true,
"requires": { "requires": {
"@types/react": ">=16.0.0" "@types/react": ">=16.0.0"
} }

View File

@@ -12,47 +12,46 @@
}, },
"browserslist": "defaults, not ie <= 11", "browserslist": "defaults, not ie <= 11",
"dependencies": { "dependencies": {
"@flowershow/core": "^0.4.10",
"@flowershow/markdowndb": "^0.1.1", "@flowershow/markdowndb": "^0.1.1",
"@flowershow/remark-callouts": "^1.0.0",
"@flowershow/remark-embed": "^1.0.0",
"@flowershow/remark-wiki-link": "^1.1.2",
"@headlessui/react": "^1.7.13", "@headlessui/react": "^1.7.13",
"@heroicons/react": "^2.0.17",
"@mapbox/rehype-prism": "^0.8.0", "@mapbox/rehype-prism": "^0.8.0",
"@mdx-js/loader": "^2.1.5", "@mdx-js/loader": "^2.1.5",
"@mdx-js/react": "^2.1.5", "@mdx-js/react": "^2.1.5",
"@next/mdx": "^13.0.2", "@next/mdx": "^13.0.2",
"@opentelemetry/api": "^1.4.0",
"@tailwindcss/forms": "^0.5.3", "@tailwindcss/forms": "^0.5.3",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"autoprefixer": "^10.4.12",
"clsx": "^1.2.1",
"fast-glob": "^3.2.11",
"feed": "^4.2.2",
"flexsearch": "^0.7.31",
"focus-visible": "^5.2.0",
"next-router-mock": "^0.9.3",
"next-superjson-plugin": "^0.5.7",
"postcss-focus-visible": "^6.0.4",
"react-hook-form": "^7.43.9",
"react-markdown": "^8.0.7",
"superjson": "^1.12.3",
"tailwindcss": "^3.3.0",
"@flowershow/core": "^0.4.10",
"@flowershow/remark-callouts": "^1.0.0",
"@flowershow/remark-embed": "^1.0.0",
"@flowershow/remark-wiki-link": "^1.1.2",
"@heroicons/react": "^2.0.17",
"@opentelemetry/api": "^1.4.0",
"@tanstack/react-table": "^8.8.5", "@tanstack/react-table": "^8.8.5",
"@types/node": "18.16.0", "@types/node": "18.16.0",
"@types/react": "18.2.0", "@types/react": "18.2.0",
"@types/react-dom": "18.2.0", "@types/react-dom": "18.2.0",
"autoprefixer": "^10.4.12",
"clsx": "^1.2.1",
"eslint": "8.39.0", "eslint": "8.39.0",
"eslint-config-next": "13.3.1", "eslint-config-next": "13.3.1",
"fast-glob": "^3.2.11",
"feed": "^4.2.2",
"flexsearch": "^0.7.31",
"focus-visible": "^5.2.0",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hastscript": "^7.2.0", "hastscript": "^7.2.0",
"mdx-mermaid": "2.0.0-rc7", "mdx-mermaid": "^2.0.0-rc7",
"mermaid": "^10.1.0",
"next": "13.2.1", "next": "13.2.1",
"next-mdx-remote": "^4.4.1", "next-mdx-remote": "^4.4.1",
"next-router-mock": "^0.9.3",
"next-superjson-plugin": "^0.5.7",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"postcss-focus-visible": "^6.0.4",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-hook-form": "^7.43.9",
"react-markdown": "^8.0.7",
"react-vega": "^7.6.0", "react-vega": "^7.6.0",
"rehype-autolink-headings": "^6.1.1", "rehype-autolink-headings": "^6.1.1",
"rehype-katex": "^6.0.3", "rehype-katex": "^6.0.3",
@@ -61,7 +60,9 @@
"remark-gfm": "^3.0.1", "remark-gfm": "^3.0.1",
"remark-math": "^5.1.1", "remark-math": "^5.1.1",
"remark-smartypants": "^2.0.0", "remark-smartypants": "^2.0.0",
"remark-toc": "^8.0.1" "remark-toc": "^8.0.1",
"superjson": "^1.12.3",
"tailwindcss": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
"eslint": "8.26.0", "eslint": "8.26.0",

View File

@@ -1,10 +1,12 @@
import { Container } from '../components/Container' import { Container } from '../components/Container'
import clientPromise from '../lib/mddb' import clientPromise from '../lib/mddb'
import fs from 'fs' import { promises as fs } from 'fs';
import { MDXRemote } from 'next-mdx-remote' import { MDXRemote } from 'next-mdx-remote'
import { serialize } from 'next-mdx-remote/serialize' import { serialize } from 'next-mdx-remote/serialize'
import { Card } from '../components/Card' import { Card } from '../components/Card'
import Head from 'next/head' import Head from 'next/head'
import parse from '../lib/markdown'
import { Mermaid } from '@flowershow/core';
export const getStaticProps = async ({ params }) => { export const getStaticProps = async ({ params }) => {
const urlPath = params.slug ? params.slug.join('/') : '' const urlPath = params.slug ? params.slug.join('/') : ''
@@ -12,8 +14,8 @@ export const getStaticProps = async ({ params }) => {
const mddb = await clientPromise const mddb = await clientPromise
const dbFile = await mddb.getFileByUrl(urlPath) const dbFile = await mddb.getFileByUrl(urlPath)
const source = fs.readFileSync(dbFile.file_path, { encoding: 'utf-8' }) const source = await fs.readFile(dbFile.file_path,'utf-8')
const mdxSource = await serialize(source, { parseFrontmatter: true }) let mdxSource = await parse(source, '.mdx')
return { return {
props: { props: {
@@ -74,7 +76,7 @@ const Meta = ({keyValuePairs}) => {
} }
export default function DRDPage({ mdxSource }) { export default function DRDPage({ mdxSource }) {
const meta = mdxSource.frontmatter const meta = mdxSource.frontMatter
const keyValuePairs = Object.entries(meta).filter( const keyValuePairs = Object.entries(meta).filter(
(entry) => entry[0] !== 'title' (entry) => entry[0] !== 'title'
) )
@@ -94,7 +96,7 @@ export default function DRDPage({ mdxSource }) {
</Card> </Card>
</header> </header>
<div className="prose dark:prose-invert"> <div className="prose dark:prose-invert">
<MDXRemote {...mdxSource} /> <MDXRemote {...mdxSource.mdxSource} components={{mermaid: Mermaid}} />
</div> </div>
</article> </article>
</Container> </Container>

View File

@@ -0,0 +1,40 @@
import { Index } from 'flexsearch';
import { useState } from 'react';
import DebouncedInput from './DebouncedInput';
export default function Catalog({ datasets }: { datasets: any[] }) {
const [indexFilter, setIndexFilter] = useState('');
const index = new Index({ tokenize: "full"});
datasets.forEach((dataset) =>
index.add(
dataset._id,
Object.entries(dataset.metadata).reduce(
(acc, curr) => acc + ' ' + curr.toString(),
''
) + ' ' + dataset.url_path
)
);
return (
<>
<DebouncedInput
value={indexFilter ?? ''}
onChange={(value) => setIndexFilter(String(value))}
className="p-2 text-sm shadow border border-block"
placeholder="Search all datasets..."
/>
<ul>
{datasets
.filter((dataset) =>
indexFilter !== ''
? index.search(indexFilter).includes(dataset._id)
: true
)
.map((dataset) => (
<li key={dataset.id}>
<a href={dataset.url_path}>{dataset.metadata.title ? dataset.metadata.title : dataset.url_path}</a>
</li>
))}
</ul>
</>
);
}

View File

@@ -8,6 +8,7 @@ import { Mermaid } from '@flowershow/core';
// here. // here.
const components = { const components = {
Table: dynamic(() => import('./Table')), Table: dynamic(() => import('./Table')),
Catalog: dynamic(() => import('./Catalog')),
mermaid: Mermaid, mermaid: Mermaid,
// Excel: dynamic(() => import('../components/Excel')), // Excel: dynamic(() => import('../components/Excel')),
// TODO: try and make these dynamic ... // TODO: try and make these dynamic ...

View File

@@ -22,7 +22,7 @@ import { serialize } from "next-mdx-remote/serialize";
* @format: used to indicate to next-mdx-remote which format to use (md or mdx) * @format: used to indicate to next-mdx-remote which format to use (md or mdx)
* @returns: { mdxSource: mdxSource, frontMatter: ...} * @returns: { mdxSource: mdxSource, frontMatter: ...}
*/ */
const parse = async function (source, format) { const parse = async function (source, format, scope) {
const { content, data, excerpt } = matter(source, { const { content, data, excerpt } = matter(source, {
excerpt: (file, options) => { excerpt: (file, options) => {
// Generate an excerpt for the file // Generate an excerpt for the file
@@ -91,7 +91,7 @@ const parse = async function (source, format) {
], ],
format, format,
}, },
scope: data, scope: { ...scope, ...data},
} }
); );

View File

@@ -0,0 +1,14 @@
import { MarkdownDB } from "@flowershow/markdowndb";
const dbPath = "markdown.db";
const client = new MarkdownDB({
client: "sqlite3",
connection: {
filename: dbPath,
},
});
const clientPromise = client.init();
export default clientPromise;

File diff suppressed because it is too large Load Diff

View File

@@ -7,21 +7,26 @@
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"export": "npm run build && next export -o out" "export": "npm run build && next export -o out",
"prebuild": "npm run mddb",
"mddb": "mddb ./content"
}, },
"dependencies": { "dependencies": {
"@flowershow/core": "^0.4.10", "@flowershow/core": "^0.4.10",
"@flowershow/markdowndb": "^0.1.1",
"@flowershow/remark-callouts": "^1.0.0", "@flowershow/remark-callouts": "^1.0.0",
"@flowershow/remark-embed": "^1.0.0", "@flowershow/remark-embed": "^1.0.0",
"@flowershow/remark-wiki-link": "^1.1.2", "@flowershow/remark-wiki-link": "^1.1.2",
"@heroicons/react": "^2.0.17", "@heroicons/react": "^2.0.17",
"@opentelemetry/api": "^1.4.0", "@opentelemetry/api": "^1.4.0",
"@tanstack/react-table": "^8.8.5", "@tanstack/react-table": "^8.8.5",
"@types/flexsearch": "^0.7.3",
"@types/node": "18.16.0", "@types/node": "18.16.0",
"@types/react": "18.2.0", "@types/react": "18.2.0",
"@types/react-dom": "18.2.0", "@types/react-dom": "18.2.0",
"eslint": "8.39.0", "eslint": "8.39.0",
"eslint-config-next": "13.3.1", "eslint-config-next": "13.3.1",
"flexsearch": "^0.7.31",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hastscript": "^7.2.0", "hastscript": "^7.2.0",
"mdx-mermaid": "2.0.0-rc7", "mdx-mermaid": "2.0.0-rc7",

View File

@@ -1,7 +1,8 @@
import { promises as fs } from 'fs'; import { existsSync, promises as fs } from 'fs';
import path from 'path'; import path from 'path';
import parse from '../lib/markdown'; import parse from '../lib/markdown';
import DRD from '../components/DRD'; import DRD from '../components/DRD';
import clientPromise from '../lib/mddb';
export const getStaticPaths = async () => { export const getStaticPaths = async () => {
const contentDir = path.join(process.cwd(), '/content/'); const contentDir = path.join(process.cwd(), '/content/');
@@ -23,9 +24,28 @@ export const getStaticProps = async (context) => {
pathToFile = context.params.path.join('/') + '/index.md'; pathToFile = context.params.path.join('/') + '/index.md';
} }
let datasets = [];
const mddbFileExists = existsSync('markdown.db');
if (mddbFileExists) {
const mddb = await clientPromise;
const datasetsFiles = await mddb.getFiles({
extensions: ['md', 'mdx'],
});
datasets = datasetsFiles
.filter((dataset) => dataset.url_path !== '/')
.map((dataset) => ({
_id: dataset._id,
url_path: dataset.url_path,
file_path: dataset.file_path,
metadata: dataset.metadata,
}));
}
const indexFile = path.join(process.cwd(), '/content/' + pathToFile); const indexFile = path.join(process.cwd(), '/content/' + pathToFile);
const readme = await fs.readFile(indexFile, 'utf8'); const readme = await fs.readFile(indexFile, 'utf8');
let { mdxSource, frontMatter } = await parse(readme, '.mdx');
let { mdxSource, frontMatter } = await parse(readme, '.mdx', { datasets });
return { return {
props: { props: {
mdxSource, mdxSource,

1424
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,10 @@
"license": "MIT", "license": "MIT",
"scripts": {}, "scripts": {},
"private": true, "private": true,
"dependencies": {},
"devDependencies": { "devDependencies": {
"@babel/preset-react": "^7.14.5", "@babel/preset-react": "^7.14.5",
"@nrwl/cypress": "15.9.2", "@nrwl/cypress": "15.9.2",
"@nrwl/eslint-plugin-nx": "15.9.2", "@nrwl/eslint-plugin-nx": "^16.0.2",
"@nrwl/jest": "15.9.2", "@nrwl/jest": "15.9.2",
"@nrwl/js": "15.9.2", "@nrwl/js": "15.9.2",
"@nrwl/linter": "15.9.2", "@nrwl/linter": "15.9.2",

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 627 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,16 @@
module.exports = {
env: {
browser: true,
es2020: true
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module'
},
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': 'warn'
}
};

24
packages/components/.gitignore vendored Normal file
View File

@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -0,0 +1,17 @@
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: ['../stories/**/*.mdx', '../stories/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
};
export default config;

View File

@@ -0,0 +1,17 @@
import 'tailwindcss/tailwind.css'
import type { Preview } from '@storybook/react';
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@@ -0,0 +1,32 @@
# `components` package
**See live:** https://storybook.portaljs.org
## Dev
Use Storybook to work on components by running:
```bash
yarn storybook
```
## Build
To build this project, run:
```bash
yarn build
```
## Install
TODO: command to install the package
### Next.js
Add this line at the start of `_app.tsx`:
```ts
// pages/_app.tsx
import "dist/styles.css";
```

View File

@@ -0,0 +1,70 @@
{
"name": "components",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "yarn storybook",
"build": "tsc && vite build && yarn build-tailwind",
"lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"prepack": "json -f package.json -I -e \"delete this.devDependencies; delete this.dependencies\"",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build",
"build-tailwind": "NODE_ENV=production npx tailwindcss -o ./dist/styles.css --minify"
},
"peerDependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"dependencies": {
"@heroicons/react": "^2.0.17",
"next-mdx-remote": "^4.4.1",
"papaparse": "^5.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-vega": "^7.6.0",
"vega": "5.20.2",
"vega-lite": "5.1.0",
"@tanstack/react-table": "^8.8.5"
},
"devDependencies": {
"@storybook/addon-essentials": "^7.0.7",
"@storybook/addon-interactions": "^7.0.7",
"@storybook/addon-links": "^7.0.7",
"@storybook/blocks": "^7.0.7",
"@storybook/react": "^7.0.7",
"@storybook/react-vite": "^7.0.7",
"@storybook/testing-library": "^0.0.14-next.2",
"@types/papaparse": "^5.3.7",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"@vitejs/plugin-react": "^4.0.0",
"autoprefixer": "^10.4.14",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"eslint-plugin-storybook": "^0.6.11",
"json": "^11.0.0",
"postcss": "^8.4.23",
"prop-types": "^15.8.1",
"storybook": "^7.0.7",
"tailwindcss": "^3.3.2",
"typescript": "^5.0.2",
"vite": "^4.3.2",
"vite-plugin-dts": "^2.3.0"
},
"files": [
"dist"
],
"main": "./dist/components.umd.js",
"module": "./dist/components.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/components.es.js",
"require": "./dist/components.umd.js"
}
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@@ -0,0 +1,32 @@
import { useEffect, useState } from "react";
const DebouncedInput = ({
value: initialValue,
onChange,
debounce = 500,
...props
}) => {
const [value, setValue] = useState(initialValue);
useEffect(() => {
setValue(initialValue);
}, [initialValue]);
useEffect(() => {
const timeout = setTimeout(() => {
onChange(value);
}, debounce);
return () => clearTimeout(timeout);
}, [value]);
return (
<input
{...props}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
);
};
export default DebouncedInput;

View File

@@ -0,0 +1,63 @@
import { VegaLite } from './VegaLite';
export type LineChartProps = {
data: Array<Array<string | number>> | string | { x: string; y: number }[];
title?: string;
xAxis?: string;
yAxis?: string;
fullWidth?: boolean;
};
export function LineChart({
data = [],
fullWidth = false,
title = '',
xAxis = 'x',
yAxis = 'y',
}: LineChartProps) {
var tmp = data;
if (Array.isArray(data)) {
tmp = data.map((r) => {
return { x: r[0], y: r[1] };
});
}
const vegaData = { table: tmp };
const spec = {
$schema: 'https://vega.github.io/schema/vega-lite/v5.json',
title,
width: 'container',
height: 300,
mark: {
type: 'line',
color: 'black',
strokeWidth: 1,
tooltip: true,
},
data: {
name: 'table',
},
selection: {
grid: {
type: 'interval',
bind: 'scales',
},
},
encoding: {
x: {
field: xAxis,
timeUnit: 'year',
type: 'temporal',
},
y: {
field: yAxis,
type: 'quantitative',
},
},
};
if (typeof data === 'string') {
spec.data = { url: data } as any;
return <VegaLite fullWidth={fullWidth} spec={spec} />;
}
return <VegaLite fullWidth={fullWidth} data={vegaData} spec={spec} />;
}

View File

@@ -0,0 +1,195 @@
import {
createColumnHelper,
FilterFn,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';
import {
ArrowDownIcon,
ArrowUpIcon,
ChevronDoubleLeftIcon,
ChevronDoubleRightIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from '@heroicons/react/24/solid';
import React, { useEffect, useMemo, useState } from 'react';
import parseCsv from '../lib/parseCsv';
import DebouncedInput from './DebouncedInput';
import loadData from '../lib/loadData';
export type TableProps = {
data?: Array<{ [key: string]: number | string }>;
cols?: Array<{ [key: string]: string }>;
csv?: string;
url?: string;
fullWidth?: boolean;
};
export const Table = ({
data: ogData = [],
cols: ogCols = [],
csv = '',
url = '',
fullWidth = false,
}: TableProps) => {
if (csv) {
const out = parseCsv(csv);
ogData = out.rows;
ogCols = out.fields;
}
const [data, setData] = React.useState(ogData);
const [cols, setCols] = React.useState(ogCols);
// const [error, setError] = React.useState(""); // TODO: add error handling
const tableCols = useMemo(() => {
const columnHelper = createColumnHelper();
return cols.map((c) =>
columnHelper.accessor<any, string>(c.key, {
header: () => c.name,
cell: (info) => info.getValue(),
})
);
}, [data, cols]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns: tableCols,
getCoreRowModel: getCoreRowModel(),
state: {
globalFilter,
},
globalFilterFn: globalFilterFn,
onGlobalFilterChange: setGlobalFilter,
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
});
useEffect(() => {
if (url) {
loadData(url).then((data) => {
const { rows, fields } = parseCsv(data);
setData(rows);
setCols(fields);
});
}
}, [url]);
return (
<div className={`${fullWidth ? 'w-[90vw] ml-[calc(50%-45vw)]' : 'w-full'}`}>
<DebouncedInput
value={globalFilter ?? ''}
onChange={(value: any) => setGlobalFilter(String(value))}
className="p-2 text-sm shadow border border-block"
placeholder="Search all columns..."
/>
<table className="w-full mt-10">
<thead className="text-left border-b border-b-slate-300">
{table.getHeaderGroups().map((hg) => (
<tr key={hg.id}>
{hg.headers.map((h) => (
<th key={h.id} className="pr-2 pb-2">
<div
{...{
className: h.column.getCanSort()
? 'cursor-pointer select-none'
: '',
onClick: h.column.getToggleSortingHandler(),
}}
>
{flexRender(h.column.columnDef.header, h.getContext())}
{{
asc: (
<ArrowUpIcon className="inline-block ml-2 h-4 w-4" />
),
desc: (
<ArrowDownIcon className="inline-block ml-2 h-4 w-4" />
),
}[h.column.getIsSorted() as string] ?? (
<div className="inline-block ml-2 h-4 w-4" />
)}
</div>
</th>
))}
</tr>
))}
</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>
))}
</tr>
))}
</tbody>
</table>
<div className="flex gap-2 items-center justify-center mt-10">
<button
className={`w-6 h-6 ${
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
}`}
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<ChevronDoubleLeftIcon />
</button>
<button
className={`w-6 h-6 ${
!table.getCanPreviousPage() ? 'opacity-25' : 'opacity-100'
}`}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeftIcon />
</button>
<span className="flex items-center gap-1">
<div>Page</div>
<strong>
{table.getState().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</strong>
</span>
<button
className={`w-6 h-6 ${
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
}`}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<ChevronRightIcon />
</button>
<button
className={`w-6 h-6 ${
!table.getCanNextPage() ? 'opacity-25' : 'opacity-100'
}`}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<ChevronDoubleRightIcon />
</button>
</div>
</div>
);
};
const globalFilterFn: FilterFn<any> = (row, columnId, filterValue: string) => {
const search = filterValue.toLowerCase();
let value = row.getValue(columnId) as string;
if (typeof value === 'number') value = String(value);
return value?.toLowerCase().includes(search);
};

View File

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

View File

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

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -0,0 +1,4 @@
export * from "./components/Table";
export * from "./components/LineChart";
export * from "./components/Vega";
export * from "./components/VegaLite";

View File

@@ -0,0 +1,21 @@
export default function applyFullWidthDirective({
Component,
defaultWFull = true,
}) {
return (props) => {
const newProps = { ...props };
let newClassName = newProps.className || "";
if (newProps.fullWidth === true) {
newClassName += " w-[90vw] ml-[calc(50%-45vw)] max-w-none";
} else if (defaultWFull) {
// So that charts and tables will have the
// same width as the text content, but images
// can have its width set using the width prop
newClassName += " w-full";
}
newProps.className = newClassName;
return <Component {...newProps} />;
};
}

View File

@@ -0,0 +1,5 @@
export default async function loadData(url: string) {
const response = await fetch(url)
const data = await response.text()
return data
}

View File

@@ -0,0 +1,20 @@
import papa from "papaparse";
const parseCsv = (csv: string) => {
csv = csv.trim();
const rawdata = papa.parse(csv, { header: true });
let cols: any[] = [];
if(rawdata.meta.fields) {
cols = rawdata.meta.fields.map((r: string) => {
return { key: r, name: r };
});
}
return {
rows: rawdata.data as any,
fields: cols,
};
};
export default parseCsv;

1
packages/components/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -0,0 +1,9 @@
import { Meta } from '@storybook/blocks';
<Meta title="Components/Introduction" />
# Welcome to the PortalJS components guide
**Official Website:** [portaljs.org](https://portaljs.org)
**Docs:** [portaljs.org/docs](https://portaljs.org/docs)
**GitHub:** [github.com/datopian/portaljs](https://github.com/datopian/portaljs)

View File

@@ -0,0 +1,59 @@
import type { Meta, StoryObj } from '@storybook/react';
import { LineChart, LineChartProps } from '../src/components/LineChart';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
title: 'Components/LineChart',
component: LineChart,
tags: ['autodocs'],
argTypes: {
data: {
description:
'Data to be displayed.\n\n E.g.: [["1990", 1], ["1991", 2]] \n\nOR\n\n "https://url.to/data.csv"',
},
title: {
description: 'Title to display on the chart. Optional.',
},
xAxis: {
description:
'Name of the X axis on the data. Required when the "data" parameter is an URL.',
},
yAxis: {
description:
'Name of the Y axis on the data. Required when the "data" parameter is an URL.',
},
fullWidth: {
description:
'Whether the component should be rendered as full bleed or not',
},
},
};
export default meta;
type Story = StoryObj<LineChartProps>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const FromDataPoints: Story = {
name: 'Line chart from array of data points',
args: {
data: [
['1850', -0.41765878],
['1851', -0.2333498],
['1852', -0.22939907],
['1853', -0.27035445],
['1854', -0.29163003],
],
},
};
export const FromURL: Story = {
name: 'Line chart from URL',
args: {
title: 'Oil Price x Year',
data: 'https://raw.githubusercontent.com/datasets/oil-prices/main/data/wti-year.csv',
xAxis: 'Date',
yAxis: 'Price',
},
};

View File

@@ -0,0 +1,69 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Table, TableProps } from '../src/components/Table';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
title: 'Components/Table',
component: Table,
tags: ['autodocs'],
argTypes: {
data: {
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."
},
csv: {
description: "CSV data as string.",
},
url: {
description: "Fetch the data from a CSV file remotely."
}
},
};
export default meta;
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",
args: {
data: [
{ id: 1, lastName: 'Snow', firstName: 'Jon', age: 35 },
{ id: 2, lastName: 'Lannister', firstName: 'Cersei', age: 42 },
{ id: 3, lastName: 'Lannister', firstName: 'Jaime', age: 45 },
{ id: 4, lastName: 'Stark', firstName: 'Arya', age: 16 },
{ id: 7, lastName: 'Clifford', firstName: 'Ferrara', age: 44 },
{ id: 8, lastName: 'Frances', firstName: 'Rossini', age: 36 },
{ id: 9, lastName: 'Roxie', firstName: 'Harvey', age: 65 },
],
cols: [
{ key: 'id', name: 'ID' },
{ key: 'firstName', name: 'First name' },
{ key: 'lastName', name: 'Last name' },
{ key: 'age', name: 'Age' },
],
},
};
export const FromRawCSV: Story = {
name: "Table from raw CSV",
args: {
csv: `
Year,Temp Anomaly
1850,-0.418
2020,0.923
`
}
};
export const FromURL: Story = {
name: "Table from URL",
args: {
url: "https://raw.githubusercontent.com/datasets/finance-vix/main/data/vix-daily.csv"
}
};

View File

@@ -0,0 +1,50 @@
import type { Meta, StoryObj } from '@storybook/react';
import { Vega } from '../src/components/Vega';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
title: 'Components/Vega',
component: Vega,
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<any>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Primary: Story = {
name: 'Chart built with Vega',
args: {
data: {
table: [
{
y: -0.418,
x: 1850,
},
{
y: 0.923,
x: 2020,
},
],
},
spec: {
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
mark: 'bar',
data: {
name: 'table',
},
encoding: {
x: {
field: 'x',
type: 'ordinal',
},
y: {
field: 'y',
type: 'quantitative',
},
},
},
},
};

View File

@@ -0,0 +1,60 @@
import type { Meta, StoryObj } from '@storybook/react';
import { VegaLite } from '../src/components/VegaLite';
// More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction
const meta: Meta = {
title: 'Components/VegaLite',
component: VegaLite,
tags: ['autodocs'],
argTypes: {
data: {
description:
'Data to be used by Vega Lite. See the Vega Lite docs: https://vega.github.io/vega-lite/docs/data.html.',
},
spec: {
description:
'Spec to be used by Vega Lite. See the Vega Lite docs: https://vega.github.io/vega-lite/docs/spec.html.',
},
},
};
export default meta;
type Story = StoryObj<any>;
// More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args
export const Primary: Story = {
name: 'Chart built with Vega Lite',
args: {
data: {
table: [
{
y: -0.418,
x: 1850,
},
{
y: 0.923,
x: 2020,
},
],
},
spec: {
$schema: 'https://vega.github.io/schema/vega-lite/v4.json',
mark: 'bar',
data: {
name: 'table',
},
encoding: {
x: {
field: 'x',
type: 'ordinal',
},
y: {
field: 'y',
type: 'quantitative',
},
},
},
},
};

View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
extend: {},
},
plugins: [],
};

View File

@@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
// "strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }],
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -0,0 +1,30 @@
import react from '@vitejs/plugin-react';
import path from 'node:path';
import { defineConfig } from 'vite';
import dts from 'vite-plugin-dts';
export default defineConfig({
plugins: [
react(),
dts({
insertTypesEntry: true,
}),
],
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'components',
formats: ['es', 'umd'],
fileName: (format) => `components.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom', 'styled-components'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM'
},
},
},
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -116,6 +116,52 @@ From the browser, access http://localhost:3000. You should see the following:
<img src="/assets/docs/datasets-index-page.png" /> <img src="/assets/docs/datasets-index-page.png" />
### Creating a search page
Typing out every link in the index page will get cumbersome eventually, and as the portal grows, finding the datasets you are looking for on the index page will become harder and harder. Luckily we have a component for that. Change your `content/index.md` file to this:
```
# Welcome to my data portal!
List of available datasets:
<Catalog datasets={datasets} />
```
Before you refresh the page, however, you will need to run the following command:
```
npm run mddb
```
This example makes use of the [markdowndb](https://github.com/datopian/markdowndb) library. For now the only thing you need to know is that you should run the command above everytime you make some change to `/content`.
From the browser, access http://localhost:3000. You should see the following, you now have a searchable automatic list of your datasets:
![](https://i.imgur.com/9HfSPIx.png)
To make this catalog look even better, we can change the text that is being displayed for each dataset to a title. Let's do that by adding the "title" [frontmatter field](https://daily-dev-tips.com/posts/what-exactly-is-frontmatter/) to the first dataset in the list. Change `content/my-awesome-dataset/index.md` to the following:
```
---
title: 'My awesome dataset'
---
# My Awesome Dataset
Built with PortalJS
## Table
<Table url="data.csv" />
```
Rerun `npm run mddb` and, from the browser, access http://localhost:3000. You should see the title appearing instead of the folder name:
![](https://i.imgur.com/nvmSnJ5.png)
Any frontmatter attribute that you add will automatically get indexed and be usable in the search box.
## Deploying your PortalJS app ## Deploying your PortalJS app
Finally, let's learn how to deploy PortalJS apps to Vercel or Cloudflare Pages. Finally, let's learn how to deploy PortalJS apps to Vercel or Cloudflare Pages.