Dev
Web
next
Components

Components

Copy to Clipboard

Thêm nút lệnh copy vào code block

Version (old)

components/copyToClipboard.tsx
const copyToClipboard = (str: string) => {
  const el = document.createElement('textarea')
  el.value = str
  el.setAttribute('readonly', '')
  el.style.position = 'absolute'
  el.style.left = '-9999px'
  document.body.appendChild(el)
  const selected =
    document.getSelection().rangeCount > 0
      ? document.getSelection().getRangeAt(0)
      : false
  el.select()
  document.execCommand('copy')
  document.body.removeChild(el)
  if (selected) {
    document.getSelection().removeAllRanges()
    document.getSelection().addRange(selected)
  }
}
 
export default copyToClipboard
lib/addCopyCodeButtons.ts
import toast from 'react-hot-toast'
import copyToClipboard from 'components/copyToClipboard'
 
function handleCopyClick(evt) {
  const { children } = evt.target.parentElement
  const { innerText } = Array.from(children)[0] as HTMLElement
 
  copyToClipboard(innerText)
  toast.success('Đã copy vào clipboard!') // Thông báo khi nhấn vào nút copy
}
 
function copyCodeListener() {
  const codeBlocks = document.querySelectorAll('pre[data-language]') // Thêm vào thẻ <pre>
 
  codeBlocks.forEach((codeBlock) => {
    const copy = document.createElement('div')
    copy.classList.add('copy-code-button')
    copy.innerHTML = 'Copy'
 
    copy.addEventListener('click', handleCopyClick)
 
    codeBlock.append(copy)
  })
}
 
export default copyCodeListener
blog/[slug].tsx
import { useEffect, useRef } from 'react'
import { useMDXComponent } from 'next-contentlayer/hooks'
import { getTweets } from 'lib/twitter'
import addCopyCodeButtons from 'lib/addCopyCodeButtons'
import components from 'components/MDXComponents'
import BlogLayout from 'layouts/blog'
import Tweet from 'components/Tweet'
import { allBlogs } from 'contentlayer/generated'
import { getPartialPost } from 'lib/contentlayer'
 
export default function Post({
  post,
  tweets,
}: {
  post: ReturnType<typeof getPartialPost>
  tweets: any[]
}) {
  const Component = useMDXComponent(post.body.code)
  const StaticTweet = ({ id }) => {
    const tweet = tweets.find((tweet) => tweet.id === id)
    return <Tweet {...tweet} />
  }
 
  // https://phannhatchanh.com/snippets/javascript/them-nut-copy-vao-code-blocks-bang-javascript/
  const effectRan = useRef(false)
  useEffect(() => {
    if (effectRan.current === true || process.env.NODE_ENV !== 'development') {
      addCopyCodeButtons()
    }
    return () => {
      effectRan.current = true
    }
  }, [])
 
  return (
    <BlogLayout post={post}>
      <Component
        components={
          {
            ...components,
            StaticTweet,
          } as any
        }
      />
    </BlogLayout>
  )
}
 
export async function getStaticPaths() {
  return {
    paths: allBlogs.map((p) => ({ params: { slug: p.slug } })),
    fallback: false,
  }
}
 
export async function getStaticProps({ params }) {
  const post = allBlogs.find((post) => post.slug === params.slug)
  const tweets = await getTweets(post.tweetIds)
 
  return {
    props: {
      post: getPartialPost(post, allBlogs),
      tweets,
    },
  }
}

Back To Top

components/BackToTop.tsx
import { useState, useEffect, FC } from 'react'
import { ChevronDoubleUpIcon } from '@heroicons/react/24/outline'
 
import Tippy from '@tippyjs/react'
 
const BackToTop: FC = () => {
  const [showButton, setShowButton] = useState(false)
 
  useEffect(() => {
    const toggleVisibility = () => {
      setShowButton(window.pageYOffset > 500)
    }
 
    window.addEventListener('scroll', toggleVisibility)
    return () => window.removeEventListener('scroll', toggleVisibility)
  }, [])
 
  return (
    <div className="fixed flex items-center justify-center p-0 text-xl rounded-full cursor-pointer bottom-2 right-2">
      {showButton && (
        <Tippy delay={[0, 50]} content="Trở lên trên" theme="tooltip">
          <ChevronDoubleUpIcon
            className="w-7 transition-colors text-accent-600 dark:text-accent-400 hover:text-[#db2777] dark:hover:text-[#db2777]"
            onClick={() => {
              window.scrollTo({
                top: 0,
                behavior: 'smooth',
              })
            }}
          />
        </Tippy>
      )}
    </div>
  )
}
 
export default BackToTop