refactor: migrate to rsc and app dir

This commit is contained in:
Timothy Lin
2023-07-07 11:17:22 +08:00
parent a03d358ef9
commit 09ba0550ca
121 changed files with 13431 additions and 12945 deletions

View File

@ -2,7 +2,7 @@ import Image from './Image'
import Link from './Link'
const Card = ({ title, description, imgSrc, href }) => (
<div className="md p-4 md:w-1/2" style={{ maxWidth: '544px' }}>
<div className="md max-w-[544px] p-4 md:w-1/2">
<div
className={`${
imgSrc && 'h-full'

View File

@ -1,23 +0,0 @@
import { useEffect } from 'react'
import Router from 'next/router'
/**
* Client-side complement to next-remote-watch
* Re-triggers getStaticProps when watched mdx files change
*
*/
export const ClientReload = () => {
// Exclude socket.io from prod bundle
useEffect(() => {
import('socket.io-client').then((module) => {
const socket = module.io()
socket.on('reload', (data) => {
Router.replace(Router.asPath, undefined, {
scroll: false,
})
})
})
}, [])
return null
}

View File

@ -7,12 +7,12 @@ export default function Footer() {
<footer>
<div className="mt-16 flex flex-col items-center">
<div className="mb-3 flex space-x-4">
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size="6" />
<SocialIcon kind="github" href={siteMetadata.github} size="6" />
<SocialIcon kind="facebook" href={siteMetadata.facebook} size="6" />
<SocialIcon kind="youtube" href={siteMetadata.youtube} size="6" />
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size="6" />
<SocialIcon kind="twitter" href={siteMetadata.twitter} size="6" />
<SocialIcon kind="mail" href={`mailto:${siteMetadata.email}`} size={6} />
<SocialIcon kind="github" href={siteMetadata.github} size={6} />
<SocialIcon kind="facebook" href={siteMetadata.facebook} size={6} />
<SocialIcon kind="youtube" href={siteMetadata.youtube} size={6} />
<SocialIcon kind="linkedin" href={siteMetadata.linkedin} size={6} />
<SocialIcon kind="twitter" href={siteMetadata.twitter} size={6} />
</div>
<div className="mb-2 flex space-x-2 text-sm text-gray-500 dark:text-gray-400">
<div>{siteMetadata.author}</div>

46
components/Header.tsx Normal file
View File

@ -0,0 +1,46 @@
import siteMetadata from '@/data/siteMetadata'
import headerNavLinks from '@/data/headerNavLinks'
import Logo from '@/data/logo.svg'
import Link from './Link'
import MobileNav from './MobileNav'
import ThemeSwitch from './ThemeSwitch'
const Header = () => {
return (
<header className="flex items-center justify-between py-10">
<div>
<Link href="/" aria-label={siteMetadata.headerTitle}>
<div className="flex items-center justify-between">
<div className="mr-3">
<Logo />
</div>
{typeof siteMetadata.headerTitle === 'string' ? (
<div className="hidden h-6 text-2xl font-semibold sm:block">
{siteMetadata.headerTitle}
</div>
) : (
siteMetadata.headerTitle
)}
</div>
</Link>
</div>
<div className="flex items-center text-base leading-5">
<div className="hidden sm:block">
{headerNavLinks.map((link) => (
<Link
key={link.title}
href={link.href}
className="p-1 font-medium text-gray-900 dark:text-gray-100 sm:p-4"
>
{link.title}
</Link>
))}
</div>
<ThemeSwitch />
<MobileNav />
</div>
</header>
)
}
export default Header

View File

@ -1,6 +0,0 @@
import NextImage from 'next/image'
// eslint-disable-next-line jsx-a11y/alt-text
const Image = ({ ...rest }) => <NextImage {...rest} />
export default Image

5
components/Image.tsx Normal file
View File

@ -0,0 +1,5 @@
import NextImage, { ImageProps } from 'next/image'
const Image = ({ ...rest }: ImageProps) => <NextImage {...rest} />
export default Image

View File

@ -1,54 +0,0 @@
import siteMetadata from '@/data/siteMetadata'
import headerNavLinks from '@/data/headerNavLinks'
import Logo from '@/data/logo.svg'
import Link from './Link'
import SectionContainer from './SectionContainer'
import Footer from './Footer'
import MobileNav from './MobileNav'
import ThemeSwitch from './ThemeSwitch'
const LayoutWrapper = ({ children }) => {
return (
<SectionContainer>
<div className="flex h-screen flex-col justify-between">
<header className="flex items-center justify-between py-10">
<div>
<Link href="/" aria-label={siteMetadata.headerTitle}>
<div className="flex items-center justify-between">
<div className="mr-3">
<Logo />
</div>
{typeof siteMetadata.headerTitle === 'string' ? (
<div className="hidden h-6 text-2xl font-semibold sm:block">
{siteMetadata.headerTitle}
</div>
) : (
siteMetadata.headerTitle
)}
</div>
</Link>
</div>
<div className="flex items-center text-base leading-5">
<div className="hidden sm:block">
{headerNavLinks.map((link) => (
<Link
key={link.title}
href={link.href}
className="p-1 font-medium text-gray-900 dark:text-gray-100 sm:p-4"
>
{link.title}
</Link>
))}
</div>
<ThemeSwitch />
<MobileNav />
</div>
</header>
<main className="mb-auto">{children}</main>
<Footer />
</div>
</SectionContainer>
)
}
export default LayoutWrapper

View File

@ -0,0 +1,27 @@
import { Inter } from 'next/font/google'
import SectionContainer from './SectionContainer'
import Footer from './Footer'
import { ReactNode } from 'react'
import Header from './Header'
interface Props {
children: ReactNode
}
const inter = Inter({
subsets: ['latin'],
})
const LayoutWrapper = ({ children }: Props) => {
return (
<SectionContainer>
<div className={`${inter.className} flex h-screen flex-col justify-between font-sans`}>
<Header />
<main className="mb-auto">{children}</main>
<Footer />
</div>
</SectionContainer>
)
}
export default LayoutWrapper

View File

@ -1,16 +1,17 @@
/* eslint-disable jsx-a11y/anchor-has-content */
import Link from 'next/link'
import { AnchorHTMLAttributes, DetailedHTMLProps } from 'react'
const CustomLink = ({ href, ...rest }) => {
const CustomLink = ({
href,
...rest
}: DetailedHTMLProps<AnchorHTMLAttributes<HTMLAnchorElement>, HTMLAnchorElement>) => {
const isInternalLink = href && href.startsWith('/')
const isAnchorLink = href && href.startsWith('#')
if (isInternalLink) {
return (
<Link href={href}>
<a {...rest} />
</Link>
)
// @ts-ignore
return <Link href={href} {...rest} />
}
if (isAnchorLink) {

View File

@ -1,26 +0,0 @@
/* eslint-disable react/display-name */
import { useMemo } from 'react'
import { getMDXComponent } from 'mdx-bundler/client'
import Image from './Image'
import CustomLink from './Link'
import TOCInline from './TOCInline'
import Pre from './Pre'
import { BlogNewsletterForm } from './NewsletterForm'
export const MDXComponents = {
Image,
TOCInline,
a: CustomLink,
pre: Pre,
BlogNewsletterForm: BlogNewsletterForm,
wrapper: ({ components, layout, ...rest }) => {
const Layout = require(`../layouts/${layout}`).default
return <Layout {...rest} />
},
}
export const MDXLayoutRenderer = ({ layout, mdxSource, ...rest }) => {
const MDXLayout = useMemo(() => getMDXComponent(mdxSource), [mdxSource])
return <MDXLayout layout={layout} components={MDXComponents} {...rest} />
}

View File

@ -0,0 +1,23 @@
/* eslint-disable react/display-name */
import React from 'react'
import { MDXLayout, ComponentMap } from 'pliny/mdx-components'
import { TOCInline } from 'pliny/ui/TOCInline'
import { Pre } from 'pliny/ui/Pre'
import { BlogNewsletterForm } from 'pliny/ui/NewsletterForm'
import Image from './Image'
import CustomLink from './Link'
export const Wrapper = ({ layout, content, ...rest }: MDXLayout) => {
const Layout = require(`../layouts/${layout}`).default
return <Layout content={content} {...rest} />
}
export const MDXComponents: ComponentMap = {
Image,
TOCInline,
a: CustomLink,
pre: Pre,
wrapper: Wrapper,
BlogNewsletterForm,
}

View File

@ -1,3 +1,5 @@
'use client'
import { useState } from 'react'
import Link from './Link'
import headerNavLinks from '@/data/headerNavLinks'
@ -20,7 +22,6 @@ const MobileNav = () => {
return (
<div className="sm:hidden">
<button
type="button"
className="ml-1 mr-1 h-8 w-8 rounded py-1"
aria-label="Toggle Menu"
onClick={onToggleNav}
@ -39,13 +40,12 @@ const MobileNav = () => {
</svg>
</button>
<div
className={`fixed top-0 left-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
className={`fixed left-0 top-0 z-10 h-full w-full transform bg-gray-200 opacity-95 duration-300 ease-in-out dark:bg-gray-800 ${
navShow ? 'translate-x-0' : 'translate-x-full'
}`}
>
<div className="flex justify-end">
<button
type="button"
className="mr-5 mt-11 h-8 w-8 rounded"
aria-label="Toggle Menu"
onClick={onToggleNav}

View File

@ -1,84 +0,0 @@
import { useRef, useState } from 'react'
import siteMetadata from '@/data/siteMetadata'
const NewsletterForm = ({ title = 'Subscribe to the newsletter' }) => {
const inputEl = useRef(null)
const [error, setError] = useState(false)
const [message, setMessage] = useState('')
const [subscribed, setSubscribed] = useState(false)
const subscribe = async (e) => {
e.preventDefault()
const res = await fetch(`/api/${siteMetadata.newsletter.provider}`, {
body: JSON.stringify({
email: inputEl.current.value,
}),
headers: {
'Content-Type': 'application/json',
},
method: 'POST',
})
const { error } = await res.json()
if (error) {
setError(true)
setMessage('Your e-mail address is invalid or you are already subscribed!')
return
}
inputEl.current.value = ''
setError(false)
setSubscribed(true)
setMessage('Successfully! 🎉 You are now subscribed.')
}
return (
<div>
<div className="pb-1 text-lg font-semibold text-gray-800 dark:text-gray-100">{title}</div>
<form className="flex flex-col sm:flex-row" onSubmit={subscribe}>
<div>
<label className="sr-only" htmlFor="email-input">
Email address
</label>
<input
autoComplete="email"
className="w-72 rounded-md px-4 focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary-600 dark:bg-black"
id="email-input"
name="email"
placeholder={subscribed ? "You're subscribed ! 🎉" : 'Enter your email'}
ref={inputEl}
required
type="email"
disabled={subscribed}
/>
</div>
<div className="mt-2 flex w-full rounded-md shadow-sm sm:mt-0 sm:ml-3">
<button
className={`w-full rounded-md bg-primary-500 py-2 px-4 font-medium text-white sm:py-0 ${
subscribed ? 'cursor-default' : 'hover:bg-primary-700 dark:hover:bg-primary-400'
} focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2 dark:ring-offset-black`}
type="submit"
disabled={subscribed}
>
{subscribed ? 'Thank you!' : 'Sign up'}
</button>
</div>
</form>
{error && (
<div className="w-72 pt-2 text-sm text-red-500 dark:text-red-400 sm:w-96">{message}</div>
)}
</div>
)
}
export default NewsletterForm
export const BlogNewsletterForm = ({ title }) => (
<div className="flex items-center justify-center">
<div className="bg-gray-100 p-6 dark:bg-gray-800 sm:px-14 sm:py-8">
<NewsletterForm title={title} />
</div>
</div>
)

View File

@ -1,4 +1,10 @@
export default function PageTitle({ children }) {
import { ReactNode } from 'react'
interface Props {
children: ReactNode
}
export default function PageTitle({ children }: Props) {
return (
<h1 className="text-3xl font-extrabold leading-9 tracking-tight text-gray-900 dark:text-gray-100 sm:text-4xl sm:leading-10 md:text-5xl md:leading-14">
{children}

View File

@ -1,36 +0,0 @@
import Link from '@/components/Link'
export default function Pagination({ totalPages, currentPage }) {
const prevPage = parseInt(currentPage) - 1 > 0
const nextPage = parseInt(currentPage) + 1 <= parseInt(totalPages)
return (
<div className="space-y-2 pt-6 pb-8 md:space-y-5">
<nav className="flex justify-between">
{!prevPage && (
<button rel="previous" className="cursor-auto disabled:opacity-50" disabled={!prevPage}>
Previous
</button>
)}
{prevPage && (
<Link href={currentPage - 1 === 1 ? `/blog/` : `/blog/page/${currentPage - 1}`}>
<button rel="previous">Previous</button>
</Link>
)}
<span>
{currentPage} of {totalPages}
</span>
{!nextPage && (
<button rel="next" className="cursor-auto disabled:opacity-50" disabled={!nextPage}>
Next
</button>
)}
{nextPage && (
<Link href={`/blog/page/${currentPage + 1}`}>
<button rel="next">Next</button>
</Link>
)}
</nav>
</div>
)
}

View File

@ -1,71 +0,0 @@
import { useState, useRef } from 'react'
const Pre = (props) => {
const textInput = useRef(null)
const [hovered, setHovered] = useState(false)
const [copied, setCopied] = useState(false)
const onEnter = () => {
setHovered(true)
}
const onExit = () => {
setHovered(false)
setCopied(false)
}
const onCopy = () => {
setCopied(true)
navigator.clipboard.writeText(textInput.current.textContent)
setTimeout(() => {
setCopied(false)
}, 2000)
}
return (
<div ref={textInput} onMouseEnter={onEnter} onMouseLeave={onExit} className="relative">
{hovered && (
<button
aria-label="Copy code"
type="button"
className={`absolute right-2 top-2 h-8 w-8 rounded border-2 bg-gray-700 p-1 dark:bg-gray-800 ${
copied
? 'border-green-400 focus:border-green-400 focus:outline-none'
: 'border-gray-300'
}`}
onClick={onCopy}
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
className={copied ? 'text-green-400' : 'text-gray-300'}
>
{copied ? (
<>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
/>
</>
) : (
<>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
/>
</>
)}
</svg>
</button>
)}
<pre>{props.children}</pre>
</div>
)
}
export default Pre

View File

@ -1,8 +1,30 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import siteMetadata from '@/data/siteMetadata'
import { CoreContent } from 'pliny/utils/contentlayer'
import type { Blog, Authors } from 'contentlayer/generated'
interface CommonSEOProps {
title: string
description: string
ogType: string
ogImage:
| string
| {
'@type': string
url: string
}[]
twImage: string
canonicalUrl?: string
}
const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl }) => {
const CommonSEO = ({
title,
description,
ogType,
ogImage,
twImage,
canonicalUrl,
}: CommonSEOProps) => {
const router = useRouter()
return (
<Head>
@ -14,7 +36,7 @@ const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl
<meta property="og:site_name" content={siteMetadata.title} />
<meta property="og:description" content={description} />
<meta property="og:title" content={title} />
{ogImage.constructor.name === 'Array' ? (
{Array.isArray(ogImage) ? (
ogImage.map(({ url }) => <meta property="og:image" content={url} key={url} />)
) : (
<meta property="og:image" content={ogImage} key={ogImage} />
@ -32,7 +54,12 @@ const CommonSEO = ({ title, description, ogType, ogImage, twImage, canonicalUrl
)
}
export const PageSEO = ({ title, description }) => {
interface PageSEOProps {
title: string
description: string
}
export const PageSEO = ({ title, description }: PageSEOProps) => {
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
return (
@ -46,7 +73,7 @@ export const PageSEO = ({ title, description }) => {
)
}
export const TagSEO = ({ title, description }) => {
export const TagSEO = ({ title, description }: PageSEOProps) => {
const ogImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const twImageUrl = siteMetadata.siteUrl + siteMetadata.socialBanner
const router = useRouter()
@ -71,6 +98,11 @@ export const TagSEO = ({ title, description }) => {
)
}
interface BlogSeoProps extends CoreContent<Blog> {
authorDetails?: CoreContent<Authors>[]
url: string
}
export const BlogSEO = ({
authorDetails,
title,
@ -80,11 +112,10 @@ export const BlogSEO = ({
url,
images = [],
canonicalUrl,
}) => {
const router = useRouter()
}: BlogSeoProps) => {
const publishedAt = new Date(date).toISOString()
const modifiedAt = new Date(lastmod || date).toISOString()
let imagesArr =
const imagesArr =
images.length === 0
? [siteMetadata.socialBanner]
: typeof images === 'string'
@ -142,7 +173,7 @@ export const BlogSEO = ({
<>
<CommonSEO
title={title}
description={summary}
description={summary || ''}
ogType="article"
ogImage={featuredImages}
twImage={twImageUrl}

View File

@ -18,16 +18,15 @@ const ScrollTopAndComment = () => {
window.scrollTo({ top: 0 })
}
const handleScrollToComment = () => {
document.getElementById('comment').scrollIntoView()
document.getElementById('comment')?.scrollIntoView()
}
return (
<div
className={`fixed right-8 bottom-8 hidden flex-col gap-3 ${show ? 'md:flex' : 'md:hidden'}`}
className={`fixed bottom-8 right-8 hidden flex-col gap-3 ${show ? 'md:flex' : 'md:hidden'}`}
>
{siteMetadata.comment.provider && (
{siteMetadata.comments?.provider && (
<button
aria-label="Scroll To Comment"
type="button"
onClick={handleScrollToComment}
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
>
@ -42,7 +41,6 @@ const ScrollTopAndComment = () => {
)}
<button
aria-label="Scroll To Top"
type="button"
onClick={handleScrollTop}
className="rounded-full bg-gray-200 p-2 text-gray-500 transition-all hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-400 dark:hover:bg-gray-600"
>

View File

@ -1,3 +0,0 @@
export default function SectionContainer({ children }) {
return <div className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</div>
}

View File

@ -0,0 +1,11 @@
import { ReactNode } from 'react'
interface Props {
children: ReactNode
}
export default function SectionContainer({ children }: Props) {
return (
<section className="mx-auto max-w-3xl px-4 sm:px-6 xl:max-w-5xl xl:px-0">{children}</section>
)
}

View File

@ -1,64 +0,0 @@
/**
* @typedef TocHeading
* @prop {string} value
* @prop {number} depth
* @prop {string} url
*/
/**
* Generates an inline table of contents
* Exclude titles matching this string (new RegExp('^(' + string + ')$', 'i')).
* If an array is passed the array gets joined with a pipe (new RegExp('^(' + array.join('|') + ')$', 'i')).
*
* @param {{
* toc: TocHeading[],
* indentDepth?: number,
* fromHeading?: number,
* toHeading?: number,
* asDisclosure?: boolean,
* exclude?: string|string[]
* }} props
*
*/
const TOCInline = ({
toc,
indentDepth = 3,
fromHeading = 1,
toHeading = 6,
asDisclosure = false,
exclude = '',
}) => {
const re = Array.isArray(exclude)
? new RegExp('^(' + exclude.join('|') + ')$', 'i')
: new RegExp('^(' + exclude + ')$', 'i')
const filteredToc = toc.filter(
(heading) =>
heading.depth >= fromHeading && heading.depth <= toHeading && !re.test(heading.value)
)
const tocList = (
<ul>
{filteredToc.map((heading) => (
<li key={heading.value} className={`${heading.depth >= indentDepth && 'ml-6'}`}>
<a href={heading.url}>{heading.value}</a>
</li>
))}
</ul>
)
return (
<>
{asDisclosure ? (
<details open>
<summary className="ml-6 pt-2 pb-2 text-xl font-bold">Table of Contents</summary>
<div className="ml-6">{tocList}</div>
</details>
) : (
tocList
)}
</>
)
}
export default TOCInline

View File

@ -1,14 +0,0 @@
import Link from 'next/link'
import kebabCase from '@/lib/utils/kebabCase'
const Tag = ({ text }) => {
return (
<Link href={`/tags/${kebabCase(text)}`}>
<a className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400">
{text.split(' ').join('-')}
</a>
</Link>
)
}
export default Tag

19
components/Tag.tsx Normal file
View File

@ -0,0 +1,19 @@
import Link from 'next/link'
import { kebabCase } from 'pliny/utils/kebabCase'
interface Props {
text: string
}
const Tag = ({ text }: Props) => {
return (
<Link
href={`/tags/${kebabCase(text)}`}
className="mr-3 text-sm font-medium uppercase text-primary-500 hover:text-primary-600 dark:hover:text-primary-400"
>
{text.split(' ').join('-')}
</Link>
)
}
export default Tag

View File

@ -1,19 +1,24 @@
'use client'
import { useEffect, useState } from 'react'
import { useTheme } from 'next-themes'
const ThemeSwitch = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme, resolvedTheme } = useTheme()
const { theme, setTheme } = useTheme()
// When mounted on client, now we can show the UI
useEffect(() => setMounted(true), [])
if (!mounted) {
return null
}
return (
<button
aria-label="Toggle Dark Mode"
type="button"
className="ml-1 mr-1 h-8 w-8 rounded p-1 sm:ml-4"
onClick={() => setTheme(theme === 'dark' || resolvedTheme === 'dark' ? 'light' : 'dark')}
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
>
<svg
xmlns="http://www.w3.org/2000/svg"
@ -21,7 +26,7 @@ const ThemeSwitch = () => {
fill="currentColor"
className="text-gray-900 dark:text-gray-100"
>
{mounted && (theme === 'dark' || resolvedTheme === 'dark') ? (
{mounted && theme === 'dark' ? (
<path
fillRule="evenodd"
d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z"

View File

@ -1,36 +0,0 @@
import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'
const GAScript = () => {
return (
<>
<Script
strategy="lazyOnload"
src={`https://www.googletagmanager.com/gtag/js?id=${siteMetadata.analytics.googleAnalyticsId}`}
/>
<Script strategy="lazyOnload" id="ga-script">
{`
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${siteMetadata.analytics.googleAnalyticsId}', {
page_path: window.location.pathname,
});
`}
</Script>
</>
)
}
export default GAScript
// https://developers.google.com/analytics/devguides/collection/gtagjs/events
export const logEvent = (action, category, label, value) => {
window.gtag?.('event', action, {
event_category: category,
event_label: label,
value: value,
})
}

View File

@ -1,27 +0,0 @@
import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'
const PlausibleScript = () => {
return (
<>
<Script
strategy="lazyOnload"
data-domain={siteMetadata.analytics.plausibleDataDomain}
src="https://plausible.io/js/plausible.js"
/>
<Script strategy="lazyOnload" id="plausible-script">
{`
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }
`}
</Script>
</>
)
}
export default PlausibleScript
// https://plausible.io/docs/custom-event-goals
export const logEvent = (eventName, ...rest) => {
return window.plausible?.(eventName, ...rest)
}

View File

@ -1,18 +0,0 @@
import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'
const PosthogScript = () => {
return (
<>
<Script strategy="lazyOnload" id="posthog-script">
{`
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
posthog.init('${siteMetadata.analytics.posthogAnalyticsId}',{api_host:'https://app.posthog.com'})
`}
</Script>
</>
)
}
export default PosthogScript

View File

@ -1,25 +0,0 @@
import Script from 'next/script'
const SimpleAnalyticsScript = () => {
return (
<>
<Script strategy="lazyOnload" id="sa-script">
{`
window.sa_event=window.sa_event||function(){var a=[].slice.call(arguments);window.sa_event.q?window.sa_event.q.push(a):window.sa_event.q=[a]};
`}
</Script>
<Script strategy="lazyOnload" src="https://scripts.simpleanalyticscdn.com/latest.js" />
</>
)
}
// https://docs.simpleanalytics.com/events
export const logEvent = (eventName, callback) => {
if (callback) {
return window.sa_event?.(eventName, callback)
} else {
return window.sa_event?.(eventName)
}
}
export default SimpleAnalyticsScript

View File

@ -1,18 +0,0 @@
import Script from 'next/script'
import siteMetadata from '@/data/siteMetadata'
const UmamiScript = () => {
return (
<>
<Script
async
defer
data-website-id={siteMetadata.analytics.umamiWebsiteId}
src="https://umami.example.com/umami.js" // Replace with your umami instance
/>
</>
)
}
export default UmamiScript

View File

@ -1,22 +0,0 @@
import GA from './GoogleAnalytics'
import Plausible from './Plausible'
import SimpleAnalytics from './SimpleAnalytics'
import Umami from './Umami'
import Posthog from './Posthog'
import siteMetadata from '@/data/siteMetadata'
const isProduction = process.env.NODE_ENV === 'production'
const Analytics = () => {
return (
<>
{isProduction && siteMetadata.analytics.plausibleDataDomain && <Plausible />}
{isProduction && siteMetadata.analytics.simpleAnalytics && <SimpleAnalytics />}
{isProduction && siteMetadata.analytics.umamiWebsiteId && <Umami />}
{isProduction && siteMetadata.analytics.googleAnalyticsId && <GA />}
{isProduction && siteMetadata.analytics.posthogAnalyticsId && <Posthog />}
</>
)
}
export default Analytics

View File

@ -1,37 +0,0 @@
import React, { useState } from 'react'
import siteMetadata from '@/data/siteMetadata'
const Disqus = ({ frontMatter }) => {
const [enableLoadComments, setEnabledLoadComments] = useState(true)
const COMMENTS_ID = 'disqus_thread'
function LoadComments() {
setEnabledLoadComments(false)
window.disqus_config = function () {
this.page.url = window.location.href
this.page.identifier = frontMatter.slug
}
if (window.DISQUS === undefined) {
const script = document.createElement('script')
script.src = 'https://' + siteMetadata.comment.disqusConfig.shortname + '.disqus.com/embed.js'
script.setAttribute('data-timestamp', +new Date())
script.setAttribute('crossorigin', 'anonymous')
script.async = true
document.body.appendChild(script)
} else {
window.DISQUS.reset({ reload: true })
}
}
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<div className="disqus-frame" id={COMMENTS_ID} />
</div>
)
}
export default Disqus

View File

@ -1,72 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useTheme } from 'next-themes'
import siteMetadata from '@/data/siteMetadata'
const Giscus = () => {
const [enableLoadComments, setEnabledLoadComments] = useState(true)
const { theme, resolvedTheme } = useTheme()
const commentsTheme =
siteMetadata.comment.giscusConfig.themeURL === ''
? theme === 'dark' || resolvedTheme === 'dark'
? siteMetadata.comment.giscusConfig.darkTheme
: siteMetadata.comment.giscusConfig.theme
: siteMetadata.comment.giscusConfig.themeURL
const COMMENTS_ID = 'comments-container'
const LoadComments = useCallback(() => {
setEnabledLoadComments(false)
const {
repo,
repositoryId,
category,
categoryId,
mapping,
reactions,
metadata,
inputPosition,
lang,
} = siteMetadata?.comment?.giscusConfig
const script = document.createElement('script')
script.src = 'https://giscus.app/client.js'
script.setAttribute('data-repo', repo)
script.setAttribute('data-repo-id', repositoryId)
script.setAttribute('data-category', category)
script.setAttribute('data-category-id', categoryId)
script.setAttribute('data-mapping', mapping)
script.setAttribute('data-reactions-enabled', reactions)
script.setAttribute('data-emit-metadata', metadata)
script.setAttribute('data-input-position', inputPosition)
script.setAttribute('data-lang', lang)
script.setAttribute('data-theme', commentsTheme)
script.setAttribute('crossorigin', 'anonymous')
script.async = true
const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.appendChild(script)
return () => {
const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.innerHTML = ''
}
}, [commentsTheme])
// Reload on theme change
useEffect(() => {
const iframe = document.querySelector('iframe.giscus-frame')
if (!iframe) return
LoadComments()
}, [LoadComments])
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<div className="giscus" id={COMMENTS_ID} />
</div>
)
}
export default Giscus

View File

@ -1,52 +0,0 @@
import React, { useState, useEffect, useCallback } from 'react'
import { useTheme } from 'next-themes'
import siteMetadata from '@/data/siteMetadata'
const Utterances = () => {
const [enableLoadComments, setEnabledLoadComments] = useState(true)
const { theme, resolvedTheme } = useTheme()
const commentsTheme =
theme === 'dark' || resolvedTheme === 'dark'
? siteMetadata.comment.utterancesConfig.darkTheme
: siteMetadata.comment.utterancesConfig.theme
const COMMENTS_ID = 'comments-container'
const LoadComments = useCallback(() => {
setEnabledLoadComments(false)
const script = document.createElement('script')
script.src = 'https://utteranc.es/client.js'
script.setAttribute('repo', siteMetadata.comment.utterancesConfig.repo)
script.setAttribute('issue-term', siteMetadata.comment.utterancesConfig.issueTerm)
script.setAttribute('label', siteMetadata.comment.utterancesConfig.label)
script.setAttribute('theme', commentsTheme)
script.setAttribute('crossorigin', 'anonymous')
script.async = true
const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.appendChild(script)
return () => {
const comments = document.getElementById(COMMENTS_ID)
if (comments) comments.innerHTML = ''
}
}, [commentsTheme])
// Reload on theme change
useEffect(() => {
const iframe = document.querySelector('iframe.utterances-frame')
if (!iframe) return
LoadComments()
}, [LoadComments])
// Added `relative` to fix a weird bug with `utterances-frame` position
return (
<div className="pt-6 pb-6 text-center text-gray-700 dark:text-gray-300">
{enableLoadComments && <button onClick={LoadComments}>Load Comments</button>}
<div className="utterances-frame relative" id={COMMENTS_ID} />
</div>
)
}
export default Utterances

View File

@ -1,39 +0,0 @@
import siteMetadata from '@/data/siteMetadata'
import dynamic from 'next/dynamic'
const UtterancesComponent = dynamic(
() => {
return import('@/components/comments/Utterances')
},
{ ssr: false }
)
const GiscusComponent = dynamic(
() => {
return import('@/components/comments/Giscus')
},
{ ssr: false }
)
const DisqusComponent = dynamic(
() => {
return import('@/components/comments/Disqus')
},
{ ssr: false }
)
const Comments = ({ frontMatter }) => {
const comment = siteMetadata?.comment
if (!comment || Object.keys(comment).length === 0) return <></>
return (
<div id="comment">
{siteMetadata.comment && siteMetadata.comment.provider === 'giscus' && <GiscusComponent />}
{siteMetadata.comment && siteMetadata.comment.provider === 'utterances' && (
<UtterancesComponent />
)}
{siteMetadata.comment && siteMetadata.comment.provider === 'disqus' && (
<DisqusComponent frontMatter={frontMatter} />
)}
</div>
)
}
export default Comments