Skip to content

More filter libraries options #866

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 30, 2025
Merged
59 changes: 50 additions & 9 deletions src/app/[language]/libraries/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { LibraryCategoryModel } from "@/features/libraries/models/library-catego
import { DATA_PATH } from "@/libs/config/project-paths.constants";
import { DEFAULT_LANGUAGE_CODE } from "@/features/localization/localization.config";
import { getLibrariesDictionary } from "@/features/localization/services/language-dictionary.service";
import { LIBRARIES_FILTER_DEFAULT_VALUE } from "@/libs/config/project.constants";
import { StructuredData } from "@/features/seo/components/structured-data.component";
import { generateArticleStructuredData } from "@/features/seo/services/structured-data.service";
import { PageMetadataProps } from "@/features/common/models/page-metadata.props";
Expand All @@ -18,11 +17,13 @@ import { generatePageMetadata } from "@/libs/metadata/metadata.service";
import { createUrlPath } from "@/libs/utils/path.utils";
import { siteTree } from "@/features/seo/site-tree";
import { getAuth0Dictionary } from "@/features/localization/services/ui-language-dictionary.service";
import { LibraryModel } from "@/features/libraries/models/library.model";
import { LibrariesDictionaryModel } from "@/features/localization/models/libraries-dictionary.model";

