diff --git a/apps/api/modules/questions/questions.routes.ts b/apps/api/modules/questions/questions.routes.ts
index 1afc0180..fc6bc035 100644
--- a/apps/api/modules/questions/questions.routes.ts
+++ b/apps/api/modules/questions/questions.routes.ts
@@ -49,6 +49,7 @@ const questionsPlugin: FastifyPluginAsync = async (fastify) => {
levelId: true,
statusId: true,
acceptedAt: true,
+ updatedAt: true,
_count: {
select: {
QuestionVote: true,
@@ -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,
};
});
diff --git a/apps/api/modules/questions/questions.schemas.ts b/apps/api/modules/questions/questions.schemas.ts
index 369e7454..89ffe882 100644
--- a/apps/api/modules/questions/questions.schemas.ts
+++ b/apps/api/modules/questions/questions.schemas.ts
@@ -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(),
@@ -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;
};
diff --git a/apps/app/src/app/(main-layout)/admin/[status]/[page]/page.tsx b/apps/app/src/app/(main-layout)/admin/[status]/[page]/page.tsx
index fe9584c1..c71aa4d2 100644
--- a/apps/app/src/app/(main-layout)/admin/[status]/[page]/page.tsx
+++ b/apps/app/src/app/(main-layout)/admin/[status]/[page]/page.tsx
@@ -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(
() =>
@@ -19,11 +20,12 @@ 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");
@@ -31,7 +33,14 @@ export default function AdminPage({
return (
-
+
);
}
diff --git a/apps/app/src/app/(main-layout)/user/questions/[page]/page.tsx b/apps/app/src/app/(main-layout)/user/questions/[page]/page.tsx
index 90e2d674..9fb363b8 100644
--- a/apps/app/src/app/(main-layout)/user/questions/[page]/page.tsx
+++ b/apps/app/src/app/(main-layout)/user/questions/[page]/page.tsx
@@ -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";
@@ -10,11 +11,12 @@ 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");
@@ -22,7 +24,13 @@ export default function UserQuestionsPage({
return (
-
+
);
}
diff --git a/apps/app/src/components/AdminPanel/AdminPanel.tsx b/apps/app/src/components/AdminPanel/AdminPanel.tsx
index 4e7bd54a..6757a382 100644
--- a/apps/app/src/components/AdminPanel/AdminPanel.tsx
+++ b/apps/app/src/components/AdminPanel/AdminPanel.tsx
@@ -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";
@@ -14,14 +15,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(() => {
@@ -33,7 +45,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 ? (
}>
diff --git a/apps/app/src/components/FilterableQuestionsList/FilterableQuestionsList.tsx b/apps/app/src/components/FilterableQuestionsList/FilterableQuestionsList.tsx
index 5c2a7f6c..650e379c 100644
--- a/apps/app/src/components/FilterableQuestionsList/FilterableQuestionsList.tsx
+++ b/apps/app/src/components/FilterableQuestionsList/FilterableQuestionsList.tsx
@@ -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";
@@ -11,6 +12,8 @@ type FilterableQuestionsListProps = Readonly<{
status?: QuestionStatus;
technology?: Technology | null;
levels?: Level[] | null;
+ order?: Order;
+ orderBy?: OrderBy;
};
children: ReactNode;
}> &
diff --git a/apps/app/src/components/FilterableQuestionsList/FilterableQuestionsListHeader.tsx b/apps/app/src/components/FilterableQuestionsList/FilterableQuestionsListHeader.tsx
index 69f2d20b..7fd15e77 100644
--- a/apps/app/src/components/FilterableQuestionsList/FilterableQuestionsListHeader.tsx
+++ b/apps/app/src/components/FilterableQuestionsList/FilterableQuestionsListHeader.tsx
@@ -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 }) => (
-
-);
-
export const FilterableQuestionsListHeader = ({
status,
technology,
levels: selectedLevels,
+ order,
+ orderBy,
}: FilterableQuestionsListHeaderProps) => {
const { mergeQueryParams } = useDevFAQRouter();
const router = useRouter();
@@ -66,6 +69,9 @@ export const FilterableQuestionsListHeader = ({
)}
+ {order && orderBy && (
+
+ )}
{status !== undefined && (
Status:
diff --git a/apps/app/src/components/QuestionsHeader.tsx b/apps/app/src/components/QuestionsHeader.tsx
index 2f62e123..2b8f0ca0 100644
--- a/apps/app/src/components/QuestionsHeader.tsx
+++ b/apps/app/src/components/QuestionsHeader.tsx
@@ -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ń");
@@ -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) => {
event.preventDefault();
@@ -28,16 +29,9 @@ export const QuestionsHeader = ({ technology, total }: QuestionsHeaderProps) =>
{technologiesLabels[technology]}:
{total} {questionsPluralize(total)}
-
+ {sort?.order && sort?.orderBy && (
+
+ )}
);
};
diff --git a/apps/app/src/components/SelectLabel.tsx b/apps/app/src/components/SelectLabel.tsx
new file mode 100644
index 00000000..ff0c95ec
--- /dev/null
+++ b/apps/app/src/components/SelectLabel.tsx
@@ -0,0 +1,5 @@
+import { ReactNode } from "react";
+
+export const SelectLabel = ({ children }: { readonly children: ReactNode }) => (
+
+);
diff --git a/apps/app/src/components/SortBySelect.tsx b/apps/app/src/components/SortBySelect.tsx
new file mode 100644
index 00000000..d8d8f952
--- /dev/null
+++ b/apps/app/src/components/SortBySelect.tsx
@@ -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) => void;
+};
+
+export const SortBySelect = ({ order, orderBy, onChange }: SortBySelectProps) => (
+
+ Sortowanie:
+
+
+);
diff --git a/apps/app/src/components/UserQuestions/UserQuestions.tsx b/apps/app/src/components/UserQuestions/UserQuestions.tsx
index c4e4130c..384ab51e 100644
--- a/apps/app/src/components/UserQuestions/UserQuestions.tsx
+++ b/apps/app/src/components/UserQuestions/UserQuestions.tsx
@@ -3,6 +3,7 @@
import { Suspense, useCallback } 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 { Loading } from "../Loading";
@@ -13,15 +14,19 @@ 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, refetch } = useGetAllQuestions({
page,
technology,
levels,
userId: userData?._user.id,
+ order,
+ orderBy,
});
const refetchQuestions = useCallback(() => {
@@ -33,7 +38,7 @@ export const UserQuestions = ({ page, technology, levels }: UserQuestionsProps)
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 ? (
{
const query = useQuery({
- queryKey: ["questions", { page, technology, levels, status, userId }],
+ queryKey: ["questions", { page, technology, levels, status, userId, order, orderBy }],
queryFn: () =>
getAllQuestions({
limit: PAGE_SIZE,
@@ -28,6 +33,8 @@ export const useGetAllQuestions = ({
...(levels && { level: levels.join(",") }),
status,
userId,
+ order,
+ orderBy,
}),
keepPreviousData: true,
});
diff --git a/apps/app/src/lib/order.ts b/apps/app/src/lib/order.ts
index b622ad27..bcdbb922 100644
--- a/apps/app/src/lib/order.ts
+++ b/apps/app/src/lib/order.ts
@@ -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 najłatwiejszych",
+ "level*desc": "trudność: od najtrudniejszych",
+ "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") {
diff --git a/packages/openapi-types/types.ts b/packages/openapi-types/types.ts
index 3bd75e52..9033a4eb 100644
--- a/packages/openapi-types/types.ts
+++ b/packages/openapi-types/types.ts
@@ -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;
};
@@ -97,6 +97,8 @@ export interface paths {
_statusId: "pending" | "accepted";
/** Format: date-time */
acceptedAt?: string;
+ /** Format: date-time */
+ updatedAt?: string;
votesCount: number;
}[];
meta: {
@@ -130,6 +132,8 @@ export interface paths {
_statusId: "pending" | "accepted";
/** Format: date-time */
acceptedAt?: string;
+ /** Format: date-time */
+ updatedAt?: string;
votesCount: number;
};
};
@@ -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;
};
@@ -246,6 +250,8 @@ export interface paths {
_statusId: "pending" | "accepted";
/** Format: date-time */
acceptedAt?: string;
+ /** Format: date-time */
+ updatedAt?: string;
votesCount: number;
};
};
@@ -293,6 +299,8 @@ export interface paths {
_statusId: "pending" | "accepted";
/** Format: date-time */
acceptedAt?: string;
+ /** Format: date-time */
+ updatedAt?: string;
votesCount: number;
};
};