Compare commits
3 Commits
openspendi
...
loader-ope
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e27410f50 | ||
|
|
8cb3cd4ddb | ||
|
|
902e5e07a0 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,7 +16,6 @@ node_modules
|
|||||||
*.launch
|
*.launch
|
||||||
.settings/
|
.settings/
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
.obsidian
|
|
||||||
|
|
||||||
# IDE - VSCode
|
# IDE - VSCode
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
1
examples/ckan-example/.env
Normal file
1
examples/ckan-example/.env
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DMS=https://demo.dev.datopian.com
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,6 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.0.17",
|
"@heroicons/react": "^2.0.17",
|
||||||
"@portaljs/ckan": "^0.0.2",
|
|
||||||
"next": "13.3.1",
|
"next": "13.3.1",
|
||||||
"next-seo": "^6.0.0",
|
"next-seo": "^6.0.0",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.14",
|
||||||
@@ -21,14 +20,14 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"@types/node": "18.16.0",
|
|
||||||
"@types/react": "18.0.38",
|
|
||||||
"@types/react-dom": "18.0.11",
|
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"eslint": "8.39.0",
|
|
||||||
"eslint-config-next": "13.3.1",
|
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"tailwindcss": "^3.3.1",
|
"tailwindcss": "^3.3.1",
|
||||||
"typescript": "5.0.4"
|
"eslint": "8.39.0",
|
||||||
|
"eslint-config-next": "13.3.1",
|
||||||
|
"typescript": "5.0.4",
|
||||||
|
"@types/node": "18.16.0",
|
||||||
|
"@types/react": "18.0.38",
|
||||||
|
"@types/react-dom": "18.0.11"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,9 +11,8 @@ import {
|
|||||||
ServerIcon,
|
ServerIcon,
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from '@heroicons/react/20/solid';
|
} from '@heroicons/react/20/solid';
|
||||||
import { CKAN } from '@portaljs/ckan';
|
|
||||||
|
|
||||||
const backend_url = getConfig().publicRuntimeConfig.DMS;
|
const dms = getConfig().publicRuntimeConfig.DMS;
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -26,12 +25,14 @@ const formatter = new Intl.DateTimeFormat('en-US', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||||
const ckan = new CKAN(backend_url)
|
|
||||||
const { dataset } = context.query;
|
const { dataset } = context.query;
|
||||||
const _dataset = await ckan.getDatasetDetails(dataset as string)
|
const response = await fetch(
|
||||||
|
`${dms}/api/3/action/package_show?id=${dataset}`
|
||||||
|
);
|
||||||
|
const _dataset = await response.json();
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
dataset: _dataset,
|
dataset: _dataset.result,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import getConfig from 'next/config';
|
import getConfig from 'next/config';
|
||||||
import styles from './index.module.css';
|
import styles from './index.module.css';
|
||||||
import { CKAN } from '@portaljs/ckan';
|
|
||||||
|
|
||||||
const backend_url = getConfig().publicRuntimeConfig.DMS
|
const dms = getConfig().publicRuntimeConfig.DMS
|
||||||
|
|
||||||
const formatter = new Intl.DateTimeFormat('en-US', {
|
const formatter = new Intl.DateTimeFormat('en-US', {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
@@ -16,11 +15,12 @@ const formatter = new Intl.DateTimeFormat('en-US', {
|
|||||||
|
|
||||||
|
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
const ckan = new CKAN(backend_url)
|
const response = await fetch(`${dms}/api/3/action/package_search`)
|
||||||
const { datasets } = await ckan.packageSearch({ limit: 1000, offset: 0, groups:[], orgs: [], tags: []})
|
const datasets = await response.json()
|
||||||
const datasetsWithDetails = await Promise.all(datasets.map(async (dataset) => {
|
const datasetsWithDetails = await Promise.all(datasets.result.results.map(async (dataset) => {
|
||||||
const _dataset = await ckan.getDatasetDetails(dataset.name)
|
const response = await fetch(`${dms}/api/3/action/package_show?id=` + dataset.name)
|
||||||
return _dataset
|
const json = await response.json()
|
||||||
|
return json.result
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -79,7 +79,7 @@ export function Index({ datasets }) {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-gray-200">
|
<tbody className="divide-y divide-gray-200">
|
||||||
{datasets.map((dataset) => (
|
{datasets.map((dataset) => (
|
||||||
<tr key={dataset.name}>
|
<tr>
|
||||||
<td className="px-3 py-4 text-sm text-gray-500">
|
<td className="px-3 py-4 text-sm text-gray-500">
|
||||||
{dataset.title}
|
{dataset.title}
|
||||||
</td>
|
</td>
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "next/core-web-vitals"
|
|
||||||
}
|
|
||||||
35
examples/ckan/.gitignore
vendored
35
examples/ckan/.gitignore
vendored
@@ -1,35 +0,0 @@
|
|||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# next.js
|
|
||||||
/.next/
|
|
||||||
/out/
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# debug
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
# local env files
|
|
||||||
.env*.local
|
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
|
||||||
*.tsbuildinfo
|
|
||||||
next-env.d.ts
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
First, run the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
||||||
|
|
||||||
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
|
|
||||||
|
|
||||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
|
|
||||||
|
|
||||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
|
||||||
|
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
|
||||||
|
|
||||||
## Learn More
|
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { MDXRemote } from 'next-mdx-remote';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { Mermaid } from '@flowershow/core';
|
|
||||||
|
|
||||||
// Custom components/renderers to pass to MDX.
|
|
||||||
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
|
||||||
// to handle import statements. Instead, you must include components in scope
|
|
||||||
// here.
|
|
||||||
const components = {
|
|
||||||
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
|
|
||||||
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.Catalog)),
|
|
||||||
FlatUiTable: dynamic(() => import('@portaljs/components').then(mod => mod.FlatUiTable)),
|
|
||||||
mermaid: Mermaid,
|
|
||||||
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
|
|
||||||
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
|
|
||||||
LineChart: dynamic(() => import('@portaljs/components').then(mod => mod.LineChart)),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
export default function DRD({ source }: { source: any }) {
|
|
||||||
return <MDXRemote {...source} components={components} />;
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# Test
|
|
||||||
|
|
||||||
Test Data Rich Stories
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
|
||||||
const nextConfig = {
|
|
||||||
reactStrictMode: true,
|
|
||||||
publicRuntimeConfig: {
|
|
||||||
DMS: process.env.DMS
|
|
||||||
? process.env.DMS.replace(/\/?$/, '')
|
|
||||||
: 'https://demo.dev.datopian.com/',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = nextConfig;
|
|
||||||
14202
examples/ckan/package-lock.json
generated
14202
examples/ckan/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "ckan",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev",
|
|
||||||
"prebuild": "npm run mddb",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint",
|
|
||||||
"mddb": "mddb ./content"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@flowershow/core": "^0.4.13",
|
|
||||||
"@flowershow/markdowndb": "^0.1.5",
|
|
||||||
"@flowershow/remark-callouts": "^1.0.0",
|
|
||||||
"@flowershow/remark-embed": "^1.0.0",
|
|
||||||
"@githubocto/flat-ui": "^0.14.1",
|
|
||||||
"@heroicons/react": "^2.0.18",
|
|
||||||
"@portaljs/ckan": "^0.0.2",
|
|
||||||
"@portaljs/components": "0.1.6",
|
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"@types/node": "20.2.3",
|
|
||||||
"@types/react": "18.2.6",
|
|
||||||
"@types/react-dom": "18.2.4",
|
|
||||||
"autoprefixer": "10.4.14",
|
|
||||||
"eslint": "8.41.0",
|
|
||||||
"eslint-config-next": "13.4.3",
|
|
||||||
"isomorphic-unfetch": "^4.0.2",
|
|
||||||
"next": "13.4.3",
|
|
||||||
"next-mdx-remote": "^4.4.1",
|
|
||||||
"papaparse": "^5.4.1",
|
|
||||||
"postcss": "8.4.23",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-query": "^3.39.3",
|
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
|
||||||
"rehype-katex": "^6.0.3",
|
|
||||||
"rehype-prism-plus": "^1.5.1",
|
|
||||||
"rehype-slug": "^5.1.0",
|
|
||||||
"remark-math": "^5.1.1",
|
|
||||||
"remark-smartypants": "^2.0.0",
|
|
||||||
"remark-toc": "^8.0.1",
|
|
||||||
"tailwindcss": "3.3.2",
|
|
||||||
"typescript": "5.0.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
import Head from "next/head";
|
|
||||||
import { CKAN, Dataset } from "@portaljs/ckan";
|
|
||||||
import {
|
|
||||||
ChevronRightIcon,
|
|
||||||
HomeIcon,
|
|
||||||
PaperClipIcon,
|
|
||||||
} from "@heroicons/react/20/solid";
|
|
||||||
import Link from "next/link";
|
|
||||||
import getConfig from "next/config";
|
|
||||||
|
|
||||||
const backend_url = getConfig().publicRuntimeConfig.DMS
|
|
||||||
|
|
||||||
export const getServerSideProps = async (context: any) => {
|
|
||||||
try {
|
|
||||||
const datasetName = context.params?.dataset;
|
|
||||||
if (!datasetName) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const ckan = new CKAN(backend_url);
|
|
||||||
const dataset = await ckan.getDatasetDetails(datasetName as string);
|
|
||||||
if (!dataset) {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
props: { dataset },
|
|
||||||
};
|
|
||||||
} catch {
|
|
||||||
return {
|
|
||||||
notFound: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DatasetPage({
|
|
||||||
dataset,
|
|
||||||
}: {
|
|
||||||
dataset: Dataset;
|
|
||||||
}): JSX.Element {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Head>
|
|
||||||
<title>{`${dataset.title || dataset.name} - Dataset`}</title>
|
|
||||||
<meta name="description" content="Generated by create next app" />
|
|
||||||
<link rel="icon" href="/favicon.ico" />
|
|
||||||
</Head>
|
|
||||||
<main className="flex min-h-screen flex-col items-center justify-between p-24 bg-zinc-900">
|
|
||||||
<div className="bg-white p-8 my-4 rounded-lg">
|
|
||||||
<nav className="flex px-4 py-8" aria-label="Breadcrumb">
|
|
||||||
<ol role="list" className="flex items-center space-x-4">
|
|
||||||
<li>
|
|
||||||
<div>
|
|
||||||
<Link href="/" className="text-gray-400 hover:text-gray-500">
|
|
||||||
<HomeIcon
|
|
||||||
className="h-5 w-5 flex-shrink-0"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span className="sr-only">Home</span>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<ChevronRightIcon
|
|
||||||
className="h-5 w-5 flex-shrink-0 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className="ml-4 text-sm font-medium text-gray-500 hover:text-gray-700"
|
|
||||||
aria-current={"page"}
|
|
||||||
>
|
|
||||||
{dataset.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
{dataset && (
|
|
||||||
<div>
|
|
||||||
<div className="px-4 sm:px-0">
|
|
||||||
<h3 className="text-base font-semibold leading-7 text-gray-900">
|
|
||||||
{dataset.title || dataset.name}
|
|
||||||
</h3>
|
|
||||||
<p className="mt-1 max-w-2xl text-sm leading-6 text-gray-500">
|
|
||||||
Dataset details
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="mt-6 border-t border-gray-100">
|
|
||||||
<dl className="divide-y divide-gray-100">
|
|
||||||
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
||||||
<dt className="text-sm font-medium leading-6 text-gray-900">
|
|
||||||
Title
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
||||||
{dataset.title}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
{dataset.tags && dataset.tags.length > 0 && (
|
|
||||||
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
||||||
<dt className="text-sm font-medium leading-6 text-gray-900">
|
|
||||||
Tags
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
||||||
{dataset.tags.map((tag) => tag.display_name).join(", ")}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{dataset.tags && dataset.tags.length > 0 && (
|
|
||||||
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
||||||
<dt className="text-sm font-medium leading-6 text-gray-900">
|
|
||||||
URL
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
||||||
{dataset.url}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
||||||
{dataset.notes && (
|
|
||||||
<>
|
|
||||||
<dt className="text-sm font-medium leading-6 text-gray-900">
|
|
||||||
Description
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-1 text-sm leading-6 text-gray-700 sm:col-span-2 sm:mt-0">
|
|
||||||
{dataset.notes}
|
|
||||||
</dd>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
|
||||||
<dt className="text-sm font-medium leading-6 text-gray-900">
|
|
||||||
Files
|
|
||||||
</dt>
|
|
||||||
<dd className="mt-2 text-sm text-gray-900 sm:col-span-2 sm:mt-0">
|
|
||||||
<ul
|
|
||||||
role="list"
|
|
||||||
className="divide-y divide-gray-100 rounded-md border border-gray-200"
|
|
||||||
>
|
|
||||||
{dataset.resources.map((resource) => (
|
|
||||||
<li key={resource.id} className="flex items-center justify-between py-4 pl-4 pr-5 text-sm leading-6">
|
|
||||||
<div className="flex w-0 flex-1 items-center">
|
|
||||||
<PaperClipIcon
|
|
||||||
className="h-5 w-5 flex-shrink-0 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
<div className="ml-4 flex min-w-0 flex-1 gap-2">
|
|
||||||
<span className="truncate font-medium">
|
|
||||||
{resource.name || resource.id}
|
|
||||||
</span>
|
|
||||||
<span className="flex-shrink-0 text-gray-400">
|
|
||||||
{resource.size}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="ml-4 flex-shrink-0">
|
|
||||||
<a
|
|
||||||
href={resource.url}
|
|
||||||
className="font-medium hover:text-indigo-500 mr-4"
|
|
||||||
>
|
|
||||||
Download
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import '@/styles/globals.css'
|
|
||||||
import '@portaljs/ckan/styles.css'
|
|
||||||
import type { AppProps } from 'next/app'
|
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
|
||||||
return <Component {...pageProps} />
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Html, Head, Main, NextScript } from 'next/document'
|
|
||||||
|
|
||||||
export default function Document() {
|
|
||||||
return (
|
|
||||||
<Html lang="en">
|
|
||||||
<Head />
|
|
||||||
<body>
|
|
||||||
<Main />
|
|
||||||
<NextScript />
|
|
||||||
</body>
|
|
||||||
</Html>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import fetch from 'isomorphic-unfetch';
|
|
||||||
|
|
||||||
const Cors = async (req: any, res: any) => {
|
|
||||||
const { url } = req.query;
|
|
||||||
try {
|
|
||||||
const resProxy = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
Range: 'bytes=0-5132288',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await resProxy.text();
|
|
||||||
return res.status(200).send(data);
|
|
||||||
} catch (error: any) {
|
|
||||||
res.status(400).send(error.toString());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Cors;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import {
|
|
||||||
CKAN,
|
|
||||||
DatasetSearchForm,
|
|
||||||
ListOfDatasets,
|
|
||||||
PackageSearchOptions,
|
|
||||||
Organization,
|
|
||||||
Group,
|
|
||||||
} from '@portaljs/ckan';
|
|
||||||
import getConfig from 'next/config';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
const backend_url = getConfig().publicRuntimeConfig.DMS;
|
|
||||||
|
|
||||||
export async function getServerSideProps() {
|
|
||||||
const ckan = new CKAN(backend_url);
|
|
||||||
const groups = await ckan.getGroupsWithDetails();
|
|
||||||
const orgs = await ckan.getOrgsWithDetails();
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
groups,
|
|
||||||
orgs,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home({
|
|
||||||
orgs,
|
|
||||||
groups,
|
|
||||||
}: {
|
|
||||||
orgs: Organization[];
|
|
||||||
groups: Group[];
|
|
||||||
}) {
|
|
||||||
const ckan = new CKAN(backend_url);
|
|
||||||
const [options, setOptions] = useState<PackageSearchOptions>({
|
|
||||||
offset: 0,
|
|
||||||
limit: 5,
|
|
||||||
tags: [],
|
|
||||||
groups: [],
|
|
||||||
orgs: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="flex min-h-screen flex-col items-center p-24 bg-zinc-900">
|
|
||||||
<DatasetSearchForm
|
|
||||||
options={options}
|
|
||||||
setOptions={setOptions}
|
|
||||||
groups={groups}
|
|
||||||
orgs={orgs}
|
|
||||||
/>
|
|
||||||
<div className="bg-white p-8 my-4 rounded-lg">
|
|
||||||
<ListOfDatasets options={options} setOptions={setOptions} ckan={ckan} />{' '}
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { existsSync, promises as fs } from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import parse from '../../lib/markdown';
|
|
||||||
|
|
||||||
import DataRichDocument from '../../components/DataRichDocument';
|
|
||||||
import clientPromise from '../../lib/mddb';
|
|
||||||
import getConfig from 'next/config';
|
|
||||||
import { CKAN } from '@portaljs/ckan';
|
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
|
||||||
const contentDir = path.join(process.cwd(), '/content/');
|
|
||||||
const contentFolders = await fs.readdir(contentDir, 'utf8');
|
|
||||||
const paths = contentFolders.map((folder: string) => ({
|
|
||||||
params: { path: [folder.split('.')[0]] },
|
|
||||||
}));
|
|
||||||
return {
|
|
||||||
paths,
|
|
||||||
fallback: false,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const backend_url = getConfig().publicRuntimeConfig.DMS;
|
|
||||||
|
|
||||||
export const getStaticProps = async (context) => {
|
|
||||||
const mddb = await clientPromise;
|
|
||||||
const storyFile = await mddb.getFileByUrl(context.params.path);
|
|
||||||
const md = await fs.readFile(
|
|
||||||
`${process.cwd()}/${storyFile.file_path}`,
|
|
||||||
'utf8'
|
|
||||||
);
|
|
||||||
|
|
||||||
const ckan = new CKAN(backend_url);
|
|
||||||
const datasets = storyFile.metadata.datasets ? await Promise.all(
|
|
||||||
storyFile.metadata.datasets.map(
|
|
||||||
async (datasetName: string) => await ckan.getDatasetDetails(datasetName)
|
|
||||||
)
|
|
||||||
) : [];
|
|
||||||
const orgs = storyFile.metadata.orgs ? await Promise.all(
|
|
||||||
storyFile.metadata.orgs.map(
|
|
||||||
async (orgName: string) => await ckan.getOrgDetails(orgName)
|
|
||||||
)
|
|
||||||
) : [];
|
|
||||||
|
|
||||||
let { mdxSource, frontMatter } = await parse(md, '.mdx', { datasets, orgs });
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
mdxSource,
|
|
||||||
frontMatter: JSON.stringify(frontMatter),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function DatasetPage({ mdxSource, frontMatter }) {
|
|
||||||
frontMatter = JSON.parse(frontMatter);
|
|
||||||
return (
|
|
||||||
<main className="flex min-h-screen flex-col justify-between p-16 bg-zinc-900">
|
|
||||||
<div className="bg-white p-8 my-4 rounded-lg">
|
|
||||||
<div className="prose mx-auto py-8">
|
|
||||||
<header>
|
|
||||||
<div className="mb-6">
|
|
||||||
<>
|
|
||||||
<h1 className="mb-2">{frontMatter.title}</h1>
|
|
||||||
{frontMatter.author && (
|
|
||||||
<p className="my-0">
|
|
||||||
<span className="font-semibold">Author: </span>
|
|
||||||
<span className="my-0">{frontMatter.author}</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{frontMatter.description && (
|
|
||||||
<p className="my-0">
|
|
||||||
<span className="font-semibold">Description: </span>
|
|
||||||
<span className="description my-0">
|
|
||||||
{frontMatter.description}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{frontMatter.modified && (
|
|
||||||
<p className="my-0">
|
|
||||||
<span className="font-semibold">Modified: </span>
|
|
||||||
<span className="description my-0">
|
|
||||||
{new Date(frontMatter.modified).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{frontMatter.files && (
|
|
||||||
<section className="py-6">
|
|
||||||
<h2 className="mt-0">Data files</h2>
|
|
||||||
<table className="table-auto">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>File</th>
|
|
||||||
<th>Format</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{frontMatter.files.map((f) => {
|
|
||||||
const fileName = f.split('/').slice(-1);
|
|
||||||
return (
|
|
||||||
<tr key={`resources-list-${f}`}>
|
|
||||||
<td>
|
|
||||||
<a target="_blank" href={f}>
|
|
||||||
{fileName}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{fileName[0]
|
|
||||||
.split('.')
|
|
||||||
.slice(-1)[0]
|
|
||||||
.toUpperCase()}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<DataRichDocument source={mdxSource} />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>
|
|
||||||
|
Before Width: | Height: | Size: 629 B |
@@ -1,71 +0,0 @@
|
|||||||
@tailwind base;
|
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@import "@flowershow/remark-callouts/styles.css";
|
|
||||||
|
|
||||||
/* mathjax */
|
|
||||||
.math-inline > mjx-container > svg {
|
|
||||||
display: inline;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* smooth scrolling in modern browsers */
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tooltip fade-out clip */
|
|
||||||
.tooltip-body::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 3.6rem; /* multiple of $line-height used on the tooltip body (defined in tooltipBodyStyle) */
|
|
||||||
height: 1.2rem; /* ($top + $height)/$line-height is the number of lines we want to clip tooltip text at*/
|
|
||||||
width: 10rem;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
rgba(255, 255, 255, 0),
|
|
||||||
rgba(255, 255, 255, 1) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h2, h3, h4, h5, h6):not(.blogitem-title) {
|
|
||||||
margin-left: -2rem !important;
|
|
||||||
padding-left: 2rem !important;
|
|
||||||
scroll-margin-top: 4.5rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading-link {
|
|
||||||
padding: 1px;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
margin: auto 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #1e293b;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .heading-link {
|
|
||||||
/* border: 1px solid #ab2b65; */
|
|
||||||
/* background: none; */
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h2, h3, h4, h5, h6):not(.blogitem-title):hover .heading-link {
|
|
||||||
opacity: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading-link svg {
|
|
||||||
transform: scale(0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
.heading-link {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
backgroundImage: {
|
|
||||||
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
|
|
||||||
'gradient-conic':
|
|
||||||
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [require('@tailwindcss/typography')],
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "es5",
|
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"allowJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": false,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"module": "esnext",
|
|
||||||
"moduleResolution": "node",
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"isolatedModules": true,
|
|
||||||
"jsx": "preserve",
|
|
||||||
"incremental": true,
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
|
||||||
"exclude": ["node_modules"]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"extends": ["next", "next/core-web-vitals"],
|
"extends": [
|
||||||
|
"next",
|
||||||
|
"next/core-web-vitals"
|
||||||
|
],
|
||||||
"ignorePatterns": ["!**/*", ".next/**/*"],
|
"ignorePatterns": ["!**/*", ".next/**/*"],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
node_modules
|
|
||||||
**/.next/**
|
|
||||||
**/_next/**
|
|
||||||
**/dist/**
|
|
||||||
**/__tmp__/**
|
|
||||||
lerna.json
|
|
||||||
.github
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -99,3 +99,4 @@ And run the production build with:
|
|||||||
```
|
```
|
||||||
npm run start
|
npm run start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,20 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import HomeIcon from "../icons/HomeIcon";
|
import HomeIcon from "../icons/HomeIcon";
|
||||||
|
|
||||||
export default function Breadcrumbs({
|
export default function Breadcrumbs({ links }: { links: { title: string, href?: string, target?: string }[] }) {
|
||||||
links,
|
|
||||||
}: {
|
|
||||||
links: { title: string; href?: string; target?: string }[];
|
|
||||||
}) {
|
|
||||||
const current = links.at(-1);
|
const current = links.at(-1);
|
||||||
|
|
||||||
return (
|
return <div className="flex items-center uppercase font-black text-xs">
|
||||||
<div className="flex items-center uppercase font-black text-xs">
|
<Link className="flex items-center" href='/'><HomeIcon /></Link>
|
||||||
<Link className="flex items-center" href="/">
|
|
||||||
<HomeIcon />
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
{/* {links.length > 1 && links.slice(0, -1).map((link) => {
|
{/* {links.length > 1 && links.slice(0, -1).map((link) => {
|
||||||
return <>
|
return <>
|
||||||
<span className="mx-4">/</span>
|
<span className="mx-4">/</span>
|
||||||
<Link href={link.href}>{link.title}</Link>
|
<Link href={link.href}>{link.title}</Link>
|
||||||
</>
|
</>
|
||||||
})} */}
|
})} */}
|
||||||
|
|
||||||
<span className="mx-4">/</span>
|
<span className="mx-4">/</span>
|
||||||
<span>{current.title}</span>
|
<span>{current.title}</span>
|
||||||
</div>
|
</div >
|
||||||
);
|
}
|
||||||
}
|
|
||||||
@@ -1,13 +1,3 @@
|
|||||||
export default function ExternalLinkIcon({ className = "" }) {
|
export default function ExternalLinkIcon({ className = "" }) {
|
||||||
return (
|
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="currentColor"><path d="M 40 10 C 38.896 10 38 10.896 38 12 C 38 13.104 38.896 14 40 14 L 47.171875 14 L 30.585938 30.585938 C 29.804938 31.366938 29.804938 32.633063 30.585938 33.414062 C 30.976938 33.805063 31.488 34 32 34 C 32.512 34 33.023063 33.805062 33.414062 33.414062 L 50 16.828125 L 50 24 C 50 25.104 50.896 26 52 26 C 53.104 26 54 25.104 54 24 L 54 12 C 54 10.896 53.104 10 52 10 L 40 10 z M 18 12 C 14.691 12 12 14.691 12 18 L 12 46 C 12 49.309 14.691 52 18 52 L 46 52 C 49.309 52 52 49.309 52 46 L 52 34 C 52 32.896 51.104 32 50 32 C 48.896 32 48 32.896 48 34 L 48 46 C 48 47.103 47.103 48 46 48 L 18 48 C 16.897 48 16 47.103 16 46 L 16 18 C 16 16.897 16.897 16 18 16 L 30 16 C 31.104 16 32 15.104 32 14 C 32 12.896 31.104 12 30 12 L 18 12 z"/></svg></div>
|
||||||
<div className={`inline-block w-4 ${className}`}>
|
}
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 64 64"
|
|
||||||
fill="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M 40 10 C 38.896 10 38 10.896 38 12 C 38 13.104 38.896 14 40 14 L 47.171875 14 L 30.585938 30.585938 C 29.804938 31.366938 29.804938 32.633063 30.585938 33.414062 C 30.976938 33.805063 31.488 34 32 34 C 32.512 34 33.023063 33.805062 33.414062 33.414062 L 50 16.828125 L 50 24 C 50 25.104 50.896 26 52 26 C 53.104 26 54 25.104 54 24 L 54 12 C 54 10.896 53.104 10 52 10 L 40 10 z M 18 12 C 14.691 12 12 14.691 12 18 L 12 46 C 12 49.309 14.691 52 18 52 L 46 52 C 49.309 52 52 49.309 52 46 L 52 34 C 52 32.896 51.104 32 50 32 C 48.896 32 48 32.896 48 34 L 48 46 C 48 47.103 47.103 48 46 48 L 18 48 C 16.897 48 16 47.103 16 46 L 16 18 C 16 16.897 16.897 16 18 16 L 30 16 C 31.104 16 32 15.104 32 14 C 32 12.896 31.104 12 30 12 L 18 12 z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
export default function HomeIcon({ className = "" }) {
|
export default function HomeIcon({ className = "" }) {
|
||||||
return (
|
return <div className={`inline-block w-4 ${className}`}><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M 12 2 A 1 1 0 0 0 11.289062 2.296875 L 1.203125 11.097656 A 0.5 0.5 0 0 0 1 11.5 A 0.5 0.5 0 0 0 1.5 12 L 4 12 L 4 20 C 4 20.552 4.448 21 5 21 L 9 21 C 9.552 21 10 20.552 10 20 L 10 14 L 14 14 L 14 20 C 14 20.552 14.448 21 15 21 L 19 21 C 19.552 21 20 20.552 20 20 L 20 12 L 22.5 12 A 0.5 0.5 0 0 0 23 11.5 A 0.5 0.5 0 0 0 22.796875 11.097656 L 12.716797 2.3027344 A 1 1 0 0 0 12.710938 2.296875 A 1 1 0 0 0 12 2 z"/></svg></div>
|
||||||
<div className={`inline-block w-4 ${className}`}>
|
}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
{" "}
|
|
||||||
<path d="M 12 2 A 1 1 0 0 0 11.289062 2.296875 L 1.203125 11.097656 A 0.5 0.5 0 0 0 1 11.5 A 0.5 0.5 0 0 0 1.5 12 L 4 12 L 4 20 C 4 20.552 4.448 21 5 21 L 9 21 C 9.552 21 10 20.552 10 20 L 10 14 L 14 14 L 14 20 C 14 20.552 14.448 21 15 21 L 19 21 C 19.552 21 20 20.552 20 20 L 20 12 L 22.5 12 A 0.5 0.5 0 0 0 23 11.5 A 0.5 0.5 0 0 0 22.796875 11.097656 L 12.716797 2.3027344 A 1 1 0 0 0 12.710938 2.296875 A 1 1 0 0 0 12 2 z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -15,18 +15,14 @@
|
|||||||
],
|
],
|
||||||
"readme": "README.md"
|
"readme": "README.md"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"owner": "luccasmmg",
|
|
||||||
"branch": "main",
|
|
||||||
"repo": "test-data-repo-1",
|
|
||||||
"files": ["data_1.csv", "data_2.csv"],
|
|
||||||
"readme": "README.md"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"owner": "datasets",
|
"owner": "datasets",
|
||||||
"branch": "main",
|
"branch": "main",
|
||||||
"repo": "investor-flow-of-funds-us",
|
"repo": "investor-flow-of-funds-us",
|
||||||
"files": ["data/monthly.csv", "data/weekly.csv"],
|
"files": [
|
||||||
|
"data/monthly.csv",
|
||||||
|
"data/weekly.csv"
|
||||||
|
],
|
||||||
"readme": "README.md"
|
"readme": "README.md"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -42,10 +38,7 @@
|
|||||||
"owner": "fivethirtyeight",
|
"owner": "fivethirtyeight",
|
||||||
"repo": "data",
|
"repo": "data",
|
||||||
"branch": "master",
|
"branch": "master",
|
||||||
"files": [
|
"files": ["nba-raptor/historical_RAPTOR_by_player.csv", "nba-raptor/historical_RAPTOR_by_team.csv"],
|
||||||
"nba-raptor/historical_RAPTOR_by_player.csv",
|
|
||||||
"nba-raptor/historical_RAPTOR_by_team.csv"
|
|
||||||
],
|
|
||||||
"readme": "nba-raptor/README.md"
|
"readme": "nba-raptor/README.md"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
2
examples/github-backed-catalog/index.d.ts
vendored
2
examples/github-backed-catalog/index.d.ts
vendored
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
declare module "*.svg" {
|
declare module '*.svg' {
|
||||||
const content: any;
|
const content: any;
|
||||||
export const ReactComponent: any;
|
export const ReactComponent: any;
|
||||||
export default content;
|
export default content;
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import matter from "gray-matter";
|
|
||||||
import mdxmermaid from "mdx-mermaid";
|
|
||||||
import { h } from "hastscript";
|
|
||||||
import remarkCallouts from "@flowershow/remark-callouts";
|
|
||||||
import remarkEmbed from "@flowershow/remark-embed";
|
|
||||||
import remarkGfm from "remark-gfm";
|
|
||||||
import remarkMath from "remark-math";
|
|
||||||
import remarkSmartypants from "remark-smartypants";
|
|
||||||
import remarkToc from "remark-toc";
|
|
||||||
import remarkWikiLink from "@flowershow/remark-wiki-link";
|
|
||||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
||||||
import rehypeKatex from "rehype-katex";
|
|
||||||
import rehypeSlug from "rehype-slug";
|
|
||||||
import rehypePrismPlus from "rehype-prism-plus";
|
|
||||||
|
|
||||||
import { serialize } from "next-mdx-remote/serialize";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a markdown or MDX file to an MDX source form + front matter data
|
|
||||||
*
|
|
||||||
* @source: the contents of a markdown or mdx file
|
|
||||||
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
|
||||||
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
|
||||||
*/
|
|
||||||
const parse = async function (source, format, scope) {
|
|
||||||
const { content, data, excerpt } = matter(source, {
|
|
||||||
excerpt: (file, options) => {
|
|
||||||
// Generate an excerpt for the file
|
|
||||||
file.excerpt = file.content.split("\n\n")[0];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mdxSource = await serialize(
|
|
||||||
{ value: content, path: format },
|
|
||||||
{
|
|
||||||
// Optionally pass remark/rehype plugins
|
|
||||||
mdxOptions: {
|
|
||||||
remarkPlugins: [
|
|
||||||
remarkEmbed,
|
|
||||||
remarkGfm,
|
|
||||||
[remarkSmartypants, { quotes: false, dashes: "oldschool" }],
|
|
||||||
remarkMath,
|
|
||||||
remarkCallouts,
|
|
||||||
remarkWikiLink,
|
|
||||||
[
|
|
||||||
remarkToc,
|
|
||||||
{
|
|
||||||
heading: "Table of contents",
|
|
||||||
tight: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[mdxmermaid, {}],
|
|
||||||
],
|
|
||||||
rehypePlugins: [
|
|
||||||
rehypeSlug,
|
|
||||||
[
|
|
||||||
rehypeAutolinkHeadings,
|
|
||||||
{
|
|
||||||
properties: { className: 'heading-link' },
|
|
||||||
test(element) {
|
|
||||||
return (
|
|
||||||
["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) &&
|
|
||||||
element.properties?.id !== "table-of-contents" &&
|
|
||||||
element.properties?.className !== "blockquote-heading"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
content() {
|
|
||||||
return [
|
|
||||||
h(
|
|
||||||
"svg",
|
|
||||||
{
|
|
||||||
xmlns: "http:www.w3.org/2000/svg",
|
|
||||||
fill: "#ab2b65",
|
|
||||||
viewBox: "0 0 20 20",
|
|
||||||
className: "w-5 h-5",
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h("path", {
|
|
||||||
fillRule: "evenodd",
|
|
||||||
clipRule: "evenodd",
|
|
||||||
d: "M9.493 2.853a.75.75 0 00-1.486-.205L7.545 6H4.198a.75.75 0 000 1.5h3.14l-.69 5H3.302a.75.75 0 000 1.5h3.14l-.435 3.148a.75.75 0 001.486.205L7.955 14h2.986l-.434 3.148a.75.75 0 001.486.205L12.456 14h3.346a.75.75 0 000-1.5h-3.14l.69-5h3.346a.75.75 0 000-1.5h-3.14l.435-3.147a.75.75 0 00-1.486-.205L12.045 6H9.059l.434-3.147zM8.852 7.5l-.69 5h2.986l.69-5H8.852z",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[rehypeKatex, { output: "mathml" }],
|
|
||||||
[rehypePrismPlus, { ignoreMissing: true }],
|
|
||||||
],
|
|
||||||
format,
|
|
||||||
},
|
|
||||||
scope,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
mdxSource: mdxSource,
|
|
||||||
frontMatter: data,
|
|
||||||
excerpt,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parse;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Octokit } from "octokit";
|
import { Octokit } from 'octokit';
|
||||||
|
|
||||||
export interface GithubProject {
|
export interface GithubProject {
|
||||||
owner: string;
|
owner: string;
|
||||||
@@ -26,42 +26,15 @@ export async function getProjectReadme(
|
|||||||
ref: branch,
|
ref: branch,
|
||||||
});
|
});
|
||||||
const data = response.data as { content?: string };
|
const data = response.data as { content?: string };
|
||||||
const fileContent = data.content ? data.content : "";
|
const fileContent = data.content ? data.content : '';
|
||||||
if (fileContent === "") {
|
if (fileContent === '') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const decodedContent = Buffer.from(fileContent, "base64").toString();
|
const decodedContent = Buffer.from(fileContent, 'base64').toString();
|
||||||
return decodedContent;
|
return decodedContent;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
console.log(error);
|
||||||
"Couldn't get project readme please make sure that you are pointing to a valid repo and that the repo in question contains a README.md"
|
return null;
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProjectDatapackage(
|
|
||||||
owner: string,
|
|
||||||
repo: string,
|
|
||||||
branch: string,
|
|
||||||
github_pat?: string
|
|
||||||
) {
|
|
||||||
const octokit = new Octokit({ auth: github_pat });
|
|
||||||
try {
|
|
||||||
const response = await octokit.rest.repos.getContent({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
path: "datapackage.json",
|
|
||||||
ref: branch,
|
|
||||||
});
|
|
||||||
const data = response.data as { content?: string };
|
|
||||||
const fileContent = data.content ? data.content : "";
|
|
||||||
if (fileContent === "") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const decodedContent = Buffer.from(fileContent, "base64").toString();
|
|
||||||
return JSON.parse(decodedContent);
|
|
||||||
} catch (error) {
|
|
||||||
return null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,13 +50,13 @@ export async function getLastUpdated(
|
|||||||
const response = await octokit.rest.repos.listCommits({
|
const response = await octokit.rest.repos.listCommits({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
|
path: readme,
|
||||||
ref: branch,
|
ref: branch,
|
||||||
});
|
});
|
||||||
return response.data[0].commit.committer.date;
|
return response.data[0].commit.committer.date;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
console.log(error);
|
||||||
"Couldn't get project list of commits please make sure that you are pointing to a valid repo"
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export async function getProjectMetadata(
|
export async function getProjectMetadata(
|
||||||
@@ -99,9 +72,8 @@ export async function getProjectMetadata(
|
|||||||
});
|
});
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(
|
console.log(error);
|
||||||
"Couldn't get project metadata please make sure that you are pointing to a valid repo"
|
return null;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,32 +94,13 @@ export async function getRepoContents(
|
|||||||
ref: branch,
|
ref: branch,
|
||||||
path: path,
|
path: path,
|
||||||
});
|
});
|
||||||
const data = response.data as {
|
const data = response.data as { download_url?: string, name: string, size: number };
|
||||||
download_url?: string;
|
contents.push({ download_url: data.download_url, name: data.name, size: data.size});
|
||||||
name: string;
|
|
||||||
size: number;
|
|
||||||
};
|
|
||||||
contents.push({
|
|
||||||
download_url: data.download_url,
|
|
||||||
name: data.name,
|
|
||||||
size: data.size,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return contents;
|
return contents;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
console.log(error);
|
||||||
error.message ===
|
return null;
|
||||||
'This endpoint can only return blobs smaller than 100 MB in size. The requested blob is too large to fetch via the API, but you can always clone the repository via Git to obtain it.: {"resource":"Blob","field":"data","code":"too_large"}'
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`The requested files ${files.join(
|
|
||||||
", "
|
|
||||||
)} are too big making it impossible to fetch via Github API`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
"Couldn't get project contents please make sure that you are pointing to a valid repo"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,20 +120,22 @@ export async function getProject(project: GithubProject, github_pat?: string) {
|
|||||||
project.readme,
|
project.readme,
|
||||||
github_pat
|
github_pat
|
||||||
);
|
);
|
||||||
let projectData = [];
|
if (!projectReadme) {
|
||||||
if (project.files) {
|
return null;
|
||||||
projectData = await getRepoContents(
|
|
||||||
project.owner,
|
|
||||||
project.repo,
|
|
||||||
project.branch,
|
|
||||||
project.files,
|
|
||||||
github_pat
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const projectBase =
|
const projectData = await getRepoContents(
|
||||||
project.readme && project.readme.split("/").length > 1
|
project.owner,
|
||||||
? project.readme.split("/").slice(0, -1).join("/")
|
project.repo,
|
||||||
: "/";
|
project.branch,
|
||||||
|
project.files,
|
||||||
|
github_pat
|
||||||
|
);
|
||||||
|
if (!projectData) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const projectBase = project.readme.split('/').length > 1
|
||||||
|
? project.readme.split('/').slice(0, -1).join('/')
|
||||||
|
: '/'
|
||||||
const last_updated = await getLastUpdated(
|
const last_updated = await getLastUpdated(
|
||||||
project.owner,
|
project.owner,
|
||||||
project.repo,
|
project.repo,
|
||||||
@@ -188,20 +143,5 @@ export async function getProject(project: GithubProject, github_pat?: string) {
|
|||||||
projectBase,
|
projectBase,
|
||||||
github_pat
|
github_pat
|
||||||
);
|
);
|
||||||
|
return { ...projectMetadata, files: projectData, readmeContent: projectReadme, last_updated, base_path: projectBase };
|
||||||
const projectDatapackage = await getProjectDatapackage(
|
|
||||||
project.owner,
|
|
||||||
project.repo,
|
|
||||||
project.branch,
|
|
||||||
github_pat
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...projectMetadata,
|
|
||||||
files: projectData,
|
|
||||||
readmeContent: projectReadme,
|
|
||||||
last_updated,
|
|
||||||
base_path: projectBase,
|
|
||||||
datapackage: projectDatapackage
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ const nextConfig = {
|
|||||||
return {
|
return {
|
||||||
beforeFiles: [
|
beforeFiles: [
|
||||||
{
|
{
|
||||||
source: "/@:org/:project*",
|
source: '/@:org/:project*',
|
||||||
destination: "/@org/:org/:project*",
|
destination: '/@org/:org/:project*',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
7396
examples/github-backed-catalog/package-lock.json
generated
7396
examples/github-backed-catalog/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,43 +6,28 @@
|
|||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint"
|
||||||
"prettier": "prettier --write ."
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@flowershow/core": "^0.4.13",
|
|
||||||
"@flowershow/markdowndb": "^0.1.5",
|
|
||||||
"@flowershow/remark-callouts": "^1.0.0",
|
|
||||||
"@flowershow/remark-embed": "^1.0.0",
|
|
||||||
"@portaljs/components": "^0.1.6",
|
|
||||||
"@types/node": "18.16.0",
|
"@types/node": "18.16.0",
|
||||||
"@types/react": "18.0.38",
|
"@types/react": "18.0.38",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"eslint": "8.39.0",
|
"eslint": "8.39.0",
|
||||||
"eslint-config-next": "13.3.1",
|
"eslint-config-next": "13.3.1",
|
||||||
"next": "13.4.3",
|
"next": "13.3.1",
|
||||||
"next-mdx-remote": "^4.4.1",
|
|
||||||
"next-seo": "^6.0.0",
|
"next-seo": "^6.0.0",
|
||||||
"octokit": "^2.0.14",
|
"octokit": "^2.0.14",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^8.0.7",
|
||||||
"react-timeago": "^7.1.0",
|
"react-timeago": "^7.1.0",
|
||||||
"rehype-autolink-headings": "^6.1.1",
|
|
||||||
"rehype-katex": "^6.0.3",
|
|
||||||
"rehype-prism-plus": "^1.5.1",
|
|
||||||
"rehype-slug": "^5.1.0",
|
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
|
||||||
"remark-smartypants": "^2.0.0",
|
|
||||||
"remark-toc": "^8.0.1",
|
|
||||||
"typescript": "5.0.4"
|
"typescript": "5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.23",
|
"postcss": "^8.4.23",
|
||||||
"prettier": "2.8.8",
|
|
||||||
"tailwindcss": "^3.3.1"
|
"tailwindcss": "^3.3.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,33 +1,22 @@
|
|||||||
import { NextSeo } from "next-seo";
|
import { NextSeo } from 'next-seo';
|
||||||
import { promises as fs } from "fs";
|
import { promises as fs } from 'fs';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import getConfig from "next/config";
|
import getConfig from 'next/config';
|
||||||
import { getProject, GithubProject } from "../../../lib/octokit";
|
import { getProject, GithubProject } from '../../../lib/octokit';
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from 'remark-gfm';
|
||||||
import Breadcrumbs from "../../../components/_shared/Breadcrumbs";
|
import Breadcrumbs from '../../../components/_shared/Breadcrumbs';
|
||||||
import parse from '../../../lib/markdown';
|
|
||||||
import DataRichDocument from '../../../components/DataRichDocument'
|
|
||||||
|
|
||||||
export default function ProjectPage({ project }) {
|
export default function ProjectPage({ project }) {
|
||||||
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`;
|
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<NextSeo
|
<NextSeo title={`${repoId}${project.base_path !== '/' ? '/' + project.base_path : ''} - GitHub Datasets`} />
|
||||||
title={`${repoId}${
|
|
||||||
project.base_path !== "/" ? "/" + project.base_path : ""
|
|
||||||
} - GitHub Datasets`}
|
|
||||||
/>
|
|
||||||
<main className="prose mx-auto my-8">
|
<main className="prose mx-auto my-8">
|
||||||
<Breadcrumbs links={[{ title: repoId, href: "" }]} />
|
<Breadcrumbs links={[{ title: repoId, href: "" }]} />
|
||||||
<h1 className="mb-0 mt-16">{project.repo_config.name || repoId}</h1>
|
<h1 className="mb-0 mt-16">{project.repo_config.name || repoId}</h1>
|
||||||
<p className="mb-8">
|
<p className='mb-8'><span className='font-semibold'>Repository:</span> <a target="_blank" href={project.html_url}>{project.html_url}</a></p>
|
||||||
<span className="font-semibold">Repository:</span>{" "}
|
|
||||||
<a target="_blank" href={project.html_url}>
|
|
||||||
{project.html_url}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<h2 className="mb-0 mt-10">Files</h2>
|
<h2 className="mb-0 mt-10">Files</h2>
|
||||||
<div className="inline-block min-w-full py-2 align-middle">
|
<div className="inline-block min-w-full py-2 align-middle">
|
||||||
@@ -65,8 +54,10 @@ export default function ProjectPage({ project }) {
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<h2 className="uppercase font-black">Readme</h2>
|
<h2 className='uppercase font-black'>Readme</h2>
|
||||||
<DataRichDocument source={project.mdxSource} />
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||||
|
{project.readmeContent}
|
||||||
|
</ReactMarkdown>
|
||||||
</main>
|
</main>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -74,14 +65,17 @@ export default function ProjectPage({ project }) {
|
|||||||
|
|
||||||
// Generates `/posts/1` and `/posts/2`
|
// Generates `/posts/1` and `/posts/2`
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const jsonDirectory = path.join(process.cwd(), "datasets.json");
|
const jsonDirectory = path.join(
|
||||||
const repos = await fs.readFile(jsonDirectory, "utf8");
|
process.cwd(),
|
||||||
|
'datasets.json'
|
||||||
|
);
|
||||||
|
const repos = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paths: JSON.parse(repos).map((repo) => {
|
paths: JSON.parse(repos).map((repo) => {
|
||||||
const projectPath =
|
const projectPath =
|
||||||
repo.readme && repo.readme.split("/").length > 1
|
repo.readme.split('/').length > 1
|
||||||
? repo.readme.split("/").slice(0, -1)
|
? repo.readme.split('/').slice(0, -1)
|
||||||
: null;
|
: null;
|
||||||
let path = [repo.repo];
|
let path = [repo.repo];
|
||||||
if (projectPath) {
|
if (projectPath) {
|
||||||
@@ -98,13 +92,16 @@ export async function getStaticPaths() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getStaticProps({ params }) {
|
export async function getStaticProps({ params }) {
|
||||||
const jsonDirectory = path.join(process.cwd(), "datasets.json");
|
const jsonDirectory = path.join(
|
||||||
const reposFile = await fs.readFile(jsonDirectory, "utf8");
|
process.cwd(),
|
||||||
|
'datasets.json'
|
||||||
|
);
|
||||||
|
const reposFile = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
const repos: GithubProject[] = JSON.parse(reposFile);
|
const repos: GithubProject[] = JSON.parse(reposFile);
|
||||||
const repo = repos.find((_repo) => {
|
const repo = repos.find((_repo) => {
|
||||||
const projectPath =
|
const projectPath =
|
||||||
_repo.readme && _repo.readme.split("/").length > 1
|
_repo.readme.split('/').length > 1
|
||||||
? _repo.readme.split("/").slice(0, -1)
|
? _repo.readme.split('/').slice(0, -1)
|
||||||
: null;
|
: null;
|
||||||
let path = [_repo.repo];
|
let path = [_repo.repo];
|
||||||
if (projectPath) {
|
if (projectPath) {
|
||||||
@@ -119,10 +116,9 @@ export async function getStaticProps({ params }) {
|
|||||||
});
|
});
|
||||||
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||||
const project = await getProject(repo, github_pat);
|
const project = await getProject(repo, github_pat);
|
||||||
let { mdxSource, frontMatter } = await parse(project.readmeContent, '.mdx', { project });
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
project: { ...project, repo_config: repo, mdxSource },
|
project: { ...project, repo_config: repo },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AppProps } from "next/app";
|
import { AppProps } from 'next/app';
|
||||||
import Head from "next/head";
|
import Head from 'next/head';
|
||||||
import "./styles.css";
|
import './styles.css';
|
||||||
|
|
||||||
function CustomApp({ Component, pageProps }: AppProps) {
|
function CustomApp({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
import { promises as fs } from "fs";
|
import { promises as fs } from 'fs';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { getProject } from "../lib/octokit";
|
import { getProject } from '../lib/octokit';
|
||||||
import getConfig from "next/config";
|
import getConfig from 'next/config';
|
||||||
import ExternalLinkIcon from "../components/icons/ExternalLinkIcon";
|
import ExternalLinkIcon from '../components/icons/ExternalLinkIcon';
|
||||||
import TimeAgo from "react-timeago";
|
import TimeAgo from 'react-timeago';
|
||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
import { NextSeo } from "next-seo";
|
|
||||||
|
|
||||||
export async function getStaticProps() {
|
export async function getStaticProps() {
|
||||||
const jsonDirectory = path.join(process.cwd(), "/datasets.json");
|
const jsonDirectory = path.join(
|
||||||
const repos = await fs.readFile(jsonDirectory, "utf8");
|
process.cwd(),
|
||||||
|
'/datasets.json'
|
||||||
|
);
|
||||||
|
const repos = await fs.readFile(jsonDirectory, 'utf8');
|
||||||
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||||
|
|
||||||
const projects = await Promise.all(
|
const projects = await Promise.all(
|
||||||
JSON.parse(repos).map(async (repo) => {
|
(JSON.parse(repos)).map(async (repo) => {
|
||||||
const project = await getProject(repo, github_pat);
|
const project = await getProject(repo, github_pat);
|
||||||
return { ...project, repo_config: repo };
|
return { ...project, repo_config: repo };
|
||||||
})
|
})
|
||||||
@@ -27,112 +29,88 @@ export async function getStaticProps() {
|
|||||||
|
|
||||||
export function Datasets({ projects }) {
|
export function Datasets({ projects }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="bg-white min-h-screen">
|
||||||
<NextSeo title="GitHub Datasets" />
|
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
||||||
<div className="bg-white min-h-screen">
|
<div className='text-center'>
|
||||||
<div className="mx-auto max-w-7xl px-6 py-16 sm:py-24 lg:px-8">
|
<h2 className="text-3xl font-bold leading-10 tracking-tight">
|
||||||
<div className="text-center">
|
GitHub Datasets
|
||||||
<h2 className="text-3xl font-bold leading-10 tracking-tight">
|
</h2>
|
||||||
GitHub Datasets
|
<p className="mt-3 mx-auto max-w-2xl text-base leading-7 text-gray-500">
|
||||||
</h2>
|
Data catalog with datasets hosted on GitHub by <Link target="_blank" className='underline' href="https://portaljs.org/">🌀 PortalJS</Link>
|
||||||
<p className="mt-3 mx-auto max-w-2xl text-base leading-7 text-gray-500">
|
</p>
|
||||||
Data catalog with datasets hosted on GitHub by{" "}
|
</div>
|
||||||
<Link
|
<div className="mt-20">
|
||||||
target="_blank"
|
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
className="underline"
|
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
href="https://portaljs.org/"
|
<table className="min-w-full divide-y divide-gray-300">
|
||||||
>
|
<thead>
|
||||||
🌀 PortalJS
|
<tr>
|
||||||
</Link>
|
<th
|
||||||
</p>
|
scope="col"
|
||||||
</div>
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
<div className="mt-20">
|
>
|
||||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
Name
|
||||||
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
</th>
|
||||||
<table className="min-w-full divide-y divide-gray-300">
|
<th
|
||||||
<thead>
|
scope="col"
|
||||||
<tr>
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
<th
|
>
|
||||||
scope="col"
|
Repository
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
</th>
|
||||||
>
|
<th
|
||||||
Name
|
scope="col"
|
||||||
</th>
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
<th
|
>
|
||||||
scope="col"
|
Description
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
</th>
|
||||||
>
|
<th
|
||||||
Repository
|
scope="col"
|
||||||
</th>
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
||||||
<th
|
>
|
||||||
scope="col"
|
Last updated
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
</th>
|
||||||
>
|
<th
|
||||||
Description
|
scope="col"
|
||||||
</th>
|
className="relative py-3.5 pl-3 pr-4 sm:pr-0"
|
||||||
<th
|
></th>
|
||||||
scope="col"
|
</tr>
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
|
</thead>
|
||||||
>
|
<tbody className="divide-y divide-gray-200">
|
||||||
Last updated
|
{projects.map((project) => (
|
||||||
</th>
|
<tr key={project.id}>
|
||||||
<th
|
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
|
||||||
scope="col"
|
{project.repo_config.name
|
||||||
className="relative py-3.5 pl-3 pr-4 sm:pr-0"
|
? project.repo_config.name
|
||||||
></th>
|
: project.full_name + (project.base_path === '/' ? '' : '/' + project.base_path)}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-6 text-sm group text-gray-500 hover:text-gray-900 transition-all duration-250">
|
||||||
|
<a href={project.html_url} target="_blank" className='flex items-center'>@{project.full_name} <ExternalLinkIcon className='ml-1' /></a>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-4 text-sm text-gray-500">
|
||||||
|
{project.repo_config.description
|
||||||
|
? project.repo_config.description
|
||||||
|
: project.description}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
|
||||||
|
<TimeAgo date={new Date(project.last_updated)} />
|
||||||
|
</td>
|
||||||
|
<td className="relative whitespace-nowrap py-6 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
||||||
|
<a
|
||||||
|
href={`/@${project.repo_config.owner}/${project.repo_config.repo}/${project.base_path === '/' ? '' : project.base_path}`}
|
||||||
|
className='border border-gray-900 text-gray-900 px-4 py-2 transition-all hover:bg-gray-900 hover:text-white'
|
||||||
|
>
|
||||||
|
info
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
))}
|
||||||
<tbody className="divide-y divide-gray-200">
|
</tbody>
|
||||||
{projects.map((project) => (
|
</table>
|
||||||
<tr key={project.id}>
|
|
||||||
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
|
|
||||||
{project.repo_config.name
|
|
||||||
? project.repo_config.name
|
|
||||||
: project.full_name +
|
|
||||||
(project.base_path === "/"
|
|
||||||
? ""
|
|
||||||
: "/" + project.base_path)}
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-3 py-6 text-sm group text-gray-500 hover:text-gray-900 transition-all duration-250">
|
|
||||||
<a
|
|
||||||
href={project.html_url}
|
|
||||||
target="_blank"
|
|
||||||
className="flex items-center"
|
|
||||||
>
|
|
||||||
@{project.full_name}{" "}
|
|
||||||
<ExternalLinkIcon className="ml-1" />
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
<td className="px-3 py-4 text-sm text-gray-500">
|
|
||||||
{project.repo_config.description
|
|
||||||
? project.repo_config.description
|
|
||||||
: project.description}
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-3 py-6 text-sm text-gray-500">
|
|
||||||
<TimeAgo date={new Date(project.last_updated)} />
|
|
||||||
</td>
|
|
||||||
<td className="relative whitespace-nowrap py-6 pl-3 pr-4 text-right text-sm font-medium sm:pr-0">
|
|
||||||
<a
|
|
||||||
href={`/@${project.repo_config.owner}/${
|
|
||||||
project.repo_config.repo
|
|
||||||
}/${
|
|
||||||
project.base_path === "/" ? "" : project.base_path
|
|
||||||
}`}
|
|
||||||
className="border border-gray-900 text-gray-900 px-4 py-2 transition-all hover:bg-gray-900 hover:text-white"
|
|
||||||
>
|
|
||||||
info
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -78,72 +78,3 @@ pre {
|
|||||||
color: rgba(55, 65, 81, 1);
|
color: rgba(55, 65, 81, 1);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@import "@flowershow/remark-callouts/styles.css";
|
|
||||||
|
|
||||||
/* mathjax */
|
|
||||||
.math-inline > mjx-container > svg {
|
|
||||||
display: inline;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* smooth scrolling in modern browsers */
|
|
||||||
html {
|
|
||||||
scroll-behavior: smooth !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tooltip fade-out clip */
|
|
||||||
.tooltip-body::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
top: 3.6rem; /* multiple of $line-height used on the tooltip body (defined in tooltipBodyStyle) */
|
|
||||||
height: 1.2rem; /* ($top + $height)/$line-height is the number of lines we want to clip tooltip text at*/
|
|
||||||
width: 10rem;
|
|
||||||
background: linear-gradient(
|
|
||||||
to right,
|
|
||||||
rgba(255, 255, 255, 0),
|
|
||||||
rgba(255, 255, 255, 1) 100%
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h2, h3, h4, h5, h6):not(.blogitem-title) {
|
|
||||||
margin-left: -2rem !important;
|
|
||||||
padding-left: 2rem !important;
|
|
||||||
scroll-margin-top: 4.5rem;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading-link {
|
|
||||||
padding: 1px;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
margin: auto 0;
|
|
||||||
border-radius: 5px;
|
|
||||||
background: #1e293b;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light .heading-link {
|
|
||||||
/* border: 1px solid #ab2b65; */
|
|
||||||
/* background: none; */
|
|
||||||
background: #e2e8f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
:is(h2, h3, h4, h5, h6):not(.blogitem-title):hover .heading-link {
|
|
||||||
opacity: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.heading-link svg {
|
|
||||||
transform: scale(0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 640px) {
|
|
||||||
.heading-link {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ module.exports = {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -8,5 +8,8 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [require("@tailwindcss/typography")],
|
plugins: [
|
||||||
};
|
require('@tailwindcss/typography')
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { Mermaid } from '@flowershow/core';
|
|||||||
const components = {
|
const components = {
|
||||||
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
|
Table: dynamic(() => import('@portaljs/components').then(mod => mod.Table)),
|
||||||
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.Catalog)),
|
Catalog: dynamic(() => import('@portaljs/components').then(mod => mod.Catalog)),
|
||||||
FlatUiTable: dynamic(() => import('@portaljs/components').then(mod => mod.FlatUiTable)),
|
|
||||||
mermaid: Mermaid,
|
mermaid: Mermaid,
|
||||||
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
|
Vega: dynamic(() => import('@portaljs/components').then(mod => mod.Vega)),
|
||||||
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
|
VegaLite: dynamic(() => import('@portaljs/components').then(mod => mod.VegaLite)),
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -17,12 +17,21 @@
|
|||||||
"@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",
|
||||||
"@opentelemetry/api": "^1.4.0",
|
"@opentelemetry/api": "^1.4.0",
|
||||||
"@portaljs/components": "^0.1.0",
|
"@portaljs/components": "^0.1.0",
|
||||||
|
"@tanstack/react-table": "^8.8.5",
|
||||||
|
"flexsearch": "0.7.21",
|
||||||
"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",
|
||||||
"next": "13.2.1",
|
"next": "13.2.1",
|
||||||
|
"next-mdx-remote": "^4.4.1",
|
||||||
|
"papaparse": "^5.4.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.43.9",
|
||||||
|
"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",
|
||||||
"rehype-prism-plus": "^1.5.1",
|
"rehype-prism-plus": "^1.5.1",
|
||||||
@@ -31,20 +40,7 @@
|
|||||||
"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",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4"
|
||||||
"@githubocto/flat-ui": "^0.14.1",
|
|
||||||
"@heroicons/react": "^2.0.17",
|
|
||||||
"@tanstack/react-table": "^8.8.5",
|
|
||||||
"flexsearch": "0.7.21",
|
|
||||||
"next-mdx-remote": "^4.4.1",
|
|
||||||
"papaparse": "^5.4.1",
|
|
||||||
"react": "^18.2.0",
|
|
||||||
"react-dom": "^18.2.0",
|
|
||||||
"react-hook-form": "^7.43.9",
|
|
||||||
"react-query": "^3.39.3",
|
|
||||||
"react-vega": "^7.6.0",
|
|
||||||
"vega": "5.25.0",
|
|
||||||
"vega-lite": "5.1.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
@@ -1,21 +0,0 @@
|
|||||||
import { MDXRemote } from 'next-mdx-remote';
|
|
||||||
import dynamic from 'next/dynamic';
|
|
||||||
import { Mermaid } from '@flowershow/core';
|
|
||||||
|
|
||||||
// Custom components/renderers to pass to MDX.
|
|
||||||
// Since the MDX files aren't loaded by webpack, they have no knowledge of how
|
|
||||||
// to handle import statements. Instead, you must include components in scope
|
|
||||||
// here.
|
|
||||||
const components = {
|
|
||||||
Table: dynamic(() => import('../portaljs/components/Table').then(mod => mod.Table)),
|
|
||||||
Catalog: dynamic(() => import('../portaljs/components/Catalog').then(mod => mod.Catalog)),
|
|
||||||
mermaid: Mermaid,
|
|
||||||
Vega: dynamic(() => import('../portaljs/components/Vega').then(mod => mod.Vega)),
|
|
||||||
VegaLite: dynamic(() => import('../portaljs/components/VegaLite').then(mod => mod.VegaLite)),
|
|
||||||
LineChart: dynamic(() => import('../portaljs/components/LineChart').then(mod => mod.LineChart)),
|
|
||||||
FlatUiTable: dynamic(() => import('../portaljs/components/FlatUiTable').then(mod => mod.FlatUiTable)),
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
export default function DRD({ source }: { source: any }) {
|
|
||||||
return <MDXRemote {...source} components={components} />;
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
import matter from "gray-matter";
|
|
||||||
import mdxmermaid from "mdx-mermaid";
|
|
||||||
import { h } from "hastscript";
|
|
||||||
import remarkCallouts from "@flowershow/remark-callouts";
|
|
||||||
import remarkEmbed from "@flowershow/remark-embed";
|
|
||||||
import remarkGfm from "remark-gfm";
|
|
||||||
import remarkMath from "remark-math";
|
|
||||||
import remarkSmartypants from "remark-smartypants";
|
|
||||||
import remarkToc from "remark-toc";
|
|
||||||
import remarkWikiLink from "@flowershow/remark-wiki-link";
|
|
||||||
import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
|
||||||
import rehypeKatex from "rehype-katex";
|
|
||||||
import rehypeSlug from "rehype-slug";
|
|
||||||
import rehypePrismPlus from "rehype-prism-plus";
|
|
||||||
|
|
||||||
import { serialize } from "next-mdx-remote/serialize";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a markdown or MDX file to an MDX source form + front matter data
|
|
||||||
*
|
|
||||||
* @source: the contents of a markdown or mdx file
|
|
||||||
* @format: used to indicate to next-mdx-remote which format to use (md or mdx)
|
|
||||||
* @returns: { mdxSource: mdxSource, frontMatter: ...}
|
|
||||||
*/
|
|
||||||
const parse = async function (source, format, scope) {
|
|
||||||
const { content, data, excerpt } = matter(source, {
|
|
||||||
excerpt: (file, options) => {
|
|
||||||
// Generate an excerpt for the file
|
|
||||||
file.excerpt = file.content.split("\n\n")[0];
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mdxSource = await serialize(
|
|
||||||
{ value: content, path: format },
|
|
||||||
{
|
|
||||||
// Optionally pass remark/rehype plugins
|
|
||||||
mdxOptions: {
|
|
||||||
remarkPlugins: [
|
|
||||||
remarkEmbed,
|
|
||||||
remarkGfm,
|
|
||||||
[remarkSmartypants, { quotes: false, dashes: "oldschool" }],
|
|
||||||
remarkMath,
|
|
||||||
remarkCallouts,
|
|
||||||
remarkWikiLink,
|
|
||||||
[
|
|
||||||
remarkToc,
|
|
||||||
{
|
|
||||||
heading: "Table of contents",
|
|
||||||
tight: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[mdxmermaid, {}],
|
|
||||||
],
|
|
||||||
rehypePlugins: [
|
|
||||||
rehypeSlug,
|
|
||||||
[
|
|
||||||
rehypeAutolinkHeadings,
|
|
||||||
{
|
|
||||||
properties: { className: 'heading-link' },
|
|
||||||
test(element) {
|
|
||||||
return (
|
|
||||||
["h2", "h3", "h4", "h5", "h6"].includes(element.tagName) &&
|
|
||||||
element.properties?.id !== "table-of-contents" &&
|
|
||||||
element.properties?.className !== "blockquote-heading"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
content() {
|
|
||||||
return [
|
|
||||||
h(
|
|
||||||
"svg",
|
|
||||||
{
|
|
||||||
xmlns: "http:www.w3.org/2000/svg",
|
|
||||||
fill: "#ab2b65",
|
|
||||||
viewBox: "0 0 20 20",
|
|
||||||
className: "w-5 h-5",
|
|
||||||
},
|
|
||||||
[
|
|
||||||
h("path", {
|
|
||||||
fillRule: "evenodd",
|
|
||||||
clipRule: "evenodd",
|
|
||||||
d: "M9.493 2.853a.75.75 0 00-1.486-.205L7.545 6H4.198a.75.75 0 000 1.5h3.14l-.69 5H3.302a.75.75 0 000 1.5h3.14l-.435 3.148a.75.75 0 001.486.205L7.955 14h2.986l-.434 3.148a.75.75 0 001.486.205L12.456 14h3.346a.75.75 0 000-1.5h-3.14l.69-5h3.346a.75.75 0 000-1.5h-3.14l.435-3.147a.75.75 0 00-1.486-.205L12.045 6H9.059l.434-3.147zM8.852 7.5l-.69 5h2.986l.69-5H8.852z",
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[rehypeKatex, { output: "mathml" }],
|
|
||||||
[rehypePrismPlus, { ignoreMissing: true }],
|
|
||||||
],
|
|
||||||
format,
|
|
||||||
},
|
|
||||||
scope,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
mdxSource: mdxSource,
|
|
||||||
frontMatter: data,
|
|
||||||
excerpt,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default parse;
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { Index } from 'flexsearch';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import DebouncedInput from './DebouncedInput';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
|
|
||||||
export function Catalog({
|
|
||||||
datasets,
|
|
||||||
facets,
|
|
||||||
}: {
|
|
||||||
datasets: any[];
|
|
||||||
facets: string[];
|
|
||||||
}) {
|
|
||||||
const [indexFilter, setIndexFilter] = useState('');
|
|
||||||
const index = new Index({ tokenize: 'full' });
|
|
||||||
datasets.forEach((dataset) =>
|
|
||||||
index.add(
|
|
||||||
dataset._id,
|
|
||||||
//This will join every metadata value + the url_path into one big string and index that
|
|
||||||
Object.entries(dataset.metadata).reduce(
|
|
||||||
(acc, curr) => acc + ' ' + curr[1].toString(),
|
|
||||||
''
|
|
||||||
) +
|
|
||||||
' ' +
|
|
||||||
dataset.url_path
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const facetValues = facets
|
|
||||||
? facets.reduce((acc, facet) => {
|
|
||||||
const possibleValues = datasets.reduce((acc, curr) => {
|
|
||||||
const facetValue = curr.metadata[facet];
|
|
||||||
if (facetValue) {
|
|
||||||
return Array.isArray(facetValue)
|
|
||||||
? acc.concat(facetValue)
|
|
||||||
: acc.concat([facetValue]);
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
acc[facet] = {
|
|
||||||
possibleValues: [...new Set(possibleValues)],
|
|
||||||
selectedValue: null,
|
|
||||||
};
|
|
||||||
return acc;
|
|
||||||
}, {})
|
|
||||||
: [];
|
|
||||||
|
|
||||||
const { register, watch } = useForm(facetValues);
|
|
||||||
|
|
||||||
const filteredDatasets = datasets
|
|
||||||
// First filter by flex search
|
|
||||||
.filter((dataset) =>
|
|
||||||
indexFilter !== ''
|
|
||||||
? index.search(indexFilter).includes(dataset._id)
|
|
||||||
: true
|
|
||||||
)
|
|
||||||
//Then check if the selectedValue for the given facet is included in the dataset metadata
|
|
||||||
.filter((dataset) => {
|
|
||||||
//Avoids a server rendering breakage
|
|
||||||
if (!watch() || Object.keys(watch()).length === 0) return true
|
|
||||||
//This will filter only the key pairs of the metadata values that were selected as facets
|
|
||||||
const datasetFacets = Object.entries(dataset.metadata).filter((entry) =>
|
|
||||||
facets.includes(entry[0])
|
|
||||||
);
|
|
||||||
//Check if the value present is included in the selected value in the form
|
|
||||||
return datasetFacets.every((elem) =>
|
|
||||||
watch()[elem[0]].selectedValue
|
|
||||||
? (elem[1] as string | string[]).includes(
|
|
||||||
watch()[elem[0]].selectedValue
|
|
||||||
)
|
|
||||||
: true
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<DebouncedInput
|
|
||||||
value={indexFilter ?? ''}
|
|
||||||
onChange={(value) => setIndexFilter(String(value))}
|
|
||||||
className="p-2 text-sm shadow border border-block mr-1"
|
|
||||||
placeholder="Search all datasets..."
|
|
||||||
/>
|
|
||||||
{Object.entries(facetValues).map((elem) => (
|
|
||||||
<select
|
|
||||||
key={elem[0]}
|
|
||||||
defaultValue=""
|
|
||||||
className="p-2 ml-1 text-sm shadow border border-block"
|
|
||||||
{...register(elem[0] + '.selectedValue')}
|
|
||||||
>
|
|
||||||
<option value="">
|
|
||||||
Filter by {elem[0]}
|
|
||||||
</option>
|
|
||||||
{(elem[1] as { possibleValues: string[] }).possibleValues.map(
|
|
||||||
(val) => (
|
|
||||||
<option
|
|
||||||
key={val}
|
|
||||||
className="dark:bg-white dark:text-black"
|
|
||||||
value={val}
|
|
||||||
>
|
|
||||||
{val}
|
|
||||||
</option>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
))}
|
|
||||||
<ul className='mb-5 pl-6 mt-5 list-disc'>
|
|
||||||
{filteredDatasets.map((dataset) => (
|
|
||||||
<li className='py-2' key={dataset._id}>
|
|
||||||
<a className='font-medium underline' href={dataset.url_path}>
|
|
||||||
{dataset.metadata.title
|
|
||||||
? dataset.metadata.title
|
|
||||||
: dataset.url_path}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
|
||||||
import Papa from 'papaparse';
|
|
||||||
import { Grid } from '@githubocto/flat-ui';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
export async function getCsv(url: string, corsProxy?: string) {
|
|
||||||
if (corsProxy) {
|
|
||||||
url = corsProxy + url
|
|
||||||
}
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
Range: 'bytes=0-5132288',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await response.text();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseCsv(file: string): Promise<any> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
Papa.parse(file, {
|
|
||||||
header: true,
|
|
||||||
dynamicTyping: true,
|
|
||||||
skipEmptyLines: true,
|
|
||||||
transform: (value: string): string => {
|
|
||||||
return value.trim();
|
|
||||||
},
|
|
||||||
complete: (results: any) => {
|
|
||||||
return resolve(results);
|
|
||||||
},
|
|
||||||
error: (error: any) => {
|
|
||||||
return reject(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const Spinning = () => {
|
|
||||||
return (
|
|
||||||
<div role="status w-fit mx-auto">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-emerald-600"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface FlatUiTableProps {
|
|
||||||
url?: string;
|
|
||||||
data?: { [key: string]: number | string }[];
|
|
||||||
rawCsv?: string;
|
|
||||||
corsProxy?: string;
|
|
||||||
}
|
|
||||||
export const FlatUiTable: React.FC<FlatUiTableProps> = ({
|
|
||||||
url,
|
|
||||||
data,
|
|
||||||
rawCsv,
|
|
||||||
corsProxy,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
// Provide the client to your App
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<TableInner corsProxy={corsProxy} url={url} data={data} rawCsv={rawCsv} />
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TableInner: React.FC<FlatUiTableProps> = ({ url, data, rawCsv, corsProxy }) => {
|
|
||||||
if (data) {
|
|
||||||
return (
|
|
||||||
<div className="w-full" style={{height: '500px'}}>
|
|
||||||
<Grid data={data} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
|
|
||||||
['dataCsv', url],
|
|
||||||
() => getCsv(url as string, corsProxy),
|
|
||||||
{ enabled: !!url }
|
|
||||||
);
|
|
||||||
const { data: parsedData, isLoading: isParsing } = useQuery(
|
|
||||||
['dataPreview', csvString],
|
|
||||||
() => parseCsv(rawCsv ? rawCsv as string : csvString as string),
|
|
||||||
{ enabled: rawCsv ? true : !!csvString }
|
|
||||||
);
|
|
||||||
if (isParsing || isDownloadingCSV)
|
|
||||||
<div className="w-full">
|
|
||||||
<Spinning />
|
|
||||||
</div>;
|
|
||||||
if (parsedData)
|
|
||||||
return (
|
|
||||||
<div className="w-full" style={{height: '500px'}}>
|
|
||||||
<Grid data={parsedData.data} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
return <Spinning />
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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: 600,
|
|
||||||
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} />;
|
|
||||||
}
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Wrapper for the Vega component
|
|
||||||
import { Vega as VegaOg } from "react-vega";
|
|
||||||
|
|
||||||
export function Vega(props) {
|
|
||||||
return <VegaOg {...props} />;
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// 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} />;
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
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} />;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export default async function loadData(url: string) {
|
|
||||||
const response = await fetch(url)
|
|
||||||
const data = await response.text()
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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,6 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
plugins: {
|
|
||||||
tailwindcss: {},
|
|
||||||
autoprefixer: {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -1,116 +1,45 @@
|
|||||||
import { expect, test } from 'vitest';
|
import { Octokit } from 'octokit';
|
||||||
import { getAllProjectsFromOrg, getProjectDataPackage } from '../lib/project';
|
import { assert, expect, test } from 'vitest'
|
||||||
import { loadDataPackage } from '../lib/loader';
|
import { getProjectDataPackage } from '../lib/octokit';
|
||||||
import { getProjectMetadata } from '../lib/project';
|
|
||||||
import { validate } from 'datapackage';
|
|
||||||
|
|
||||||
test(
|
export async function getAllDataPackagesFromOrg(
|
||||||
'Test OS-Data',
|
org: string,
|
||||||
async () => {
|
branch?: string,
|
||||||
const repos = await getAllProjectsFromOrg(
|
github_pat?: string
|
||||||
'os-data',
|
) {
|
||||||
'main',
|
const octokit = new Octokit({ auth: github_pat });
|
||||||
process.env.VITE_GITHUB_PAT
|
const repos = await octokit.rest.repos.listForOrg({ org, type: 'public', per_page: 100 });
|
||||||
);
|
let failedDataPackages = [];
|
||||||
if (repos.failed.length > 0)
|
const datapackages = await Promise.all(
|
||||||
console.log('Failed to get datapackage on', repos.failed);
|
repos.data.map(async (_repo) => {
|
||||||
let failedDatapackages = await Promise.all(
|
const datapackage = await getProjectDataPackage(
|
||||||
repos.results.map(async (item) => {
|
org,
|
||||||
try {
|
_repo.name,
|
||||||
const { valid, errors } = await validate(item.datapackage);
|
branch ? branch : 'main',
|
||||||
return errors.length > 0 ? item.repo.name : null;
|
github_pat
|
||||||
} catch {
|
);
|
||||||
return item.repo.name;
|
if (!datapackage) {
|
||||||
}
|
failedDataPackages.push(_repo.name)
|
||||||
})
|
return null
|
||||||
);
|
};
|
||||||
failedDatapackages = failedDatapackages.filter((item) => item !== null);
|
return {...datapackage, repo: _repo.name};
|
||||||
if (failedDatapackages.length > 0) {
|
})
|
||||||
console.log('Failed to validate datapackage on ', failedDatapackages);
|
);
|
||||||
} else {
|
return {
|
||||||
console.log('No invalid packages');
|
datapackages: datapackages.filter((item) => item !== null),
|
||||||
}
|
failedDataPackages,
|
||||||
},
|
};
|
||||||
{ timeout: 100000 }
|
}
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
test('Test OS-Data', async () => {
|
||||||
'Test Gift-Data',
|
const repos = await getAllDataPackagesFromOrg('os-data', 'main', process.env.VITE_GITHUB_PAT)
|
||||||
async () => {
|
if (repos.failedDataPackages.length > 0) console.log(repos.failedDataPackages)
|
||||||
const repos = await getAllProjectsFromOrg(
|
expect(repos.failedDataPackages.length).toBe(0)
|
||||||
'gift-data',
|
}, {timeout: 100000})
|
||||||
'main',
|
|
||||||
process.env.VITE_GITHUB_PAT
|
test('Test Gift-Data', async () => {
|
||||||
);
|
const repos = await getAllDataPackagesFromOrg('gift-data', 'main', process.env.VITE_GITHUB_PAT)
|
||||||
if (repos.failed.length > 0) console.log(repos.failed);
|
if (repos.failedDataPackages.length > 0) console.log(repos.failedDataPackages)
|
||||||
let failedDatapackages = await Promise.all(
|
expect(repos.failedDataPackages.length).toBe(0)
|
||||||
repos.results.map(async (item) => {
|
}, {timeout: 100000})
|
||||||
try {
|
|
||||||
const { valid, errors } = await validate(item.datapackage);
|
|
||||||
return errors.length > 0 ? item.repo.name : null;
|
|
||||||
} catch {
|
|
||||||
return item.repo.name;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
failedDatapackages = failedDatapackages.filter((item) => item !== null);
|
|
||||||
if (failedDatapackages.length > 0) {
|
|
||||||
console.log('Failed to validate datapackage on ', failedDatapackages);
|
|
||||||
} else {
|
|
||||||
console.log('No invalid packages');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ timeout: 100000 }
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'Test getting one dataset from github',
|
|
||||||
async () => {
|
|
||||||
const datapackage = await getProjectDataPackage(
|
|
||||||
'os-data',
|
|
||||||
'berlin-berlin',
|
|
||||||
'main',
|
|
||||||
process.env.VITE_GITHUB_PAT
|
|
||||||
);
|
|
||||||
const repo = await getProjectMetadata(
|
|
||||||
'os-data',
|
|
||||||
'berlin-berlin',
|
|
||||||
process.env.VITE_GITHUB_PAT
|
|
||||||
);
|
|
||||||
const project = loadDataPackage(datapackage, repo);
|
|
||||||
delete project['datapackage'];
|
|
||||||
delete project.files[0]['dialect'];
|
|
||||||
delete project.files[0]['schema'];
|
|
||||||
expect(project).toStrictEqual({
|
|
||||||
name: 'berlin-berlin',
|
|
||||||
title: 'Berlin-Berlin',
|
|
||||||
description: null,
|
|
||||||
owner: {
|
|
||||||
name: 'os-data',
|
|
||||||
logo: 'https://avatars.githubusercontent.com/u/13695166?v=4',
|
|
||||||
title: 'os-data',
|
|
||||||
},
|
|
||||||
repo: {
|
|
||||||
name: 'berlin-berlin',
|
|
||||||
full_name: 'os-data/berlin-berlin',
|
|
||||||
url: 'https://github.com/os-data/berlin-berlin',
|
|
||||||
},
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
name: 'berlin-gesamt',
|
|
||||||
format: 'csv',
|
|
||||||
path: 'https://storage.openspending.org/berlin-berlin/berlin-gesamt.csv',
|
|
||||||
mediatype: 'text/csv',
|
|
||||||
bytes: 81128743,
|
|
||||||
encoding: 'utf-8',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
author: 'Michael Peters <michael.peters@okfn.de>',
|
|
||||||
cityCode: 'Berlin',
|
|
||||||
countryCode: 'DE',
|
|
||||||
fiscalPeriod: { start: '2014-01-01', end: '2019-12-31' },
|
|
||||||
readme: '',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{ timeout: 100000 }
|
|
||||||
);
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function DatasetCard({ dataset }: { dataset: Project }) {
|
|||||||
className="overflow-hidden rounded-xl border border-gray-200"
|
className="overflow-hidden rounded-xl border border-gray-200"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={`/@${dataset.owner.name}/${dataset.repo.name}`}
|
href=""
|
||||||
className="flex items-center gap-x-4 border-b border-gray-900/5 bg-gray-50 p-6"
|
className="flex items-center gap-x-4 border-b border-gray-900/5 bg-gray-50 p-6"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -60,8 +60,8 @@ export default function DatasetCard({ dataset }: { dataset: Project }) {
|
|||||||
<dd className="flex items-start gap-x-2">
|
<dd className="flex items-start gap-x-2">
|
||||||
<div className="font-medium text-gray-900">
|
<div className="font-medium text-gray-900">
|
||||||
<Link
|
<Link
|
||||||
// TODO: this link may be incorrect for some datasets
|
// TODO: where do we get the info needed for this link?
|
||||||
href={`https://github.com/${dataset.owner.name}/${dataset.repo.name}/blob/main/datapackage.json`}
|
href=""
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="flex items-center hover:text-gray-700"
|
className="flex items-center hover:text-gray-700"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,30 +2,9 @@ import { useForm } from 'react-hook-form';
|
|||||||
import DatasetsGrid from './DatasetsGrid';
|
import DatasetsGrid from './DatasetsGrid';
|
||||||
import { Project } from '../lib/project.interface';
|
import { Project } from '../lib/project.interface';
|
||||||
import { Index } from 'flexsearch';
|
import { Index } from 'flexsearch';
|
||||||
import {
|
|
||||||
ChevronDoubleLeftIcon,
|
|
||||||
ChevronDoubleRightIcon,
|
|
||||||
ChevronLeftIcon,
|
|
||||||
ChevronRightIcon,
|
|
||||||
} from '@heroicons/react/24/solid';
|
|
||||||
import { useState } from 'react';
|
|
||||||
|
|
||||||
export default function DatasetsSearch({
|
|
||||||
datasets,
|
|
||||||
availableCountries,
|
|
||||||
minPeriod,
|
|
||||||
maxPeriod,
|
|
||||||
}: {
|
|
||||||
datasets: Project[];
|
|
||||||
availableCountries;
|
|
||||||
minPeriod: string;
|
|
||||||
maxPeriod: string;
|
|
||||||
}) {
|
|
||||||
const itemsPerPage = 6;
|
|
||||||
const [page, setPage] = useState(1);
|
|
||||||
|
|
||||||
|
export default function DatasetsSearch({ datasets }: { datasets: Project[] }) {
|
||||||
const index = new Index({ tokenize: 'full' });
|
const index = new Index({ tokenize: 'full' });
|
||||||
|
|
||||||
datasets.forEach((dataset: Project) =>
|
datasets.forEach((dataset: Project) =>
|
||||||
index.add(
|
index.add(
|
||||||
dataset.name,
|
dataset.name,
|
||||||
@@ -42,53 +21,12 @@ export default function DatasetsSearch({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const filteredDatasets = datasets
|
const allCountries = datasets
|
||||||
.filter((dataset: Project) =>
|
.map((item) => item.countryCode)
|
||||||
watch().searchTerm && watch().searchTerm !== ''
|
.filter((v) => v) // Filters false values
|
||||||
? index.search(watch().searchTerm).includes(dataset.name)
|
.filter((v, i, a) => a.indexOf(v) === i) // Remove duplicates
|
||||||
: true
|
// TODO: title should be the full name
|
||||||
)
|
.map((code) => ({ code, title: code }));
|
||||||
.filter((dataset) =>
|
|
||||||
watch().country && watch().country !== ''
|
|
||||||
? dataset.countryCode === watch().country
|
|
||||||
: true
|
|
||||||
)
|
|
||||||
.filter((dataset) => {
|
|
||||||
const filterMinDate = watch().minDate;
|
|
||||||
const filterMaxDate = watch().maxDate;
|
|
||||||
|
|
||||||
const datasetMinDate = dataset.fiscalPeriod?.start;
|
|
||||||
const datasetMaxDate = dataset.fiscalPeriod?.end;
|
|
||||||
|
|
||||||
let datasetStartOverlaps = false;
|
|
||||||
if (datasetMinDate) {
|
|
||||||
datasetStartOverlaps =
|
|
||||||
datasetMinDate >= filterMinDate && datasetMinDate <= filterMaxDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
let datasetEndOverlaps = false;
|
|
||||||
if (datasetMaxDate) {
|
|
||||||
datasetEndOverlaps =
|
|
||||||
datasetMaxDate >= filterMinDate && datasetMaxDate <= filterMaxDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterMinDate && filterMaxDate) {
|
|
||||||
return datasetStartOverlaps || datasetEndOverlaps;
|
|
||||||
} else if (filterMinDate) {
|
|
||||||
return datasetMinDate >= filterMinDate;
|
|
||||||
} else if (filterMaxDate) {
|
|
||||||
return datasetMinDate <= filterMaxDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginatedDatasets = filteredDatasets.slice(
|
|
||||||
(page - 1) * itemsPerPage,
|
|
||||||
(page - 1) * itemsPerPage + itemsPerPage
|
|
||||||
);
|
|
||||||
|
|
||||||
const pageCount = Math.ceil(filteredDatasets.length / itemsPerPage) || 1;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,7 +37,7 @@ export default function DatasetsSearch({
|
|||||||
<input
|
<input
|
||||||
placeholder="Search datasets"
|
placeholder="Search datasets"
|
||||||
aria-label="Search datasets"
|
aria-label="Search datasets"
|
||||||
{...register('searchTerm', { onChange: () => setPage(1) })}
|
{...register('searchTerm')}
|
||||||
className="h-[3em] relative w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
|
className="h-[3em] relative w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
{watch().searchTerm !== '' && (
|
{watch().searchTerm !== '' && (
|
||||||
@@ -117,10 +55,10 @@ export default function DatasetsSearch({
|
|||||||
<label className="text-sm text-gray-600 font-medium">Country</label>
|
<label className="text-sm text-gray-600 font-medium">Country</label>
|
||||||
<select
|
<select
|
||||||
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
|
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
|
||||||
{...register('country', { onChange: () => setPage(1) })}
|
{...register('country')}
|
||||||
>
|
>
|
||||||
<option value="">All</option>
|
<option value="">All</option>
|
||||||
{availableCountries.map((country) => {
|
{allCountries.map((country) => {
|
||||||
return (
|
return (
|
||||||
<option key={country.code} value={country.code}>
|
<option key={country.code} value={country.code}>
|
||||||
{country.title}
|
{country.title}
|
||||||
@@ -130,82 +68,72 @@ export default function DatasetsSearch({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/6">
|
<div className="sm:basis-1/6">
|
||||||
<label className="text-sm text-gray-600 font-medium">
|
<label className="text-sm text-gray-600 font-medium">Min. date</label>
|
||||||
Fiscal Period Start
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
aria-label="Min. date"
|
aria-label="Min. date"
|
||||||
type="text"
|
type="date"
|
||||||
placeholder={minPeriod}
|
{...register('minDate')}
|
||||||
onFocus={(e) => (e.target.type = 'date')}
|
|
||||||
onBlur={(e) => (e.target.type = 'text')}
|
|
||||||
{...register('minDate', { onChange: () => setPage(1) })}
|
|
||||||
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
|
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
|
{watch().minDate !== '' && (
|
||||||
|
<button
|
||||||
|
onClick={() => resetField('minDate')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="sm:basis-1/6">
|
<div className="sm:basis-1/6">
|
||||||
<label className="text-sm text-gray-600 font-medium">
|
<label className="text-sm text-gray-600 font-medium">Max. date</label>
|
||||||
Fiscal Period End
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
aria-label="Max. date"
|
aria-label="Max. date"
|
||||||
type="text"
|
type="date"
|
||||||
placeholder={maxPeriod}
|
{...register('maxDate')}
|
||||||
onFocus={(e) => (e.target.type = 'date')}
|
|
||||||
onBlur={(e) => (e.target.type = 'text')}
|
|
||||||
{...register('maxDate', { onChange: () => setPage(1) })}
|
|
||||||
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
|
className="h-[3em] w-full rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-emerald-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-emerald-400 sm:text-sm"
|
||||||
/>
|
/>
|
||||||
|
{watch().maxDate !== '' && (
|
||||||
|
<button
|
||||||
|
onClick={() => resetField('maxDate')}
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 mb-5">
|
<div className="min-w-full mt-10 align-middle">
|
||||||
<span className="text-lg font-medium">
|
<DatasetsGrid
|
||||||
{filteredDatasets.length} datasets found
|
datasets={datasets
|
||||||
</span>
|
.filter((dataset: Project) =>
|
||||||
</div>
|
watch().searchTerm && watch().searchTerm !== ''
|
||||||
<div className="min-w-full align-middle">
|
? index.search(watch().searchTerm).includes(dataset.name)
|
||||||
<DatasetsGrid datasets={paginatedDatasets} />
|
: true
|
||||||
<div className="w-full flex justify-center mt-10">
|
)
|
||||||
<button
|
.filter((dataset) =>
|
||||||
onClick={() => setPage(1)}
|
watch().country && watch().country !== ''
|
||||||
disabled={page <= 1}
|
? dataset.countryCode === watch().country
|
||||||
className="disabled:text-gray-400"
|
: true
|
||||||
>
|
)
|
||||||
<ChevronDoubleLeftIcon className="w-6 h-6" />
|
// TODO: Does that really makes sense?
|
||||||
</button>
|
// What if the fiscalPeriod is 2015-2017 and inputs are
|
||||||
<button
|
// set to 2015-2016. It's going to be filtered out but
|
||||||
onClick={() => {
|
// it shouldn't.
|
||||||
if (page > 1) setPage((prev) => --prev);
|
.filter((dataset) =>
|
||||||
}}
|
watch().minDate && watch().minDate !== ''
|
||||||
disabled={page <= 1}
|
? dataset.fiscalPeriod?.start >= watch().minDate
|
||||||
className="disabled:text-gray-400"
|
: true
|
||||||
>
|
)
|
||||||
<ChevronLeftIcon className="w-6 h-6" />
|
.filter((dataset) =>
|
||||||
</button>
|
watch().maxDate && watch().maxDate !== ''
|
||||||
<span className="mx-5">
|
? dataset.fiscalPeriod?.end <= watch().maxDate
|
||||||
Page {page} of {pageCount}
|
: true
|
||||||
</span>
|
)}
|
||||||
<button
|
/>
|
||||||
onClick={() => {
|
|
||||||
if (page < pageCount) setPage((prev) => ++prev);
|
|
||||||
}}
|
|
||||||
disabled={page >= pageCount}
|
|
||||||
className="disabled:text-gray-400"
|
|
||||||
>
|
|
||||||
<ChevronRightIcon className="w-6 h-6" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setPage(pageCount)}
|
|
||||||
disabled={page >= pageCount}
|
|
||||||
className="disabled:text-gray-400"
|
|
||||||
>
|
|
||||||
<ChevronDoubleRightIcon className="w-6 h-6" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -225,9 +153,9 @@ const CloseIcon = () => {
|
|||||||
id="Vector"
|
id="Vector"
|
||||||
d="M18 18L12 12M12 12L6 6M12 12L18 6M12 12L6 18"
|
d="M18 18L12 12M12 12L6 6M12 12L18 6M12 12L6 18"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
strokeWidth="2"
|
stroke-width="2"
|
||||||
strokeLinecap="round"
|
stroke-linecap="round"
|
||||||
strokeLinejoin="round"
|
stroke-linejoin="round"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
import { QueryClient, QueryClientProvider, useQuery } from 'react-query';
|
|
||||||
import Papa from 'papaparse';
|
|
||||||
import { Grid } from '@githubocto/flat-ui';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
|
||||||
|
|
||||||
export async function getCsv(url: string) {
|
|
||||||
const response = await fetch(url, {
|
|
||||||
headers: {
|
|
||||||
Range: 'bytes=0-5132288',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const data = await response.text();
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseCsv(file: string): Promise<any> {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
Papa.parse(file, {
|
|
||||||
header: true,
|
|
||||||
dynamicTyping: true,
|
|
||||||
skipEmptyLines: true,
|
|
||||||
transform: (value: string): string => {
|
|
||||||
return value.trim();
|
|
||||||
},
|
|
||||||
complete: (results: any) => {
|
|
||||||
return resolve(results);
|
|
||||||
},
|
|
||||||
error: (error: any) => {
|
|
||||||
return reject(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const Spinning = () => {
|
|
||||||
return (
|
|
||||||
<div role="status w-fit mx-auto">
|
|
||||||
<svg
|
|
||||||
aria-hidden="true"
|
|
||||||
className="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-emerald-600"
|
|
||||||
viewBox="0 0 100 101"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
|
||||||
fill="currentColor"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
|
||||||
fill="currentFill"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
<span className="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface FlatUiTableProps {
|
|
||||||
url?: string;
|
|
||||||
data?: { [key: string]: number | string }[];
|
|
||||||
rawCsv?: string;
|
|
||||||
}
|
|
||||||
export const FlatUiTable: React.FC<FlatUiTableProps> = ({
|
|
||||||
url,
|
|
||||||
data,
|
|
||||||
rawCsv,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
// Provide the client to your App
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<TableInner url={url} data={data} rawCsv={rawCsv} />
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TableInner: React.FC<FlatUiTableProps> = ({ url, data, rawCsv }) => {
|
|
||||||
if (data) {
|
|
||||||
return (
|
|
||||||
<div className="w-full" style={{height: '500px'}}>
|
|
||||||
<Grid data={data} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { data: csvString, isLoading: isDownloadingCSV } = useQuery(
|
|
||||||
['dataCsv', url],
|
|
||||||
() => getCsv(url),
|
|
||||||
{ enabled: !!url }
|
|
||||||
);
|
|
||||||
const { data: parsedData, isLoading: isParsing } = useQuery(
|
|
||||||
['dataPreview', csvString],
|
|
||||||
() => parseCsv(rawCsv ? rawCsv : csvString),
|
|
||||||
{ enabled: rawCsv ? true : !!csvString }
|
|
||||||
);
|
|
||||||
if (isParsing || isDownloadingCSV)
|
|
||||||
<div className="w-full">
|
|
||||||
<Spinning />
|
|
||||||
</div>;
|
|
||||||
if (parsedData)
|
|
||||||
return (
|
|
||||||
<div className="w-full" style={{height: '500px'}}>
|
|
||||||
<Grid data={parsedData.data} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,186 +1,53 @@
|
|||||||
import Image from 'next/image';
|
import Image from 'next/image'
|
||||||
import { Container } from './Container';
|
import { Button } from './Button'
|
||||||
import logo from '../public/logo.svg';
|
import { Container } from './Container'
|
||||||
import Link from 'next/link';
|
import logo from "../public/logo.svg"
|
||||||
import { useRouter } from 'next/router';
|
import Link from 'next/link'
|
||||||
import { Bars3Icon } from '@heroicons/react/24/outline';
|
import { useRouter } from 'next/router'
|
||||||
import { useState } from 'react';
|
|
||||||
import { Fragment } from 'react';
|
|
||||||
import { Menu, Transition } from '@headlessui/react';
|
|
||||||
import { ChevronDownIcon } from '@heroicons/react/20/solid';
|
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const [menuOpen, setMenuOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const isActive = (navLink) => {
|
const isActive = (navLink) => {
|
||||||
return router.asPath.split('?')[0] == navLink.href;
|
return router.asPath.split("?")[0] == navLink.href;
|
||||||
};
|
}
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{
|
{
|
||||||
title: 'Datasets',
|
title: "Home",
|
||||||
href: '/#datasets',
|
href: "/#header"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Blog',
|
title: "Datasets",
|
||||||
href: '/blog',
|
href: "/#datasets"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'About',
|
title: "Community",
|
||||||
href: '/about',
|
href: "https://community.openspending.org/"
|
||||||
children: [
|
}
|
||||||
{
|
]
|
||||||
title: 'Fiscal Data Package',
|
|
||||||
href: '/about/fiscaldatapackage/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Tools',
|
|
||||||
href: '/about/tools/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Funders',
|
|
||||||
href: '/about/funders/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Presentations',
|
|
||||||
href: '/about/presentations/',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Resources',
|
|
||||||
href: '/resources',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: 'Follow the money',
|
|
||||||
href: '/resources/journo',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Map of Spending Projects',
|
|
||||||
href: '/resources/map-of-spending-projects/',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Working Group On Open Spending Data',
|
|
||||||
href: '/resources/wg/',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="relative z-50 pb-11 lg:pt-11">
|
<header className="z-50 pb-5 lg:pt-11 sticky top-0 backdrop-blur" id="header">
|
||||||
<Container className="flex flex-wrap justify-between lg:flex-nowrap mt-10 lg:mt-0">
|
<Container className="flex flex-wrap items-center justify-center sm:justify-between lg:flex-nowrap">
|
||||||
<Link href="/" className="lg:mt-0 lg:grow lg:basis-0 flex items-center">
|
<div className="mt-10 lg:mt-0 lg:grow lg:basis-0 flex items-center">
|
||||||
<Image src={logo} alt="OpenSpending" className="h-12 w-auto" />
|
<Image src={logo} alt="OpenSpending" className="h-12 w-auto" />
|
||||||
</Link>
|
</div>
|
||||||
<ul className="hidden list-none sm:flex gap-x-5 text-base font-medium">
|
<ul className='list-none flex gap-x-5 text-base font-medium'>
|
||||||
{navLinks.map((link, i) => (
|
{navLinks.map((link, i) => (
|
||||||
<li key={`nav-link-${i}`}>
|
<li key={`nav-link-${i}`}>
|
||||||
<Dropdown navItem={link} />
|
<Link
|
||||||
</li>
|
className={`text-emerald-900 hover:text-emerald-600 ${isActive(link) ? "text-emerald-600" : ""}`}
|
||||||
))}
|
href={link.href}
|
||||||
|
scroll={false}
|
||||||
|
>
|
||||||
|
{link.title}
|
||||||
|
</Link>
|
||||||
|
</li>))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="sm:hidden sm:mt-10 lg:mt-0 lg:grow lg:basis-0 lg:justify-end">
|
<div className="hidden sm:mt-10 sm:flex lg:mt-0 lg:grow lg:basis-0 lg:justify-end">
|
||||||
<button onClick={() => setMenuOpen(!menuOpen)}>
|
|
||||||
<Bars3Icon className="w-8 h-8" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
{menuOpen && (
|
|
||||||
<div className={`sm:hidden basis-full mt-5 text-center`}>
|
|
||||||
<ul className="gap-x-5 text-base font-medium">
|
|
||||||
{navLinks.map((link, i) => (
|
|
||||||
<li key={`nav-link-${i}`}>
|
|
||||||
<Link
|
|
||||||
className={`text-emerald-900 hover:text-emerald-600 ${
|
|
||||||
isActive(link) ? 'text-emerald-600' : ''
|
|
||||||
}`}
|
|
||||||
href={link.href}
|
|
||||||
scroll={false}
|
|
||||||
>
|
|
||||||
{link.title}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Container>
|
</Container>
|
||||||
</header>
|
</header >
|
||||||
);
|
)
|
||||||
}
|
|
||||||
|
|
||||||
function classNames(...classes) {
|
|
||||||
return classes.filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function Dropdown({ navItem }: { navItem: any }) {
|
|
||||||
const [showDropDown, setShowDropDown] = useState(false);
|
|
||||||
return (
|
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
|
||||||
{({ open }) => (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<Menu.Button
|
|
||||||
onMouseEnter={() => setShowDropDown(true)}
|
|
||||||
onMouseLeave={() => setShowDropDown(false)}
|
|
||||||
className="text-emerald-900 hover:text-emerald-600 inline-flex w-full justify-center gap-x-1.5 px-3 py-2 text-sm font-semibold"
|
|
||||||
>
|
|
||||||
<Link href={navItem.href}>{navItem.title}</Link>
|
|
||||||
{navItem.children && (
|
|
||||||
<ChevronDownIcon
|
|
||||||
className="-mr-1 h-5 w-5 text-gray-400"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{navItem.children && (
|
|
||||||
<Transition
|
|
||||||
as={Fragment}
|
|
||||||
show={showDropDown}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Menu.Items
|
|
||||||
static
|
|
||||||
onMouseEnter={() => setShowDropDown(true)}
|
|
||||||
onMouseLeave={() => setShowDropDown(false)}
|
|
||||||
className="absolute right-0 z-10 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
|
|
||||||
>
|
|
||||||
<div className="py-1">
|
|
||||||
{navItem.children.map((item) => (
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<a
|
|
||||||
key={item.href}
|
|
||||||
href={item.href}
|
|
||||||
className={classNames(
|
|
||||||
active
|
|
||||||
? 'bg-gray-100 text-emerald-900 hover:text-emerald-600'
|
|
||||||
: 'text-gray-700',
|
|
||||||
'block px-4 py-2 text-sm'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.title}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Menu>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Button } from './Button';
|
import { Button } from './Button'
|
||||||
import { Container } from './Container';
|
import { Container } from './Container'
|
||||||
|
|
||||||
export function Hero({ countriesCount, datasetsCount, filesCount }) {
|
export function Hero() {
|
||||||
return (
|
return (
|
||||||
<div className="relative pb-20 pt-10 sm:py-40" id="hero">
|
<div className="relative pb-20 pt-10 sm:py-40">
|
||||||
<div className="absolute inset-x-0 -bottom-14 -top-48 overflow-hidden bg-green-50 bg-opacity-50">
|
<div className="absolute inset-x-0 -bottom-14 -top-48 overflow-hidden bg-green-50 bg-opacity-50">
|
||||||
<div className="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-white" />
|
<div className="absolute inset-x-0 top-0 h-40 bg-gradient-to-b from-white" />
|
||||||
<div className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-white" />
|
<div className="absolute inset-x-0 bottom-0 h-40 bg-gradient-to-t from-white" />
|
||||||
@@ -15,44 +15,33 @@ export function Hero({ countriesCount, datasetsCount, filesCount }) {
|
|||||||
</h1>
|
</h1>
|
||||||
<div className="mt-6 space-y-6 font-display text-2xl tracking-tight text-emerald-900">
|
<div className="mt-6 space-y-6 font-display text-2xl tracking-tight text-emerald-900">
|
||||||
<p>
|
<p>
|
||||||
By understanding how governments spend money in our name can we
|
By understanding how governments spend money in our name can we have a say
|
||||||
have a say in how that money will affect our own lives. The
|
in how that money will affect our own lives. The journey starts here.
|
||||||
journey starts here.
|
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
OpenSpending is a free, open and global platform to search,
|
OpenSpending is a free, open and global platform to search, visualise and analyse
|
||||||
visualise and analyse fiscal data in the public sphere.
|
fiscal data in the public sphere.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Button href="#datasets" className="mt-10">
|
<Button href="#datasets" className="mt-10">
|
||||||
Search datasets
|
Search datasets
|
||||||
</Button>
|
</Button>
|
||||||
<dl className="mt-10 grid grid-cols-1 sm:grid-cols-3 gap-x-10 gap-y-6 sm:mt-16 sm:gap-x-16 sm:gap-y-10 sm:text-center lg:auto-cols-auto lg:grid-flow-col lg:grid-cols-none lg:justify-start lg:text-left">
|
<dl className="mt-10 grid grid-cols-2 gap-x-10 gap-y-6 sm:mt-16 sm:gap-x-16 sm:gap-y-10 sm:text-center lg:auto-cols-auto lg:grid-flow-col lg:grid-cols-none lg:justify-start lg:text-left">
|
||||||
{[
|
{[
|
||||||
// Added the plus sign because some datasets do not
|
['Countries', '75'],
|
||||||
// contain defined countries
|
['Datasets', '2091'],
|
||||||
['Countries', '+' + countriesCount],
|
['Files', '9230'],
|
||||||
['Datasets', datasetsCount],
|
|
||||||
['Files', filesCount],
|
|
||||||
].map(([name, value]) => (
|
].map(([name, value]) => (
|
||||||
<div key={name}>
|
<div key={name}>
|
||||||
<div className='flex gap-x-2 items-center sm:hidden' key={name}>
|
<dt className="font-mono text-sm text-emerald-600">{name}</dt>
|
||||||
<dd className="mt-0.5 text-2xl font-semibold tracking-tight text-emerald-900">
|
<dd className="mt-0.5 text-2xl font-semibold tracking-tight text-emerald-900">
|
||||||
{value}
|
{value}
|
||||||
</dd>
|
</dd>
|
||||||
<dt className="font-mono text-sm text-emerald-600">{name}</dt>
|
|
||||||
</div>
|
|
||||||
<div className='hidden sm:block' key={name}>
|
|
||||||
<dt className="font-mono text-sm text-emerald-600">{name}</dt>
|
|
||||||
<dd className="mt-0.5 text-2xl font-semibold tracking-tight text-emerald-900">
|
|
||||||
{value}
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
export default function Footer() {
|
|
||||||
return (
|
|
||||||
<footer>
|
|
||||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-between md:flex-row">
|
|
||||||
<div className="flex gap-x-2 items-center mx-auto">
|
|
||||||
<p className="mt-8 text-base text-slate-500 md:mt-0">Maintained by</p>
|
|
||||||
<a href="https://www.datopian.com/" target="_blank">
|
|
||||||
<Image
|
|
||||||
alt="Datopian logo"
|
|
||||||
className="mb-2"
|
|
||||||
src="/datopian-logotype.png"
|
|
||||||
width={120}
|
|
||||||
height={30}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user