[@portaljs/components][xl] - @portaljs/ckan package

+ Accompaning example using said package
+ Removed the "example" from all the examples names
This commit is contained in:
Luccas Mateus de Medeiros Gomes
2023-05-23 14:13:36 -03:00
parent 622428a015
commit 91c76c213c
70 changed files with 26359 additions and 1 deletions

View File

@@ -0,0 +1,98 @@
import Link from "next/link";
import { format } from "timeago.js";
import { Dataset } from "../interfaces/dataset.interface";
import ResourceCard from "./ResourceCard";
export default function DatasetCard({
dataset,
showOrg = true,
urlPrefix = ""
}: {
dataset: Dataset;
showOrg: boolean;
urlPrefix?: string;
}) {
const resourceBgColors = {
PDF: "bg-cyan-300",
CSV: "bg-emerald-300",
JSON: "bg-yellow-300",
ODS: "bg-amber-400",
XLS: "bg-orange-300",
DOC: "bg-red-300",
SHP: "bg-purple-400",
HTML: "bg-pink-300",
};
const resourceBgColorsProxy = new Proxy(resourceBgColors, {
get: (obj, prop) => {
if (prop in obj) {
return obj[prop]
}
return "bg-amber-400"
}
})
function DatasetInformations() {
return (
<div className="flex align-center gap-2">
{(dataset.resources.length > 0 && dataset.resources[0].format && (
<>
{showOrg !== false && (
<span
className={`${
resourceBgColorsProxy[
dataset.resources[0].format as keyof typeof resourceBgColors
]
} px-4 py-1 rounded-full text-xs`}
>
{dataset.organization
? dataset.organization.title
: "No organization"}
</span>
)}
<span
className={`${
resourceBgColorsProxy[
dataset.resources[0].format as keyof typeof resourceBgColors
]
} px-4 py-1 rounded-full text-xs`}
>
{dataset.metadata_created && format(dataset.metadata_created)}
</span>
</>
)) || (
<>
{showOrg !== false && (
<span className="bg-gray-200 px-4 py-1 rounded-full text-xs">
{dataset.organization
? dataset.organization.title
: "No organization"}
</span>
)}
<span className="bg-gray-200 px-4 py-1 rounded-full text-xs">
{dataset.metadata_created && format(dataset.metadata_created)}
</span>
</>
)}
</div>
);
}
return (
<article className="grid grid-cols-1 md:grid-cols-7 gap-x-2">
<ResourceCard
resource={dataset?.resources.find((resource) => resource.format)}
/>
<div className="col-span-6 place-content-start flex flex-col gap-1">
<Link href={`${urlPrefix}/@${dataset.organization?.name}/${dataset.name}`}>
<h1 className="m-auto md:m-0 font-semibold text-lg text-zinc-900">
{dataset.title || "No title"}
</h1>
</Link>
<p className="text-sm font-normal text-stone-500 line-clamp-2 h-[44px] overflow-y-hidden ">
{dataset.notes?.replace(/<\/?[^>]+(>|$)/g, "") || "No description"}
</p>
<DatasetInformations />
</div>
</article>
);
}

View File

