diff --git a/components/CopyButton.tsx b/components/CopyButton.tsx new file mode 100644 index 0000000..090d64b --- /dev/null +++ b/components/CopyButton.tsx @@ -0,0 +1,36 @@ +import React from "react"; + +type Props = { + text: string; +}; + +const CopyButton = ({ text }: Props) => { + return ( +
{ + try { + navigator.clipboard.writeText(text).then(() => { + const copyIcon = document.querySelector(".bi-copy"); + if (copyIcon) { + copyIcon.classList.remove("bi-copy"); + copyIcon.classList.add("bi-check2"); + copyIcon.classList.add("text-success"); + } + setTimeout(() => { + if (copyIcon) { + copyIcon.classList.remove("bi-check2"); + copyIcon.classList.remove("text-success"); + copyIcon.classList.add("bi-copy"); + } + }, 1000); + }); + } catch (err) { + console.log(err); + } + }} + >
+ ); +}; + +export default CopyButton; diff --git a/components/DashboardItem.tsx b/components/DashboardItem.tsx index 337fc36..d255c4b 100644 --- a/components/DashboardItem.tsx +++ b/components/DashboardItem.tsx @@ -14,7 +14,12 @@ export default function dashboardItem({

{name}

-

{value}

+

+ {value < 1000 ? value : (value / 1000).toFixed(1) + "k"} +

+

+ {value} +

); diff --git a/components/Drawer.tsx b/components/Drawer.tsx new file mode 100644 index 0000000..12f931e --- /dev/null +++ b/components/Drawer.tsx @@ -0,0 +1,88 @@ +import React, { ReactNode, useEffect } from "react"; +import ClickAwayHandler from "@/components/ClickAwayHandler"; +import { Drawer as D } from "vaul"; + +type Props = { + toggleDrawer: Function; + children: ReactNode; + className?: string; + dismissible?: boolean; +}; + +export default function Drawer({ + toggleDrawer, + className, + children, + dismissible = true, +}: Props) { + const [drawerIsOpen, setDrawerIsOpen] = React.useState(true); + + useEffect(() => { + if (window.innerWidth >= 640) { + document.body.style.overflow = "hidden"; + document.body.style.position = "relative"; + return () => { + document.body.style.overflow = "auto"; + document.body.style.position = ""; + }; + } + }, []); + + if (window.innerWidth < 640) { + return ( + dismissible && setTimeout(() => toggleDrawer(), 350)} + dismissible={dismissible} + > + + + dismissible && setDrawerIsOpen(false)} + > + +
+
+ {children} +
+ + + + + ); + } else { + return ( + dismissible && setTimeout(() => toggleDrawer(), 350)} + dismissible={dismissible} + direction="right" + > + + + dismissible && setDrawerIsOpen(false)} + className="z-30" + > + +
+ {children} +
+
+
+
+
+ ); + } +} diff --git a/components/FilterSearchDropdown.tsx b/components/FilterSearchDropdown.tsx index a0adc37..70f0aca 100644 --- a/components/FilterSearchDropdown.tsx +++ b/components/FilterSearchDropdown.tsx @@ -1,6 +1,8 @@ import { dropdownTriggerer } from "@/lib/client/utils"; -import React from "react"; +import React, { useEffect } from "react"; import { useTranslation } from "next-i18next"; +import { resetInfiniteQueryPagination } from "@/hooks/store/links"; +import { useQueryClient } from "@tanstack/react-query"; type Props = { setSearchFilter: Function; @@ -18,6 +20,7 @@ export default function FilterSearchDropdown({ searchFilter, }: Props) { const { t } = useTranslation(); + const queryClient = useQueryClient(); return (
@@ -41,9 +44,10 @@ export default function FilterSearchDropdown({ name="search-filter-checkbox" className="checkbox checkbox-primary" checked={searchFilter.name} - onChange={() => - setSearchFilter({ ...searchFilter, name: !searchFilter.name }) - } + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSearchFilter({ ...searchFilter, name: !searchFilter.name }); + }} /> {t("name")} @@ -59,9 +63,10 @@ export default function FilterSearchDropdown({ name="search-filter-checkbox" className="checkbox checkbox-primary" checked={searchFilter.url} - onChange={() => - setSearchFilter({ ...searchFilter, url: !searchFilter.url }) - } + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSearchFilter({ ...searchFilter, url: !searchFilter.url }); + }} /> {t("link")} @@ -77,12 +82,13 @@ export default function FilterSearchDropdown({ name="search-filter-checkbox" className="checkbox checkbox-primary" checked={searchFilter.description} - onChange={() => + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); setSearchFilter({ ...searchFilter, description: !searchFilter.description, - }) - } + }); + }} /> {t("description")} @@ -100,9 +106,10 @@ export default function FilterSearchDropdown({ name="search-filter-checkbox" className="checkbox checkbox-primary" checked={searchFilter.tags} - onChange={() => - setSearchFilter({ ...searchFilter, tags: !searchFilter.tags }) - } + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSearchFilter({ ...searchFilter, tags: !searchFilter.tags }); + }} /> {t("tags")} @@ -118,12 +125,13 @@ export default function FilterSearchDropdown({ name="search-filter-checkbox" className="checkbox checkbox-primary" checked={searchFilter.textContent} - onChange={() => + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); setSearchFilter({ ...searchFilter, textContent: !searchFilter.textContent, - }) - } + }); + }} /> {t("full_content")} diff --git a/components/IconPicker.tsx b/components/IconPicker.tsx new file mode 100644 index 0000000..994a5af --- /dev/null +++ b/components/IconPicker.tsx @@ -0,0 +1,46 @@ +import { icons } from "@/lib/client/icons"; +import React, { useMemo, useState } from "react"; +import Fuse from "fuse.js"; +import TextInput from "./TextInput"; + +const fuse = new Fuse(icons, { + keys: [{ name: "name", weight: 4 }, "tags", "categories"], + threshold: 0.2, + useExtendedSearch: true, +}); + +type Props = {}; + +const IconPicker = (props: Props) => { + const [query, setQuery] = useState(""); + + const filteredQueryResultsSelector = useMemo(() => { + if (!query) { + return icons; + } + return fuse.search(query).map((result) => result.item); + }, [query]); + + return ( +
+ setQuery(e.target.value)} + /> +
+ {filteredQueryResultsSelector.map((icon) => { + const IconComponent = icon.Icon; + return ( +
console.log(icon.name)}> + +
+ ); + })} +
+
+ ); +}; + +export default IconPicker; diff --git a/components/LinkDetails.tsx b/components/LinkDetails.tsx new file mode 100644 index 0000000..e489078 --- /dev/null +++ b/components/LinkDetails.tsx @@ -0,0 +1,302 @@ +import React, { useEffect, useState } from "react"; +import { + LinkIncludingShortenedCollectionAndTags, + ArchivedFormat, +} from "@/types/global"; +import Link from "next/link"; +import { useSession } from "next-auth/react"; +import { + pdfAvailable, + readabilityAvailable, + monolithAvailable, + screenshotAvailable, +} from "@/lib/shared/getArchiveValidity"; +import PreservedFormatRow from "@/components/PreserverdFormatRow"; +import getPublicUserData from "@/lib/client/getPublicUserData"; +import { useTranslation } from "next-i18next"; +import { BeatLoader } from "react-spinners"; +import { useUser } from "@/hooks/store/user"; +import { useGetLink } from "@/hooks/store/links"; +import LinkIcon from "./LinkViews/LinkComponents/LinkIcon"; +import CopyButton from "./CopyButton"; +import { useRouter } from "next/router"; + +type Props = { + className?: string; + link: LinkIncludingShortenedCollectionAndTags; +}; + +export default function LinkDetails({ className, link }: Props) { + const { t } = useTranslation(); + const session = useSession(); + const getLink = useGetLink(); + const { data: user = {} } = useUser(); + + const [collectionOwner, setCollectionOwner] = useState({ + id: null as unknown as number, + name: "", + username: "", + image: "", + archiveAsScreenshot: undefined as unknown as boolean, + archiveAsMonolith: undefined as unknown as boolean, + archiveAsPDF: undefined as unknown as boolean, + }); + + useEffect(() => { + const fetchOwner = async () => { + if (link.collection.ownerId !== user.id) { + const owner = await getPublicUserData( + link.collection.ownerId as number + ); + setCollectionOwner(owner); + } else if (link.collection.ownerId === user.id) { + setCollectionOwner({ + id: user.id as number, + name: user.name, + username: user.username as string, + image: user.image as string, + archiveAsScreenshot: user.archiveAsScreenshot as boolean, + archiveAsMonolith: user.archiveAsScreenshot as boolean, + archiveAsPDF: user.archiveAsPDF as boolean, + }); + } + }; + + fetchOwner(); + }, [link.collection.ownerId]); + + const isReady = () => { + return ( + link && + (collectionOwner.archiveAsScreenshot === true + ? link.pdf && link.pdf !== "pending" + : true) && + (collectionOwner.archiveAsMonolith === true + ? link.monolith && link.monolith !== "pending" + : true) && + (collectionOwner.archiveAsPDF === true + ? link.pdf && link.pdf !== "pending" + : true) && + link.readable && + link.readable !== "pending" + ); + }; + + const atLeastOneFormatAvailable = () => { + return ( + screenshotAvailable(link) || + pdfAvailable(link) || + readabilityAvailable(link) || + monolithAvailable(link) + ); + }; + + useEffect(() => { + (async () => { + await getLink.mutateAsync({ + id: link.id as number, + }); + })(); + + let interval: any; + + if (!isReady()) { + interval = setInterval(async () => { + await getLink.mutateAsync({ + id: link.id as number, + }); + }, 5000); + } else { + if (interval) { + clearInterval(interval); + } + } + + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [link?.monolith]); + + const router = useRouter(); + + const isPublicRoute = router.pathname.startsWith("/public") ? true : false; + + return ( +
+ + + {link.name &&

{link.name}

} + + {link.url && ( + <> +
+ +

{t("link")}

+ +
+ + {link.url} + + +
+ + )} + +
+ +

{t("collection")}

+ + +

{link.collection.name}

+ + + + {link.tags[0] && ( + <> +
+ +
+

{t("tags")}

+
+ +
+ {link.tags.map((tag) => + isPublicRoute ? ( +
+ {tag.name} +
+ ) : ( + + {tag.name} + + ) + )} +
+ + )} + + {link.description && ( + <> +
+ +
+

{t("notes")}

+ +
+

{link.description}

+
+
+ + )} + +
+ +

+ {link.url ? t("preserved_formats") : t("file")} +

+ +
+ {monolithAvailable(link) ? ( + + ) : undefined} + + {screenshotAvailable(link) ? ( + + ) : undefined} + + {pdfAvailable(link) ? ( + + ) : undefined} + + {readabilityAvailable(link) ? ( + + ) : undefined} + + {!isReady() && !atLeastOneFormatAvailable() ? ( +
+ + +

{t("preservation_in_queue")}

+

{t("check_back_later")}

+
+ ) : link.url && !isReady() && atLeastOneFormatAvailable() ? ( +
+ +

{t("there_are_more_formats")}

+

{t("check_back_later")}

+
+ ) : undefined} + + {link.url && ( + +

{t("view_latest_snapshot")}

+ + + )} +
+
+ ); +} diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx index ad7e48b..9081e47 100644 --- a/components/LinkViews/LinkComponents/LinkActions.tsx +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -202,9 +202,6 @@ export default function LinkActions({ link={link} /> )} - {/* {expandedLink ? ( - setExpandedLink(false)} link={link} /> - ) : undefined} */} ); } diff --git a/components/LinkViews/LinkComponents/LinkCard.tsx b/components/LinkViews/LinkComponents/LinkCard.tsx index 0e0796b..f1e32df 100644 --- a/components/LinkViews/LinkComponents/LinkCard.tsx +++ b/components/LinkViews/LinkComponents/LinkCard.tsx @@ -22,6 +22,7 @@ import { useTranslation } from "next-i18next"; import { useCollections } from "@/hooks/store/collections"; import { useUser } from "@/hooks/store/user"; import { useGetLink, useLinks } from "@/hooks/store/links"; +import { useRouter } from "next/router"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -90,6 +91,10 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { const isVisible = useOnScreen(ref); const permissions = usePermissions(collection?.id as number); + const router = useRouter(); + + let isPublic = router.pathname.startsWith("/public") ? true : undefined; + useEffect(() => { let interval: NodeJS.Timeout | null = null; @@ -99,7 +104,7 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { link.preview !== "unavailable" ) { interval = setInterval(async () => { - getLink.mutateAsync(link.id as number); + getLink.mutateAsync({ id: link.id as number, isPublicRoute: isPublic }); }, 5000); } @@ -110,8 +115,6 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) { }; }, [isVisible, link.preview]); - const [showInfo, setShowInfo] = useState(false); - const selectedStyle = selectedLinks.some( (selectedLink) => selectedLink.id === link.id ) @@ -196,63 +199,10 @@ export default function LinkCard({ link, flipDropdown, editMode }: Props) {
- {showInfo && ( -
-
setShowInfo(!showInfo)} - className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10" - > - -
-

- {t("description")} -

- -
-

- {link.description ? ( - unescapeString(link.description) - ) : ( - - {t("no_description")} - - )} -

- {link.tags && link.tags[0] && ( - <> -

- {t("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/LinkComponents/LinkList.tsx b/components/LinkViews/LinkComponents/LinkList.tsx index 745e96f..31a214b 100644 --- a/components/LinkViews/LinkComponents/LinkList.tsx +++ b/components/LinkViews/LinkComponents/LinkList.tsx @@ -80,8 +80,6 @@ export default function LinkCardCompact({ const permissions = usePermissions(collection?.id as number); - const [showInfo, setShowInfo] = useState(false); - const selectedStyle = selectedLinks.some( (selectedLink) => selectedLink.id === link.id ) @@ -96,7 +94,7 @@ export default function LinkCardCompact({ <>
selectable @@ -106,20 +104,6 @@ export default function LinkCardCompact({ : undefined } > - {/* {showCheckbox && - editMode && - (permissions === true || - permissions?.canCreate || - permissions?.canDelete) && ( - selectedLink.id === link.id - )} - onChange={() => handleCheckboxClick(link)} - /> - )} */}
@@ -157,8 +141,6 @@ export default function LinkCardCompact({ collection={collection} position="top-3 right-3" flipDropdown={flipDropdown} - // toggleShowInfo={() => setShowInfo(!showInfo)} - // linkInfo={showInfo} />
{ - getLink.mutateAsync(link.id as number); + getLink.mutateAsync({ id: link.id as number }); }, 5000); } @@ -107,8 +107,6 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) { }; }, [isVisible, link.preview]); - const [showInfo, setShowInfo] = useState(false); - const selectedStyle = selectedLinks.some( (selectedLink) => selectedLink.id === link.id ) @@ -207,57 +205,6 @@ export default function LinkMasonry({ link, flipDropdown, editMode }: Props) {
- {showInfo && ( -
-
setShowInfo(!showInfo)} - className=" float-right btn btn-sm outline-none btn-circle btn-ghost z-10" - > - -
-

- {t("description")} -

- -
-

- {link.description ? ( - unescapeString(link.description) - ) : ( - - {t("no_description")} - - )} -

- {link.tags && link.tags[0] && ( - <> -

- {t("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/Modal.tsx b/components/Modal.tsx index 4691df7..2eb97e4 100644 --- a/components/Modal.tsx +++ b/components/Modal.tsx @@ -32,7 +32,7 @@ export default function Modal({ return ( dismissible && setTimeout(() => toggleModal(), 100)} + onClose={() => dismissible && setTimeout(() => toggleModal(), 350)} dismissible={dismissible} > diff --git a/components/ModalContent/EditCollectionSharingModal.tsx b/components/ModalContent/EditCollectionSharingModal.tsx index 5763b5d..9f84cb1 100644 --- a/components/ModalContent/EditCollectionSharingModal.tsx +++ b/components/ModalContent/EditCollectionSharingModal.tsx @@ -15,6 +15,7 @@ import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; import { useUpdateCollection } from "@/hooks/store/collections"; import { useUser } from "@/hooks/store/user"; +import CopyButton from "../CopyButton"; type Props = { onClose: Function; @@ -133,19 +134,9 @@ export default function EditCollectionSharingModal({ {collection.isPublic && (

{t("sharable_link_guide")}

-
{ - try { - navigator.clipboard - .writeText(publicCollectionURL) - .then(() => toast.success(t("copied"))); - } catch (err) { - console.log(err); - } - }} - className="w-full hide-scrollbar overflow-x-auto whitespace-nowrap rounded-md p-2 bg-base-200 border-neutral-content border-solid border outline-none hover:border-primary dark:hover:border-primary duration-100 cursor-text" - > +
{publicCollectionURL} +
)} diff --git a/components/ModalContent/LinkDetailModal.tsx b/components/ModalContent/LinkDetailModal.tsx new file mode 100644 index 0000000..2e6308a --- /dev/null +++ b/components/ModalContent/LinkDetailModal.tsx @@ -0,0 +1,145 @@ +import React, { useEffect, useState } from "react"; +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import getPublicUserData from "@/lib/client/getPublicUserData"; +import { useTranslation } from "next-i18next"; +import { useUser } from "@/hooks/store/user"; +import { useGetLink } from "@/hooks/store/links"; +import Drawer from "../Drawer"; +import LinkDetails from "../LinkDetails"; +import Link from "next/link"; +import usePermissions from "@/hooks/usePermissions"; +import { useRouter } from "next/router"; + +type Props = { + onClose: Function; + onEdit: Function; + link: LinkIncludingShortenedCollectionAndTags; +}; + +export default function LinkDetailModal({ onClose, onEdit, link }: Props) { + const { t } = useTranslation(); + const getLink = useGetLink(); + const { data: user = {} } = useUser(); + + const [collectionOwner, setCollectionOwner] = useState({ + id: null as unknown as number, + name: "", + username: "", + image: "", + archiveAsScreenshot: undefined as unknown as boolean, + archiveAsMonolith: undefined as unknown as boolean, + archiveAsPDF: undefined as unknown as boolean, + }); + + useEffect(() => { + const fetchOwner = async () => { + if (link.collection.ownerId !== user.id) { + const owner = await getPublicUserData( + link.collection.ownerId as number + ); + setCollectionOwner(owner); + } else if (link.collection.ownerId === user.id) { + setCollectionOwner({ + id: user.id as number, + name: user.name, + username: user.username as string, + image: user.image as string, + archiveAsScreenshot: user.archiveAsScreenshot as boolean, + archiveAsMonolith: user.archiveAsScreenshot as boolean, + archiveAsPDF: user.archiveAsPDF as boolean, + }); + } + }; + + fetchOwner(); + }, [link.collection.ownerId]); + + const isReady = () => { + return ( + link && + (collectionOwner.archiveAsScreenshot === true + ? link.pdf && link.pdf !== "pending" + : true) && + (collectionOwner.archiveAsMonolith === true + ? link.monolith && link.monolith !== "pending" + : true) && + (collectionOwner.archiveAsPDF === true + ? link.pdf && link.pdf !== "pending" + : true) && + link.readable && + link.readable !== "pending" + ); + }; + + useEffect(() => { + (async () => { + await getLink.mutateAsync({ + id: link.id as number, + }); + })(); + + let interval: any; + + if (!isReady()) { + interval = setInterval(async () => { + await getLink.mutateAsync({ + id: link.id as number, + }); + }, 5000); + } else { + if (interval) { + clearInterval(interval); + } + } + + return () => { + if (interval) { + clearInterval(interval); + } + }; + }, [link?.monolith]); + + const permissions = usePermissions(link.collection.id as number); + + const router = useRouter(); + + const isPublicRoute = router.pathname.startsWith("/public") ? true : false; + + return ( + +
onClose()} + >
+ + +
+ + + {permissions === true || + (permissions?.canUpdate && ( + <> +
+
+ +
+
{ + onEdit(); + onClose(); + }} + > + {t("edit_link")} +
+
+ + ))} +
+
+ ); +} diff --git a/components/ModalContent/NewLinkModal.tsx b/components/ModalContent/NewLinkModal.tsx index 0c00086..61ee8bc 100644 --- a/components/ModalContent/NewLinkModal.tsx +++ b/components/ModalContent/NewLinkModal.tsx @@ -61,7 +61,7 @@ export default function NewLinkModal({ onClose }: Props) { }; useEffect(() => { - if (router.query.id) { + if (router.pathname.startsWith("/collections/") && router.query.id) { const currentCollection = collections.find( (e) => e.id == Number(router.query.id) ); diff --git a/components/ModalContent/NewTokenModal.tsx b/components/ModalContent/NewTokenModal.tsx index 4d39aec..5c57012 100644 --- a/components/ModalContent/NewTokenModal.tsx +++ b/components/ModalContent/NewTokenModal.tsx @@ -7,6 +7,7 @@ import { dropdownTriggerer } from "@/lib/client/utils"; import Button from "../ui/Button"; import { useTranslation } from "next-i18next"; import { useAddToken } from "@/hooks/store/tokens"; +import CopyButton from "../CopyButton"; type Props = { onClose: Function; @@ -68,21 +69,14 @@ export default function NewTokenModal({ onClose }: Props) {

{t("access_token_created")}

{t("token_creation_notice")}

- {}} - className="w-full" - /> - +
+
+ {newToken} +
+ +
+
+
) : ( <> diff --git a/components/ModalContent/PreservedFormatsModal.tsx b/components/ModalContent/PreservedFormatsModal.tsx index 5a0983c..d883a21 100644 --- a/components/ModalContent/PreservedFormatsModal.tsx +++ b/components/ModalContent/PreservedFormatsModal.tsx @@ -91,14 +91,14 @@ export default function PreservedFormatsModal({ onClose, link }: Props) { useEffect(() => { (async () => { - await getLink.mutateAsync(link.id as number); + await getLink.mutateAsync({ id: link.id as number }); })(); let interval: NodeJS.Timeout | null = null; if (!isReady()) { interval = setInterval(async () => { - await getLink.mutateAsync(link.id as number); + await getLink.mutateAsync({ id: link.id as number }); }, 5000); } else { if (interval) { @@ -124,7 +124,7 @@ export default function PreservedFormatsModal({ onClose, link }: Props) { toast.dismiss(load); if (response.ok) { - await getLink.mutateAsync(link?.id as number); + await getLink.mutateAsync({ id: link.id as number }); toast.success(t("link_being_archived")); } else toast.error(data.response); diff --git a/components/ModalContent/UploadFileModal.tsx b/components/ModalContent/UploadFileModal.tsx index 7cb6fda..1627bb1 100644 --- a/components/ModalContent/UploadFileModal.tsx +++ b/components/ModalContent/UploadFileModal.tsx @@ -70,7 +70,7 @@ export default function UploadFileModal({ onClose }: Props) { useEffect(() => { setOptionsExpanded(false); - if (router.query.id) { + if (router.pathname.startsWith("/collections/") && router.query.id) { const currentCollection = collections.find( (e) => e.id == Number(router.query.id) ); diff --git a/components/PreserverdFormatRow.tsx b/components/PreserverdFormatRow.tsx index 58ee648..7919807 100644 --- a/components/PreserverdFormatRow.tsx +++ b/components/PreserverdFormatRow.tsx @@ -49,11 +49,9 @@ export default function PreservedFormatRow({ }; return ( -
+
-
- -
+

{name}

@@ -61,7 +59,7 @@ export default function PreservedFormatRow({ {downloadable || false ? (
handleDownload()} - className="btn btn-sm btn-square" + className="btn btn-sm btn-square btn-ghost" >
@@ -72,9 +70,9 @@ export default function PreservedFormatRow({ isPublic ? "/public" : "" }/preserved/${link?.id}?format=${format}`} target="_blank" - className="btn btn-sm btn-square" + className="btn btn-sm btn-square btn-ghost" > - +
diff --git a/components/ReadableView.tsx b/components/ReadableView.tsx index 073e879..18477a0 100644 --- a/components/ReadableView.tsx +++ b/components/ReadableView.tsx @@ -46,13 +46,6 @@ export default function ReadableView({ link }: Props) { const router = useRouter(); const getLink = useGetLink(); - const { data: collections = [] } = useCollections(); - - const collection = useMemo(() => { - return collections.find( - (e) => e.id === link.collection.id - ) as CollectionIncludingMembersAndLinkCount; - }, [collections, link]); useEffect(() => { const fetchLinkContent = async () => { @@ -73,7 +66,7 @@ export default function ReadableView({ link }: Props) { }, [link]); useEffect(() => { - if (link) getLink.mutateAsync(link?.id as number); + if (link) getLink.mutateAsync({ id: link.id as number }); let interval: NodeJS.Timeout | null = null; if ( @@ -88,7 +81,10 @@ export default function ReadableView({ link }: Props) { !link?.monolith) ) { interval = setInterval( - () => getLink.mutateAsync(link.id as number), + () => + getLink.mutateAsync({ + id: link.id as number, + }), 5000 ); } else { @@ -242,13 +238,6 @@ export default function ReadableView({ link }: Props) { {link?.name ?

{unescapeString(link?.description)}

: undefined}
- -
diff --git a/components/SortDropdown.tsx b/components/SortDropdown.tsx index 7dee461..af9ed5f 100644 --- a/components/SortDropdown.tsx +++ b/components/SortDropdown.tsx @@ -3,6 +3,8 @@ import { Sort } from "@/types/global"; import { dropdownTriggerer } from "@/lib/client/utils"; import { TFunction } from "i18next"; import useLocalSettingsStore from "@/store/localSettings"; +import { resetInfiniteQueryPagination } from "@/hooks/store/links"; +import { useQueryClient } from "@tanstack/react-query"; type Props = { sortBy: Sort; @@ -12,6 +14,7 @@ type Props = { export default function SortDropdown({ sortBy, setSort, t }: Props) { const { updateSettings } = useLocalSettingsStore(); + const queryClient = useQueryClient(); useEffect(() => { updateSettings({ sortBy }); @@ -39,7 +42,10 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) { name="sort-radio" className="radio checked:bg-primary" checked={sortBy === Sort.DateNewestFirst} - onChange={() => setSort(Sort.DateNewestFirst)} + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSort(Sort.DateNewestFirst); + }} /> {t("date_newest_first")} @@ -57,7 +63,10 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) { name="sort-radio" className="radio checked:bg-primary" checked={sortBy === Sort.DateOldestFirst} - onChange={() => setSort(Sort.DateOldestFirst)} + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSort(Sort.DateOldestFirst); + }} /> {t("date_oldest_first")} @@ -75,7 +84,10 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) { name="sort-radio" className="radio checked:bg-primary" checked={sortBy === Sort.NameAZ} - onChange={() => setSort(Sort.NameAZ)} + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSort(Sort.NameAZ); + }} /> {t("name_az")} @@ -91,7 +103,10 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) { name="sort-radio" className="radio checked:bg-primary" checked={sortBy === Sort.NameZA} - onChange={() => setSort(Sort.NameZA)} + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSort(Sort.NameZA); + }} /> {t("name_za")} @@ -107,7 +122,10 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) { name="sort-radio" className="radio checked:bg-primary" checked={sortBy === Sort.DescriptionAZ} - onChange={() => setSort(Sort.DescriptionAZ)} + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSort(Sort.DescriptionAZ); + }} /> {t("description_az")} @@ -125,7 +143,10 @@ export default function SortDropdown({ sortBy, setSort, t }: Props) { name="sort-radio" className="radio checked:bg-primary" checked={sortBy === Sort.DescriptionZA} - onChange={() => setSort(Sort.DescriptionZA)} + onChange={() => { + resetInfiniteQueryPagination(queryClient, ["links"]); + setSort(Sort.DescriptionZA); + }} /> {t("description_za")} diff --git a/hooks/store/links.tsx b/hooks/store/links.tsx index 4702ff4..6d2177f 100644 --- a/hooks/store/links.tsx +++ b/hooks/store/links.tsx @@ -159,20 +159,23 @@ const useUpdateLink = () => { return data.response; }, onSuccess: (data) => { - queryClient.setQueryData(["dashboardData"], (oldData: any) => { - if (!oldData) return undefined; - return oldData.map((e: any) => (e.id === data.id ? data : e)); - }); + // queryClient.setQueryData(["dashboardData"], (oldData: any) => { + // if (!oldData) return undefined; + // return oldData.map((e: any) => (e.id === data.id ? data : e)); + // }); - queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { - if (!oldData) return undefined; - return { - pages: oldData.pages.map((page: any) => - page.map((item: any) => (item.id === data.id ? data : item)) - ), - pageParams: oldData.pageParams, - }; - }); + // queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { + // if (!oldData) return undefined; + // return { + // pages: oldData.pages.map((page: any) => + // page.map((item: any) => (item.id === data.id ? data : item)) + // ), + // pageParams: oldData.pageParams, + // }; + // }); + + queryClient.invalidateQueries({ queryKey: ["links"] }); // Temporary workaround + queryClient.invalidateQueries({ queryKey: ["dashboardData"] }); // Temporary workaround queryClient.invalidateQueries({ queryKey: ["collections"] }); queryClient.invalidateQueries({ queryKey: ["tags"] }); @@ -222,9 +225,21 @@ const useDeleteLink = () => { const useGetLink = () => { const queryClient = useQueryClient(); + const router = useRouter(); + return useMutation({ - mutationFn: async (id: number) => { - const response = await fetch(`/api/v1/links/${id}`); + mutationFn: async ({ + id, + isPublicRoute = router.pathname.startsWith("/public") ? true : undefined, + }: { + id: number; + isPublicRoute?: boolean; + }) => { + const path = isPublicRoute + ? `/api/v1/public/links/${id}` + : `/api/v1/links/${id}`; + + const response = await fetch(path); const data = await response.json(); if (!response.ok) throw new Error(data.response); @@ -247,7 +262,20 @@ const useGetLink = () => { }; }); - queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); + queryClient.setQueriesData( + { queryKey: ["publicLinks"] }, + (oldData: any) => { + if (!oldData) return undefined; + return { + pages: oldData.pages.map((page: any) => + page.map((item: any) => (item.id === data.id ? data : item)) + ), + pageParams: oldData.pageParams, + }; + } + ); + + // queryClient.invalidateQueries({ queryKey: ["publicLinks"] }); }, }); }; @@ -398,14 +426,13 @@ const useBulkEditLinks = () => { return data.response; }, onSuccess: (data, { links, newData, removePreviousTags }) => { - queryClient.setQueryData(["dashboardData"], (oldData: any) => { - if (!oldData) return undefined; - return oldData.map((e: any) => - data.find((d: any) => d.id === e.id) ? data : e - ); - }); - - // TODO: Fix this + // TODO: Fix these + // queryClient.setQueryData(["dashboardData"], (oldData: any) => { + // if (!oldData) return undefined; + // return oldData.map((e: any) => + // data.find((d: any) => d.id === e.id) ? data : e + // ); + // }); // queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => { // if (!oldData) return undefined; // return { @@ -417,6 +444,7 @@ const useBulkEditLinks = () => { // }; // }); queryClient.invalidateQueries({ queryKey: ["links"] }); // Temporary workaround + queryClient.invalidateQueries({ queryKey: ["dashboardData"] }); // Temporary workaround queryClient.invalidateQueries({ queryKey: ["collections"] }); queryClient.invalidateQueries({ queryKey: ["tags"] }); @@ -425,6 +453,22 @@ const useBulkEditLinks = () => { }); }; +const resetInfiniteQueryPagination = async ( + queryClient: any, + queryKey: any +) => { + queryClient.setQueriesData({ queryKey }, (oldData: any) => { + if (!oldData) return undefined; + + return { + pages: oldData.pages.slice(0, 1), + pageParams: oldData.pageParams.slice(0, 1), + }; + }); + + await queryClient.invalidateQueries(queryKey); +}; + export { useLinks, useAddLink, @@ -434,4 +478,5 @@ export { useUploadFile, useGetLink, useBulkEditLinks, + resetInfiniteQueryPagination, }; diff --git a/lib/client/getPublicCollectionData.ts b/lib/client/getPublicCollectionData.ts index a8a74a3..c87abd1 100644 --- a/lib/client/getPublicCollectionData.ts +++ b/lib/client/getPublicCollectionData.ts @@ -1,12 +1,4 @@ -import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; -import { Dispatch, SetStateAction } from "react"; - -const getPublicCollectionData = async ( - collectionId: number, - setData: Dispatch< - SetStateAction - > -) => { +const getPublicCollectionData = async (collectionId: number) => { const res = await fetch("/api/v1/public/collections/" + collectionId); if (res.status === 400) @@ -14,8 +6,6 @@ const getPublicCollectionData = async ( const data = await res.json(); - setData(data.response); - return data; }; diff --git a/lib/client/icons.ts b/lib/client/icons.ts new file mode 100644 index 0000000..2083bd0 --- /dev/null +++ b/lib/client/icons.ts @@ -0,0 +1,18 @@ +import * as Icons from "@phosphor-icons/react"; +import { icons as iconData } from "@phosphor-icons/core"; +import { IconEntry as CoreEntry } from "@phosphor-icons/core"; + +interface IconEntry extends CoreEntry { + Icon: Icons.Icon; +} + +export const icons: ReadonlyArray = iconData.map((entry) => ({ + ...entry, + Icon: Icons[entry.pascal_name as keyof typeof Icons] as Icons.Icon, +})); + +if (process.env.NODE_ENV === "development") { + console.log(`${icons.length} icons`); +} + +export const iconCount = Intl.NumberFormat("en-US").format(icons.length * 6); diff --git a/package.json b/package.json index 6a592e9..0e72474 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "linkwarden", - "version": "v2.7.0", + "version": "v2.8.0", "main": "index.js", "repository": "https://github.com/linkwarden/linkwarden.git", "author": "Daniel31X13 ", @@ -25,6 +25,8 @@ "@aws-sdk/client-s3": "^3.379.1", "@headlessui/react": "^1.7.15", "@mozilla/readability": "^0.4.4", + "@phosphor-icons/core": "^2.1.1", + "@phosphor-icons/react": "^2.1.7", "@prisma/client": "^4.16.2", "@stripe/stripe-js": "^1.54.1", "@tanstack/react-query": "^5.51.15", @@ -50,6 +52,7 @@ "eslint-config-next": "13.4.9", "formidable": "^3.5.1", "framer-motion": "^10.16.4", + "fuse.js": "^7.0.0", "handlebars": "^4.7.8", "himalaya": "^1.1.0", "i18next": "^23.11.5", @@ -76,7 +79,7 @@ "socks-proxy-agent": "^8.0.2", "stripe": "^12.13.0", "tailwind-merge": "^2.3.0", - "vaul": "^0.8.8", + "vaul": "^0.9.1", "zustand": "^4.3.8" }, "devDependencies": { @@ -92,7 +95,7 @@ "postcss": "^8.4.26", "prettier": "3.1.1", "prisma": "^4.16.2", - "tailwindcss": "^3.3.3", + "tailwindcss": "^3.4.10", "ts-node": "^10.9.2", "typescript": "4.9.4" } diff --git a/pages/dashboard.tsx b/pages/dashboard.tsx index 1d9192c..e0b107f 100644 --- a/pages/dashboard.tsx +++ b/pages/dashboard.tsx @@ -114,14 +114,14 @@ export default function Dashboard() {
-
+
-
+
-
+
diff --git a/pages/links/[id].tsx b/pages/links/[id].tsx new file mode 100644 index 0000000..cfa8785 --- /dev/null +++ b/pages/links/[id].tsx @@ -0,0 +1,41 @@ +import LinkDetails from "@/components/LinkDetails"; +import { useGetLink } from "@/hooks/store/links"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import getServerSideProps from "@/lib/client/getServerSideProps"; + +const Index = () => { + const router = useRouter(); + const { id } = router.query; + + useState; + + const getLink = useGetLink(); + + useEffect(() => { + getLink.mutate({ id: Number(id) }); + }, []); + + return ( +
+ {getLink.data ? ( + + ) : ( +
+
+
+
+
+
+
+ )} +
+ ); +}; + +export default Index; + +export { getServerSideProps }; diff --git a/pages/preserved/[id].tsx b/pages/preserved/[id].tsx index c5806ff..de21605 100644 --- a/pages/preserved/[id].tsx +++ b/pages/preserved/[id].tsx @@ -20,7 +20,7 @@ export default function Index() { useEffect(() => { const fetchLink = async () => { if (router.query.id) { - await getLink.mutateAsync(Number(router.query.id)); + await getLink.mutateAsync({ id: Number(router.query.id) }); } }; diff --git a/pages/public/collections/[id].tsx b/pages/public/collections/[id].tsx index 98153cb..4c5f777 100644 --- a/pages/public/collections/[id].tsx +++ b/pages/public/collections/[id].tsx @@ -20,7 +20,6 @@ import EditCollectionSharingModal from "@/components/ModalContent/EditCollection import { useTranslation } from "next-i18next"; import getServerSideProps from "@/lib/client/getServerSideProps"; import LinkListOptions from "@/components/LinkListOptions"; -import { useCollections } from "@/hooks/store/collections"; import { usePublicLinks } from "@/hooks/store/publicLinks"; import Links from "@/components/LinkViews/Links"; @@ -29,8 +28,6 @@ export default function PublicCollections() { const { settings } = useLocalSettingsStore(); - const { data: collections = [] } = useCollections(); - const router = useRouter(); const [collectionOwner, setCollectionOwner] = useState< @@ -66,25 +63,22 @@ export default function PublicCollections() { useEffect(() => { if (router.query.id) { - getPublicCollectionData(Number(router.query.id), setCollection).then( - (res) => { - if (res.status === 400) { - router.push("/dashboard"); - } + getPublicCollectionData(Number(router.query.id)).then((res) => { + if (res.status === 400) { + router.push("/dashboard"); + } else { + setCollection(res.response); } - ); + }); } - }, [collections]); + }, []); useEffect(() => { - const fetchOwner = async () => { - if (collection) { - const owner = await getPublicUserData(collection.ownerId as number); - setCollectionOwner(owner); - } - }; - - fetchOwner(); + if (collection) { + getPublicUserData(collection.ownerId as number).then((owner) => + setCollectionOwner(owner) + ); + } }, [collection]); const [editCollectionSharingModal, setEditCollectionSharingModal] = @@ -233,9 +227,7 @@ export default function PublicCollections() { placeholderCount={1} useData={data} /> - {!data.isLoading && links && !links[0] && ( -

{t("collection_is_empty")}

- )} + {!data.isLoading && links && !links[0] &&

{t("nothing_found")}

} {/*

List created with Linkwarden. diff --git a/pages/public/links/[id].tsx b/pages/public/links/[id].tsx new file mode 100644 index 0000000..cfa8785 --- /dev/null +++ b/pages/public/links/[id].tsx @@ -0,0 +1,41 @@ +import LinkDetails from "@/components/LinkDetails"; +import { useGetLink } from "@/hooks/store/links"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import getServerSideProps from "@/lib/client/getServerSideProps"; + +const Index = () => { + const router = useRouter(); + const { id } = router.query; + + useState; + + const getLink = useGetLink(); + + useEffect(() => { + getLink.mutate({ id: Number(id) }); + }, []); + + return ( +

+ {getLink.data ? ( + + ) : ( +
+
+
+
+
+
+
+ )} +
+ ); +}; + +export default Index; + +export { getServerSideProps }; diff --git a/pages/public/preserved/[id].tsx b/pages/public/preserved/[id].tsx index 2f415ae..31544b0 100644 --- a/pages/public/preserved/[id].tsx +++ b/pages/public/preserved/[id].tsx @@ -6,10 +6,9 @@ import { } from "@/types/global"; import ReadableView from "@/components/ReadableView"; import getServerSideProps from "@/lib/client/getServerSideProps"; -import { useGetLink, useLinks } from "@/hooks/store/links"; +import { useGetLink } from "@/hooks/store/links"; export default function Index() { - const { links } = useLinks(); const getLink = useGetLink(); const [link, setLink] = useState(); @@ -19,18 +18,14 @@ export default function Index() { useEffect(() => { const fetchLink = async () => { if (router.query.id) { - await getLink.mutateAsync(Number(router.query.id)); + const get = await getLink.mutateAsync({ id: Number(router.query.id) }); + setLink(get); } }; fetchLink(); }, []); - useEffect(() => { - if (links && links[0]) - setLink(links.find((e) => e.id === Number(router.query.id))); - }, [links]); - return (
{/*
@@ -39,6 +34,12 @@ export default function Index() { {link && Number(router.query.format) === ArchivedFormat.readability && ( )} + {link && Number(router.query.format) === ArchivedFormat.monolith && ( + + )} {link && Number(router.query.format) === ArchivedFormat.pdf && (