el.xwx.moe/components/CollectionListing.tsx

369 lines
10 KiB
TypeScript
Raw Normal View History

2024-03-02 08:07:33 -06:00
import React, { useEffect, useMemo, useState } from "react";
import Tree, {
mutateTree,
moveItemOnTree,
RenderItemParams,
TreeItem,
TreeData,
ItemId,
TreeSourcePosition,
TreeDestinationPosition,
} from "@atlaskit/tree";
import useCollectionStore from "@/store/collections";
2024-03-01 05:59:14 -06:00
import { Collection } from "@prisma/client";
import Link from "next/link";
2024-03-01 08:33:58 -06:00
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
2024-03-01 13:02:55 -06:00
import { useRouter } from "next/router";
import useAccountStore from "@/store/account";
import toast from "react-hot-toast";
2024-06-09 08:27:16 -05:00
import { useTranslation } from "next-i18next";
2024-03-01 05:59:14 -06:00
interface ExtendedTreeItem extends TreeItem {
data: Collection;
}
2024-03-01 13:02:55 -06:00
const CollectionListing = () => {
2024-06-09 08:27:16 -05:00
const { t } = useTranslation();
const { collections, updateCollection } = useCollectionStore();
const { account, updateAccount } = useAccountStore();
2024-03-01 13:02:55 -06:00
const router = useRouter();
const currentPath = router.asPath;
2024-03-02 08:07:33 -06:00
const initialTree = useMemo(() => {
if (collections.length > 0) {
return buildTreeFromCollections(
collections,
router,
account.collectionOrder
);
2024-03-02 08:07:33 -06:00
}
return undefined;
}, [collections, router]);
const [tree, setTree] = useState(initialTree);
useEffect(() => {
2024-03-02 08:07:33 -06:00
setTree(initialTree);
}, [initialTree]);
useEffect(() => {
if (account.username) {
2024-03-10 05:08:28 -05:00
if (
(!account.collectionOrder || account.collectionOrder.length === 0) &&
collections.length > 0
)
updateAccount({
...account,
collectionOrder: collections
2024-03-05 07:50:47 -06:00
.filter(
(e) =>
e.parentId === null ||
!collections.find((i) => i.id === e.parentId)
) // Filter out collections with non-null parentId
.map((e) => e.id as number), // Use "as number" to assert that e.id is a number
});
else {
2024-03-05 07:50:47 -06:00
const newCollectionOrder: number[] = [
...(account.collectionOrder || []),
];
// Start with collections that are in both account.collectionOrder and collections
const existingCollectionIds = collections.map((c) => c.id as number);
const filteredCollectionOrder = account.collectionOrder.filter((id) =>
existingCollectionIds.includes(id)
);
// Add new collections that are not in account.collectionOrder and meet the specific conditions
collections.forEach((collection) => {
if (
!filteredCollectionOrder.includes(collection.id as number) &&
(!collection.parentId || collection.ownerId === account.id)
) {
filteredCollectionOrder.push(collection.id as number);
}
});
// check if the newCollectionOrder is the same as the old one
if (
JSON.stringify(newCollectionOrder) !==
JSON.stringify(account.collectionOrder)
) {
updateAccount({
...account,
collectionOrder: newCollectionOrder,
});
}
}
}
2024-03-05 07:50:47 -06:00
}, [collections]);
const onExpand = (movedCollectionId: ItemId) => {
2024-03-02 08:07:33 -06:00
setTree((currentTree) =>
mutateTree(currentTree!, movedCollectionId, { isExpanded: true })
2024-03-02 08:07:33 -06:00
);
2024-03-01 05:59:14 -06:00
};
2024-02-22 01:51:51 -06:00
const onCollapse = (movedCollectionId: ItemId) => {
2024-03-02 08:07:33 -06:00
setTree((currentTree) =>
mutateTree(currentTree as TreeData, movedCollectionId, {
isExpanded: false,
})
2024-03-02 08:07:33 -06:00
);
2024-03-01 05:59:14 -06:00
};
const onDragEnd = async (
2024-03-01 05:59:14 -06:00
source: TreeSourcePosition,
destination: TreeDestinationPosition | undefined
) => {
if (!destination || !tree) {
return;
}
if (
source.index === destination.index &&
source.parentId === destination.parentId
) {
return;
}
const movedCollectionId = Number(
tree.items[source.parentId].children[source.index]
);
const movedCollection = collections.find((c) => c.id === movedCollectionId);
const destinationCollection = collections.find(
(c) => c.id === Number(destination.parentId)
);
if (
(movedCollection?.ownerId !== account.id &&
destination.parentId !== source.parentId) ||
(destinationCollection?.ownerId !== account.id &&
destination.parentId !== "root")
) {
2024-06-09 08:27:16 -05:00
return toast.error(t("cant_change_collection_you_dont_own"));
}
2024-03-01 05:59:14 -06:00
setTree((currentTree) => moveItemOnTree(currentTree!, source, destination));
const updatedCollectionOrder = [...account.collectionOrder];
if (source.parentId !== destination.parentId) {
await updateCollection({
...movedCollection,
parentId:
destination.parentId && destination.parentId !== "root"
? Number(destination.parentId)
: destination.parentId === "root"
? "root"
: null,
} as any);
}
if (
destination.index !== undefined &&
destination.parentId === source.parentId &&
source.parentId === "root"
) {
2024-03-05 07:50:47 -06:00
updatedCollectionOrder.includes(movedCollectionId) &&
updatedCollectionOrder.splice(source.index, 1);
updatedCollectionOrder.splice(destination.index, 0, movedCollectionId);
await updateAccount({
...account,
collectionOrder: updatedCollectionOrder,
});
} else if (
destination.index !== undefined &&
destination.parentId === "root"
) {
updatedCollectionOrder.splice(destination.index, 0, movedCollectionId);
await updateAccount({
...account,
collectionOrder: updatedCollectionOrder,
});
} else if (
source.parentId === "root" &&
destination.parentId &&
destination.parentId !== "root"
) {
updatedCollectionOrder.splice(source.index, 1);
await updateAccount({
...account,
collectionOrder: updatedCollectionOrder,
});
}
2024-03-01 05:59:14 -06:00
};
2024-02-22 02:04:01 -06:00
if (!tree) {
return (
<p className="text-neutral text-xs font-semibold truncate w-full px-2 mt-5 mb-8">
2024-06-09 08:27:16 -05:00
{t("you_have_no_collections")}
</p>
);
2024-03-01 13:02:55 -06:00
} else
return (
<Tree
tree={tree}
renderItem={(itemProps) => renderItem({ ...itemProps }, currentPath)}
onExpand={onExpand}
onCollapse={onCollapse}
onDragEnd={onDragEnd}
isDragEnabled
isNestingEnabled
/>
);
};
2024-03-01 13:02:55 -06:00
export default CollectionListing;
2024-03-01 07:37:20 -06:00
2024-03-01 13:02:55 -06:00
const renderItem = (
{ item, onExpand, onCollapse, provided }: RenderItemParams,
currentPath: string
) => {
2024-03-01 07:37:20 -06:00
const collection = item.data;
return (
2024-03-01 13:02:55 -06:00
<div ref={provided.innerRef} {...provided.draggableProps} className="mb-1">
<div
className={`${currentPath === `/collections/${collection.id}`
2024-03-01 13:02:55 -06:00
? "bg-primary/20 is-active"
: "hover:bg-neutral/20"
} duration-100 flex gap-1 items-center pr-2 pl-1 rounded-md`}
2024-03-01 07:37:20 -06:00
>
2024-03-01 13:02:55 -06:00
{Icon(item as ExtendedTreeItem, onExpand, onCollapse)}
2024-03-01 07:37:20 -06:00
2024-03-01 13:02:55 -06:00
<Link
href={`/collections/${collection.id}`}
className="w-full"
{...provided.dragHandleProps}
>
<div
className={`py-1 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
>
2024-03-01 07:37:20 -06:00
<i
2024-03-01 13:02:55 -06:00
className="bi-folder-fill text-2xl drop-shadow"
style={{ color: collection.color }}
2024-03-01 07:37:20 -06:00
></i>
2024-03-01 13:02:55 -06:00
<p className="truncate w-full">{collection.name}</p>
{collection.isPublic && (
2024-03-01 13:02:55 -06:00
<i
className="bi-globe2 text-sm text-black/50 dark:text-white/50 drop-shadow"
title="This collection is being shared publicly."
></i>
)}
2024-03-01 13:02:55 -06:00
<div className="drop-shadow text-neutral text-xs">
{collection._count?.links}
</div>
2024-03-01 07:37:20 -06:00
</div>
2024-03-01 13:02:55 -06:00
</Link>
</div>
2024-03-01 07:37:20 -06:00
</div>
);
};
const Icon = (
item: ExtendedTreeItem,
onExpand: (id: ItemId) => void,
onCollapse: (id: ItemId) => void
) => {
if (item.children && item.children.length > 0) {
return item.isExpanded ? (
<button onClick={() => onCollapse(item.id)}>
2024-03-01 13:02:55 -06:00
<div className="bi-caret-down-fill opacity-50 hover:opacity-100 duration-200"></div>
2024-03-01 07:37:20 -06:00
</button>
) : (
<button onClick={() => onExpand(item.id)}>
2024-03-01 13:02:55 -06:00
<div className="bi-caret-right-fill opacity-40 hover:opacity-100 duration-200"></div>
2024-03-01 07:37:20 -06:00
</button>
);
}
// return <span>&bull;</span>;
2024-03-05 07:50:47 -06:00
return <div></div>;
2024-03-01 07:37:20 -06:00
};
2024-03-01 08:33:58 -06:00
const buildTreeFromCollections = (
2024-03-01 13:02:55 -06:00
collections: CollectionIncludingMembersAndLinkCount[],
router: ReturnType<typeof useRouter>,
order?: number[]
2024-03-01 08:33:58 -06:00
): TreeData => {
if (order) {
collections.sort((a: any, b: any) => {
return order.indexOf(a.id) - order.indexOf(b.id);
});
}
2024-03-01 08:33:58 -06:00
const items: { [key: string]: ExtendedTreeItem } = collections.reduce(
(acc: any, collection) => {
acc[collection.id as number] = {
id: collection.id,
children: [],
hasChildren: false,
isExpanded: false,
data: {
id: collection.id,
2024-03-01 13:02:55 -06:00
parentId: collection.parentId,
2024-03-01 08:33:58 -06:00
name: collection.name,
description: collection.description,
color: collection.color,
isPublic: collection.isPublic,
ownerId: collection.ownerId,
createdAt: collection.createdAt,
updatedAt: collection.updatedAt,
_count: {
links: collection._count?.links,
},
},
};
return acc;
},
{}
);
2024-03-01 13:02:55 -06:00
const activeCollectionId = Number(router.asPath.split("/collections/")[1]);
if (activeCollectionId) {
for (const item in items) {
const collection = items[item];
if (Number(item) === activeCollectionId && collection.data.parentId) {
// get all the parents of the active collection recursively until root and set isExpanded to true
let parentId = collection.data.parentId || null;
2024-03-05 07:50:47 -06:00
while (parentId && items[parentId]) {
2024-03-01 13:02:55 -06:00
items[parentId].isExpanded = true;
parentId = items[parentId].data.parentId;
}
}
}
}
2024-03-01 08:33:58 -06:00
collections.forEach((collection) => {
const parentId = collection.parentId;
if (parentId && items[parentId] && collection.id) {
items[parentId].children.push(collection.id);
items[parentId].hasChildren = true;
}
});
const rootId = "root";
items[rootId] = {
id: rootId,
children: (collections
2024-03-05 07:50:47 -06:00
.filter(
(c) =>
c.parentId === null || !collections.find((i) => i.id === c.parentId)
)
2024-03-01 08:33:58 -06:00
.map((c) => c.id) || "") as unknown as string[],
hasChildren: true,
isExpanded: true,
data: { name: "Root" } as Collection,
};
return { rootId, items };
};