Skip to content
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

feat: add ability to sort questions by update / edit date #507

Draft
wants to merge 10 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions apps/api/modules/questions/questions.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
levelId: true,
statusId: true,
acceptedAt: true,
updatedAt: true,
_count: {
select: {
QuestionVote: true,
Expand All @@ -66,6 +67,7 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
_levelId: q.levelId,
_statusId: q.statusId,
acceptedAt: q.acceptedAt?.toISOString(),
updatedAt: q.updatedAt?.toISOString(),
votesCount: q._count.QuestionVote,
};
});
Expand Down
2 changes: 2 additions & 0 deletions apps/api/modules/questions/questions.schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const generateGetQuestionsQuerySchema = <
Type.Literal("acceptedAt"),
Type.Literal("level"),
Type.Literal("votesCount"),
Type.Literal("updatedAt"),
]),
order: Type.Union([Type.Literal("asc"), Type.Literal("desc")]),
userId: Type.Integer(),
Expand All @@ -51,6 +52,7 @@ const generateQuestionShape = <
_levelId: Type.Union(args.levels.map((val) => Type.Literal(val))),
_statusId: Type.Union(args.statuses.map((val) => Type.Literal(val))),
acceptedAt: Type.Optional(Type.String({ format: "date-time" })),
updatedAt: Type.Optional(Type.String({ format: "date-time" })),
} as const;
};

Expand Down
13 changes: 11 additions & 2 deletions apps/app/src/app/(main-layout)/admin/[status]/[page]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { parseQueryLevels } from "../../../../../lib/level";
import { statuses } from "../../../../../lib/question";
import { parseTechnologyQuery } from "../../../../../lib/technologies";
import { Params, SearchParams } from "../../../../../types";
import { DEFAULT_SORT_BY_QUERY, parseQuerySortBy } from "../../../../../lib/order";

const AdminPanel = dynamic(
() =>
Expand All @@ -19,19 +20,27 @@ export default function AdminPage({
searchParams,
}: {
params: Params<"status" | "page">;
searchParams?: SearchParams<"technology" | "level">;
searchParams?: SearchParams<"technology" | "level" | "sortBy">;
}) {
const page = Number.parseInt(params.page);
const technology = parseTechnologyQuery(searchParams?.technology);
const levels = parseQueryLevels(searchParams?.level);
const sortBy = parseQuerySortBy(searchParams?.sortBy || DEFAULT_SORT_BY_QUERY);

if (Number.isNaN(page) || !statuses.includes(params.status)) {
return redirect("/admin");
}

return (
<PrivateRoute role="admin" loginPreviousPath="/">
<AdminPanel page={page} technology={technology} status={params.status} levels={levels} />
<AdminPanel
page={page}
technology={technology}
status={params.status}
levels={levels}
order={sortBy?.order}
orderBy={sortBy?.orderBy}
/>
</PrivateRoute>
);
}
12 changes: 10 additions & 2 deletions apps/app/src/app/(main-layout)/user/questions/[page]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { redirect } from "next/navigation";
import { PrivateRoute } from "../../../../../components/PrivateRoute";
import { UserQuestions } from "../../../../../components/UserQuestions/UserQuestions";
import { parseQueryLevels } from "../../../../../lib/level";
import { DEFAULT_SORT_BY_QUERY, parseQuerySortBy } from "../../../../../lib/order";
import { parseTechnologyQuery } from "../../../../../lib/technologies";
import { Params, SearchParams } from "../../../../../types";

Expand All @@ -10,19 +11,26 @@ export default function UserQuestionsPage({
searchParams,
}: {
params: Params<"page">;
searchParams?: SearchParams<"technology" | "level">;
searchParams?: SearchParams<"technology" | "level" | "sortBy">;
}) {
const page = Number.parseInt(params.page);
const technology = parseTechnologyQuery(searchParams?.technology);
const levels = parseQueryLevels(searchParams?.level);
const sortBy = parseQuerySortBy(searchParams?.sortBy || DEFAULT_SORT_BY_QUERY);

if (Number.isNaN(page)) {
return redirect("/user/questions");
}

return (
<PrivateRoute loginPreviousPath="/">
<UserQuestions page={page} technology={technology} levels={levels} />
<UserQuestions
page={page}
technology={technology}
levels={levels}
order={sortBy?.order}
orderBy={sortBy?.orderBy}
/>
</PrivateRoute>
);
}
16 changes: 14 additions & 2 deletions apps/app/src/components/AdminPanel/AdminPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Suspense, useCallback } from "react";
import { useGetAllQuestions } from "../../hooks/useGetAllQuestions";
import { Level } from "../../lib/level";
import { Order, OrderBy } from "../../lib/order";
import { QuestionStatus } from "../../lib/question";
import { Technology } from "../../lib/technologies";
import { FilterableQuestionsList } from "../FilterableQuestionsList/FilterableQuestionsList";
Expand All @@ -13,14 +14,25 @@ type AdminPanelProps = Readonly<{
technology: Technology | null;
levels: Level[] | null;
status: QuestionStatus;
order?: Order;
orderBy?: OrderBy;
}>;

