From 82df9dfd363331964e7a640e07368e83adbc75ae Mon Sep 17 00:00:00 2001 From: Jaleel Bennett Date: Thu, 1 Aug 2024 15:20:36 -0400 Subject: [PATCH] feat(bookmarks): implemented bookmark views and order by management --- app/bookmarks/bookmark-button.tsx | 49 --- app/bookmarks/bookmark-wrapper.tsx | 5 +- app/bookmarks/components/bookmark-list.tsx | 78 ++++- .../components/bookmarks-display.tsx | 328 ++++++++++++++++++ .../components/bookmarks-search-box.tsx | 88 +++++ app/bookmarks/layout.tsx | 4 +- app/bookmarks/page.tsx | 4 +- components/icons.tsx | 2 + components/menu.tsx | 2 +- components/ui/popover.tsx | 31 ++ lib/parser.ts | 48 ++- package.json | 1 + pnpm-lock.yaml | 26 ++ 13 files changed, 581 insertions(+), 85 deletions(-) delete mode 100644 app/bookmarks/bookmark-button.tsx create mode 100644 app/bookmarks/components/bookmarks-display.tsx create mode 100644 app/bookmarks/components/bookmarks-search-box.tsx create mode 100644 components/ui/popover.tsx diff --git a/app/bookmarks/bookmark-button.tsx b/app/bookmarks/bookmark-button.tsx deleted file mode 100644 index edcf064..0000000 --- a/app/bookmarks/bookmark-button.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { Icons } from "@/components/icons"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { getCurrentUser } from "@/lib/session"; -import { getUser } from "@/data-access/users"; -import { SparkleBg } from "@/components/sparkle-bg"; -import { CreateBookmarkForm } from "./create-bookmark-form"; -import { User } from "lucia"; -import { ExisitingUser } from "./bookmark-wrapper"; - -export async function BookmarkButton() { - const userSession = (await getCurrentUser()) as User; - const user = (await getUser(userSession.id)) as ExisitingUser; - - return ( - - - - - - - Create a new bookmark - - A bookmark is a way to save articles for later reading. - - - - - - ); -} diff --git a/app/bookmarks/bookmark-wrapper.tsx b/app/bookmarks/bookmark-wrapper.tsx index a4e4659..789844b 100644 --- a/app/bookmarks/bookmark-wrapper.tsx +++ b/app/bookmarks/bookmark-wrapper.tsx @@ -14,7 +14,10 @@ import { User } from "lucia"; export type Bookmark = { id: number; -} & z.infer; +} & z.infer & { + createdAt: Date; + updatedAt: Date | null; + }; function BookmarkSkeleton() { return ( diff --git a/app/bookmarks/components/bookmark-list.tsx b/app/bookmarks/components/bookmark-list.tsx index c847c59..346a103 100644 --- a/app/bookmarks/components/bookmark-list.tsx +++ b/app/bookmarks/components/bookmark-list.tsx @@ -24,8 +24,13 @@ import { import { Bookmark } from "../bookmark-wrapper"; import { usePathname, useSearchParams } from "next/navigation"; import * as cheerio from "cheerio"; -import { useMemo } from "react"; -import { BookmarksSearchBox } from "./bokmarks-search-box"; +import { useEffect, useMemo, useState } from "react"; +import { BookmarksSearchBox } from "./bookmarks-search-box"; +import { BookmarksDisplayMenu } from "./bookmarks-display"; +import { BookmarkButton } from "./bookmark-button"; + +export type Layout = "grid" | "rows"; +export type OrderBy = "date" | "readTime" | "title"; function extractFirstSentence(htmlContent: string): string { const $ = cheerio.load(htmlContent); @@ -55,16 +60,48 @@ export default function BookmarksList({ const searchParams = useSearchParams(); const searchTerm = searchParams.get("search") || ""; - const filteredBookmarks = useMemo(() => { - if (!searchTerm) return bookmarks; - return bookmarks.filter((bookmark) => - bookmark.title.toLowerCase().includes(searchTerm.toLowerCase()), - ); - }, [bookmarks, searchTerm]); + const [layout, setLayout] = useState<"grid" | "rows">(() => { + return (localStorage.getItem("layout") as "grid" | "rows") || "grid"; + }); + const [orderBy, setOrderBy] = useState(() => { + return (localStorage.getItem("orderBy") as OrderBy) || "date"; + }); + + useEffect(() => { + localStorage.setItem("layout", layout); + }, [layout]); + + useEffect(() => { + localStorage.setItem("orderBy", orderBy); + }, [orderBy]); + + const sortedAndFilteredBookmarks = useMemo(() => { + let filtered = bookmarks; + if (searchTerm) { + filtered = bookmarks.filter((bookmark) => + bookmark.title.toLowerCase().includes(searchTerm.toLowerCase()), + ); + } + + return filtered.sort((a, b) => { + switch (orderBy) { + case "date": + return b.createdAt && a.createdAt + ? new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() + : 0; + case "readTime": + return parseInt(a.readTime || "0") - parseInt(b.readTime || "0"); + case "title": + return a.title.localeCompare(b.title); + default: + return 0; + } + }); + }, [bookmarks, searchTerm, orderBy]); return ( -
-
+
+
Bookmarks @@ -73,14 +110,27 @@ export default function BookmarksList({ Manage your bookmarked articles
-
+
+ +
- {/* Grid layout */} -
- {filteredBookmarks.map((bookmark) => ( +
+ {sortedAndFilteredBookmarks.map((bookmark) => ( void; + orderBy: OrderBy; + setOrderBy: Dispatch>; +}) { + const [open, setOpen] = useState(false); + const [openPopover, setOpenPopover] = useState(false); + const { toast } = useToast(); + + const handleSetDefault = () => { + toast({ + description: "Your current display settings have been set as default.", + }); + }; + + const handleResetToDefaults = () => { + setLayout("grid"); + setOrderBy("date"); + toast({ + description: "Display settings have been reset to defaults.", + }); + }; + + return ( + setOpen(isOpen)}> + + + + + + + + + + setOpenPopover(isOpen)} + > + +
+ + + + + + + + + + Ordering + + +
+
+ + setOrderBy("date")} + /> + setOrderBy("readTime")} + /> + setOrderBy("title")} + /> + +
+
+ + + + +
+
+ ); +} + +function OrderButton({ + name, + active, + onClick, +}: { + name: string; + active: boolean; + onClick: () => void; +}) { + return ( + + ); +} diff --git a/app/bookmarks/components/bookmarks-search-box.tsx b/app/bookmarks/components/bookmarks-search-box.tsx new file mode 100644 index 0000000..09b9cbd --- /dev/null +++ b/app/bookmarks/components/bookmarks-search-box.tsx @@ -0,0 +1,88 @@ +import { Icons } from "@/components/icons"; +import { Input } from "@/components/ui/input"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useDebouncedCallback } from "use-debounce"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; + +type BookmarksSearchBoxProps = { + debounceTimeoutMs?: number; +}; + +export function BookmarksSearchBox({ + debounceTimeoutMs = 300, +}: BookmarksSearchBoxProps) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const [value, setValue] = useState(searchParams.get("search") ?? ""); + const inputRef = useRef(null); + + const debouncedUpdateSearchParam = useDebouncedCallback((value: string) => { + const params = new URLSearchParams(searchParams); + if (value) { + params.set("search", value); + } else { + params.delete("search"); + } + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, debounceTimeoutMs); + + const onKeyDown = useCallback((e: KeyboardEvent) => { + const target = e.target as HTMLElement; + // only focus on filter input when: + // - user is not typing in an input or textarea + // - there is no existing modal backdrop (i.e. no other modal is open) + if ( + e.key === "/" && + target.tagName !== "INPUT" && + target.tagName !== "TEXTAREA" + ) { + e.preventDefault(); + inputRef.current?.focus(); + } + }, []); + + useEffect(() => { + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [onKeyDown]); + + // Update the search input value when the search query changes + useEffect(() => { + const search = searchParams.get("search"); + if (search !== null && search !== value) { + setValue(search); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchParams]); + + return ( +
+
+ +
+ { + setValue(e.target.value); + debouncedUpdateSearchParam(e.target.value); + }} + /> + {value && ( + + )} +
+ ); +} diff --git a/app/bookmarks/layout.tsx b/app/bookmarks/layout.tsx index 1241aa1..d7eeb23 100644 --- a/app/bookmarks/layout.tsx +++ b/app/bookmarks/layout.tsx @@ -8,9 +8,7 @@ export default async function ArticleLayout({ children }: ArticleLayoutProps) { return ( <> -
-
{children}
-
+
{children}
); } diff --git a/app/bookmarks/page.tsx b/app/bookmarks/page.tsx index 977f174..f17741f 100644 --- a/app/bookmarks/page.tsx +++ b/app/bookmarks/page.tsx @@ -9,10 +9,10 @@ export default async function BookmarkPage() { } return ( -
+