diff --git a/.vscode/settings.json b/.vscode/settings.json index bc5e22b43..8cd4f4119 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "editor.tabSize": 4, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" - } + }, + "cSpell.words": ["uidotdev"] } diff --git a/packages/frontend/src/assets/img/share.png b/packages/frontend/src/assets/img/share.png new file mode 100644 index 000000000..24dc46da3 Binary files /dev/null and b/packages/frontend/src/assets/img/share.png differ diff --git a/packages/frontend/src/features/post/components/Post/Post.tsx b/packages/frontend/src/features/post/components/Post/Post.tsx index a5500e9c2..b7f9964f7 100644 --- a/packages/frontend/src/features/post/components/Post/Post.tsx +++ b/packages/frontend/src/features/post/components/Post/Post.tsx @@ -14,6 +14,8 @@ import { PostActionMenu } from './PostActionMenu' import { PostBlockedMask } from './PostBlockedMask' import PostFooter from './PostFooter' import { PostReportedMask } from './PostReportedMask' +import ShareLinkTransition from '../ShareLinkTransition/ShareLinkTransition' +import { useCopy } from '@/features/shared/hooks/useCopy' export default function Post({ id = '', @@ -87,6 +89,8 @@ export default function Post({ votedEpoch, ]) + const { hasCopied, copyToClipboard } = useCopy() + const [localUpCount, setLocalUpCount] = useState(upCount) const [localDownCount, setLocalDownCount] = useState(downCount) @@ -98,6 +102,13 @@ export default function Post({ const [isMineState, setIsMineState] = useState(isMine) const [isError, setIsError] = useState(false) + const handleShareClick = () => { + if (id) { + const postLink = `${window.location.origin}/posts/${id}` + copyToClipboard(postLink) + } + } + // set isAction when finalAction is changed useEffect(() => { setIsMineState(isMine) @@ -170,6 +181,7 @@ export default function Post({ {isReported && } {isBlocked && } {} + {}
{compact && status === PostStatus.Success ? ( {postInfo} @@ -190,6 +202,7 @@ export default function Post({ voteAction={isAction} handleVote={handleVote} handleComment={onComment} + handleShare={handleShareClick} />
{compact && imageUrl && ( diff --git a/packages/frontend/src/features/post/components/Post/PostFooter.tsx b/packages/frontend/src/features/post/components/Post/PostFooter.tsx index 32364b4f5..7373dcfc6 100644 --- a/packages/frontend/src/features/post/components/Post/PostFooter.tsx +++ b/packages/frontend/src/features/post/components/Post/PostFooter.tsx @@ -1,5 +1,6 @@ import CommentImg from '@/assets/img/comment.png' import DownVoteImg from '@/assets/img/downvote.png' +import ShareImg from '@/assets/img/share.png' import UpVoteImg from '@/assets/img/upvote.png' import { VoteAction } from '@/types/Vote' @@ -12,6 +13,7 @@ interface PostFooterProps { voteAction: VoteAction | null handleVote: (voteType: VoteAction) => void handleComment: () => void + handleShare: () => void } function PostFooter({ @@ -23,6 +25,7 @@ function PostFooter({ voteAction, handleVote, handleComment, + handleShare, }: PostFooterProps) { return (
@@ -45,6 +48,7 @@ function PostFooter({ count={countComment} onClick={handleComment} /> +
) } @@ -173,4 +177,28 @@ function UpVoteBtn({ ) } +interface ShareBtnProps { + onClick: () => void + isLoggedIn: boolean +} +function ShareBtn({ onClick, isLoggedIn }: ShareBtnProps) { + const cursor = isLoggedIn ? 'pointer' : 'not-allowed' + + return ( + <> + + + ) +} + export default PostFooter diff --git a/packages/frontend/src/features/post/components/ShareLinkTransition/ShareLinkTransition.tsx b/packages/frontend/src/features/post/components/ShareLinkTransition/ShareLinkTransition.tsx new file mode 100644 index 000000000..92de2a1ad --- /dev/null +++ b/packages/frontend/src/features/post/components/ShareLinkTransition/ShareLinkTransition.tsx @@ -0,0 +1,50 @@ +import ShareImg from '@/assets/img/share.png' +import { motion, useAnimation } from 'framer-motion' +import { useEffect } from 'react' + +interface ShareLinkTransitionProps { + isOpen: boolean +} + +export default function ShareLinkTransition({ + isOpen, +}: ShareLinkTransitionProps) { + const controls = useAnimation() + + useEffect(() => { + if (!isOpen) return + controls.start({ + y: [100, 0], + opacity: [0, 1, 0], + transition: { + y: { + type: 'spring', + stiffness: 50, + damping: 10, + duration: 1, + }, + opacity: { + duration: 2, // Complete fade-in-out cycle + ease: 'easeInOut', + }, + }, + }) + }, [controls, isOpen]) + + if (!isOpen) return null + + return ( + + + share + 貼文連結已複製成功! + + + ) +} diff --git a/packages/frontend/src/features/shared/components/Backdrop/Backdrop.tsx b/packages/frontend/src/features/shared/components/Backdrop/Backdrop.tsx index 7b189445e..6a6eeb96c 100644 --- a/packages/frontend/src/features/shared/components/Backdrop/Backdrop.tsx +++ b/packages/frontend/src/features/shared/components/Backdrop/Backdrop.tsx @@ -25,7 +25,7 @@ export default function Backdrop({ }, } - const chidrenVarients = { + const childrenVariants = { hidden: { opacity: 0 }, visible: { opacity: 1, @@ -63,7 +63,7 @@ export default function Backdrop({ h-full mt-0 `} - variants={chidrenVarients} + variants={childrenVariants} initial="hidden" animate="visible" > diff --git a/packages/frontend/src/features/shared/hooks/useCopy.ts b/packages/frontend/src/features/shared/hooks/useCopy.ts new file mode 100644 index 000000000..818628bf3 --- /dev/null +++ b/packages/frontend/src/features/shared/hooks/useCopy.ts @@ -0,0 +1,24 @@ +import { useEffect, useState } from 'react' +import { useCopyToClipboard } from 'react-use' + +export function useCopy(autoClearTimer: number = 2000) { + const [copiedText, copyToClipboard] = useCopyToClipboard() + const [hasCopied, setHasCopied] = useState(false) + + const handleCopy = (text: string) => { + copyToClipboard(text) + setHasCopied(true) + } + + useEffect(() => { + if (!hasCopied || autoClearTimer === 0) return + + const timer = setTimeout(() => { + setHasCopied(false) + }, autoClearTimer) + + return () => clearTimeout(timer) + }, [hasCopied, autoClearTimer]) + + return { copiedText, hasCopied, copyToClipboard: handleCopy } +}