diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx
index 71dc075..ec586ea 100644
--- a/components/InputSelect/CollectionSelection.tsx
+++ b/components/InputSelect/CollectionSelection.tsx
@@ -4,18 +4,26 @@ import { useEffect, useState } from "react";
import { styles } from "./styles";
import { Options } from "./types";
import CreatableSelect from "react-select/creatable";
+import Select from "react-select";
type Props = {
onChange: any;
- defaultValue:
+ showDefaultValue?: boolean;
+ defaultValue?:
| {
label: string;
value?: number;
}
| undefined;
+ creatable?: boolean;
};
-export default function CollectionSelection({ onChange, defaultValue }: Props) {
+export default function CollectionSelection({
+ onChange,
+ defaultValue,
+ showDefaultValue = true,
+ creatable = true,
+}: Props) {
const { collections } = useCollectionStore();
const router = useRouter();
@@ -42,16 +50,31 @@ export default function CollectionSelection({ onChange, defaultValue }: Props) {
setOptions(formatedCollections);
}, [collections]);
- return (
-
- );
+ if (creatable) {
+ return (
+
+ );
+ } else {
+ return (
+
+ );
+ }
}
diff --git a/components/LinkViews/Layouts/CardView.tsx b/components/LinkViews/Layouts/CardView.tsx
index 3e341cc..77b0a54 100644
--- a/components/LinkViews/Layouts/CardView.tsx
+++ b/components/LinkViews/Layouts/CardView.tsx
@@ -3,8 +3,12 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
export default function CardView({
links,
+ showCheckbox = true,
+ editMode,
}: {
links: LinkIncludingShortenedCollectionAndTags[];
+ showCheckbox?: boolean;
+ editMode?: boolean;
}) {
return (
@@ -15,6 +19,7 @@ export default function CardView({
link={e}
count={i}
flipDropdown={i === links.length - 1}
+ editMode={editMode}
/>
);
})}
diff --git a/components/LinkViews/Layouts/ListView.tsx b/components/LinkViews/Layouts/ListView.tsx
index 1839284..1939f81 100644
--- a/components/LinkViews/Layouts/ListView.tsx
+++ b/components/LinkViews/Layouts/ListView.tsx
@@ -3,11 +3,13 @@ import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
export default function ListView({
links,
+ editMode,
}: {
links: LinkIncludingShortenedCollectionAndTags[];
+ editMode?: boolean;
}) {
return (
-
+
{links.map((e, i) => {
return (
);
})}
diff --git a/components/LinkViews/LinkCard.tsx b/components/LinkViews/LinkCard.tsx
index fc3060d..6f2d6cd 100644
--- a/components/LinkViews/LinkCard.tsx
+++ b/components/LinkViews/LinkCard.tsx
@@ -17,22 +17,32 @@ import LinkIcon from "./LinkComponents/LinkIcon";
import useOnScreen from "@/hooks/useOnScreen";
import { generateLinkHref } from "@/lib/client/generateLinkHref";
import useAccountStore from "@/store/account";
+import usePermissions from "@/hooks/usePermissions";
+import toast from "react-hot-toast";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
count: number;
className?: string;
flipDropdown?: boolean;
+ editMode?: boolean;
};
-export default function LinkGrid({
- link,
- flipDropdown,
-}: Props) {
+export default function LinkCard({ link, flipDropdown, editMode }: Props) {
const { collections } = useCollectionStore();
const { account } = useAccountStore();
- const { links, getLink } = useLinkStore();
+ const { links, getLink, setSelectedLinks, selectedLinks } = useLinkStore();
+
+ const handleCheckboxClick = (
+ link: LinkIncludingShortenedCollectionAndTags
+ ) => {
+ if (selectedLinks.includes(link)) {
+ setSelectedLinks(selectedLinks.filter((e) => e !== link));
+ } else {
+ setSelectedLinks([...selectedLinks, link]);
+ }
+ };
let shortendURL;
@@ -59,6 +69,7 @@ export default function LinkGrid({
const ref = useRef
(null);
const isVisible = useOnScreen(ref);
+ const permissions = usePermissions(collection?.id as number);
useEffect(() => {
let interval: any;
@@ -82,131 +93,261 @@ export default function LinkGrid({
const [showInfo, setShowInfo] = useState(false);
+ const selectedStyle = selectedLinks.some(
+ (selectedLink) => selectedLink.id === link.id
+ )
+ ? "border-primary bg-base-300"
+ : "border-neutral-content";
+
+ const selectable =
+ editMode &&
+ (permissions === true || permissions?.canCreate || permissions?.canDelete);
+
return (
+ selectable
+ ? handleCheckboxClick(link)
+ : editMode
+ ? toast.error(
+ "You don't have permission to edit or delete this item."
+ )
+ : undefined
+ }
>
-
-
- {previewAvailable(link) ? (
-
{
- const target = e.target as HTMLElement;
- target.style.display = "none";
- }}
- />
- ) : link.preview === "unavailable" ? (
-
- ) : (
-
- )}
-
+
-
-
-
-
-
-
-
-
- {unescapeString(link.name || link.description) || link.url}
-
-
-
-
-
-
{shortendURL}
+
+ {previewAvailable(link) ? (
+
{
+ const target = e.target as HTMLElement;
+ target.style.display = "none";
+ }}
+ />
+ ) : link.preview === "unavailable" ? (
+
+ ) : (
+
+ )}
+
+
+
-
-
-
+
-
-
- {collection ? (
-
- ) : undefined}
-
-
-
-
+
+
+ {unescapeString(link.name || link.description) || link.url}
+
- {showInfo ? (
-
-
setShowInfo(!showInfo)}
- className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
- >
-
-
-
Description
-
-
-
- {link.description ? (
- unescapeString(link.description)
- ) : (
-
- No description provided.
-
- )}
-
- {link.tags[0] ? (
- <>
-
Tags
-
-
-
-
-
- {link.tags.map((e, i) => (
-
{
- e.stopPropagation();
- }}
- className="btn btn-xs btn-ghost truncate max-w-[19rem]"
- >
- #{e.name}
-
- ))}
+
- >
- ) : undefined}
-
- ) : undefined}
+
-
setShowInfo(!showInfo)}
- linkInfo={showInfo}
- flipDropdown={flipDropdown}
- />
+
+
+
+
+ {collection && (
+
+ )}
+
+
+
+
+
+ {showInfo && (
+
+
setShowInfo(!showInfo)}
+ className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
+ >
+
+
+
Description
+
+
+
+ {link.description ? (
+ unescapeString(link.description)
+ ) : (
+
+ No description provided.
+
+ )}
+
+ {link.tags[0] && (
+ <>
+
+ Tags
+
+
+
+
+
+
+ {link.tags.map((e, i) => (
+ {
+ e.stopPropagation();
+ }}
+ className="btn btn-xs btn-ghost truncate max-w-[19rem]"
+ >
+ #{e.name}
+
+ ))}
+
+
+ >
+ )}
+
+ )}
+
+ setShowInfo(!showInfo)}
+ linkInfo={showInfo}
+ flipDropdown={flipDropdown}
+ />
+ >
+ ) : (
+ <>
+
+
+ {previewAvailable(link) ? (
+
{
+ const target = e.target as HTMLElement;
+ target.style.display = "none";
+ }}
+ />
+ ) : link.preview === "unavailable" ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+
+
+
+ {unescapeString(link.name || link.description) || link.url}
+
+
+
+
+
+
+
+
+
+ {collection && (
+
+ )}
+
+
+
+
+
+ {showInfo && (
+
+
setShowInfo(!showInfo)}
+ className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10"
+ >
+
+
+
Description
+
+
+
+ {link.description ? (
+ unescapeString(link.description)
+ ) : (
+
+ No description provided.
+
+ )}
+
+ {link.tags[0] && (
+ <>
+
+ Tags
+
+
+
+
+
+
+ {link.tags.map((e, i) => (
+ {
+ e.stopPropagation();
+ }}
+ className="btn btn-xs btn-ghost truncate max-w-[19rem]"
+ >
+ #{e.name}
+
+ ))}
+
+
+ >
+ )}
+
+ )}
+
+ setShowInfo(!showInfo)}
+ linkInfo={showInfo}
+ flipDropdown={flipDropdown}
+ />
+ >
+ )}
);
}
diff --git a/components/LinkViews/LinkGrid.tsx b/components/LinkViews/LinkGrid.tsx
index 505b7fd..be4d6ed 100644
--- a/components/LinkViews/LinkGrid.tsx
+++ b/components/LinkViews/LinkGrid.tsx
@@ -101,7 +101,7 @@ export default function LinkGrid({ link }: Props) {
{ }}
+ toggleShowInfo={() => {}}
linkInfo={false}
link={link}
collection={collection}
diff --git a/components/LinkViews/LinkList.tsx b/components/LinkViews/LinkList.tsx
index f4e4500..bc91bd0 100644
--- a/components/LinkViews/LinkList.tsx
+++ b/components/LinkViews/LinkList.tsx
@@ -14,21 +14,41 @@ import Link from "next/link";
import { isPWA } from "@/lib/client/utils";
import { generateLinkHref } from "@/lib/client/generateLinkHref";
import useAccountStore from "@/store/account";
+import usePermissions from "@/hooks/usePermissions";
+import toast from "react-hot-toast";
type Props = {
link: LinkIncludingShortenedCollectionAndTags;
count: number;
className?: string;
flipDropdown?: boolean;
+ editMode?: boolean;
};
export default function LinkCardCompact({
link,
flipDropdown,
+ editMode,
}: Props) {
const { collections } = useCollectionStore();
const { account } = useAccountStore();
- const { links } = useLinkStore();
+ const { links, setSelectedLinks, selectedLinks } = useLinkStore();
+
+ const handleCheckboxClick = (
+ link: LinkIncludingShortenedCollectionAndTags
+ ) => {
+ const linkIndex = selectedLinks.findIndex(
+ (selectedLink) => selectedLink.id === link.id
+ );
+
+ if (linkIndex !== -1) {
+ const updatedLinks = [...selectedLinks];
+ updatedLinks.splice(linkIndex, 1);
+ setSelectedLinks(updatedLinks);
+ } else {
+ setSelectedLinks([...selectedLinks, link]);
+ }
+ };
let shortendURL;
@@ -53,103 +73,186 @@ export default function LinkCardCompact({
);
}, [collections, links]);
+ const permissions = usePermissions(collection?.id as number);
+
const [showInfo, setShowInfo] = useState(false);
+ const selectedStyle = selectedLinks.some(
+ (selectedLink) => selectedLink.id === link.id
+ )
+ ? "border border-primary bg-base-300"
+ : "border-transparent";
+
+ const selectable =
+ editMode &&
+ (permissions === true || permissions?.canCreate || permissions?.canDelete);
+
return (
<>
+ selectable
+ ? handleCheckboxClick(link)
+ : editMode
+ ? toast.error(
+ "You don't have permission to edit or delete this item."
+ )
+ : undefined
+ }
>
-
-
-
-
-
-
-
- {unescapeString(link.name || link.description) || link.url}
-
-
-
-
- {collection ? (
-
- ) : undefined}
- {link.url ? (
-
- ) : (
-
- {link.type}
-
- )}
-
+ {/* {showCheckbox &&
+ editMode &&
+ (permissions === true ||
+ permissions?.canCreate ||
+ permissions?.canDelete) && (
+
selectedLink.id === link.id
+ )}
+ onChange={() => handleCheckboxClick(link)}
+ />
+ )} */}
+ {!editMode ? (
+ <>
+
+
+
-
-
-
-
setShowInfo(!showInfo)}
- // linkInfo={showInfo}
- />
- {showInfo ? (
-
-
-
Description
+
+
+ {unescapeString(link.name || link.description) || link.url}
+
-
-
- {link.description ? (
- unescapeString(link.description)
- ) : (
-
- No description provided.
-
- )}
-
- {link.tags[0] ? (
- <>
-
- Tags
+
+
+ {collection && (
+
+ )}
+ {link.url ? (
+
+ ) : (
+
+ {link.type}
+
+ )}
+
+
+
+
+
+
setShowInfo(!showInfo)}
+ // linkInfo={showInfo}
+ />
+ {showInfo && (
+
+
+
+ Description
+
+ {link.description ? (
+ unescapeString(link.description)
+ ) : (
+
+ No description provided.
+
+ )}
+
+ {link.tags[0] && (
+ <>
+
+ Tags
+
-
-
- {link.tags.map((e, i) => (
- {
- e.stopPropagation();
- }}
- className="btn btn-xs btn-ghost truncate max-w-[19rem]"
- >
- #{e.name}
-
- ))}
-
+
+
+
+
+ {link.tags.map((e, i) => (
+ {
+ e.stopPropagation();
+ }}
+ className="btn btn-xs btn-ghost truncate max-w-[19rem]"
+ >
+ #{e.name}
+
+ ))}
+
+
+ >
+ )}
+
+
+ )}
+ >
+ ) : (
+ <>
+
+
+
+
+
+
+
+ {unescapeString(link.name || link.description) || link.url}
+
+
+
+
+ {collection ? (
+
+ ) : undefined}
+ {link.url ? (
+
+ ) : (
+
+ {link.type}
+
+ )}
+
- >
- ) : undefined}
+
+
-
- ) : undefined}
+ setShowInfo(!showInfo)}
+ // linkInfo={showInfo}
+ />
+ >
+ )}
-
>
);
diff --git a/components/ModalContent/BulkDeleteLinksModal.tsx b/components/ModalContent/BulkDeleteLinksModal.tsx
new file mode 100644
index 0000000..6de26cd
--- /dev/null
+++ b/components/ModalContent/BulkDeleteLinksModal.tsx
@@ -0,0 +1,75 @@
+import React from "react";
+import useLinkStore from "@/store/links";
+import toast from "react-hot-toast";
+import Modal from "../Modal";
+
+type Props = {
+ onClose: Function;
+};
+
+export default function BulkDeleteLinksModal({ onClose }: Props) {
+ const { selectedLinks, setSelectedLinks, deleteLinksById } = useLinkStore();
+
+ const deleteLink = async () => {
+ const load = toast.loading(
+ `Deleting ${selectedLinks.length} Link${
+ selectedLinks.length > 1 ? "s" : ""
+ }...`
+ );
+
+ const response = await deleteLinksById(
+ selectedLinks.map((link) => link.id as number)
+ );
+
+ toast.dismiss(load);
+
+ if (response.ok) {
+ toast.success(
+ `Deleted ${selectedLinks.length} Link${
+ selectedLinks.length > 1 ? "s" : ""
+ }`
+ );
+
+ setSelectedLinks([]);
+ onClose();
+ } else toast.error(response.data as string);
+ };
+
+ return (
+
+
+ Delete {selectedLinks.length} Link{selectedLinks.length > 1 ? "s" : ""}
+
+
+
+
+
+ {selectedLinks.length > 1 ? (
+
Are you sure you want to delete {selectedLinks.length} links?
+ ) : (
+
Are you sure you want to delete this link?
+ )}
+
+
+
+
+ Warning: This action is irreversible!
+
+
+
+
+ Hold the Shift key while clicking
+ 'Delete' to bypass this confirmation in the future.
+
+
+
+
+
+ );
+}
diff --git a/components/ModalContent/BulkEditLinksModal.tsx b/components/ModalContent/BulkEditLinksModal.tsx
new file mode 100644
index 0000000..07b3914
--- /dev/null
+++ b/components/ModalContent/BulkEditLinksModal.tsx
@@ -0,0 +1,102 @@
+import React, { useState } from "react";
+import CollectionSelection from "@/components/InputSelect/CollectionSelection";
+import TagSelection from "@/components/InputSelect/TagSelection";
+import useLinkStore from "@/store/links";
+import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
+import toast from "react-hot-toast";
+import Modal from "../Modal";
+
+type Props = {
+ onClose: Function;
+};
+
+export default function BulkEditLinksModal({ onClose }: Props) {
+ const { updateLinks, selectedLinks, setSelectedLinks } = useLinkStore();
+ const [submitLoader, setSubmitLoader] = useState(false);
+ const [removePreviousTags, setRemovePreviousTags] = useState(false);
+ const [updatedValues, setUpdatedValues] = useState<
+ Pick
+ >({ tags: [] });
+
+ const setCollection = (e: any) => {
+ const collectionId = e?.value || null;
+ console.log(updatedValues);
+ setUpdatedValues((prevValues) => ({ ...prevValues, collectionId }));
+ };
+
+ const setTags = (e: any) => {
+ const tags = e.map((tag: any) => ({ name: tag.label }));
+ setUpdatedValues((prevValues) => ({ ...prevValues, tags }));
+ };
+
+ const submit = async () => {
+ if (!submitLoader) {
+ setSubmitLoader(true);
+
+ const load = toast.loading("Updating...");
+
+ const response = await updateLinks(
+ selectedLinks,
+ removePreviousTags,
+ updatedValues
+ );
+
+ toast.dismiss(load);
+
+ if (response.ok) {
+ toast.success(`Updated!`);
+ setSelectedLinks([]);
+ onClose();
+ } else toast.error(response.data as string);
+
+ setSubmitLoader(false);
+ return response;
+ }
+ };
+
+ return (
+
+
+ Edit {selectedLinks.length} Link{selectedLinks.length > 1 ? "s" : ""}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/components/ModalContent/DeleteLinkModal.tsx b/components/ModalContent/DeleteLinkModal.tsx
index a2b123c..1a3a476 100644
--- a/components/ModalContent/DeleteLinkModal.tsx
+++ b/components/ModalContent/DeleteLinkModal.tsx
@@ -15,7 +15,6 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
useState(activeLink);
const { removeLink } = useLinkStore();
- const [submitLoader, setSubmitLoader] = useState(false);
const router = useRouter();
diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx
index f54a3ed..9a73d5b 100644
--- a/components/ModalContent/EditCollectionSharingModal.tsx
+++ b/components/ModalContent/EditCollectionSharingModal.tsx
@@ -234,11 +234,8 @@ export default function EditCollectionSharingModal({
: undefined;
return (
- <>
-
+
+
@@ -433,7 +430,7 @@ export default function EditCollectionSharingModal({
- >
+
);
})}
diff --git a/components/ModalContent/EditLinkModal.tsx b/components/ModalContent/EditLinkModal.tsx
index 9ba27fe..b77b76c 100644
--- a/components/ModalContent/EditLinkModal.tsx
+++ b/components/ModalContent/EditLinkModal.tsx
@@ -124,6 +124,7 @@ export default function EditLinkModal({ onClose, activeLink }: Props) {
label: "Unorganized",
}
}
+ creatable={false}
/>
) : null}
diff --git a/components/SortDropdown.tsx b/components/SortDropdown.tsx
index 2f42e3a..81d4496 100644
--- a/components/SortDropdown.tsx
+++ b/components/SortDropdown.tsx
@@ -14,7 +14,7 @@ export default function SortDropdown({ sortBy, setSort }: Props) {
tabIndex={0}
role="button"
onMouseDown={dropdownTriggerer}
- className="btn btn-sm btn-square btn-ghost"
+ className="btn btn-sm btn-square btn-ghost border-none"
>
diff --git a/components/ViewDropdown.tsx b/components/ViewDropdown.tsx
index 23558d2..5ab821e 100644
--- a/components/ViewDropdown.tsx
+++ b/components/ViewDropdown.tsx
@@ -1,4 +1,4 @@
-import React, { Dispatch, SetStateAction, useEffect, useState } from "react";
+import React, { Dispatch, SetStateAction, useEffect } from "react";
import useLocalSettingsStore from "@/store/localSettings";
import { ViewMode } from "@/types/global";
diff --git a/hooks/useCollectivePermissions.ts b/hooks/useCollectivePermissions.ts
new file mode 100644
index 0000000..b1e3b7c
--- /dev/null
+++ b/hooks/useCollectivePermissions.ts
@@ -0,0 +1,34 @@
+import useAccountStore from "@/store/account";
+import useCollectionStore from "@/store/collections";
+import { Member } from "@/types/global";
+import { useEffect, useState } from "react";
+
+export default function useCollectivePermissions(collectionIds: number[]) {
+ const { collections } = useCollectionStore();
+
+ const { account } = useAccountStore();
+
+ const [permissions, setPermissions] = useState
();
+ useEffect(() => {
+ for (const collectionId of collectionIds) {
+ const collection = collections.find((e) => e.id === collectionId);
+
+ if (collection) {
+ let getPermission: Member | undefined = collection.members.find(
+ (e) => e.userId === account.id
+ );
+
+ if (
+ getPermission?.canCreate === false &&
+ getPermission?.canUpdate === false &&
+ getPermission?.canDelete === false
+ )
+ getPermission = undefined;
+
+ setPermissions(account.id === collection.ownerId || getPermission);
+ }
+ }
+ }, [account, collections, collectionIds]);
+
+ return permissions;
+}
diff --git a/hooks/useLinks.tsx b/hooks/useLinks.tsx
index 77e4dce..6a06c1b 100644
--- a/hooks/useLinks.tsx
+++ b/hooks/useLinks.tsx
@@ -18,7 +18,8 @@ export default function useLinks(
searchByTextContent,
}: LinkRequestQuery = { sort: 0 }
) {
- const { links, setLinks, resetLinks } = useLinkStore();
+ const { links, setLinks, resetLinks, selectedLinks, setSelectedLinks } =
+ useLinkStore();
const router = useRouter();
const { reachedBottom, setReachedBottom } = useDetectPageBottom();
@@ -68,8 +69,12 @@ export default function useLinks(
};
useEffect(() => {
+ // Save the selected links before resetting the links
+ // and then restore the selected links after resetting the links
+ const previouslySelected = selectedLinks;
resetLinks();
+ setSelectedLinks(previouslySelected);
getLinks(true);
}, [
router,
diff --git a/lib/api/archiveHandler.ts b/lib/api/archiveHandler.ts
index 88ff97e..c43b6db 100644
--- a/lib/api/archiveHandler.ts
+++ b/lib/api/archiveHandler.ts
@@ -23,8 +23,7 @@ export default async function archiveHandler(link: LinksAndCollectionAndOwner) {
const browser = await chromium.launch();
const context = await browser.newContext({
...devices["Desktop Chrome"],
- ignoreHTTPSErrors:
- process.env.IGNORE_HTTPS_ERRORS === "true",
+ ignoreHTTPSErrors: process.env.IGNORE_HTTPS_ERRORS === "true",
});
const page = await context.newPage();
diff --git a/lib/api/controllers/links/bulk/deleteLinksById.ts b/lib/api/controllers/links/bulk/deleteLinksById.ts
new file mode 100644
index 0000000..466db98
--- /dev/null
+++ b/lib/api/controllers/links/bulk/deleteLinksById.ts
@@ -0,0 +1,58 @@
+import { prisma } from "@/lib/api/db";
+import { UsersAndCollections } from "@prisma/client";
+import getPermission from "@/lib/api/getPermission";
+import removeFile from "@/lib/api/storage/removeFile";
+
+export default async function deleteLinksById(
+ userId: number,
+ linkIds: number[]
+) {
+ if (!linkIds || linkIds.length === 0) {
+ return { response: "Please choose valid links.", status: 401 };
+ }
+
+ const collectionIsAccessibleArray = [];
+
+ // Check if the user has access to the collection of each link
+ // if any of the links are not accessible, return an error
+ // if all links are accessible, continue with the deletion
+ // and add the collection to the collectionIsAccessibleArray
+ for (const linkId of linkIds) {
+ const collectionIsAccessible = await getPermission({ userId, linkId });
+
+ const memberHasAccess = collectionIsAccessible?.members.some(
+ (e: UsersAndCollections) => e.userId === userId && e.canDelete
+ );
+
+ if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess)) {
+ return { response: "Collection is not accessible.", status: 401 };
+ }
+
+ collectionIsAccessibleArray.push(collectionIsAccessible);
+ }
+
+ const deletedLinks = await prisma.link.deleteMany({
+ where: {
+ id: { in: linkIds },
+ },
+ });
+
+ // Loop through each link and delete the associated files
+ // if the user has access to the collection
+ for (let i = 0; i < linkIds.length; i++) {
+ const linkId = linkIds[i];
+ const collectionIsAccessible = collectionIsAccessibleArray[i];
+
+ removeFile({
+ filePath: `archives/${collectionIsAccessible?.id}/${linkId}.pdf`,
+ });
+ removeFile({
+ filePath: `archives/${collectionIsAccessible?.id}/${linkId}.png`,
+ });
+ removeFile({
+ filePath: `archives/${collectionIsAccessible?.id}/${linkId}_readability.json`,
+ });
+ }
+
+ return { response: deletedLinks, status: 200 };
+}
diff --git a/lib/api/controllers/links/bulk/updateLinks.ts b/lib/api/controllers/links/bulk/updateLinks.ts
new file mode 100644
index 0000000..a214c30
--- /dev/null
+++ b/lib/api/controllers/links/bulk/updateLinks.ts
@@ -0,0 +1,50 @@
+import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
+import updateLinkById from "../linkId/updateLinkById";
+
+export default async function updateLinks(
+ userId: number,
+ links: LinkIncludingShortenedCollectionAndTags[],
+ removePreviousTags: boolean,
+ newData: Pick<
+ LinkIncludingShortenedCollectionAndTags,
+ "tags" | "collectionId"
+ >
+) {
+ let allUpdatesSuccessful = true;
+
+ // Have to use a loop here rather than updateMany, see the following:
+ // https://github.com/prisma/prisma/issues/3143
+ for (const link of links) {
+ let updatedTags = [...link.tags, ...(newData.tags ?? [])];
+
+ if (removePreviousTags) {
+ // If removePreviousTags is true, replace the existing tags with new tags
+ updatedTags = [...(newData.tags ?? [])];
+ }
+
+ const updatedData: LinkIncludingShortenedCollectionAndTags = {
+ ...link,
+ tags: updatedTags,
+ collection: {
+ ...link.collection,
+ id: newData.collectionId ?? link.collection.id,
+ },
+ };
+
+ const updatedLink = await updateLinkById(
+ userId,
+ link.id as number,
+ updatedData
+ );
+
+ if (updatedLink.status !== 200) {
+ allUpdatesSuccessful = false;
+ }
+ }
+
+ if (allUpdatesSuccessful) {
+ return { response: "All links updated successfully", status: 200 };
+ } else {
+ return { response: "Some links failed to update", status: 400 };
+ }
+}
diff --git a/lib/api/controllers/links/linkId/deleteLinkById.ts b/lib/api/controllers/links/linkId/deleteLinkById.ts
index 90adba4..db68ee7 100644
--- a/lib/api/controllers/links/linkId/deleteLinkById.ts
+++ b/lib/api/controllers/links/linkId/deleteLinkById.ts
@@ -1,5 +1,5 @@
import { prisma } from "@/lib/api/db";
-import { Collection, Link, UsersAndCollections } from "@prisma/client";
+import { Link, UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import removeFile from "@/lib/api/storage/removeFile";
diff --git a/lib/api/controllers/links/linkId/updateLinkById.ts b/lib/api/controllers/links/linkId/updateLinkById.ts
index ce6b920..62b2945 100644
--- a/lib/api/controllers/links/linkId/updateLinkById.ts
+++ b/lib/api/controllers/links/linkId/updateLinkById.ts
@@ -1,6 +1,6 @@
import { prisma } from "@/lib/api/db";
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
-import { Collection, Link, UsersAndCollections } from "@prisma/client";
+import { UsersAndCollections } from "@prisma/client";
import getPermission from "@/lib/api/getPermission";
import moveFile from "@/lib/api/storage/moveFile";
@@ -16,6 +16,10 @@ export default async function updateLinkById(
};
const collectionIsAccessible = await getPermission({ userId, linkId });
+ const targetCollectionIsAccessible = await getPermission({
+ userId,
+ collectionId: data.collection.id,
+ });
const memberHasAccess = collectionIsAccessible?.members.some(
(e: UsersAndCollections) => e.userId === userId && e.canUpdate
@@ -25,6 +29,28 @@ export default async function updateLinkById(
collectionIsAccessible?.ownerId === data.collection.ownerId &&
data.collection.ownerId === userId;
+ const targetCollectionsAccessible =
+ targetCollectionIsAccessible?.ownerId === userId;
+
+ const targetCollectionMatchesData = data.collection.id
+ ? data.collection.id === targetCollectionIsAccessible?.id
+ : true && data.collection.name
+ ? data.collection.name === targetCollectionIsAccessible?.name
+ : true && data.collection.ownerId
+ ? data.collection.ownerId === targetCollectionIsAccessible?.ownerId
+ : true;
+
+ if (!targetCollectionsAccessible)
+ return {
+ response: "Target collection is not accessible.",
+ status: 401,
+ };
+ else if (!targetCollectionMatchesData)
+ return {
+ response: "Target collection does not match the data.",
+ status: 401,
+ };
+
const unauthorizedSwitchCollection =
!isCollectionOwner && collectionIsAccessible?.id !== data.collection.id;
diff --git a/lib/api/controllers/users/userId/updateUserById.ts b/lib/api/controllers/users/userId/updateUserById.ts
index 162f246..6bb7480 100644
--- a/lib/api/controllers/users/userId/updateUserById.ts
+++ b/lib/api/controllers/users/userId/updateUserById.ts
@@ -97,18 +97,18 @@ export default async function updateUserById(
id: { not: userId },
OR: emailEnabled
? [
- {
- username: data.username.toLowerCase(),
- },
- {
- email: data.email?.toLowerCase(),
- },
- ]
+ {
+ username: data.username.toLowerCase(),
+ },
+ {
+ email: data.email?.toLowerCase(),
+ },
+ ]
: [
- {
- username: data.username.toLowerCase(),
- },
- ],
+ {
+ username: data.username.toLowerCase(),
+ },
+ ],
},
});
diff --git a/lib/api/getPermission.ts b/lib/api/getPermission.ts
index 61dc5c5..93dd04c 100644
--- a/lib/api/getPermission.ts
+++ b/lib/api/getPermission.ts
@@ -3,12 +3,14 @@ import { prisma } from "@/lib/api/db";
type Props = {
userId: number;
collectionId?: number;
+ collectionName?: string;
linkId?: number;
};
export default async function getPermission({
userId,
collectionId,
+ collectionName,
linkId,
}: Props) {
if (linkId) {
@@ -24,10 +26,11 @@ export default async function getPermission({
});
return check;
- } else if (collectionId) {
+ } else if (collectionId || collectionName) {
const check = await prisma.collection.findFirst({
where: {
- id: collectionId,
+ id: collectionId || undefined,
+ name: collectionName || undefined,
OR: [{ ownerId: userId }, { members: { some: { userId } } }],
},
include: { members: true },
diff --git a/lib/client/generateLinkHref.ts b/lib/client/generateLinkHref.ts
index 5911ae7..47c1888 100644
--- a/lib/client/generateLinkHref.ts
+++ b/lib/client/generateLinkHref.ts
@@ -1,27 +1,39 @@
-import { AccountSettings, ArchivedFormat, LinkIncludingShortenedCollectionAndTags } from "@/types/global";
+import {
+ AccountSettings,
+ ArchivedFormat,
+ LinkIncludingShortenedCollectionAndTags,
+} from "@/types/global";
import { LinksRouteTo } from "@prisma/client";
-import { pdfAvailable, readabilityAvailable, screenshotAvailable } from "../shared/getArchiveValidity";
+import {
+ pdfAvailable,
+ readabilityAvailable,
+ screenshotAvailable,
+} from "../shared/getArchiveValidity";
-export const generateLinkHref = (link: LinkIncludingShortenedCollectionAndTags, account: AccountSettings): string => {
+export const generateLinkHref = (
+ link: LinkIncludingShortenedCollectionAndTags,
+ account: AccountSettings
+): string => {
+ // Return the links href based on the account's preference
+ // If the user's preference is not available, return the original link
+ switch (account.linksRouteTo) {
+ case LinksRouteTo.ORIGINAL:
+ return link.url || "";
+ case LinksRouteTo.PDF:
+ if (!pdfAvailable(link)) return link.url || "";
- // Return the links href based on the account's preference
- // If the user's preference is not available, return the original link
- switch (account.linksRouteTo) {
- case LinksRouteTo.ORIGINAL:
- return link.url || '';
- case LinksRouteTo.PDF:
- if (!pdfAvailable(link)) return link.url || '';
+ return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`;
+ case LinksRouteTo.READABLE:
+ if (!readabilityAvailable(link)) return link.url || "";
- return `/preserved/${link?.id}?format=${ArchivedFormat.pdf}`;
- case LinksRouteTo.READABLE:
- if (!readabilityAvailable(link)) return link.url || '';
+ return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`;
+ case LinksRouteTo.SCREENSHOT:
+ if (!screenshotAvailable(link)) return link.url || "";
- return `/preserved/${link?.id}?format=${ArchivedFormat.readability}`;
- case LinksRouteTo.SCREENSHOT:
- if (!screenshotAvailable(link)) return link.url || '';
-
- return `/preserved/${link?.id}?format=${link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg}`;
- default:
- return link.url || '';
- }
-};
\ No newline at end of file
+ return `/preserved/${link?.id}?format=${
+ link?.image?.endsWith("png") ? ArchivedFormat.png : ArchivedFormat.jpeg
+ }`;
+ default:
+ return link.url || "";
+ }
+};
diff --git a/lib/shared/getArchiveValidity.ts b/lib/shared/getArchiveValidity.ts
index ec74b22..0da5504 100644
--- a/lib/shared/getArchiveValidity.ts
+++ b/lib/shared/getArchiveValidity.ts
@@ -1,6 +1,8 @@
import { LinkIncludingShortenedCollectionAndTags } from "@/types/global";
-export function screenshotAvailable(link: LinkIncludingShortenedCollectionAndTags) {
+export function screenshotAvailable(
+ link: LinkIncludingShortenedCollectionAndTags
+) {
return (
link &&
link.image &&
@@ -15,7 +17,9 @@ export function pdfAvailable(link: LinkIncludingShortenedCollectionAndTags) {
);
}
-export function readabilityAvailable(link: LinkIncludingShortenedCollectionAndTags) {
+export function readabilityAvailable(
+ link: LinkIncludingShortenedCollectionAndTags
+) {
return (
link &&
link.readable &&
diff --git a/pages/api/v1/links/index.ts b/pages/api/v1/links/index.ts
index 58a352a..35b0243 100644
--- a/pages/api/v1/links/index.ts
+++ b/pages/api/v1/links/index.ts
@@ -3,6 +3,8 @@ import getLinks from "@/lib/api/controllers/links/getLinks";
import postLink from "@/lib/api/controllers/links/postLink";
import { LinkRequestQuery } from "@/types/global";
import verifyUser from "@/lib/api/verifyUser";
+import deleteLinksById from "@/lib/api/controllers/links/bulk/deleteLinksById";
+import updateLinks from "@/lib/api/controllers/links/bulk/updateLinks";
export default async function links(req: NextApiRequest, res: NextApiResponse) {
const user = await verifyUser({ req, res });
@@ -39,5 +41,20 @@ export default async function links(req: NextApiRequest, res: NextApiResponse) {
return res.status(newlink.status).json({
response: newlink.response,
});
+ } else if (req.method === "PUT") {
+ const updated = await updateLinks(
+ user.id,
+ req.body.links,
+ req.body.removePreviousTags,
+ req.body.newData
+ );
+ return res.status(updated.status).json({
+ response: updated.response,
+ });
+ } else if (req.method === "DELETE") {
+ const deleted = await deleteLinksById(user.id, req.body.linkIds);
+ return res.status(deleted.status).json({
+ response: deleted.response,
+ });
}
}
diff --git a/pages/collections/[id].tsx b/pages/collections/[id].tsx
index fd0f970..323e6e6 100644
--- a/pages/collections/[id].tsx
+++ b/pages/collections/[id].tsx
@@ -24,15 +24,18 @@ import CardView from "@/components/LinkViews/Layouts/CardView";
// import GridView from "@/components/LinkViews/Layouts/GridView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import { dropdownTriggerer } from "@/lib/client/utils";
-import Link from "next/link";
import NewCollectionModal from "@/components/ModalContent/NewCollectionModal";
+import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal";
+import toast from "react-hot-toast";
+import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal";
export default function Index() {
const { settings } = useLocalSettingsStore();
const router = useRouter();
- const { links } = useLinkStore();
+ const { links, selectedLinks, setSelectedLinks, deleteLinksById } =
+ useLinkStore();
const { collections } = useCollectionStore();
const [sortBy, setSortBy] = useState(Sort.DateNewestFirst);
@@ -81,6 +84,9 @@ export default function Index() {
};
fetchOwner();
+
+ // When the collection changes, reset the selected links
+ setSelectedLinks([]);
}, [activeCollection]);
const [editCollectionModal, setEditCollectionModal] = useState(false);
@@ -88,6 +94,14 @@ export default function Index() {
const [editCollectionSharingModal, setEditCollectionSharingModal] =
useState(false);
const [deleteCollectionModal, setDeleteCollectionModal] = useState(false);
+ const [bulkDeleteLinksModal, setBulkDeleteLinksModal] = useState(false);
+ const [bulkEditLinksModal, setBulkEditLinksModal] = useState(false);
+ const [editMode, setEditMode] = useState(false);
+ useEffect(() => {
+ return () => {
+ setEditMode(false);
+ };
+ }, [router]);
const [viewMode, setViewMode] = useState(
localStorage.getItem("viewMode") || ViewMode.Card
@@ -102,6 +116,35 @@ export default function Index() {
// @ts-ignore
const LinkComponent = linkView[viewMode];
+ const handleSelectAll = () => {
+ if (selectedLinks.length === links.length) {
+ setSelectedLinks([]);
+ } else {
+ setSelectedLinks(links.map((link) => link));
+ }
+ };
+
+ const bulkDeleteLinks = async () => {
+ const load = toast.loading(
+ `Deleting ${selectedLinks.length} Link${
+ selectedLinks.length > 1 ? "s" : ""
+ }...`
+ );
+
+ const response = await deleteLinksById(
+ selectedLinks.map((link) => link.id as number)
+ );
+
+ toast.dismiss(load);
+
+ response.ok &&
+ toast.success(
+ `Deleted ${selectedLinks.length} Link${
+ selectedLinks.length > 1 ? "s" : ""
+ }!`
+ );
+ };
+
return (
- {permissions === true ? (
+ {permissions === true && (
-
- ) : undefined}
+ )}
-
- {permissions === true ? (
+ {permissions === true && (
-
- ) : undefined}
+ )}
-
)}
- {activeCollection ? (
+ {activeCollection && (
By {collectionOwner.name}
- {activeCollection.members.length > 0
- ? ` and ${activeCollection.members.length} others`
- : undefined}
+ {activeCollection.members.length > 0 &&
+ ` and ${activeCollection.members.length} others`}
.
- ) : undefined}
+ )}
- {activeCollection?.description ? (
+ {activeCollection?.description && (
{activeCollection?.description}
- ) : undefined}
+ )}
{/* {collections.some((e) => e.parentId === activeCollection.id) ? (
+ {editMode && (
+
+ {links.length > 0 && (
+
+ handleSelectAll()}
+ checked={
+ selectedLinks.length === links.length && links.length > 0
+ }
+ />
+ {selectedLinks.length > 0 ? (
+
+ {selectedLinks.length}{" "}
+ {selectedLinks.length === 1 ? "link" : "links"} selected
+
+ ) : (
+ Nothing selected
+ )}
+
+ )}
+
+
+
+
+
+ )}
+
{links.some((e) => e.pinnedBy && e.pinnedBy[0]) ? (
-
+
) : (
)}
+ {bulkDeleteLinksModal && (
+
{
+ setBulkDeleteLinksModal(false);
+ }}
+ />
+ )}
+ {bulkEditLinksModal && (
+ {
+ setBulkEditLinksModal(false);
+ }}
+ />
+ )}
);
}
diff --git a/pages/tags/[id].tsx b/pages/tags/[id].tsx
index 47b41e1..8924a3c 100644
--- a/pages/tags/[id].tsx
+++ b/pages/tags/[id].tsx
@@ -1,6 +1,6 @@
import useLinkStore from "@/store/links";
import { useRouter } from "next/router";
-import { FormEvent, useEffect, useState } from "react";
+import { FormEvent, use, useEffect, useState } from "react";
import MainLayout from "@/layouts/MainLayout";
import useTagStore from "@/store/tags";
import SortDropdown from "@/components/SortDropdown";
@@ -12,11 +12,15 @@ import CardView from "@/components/LinkViews/Layouts/CardView";
// import GridView from "@/components/LinkViews/Layouts/GridView";
import ListView from "@/components/LinkViews/Layouts/ListView";
import { dropdownTriggerer } from "@/lib/client/utils";
+import BulkDeleteLinksModal from "@/components/ModalContent/BulkDeleteLinksModal";
+import BulkEditLinksModal from "@/components/ModalContent/BulkEditLinksModal";
+import useCollectivePermissions from "@/hooks/useCollectivePermissions";
export default function Index() {
const router = useRouter();
- const { links } = useLinkStore();
+ const { links, selectedLinks, deleteLinksById, setSelectedLinks } =
+ useLinkStore();
const { tags, updateTag, removeTag } = useTagStore();
const [sortBy, setSortBy] = useState(Sort.DateNewestFirst);
@@ -26,11 +30,31 @@ export default function Index() {
const [activeTag, setActiveTag] = useState();
+ const [bulkDeleteLinksModal, setBulkDeleteLinksModal] = useState(false);
+ const [bulkEditLinksModal, setBulkEditLinksModal] = useState(false);
+ const [editMode, setEditMode] = useState(false);
+ useEffect(() => {
+ return () => {
+ setEditMode(false);
+ };
+ }, [router]);
+
+ const collectivePermissions = useCollectivePermissions(
+ selectedLinks.map((link) => link.collectionId as number)
+ );
+
useLinks({ tagId: Number(router.query.id), sort: sortBy });
useEffect(() => {
- setActiveTag(tags.find((e) => e.id === Number(router.query.id)));
- }, [router, tags]);
+ const tag = tags.find((e) => e.id === Number(router.query.id));
+
+ if (tags.length > 0 && !tag?.id) {
+ router.push("/dashboard");
+ return;
+ }
+
+ setActiveTag(tag);
+ }, [router, tags, Number(router.query.id), setActiveTag]);
useEffect(() => {
setNewTagName(activeTag?.name);
@@ -91,6 +115,35 @@ export default function Index() {
setRenameTag(false);
};
+ const handleSelectAll = () => {
+ if (selectedLinks.length === links.length) {
+ setSelectedLinks([]);
+ } else {
+ setSelectedLinks(links.map((link) => link));
+ }
+ };
+
+ const bulkDeleteLinks = async () => {
+ const load = toast.loading(
+ `Deleting ${selectedLinks.length} Link${
+ selectedLinks.length > 1 ? "s" : ""
+ }...`
+ );
+
+ const response = await deleteLinksById(
+ selectedLinks.map((link) => link.id as number)
+ );
+
+ toast.dismiss(load);
+
+ response.ok &&
+ toast.success(
+ `Deleted ${selectedLinks.length} Link${
+ selectedLinks.length > 1 ? "s" : ""
+ }!`
+ );
+ };
+
const [viewMode, setViewMode] = useState(
localStorage.getItem("viewMode") || ViewMode.Card
);
@@ -195,16 +248,102 @@ export default function Index() {
+
{
+ setEditMode(!editMode);
+ setSelectedLinks([]);
+ }}
+ className={`btn btn-square btn-sm btn-ghost ${
+ editMode
+ ? "bg-primary/20 hover:bg-primary/20"
+ : "hover:bg-neutral/20"
+ }`}
+ >
+
+
+ {editMode && (
+
+ {links.length > 0 && (
+
+ handleSelectAll()}
+ checked={
+ selectedLinks.length === links.length && links.length > 0
+ }
+ />
+ {selectedLinks.length > 0 ? (
+
+ {selectedLinks.length}{" "}
+ {selectedLinks.length === 1 ? "link" : "links"} selected
+
+ ) : (
+ Nothing selected
+ )}
+
+ )}
+
+
+
+
+
+ )}
e.tags.some((e) => e.id === Number(router.query.id))
)}
/>
+ {bulkDeleteLinksModal && (
+ {
+ setBulkDeleteLinksModal(false);
+ }}
+ />
+ )}
+ {bulkEditLinksModal && (
+ {
+ setBulkEditLinksModal(false);
+ }}
+ />
+ )}
);
}
diff --git a/store/links.ts b/store/links.ts
index ab74b03..408a3ee 100644
--- a/store/links.ts
+++ b/store/links.ts
@@ -10,10 +10,12 @@ type ResponseObject = {
type LinkStore = {
links: LinkIncludingShortenedCollectionAndTags[];
+ selectedLinks: LinkIncludingShortenedCollectionAndTags[];
setLinks: (
data: LinkIncludingShortenedCollectionAndTags[],
isInitialCall: boolean
) => void;
+ setSelectedLinks: (links: LinkIncludingShortenedCollectionAndTags[]) => void;
addLink: (
body: LinkIncludingShortenedCollectionAndTags
) => Promise;
@@ -21,12 +23,22 @@ type LinkStore = {
updateLink: (
link: LinkIncludingShortenedCollectionAndTags
) => Promise;
+ updateLinks: (
+ links: LinkIncludingShortenedCollectionAndTags[],
+ removePreviousTags: boolean,
+ newData: Pick<
+ LinkIncludingShortenedCollectionAndTags,
+ "tags" | "collectionId"
+ >
+ ) => Promise;
removeLink: (linkId: number) => Promise;
+ deleteLinksById: (linkIds: number[]) => Promise;
resetLinks: () => void;
};
const useLinkStore = create()((set) => ({
links: [],
+ selectedLinks: [],
setLinks: async (data, isInitialCall) => {
isInitialCall &&
set(() => ({
@@ -45,6 +57,7 @@ const useLinkStore = create()((set) => ({
),
}));
},
+ setSelectedLinks: (links) => set({ selectedLinks: links }),
addLink: async (body) => {
const response = await fetch("/api/v1/links", {
body: JSON.stringify(body),
@@ -122,6 +135,41 @@ const useLinkStore = create()((set) => ({
return { ok: response.ok, data: data.response };
},
+ updateLinks: async (links, removePreviousTags, newData) => {
+ const response = await fetch("/api/v1/links", {
+ body: JSON.stringify({ links, removePreviousTags, newData }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ method: "PUT",
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ set((state) => ({
+ links: state.links.map((e) =>
+ links.some((link) => link.id === e.id)
+ ? {
+ ...e,
+ collectionId: newData.collectionId ?? e.collectionId,
+ collection: {
+ ...e.collection,
+ id: newData.collectionId ?? e.collection.id,
+ },
+ tags: removePreviousTags
+ ? [...(newData.tags ?? [])]
+ : [...e.tags, ...(newData.tags ?? [])],
+ }
+ : e
+ ),
+ }));
+ useTagStore.getState().setTags();
+ useCollectionStore.getState().setCollections();
+ }
+
+ return { ok: response.ok, data: data.response };
+ },
removeLink: async (linkId) => {
const response = await fetch(`/api/v1/links/${linkId}`, {
headers: {
@@ -142,6 +190,27 @@ const useLinkStore = create()((set) => ({
return { ok: response.ok, data: data.response };
},
+ deleteLinksById: async (linkIds: number[]) => {
+ const response = await fetch("/api/v1/links", {
+ body: JSON.stringify({ linkIds }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ method: "DELETE",
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ set((state) => ({
+ links: state.links.filter((e) => !linkIds.includes(e.id as number)),
+ }));
+ useTagStore.getState().setTags();
+ useCollectionStore.getState().setCollections();
+ }
+
+ return { ok: response.ok, data: data.response };
+ },
resetLinks: () => set({ links: [] }),
}));
diff --git a/store/localSettings.ts b/store/localSettings.ts
index e38bae8..6c79d6b 100644
--- a/store/localSettings.ts
+++ b/store/localSettings.ts
@@ -1,5 +1,4 @@
import { create } from "zustand";
-import { ViewMode } from "@/types/global";
type LocalSettings = {
theme?: string;