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,6 +2,7 @@ import { useRouter } from "next/navigation";
import { ChangeEvent, ReactNode } from "react";
import { useDevFAQRouter } from "../../hooks/useDevFAQRouter";
import { levels } from "../../lib/level";
import { Order, OrderBy, sortByLabels } from "../../lib/order";
import { QuestionStatus, statuses } from "../../lib/question";
import { technologies, technologiesLabels, Technology } from "../../lib/technologies";
import { Level } from "../QuestionItem/QuestionLevel";
Expand All @@ -11,6 +12,8 @@ type FilterableQuestionsListHeaderProps = Readonly<{
status?: QuestionStatus;
technology?: Technology | null;
levels?: Level[] | null;
order?: Order;
orderBy?: OrderBy;
}>;

const SelectLabel = ({ children }: { readonly children: ReactNode }) => (
Expand All @@ -21,6 +24,8 @@ export const FilterableQuestionsListHeader = ({
status,
technology,
levels: selectedLevels,
order,
orderBy,
}: FilterableQuestionsListHeaderProps) => {
const { mergeQueryParams } = useDevFAQRouter();
const router = useRouter();
Expand Down Expand Up @@ -66,6 +71,22 @@ export const FilterableQuestionsListHeader = ({
</Select>
</SelectLabel>
)}
{order && orderBy && (
<SelectLabel>
Sortuj według:
<Select
variant="default"
value={`${orderBy}*${order}`}
onChange={handleSelectChange("sortBy")}
>
{Object.entries(sortByLabels).map(([sortBy, label]) => (
<option key={sortBy} value={sortBy}>
{label}
</option>
))}
</Select>
</SelectLabel>
grzegorzpokorski marked this conversation as resolved.
Show resolved Hide resolved
)}
{status !== undefined && (
<SelectLabel>
Status:
Expand Down
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: order,
orderBy: orderBy,
grzegorzpokorski marked this conversation as resolved.
Show resolved Hide resolved
}),
keepPreviousData: true,
});
Expand Down
8 changes: 5 additions & 3 deletions apps/app/src/lib/order.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
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";
Expand All @@ -11,10 +11,12 @@ export const sortByLabels: Record<`${OrderBy}*${Order}`, string> = {
"level*desc": "od najtrudniejszych",
"votesCount*asc": "od najmniej popularnych",
"votesCount*desc": "od najpopularniejszych",
"updatedAt*desc": "daty edycji (najnowsze)",
"updatedAt*asc": "daty edycji (najstarsze)",
grzegorzpokorski marked this conversation as resolved.
Show resolved Hide resolved
};

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
12 changes: 10 additions & 2 deletions packages/openapi-types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export interface paths {
level?: string;
limit?: number;
offset?: number;
orderBy?: "acceptedAt" | "level" | "votesCount";
orderBy?: "acceptedAt" | "level" | "votesCount" | "updatedAt";
order?: "asc" | "desc";
userId?: number;
};
Expand All @@ -97,6 +97,8 @@ export interface paths {
_statusId: "pending" | "accepted";
/** Format: date-time */
acceptedAt?: string;
/** Format: date-time */
updatedAt?: string;
votesCount: number;
}[];
meta: {
Expand Down Expand Up @@ -130,6 +132,8 @@ export interface paths {
_statusId: "pending" | "accepted";
/** Format: date-time */
acceptedAt?: string;
/** Format: date-time */
updatedAt?: string;
votesCount: number;
};
};
Expand All @@ -147,7 +151,7 @@ export interface paths {
level?: string;
limit?: number;
offset?: number;
orderBy?: "acceptedAt" | "level" | "votesCount";
orderBy?: "acceptedAt" | "level" | "votesCount" | "updatedAt";
order?: "asc" | "desc";
userId?: number;
};
Expand Down Expand Up @@ -246,6 +250,8 @@ export interface paths {
_statusId: "pending" | "accepted";
/** Format: date-time */
acceptedAt?: string;
/** Format: date-time */
updatedAt?: string;
votesCount: number;
};
};
Expand Down Expand Up @@ -293,6 +299,8 @@ export interface paths {
_statusId: "pending" | "accepted";
/** Format: date-time */
acceptedAt?: string;
/** Format: date-time */
updatedAt?: string;
votesCount: number;
};
};
Expand Down