From 346ddc16be81d2d54755b2cbd52eb65efec479fb Mon Sep 17 00:00:00 2001 From: Charity <81490612+charitea@users.noreply.github.com> Date: Fri, 6 Feb 2026 18:34:23 -0500 Subject: [PATCH] feat: add functionality to copy card link (#358) * Implemented link copying feature in CardDropdown and CardModal components. * Updated Dropdown component to accept new props for card identification. --- .../src/views/card/components/Dropdown.tsx | 37 +++++++++ apps/web/src/views/card/index.tsx | 21 +++-- apps/web/src/views/public/board/CardModal.tsx | 83 +++++++++++++------ apps/web/src/views/public/board/index.tsx | 51 +++++++----- 4 files changed, 138 insertions(+), 54 deletions(-) diff --git a/apps/web/src/views/card/components/Dropdown.tsx b/apps/web/src/views/card/components/Dropdown.tsx index c6308398..e1656489 100644 --- a/apps/web/src/views/card/components/Dropdown.tsx +++ b/apps/web/src/views/card/components/Dropdown.tsx @@ -1,6 +1,7 @@ import { t } from "@lingui/core/macro"; import { HiEllipsisHorizontal, + HiLink, HiOutlineCheckCircle, HiOutlineTrash, } from "react-icons/hi2"; @@ -10,18 +11,54 @@ import { authClient } from "@kan/auth/client"; import Dropdown from "~/components/Dropdown"; import { usePermissions } from "~/hooks/usePermissions"; import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; export default function CardDropdown({ + cardPublicId, + isTemplate, + boardPublicId, cardCreatedBy, }: { + cardPublicId: string; + isTemplate?: boolean; + boardPublicId?: string; cardCreatedBy?: string | null; }) { const { openModal } = useModal(); + const { showPopup } = usePopup(); const { canEditCard, canDeleteCard } = usePermissions(); const { data: session } = authClient.useSession(); const isCreator = cardCreatedBy && session?.user.id === cardCreatedBy; + const handleCopyCardLink = async () => { + const path = + isTemplate && boardPublicId + ? `/templates/${boardPublicId}/cards/${cardPublicId}` + : `/cards/${cardPublicId}`; + const url = `${window.location.origin}${path}`; + try { + await navigator.clipboard.writeText(url); + showPopup({ + header: t`Link copied`, + icon: "success", + message: t`Card URL copied to clipboard`, + }); + } catch (error) { + console.error(error); + showPopup({ + header: t`Unable to copy link`, + icon: "error", + message: t`Please try again.`, + }); + } + }; + const items = [ + { + label: t`Copy card link`, + action: handleCopyCardLink, + icon: , + }, ...(canEditCard ? [ { diff --git a/apps/web/src/views/card/index.tsx b/apps/web/src/views/card/index.tsx index 3425f808..da2bc27c 100644 --- a/apps/web/src/views/card/index.tsx +++ b/apps/web/src/views/card/index.tsx @@ -5,6 +5,8 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { IoChevronForwardSharp } from "react-icons/io5"; +import { authClient } from "@kan/auth/client"; + import Avatar from "~/components/Avatar"; import Editor from "~/components/Editor"; import FeedbackModal from "~/components/FeedbackModal"; @@ -14,8 +16,6 @@ import Modal from "~/components/modal"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { PageHead } from "~/components/PageHead"; import { EditYouTubeModal } from "~/components/YouTubeEmbed/EditYouTubeModal"; -import { authClient } from "@kan/auth/client"; - import { usePermissions } from "~/hooks/usePermissions"; import { useModal } from "~/providers/modal"; import { usePopup } from "~/providers/popup"; @@ -317,7 +317,12 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
- +
)} @@ -374,8 +379,14 @@ export default function CardPage({ isTemplate }: { isTemplate?: boolean }) {
setValue("description", e) : undefined} - onBlur={canEdit ? () => handleSubmit(onSubmit)() : undefined} + onChange={ + canEdit + ? (e) => setValue("description", e) + : undefined + } + onBlur={ + canEdit ? () => handleSubmit(onSubmit)() : undefined + } workspaceMembers={board?.workspace.members ?? []} readOnly={!canEdit} /> diff --git a/apps/web/src/views/public/board/CardModal.tsx b/apps/web/src/views/public/board/CardModal.tsx index 35e65a61..ed555e88 100644 --- a/apps/web/src/views/public/board/CardModal.tsx +++ b/apps/web/src/views/public/board/CardModal.tsx @@ -1,12 +1,13 @@ import { useRouter } from "next/router"; import { t } from "@lingui/core/macro"; import { useEffect, useRef, useState } from "react"; -import { HiXMark } from "react-icons/hi2"; +import { HiLink, HiXMark } from "react-icons/hi2"; import Badge from "~/components/Badge"; import Editor from "~/components/Editor"; import LabelIcon from "~/components/LabelIcon"; import { useModal } from "~/providers/modal"; +import { usePopup } from "~/providers/popup"; import { api } from "~/utils/api"; import ActivityList from "~/views/card/components/ActivityList"; import { AttachmentThumbnails } from "~/views/card/components/AttachmentThumbnails"; @@ -23,10 +24,29 @@ export function CardModal({ }) { const router = useRouter(); const { closeModal, isOpen } = useModal(); + const { showPopup } = usePopup(); const [showFade, setShowFade] = useState(false); const [showTopFade, setShowTopFade] = useState(false); const scrollRef = useRef(null); + const handleCopyCardLink = async () => { + try { + await navigator.clipboard.writeText(window.location.href); + showPopup({ + header: t`Link copied`, + icon: "success", + message: t`Card URL copied to clipboard`, + }); + } catch (error) { + console.error(error); + showPopup({ + header: t`Unable to copy link`, + icon: "error", + message: t`Please try again.`, + }); + } + }; + const { data, isLoading } = api.card.byId.useQuery( { cardPublicId: cardPublicId ?? "", @@ -65,33 +85,44 @@ export function CardModal({
- + + undefined, + { shallow: true }, + ); + }, 400); + }} + > + + +
{isLoading ? (
diff --git a/apps/web/src/views/public/board/index.tsx b/apps/web/src/views/public/board/index.tsx index ff7ba5ff..06c6c95f 100644 --- a/apps/web/src/views/public/board/index.tsx +++ b/apps/web/src/views/public/board/index.tsx @@ -30,7 +30,7 @@ export default function PublicBoardView() { const { showPopup } = usePopup(); const [isRouteLoaded, setIsRouteLoaded] = useState(false); const { openModal } = useModal(); - + const { ref: scrollRef, onMouseDown } = useDragToScroll({ enabled: true, direction: "horizontal", @@ -70,30 +70,35 @@ export default function PublicBoardView() { }, ); - const CopyBoardLink = () => { - return ( - - ); + const handleCopyBoardLink = async () => { + try { + await navigator.clipboard.writeText(window.location.href); + showPopup({ + header: t`Link copied`, + icon: "success", + message: t`Board URL copied to clipboard`, + }); + } catch (error) { + console.error(error); + showPopup({ + header: t`Unable to copy link`, + icon: "error", + message: t`Please try again.`, + }); + } }; + const CopyBoardLink = () => ( + + ); + const pathWithoutQuery = router.asPath.split("?")[0]; const splitPath = pathWithoutQuery?.split("/") ?? []; const cardPublicId = splitPath.length > 3 ? splitPath[3] : null;