Skip to content

Commit

Permalink
feat(app): add question level filtering (#413)
Browse files Browse the repository at this point in the history
* feat(app): add question level filtering

* refactor(app): remove Boolean

* feat(app): add Params type

* feat(app): create useQuestionsLevel hook

* refactor(app): simplify query validation

* feat(app): improve types

* refactor(app): refactor questions filtering

* refactor(app): change way to remove level from query

* refactor(app): change options map

* refactor(app): remove unused exports

* refactor(app): change variable name

* refactor(app): invoke to methods from Number object

Co-authored-by: Michał Miszczyszyn <michal@mmiszy.pl>
  • Loading branch information
AdiPol1359 and typeofweb committed Dec 14, 2022
1 parent 874d911 commit 744b1b8
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 49 deletions.
Original file line number Diff line number Diff line change
@@ -1,32 +1,36 @@
import { redirect } from "next/navigation";
import { QuestionItem } from "../../../../../components/QuestionItem/QuestionItem";
import { QuestionsHeader } from "../../../../../components/QuestionsHeader/QuestionsHeader";
import { QuestionsHeader } from "../../../../../components/QuestionsHeader";
import { QuestionsPagination } from "../../../../../components/QuestionsPagination";
import { PAGE_SIZE } from "../../../../../lib/constants";
import { getQuerySortBy, DEFAULT_SORT_BY_QUERY } from "../../../../../lib/order";
import { DEFAULT_SORT_BY_QUERY, parseQuerySortBy } from "../../../../../lib/order";
import { parseQueryLevels } from "../../../../../lib/level";
import { technologies } from "../../../../../lib/technologies";
import { getAllQuestions } from "../../../../../services/questions.service";
import { Params, SearchParams } from "../../../../../types";

export default async function QuestionsPage({
params,
searchParams,
}: {
params: { technology: string; page: string };
searchParams?: { sortBy?: string };
params: Params<"technology" | "page">;
searchParams?: SearchParams<"sortBy" | "level">;
}) {
const page = parseInt(params.page);
const querySortBy = getQuerySortBy(searchParams?.sortBy || DEFAULT_SORT_BY_QUERY);
const page = Number.parseInt(params.page);
const sortBy = parseQuerySortBy(searchParams?.sortBy || DEFAULT_SORT_BY_QUERY);
const levels = parseQueryLevels(searchParams?.level);

if (!technologies.includes(params.technology) || isNaN(page)) {
if (!technologies.includes(params.technology) || Number.isNaN(page)) {
return redirect("/");
}

const { data } = await getAllQuestions({
category: params.technology,
limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE,
orderBy: querySortBy?.orderBy,
order: querySortBy?.order,
orderBy: sortBy?.orderBy,
order: sortBy?.order,
level: levels?.join(","),
});

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import { ChangeEvent } from "react";
import { technologiesLabel, Technology } from "../../lib/technologies";
import { pluralize } from "../../utils/intl";
import { Select } from "../Select/Select";
import { useQuestionsOrderBy } from "../../hooks/useQuestionsOrderBy";
import { ChangeEvent, Fragment } from "react";
import { technologiesLabel, Technology } from "../lib/technologies";
import { pluralize } from "../utils/intl";
import { useQuestionsOrderBy } from "../hooks/useQuestionsOrderBy";
import { sortByLabels } from "../lib/order";
import { Select } from "./Select/Select";

const questionsPluralize = pluralize("pytanie", "pytania", "pytań");

Expand All @@ -30,10 +31,11 @@ export const QuestionsHeader = ({ technology, total }: QuestionsHeaderProps) =>
<label>
Sortuj według:
<Select variant="default" value={sortBy} onChange={handleSelectChange} className="ml-3">
<option value="acceptedAt*desc">od najnowszych</option>
<option value="acceptedAt*asc">od najstarszych</option>
<option value="votesCount*asc">od najmniej popularnych</option>
<option value="votesCount*desc">od najpopularniejszych</option>
{Object.entries(sortByLabels).map(([sortBy, label]) => (
<option key={sortBy} value={sortBy}>
{label}
</option>
))}
</Select>
</label>
</header>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import { twMerge } from "tailwind-merge";
type LevelButtonProps = Readonly<{
variant: "junior" | "mid" | "senior";
isActive: boolean;
onClick?: () => void;
children: ReactNode;
}>;

export const LevelButton = ({ variant, isActive, children }: LevelButtonProps) => (
export const LevelButton = ({ variant, isActive, onClick, children }: LevelButtonProps) => (
<button
className={twMerge(
"h-20 w-20 transition-colors duration-100 sm:h-8 sm:w-full small-filters:h-14 small-filters:w-14",
"h-20 w-20 capitalize transition-colors duration-100 sm:h-8 sm:w-full small-filters:h-14 small-filters:w-14",
"rounded-md text-sm text-neutral-500 active:translate-y-px dark:text-neutral-200",
!isActive &&
"level-button bg-white shadow-[0px_1px_4px] shadow-neutral-400 dark:shadow-neutral-900",
Expand All @@ -19,6 +20,7 @@ export const LevelButton = ({ variant, isActive, children }: LevelButtonProps) =
variant === "mid" && "level-button-mid",
variant === "senior" && "level-button-senior",
)}
onClick={onClick}
>
{children}
</button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
"use client";

import { useQuestionsLevels } from "../../../hooks/useQuestionsLevels";
import { QuestionsSidebarSection } from "../QuestionsSidebarSection";
import { levels } from "../../../lib/level";
import { LevelButton } from "./LevelButton";

export const LevelFilter = () => {
const { queryLevels, addLevel, removeLevel } = useQuestionsLevels();

return (
<QuestionsSidebarSection title="Wybierz poziom">
<div className="flex justify-center gap-3 sm:flex-col small-filters:flex-row">
<LevelButton variant="junior" isActive={true}>
Junior
</LevelButton>
<LevelButton variant="mid" isActive={false}>
Mid
</LevelButton>
<LevelButton variant="senior" isActive={false}>
Senior
</LevelButton>
{levels.map((level) => {
const isActive = Boolean(queryLevels?.includes(level));
const handleClick = isActive ? removeLevel : addLevel;

return (
<LevelButton
key={level}
variant={level}
isActive={isActive}
onClick={() => handleClick(level)}
>
{level}
</LevelButton>
);
})}
</div>
</QuestionsSidebarSection>
);
Expand Down
25 changes: 25 additions & 0 deletions apps/app/src/hooks/useQuestionsLevels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { useSearchParams } from "next/navigation";
import { parseQueryLevels, Level } from "../lib/level";
import { useDevFAQRouter } from "./useDevFAQRouter";

export const useQuestionsLevels = () => {
const searchParams = useSearchParams();
const { mergeQueryParams } = useDevFAQRouter();

const queryLevel = searchParams.get("level");
const queryLevels = parseQueryLevels(queryLevel);

const addLevel = (level: Level) => {
mergeQueryParams({ level: [...(queryLevels || []), level].join(",") });
};

const removeLevel = (level: Level) => {
if (queryLevels) {
const newQueryLevels = queryLevels.filter((l) => l !== level);

mergeQueryParams({ level: newQueryLevels.join(",") });
}
};

return { queryLevels, addLevel, removeLevel };
};
4 changes: 2 additions & 2 deletions apps/app/src/hooks/useQuestionsOrderBy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useSearchParams } from "next/navigation";
import { validateSortByQuery, DEFAULT_SORT_BY_QUERY } from "../lib/order";
import { parseQuerySortBy, DEFAULT_SORT_BY_QUERY } from "../lib/order";
import { useDevFAQRouter } from "./useDevFAQRouter";

export const useQuestionsOrderBy = () => {
Expand All @@ -9,7 +9,7 @@ export const useQuestionsOrderBy = () => {
const sortBy = searchParams.get("sortBy") || DEFAULT_SORT_BY_QUERY;

const setSortByFromString = (sortBy: string) => {
if (validateSortByQuery(sortBy)) {
if (parseQuerySortBy(sortBy)) {
mergeQueryParams({ sortBy });
}
};
Expand Down
19 changes: 19 additions & 0 deletions apps/app/src/lib/level.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { QueryParam } from "../types";

export const levels = ["junior", "mid", "senior"] as const;

export type Level = typeof levels[number];

export const parseQueryLevels = (query?: QueryParam | null) => {
if (typeof query !== "string") {
return null;
}

const splittedQuery = query.split(",");

if (!splittedQuery.every((level) => levels.includes(level))) {
return null;
}

return splittedQuery as Level[];
};
36 changes: 18 additions & 18 deletions apps/app/src/lib/order.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
const orderBy = ["acceptedAt", "level", "votesCount"] as const;
const order = ["asc", "desc"] as const;
import { QueryParam } from "../types";

type OrderBy = typeof orderBy[number];
type Order = typeof order[number];
const ordersBy = ["acceptedAt", "level", "votesCount"] as const;
const orders = ["asc", "desc"] as const;

export const DEFAULT_SORT_BY_QUERY = "acceptedAt*desc";

export const validateOrderBy = (data: string): data is OrderBy => {
return orderBy.includes(data);
export const sortByLabels: Record<`${OrderBy}*${Order}`, string> = {
"acceptedAt*asc": "od najstarszych",
"acceptedAt*desc": "od najnowszych",
"level*asc": "od najprostszych",
"level*desc": "od najtrudniejszych",
"votesCount*asc": "od najmniej popularnych",
"votesCount*desc": "od najpopularniejszych",
};

export const validateOrder = (data: string): data is Order => {
return order.includes(data);
};
type OrderBy = typeof ordersBy[number];
type Order = typeof orders[number];

export const validateSortByQuery = (query?: string): query is `${OrderBy}*${Order}` => {
const [orderBy, order] = query?.split("*") || [];
export const parseQuerySortBy = (query: QueryParam) => {
if (typeof query !== "string") {
return null;
}

return Boolean(orderBy && order && validateOrderBy(orderBy) && validateOrder(order));
};
const [orderBy, order] = query.split("*");

export const getQuerySortBy = (query?: string) => {
if (!validateSortByQuery(query)) {
if (!orderBy || !order || !ordersBy.includes(orderBy) || !orders.includes(order)) {
return null;
}

const [orderBy, order] = query.split("*") as [OrderBy, Order];

return { orderBy, order };
};
10 changes: 10 additions & 0 deletions apps/app/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,13 @@ import { paths } from "openapi-types";

export type UserData =
paths["/auth/me"]["get"]["responses"][200]["content"]["application/json"]["data"];

export type Params<T extends string> = {
readonly [K in T]: string;
};

export type QueryParam = string | readonly string[] | undefined;

export type SearchParams<T extends string> = {
readonly [K in T]?: QueryParam;
};

0 comments on commit 744b1b8

Please sign in to comment.