Implement dataset page v0.1 + Home improvements (#892)

* [examples/openspending][xl]: implement dataset page v0.1, add pagination to the datasets grid

* [examples/openspending][m] - fix build + add tests

---------

Co-authored-by: Luccas Mateus de Medeiros Gomes <luccasmmg@gmail.com>
This commit is contained in:
João Demenech
2023-05-18 21:19:01 -03:00
committed by GitHub
parent 2115a3fdb3
commit adb6d1bb0e
19 changed files with 646 additions and 344 deletions

View File

@@ -1,126 +0,0 @@
import { NextSeo } from 'next-seo';
import { promises as fs } from 'fs';
import path from 'path';
import getConfig from 'next/config';
import { getProject, GithubProject } from '../../../lib/octokit';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import Breadcrumbs from '../../../components/_shared/Breadcrumbs';
export default function ProjectPage({ project }) {
const repoId = `@${project.repo_config.owner}/${project.repo_config.repo}`
return (
<>
<NextSeo title={`${repoId}${project.base_path !== '/' ? '/' + project.base_path : ''} - GitHub Datasets`} />
<main className="prose mx-auto my-8">
<Breadcrumbs links={[{ title: repoId, href: "" }]} />
<h1 className="mb-0 mt-16">{project.repo_config.name || repoId}</h1>
<p className='mb-8'><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>
<div className="inline-block min-w-full py-2 align-middle">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Size
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{project.files?.map((file) => (
<tr key={file.download_url}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<a href={file.download_url}>{file.name}</a>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{file.size} Bytes
</td>
</tr>
))}
</tbody>
</table>
</div>
{project.readmeContent && <>
<hr />
<h2 className='uppercase font-black'>Readme</h2>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{project.readmeContent}
</ReactMarkdown>
</>}
</main>
</>
);
}
// Generates `/posts/1` and `/posts/2`
export async function getStaticPaths() {
const jsonDirectory = path.join(
process.cwd(),
'datasets.json'
);
const repos = await fs.readFile(jsonDirectory, 'utf8');
return {
paths: JSON.parse(repos).map((repo) => {
const projectPath =
repo.readme && repo.readme.split('/').length > 1
? repo.readme.split('/').slice(0, -1)
: null;
let path = [repo.name];
if (projectPath) {
projectPath.forEach((element) => {
path.push(element);
});
}
return {
params: { org: repo.owner, path },
};
}),
fallback: false, // can also be true or 'blocking'
};
}
export async function getStaticProps({ params }) {
const jsonDirectory = path.join(
process.cwd(),
'datasets.json'
);
const reposFile = await fs.readFile(jsonDirectory, 'utf8');
const repos: GithubProject[] = JSON.parse(reposFile);
const repo = repos.find((_repo) => {
const projectPath =
_repo.readme && _repo.readme.split('/').length > 1
? _repo.readme.split('/').slice(0, -1)
: null;
let path = [_repo.name];
if (projectPath) {
projectPath.forEach((element) => {
path.push(element);
});
}
return (
_repo.owner == params.org &&
JSON.stringify(path) === JSON.stringify(params.path)
);
});
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const project = await getProject(repo, github_pat);
return {
props: {
project: { ...project, repo_config: repo },
},
};
}

View File

@@ -0,0 +1,234 @@
import { NextSeo } from 'next-seo';
import getConfig from 'next/config';
import {
getAllProjectsFromOrg,
getProjectDataPackage,
getProjectMetadata,
getProjectReadme,
} from '../../../lib/project';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { loadDataPackage } from '../../../lib/loader';
import Layout from '../../../components/_shared/Layout';
import Link from 'next/link';
import { Project } from '../../../lib/project.interface';
import ExternalLinkIcon from '../../../components/icons/ExternalLinkIcon';
export default function ProjectPage({
project,
readme,
}: {
project: Project;
readme: string;
}) {
// Get description from datapackage or calculate
// excerpt from README by getting all the content
// up to the first dot.
const description =
project.description || (readme && readme.slice(0, readme.indexOf('.') + 1));
return (
<Layout>
<NextSeo title={`${project.title} - OpenSpending`} />
<main className="prose mx-auto my-8">
<h1 className="mb-1 mt-16">{project.title || project.name}</h1>
<Link target="_blank" href={project.repo.url}>
@{project.repo.full_name}
</Link>
{description && (
<div className="inline-block min-w-full py-2 align-middle mt-5">
{description}
</div>
)}
<div className="inline-block min-w-full py-2 align-middle">
<table className="min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Name
</th>
{project.datapackage.countryCode && (
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Country
</th>
)}
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Metadata
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{project.name}
</td>
{project.datapackage.countryCode && (
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{project.datapackage.countryCode}
</td>
)}
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<Link
// TODO: this link may be incorrect for some datasets
href={`https://github.com/${project.owner.name}/${project.repo.name}/blob/main/datapackage.json`}
target="_blank"
className="flex items-center hover:text-gray-700"
>
datapackage.json <ExternalLinkIcon className="ml-1" />
</Link>
</td>
</tr>
</tbody>
</table>
</div>
<h3 className="mb-1 mt-10">Data files</h3>
<p>
This dataset contains {project.files.length} file
{project.files.length != 1 ? '' : 's'}
</p>
<div className="inline-block min-w-full py-2 align-middle">
<table className="mt-0 min-w-full divide-y divide-gray-300">
<thead>
<tr>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Name
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Format
</th>
<th
scope="col"
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900"
>
Size
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{project.files?.map((file) => {
let size: number | string = file.size;
if (!size) {
if (file.bytes) {
if (file.bytes > 1000000) {
size = (file.bytes / 1000000).toFixed(2) + ' MB';
} else {
size = (file.bytes / 1000).toFixed(2) + ' kB';
}
}
}
return (
<tr key={file.name}>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{file.name}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{file.format}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
{size}
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500">
<Link
target="_blank"
href={
file.path.startsWith('http')
? file.path
: `https://raw.githubusercontent.com/${project.owner.name}/${project.repo.name}/main/${file.path}`
}
>
Download
</Link>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{readme && (
<>
<hr />
<h2 className="uppercase font-black">Readme</h2>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{readme}</ReactMarkdown>
</>
)}
</main>
</Layout>
);
}
// Generates `/posts/1` and `/posts/2`
export async function getStaticPaths() {
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const allProjects = await getAllProjectsFromOrg(
'os-data',
'main',
github_pat
);
console.log(allProjects)
const paths = allProjects.results.map((project) => ({
params: {
// TODO: dynamize the org
org: 'os-data',
project: project.repo.name,
},
}));
return {
paths,
fallback: false, // can also be true or 'blocking'
};
}
export async function getStaticProps({ params }) {
const { org: orgName, project: projectName } = params;
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const datapackage = await getProjectDataPackage(
orgName,
projectName,
'main',
github_pat
);
const repo = await getProjectMetadata(orgName, projectName, github_pat);
const project = loadDataPackage(datapackage, repo);
// TODO: should this be moved to the loader?
const readme = await getProjectReadme(orgName, projectName, 'main', github_pat);
return {
props: {
project,
readme,
},
};
}

View File

@@ -1,13 +1,11 @@
import { AppProps } from 'next/app';
import Head from 'next/head';
import './styles.css';
import { NextSeo } from 'next-seo';
function CustomApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<title>GitHub Datasets</title>
</Head>
<NextSeo title="OpenSpending" />
<main className="app">
<Component {...pageProps} />
</main>

View File

@@ -1,65 +1,58 @@
import { promises as fs } from 'fs';
import path from 'path';
import {
GithubProject,
getProjectDataPackage,
getProjectMetadata,
} from '../lib/octokit';
import { getAllProjectsFromOrg } from '../lib/project';
import getConfig from 'next/config';
import ExternalLinkIcon from '../components/icons/ExternalLinkIcon';
import TimeAgo from 'react-timeago';
import Link from 'next/link';
import { Hero } from '../components/Hero';
import { Header } from '../components/Header';
import { Container } from '../components/Container';
import { FiscalDataPackage } from '../lib/datapackage.interface';
import { loadDataPackage } from '../lib/loader';
import DatasetsSearch from '../components/DatasetsSearch';
import Layout from '../components/_shared/Layout';
export async function getStaticProps() {
const jsonDirectory = path.join(process.cwd(), '/datasets.json');
const repos = await fs.readFile(jsonDirectory, 'utf8');
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const datapackages = await Promise.all(
JSON.parse(repos).map(async (_repo: GithubProject) => {
const datapackage = await getProjectDataPackage(
_repo.owner,
_repo.name,
'main',
github_pat
);
const repo = await getProjectMetadata(
_repo.owner,
_repo.name,
github_pat
);
// TODO: support other orgs
// const orgsListPath = path.join(process.cwd(), '/orgs.json');
// const orgs = await fs.readFile(orgsListPath, 'utf8');
return {
datapackage,
repo,
};
})
const github_pat = getConfig().serverRuntimeConfig.github_pat;
const allProjects = await getAllProjectsFromOrg(
'os-data',
'main',
github_pat
);
const projects = datapackages.map(
const projects = allProjects.results.map(
(item: { datapackage: FiscalDataPackage & { repo: string }; repo: any }) =>
loadDataPackage(item.datapackage, item.repo)
);
const availableCountries = projects
.map((item) => item.countryCode)
.filter((v) => v) // Filters false values
.filter((v, i, a) => a.indexOf(v) === i) // Remove duplicates
// TODO: title should be the full name
.map((code) => ({ code, title: code }));
return {
props: {
projects: JSON.stringify(projects),
availableCountries,
},
};
}
export function Datasets({ projects }) {
export function Home({ projects, availableCountries }) {
projects = JSON.parse(projects);
return (
<div className="bg-white min-h-screen">
<Header />
<Hero />
<Layout>
<Hero
countriesCount={availableCountries.length}
datasetsCount={projects.length}
filesCount={projects.reduce(
(partialSum, a) => partialSum + a.files.length,
0
)}
/>
<section className="py-20 sm:py-32">
<Container>
<div className="mx-auto max-w-2xl lg:mx-0">
@@ -74,12 +67,15 @@ export function Datasets({ projects }) {
</p>
</div>
<div className="mt-10">
<DatasetsSearch datasets={projects} />
<DatasetsSearch
datasets={projects}
availableCountries={availableCountries}
/>
</div>
</Container>
</section>
</div>
</Layout>
);
}
export default Datasets;
export default Home;