diff --git a/components/LinkCard.tsx b/components/LinkCard.tsx index fc23253..1a9c80e 100644 --- a/components/LinkCard.tsx +++ b/components/LinkCard.tsx @@ -2,31 +2,17 @@ import { CollectionIncludingMembersAndLinkCount, LinkIncludingShortenedCollectionAndTags, } from "@/types/global"; -import { - faFolder, - faEllipsis, - faLink, -} from "@fortawesome/free-solid-svg-icons"; +import { faLink } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useEffect, useState } from "react"; -import Image from "next/image"; import useLinkStore from "@/store/links"; import useCollectionStore from "@/store/collections"; -import useAccountStore from "@/store/account"; -import { - faCalendarDays, - faFileImage, - faFilePdf, -} from "@fortawesome/free-regular-svg-icons"; -import usePermissions from "@/hooks/usePermissions"; -import { toast } from "react-hot-toast"; import isValidUrl from "@/lib/shared/isValidUrl"; -import Link from "next/link"; import unescapeString from "@/lib/client/unescapeString"; -import { useRouter } from "next/router"; -import EditLinkModal from "./ModalContent/EditLinkModal"; -import DeleteLinkModal from "./ModalContent/DeleteLinkModal"; -import PreservedFormatsModal from "./ModalContent/PreservedFormatsModal"; +import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions"; +import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate"; +import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection"; +import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon"; type Props = { link: LinkIncludingShortenedCollectionAndTags; @@ -35,15 +21,24 @@ type Props = { }; export default function LinkCard({ link, count, className }: Props) { - const router = useRouter(); - - const permissions = usePermissions(link.collection.id as number); + const { links } = useLinkStore(); const { collections } = useCollectionStore(); - const { links } = useLinkStore(); + const [collection, setCollection] = + useState( + collections.find( + (e) => e.id === link.collection.id, + ) as CollectionIncludingMembersAndLinkCount, + ); - const { account } = useAccountStore(); + useEffect(() => { + setCollection( + collections.find( + (e) => e.id === link.collection.id, + ) as CollectionIncludingMembersAndLinkCount, + ); + }, [collections, links]); let shortendURL; @@ -53,179 +48,22 @@ export default function LinkCard({ link, count, className }: Props) { console.log(error); } - const [collection, setCollection] = - useState( - collections.find( - (e) => e.id === link.collection.id - ) as CollectionIncludingMembersAndLinkCount - ); - - useEffect(() => { - setCollection( - collections.find( - (e) => e.id === link.collection.id - ) as CollectionIncludingMembersAndLinkCount - ); - }, [collections, links]); - - const { removeLink, updateLink } = useLinkStore(); - - const pinLink = async () => { - const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0]; - - const load = toast.loading("Applying..."); - - const response = await updateLink({ - ...link, - pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }], - }); - - toast.dismiss(load); - - response.ok && - toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`); - }; - - const deleteLink = async () => { - const load = toast.loading("Deleting..."); - - const response = await removeLink(link.id as number); - - toast.dismiss(load); - - response.ok && toast.success(`Link Deleted.`); - }; - const url = isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined; - const formattedDate = new Date(link.createdAt as string).toLocaleString( - "en-US", - { - year: "numeric", - month: "short", - day: "numeric", - } - ); - - const [editLinkModal, setEditLinkModal] = useState(false); - const [deleteLinkModal, setDeleteLinkModal] = useState(false); - const [preservedFormatsModal, setPreservedFormatsModal] = useState(false); - return (
- {permissions === true || - permissions?.canUpdate || - permissions?.canDelete ? ( -
-
- -
-
    - {permissions === true ? ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - pinLink(); - }} - > - {link?.pinnedBy && link.pinnedBy[0] - ? "Unpin" - : "Pin to Dashboard"} -
    -
  • - ) : undefined} - {permissions === true || permissions?.canUpdate ? ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - setEditLinkModal(true); - }} - > - Edit -
    -
  • - ) : undefined} - {permissions === true ? ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - setPreservedFormatsModal(true); - // updateArchive(); - }} - > - Preserved Formats -
    -
  • - ) : undefined} - {permissions === true || permissions?.canDelete ? ( -
  • -
    { - (document?.activeElement as HTMLElement)?.blur(); - e.shiftKey ? deleteLink() : setDeleteLinkModal(true); - }} - > - Delete -
    -
  • - ) : undefined} -
-
- ) : undefined} -
link.url && window.open(link.url || "", "_blank")} className="flex flex-col justify-between cursor-pointer h-full w-full gap-1 p-3" > - {link.url && url ? ( - { - const target = e.target as HTMLElement; - target.style.display = "none"; - }} - /> - ) : link.type === "pdf" ? ( - - ) : link.type === "image" ? ( - - ) : undefined} +
+ +

{count + 1}

@@ -243,65 +81,33 @@ export default function LinkCard({ link, count, className }: Props) {
{link.type}
)} -
{ - e.stopPropagation(); - router.push(`/collections/${link.collection.id}`); - }} - className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100" - > - -

{collection?.name}

-
+ -
- -

{formattedDate}

-
+ {/* {link.tags[0] ? ( -
-
- {link.tags.map((e, i) => ( - { - e.stopPropagation(); - }} - className="btn btn-xs btn-ghost truncate max-w-[19rem]" - > - #{e.name} - - ))} -
-
-
- ) : ( -

No Tags

- )} */} +
+
+{link.tags.map((e, i) => ( + { + e.stopPropagation(); + }} + className="btn btn-xs btn-ghost truncate max-w-[19rem]" +> + #{e.name} + +))} +
+
+
+) : ( +

No Tags

+)} */}
- {editLinkModal ? ( - setEditLinkModal(false)} - activeLink={link} - /> - ) : undefined} - {deleteLinkModal ? ( - setDeleteLinkModal(false)} - activeLink={link} - /> - ) : undefined} - {preservedFormatsModal ? ( - setPreservedFormatsModal(false)} - activeLink={link} - /> - ) : undefined} + +
); } diff --git a/components/LinkViews/CompactGridView.tsx b/components/LinkViews/CompactGridView.tsx new file mode 100644 index 0000000..f71ac5c --- /dev/null +++ b/components/LinkViews/CompactGridView.tsx @@ -0,0 +1,16 @@ +import LinkCardCompact from "@/components/LinkViews/LinkComponents/LinkCardCompact"; +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; + +export default function CompactGridView({ + links, +}: { + links: LinkIncludingShortenedCollectionAndTags[]; +}) { + return ( +
+ {links.map((e, i) => { + return ; + })} +
+ ); +} diff --git a/components/LinkViews/DefaultGridView.tsx b/components/LinkViews/DefaultGridView.tsx new file mode 100644 index 0000000..7dbbc79 --- /dev/null +++ b/components/LinkViews/DefaultGridView.tsx @@ -0,0 +1,16 @@ +import LinkCard from "@/components/LinkCard"; +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; + +export default function DefaultGridView({ + links, +}: { + links: LinkIncludingShortenedCollectionAndTags[]; +}) { + return ( +
+ {links.map((e, i) => { + return ; + })} +
+ ); +} diff --git a/components/LinkViews/LinkComponents/LinkActions.tsx b/components/LinkViews/LinkComponents/LinkActions.tsx new file mode 100644 index 0000000..ef4e6c7 --- /dev/null +++ b/components/LinkViews/LinkComponents/LinkActions.tsx @@ -0,0 +1,165 @@ +import { useState } from "react"; +import { + CollectionIncludingMembersAndLinkCount, + LinkIncludingShortenedCollectionAndTags, +} from "@/types/global"; +import usePermissions from "@/hooks/usePermissions"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faEllipsis } from "@fortawesome/free-solid-svg-icons"; +import EditLinkModal from "@/components/ModalContent/EditLinkModal"; +import DeleteLinkModal from "@/components/ModalContent/DeleteLinkModal"; +import PreservedFormatsModal from "@/components/ModalContent/PreservedFormatsModal"; +import useLinkStore from "@/store/links"; +import { toast } from "react-hot-toast"; +import useAccountStore from "@/store/account"; + +export default function LinkActions({ + link, + collection, +}: { + link: LinkIncludingShortenedCollectionAndTags; + collection: CollectionIncludingMembersAndLinkCount; +}) { + const permissions = usePermissions(link.collection.id as number); + + const [editLinkModal, setEditLinkModal] = useState(false); + const [deleteLinkModal, setDeleteLinkModal] = useState(false); + const [preservedFormatsModal, setPreservedFormatsModal] = useState(false); + const [expandedLink, setExpandedLink] = useState(false); + + const { account } = useAccountStore(); + + const { removeLink, updateLink } = useLinkStore(); + + const pinLink = async () => { + const isAlreadyPinned = link?.pinnedBy && link.pinnedBy[0]; + + const load = toast.loading("Applying..."); + + const response = await updateLink({ + ...link, + pinnedBy: isAlreadyPinned ? undefined : [{ id: account.id }], + }); + + toast.dismiss(load); + + response.ok && + toast.success(`Link ${isAlreadyPinned ? "Unpinned!" : "Pinned!"}`); + }; + + const deleteLink = async () => { + const load = toast.loading("Deleting..."); + + const response = await removeLink(link.id as number); + + toast.dismiss(load); + + response.ok && toast.success(`Link Deleted.`); + }; + + return ( +
+ {permissions === true || + permissions?.canUpdate || + permissions?.canDelete ? ( +
+
+ +
+
    + {permissions === true ? ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + pinLink(); + }} + > + {link?.pinnedBy && link.pinnedBy[0] + ? "Unpin" + : "Pin to Dashboard"} +
    +
  • + ) : undefined} + {permissions === true || permissions?.canUpdate ? ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + setEditLinkModal(true); + }} + > + Edit +
    +
  • + ) : undefined} + {permissions === true ? ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + setPreservedFormatsModal(true); + // updateArchive(); + }} + > + Preserved Formats +
    +
  • + ) : undefined} + {permissions === true || permissions?.canDelete ? ( +
  • +
    { + (document?.activeElement as HTMLElement)?.blur(); + e.shiftKey ? deleteLink() : setDeleteLinkModal(true); + }} + > + Delete +
    +
  • + ) : undefined} +
+
+ ) : undefined} + + {editLinkModal ? ( + setEditLinkModal(false)} + activeLink={link} + /> + ) : undefined} + {deleteLinkModal ? ( + setDeleteLinkModal(false)} + activeLink={link} + /> + ) : undefined} + {preservedFormatsModal ? ( + setPreservedFormatsModal(false)} + activeLink={link} + /> + ) : undefined} + {/* {expandedLink ? ( + setExpandedLink(false)} link={link} /> + ) : undefined} */} +
+ ); +} diff --git a/components/LinkViews/LinkComponents/LinkCardCompact.tsx b/components/LinkViews/LinkComponents/LinkCardCompact.tsx new file mode 100644 index 0000000..2e6f60e --- /dev/null +++ b/components/LinkViews/LinkComponents/LinkCardCompact.tsx @@ -0,0 +1,92 @@ +import { + CollectionIncludingMembersAndLinkCount, + LinkIncludingShortenedCollectionAndTags, +} from "@/types/global"; +import { useEffect, useState } from "react"; +import useLinkStore from "@/store/links"; +import useCollectionStore from "@/store/collections"; +import unescapeString from "@/lib/client/unescapeString"; +import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions"; +import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate"; +import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection"; +import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon"; + +type Props = { + link: LinkIncludingShortenedCollectionAndTags; + count: number; + className?: string; +}; + +export default function LinkCardCompact({ link, count, className }: Props) { + const { collections } = useCollectionStore(); + + const { links } = useLinkStore(); + + let shortendURL; + + try { + shortendURL = new URL(link.url || "").host.toLowerCase(); + } catch (error) { + console.log(error); + } + + const [collection, setCollection] = + useState( + collections.find( + (e) => e.id === link.collection.id, + ) as CollectionIncludingMembersAndLinkCount, + ); + + useEffect(() => { + setCollection( + collections.find( + (e) => e.id === link.collection.id, + ) as CollectionIncludingMembersAndLinkCount, + ); + }, [collections, links]); + + return ( +
+
link.url && window.open(link.url || "", "_blank")} + className="flex items-center cursor-pointer p-3" + > +
+ +
+ +
+

+ {unescapeString(link.name || link.description) || link.url} +

+ +
+
+ + · + {link.url ? ( +
{ + e.preventDefault(); + window.open(link.url || "", "_blank"); + }} + className="flex items-center hover:opacity-60 cursor-pointer duration-100" + > +

{shortendURL}

+
+ ) : ( +
+ {link.type} +
+ )} +
+ · + +
+
+
+ + +
+ ); +} diff --git a/components/LinkViews/LinkComponents/LinkCollection.tsx b/components/LinkViews/LinkComponents/LinkCollection.tsx new file mode 100644 index 0000000..606352a --- /dev/null +++ b/components/LinkViews/LinkComponents/LinkCollection.tsx @@ -0,0 +1,34 @@ +import { + CollectionIncludingMembersAndLinkCount, + LinkIncludingShortenedCollectionAndTags, +} from "@/types/global"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFolder } from "@fortawesome/free-solid-svg-icons"; +import { useRouter } from "next/router"; + +export default function LinkCollection({ + link, + collection, +}: { + link: LinkIncludingShortenedCollectionAndTags; + collection: CollectionIncludingMembersAndLinkCount; +}) { + const router = useRouter(); + + return ( +
{ + e.preventDefault(); + router.push(`/collections/${link.collection.id}`); + }} + className="flex items-center gap-1 max-w-full w-fit hover:opacity-70 duration-100" + > + +

{collection?.name}

+
+ ); +} diff --git a/components/LinkViews/LinkComponents/LinkDate.tsx b/components/LinkViews/LinkComponents/LinkDate.tsx new file mode 100644 index 0000000..282ab70 --- /dev/null +++ b/components/LinkViews/LinkComponents/LinkDate.tsx @@ -0,0 +1,25 @@ +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCalendarDays } from "@fortawesome/free-regular-svg-icons"; + +export default function LinkDate({ + link, +}: { + link: LinkIncludingShortenedCollectionAndTags; +}) { + const formattedDate = new Date(link.createdAt as string).toLocaleString( + "en-US", + { + year: "numeric", + month: "short", + day: "numeric", + }, + ); + + return ( +
+ +

{formattedDate}

+
+ ); +} diff --git a/components/LinkViews/LinkComponents/LinkIcon.tsx b/components/LinkViews/LinkComponents/LinkIcon.tsx new file mode 100644 index 0000000..e9d08cb --- /dev/null +++ b/components/LinkViews/LinkComponents/LinkIcon.tsx @@ -0,0 +1,40 @@ +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFileImage, faFilePdf } from "@fortawesome/free-regular-svg-icons"; +import Image from "next/image"; +import isValidUrl from "@/lib/shared/isValidUrl"; + +export default function LinkIcon({ + link, +}: { + link: LinkIncludingShortenedCollectionAndTags; +}) { + const url = + isValidUrl(link.url || "") && link.url ? new URL(link.url) : undefined; + + const iconClasses: string = + "w-12 bg-primary/20 text-primary shadow rounded-md p-2 select-none z-10"; + + return ( +
+ {link.url && url ? ( + { + const target = e.target as HTMLElement; + target.style.display = "none"; + }} + /> + ) : link.type === "pdf" ? ( + + ) : link.type === "image" ? ( + + ) : undefined} +
+ ); +} diff --git a/components/LinkViews/LinkComponents/LinkRow.tsx b/components/LinkViews/LinkComponents/LinkRow.tsx new file mode 100644 index 0000000..2c2f5c9 --- /dev/null +++ b/components/LinkViews/LinkComponents/LinkRow.tsx @@ -0,0 +1,92 @@ +import { + CollectionIncludingMembersAndLinkCount, + LinkIncludingShortenedCollectionAndTags, +} from "@/types/global"; +import { useEffect, useState } from "react"; +import useLinkStore from "@/store/links"; +import useCollectionStore from "@/store/collections"; +import unescapeString from "@/lib/client/unescapeString"; +import LinkActions from "@/components/LinkViews/LinkComponents/LinkActions"; +import LinkDate from "@/components/LinkViews/LinkComponents/LinkDate"; +import LinkCollection from "@/components/LinkViews/LinkComponents/LinkCollection"; +import LinkIcon from "@/components/LinkViews/LinkComponents/LinkIcon"; + +type Props = { + link: LinkIncludingShortenedCollectionAndTags; + count: number; + className?: string; +}; + +export default function LinkCardCompact({ link, count, className }: Props) { + const { collections } = useCollectionStore(); + + const { links } = useLinkStore(); + + let shortendURL; + + try { + shortendURL = new URL(link.url || "").host.toLowerCase(); + } catch (error) { + console.log(error); + } + + const [collection, setCollection] = + useState( + collections.find( + (e) => e.id === link.collection.id, + ) as CollectionIncludingMembersAndLinkCount, + ); + + useEffect(() => { + setCollection( + collections.find( + (e) => e.id === link.collection.id, + ) as CollectionIncludingMembersAndLinkCount, + ); + }, [collections, links]); + + return ( +
+
link.url && window.open(link.url || "", "_blank")} + className="flex items-center cursor-pointer p-3" + > +
+ +
+ +
+

+ {unescapeString(link.name || link.description) || link.url} +

+ +
+
+ + · + {link.url ? ( +
{ + e.preventDefault(); + window.open(link.url || "", "_blank"); + }} + className="flex items-center hover:opacity-60 cursor-pointer duration-100" + > +

{shortendURL}

+
+ ) : ( +
+ {link.type} +
+ )} +
+ · + +
+
+
+ + +
+ ); +} diff --git a/components/LinkViews/ListView.tsx b/components/LinkViews/ListView.tsx new file mode 100644 index 0000000..bb06327 --- /dev/null +++ b/components/LinkViews/ListView.tsx @@ -0,0 +1,18 @@ +import LinkRow from "@/components/LinkViews/LinkComponents/LinkRow"; +import { LinkIncludingShortenedCollectionAndTags } from "@/types/global"; + +export default function ListView({ + links, +}: { + links: LinkIncludingShortenedCollectionAndTags[]; +}) { + return ( +
+
+ {links.map((e, i) => { + return ; + })} +
+
+ ); +} diff --git a/components/SortDropdown.tsx b/components/SortDropdown.tsx index 7aebd56..e125e6b 100644 --- a/components/SortDropdown.tsx +++ b/components/SortDropdown.tsx @@ -16,11 +16,14 @@ export default function SortDropdown({ sortBy, setSort }: Props) { role="button" className="btn btn-sm btn-square btn-ghost" > - + viewBox="0 0 24 24" + fill="none" + stroke="currentColor" + > + +