diff --git a/components/CollectionCard.tsx b/components/CollectionCard.tsx index 432425c..4ff9677 100644 --- a/components/CollectionCard.tsx +++ b/components/CollectionCard.tsx @@ -1,5 +1,8 @@ import Link from "next/link"; -import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; +import { + AccountSettings, + CollectionIncludingMembersAndLinkCount, +} from "@/types/global"; import React, { useEffect, useState } from "react"; import ProfilePhoto from "./ProfilePhoto"; import usePermissions from "@/hooks/usePermissions"; @@ -12,12 +15,11 @@ import { dropdownTriggerer } from "@/lib/client/utils"; import { useTranslation } from "next-i18next"; import { useUser } from "@/hooks/store/user"; -type Props = { +export default function CollectionCard({ + collection, +}: { collection: CollectionIncludingMembersAndLinkCount; - className?: string; -}; - -export default function CollectionCard({ collection, className }: Props) { +}) { const { t } = useTranslation(); const { settings } = useLocalSettingsStore(); const { data: user = {} } = useUser(); @@ -33,15 +35,9 @@ export default function CollectionCard({ collection, className }: Props) { const permissions = usePermissions(collection.id as number); - 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, - }); + const [collectionOwner, setCollectionOwner] = useState< + Partial + >({}); useEffect(() => { const fetchOwner = async () => { @@ -132,12 +128,12 @@ export default function CollectionCard({ collection, className }: Props) { className="flex items-center absolute bottom-3 left-3 z-10 btn px-2 btn-ghost rounded-full" onClick={() => setEditCollectionSharingModal(true)} > - {collectionOwner.id ? ( + {collectionOwner.id && ( - ) : undefined} + )} {collection.members .sort((a, b) => (a.userId as number) - (b.userId as number)) .map((e, i) => { @@ -151,13 +147,13 @@ export default function CollectionCard({ collection, className }: Props) { ); }) .slice(0, 3)} - {collection.members.length - 3 > 0 ? ( + {collection.members.length - 3 > 0 && (
+{collection.members.length - 3}
- ) : null} + )}
- {collection.isPublic ? ( + {collection.isPublic && ( - ) : undefined} + )}
- {editCollectionModal ? ( + {editCollectionModal && ( setEditCollectionModal(false)} activeCollection={collection} /> - ) : undefined} - {editCollectionSharingModal ? ( + )} + {editCollectionSharingModal && ( setEditCollectionSharingModal(false)} activeCollection={collection} /> - ) : undefined} - {deleteCollectionModal ? ( + )} + {deleteCollectionModal && ( setDeleteCollectionModal(false)} activeCollection={collection} /> - ) : undefined} + )}
); } diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index e83d388..3ff2415 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -17,6 +17,8 @@ import toast from "react-hot-toast"; import { useTranslation } from "next-i18next"; import { useCollections, useUpdateCollection } from "@/hooks/store/collections"; import { useUpdateUser, useUser } from "@/hooks/store/user"; +import Icon from "./Icon"; +import { IconWeight } from "@phosphor-icons/react"; interface ExtendedTreeItem extends TreeItem { data: Collection; @@ -43,6 +45,7 @@ const CollectionListing = () => { return buildTreeFromCollections( collections, router, + tree, user.collectionOrder ); } else return undefined; @@ -256,7 +259,7 @@ const renderItem = ( : "hover:bg-neutral/20" } duration-100 flex gap-1 items-center pr-2 pl-1 rounded-md`} > - {Icon(item as ExtendedTreeItem, onExpand, onCollapse)} + {Dropdown(item as ExtendedTreeItem, onExpand, onCollapse)} - + {collection.icon ? ( + + ) : ( + + )} +

{collection.name}

- {collection.isPublic ? ( + {collection.isPublic && ( - ) : undefined} + )}
{collection._count?.links}
@@ -288,7 +302,7 @@ const renderItem = ( ); }; -const Icon = ( +const Dropdown = ( item: ExtendedTreeItem, onExpand: (id: ItemId) => void, onCollapse: (id: ItemId) => void @@ -311,6 +325,7 @@ const Icon = ( const buildTreeFromCollections = ( collections: CollectionIncludingMembersAndLinkCount[], router: ReturnType, + tree?: TreeData, order?: number[] ): TreeData => { if (order) { @@ -325,13 +340,15 @@ const buildTreeFromCollections = ( id: collection.id, children: [], hasChildren: false, - isExpanded: false, + isExpanded: tree?.items[collection.id as number]?.isExpanded || false, data: { id: collection.id, parentId: collection.parentId, name: collection.name, description: collection.description, color: collection.color, + icon: collection.icon, + iconWeight: collection.iconWeight, isPublic: collection.isPublic, ownerId: collection.ownerId, createdAt: collection.createdAt, diff --git a/components/CopyButton.tsx b/components/CopyButton.tsx new file mode 100644 index 0000000..b949475 --- /dev/null +++ b/components/CopyButton.tsx @@ -0,0 +1,32 @@ +import React, { useState } from "react"; + +type Props = { + text: string; +}; + +const CopyButton: React.FC = ({ text }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => { + setCopied(false); + }, 1000); + } catch (err) { + console.log(err); + } + }; + + return ( +
+ ); +}; + +export default CopyButton; diff --git a/components/DashboardItem.tsx b/components/DashboardItem.tsx index 337fc36..efc1303 100644 --- a/components/DashboardItem.tsx +++ b/components/DashboardItem.tsx @@ -14,7 +14,7 @@ export default function dashboardItem({

{name}

-

{value}

+

{value || 0}

); diff --git a/components/Drawer.tsx b/components/Drawer.tsx new file mode 100644 index 0000000..49cd9eb --- /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/Dropdown.tsx b/components/Dropdown.tsx index 4a48ab7..90371d8 100644 --- a/components/Dropdown.tsx +++ b/components/Dropdown.tsx @@ -60,47 +60,49 @@ export default function Dropdown({ } }, [points, dropdownHeight]); - return !points || pos ? ( - { - setDropdownHeight(e.height); - setDropdownWidth(e.width); - }} - style={ - points - ? { - position: "fixed", - top: `${pos?.y}px`, - left: `${pos?.x}px`, - } - : undefined - } - onClickOutside={onClickOutside} - className={`${ - className || "" - } py-1 shadow-md border border-neutral-content bg-base-200 rounded-md flex flex-col z-20`} - > - {items.map((e, i) => { - const inner = e && ( -
-
-

{e.name}

+ return ( + (!points || pos) && ( + { + setDropdownHeight(e.height); + setDropdownWidth(e.width); + }} + style={ + points + ? { + position: "fixed", + top: `${pos?.y}px`, + left: `${pos?.x}px`, + } + : undefined + } + onClickOutside={onClickOutside} + className={`${ + className || "" + } py-1 shadow-md border border-neutral-content bg-base-200 rounded-md flex flex-col z-20`} + > + {items.map((e, i) => { + const inner = e && ( +
+
+

{e.name}

+
-
- ); + ); - return e && e.href ? ( - - {inner} - - ) : ( - e && ( -
+ return e && e.href ? ( + {inner} -
- ) - ); - })} - - ) : null; + + ) : ( + e && ( +
+ {inner} +
+ ) + ); + })} + + ) + ); } diff --git a/components/Icon.tsx b/components/Icon.tsx new file mode 100644 index 0000000..1830f0b --- /dev/null +++ b/components/Icon.tsx @@ -0,0 +1,18 @@ +import React, { forwardRef } from "react"; +import * as Icons from "@phosphor-icons/react"; + +type Props = { + icon: string; +} & Icons.IconProps; + +const Icon = forwardRef(({ icon, ...rest }, ref) => { + const IconComponent: any = Icons[icon as keyof typeof Icons]; + + if (!IconComponent) { + return null; + } else return ; +}); + +Icon.displayName = "Icon"; + +export default Icon; diff --git a/components/IconGrid.tsx b/components/IconGrid.tsx new file mode 100644 index 0000000..b6333a7 --- /dev/null +++ b/components/IconGrid.tsx @@ -0,0 +1,49 @@ +import { icons } from "@/lib/client/icons"; +import Fuse from "fuse.js"; +import { useMemo } from "react"; + +const fuse = new Fuse(icons, { + keys: [{ name: "name", weight: 4 }, "tags", "categories"], + threshold: 0.2, + useExtendedSearch: true, +}); + +type Props = { + query: string; + color: string; + weight: "light" | "regular" | "bold" | "fill" | "duotone" | "thin"; + iconName?: string; + setIconName: Function; +}; + +const IconGrid = ({ query, color, weight, iconName, setIconName }: Props) => { + const filteredQueryResultsSelector = useMemo(() => { + if (!query) { + return icons; + } + return fuse.search(query).map((result) => result.item); + }, [query]); + + return ( + <> + {filteredQueryResultsSelector.map((icon) => { + const IconComponent = icon.Icon; + return ( +
setIconName(icon.pascal_name)} + className={`cursor-pointer btn p-1 box-border bg-base-100 border-none w-full ${ + icon.pascal_name === iconName + ? "outline outline-1 outline-primary" + : "" + }`} + > + +
+ ); + })} + + ); +}; + +export default IconGrid; diff --git a/components/IconPicker.tsx b/components/IconPicker.tsx new file mode 100644 index 0000000..82643a8 --- /dev/null +++ b/components/IconPicker.tsx @@ -0,0 +1,83 @@ +import React, { useState } from "react"; +import TextInput from "./TextInput"; +import Popover from "./Popover"; +import { HexColorPicker } from "react-colorful"; +import { useTranslation } from "next-i18next"; +import Icon from "./Icon"; +import { IconWeight } from "@phosphor-icons/react"; +import IconGrid from "./IconGrid"; +import IconPopover from "./IconPopover"; +import clsx from "clsx"; + +type Props = { + alignment?: string; + color: string; + setColor: Function; + iconName?: string; + setIconName: Function; + weight: "light" | "regular" | "bold" | "fill" | "duotone" | "thin"; + setWeight: Function; + hideDefaultIcon?: boolean; + reset: Function; + className?: string; +}; + +const IconPicker = ({ + alignment, + color, + setColor, + iconName, + setIconName, + weight, + setWeight, + hideDefaultIcon, + className, + reset, +}: Props) => { + const { t } = useTranslation(); + const [iconPicker, setIconPicker] = useState(false); + + return ( +
+
setIconPicker(!iconPicker)} + className="btn btn-square w-20 h-20" + > + {iconName ? ( + + ) : !iconName && hideDefaultIcon ? ( +

{t("set_custom_icon")}

+ ) : ( + + )} +
+ {iconPicker && ( + setIconPicker(false)} + className={clsx( + className, + alignment || "lg:-translate-x-1/3 top-20 left-0" + )} + /> + )} +
+ ); +}; + +export default IconPicker; diff --git a/components/IconPopover.tsx b/components/IconPopover.tsx new file mode 100644 index 0000000..cdbe8f6 --- /dev/null +++ b/components/IconPopover.tsx @@ -0,0 +1,142 @@ +import React, { useState } from "react"; +import TextInput from "./TextInput"; +import Popover from "./Popover"; +import { HexColorPicker } from "react-colorful"; +import { useTranslation } from "next-i18next"; +import IconGrid from "./IconGrid"; +import clsx from "clsx"; + +type Props = { + alignment?: string; + color: string; + setColor: Function; + iconName?: string; + setIconName: Function; + weight: "light" | "regular" | "bold" | "fill" | "duotone" | "thin"; + setWeight: Function; + reset: Function; + className?: string; + onClose: Function; +}; + +const IconPopover = ({ + alignment, + color, + setColor, + iconName, + setIconName, + weight, + setWeight, + reset, + className, + onClose, +}: Props) => { + const { t } = useTranslation(); + const [query, setQuery] = useState(""); + + return ( + onClose()} + className={clsx( + className, + "fade-in bg-base-200 border border-neutral-content p-2 w-[22.5rem] rounded-lg shadow-md" + )} + > +
+
+ setQuery(e.target.value)} + /> + +
+ +
+ +
+ setColor(e)} /> + +
+ + + + + + +
+
+
} + > + {t("reset_defaults")} +
+
+
+
+ ); +}; + +export default IconPopover; diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 81bef39..f53df37 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -16,6 +16,8 @@ type Props = { } | undefined; creatable?: boolean; + autoFocus?: boolean; + onBlur?: any; }; export default function CollectionSelection({ @@ -23,6 +25,8 @@ export default function CollectionSelection({ defaultValue, showDefaultValue = true, creatable = true, + autoFocus, + onBlur, }: Props) { const { data: collections = [] } = useCollections(); @@ -76,7 +80,7 @@ export default function CollectionSelection({ return (
{data.label} @@ -104,6 +108,8 @@ export default function CollectionSelection({ onChange={onChange} options={options} styles={styles} + autoFocus={autoFocus} + onBlur={onBlur} defaultValue={showDefaultValue ? defaultValue : null} components={{ Option: customOption, @@ -120,7 +126,9 @@ export default function CollectionSelection({ onChange={onChange} options={options} styles={styles} + autoFocus={autoFocus} defaultValue={showDefaultValue ? defaultValue : null} + onBlur={onBlur} components={{ Option: customOption, }} diff --git a/components/InputSelect/TagSelection.tsx b/components/InputSelect/TagSelection.tsx index efd246b..08460ea 100644 --- a/components/InputSelect/TagSelection.tsx +++ b/components/InputSelect/TagSelection.tsx @@ -10,9 +10,16 @@ type Props = { value: number; label: string; }[]; + autoFocus?: boolean; + onBlur?: any; }; -export default function TagSelection({ onChange, defaultValue }: Props) { +export default function TagSelection({ + onChange, + defaultValue, + autoFocus, + onBlur, +}: Props) { const { data: tags = [] } = useTags(); const [options, setOptions] = useState([]); @@ -34,8 +41,9 @@ export default function TagSelection({ onChange, defaultValue }: Props) { options={options} styles={styles} defaultValue={defaultValue} - // menuPosition="fixed" isMulti + autoFocus={autoFocus} + onBlur={onBlur} /> ); } diff --git a/components/InputSelect/styles.ts b/components/InputSelect/styles.ts index 96aad6d..f05f4a5 100644 --- a/components/InputSelect/styles.ts +++ b/components/InputSelect/styles.ts @@ -14,7 +14,7 @@ export const styles: StylesConfig = { ? "oklch(var(--p))" : "oklch(var(--nc))", }, - transition: "all 50ms", + transition: "all 100ms", }), control: (styles, state) => ({ ...styles, @@ -50,19 +50,28 @@ export const styles: StylesConfig = { multiValue: (styles) => { return { ...styles, - backgroundColor: "#0ea5e9", - color: "white", + backgroundColor: "oklch(var(--b2))", + color: "oklch(var(--bc))", + display: "flex", + alignItems: "center", + gap: "0.1rem", + marginRight: "0.4rem", }; }, multiValueLabel: (styles) => ({ ...styles, - color: "white", + color: "oklch(var(--bc))", }), multiValueRemove: (styles) => ({ ...styles, + height: "1.2rem", + width: "1.2rem", + borderRadius: "100px", + transition: "all 100ms", + color: "oklch(var(--w))", ":hover": { - color: "white", - backgroundColor: "#38bdf8", + color: "red", + backgroundColor: "oklch(var(--nc))", }, }), menuPortal: (base) => ({ ...base, zIndex: 9999 }), diff --git a/components/LinkDetails.tsx b/components/LinkDetails.tsx new file mode 100644 index 0000000..e745055 --- /dev/null +++ b/components/LinkDetails.tsx @@ -0,0 +1,663 @@ +import React, { useEffect, useState } from "react"; +import { + LinkIncludingShortenedCollectionAndTags, + ArchivedFormat, +} from "@/types/global"; +import Link from "next/link"; +import { + pdfAvailable, + readabilityAvailable, + monolithAvailable, + screenshotAvailable, + previewAvailable, +} 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, + useUpdateLink, + useUpdatePreview, +} from "@/hooks/store/links"; +import LinkIcon from "./LinkViews/LinkComponents/LinkIcon"; +import CopyButton from "./CopyButton"; +import { useRouter } from "next/router"; +import Icon from "./Icon"; +import { IconWeight } from "@phosphor-icons/react"; +import Image from "next/image"; +import clsx from "clsx"; +import toast from "react-hot-toast"; +import CollectionSelection from "./InputSelect/CollectionSelection"; +import TagSelection from "./InputSelect/TagSelection"; +import unescapeString from "@/lib/client/unescapeString"; +import IconPopover from "./IconPopover"; +import TextInput from "./TextInput"; +import usePermissions from "@/hooks/usePermissions"; + +type Props = { + className?: string; + activeLink: LinkIncludingShortenedCollectionAndTags; + standalone?: boolean; + mode?: "view" | "edit"; + setMode?: Function; +}; + +export default function LinkDetails({ + className, + activeLink, + standalone, + mode = "view", + setMode, +}: Props) { + const [link, setLink] = + useState(activeLink); + + useEffect(() => { + setLink(activeLink); + }, [activeLink]); + + const permissions = usePermissions(link.collection.id as number); + + 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" + ); + }; + + 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; + + const updateLink = useUpdateLink(); + const updatePreview = useUpdatePreview(); + + const submit = async (e?: any) => { + e?.preventDefault(); + + const { updatedAt: b, ...oldLink } = activeLink; + const { updatedAt: a, ...newLink } = link; + + if (JSON.stringify(oldLink) === JSON.stringify(newLink)) { + return; + } + + const load = toast.loading(t("updating")); + + await updateLink.mutateAsync(link, { + onSettled: (data, error) => { + toast.dismiss(load); + + if (error) { + toast.error(error.message); + } else { + toast.success(t("updated")); + setMode && setMode("view"); + setLink(data); + } + }, + }); + }; + + const setCollection = (e: any) => { + if (e?.__isNew__) e.value = null; + setLink({ + ...link, + collection: { id: e?.value, name: e?.label, ownerId: e?.ownerId }, + }); + }; + + const setTags = (e: any) => { + const tagNames = e.map((e: any) => ({ name: e.label })); + setLink({ ...link, tags: tagNames }); + }; + + const [iconPopover, setIconPopover] = useState(false); + + return ( +
+
+
+ {previewAvailable(link) ? ( + { + const target = e.target as HTMLElement; + target.style.display = "none"; + }} + /> + ) : link.preview === "unavailable" ? ( +
+ ) : ( +
+ )} + + {!standalone && (permissions === true || permissions?.canUpdate) && ( +
+ +
+ )} +
+ + {!standalone && (permissions === true || permissions?.canUpdate) ? ( +
+
+ setIconPopover(true)} + /> +
+ {iconPopover && ( + setLink({ ...link, color })} + weight={(link.iconWeight || "regular") as IconWeight} + setWeight={(iconWeight: string) => + setLink({ ...link, iconWeight }) + } + iconName={link.icon as string} + setIconName={(icon: string) => setLink({ ...link, icon })} + reset={() => + setLink({ + ...link, + color: "", + icon: "", + iconWeight: "", + }) + } + className="top-12" + onClose={() => { + setIconPopover(false); + submit(); + }} + /> + )} +
+ ) : ( +
+ setIconPopover(true)} /> +
+ )} + +
+ {mode === "view" && ( +
+

+ {link.name || t("untitled")} +

+
+ )} + + {mode === "edit" && ( + <> +
+ +
+

+ {t("name")} +

+ setLink({ ...link, name: e.target.value })} + placeholder={t("placeholder_example_link")} + className="bg-base-200" + /> +
+ + )} + + {link.url && mode === "view" ? ( + <> +
+ +

{t("link")}

+ +
+
+ + {link.url} + +
+ +
+
+
+ + ) : activeLink.url ? ( + <> +
+ +
+

+ {t("link")} +

+ setLink({ ...link, url: e.target.value })} + placeholder={t("placeholder_example_link")} + className="bg-base-200" + /> +
+ + ) : undefined} + +
+ +
+

+ {t("collection")} +

+ + {mode === "view" ? ( +
+ +

{link.collection.name}

+
+ {link.collection.icon ? ( + + ) : ( + + )} +
+ +
+ ) : ( + + )} +
+ +
+ +
+

+ {t("tags")} +

+ + {mode === "view" ? ( +
+ {link.tags && link.tags[0] ? ( + link.tags.map((tag) => + isPublicRoute ? ( +
+ {tag.name} +
+ ) : ( + + {tag.name} + + ) + ) + ) : ( +
{t("no_tags")}
+ )} +
+ ) : ( + ({ + label: e.name, + value: e.id, + }))} + /> + )} +
+ +
+ +
+

+ {t("description")} +

+ + {mode === "view" ? ( +
+ {link.description ? ( +

{link.description}

+ ) : ( +

{t("no_description_provided")}

+ )} +
+ ) : ( +