@@ -0,0 +1,146 @@
import { Field, Form, Formik, useFormikContext } from "formik";
import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { PackageSearchOptions, Tag, Group, Organization, FilterObj } from "../interfaces";
function AutoSubmit({
setOptions,
options,
}: {
options: PackageSearchOptions;
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
}) {
const { values } = useFormikContext<{
tags: string[];
orgs: string[];
groups: string[];
}>();
useEffect(() => {
setOptions({
...options,
groups: values.groups,
tags: values.tags,
orgs: values.orgs,
});
}, [values]);
return null;
}
export default function DatasetSearchFilters({
tags,
orgs,
groups,
setOptions,
options,
filtersName,
}: {
tags: Array<Tag>;
orgs: Array<Organization>;
groups: Array<Group>;
options: PackageSearchOptions;
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
filtersName?: FilterObj | undefined;
}) {
const [seeMoreOrgs, setSeeMoreOrgs] = useState(false);
const [seeMoreTags, setSeeMoreTags] = useState(false);
const [seeMoreGroups, setSeeMoreGroups] = useState(false);
return (
<Formik
initialValues={{
tags: [],
orgs: [],
groups: [],
}}
onSubmit={async (values) => {
alert(JSON.stringify(values, null, 2));
}}
>
<Form>
<section className="bg-white rounded-lg xl:p-8 p-4 mb-4 max-h-[400px] overflow-y-auto">
<h1 className="font-bold pb-4">Refine by {`${filtersName?.org || "Organization"}`}</h1>
{orgs
.filter((org) => org.title || org.id)
.slice(0, seeMoreOrgs ? orgs.length : 5)
.map((org) => (
<div key={org.id}>
<Field
type="checkbox"
id={org.id}
name="orgs"
value={org.name}
></Field>
<label className="ml-1.5" htmlFor={org.id}>
{org.title || org.display_name}
</label>
</div>
))}
{orgs.length > 5 && (
<button
className="bg-gray-300 px-2 rounded text-gray-600 mt-2"
type="button"
onClick={() => setSeeMoreOrgs(!seeMoreOrgs)}
>
Show {seeMoreOrgs ? "less" : "more"}
</button>
)}
</section>
<section className="bg-white rounded-lg xl:p-8 p-4 mb-4 max-h-[400px] overflow-y-auto">
<h1 className="font-bold pb-4">Refine by {`${filtersName?.group || "Theme"}`}</h1>
{groups.slice(0, seeMoreGroups ? groups.length : 5).map((group) => (
<div key={group.id}>
<Field
type="checkbox"
id={group.id}
name="groups"
value={group.name}
></Field>
<label className="ml-1.5" htmlFor={group.id}>
{group.display_name}
</label>
</div>
))}
{groups.length > 5 && (
<button
onClick={() => setSeeMoreGroups(!seeMoreGroups)}
type="button"
className="bg-gray-300 px-2 rounded text-gray-600 mt-2"
>
See {seeMoreGroups ? "less" : "more..."}
</button>
)}
</section>
<section className="bg-white rounded-lg xl:p-8 p-4 mb-4 max-h-[400px] overflow-y-auto">
<h1 className="font-bold pb-4">Refine by Keyword</h1>
<div className="flex gap-2 flex-wrap">
{tags.slice(0, seeMoreTags ? tags.length : 5).map((tag) => (
<div key={tag.id}>
<Field
type="checkbox"
className="hidden tag-checkbox"
id={tag.id}
name="tags"
value={tag.name}
></Field>
<label
className="bg-gray-200 px-4 py-1 rounded-full text-xs block"
htmlFor={tag.id}
>
{tag.display_name}
</label>
</div>
))}
</div>
{tags.length > 5 && (
<button
onClick={() => setSeeMoreTags(!seeMoreTags)}
type="button"
className="bg-gray-300 px-2 rounded text-gray-600 mt-2"
>
See {seeMoreTags ? "less" : "more..."}
</button>
)}
</section>
<AutoSubmit options={options} setOptions={setOptions} />
</Form>
</Formik>
);
}

View File

@@ -0,0 +1,91 @@
import { Field, Form, Formik } from 'formik';
import { Dispatch, SetStateAction } from 'react';
import {
PackageSearchOptions,
Group,
Organization,
FilterObj,
} from '../interfaces';
export default function DatasetSearchForm({
orgs,
groups,
setOptions,
options,
filtersName,
}: {
orgs: Array<Organization>;
groups: Array<Group>;
options: PackageSearchOptions;
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
filtersName?: FilterObj | undefined;
}) {
return (
<Formik
initialValues={{
org: '',
group: '',
query: '',
}}
enableReinitialize={true}
onSubmit={async (values) => {
const org = orgs.find(
(org) => (org.title || org.display_name) === values.org
);
const group = groups.find(
(group) => group.display_name === values.group
);
setOptions({
...options,
groups: group ? [group.name] : [],
orgs: org ? [org.name] : [],
query: values.query,
});
}}
>
<div className="mx-auto" style={{ width: 'min(1100px, 95vw)' }}>
<Form className="min-h-[80px] flex flex-col lg:flex-row bg-white inline-block px-5 py-3 rounded-xl">
<Field
type="text"
placeholder="Search Datasets"
className="mx-4 grow py-4 border-0 placeholder:text-neutral-400"
name="query"
/>
<Field
list="groups"
name="group"
placeholder={`${filtersName?.group || 'Theme'}`}
className="lg:border-l p-4 mx-2 placeholder:text-neutral-400"
></Field>
<datalist aria-label="Formats" id="groups">
<option value="">{`${filtersName?.group || 'Theme'}`}</option>
{groups.map((group, index) => (
<option key={index}>{group.display_name}</option>
))}
</datalist>
<Field
list="orgs"
name="org"
placeholder="Organization"
className="lg:border-l p-4 mx-2 placeholder:text-neutral-400"
autoComplete="off"
></Field>
<datalist aria-label="Formats" id="orgs">
<option value="">Organization</option>
{orgs.map((org, index) => (
<option key={index}>{org.title || org.display_name}</option>
))}
</datalist>
<button
className="font-bold text-black px-12 py-4 rounded-lg bg-accent hover:bg-cyan-500 duration-150"
type="submit"
>
SEARCH
</button>
</Form>
</div>
</Formik>
);
}

View File

