Skip to content

Commit 1de564c

Browse files
CopilotkingRayhan
andauthored
feat: add article sorter to dashboard article list
- Add sort_by (title/created_at/published_at), sort_order (asc/desc), and status (all/published/draft) to myArticleInput schema - Update myArticles action to apply dynamic sorting and status filtering via sqlkit asc/isNull/isNotNull - Add sorter toolbar to DashboardArticleList with status filter tabs, sort field dropdown, and sort order toggle button Agent-Logs-Url: https://github.com/techdiary-dev/techdiary.dev/sessions/a0bc19b6-228e-48b9-8d7b-e5422cb0c559 Co-authored-by: kingRayhan <7611746+kingRayhan@users.noreply.github.com>
1 parent fbd0a74 commit 1de564c

3 files changed

Lines changed: 107 additions & 33 deletions

File tree

src/app/dashboard/_components/DashboardArticleList.tsx

Lines changed: 90 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import VisibilitySensor from "@/components/VisibilitySensor";
1313
import { useTranslation } from "@/i18n/use-translation";
1414
import { actionPromisify, formattedTime } from "@/lib/utils";
1515
import {
16+
ArrowDownIcon,
17+
ArrowUpIcon,
1618
CardStackIcon,
1719
DotsHorizontalIcon,
1820
Pencil1Icon,
@@ -29,15 +31,31 @@ import clsx from "clsx";
2931
import { addDays, differenceInHours } from "date-fns";
3032
import { TrashIcon } from "lucide-react";
3133
import Link from "next/link";
34+
import { useState } from "react";
35+
36+
type SortBy = "created_at" | "title" | "published_at";
37+
type SortOrder = "asc" | "desc";
38+
type StatusFilter = "all" | "published" | "draft";
3239

3340
const DashboardArticleList = () => {
3441
const { _t } = useTranslation();
3542
const queryClient = useQueryClient();
3643
const appConfirm = useAppConfirm();
44+
45+
const [sortBy, setSortBy] = useState<SortBy>("created_at");
46+
const [sortOrder, setSortOrder] = useState<SortOrder>("desc");
47+
const [status, setStatus] = useState<StatusFilter>("all");
48+
3749
const feedInfiniteQuery = useInfiniteQuery({
38-
queryKey: ["dashboard-articles"],
50+
queryKey: ["dashboard-articles", sortBy, sortOrder, status],
3951
queryFn: ({ pageParam }) =>
40-
articleActions.myArticles({ limit: 10, page: pageParam }),
52+
articleActions.myArticles({
53+
limit: 10,
54+
page: pageParam,
55+
sort_by: sortBy,
56+
sort_order: sortOrder,
57+
status,
58+
}),
4159
initialPageParam: 1,
4260
getNextPageParam: (lastPage) => {
4361
const _page = lastPage?.meta?.currentPage ?? 1;
@@ -54,9 +72,9 @@ const DashboardArticleList = () => {
5472
onMutate: async (article_id: string) => {
5573
await queryClient.cancelQueries({ queryKey: ["dashboard-articles"] });
5674

57-
const previousData = queryClient.getQueryData(["dashboard-articles"]);
75+
const previousData = queryClient.getQueryData(["dashboard-articles", sortBy, sortOrder, status]);
5876

59-
queryClient.setQueryData(["dashboard-articles"], (old: any) => {
77+
queryClient.setQueryData(["dashboard-articles", sortBy, sortOrder, status], (old: any) => {
6078
if (!old) return old;
6179

6280
return {
@@ -80,7 +98,7 @@ const DashboardArticleList = () => {
8098
},
8199
onError: (err, variables, context) => {
82100
if (context?.previousData) {
83-
queryClient.setQueryData(["dashboard-articles"], context.previousData);
101+
queryClient.setQueryData(["dashboard-articles", sortBy, sortOrder, status], context.previousData);
84102
}
85103
},
86104
});
@@ -94,9 +112,9 @@ const DashboardArticleList = () => {
94112
onMutate: async (article_id: string) => {
95113
await queryClient.cancelQueries({ queryKey: ["dashboard-articles"] });
96114

97-
const previousData = queryClient.getQueryData(["dashboard-articles"]);
115+
const previousData = queryClient.getQueryData(["dashboard-articles", sortBy, sortOrder, status]);
98116

99-
queryClient.setQueryData(["dashboard-articles"], (old: any) => {
117+
queryClient.setQueryData(["dashboard-articles", sortBy, sortOrder, status], (old: any) => {
100118
if (!old) return old;
101119

102120
return {
@@ -116,11 +134,23 @@ const DashboardArticleList = () => {
116134
},
117135
onError: (_, __, context) => {
118136
if (context?.previousData) {
119-
queryClient.setQueryData(["dashboard-articles"], context.previousData);
137+
queryClient.setQueryData(["dashboard-articles", sortBy, sortOrder, status], context.previousData);
120138
}
121139
},
122140
});
123141

142+
const sortByLabels: Record<SortBy, string> = {
143+
created_at: _t("Created at"),
144+
title: _t("Title"),
145+
published_at: _t("Published at"),
146+
};
147+
148+
const statusFilters: { value: StatusFilter; label: string }[] = [
149+
{ value: "all", label: _t("All") },
150+
{ value: "published", label: _t("Published") },
151+
{ value: "draft", label: _t("Draft") },
152+
];
153+
124154
return (
125155
<div>
126156
<div className="flex items-center gap-2 justify-between">
@@ -134,6 +164,58 @@ const DashboardArticleList = () => {
134164
</Button>
135165
</div>
136166

167+
{/* Sorter toolbar */}
168+
<div className="flex flex-wrap items-center gap-2 mt-3">
169+
{/* Status filter */}
170+
<div className="flex items-center rounded-md border border-border overflow-hidden">
171+
{statusFilters.map((filter) => (
172+
<button
173+
key={filter.value}
174+
onClick={() => setStatus(filter.value)}
175+
className={clsx(
176+
"px-3 py-1.5 text-sm transition-colors",
177+
status === filter.value
178+
? "bg-primary text-primary-foreground"
179+
: "hover:bg-muted"
180+
)}
181+
>
182+
{filter.label}
183+
</button>
184+
))}
185+
</div>
186+
187+
{/* Sort by */}
188+
<DropdownMenu>
189+
<DropdownMenuTrigger asChild>
190+
<Button variant="outline" size="sm" className="text-sm">
191+
{sortByLabels[sortBy]}
192+
</Button>
193+
</DropdownMenuTrigger>
194+
<DropdownMenuContent>
195+
{(Object.keys(sortByLabels) as SortBy[]).map((key) => (
196+
<DropdownMenuItem key={key} onClick={() => setSortBy(key)}>
197+
{sortByLabels[key]}
198+
</DropdownMenuItem>
199+
))}
200+
</DropdownMenuContent>
201+
</DropdownMenu>
202+
203+
{/* Sort order toggle */}
204+
<Button
205+
variant="outline"
206+
size="sm"
207+
onClick={() => setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"))}
208+
className="flex items-center gap-1"
209+
>
210+
{sortOrder === "asc" ? (
211+
<ArrowUpIcon className="h-4 w-4" />
212+
) : (
213+
<ArrowDownIcon className="h-4 w-4" />
214+
)}
215+
<span className="text-sm">{sortOrder === "asc" ? _t("Ascending") : _t("Descending")}</span>
216+
</Button>
217+
</div>
218+
137219
<div className="flex flex-col divide-y divide-dashed divide-border-color mt-2">
138220
{feedInfiniteQuery.isFetching &&
139221
Array.from({ length: 10 }).map((_, i) => (
@@ -176,18 +258,6 @@ const DashboardArticleList = () => {
176258

177259
<div className="flex items-center gap-10 justify-between">
178260
<div className="flex gap-4 items-center">
179-
{/* {!article.approved_at && (
180-
<p className="bg-yellow-400/30 rounded-sm px-2 py-1 text-sm">
181-
🚧 {_t("অনুমোদনাধীন")}
182-
</p>
183-
)} */}
184-
185-
{/* {article.approved_at && (
186-
<p className="bg-green-400/30 rounded-sm px-2 py-1 text-sm">
187-
✅ {_t("অনুমোদিত")}
188-
</p>
189-
)} */}
190-
191261
{!Boolean(article?.published_at) && (
192262
<p className="bg-yellow-400/30 rounded-sm px-2 py-1 text-sm">
193263
🚧 {_t("Draft")}
@@ -199,16 +269,6 @@ const DashboardArticleList = () => {
199269
{_t("Published")}
200270
</p>
201271
)}
202-
203-
{/* <div className="text-forground-muted flex items-center gap-1">
204-
<ChatBubbleIcon className="h-4 w-4" />
205-
<p>{article?.comments_count || 0} </p>
206-
</div> */}
207-
208-
{/* <div className="text-forground-muted flex items-center gap-1">
209-
<ThickArrowUpIcon className="h-4 w-4" />
210-
<p>{article?.votes?.score || 0} </p>
211-
</div> */}
212272
</div>
213273
<DropdownMenu>
214274
<DropdownMenuTrigger className="flex items-center gap-2">

src/backend/services/article.actions.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from "@/lib/utils";
1212
import { addDays } from "date-fns";
1313
import * as sk from "sqlkit";
14-
import { and, desc, eq, like, neq, or } from "sqlkit";
14+
import { and, asc, desc, eq, isNotNull, isNull, like, neq, or } from "sqlkit";
1515
import { z } from "zod/v4";
1616
import { ActionResponse } from "../models/action-contracts";
1717
import { Article, User } from "../models/domain-models";
@@ -664,8 +664,19 @@ export async function myArticles(
664664
}
665665

666666
try {
667+
const sortFn = input.sort_order === "asc" ? asc : desc;
668+
const statusCondition =
669+
input.status === "published"
670+
? isNotNull<Article>("published_at")
671+
: input.status === "draft"
672+
? isNull<Article>("published_at")
673+
: undefined;
674+
667675
const articles = await persistenceRepository.article.paginate({
668-
where: eq("author_id", sessionUserId!),
676+
where: and(
677+
eq("author_id", sessionUserId!),
678+
...(statusCondition ? [statusCondition] : [])
679+
),
669680
columns: [
670681
"id",
671682
"title",
@@ -677,7 +688,7 @@ export async function myArticles(
677688
],
678689
limit: input.limit,
679690
page: input.page,
680-
orderBy: [desc("created_at")],
691+
orderBy: [sortFn(input.sort_by)],
681692
});
682693
return articles;
683694
} catch (error) {

src/backend/services/inputs/article.input.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ export const ArticleRepositoryInput = {
119119
myArticleInput: z.object({
120120
page: z.number().default(1),
121121
limit: z.number().default(10),
122+
sort_by: z.enum(["created_at", "title", "published_at"]).default("created_at"),
123+
sort_order: z.enum(["asc", "desc"]).default("desc"),
124+
status: z.enum(["all", "published", "draft"]).default("all"),
122125
}),
123126

124127
tagFeedInput: z.object({

0 commit comments

Comments
 (0)