Skip to content

Commit

Permalink
Feat/Introducing free text filters + removing filters from url (#499)
Browse files Browse the repository at this point in the history
feat: replace url states with provider
  • Loading branch information
admy7 authored Nov 12, 2024
1 parent 5cb6ec7 commit 25adf11
Show file tree
Hide file tree
Showing 22 changed files with 548 additions and 346 deletions.
2 changes: 1 addition & 1 deletion src/app/api/datasets/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function POST(request: Request) {
const response = await datasetList(options, session);

const result = {
datasets: response.data.results,
results: response.data.results,
count: response.data.count,
};

Expand Down
47 changes: 8 additions & 39 deletions src/app/datasets/FilterList/ClearFilterButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,49 +5,18 @@
"use client";

import Button from "@/components/Button";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { useFilters } from "@/providers/FilterProvider";

type ClearFilterButtonProps = {
facetGroups: string[];
};

export default function ClearFilterButton({
facetGroups,
}: ClearFilterButtonProps) {
const queryParams = useSearchParams();

useEffect(() => {}, [queryParams]);

function isAnyGroupFilterApplied() {
if (!queryParams) return false;

return Array.from(queryParams.keys()).some(
(key) =>
key !== "page" &&
key !== "q" &&
facetGroups.some((group) => key.includes(group))
);
}
function getQueryStringWithoutGroupFilter() {
if (!queryParams) return "";
return Array.from(queryParams.keys())
.filter(
(x) => facetGroups.every((group) => !x.includes(group)) && x !== "page"
)
.map((x) => `&${x}=${queryParams.get(x)}`)
.join("");
}
export default function ClearFilterButton() {
const { clearActiveFilters } = useFilters();

return (
<div className="flex justify-end">
{isAnyGroupFilterApplied() && (
<Button
href={`/datasets?page=1${getQueryStringWithoutGroupFilter()}`}
text="Clear Filters"
type="warning"
/>
)}
<Button
text="Clear Filters"
type="warning"
onClick={clearActiveFilters}
/>
</div>
);
}
76 changes: 44 additions & 32 deletions src/app/datasets/FilterList/DropdownFilterItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,63 @@
// SPDX-License-Identifier: Apache-2.0

import { Disclosure } from "@headlessui/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
import { FilterItemProps } from "./FilterItem";
import { useFilters } from "@/providers/FilterProvider";

type DropdownFilterContentProps = FilterItemProps;

export default function DropdownFilterContent({
filter,
}: DropdownFilterContentProps) {
const router = useRouter();
const searchParams = useSearchParams();
const [options, setOptions] = useState<string[]>([]);
const { activeFilters, addActiveFilter, removeActiveFilter } = useFilters();

useEffect(() => {
const paramValue =
searchParams?.get(`${filter.source}-${filter.key}`) || "";
if (paramValue) {
setOptions(paramValue.split(","));
} else {
setOptions([]);
}
}, [searchParams, filter.key, filter.source]);
const correspondingActiveFilter = activeFilters.find(
(f) => f.key === filter.key
);

const handleCheckboxChange = (value: string, checked: boolean) => {
const newOptions = checked
? [...options, value]
: options.filter((v) => v !== value);
const handleCheckboxChange = (
value: string,
label: string,
checked: boolean
) => {
const isFilterAlreadyActive = correspondingActiveFilter !== undefined;

setOptions(newOptions);
updateUrl(newOptions);
};
if (isFilterAlreadyActive) {
const currentValues = correspondingActiveFilter.values as {
value: string;
}[];
const newValues = checked
? [...currentValues, { value }]
: currentValues.filter((v) => v.value !== value);

const updateUrl = (newOptions: string[]) => {
const params = new URLSearchParams(searchParams?.toString() || "");
if (!newValues.length) {
removeActiveFilter(filter.key, filter.source);
return;
}

if (newOptions.length > 0) {
params.set(`${filter.source}-${filter.key}`, newOptions.join(","));
const newActiveFilter = {
...correspondingActiveFilter,
values: newValues,
};
addActiveFilter(newActiveFilter);
} else {
params.delete(`${filter.source}-${filter.key}`);
const newActiveFilter = {
source: filter.source,
type: filter.type,
key: filter.key,
label: filter.label,
values: [{ value, label }],
};
addActiveFilter(newActiveFilter);
}
};

params.set("page", "1");

const newUrl = `${window.location.pathname}?${params.toString()}`;
router.push(newUrl);
const isChecked = (item: { value: string }) => {
return correspondingActiveFilter
? correspondingActiveFilter
?.values!.map((v) => v.value)
.includes(item.value)
: false;
};

return (
Expand All @@ -63,9 +75,9 @@ export default function DropdownFilterContent({
className="h-4 w-4 border rounded-md checked:accent-warning flex-none"
id={`${filter.source}-${item.value}`}
value={item.value}
checked={options.includes(item.value)}
checked={isChecked(item)}
onChange={(e) =>
handleCheckboxChange(item.value, e.target.checked)
handleCheckboxChange(item.value, item.label, e.target.checked)
}
/>
<label
Expand Down
18 changes: 17 additions & 1 deletion src/app/datasets/FilterList/FilterItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,22 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Disclosure } from "@headlessui/react";
import { default as DropdownFilterContent } from "./DropdownFilterItem";
import FreeTextFilterContent from "./FreeTextFilterItem";
import { useFilters } from "@/providers/FilterProvider";

export type FilterItemProps = {
filter: Filter;
};

function FilterItem({ filter }: FilterItemProps) {
const { activeFilters } = useFilters();

const correspondingActiveFilter = activeFilters.find(
(activeFilter) =>
activeFilter.key === filter.key && activeFilter.source === filter.source
);
const isFilterActive = correspondingActiveFilter !== undefined;
const nbActiveValues = correspondingActiveFilter?.values?.length || 0;

const getFilterContent = (type: FilterType) => {
switch (type) {
case FilterType.DROPDOWN:
Expand All @@ -35,7 +45,13 @@ function FilterItem({ filter }: FilterItemProps) {
{({ open }) => (
<>
<Disclosure.Button className="flex w-full justify-between px-4 py-2 text-left">
<span className="text-base px-1.5">{filter.label}</span>
<div>
<span className="text-base px-1.5">{filter.label}</span>
<span className="text-base text-info">
{isFilterActive &&
`(${nbActiveValues} ${nbActiveValues > 1 ? "filters" : "filter"} active)`}
</span>
</div>
<FontAwesomeIcon
icon={faChevronDown}
className={`h-5 w-5 transition-transform ${open ? "rotate-180" : ""}`}
Expand Down
42 changes: 18 additions & 24 deletions src/app/datasets/FilterList/FreeTextFilterItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,20 @@
// SPDX-License-Identifier: Apache-2.0

import Button from "@/components/Button";
import { FilterType } from "@/services/discovery/types/filter.type";
import { ActiveFilter, Operator } from "@/services/discovery/types/filter.type";
import { faCheck, faPlusCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Disclosure } from "@headlessui/react";
import { useState } from "react";
import { FilterItemProps } from "./FilterItem";
import { useFilters } from "@/providers/FilterProvider";

type FreeTextFilterContentProps = FilterItemProps;

type FreeTextFilterValue = {
value: string;
operator: FilterType;
};

type FreeTextFilterOutput = {
filterKey: string;
values: FreeTextFilterValue[];
};

export default function FreeTextFilterContent({
filter,
}: FreeTextFilterContentProps) {
const { addActiveFilter } = useFilters();
const [nbFilters, setNbFilters] = useState(1);
const [openedDropdown, setOpenedDropdown] = useState<number | null>(null);

Expand Down Expand Up @@ -56,28 +48,30 @@ export default function FreeTextFilterContent({

const handleSubmitValue = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();

const formData = new FormData(event.target as HTMLFormElement);
const filterValues: FreeTextFilterValue[] = Array.from({
length: nbFilters,
}).reduce((acc: FreeTextFilterValue[], _, index) => {
const values = [] as { value: string; operator: Operator }[];

for (let index = 0; index < nbFilters; index++) {
const value = formData.get(`${filter.key}-${index}-value`) as string;
const operator = formData.get(
`${filter.key}-${index}-operator`
) as FilterType;
) as Operator;

if (value && operator) {
acc.push({ value, operator });
values.push({ value, operator });
}
}

return acc;
}, []);

const filterOutput: FreeTextFilterOutput = {
filterKey: filter.key,
values: filterValues,
};
const newActiveFilter = {
source: filter.source,
type: filter.type,
key: filter.key,
label: filter.label,
values,
} as ActiveFilter;

return filterOutput;
addActiveFilter(newActiveFilter);
};

return (
Expand Down
11 changes: 5 additions & 6 deletions src/app/datasets/FilterList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
//
// SPDX-License-Identifier: Apache-2.0

import { Filter } from "@/services/discovery/types/filter.type";
import FilterItem from "./FilterItem";
"use client";

type FilterListProps = {
filters: Filter[];
};
import FilterItem from "./FilterItem";
import { useFilters } from "@/providers/FilterProvider";

export default async function FilterList({ filters }: FilterListProps) {
export default function FilterList() {
const { filters } = useFilters();
const filtersSortedBySource = filters.sort((f1, f2) =>
f2.source.localeCompare(f1.source)
);
Expand Down
32 changes: 15 additions & 17 deletions src/app/datasets/page.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,32 @@
// SPDX-FileCopyrightText: 2024 PNED G.I.E.
//
// SPDX-License-Identifier: Apache-2.0
"use client";

import Error from "@/app/error";
import PageContainer from "@/components/PageContainer";
import SearchBar from "@/components/Searchbar";
import DatasetsProvider from "@/providers/datasets/DatasetsProvider";
import { Filter } from "@/services/discovery/types/filter.type";
import { redirect } from "next/navigation";
import { GET } from "../api/filters/route";
import { redirect, useSearchParams } from "next/navigation";
import DatasetCount from "./DatasetCount";
import DatasetListContainer from "./DatasetListContainer";
import FilterList from "./FilterList";
import NoDatasetMessage from "./NoDatasetMessage";
import { useFilters } from "@/providers/FilterProvider";
import ActiveFilters from "@/components/ActiveFilters";

export default async function DatasetsPage({
searchParams,
}: {
searchParams?: { [key: string]: string | string[] | undefined };
}) {
if (!searchParams?.page) {
export default function DatasetsPage() {
const searchParams = useSearchParams() as URLSearchParams;

if (!searchParams.get("page")) {
redirect("/datasets?page=1");
}

const getFiltersResponse = await GET();
if (getFiltersResponse.status !== 200) {
return <Error statusCode={getFiltersResponse.status} />;
}
const { error } = useFilters();

const filters = (await getFiltersResponse.json()) as Filter[];
if (error) {
return <Error statusCode={error.statusCode} />;
}

return (
<PageContainer>
Expand All @@ -40,16 +38,16 @@ export default async function DatasetsPage({
<DatasetCount />
<div className="col-start-0 col-span-12 flex flex-col gap-4 sm:block xl:hidden">
<div className="my-4 h-fit">
<FilterList filters={filters} />
<FilterList />
</div>
</div>
<div className="col-start-0 col-span-4 flex flex-col gap-y-6">
<div className="col-start-0 col-span-4 mr-6 hidden h-fit xl:block px-6">
<FilterList filters={filters} />
<FilterList />
</div>
</div>
<div className="col-span-12 xl:col-span-8">
{/* <AppliedFilters searchFacets={filters} /> */}
<ActiveFilters />
<NoDatasetMessage />
<DatasetListContainer />
</div>
Expand Down
5 changes: 4 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import SessionProviderWrapper from "./SessionProviderWrapper";
import "./globals.css";
config.autoAddCss = false;
import contentConfig from "@/config/contentConfig";
import { FilterProvider } from "@/providers/FilterProvider";

export default function RootLayout({
children,
Expand All @@ -37,7 +38,9 @@ export default function RootLayout({
<div>
<Header />
</div>
<div>{children}</div>
<FilterProvider>
<div>{children}</div>
</FilterProvider>
<Footer />
</SessionProviderWrapper>
</div>
Expand Down
Loading

0 comments on commit 25adf11

Please sign in to comment.