export const AdminPanel = ({ page, technology, levels, status }: AdminPanelProps) => {
export const AdminPanel = ({
page,
technology,
levels,
status,
order,
orderBy,
}: AdminPanelProps) => {
const { isSuccess, data, refetch } = useGetAllQuestions({
page,
status,
technology,
levels,
order,
orderBy,
});

const refetchQuestions = useCallback(() => {
Expand All @@ -32,7 +44,7 @@ export const AdminPanel = ({ page, technology, levels, status }: AdminPanelProps
page={page}
total={data?.data.meta.total || 0}
getHref={(page) => `/admin/${status}/${page}`}
data={{ status, technology, levels }}
data={{ status, technology, levels, order, orderBy }}
>
{isSuccess && data.data.data.length > 0 ? (
<Suspense>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ComponentProps, ReactNode } from "react";
import { Order, OrderBy } from "../../lib/order";
import { QuestionStatus } from "../../lib/question";
import { Technology } from "../../lib/technologies";
import { Level } from "../QuestionItem/QuestionLevel";
Expand All @@ -11,6 +12,8 @@ type FilterableQuestionsListProps = Readonly<{
status?: QuestionStatus;
technology?: Technology | null;
levels?: Level[] | null;
order?: Order;
orderBy?: OrderBy;
};
children: ReactNode;
}> &
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@ import { useRouter } from "next/navigation";
import { ChangeEvent, ReactNode } from "react";
import { useDevFAQRouter } from "../../hooks/useDevFAQRouter";
import { levels } from "../../lib/level";
import { Order, OrderBy } from "../../lib/order";
import { QuestionStatus, statuses } from "../../lib/question";
import { technologies, technologiesLabels, Technology } from "../../lib/technologies";
import { Level } from "../QuestionItem/QuestionLevel";
import { Select } from "../Select/Select";
import { SelectLabel } from "../SelectLabel";
import { SortBySelect } from "../SortBySelect";

type FilterableQuestionsListHeaderProps = Readonly<{
status?: QuestionStatus;
technology?: Technology | null;
levels?: Level[] | null;
order?: Order;
orderBy?: OrderBy;
}>;

const SelectLabel = ({ children }: { readonly children: ReactNode }) => (
<label className="flex flex-wrap items-baseline gap-1.5 md:gap-3">{children}</label>
);

export const FilterableQuestionsListHeader = ({
status,
technology,
levels: selectedLevels,
order,
orderBy,
}: FilterableQuestionsListHeaderProps) => {
const { mergeQueryParams } = useDevFAQRouter();
const router = useRouter();
Expand Down Expand Up @@ -66,6 +69,9 @@ export const FilterableQuestionsListHeader = ({
</Select>
</SelectLabel>
)}
{order && orderBy && (
<SortBySelect order={order} orderBy={orderBy} onChange={handleSelectChange("sortBy")} />
)}
{status !== undefined && (
<SelectLabel>
Status:
Expand Down
18 changes: 6 additions & 12 deletions apps/app/src/components/QuestionsHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { ChangeEvent } from "react";
import { technologiesLabels, Technology } from "../lib/technologies";
import { pluralize } from "../utils/intl";
import { useQuestionsOrderBy } from "../hooks/useQuestionsOrderBy";
import { sortByLabels } from "../lib/order";
import { Select } from "./Select/Select";
import { parseQuerySortBy } from "../lib/order";
import { SortBySelect } from "./SortBySelect";

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

Expand All @@ -16,6 +16,7 @@ type QuestionsHeaderProps = Readonly<{

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

const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
event.preventDefault();
Expand All @@ -28,16 +29,9 @@ export const QuestionsHeader = ({ technology, total }: QuestionsHeaderProps) =>
<strong>{technologiesLabels[technology]}: </strong>
{total} {questionsPluralize(total)}
</output>
<label className="flex flex-wrap items-baseline gap-1.5 md:gap-3">
Sortuj według:
<Select variant="default" value={sortBy} onChange={handleSelectChange}>
{Object.entries(sortByLabels).map(([sortBy, label]) => (
<option key={sortBy} value={sortBy}>
{label}
</option>
))}
</Select>
</label>
{sort?.order && sort?.orderBy && (
<SortBySelect order={sort.order} orderBy={sort.orderBy} onChange={handleSelectChange} />
)}
</header>
);
};
5 changes: 5 additions & 0 deletions apps/app/src/components/SelectLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ReactNode } from "react";

export const SelectLabel = ({ children }: { readonly children: ReactNode }) => (
<label className="flex flex-wrap items-baseline gap-1.5 md:gap-3">{children}</label>
);
23 changes: 23 additions & 0 deletions apps/app/src/components/SortBySelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ChangeEvent } from "react";
import { Order, OrderBy, sortByLabels } from "../lib/order";
import { Select } from "./Select/Select";
import { SelectLabel } from "./SelectLabel";

type SortBySelectProps = {
order: Order;
orderBy: OrderBy;
onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
};

export const SortBySelect = ({ order, orderBy, onChange }: SortBySelectProps) => (
<SelectLabel>
Sortowanie:
<Select variant="default" value={`${orderBy}*${order}`} onChange={onChange}>
{Object.entries(sortByLabels).map(([sortBy, label]) => (
<option key={sortBy} value={sortBy}>
{label}
</option>
))}
</Select>
</SelectLabel>
);
9 changes: 7 additions & 2 deletions apps/app/src/components/UserQuestions/UserQuestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { Suspense } from "react";
import { useGetAllQuestions } from "../../hooks/useGetAllQuestions";
import { useUser } from "../../hooks/useUser";
import { Order, OrderBy } from "../../lib/order";
import { Technology } from "../../lib/technologies";
import { FilterableQuestionsList } from "../FilterableQuestionsList/FilterableQuestionsList";
import { Level } from "../QuestionItem/QuestionLevel";
Expand All @@ -12,23 +13,27 @@ type UserQuestionsProps = Readonly<{
page: number;
technology: Technology | null;
levels: Level[] | null;
order?: Order;
orderBy?: OrderBy;
}>;

export const UserQuestions = ({ page, technology, levels }: UserQuestionsProps) => {
export const UserQuestions = ({ page, technology, levels, order, orderBy }: UserQuestionsProps) => {
const { userData } = useUser();
const { isSuccess, data } = useGetAllQuestions({
page,
technology,
levels,
userId: userData?._user.id,
order,
orderBy,
});

return (
<FilterableQuestionsList
page={page}
total={data?.data.meta.total || 0}
getHref={(page) => `/user/questions/${page}`}
data={{ technology, levels }}
data={{ technology, levels, order, orderBy }}
>
{isSuccess && data.data.data.length > 0 ? (
<Suspense>
Expand Down
9 changes: 8 additions & 1 deletion apps/app/src/hooks/useGetAllQuestions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { PAGE_SIZE } from "../lib/constants";
import { Level } from "../lib/level";
import { Order, OrderBy, sortByLabels } from "../lib/order";
import { QuestionStatus } from "../lib/question";
import { Technology } from "../lib/technologies";
import { getAllQuestions } from "../services/questions.service";
Expand All @@ -11,15 +12,19 @@ export const useGetAllQuestions = ({
levels,
status,
userId,
order,
orderBy,
}: {
page: number;
technology: Technology | null;
levels: Level[] | null;
status?: QuestionStatus;
userId?: number;
order?: Order;
orderBy?: OrderBy;
}) => {
const query = useQuery({
queryKey: ["questions", { page, technology, levels, status, userId }],
queryKey: ["questions", { page, technology, levels, status, userId, order, orderBy }],
queryFn: () =>
getAllQuestions({
limit: PAGE_SIZE,
Expand All @@ -28,6 +33,8 @@ export const useGetAllQuestions = ({
...(levels && { level: levels.join(",") }),
status,
userId,
order,
orderBy,
}),
keepPreviousData: true,
});
Expand Down
20 changes: 11 additions & 9 deletions apps/app/src/lib/order.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { QueryParam } from "../types";

const ordersBy = ["acceptedAt", "level", "votesCount"] as const;
const ordersBy = ["acceptedAt", "level", "votesCount", "updatedAt"] as const;
const orders = ["asc", "desc"] as const;

export const DEFAULT_SORT_BY_QUERY = "acceptedAt*desc";
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",
"acceptedAt*asc": "data dodania: najstarsze",
"acceptedAt*desc": "data dodania: najnowsze",
"level*asc": "trudność: od najtrudniejszych",
grzegorzpokorski marked this conversation as resolved.
Show resolved Hide resolved
"level*desc": "trudność: od najprostszych",
grzegorzpokorski marked this conversation as resolved.
Show resolved Hide resolved
"votesCount*asc": "popularność: najmniejsza",
"votesCount*desc": "popularność: największa",
"updatedAt*asc": "data edycji: najstarsze",
"updatedAt*desc": "data edycji: najnowsze",
};

type OrderBy = typeof ordersBy[number];
type Order = typeof orders[number];
export type OrderBy = typeof ordersBy[number];
export type Order = typeof orders[number];

export const parseQuerySortBy = (query: QueryParam) => {
if (typeof query !== "string") {
Expand Down
Loading