mirror of
https://github.com/d0zingcat/NotionNext.git
synced 2026-05-20 15:09:40 +00:00
Code🤣
This commit is contained in:
14
components/Ackee.js
Normal file
14
components/Ackee.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useRouter } from 'next/router'
|
||||
import useAckee from 'use-ackee'
|
||||
|
||||
const Ackee = ({ ackeeServerUrl, ackeeDomainId }) => {
|
||||
const router = useRouter()
|
||||
useAckee(
|
||||
router.asPath,
|
||||
{ server: ackeeServerUrl, domainId: ackeeDomainId },
|
||||
{ detailed: false, ignoreLocalhost: true }
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
export default Ackee
|
||||
25
components/BlogPost.js
Normal file
25
components/BlogPost.js
Normal file
@@ -0,0 +1,25 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
const BlogPost = ({ post }) => {
|
||||
return (
|
||||
<article key={post.id}
|
||||
className='md:mx-2 w-full md:max-w-md duration-200 transform hover:scale-105 hover:shadow-2xl bg-white dark:bg-gray-800 dark:hover:bg-gray-600 overflow-hidden'>
|
||||
{/* 封面图 */}
|
||||
{post.page_cover && post.page_cover.length > 1 && (
|
||||
<a href={`${BLOG.path}/article/${post.slug}`} className='md:flex-shrink-0 md:w-52 md:h-52 rounded-lg'>
|
||||
<img className='w-full max-h-60 object-cover p-3 cursor-pointer' src={post.page_cover} alt={post.title} />
|
||||
</a>
|
||||
)}
|
||||
|
||||
<main className='px-8 py-2'>
|
||||
<a href={`${BLOG.path}/article/${post.slug}`}
|
||||
className='block my-3 text-2xl leading-tight font-semibold text-black dark:text-gray-200 hover:underline'>
|
||||
{post.title}
|
||||
</a>
|
||||
<p className='mt-2 text-gray-500 dark:text-gray-400 text-sm'>{post.summary}</p>
|
||||
</main>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPost
|
||||
24
components/BlogPostMini.js
Normal file
24
components/BlogPostMini.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import BLOG from '@/blog.config'
|
||||
|
||||
const BlogPostMini = ({ post }) => {
|
||||
return (
|
||||
<a key={post.id} href={`${BLOG.path}/article/${post.slug}`}
|
||||
className='md:flex w-full border my-2 duration-200 transform hover:scale-105 hover:shadow-2xl bg-white dark:bg-gray-800 dark:hover:bg-gray-600'>
|
||||
{/* 封面图 */}
|
||||
{post.page_cover && post.page_cover.length > 1 && (
|
||||
<img className='md:w-40 w-full max-h-32 object-cover cursor-pointer' src={post.page_cover} alt={post.title} />
|
||||
)}
|
||||
|
||||
<main className='px-2 py-1'>
|
||||
<a href={`${BLOG.path}/article/${post.slug}`}
|
||||
className='block my-3 leading-tight font-semibold text-black dark:text-gray-200 hover:underline'>
|
||||
{post.title}
|
||||
</a>
|
||||
<p className='mt-2 text-gray-500 dark:text-gray-400 text-xs overflow-x-hidden'>{post.summary}</p>
|
||||
<p className='mt-2 text-gray-500 dark:text-gray-400 text-xs overflow-x-hidden'>{BLOG.link}/article/{post.slug}</p>
|
||||
</main>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
export default BlogPostMini
|
||||
67
components/Comment.js
Normal file
67
components/Comment.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const GitalkComponent = dynamic(
|
||||
() => {
|
||||
return import('gitalk/dist/gitalk-component')
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
const UtterancesComponent = dynamic(
|
||||
() => {
|
||||
return import('@/components/Utterances')
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
const CusdisComponent = dynamic(
|
||||
() => {
|
||||
return import('react-cusdis').then(m => m.ReactCusdis)
|
||||
},
|
||||
{ ssr: false }
|
||||
)
|
||||
|
||||
const Comment = ({ frontMatter }) => {
|
||||
const router = useRouter()
|
||||
|
||||
return <div className='comment'>
|
||||
<div className='font-bold text-gray-800 pt-2 pb-4 dark:text-gray-300'>留下评论</div>
|
||||
|
||||
{/* 评论插件 */}
|
||||
{BLOG.comment.provider === 'gitalk' && (
|
||||
<GitalkComponent
|
||||
options={{
|
||||
id: frontMatter.id,
|
||||
title: frontMatter.title,
|
||||
clientID: BLOG.comment.gitalkConfig.clientID,
|
||||
clientSecret: BLOG.comment.gitalkConfig.clientSecret,
|
||||
repo: BLOG.comment.gitalkConfig.repo,
|
||||
owner: BLOG.comment.gitalkConfig.owner,
|
||||
admin: BLOG.comment.gitalkConfig.admin,
|
||||
distractionFreeMode: BLOG.comment.gitalkConfig.distractionFreeMode
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{BLOG.comment.provider === 'utterances' && (
|
||||
<UtterancesComponent issueTerm={frontMatter.id} className='px-2' />
|
||||
)}
|
||||
{BLOG.comment.provider === 'cusdis' && (
|
||||
<>
|
||||
<script defer src='https://cusdis.com/js/widget/lang/zh-cn.js' />
|
||||
<CusdisComponent
|
||||
attrs={{
|
||||
host: BLOG.comment.cusdisConfig.host,
|
||||
appId: BLOG.comment.cusdisConfig.appId,
|
||||
pageId: frontMatter.id,
|
||||
pageTitle: frontMatter.title,
|
||||
pageUrl: BLOG.link + router.asPath,
|
||||
theme: BLOG.appearance
|
||||
}}
|
||||
lang={BLOG.lang.toLowerCase()}
|
||||
/>
|
||||
</>
|
||||
|
||||
)}</div>
|
||||
}
|
||||
|
||||
export default Comment
|
||||
45
components/CommonHead.js
Normal file
45
components/CommonHead.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import Head from 'next/head'
|
||||
|
||||
const CommonHead = ({ meta }) => {
|
||||
const url = BLOG.path.length ? `${BLOG.link}/${BLOG.path}` : BLOG.link
|
||||
|
||||
return <Head>
|
||||
<title>{meta.title}</title>
|
||||
<meta content={BLOG.darkBackground} name='theme-color' />
|
||||
<meta name='robots' content='follow, index' />
|
||||
<meta charSet='UTF-8' />
|
||||
{BLOG.seo.googleSiteVerification && (
|
||||
<meta
|
||||
name='google-site-verification'
|
||||
content={BLOG.seo.googleSiteVerification}
|
||||
/>
|
||||
)}
|
||||
{BLOG.seo.keywords && (
|
||||
<meta name='keywords' content={BLOG.seo.keywords.join(', ')} />
|
||||
)}
|
||||
<meta name='description' content={meta.description} />
|
||||
<meta property='og:locale' content={BLOG.lang} />
|
||||
<meta property='og:title' content={meta.title} />
|
||||
<meta property='og:description' content={meta.description} />
|
||||
<meta
|
||||
property='og:url'
|
||||
content={meta.slug ? `${url}/${meta.slug}` : url}
|
||||
/>
|
||||
<meta property='og:type' content={meta.type} />
|
||||
<meta name='twitter:card' content='summary_large_image' />
|
||||
<meta name='twitter:description' content={meta.description} />
|
||||
<meta name='twitter:title' content={meta.title} />
|
||||
{meta.type === 'article' && (
|
||||
<>
|
||||
<meta
|
||||
property='article:published_time'
|
||||
content={meta.date || meta.createdTime}
|
||||
/>
|
||||
<meta property='article:author' content={BLOG.author} />
|
||||
</>
|
||||
)}
|
||||
</Head>
|
||||
}
|
||||
|
||||
export default CommonHead
|
||||
22
components/ContactButton.js
Normal file
22
components/ContactButton.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
/**
|
||||
* 悬浮在屏幕右下角,联系我的按钮
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const ContactButton = () => {
|
||||
return (
|
||||
<Link href='/article/about'>
|
||||
<a className={'fixed right-10 bottom-40 animate__fadeInRight animate__animated animate__faster'}>
|
||||
<span
|
||||
className='dark:bg-black bg-white px-5 py-3 cursor-pointer shadow-card text-xl hover:bg-blue-500 transform duration-200 hover:text-white hover:shadow'>
|
||||
<span className='dark:text-gray-200 fa fa-info' title='about' />
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContactButton
|
||||
16
components/Container.js
Normal file
16
components/Container.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
const Container = ({ children, layout, fullWidth, ...customMeta }) => {
|
||||
return (
|
||||
<div>
|
||||
{/* 公共头 */}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Container.propTypes = {
|
||||
children: PropTypes.node
|
||||
}
|
||||
|
||||
export default Container
|
||||
33
components/Cusdis.js
Normal file
33
components/Cusdis.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect } from 'react'
|
||||
const Cusdis = ({ id, url, title }) => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement('script')
|
||||
const anchor = document.getElementById('comments')
|
||||
script.setAttribute(
|
||||
'src',
|
||||
BLOG.comment.cusdisConfig.scriptSrc ||
|
||||
'https://cusdis.com/js/cusdis.es.js'
|
||||
)
|
||||
script.setAttribute('async', true)
|
||||
script.setAttribute('defer', true)
|
||||
anchor.appendChild(script)
|
||||
return () => {
|
||||
anchor.innerHTML = ''
|
||||
}
|
||||
})
|
||||
return (
|
||||
<div id="comments">
|
||||
<div
|
||||
id="cusdis_thread"
|
||||
data-host={BLOG.comment.cusdisConfig.host || 'https://cusdis.com'}
|
||||
data-app-id={BLOG.comment.cusdisConfig.appId}
|
||||
data-page-id={id}
|
||||
data-page-url={url}
|
||||
data-page-title={title}
|
||||
></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cusdis
|
||||
19
components/DarkModeButton.js
Normal file
19
components/DarkModeButton.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { useTheme } from '@/lib/theme'
|
||||
import localStorage from 'localStorage'
|
||||
|
||||
const DarkModeButton = () => {
|
||||
const { theme, changeTheme } = useTheme()
|
||||
const handleChangeDarkMode = () => {
|
||||
const newTheme = (theme === 'light' ? 'dark' : 'light')
|
||||
changeTheme(newTheme)
|
||||
localStorage.setItem('theme', newTheme)
|
||||
}
|
||||
return <div className=''>
|
||||
<div onClick={handleChangeDarkMode}
|
||||
className='border w-10 h-10 justify-center align-middle font-bold text-lg rounded flex p-2.5 cursor-pointer text-gray-600 hover:scale-125 transform duration-200
|
||||
dark:bg-gray-600 dark:text-gray-300 dark:hover:bg-gray-100 dark:hover:text-black'>
|
||||
<span className={'fa px-1 ' + (theme === 'dark' ? ' fa-sun-o' : ' fa-moon-o')} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
export default DarkModeButton
|
||||
23
components/Footer.js
Normal file
23
components/Footer.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import React from 'react'
|
||||
import SocialButton from '@/components/SocialButton'
|
||||
|
||||
const Footer = ({ fullWidth = true }) => {
|
||||
const d = new Date()
|
||||
const y = d.getFullYear()
|
||||
const from = +BLOG.since
|
||||
return (
|
||||
<div
|
||||
className='py-4 flex-shrink-0 m-auto w-full text-gray-500 dark:text-gray-400 bottom-0'
|
||||
>
|
||||
<SocialButton/>
|
||||
<div className='text-sm'>
|
||||
<span className='fa fa-shield leading-6'><a href='https://beian.miit.gov.cn/' className='ml-1'>闽ICP备20010331号</a></span>
|
||||
<br/>
|
||||
<span className='fa fa-copyright leading-6'> {from === y || !from ? y : `${from} - ${y}`} {BLOG.author} </span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Footer
|
||||
18
components/Gtag.js
Normal file
18
components/Gtag.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import * as gtag from '@/lib/gtag'
|
||||
|
||||
const Gtag = () => {
|
||||
const router = useRouter()
|
||||
useEffect(() => {
|
||||
const handleRouteChange = url => {
|
||||
gtag.pageview(url)
|
||||
}
|
||||
router.events.on('routeChangeComplete', handleRouteChange)
|
||||
return () => {
|
||||
router.events.off('routeChangeComplete', handleRouteChange)
|
||||
}
|
||||
}, [router.events])
|
||||
return null
|
||||
}
|
||||
export default Gtag
|
||||
94
components/Header.js
Normal file
94
components/Header.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import Image from 'next/image'
|
||||
|
||||
const NavBar = () => {
|
||||
const links = []
|
||||
return (
|
||||
<div className='flex-shrink-0'>
|
||||
<ul className='flex flex-row'>
|
||||
{links.map(
|
||||
link =>
|
||||
link.show && (
|
||||
<li
|
||||
key={link.id}
|
||||
className='block ml-4 text-black dark:text-gray-50 nav'
|
||||
>
|
||||
<Link href={link.to}>
|
||||
<a>{(link.icon && (<i className={'px-1 fa ' + link.icon} />))} {link.name}</a>
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const Header = ({ navBarTitle, fullWidth }) => {
|
||||
const navRef = useRef(null)
|
||||
const sentinelRef = useRef([])
|
||||
// 当Header移出屏幕时改变的样式
|
||||
const handler = ([entry]) => {
|
||||
if (navRef && navRef.current) {
|
||||
if (!entry.isIntersecting && entry !== undefined) {
|
||||
navRef.current.classList.add('sticky-nav-full')
|
||||
} else {
|
||||
navRef.current.classList.remove('sticky-nav-full')
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
const observer = new window.IntersectionObserver(handler)
|
||||
observer.observe(sentinelRef.current)
|
||||
// Don't touch this, I have no idea how it works XD
|
||||
// return () => {
|
||||
// if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
|
||||
// }
|
||||
}, [sentinelRef])
|
||||
return (
|
||||
<>
|
||||
<div className='observer-element h-0.5' ref={sentinelRef}/>
|
||||
<div
|
||||
className={`sticky-nav m-auto w-full h-6 flex flex-row justify-between items-center mb-2 py-8 bg-opacity-60 ${
|
||||
!fullWidth ? 'max-w-5xl px-4' : 'px-4 md:px-24'
|
||||
}`}
|
||||
id='sticky-nav'
|
||||
ref={navRef}
|
||||
>
|
||||
<div className='flex items-center'>
|
||||
<Link href='/'>
|
||||
<a>
|
||||
<div className='h-6'>
|
||||
<Image
|
||||
alt={BLOG.author}
|
||||
width={24}
|
||||
height={24}
|
||||
src='/favicon.svg'
|
||||
className='rounded-full'
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
{navBarTitle
|
||||
? (
|
||||
<p className='ml-2 font-medium text-day dark:text-night header-name'>
|
||||
{navBarTitle}
|
||||
</p>
|
||||
)
|
||||
: (
|
||||
<p className='ml-2 font-medium text-day dark:text-night header-name'>
|
||||
{BLOG.title} {' '}
|
||||
{BLOG.title},{' '}
|
||||
<span className='font-normal'>{BLOG.description}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<NavBar />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
70
components/LeftAside.js
Normal file
70
components/LeftAside.js
Normal file
@@ -0,0 +1,70 @@
|
||||
import Tags from '@/components/Tags'
|
||||
import { useLocale } from '@/lib/locale'
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useState } from 'react'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
import SocialButton from '@/components/SocialButton'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
const LeftAside = ({ tags, currentTag }) => {
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
Router.push({ pathname: '/', query: { s: searchValue } })
|
||||
}
|
||||
}
|
||||
return <aside
|
||||
style={{ width: '330px' }}
|
||||
className='px-10 hidden xl:block py-5 bg-gray-50 dark:bg-gray-800 duration-200 border-r dark:border-black'
|
||||
>
|
||||
|
||||
<div className='sticky top-16'>
|
||||
<div className='my-5 flex'>
|
||||
<Link href='/'>
|
||||
<a
|
||||
className='hover:shadow-xl dark:border-gray-600 border-black border-2 bg-white dark:bg-gray-800 dark:text-gray-300 font-semibold hover:bg-gray-800 hover:text-white p-2 duration-200'>{BLOG.title}</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className='text-gray-500 dark:text-gray-300'>
|
||||
<i className='fa fa-map-marker mr-1' />
|
||||
Fuzhou, China
|
||||
</div>
|
||||
|
||||
<hr className='my-5'/>
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className='flex justify-center items-center py-5 '>
|
||||
<i className='fa fa-search absolute right-8 text-gray-400' />
|
||||
<input
|
||||
type='text'
|
||||
placeholder={
|
||||
currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`
|
||||
}
|
||||
className='hover:shadow-xl duration-200 px-5 bg-gray-100 rounded w-full py-2 border-black dark:border-gray-600 bg-white text-black dark:bg-gray-700 dark:text-white'
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={e => setSearchValue(e.target.value)}
|
||||
defaultValue={router.query.s ?? ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<hr className='my-5'/>
|
||||
|
||||
<div>
|
||||
<span className='dark:text-gray-200'>标签</span>
|
||||
<Tags tags={tags} currentTag={currentTag} />
|
||||
</div>
|
||||
|
||||
<div className='bottom-1 fixed'>
|
||||
<div className='justify-center flex '><DarkModeButton /></div>
|
||||
<Footer/>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
export default LeftAside
|
||||
44
components/Pagination.js
Normal file
44
components/Pagination.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useLocale } from '@/lib/locale'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
const Pagination = ({ page, showNext }) => {
|
||||
const locale = useLocale()
|
||||
const router = useRouter()
|
||||
const currentPage = +page
|
||||
return (
|
||||
<div className=' my-10 flex justify-between font-medium text-black dark:text-gray-100 mx-5'>
|
||||
<Link
|
||||
href={
|
||||
{
|
||||
pathname: (currentPage - 1 === 1 ? `${BLOG.path || '/'}` : `/page/${currentPage - 1}`),
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}
|
||||
}
|
||||
>
|
||||
<button
|
||||
rel='prev'
|
||||
className={`${currentPage === 1 ? 'invisible' : 'block'} duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
← {locale.PAGINATION.PREV}
|
||||
</button>
|
||||
</Link>
|
||||
<Link href={
|
||||
{
|
||||
pathname: `/page/${currentPage + 1}`,
|
||||
query: router.query.s ? { s: router.query.s } : {}
|
||||
}
|
||||
}>
|
||||
<button
|
||||
rel='next'
|
||||
className={`${+showNext ? 'block' : 'invisible'} duration-200 px-4 py-2 hover:border-black border-b-2 hover:font-bold`}
|
||||
>
|
||||
{locale.PAGINATION.NEXT} →
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Pagination
|
||||
35
components/Progress.js
Normal file
35
components/Progress.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部;当屏幕下滑500像素后会出现该控件
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const Progress = ({ targetRef }) => {
|
||||
const [percent, changePercent] = useState(0)
|
||||
useEffect(() => {
|
||||
const scrollListener = throttle(() => {
|
||||
if (targetRef.current) {
|
||||
const fullHeight = targetRef.current.clientHeight
|
||||
const per = parseFloat(((window.scrollY / (fullHeight) * 100)).toFixed(0))
|
||||
changePercent(per)
|
||||
}
|
||||
// console.log('滚动信息', window.scrollY, fullHeight, per)
|
||||
}, 1)
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [percent])
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 顶部进度条 */}
|
||||
<div className='h-1.5 fixed top-0 w-full shadow-2xl z-40'>
|
||||
<div className='h-1 bg-blue-500 fixed top-0 w-1 duration-200' style={{ width: `${percent}%` }}/>
|
||||
{/* <div className='debug'>{percent}</div> */}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Progress
|
||||
63
components/RewardButton.js
Normal file
63
components/RewardButton.js
Normal file
@@ -0,0 +1,63 @@
|
||||
import React from 'react'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
|
||||
/**
|
||||
* 赞赏模块
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const RewardButton = () => {
|
||||
const [popoverShow, setPopoverShow] = React.useState(false)
|
||||
const btnRef = React.createRef()
|
||||
const popoverRef = React.createRef()
|
||||
|
||||
const openPopover = () => {
|
||||
createPopper(btnRef.current, popoverRef.current, {
|
||||
placement: 'top'
|
||||
})
|
||||
setPopoverShow(true)
|
||||
}
|
||||
const closePopover = () => {
|
||||
setPopoverShow(false)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => {
|
||||
openPopover()
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
closePopover()
|
||||
}}>
|
||||
<div className='animate__jello animate__animated animate__faster'>
|
||||
<div
|
||||
ref={btnRef}
|
||||
className='bg-blue-500 text-white hover:bg-white hover:text-black hover:shadow-2xl border duration-200 transform hover:scale-110 px-3 py-2 rounded cursor-pointer'>
|
||||
<div>
|
||||
<span className='fa fa-qrcode mr-2' />
|
||||
<span>打赏</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={
|
||||
(popoverShow ? 'animate__animated animate__fadeIn ' : 'hidden ') +
|
||||
' animate__faster border-0 transform block z-50 font-normal'
|
||||
}
|
||||
ref={popoverRef}
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className='border animate__animated animate__fadeIn hover:shadow-2xl duration-200 my-5 px-5 py-6 w-96 grid justify-center bg-white dark:bg-black dark:text-gray-200'>
|
||||
<span>
|
||||
<img className='md:w-72 m-auto' src='/reward_code.jpg' />
|
||||
</span>
|
||||
<br />
|
||||
<span className='text-center text-gray-500'>微信赞赏码或支付宝tlyong@126.com赞助</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default RewardButton
|
||||
14
components/RightAside.js
Normal file
14
components/RightAside.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import Toc from '@/components/Toc'
|
||||
|
||||
const RightAside = ({ toc }) => {
|
||||
// 无目录就直接返回空
|
||||
if (toc.length < 1) return <></>
|
||||
|
||||
return <aside className='bg-gray-800 px-5 hidden lg:block py-5 hover:shadow-2xl duration-200'>
|
||||
<div className='sticky top-8 w-60 overflow-x-auto'>
|
||||
<Toc toc={toc}/>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
export default RightAside
|
||||
13
components/RightWidget.js
Normal file
13
components/RightWidget.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from 'react'
|
||||
import TopJumper from '@/components/TopJumper'
|
||||
import ShareButton from '@/components/ShareButton'
|
||||
|
||||
const RightWidget = ({ post }) => {
|
||||
return <div className='fixed right-0 lg:mr-72 bottom-10 flex justify-center'>
|
||||
<div className='flex-wrap'>
|
||||
<ShareButton post={post}/>
|
||||
<TopJumper/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
export default RightWidget
|
||||
84
components/ShareBar.js
Normal file
84
components/ShareBar.js
Normal file
@@ -0,0 +1,84 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useRouter } from 'next/router'
|
||||
import React from 'react'
|
||||
import { createPopper } from '@popperjs/core'
|
||||
import copy from 'copy-to-clipboard'
|
||||
import QRCode from 'qrcode.react'
|
||||
|
||||
const ShareBar = ({ post }) => {
|
||||
const router = useRouter()
|
||||
const shareUrl = BLOG.link + router.asPath
|
||||
|
||||
// 二维码悬浮
|
||||
const [qrCodeShow, setQrCodeShow] = React.useState(false)
|
||||
const btnRef = React.createRef()
|
||||
const popoverRef = React.createRef()
|
||||
|
||||
const openPopover = () => {
|
||||
createPopper(btnRef.current, popoverRef.current, {
|
||||
placement: 'left'
|
||||
})
|
||||
setQrCodeShow(true)
|
||||
}
|
||||
const closePopover = () => {
|
||||
setQrCodeShow(false)
|
||||
}
|
||||
|
||||
const copyUrl = () => {
|
||||
copy(shareUrl)
|
||||
alert('当前链接已复制到剪贴板')
|
||||
}
|
||||
|
||||
return <>
|
||||
<div className='text-gray-500 flex-col text-center space-y-2 w-12 border my-1 bg-white dark:bg-gray-800 dark:text-white overflow-hidden'>
|
||||
<div>
|
||||
分享
|
||||
</div>
|
||||
<div>
|
||||
<a className='fa fa-facebook-square cursor-pointer text-3xl'
|
||||
href={`https://www.facebook.com/sharer.php?u=${shareUrl}`} />
|
||||
</div>
|
||||
<div>
|
||||
<a className='fa fa-twitter-square text-3xl' target='_blank' rel='noreferrer'
|
||||
href={`https://twitter.com/intent/tweet?title=${post.title}&url${shareUrl}`} />
|
||||
</div>
|
||||
<div>
|
||||
<a className='fa fa-telegram text-3xl' href={`https://telegram.me/share/url?url=${shareUrl}&text=${post.title}`} />
|
||||
</div>
|
||||
<div>
|
||||
<a className='fa fa-wechat cursor-pointer text-3xl' ref={btnRef}
|
||||
onMouseEnter={() => { openPopover() }}
|
||||
onMouseLeave={() => { closePopover() }}>
|
||||
<div ref={popoverRef}
|
||||
className={(qrCodeShow ? 'animate__animated animate__fadeIn ' : 'hidden') + ' text-center py-2 bg-white'}>
|
||||
<div className='p-2 bg-white border-0 duration-200 transform block z-50 font-normal shadow-xl'>
|
||||
<QRCode
|
||||
value={shareUrl}// 生成二维码的内容
|
||||
fgColor='#000000' // 二维码的颜色
|
||||
/>
|
||||
</div>
|
||||
<span className='bg-white text-black font-semibold p-1 mb-0 rounded-t-lg text-sm mx-auto'>
|
||||
扫一扫分享
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a className='fa fa-weibo text-3xl' target='_blank' rel='noreferrer'
|
||||
href={`https://service.weibo.com/share/share.php?url=${shareUrl}&title=${post.title}`} />
|
||||
</div>
|
||||
<div>
|
||||
<a className='fa fa-qq text-3xl' target='_blank' rel='noreferrer'
|
||||
href={`http://connect.qq.com/widget/shareqq/index.html?url=${shareUrl}&sharesource=qzone&title=${post.title}&desc=${post.summary}`} />
|
||||
</div>
|
||||
<div>
|
||||
<a className='fa fa-star text-3xl' target='_blank' rel='noreferrer'
|
||||
href={`https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=${shareUrl}&sharesource=qzone&title=${post.title}&summary=${post.summary}`} />
|
||||
</div>
|
||||
<div>
|
||||
<a className='fa fa-link cursor-pointer text-3xl' onClick={() => { copyUrl() }} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
export default ShareBar
|
||||
42
components/ShareButton.js
Normal file
42
components/ShareButton.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import ShareBar from '@/components/ShareBar'
|
||||
|
||||
/**
|
||||
* 悬浮在屏幕右下角,分享按钮
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const ShareButton = ({ post }) => {
|
||||
const [popoverShow, setPopoverShow] = React.useState(false)
|
||||
const btnRef = React.createRef()
|
||||
|
||||
const openPopover = () => {
|
||||
setPopoverShow(true)
|
||||
}
|
||||
const closePopover = () => {
|
||||
setPopoverShow(false)
|
||||
}
|
||||
return (
|
||||
<div className='my-2'
|
||||
onMouseEnter={() => { openPopover() }}
|
||||
onMouseLeave={() => { closePopover() }}>
|
||||
<div className=' overflow-hidden '>
|
||||
<div
|
||||
className={
|
||||
(popoverShow ? ' block ' : ' hidden ') +
|
||||
' duration-200 transform transition z-50 font-normal'
|
||||
}
|
||||
>
|
||||
<ShareBar post={post}/>
|
||||
</div>
|
||||
<div
|
||||
ref={btnRef}
|
||||
className='border dark:bg-black bg-white px-4 py-3 cursor-pointer text-md hover:bg-blue-500 transform duration-200 hover:text-white hover:shadow'>
|
||||
<div className='dark:text-gray-200 fa fa-share-alt' title='share' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShareButton
|
||||
28
components/SocialButton.js
Normal file
28
components/SocialButton.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react'
|
||||
|
||||
const SocialButton = () => {
|
||||
return <>
|
||||
<div className='space-x-3 text-xl'>
|
||||
<a className='fa fa-rss hover:underline' href='/feed' target='_blank' id='feed'/>
|
||||
<a className='fa fa-info hover:underline mx-1' href='/article/about' id='about'/>
|
||||
<a className='fa fa-github' target='_blank' rel='noreferrer' title={'github'}
|
||||
href={'https://github.com/tangly1024'} />
|
||||
<a className='fa fa-twitter' target='_blank' rel='noreferrer' title={'twitter'}
|
||||
href={'https://twitter.com/troy1024_1'} />
|
||||
<a className='fa fa-telegram' href={'https://t.me/tangly_1024'} title={'telegram'} />
|
||||
<a className='fa fa-weibo' target='_blank' rel='noreferrer' title={'weibo'}
|
||||
href={'http://weibo.com/tangly1024'} />
|
||||
<span id='busuanzi_container_site_pv' className='hidden'><span className='s'> | </span>
|
||||
<a href='https://www.cnzz.com/stat/website.php?web_id=1279970751' target='_blank'
|
||||
id='busuanzi_container_site_pv'
|
||||
className='fa fa-user' rel='noreferrer'> pv <span id='busuanzi_value_site_pv'></span></a>
|
||||
</span>
|
||||
|
||||
<span id='busuanzi_container_site_uv' className='hidden'><span className='s'> | </span>
|
||||
<a href='http://tongji.baidu.com/web/10000363165/overview/index?siteId=16809429' target='_blank'
|
||||
className='fa fa-eye' rel='noreferrer'> uv <span id='busuanzi_value_site_uv'></span></a>
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
export default SocialButton
|
||||
14
components/TagItem.js
Normal file
14
components/TagItem.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const TagItem = ({ tag }) => (
|
||||
<Link href={`/tag/${encodeURIComponent(tag)}`}>
|
||||
<a>
|
||||
<p className="hover:shadow hover:scale-105 hover:bg-blue-500 bg-gray-200 hover:text-white duration-200 mr-1 px-2 py-1 leading-none text-sm
|
||||
dark:bg-gray-500 dark:hover:bg-black">
|
||||
{tag}
|
||||
</p>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
|
||||
export default TagItem
|
||||
27
components/Tags.js
Normal file
27
components/Tags.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
const Tags = ({ tags, currentTag }) => {
|
||||
if (!tags) return <></>
|
||||
return (
|
||||
<ul className='flex flex-wrap py-1 max-w-full overflow-x-auto'>
|
||||
{Object.keys(tags).map(key => {
|
||||
const selected = key === currentTag
|
||||
return (
|
||||
<Link key={key} href={`/tag/${encodeURIComponent(key)}`}>
|
||||
<li
|
||||
className={`cursor-pointer hover:bg-gray-600 rounded-sm hover:text-white duration-200 mr-1 my-1 px-2 py-1 font-medium text-xs whitespace-nowrap
|
||||
dark:text-gray-300 dark:hover:bg-gray-600 ${selected ? 'text-white bg-black dark:border-gray-600' : 'bg-gray-200 text-gray-600 dark:bg-gray-900 dark:border-gray-600'
|
||||
}`}
|
||||
>
|
||||
<a>
|
||||
{`${key} (${tags[key]})`}
|
||||
</a>
|
||||
</li>
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default Tags
|
||||
86
components/Toc.js
Normal file
86
components/Toc.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import React from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { uuidToId } from 'notion-utils'
|
||||
import { cs } from 'react-notion-x'
|
||||
|
||||
/**
|
||||
* 目录组件
|
||||
*/
|
||||
const Toc = ({ toc }) => {
|
||||
// 无目录就直接返回空
|
||||
if (toc.length < 1) return <></>
|
||||
|
||||
// 监听滚动事件
|
||||
React.useEffect(() => {
|
||||
window.addEventListener('scroll', actionSectionScrollSpy)
|
||||
actionSectionScrollSpy()
|
||||
return () => {
|
||||
window.removeEventListener('scroll', actionSectionScrollSpy)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 同步选中目录事件
|
||||
const [activeSection, setActiveSection] = React.useState(null)
|
||||
const throttleMs = 100
|
||||
const actionSectionScrollSpy = throttle(() => {
|
||||
const sections = document.getElementsByClassName('notion-h')
|
||||
let prevBBox = null
|
||||
let currentSectionId = activeSection
|
||||
for (let i = 0; i < sections.length; ++i) {
|
||||
const section = sections[i]
|
||||
if (!section || !(section instanceof Element)) continue
|
||||
if (!currentSectionId) {
|
||||
currentSectionId = section.getAttribute('data-id')
|
||||
}
|
||||
const bbox = section.getBoundingClientRect()
|
||||
const prevHeight = prevBBox ? bbox.top - prevBBox.bottom : 0
|
||||
const offset = Math.max(150, prevHeight / 4)
|
||||
// GetBoundingClientRect returns values relative to viewport
|
||||
if (bbox.top - offset < 0) {
|
||||
currentSectionId = section.getAttribute('data-id')
|
||||
prevBBox = bbox
|
||||
continue
|
||||
}
|
||||
// No need to continue loop, if last element has been detected
|
||||
break
|
||||
}
|
||||
setActiveSection(currentSectionId)
|
||||
}, throttleMs)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='text-center font-bold text-white'>
|
||||
文章目录
|
||||
</div>
|
||||
<nav className='notion-table-of-contents text-gray-400 underline'>
|
||||
{toc.map((tocItem) => {
|
||||
const id = uuidToId(tocItem.id)
|
||||
return (
|
||||
<a
|
||||
key={id}
|
||||
href={`#${id}`}
|
||||
className={cs(
|
||||
'notion-table-of-contents-item',
|
||||
`notion-table-of-contents-item-indent-level-${tocItem.indentLevel}`,
|
||||
activeSection === id &&
|
||||
' font-bold text-white'
|
||||
)}
|
||||
>
|
||||
<span
|
||||
className='notion-table-of-contents-item-body'
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginLeft: tocItem.indentLevel * 16
|
||||
}}
|
||||
>
|
||||
{tocItem.text}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</nav>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toc
|
||||
39
components/TopJumper.js
Normal file
39
components/TopJumper.js
Normal file
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import throttle from 'lodash.throttle'
|
||||
import { useLocale } from '@/lib/locale'
|
||||
|
||||
/**
|
||||
* 跳转到网页顶部;当屏幕下滑500像素后会出现该控件
|
||||
* @returns {JSX.Element}
|
||||
* @constructor
|
||||
*/
|
||||
const TopJumper = () => {
|
||||
const locale = useLocale()
|
||||
|
||||
const [show, switchShow] = useState(false)
|
||||
useEffect(() => {
|
||||
const scrollListener = throttle(() => {
|
||||
// 处理是否显示回到顶部按钮
|
||||
const shouldShow = window.scrollY > 100
|
||||
if (shouldShow !== show) {
|
||||
switchShow(shouldShow)
|
||||
}
|
||||
}, 500)
|
||||
document.addEventListener('scroll', scrollListener)
|
||||
return () => document.removeEventListener('scroll', scrollListener)
|
||||
}, [show])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={(show ? 'animate__fadeInUp' : 'animate__fadeOutUp') + ' animate__animated animate__faster'}>
|
||||
<div
|
||||
className='border dark:bg-black bg-white cursor-pointer hover:bg-blue-500 transform duration-200 hover:text-white hover:shadow-2xl hover:scale-125'
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: 'smooth' })}>
|
||||
<a className='dark:text-gray-200 fa fa-arrow-up p-4' title={locale.POST.TOP}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
export default TopJumper
|
||||
88
components/TopNav.js
Normal file
88
components/TopNav.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import Link from 'next/link'
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useLocale } from '@/lib/locale'
|
||||
import Router, { useRouter } from 'next/router'
|
||||
import Tags from '@/components/Tags'
|
||||
import localStorage from 'localStorage'
|
||||
import { useTheme } from '@/lib/theme'
|
||||
import DarkModeButton from '@/components/DarkModeButton'
|
||||
import SocialButton from '@/components/SocialButton'
|
||||
|
||||
const TopNav = ({ tags, currentTag }) => {
|
||||
const locale = useLocale()
|
||||
const [hiddenMenu, switchHiddenMenu] = useState(!currentTag)
|
||||
// 点击按钮更改菜单状态
|
||||
const handleMenuClick = () => {
|
||||
switchHiddenMenu(!hiddenMenu)
|
||||
}
|
||||
const router = useRouter()
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const handleKeyUp = (e) => {
|
||||
if (e.keyCode === 13) {
|
||||
Router.push({ pathname: '/', query: { s: searchValue } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='bg-white dark:bg-gray-600 block xl:hidden'>
|
||||
{/* 隐藏的顶部菜单 */}
|
||||
<div
|
||||
className={(hiddenMenu ? 'h-0 ' : 'h-full ') + ' overflow-hidden bg-gray-800 text-xl text-gray-200 w-full transform ease-in-out duration-500'}>
|
||||
<ul className='mx-5 duration-300'>
|
||||
<li>
|
||||
<div>
|
||||
<Tags tags={tags} currentTag={currentTag} />
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<SocialButton/>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{/* 导航栏 */}
|
||||
<div
|
||||
id='sticky-nav'
|
||||
className='text-sm ticky-nav m-auto w-full flex flex-row justify-between items-center px-5 pt-3 pb-2'
|
||||
>
|
||||
<div>
|
||||
<Link href='/'>
|
||||
<a
|
||||
className='flex justify-center border-black border-2 bg-whitefont-semibold hover:bg-gray-800 hover:text-white p-2 duration-200
|
||||
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-100 dark:hover:text-black
|
||||
'>{BLOG.title}</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{/* 搜索框 */}
|
||||
<div className='px-4 flex w-20'>
|
||||
<i className='py-3 fa fa-search text-gray-400 absolute cursor-pointer px-2' />
|
||||
<input
|
||||
type='text'
|
||||
placeholder={currentTag ? `${locale.SEARCH.TAGS} #${currentTag}` : `${locale.SEARCH.ARTICLES}`}
|
||||
className={'transition duration-200 leading-10 pl-8 block border-gray-300 dark:border-gray-600 bg-white text-black dark:bg-gray-800 dark:text-white'}
|
||||
onKeyUp={handleKeyUp}
|
||||
onChange={e => setSearchValue(e.target.value)}
|
||||
defaultValue={router.query.s ?? ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-nowrap space-x-1'>
|
||||
<div onClick={handleMenuClick}
|
||||
className='p-2.5 cursor-pointer text-gray-600 bg-white hover:bg-gray-800 hover:text-white duration-200
|
||||
dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-100 dark:hover:text-black'>
|
||||
<div className={'fa' + (hiddenMenu ? ' fa-bars ' : ' fa-times')} />
|
||||
<span
|
||||
className='px-0.5'>{hiddenMenu ? '' : ''}</span>
|
||||
</div>
|
||||
<DarkModeButton/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TopNav
|
||||
36
components/Utterances.js
Normal file
36
components/Utterances.js
Normal file
@@ -0,0 +1,36 @@
|
||||
import BLOG from '@/blog.config'
|
||||
import { useEffect } from 'react'
|
||||
const Utterances = ({ issueTerm, layout }) => {
|
||||
useEffect(() => {
|
||||
const theme =
|
||||
BLOG.appearance === 'auto'
|
||||
? 'preferred-color-scheme'
|
||||
: BLOG.appearance === 'light'
|
||||
? 'github-light'
|
||||
: 'github-dark'
|
||||
const script = document.createElement('script')
|
||||
const anchor = document.getElementById('comments')
|
||||
script.setAttribute('src', 'https://utteranc.es/client.js')
|
||||
script.setAttribute('crossorigin', 'anonymous')
|
||||
script.setAttribute('async', true)
|
||||
script.setAttribute('repo', BLOG.comment.utterancesConfig.repo)
|
||||
script.setAttribute('issue-term', issueTerm)
|
||||
script.setAttribute('theme', theme)
|
||||
anchor.appendChild(script)
|
||||
return () => {
|
||||
anchor.innerHTML = ''
|
||||
}
|
||||
})
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
id="comments"
|
||||
className={layout && layout === 'fullWidth' ? '' : 'md:-ml-16'}
|
||||
>
|
||||
<div className="utterances-frame"></div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Utterances
|
||||
41
components/Vercel.js
Normal file
41
components/Vercel.js
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user