Skip to content

Commit 03b6dd4

Browse files
authored
feat(app): add filtering questions (#407)
* feat(app): add filtering questions * refactor(app): change parameter type * refactor(app): remove select state * feat(app): add devFAQ router hook * refactor(app): change hook extension * refactor(app): move logic into hook * feat(app): add dark mode to default select variant * feat(app): add orderBy select value * refactor(app): move logic into hook * refactor(app): change parameters type * feat(app): add query order validation * refactor(app): rename some variables * chore(app): add .env.local to gitignore * refactor(app): change NEXT_PUBLIC_API_URL
1 parent ba8a746 commit 03b6dd4

File tree

13 files changed

+163
-5
lines changed

13 files changed

+163
-5
lines changed
File renamed without changes.

apps/app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.vscode
22
.vercel
33
storybook-static
4+
.env*.local

apps/app/public/select.svg

Lines changed: 1 addition & 0 deletions
Loading

apps/app/src/app/(main-layout)/questions/[technology]/[page]/page.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
import { redirect } from "next/navigation";
22
import { QuestionItem } from "../../../../../components/QuestionItem/QuestionItem";
3+
import { QuestionsHeader } from "../../../../../components/QuestionsHeader/QuestionsHeader";
34
import { QuestionsPagination } from "../../../../../components/QuestionsPagination";
45
import { PAGE_SIZE } from "../../../../../lib/constants";
6+
import { getQuerySortBy, DEFAULT_SORT_BY_QUERY } from "../../../../../lib/order";
57
import { technologies } from "../../../../../lib/technologies";
68
import { getAllQuestions } from "../../../../../services/questions.service";
79

810
export default async function QuestionsPage({
911
params,
12+
searchParams,
1013
}: {
1114
params: { technology: string; page: string };
15+
searchParams?: { sortBy?: string };
1216
}) {
1317
const page = parseInt(params.page);
18+
const querySortBy = getQuerySortBy(searchParams?.sortBy || DEFAULT_SORT_BY_QUERY);
1419

1520
if (!technologies.includes(params.technology) || isNaN(page)) {
1621
return redirect("/");
@@ -20,10 +25,13 @@ export default async function QuestionsPage({
2025
category: params.technology,
2126
limit: PAGE_SIZE,
2227
offset: (page - 1) * PAGE_SIZE,
28+
orderBy: querySortBy?.orderBy,
29+
order: querySortBy?.order,
2330
});
2431

2532
return (
2633
<div className="flex flex-col gap-y-10">
34+
<QuestionsHeader technology={params.technology} total={data.meta.total} />
2735
{data.data.map(({ id, question, _levelId, acceptedAt, votesCount }) => (
2836
<QuestionItem
2937
key={id}

apps/app/src/components/AddQuestionModal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,12 @@ export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
2121
</h2>
2222
<form onSubmit={handleFormSubmit}>
2323
<div className="mt-10 flex flex-col gap-y-3 px-5 sm:flex-row sm:justify-evenly sm:gap-x-5">
24-
<Select className="w-full" aria-label="Wybierz technologię">
24+
<Select variant="purple" className="w-full" aria-label="Wybierz technologię">
2525
<option>Wybierz Technologię</option>
2626
<option>HTML5</option>
2727
<option>JavaScript</option>
2828
</Select>
29-
<Select className="w-full" aria-label="Wybierz poziom">
29+
<Select variant="purple" className="w-full" aria-label="Wybierz poziom">
3030
<option>Wybierz Poziom</option>
3131
<option value="junior">Junior</option>
3232
<option value="mid">Mid</option>
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use client";
2+
3+
import { ChangeEvent } from "react";
4+
import { technologiesLabel, Technology } from "../../lib/technologies";
5+
import { pluralize } from "../../utils/intl";
6+
import { Select } from "../Select/Select";
7+
import { useQuestionsOrderBy } from "../../hooks/useQuestionsOrderBy";
8+
9+
const questionsPluralize = pluralize("pytanie", "pytania", "pytań");
10+
11+
type QuestionsHeaderProps = Readonly<{
12+
technology: Technology;
13+
total: number;
14+
}>;
15+
16+
export const QuestionsHeader = ({ technology, total }: QuestionsHeaderProps) => {
17+
const { sortBy, setSortByFromString } = useQuestionsOrderBy();
18+
19+
const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
20+
event.preventDefault();
21+
setSortByFromString(event.target.value);
22+
};
23+
24+
return (
25+
<header className="flex justify-between text-neutral-400">
26+
<output>
27+
<strong>{technologiesLabel[technology]}: </strong>
28+
{total} {questionsPluralize(total)}
29+
</output>
30+
<label>
31+
Sortuj według:
32+
<Select variant="default" value={sortBy} onChange={handleSelectChange} className="ml-3">
33+
<option value="acceptedAt*desc">od najnowszych</option>
34+
<option value="acceptedAt*asc">od najstarszych</option>
35+
<option value="votesCount*asc">od najmniej popularnych</option>
36+
<option value="votesCount*desc">od najpopularniejszych</option>
37+
</Select>
38+
</label>
39+
</header>
40+
);
41+
};

apps/app/src/components/Select/Select.stories.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,14 @@ export default meta;
1919

2020
type Story = StoryObj<typeof Select>;
2121

22-
export const Default: Story = {};
22+
export const Default: Story = {
23+
args: {
24+
variant: "default",
25+
},
26+
};
27+
28+
export const Purple: Story = {
29+
args: {
30+
variant: "purple",
31+
},
32+
};

apps/app/src/components/Select/Select.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,23 @@
11
import { twMerge } from "tailwind-merge";
22
import type { SelectHTMLAttributes } from "react";
33

4-
export const Select = ({ className, ...props }: SelectHTMLAttributes<HTMLSelectElement>) => (
4+
const variants = {
5+
default:
6+
"select bg-white py-1 pl-1 pr-6 text-neutral-700 dark:bg-white-dark dark:text-neutral-200",
7+
purple:
8+
"select-purple border-b border-primary bg-transparent py-2 pr-6 pl-1 capitalize text-primary transition-shadow duration-100 focus:shadow-[0_0_10px] focus:shadow-primary dark:border-neutral-200 dark:text-neutral-200 dark:focus:shadow-white",
9+
};
10+
11+
type SelectProps = Readonly<{
12+
variant: keyof typeof variants;
13+
}> &
14+
SelectHTMLAttributes<HTMLSelectElement>;
15+
16+
export const Select = ({ variant, className, ...props }: SelectProps) => (
517
<select
618
className={twMerge(
7-
"select-purple cursor-pointer appearance-none rounded-none border-b border-primary bg-transparent py-2 pr-6 pl-1 text-base capitalize text-primary transition-shadow duration-100 focus:shadow-[0_0_10px] focus:shadow-primary focus:outline-0 dark:border-neutral-200 dark:text-neutral-200 dark:focus:shadow-white",
19+
"cursor-pointer appearance-none rounded-none text-base focus:outline-0",
20+
variants[variant],
821
className,
922
)}
1023
{...props}

apps/app/src/hooks/useDevFAQRouter.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { usePathname, useSearchParams, useRouter } from "next/navigation";
2+
3+
export const useDevFAQRouter = () => {
4+
const router = useRouter();
5+
const searchParams = useSearchParams();
6+
const pathname = usePathname();
7+
8+
const mergeQueryParams = (data: Record<string, string>) => {
9+
const params = { ...Object.fromEntries(searchParams.entries()), ...data };
10+
const query = new URLSearchParams(params).toString();
11+
12+
if (pathname) {
13+
router.push(`${pathname}?${query}`);
14+
}
15+
};
16+
17+
return { mergeQueryParams };
18+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { useSearchParams } from "next/navigation";
2+
import { validateSortByQuery, DEFAULT_SORT_BY_QUERY } from "../lib/order";
3+
import { useDevFAQRouter } from "./useDevFAQRouter";
4+
5+
export const useQuestionsOrderBy = () => {
6+
const searchParams = useSearchParams();
7+
const { mergeQueryParams } = useDevFAQRouter();
8+
9+
const sortBy = searchParams.get("sortBy") || DEFAULT_SORT_BY_QUERY;
10+
11+
const setSortByFromString = (sortBy: string) => {
12+
if (validateSortByQuery(sortBy)) {
13+
mergeQueryParams({ sortBy });
14+
}
15+
};
16+
17+
return { sortBy, setSortByFromString };
18+
};

0 commit comments

Comments
 (0)