feat(blog): migrate to nextJS

This commit is contained in:
Kim, Jimin 2023-07-28 15:43:29 +09:00
parent 62b34c9c48
commit badaa09950
Signed by: pomp
GPG key ID: CE1DDB8A4A765403
38 changed files with 2226 additions and 445 deletions

View file

@ -1,14 +1,7 @@
module.exports = { module.exports = {
root: true, root: true,
env: { browser: true, es2020: true }, extends: ["next/core-web-vitals", "@developomp-site/eslint-config"],
extends: [
"plugin:react-hooks/recommended",
"@developomp-site/eslint-config",
],
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
plugins: ["react-refresh"],
rules: { rules: {
"react-refresh/only-export-components": "warn",
"react-hooks/exhaustive-deps": "off", "react-hooks/exhaustive-deps": "off",
}, },
} }

35
apps/blog/.gitignore vendored Normal file
View file

@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

View file

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en" class="dark">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="developomp's Blog" />
<title>pomp's blog</title>
<!-- OpenGraph -->
<meta property="og:title" content="pomp's blog" />
<meta property="og:site_name" content="developomp's Blog" />
<meta property="og:description" content="developomp's Blog" />
<meta property="og:type" content="website" />
<meta property="og:url" content="https://blog.developomp.com" />
<meta
property="og:image"
content="https://blog.developomp.com/favicon.svg"
/>
</head>
<body class="overflow-x-hidden overflow-y-scroll">
<noscript>
<figure>
<img src="/img/nojs.avif" alt="No javascript?" />
<figcaption>
Image compressed down to 4.5kB because you probably have
potato internet :D
</figcaption>
</figure>
</noscript>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

8
apps/blog/next.config.js Normal file
View file

@ -0,0 +1,8 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
distDir: "build",
images: { unoptimized: true },
}
module.exports = nextConfig

View file

@ -2,49 +2,46 @@
"name": "@developomp-site/blog", "name": "@developomp-site/blog",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "open-cli http://localhost:3000 && next dev",
"build": "vite build", "build": "next build",
"lint": "eslint .", "lint": "next lint",
"preview": "vite preview",
"clean": "rm -rf .turbo build node_modules" "clean": "rm -rf .turbo build node_modules"
}, },
"dependencies": { "devDependencies": {
"@developomp-site/content": "workspace:*", "@developomp-site/content": "workspace:*",
"@developomp-site/eslint-config": "workspace:*",
"@developomp-site/prettier-config": "workspace:*",
"@developomp-site/tailwind-config": "workspace:*",
"@fontsource/noto-sans-kr": "^5.0.5", "@fontsource/noto-sans-kr": "^5.0.5",
"@fontsource/source-code-pro": "^5.0.5", "@fontsource/source-code-pro": "^5.0.5",
"@fortawesome/free-brands-svg-icons": "^6.4.0", "@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@kunukn/react-collapse": "^2.2.10", "@kunukn/react-collapse": "^2.2.10",
"highlight.js": "^11.8.0",
"hoofd": "^1.7.0",
"katex": "^0.16.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"wouter": "^2.11.0",
"zustand": "^4.3.9"
},
"devDependencies": {
"@developomp-site/eslint-config": "workspace:*",
"@developomp-site/prettier-config": "workspace:*",
"@developomp-site/tailwind-config": "workspace:*",
"@types/highlight.js": "^10.1.0", "@types/highlight.js": "^10.1.0",
"@types/katex": "^0.16.0", "@types/katex": "^0.16.2",
"@types/react": "^18.2.14", "@types/node": "20.4.5",
"@types/react": "18.2.17",
"@types/react-collapse": "^5.0.1", "@types/react-collapse": "^5.0.1",
"@types/react-dom": "^18.2.6", "@types/react-dom": "18.2.7",
"@typescript-eslint/eslint-plugin": "^5.61.0", "@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^5.61.0", "@typescript-eslint/parser": "^6.2.0",
"@vitejs/plugin-react": "^4.0.2", "autoprefixer": "10.4.14",
"eslint": "^8.44.0", "eslint": "8.45.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-config-next": "13.4.12",
"eslint-plugin-react-refresh": "^0.4.2", "eslint-plugin-prettier": "^4.2.1",
"prettier-plugin-tailwindcss": "^0.3.0", "highlight.js": "^11.8.0",
"tailwindcss": "^3.3.2", "katex": "^0.16.8",
"typescript": "^5.1.6", "next": "13.4.12",
"vite": "^4.4.2", "open-cli": "^7.2.0",
"vite-plugin-dynamic-import": "^1.5.0" "postcss": "8.4.27",
"prettier": "^2.8.8",
"prettier-plugin-tailwindcss": "^0.4.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.1.6",
"zustand": "^4.3.9"
} }
} }