export async function generateMetadata({
params: { language },
}: PageMetadataProps): Promise<Metadata> {
const dictionary = getLibrariesDictionary(language);
const dictionary: LibrariesDictionaryModel = getLibrariesDictionary(language);

return generatePageMetadata({
languageCode: language,
Expand All @@ -37,7 +38,9 @@ export default function Libraries({
}: {
params: { language: string };
searchParams?: {
filter?: string;
programming_language?: string;
algorithm?: keyof LibraryModel["support"];
support?: keyof LibraryModel["support"];
};
}) {
const librariesDictionary = getLibrariesDictionary(languageCode);
Expand All @@ -47,19 +50,55 @@ export default function Libraries({
encoding: "utf-8",
});

const query: string | null = searchParams?.filter || "";
const programmingLanguage = searchParams?.programming_language;
const algorithm = searchParams?.algorithm;
const support = searchParams?.support;
const query = programmingLanguage ?? algorithm ?? support ?? "";
const dictionary = JSON.parse(source) as LibraryDictionaryModel;
const allOptions = Object.keys(Object.values(dictionary)[0].libs[0].support);
const indexAlgorithmStart = allOptions.findIndex(
(option) => option === "hs256"
);

const categoryOptions: { id: string; name: string }[] = Object.values(
dictionary,
dictionary
).map((library) => ({
id: library.id,
name: library.name,
}));

let categories: LibraryCategoryModel[] = dictionary[query]
? [dictionary[query]]
: Object.values(dictionary);
const supportOptions: { value: string; label: string }[] = allOptions
.slice(0, indexAlgorithmStart)
.map((key) => ({
value: key,
label: key.toUpperCase(),
}));

const algorithmOptions: { value: string; label: string }[] = allOptions
.slice(indexAlgorithmStart)
.map((key) => ({
value: key,
label: key.toUpperCase(),
}));

const categories: LibraryCategoryModel[] =
programmingLanguage && programmingLanguage !== "all"
? [dictionary[programmingLanguage]]
: Object.values(dictionary);

const categoryToFilter = algorithm ?? support;

const filteredCategories = categoryToFilter
? categories.map((category) => {
const filteredLibs = category.libs.filter(
(lib) => lib.support[categoryToFilter]
);
return {
...category,
libs: filteredLibs,
};
})
: categories;

return (
<>
Expand Down Expand Up @@ -166,11 +205,13 @@ export default function Libraries({
languageCode={languageCode}
query={query || librariesDictionary.filterPicker.defaultValue.value}
categoryOptions={categoryOptions}
algorithmOptions={algorithmOptions}
supportOptions={supportOptions}
dictionary={librariesDictionary}
/>
<LibraryResultsComponent
languageCode={languageCode}
categories={categories}
categories={filteredCategories}
dictionary={librariesDictionary}
/>
<Auth0CtaComponent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,29 @@
import React, { useEffect, useState } from "react";
import styles from "./debugger-picker.module.scss";
import Select, { SingleValue } from "react-select";
import Select, { SingleValue, OptionsOrGroups, GroupBase } from "react-select";
import { DebuggerPickerOptionModel } from "@/features/common/models/debugger-picker-option.model";
import { LibraryFilterLabel } from "@/features/libraries/models/library-filters.model";


interface PickerLabelProps {
label: string | null;
}

const getGroupLabel = (
options: OptionsOrGroups<
DebuggerPickerOptionModel,
GroupBase<DebuggerPickerOptionModel>
>,
selected: DebuggerPickerOptionModel
): LibraryFilterLabel | undefined => {
if (!Array.isArray(options)) return undefined;

const group = (options as GroupBase<DebuggerPickerOptionModel>[]).find(
(group) => group.options.some((opt) => opt.value === selected.value)
);
return group ? group.label as LibraryFilterLabel : undefined;
};

const PickerLabel: React.FC<PickerLabelProps> = ({ label }) => {
return (
<div className={styles.picker__label}>
Expand All @@ -18,9 +35,16 @@ const PickerLabel: React.FC<PickerLabelProps> = ({ label }) => {
interface DebuggerPickerComponentProps {
label: string | null;
languageCode: string;
options: DebuggerPickerOptionModel[];
options: OptionsOrGroups<
DebuggerPickerOptionModel,
GroupBase<DebuggerPickerOptionModel>
>;
isGrouped?: boolean;
selectedOptionCode: DebuggerPickerOptionModel | null;
handleSelection: (value: string) => void;
handleSelection: (
selection: string,
parentLabel?: LibraryFilterLabel
) => void;
placeholder: string | null;
minWidth: string | null;
}
Expand All @@ -37,12 +61,14 @@ export const DebuggerPickerComponent: React.FC<
}) => {
const [isClient, setIsClient] = useState(false);

const handleChange = (selection: SingleValue<DebuggerPickerOptionModel>) => {
const handleChange = (
selection: SingleValue<DebuggerPickerOptionModel>
) => {
if (!selection) {
return;
}

handleSelection(selection.value);
const groupLabel = getGroupLabel(options, selection);
handleSelection(selection.value, groupLabel);
};

useEffect(() => {
Expand Down
4 changes: 3 additions & 1 deletion src/features/common/models/debugger-picker-option.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { LibraryFilterLabel } from "@/features/libraries/models/library-filters.model";

export interface DebuggerPickerOptionModel {
value: any;
label: string;
label: string | LibraryFilterLabel;
isDisabled?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import { useDecoderStore } from "@/features/decoder/services/decoder.store";
import { useDebuggerStore } from "@/features/debugger/services/debugger.store";
import { DebuggerWidgetValues } from "@/features/common/values/debugger-widget.values";
import { DebuggerPickerComponent } from "@/features/common/components/debugger-picker/debugger-picker.component";
import { DebuggerPickerOptionModel } from "@/features/common/models/debugger-picker-option.model";
import {
algDictionary,
jwsExampleAlgHeaderParameterValuesDictionary,
} from "@/features/common/values/jws-alg-header-parameter-values.dictionary";
import { useButton } from "@react-aria/button";
import { clsx } from "clsx";
import { PrimaryFont } from "@/libs/theme/fonts";
import { DebuggerPickerOptionModel } from "@/features/common/models/debugger-picker-option.model";

enum PickerStates {
IDLE = "IDLE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,59 +3,96 @@
import React, { useMemo } from "react";
import styles from "./library-hero.module.scss";
import { BoxComponent } from "@/features/common/components/box/box.component";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { LIBRARIES_FILTER_QUERY_PARAM_KEY } from "@/libs/config/project.constants";
import { usePathname, useRouter } from "next/navigation";
import {
LIBRARIES_FILTER_ALGORITHM_KEY,
LIBRARIES_FILTER_PROGRAMMING_LANGUAGE_KEY,
LIBRARIES_FILTER_SUPPORT_KEY,
} from "@/libs/config/project.constants";
import { clsx } from "clsx";
import { getLocalizedSecondaryFont } from "@/libs/theme/fonts";
import { LibrariesDictionaryModel } from "@/features/localization/models/libraries-dictionary.model";
import { DebuggerPickerComponent } from "@/features/common/components/debugger-picker/debugger-picker.component";
import { LibraryFilterLabel } from "../../models/library-filters.model";
import { DebuggerPickerOptionModel } from "@/features/common/models/debugger-picker-option.model";
import { GroupBase } from "react-select";

interface LibraryHeroComponentProps {
languageCode: string;
query: string;
categoryOptions: { id: string; name: string }[];
algorithmOptions: { value: string; label: string }[];
supportOptions: { value: string; label: string }[];
dictionary: LibrariesDictionaryModel;
}

export const LibraryHeroComponent: React.FC<LibraryHeroComponentProps> = ({
languageCode,
query,
categoryOptions,
algorithmOptions,
supportOptions,
dictionary,
}) => {
const searchParams = useSearchParams();
const pathname = usePathname();
const { replace } = useRouter();

const handleSelection = (selection: string) => {
const params = new URLSearchParams(searchParams);

if (selection) {
params.set(LIBRARIES_FILTER_QUERY_PARAM_KEY, selection);
} else {
params.delete(LIBRARIES_FILTER_QUERY_PARAM_KEY);
const handleSelection = (
selection: string,
parentLabel?: LibraryFilterLabel
) => {
if (!parentLabel) {
return;
}
const params = new URLSearchParams("");
switch (parentLabel) {
case "ProgrammingLanguage":
params.set(LIBRARIES_FILTER_PROGRAMMING_LANGUAGE_KEY, selection);
break;
case "Algorithm":
params.set(LIBRARIES_FILTER_ALGORITHM_KEY, selection);
break;
case "Support":
params.set(LIBRARIES_FILTER_SUPPORT_KEY, selection);
break;
default:
break;
}

replace(`${pathname}?${params.toString()}`);
};

const options = useMemo(() => {
return [
{
value: dictionary.filterPicker.defaultValue.value,
label: dictionary.filterPicker.defaultValue.label,
label: "ProgrammingLanguage",
options: [
{
value: dictionary.filterPicker.defaultValue.value,
label: dictionary.filterPicker.defaultValue.label,
},
...categoryOptions.map((categoryOption) => {
return {
value: categoryOption.id,
label: categoryOption.name,
};
}),
],
},
{
label: "Support",
options: [...supportOptions],
},
{
label: "Algorithm",
options: [...algorithmOptions],
},
...categoryOptions.map((categoryOption) => {
return {
value: categoryOption.id,
label: categoryOption.name,
};
}),
];
] as GroupBase<DebuggerPickerOptionModel>[];
}, [
categoryOptions,
dictionary.filterPicker.defaultValue.label,
dictionary.filterPicker.defaultValue.value,
algorithmOptions,
supportOptions
]);

return (
Expand All @@ -67,7 +104,7 @@ export const LibraryHeroComponent: React.FC<LibraryHeroComponentProps> = ({
<h1
className={clsx(
styles.heroTitle,
getLocalizedSecondaryFont(languageCode),
getLocalizedSecondaryFont(languageCode)
)}
>
{dictionary.title}
Expand All @@ -80,7 +117,9 @@ export const LibraryHeroComponent: React.FC<LibraryHeroComponentProps> = ({
languageCode={languageCode}
options={options}
selectedOptionCode={
options.filter((option) => option.value === query)[0]
options
.flatMap((group) => group.options)
.filter((option) => option.value === query)[0]
}
handleSelection={handleSelection}
placeholder={null}
Expand Down
1 change: 1 addition & 0 deletions src/features/libraries/models/library-filters.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type LibraryFilterLabel = "ProgrammingLanguage" | "Algorithm" | "Support"
3 changes: 3 additions & 0 deletions src/libs/config/project.constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export const BASE_URL = "https://jwt.io";
export const LIBRARIES_FILTER_QUERY_PARAM_KEY = "filter";
export const LIBRARIES_FILTER_DEFAULT_VALUE = "all";
export const LIBRARIES_FILTER_PROGRAMMING_LANGUAGE_KEY = "programming_language";
export const LIBRARIES_FILTER_ALGORITHM_KEY = "algorithm";
export const LIBRARIES_FILTER_SUPPORT_KEY = "support";
export enum SupportedTokenHashParamValues {
TOKEN = "token",
ACCESS_TOKEN = "access_token",
Expand Down