From e0999f701cd1834c3d940113cd8dd5247c5fe95f Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Fri, 19 Apr 2024 00:09:27 +0100
Subject: feature: Nested lists (#110). Fixes #62

* feature: Add support for nested lists

* prevent moving the parent to a subtree
---
 .../dashboard/lists/CollapsibleBookmarkLists.tsx   | 111 +++++++++++++++++++++
 1 file changed, 111 insertions(+)
 create mode 100644 apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx

(limited to 'apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx')

diff --git a/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx
new file mode 100644
index 00000000..da1b7408
--- /dev/null
+++ b/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx
@@ -0,0 +1,111 @@
+import { useEffect, useState } from "react";
+import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
+import { FullPageSpinner } from "@/components/ui/full-page-spinner";
+
+import {
+  augmentBookmarkListsWithInitialData,
+  useBookmarkLists,
+} from "@hoarder/shared-react/hooks/lists";
+import { ZBookmarkList } from "@hoarder/shared/types/lists";
+import { ZBookmarkListTreeNode } from "@hoarder/shared/utils/listUtils";
+
+type RenderFunc = (params: {
+  item: ZBookmarkListTreeNode;
+  level: number;
+  open: boolean;
+}) => React.ReactNode;
+
+type IsOpenFunc = (list: ZBookmarkListTreeNode) => boolean;
+
+function ListItem({
+  node,
+  render,
+  level,
+  className,
+  isOpenFunc,
+}: {
+  node: ZBookmarkListTreeNode;
+  render: RenderFunc;
+  isOpenFunc: IsOpenFunc;
+  level: number;
+  className?: string;
+}) {
+  // Not the most efficient way to do this, but it works for now
+  const isAnyChildOpen = (
+    node: ZBookmarkListTreeNode,
+    isOpenFunc: IsOpenFunc,
+  ): boolean => {
+    if (isOpenFunc(node)) {
+      return true;
+    }
+    return node.children.some((l) => isAnyChildOpen(l, isOpenFunc));
+  };
+  const [open, setOpen] = useState(false);
+  useEffect(() => {
+    setOpen((curr) => curr || isAnyChildOpen(node, isOpenFunc));
+  }, [node, isOpenFunc]);
+
+  return (
+    <Collapsible open={open} onOpenChange={setOpen} className={className}>
+      {render({
+        item: node,
+        level,
+        open,
+      })}
+      <CollapsibleContent>
+        {node.children.map((l) => (
+          <ListItem
+            isOpenFunc={isOpenFunc}
+            key={l.item.id}
+            node={l}
+            render={render}
+            level={level + 1}
+            className={className}
+          />
+        ))}
+      </CollapsibleContent>
+    </Collapsible>
+  );
+}
+
+export function CollapsibleBookmarkLists({
+  render,
+  initialData,
+  className,
+  isOpenFunc,
+}: {
+  initialData?: ZBookmarkList[];
+  render: RenderFunc;
+  isOpenFunc?: IsOpenFunc;
+  className?: string;
+}) {
+  let { data } = useBookmarkLists(undefined, {
+    initialData: initialData ? { lists: initialData } : undefined,
+  });
+
+  // TODO: This seems to be a bug in react query
+  if (initialData) {
+    data = augmentBookmarkListsWithInitialData(data, initialData);
+  }
+
+  if (!data) {
+    return <FullPageSpinner />;
+  }
+
+  const { root } = data;
+
+  return (
+    <div>
+      {Object.values(root).map((l) => (
+        <ListItem
+          key={l.item.id}
+          node={l}
+          render={render}
+          level={0}
+          className={className}
+          isOpenFunc={isOpenFunc ?? (() => false)}
+        />
+      ))}
+    </div>
+  );
+}
-- 
cgit v1.2.3-70-g09d2

