feat(blog): port from CRA to vite + react

This commit is contained in:
Kim, Jimin 2023-07-07 19:18:32 +09:00
parent 8243d38270
commit e48b65b14c
109 changed files with 1493 additions and 10360 deletions

2
apps/blog/.eslintignore Normal file
View file

@ -0,0 +1,2 @@
/dist/
/node_modules/

View file

@ -1,12 +0,0 @@
{
"root": true,
"extends": ["@developomp-site/eslint-config", "plugin:react/recommended"],
"settings": {
"react": {
"version": "detect"
}
},
"rules": {
"react/react-in-jsx-scope": "off"
}
}

14
apps/blog/.eslintrc.cjs Normal file
View file

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

176
apps/blog/.gitignore vendored
View file

@ -1,176 +1,24 @@
# auto generated files
/src/data/**
!/src/data/NavbarData.tsx
/public/img/skills.svg
/public/img/projects.svg
# production
build/
# Created by https://www.toptal.com/developers/gitignore/api/firebase,node,git,visualstudiocode
# Edit at https://www.toptal.com/developers/gitignore?templates=firebase,node,git,visualstudiocode
### Firebase ###
.idea
**/node_modules/*
**/.firebaserc
### Firebase Patch ###
.runtimeconfig.json
.firebase/
### Git ###
# Created by git for backups. To disable backups in Git:
# $ git config --global mergetool.keepBackup false
*.orig
# Created by git when using merge tools for conflicts
*.BACKUP.*
*.BASE.*
*.LOCAL.*
*.REMOTE.*
*_BACKUP_*.txt
*_BASE_*.txt
*_LOCAL_*.txt
*_REMOTE_*.txt
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
node_modules
dist
dist-ssr
*.local
# Storybook build outputs
.out
.storybook-out
storybook-static
# rollup.js default build output
dist/
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Temporary folders
tmp/
temp/
### VisualStudioCode ###
# Editor directories and files
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/firebase,node,git,visualstudiocode
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

36
apps/blog/index.html Normal file
View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<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>

View file

@ -2,76 +2,48 @@
"name": "@developomp-site/blog",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"cp": "cp -a ../../packages/content/dist/public/. ./public",
"dev": "pnpm cp && react-scripts start",
"build": "pnpm cp && react-scripts build",
"dev": "vite",
"build": "vite build",
"lint": "eslint .",
"preview": "vite serve build --open --port 3000",
"preview": "vite preview",
"clean": "rm -rf .turbo build node_modules"
},
"dependencies": {
"@developomp-site/content": "workspace:*",
"@developomp-site/theme": "workspace:*",
"@fontsource/noto-sans-kr": "^5.0.3",
"@fontsource/source-code-pro": "^5.0.3",
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/react-fontawesome": "^0.2.0",
"elasticlunr": "^0.9.5",
"highlight.js": "^11.7.0",
"@kunukn/react-collapse": "^2.2.10",
"highlight.js": "^11.8.0",
"hoofd": "^1.7.0",
"katex": "^0.16.4",
"local-storage-fallback": "^4.1.2",
"katex": "^0.16.8",
"react": "^18.2.0",
"react-collapse": "^5.1.1",
"react-date-range": "^1.4.0",
"react-device-detect": "^2.2.2",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-router-dom": "^6.4.5",
"react-scripts": "^5.0.1",
"react-select": "^5.7.0",
"react-tooltip": "^4.5.1",
"styled-components": "^5.3.6"
"wouter": "^2.11.0",
"zustand": "^4.3.9"
},
"devDependencies": {
"@developomp-site/eslint-config": "workspace:*",
"@styled/typescript-styled-plugin": "^1.0.0",
"@types/elasticlunr": "^0.9.5",
"@developomp-site/tailwind-config": "workspace:*",
"@types/highlight.js": "^10.1.0",
"@types/jsdom": "^20.0.1",
"@types/katex": "^0.14.0",
"@types/node": "^18.11.11",
"@types/react": "^18.0.26",
"@types/react": "^18.0.37",
"@types/react-collapse": "^5.0.1",
"@types/react-date-range": "^1.4.4",
"@types/react-dom": "^18.0.9",
"@types/react-select": "^5.0.1",
"@types/styled-components": "^5.1.26",
"eslint": "^8.43.0",
"eslint-plugin-react": "^7.32.2",
"jsdom": "^20.0.3",
"prettier": "^2.8.1",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
"@vitejs/plugin-react": "^4.0.0",
"eslint": "^8.38.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.3.4",
"prettier-plugin-tailwindcss": "^0.3.0",
"simple-icons": "^7.21.0",
"tslint-config-prettier": "^1.18.0",
"typescript": "^4.9.4",
"vite": "^4.3.9"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"tailwindcss": "^3.3.2",
"typescript": "^5.0.4",
"vite": "^4.3.9",
"vite-plugin-dynamic-import": "^1.4.1"
}
}

View file

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/icon/icon_circle.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="developomp's Blog" />
<title>pomp</title>
<!-- OpenGraph -->
<meta property="og:title" content="pomp" />
<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="%PUBLIC_URL%/icon/icon_circle.svg" />
</head>
<body>
<noscript>
English: Oops! It seems like JavaScript is not enabled!
<br />
Korean: 이런! 자바스크립트를 사용할 수 없습니다!
</noscript>
<div id="root"></div>
</body>
</html>

View file

@ -1,89 +1,41 @@
import darkTheme from "@developomp-site/theme/dist/dark.json"
import lightTheme from "@developomp-site/theme/dist/light.json"
import { useMeta, useTitle, useTitleTemplate } from "hoofd"
import { useContext, useEffect, useState } from "react"
import { isIE } from "react-device-detect"
import { Route, Routes } from "react-router-dom"
import styled, { ThemeProvider } from "styled-components"
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 { globalContext } from "./globalContext"
import Home from "./pages/Home"
import NotFound from "./pages/NotFound"
import Page from "./pages/Page"
import Search from "./pages/Search"
import GlobalStyle from "./styles/globalStyle"
const IENotSupported = styled.p`
margin: auto;
font-size: 2rem;
margin-top: 2rem;
text-align: center;
font-family: ${(props) => props.theme.theme.font.sansSerif};
`
const StyledContentContainer = styled.div`
flex: 1 1 auto;
margin-bottom: 3rem;
margin-top: 5rem;
`
export default function App() {
const { globalState } = useContext(globalContext)
const [isLoading, setIsLoading] = useState(true)
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")
useTitle("Home")
useMeta({ property: "og:title", content: "Home" })
useEffect(() => {
// set loading to false if all fonts are loaded
// checks if document.fonts.onloadingdone is supported on the browser
if (typeof document.fonts.onloadingdone != undefined) {
document.fonts.onloadingdone = () => {
setIsLoading(false)
}
} else {
setIsLoading(false)
}
}, [])
if (isIE)
return (
<IENotSupported>
Internet Explorer is <b>not supported.</b>
</IENotSupported>
)
return (
<ThemeProvider
theme={{
currentTheme: globalState.currentTheme,
theme:
globalState.currentTheme === "dark"
? darkTheme
: lightTheme,
}}
>
<GlobalStyle />
<>
<Header />
<StyledContentContainer>
{isLoading ? (
<Loading />
) : (
<Routes>
<Route index element={<Home />} />
<Route path="search" element={<Search />} />
<Route path="404" element={<NotFound />} />
<Route path="loading" element={<Loading />} />
<Route path="*" element={<Page />} />
</Routes>
)}
</StyledContentContainer>
<main className="mb-8 mt-20 flex w-screen grow flex-col items-center gap-8 px-8">
<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 />
</ThemeProvider>
</>
)
}
export default App

View file

@ -1,24 +1,16 @@
import styled, { css } from "styled-components"
import { ReactNode } from "react"
export const cardCSS = css`
margin: auto;
background-color: ${({ theme }) =>
theme.currentTheme
? theme.theme.component.card.color.background
: "white"};
padding: 2rem;
border-radius: 6px;
box-shadow: ${({ theme }) =>
theme.currentTheme === "dark"
? "0 4px 10px rgb(0 0 0 / 30%), 0 0 1px rgb(0 0 0 / 30%)"
: "0 4px 10px rgb(0 0 0 / 5%), 0 0 1px rgb(0 0 0 / 10%)"};
interface Props {
children?: ReactNode
className?: string
}
@media screen and (max-width: ${({ theme }) =>
theme.theme.maxDisplayWidth.mobile}) {
padding: 1rem;
}
`
export default styled.div`
${cardCSS}
`
export default function Card({ children, className }: Props) {
return (
<div
className={`${className} flex h-fit w-full max-w-screen-mobile flex-col rounded-md bg-light-card-bg p-8 shadow-lg dark:bg-dark-card-bg`}
>
{children}
</div>
)
}

View file

