Compare commits
3 Commits
fivethirty
...
fivethirty
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e6a8ba114e | ||
|
|
d49abb1161 | ||
|
|
fe1850fdba |
1
.gitignore
vendored
@@ -16,7 +16,6 @@ node_modules
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
.obsidian
|
||||
|
||||
# IDE - VSCode
|
||||
.vscode/*
|
||||
|
||||
@@ -10,24 +10,12 @@ This is also a Next.JS project so you can use the following steps to run the web
|
||||
|
||||
## Getting started
|
||||
|
||||
To get started first clone this repo in your local machine like so:
|
||||
|
||||
```bash
|
||||
npx create-next-app turing --example https://github.com/datopian/portaljs/tree/main/examples/turing
|
||||
cd turing
|
||||
```
|
||||
|
||||
Then install the npm dependencies:
|
||||
To get started first install the npm dependencies:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
Next, run this command, its related to [markdowndb](https://github.com/datopian/markdowndb):
|
||||
```bash
|
||||
npm run mddb
|
||||
```
|
||||
|
||||
Next, run the development server:
|
||||
|
||||
```bash
|
||||
@@ -140,7 +140,7 @@ function MobileNavigation(props) {
|
||||
</div>
|
||||
<nav className="mt-6">
|
||||
<ul className="-my-2 divide-y divide-zinc-100 text-base text-zinc-800 dark:divide-zinc-100/5 dark:text-zinc-300">
|
||||
<MobileNavItem href="https://github.com/datopian/portaljs/tree/main/examples/turing">
|
||||
<MobileNavItem href="https://github.com/leondz/hatespeechdata">
|
||||
View on Github <GithubIcon />
|
||||
</MobileNavItem>
|
||||
</ul>
|
||||
@@ -179,7 +179,7 @@ function DesktopNavigation(props) {
|
||||
return (
|
||||
<nav {...props}>
|
||||
<ul className="flex rounded-full bg-white/90 px-3 text-sm font-medium text-zinc-800 shadow-lg shadow-zinc-800/5 ring-1 ring-zinc-900/5 backdrop-blur dark:bg-zinc-800/90 dark:text-zinc-200 dark:ring-white/10">
|
||||
<NavItem href="https://github.com/datopian/portaljs/tree/main/examples/turing">
|
||||
<NavItem href="https://github.com/leondz/hatespeechdata">
|
||||
View on Github <GithubIcon />
|
||||
</NavItem>
|
||||
</ul>
|
||||
@@ -36,7 +36,7 @@ In the following page type `content/datasets/<name-of-the-file>.md`. if you want
|
||||
|
||||
### Fill in content
|
||||
|
||||
Copy the contents of `templates/dataset.md` or `templates/keywords.md` respectively to the camp below, filling out the fields with the correct data format. Everything below the second `---` will automatically get rendered into the page, so you may add any standard markdown fields e.g tables, headings, lists...
|
||||
Copy the contents of `templates/dataset.md` or `templates/keywords.md` respectively to the camp below, filling out the fields with the correct data format
|
||||
|
||||

|
||||
|
||||
BIN
examples/alan-turing-portal/markdown.db
Normal file
@@ -6,6 +6,7 @@ import { Card } from '../components/Card'
|
||||
import Head from 'next/head'
|
||||
import parse from '../lib/markdown'
|
||||
import { Mermaid } from '@flowershow/core';
|
||||
import { Header } from '../components/Header';
|
||||
|
||||
export const getStaticProps = async ({ params }) => {
|
||||
const urlPath = params.slug ? params.slug.join('/') : ''
|
||||
@@ -81,13 +82,15 @@ export default function DRDPage({ mdxSource }) {
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Head>
|
||||
<title>{meta.title}</title>
|
||||
</Head>
|
||||
<Container className="mt-9 relative">
|
||||
<Container className="mt-16 lg:mt-32 relative">
|
||||
<Header />
|
||||
<article>
|
||||
<header className="flex flex-col">
|
||||
<h1 className="text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
<h1 className="mt-6 text-4xl font-bold tracking-tight text-zinc-800 dark:text-zinc-100 sm:text-5xl">
|
||||
{meta.title}
|
||||
</h1>
|
||||
<Card as="article">
|
||||
|
Before Width: | Height: | Size: 566 B After Width: | Height: | Size: 566 B |
1
examples/ckan-example/.env
Normal file
@@ -0,0 +1 @@
|
||||
DMS=https://demo.dev.datopian.com
|
||||
@@ -10,7 +10,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.0.17",
|
||||
"@portaljs/ckan": "^0.0.2",
|
||||
"next": "13.3.1",
|
||||
"next-seo": "^6.0.0",
|
||||
"octokit": "^2.0.14",
|
||||
@@ -21,14 +20,14 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"eslint": "8.39.0",
|
||||
"eslint-config-next": "13.3.1",
|
||||
"postcss": "^8.4.23",
|
||||
"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,
|
||||
UserIcon,
|
||||
} 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', {
|
||||
year: 'numeric',
|
||||
@@ -26,12 +25,14 @@ const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
});
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
const ckan = new CKAN(backend_url)
|
||||
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 {
|
||||
props: {
|
||||
dataset: _dataset,
|
||||
dataset: _dataset.result,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,8 +1,7 @@
|
||||
import getConfig from 'next/config';
|
||||
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', {
|
||||
year: 'numeric',
|
||||
@@ -16,11 +15,12 @@ const formatter = new Intl.DateTimeFormat('en-US', {
|
||||
|
||||
|
||||
export async function getServerSideProps() {
|
||||
const ckan = new CKAN(backend_url)
|
||||
const { datasets } = await ckan.packageSearch({ limit: 1000, offset: 0, groups:[], orgs: [], tags: []})
|
||||
const datasetsWithDetails = await Promise.all(datasets.map(async (dataset) => {
|
||||
const _dataset = await ckan.getDatasetDetails(dataset.name)
|
||||
return _dataset
|
||||
const response = await fetch(`${dms}/api/3/action/package_search`)
|
||||
const datasets = await response.json()
|
||||
const datasetsWithDetails = await Promise.all(datasets.result.results.map(async (dataset) => {
|
||||
const response = await fetch(`${dms}/api/3/action/package_show?id=` + dataset.name)
|
||||
const json = await response.json()
|
||||
return json.result
|
||||
}))
|
||||
|
||||
return {
|
||||
@@ -79,7 +79,7 @@ export function Index({ datasets }) {
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{datasets.map((dataset) => (
|
||||
<tr key={dataset.name}>
|
||||
<tr>
|
||||
<td className="px-3 py-4 text-sm text-gray-500">
|
||||
{dataset.title}
|
||||
</td>
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,11 +1,46 @@
|
||||
[
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/nba-forecasts",
|
||||
"name": "nba-forecasts",
|
||||
"displayName": "nba-<span class=\"lastword\">forecasts</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-08T22:33:43.000Z",
|
||||
"title": "2022-23 NBA Predictions",
|
||||
"url": "https://projects.fivethirtyeight.com/2023-nba-predictions/"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"https://projects.fivethirtyeight.com/nba-model/nba_elo.csv",
|
||||
"https://projects.fivethirtyeight.com/nba-model/nba_elo_latest.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/soccer-spi",
|
||||
"name": "soccer-spi",
|
||||
"displayName": "soccer-<span class=\"lastword\">spi</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-08T22:17:18.000Z",
|
||||
"title": "Club Soccer Predictions",
|
||||
"url": "https://projects.fivethirtyeight.com/soccer-predictions/"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"https://projects.fivethirtyeight.com/soccer-api/club/spi_matches.csv",
|
||||
"https://projects.fivethirtyeight.com/soccer-api/club/spi_matches_latest.csv",
|
||||
"https://projects.fivethirtyeight.com/soccer-api/club/spi_global_rankings.csv",
|
||||
"https://projects.fivethirtyeight.com/soccer-api/international/spi_matches_intl.csv",
|
||||
"https://projects.fivethirtyeight.com/soccer-api/international/spi_global_rankings_intl.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/polls",
|
||||
"name": "polls",
|
||||
"displayName": "<span class=\"lastword\">polls</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-11T14:35:40.000Z",
|
||||
"date": "2023-05-08T20:36:59.000Z",
|
||||
"title": "Latest Polls",
|
||||
"url": "https://projects.fivethirtyeight.com/polls/"
|
||||
}
|
||||
@@ -28,45 +63,13 @@
|
||||
"https://projects.fivethirtyeight.com/2020-general-data/presidential_poll_averages_2020.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/congress-generic-ballot",
|
||||
"name": "congress-generic-ballot",
|
||||
"displayName": "congress-generic-<span class=\"lastword\">ballot</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-11T14:35:40.000Z",
|
||||
"title": "Do Voters Want Democrats Or Republicans In Congress?",
|
||||
"url": "https://projects.fivethirtyeight.com/congress-generic-ballot-polls/"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"https://projects.fivethirtyeight.com/generic-ballot-data/generic_polllist.csv",
|
||||
"https://projects.fivethirtyeight.com/polls/data/generic_ballot_averages.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/nba-forecasts",
|
||||
"name": "nba-forecasts",
|
||||
"displayName": "nba-<span class=\"lastword\">forecasts</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-11T11:15:46.000Z",
|
||||
"title": "2022-23 NBA Predictions",
|
||||
"url": "https://projects.fivethirtyeight.com/2023-nba-predictions/"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"https://projects.fivethirtyeight.com/nba-model/nba_elo.csv",
|
||||
"https://projects.fivethirtyeight.com/nba-model/nba_elo_latest.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/nba-raptor",
|
||||
"name": "nba-raptor",
|
||||
"displayName": "nba-<span class=\"lastword\">raptor</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-11T11:13:20.000Z",
|
||||
"date": "2023-05-08T11:15:48.000Z",
|
||||
"title": "The Best NBA Players, According To RAPTOR",
|
||||
"rowspan": 3,
|
||||
"url": "https://projects.fivethirtyeight.com/nba-player-ratings/"
|
||||
@@ -89,32 +92,13 @@
|
||||
"https://projects.fivethirtyeight.com/nba-model/2023/latest_RAPTOR_by_player.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/soccer-spi",
|
||||
"name": "soccer-spi",
|
||||
"displayName": "soccer-<span class=\"lastword\">spi</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-11T05:25:51.000Z",
|
||||
"title": "Club Soccer Predictions",
|
||||
"url": "https://projects.fivethirtyeight.com/soccer-predictions/"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"https://projects.fivethirtyeight.com/soccer-api/club/spi_matches.csv",
|
||||
"https://projects.fivethirtyeight.com/soccer-api/club/spi_matches_latest.csv",
|
||||
"https://projects.fivethirtyeight.com/soccer-api/club/spi_global_rankings.csv",
|
||||
"https://projects.fivethirtyeight.com/soccer-api/international/spi_matches_intl.csv",
|
||||
"https://projects.fivethirtyeight.com/soccer-api/international/spi_global_rankings_intl.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/nhl-forecasts",
|
||||
"name": "nhl-forecasts",
|
||||
"displayName": "nhl-<span class=\"lastword\">forecasts</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-11T04:53:22.000Z",
|
||||
"date": "2023-05-08T04:18:20.000Z",
|
||||
"title": "2022-23 NHL Predictions",
|
||||
"url": "https://projects.fivethirtyeight.com/2023-nhl-predictions/"
|
||||
}
|
||||
@@ -130,7 +114,7 @@
|
||||
"displayName": "mlb-<span class=\"lastword\">elo</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-11T02:35:49.000Z",
|
||||
"date": "2023-05-08T02:25:55.000Z",
|
||||
"title": "2023 MLB Predictions",
|
||||
"url": "https://projects.fivethirtyeight.com/2023-mlb-predictions/"
|
||||
}
|
||||
@@ -140,6 +124,22 @@
|
||||
"https://projects.fivethirtyeight.com/mlb-api/mlb_elo_latest.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/congress-generic-ballot",
|
||||
"name": "congress-generic-ballot",
|
||||
"displayName": "congress-generic-<span class=\"lastword\">ballot</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2023-05-02T13:48:41.000Z",
|
||||
"title": "Do Voters Want Democrats Or Republicans In Congress?",
|
||||
"url": "https://projects.fivethirtyeight.com/congress-generic-ballot-polls/"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"https://projects.fivethirtyeight.com/generic-ballot-data/generic_polllist.csv",
|
||||
"https://projects.fivethirtyeight.com/polls/data/generic_ballot_averages.csv"
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/congress-demographics",
|
||||
"name": "congress-demographics",
|
||||
@@ -1169,6 +1169,18 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/undefeated-boxers",
|
||||
"name": "undefeated-boxers",
|
||||
"displayName": "undefeated-<span class=\"lastword\">boxers</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2017-08-18T18:47:32.000Z",
|
||||
"title": "Mayweather Is Defined By The Zero Next To His Name",
|
||||
"url": "https://fivethirtyeight.com/features/mayweather-is-defined-by-the-zero-next-to-his-name/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/chess-transfers",
|
||||
"name": "chess-transfers",
|
||||
@@ -2127,18 +2139,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/undefeated-boxers",
|
||||
"name": "undefeated-boxers",
|
||||
"displayName": "undefeated-<span class=\"lastword\">boxers</span>",
|
||||
"articles": [
|
||||
{
|
||||
"date": "2017-08-18T18:47:32.000Z",
|
||||
"title": "Mayweather Is Defined By The Zero Next To His Name",
|
||||
"url": "https://fivethirtyeight.com/features/mayweather-is-defined-by-the-zero-next-to-his-name/"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fivethirtyeight/data/tree/master/march-madness-predictions",
|
||||
"name": "march-madness-predictions",
|
||||
@@ -9,9 +9,7 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@heroicons/react": "^2.0.18",
|
||||
"@portaljs/components": "^0.1.7",
|
||||
"@portaljs/components": "^0.1.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@types/node": "20.1.1",
|
||||
"@types/react": "18.2.6",
|
||||
@@ -19,7 +17,6 @@
|
||||
"autoprefixer": "10.4.14",
|
||||
"eslint": "8.40.0",
|
||||
"eslint-config-next": "13.4.1",
|
||||
"flexsearch": "^0.7.31",
|
||||
"next": "13.4.1",
|
||||
"next-mdx-remote": "^4.4.1",
|
||||
"next-seo": "^6.0.0",
|
||||
@@ -28,15 +25,12 @@
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-markdown": "^8.0.7",
|
||||
"remark": "^14.0.3",
|
||||
"remark-code-frontmatter": "^1.0.0",
|
||||
"remark-excerpt": "^1.0.0-beta.1",
|
||||
"remark-extract-frontmatter": "^3.2.0",
|
||||
"remark-frontmatter": "^4.0.1",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"tailwindcss": "3.3.2",
|
||||
"timeago.js": "^4.0.2",
|
||||
"to-vfile": "^7.2.4",
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
}
|
||||
38
examples/fiverthirtyeight-example/pages/_document.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Html, Head, Main, NextScript } from 'next/document';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head>
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href="https://projects.fivethirtyeight.com/shared/favicon.ico"
|
||||
/>
|
||||
</Head>
|
||||
<body>
|
||||
<header className="max-w-5xl mx-auto mt-8 w-full">
|
||||
<div className="border-b-2 pb-2.5 mx-2 border-zinc-800">
|
||||
<h1>
|
||||
<span className="sr-only">FiveThirtyEight</span>
|
||||
<a
|
||||
className="flex gap-x-2 items-center"
|
||||
href="http://fivethirtyeight.com"
|
||||
>
|
||||
<img
|
||||
width="197"
|
||||
height="25"
|
||||
alt="FiveThirtyEight"
|
||||
src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MjEgNTMuNzYiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojMDEwMTAxO308L3N0eWxlPjwvZGVmcz48dGl0bGU+QXJ0Ym9hcmQgOTU8L3RpdGxlPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTAgMGgyNXY4SDl2MTBoMTV2OEg5djE3SDBWMHpNMzEgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdIMzF6bTUtMzZoOHY4aC04ek0xNzkgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdoLTE3em01LTM2aDh2OGgtOHpNMzE2IDM2aDVWMThoLTV2LThoMTN2MjZoNHY3aC0xN3ptNS0zNmg4djhoLTh6TTU0IDI3VjEwaDh2MTVsNCA5Ljk4aDFMNzEgMjVWMTBoOHYxN2wtNyAxNkg2MWwtNy0xNnpNMTExIDQzSDk3LjQyQzg5LjIzIDQzIDg1IDM5LjE5IDg1IDMxLjE3VjIyYzAtNy41NyA0LjMtMTMgMTMtMTMgOS4zMyAwIDEzIDUuMDcgMTMgMTR2N0g5NHYxLjc0YzAgMi42MiAxIDQuMjYgMy40MiA0LjI2SDExMXpNOTQgMjNoOHYtMS41NWMwLTIuNjItMS4wNi01LjQ1LTQuMTMtNS40NS0yLjc5IDAtMy44NyAyLjItMy44NyA1LjQ1ek0xMjUgOGgtMTBWMGgyOXY4aC0xMHYzNWgtOVY4ek0yMDIgNDNWMTBoOHY0YzEuMTQtMi40NSAzLjc1LTQgNy4yMi00SDIyMHY4aC02Yy0yLjg0IDAtNCAuOTQtNCAzLjlWNDN6TTI0NSA0M2gtNC44NEMyMzMuMDUgNDMgMjMwIDM5LjMxIDIzMCAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0gyNDV6TTQyMSA0M2gtNC44NEM0MDkuMDUgNDMgNDA2IDM5LjMxIDQwNiAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0g0MjF6TTI1NC4yNiA1My43Nmw0LjYxLTkuNUwyNTEgMjdWMTBoOHYxNWw0IDEwaDFsNC0xMFYxMGg4djE3bC0xMi4zIDI2Ljc2aC05LjQ0ek0yODQgMGgyNXY4aC0xNnY5aDE1djhoLTE1djEwaDE2djhoLTI1VjB6TTMzNyA0OHYtMmgxNi4xYzIgMCAyLjktLjE4IDIuOS0xLjI3di0uMzRjMC0xLjA4LS45MS0xLjM5LTIuOS0xLjM5SDM0MHYtNWw1LTVjLTUuMjktMS40OC04LTUuNDMtOC0xMXYtMWMwLTcuNTYgNC40NC0xMiAxNC0xMmEyMS45MyAyMS45MyAwIDAgMSA1Ljk1IDFMMzYxIDRsNSAzLTQgNmMxLjM3IDEuOTMgMyA0LjkzIDMgOHYxYzAgNy0zLjMgMTAuNjYtMTIgMTFsLTMgNGg2YzUuOTIgMCA5IDIuNjIgOSA3LjY4di4xMWMwIDUuMDYtMi43MSA4LjIxLTguNjIgOC4yMWgtMTNjLTQuMjkgMC02LjM4LTEuODQtNi4zOC01em0xOS0yNXYtM2MwLTMuMy0xLjMzLTQtNS00cy01IC43LTUgNHYzYzAgMy4zIDEuMzkgNCA1IDRzNS0uNyA1LTR6TTM4MCA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuNC00IDctNCA2LjI2IDAgOSAzLjA4IDkgMTAuNzZWNDNoLThWMjJjMC0zLjEzLTEuMDctNS00LTVzLTQgMS44Ny00IDV6TTE1NyA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuOTEtNCA3LjQ5LTQgNi4yNiAwIDguNTEgMy4xMyA4LjUxIDEwLjgxVjQzaC04VjIxYzAtMy4xMy0xLjA3LTQuNDQtNC00LjQ0cy00IDIuMjYtNCA1LjM5eiIvPjwvc3ZnPg=="
|
||||
/>{' '}
|
||||
by PortalJS
|
||||
</a>
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { NextSeo } from 'next-seo';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import getConfig from 'next/config';
|
||||
import { getProjectReadme, GithubProject } from '@/lib/octokit';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import extract from 'remark-extract-frontmatter';
|
||||
import { Dataset } from '..';
|
||||
import { GetStaticProps } from 'next';
|
||||
import { Table } from '@portaljs/components';
|
||||
import Breadcrumbs from '@/components/Breadcrumbs';
|
||||
import { ReactMarkdown } from 'react-markdown/lib/react-markdown';
|
||||
import remarkFrontmatter from 'remark-frontmatter';
|
||||
|
||||
export default function DatasetPage({
|
||||
dataset,
|
||||
}: {
|
||||
dataset: Dataset & {
|
||||
readme: string | null;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<NextSeo title={`${dataset.name} page`} />
|
||||
<main className="max-w-5xl px-2 prose mx-auto my-8 prose-thead:border-b-4 prose-table:max-w-5xl prose-table:overflow-scroll prose-thead:overflow-scroll prose-tbody:overflow-scroll prose-thead:pb-2 prose-thead:border-zinc-900 prose-th:uppercase prose-th:text-left prose-th:font-light prose-th:text-xs">
|
||||
<Breadcrumbs links={[{ title: dataset.name, href: '' }]} />
|
||||
<h1 className="uppercase mb-0 mt-16">{dataset.name}</h1>
|
||||
<p className="mb-8">
|
||||
<span className="font-semibold">Repository:</span>{' '}
|
||||
<a target="_blank" href={dataset.url}>
|
||||
{dataset.url}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 className="mb-0 mt-10">FILES</h2>
|
||||
<div className="inline-block min-w-full py-2 align-middle">
|
||||
<table className="min-w-full divide-y divide-gray-300">
|
||||
<thead className="border-b-4 pb-2 border-zinc-900">
|
||||
<tr>
|
||||
<th
|
||||
className="uppercase text-left font-light text-xs pb-3"
|
||||
scope="col"
|
||||
>
|
||||
Name
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200">
|
||||
{dataset.files?.map((file) => (
|
||||
<tr key={file}>
|
||||
<td className="whitespace-nowrap text-left py-4 text-sm text-gray-500">
|
||||
<a href={file}>{file.split('/').slice(-1)}</a>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{dataset.files && dataset.files.length > 0 && (
|
||||
<>
|
||||
<h2 className="mb-0 mt-10">DATA PREVIEWS</h2>
|
||||
{dataset.files?.map((file) => (
|
||||
<div key={file} className="preview-table my-8">
|
||||
<h3>{file.split('/').slice(-1)}</h3>
|
||||
<Table url={file} />
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{dataset.readme && (
|
||||
<>
|
||||
<h2 className="uppercase font-black">Readme</h2>
|
||||
{dataset.readme && (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[
|
||||
remarkFrontmatter,
|
||||
remarkGfm,
|
||||
[extract, { remove: true }],
|
||||
]}
|
||||
>
|
||||
{dataset.readme}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const datasetsFile = path.join(process.cwd(), 'datasets.json');
|
||||
const datasets = await fs.readFile(datasetsFile, 'utf8');
|
||||
|
||||
return {
|
||||
paths: JSON.parse(datasets).map((dataset: Dataset) => {
|
||||
return {
|
||||
params: { datasetName: dataset.name },
|
||||
};
|
||||
}),
|
||||
fallback: false, // can also be true or 'blocking'
|
||||
};
|
||||
}
|
||||
|
||||
export const getStaticProps: GetStaticProps = async ({ params }) => {
|
||||
const datasetsFile = path.join(process.cwd(), 'datasets.json');
|
||||
const datasetsString = await fs.readFile(datasetsFile, 'utf8');
|
||||
const datasets: Dataset[] = JSON.parse(datasetsString);
|
||||
const dataset: Dataset | undefined = datasets.find(
|
||||
(_dataset) => _dataset.name === params?.datasetName
|
||||
);
|
||||
const github_pat = getConfig().serverRuntimeConfig.github_pat;
|
||||
const readmes = await Promise.all(['/README.md', '/readme.md', '/Readme.md'].map(async (readme) => await getProjectReadme(
|
||||
'fivethirtyeight',
|
||||
'data',
|
||||
'master',
|
||||
dataset?.name + readme,
|
||||
github_pat
|
||||
)));
|
||||
const readme = readmes.find(item => item !== null)
|
||||
if (!readme) console.log('Readme not found for ' + dataset?.name)
|
||||
return {
|
||||
props: {
|
||||
dataset: {
|
||||
...dataset,
|
||||
readme,
|
||||
files: dataset && dataset.files ? dataset.files : null,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -3,8 +3,6 @@ import { Inter } from 'next/font/google';
|
||||
import { format } from 'timeago.js';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
import { NextSeo } from 'next-seo';
|
||||
import Layout from '@/components/Layout';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
@@ -22,42 +20,49 @@ export interface Dataset {
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
// Request a weekday along with a long date
|
||||
const options = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
} as const;
|
||||
|
||||
export function MobileItem({ dataset }: { dataset: Dataset }) {
|
||||
return (
|
||||
<div className="flex gap-x-2 pb-2 py-4 items-center justify-between border-b border-zinc-600">
|
||||
<div className="flex flex-col">
|
||||
<span className="font-mono font-light">{dataset.name}</span>
|
||||
<span className="font-light">{dataset.name}</span>
|
||||
{dataset.articles.map((article) => (
|
||||
<div key={article.title} className="py-1 flex flex-col">
|
||||
<span className="font-bold hover:underline">{article.title}</span>
|
||||
<span className="font-light text-base">
|
||||
{format(article.date).includes('years')
|
||||
? new Date(article.date).toLocaleString('en-US', options)
|
||||
: format(article.date)}
|
||||
{format(article.date)}
|
||||
</span>{' '}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col justify-start">
|
||||
<a
|
||||
className="ml-2 border border-zinc-900 font-light px-4 py-1 text-sm transition hover:bg-zinc-900 hover:text-white"
|
||||
className="border border-zinc-900 font-light px-4 py-1 text-sm transition hover:bg-zinc-900 hover:text-white"
|
||||
href={dataset.url}
|
||||
target="_blank"
|
||||
>
|
||||
info
|
||||
</a>
|
||||
<a
|
||||
className="ml-2 border border-[#3c3c3c] px-[25px] py-2.5 text-sm transition bg-[#3c3c3c] text-white hover:bg-zinc-900"
|
||||
className="ml-2 border border-zinc-900 font-light px-4 py-1 text-sm transition hover:bg-zinc-900 hover:text-white"
|
||||
href={`/datasets/${dataset.name}`}
|
||||
>
|
||||
explore
|
||||
</a>
|
||||
{/*
|
||||
<button>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="w-12 h-12 text-blue-400 hover:text-blue-300 transition mt-1"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-.53 14.03a.75.75 0 001.06 0l3-3a.75.75 0 10-1.06-1.06l-1.72 1.72V8.25a.75.75 0 00-1.5 0v5.69l-1.72-1.72a.75.75 0 00-1.06 1.06l3 3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button> */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -73,42 +78,53 @@ export function DesktopItem({ dataset }: { dataset: Dataset }) {
|
||||
index === dataset.articles.length - 1 ? 'border-b' : ''
|
||||
} border-zinc-400`}
|
||||
>
|
||||
<td className="py-8 font-light font-mono text-[13px] text-zinc-700">
|
||||
{index === 0 ? dataset.name : ''}
|
||||
</td>
|
||||
<td className="py-8 font-light">{index === 0 ? dataset.name : ''}</td>
|
||||
<td>
|
||||
<a
|
||||
className="py-8 font-bold hover:underline pr-2"
|
||||
href={article.url}
|
||||
>
|
||||
<a className="py-8 font-bold hover:underline" href={article.url}>
|
||||
{article.title}
|
||||
</a>
|
||||
</td>
|
||||
<td className="py-8 font-light text-[14px] min-w-[138px] font-mono text-[#999]">
|
||||
{format(article.date).includes('years')
|
||||
? new Date(article.date).toLocaleString('en-US', options)
|
||||
: format(article.date)}
|
||||
<td className="py-8 font-light text-base min-w-[120px]">
|
||||
{format(article.date)}
|
||||
</td>
|
||||
<td>
|
||||
<td className="py-8">
|
||||
{index === 0 && (
|
||||
<a
|
||||
className="ml-2 border border-[#3c3c3c] px-[25px] py-2.5 text-sm transition bg-[#3c3c3c] text-white hover:bg-zinc-900"
|
||||
href={`/datasets/${dataset.name}`}
|
||||
>
|
||||
explore
|
||||
</a>
|
||||
<a
|
||||
className="border border-zinc-900 font-light px-[25px] py-2.5 text-sm transition hover:bg-zinc-900 hover:text-white"
|
||||
href={dataset.url}
|
||||
target="_blank"
|
||||
>
|
||||
info
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-8">
|
||||
{index === 0 && (
|
||||
<a
|
||||
className="ml-2 border border-zinc-900 font-light px-[25px] py-2.5 text-sm transition hover:bg-zinc-900 hover:text-white"
|
||||
href={dataset.url}
|
||||
href={`/datasets/${dataset.name}`}
|
||||
>
|
||||
info
|
||||
explore
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
{/*
|
||||
<td>
|
||||
<button>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="w-12 h-12 text-blue-400 hover:text-blue-300 transition mt-1"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M12 2.25c-5.385 0-9.75 4.365-9.75 9.75s4.365 9.75 9.75 9.75 9.75-4.365 9.75-9.75S17.385 2.25 12 2.25zm-.53 14.03a.75.75 0 001.06 0l3-3a.75.75 0 10-1.06-1.06l-1.72 1.72V8.25a.75.75 0 00-1.5 0v5.69l-1.72-1.72a.75.75 0 00-1.06 1.06l3 3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</td>*/}
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
@@ -127,8 +143,6 @@ export async function getStaticProps() {
|
||||
export default function Home({ datasets }: { datasets: Dataset[] }) {
|
||||
return (
|
||||
<>
|
||||
<NextSeo title="FiveThirtyEight tribute by PortalJS" />
|
||||
<Layout>
|
||||
<main
|
||||
className={`flex min-h-screen flex-col items-center max-w-5xl mx-auto pt-20 px-2.5 ${inter.className}`}
|
||||
>
|
||||
@@ -136,7 +150,7 @@ export default function Home({ datasets }: { datasets: Dataset[] }) {
|
||||
<h1 className="text-[40px] font-bold text-zinc-800 text-center">
|
||||
Our Data
|
||||
</h1>
|
||||
<p className="max-w-[600px] text-[17px] text-center text-[#6d6f71]">
|
||||
<p className="max-w-2xl text-lg text-center text-zinc-700">
|
||||
We’re sharing the data and code behind some of our articles and
|
||||
graphics. We hope you’ll use it to check our work and to create
|
||||
stories and visualizations of your own.
|
||||
@@ -150,13 +164,13 @@ export default function Home({ datasets }: { datasets: Dataset[] }) {
|
||||
<table className="w-full mt-10 mb-4 hidden md:table">
|
||||
<thead className="border-b-4 pb-2 border-zinc-900">
|
||||
<tr>
|
||||
<th className="uppercase text-left font-normal text-xs pb-3">
|
||||
<th className="uppercase text-left font-light text-xs pb-3">
|
||||
data set
|
||||
</th>
|
||||
<th className="uppercase text-left font-normal text-xs pb-3">
|
||||
<th className="uppercase text-left font-light text-xs pb-3">
|
||||
related content
|
||||
</th>
|
||||
<th className="uppercase text-left font-normal text-xs pb-3">
|
||||
<th className="uppercase text-left font-light text-xs pb-3">
|
||||
last updated
|
||||
</th>
|
||||
</tr>
|
||||
@@ -192,7 +206,6 @@ export default function Home({ datasets }: { datasets: Dataset[] }) {
|
||||
.
|
||||
</p>
|
||||
</main>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 629 B After Width: | Height: | Size: 629 B |
@@ -3,5 +3,6 @@
|
||||
@tailwind utilities;
|
||||
|
||||
.preview-table > div {
|
||||
overflow-x: scroll;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
35
examples/fivethirtyeight/.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,44 +0,0 @@
|
||||
This is a replica of the awesome data.fivethirtyeight.com using PortalJS.
|
||||
|
||||
You might be asking why we did that, there are three main reasons:
|
||||
|
||||
- The website has a great UI, with multiple datasets being displayed elegantly and with simplicity.
|
||||
- PortalJS allows us to add more functionality to it e.g dataset previews and search functionality.
|
||||
- The project follows our same principles of open sourcing and free data, with every dataset being publicly available on Github.
|
||||
|
||||
## 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,99 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { XMarkIcon } from '@heroicons/react/20/solid';
|
||||
import { Transition } from '@headlessui/react';
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
const [isShowing, setShow] = useState(true);
|
||||
return (
|
||||
<>
|
||||
<Transition
|
||||
show={isShowing}
|
||||
enter="transition-opacity duration-75"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity duration-150"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="flex items-center gap-x-6 bg-[#3c3c3c] px-6 py-2.5 sm:px-3.5 sm:before:flex-1">
|
||||
<p className="text-sm leading-6 text-white">
|
||||
This is a replica to the awesome{' '}
|
||||
<a
|
||||
className="hover:underline font-bold"
|
||||
href="https://data.fivethirtyeight.com"
|
||||
>
|
||||
data.fivethirtyeight.com
|
||||
</a>{' '}
|
||||
website.{' '}
|
||||
<a
|
||||
className="hover:underline font-bold"
|
||||
href="https://github.com/datopian/portaljs/tree/main/examples/fivethirtyeight"
|
||||
>
|
||||
Read more here
|
||||
</a>{' '}
|
||||
</p>
|
||||
<div className="flex flex-1 justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShow(false)}
|
||||
className="-m-3 p-3 focus-visible:outline-offset-[-4px]"
|
||||
>
|
||||
<span className="sr-only">Dismiss</span>
|
||||
<XMarkIcon className="h-5 w-5 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
<header className="max-w-5xl mx-auto mt-8 w-full">
|
||||
<div className="border-b-2 pb-2.5 mx-2 border-zinc-800 flex justify-between">
|
||||
<h1 className="flex gap-x-1 items-end">
|
||||
<span className="sr-only">FiveThirtyEight</span>
|
||||
<img
|
||||
width="197"
|
||||
height="25"
|
||||
alt="FiveThirtyEight"
|
||||
src="data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MjEgNTMuNzYiPjxkZWZzPjxzdHlsZT4uY2xzLTF7ZmlsbDojMDEwMTAxO308L3N0eWxlPjwvZGVmcz48dGl0bGU+QXJ0Ym9hcmQgOTU8L3RpdGxlPjxwYXRoIGNsYXNzPSJjbHMtMSIgZD0iTTAgMGgyNXY4SDl2MTBoMTV2OEg5djE3SDBWMHpNMzEgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdIMzF6bTUtMzZoOHY4aC04ek0xNzkgMzZoNVYxOGgtNXYtOGgxM3YyNmg0djdoLTE3em01LTM2aDh2OGgtOHpNMzE2IDM2aDVWMThoLTV2LThoMTN2MjZoNHY3aC0xN3ptNS0zNmg4djhoLTh6TTU0IDI3VjEwaDh2MTVsNCA5Ljk4aDFMNzEgMjVWMTBoOHYxN2wtNyAxNkg2MWwtNy0xNnpNMTExIDQzSDk3LjQyQzg5LjIzIDQzIDg1IDM5LjE5IDg1IDMxLjE3VjIyYzAtNy41NyA0LjMtMTMgMTMtMTMgOS4zMyAwIDEzIDUuMDcgMTMgMTR2N0g5NHYxLjc0YzAgMi42MiAxIDQuMjYgMy40MiA0LjI2SDExMXpNOTQgMjNoOHYtMS41NWMwLTIuNjItMS4wNi01LjQ1LTQuMTMtNS40NS0yLjc5IDAtMy44NyAyLjItMy44NyA1LjQ1ek0xMjUgOGgtMTBWMGgyOXY4aC0xMHYzNWgtOVY4ek0yMDIgNDNWMTBoOHY0YzEuMTQtMi40NSAzLjc1LTQgNy4yMi00SDIyMHY4aC02Yy0yLjg0IDAtNCAuOTQtNCAzLjlWNDN6TTI0NSA0M2gtNC44NEMyMzMuMDUgNDMgMjMwIDM5LjMxIDIzMCAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0gyNDV6TTQyMSA0M2gtNC44NEM0MDkuMDUgNDMgNDA2IDM5LjMxIDQwNiAzMS44NVYxOGgtNnYtOGg2VjNoOHY3aDd2OGgtN2wtLjA3IDEzLjkzYzAgMi4yMi45MyA0LjA3IDMuNjYgNC4wN0g0MjF6TTI1NC4yNiA1My43Nmw0LjYxLTkuNUwyNTEgMjdWMTBoOHYxNWw0IDEwaDFsNC0xMFYxMGg4djE3bC0xMi4zIDI2Ljc2aC05LjQ0ek0yODQgMGgyNXY4aC0xNnY5aDE1djhoLTE1djEwaDE2djhoLTI1VjB6TTMzNyA0OHYtMmgxNi4xYzIgMCAyLjktLjE4IDIuOS0xLjI3di0uMzRjMC0xLjA4LS45MS0xLjM5LTIuOS0xLjM5SDM0MHYtNWw1LTVjLTUuMjktMS40OC04LTUuNDMtOC0xMXYtMWMwLTcuNTYgNC40NC0xMiAxNC0xMmEyMS45MyAyMS45MyAwIDAgMSA1Ljk1IDFMMzYxIDRsNSAzLTQgNmMxLjM3IDEuOTMgMyA0LjkzIDMgOHYxYzAgNy0zLjMgMTAuNjYtMTIgMTFsLTMgNGg2YzUuOTIgMCA5IDIuNjIgOSA3LjY4di4xMWMwIDUuMDYtMi43MSA4LjIxLTguNjIgOC4yMWgtMTNjLTQuMjkgMC02LjM4LTEuODQtNi4zOC01em0xOS0yNXYtM2MwLTMuMy0xLjMzLTQtNS00cy01IC43LTUgNHYzYzAgMy4zIDEuMzkgNCA1IDRzNS0uNyA1LTR6TTM4MCA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuNC00IDctNCA2LjI2IDAgOSAzLjA4IDkgMTAuNzZWNDNoLThWMjJjMC0zLjEzLTEuMDctNS00LTVzLTQgMS44Ny00IDV6TTE1NyA0M2gtOFYwaDh2MTRjMS4xNC0yLjY3IDMuOTEtNCA3LjQ5LTQgNi4yNiAwIDguNTEgMy4xMyA4LjUxIDEwLjgxVjQzaC04VjIxYzAtMy4xMy0xLjA3LTQuNDQtNC00LjQ0cy00IDIuMjYtNCA1LjM5eiIvPjwvc3ZnPg=="
|
||||
/>{' '}
|
||||
<span className="-mb-0.5 text-[#3c3c3c]">replica</span>
|
||||
</h1>
|
||||
<div className="md:flex items-center gap-x-3 text-[#3c3c3c] -mb-1 hidden">
|
||||
<a
|
||||
className="hover:opacity-75 transition"
|
||||
href="https://portaljs.org"
|
||||
>
|
||||
Built with 🌀PortalJS
|
||||
</a>
|
||||
<hr className="h-[80%] border border-[#3c3c3c] opacity-75 my-2"></hr>
|
||||
<a
|
||||
className="hover:opacity-75 transition"
|
||||
href="https://github.com/datopian/portaljs/tree/main/examples/fivethirtyeight"
|
||||
>
|
||||
Github
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-2 py-1.5 text-[14px] text-[#3c3c3c] md:hidden">
|
||||
<ul className="flex gap-x-4">
|
||||
<li>
|
||||
<a
|
||||
className="hover:opacity-75 transition"
|
||||
href="https://portaljs.org"
|
||||
>
|
||||
PortalJS
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
className="hover:opacity-75 transition"
|
||||
href="https://github.com/datopian/portaljs/tree/main/examples/fivethirtyeight"
|
||||
>
|
||||
View on Github
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
}
|
||||