Skip to content
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>
)}
{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,
}),
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)",
};

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