mirror of
https://github.com/makeplane/plane.git
synced 2025-12-22 22:59:33 +01:00
[WEB-3892] chore: link item improvements (#6944)
* chore: code refactor * chore: global link block component added * chore: link item improvement and code refactor
This commit is contained in:
committed by
GitHub
parent
18fb3b8450
commit
be5d77d978
@@ -32,3 +32,4 @@ export * from "./tag";
|
||||
export * from "./tabs";
|
||||
export * from "./calendar";
|
||||
export * from "./color-picker";
|
||||
export * from "./link";
|
||||
|
||||
68
packages/ui/src/link/block.tsx
Normal file
68
packages/ui/src/link/block.tsx
Normal file
@@ -0,0 +1,68 @@
|
||||
import React, { FC } from "react";
|
||||
// plane utils
|
||||
import { calculateTimeAgo, cn, getIconForLink } from "@plane/utils";
|
||||
// plane ui
|
||||
import { CustomMenu, TContextMenuItem } from "@plane/ui";
|
||||
|
||||
export type TLinkItemBlockProps = {
|
||||
title: string;
|
||||
url: string;
|
||||
createdAt?: Date | string;
|
||||
menuItems?: TContextMenuItem[];
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export const LinkItemBlock: FC<TLinkItemBlockProps> = (props) => {
|
||||
// props
|
||||
const { title, url, createdAt, menuItems, onClick } = props;
|
||||
// icons
|
||||
const Icon = getIconForLink(url);
|
||||
return (
|
||||
<div
|
||||
onClick={onClick}
|
||||
className="cursor-pointer group flex items-center bg-custom-background-100 px-4 w-[230px] h-[56px] border-[0.5px] border-custom-border-200 rounded-md gap-4"
|
||||
>
|
||||
<div className="flex-shrink-0 size-8 rounded p-2 bg-custom-background-90 grid place-items-center">
|
||||
<Icon className="size-4 stroke-2 text-custom-text-350 group-hover:text-custom-text-100" />
|
||||
</div>
|
||||
<div className="flex-1 truncate">
|
||||
<div className="text-sm font-medium truncate">{title}</div>
|
||||
{createdAt && <div className="text-xs font-medium text-custom-text-400">{calculateTimeAgo(createdAt)}</div>}
|
||||
</div>
|
||||
{menuItems && (
|
||||
<div className="hidden group-hover:block">
|
||||
<CustomMenu placement="bottom-end" menuItemsClassName="z-20" closeOnSelect verticalEllipsis>
|
||||
{menuItems.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
className={cn("flex items-center gap-2 w-full ", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
packages/ui/src/link/index.ts
Normal file
1
packages/ui/src/link/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./block";
|
||||
@@ -1,109 +1,64 @@
|
||||
import {
|
||||
Github,
|
||||
Linkedin,
|
||||
Twitter,
|
||||
Facebook,
|
||||
Instagram,
|
||||
Youtube,
|
||||
Dribbble,
|
||||
Figma,
|
||||
FileText,
|
||||
FileImage,
|
||||
FileVideo,
|
||||
FileAudio,
|
||||
FileArchive,
|
||||
FileSpreadsheet,
|
||||
FileCode,
|
||||
Mail,
|
||||
Chrome,
|
||||
Link2,
|
||||
} from "lucide-react";
|
||||
Github,
|
||||
Linkedin,
|
||||
Twitter,
|
||||
Facebook,
|
||||
Instagram,
|
||||
Youtube,
|
||||
Dribbble,
|
||||
Figma,
|
||||
FileText,
|
||||
FileImage,
|
||||
FileVideo,
|
||||
FileAudio,
|
||||
FileArchive,
|
||||
FileSpreadsheet,
|
||||
FileCode,
|
||||
Mail,
|
||||
Chrome,
|
||||
Link2,
|
||||
} from "lucide-react";
|
||||
|
||||
type IconMatcher = {
|
||||
pattern: RegExp;
|
||||
icon: typeof Github;
|
||||
};
|
||||
|
||||
// get-icon-for-link.ts
|
||||
const SOCIAL_MEDIA_MATCHERS: IconMatcher[] = [
|
||||
{ pattern: /github\.com/, icon: Github },
|
||||
{ pattern: /linkedin\.com/, icon: Linkedin },
|
||||
{ pattern: /(twitter\.com|x\.com)/, icon: Twitter },
|
||||
{ pattern: /facebook\.com/, icon: Facebook },
|
||||
{ pattern: /instagram\.com/, icon: Instagram },
|
||||
{ pattern: /youtube\.com/, icon: Youtube },
|
||||
{ pattern: /dribbble\.com/, icon: Dribbble },
|
||||
];
|
||||
|
||||
export const getIconForLink = (url: string) => {
|
||||
const lowerUrl = url.toLowerCase();
|
||||
const PRODUCTIVITY_MATCHERS: IconMatcher[] = [
|
||||
{ pattern: /figma\.com/, icon: Figma },
|
||||
{ pattern: /(google\.com|docs\.|doc\.)/, icon: FileText },
|
||||
];
|
||||
|
||||
// Social Media
|
||||
if (lowerUrl.indexOf("github.com") !== -1) return Github;
|
||||
if (lowerUrl.indexOf("linkedin.com") !== -1) return Linkedin;
|
||||
if (lowerUrl.indexOf("twitter.com") !== -1 || lowerUrl.indexOf("x.com") !== -1) return Twitter;
|
||||
if (lowerUrl.indexOf("facebook.com") !== -1) return Facebook;
|
||||
if (lowerUrl.indexOf("instagram.com") !== -1) return Instagram;
|
||||
if (lowerUrl.indexOf("youtube.com") !== -1 || lowerUrl.indexOf("youtu.be") !== -1) return Youtube;
|
||||
if (lowerUrl.indexOf("dribbble.com") !== -1) return Dribbble;
|
||||
const FILE_TYPE_MATCHERS: IconMatcher[] = [
|
||||
{ pattern: /\.(jpg|jpeg|png|gif|bmp|svg|webp)$/, icon: FileImage },
|
||||
{ pattern: /\.(mp4|mov|avi|wmv|flv|mkv)$/, icon: FileVideo },
|
||||
{ pattern: /\.(mp3|wav|ogg)$/, icon: FileAudio },
|
||||
{ pattern: /\.(zip|rar|7z|tar|gz)$/, icon: FileArchive },
|
||||
{ pattern: /\.(xls|xlsx|csv)$/, icon: FileSpreadsheet },
|
||||
{ pattern: /\.(pdf|doc|docx|txt)$/, icon: FileText },
|
||||
{ pattern: /\.(html|js|ts|jsx|tsx|css|scss)$/, icon: FileCode },
|
||||
];
|
||||
|
||||
// Productivity / Tools
|
||||
if (lowerUrl.indexOf("figma.com") !== -1) return Figma;
|
||||
const OTHER_MATCHERS: IconMatcher[] = [
|
||||
{ pattern: /^mailto:/, icon: Mail },
|
||||
{ pattern: /^http/, icon: Chrome },
|
||||
];
|
||||
|
||||
if (
|
||||
lowerUrl.indexOf("google.com") !== -1 ||
|
||||
lowerUrl.indexOf("docs.") !== -1 ||
|
||||
lowerUrl.indexOf("doc.") !== -1
|
||||
) return FileText;
|
||||
export const getIconForLink = (url: string) => {
|
||||
const lowerUrl = url.toLowerCase();
|
||||
|
||||
// File types
|
||||
if (
|
||||
lowerUrl.indexOf(".jpg") !== -1 ||
|
||||
lowerUrl.indexOf(".jpeg") !== -1 ||
|
||||
lowerUrl.indexOf(".png") !== -1 ||
|
||||
lowerUrl.indexOf(".gif") !== -1 ||
|
||||
lowerUrl.indexOf(".bmp") !== -1 ||
|
||||
lowerUrl.indexOf(".svg") !== -1 ||
|
||||
lowerUrl.indexOf(".webp") !== -1
|
||||
) return FileImage;
|
||||
|
||||
if (
|
||||
lowerUrl.indexOf(".mp4") !== -1 ||
|
||||
lowerUrl.indexOf(".mov") !== -1 ||
|
||||
lowerUrl.indexOf(".avi") !== -1 ||
|
||||
lowerUrl.indexOf(".wmv") !== -1 ||
|
||||
lowerUrl.indexOf(".flv") !== -1 ||
|
||||
lowerUrl.indexOf(".mkv") !== -1
|
||||
) return FileVideo;
|
||||
|
||||
if (
|
||||
lowerUrl.indexOf(".mp3") !== -1 ||
|
||||
lowerUrl.indexOf(".wav") !== -1 ||
|
||||
lowerUrl.indexOf(".ogg") !== -1
|
||||
) return FileAudio;
|
||||
|
||||
if (
|
||||
lowerUrl.indexOf(".zip") !== -1 ||
|
||||
lowerUrl.indexOf(".rar") !== -1 ||
|
||||
lowerUrl.indexOf(".7z") !== -1 ||
|
||||
lowerUrl.indexOf(".tar") !== -1 ||
|
||||
lowerUrl.indexOf(".gz") !== -1
|
||||
) return FileArchive;
|
||||
|
||||
if (
|
||||
lowerUrl.indexOf(".xls") !== -1 ||
|
||||
lowerUrl.indexOf(".xlsx") !== -1 ||
|
||||
lowerUrl.indexOf(".csv") !== -1
|
||||
) return FileSpreadsheet;
|
||||
|
||||
if (
|
||||
lowerUrl.indexOf(".pdf") !== -1 ||
|
||||
lowerUrl.indexOf(".doc") !== -1 ||
|
||||
lowerUrl.indexOf(".docx") !== -1 ||
|
||||
lowerUrl.indexOf(".txt") !== -1
|
||||
) return FileText;
|
||||
|
||||
if (
|
||||
lowerUrl.indexOf(".html") !== -1 ||
|
||||
lowerUrl.indexOf(".js") !== -1 ||
|
||||
lowerUrl.indexOf(".ts") !== -1 ||
|
||||
lowerUrl.indexOf(".jsx") !== -1 ||
|
||||
lowerUrl.indexOf(".tsx") !== -1 ||
|
||||
lowerUrl.indexOf(".css") !== -1 ||
|
||||
lowerUrl.indexOf(".scss") !== -1
|
||||
) return FileCode;
|
||||
|
||||
// Other
|
||||
if (lowerUrl.indexOf("mailto:") !== -1) return Mail;
|
||||
if (lowerUrl.indexOf("http") === 0) return Chrome;
|
||||
|
||||
return Link2;
|
||||
};
|
||||
const allMatchers = [...SOCIAL_MEDIA_MATCHERS, ...PRODUCTIVITY_MATCHERS, ...FILE_TYPE_MATCHERS, ...OTHER_MATCHERS];
|
||||
|
||||
const matchedIcon = allMatchers.find(({ pattern }) => pattern.test(lowerUrl));
|
||||
return matchedIcon?.icon ?? Link2;
|
||||
};
|
||||
|
||||
@@ -1,21 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { FC, useCallback, useMemo } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import {
|
||||
Pencil,
|
||||
ExternalLink,
|
||||
Link,
|
||||
Trash2
|
||||
} from "lucide-react";
|
||||
import { Pencil, ExternalLink, Link, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TOAST_TYPE, setToast, CustomMenu, TContextMenuItem } from "@plane/ui";
|
||||
import { TOAST_TYPE, setToast, TContextMenuItem, LinkItemBlock } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn, copyTextToClipboard,getIconForLink } from "@plane/utils";
|
||||
|
||||
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "@plane/utils";
|
||||
// hooks
|
||||
import { useHome } from "@/hooks/store/use-home";
|
||||
// types
|
||||
@@ -34,101 +25,76 @@ export const ProjectLinkDetail: FC<TProjectLinkDetail> = observer((props) => {
|
||||
quickLinks: { getLinkById, toggleLinkModal, setLinkData },
|
||||
} = useHome();
|
||||
const { t } = useTranslation();
|
||||
|
||||
// derived values
|
||||
const linkDetail = getLinkById(linkId);
|
||||
if (!linkDetail) return <></>;
|
||||
|
||||
const viewLink = linkDetail.url;
|
||||
if (!linkDetail) return null;
|
||||
|
||||
const Icon = getIconForLink(linkDetail.url);
|
||||
// handlers
|
||||
const handleEdit = useCallback(
|
||||
(modalToggle: boolean) => {
|
||||
toggleLinkModal(modalToggle);
|
||||
setLinkData(linkDetail);
|
||||
},
|
||||
[linkDetail, setLinkData, toggleLinkModal]
|
||||
);
|
||||
|
||||
const handleEdit = (modalToggle: boolean) => {
|
||||
toggleLinkModal(modalToggle);
|
||||
setLinkData(linkDetail);
|
||||
};
|
||||
|
||||
const handleCopyText = () =>
|
||||
copyTextToClipboard(viewLink).then(() => {
|
||||
const handleCopyText = useCallback(() => {
|
||||
copyTextToClipboard(linkDetail.url).then(() => {
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: t("link_copied"),
|
||||
message: t("view_link_copied_to_clipboard"),
|
||||
});
|
||||
});
|
||||
const handleOpenInNewTab = () => window.open(`${viewLink}`, "_blank");
|
||||
}, [linkDetail.url, t]);
|
||||
|
||||
const handleOpenInNewTab = useCallback(() => {
|
||||
window.open(linkDetail.url, "_blank", "noopener,noreferrer");
|
||||
}, [linkDetail.url]);
|
||||
|
||||
const MENU_ITEMS: TContextMenuItem[] = [
|
||||
{
|
||||
key: "edit",
|
||||
action: () => handleEdit(true),
|
||||
title: t("edit"),
|
||||
icon: Pencil,
|
||||
},
|
||||
{
|
||||
key: "open-new-tab",
|
||||
action: handleOpenInNewTab,
|
||||
title: t("open_in_new_tab"),
|
||||
icon: ExternalLink,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
action: handleCopyText,
|
||||
title: t("copy_link"),
|
||||
icon: Link,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
action: () => linkOperations.remove(linkId),
|
||||
title: t("delete"),
|
||||
icon: Trash2,
|
||||
},
|
||||
];
|
||||
const handleDelete = useCallback(() => {
|
||||
linkOperations.remove(linkId);
|
||||
}, [linkId, linkOperations]);
|
||||
|
||||
// derived values
|
||||
const menuItems = useMemo<TContextMenuItem[]>(
|
||||
() => [
|
||||
{
|
||||
key: "edit",
|
||||
action: () => handleEdit(true),
|
||||
title: t("edit"),
|
||||
icon: Pencil,
|
||||
},
|
||||
{
|
||||
key: "open-new-tab",
|
||||
action: handleOpenInNewTab,
|
||||
title: t("open_in_new_tab"),
|
||||
icon: ExternalLink,
|
||||
},
|
||||
{
|
||||
key: "copy-link",
|
||||
action: handleCopyText,
|
||||
title: t("copy_link"),
|
||||
icon: Link,
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
action: handleDelete,
|
||||
title: t("delete"),
|
||||
icon: Trash2,
|
||||
},
|
||||
],
|
||||
[handleEdit, handleOpenInNewTab, handleCopyText, handleDelete, t]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
<LinkItemBlock
|
||||
title={linkDetail.title || linkDetail.url}
|
||||
url={linkDetail.url}
|
||||
createdAt={linkDetail.created_at}
|
||||
menuItems={menuItems}
|
||||
onClick={handleOpenInNewTab}
|
||||
className="cursor-pointer group flex items-center bg-custom-background-100 px-4 w-[230px] h-[56px] border-[0.5px] border-custom-border-200 rounded-md gap-4 hover:shadow-md transition-shadow"
|
||||
>
|
||||
<div className="flex-shrink-0 size-8 rounded p-2 bg-custom-background-80 grid place-items-center">
|
||||
<Icon className="size-4 stroke-2 text-custom-text-350" />
|
||||
</div>
|
||||
<div className="flex-1 truncate">
|
||||
<div className="text-sm font-medium truncate">{linkDetail.title || linkDetail.url}</div>
|
||||
<div className="text-xs font-medium text-custom-text-400">{calculateTimeAgo(linkDetail.created_at)}</div>
|
||||
</div>
|
||||
<div className="hidden group-hover:block">
|
||||
<CustomMenu placement="bottom-end" menuItemsClassName="z-20" closeOnSelect verticalEllipsis>
|
||||
{MENU_ITEMS.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
className={cn("flex items-center gap-2 w-full ", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("text-custom-text-300 whitespace-pre-line", {
|
||||
"text-custom-text-400": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
import { FC } from "react";
|
||||
// hooks
|
||||
// ui
|
||||
import { Pencil, Trash2, LinkIcon, ExternalLink } from "lucide-react";
|
||||
import { Pencil, Trash2, ExternalLink } from "lucide-react";
|
||||
import { Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { getIconForLink } from "@plane/utils";
|
||||
// icons
|
||||
// types
|
||||
// helpers
|
||||
@@ -34,6 +35,8 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
||||
const linkDetail = getLinkById(linkId);
|
||||
if (!linkDetail) return <></>;
|
||||
|
||||
const Icon = getIconForLink(linkDetail.url);
|
||||
|
||||
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
||||
toggleIssueLinkModalStore(modalToggle);
|
||||
setIssueLinkData(linkDetail);
|
||||
@@ -57,7 +60,7 @@ export const IssueLinkDetail: FC<TIssueLinkDetail> = (props) => {
|
||||
>
|
||||
<div className="flex items-start gap-2 truncate">
|
||||
<span className="py-1">
|
||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||
<Icon className="size-3 stroke-2 text-custom-text-350 group-hover:text-custom-text-100 flex-shrink-0" />
|
||||
</span>
|
||||
<Tooltip
|
||||
tooltipContent={linkDetail.title && linkDetail.title !== "" ? linkDetail.title : linkDetail.url}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Pencil, Trash2, LinkIcon, Copy } from "lucide-react";
|
||||
import { Pencil, Trash2, Copy } from "lucide-react";
|
||||
import { EIssueServiceType } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { TIssueServiceType } from "@plane/types";
|
||||
// ui
|
||||
import { Tooltip, TOAST_TYPE, setToast, CustomMenu } from "@plane/ui";
|
||||
import { calculateTimeAgo, getIconForLink } from "@plane/utils";
|
||||
// helpers
|
||||
import { calculateTimeAgoShort } from "@/helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
@@ -37,6 +37,8 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
||||
const linkDetail = getLinkById(linkId);
|
||||
if (!linkDetail) return <></>;
|
||||
|
||||
const Icon = getIconForLink(linkDetail.url);
|
||||
|
||||
const toggleIssueLinkModal = (modalToggle: boolean) => {
|
||||
toggleIssueLinkModalStore(modalToggle);
|
||||
setIssueLinkData(linkDetail);
|
||||
@@ -48,7 +50,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
||||
className="group col-span-12 lg:col-span-6 xl:col-span-4 2xl:col-span-3 3xl:col-span-2 flex items-center justify-between gap-3 h-10 flex-shrink-0 px-3 bg-custom-background-90 hover:bg-custom-background-80 border-[0.5px] border-custom-border-200 rounded"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 truncate flex-grow">
|
||||
<LinkIcon className="size-4 flex-shrink-0 text-custom-text-400 group-hover:text-custom-text-200" />
|
||||
<Icon className="size-4 flex-shrink-0 stroke-2 text-custom-text-350 group-hover:text-custom-text-100" />
|
||||
<Tooltip tooltipContent={linkDetail.url} isMobile={isMobile}>
|
||||
<a
|
||||
href={linkDetail.url}
|
||||
@@ -62,7 +64,7 @@ export const IssueLinkItem: FC<TIssueLinkItem> = observer((props) => {
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<p className="p-1 text-xs align-bottom leading-5 text-custom-text-400 group-hover-text-custom-text-200">
|
||||
{calculateTimeAgoShort(linkDetail.created_at)}
|
||||
{calculateTimeAgo(linkDetail.created_at)}
|
||||
</p>
|
||||
<span
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { Copy, LinkIcon, Pencil, Trash2 } from "lucide-react";
|
||||
import { Copy, Pencil, Trash2 } from "lucide-react";
|
||||
// plane types
|
||||
import { ILinkDetails } from "@plane/types";
|
||||
// plane ui
|
||||
import { setToast, TOAST_TYPE, Tooltip } from "@plane/ui";
|
||||
import { getIconForLink } from "@plane/utils";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "@/helpers/date-time.helper";
|
||||
import { copyTextToClipboard } from "@/helpers/string.helper";
|
||||
@@ -27,6 +28,8 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
||||
// platform os
|
||||
const { isMobile } = usePlatformOS();
|
||||
|
||||
const Icon = getIconForLink(link.url);
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
copyTextToClipboard(text).then(() =>
|
||||
setToast({
|
||||
@@ -42,7 +45,7 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
||||
<div className="flex w-full items-start justify-between gap-2">
|
||||
<div className="flex items-start gap-2 truncate">
|
||||
<span className="py-1">
|
||||
<LinkIcon className="h-3 w-3 flex-shrink-0" />
|
||||
<Icon className="size-3 stroke-2 text-custom-text-350 group-hover:text-custom-text-100 flex-shrink-0" />
|
||||
</span>
|
||||
<Tooltip tooltipContent={link.title && link.title !== "" ? link.title : link.url} isMobile={isMobile}>
|
||||
<a href={link.url} target="_blank" rel="noopener noreferrer" className="cursor-pointer truncate text-xs">
|
||||
@@ -87,9 +90,8 @@ export const ModulesLinksListItem: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-5">
|
||||
<p className="mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
||||
Added {calculateTimeAgo(link.created_at)}
|
||||
<br />
|
||||
<p className="flex items-center gap-1.5 mt-0.5 stroke-[1.5] text-xs text-custom-text-300">
|
||||
Added {calculateTimeAgo(link.created_at)}{" "}
|
||||
{createdByDetails && (
|
||||
<>by {createdByDetails?.is_bot ? createdByDetails?.first_name + " Bot" : createdByDetails?.display_name}</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user