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

@@ -9,7 +9,7 @@ export default function DatasetCard({ dataset }: { dataset: Project }) {
className="overflow-hidden rounded-xl border border-gray-200"
>
<Link
href=""
href={`/@${dataset.owner.name}/${dataset.repo.name}`}
className="flex items-center gap-x-4 border-b border-gray-900/5 bg-gray-50 p-6"
>
<img
@@ -60,8 +60,8 @@ export default function DatasetCard({ dataset }: { dataset: Project }) {
<dd className="flex items-start gap-x-2">
<div className="font-medium text-gray-900">
<Link
// TODO: where do we get the info needed for this link?
href=""
// TODO: this link may be incorrect for some datasets
href={`https://github.com/${dataset.owner.name}/${dataset.repo.name}/blob/main/datapackage.json`}
target="_blank"
className="flex items-center hover:text-gray-700"
>

View File

@@ -2,9 +2,26 @@ import { useForm } from 'react-hook-form';
import DatasetsGrid from './DatasetsGrid';
import { Project } from '../lib/project.interface';
import { Index } from 'flexsearch';
import {
ChevronDoubleLeftIcon,
ChevronDoubleRightIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from '@heroicons/react/24/solid';
import { useState } from 'react';
export default function DatasetsSearch({
datasets,
availableCountries,
}: {
datasets: Project[];
availableCountries;
}) {
const itemsPerPage = 6;
const [page, setPage] = useState(1);
export default function DatasetsSearch({ datasets }: { datasets: Project[] }) {
const index = new Index({ tokenize: 'full' });
datasets.forEach((dataset: Project) =>
index.add(
dataset.name,
@@ -21,12 +38,38 @@ export default function DatasetsSearch({ datasets }: { datasets: Project[] }) {
},
});
const allCountries = datasets
.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 }));
const filteredDatasets = datasets
.filter((dataset: Project) =>
watch().searchTerm && watch().searchTerm !== ''
? index.search(watch().searchTerm).includes(dataset.name)
: true
)
.filter((dataset) =>
watch().country && watch().country !== ''
? dataset.countryCode === watch().country
: true
)
// TODO: Does that really makes sense?
// What if the fiscalPeriod is 2015-2017 and inputs are
// set to 2015-2016. It's going to be filtered out but
// it shouldn't.
.filter((dataset) =>
watch().minDate && watch().minDate !== ''
? dataset.fiscalPeriod?.start >= watch().minDate
: true
)
.filter((dataset) =>
watch().maxDate && watch().maxDate !== ''
? dataset.fiscalPeriod?.end <= watch().maxDate
: true
);
const paginatedDatasets = filteredDatasets.slice(
(page - 1) * itemsPerPage,
(page - 1) * itemsPerPage + itemsPerPage
);
const pageCount = Math.ceil(filteredDatasets.length / itemsPerPage) || 1;
return (
<>
@@ -37,7 +80,7 @@ export default function DatasetsSearch({ datasets }: { datasets: Project[] }) {
<input
placeholder="Search datasets"
aria-label="Search datasets"
{...register('searchTerm')}
{...register('searchTerm', { onChange: () => setPage(1) })}
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 !== '' && (
@@ -55,10 +98,10 @@ export default function DatasetsSearch({ datasets }: { datasets: Project[] }) {
<label className="text-sm text-gray-600 font-medium">Country</label>
<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"
{...register('country')}
{...register('country', { onChange: () => setPage(1) })}
>
<option value="">All</option>
{allCountries.map((country) => {
{availableCountries.map((country) => {
return (
<option key={country.code} value={country.code}>
{country.title}
@@ -73,17 +116,9 @@ export default function DatasetsSearch({ datasets }: { datasets: Project[] }) {
<input
aria-label="Min. date"
type="date"
{...register('minDate')}
{...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"
/>
{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 className="sm:basis-1/6">
@@ -92,48 +127,56 @@ export default function DatasetsSearch({ datasets }: { datasets: Project[] }) {
<input
aria-label="Max. date"
type="date"
{...register('maxDate')}
{...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"
/>
{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 className="min-w-full mt-10 align-middle">
<DatasetsGrid
datasets={datasets
.filter((dataset: Project) =>
watch().searchTerm && watch().searchTerm !== ''
? index.search(watch().searchTerm).includes(dataset.name)
: true
)
.filter((dataset) =>
watch().country && watch().country !== ''
? dataset.countryCode === watch().country
: true
)
// TODO: Does that really makes sense?
// What if the fiscalPeriod is 2015-2017 and inputs are
// set to 2015-2016. It's going to be filtered out but
// it shouldn't.
.filter((dataset) =>
watch().minDate && watch().minDate !== ''
? dataset.fiscalPeriod?.start >= watch().minDate
: true
)
.filter((dataset) =>
watch().maxDate && watch().maxDate !== ''
? dataset.fiscalPeriod?.end <= watch().maxDate
: true
)}
/>
<div className="mt-5 mb-5">
<span className="text-lg font-medium">
{filteredDatasets.length} datasets found
</span>
</div>
<div className="min-w-full align-middle">
<DatasetsGrid datasets={paginatedDatasets} />
<div className="w-full flex justify-center mt-10">
<button
onClick={() => setPage(1)}
disabled={page <= 1}
className="disabled:text-gray-400"
>
<ChevronDoubleLeftIcon className="w-6 h-6" />
</button>
<button
onClick={() => {
if (page > 1) setPage((prev) => --prev);
}}
disabled={page <= 1}
className="disabled:text-gray-400"
>
<ChevronLeftIcon className="w-6 h-6" />
</button>
<span className="mx-5">
Page {page} of {pageCount}
</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>
</>
);

View File

@@ -1,53 +1,82 @@
import Image from 'next/image'
import { Button } from './Button'
import { Container } from './Container'
import logo from "../public/logo.svg"
import Link from 'next/link'
import { useRouter } from 'next/router'
import Image from 'next/image';
import { Container } from './Container';
import logo from '../public/logo.svg';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { Bars3Icon } from '@heroicons/react/24/outline';
import { useState } from 'react';
export function Header() {
const [menuOpen, setMenuOpen] = useState<boolean>(false);
const router = useRouter();
const isActive = (navLink) => {
return router.asPath.split("?")[0] == navLink.href;
}
return router.asPath.split('?')[0] == navLink.href;
};
const navLinks = [
{
title: "Home",
href: "/#header"
title: 'Home',
href: '/',
},
{
title: "Datasets",
href: "/#datasets"
title: 'Datasets',
href: '/#datasets',
},
{
title: "Community",
href: "https://community.openspending.org/"
}
]
// {
// title: "Community",
// href: "https://community.openspending.org/"
// }
];
return (
<header className="z-50 pb-5 lg:pt-11 sticky top-0 backdrop-blur" id="header">
<Container className="flex flex-wrap items-center justify-center sm:justify-between lg:flex-nowrap">
<div className="mt-10 lg:mt-0 lg:grow lg:basis-0 flex items-center">
<header className="relative z-50 pb-11 lg:pt-11">
<Container className="flex flex-wrap items-center justify-between lg:flex-nowrap mt-10 lg:mt-0">
<Link href="/" className="lg:mt-0 lg:grow lg:basis-0 flex items-center">
<Image src={logo} alt="OpenSpending" className="h-12 w-auto" />
</div>
<ul className='list-none flex gap-x-5 text-base font-medium'>
</Link>
<ul className="hidden list-none sm:flex 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" : ""}`}
className={`text-emerald-900 hover:text-emerald-600 ${
isActive(link) ? 'text-emerald-600' : ''
}`}
href={link.href}
scroll={false}
>
{link.title}
</Link>
</li>))}
</li>
))}
</ul>
<div className="hidden sm:mt-10 sm:flex lg:mt-0 lg:grow lg:basis-0 lg:justify-end">
<div className="hidden xl:block xl:grow"></div>
<div className="sm:hidden sm:mt-10 lg:mt-0 lg:grow lg:basis-0 lg:justify-end">
<button onClick={() => setMenuOpen(!menuOpen)}>
<Bars3Icon className="w-8 h-8" />
</button>
</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>
</header >
)
</header>
);
}

View File

@@ -1,9 +1,9 @@
import { Button } from './Button'
import { Container } from './Container'
import { Button } from './Button';
import { Container } from './Container';
export function Hero() {
export function Hero({ countriesCount, datasetsCount, filesCount }) {
return (
<div className="relative pb-20 pt-10 sm:py-40">
<div className="relative pb-20 pt-10 sm:py-40" id="hero">
<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 bottom-0 h-40 bg-gradient-to-t from-white" />
@@ -15,12 +15,13 @@ export function Hero() {
</h1>
<div className="mt-6 space-y-6 font-display text-2xl tracking-tight text-emerald-900">
<p>
By understanding how governments spend money in our name can we have a say
in how that money will affect our own lives. The journey starts here.
By understanding how governments spend money in our name can we
have a say in how that money will affect our own lives. The
journey starts here.
</p>
<p>
OpenSpending is a free, open and global platform to search, visualise and analyse
fiscal data in the public sphere.
OpenSpending is a free, open and global platform to search,
visualise and analyse fiscal data in the public sphere.
</p>
</div>
<Button href="#datasets" className="mt-10">
@@ -28,9 +29,11 @@ export function Hero() {
</Button>
<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">
{[
['Countries', '75'],
['Datasets', '2091'],
['Files', '9230'],
// Added the plus sign because some datasets do not
// contain defined countries
['Countries', '+' + countriesCount],
['Datasets', datasetsCount],
['Files', filesCount],
].map(([name, value]) => (
<div key={name}>
<dt className="font-mono text-sm text-emerald-600">{name}</dt>
@@ -43,5 +46,5 @@ export function Hero() {
</div>
</Container>
</div>
)
);
}

View File

@@ -9,7 +9,7 @@ import { Grid } from '@githubocto/flat-ui';
const queryClient = new QueryClient();
async function getCsv(url: string) {
export async function getCsv(url: string) {
const response = await fetch(url, {
headers: {
Range: 'bytes=0-5132288',

View File

@@ -0,0 +1,10 @@
import { Header } from '../Header';
export default function Layout({ children }) {
return (
<div className="bg-white min-h-screen">
<Header />
{children}
</div>
);
}