@@ -0,0 +1,103 @@
import { Dispatch, SetStateAction, useState } from 'react';
import useSWR from 'swr';
import { SWRConfig } from 'swr';
import { PackageSearchOptions } from '../interfaces';
import DatasetCard from './DatasetCard';
import Pagination from './Pagination';
import CKAN from '../lib/ckanapi';
export default function ListOfDatasets({
ckan,
options,
setOptions,
urlPrefix = '',
}: {
ckan: CKAN;
options: PackageSearchOptions;
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
urlPrefix?: string;
}) {
return (
<SWRConfig>
<ListOfDatasetsInner
ckan={ckan}
options={options}
setOptions={setOptions}
urlPrefix={urlPrefix}
/>
</SWRConfig>
);
}
function ListOfDatasetsInner({
ckan,
options,
setOptions,
urlPrefix = '',
}: {
ckan: CKAN;
options: PackageSearchOptions;
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
urlPrefix?: string;
}) {
return (
<div className="grid grid-cols-1 gap-4 homepage-padding">
<ListItems
ckan={ckan}
setOptions={setOptions}
options={options}
urlPrefix={urlPrefix}
/>
<div style={{ display: 'none' }}>
<ListItems
ckan={ckan}
setOptions={setOptions}
options={{ ...options, offset: options.offset + 5 }}
/>
</div>
</div>
);
}
function ListItems({
ckan,
options,
setOptions,
urlPrefix = '',
}: {
ckan: CKAN;
options: PackageSearchOptions;
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
urlPrefix?: string;
}) {
const { data } = useSWR(['package_search', options], async () =>
ckan.packageSearch({ ...options })
);
//Define which page buttons are going to be displayed in the pagination list
const [subsetOfPages, setSubsetOfPages] = useState(0);
return (
<>
<h2 className="text-4xl capitalize font-bold text-zinc-900">
{data?.count} Datasets
</h2>
{data?.datasets?.map((dataset) => (
<DatasetCard
key={dataset.id}
dataset={dataset}
showOrg={true}
urlPrefix={urlPrefix}
/>
))}
{data?.count && (
<Pagination
options={options}
subsetOfPages={subsetOfPages}
setSubsetOfPages={setSubsetOfPages}
setOptions={setOptions}
count={data.count}
/>
)}
</>
);
}

View File

@@ -0,0 +1,80 @@
import { Dispatch, SetStateAction } from "react";
import { PackageSearchOptions } from "../interfaces";
export default function Pagination({
options,
setOptions,
subsetOfPages,
setSubsetOfPages,
count,
}: {
options: PackageSearchOptions;
setOptions: Dispatch<SetStateAction<PackageSearchOptions>>;
subsetOfPages: number;
setSubsetOfPages: Dispatch<SetStateAction<number>>;
count: number;
}) {
return (
<div className="flex gap-2 align-center">
{subsetOfPages !== 0 && (
<button
className="font-semibold flex items-center gap-2"
onClick={() => setSubsetOfPages(subsetOfPages - 5)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
/>
</svg>
Prev
</button>
)}
{Array.from(Array(Math.ceil(count / 5)).keys()).map((x) => (
<button
key={x}
className={`${
x == options.offset / 5 ? "bg-orange-500 text-white" : ""
} px-2 rounded font-semibold text-zinc-900`}
onClick={() => setOptions({ ...options, offset: x * 5 })}
style={{
display:
x >= subsetOfPages && x < subsetOfPages + 5 ? "block" : "none",
}}
>
{x + 1}
</button>
))}
{subsetOfPages !== Math.ceil(count / 5) && count > 25 && (
<button
className="font-semibold flex items-center gap-2"
onClick={() => setSubsetOfPages(subsetOfPages + 5)}
>
Next
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/>
</svg>
</button>
)}
</div>
);
}

View File

@@ -0,0 +1,47 @@
import { Resource } from "../interfaces";
export default function ResourceCard({
resource,
small,
}: {
resource?: Resource;
small?: boolean;
}) {
const resourceTextColors = {
PDF: "text-cyan-300",
CSV: "text-emerald-300",
JSON: "text-yellow-300",
XLS: "text-orange-300",
ODS: "text-amber-400",
DOC: "text-red-300",
SHP: "text-purple-400",
HTML: "text-pink-300",
};
return (
<div className="col-span-1 md:pt-1.5 place-content-center md:place-content-start">
<div
className="bg-slate-900 rounded-lg max-w-[90px] min-w-[60px] mx-auto md:mx-0 flex place-content-center my-auto"
style={{ minHeight: small ? "60px" : "90px" }}
>
{(resource && resource.format && (
<span
className={`${
resourceTextColors[
resource.format as keyof typeof resourceTextColors
]
? resourceTextColors[
resource.format as keyof typeof resourceTextColors
]
: "text-gray-200"
} font-bold ${small ? "text-lg" : "text-2xl"} my-auto`}
>
{resource.format}
</span>
)) || (
<span className="font-bold text-2xl text-gray-200 my-auto">NONE</span>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,6 @@
import DatasetCard from "./DatasetCard";
import ListOfDatasets from "./ListOfDatasets";
import DatasetSearchForm from "./DatasetSearchForm";
import DatasetSearchFilters from "./DatasetSearchFilters";
export { DatasetCard, ListOfDatasets, DatasetSearchForm, DatasetSearchFilters };