View file

@ -1,4 +1,4 @@
export default { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {},

View file

@ -1,41 +0,0 @@
import { useTitleTemplate } from "hoofd"
import { Route, Switch } from "wouter"
import Footer from "@/components/Footer"
import Header from "@/components/Header"
import Loading from "@/components/Loading"
import Home from "@/pages/Home"
import NotFound from "@/pages/NotFound"
import Page from "@/pages/Page"
function App() {
useTitleTemplate("pomp's blog | %s")
return (
<>
<Header />
<main className="mx-auto mb-8 mt-20 flex w-full max-w-screen-mobile grow flex-col items-center gap-8 px-4">
<Switch>
<Route path="/">
<Home />
</Route>
{/* <Route path="/search">
<Search />
</Route> */}
<Route path="/404">
<NotFound />
</Route>
<Route path="/loading">
<Loading />
</Route>
<Route>
<Page />
</Route>
</Switch>
</main>
<Footer />
</>
)
}
export default App

View file

@ -1,19 +1,17 @@
import { useTitle } from "hoofd" "use client"
import { type ReactNode, useEffect, useState } from "react" import { type ReactNode, useEffect, useState } from "react"
import PostCard from "@/components/PostCard" import PostCard from "@/components/PostCard"
import ShowMoreButton from "@/components/ShowMoreButton"
import contentMap from "@/contentMap" import contentMap from "@/contentMap"
import ShowMoreButton from "./ShowMoreButton"
const totalPosts = Object.keys(contentMap.posts).length const totalPosts = Object.keys(contentMap.posts).length
export default function Home() { export default function Home() {
const [howMany, setHowMany] = useState(5) const [howMany, setHowMany] = useState(5)
const [postCards, setPostCards] = useState<ReactNode[]>([]) const [postCards, setPostCards] = useState<ReactNode[]>([])
useTitle("Home")
useEffect(() => { useEffect(() => {
const postCards: ReactNode[] = [] const postCards: ReactNode[] = []

View file

@ -4,7 +4,7 @@ import {
faListUl, faListUl,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { Link } from "wouter" import Link from "next/link"
interface Props { interface Props {
seriesHome: string seriesHome: string
@ -20,7 +20,7 @@ export default function SeriesControlButtons({
return ( return (
<div className="mb-5 flex justify-between"> <div className="mb-5 flex justify-between">
{prevURL ? ( {prevURL ? (
<Link to={prevURL}> <Link href={prevURL}>
<button className="button"> <button className="button">
<FontAwesomeIcon icon={faArrowLeft} /> <FontAwesomeIcon icon={faArrowLeft} />
</button> </button>
@ -31,14 +31,14 @@ export default function SeriesControlButtons({
</button> </button>
)} )}
<Link to={seriesHome}> <Link href={seriesHome}>
<button className="button"> <button className="button">
<FontAwesomeIcon icon={faListUl} /> <FontAwesomeIcon icon={faListUl} />
</button> </button>
</Link> </Link>
{nextURL ? ( {nextURL ? (
<Link to={nextURL}> <Link href={nextURL}>
<button className="button"> <button className="button">
<FontAwesomeIcon icon={faArrowRight} /> <FontAwesomeIcon icon={faArrowRight} />
</button> </button>

View file

@ -1,3 +1,5 @@
"use client"
import "./Toc.scss" import "./Toc.scss"
import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons" import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons"
@ -10,8 +12,9 @@ interface Props {
} }
export default function Toc({ data }: Props) { export default function Toc({ data }: Props) {
const [isTocOpened, setIsTocOpened] = useState( const [isTocOpened, setIsTocOpened] = useState<boolean>(
localStorage.getItem("isTocOpened") === "true" typeof window !== "undefined" &&
localStorage.getItem("isTocOpened") === "true"
) )
useEffect(() => { useEffect(() => {

View file

@ -2,27 +2,47 @@ import type { PageData } from "@developomp-site/content/src/types/types"
import contentMap from "@/contentMap" import contentMap from "@/contentMap"
import { Params } from "./page"
export enum PageType { export enum PageType {
POST, POST,
SERIES, SERIES,
SERIES_HOME, SERIES_HOME,
} }
export async function fetchContent(content_id: string) { export interface Data {
pageData: PageData
pageType: PageType
}
export async function getData(params: Params): Promise<Data> {
const contentID = `/${params.category}/${params.slug.join("/")}`
const content = await fetchContent(contentID)
const pageType = categorizePageType(contentID) || PageType.POST
const pageData = parsePageData(content, pageType, contentID)
return {
pageData,
pageType,
}
}
export async function fetchContent(contentID: string) {
try { try {
return await import( return await import(
`@developomp-site/content/dist/content${content_id}.json` `@developomp-site/content/dist/content${contentID}.json`
) )
} catch (err) { } catch (err) {
return return
} }
} }
export function categorizePageType(content_id: string): PageType | undefined { export function categorizePageType(contentID: string): PageType | undefined {
if (content_id.startsWith("/post")) return PageType.POST if (contentID.startsWith("/post")) return PageType.POST
if (content_id.startsWith("/series")) { if (contentID.startsWith("/series")) {
// if the URL looks like /series/series-title (if the url has two slashes) // if the URL looks like /series/series-title (if the url has two slashes)
if ([...(content_id.match(/\//g) || [])].length == 2) if ([...(contentID.match(/\//g) || [])].length == 2)
return PageType.SERIES_HOME return PageType.SERIES_HOME
// if the URL looks like /series/series-title/post-title (if the url does not have 2 slashes) // if the URL looks like /series/series-title/post-title (if the url does not have 2 slashes)

View file

@ -1,61 +1,66 @@
import "./Page.scss" import "./Page.scss"
import type { PageData } from "@developomp-site/content/src/types/types" import { type Metadata } from "next"
import { useMeta, useTitle } from "hoofd" import { type ParsedUrlQuery } from "querystring"
import { useEffect, useState } from "react"
import { useLocation } from "wouter"
import NotFound, { metadata as notFoundMetadata } from "@/app/not-found"
import Card from "@/components/Card" import Card from "@/components/Card"
import Loading from "@/components/Loading"
import PostCard from "@/components/PostCard" import PostCard from "@/components/PostCard"
import Tag from "@/components/Tag" import Tag from "@/components/Tag"
import TagList from "@/components/TagList" import TagList from "@/components/TagList"
import contentMap from "@/contentMap" import contentMap from "@/contentMap"
import NotFound from "../NotFound" import { getData, PageType } from "./helper"
import {
categorizePageType,
fetchContent,
PageType,
parsePageData,
} from "./helper"
import Meta from "./Meta" import Meta from "./Meta"
import SeriesControlButtons from "./SeriesControlButtons" import SeriesControlButtons from "./SeriesControlButtons"
import Toc from "./Toc" import Toc from "./Toc"
export default function Page() { export interface Params extends ParsedUrlQuery {
const [location] = useLocation() category: "posts" | "series"
const [pageData, setPageData] = useState<PageData | undefined>(undefined) slug: string[]
const [pageType, setPageType] = useState<PageType>(PageType.POST) }
const [isLoading, setLoading] = useState(true)
useTitle(pageData?.title || "Loading") interface Props {
useMeta({ property: "og:title", content: pageData?.title }) params: Params
}
useEffect(() => { export async function generateStaticParams(): Promise<Params[]> {
setPageData(undefined) return Object.keys(contentMap.posts).map((key) => {
setLoading(true) const contentID = key.replace(/\/$/, "") // remove trailing slash
const parts = contentID
.split("/") // /a/b/c/ => ['', 'a', 'b', 'c', '']
.filter((x) => x) // ['', 'a', 'b', 'c', ''] => ['a', 'b', 'c']
const content_id = location.replace(/\/$/, "") // remove trailing slash const category = parts[0]
if (category !== "posts" && category !== "series")
throw "Invalid Page Type"
fetchContent(content_id).then((fetched_content) => { const slug = parts.slice(1) // ['a', 'b', 'c'] => ['b', 'c']
const pageType = categorizePageType(content_id)
// stop loading without setting pageData so 404 page will display return { category, slug }
if (!fetched_content || pageType === undefined) { })
setLoading(false) }
return
}
setPageData(parsePageData(fetched_content, pageType, content_id)) export async function generateMetadata({ params }: Props): Promise<Metadata> {
setPageType(pageType) if (params.category != "posts" && params.category != "series")
setLoading(false) return notFoundMetadata
})
}, [location])
if (isLoading) return <Loading /> const { pageData } = await getData(params)
if (!pageData) return <NotFound /> return {
metadataBase: new URL("https://blog.developomp.com"),
title: pageData.title,
openGraph: {
title: pageData.title,
},
}
}
export default async function Page({ params }: Props) {
if (params.category != "posts" && params.category != "series")
return <NotFound />
const { pageData, pageType } = await getData(params)
return ( return (
<> <>

View file

@ -0,0 +1,84 @@
import "@fortawesome/fontawesome-svg-core/styles.css"
import "@fontsource/noto-sans-kr/400.css"
import "@fontsource/noto-sans-kr/700.css"
import "@fontsource/source-code-pro"
import "katex/dist/katex.min.css"
import "./globals.css"
import "../styles/global.scss"
import "../styles/anchor.scss"
import "../styles/blockQuote.scss"
import "../styles/button.scss"
import "../styles/callout.scss"
import "../styles/checkbox.scss"
import "../styles/code.scss"
import "../styles/colorChip.scss"
import "../styles/heading.scss"
import "../styles/hr.scss"
import "../styles/img.scss"
import "../styles/katex.scss"
import "../styles/kbd.scss"
import "../styles/list.scss"
import "../styles/mark.scss"
import "../styles/scrollbar.scss"
import "../styles/subSup.scss"
import "../styles/table.scss"
import { type Metadata } from "next"
import Image from "next/image"
import Footer from "@/components/Footer"
import Header from "@/components/Header"
export const metadata: Metadata = {
metadataBase: new URL("https://blog.developomp.com"),
title: {
template: "pomp's blog | %s",
default: "",
},
description: "developomp's Blog",
openGraph: {
title: "pomp's blog",
siteName: "developomp's Blog",
description: "developomp's Blog",
type: "website",
url: "https://blog.developomp.com",
images: "https://blog.developomp.com/favicon.svg",
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className="dark">
<head>
<link
rel="shortcut icon"
type="image/svg+xml"
href="favicon.svg"
/>
<meta name="theme-color" content="#000000" />
</head>
<body className="overflow-x-hidden overflow-y-scroll">
<noscript>
<figure>
<Image src="/img/nojs.avif" alt="No javascript?" />
<figcaption>
Image compressed down to 4.5kB because you probably
have potato internet :D
</figcaption>
</figure>
</noscript>
<Header />
<main className="mx-auto mb-8 mt-20 flex w-full max-w-screen-mobile grow flex-col items-center gap-8 px-4">
{children}
</main>
<Footer />
</body>
</html>
)
}

View file

@ -1,11 +1,16 @@
import { useMeta, useTitle } from "hoofd" import { type Metadata } from "next"
import Card from "@/components/Card" import Card from "@/components/Card"
export default function NotFound() { export const metadata: Metadata = {
useTitle("404") metadataBase: new URL("https://blog.developomp.com"),
useMeta({ property: "og:title", content: "pomp's blog | Page Not Found" }) title: "404",
openGraph: {
title: "pomp's blog | Page Not Found",
},
}
export default function NotFound() {
return ( return (
<Card className="items-center gap-4"> <Card className="items-center gap-4">
<h1 className="text-7xl">404</h1> <h1 className="text-7xl">404</h1>

View file

@ -0,0 +1,12 @@
import { Metadata } from "next"
import Home from "./Home"
export const metadata: Metadata = {
metadataBase: new URL("https://blog.developomp.com"),
title: "pomp's blog | Home",
}
export default function Page() {
return <Home />
}

View file

@ -1,4 +1,5 @@
import { Link } from "wouter" import Image from "next/image"
import Link from "next/link"
import ReadProgress from "./ReadProgress" import ReadProgress from "./ReadProgress"
import ThemeToggleButton from "./ThemeToggleButton" import ThemeToggleButton from "./ThemeToggleButton"
@ -7,18 +8,17 @@ export default function Header() {
return ( return (
<header className="fixed z-50 h-16 w-full bg-light-ui shadow-lg dark:bg-dark-ui"> <header className="fixed z-50 h-16 w-full bg-light-ui shadow-lg dark:bg-dark-ui">
<div className="mx-auto flex h-[60px] max-w-screen-desktop items-center justify-between"> <div className="mx-auto flex h-[60px] max-w-screen-desktop items-center justify-between">
<Link to="/"> <Link
<a aria-label="homepage"
aria-label="homepage" href="/"
className="ml-4 h-10 cursor-pointer" className="ml-4 h-10 cursor-pointer"
> >
<img <Image
width="40px" src="/favicon.svg"
height="40px" alt="logo"
src="/favicon.svg" width={40}
alt="logo" height={40}
/> />
</a>
</Link> </Link>
<div className="flex h-full"> <div className="flex h-full">
<ThemeToggleButton /> <ThemeToggleButton />

View file

@ -1,19 +1,21 @@
import { useCallback, useEffect, useState } from "react" "use client"
import { useLocation } from "wouter"
const st = "scrollTop" import { usePathname } from "next/navigation"
const sh = "scrollHeight" import { useCallback, useEffect, useState } from "react"
const h = document.documentElement
const b = document.body
// https://stackoverflow.com/a/8028584/12979111 // https://stackoverflow.com/a/8028584/12979111
function calculateScrollPercent() { function calculateScrollPercent() {
const st = "scrollTop"
const sh = "scrollHeight"
const h = document.documentElement
const b = document.body
return ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100 return ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100
} }
export default function ReadProgress() { export default function ReadProgress() {
const [scroll, setScroll] = useState(0) const [scroll, setScroll] = useState(0)
const [location] = useLocation() const pathname = usePathname()
const scrollHandler = useCallback(() => { const scrollHandler = useCallback(() => {
setScroll(calculateScrollPercent()) setScroll(calculateScrollPercent())
}, []) }, [])
@ -43,7 +45,7 @@ export default function ReadProgress() {
setTimeout(() => { setTimeout(() => {
scrollHandler() scrollHandler()
}, 100) }, 100)
}, [location]) }, [pathname])
return ( return (
<div className="h-1 bg-light-scroll-progress-bg dark:bg-dark-scroll-progress-bg"> <div className="h-1 bg-light-scroll-progress-bg dark:bg-dark-scroll-progress-bg">

View file

@ -1,13 +1,15 @@
import { faSearch } from "@fortawesome/free-solid-svg-icons" import { faSearch } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { Link } from "wouter" import Link from "next/link"
export default function SearchButton() { export default function SearchButton() {
return ( return (
<Link to="/search" aria-label="go to search page"> <Link
<a className="flex w-20 cursor-pointer items-center justify-center text-light-text-default hover:bg-light-ui-hover hover:text-light-text-default dark:text-dark-text-default dark:hover:bg-dark-ui-hover dark:hover:text-dark-text-default"> href="/search"
<FontAwesomeIcon icon={faSearch} /> aria-label="go to search page"
</a> className="flex w-20 cursor-pointer items-center justify-center text-light-text-default hover:bg-light-ui-hover hover:text-light-text-default dark:text-dark-text-default dark:hover:bg-dark-ui-hover dark:hover:text-dark-text-default"
>
<FontAwesomeIcon icon={faSearch} />
</Link> </Link>
) )
} }

View file

@ -1,3 +1,5 @@
"use client"
import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons" import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
@ -15,7 +17,7 @@ export default function ThemeToggleButton() {
aria-label="theme toggle" aria-label="theme toggle"
> >
{theme === Theme.Dark ? ( {theme === Theme.Dark ? (
<FontAwesomeIcon icon={faMoon} /> <FontAwesomeIcon icon={faMoon} size={"1x"} />
) : ( ) : (
<FontAwesomeIcon icon={faSun} /> <FontAwesomeIcon icon={faSun} />
)} )}

View file

@ -5,7 +5,7 @@ import {
faHourglass, faHourglass,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { Link } from "wouter" import Link from "next/link"
import Card from "@/components/Card" import Card from "@/components/Card"
import Tag from "@/components/Tag" import Tag from "@/components/Tag"
@ -25,42 +25,40 @@ export default function PostCard({ postData, className }: Props) {
return ( return (
<Link href={content_id} className={`${className} w-full`}> <Link href={content_id} className={`${className} w-full`}>
<a className="w-full"> <Card className="w-full cursor-pointer fill-light-text-gray text-light-text-gray hover:shadow-glow dark:fill-dark-text-gray dark:text-dark-text-gray">
<Card className="w-full cursor-pointer fill-light-text-gray text-light-text-gray hover:shadow-glow dark:fill-dark-text-gray dark:text-dark-text-gray"> <h2 className="mb-8 text-3xl">
<h2 className="mb-8 text-3xl"> {title}
{title} {/* show "(series)" for urls that matches regex "/series/<series-title>" */}
{/* show "(series)" for urls that matches regex "/series/<series-title>" */} {/\/series\/[^/]*$/.test(content_id) && " (series)"}
{/\/series\/[^/]*$/.test(content_id) && " (series)"} </h2>
</h2> <small>
<small> <TagList>
<TagList> {tags &&
{tags && tags.map((tag) => (
tags.map((tag) => ( <Tag key={title + tag} text={tag} />
<Tag key={title + tag} text={tag} /> ))}
))} </TagList>
</TagList> <hr />
<hr /> <div className="flex flex-wrap items-center gap-x-4">
<div className="flex flex-wrap items-center gap-x-4"> <div className="flex items-center gap-2 whitespace-nowrap">
<div className="flex items-center gap-2 whitespace-nowrap"> <FontAwesomeIcon icon={faCalendar} />
<FontAwesomeIcon icon={faCalendar} /> {date || "Unknown date"}
{date || "Unknown date"}
</div>
<div className="flex items-center gap-2 whitespace-nowrap">
<FontAwesomeIcon icon={faBook} />
{readTime
? readTime + " read"
: "unknown read time"}
</div>
<div className="flex items-center gap-2 whitespace-nowrap">
<FontAwesomeIcon icon={faHourglass} />
{typeof wordCount === "number"
? wordCount + " words"
: "unknown length"}
</div>
</div> </div>
</small> <div className="flex items-center gap-2 whitespace-nowrap">
</Card> <FontAwesomeIcon icon={faBook} />
</a> {readTime
? readTime + " read"
: "unknown read time"}
</div>
<div className="flex items-center gap-2 whitespace-nowrap">
<FontAwesomeIcon icon={faHourglass} />
{typeof wordCount === "number"
? wordCount + " words"
: "unknown length"}
</div>
</div>
</small>
</Card>
</Link> </Link>
) )
} }

View file

@ -1,31 +0,0 @@
import "@fontsource/noto-sans-kr/400.css"
import "@fontsource/noto-sans-kr/700.css"
import "@fontsource/source-code-pro"
import "katex/dist/katex.min.css"
import "./index.css"
import "./styles/anchor.scss"
import "./styles/blockQuote.scss"
import "./styles/button.scss"
import "./styles/callout.scss"
import "./styles/checkbox.scss"
import "./styles/code.scss"
import "./styles/colorChip.scss"
import "./styles/global.scss"
import "./styles/heading.scss"
import "./styles/hr.scss"
import "./styles/img.scss"
import "./styles/katex.scss"
import "./styles/kbd.scss"
import "./styles/list.scss"
import "./styles/mark.scss"
import "./styles/scrollbar.scss"
import "./styles/subSup.scss"
import "./styles/table.scss"
import ReactDOM from "react-dom/client"
import App from "@/App.tsx"
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<App />
)

View file

@ -1,3 +0,0 @@
import Home from "./Home"
export default Home

View file

@ -1,3 +0,0 @@
import NotFound from "./NotFound"
export default NotFound

View file

@ -1,3 +0,0 @@
import Page from "./Page"
export default Page

View file

@ -1,6 +1,5 @@
html, html,
body, body {
#root {
/* style */ /* style */
@apply flex flex-col; @apply flex flex-col;

View file

@ -1,3 +1,5 @@
"use client"
import { create } from "zustand" import { create } from "zustand"
const themeKey = "theme" const themeKey = "theme"
@ -15,6 +17,8 @@ export type ThemeState = {
* Reads site theme setting from local storage * Reads site theme setting from local storage
*/ */
function getStoredThemeSetting(): Theme { function getStoredThemeSetting(): Theme {
if (typeof window === "undefined") return Theme.Dark
const storedTheme = localStorage.getItem(themeKey) const storedTheme = localStorage.getItem(themeKey)
// fix invalid values // fix invalid values
@ -40,6 +44,8 @@ function setTheme(targetTheme: Theme) {
* Applies tailwind theme using classes based on current theme setting * Applies tailwind theme using classes based on current theme setting
*/ */
function applyTheme() { function applyTheme() {
if (typeof window === "undefined") return
if (getStoredThemeSetting() === Theme.Dark) { if (getStoredThemeSetting() === Theme.Dark) {
document.documentElement.classList.add("dark") document.documentElement.classList.add("dark")
} else { } else {
@ -48,12 +54,14 @@ function applyTheme() {
} }
export const useTheme = create<ThemeState>()((set) => { export const useTheme = create<ThemeState>()((set) => {
applyTheme() if (typeof window !== "undefined") {
addEventListener("storage", () => {
setTheme(getStoredThemeSetting())
applyTheme() applyTheme()
})
addEventListener("storage", () => {
setTheme(getStoredThemeSetting())
applyTheme()
})
}
return { return {
theme: getStoredThemeSetting(), theme: getStoredThemeSetting(),

View file

@ -1 +0,0 @@
/// <reference types="vite/client" />

View file

@ -1,5 +1,9 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
presets: [require("@developomp-site/tailwind-config/tailwind.config.js")], presets: [require("@developomp-site/tailwind-config/tailwind.config.js")],
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
} }

View file

@ -1,31 +1,42 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "ES2020", "target": "es5",
"useDefineForClassFields": true, "lib": [
"lib": ["ES2020", "DOM", "DOM.Iterable"], "dom",
"module": "ESNext", "dom.iterable",
"skipLibCheck": true, "esnext"
],
/* Bundler mode */ "allowJs": true,
"moduleResolution": "bundler", "skipLibCheck": true,
"allowImportingTsExtensions": true, "strict": true,
"resolveJsonModule": true, "forceConsistentCasingInFileNames": true,
"isolatedModules": true, "noEmit": true,
"noEmit": true, "esModuleInterop": true,
"jsx": "react-jsx", "module": "esnext",
"moduleResolution": "bundler",
/* Linting */ "resolveJsonModule": true,
"strict": true, "isolatedModules": true,
"noUnusedLocals": true, "jsx": "preserve",
"noUnusedParameters": true, "incremental": true,
"noFallthroughCasesInSwitch": true, "plugins": [
{
/* Absolute import */ "name": "next"
"baseUrl": "./src", }
"paths": { ],
"@/*": ["./*"] "paths": {
} "@/*": [
}, "./src/*"
"include": ["src"], ]
"references": [{ "path": "./tsconfig.node.json" }] }
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"build/types/**/*.ts"
],
"exclude": [
"node_modules"
]
} }

View file

@ -1,10 +0,0 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View file

@ -1,18 +0,0 @@
import react from "@vitejs/plugin-react"
import path from "path"
import { defineConfig } from "vite"
import dynamicImport from "vite-plugin-dynamic-import"
// https://vitejs.dev/config/
export default defineConfig(() => ({
plugins: [react(), dynamicImport()],
build: {
outDir: "build",
},
resolve: {
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
},
server: {
port: 3000,
open: true,
},
}))

View file

@ -16,12 +16,6 @@
"target": "blog", "target": "blog",
"cleanUrls": true, "cleanUrls": true,
"public": "apps/blog/build", "public": "apps/blog/build",
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
],
"ignore": ["**/.*"] "ignore": ["**/.*"]
}, },
{ {

1935
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff