Skip to content

feat(app): add filtering questions #407

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 15 commits into from
Dec 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
1 change: 1 addition & 0 deletions apps/app/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.vscode
.vercel
storybook-static
.env*.local
1 change: 1 addition & 0 deletions apps/app/public/select.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { redirect } from "next/navigation";
import { QuestionItem } from "../../../../../components/QuestionItem/QuestionItem";
import { QuestionsHeader } from "../../../../../components/QuestionsHeader/QuestionsHeader";
import { QuestionsPagination } from "../../../../../components/QuestionsPagination";
import { PAGE_SIZE } from "../../../../../lib/constants";
import { getQuerySortBy, DEFAULT_SORT_BY_QUERY } from "../../../../../lib/order";
import { technologies } from "../../../../../lib/technologies";
import { getAllQuestions } from "../../../../../services/questions.service";

export default async function QuestionsPage({
params,
searchParams,
}: {
params: { technology: string; page: string };
searchParams?: { sortBy?: string };
}) {
const page = parseInt(params.page);
const querySortBy = getQuerySortBy(searchParams?.sortBy || DEFAULT_SORT_BY_QUERY);

if (!technologies.includes(params.technology) || isNaN(page)) {
return redirect("/");
Expand All @@ -20,10 +25,13 @@ export default async function QuestionsPage({
category: params.technology,
limit: PAGE_SIZE,
offset: (page - 1) * PAGE_SIZE,
orderBy: querySortBy?.orderBy,
order: querySortBy?.order,
});

return (
<div className="flex flex-col gap-y-10">
<QuestionsHeader technology={params.technology} total={data.meta.total} />
{data.data.map(({ id, question, _levelId, acceptedAt, votesCount }) => (
<QuestionItem
key={id}
Expand Down
4 changes: 2 additions & 2 deletions apps/app/src/components/AddQuestionModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ export const AddQuestionModal = (props: ComponentProps<typeof BaseModal>) => {
</h2>
<form onSubmit={handleFormSubmit}>
<div className="mt-10 flex flex-col gap-y-3 px-5 sm:flex-row sm:justify-evenly sm:gap-x-5">
<Select className="w-full" aria-label="Wybierz technologię">
<Select variant="purple" className="w-full" aria-label="Wybierz technologię">
<option>Wybierz Technologię</option>
<option>HTML5</option>
<option>JavaScript</option>
</Select>
<Select className="w-full" aria-label="Wybierz poziom">
<Select variant="purple" className="w-full" aria-label="Wybierz poziom">
<option>Wybierz Poziom</option>
<option value="junior">Junior</option>
<option value="mid">Mid</option>
Expand Down
41 changes: 41 additions & 0 deletions apps/app/src/components/QuestionsHeader/QuestionsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"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";

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

type QuestionsHeaderProps = Readonly<{
technology: Technology;
total: number;
}>;

export const QuestionsHeader = ({ technology, total }: QuestionsHeaderProps) => {
const { sortBy, setSortByFromString } = useQuestionsOrderBy();

const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
event.preventDefault();
setSortByFromString(event.target.value);
};

return (
<header className="flex justify-between text-neutral-400">
<output>
<strong>{technologiesLabel[technology]}: </strong>
{total} {questionsPluralize(total)}
</output>
<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>
</Select>
</label>
</header>
);
};
12 changes: 11 additions & 1 deletion apps/app/src/components/Select/Select.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,14 @@ export default meta;

type Story = StoryObj<typeof Select>;

export const Default: Story = {};
export const Default: Story = {
args: {
variant: "default",
},
};

export const Purple: Story = {
args: {
variant: "purple",
},
};
17 changes: 15 additions & 2 deletions apps/app/src/components/Select/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import { twMerge } from "tailwind-merge";
import type { SelectHTMLAttributes } from "react";

export const Select = ({ className, ...props }: SelectHTMLAttributes<HTMLSelectElement>) => (
const variants = {
default:
"select bg-white py-1 pl-1 pr-6 text-neutral-700 dark:bg-white-dark dark:text-neutral-200",
purple:
"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",
};

type SelectProps = Readonly<{
variant: keyof typeof variants;
}> &
SelectHTMLAttributes<HTMLSelectElement>;

export const Select = ({ variant, className, ...props }: SelectProps) => (
<select
className={twMerge(
"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",
"cursor-pointer appearance-none rounded-none text-base focus:outline-0",
variants[variant],
className,
)}
{...props}
Expand Down
18 changes: 18 additions & 0 deletions apps/app/src/hooks/useDevFAQRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { usePathname, useSearchParams, useRouter } from "next/navigation";

export const useDevFAQRouter = () => {
const router = useRouter();
const searchParams = useSearchParams();
const pathname = usePathname();

const mergeQueryParams = (data: Record<string, string>) => {
const params = { ...Object.fromEntries(searchParams.entries()), ...data };
const query = new URLSearchParams(params).toString();

if (pathname) {
router.push(`${pathname}?${query}`);
}
};

return { mergeQueryParams };
};
18 changes: 18 additions & 0 deletions apps/app/src/hooks/useQuestionsOrderBy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { useSearchParams } from "next/navigation";
import { validateSortByQuery, DEFAULT_SORT_BY_QUERY } from "../lib/order";
import { useDevFAQRouter } from "./useDevFAQRouter";

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

const sortBy = searchParams.get("sortBy") || DEFAULT_SORT_BY_QUERY;

const setSortByFromString = (sortBy: string) => {
if (validateSortByQuery(sortBy)) {
mergeQueryParams({ sortBy });
}
};

return { sortBy, setSortByFromString };
};
31 changes: 31 additions & 0 deletions apps/app/src/lib/order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const orderBy = ["acceptedAt", "level", "votesCount"] as const;
const order = ["asc", "desc"] as const;

type OrderBy = typeof orderBy[number];
type Order = typeof order[number];

export const DEFAULT_SORT_BY_QUERY = "acceptedAt*desc";

export const validateOrderBy = (data: string): data is OrderBy => {
return orderBy.includes(data);
};

export const validateOrder = (data: string): data is Order => {
return order.includes(data);
};

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

return Boolean(orderBy && order && validateOrderBy(orderBy) && validateOrder(order));
};

export const getQuerySortBy = (query?: string) => {
if (!validateSortByQuery(query)) {
return null;
}

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

return { orderBy, order };
};
10 changes: 10 additions & 0 deletions apps/app/src/lib/technologies.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
export const technologies = ["html", "css", "js", "angular", "react", "git", "other"] as const;
export type Technology = typeof technologies[number];

export const technologiesLabel: Record<Technology, string> = {
html: "HTML5",
css: "CSS3",
js: "JS",
angular: "Angular",
react: "React",
git: "Git",
other: "Inne",
};
7 changes: 7 additions & 0 deletions apps/app/src/styles/tailwind.css
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@
background-repeat: no-repeat;
}

.select {
background-image: url("/select.svg");
background-size: 12px 6px;
background-repeat: no-repeat;
background-position: calc(100% - 5px) 50%;
}

.dark .level-button {
background-color: theme("colors.white-dark");
}
Expand Down