aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/dashboard/lists/CollapsibleBookmarkLists.tsx
blob: 2f8fca6a0c6e1b86afa0133f25408fc8bcc20c5c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { useEffect, useState } from "react";
import { Collapsible, CollapsibleContent } from "@/components/ui/collapsible";
import { FullPageSpinner } from "@/components/ui/full-page-spinner";
import { api } from "@/lib/trpc";
import { keepPreviousData } from "@tanstack/react-query";

import { useBookmarkLists } from "@karakeep/shared-react/hooks/lists";
import { ZBookmarkList } from "@karakeep/shared/types/lists";
import { ZBookmarkListTreeNode } from "@karakeep/shared/utils/listUtils";

type RenderFunc = (params: {
  item: ZBookmarkListTreeNode;
  level: number;
  open: boolean;
  numBookmarks?: number;
}) => 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]);
  const { data: listStats } = api.lists.stats.useQuery(undefined, {
    placeholderData: keepPreviousData,
  });

  return (
    <Collapsible open={open} onOpenChange={setOpen} className={className}>
      {render({
        item: node,
        level,
        open,
        numBookmarks: listStats?.stats.get(node.item.id),
      })}
      <CollapsibleContent>
        {node.children
          .sort((a, b) => a.item.name.localeCompare(b.item.name))
          .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: { lists: initialData },
  });

  if (!data) {
    return <FullPageSpinner />;
  }

  const { root } = data;

  return (
    <div>
      {Object.values(root)
        .sort((a, b) => a.item.name.localeCompare(b.item.name))
        .map((l) => (
          <ListItem
            key={l.item.id}
            node={l}
            render={render}
            level={0}
            className={className}
            isOpenFunc={isOpenFunc ?? (() => false)}
          />
        ))}
    </div>
  );
}