@ -1,46 +1,14 @@
import { type FC } from "react"
import styled from "styled-components"
import GithubLinkIcon from "../GithubLinkIcon"
const StyledFooter = styled.footer`
display: flex;
// congratulation. You've found the lucky 7s
min-height: 7.77rem;
max-height: 7.77rem;
align-items: center;
justify-content: center;
background-color: ${({ theme }) =>
theme.theme.component.footer.color.background};
`
const StyledFooterContainer = styled.div`
display: flex;
padding: 0 1rem 0 1rem;
justify-content: space-between;
text-align: center;
color: gray;
width: 100%;
max-width: ${({ theme }) => theme.theme.maxDisplayWidth.desktop};
`
const Footer: FC = () => {
export default function Footer() {
return (
<StyledFooter>
<StyledFooterContainer>
<footer className="flex h-32 justify-center bg-light-footer-bg px-4 text-light-footer-text dark:bg-dark-footer-bg dark:text-dark-footer-text">
<div className="flex h-full w-full max-w-screen-desktop items-center justify-between text-center">
<div>
Created by <b>developomp</b>
</div>
<GithubLinkIcon link="https://github.com/developomp/developomp-site" />
</StyledFooterContainer>
</StyledFooter>
<GithubLinkIcon href="https://github.com/developomp/developomp-site" />
</div>
</footer>
)
}
export default Footer

View file

@ -1,36 +1,19 @@
import { faGithub } from "@fortawesome/free-brands-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import type { FC, ReactNode } from "react"
import styled from "styled-components"
const StyledGithubLink = styled.a<{ size?: string }>`
font-size: ${(props) => props.size || "2.5rem"};
color: ${({ theme }) =>
theme.currentTheme === "dark" ? "grey" : "lightgrey"};
:hover {
color: ${({ theme }) => theme.theme.color.text.highContrast};
}
`
interface Props {
link: string
size?: string
children?: ReactNode
href: string
}
const GithubLinkIcon: FC<Props> = ({ link, size, children }) => {
export default function GithubLinkIcon({ href }: Props) {
return (
<StyledGithubLink
aria-label="GitHub repository"
size={size}
href={link}
<a
className="text-5xl text-light-footer-text transition-colors duration-75 hover:text-light-text-high-contrast dark:text-dark-footer-text dark:hover:text-dark-text-high-contrast"
href={href}
target="_blank"
aria-label="GitHub link"
>
<FontAwesomeIcon icon={faGithub} />
{children}
</StyledGithubLink>
</a>
)
}
export default GithubLinkIcon

View file

@ -1,22 +0,0 @@
import { type FC } from "react"
import styled from "styled-components"
import SearchButton from "./SearchButton"
import ThemeToggleButton from "./ThemeToggleButton"
const RightButtons = styled.div`
display: flex;
height: 100%;
margin-left: auto;
`
const Buttons: FC = () => {
return (
<RightButtons>
<ThemeToggleButton />
<SearchButton />
</RightButtons>
)
}
export default Buttons

View file

@ -1,30 +0,0 @@
import { faSearch } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { Link } from "react-router-dom"
import ReactTooltip from "react-tooltip"
import HeaderButton from "../HeaderButton"
const SearchButton = () => {
return (
<>
<div>
<Link
data-tip
data-for="search"
to="/search"
aria-label="go to search page"
>
<HeaderButton>
<FontAwesomeIcon icon={faSearch} />
</HeaderButton>
</Link>
</div>
<ReactTooltip id="search" type="dark" effect="solid">
<span>Search</span>
</ReactTooltip>
</>
)
}
export default SearchButton

View file

@ -1,50 +0,0 @@
import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { useContext } from "react"
import { isMobile } from "react-device-detect"
import ReactTooltip from "react-tooltip"
import styled from "styled-components"
import { ActionsEnum, globalContext } from "../../../globalContext"
import { HeaderButtonCSS } from "../HeaderButton"
const StyledThemeButton = styled.button`
${HeaderButtonCSS}
border: none;
width: 72px;
${({ theme }) =>
theme.currentTheme === "dark" ? "transform: scaleX(-1)" : ""};
`
const ThemeToggleButton = () => {
const { globalState, dispatch } = useContext(globalContext)
const theme = globalState.currentTheme
return (
<>
<StyledThemeButton
data-tip
aria-label="theme toggle"
data-for="theme"
onClick={() =>
dispatch({
type: ActionsEnum.UPDATE_THEME,
payload: theme === "dark" ? "light" : "dark",
})
}
>
{theme == "dark" && <FontAwesomeIcon icon={faMoon} />}
{theme == "light" && <FontAwesomeIcon icon={faSun} />}
</StyledThemeButton>
{!isMobile && (
<ReactTooltip id="theme" type="dark" effect="solid">
<span>Using {theme} theme</span>
</ReactTooltip>
)}
</>
)
}
export default ThemeToggleButton

View file

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

View file

@ -1,62 +1,31 @@
import { type FC } from "react"
import { Link } from "react-router-dom"
import styled from "styled-components"
import { Link } from "wouter"
import Buttons from "./Buttons"
import ReadProgress from "./ReadProgress"
import ThemeToggleButton from "./ThemeToggleButton"
const StyledHeader = styled.header`
/* set z index to arbitrarily high value to prevent other components from drawing over it */
z-index: 9999;
position: fixed;
width: 100%;
background-color: ${({ theme }) =>
theme.theme.component.ui.color.background.default};
color: ${({ theme }) => theme.theme.color.text.default};
box-shadow: 0 4px 10px rgb(0 0 0 / 5%);
`
const Container = styled.div`
margin: 0 auto;
align-items: center;
display: flex;
height: 4rem;
/* account for 20px scrollbar width */
@media only screen and (min-width: calc(${({ theme }) =>
theme.theme.maxDisplayWidth.desktop} + 20px)) {
width: calc(
${({ theme }) => theme.theme.maxDisplayWidth.desktop} - 20px
);
}
`
const Icon = styled.img`
height: 2.5rem;
display: block;
margin: 1rem;
`
const Header: FC = () => {
export default function Header() {
return (
<StyledHeader>
<Container>
<Link to="/" aria-label="homepage">
<Icon
width={40}
height={40}
src="/icon/icon_circle.svg"
alt="logo"
/>
<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">
<Link to="/">
<a
aria-label="homepage"
className="ml-4 h-10 cursor-pointer"
>
<img
width="40px"
height="40px"
src="/favicon.svg"
alt="logo"
/>
</a>
</Link>
<Buttons />
</Container>
<div className="flex h-full">
<ThemeToggleButton />
{/* <SearchButton /> */}
</div>
</div>
<ReadProgress />
</StyledHeader>
</header>
)
}
export default Header

View file

@ -1,43 +0,0 @@
/**
* @file Manages the configuration settings for the widget.
*/
import styled, { css } from "styled-components"
export const HeaderButtonCSS = css`
/* style */
display: flex;
cursor: pointer;
align-items: center;
justify-content: center;
/* size */
height: 100%;
min-width: 2.5rem;
margin: 0;
padding: 0 1rem 0 1rem;
/* text */
text-decoration: none;
/* color */
color: ${({ theme }) => theme.theme.color.text.default};
background-color: ${({ theme }) =>
theme.theme.component.ui.color.background.default};
/* animation */
transition: transform 0.1s linear;
&:hover {
background-color: ${({ theme }) =>
theme.theme.component.ui.color.background.hover};
}
`
export default styled.div`
${HeaderButtonCSS}
`

View file

@ -1,33 +1,21 @@
import { useCallback, useEffect, useState } from "react"
import { useLocation } from "react-router-dom"
import styled from "styled-components"
const Background = styled.div`
height: 0.2rem;
background-color: ${({ theme }) =>
theme.theme.component.scrollProgressBar.color.background};
`
const ProgressBar = styled.div`
height: 100%;
background-color: ${({ theme }) =>
theme.theme.component.scrollProgressBar.color.foreground};
`
import { useLocation } from "wouter"
const st = "scrollTop"
const sh = "scrollHeight"
const h = document.documentElement
const b = document.body
const ReadProgress = () => {
const [scroll, setScroll] = useState(0)
const location = useLocation()
// https://stackoverflow.com/a/8028584/12979111
function calculateScrollPercent() {
return ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100
}
// https://stackoverflow.com/a/8028584/12979111
export default function ReadProgress() {
const [scroll, setScroll] = useState(0)
const [location] = useLocation()
const scrollHandler = useCallback(() => {
setScroll(
((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100
)
setScroll(calculateScrollPercent())
}, [])
useEffect(() => {
@ -38,13 +26,19 @@ const ReadProgress = () => {
resizeObserver.observe(document.body)
window.addEventListener("scroll", scrollHandler)
// progress bar gets de-synced after
const intervalId = setInterval(() => {
scrollHandler()
}, 1000)
return () => {
clearInterval(intervalId)
resizeObserver.disconnect()
window.removeEventListener("scroll", scrollHandler)
}
}, [])
})
// update on path change
// a hack to fix progress bar de-sync on navigation
useEffect(() => {
setTimeout(() => {
scrollHandler()
@ -52,10 +46,11 @@ const ReadProgress = () => {
}, [location])
return (
<Background>
<ProgressBar style={{ width: `${scroll}%` }} />
</Background>
<div className="h-1 bg-light-scroll-progress-bg dark:bg-dark-scroll-progress-bg">
<div
className="h-full bg-light-scroll-progress-fg dark:bg-dark-scroll-progress-fg"
style={{ width: `${scroll}%` }}
/>
</div>
)
}
export default ReadProgress

View file

@ -0,0 +1,13 @@
import { faSearch } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { Link } from "wouter"
export default function SearchButton() {
return (
<Link to="/search" 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} />
</a>
</Link>
)
}

View file

@ -0,0 +1,24 @@
import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { Theme, useTheme } from "@/theme"
export default function ThemeToggleButton() {
const { theme, setTheme } = useTheme()
return (
<button
onClick={() =>
setTheme(theme === Theme.Dark ? Theme.Light : Theme.Dark)
}
className="flex w-20 items-center justify-center hover:bg-light-ui-hover dark:scale-x-[-1] dark:hover:bg-dark-ui-hover"
aria-label="theme toggle"
>
{theme === Theme.Dark ? (
<FontAwesomeIcon icon={faMoon} />
) : (
<FontAwesomeIcon icon={faSun} />
)}
</button>
)
}

View file

@ -1,3 +1,3 @@
import Navbar from "./Header"
import Header from "./Header"
export default Navbar
export default Header

View file

@ -1,139 +0,0 @@
/**
* inspired by https://codepen.io/avstorm/pen/RwNzPNN
*/
import styled from "styled-components"
import MainContent from "./MainContent"
const StyledContainer = styled(MainContent)`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
animation: fadein 2s;
@keyframes fadein {
from {
opacity: 0;
}
50% {
opacity: 0;
}
to {
opacity: 1;
}
}
`
const StyledSVG = styled.svg`
--color: ${({ theme }) => theme.theme.color.text.default};
display: block;
margin: 1rem;
margin-bottom: 4.5rem;
transform: scale(2);
#teabag {
transform-origin: top center;
transform: rotate(3deg);
animation: swingAnimation 2s infinite;
}
#steamL {
stroke-dasharray: 13;
stroke-dashoffset: 13;
animation: steamLargeAnimation 2s infinite;
}
#steamR {
stroke-dasharray: 9;
stroke-dashoffset: 9;
animation: steamSmallAnimation 2s infinite;
}
@keyframes swingAnimation {
50% {
transform: rotate(-3deg);
}
}
@keyframes steamLargeAnimation {
0% {
stroke-dashoffset: 13;
opacity: 0.6;
}
100% {
stroke-dashoffset: 39;
opacity: 0;
}
}
@keyframes steamSmallAnimation {
10% {
stroke-dashoffset: 9;
opacity: 0.6;
}
80% {
stroke-dashoffset: 27;
opacity: 0;
}
100% {
stroke-dashoffset: 27;
opacity: 0;
}
}
`
const Loading = () => {
return (
<StyledContainer>
<StyledSVG
width="37"
height="48"
viewBox="0 0 37 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M27.0819 17H3.02508C1.91076 17 1.01376 17.9059 1.0485 19.0197C1.15761 22.5177 1.49703 29.7374 2.5 34C4.07125 40.6778 7.18553 44.8868 8.44856 46.3845C8.79051 46.79 9.29799 47 9.82843 47H20.0218C20.639 47 21.2193 46.7159 21.5659 46.2052C22.6765 44.5687 25.2312 40.4282 27.5 34C28.9757 29.8188 29.084 22.4043 29.0441 18.9156C29.0319 17.8436 28.1539 17 27.0819 17Z"
stroke="var(--color)"
strokeWidth="2"
/>
<path
d="M29 23.5C29 23.5 34.5 20.5 35.5 25.4999C36.0986 28.4926 34.2033 31.5383 32 32.8713C29.4555 34.4108 28 34 28 34"
stroke="var(--color)"
strokeWidth="2"
/>
<path
id="teabag"
fill="var(--color)"
fillRule="evenodd"
clipRule="evenodd"
d="M16 25V17H14V25H12C10.3431 25 9 26.3431 9 28V34C9 35.6569 10.3431 37 12 37H18C19.6569 37 21 35.6569 21 34V28C21 26.3431 19.6569 25 18 25H16ZM11 28C11 27.4477 11.4477 27 12 27H18C18.5523 27 19 27.4477 19 28V34C19 34.5523 18.5523 35 18 35H12C11.4477 35 11 34.5523 11 34V28Z"
/>
<path
id="steamL"
d="M17 1C17 1 17 4.5 14 6.5C11 8.5 11 12 11 12"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke="var(--color)"
/>
<path
id="steamR"
d="M21 6C21 6 21 8.22727 19 9.5C17 10.7727 17 13 17 13"
stroke="var(--color)"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</StyledSVG>
<h2>Loading...</h2>
</StyledContainer>
)
}
export default Loading

View file

@ -0,0 +1,73 @@
.loading-card {
animation: fade-in 2s;
svg {
display: block;
margin: 1rem;
margin-bottom: 4.5rem;
transform: scale(2);
#teabag {
transform-origin: top center;
transform: rotate(3deg);
animation: swingAnimation 2s infinite;
}
#steamL {
stroke-dasharray: 13;
stroke-dashoffset: 13;
animation: steamLargeAnimation 2s infinite;
}
#steamR {
stroke-dasharray: 9;
stroke-dashoffset: 9;
animation: steamSmallAnimation 2s infinite;
}
}
}
@keyframes fade-in {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes swingAnimation {
50% {
transform: rotate(-3deg);
}
}
@keyframes steamLargeAnimation {
0% {
stroke-dashoffset: 13;
opacity: 0.6;
}
100% {
stroke-dashoffset: 39;
opacity: 0;
}
}
@keyframes steamSmallAnimation {
10% {
stroke-dashoffset: 9;
opacity: 0.6;
}
80% {
stroke-dashoffset: 27;
opacity: 0;
}
100% {
stroke-dashoffset: 27;
opacity: 0;
}
}

View file

@ -0,0 +1,18 @@
/**
* inspired by https://codepen.io/avstorm/pen/RwNzPNN
*/
import "./Loading.scss"
import Card from "@/components/Card"
import TeaCup from "./TeaCup"
export default function Loading() {
return (
<Card className="loading-card m-4 flex h-fit w-full max-w-screen-mobile flex-col items-center justify-center stroke-light-text-default text-center dark:stroke-dark-text-default">
<TeaCup />
<h1 className="text-4xl">Loading...</h1>
</Card>
)
}

View file

@ -0,0 +1,44 @@
export default function TeaCup() {
return (
<svg
id="teacup"
width="37"
height="48"
viewBox="0 0 37 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="teacup-body"
d="M27.0819 17H3.02508C1.91076 17 1.01376 17.9059 1.0485 19.0197C1.15761 22.5177 1.49703 29.7374 2.5 34C4.07125 40.6778 7.18553 44.8868 8.44856 46.3845C8.79051 46.79 9.29799 47 9.82843 47H20.0218C20.639 47 21.2193 46.7159 21.5659 46.2052C22.6765 44.5687 25.2312 40.4282 27.5 34C28.9757 29.8188 29.084 22.4043 29.0441 18.9156C29.0319 17.8436 28.1539 17 27.0819 17Z"
strokeWidth="2"
/>
<path
id="teacup-handle"
d="M29 23.5C29 23.5 34.5 20.5 35.5 25.4999C36.0986 28.4926 34.2033 31.5383 32 32.8713C29.4555 34.4108 28 34 28 34"
strokeWidth="2"
/>
<path
id="teabag"
className="fill-light-text-default stroke-none dark:fill-dark-text-default"
fillRule="evenodd"
clipRule="evenodd"
d="M16 25V17H14V25H12C10.3431 25 9 26.3431 9 28V34C9 35.6569 10.3431 37 12 37H18C19.6569 37 21 35.6569 21 34V28C21 26.3431 19.6569 25 18 25H16ZM11 28C11 27.4477 11.4477 27 12 27H18C18.5523 27 19 27.4477 19 28V34C19 34.5523 18.5523 35 18 35H12C11.4477 35 11 34.5523 11 34V28Z"
/>
<path
id="steamL"
d="M17 1C17 1 17 4.5 14 6.5C11 8.5 11 12 11 12"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
id="steamR"
d="M21 6C21 6 21 8.22727 19 9.5C17 10.7727 17 13 17 13"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

View file

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

View file

@ -1,28 +0,0 @@
import styled, { css } from "styled-components"
import Card from "./Card"
export const mainContentCSS = css`
margin-top: 1rem;
width: 50%;
img {
max-width: 100%;
}
table img {
max-width: fit-content;
}
@media screen and (max-width: ${({ theme }) =>
theme.theme.maxDisplayWidth.mobile}) {
width: auto;
margin: 1rem;
}
`
const MainContent = styled(Card)`
${mainContentCSS}
`
export default MainContent

View file

@ -5,46 +5,11 @@ import {
faHourglass,
} from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { Link } from "react-router-dom"
import styled from "styled-components"
import { Link } from "wouter"
import MainContent from "./MainContent"
import Tag from "./Tag"
import TagList from "./TagList"
const StyledPostCard = styled(MainContent)`
box-shadow: 0 4px 10px rgb(0 0 0 / 10%);
text-align: left;
margin-bottom: 2rem;
:hover {
cursor: pointer;
box-shadow: 0 4px 10px
${({ theme }) => theme.theme.component.card.color.hoverGlow};
}
`
const PostCardContainer = styled(Link)`
display: block;
padding: 2rem;
text-decoration: none;
padding: 0;
/* override link color */
color: ${({ theme }) => theme.theme.color.text.gray};
&:hover {
color: ${({ theme }) => theme.theme.color.text.gray};
}
`
const Title = styled.h1`
font-size: 2rem;
font-style: bold;
margin: 0;
margin-bottom: 1rem;
`
const MetaContainer = styled.small``
import Card from "@/components/Card"
import Tag from "@/components/Tag"
import TagList from "@/components/TagList"
interface PostCardData extends PostData {
content_id: string
@ -52,47 +17,44 @@ interface PostCardData extends PostData {
interface Props {
postData: PostCardData
className?: string
}
const PostCard = (props: Props) => {
const { postData } = props
export default function PostCard({ postData, className }: Props) {
const { content_id, wordCount, date, readTime, title, tags } = postData
return (
<StyledPostCard>
<PostCardContainer to={content_id}>
<Title>
{title || "No title"}
{/* show "(series)" for urls that matches regex "/series/<series-title>" */}
{/\/series\/[^/]*$/.test(content_id) && " (series)"}
</Title>
<br />
<MetaContainer>
<TagList direction="left">
{tags &&
tags.map((tag) => {
return <Tag key={title + tag} text={tag} />
})}
</TagList>
<hr />
<FontAwesomeIcon icon={faCalendar} />
&nbsp;&nbsp;&nbsp;
{date || "Unknown date"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faHourglass} />
&nbsp;&nbsp;&nbsp;
{readTime ? readTime + " read" : "unknown read time"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faBook} />
&nbsp;&nbsp;&nbsp;
{typeof wordCount === "number"
? wordCount + " words"
: "unknown length"}
</MetaContainer>
</PostCardContainer>
</StyledPostCard>
<Link href={content_id}>
<a className={`${className} w-full`}>
<Card className="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">
{title}
{/* show "(series)" for urls that matches regex "/series/<series-title>" */}
{/\/series\/[^/]*$/.test(content_id) && " (series)"}
</h2>
<small>
<TagList>
{tags &&
tags.map((tag) => (
<Tag key={title + tag} text={tag} />
))}
</TagList>
<hr />
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faCalendar} />
{date || "Unknown date"}
<FontAwesomeIcon icon={faBook} />
{readTime
? readTime + " read"
: "unknown read time"}
<FontAwesomeIcon icon={faHourglass} />
{typeof wordCount === "number"
? wordCount + " words"
: "unknown length"}
</div>
</small>
</Card>
</a>
</Link>
)
}
export default PostCard

View file

@ -1,28 +1,20 @@
import { faHashtag } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import type { FC, MouseEvent } from "react"
import styled from "styled-components"
const StyledTag = styled.div`
text-align: center;
margin-right: 0.8rem;
border-radius: 10px;
color: ${({ theme }) => theme.theme.color.text.gray};
`
import type { MouseEvent } from "react"
interface Props {
text: string
onClick?: (event: MouseEvent<never>) => void
}
const Tag: FC<Props> = (props) => {
export default function Tag(props: Props) {
return (
<StyledTag onClick={props.onClick || undefined}>
<FontAwesomeIcon icon={faHashtag} /> &nbsp;{props.text}
</StyledTag>
<div
className="mr-3 flex items-center rounded-lg text-center"
onClick={props.onClick || undefined}
>
<FontAwesomeIcon icon={faHashtag} />
{props.text}
</div>
)
}
export default Tag

View file

@ -1,26 +1,13 @@
import { ReactNode } from "react"
import styled from "styled-components"
const StyledTagList = styled.div<{ direction: string }>`
display: flex;
flex-wrap: wrap;
row-gap: 0.5rem;
column-gap: 0.5rem;
flex-direction: row;
justify-content: ${({ direction }) => direction};
`
interface Props {
direction?: string
children?: ReactNode | undefined
}
const TagList = (props: Props) => {
export default function TagList(props: Props) {
return (
<StyledTagList direction={props.direction || "center"}>
<div className="flex flex-wrap justify-start gap-2">
{props.children}
</StyledTagList>
</div>
)
}
export default TagList

View file

@ -1,6 +1,4 @@
import contentMapJson from "@developomp-site/content/dist/map.json"
import { ContentMap } from "@developomp-site/content/src/types/types"
const contentMap: ContentMap = contentMapJson
export default contentMap
export default contentMapJson as ContentMap

View file

@ -1,68 +0,0 @@
import type { Theme } from "@developomp-site/theme"
import darkTheme from "@developomp-site/theme/dist/dark.json"
import lightTheme from "@developomp-site/theme/dist/light.json"
import storage from "local-storage-fallback"
import type { Dispatch, ReactElement, ReactNode } from "react"
import { createContext, useEffect, useReducer } from "react"
export type SiteTheme = "dark" | "light"
export enum ActionsEnum {
UPDATE_THEME,
UPDATE_LOCALE,
}
// union of all actions
export type GlobalAction = {
type: ActionsEnum.UPDATE_THEME
payload: SiteTheme
}
export interface IGlobalState {
currentTheme: SiteTheme
theme: Theme
}
export interface IGlobalContext {
globalState: IGlobalState
dispatch: Dispatch<GlobalAction>
}
const defaultState: IGlobalState = {
currentTheme: (storage.getItem("theme") || "dark") as SiteTheme,
theme:
((storage.getItem("theme") || "dark") as SiteTheme) === "dark"
? darkTheme
: lightTheme,
}
export const globalContext = createContext({} as IGlobalContext)
function reducer(state = defaultState, action: GlobalAction): IGlobalState {
switch (action.type) {
case ActionsEnum.UPDATE_THEME:
state.currentTheme = action.payload
state.theme = state.currentTheme === "dark" ? darkTheme : lightTheme
break
default:
break
}
return { ...state }
}
export function GlobalStore(props: { children: ReactNode }): ReactElement {
const [globalState, dispatch] = useReducer(reducer, defaultState)
// save theme when it is changed
useEffect(() => {
storage.setItem("theme", globalState.currentTheme)
}, [globalState.currentTheme])
return (
<globalContext.Provider value={{ globalState, dispatch }}>
{props.children}
</globalContext.Provider>
)
}

3
apps/blog/src/index.css Normal file
View file

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View file

@ -1,19 +0,0 @@
import "@fontsource/noto-sans-kr/400.css"
import "@fontsource/noto-sans-kr/700.css"
import "@fontsource/source-code-pro"
import { createRoot } from "react-dom/client"
import { BrowserRouter } from "react-router-dom"
import App from "./App"
import { GlobalStore } from "./globalContext"
const container = document.getElementById("root") as HTMLElement
const root = createRoot(container)
root.render(
<GlobalStore>
<BrowserRouter>
<App />
</BrowserRouter>
</GlobalStore>
)

29
apps/blog/src/main.tsx Normal file
View file

@ -0,0 +1,29 @@
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/checkbox.scss"
import "./styles/code.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,46 +1,29 @@
/**
* PostList.tsx
* show posts in recent order
*/
import { useTitle } from "hoofd"
import { type ReactNode, useEffect, useState } from "react"
import { useMeta, useTitle } from "hoofd"
import { type FC, useCallback, useEffect, useState } from "react"
import styled from "styled-components"
import PostCard from "@/components/PostCard"
import contentMap from "@/contentMap"
import PostCard from "../../components/PostCard"
import contentMap from "../../contentMap"
import ShowMoreButton from "./ShowMoreButton"
const PostList = styled.div`
flex-direction: column;
align-items: center;
text-align: center;
const totalPosts = Object.keys(contentMap.posts).length
color: ${({ theme }) => theme.theme.color.text.default};
`
const Home: FC = () => {
export default function Home() {
const [howMany, setHowMany] = useState(5)
const [postsLength, setPostsLength] = useState(0)
const [postCards, setPostCards] = useState<JSX.Element[]>([])
const [postCards, setPostCards] = useState<ReactNode[]>([])
useTitle("Home")
useMeta({ property: "og:title", content: "Home" })
const loadPostCards = useCallback(() => {
let postCount = 0
const postCards = [] as JSX.Element[]
useEffect(() => {
const postCards: ReactNode[] = []
for (const date of Object.keys(contentMap.date).reverse()) {
if (postCount >= howMany) break
if (postCards.length >= howMany) break
const length = contentMap.date[date].length
for (let i = contentMap.date[date].length - 1; i >= 0; i--) {
if (postCards.length >= howMany) break
for (let i = 0; i < length; i++) {
if (postCount >= howMany) break
postCount++
const content_id = contentMap.date[date][length - i - 1]
const content_id = contentMap.date[date][i]
postCards.push(
<PostCard
@ -55,30 +38,19 @@ const Home: FC = () => {
}
setPostCards(postCards)
}, [howMany, postCards])
useEffect(() => {
loadPostCards()
setPostsLength(Object.keys(contentMap.posts).length)
}, [howMany])
return (
<>
<PostList>
<h1>Recent Posts</h1>
<div className="flex h-full w-full flex-col items-center gap-8">
<h1>Recent Posts</h1>
{postCards}
{postCards}
{postsLength > howMany && (
<ShowMoreButton
action={() => {
setHowMany((prev) => prev + 5)
}}
/>
)}
</PostList>
</>
{totalPosts > howMany && (
<ShowMoreButton
action={() => setHowMany((prev) => prev + 10)}
/>
)}
</div>
)
}
export default Home

View file

@ -1,21 +1,11 @@
import { type FC } from "react"
import styled from "styled-components"
import buttonStyle from "../../styles/button"
const Button = styled.button`
${buttonStyle}
/* center div */
margin: 0 auto;
`
interface Props {
action(): void
}
const ShowMoreButton: FC<Props> = (props) => {
return <Button onClick={props.action}>Show more posts</Button>
export default function ShowMoreButton({ action }: Props) {
return (
<button className="button" onClick={action}>
Show more posts
</button>
)
}
export default ShowMoreButton

View file

@ -1,29 +0,0 @@
import { useMeta, useTitle } from "hoofd"
import styled from "styled-components"
import MainContent from "../components/MainContent"
const StyledNotFound = styled(MainContent)`
text-align: center;
`
const Styled404 = styled.h1`
font-size: 5rem;
`
const NotFound = () => {
useTitle("404")
useMeta({ property: "og:title", content: "Page Not Found" })
return (
<>
<StyledNotFound>
<Styled404>404</Styled404>
<br />
Page was not found :(
</StyledNotFound>
</>
)
}
export default NotFound

View file

@ -0,0 +1,15 @@
import { useMeta, useTitle } from "hoofd"
import Card from "@/components/Card"
export default function NotFound() {
useTitle("404")
useMeta({ property: "og:title", content: "pomp's blog | Page Not Found" })
return (
<Card className="items-center gap-4">
<h1 className="text-7xl">404</h1>
Page not found :(
</Card>
)
}

View file

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

View file

@ -6,15 +6,10 @@ import {
faHourglass,
} from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import styled from "styled-components"
const StyledMetaContainer = styled.div`
color: ${({ theme }) => theme.theme.color.text.gray};
`
const Meta = (props: { fetchedPage: PageData }) => {
export default function Meta(props: { fetchedPage: PageData }) {
return (
<StyledMetaContainer>
<div className="text-light-text-gray dark:text-dark-text-gray">
{/* posts count */}
{props.fetchedPage.length > 0 && (
<>
@ -45,8 +40,6 @@ const Meta = (props: { fetchedPage: PageData }) => {
" word" +
(props.fetchedPage.wordCount > 1 && "s")
: "unknown words"}
</StyledMetaContainer>
</div>
)
}
export default Meta

View file

@ -0,0 +1,9 @@
.page {
h2,
h3,
h4,
h5,
h6 {
@apply mb-2 mt-16;
}
}

View file

@ -1,15 +1,17 @@
import "./Page.scss"
import type { PageData } from "@developomp-site/content/src/types/types"
import { useMeta, useTitle } from "hoofd"
import { useEffect, useState } from "react"
import { useLocation } from "react-router-dom"
import styled from "styled-components"
import { useLocation } from "wouter"
import Card from "@/components/Card"
import Loading from "@/components/Loading"
import PostCard from "@/components/PostCard"
import Tag from "@/components/Tag"
import TagList from "@/components/TagList"
import contentMap from "@/contentMap"
import Loading from "../../components/Loading"
import MainContent from "../../components/MainContent"
import PostCard from "../../components/PostCard"
import Tag from "../../components/Tag"
import TagList from "../../components/TagList"
import contentMap from "../../contentMap"
import NotFound from "../NotFound"
import {
categorizePageType,
@ -21,41 +23,32 @@ import Meta from "./Meta"
import SeriesControlButtons from "./SeriesControlButtons"
import Toc from "./Toc"
const StyledTitle = styled.h1`
margin-bottom: 1rem;
line-height: 2.5rem;
word-wrap: break-word;
`
export default function Page() {
const { pathname } = useLocation()
const [location] = useLocation()
const [pageData, setPageData] = useState<PageData | undefined>(undefined)
const [pageType, setPageType] = useState<PageType>(PageType.POST)
const [isLoading, setIsLoading] = useState(true)
const [isLoading, setLoading] = useState(true)
useTitle(pageData?.title || "Loading")
useMeta({ property: "og:title", content: pageData?.title })
// this code runs if either the url or the locale changes
useEffect(() => {
const content_id = pathname.replace(/\/$/, "") // remove trailing slash
const pageType = categorizePageType(content_id)
const content_id = location.replace(/\/$/, "") // remove trailing slash
fetchContent(pageType, content_id).then((fetched_content) => {
if (!fetched_content) {
// stop loading without fetching pageData so 404 page will display
setIsLoading(false)
fetchContent(content_id).then((fetched_content) => {
const pageType = categorizePageType(content_id)
// stop loading without setting pageData so 404 page will display
if (!fetched_content || pageType === undefined) {
setLoading(false)
return
}
setPageData(parsePageData(fetched_content, pageType, content_id))
setPageType(pageType)
setIsLoading(false)
setLoading(false)
})
}, [pathname])
}, [location])
if (isLoading) return <Loading />
@ -63,7 +56,7 @@ export default function Page() {
return (
<>
<MainContent>
<Card className="page">
{/* next/previous series post buttons */}
{pageType == PageType.SERIES && (
<SeriesControlButtons
@ -73,12 +66,12 @@ export default function Page() {
/>
)}
<StyledTitle>{pageData.title}</StyledTitle>
<h1 className="mb-4 leading-10">{pageData.title}</h1>
<small>
{/* Post tags */}
{pageData.tags.length > 0 && (
<TagList direction="left">
<TagList>
{pageData.tags.map((tag) => {
return (
<div key={pageData?.title + tag}>
@ -110,7 +103,7 @@ export default function Page() {
__html: pageData.content,
}}
/>
</MainContent>
</Card>
{/* series post list */}

View file

@ -4,26 +4,7 @@ import {
faListUl,
} from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { Link } from "react-router-dom"
import styled from "styled-components"
import buttonStyle from "../../styles/button"
const Container = styled.div`
display: flex;
justify-content: space-between;
`
const Button = styled.div`
${buttonStyle}
`
const DisabledButton = styled.div`
${buttonStyle}
color: grey;
cursor: default;
`
import { Link } from "wouter"
interface Props {
seriesHome: string
@ -31,40 +12,42 @@ interface Props {
nextURL?: string
}
function SeriesControlButtons({ prevURL, seriesHome, nextURL }: Props) {
export default function SeriesControlButtons({
prevURL,
seriesHome,
nextURL,
}: Props) {
return (
<Container>
<div className="mb-5 flex justify-between">
{prevURL ? (
<Link to={prevURL}>
<Button>
<button className="button">
<FontAwesomeIcon icon={faArrowLeft} />
</Button>
</button>
</Link>
) : (
<DisabledButton>
<button className="button-disabled">
<FontAwesomeIcon icon={faArrowLeft} />
</DisabledButton>
</button>
)}
<Link to={seriesHome}>
<Button>
<button className="button">
<FontAwesomeIcon icon={faListUl} />
</Button>
</button>
</Link>
{nextURL ? (
<Link to={nextURL}>
<Button>
<button className="button">
<FontAwesomeIcon icon={faArrowRight} />
</Button>
</button>
</Link>
) : (
<DisabledButton>
<button className="button-disabled">
<FontAwesomeIcon icon={faArrowRight} />
</DisabledButton>
</button>
)}
</Container>
</div>
)
}
export default SeriesControlButtons

View file

@ -0,0 +1,4 @@
/* https://github.com/kunukn/react-collapse */
.ReactCollapse--collapse {
transition: height 200ms ease-out !important;
}

View file

@ -1,59 +1,39 @@
import "./Toc.scss"
import { faCaretDown, faCaretUp } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import storage from "local-storage-fallback"
import { useEffect, useState } from "react"
import { Collapse } from "react-collapse"
import styled from "styled-components"
const StyledTocToggleButton = styled.button`
cursor: pointer;
border: none;
text-align: left;
background-color: rgba(0, 0, 0, 0);
width: 100%;
padding: 0.5rem;
color: ${({ theme }) => theme.theme.color.text.highContrast};
`
interface Props {
data?: string
}
const StyledCollapseContainer = styled.div`
* {
transition: height 200ms ease-out;
}
`
const Toc = (props: { data?: string }) => {
export default function Toc({ data }: Props) {
const [isTocOpened, setIsTocOpened] = useState(
storage.getItem("isTocOpened") == "true"
localStorage.getItem("isTocOpened") === "true"
)
useEffect(() => {
storage.setItem("isTocOpened", isTocOpened.toString())
localStorage.setItem("isTocOpened", isTocOpened.toString())
}, [isTocOpened])
if (!props.data) return <></>
if (!data) return <></>
return (
<>
<StyledTocToggleButton
onClick={() => {
setIsTocOpened((prev) => !prev)
}}
>
<button onClick={() => setIsTocOpened((prev) => !prev)}>
<strong>
Table of Contents
Table of Contents{" "}
<FontAwesomeIcon
icon={isTocOpened ? faCaretUp : faCaretDown}
/>
</strong>
</StyledTocToggleButton>
<StyledCollapseContainer>
<Collapse isOpened={isTocOpened}>
<div dangerouslySetInnerHTML={{ __html: props.data }} />
</Collapse>
</StyledCollapseContainer>
</button>
<Collapse isOpened={isTocOpened}>
<div dangerouslySetInnerHTML={{ __html: data }} />
</Collapse>
<hr />
</>
)
}
export default Toc

View file

@ -1,35 +1,25 @@
import portfolio from "@developomp-site/content/dist/portfolio.json"
import type { PageData } from "@developomp-site/content/src/types/types"
import contentMap from "../../contentMap"
import contentMap from "@/contentMap"
export enum PageType {
POST,
SERIES,
SERIES_HOME,
PORTFOLIO_PROJECT,
UNSEARCHABLE,
}
export async function fetchContent(pageType: PageType, url: string) {
export async function fetchContent(content_id: string) {
try {
if (pageType == PageType.UNSEARCHABLE) {
return await import(
`@developomp-site/content/dist/content/unsearchable${url}.json`
)
} else {
return await import(
`@developomp-site/content/dist/content${url}.json`
)
}
return await import(
`@developomp-site/content/dist/content${content_id}.json`
)
} catch (err) {
return
}
}
export function categorizePageType(content_id: string): PageType {
export function categorizePageType(content_id: string): PageType | undefined {
if (content_id.startsWith("/post")) return PageType.POST
if (content_id.startsWith("/portfolio")) return PageType.PORTFOLIO_PROJECT
if (content_id.startsWith("/series")) {
// if the URL looks like /series/series-title (if the url has two slashes)
if ([...(content_id.match(/\//g) || [])].length == 2)
@ -38,8 +28,6 @@ export function categorizePageType(content_id: string): PageType {
// if the URL looks like /series/series-title/post-title (if the url does not have 2 slashes)
return PageType.SERIES
}
return PageType.UNSEARCHABLE
}
export function parsePageData(
@ -69,7 +57,7 @@ export function parsePageData(
order: [],
length: 0,
// portfolio
// portfolio (unused)
image: "",
overview: "",
@ -137,31 +125,6 @@ export function parsePageData(
break
}
case PageType.PORTFOLIO_PROJECT: {
const data =
portfolio.projects[
content_id as keyof typeof portfolio.projects
]
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
pageData.title = data.name
pageData.image = data.image
pageData.overview = data.overview
pageData.badges = data.badges
pageData.repo = data.repo
break
}
case PageType.UNSEARCHABLE: {
pageData.title = contentMap.unsearchable[content_id].title
pageData.content = fetched_content.content
break
}
}
return pageData

View file

@ -1,28 +0,0 @@
import { DateRange } from "react-date-range"
import styled from "styled-components"
export const DateRangeControl = styled.div`
width: 350px;
@media screen and (max-width: ${(props) =>
props.theme.theme.maxDisplayWidth.mobile}) {
margin-top: 2rem;
}
`
export const ClearDateButton = styled.button`
width: 100%;
height: 2.5rem;
border: none;
cursor: pointer;
background-color: tomato; /* 🍅 mmm tomato 🍅 */
color: white;
font-weight: bold;
`
export const StyledDateRange = styled(DateRange)`
width: 100%;
height: 350px;
`

View file

@ -1,277 +0,0 @@
import "react-date-range/dist/styles.css"
import "react-date-range/dist/theme/default.css"
import searchData from "@developomp-site/content/dist/search.json"
import elasticlunr from "elasticlunr" // search engine
import { useMeta, useTitle } from "hoofd"
import { useCallback, useEffect, useState } from "react"
import { Range } from "react-date-range"
import { useSearchParams } from "react-router-dom"
import styled from "styled-components"
import Loading from "../../components/Loading"
import MainContent from "../../components/MainContent"
import PostCard from "../../components/PostCard"
import contentMap from "../../contentMap"
import { ClearDateButton, DateRangeControl, StyledDateRange } from "./DateRange"
import SearchBar from "./SearchBar"
import TagSelect, { TagsData } from "./TagSelect"
const searchIndex = elasticlunr.Index.load(searchData as never)
export interface SearchParams {
date_from: string
date_to: string
tags: string[]
query: string
}
const defaultDateRange = [
{
startDate: undefined,
endDate: undefined,
key: "selection",
},
]
const StyledSearch = styled(MainContent)`
text-align: center;
margin-bottom: 2rem;
`
const StyledSearchContainer = styled.div`
display: flex;
align-items: flex-start;
@media screen and (max-width: ${(props) =>
props.theme.theme.maxDisplayWidth.mobile}) {
flex-direction: column-reverse;
align-items: center;
}
`
const StyledSearchControlContainer = styled.div`
width: 100%;
margin-left: 1rem;
@media screen and (max-width: ${(props) =>
props.theme.theme.maxDisplayWidth.mobile}) {
margin-top: 2rem;
margin-left: 0;
}
`
// check if post date is withing the range
function isDateInRange(dateStringToCompare: string, range: Range): boolean {
if (!dateStringToCompare) throw Error("No date to compare")
const dateToCompare = new Date(dateStringToCompare)
const { startDate, endDate } = range
const startDateExists = !!startDate
const endDateExists = !!endDate
if (endDateExists && !startDateExists) return dateToCompare < endDate
if (startDateExists && !endDateExists) return dateToCompare > startDate
if (startDateExists && endDateExists)
return dateToCompare > startDate && dateToCompare < endDate
return true
}
function isSelectedTagsInPost(selectedTags?: TagsData[], postTags?: string[]) {
if (!selectedTags || selectedTags.length <= 0) return true
if (!postTags || postTags.length <= 0) return false
// if tag is empty or undefined
const tagValues = selectedTags.map((value) => value.value)
if (!postTags.every((val) => tagValues.includes(val))) return false
return true
}
const Search = () => {
// URL search parameters
const [URLSearchParams, setURLSearchParams] = useSearchParams()
const [initialized, setInitialized] = useState(false)
const [dateRange, setDateRange] = useState<Range[]>(defaultDateRange)
const [selectedTags, setSelectedTags] = useState<TagsData[]>([])
const [searchInput, setSearchInput] = useState("")
const [postCards, setPostCards] = useState<JSX.Element[]>([])
useTitle("Search")
useMeta({ property: "og:title", content: "Search" })
const doSearch = useCallback(() => {
try {
const _postCards: JSX.Element[] = []
for (const res of searchIndex.search(searchInput)) {
const postData = contentMap.posts[res.ref]
if (
postData && // if post data exists
isDateInRange(postData.date, dateRange[0]) && // date is within range
isSelectedTagsInPost(selectedTags, postData.tags) // if post include tags
) {
_postCards.push(
<PostCard
key={res.ref}
postData={{
content_id: res.ref,
...postData,
}}
/>
)
}
}
// apply search result
setPostCards(_postCards)
// eslint-disable-next-line no-empty
} catch (err) {
console.error(err)
}
}, [dateRange, selectedTags, searchInput])
// parse search parameters
useEffect(() => {
for (const [key, value] of URLSearchParams.entries()) {
switch (key) {
case "date_from":
setDateRange((prev) => [
{ ...prev[0], startDate: new Date(value) },
])
break
case "date_to":
setDateRange((prev) => [
{ ...prev[0], endDate: new Date(value) },
])
break
case "tags":
setSelectedTags(
value.split(",").map((elem) => {
return { value: elem, label: elem }
})
)
break
case "query":
setSearchInput(value)
break
}
}
setInitialized(true)
}, [])
// update URL when data changes
useEffect(() => {
if (!initialized) return
let date_from
let date_to
// convert Date to YYYY-MM-DD string if it exists
if (dateRange[0].startDate)
date_from = dateRange[0].startDate.toISOString().split("T")[0]
if (dateRange[0].endDate)
date_to = dateRange[0].endDate.toISOString().split("T")[0]
setURLSearchParams({
...(date_from && {
date_from: date_from,
}),
...(date_to && {
date_to: date_to,
}),
...(selectedTags.length > 0 && {
tags: selectedTags.map((value) => value.value).join(","),
}),
...(searchInput && {
query: searchInput,
}),
})
}, [dateRange, selectedTags, searchInput])
// run search if date range and selected tags change
useEffect(() => {
doSearch()
}, [dateRange, selectedTags])
// run search if user stops typing
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
doSearch()
}, 200)
return () => clearTimeout(delayDebounceFn)
}, [searchInput])
if (!initialized) return <Loading />
return (
<>
<StyledSearch>
<h1>Search</h1>
<StyledSearchContainer>
<DateRangeControl>
<ClearDateButton
onClick={() => {
setDateRange(defaultDateRange)
}}
>
Reset date range
</ClearDateButton>
<StyledDateRange
editableDateInputs
retainEndDateOnFirstSelection
moveRangeOnFirstSelection={false}
ranges={dateRange}
onChange={(rangesByKey) => {
setDateRange([rangesByKey.selection])
}}
/>
</DateRangeControl>
<StyledSearchControlContainer
onSubmit={(event) => event.preventDefault()}
>
<SearchBar
autoFocus
type="search"
value={searchInput}
autoComplete="off"
placeholder="Search"
onChange={(event) =>
setSearchInput(event.target.value)
}
onKeyPress={(event) => {
event.key === "Enter" &&
searchInput &&
doSearch()
}}
/>
{postCards.length} result{postCards.length > 1 && "s"}
<TagSelect
defaultValue={selectedTags}
onChange={(newValue) => {
setSelectedTags(newValue as TagsData[])
}}
/>
</StyledSearchControlContainer>
</StyledSearchContainer>
</StyledSearch>
{postCards}
</>
)
}
export default Search

View file

@ -1,31 +0,0 @@
import styled from "styled-components"
export default styled.input`
width: 100%;
border-radius: 100px; /* arbitrarily large value */
height: 2.5rem;
text-align: center;
font-size: 1.2rem;
outline: none;
color: ${({ theme }) => theme.theme.color.text.default};
border: 1px solid
${(props) => props.theme.theme.component.input.color.border.default};
background-color: ${(props) =>
props.theme.theme.component.input.color.background.default};
::placeholder {
color: ${(props) =>
props.theme.theme.component.input.color.placeHolder};
opacity: 1;
}
&:hover {
border: 1px solid
${({ theme }) => theme.theme.component.input.color.border.hover};
}
&:focus {
border: 1px solid
${({ theme }) => theme.theme.component.input.color.border.focus};
}
`

View file

@ -1,113 +0,0 @@
import { useContext } from "react"
import Select from "react-select"
import styled from "styled-components"
import contentMap from "../../contentMap"
import { globalContext } from "../../globalContext"
const StyledReactTagsContainer = styled.div`
width: 100%;
margin-top: 1.5rem;
`
export interface TagsData {
value: string
label: string
}
const options: TagsData[] = contentMap.meta.tags.map((elem) => ({
value: elem,
label: elem,
}))
interface TagSelectProps {
defaultValue: TagsData[]
onChange(newValue: unknown): void
}
const TagSelect = (props: TagSelectProps) => {
const { globalState } = useContext(globalContext)
const { theme } = globalState
const { onChange, defaultValue: selectedTags } = props
return (
<StyledReactTagsContainer>
<Select
placeholder="Select tags..."
theme={(reactSelectTheme) => ({
...reactSelectTheme,
colors: {
...reactSelectTheme.colors,
neutral0:
theme.component.input.color.background.default,
neutral5: "hsl(0, 0%, 20%)",
neutral10: "hsl(0, 0%, 30%)",
neutral20: "hsl(0, 0%, 40%)",
neutral30: "hsl(0, 0%, 50%)",
neutral40: "hsl(0, 0%, 60%)",
neutral50: "hsl(0, 0%, 70%)",
neutral60: "hsl(0, 0%, 80%)",
neutral70: "hsl(0, 0%, 90%)",
neutral80: "hsl(0, 0%, 95%)",
neutral90: "hsl(0, 0%, 100%)",
primary25: "hotpink",
primary: "black",
},
})}
styles={{
option: (styles) => ({
...styles,
backgroundColor:
theme.component.input.color.background.default,
color: theme.color.text.default,
cursor: "pointer",
":hover": {
backgroundColor:
theme.component.input.color.background
.itemHover,
},
}),
control: (styles) => ({
...styles,
backgroundColor:
theme.component.input.color.background.default,
border: `1px solid ${theme.component.input.color.border.default}`,
":hover": {
border: `1px solid ${theme.component.input.color.border.hover}`,
},
":focus": {
border: `1px solid ${theme.component.input.color.border.focus}`,
},
}),
multiValue: (styles) => ({
...styles,
color: theme.color.text.default,
backgroundColor:
theme.component.ui.color.background.default,
borderRadius: "10px",
}),
multiValueLabel: (styles) => ({
...styles,
color: theme.color.text.default,
marginLeft: "0.2rem",
}),
multiValueRemove: (styles) => ({
...styles,
marginRight: "0.3rem",
cursor: "pointer",
color: theme.component.input.color.placeHolder,
":hover": {
color: theme.color.text.default,
},
}),
}}
defaultValue={selectedTags}
onChange={onChange}
options={options}
isMulti
/>
</StyledReactTagsContainer>
)
}
export default TagSelect

View file

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

View file

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View file

@ -0,0 +1,13 @@
a {
@apply text-anchor no-underline hover:text-anchor-accent active:text-anchor-accent;
}
/* The "#" thingy used beside headers */
a.header-anchor {
@apply inline-block text-light-anchor-header dark:text-dark-anchor-header;
}
/* footnote anchors */
a[id^="fnref"] {
@apply inline;
}

View file

@ -1,31 +0,0 @@
import { css } from "styled-components"
export default css`
a {
text-decoration: none;
color: ${(props) => props.theme.theme.component.anchor.color.default};
&:hover {
color: ${(props) => props.theme.theme.component.anchor.color.hover};
}
&:active {
color: ${(props) =>
props.theme.theme.component.anchor.color.active};
}
}
/* The "#" thingy used beside headers */
a.header-anchor {
/* compensate for navbar height*/
display: inline-block;
color: ${(props) => props.theme.theme.component.anchor.color.header};
}
/* footnote anchors */
a[id^="fnref"] {
display: inline;
}
`

View file

@ -0,0 +1,3 @@
blockquote {
@apply mx-2 border-l-[6px] border-l-light-blockquote-accent bg-light-blockquote-bg py-5 pl-10 dark:border-l-dark-blockquote-accent dark:bg-dark-blockquote-bg;
}

View file

@ -1,19 +0,0 @@
import { css } from "styled-components"
export default css`
blockquote {
background-color: ${({ theme }) =>
theme.theme.component.blockQuote.color.background};
border-left: 0.4rem solid
${({ theme }) => theme.theme.component.blockQuote.color.borderLeft};
padding-top: 0.1rem;
padding-right: 1rem;
padding-bottom: 0.1rem;
padding-left: 1.5rem;
@media screen and (max-width: ${({ theme }) =>
theme.theme.maxDisplayWidth.mobile}) {
margin: 0.5rem;
}
}
`

View file

@ -0,0 +1,29 @@
.button {
/* style */
@apply flex items-center justify-center rounded-lg border-none;
/* spacing */
@apply m-0 h-12 px-4;
min-width: 4.5rem;
/* text */
@apply no-underline;
/* color */
@apply bg-light-ui text-light-text-default hover:bg-light-ui-hover dark:bg-dark-ui dark:text-dark-text-default dark:hover:bg-dark-ui-hover;
/* animation */
transition: transform 0.1s linear;
}
.button-disabled {
@extend .button;
@apply cursor-default text-gray-500 hover:bg-light-ui dark:text-gray-500 dark:hover:bg-dark-ui;
}

View file

@ -1,37 +0,0 @@
import { css } from "styled-components"
export default css`
/* style */
display: flex;
cursor: pointer;
align-items: center;
justify-content: center;
border: none;
border-radius: 0.5rem;
/* size */
height: 3rem;
min-width: 2.5rem;
margin: 0;
padding: 0 1rem 0 1rem;
/* text */
text-decoration: none;
/* color */
color: ${({ theme }) => theme.theme.color.text.default};
background-color: ${({ theme }) =>
theme.theme.component.ui.color.background.default};
&:hover {
background-color: ${({ theme }) =>
theme.theme.component.ui.color.background.hover};
}
/* animation */
transition: transform 0.1s linear;
`

View file

@ -0,0 +1,13 @@
input[type="checkbox"] {
/* default width and height */
width: 13px;
height: 13px;
}
input[type="checkbox"][disabled][checked] {
filter: invert(100%) brightness(5);
}
input[type="checkbox"][disabled] {
filter: invert(100%) brightness(5);
}

View file

@ -1,17 +0,0 @@
import { css } from "styled-components"
export default css`
input[type="checkbox"] {
/* default width and height */
width: 13px;
height: 13px;
}
input[type="checkbox"][disabled][checked] {
filter: invert(100%) brightness(5);
}
input[type="checkbox"][disabled] {
filter: invert(100%) brightness(5);
}
`

View file

@ -0,0 +1,39 @@
:not(.dark) {
@import "highlight.js/styles/default";
}
:is(.dark) {
@import "highlight.js/styles/atom-one-dark-reasonable";
}
code {
@apply border font-source-code-pro;
}
/* inline code */
:not(pre) > code {
word-wrap: break-word;
border-radius: 3px;
padding: 0 3px;
@apply border-[1px] border-solid;
/* color */
@apply border-light-inline-code-border bg-light-inline-code-bg text-light-inline-code-text dark:border-dark-inline-code-border dark:bg-dark-inline-code-bg dark:text-dark-inline-code-text;
}
/* code block */
pre > code {
@apply border-light-block-code-border dark:border-dark-block-code-border;
}
/* // todo: fix highlight not working properly when scrolled horizontally // */
.highlighted-line {
@apply bg-light-block-code-highlight dark:bg-dark-block-code-highlight;
display: block;
min-width: min-content;
margin: 0 -1rem;
padding: 0 1rem;
}

View file

@ -1,37 +0,0 @@
import { css } from "styled-components"
export default css`
/* highlight.js code style */
${({ theme }) => theme.theme.component.code.block.style}
/* inline code */
:not(pre) > code {
font-family: ${({ theme }) => theme.theme.font.monospace};
word-wrap: break-word;
color: ${({ theme }) => theme.theme.component.code.inline.color.text};
background-color: ${({ theme }) =>
theme.theme.component.code.inline.color.background};
border: 1px solid
${({ theme }) => theme.theme.component.code.inline.color.border};
border-radius: 3px;
padding: 0 3px;
}
/* code block */
pre > code {
font-family: ${(props) => props.theme.theme.font.monospace};
border: 1px solid
${({ theme }) => theme.theme.component.code.block.color.border};
}
/* // todo: fix highlight not working properly when scrolled horizontally // */
.highlighted-line {
background-color: ${({ theme }) =>
theme.theme.component.code.block.color.highlight};
display: block;
min-width: min-content;
margin: 0 -1rem;
padding: 0 1rem;
}
`

View file

@ -0,0 +1,28 @@
html,
body,
#root {
/* style */
@apply flex flex-col;
/* spacing */
@apply m-0 min-h-screen;
/* text */
@apply font-noto-sans text-base font-normal leading-8 antialiased;
/* color */
@apply bg-light-ui-bg fill-light-text-default text-light-text-default dark:bg-dark-ui-bg dark:fill-dark-text-default dark:text-dark-text-default;
}
* {
/* transitions */
transition: background-color 0.1s, transform 0.1s;
/* scrolling */
@apply scroll-m-16 scroll-smooth;
}

View file

@ -1,76 +0,0 @@
import "katex/dist/katex.min.css"
import { createGlobalStyle, css } from "styled-components"
import anchorCSS from "./anchor"
import blockquoteCSS from "./blockQuote"
import checkbox from "./checkbox"
import codeCSS from "./code"
import headerCSS from "./header"
import hrCSS from "./hr"
import katexCSS from "./katex"
import kbdCSS from "./kbd"
import markCSS from "./mark"
import scrollbarCSS from "./scrollbar"
import tableCSS from "./table"
const globalCSS = css`
body {
overflow-x: hidden;
overflow-y: scroll;
}
html,
body,
#root {
/* size */
min-height: 100vh;
margin: 0;
/* style */
display: flex;
flex-flow: column;
/* text */
line-height: 2rem;
font-size: 1rem;
font-family: ${({ theme }) => theme.theme.font.sansSerif};
font-weight: 400;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
/* color */
background-color: ${({ theme }) => theme.theme.color.background};
color: ${({ theme }) => theme.theme.color.text.default};
}
* {
transition: color 0.1s linear;
scroll-behavior: smooth;
scroll-margin: 4rem;
}
`
/**
* Theme that will be used throughout the website
* prettier extension does not work here
* see https://github.com/styled-components/vscode-styled-components/issues/175
*/
export default createGlobalStyle`
${anchorCSS}
${scrollbarCSS}
${checkbox}
${codeCSS}
${kbdCSS}
${tableCSS}
${blockquoteCSS}
${hrCSS}
${headerCSS}
${markCSS}
${katexCSS}
${globalCSS}
`

View file

@ -1,38 +0,0 @@
import { css } from "styled-components"
export default css`
/* intentionally left out h1 */
h2,
h3,
h4,
h5,
h6 {
margin-top: 3rem;
padding-top: 0.5rem;
margin-bottom: 0.5rem;
font-weight: 700;
}
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 1.5rem;
}
h3 {
font-size: 1rem;
text-indent: 0.5rem;
}
h4 {
font-size: 1rem;
text-indent: 1rem;
}
h5 {
font-size: 1rem;
text-indent: 1.5rem;
}
h6 {
font-size: 1rem;
text-indent: 2rem;
}
`

View file

@ -0,0 +1,32 @@
h1,
h2,
h3,
h4,
h5,
h6 {
@apply py-2 font-bold;
}
h1 {
@apply text-5xl;
}
h2 {
@apply text-3xl;
}
h3 {
@apply text-2xl;
}
h4 {
@apply text-xl;
}
h5 {
@apply text-base;
}
h6 {
@apply text-sm;
}

View file

@ -0,0 +1,6 @@
hr {
@apply my-2;
border: 0;
border-bottom: 1px solid;
}

View file

@ -1,8 +0,0 @@
import { css } from "styled-components"
export default css`
hr {
border: 0;
border-bottom: 1px solid;
}
`

View file

@ -0,0 +1,7 @@
img {
max-width: 100%;
}
table img {
max-width: fit-content;
}

View file

@ -0,0 +1,5 @@
/* prevent overflowing on small displays */
.katex-html {
overflow: auto;
padding: 0.5rem;
}

View file

@ -1,9 +0,0 @@
import { css } from "styled-components"
export default css`
// prevent overflowing on small displays
.katex-html {
overflow: auto;
padding: 0.5rem;
}
`

View file

@ -0,0 +1,20 @@
/* https://www.rgagnon.com/jsdetails/js-nice-effect-the-KBD-tag.html */
kbd {
margin: 0px 0.1em;
padding: 0.1em 0.6em;
border-radius: 3px;
line-height: 1.4;
font-size: 13.5px;
display: inline-block;
border: 1px solid;
box-shadow: 0px 1px 0px #00000033, inset 0px 0px 0px 2px white;
@apply border-light-kbd-border bg-light-kbd-bg text-light-kbd-text dark:border-dark-kbd-border dark:bg-dark-kbd-bg dark:text-dark-kbd-text;
}
:is(.dark) {
kbd {
box-shadow: 0px 1px 0px #ffffff4d, inset 0px 0px 0px 2px black;
}
}

View file

@ -1,22 +0,0 @@
import { css } from "styled-components"
export default css`
/* https://www.rgagnon.com/jsdetails/js-nice-effect-the-KBD-tag.html */
kbd {
margin: 0px 0.1em;
padding: 0.1em 0.6em;
border-radius: 3px;
border: 1px solid
${({ theme }) => theme.theme.component.kbd.color.border};
color: ${({ theme }) => theme.theme.component.kbd.color.text};
line-height: 1.4;
font-size: 13.5px;
display: inline-block;
box-shadow: 0px 1px 0px
${({ theme }) => theme.theme.component.kbd.color.outerShadow},
inset 0px 0px 0px 2px
${({ theme }) => theme.theme.component.kbd.color.innerShadow};
background-color: ${({ theme }) =>
theme.theme.component.kbd.color.background};
}
`

View file

@ -0,0 +1,12 @@
ul,
ol {
@apply pl-11;
}
ul {
@apply list-disc;
}
ol {
@apply list-decimal;
}

View file

@ -0,0 +1,3 @@
mark {
@apply bg-light-mark-bg text-light-mark-text dark:bg-dark-mark-bg dark:text-dark-mark-text;
}

View file

@ -1,9 +0,0 @@
import { css } from "styled-components"
export default css`
mark {
background-color: ${({ theme }) =>
theme.theme.component.mark.color.background};
color: ${({ theme }) => theme.theme.component.mark.color.text};
}
`

View file

@ -0,0 +1,14 @@
body::-webkit-scrollbar {
@apply w-2;
}
body::-webkit-scrollbar-track {
@apply bg-light-scrollbar-track dark:bg-dark-scrollbar-track;
}
body::-webkit-scrollbar-thumb {
@apply rounded-full;
/* color */
@apply bg-light-scrollbar-thumb dark:bg-dark-scrollbar-thumb;
}

View file

@ -1,23 +0,0 @@
import { css } from "styled-components"
export default css`
body::-webkit-scrollbar {
width: ${(props) => props.theme.theme.component.scrollbar.width};
}
body::-webkit-scrollbar-track {
border-radius: ${(props) =>
props.theme.theme.component.scrollbar.borderRadius};
background: ${(props) =>
props.theme.theme.component.scrollbar.color.track};
box-shadow: inset 0 0 5px rgb(0 0 0 / 10%);
}
body::-webkit-scrollbar-thumb {
border-radius: ${(props) =>
props.theme.theme.component.scrollbar.borderRadius};
background: ${(props) =>
props.theme.theme.component.scrollbar.color.thumb};
box-shadow: inset 0 0 10px rgb(0 0 0 / 20%);
}
`

View file

@ -0,0 +1,4 @@
sub,
sup {
font-size: smaller;
}

View file

@ -0,0 +1,13 @@
table {
@apply w-full border-collapse border-spacing-0;
td,
th {
@apply border border-light-table-border p-2 dark:border-dark-table-border;
}
/* table alternating color */
tr:nth-child(even) {
@apply bg-light-table-alt dark:bg-dark-table-alt;
}
}

View file

@ -1,22 +0,0 @@
import { css } from "styled-components"
export default css`
table {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
td,
th {
padding: 8px;
border: 1px solid
${({ theme }) => theme.theme.component.table.color.border};
}
/* table alternating color */
tr:nth-child(even) {
background-color: ${({ theme }) =>
theme.theme.component.table.color.even};
}
}
`

57
apps/blog/src/theme.ts Normal file
View file

@ -0,0 +1,57 @@
import { create } from "zustand"
const themeKey = "theme"
export enum Theme {
Dark = "dark",
Light = "light",
}
export type ThemeState = {
theme: Theme
setTheme: (theme: Theme) => void
}
/**
* Reads site theme setting from local storage
*/
function getStoredThemeSetting(): Theme {
const storedTheme = localStorage.getItem(themeKey)
// fix invalid values
if (
!storedTheme ||
(storedTheme != Theme.Dark && storedTheme != Theme.Light)
) {
localStorage.setItem(themeKey, Theme.Dark)
return Theme.Dark
}
return storedTheme
}
/**
* Applies tailwind theme using classes based on current theme setting
*/
function applyTheme() {
if (getStoredThemeSetting() === Theme.Dark) {
document.documentElement.classList.add("dark")
} else {
document.documentElement.classList.remove("dark")
}
}
export const useTheme = create<ThemeState>()((set) => {
applyTheme()
return {
theme: getStoredThemeSetting(),
setTheme: (themeSetting: Theme) => {
localStorage.setItem(themeKey, themeSetting)
applyTheme()
set((state) => ({
...state,
theme: getStoredThemeSetting(),
}))
},
}
})

View file

@ -1,56 +0,0 @@
// import "./DarkMode.css"
import { ChangeEventHandler } from "react"
/* NEW (START) */
const setDark = () => {
localStorage.setItem("theme", "dark")
document.documentElement.setAttribute("data-theme", "dark")
}
const setLight = () => {
localStorage.setItem("theme", "light")
document.documentElement.setAttribute("data-theme", "light")
}
const storedTheme = localStorage.getItem("theme")
const prefersDark =
window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
const defaultDark =
storedTheme === "dark" || (storedTheme === null && prefersDark)
if (defaultDark) {
setDark()
}
const toggleTheme: ChangeEventHandler<HTMLInputElement> = (e) => {
if (e.target.checked) {
setDark()
} else {
setLight()
}
}
/* NEW (END) */
const DarkMode = () => {
return (
<div className="toggle-theme-wrapper">
<span></span>
<label className="toggle-theme" htmlFor="checkbox">
<input
type="checkbox"
id="checkbox"
// NEW
onChange={toggleTheme}
defaultChecked={defaultDark}
/>
<div className="slider round"></div>
</label>
<span>🌒</span>
</div>
)
}
export default DarkMode

1
apps/blog/src/vite-env.d.ts vendored Normal file
View file

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

View file

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

View file

@ -1,28 +1,31 @@
{
"compilerOptions": {
"plugins": [
{
"name": "@styled/typescript-styled-plugin",
"validate": false
}
],
"target": "es5",
"module": "esnext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"downlevelIteration": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"moduleResolution": "node",
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
/* Absolute import */
"baseUrl": "./src",
"paths": {
"@/*": ["./*"]
}
},
"include": ["src/**/*", "types/**/*"]
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}

View file

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

View file

@ -1,7 +0,0 @@
import "react-date-range"
declare module "react-date-range" {
export interface DateRangeProps extends Range, CommonCalendarProps {
retainEndDateOnFirstSelection?: boolean | undefined
}
}

View file

@ -1,19 +0,0 @@
declare module "read-time-estimate" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function toc(
string: string,
customWordTime: number,
customImageTime: number,
chineseKoreanReadTime: number,
imageTags: string[]
): {
humanizedDuration: string
duration: number
totalWords: number
wordTime: number
totalImages: number
imageTime: number
otherLanguageTimeCharacters: number
otherLanguageTime: number
}
}

View file

@ -1,12 +0,0 @@
import "styled-components"
import type { Theme } from "@developomp-site/theme"
import { SiteTheme } from "../src/globalContext"
declare module "styled-components" {
export interface DefaultTheme {
currentTheme: SiteTheme
theme: Theme
}
}

18
apps/blog/vite.config.ts Normal file
View file

@ -0,0 +1,18 @@
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,
},
}))