[@portaljs/components][xl] - @portaljs/ckan package
+ Accompaning example using said package + Removed the "example" from all the examples names
This commit is contained in:
98
packages/ckan/src/components/DatasetCard.tsx
Normal file
98
packages/ckan/src/components/DatasetCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
146
packages/ckan/src/components/DatasetSearchFilters.tsx
Normal file
146
packages/ckan/src/components/DatasetSearchFilters.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
91
packages/ckan/src/components/DatasetSearchForm.tsx
Normal file
91
packages/ckan/src/components/DatasetSearchForm.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
103
packages/ckan/src/components/ListOfDatasets.tsx
Normal file
103
packages/ckan/src/components/ListOfDatasets.tsx
Normal 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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
80
packages/ckan/src/components/Pagination.tsx
Normal file
80
packages/ckan/src/components/Pagination.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
47
packages/ckan/src/components/ResourceCard.tsx
Normal file
47
packages/ckan/src/components/ResourceCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
6
packages/ckan/src/components/index.tsx
Normal file
6
packages/ckan/src/components/index.tsx
Normal 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 };
|
||||
Reference in New Issue
Block a user