feat: add copy button to memo (#267)

* feat: copy-content

* Update web/src/less/memo-detail.less

Co-authored-by: boojack <stevenlgtm@gmail.com>
pull/275/head
f97 3 years ago committed by GitHub
parent ca2557eb7e
commit 2ea612e2fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,3 +1,4 @@
import copy from "copy-to-clipboard";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { memo, useEffect, useRef, useState } from "react"; import { memo, useEffect, useRef, useState } from "react";
@ -61,6 +62,11 @@ const Memo: React.FC<Props> = (props: Props) => {
navigate(`/m/${memo.id}`); navigate(`/m/${memo.id}`);
}; };
const handleCopyContent = () => {
copy(memo.content);
toastHelper.success(t("message.succeed-copy-content"));
};
const handleTogglePinMemoBtnClick = async () => { const handleTogglePinMemoBtnClick = async () => {
try { try {
if (memo.pinned) { if (memo.pinned) {
@ -205,6 +211,9 @@ const Memo: React.FC<Props> = (props: Props) => {
<span className="btn" onClick={handleMarkMemoClick}> <span className="btn" onClick={handleMarkMemoClick}>
{t("common.mark")} {t("common.mark")}
</span> </span>
<span className="btn" onClick={handleCopyContent}>
{t("memo.copy")}
</span>
<span className="btn" onClick={handleViewMemoDetailPage}> <span className="btn" onClick={handleViewMemoDetailPage}>
{t("memo.view-detail")} {t("memo.view-detail")}
</span> </span>

@ -1,3 +1,4 @@
import copy from "copy-to-clipboard";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { editorStateService, memoService, userService } from "../services"; import { editorStateService, memoService, userService } from "../services";
@ -130,6 +131,11 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
editorStateService.setEditMemoWithId(memo.id); editorStateService.setEditMemoWithId(memo.id);
}; };
const handleCopyContent = () => {
copy(memo.content);
toastHelper.success(t("message.succeed-copy-content"));
};
const handleVisibilitySelectorChange = async (visibility: Visibility) => { const handleVisibilitySelectorChange = async (visibility: Visibility) => {
if (memo.visibility === visibility) { if (memo.visibility === visibility) {
return; return;
@ -171,6 +177,9 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
<button className="btn edit-btn" onClick={handleGotoMemoLinkBtnClick}> <button className="btn edit-btn" onClick={handleGotoMemoLinkBtnClick}>
<Icon.ExternalLink className="icon-img" /> <Icon.ExternalLink className="icon-img" />
</button> </button>
<button className="btn copy-btn" onClick={handleCopyContent}>
<Icon.Clipboard className="icon-img" />
</button>
<button className="btn edit-btn" onClick={handleEditMemoBtnClick}> <button className="btn edit-btn" onClick={handleEditMemoBtnClick}>
<Icon.Edit3 className="icon-img" /> <Icon.Edit3 className="icon-img" />
</button> </button>

@ -41,31 +41,47 @@
@apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white rounded-lg border border-white hover:border-gray-200; @apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white rounded-lg border border-white hover:border-gray-200;
> .memo-header { > .memo-header {
@apply mb-2 w-full flex flex-row justify-start items-center text-sm font-mono text-gray-400; @apply mb-2 w-full flex flex-row justify-between items-center text-sm font-mono text-gray-400;
> .split-text { > .status-container {
@apply mx-2; @apply flex flex-row justify-start items-center;
}
> .name-text { > .split-text {
@apply hover:text-green-600 hover:underline; @apply mx-2;
}
> .name-text {
@apply hover:text-green-600 hover:underline;
}
> .visibility-selector {
> .status-text {
@apply flex flex-row justify-start items-center leading-5 text-xs cursor-pointer ml-2 rounded border px-1;
&.public {
@apply border-green-600 text-green-600;
}
&.protected {
@apply border-gray-400 text-gray-400;
}
}
.action-button {
@apply px-2 leading-7 w-full rounded text-gray-600 hover:bg-gray-100;
}
}
} }
> .visibility-selector { .btns-container {
> .status-text { .flex(row, flex-start, center);
@apply flex flex-row justify-start items-center leading-5 text-xs cursor-pointer ml-2 rounded border px-1;
&.public {
@apply border-green-600 text-green-600;
}
&.protected { > .btn {
@apply border-gray-400 text-gray-400; @apply rounded;
}
} }
.action-button { > .copy-btn {
@apply px-2 leading-7 w-full rounded text-gray-600 hover:bg-gray-100; @apply hover:bg-gray-100;
} }
} }
} }

@ -74,6 +74,7 @@
}, },
"memo": { "memo": {
"view-detail": "View Detail", "view-detail": "View Detail",
"copy": "Copy",
"visibility": { "visibility": {
"private": "Private", "private": "Private",
"protected": "Protected", "protected": "Protected",
@ -138,6 +139,7 @@
"user-not-found": "User not found", "user-not-found": "User not found",
"password-changed": "Password Changed", "password-changed": "Password Changed",
"private-only": "This memo is private only.", "private-only": "This memo is private only.",
"copied": "Copied" "copied": "Copied",
"succeed-copy-content": "Succeed to copy content to clipboard."
} }
} }

@ -74,6 +74,7 @@
}, },
"memo": { "memo": {
"view-detail": "Xem chi tiết", "view-detail": "Xem chi tiết",
"copy": "Sao chép",
"visibility": { "visibility": {
"private": "Private", "private": "Private",
"protected": "Protected", "protected": "Protected",
@ -138,6 +139,7 @@
"user-not-found": "Không tìm thấy người dùng này", "user-not-found": "Không tìm thấy người dùng này",
"password-changed": "Mật khẩu đã được thay đổi", "password-changed": "Mật khẩu đã được thay đổi",
"private-only": "Memo này có trạng thái riêng tư.", "private-only": "Memo này có trạng thái riêng tư.",
"copied": "Đã sao chép" "copied": "Đã sao chép",
"succeed-copy-content": "Đã sao chép nội dung memo thành công."
} }
} }

@ -74,6 +74,7 @@
}, },
"memo": { "memo": {
"view-detail": "查看详情", "view-detail": "查看详情",
"copy": "Copy",
"visibility": { "visibility": {
"private": "仅自己可见", "private": "仅自己可见",
"protected": "对所有用户公开", "protected": "对所有用户公开",
@ -138,6 +139,7 @@
"user-not-found": "未找到用户", "user-not-found": "未找到用户",
"password-changed": "密码已修改", "password-changed": "密码已修改",
"private-only": "This memo is private only.", "private-only": "This memo is private only.",
"copied": "Copied" "copied": "Copied",
"succeed-copy-content": "Succeed to copy content to clipboard."
} }
} }

@ -1,3 +1,4 @@
import copy from "copy-to-clipboard";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -65,6 +66,11 @@ const MemoDetail = () => {
}); });
}; };
const handleCopyContent = () => {
copy(state.memo.content);
toastHelper.success(t("message.succeed-copy-content"));
};
return ( return (
<section className="page-wrapper memo-detail"> <section className="page-wrapper memo-detail">
<div className="page-container"> <div className="page-container">
@ -92,38 +98,45 @@ const MemoDetail = () => {
<main className="memos-wrapper"> <main className="memos-wrapper">
<div className="memo-container"> <div className="memo-container">
<div className="memo-header"> <div className="memo-header">
<span className="time-text">{dayjs(state.memo.createdTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss")}</span> <div className="status-container">
{user?.id === state.memo.creatorId ? ( <span className="time-text">{dayjs(state.memo.createdTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss")}</span>
<Dropdown {user?.id === state.memo.creatorId ? (
className="visibility-selector" <Dropdown
trigger={ className="visibility-selector"
<span className={`status-text ${state.memo.visibility.toLowerCase()}`}> trigger={
{state.memo.visibility} <Icon.ChevronDown className="w-4 h-auto ml-px" /> <span className={`status-text ${state.memo.visibility.toLowerCase()}`}>
</span> {state.memo.visibility} <Icon.ChevronDown className="w-4 h-auto ml-px" />
}
actions={
<>
<span className="action-button" onClick={() => handleVisibilitySelectorChange("PRIVATE")}>
Private
</span>
<span className="action-button" onClick={() => handleVisibilitySelectorChange("PROTECTED")}>
Protected
</span> </span>
<span className="action-button" onClick={() => handleVisibilitySelectorChange("PUBLIC")}> }
Public actions={
</span> <>
</> <span className="action-button" onClick={() => handleVisibilitySelectorChange("PRIVATE")}>
} Private
actionsClassName="!w-28 !left-0 !p-1" </span>
/> <span className="action-button" onClick={() => handleVisibilitySelectorChange("PROTECTED")}>
) : ( Protected
<> </span>
<span className="split-text">by</span> <span className="action-button" onClick={() => handleVisibilitySelectorChange("PUBLIC")}>
<a className="name-text" href={`/u/${state.memo.creator.id}`}> Public
{state.memo.creator.name} </span>
</a> </>
</> }
)} actionsClassName="!w-28 !left-0 !p-1"
/>
) : (
<>
<span className="split-text">by</span>
<a className="name-text" href={`/u/${state.memo.creator.id}`}>
{state.memo.creator.name}
</a>
</>
)}
</div>
<div className="btns-container">
<button className="btn copy-btn" onClick={handleCopyContent}>
<Icon.Clipboard className="icon-img" />
</button>
</div>
</div> </div>
<MemoContent className="memo-content" content={state.memo.content} onMemoContentClick={() => undefined} /> <MemoContent className="memo-content" content={state.memo.content} onMemoContentClick={() => undefined} />
<MemoResources memo={state.memo} /> <MemoResources memo={state.memo} />

Loading…
Cancel
Save