aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaksh Pareek <dakshpareek7@gmail.com>2025-01-12 21:53:34 +0530
committerGitHub <noreply@github.com>2025-01-12 16:23:34 +0000
commitb6293d118e7545b81e216073e66cd54c5b1a0b00 (patch)
tree19c2308ea7ea26593d890770945a2c5c71440048
parentb8bd7d7eb27aaaadae728599f64a0874f66196ea (diff)
downloadkarakeep-b6293d118e7545b81e216073e66cd54c5b1a0b00.tar.zst
feat: Add Bookmark Sorting Feature (#812)
* feat: add bookmark sorting by creation date - Add sort order toggle in GlobalActions component - Implement ascending/descending sort functionality - Update translations for sorting feature in all languages - Add sort order icons and dropdown menu - Maintain sort preference in URL params * feat: add bookmark sorting by creation date - Add sort order toggle in GlobalActions component - Implement ascending/descending sort functionality - Update translations for sorting feature in all languages - Add sort order icons and dropdown menu - Maintain sort preference in URL params during session Note: Sort order resets to default on page refresh, server-side persistence can be implemented in future enhancement * feat: Add global sort by date feature with shared sort order state - Implement global sort order functionality using a shared Zustand store (`useSortOrder` hook). - Update `getBookmarks` and `searchBookmarks` endpoints to accept a `sortOrder` parameter. - Refactor code to import `ZSortOrder` from shared types (`bookmarks.ts`), ensuring consistency across the codebase. - Update components (`UpdatableBookmarksGrid`, `bookmark-search`) to use the shared `useSortOrder` hook. - Remove unused `zSortBy` definition from `packages/shared/types/bookmarks.ts` to avoid confusion. - Ensure consistent naming conventions by prefixing Zod inferred types with `Z`. - Clean up code and address previous PR feedback comments. * tiny fixes and fixing TS errors --------- Co-authored-by: Mohamed Bassem <me@mbassem.com>
-rw-r--r--apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx2
-rw-r--r--apps/web/components/dashboard/GlobalActions.tsx2
-rw-r--r--apps/web/components/dashboard/SortOrderToggle.tsx56
-rw-r--r--apps/web/components/dashboard/bookmarks/Bookmarks.tsx2
-rw-r--r--apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx19
-rw-r--r--apps/web/lib/hooks/bookmark-search.ts9
-rw-r--r--apps/web/lib/i18n/locales/da/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/de/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/en/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/es/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/fr/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/hr/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/it/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/ja/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/nl/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/pl/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/ru/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/sv/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/tr/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/zh/translation.json7
-rw-r--r--apps/web/lib/i18n/locales/zhtw/translation.json7
-rw-r--r--apps/web/lib/store/useSortOrderStore.ts13
-rw-r--r--packages/shared/types/bookmarks.ts5
-rw-r--r--packages/trpc/routers/bookmarks.ts50
24 files changed, 231 insertions, 32 deletions
diff --git a/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx b/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx
index 8644dcbf..99bb5ab8 100644
--- a/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx
+++ b/apps/mobile/components/bookmarks/UpdatingBookmarkList.tsx
@@ -11,7 +11,7 @@ export default function UpdatingBookmarkList({
query,
header,
}: {
- query: ZGetBookmarksRequest;
+ query: Omit<ZGetBookmarksRequest, "sortOrder">; // Sort order is not supported in mobile yet
header?: React.ReactElement;
}) {
const apiUtils = api.useUtils();
diff --git a/apps/web/components/dashboard/GlobalActions.tsx b/apps/web/components/dashboard/GlobalActions.tsx
index 9c05dddf..ecbb70bf 100644
--- a/apps/web/components/dashboard/GlobalActions.tsx
+++ b/apps/web/components/dashboard/GlobalActions.tsx
@@ -2,11 +2,13 @@
import BulkBookmarksAction from "@/components/dashboard/BulkBookmarksAction";
import ChangeLayout from "@/components/dashboard/ChangeLayout";
+import SortOrderToggle from "@/components/dashboard/SortOrderToggle";
export default function GlobalActions() {
return (
<div className="flex min-w-max flex-wrap overflow-hidden">
<ChangeLayout />
+ <SortOrderToggle />
<BulkBookmarksAction />
</div>
);
diff --git a/apps/web/components/dashboard/SortOrderToggle.tsx b/apps/web/components/dashboard/SortOrderToggle.tsx
new file mode 100644
index 00000000..8c0f617d
--- /dev/null
+++ b/apps/web/components/dashboard/SortOrderToggle.tsx
@@ -0,0 +1,56 @@
+import { ButtonWithTooltip } from "@/components/ui/button";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { useTranslation } from "@/lib/i18n/client";
+import { useSortOrderStore } from "@/lib/store/useSortOrderStore";
+import { Check, SortAsc, SortDesc } from "lucide-react";
+
+export default function SortOrderToggle() {
+ const { t } = useTranslation();
+
+ const { sortOrder: currentSort, setSortOrder } = useSortOrderStore();
+
+ return (
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <ButtonWithTooltip
+ tooltip={t("actions.sort.title")}
+ delayDuration={100}
+ variant="ghost"
+ >
+ {currentSort === "asc" ? (
+ <SortAsc size={18} />
+ ) : (
+ <SortDesc size={18} />
+ )}
+ </ButtonWithTooltip>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="w-fit">
+ <DropdownMenuItem
+ className="cursor-pointer justify-between"
+ onClick={() => setSortOrder("desc")}
+ >
+ <div className="flex items-center">
+ <SortDesc size={16} className="mr-2" />
+ <span>{t("actions.sort.newest_first")}</span>
+ </div>
+ {currentSort === "desc" && <Check className="ml-2 h-4 w-4" />}
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ className="cursor-pointer justify-between"
+ onClick={() => setSortOrder("asc")}
+ >
+ <div className="flex items-center">
+ <SortAsc size={16} className="mr-2" />
+ <span>{t("actions.sort.oldest_first")}</span>
+ </div>
+ {currentSort === "asc" && <Check className="ml-2 h-4 w-4" />}
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
+ );
+}
diff --git a/apps/web/components/dashboard/bookmarks/Bookmarks.tsx b/apps/web/components/dashboard/bookmarks/Bookmarks.tsx
index 5729e846..3f606346 100644
--- a/apps/web/components/dashboard/bookmarks/Bookmarks.tsx
+++ b/apps/web/components/dashboard/bookmarks/Bookmarks.tsx
@@ -13,7 +13,7 @@ export default async function Bookmarks({
showDivider,
showEditorCard = false,
}: {
- query: ZGetBookmarksRequest;
+ query: Omit<ZGetBookmarksRequest, "sortOrder">; // Sort order is handled by the store
header?: React.ReactNode;
showDivider?: boolean;
showEditorCard?: boolean;
diff --git a/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx b/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx
index d18eeb1b..e43d061b 100644
--- a/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx
+++ b/apps/web/components/dashboard/bookmarks/UpdatableBookmarksGrid.tsx
@@ -1,6 +1,8 @@
"use client";
+import { useEffect } from "react";
import UploadDropzone from "@/components/dashboard/UploadDropzone";
+import { useSortOrderStore } from "@/lib/store/useSortOrderStore";
import { api } from "@/lib/trpc";
import type {
@@ -16,14 +18,18 @@ export default function UpdatableBookmarksGrid({
bookmarks: initialBookmarks,
showEditorCard = false,
}: {
- query: ZGetBookmarksRequest;
+ query: Omit<ZGetBookmarksRequest, "sortOrder">; // Sort order is handled by the store
bookmarks: ZGetBookmarksResponse;
showEditorCard?: boolean;
itemsPerPage?: number;
}) {
- const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
+ const sortOrder = useSortOrderStore((state) => state.sortOrder);
+
+ const finalQuery = { ...query, sortOrder };
+
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } =
api.bookmarks.getBookmarks.useInfiniteQuery(
- { ...query, useCursorV2: true },
+ { ...finalQuery, useCursorV2: true },
{
initialData: () => ({
pages: [initialBookmarks],
@@ -31,9 +37,14 @@ export default function UpdatableBookmarksGrid({
}),
initialCursor: null,
getNextPageParam: (lastPage) => lastPage.nextCursor,
+ refetchOnMount: true,
},
);
+ useEffect(() => {
+ refetch();
+ }, [sortOrder, refetch]);
+
const grid = (
<BookmarksGrid
bookmarks={data!.pages.flatMap((b) => b.bookmarks)}
@@ -45,7 +56,7 @@ export default function UpdatableBookmarksGrid({
);
return (
- <BookmarkGridContextProvider query={query}>
+ <BookmarkGridContextProvider query={finalQuery}>
{showEditorCard ? <UploadDropzone>{grid}</UploadDropzone> : grid}
</BookmarkGridContextProvider>
);
diff --git a/apps/web/lib/hooks/bookmark-search.ts b/apps/web/lib/hooks/bookmark-search.ts
index 386355f7..5ffec1b0 100644
--- a/apps/web/lib/hooks/bookmark-search.ts
+++ b/apps/web/lib/hooks/bookmark-search.ts
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
+import { useSortOrderStore } from "@/lib/store/useSortOrderStore";
import { api } from "@/lib/trpc";
import { keepPreviousData } from "@tanstack/react-query";
@@ -8,6 +9,7 @@ import { parseSearchQuery } from "@hoarder/shared/searchQueryParser";
function useSearchQuery() {
const searchParams = useSearchParams();
const searchQuery = decodeURIComponent(searchParams.get("q") ?? "");
+
const parsed = useMemo(() => parseSearchQuery(searchQuery), [searchQuery]);
return { searchQuery, parsedSearchQuery: parsed };
}
@@ -53,6 +55,7 @@ export function useDoBookmarkSearch() {
export function useBookmarkSearch() {
const { searchQuery } = useSearchQuery();
+ const sortOrder = useSortOrderStore((state) => state.sortOrder);
const {
data,
@@ -62,9 +65,11 @@ export function useBookmarkSearch() {
hasNextPage,
fetchNextPage,
isFetchingNextPage,
+ refetch,
} = api.bookmarks.searchBookmarks.useInfiniteQuery(
{
text: searchQuery,
+ sortOrder,
},
{
placeholderData: keepPreviousData,
@@ -74,6 +79,10 @@ export function useBookmarkSearch() {
},
);
+ useEffect(() => {
+ refetch();
+ }, [refetch, sortOrder]);
+
if (error) {
throw error;
}
diff --git a/apps/web/lib/i18n/locales/da/translation.json b/apps/web/lib/i18n/locales/da/translation.json
index 2ef97e76..d4744bac 100644
--- a/apps/web/lib/i18n/locales/da/translation.json
+++ b/apps/web/lib/i18n/locales/da/translation.json
@@ -30,7 +30,12 @@
"close": "Luk",
"edit_tags": "Rediger tags",
"save": "Gem",
- "merge": "Sammenflet"
+ "merge": "Sammenflet",
+ "sort": {
+ "title": "Sortér",
+ "newest_first": "Nyeste først",
+ "oldest_first": "Ældste først"
+ }
},
"settings": {
"import": {
diff --git a/apps/web/lib/i18n/locales/de/translation.json b/apps/web/lib/i18n/locales/de/translation.json
index d868b7f9..cb376be6 100644
--- a/apps/web/lib/i18n/locales/de/translation.json
+++ b/apps/web/lib/i18n/locales/de/translation.json
@@ -63,7 +63,12 @@
"cancel": "Abbrechen",
"apply_all": "Alle anwenden",
"ignore": "Ignorieren",
- "recrawl": "Erneutes Crawlen"
+ "recrawl": "Erneutes Crawlen",
+ "sort": {
+ "title": "Sortieren",
+ "newest_first": "Neueste zuerst",
+ "oldest_first": "Älteste zuerst"
+ }
},
"settings": {
"back_to_app": "Zurück zur App",
diff --git a/apps/web/lib/i18n/locales/en/translation.json b/apps/web/lib/i18n/locales/en/translation.json
index d23d5a96..ac08fa3f 100644
--- a/apps/web/lib/i18n/locales/en/translation.json
+++ b/apps/web/lib/i18n/locales/en/translation.json
@@ -63,7 +63,12 @@
"merge": "Merge",
"cancel": "Cancel",
"apply_all": "Apply All",
- "ignore": "Ignore"
+ "ignore": "Ignore",
+ "sort": {
+ "title": "Sort",
+ "newest_first": "Newest First",
+ "oldest_first": "Oldest First"
+ }
},
"highlights": {
"no_highlights": "You don't have any highlights yet."
diff --git a/apps/web/lib/i18n/locales/es/translation.json b/apps/web/lib/i18n/locales/es/translation.json
index 55e60ab5..40c6cb01 100644
--- a/apps/web/lib/i18n/locales/es/translation.json
+++ b/apps/web/lib/i18n/locales/es/translation.json
@@ -107,7 +107,12 @@
"recrawl": "Volver a crawlear",
"close_bulk_edit": "Cerrar editor en masa",
"bulk_edit": "Editar en masa",
- "manage_lists": "Administrar listas"
+ "manage_lists": "Administrar listas",
+ "sort": {
+ "title": "Ordenar",
+ "newest_first": "Más recientes primero",
+ "oldest_first": "Más antiguos primero"
+ }
},
"layouts": {
"compact": "Compacto",
diff --git a/apps/web/lib/i18n/locales/fr/translation.json b/apps/web/lib/i18n/locales/fr/translation.json
index 142e9148..dade8d21 100644
--- a/apps/web/lib/i18n/locales/fr/translation.json
+++ b/apps/web/lib/i18n/locales/fr/translation.json
@@ -60,7 +60,12 @@
"merge": "Fusionner",
"cancel": "Annuler",
"apply_all": "Tout appliquer",
- "ignore": "Ignorer"
+ "ignore": "Ignorer",
+ "sort": {
+ "title": "Trier",
+ "newest_first": "Plus récents d'abord",
+ "oldest_first": "Plus anciens d'abord"
+ }
},
"settings": {
"back_to_app": "Retour à l'application",
diff --git a/apps/web/lib/i18n/locales/hr/translation.json b/apps/web/lib/i18n/locales/hr/translation.json
index 83ae3851..6e250924 100644
--- a/apps/web/lib/i18n/locales/hr/translation.json
+++ b/apps/web/lib/i18n/locales/hr/translation.json
@@ -153,7 +153,12 @@
"close": "Zatvori",
"merge": "Spoji",
"cancel": "Otkaži",
- "apply_all": "Primijeni sve"
+ "apply_all": "Primijeni sve",
+ "sort": {
+ "title": "Sortiraj",
+ "newest_first": "Najnovije prvo",
+ "oldest_first": "Najstarije prvo"
+ }
},
"highlights": {
"no_highlights": "Još nemate nijednu istaknutu stavku."
diff --git a/apps/web/lib/i18n/locales/it/translation.json b/apps/web/lib/i18n/locales/it/translation.json
index 6479f6c8..e24b6b7f 100644
--- a/apps/web/lib/i18n/locales/it/translation.json
+++ b/apps/web/lib/i18n/locales/it/translation.json
@@ -30,7 +30,12 @@
"merge": "Unisci",
"cancel": "Cancella",
"apply_all": "Applica a tutto",
- "copy_link": "Copia link"
+ "copy_link": "Copia link",
+ "sort": {
+ "title": "Ordina",
+ "newest_first": "Prima i più recenti",
+ "oldest_first": "Prima i più vecchi"
+ }
},
"common": {
"attachments": "Allegati",
diff --git a/apps/web/lib/i18n/locales/ja/translation.json b/apps/web/lib/i18n/locales/ja/translation.json
index e44603bb..76767fcc 100644
--- a/apps/web/lib/i18n/locales/ja/translation.json
+++ b/apps/web/lib/i18n/locales/ja/translation.json
@@ -30,7 +30,12 @@
"recrawl": "再クロール",
"ignore": "無視する",
"cancel": "キャンセル",
- "download_full_page_archive": "全てのページアーカイブをダウンロード"
+ "download_full_page_archive": "全てのページアーカイブをダウンロード",
+ "sort": {
+ "title": "並び替え",
+ "newest_first": "新しい順",
+ "oldest_first": "古い順"
+ }
},
"admin": {
"actions": {
diff --git a/apps/web/lib/i18n/locales/nl/translation.json b/apps/web/lib/i18n/locales/nl/translation.json
index 08953704..569e38bc 100644
--- a/apps/web/lib/i18n/locales/nl/translation.json
+++ b/apps/web/lib/i18n/locales/nl/translation.json
@@ -62,7 +62,12 @@
"close": "Sluiten",
"merge": "Samenvoegen",
"cancel": "Annuleer",
- "ignore": "Negeren"
+ "ignore": "Negeren",
+ "sort": {
+ "title": "Sorteren",
+ "newest_first": "Nieuwste eerst",
+ "oldest_first": "Oudste eerst"
+ }
},
"settings": {
"ai": {
diff --git a/apps/web/lib/i18n/locales/pl/translation.json b/apps/web/lib/i18n/locales/pl/translation.json
index b072b56d..0d026542 100644
--- a/apps/web/lib/i18n/locales/pl/translation.json
+++ b/apps/web/lib/i18n/locales/pl/translation.json
@@ -55,7 +55,12 @@
"sign_out": "Wyloguj się",
"merge": "Scal",
"cancel": "Anuluj",
- "apply_all": "Zastosuj wszystko"
+ "apply_all": "Zastosuj wszystko",
+ "sort": {
+ "title": "Sortowanie",
+ "newest_first": "Najnowsze pierwsze",
+ "oldest_first": "Najstarsze pierwsze"
+ }
},
"settings": {
"info": {
diff --git a/apps/web/lib/i18n/locales/ru/translation.json b/apps/web/lib/i18n/locales/ru/translation.json
index eabe2e3f..4a8cdd52 100644
--- a/apps/web/lib/i18n/locales/ru/translation.json
+++ b/apps/web/lib/i18n/locales/ru/translation.json
@@ -110,7 +110,12 @@
"copy_link": "Скопировать ссылку",
"unselect_all": "Отменить выбор",
"select_all": "Выбрать все",
- "apply_all": "Применить всё"
+ "apply_all": "Применить всё",
+ "sort": {
+ "title": "Сортировка",
+ "newest_first": "Сначала новые",
+ "oldest_first": "Сначала старые"
+ }
},
"editor": {
"text_toolbar": {
diff --git a/apps/web/lib/i18n/locales/sv/translation.json b/apps/web/lib/i18n/locales/sv/translation.json
index 9adf3bec..26db4c88 100644
--- a/apps/web/lib/i18n/locales/sv/translation.json
+++ b/apps/web/lib/i18n/locales/sv/translation.json
@@ -55,7 +55,12 @@
"edit_tags": "Ändra taggar",
"summarize_with_ai": "Sammanfatta med AI",
"save": "Spara",
- "merge": "Sammanfoga"
+ "merge": "Sammanfoga",
+ "sort": {
+ "title": "Sortera",
+ "newest_first": "Nyast först",
+ "oldest_first": "Äldst först"
+ }
},
"settings": {
"back_to_app": "Tillbaka till app",
diff --git a/apps/web/lib/i18n/locales/tr/translation.json b/apps/web/lib/i18n/locales/tr/translation.json
index b14df6d7..9840c6f0 100644
--- a/apps/web/lib/i18n/locales/tr/translation.json
+++ b/apps/web/lib/i18n/locales/tr/translation.json
@@ -63,7 +63,12 @@
"merge": "Birleştir",
"cancel": "İptal",
"apply_all": "Hepsine Uygula",
- "ignore": "Yoksay"
+ "ignore": "Yoksay",
+ "sort": {
+ "title": "Sırala",
+ "newest_first": "En yeni önce",
+ "oldest_first": "En eski önce"
+ }
},
"highlights": {
"no_highlights": "Henüz hiçbir öne çıkarılmış içeriğiniz yok."
diff --git a/apps/web/lib/i18n/locales/zh/translation.json b/apps/web/lib/i18n/locales/zh/translation.json
index be09b221..87d098a6 100644
--- a/apps/web/lib/i18n/locales/zh/translation.json
+++ b/apps/web/lib/i18n/locales/zh/translation.json
@@ -61,7 +61,12 @@
"cancel": "取消",
"apply_all": "全部应用",
"ignore": "忽略",
- "recrawl": "重新抓取"
+ "recrawl": "重新抓取",
+ "sort": {
+ "title": "排序",
+ "newest_first": "最新优先",
+ "oldest_first": "最早优先"
+ }
},
"settings": {
"back_to_app": "返回应用",
diff --git a/apps/web/lib/i18n/locales/zhtw/translation.json b/apps/web/lib/i18n/locales/zhtw/translation.json
index bcdb83fb..693b0c0a 100644
--- a/apps/web/lib/i18n/locales/zhtw/translation.json
+++ b/apps/web/lib/i18n/locales/zhtw/translation.json
@@ -60,7 +60,12 @@
"merge": "合併",
"cancel": "取消",
"apply_all": "全部套用",
- "ignore": "忽略"
+ "ignore": "忽略",
+ "sort": {
+ "title": "排序",
+ "newest_first": "最新優先",
+ "oldest_first": "最舊優先"
+ }
},
"settings": {
"back_to_app": "返回應用程式",
diff --git a/apps/web/lib/store/useSortOrderStore.ts b/apps/web/lib/store/useSortOrderStore.ts
new file mode 100644
index 00000000..217e142e
--- /dev/null
+++ b/apps/web/lib/store/useSortOrderStore.ts
@@ -0,0 +1,13 @@
+import { create } from "zustand";
+
+import { ZSortOrder } from "@hoarder/shared/types/bookmarks";
+
+interface SortOrderState {
+ sortOrder: ZSortOrder;
+ setSortOrder: (sortOrder: ZSortOrder) => void;
+}
+
+export const useSortOrderStore = create<SortOrderState>((set) => ({
+ sortOrder: "desc", // default sort order
+ setSortOrder: (sortOrder) => set({ sortOrder }),
+}));
diff --git a/packages/shared/types/bookmarks.ts b/packages/shared/types/bookmarks.ts
index 0a414ff9..650f151c 100644
--- a/packages/shared/types/bookmarks.ts
+++ b/packages/shared/types/bookmarks.ts
@@ -12,6 +12,9 @@ export const enum BookmarkTypes {
UNKNOWN = "unknown",
}
+export const zSortOrder = z.enum(["asc", "desc"]);
+export type ZSortOrder = z.infer<typeof zSortOrder>;
+
export const zAssetTypesSchema = z.enum([
"screenshot",
"bannerImage",
@@ -166,6 +169,7 @@ export const zGetBookmarksRequestSchema = z.object({
// The value is currently not being used, but keeping it so that client can still set it to true for older
// servers.
useCursorV2: z.boolean().optional(),
+ sortOrder: zSortOrder.optional().default("desc"),
});
export type ZGetBookmarksRequest = z.infer<typeof zGetBookmarksRequestSchema>;
@@ -211,4 +215,5 @@ export const zSearchBookmarksRequestSchema = z.object({
text: z.string(),
limit: z.number().max(MAX_NUM_BOOKMARKS_PER_PAGE).optional(),
cursor: zSearchBookmarksCursor.nullish(),
+ sortOrder: zSortOrder.optional().default("desc"),
});
diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts
index f7f8e6b7..349ff688 100644
--- a/packages/trpc/routers/bookmarks.ts
+++ b/packages/trpc/routers/bookmarks.ts
@@ -1,5 +1,17 @@
import { experimental_trpcMiddleware, TRPCError } from "@trpc/server";
-import { and, desc, eq, exists, gt, inArray, lt, lte, or } from "drizzle-orm";
+import {
+ and,
+ asc,
+ desc,
+ eq,
+ exists,
+ gt,
+ gte,
+ inArray,
+ lt,
+ lte,
+ or,
+} from "drizzle-orm";
import invariant from "tiny-invariant";
import { z } from "zod";
@@ -566,6 +578,7 @@ export const bookmarksAppRouter = router({
if (!input.limit) {
input.limit = DEFAULT_NUM_BOOKMARKS_PER_PAGE;
}
+ const sortOrder = input.sortOrder || "desc";
const client = await getSearchIdxClient();
if (!client) {
throw new TRPCError({
@@ -592,7 +605,7 @@ export const bookmarksAppRouter = router({
filter,
showRankingScore: true,
attributesToRetrieve: ["id"],
- sort: ["createdAt:desc"],
+ sort: [`createdAt:${sortOrder}`],
limit: input.limit,
...(input.cursor
? {
@@ -734,18 +747,31 @@ export const bookmarksAppRouter = router({
)
: undefined,
input.cursor
- ? or(
- lt(bookmarks.createdAt, input.cursor.createdAt),
- and(
- eq(bookmarks.createdAt, input.cursor.createdAt),
- lte(bookmarks.id, input.cursor.id),
- ),
- )
+ ? input.sortOrder === "asc"
+ ? or(
+ gt(bookmarks.createdAt, input.cursor.createdAt),
+ and(
+ eq(bookmarks.createdAt, input.cursor.createdAt),
+ gte(bookmarks.id, input.cursor.id),
+ ),
+ )
+ : or(
+ lt(bookmarks.createdAt, input.cursor.createdAt),
+ and(
+ eq(bookmarks.createdAt, input.cursor.createdAt),
+ lte(bookmarks.id, input.cursor.id),
+ ),
+ )
: undefined,
),
)
.limit(input.limit + 1)
- .orderBy(desc(bookmarks.createdAt), desc(bookmarks.id)),
+ .orderBy(
+ input.sortOrder === "asc"
+ ? asc(bookmarks.createdAt)
+ : desc(bookmarks.createdAt),
+ desc(bookmarks.id),
+ ),
);
// TODO: Consider not inlining the tags in the response of getBookmarks as this query is getting kinda expensive
const results = await ctx.db
@@ -849,7 +875,9 @@ export const bookmarksAppRouter = router({
bookmarksArr.sort((a, b) => {
if (a.createdAt != b.createdAt) {
- return b.createdAt.getTime() - a.createdAt.getTime();
+ return input.sortOrder === "asc"
+ ? a.createdAt.getTime() - b.createdAt.getTime()
+ : b.createdAt.getTime() - a.createdAt.getTime();
} else {
return b.id.localeCompare(a.id);
}