feat(blog): port from CRA to vite + react
This commit is contained in:
parent
8243d38270
commit
e48b65b14c
109 changed files with 1493 additions and 10360 deletions
7
.vscode/settings.json
vendored
7
.vscode/settings.json
vendored
|
@ -38,6 +38,7 @@
|
|||
"inqling",
|
||||
"Jimin",
|
||||
"katex",
|
||||
"kunukn",
|
||||
"Librewolf",
|
||||
"linaria",
|
||||
"nodedotjs",
|
||||
|
@ -63,7 +64,8 @@
|
|||
"wbtimeline",
|
||||
"webassembly",
|
||||
"wouter",
|
||||
"YYYYMMDD"
|
||||
"YYYYMMDD",
|
||||
"zustand"
|
||||
],
|
||||
"eslint.workingDirectories": [{ "mode": "auto" }],
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
|
@ -82,5 +84,6 @@
|
|||
},
|
||||
"[xml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-xml"
|
||||
}
|
||||
},
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
|
|
@ -14,10 +14,9 @@
|
|||
- `blog` - https://blog.developomp.com
|
||||
- `portfolio` - https://portfolio.developomp.com
|
||||
- `packages` - shared stuff used across different packages
|
||||
- `content` - Contents for the blog
|
||||
- `content` - Shared content
|
||||
- `eslint-config` - ESLint configuration files
|
||||
- `tailwind-config` -tailwindCSS configuration files
|
||||
- `theme` - universal developomp theme
|
||||
- `tailwind-config` -tailwind configuration with pomp-specific extensions
|
||||
|
||||
## Setting Up
|
||||
|
||||
|
|
2
apps/blog/.eslintignore
Normal file
2
apps/blog/.eslintignore
Normal file
|
@ -0,0 +1,2 @@
|
|||
/dist/
|
||||
/node_modules/
|
|
@ -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
14
apps/blog/.eslintrc.cjs
Normal 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
176
apps/blog/.gitignore
vendored
|
@ -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
36
apps/blog/index.html
Normal 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>
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
6
apps/blog/postcss.config.js
Normal file
6
apps/blog/postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
1
apps/blog/public/favicon.svg
Normal file
1
apps/blog/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 4.9 KiB |
BIN
apps/blog/public/img/nojs.avif
Normal file
BIN
apps/blog/public/img/nojs.avif
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
|
@ -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>
|
|
@ -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 ? (
|
||||
<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 />
|
||||
) : (
|
||||
<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>
|
||||
</Route>
|
||||
<Route>
|
||||
<Page />
|
||||
</Route>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
import Buttons from "./Buttons"
|
||||
|
||||
export default Buttons
|
|
@ -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"
|
||||
<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
|
||||
|
|
|
@ -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}
|
||||
`
|
|
@ -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
|
||||
|
|
13
apps/blog/src/components/Header/SearchButton.tsx
Normal file
13
apps/blog/src/components/Header/SearchButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
24
apps/blog/src/components/Header/ThemeToggleButton.tsx
Normal file
24
apps/blog/src/components/Header/ThemeToggleButton.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
import Navbar from "./Header"
|
||||
import Header from "./Header"
|
||||
|
||||
export default Navbar
|
||||
export default Header
|
||||
|
|
|
@ -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
|
73
apps/blog/src/components/Loading/Loading.scss
Normal file
73
apps/blog/src/components/Loading/Loading.scss
Normal 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;
|
||||
}
|
||||
}
|
18
apps/blog/src/components/Loading/Loading.tsx
Normal file
18
apps/blog/src/components/Loading/Loading.tsx
Normal 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>
|
||||
)
|
||||
}
|
44
apps/blog/src/components/Loading/TeaCup.tsx
Normal file
44
apps/blog/src/components/Loading/TeaCup.tsx
Normal 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>
|
||||
)
|
||||
}
|
3
apps/blog/src/components/Loading/index.ts
Normal file
3
apps/blog/src/components/Loading/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Loading from "./Loading"
|
||||
|
||||
export default Loading
|
|
@ -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
|
|
@ -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"}
|
||||
<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)"}
|
||||
</Title>
|
||||
|
||||
<br />
|
||||
|
||||
<MetaContainer>
|
||||
<TagList direction="left">
|
||||
</h2>
|
||||
<small>
|
||||
<TagList>
|
||||
{tags &&
|
||||
tags.map((tag) => {
|
||||
return <Tag key={title + tag} text={tag} />
|
||||
})}
|
||||
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={faHourglass} />
|
||||
|
||||
{readTime ? readTime + " read" : "unknown read time"}
|
||||
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
|
||||
{readTime
|
||||
? readTime + " read"
|
||||
: "unknown read time"}
|
||||
<FontAwesomeIcon icon={faHourglass} />
|
||||
{typeof wordCount === "number"
|
||||
? wordCount + " words"
|
||||
: "unknown length"}
|
||||
</MetaContainer>
|
||||
</PostCardContainer>
|
||||
</StyledPostCard>
|
||||
</div>
|
||||
</small>
|
||||
</Card>
|
||||
</a>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
export default PostCard
|
||||
|
|
|
@ -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} /> {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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
3
apps/blog/src/index.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
|
@ -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
29
apps/blog/src/main.tsx
Normal 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 />
|
||||
)
|
|
@ -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>
|
||||
<div className="flex h-full w-full flex-col items-center gap-8">
|
||||
<h1>Recent Posts</h1>
|
||||
|
||||
{postCards}
|
||||
|
||||
{postsLength > howMany && (
|
||||
{totalPosts > howMany && (
|
||||
<ShowMoreButton
|
||||
action={() => {
|
||||
setHowMany((prev) => prev + 5)
|
||||
}}
|
||||
action={() => setHowMany((prev) => prev + 10)}
|
||||
/>
|
||||
)}
|
||||
</PostList>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
15
apps/blog/src/pages/NotFound/NotFound.tsx
Normal file
15
apps/blog/src/pages/NotFound/NotFound.tsx
Normal 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>
|
||||
)
|
||||
}
|
3
apps/blog/src/pages/NotFound/index.ts
Normal file
3
apps/blog/src/pages/NotFound/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import NotFound from "./NotFound"
|
||||
|
||||
export default NotFound
|
|
@ -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
|
||||
|
|
9
apps/blog/src/pages/Page/Page.scss
Normal file
9
apps/blog/src/pages/Page/Page.scss
Normal file
|
@ -0,0 +1,9 @@
|
|||
.page {
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply mb-2 mt-16;
|
||||
}
|
||||
}
|
|
@ -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 content_id = location.replace(/\/$/, "") // remove trailing slash
|
||||
|
||||
fetchContent(content_id).then((fetched_content) => {
|
||||
const pageType = categorizePageType(content_id)
|
||||
|
||||
fetchContent(pageType, content_id).then((fetched_content) => {
|
||||
if (!fetched_content) {
|
||||
// stop loading without fetching pageData so 404 page will display
|
||||
setIsLoading(false)
|
||||
|
||||
// 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 */}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
4
apps/blog/src/pages/Page/Toc.scss
Normal file
4
apps/blog/src/pages/Page/Toc.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
/* https://github.com/kunukn/react-collapse */
|
||||
.ReactCollapse--collapse {
|
||||
transition: height 200ms ease-out !important;
|
||||
}
|
|
@ -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>
|
||||
</button>
|
||||
<Collapse isOpened={isTocOpened}>
|
||||
<div dangerouslySetInnerHTML={{ __html: props.data }} />
|
||||
<div dangerouslySetInnerHTML={{ __html: data }} />
|
||||
</Collapse>
|
||||
</StyledCollapseContainer>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toc
|
||||
|
|
|
@ -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`
|
||||
`@developomp-site/content/dist/content${content_id}.json`
|
||||
)
|
||||
} else {
|
||||
return await import(
|
||||
`@developomp-site/content/dist/content${url}.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
|
||||
|
|
|
@ -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;
|
||||
`
|
|
@ -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
|
|
@ -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};
|
||||
}
|
||||
`
|
|
@ -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
|
|
@ -1,3 +0,0 @@
|
|||
import Search from "./Search"
|
||||
|
||||
export default Search
|
1
apps/blog/src/react-app-env.d.ts
vendored
1
apps/blog/src/react-app-env.d.ts
vendored
|
@ -1 +0,0 @@
|
|||
/// <reference types="react-scripts" />
|
13
apps/blog/src/styles/anchor.scss
Normal file
13
apps/blog/src/styles/anchor.scss
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
`
|
3
apps/blog/src/styles/blockQuote.scss
Normal file
3
apps/blog/src/styles/blockQuote.scss
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
29
apps/blog/src/styles/button.scss
Normal file
29
apps/blog/src/styles/button.scss
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
`
|
13
apps/blog/src/styles/checkbox.scss
Normal file
13
apps/blog/src/styles/checkbox.scss
Normal 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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
`
|
39
apps/blog/src/styles/code.scss
Normal file
39
apps/blog/src/styles/code.scss
Normal 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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
`
|
28
apps/blog/src/styles/global.scss
Normal file
28
apps/blog/src/styles/global.scss
Normal 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;
|
||||
}
|
|
@ -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}
|
||||
`
|
|
@ -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;
|
||||
}
|
||||
`
|
32
apps/blog/src/styles/heading.scss
Normal file
32
apps/blog/src/styles/heading.scss
Normal 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;
|
||||
}
|
6
apps/blog/src/styles/hr.scss
Normal file
6
apps/blog/src/styles/hr.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
hr {
|
||||
@apply my-2;
|
||||
|
||||
border: 0;
|
||||
border-bottom: 1px solid;
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
import { css } from "styled-components"
|
||||
|
||||
export default css`
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
`
|
7
apps/blog/src/styles/img.scss
Normal file
7
apps/blog/src/styles/img.scss
Normal file
|
@ -0,0 +1,7 @@
|
|||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
table img {
|
||||
max-width: fit-content;
|
||||
}
|
5
apps/blog/src/styles/katex.scss
Normal file
5
apps/blog/src/styles/katex.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
/* prevent overflowing on small displays */
|
||||
.katex-html {
|
||||
overflow: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
import { css } from "styled-components"
|
||||
|
||||
export default css`
|
||||
// prevent overflowing on small displays
|
||||
.katex-html {
|
||||
overflow: auto;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
`
|
20
apps/blog/src/styles/kbd.scss
Normal file
20
apps/blog/src/styles/kbd.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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};
|
||||
}
|
||||
`
|
12
apps/blog/src/styles/list.scss
Normal file
12
apps/blog/src/styles/list.scss
Normal file
|
@ -0,0 +1,12 @@
|
|||
ul,
|
||||
ol {
|
||||
@apply pl-11;
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply list-disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
@apply list-decimal;
|
||||
}
|
3
apps/blog/src/styles/mark.scss
Normal file
3
apps/blog/src/styles/mark.scss
Normal 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;
|
||||
}
|
|
@ -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};
|
||||
}
|
||||
`
|
14
apps/blog/src/styles/scrollbar.scss
Normal file
14
apps/blog/src/styles/scrollbar.scss
Normal 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;
|
||||
}
|
|
@ -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%);
|
||||
}
|
||||
`
|
4
apps/blog/src/styles/subSup.scss
Normal file
4
apps/blog/src/styles/subSup.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
sub,
|
||||
sup {
|
||||
font-size: smaller;
|
||||
}
|
13
apps/blog/src/styles/table.scss
Normal file
13
apps/blog/src/styles/table.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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
57
apps/blog/src/theme.ts
Normal 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(),
|
||||
}))
|
||||
},
|
||||
}
|
||||
})
|
|
@ -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
1
apps/blog/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
5
apps/blog/tailwind.config.js
Normal file
5
apps/blog/tailwind.config.js
Normal 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}"],
|
||||
}
|
|
@ -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" }]
|
||||
}
|
||||
|
|
10
apps/blog/tsconfig.node.json
Normal file
10
apps/blog/tsconfig.node.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
7
apps/blog/types/react-date-range.d.ts
vendored
7
apps/blog/types/react-date-range.d.ts
vendored
|
@ -1,7 +0,0 @@
|
|||
import "react-date-range"
|
||||
|
||||
declare module "react-date-range" {
|
||||
export interface DateRangeProps extends Range, CommonCalendarProps {
|
||||
retainEndDateOnFirstSelection?: boolean | undefined
|
||||
}
|
||||
}
|
19
apps/blog/types/read-time-estimate.d.ts
vendored
19
apps/blog/types/read-time-estimate.d.ts
vendored
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
18
apps/blog/vite.config.ts
Normal 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,
|
||||
},
|
||||
}))
|
|
@ -19,7 +19,9 @@
|
|||
"@types/tinycolor2": "^1.4.3",
|
||||
"canvas": "^2.11.2",
|
||||
"ejs": "^3.1.8",
|
||||
"elasticlunr": "^0.9.5",
|
||||
"gray-matter": "^4.0.3",
|
||||
"jsdom": "^22.1.0",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-anchor": "^8.6.5",
|
||||
"markdown-it-attrs": "^4.1.4",
|
||||
|
@ -40,6 +42,9 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@developomp-site/eslint-config": "workspace:*",
|
||||
"@types/elasticlunr": "^0.9.5",
|
||||
"@types/jsdom": "^21.1.1",
|
||||
"@types/node": "^20.4.0",
|
||||
"eslint": "^8.44.0",
|
||||
"prettier": "^2.8.8"
|
||||
}
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"env": {
|
||||
"node": true
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"semi": false
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue