Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
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.
10 changes: 9 additions & 1 deletion apps/app/src/app/questions/[technology]/[page]/page.tsx
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 { technologies } from "../../../../lib/technologies";
import { validateOrder, validateOrderBy, DEFAULT_ORDER_QUERY } from "../../../../lib/order";
import { technologies, Technology } 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 [orderBy, order] = (searchParams?.sortBy || DEFAULT_ORDER_QUERY).split("*");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To i walidacja nie powinny być w komponencie.


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,
...(validateOrderBy(orderBy) && { orderBy }),
...(validateOrder(order) && { 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
47 changes: 47 additions & 0 deletions apps/app/src/components/QuestionsHeader/QuestionsHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import { ChangeEvent } from "react";
import { DEFAULT_ORDER_QUERY } from "../../lib/order";
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 { setOrderBy } = useQuestionsOrderBy();

const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
event.preventDefault();
setOrderBy(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"
defaultValue={DEFAULT_ORDER_QUERY}
onChange={handleSelectChange}
className="ml-3"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brakuje value={orderBy}

>
<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
13 changes: 13 additions & 0 deletions apps/app/src/hooks/useDevFAQRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useSearchParams } from "next/navigation";

export const useDevFAQRouter = () => {
const searchParams = useSearchParams();

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

return new URLSearchParams(params).toString();
};

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

export const useQuestionsOrderBy = () => {
const pathname = usePathname();
const router = useRouter();
const { mergeQueryParams } = useDevFAQRouter();

const setOrderBy = (order: string) => {
if (pathname) {
router.push(`${pathname}?${mergeQueryParams({ sortBy: order })}`);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Czemu tego wszystkiego nie robi mergeQueryParams?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A gdzie walidacja tego order ?

}
};

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

export const DEFAULT_ORDER_QUERY = "acceptedAt*desc";

export const validateOrderBy = (data: unknown): data is typeof orderBy[number] => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const validateOrderBy = (data: unknown): data is typeof orderBy[number] => {
export const validateOrderBy = (data: string): data is typeof orderBy[number] => {

return orderBy.includes(data);
};

export const validateOrder = (data: unknown): data is typeof order[number] => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export const validateOrder = (data: unknown): data is typeof order[number] => {
export const validateOrder = (data: string): data is typeof order[number] => {

return order.includes(data);
};
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