[example/538] - individual pages (#865)
* [example/538] - individual pages * [examples/538][sm] - force inclusion of classes * [examples/538] - changes requested by demenech
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import '@/styles/globals.css'
|
||||
import '@portaljs/components/styles.css'
|
||||
|
||||
import type { AppProps } from 'next/app'
|
||||
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
|
||||
@@ -11,6 +11,25 @@ export default function Document() {
|
||||
/>
|
||||
</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>
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1,33 +1,36 @@
|
||||
import Image from 'next/image';
|
||||
import { Inter } from 'next/font/google';
|
||||
import { format } from 'timeago.js'
|
||||
import { format } from 'timeago.js';
|
||||
import { promises as fs } from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
interface Article {
|
||||
export interface Article {
|
||||
date: string;
|
||||
title: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface Dataset {
|
||||
export interface Dataset {
|
||||
url: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
articles: Article[];
|
||||
files?: string[];
|
||||
}
|
||||
|
||||
export function MobileItem({dataset} : { dataset: Dataset}) {
|
||||
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-light">{dataset.name}</span>
|
||||
{dataset.articles.map((article) => (
|
||||
<div key={article.title} className='py-1 flex flex-col'>
|
||||
<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)}</span>{' '}
|
||||
<span className="font-light text-base">
|
||||
{format(article.date)}
|
||||
</span>{' '}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -39,6 +42,12 @@ export function MobileItem({dataset} : { dataset: Dataset}) {
|
||||
>
|
||||
info
|
||||
</a>
|
||||
<a
|
||||
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
|
||||
@@ -59,18 +68,25 @@ export function MobileItem({dataset} : { dataset: Dataset}) {
|
||||
);
|
||||
}
|
||||
|
||||
export function DesktopItem({dataset} : { dataset: Dataset}) {
|
||||
export function DesktopItem({ dataset }: { dataset: Dataset }) {
|
||||
return (
|
||||
<>
|
||||
{dataset.articles.map((article, index) => (
|
||||
<tr key={article.url} className={`${index === (dataset.articles.length - 1) ? 'border-b' : ''} border-zinc-400`}>
|
||||
<tr
|
||||
key={article.url}
|
||||
className={`${
|
||||
index === dataset.articles.length - 1 ? 'border-b' : ''
|
||||
} border-zinc-400`}
|
||||
>
|
||||
<td className="py-8 font-light">{index === 0 ? dataset.name : ''}</td>
|
||||
<td>
|
||||
<a className="py-8 font-bold hover:underline" href={article.url}>
|
||||
{article.title}
|
||||
</a>
|
||||
</td>
|
||||
<td className="py-8 font-light text-base min-w-[120px]">{format(article.date)}</td>
|
||||
<td className="py-8 font-light text-base min-w-[120px]">
|
||||
{format(article.date)}
|
||||
</td>
|
||||
<td className="py-8">
|
||||
{index === 0 && (
|
||||
<a
|
||||
@@ -82,6 +98,16 @@ export function DesktopItem({dataset} : { dataset: Dataset}) {
|
||||
</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={`/datasets/${dataset.name}`}
|
||||
>
|
||||
explore
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
{/*
|
||||
<td>
|
||||
<button>
|
||||
@@ -106,35 +132,17 @@ export function DesktopItem({dataset} : { dataset: Dataset}) {
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
const jsonDirectory = path.join(
|
||||
process.cwd(),
|
||||
'/datasets.json'
|
||||
);
|
||||
const jsonDirectory = path.join(process.cwd(), '/datasets.json');
|
||||
const datasetString = await fs.readFile(jsonDirectory, 'utf8');
|
||||
const datasets = JSON.parse(datasetString)
|
||||
const datasets = JSON.parse(datasetString);
|
||||
return {
|
||||
props: { datasets },
|
||||
};
|
||||
}
|
||||
|
||||
export default function Home( { datasets }: { datasets: Dataset[] }) {
|
||||
export default function Home({ datasets }: { datasets: Dataset[] }) {
|
||||
return (
|
||||
<>
|
||||
<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
|
||||
className={`flex min-h-screen flex-col items-center max-w-5xl mx-auto pt-20 px-2.5 ${inter.className}`}
|
||||
>
|
||||
@@ -148,7 +156,11 @@ export default function Home( { datasets }: { datasets: Dataset[] }) {
|
||||
stories and visualizations of your own.
|
||||
</p>
|
||||
</div>
|
||||
<article className="w-full px-2 md:hidden py-4">{datasets.map(dataset => <MobileItem key={dataset.name} dataset={dataset} />)}</article>
|
||||
<article className="w-full px-2 md:hidden py-4">
|
||||
{datasets.map((dataset) => (
|
||||
<MobileItem key={dataset.name} dataset={dataset} />
|
||||
))}
|
||||
</article>
|
||||
<table className="w-full mt-10 mb-4 hidden md:table">
|
||||
<thead className="border-b-4 pb-2 border-zinc-900">
|
||||
<tr>
|
||||
@@ -163,7 +175,11 @@ export default function Home( { datasets }: { datasets: Dataset[] }) {
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>{datasets.map(dataset => <DesktopItem key={dataset.name} dataset={dataset} />)}</tbody>
|
||||
<tbody>
|
||||
{datasets.map((dataset) => (
|
||||
<DesktopItem key={dataset.name} dataset={dataset} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<p className="text-[13px] py-8">
|
||||
Unless otherwise noted, our data sets are available under the{' '}
|
||||
|
||||
Reference in New Issue
Block a user