chore: change eslint & prettier config

This commit is contained in:
Kim, Jimin 2023-06-29 15:53:51 +09:00
parent fc827d74fe
commit b43871c516
103 changed files with 3581 additions and 3543 deletions

View file

@ -1,9 +1,9 @@
module.exports = { module.exports = {
root: true, root: true,
extends: ["@developomp-site/eslint-config"], extends: ["@developomp-site/eslint-config"],
settings: { settings: {
next: { next: {
rootDir: ["apps/*/"], rootDir: ["apps/*/"],
}, },
}, },
} }

View file

@ -11,7 +11,7 @@
"blog": [ "blog": [
"developomp-site-blog" "developomp-site-blog"
], ],
"portfolio": [ "portfolio": [
"developomp-site-portfolio" "developomp-site-portfolio"
] ]
} }

View file

@ -2,37 +2,37 @@
name: Deploy pages name: Deploy pages
on: on:
push: push:
branches: branches:
- master - master
jobs: jobs:
deploy: deploy:
if: ${{ github.repository_owner == 'developomp' }} if: ${{ github.repository_owner == 'developomp' }}
name: Deploy name: Deploy
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout Repo - name: Checkout Repo
uses: actions/checkout@master uses: actions/checkout@master
- uses: pnpm/action-setup@v2 - uses: pnpm/action-setup@v2
with: with:
version: 8 version: 8
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 18
cache: pnpm cache: pnpm
- name: Install dependencies - name: Install dependencies
run: pnpm install run: pnpm install
- name: Build - name: Build
run: pnpm build run: pnpm build
- name: Deploy to Firebase - name: Deploy to Firebase
uses: w9jds/firebase-action@master uses: w9jds/firebase-action@master
with: with:
args: deploy args: deploy
env: env:
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }} GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}

4
.gitignore vendored
View file

@ -8,10 +8,10 @@ node_modules
*.log *.log
.next .next
dist dist
build
dist-ssr dist-ssr
*.local *.local
.env .env
.cache .cache
server/dist
public/dist
storybook-static/ storybook-static/
.svelte-kit/

View file

@ -1,4 +1,19 @@
{ {
"useTabs": true, "useTabs": false,
"semi": false "tabWidth": 4,
"semi": false,
"overrides": [
{
"files": "*.md",
"options": {
"tabWidth": 2
}
},
{
"files": ".firebaserc",
"options": {
"tabWidth": 2
}
}
]
} }

View file

@ -1,13 +1,13 @@
{ {
"recommendations": [ "recommendations": [
"esbenp.prettier-vscode", "esbenp.prettier-vscode",
"dbaeumer.vscode-eslint", "dbaeumer.vscode-eslint",
"naumovs.color-highlight", "naumovs.color-highlight",
"streetsidesoftware.code-spell-checker", "streetsidesoftware.code-spell-checker",
"aaron-bond.better-comments", "aaron-bond.better-comments",
"styled-components.vscode-styled-components", "styled-components.vscode-styled-components",
"bradlc.vscode-tailwindcss", "bradlc.vscode-tailwindcss",
"unifiedjs.vscode-mdx", "unifiedjs.vscode-mdx",
"svelte.svelte-vscode" "svelte.svelte-vscode"
] ]
} }

148
.vscode/settings.json vendored
View file

@ -1,76 +1,76 @@
{ {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnPaste": true, "editor.formatOnPaste": true,
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.detectIndentation": false, "editor.detectIndentation": false,
"editor.insertSpaces": false, "editor.insertSpaces": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": true
}, },
"cSpell.words": [ "cSpell.words": [
"bspwm", "bspwm",
"cairographics", "cairographics",
"classnet", "classnet",
"deno", "deno",
"developomp", "developomp",
"developomp's", "developomp's",
"dompurify", "dompurify",
"elasticlunr", "elasticlunr",
"Exyle", "Exyle",
"exyleio", "exyleio",
"Fontawesome", "Fontawesome",
"Fonticons", "Fonticons",
"fontsource", "fontsource",
"fortawesome", "fortawesome",
"Freedesktop", "Freedesktop",
"GDSC", "GDSC",
"githubactions", "githubactions",
"githubpages", "githubpages",
"gnubash", "gnubash",
"godotengine", "godotengine",
"heroicon", "heroicon",
"hljs", "hljs",
"hongik", "hongik",
"hoofd", "hoofd",
"inqling", "inqling",
"Jimin", "Jimin",
"katex", "katex",
"Librewolf", "Librewolf",
"linaria", "linaria",
"nodedotjs", "nodedotjs",
"noto", "noto",
"pnpm", "pnpm",
"pocketbase", "pocketbase",
"polybar", "polybar",
"Pomky", "Pomky",
"precompress", "precompress",
"rainmeter", "rainmeter",
"sxhkd", "sxhkd",
"tailwindcss", "tailwindcss",
"tauri", "tauri",
"texmath", "texmath",
"tinycolor", "tinycolor",
"tsup", "tsup",
"Turborepo", "Turborepo",
"ungoogled", "ungoogled",
"unixporn", "unixporn",
"wbtimeline", "wbtimeline",
"webassembly", "webassembly",
"wouter", "wouter",
"YYYYMMDD" "YYYYMMDD"
], ],
"eslint.workingDirectories": [{ "mode": "auto" }], "eslint.workingDirectories": [{ "mode": "auto" }],
"[svg]": { "[svg]": {
"editor.defaultFormatter": "jock.svg" "editor.defaultFormatter": "jock.svg"
}, },
// prevent tailwind-related warnings // prevent tailwind-related warnings
"css.lint.unknownAtRules": "ignore", "css.lint.unknownAtRules": "ignore",
"less.lint.unknownAtRules": "ignore", "less.lint.unknownAtRules": "ignore",
"scss.lint.unknownAtRules": "ignore", "scss.lint.unknownAtRules": "ignore",
// for .ejs files // for .ejs files
"html.validate.styles": false, "html.validate.styles": false,
"color-highlight.markerType": "outline", "color-highlight.markerType": "outline",
"[dotenv]": { "[dotenv]": {
"editor.defaultFormatter": "foxundermoon.shell-format" "editor.defaultFormatter": "foxundermoon.shell-format"
} }
} }

View file

@ -1,30 +1,30 @@
{ {
"root": true, "root": true,
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:json/recommended", "plugin:json/recommended",
"prettier" "prettier"
], ],
"settings": { "settings": {
"node": { "node": {
"tryExtensions": [".js", ".jsx", ".json"] "tryExtensions": [".js", ".jsx", ".json"]
}, },
"react": { "react": {
"version": "18.0" "version": "18.0"
} }
}, },
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"ecmaFeatures": { "ecmaFeatures": {
"jsx": true "jsx": true
}, },
"sourceType": "module" "sourceType": "module"
}, },
"plugins": ["@typescript-eslint"], "plugins": ["@typescript-eslint"],
"rules": { "rules": {
"@typescript-eslint/no-empty-interface": "off", "@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"react/react-in-jsx-scope": ["off"] "react/react-in-jsx-scope": ["off"]
} }
} }

View file

@ -1,71 +1,71 @@
{ {
"name": "@developomp-site/blog", "name": "@developomp-site/blog",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"cp": "cp -a ../../packages/blog-content/dist/public/. ./public", "cp": "cp -a ../../packages/blog-content/dist/public/. ./public",
"dev": "pnpm cp && react-scripts start", "dev": "pnpm cp && react-scripts start",
"build": "pnpm cp && react-scripts build", "build": "pnpm cp && react-scripts build",
"clean": "rm -rf .turbo build node_modules" "clean": "rm -rf .turbo build node_modules"
}, },
"dependencies": { "dependencies": {
"@developomp-site/blog-content": "workspace:*", "@developomp-site/blog-content": "workspace:*",
"@developomp-site/theme": "workspace:*", "@developomp-site/theme": "workspace:*",
"@fontsource/noto-sans-kr": "^5.0.3", "@fontsource/noto-sans-kr": "^5.0.3",
"@fontsource/source-code-pro": "^5.0.3", "@fontsource/source-code-pro": "^5.0.3",
"@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-brands-svg-icons": "^6.2.1", "@fortawesome/free-brands-svg-icons": "^6.2.1",
"@fortawesome/free-regular-svg-icons": "^6.2.1", "@fortawesome/free-regular-svg-icons": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"elasticlunr": "^0.9.5", "elasticlunr": "^0.9.5",
"highlight.js": "^11.7.0", "highlight.js": "^11.7.0",
"katex": "^0.16.4", "katex": "^0.16.4",
"local-storage-fallback": "^4.1.2", "local-storage-fallback": "^4.1.2",
"react": "^18.2.0", "react": "^18.2.0",
"react-collapse": "^5.1.1", "react-collapse": "^5.1.1",
"react-date-range": "^1.4.0", "react-date-range": "^1.4.0",
"react-device-detect": "^2.2.2", "react-device-detect": "^2.2.2",
"react-dnd": "^16.0.1", "react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1", "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0", "react-helmet-async": "^1.3.0",
"react-router-dom": "^6.4.5", "react-router-dom": "^6.4.5",
"react-scripts": "^5.0.1", "react-scripts": "^5.0.1",
"react-select": "^5.7.0", "react-select": "^5.7.0",
"react-tooltip": "^4.5.1", "react-tooltip": "^4.5.1",
"styled-components": "^5.3.6" "styled-components": "^5.3.6"
}, },
"devDependencies": { "devDependencies": {
"@developomp-site/tsconfig": "workspace:*", "@developomp-site/tsconfig": "workspace:*",
"@styled/typescript-styled-plugin": "^1.0.0", "@styled/typescript-styled-plugin": "^1.0.0",
"@types/elasticlunr": "^0.9.5", "@types/elasticlunr": "^0.9.5",
"@types/highlight.js": "^10.1.0", "@types/highlight.js": "^10.1.0",
"@types/jsdom": "^20.0.1", "@types/jsdom": "^20.0.1",
"@types/katex": "^0.14.0", "@types/katex": "^0.14.0",
"@types/node": "^18.11.11", "@types/node": "^18.11.11",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-collapse": "^5.0.1", "@types/react-collapse": "^5.0.1",
"@types/react-date-range": "^1.4.4", "@types/react-date-range": "^1.4.4",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"@types/react-select": "^5.0.1", "@types/react-select": "^5.0.1",
"@types/styled-components": "^5.1.26", "@types/styled-components": "^5.1.26",
"jsdom": "^20.0.3", "jsdom": "^20.0.3",
"prettier": "^2.8.1", "prettier": "^2.8.1",
"simple-icons": "^7.21.0", "simple-icons": "^7.21.0",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typescript": "^4.9.4" "typescript": "^4.9.4"
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
"not dead", "not dead",
"not op_mini all" "not op_mini all"
], ],
"development": [ "development": [
"last 1 chrome version", "last 1 chrome version",
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
} }
} }

View file

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

View file

@ -21,74 +21,77 @@ import GlobalStyle from "./styles/globalStyle"
import { globalContext } from "./globalContext" import { globalContext } from "./globalContext"
const IENotSupported = styled.p` const IENotSupported = styled.p`
margin: auto; margin: auto;
font-size: 2rem; font-size: 2rem;
margin-top: 2rem; margin-top: 2rem;
text-align: center; text-align: center;
font-family: ${(props) => props.theme.theme.font.sansSerif}; font-family: ${(props) => props.theme.theme.font.sansSerif};
` `
const StyledContentContainer = styled.div` const StyledContentContainer = styled.div`
flex: 1 1 auto; flex: 1 1 auto;
margin-bottom: 3rem; margin-bottom: 3rem;
margin-top: 5rem; margin-top: 5rem;
` `
export default function App() { export default function App() {
const { globalState } = useContext(globalContext) const { globalState } = useContext(globalContext)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
useEffect(() => { useEffect(() => {
// set loading to false if all fonts are loaded // set loading to false if all fonts are loaded
// checks if document.fonts.onloadingdone is supported on the browser // checks if document.fonts.onloadingdone is supported on the browser
if (typeof document.fonts.onloadingdone != undefined) { if (typeof document.fonts.onloadingdone != undefined) {
document.fonts.onloadingdone = () => { document.fonts.onloadingdone = () => {
setIsLoading(false) setIsLoading(false)
} }
} else { } else {
setIsLoading(false) setIsLoading(false)
} }
}, []) }, [])
if (isIE) if (isIE)
return ( return (
<IENotSupported> <IENotSupported>
Internet Explorer is <b>not supported.</b> Internet Explorer is <b>not supported.</b>
</IENotSupported> </IENotSupported>
) )
return ( return (
<ThemeProvider <ThemeProvider
theme={{ theme={{
currentTheme: globalState.currentTheme, currentTheme: globalState.currentTheme,
theme: globalState.currentTheme === "dark" ? darkTheme : lightTheme, theme:
}} globalState.currentTheme === "dark"
> ? darkTheme
<Helmet> : lightTheme,
<meta property="og:site_name" content="developomp" /> }}
<meta property="og:title" content="Home" /> >
<meta property="og:description" content="developomp's blog" /> <Helmet>
<meta name="description" content="developomp's blog" /> <meta property="og:site_name" content="developomp" />
</Helmet> <meta property="og:title" content="Home" />
<meta property="og:description" content="developomp's blog" />
<meta name="description" content="developomp's blog" />
</Helmet>
<GlobalStyle /> <GlobalStyle />
<Header /> <Header />
<StyledContentContainer> <StyledContentContainer>
{isLoading ? ( {isLoading ? (
<Loading /> <Loading />
) : ( ) : (
<Routes> <Routes>
<Route index element={<Home />} /> <Route index element={<Home />} />
<Route path="search" element={<Search />} /> <Route path="search" element={<Search />} />
<Route path="404" element={<NotFound />} /> <Route path="404" element={<NotFound />} />
<Route path="loading" element={<Loading />} /> <Route path="loading" element={<Loading />} />
<Route path="*" element={<Page />} /> <Route path="*" element={<Page />} />
</Routes> </Routes>
)} )}
</StyledContentContainer> </StyledContentContainer>
<Footer /> <Footer />
</ThemeProvider> </ThemeProvider>
) )
} }

View file

@ -1,22 +1,24 @@
import styled, { css } from "styled-components" import styled, { css } from "styled-components"
export const cardCSS = css` export const cardCSS = css`
margin: auto; margin: auto;
background-color: ${({ theme }) => background-color: ${({ theme }) =>
theme.currentTheme ? theme.theme.component.card.color.background : "white"}; theme.currentTheme
padding: 2rem; ? theme.theme.component.card.color.background
border-radius: 6px; : "white"};
box-shadow: ${({ theme }) => padding: 2rem;
theme.currentTheme === "dark" border-radius: 6px;
? "0 4px 10px rgb(0 0 0 / 30%), 0 0 1px rgb(0 0 0 / 30%)" box-shadow: ${({ theme }) =>
: "0 4px 10px rgb(0 0 0 / 5%), 0 0 1px rgb(0 0 0 / 10%)"}; 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%)"};
@media screen and (max-width: ${({ theme }) => @media screen and (max-width: ${({ theme }) =>
theme.theme.maxDisplayWidth.mobile}) { theme.theme.maxDisplayWidth.mobile}) {
padding: 1rem; padding: 1rem;
} }
` `
export default styled.div` export default styled.div`
${cardCSS} ${cardCSS}
` `

View file

@ -3,41 +3,41 @@ import styled from "styled-components"
import GithubLinkIcon from "../GithubLinkIcon" import GithubLinkIcon from "../GithubLinkIcon"
const StyledFooter = styled.footer` const StyledFooter = styled.footer`
display: flex; display: flex;
// congratulation. You've found the lucky 7s // congratulation. You've found the lucky 7s
min-height: 7.77rem; min-height: 7.77rem;
max-height: 7.77rem; max-height: 7.77rem;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: ${({ theme }) => background-color: ${({ theme }) =>
theme.theme.component.footer.color.background}; theme.theme.component.footer.color.background};
` `
const StyledFooterContainer = styled.div` const StyledFooterContainer = styled.div`
display: flex; display: flex;
padding: 0 1rem 0 1rem; padding: 0 1rem 0 1rem;
justify-content: space-between; justify-content: space-between;
text-align: center; text-align: center;
color: gray; color: gray;
width: 100%; width: 100%;
max-width: ${({ theme }) => theme.theme.maxDisplayWidth.desktop}; max-width: ${({ theme }) => theme.theme.maxDisplayWidth.desktop};
` `
export default () => { export default () => {
return ( return (
<StyledFooter> <StyledFooter>
<StyledFooterContainer> <StyledFooterContainer>
<div> <div>
Created by <b>developomp</b> Created by <b>developomp</b>
</div> </div>
<GithubLinkIcon link="https://github.com/developomp/developomp-site" /> <GithubLinkIcon link="https://github.com/developomp/developomp-site" />
</StyledFooterContainer> </StyledFooterContainer>
</StyledFooter> </StyledFooter>
) )
} }

View file

@ -5,31 +5,31 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faGithub } from "@fortawesome/free-brands-svg-icons" import { faGithub } from "@fortawesome/free-brands-svg-icons"
const StyledGithubLink = styled.a<{ size?: string }>` const StyledGithubLink = styled.a<{ size?: string }>`
font-size: ${(props) => props.size || "2.5rem"}; font-size: ${(props) => props.size || "2.5rem"};
color: ${({ theme }) => color: ${({ theme }) =>
theme.currentTheme === "dark" ? "grey" : "lightgrey"}; theme.currentTheme === "dark" ? "grey" : "lightgrey"};
:hover { :hover {
color: ${({ theme }) => theme.theme.color.text.highContrast}; color: ${({ theme }) => theme.theme.color.text.highContrast};
} }
` `
interface Props { interface Props {
link: string link: string
size?: string size?: string
children?: ReactNode children?: ReactNode
} }
export default ({ link, size, children }: Props) => { export default ({ link, size, children }: Props) => {
return ( return (
<StyledGithubLink <StyledGithubLink
aria-label="GitHub repository" aria-label="GitHub repository"
size={size} size={size}
href={link} href={link}
target="_blank" target="_blank"
> >
<FontAwesomeIcon icon={faGithub} /> <FontAwesomeIcon icon={faGithub} />
{children} {children}
</StyledGithubLink> </StyledGithubLink>
) )
} }

View file

@ -4,16 +4,16 @@ import ThemeToggleButton from "./ThemeToggleButton"
import SearchButton from "./SearchButton" import SearchButton from "./SearchButton"
const RightButtons = styled.div` const RightButtons = styled.div`
display: flex; display: flex;
height: 100%; height: 100%;
margin-left: auto; margin-left: auto;
` `
export default () => { export default () => {
return ( return (
<RightButtons> <RightButtons>
<ThemeToggleButton /> <ThemeToggleButton />
<SearchButton /> <SearchButton />
</RightButtons> </RightButtons>
) )
} }

View file

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

View file

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

View file

@ -9,50 +9,52 @@ import Sidebar from "../Sidebar"
import Buttons from "./Buttons" import Buttons from "./Buttons"
const Header = styled.header` const Header = styled.header`
/* set z index to arbitrarily high value to prevent other components from drawing over it */ /* set z index to arbitrarily high value to prevent other components from drawing over it */
z-index: 9999; z-index: 9999;
position: fixed; position: fixed;
width: 100%; width: 100%;
background-color: ${({ theme }) => background-color: ${({ theme }) =>
theme.theme.component.ui.color.background.default}; theme.theme.component.ui.color.background.default};
color: ${({ theme }) => theme.theme.color.text.default}; color: ${({ theme }) => theme.theme.color.text.default};
box-shadow: 0 4px 10px rgb(0 0 0 / 5%); box-shadow: 0 4px 10px rgb(0 0 0 / 5%);
` `
const Container = styled.div` const Container = styled.div`
margin: 0 auto; margin: 0 auto;
align-items: center; align-items: center;
display: flex; display: flex;
height: 4rem; height: 4rem;
/* account for 20px scrollbar width */ /* account for 20px scrollbar width */
@media only screen and (min-width: calc(${({ theme }) => @media only screen and (min-width: calc(${({ theme }) =>
theme.theme.maxDisplayWidth.desktop} + 20px)) { theme.theme.maxDisplayWidth.desktop} + 20px)) {
width: calc(${({ theme }) => theme.theme.maxDisplayWidth.desktop} - 20px); width: calc(
} ${({ theme }) => theme.theme.maxDisplayWidth.desktop} - 20px
);
}
` `
const Icon = styled.img` const Icon = styled.img`
height: 2.5rem; height: 2.5rem;
display: block; display: block;
margin: 1rem; margin: 1rem;
` `
export default () => { export default () => {
return ( return (
<Header> <Header>
<Container> <Container>
<Link to="/" aria-label="homepage"> <Link to="/" aria-label="homepage">
<Icon src="/icon/icon_circle.svg" alt="logo" /> <Icon src="/icon/icon_circle.svg" alt="logo" />
</Link> </Link>
<Nav /> <Nav />
<Buttons /> <Buttons />
<Sidebar /> <Sidebar />
</Container> </Container>
<ReadProgress /> <ReadProgress />
</Header> </Header>
) )
} }

View file

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

View file

@ -6,29 +6,29 @@ import HeaderButton from "./HeaderButton"
import NavbarData from "../../data/NavbarData" import NavbarData from "../../data/NavbarData"
const Nav = styled.div` const Nav = styled.div`
display: flex; display: flex;
height: 100%; height: 100%;
@media only screen and (max-width: ${({ theme }) => @media only screen and (max-width: ${({ theme }) =>
theme.theme.maxDisplayWidth.mobile}) { theme.theme.maxDisplayWidth.mobile}) {
display: none; display: none;
} }
` `
export default () => { export default () => {
return ( return (
<Nav> <Nav>
{NavbarData.map(({ path, title }, index) => { {NavbarData.map(({ path, title }, index) => {
return path.at(0) === "/" ? ( return path.at(0) === "/" ? (
<Link key={index} to={path}> <Link key={index} to={path}>
<HeaderButton>{title}</HeaderButton> <HeaderButton>{title}</HeaderButton>
</Link> </Link>
) : ( ) : (
<a key={index} target="_blank" href={path}> <a key={index} target="_blank" href={path}>
<HeaderButton>{title}</HeaderButton> <HeaderButton>{title}</HeaderButton>
</a> </a>
) )
})} })}
</Nav> </Nav>
) )
} }

View file

@ -3,15 +3,15 @@ import { useLocation } from "react-router-dom"
import styled from "styled-components" import styled from "styled-components"
const Background = styled.div` const Background = styled.div`
height: 0.2rem; height: 0.2rem;
background-color: ${({ theme }) => background-color: ${({ theme }) =>
theme.theme.component.scrollProgressBar.color.background}; theme.theme.component.scrollProgressBar.color.background};
` `
const ProgressBar = styled.div` const ProgressBar = styled.div`
height: 100%; height: 100%;
background-color: ${({ theme }) => background-color: ${({ theme }) =>
theme.theme.component.scrollProgressBar.color.foreground}; theme.theme.component.scrollProgressBar.color.foreground};
` `
const st = "scrollTop" const st = "scrollTop"
@ -20,40 +20,42 @@ const h = document.documentElement
const b = document.body const b = document.body
const ReadProgress = () => { const ReadProgress = () => {
const [scroll, setScroll] = useState(0) const [scroll, setScroll] = useState(0)
const location = useLocation() const location = useLocation()
// https://stackoverflow.com/a/8028584/12979111 // https://stackoverflow.com/a/8028584/12979111
const scrollHandler = useCallback(() => { const scrollHandler = useCallback(() => {
setScroll(((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100) setScroll(
}, []) ((h[st] || b[st]) / ((h[sh] || b[sh]) - h.clientHeight)) * 100
)
}, [])
useEffect(() => { useEffect(() => {
const resizeObserver = new ResizeObserver(() => { const resizeObserver = new ResizeObserver(() => {
scrollHandler() scrollHandler()
}) })
resizeObserver.observe(document.body) resizeObserver.observe(document.body)
window.addEventListener("scroll", scrollHandler) window.addEventListener("scroll", scrollHandler)
return () => { return () => {
resizeObserver.disconnect() resizeObserver.disconnect()
window.removeEventListener("scroll", scrollHandler) window.removeEventListener("scroll", scrollHandler)
} }
}, []) }, [])
// update on path change // update on path change
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
scrollHandler() scrollHandler()
}, 100) }, 100)
}, [location]) }, [location])
return ( return (
<Background> <Background>
<ProgressBar style={{ width: `${scroll}%` }} /> <ProgressBar style={{ width: `${scroll}%` }} />
</Background> </Background>
) )
} }
export default ReadProgress export default ReadProgress

View file

@ -7,133 +7,133 @@ import styled from "styled-components"
import MainContent from "./MainContent" import MainContent from "./MainContent"
const StyledContainer = styled(MainContent)` const StyledContainer = styled(MainContent)`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
animation: fadein 2s; animation: fadein 2s;
@keyframes fadein { @keyframes fadein {
from { from {
opacity: 0; opacity: 0;
} }
50% { 50% {
opacity: 0; opacity: 0;
} }
to { to {
opacity: 1; opacity: 1;
} }
} }
` `
const StyledSVG = styled.svg` const StyledSVG = styled.svg`
--color: ${({ theme }) => theme.theme.color.text.default}; --color: ${({ theme }) => theme.theme.color.text.default};
display: block; display: block;
margin: 1rem; margin: 1rem;
margin-bottom: 4.5rem; margin-bottom: 4.5rem;
transform: scale(2); transform: scale(2);
#teabag { #teabag {
transform-origin: top center; transform-origin: top center;
transform: rotate(3deg); transform: rotate(3deg);
animation: swingAnimation 2s infinite; animation: swingAnimation 2s infinite;
} }
#steamL { #steamL {
stroke-dasharray: 13; stroke-dasharray: 13;
stroke-dashoffset: 13; stroke-dashoffset: 13;
animation: steamLargeAnimation 2s infinite; animation: steamLargeAnimation 2s infinite;
} }
#steamR { #steamR {
stroke-dasharray: 9; stroke-dasharray: 9;
stroke-dashoffset: 9; stroke-dashoffset: 9;
animation: steamSmallAnimation 2s infinite; animation: steamSmallAnimation 2s infinite;
} }
@keyframes swingAnimation { @keyframes swingAnimation {
50% { 50% {
transform: rotate(-3deg); transform: rotate(-3deg);
} }
} }
@keyframes steamLargeAnimation { @keyframes steamLargeAnimation {
0% { 0% {
stroke-dashoffset: 13; stroke-dashoffset: 13;
opacity: 0.6; opacity: 0.6;
} }
100% { 100% {
stroke-dashoffset: 39; stroke-dashoffset: 39;
opacity: 0; opacity: 0;
} }
} }
@keyframes steamSmallAnimation { @keyframes steamSmallAnimation {
10% { 10% {
stroke-dashoffset: 9; stroke-dashoffset: 9;
opacity: 0.6; opacity: 0.6;
} }
80% { 80% {
stroke-dashoffset: 27; stroke-dashoffset: 27;
opacity: 0; opacity: 0;
} }
100% { 100% {
stroke-dashoffset: 27; stroke-dashoffset: 27;
opacity: 0; opacity: 0;
} }
} }
` `
const Loading = () => { const Loading = () => {
return ( return (
<StyledContainer> <StyledContainer>
<StyledSVG <StyledSVG
width="37" width="37"
height="48" height="48"
viewBox="0 0 37 48" viewBox="0 0 37 48"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <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" 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)" stroke="var(--color)"
strokeWidth="2" strokeWidth="2"
/> />
<path <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" 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)" stroke="var(--color)"
strokeWidth="2" strokeWidth="2"
/> />
<path <path
id="teabag" id="teabag"
fill="var(--color)" fill="var(--color)"
fillRule="evenodd" fillRule="evenodd"
clipRule="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" 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 <path
id="steamL" id="steamL"
d="M17 1C17 1 17 4.5 14 6.5C11 8.5 11 12 11 12" d="M17 1C17 1 17 4.5 14 6.5C11 8.5 11 12 11 12"
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
stroke="var(--color)" stroke="var(--color)"
/> />
<path <path
id="steamR" id="steamR"
d="M21 6C21 6 21 8.22727 19 9.5C17 10.7727 17 13 17 13" d="M21 6C21 6 21 8.22727 19 9.5C17 10.7727 17 13 17 13"
stroke="var(--color)" stroke="var(--color)"
strokeWidth="2" strokeWidth="2"
strokeLinecap="round" strokeLinecap="round"
strokeLinejoin="round" strokeLinejoin="round"
/> />
</StyledSVG> </StyledSVG>
<h2>Loading...</h2> <h2>Loading...</h2>
</StyledContainer> </StyledContainer>
) )
} }
export default Loading export default Loading

View file

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

View file

@ -5,9 +5,9 @@ import { PostData } from "@developomp-site/blog-content/src/types/types"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { import {
faBook, faBook,
faCalendar, faCalendar,
faHourglass, faHourglass,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
import Tag from "./Tag" import Tag from "./Tag"
@ -15,85 +15,85 @@ import TagList from "./TagList"
import MainContent from "./MainContent" import MainContent from "./MainContent"
const PostCard = styled(MainContent)` const PostCard = styled(MainContent)`
box-shadow: 0 4px 10px rgb(0 0 0 / 10%); box-shadow: 0 4px 10px rgb(0 0 0 / 10%);
text-align: left; text-align: left;
margin-bottom: 2rem; margin-bottom: 2rem;
:hover { :hover {
cursor: pointer; cursor: pointer;
box-shadow: 0 4px 10px box-shadow: 0 4px 10px
${({ theme }) => theme.theme.component.card.color.hoverGlow}; ${({ theme }) => theme.theme.component.card.color.hoverGlow};
} }
` `
const PostCardContainer = styled(Link)` const PostCardContainer = styled(Link)`
display: block; display: block;
padding: 2rem; padding: 2rem;
text-decoration: none; text-decoration: none;
padding: 0; padding: 0;
/* override link color */ /* override link color */
color: ${({ theme }) => theme.theme.color.text.gray}; color: ${({ theme }) => theme.theme.color.text.gray};
&:hover { &:hover {
color: ${({ theme }) => theme.theme.color.text.gray}; color: ${({ theme }) => theme.theme.color.text.gray};
} }
` `
const Title = styled.h1` const Title = styled.h1`
font-size: 2rem; font-size: 2rem;
font-style: bold; font-style: bold;
margin: 0; margin: 0;
margin-bottom: 1rem; margin-bottom: 1rem;
` `
const MetaContainer = styled.small`` const MetaContainer = styled.small``
interface PostCardData extends PostData { interface PostCardData extends PostData {
content_id: string content_id: string
} }
interface Props { interface Props {
postData: PostCardData postData: PostCardData
} }
export default (props: Props) => { export default (props: Props) => {
const { postData } = props const { postData } = props
const { content_id, wordCount, date, readTime, title, tags } = postData const { content_id, wordCount, date, readTime, title, tags } = postData
return ( return (
<PostCard> <PostCard>
<PostCardContainer to={content_id}> <PostCardContainer to={content_id}>
<Title> <Title>
{title || "No title"} {title || "No title"}
{/* show "(series)" for urls that matches regex "/series/<series-title>" */} {/* show "(series)" for urls that matches regex "/series/<series-title>" */}
{/\/series\/[^/]*$/.test(content_id) && " (series)"} {/\/series\/[^/]*$/.test(content_id) && " (series)"}
</Title> </Title>
<br /> <br />
<MetaContainer> <MetaContainer>
<TagList direction="left"> <TagList direction="left">
{tags && {tags &&
tags.map((tag) => { tags.map((tag) => {
return <Tag key={title + tag} text={tag} /> return <Tag key={title + tag} text={tag} />
})} })}
</TagList> </TagList>
<hr /> <hr />
<FontAwesomeIcon icon={faCalendar} /> <FontAwesomeIcon icon={faCalendar} />
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
{date || "Unknown date"} {date || "Unknown date"}
&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faHourglass} /> <FontAwesomeIcon icon={faHourglass} />
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
{readTime ? readTime + " read" : "unknown read time"} {readTime ? readTime + " read" : "unknown read time"}
&nbsp;&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faBook} /> <FontAwesomeIcon icon={faBook} />
&nbsp;&nbsp;&nbsp; &nbsp;&nbsp;&nbsp;
{typeof wordCount === "number" {typeof wordCount === "number"
? wordCount + " words" ? wordCount + " words"
: "unknown length"} : "unknown length"}
</MetaContainer> </MetaContainer>
</PostCardContainer> </PostCardContainer>
</PostCard> </PostCard>
) )
} }

View file

@ -12,99 +12,112 @@ import NavbarData from "../../data/NavbarData"
import { HeaderButtonCSS } from "../Header/HeaderButton" import { HeaderButtonCSS } from "../Header/HeaderButton"
const SidebarOpenButton = styled.div` const SidebarOpenButton = styled.div`
${HeaderButtonCSS} ${HeaderButtonCSS}
@media only screen and (min-width: ${({ theme }) => @media only screen and (min-width: ${({ theme }) =>
theme.theme.maxDisplayWidth.mobile}) { theme.theme.maxDisplayWidth.mobile}) {
display: none; display: none;
} }
` `
const SidebarCloseButton = styled.div` const SidebarCloseButton = styled.div`
${HeaderButtonCSS} ${HeaderButtonCSS}
height: 4rem; height: 4rem;
font-size: 1.1rem; font-size: 1.1rem;
svg { svg {
margin-top: 0.2rem; margin-top: 0.2rem;
margin-right: 0.5rem; margin-right: 0.5rem;
} }
` `
const StyledOverlay = styled.div<{ isSidebarOpen: boolean }>` const StyledOverlay = styled.div<{ isSidebarOpen: boolean }>`
display: ${(props) => (props.isSidebarOpen ? "block" : "none")}; display: ${(props) => (props.isSidebarOpen ? "block" : "none")};
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: 20; z-index: 20;
transition-property: opacity; transition-property: opacity;
background-color: rgba(0, 0, 0, 25%); background-color: rgba(0, 0, 0, 25%);
* { * {
overflow: scroll; overflow: scroll;
} }
` `
const SidebarNav = styled.nav<{ isSidebarOpen: boolean }>` const SidebarNav = styled.nav<{ isSidebarOpen: boolean }>`
width: 250px; width: 250px;
height: 100vh; height: 100vh;
display: flex; display: flex;
justify-content: center; justify-content: center;
position: fixed; position: fixed;
top: 0; top: 0;
right: ${(props) => (props.isSidebarOpen ? "0" : "-100%")}; right: ${(props) => (props.isSidebarOpen ? "0" : "-100%")};
transition: 350ms; transition: 350ms;
z-index: 30; z-index: 30;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
background-color: ${({ theme }) => background-color: ${({ theme }) =>
theme.theme.component.header.color.background}; theme.theme.component.header.color.background};
color: ${({ theme }) => theme.theme.component.header.color.text}; color: ${({ theme }) => theme.theme.component.header.color.text};
` `
const SidebarWrap = styled.div` const SidebarWrap = styled.div`
width: 100%; width: 100%;
` `
const Sidebar = () => { const Sidebar = () => {
const [isSidebarOpen, setSidebarOpen] = useState(false) const [isSidebarOpen, setSidebarOpen] = useState(false)
const toggleSidebar = useCallback(() => { const toggleSidebar = useCallback(() => {
setSidebarOpen((prev) => !prev) setSidebarOpen((prev) => !prev)
document.body.style.overflow = isSidebarOpen ? "" : "hidden" document.body.style.overflow = isSidebarOpen ? "" : "hidden"
}, [isSidebarOpen]) }, [isSidebarOpen])
return ( return (
<> <>
<StyledOverlay isSidebarOpen={isSidebarOpen} onClick={toggleSidebar} /> <StyledOverlay
isSidebarOpen={isSidebarOpen}
onClick={toggleSidebar}
/>
<SidebarOpenButton data-tip data-for="sidebar" onClick={toggleSidebar}> <SidebarOpenButton
<FontAwesomeIcon icon={faEllipsisV}></FontAwesomeIcon> data-tip
{!isMobile && ( data-for="sidebar"
<ReactTooltip id="sidebar" type="dark" effect="solid"> onClick={toggleSidebar}
<span>open sidebar</span> >
</ReactTooltip> <FontAwesomeIcon icon={faEllipsisV}></FontAwesomeIcon>
)} {!isMobile && (
</SidebarOpenButton> <ReactTooltip id="sidebar" type="dark" effect="solid">
<span>open sidebar</span>
</ReactTooltip>
)}
</SidebarOpenButton>
<SidebarNav isSidebarOpen={isSidebarOpen}> <SidebarNav isSidebarOpen={isSidebarOpen}>
<SidebarWrap> <SidebarWrap>
{/* close sidebar button */} {/* close sidebar button */}
<SidebarCloseButton onClick={toggleSidebar}> <SidebarCloseButton onClick={toggleSidebar}>
<FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>Close <FontAwesomeIcon icon={faTimes}></FontAwesomeIcon>Close
</SidebarCloseButton> </SidebarCloseButton>
{/* sidebar items */} {/* sidebar items */}
{NavbarData.map((item, index) => { {NavbarData.map((item, index) => {
return <SubMenu onClick={toggleSidebar} item={item} key={index} /> return (
})} <SubMenu
</SidebarWrap> onClick={toggleSidebar}
</SidebarNav> item={item}
</> key={index}
) />
)
})}
</SidebarWrap>
</SidebarNav>
</>
)
} }
export default Sidebar export default Sidebar

View file

@ -11,70 +11,74 @@ import styled, { css } from "styled-components"
import button from "../../styles/button" import button from "../../styles/button"
const sharedStyle = css` const sharedStyle = css`
${button}; ${button};
display: flex; display: flex;
width: 100%; width: 100%;
margin: 0; margin: 0;
border-radius: 0; border-radius: 0;
justify-content: space-between; justify-content: space-between;
height: 2rem; height: 2rem;
align-items: center; align-items: center;
padding: 20px; padding: 20px;
list-style: none; list-style: none;
svg { svg {
scale: 1.5; scale: 1.5;
} }
&:hover { &:hover {
color: inherit; color: inherit;
} }
` `
const SidebarLink = styled(Link)` const SidebarLink = styled(Link)`
${sharedStyle} ${sharedStyle}
` `
const SidebarAnchor = styled.a` const SidebarAnchor = styled.a`
${sharedStyle} ${sharedStyle}
` `
const SidebarLabel = styled.span` const SidebarLabel = styled.span`
margin-left: 1rem; margin-left: 1rem;
` `
interface Props { interface Props {
item: Item item: Item
onClick: () => void onClick: () => void
} }
const SubMenu = ({ item, onClick }: Props) => { const SubMenu = ({ item, onClick }: Props) => {
const { path, icon, title } = item const { path, icon, title } = item
const [isSubNavOpen, setSubNavOpen] = useState(false) const [isSubNavOpen, setSubNavOpen] = useState(false)
const handleSidebarLinkClick = useCallback(() => { const handleSidebarLinkClick = useCallback(() => {
onClick() onClick()
setSubNavOpen((prev) => !prev) setSubNavOpen((prev) => !prev)
}, [isSubNavOpen]) }, [isSubNavOpen])
if (path.at(0) == "/") { if (path.at(0) == "/") {
return ( return (
<SidebarLink to={path} onClick={handleSidebarLinkClick}> <SidebarLink to={path} onClick={handleSidebarLinkClick}>
<div> <div>
{icon} {icon}
<SidebarLabel>{title}</SidebarLabel> <SidebarLabel>{title}</SidebarLabel>
</div> </div>
</SidebarLink> </SidebarLink>
) )
} }
return ( return (
<SidebarAnchor target="_blank" href={path} onClick={handleSidebarLinkClick}> <SidebarAnchor
<div> target="_blank"
{icon} href={path}
<SidebarLabel>{title}</SidebarLabel> onClick={handleSidebarLinkClick}
</div> >
</SidebarAnchor> <div>
) {icon}
<SidebarLabel>{title}</SidebarLabel>
</div>
</SidebarAnchor>
)
} }
export default SubMenu export default SubMenu

View file

@ -5,23 +5,23 @@ import { faHashtag } from "@fortawesome/free-solid-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
const Tag = styled.div` const Tag = styled.div`
text-align: center; text-align: center;
margin-right: 0.8rem; margin-right: 0.8rem;
border-radius: 10px; border-radius: 10px;
color: ${({ theme }) => theme.theme.color.text.gray}; color: ${({ theme }) => theme.theme.color.text.gray};
` `
interface Props { interface Props {
text: string text: string
onClick?: (event: MouseEvent<never>) => void onClick?: (event: MouseEvent<never>) => void
} }
export default (props: Props) => { export default (props: Props) => {
return ( return (
<Tag onClick={props.onClick || undefined}> <Tag onClick={props.onClick || undefined}>
<FontAwesomeIcon icon={faHashtag} /> &nbsp;{props.text} <FontAwesomeIcon icon={faHashtag} /> &nbsp;{props.text}
</Tag> </Tag>
) )
} }

View file

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

View file

@ -1,39 +1,39 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { import {
faHome, faHome,
faFileLines, faFileLines,
faUser, faUser,
faUserTie, faUserTie,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
// item from sidebar data // item from sidebar data
export type Item = { export type Item = {
path: string path: string
icon: JSX.Element icon: JSX.Element
title: string title: string
} }
const NavbarData: Item[] = [ const NavbarData: Item[] = [
{ {
title: "Home", title: "Home",
path: "/", path: "/",
icon: <FontAwesomeIcon icon={faHome} />, icon: <FontAwesomeIcon icon={faHome} />,
}, },
{ {
title: "About", title: "About",
path: "https://developomp.com", path: "https://developomp.com",
icon: <FontAwesomeIcon icon={faUser} />, icon: <FontAwesomeIcon icon={faUser} />,
}, },
{ {
title: "Portfolio", title: "Portfolio",
path: "https://portfolio.developomp.com", path: "https://portfolio.developomp.com",
icon: <FontAwesomeIcon icon={faFileLines} />, icon: <FontAwesomeIcon icon={faFileLines} />,
}, },
{ {
title: "Resume", title: "Resume",
path: "/resume", path: "/resume",
icon: <FontAwesomeIcon icon={faUserTie} />, icon: <FontAwesomeIcon icon={faUserTie} />,
}, },
] ]
export default NavbarData export default NavbarData

View file

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

View file

@ -12,11 +12,11 @@ import App from "./App"
const container = document.getElementById("root") as HTMLElement const container = document.getElementById("root") as HTMLElement
const root = createRoot(container) const root = createRoot(container)
root.render( root.render(
<GlobalStore> <GlobalStore>
<BrowserRouter> <BrowserRouter>
<HelmetProvider> <HelmetProvider>
<App /> <App />
</HelmetProvider> </HelmetProvider>
</BrowserRouter> </BrowserRouter>
</GlobalStore> </GlobalStore>
) )

View file

@ -13,75 +13,75 @@ import ShowMoreButton from "./ShowMoreButton"
import contentMap from "../../contentMap" import contentMap from "../../contentMap"
const PostList = styled.div` const PostList = styled.div`
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
text-align: center; text-align: center;
color: ${({ theme }) => theme.theme.color.text.default}; color: ${({ theme }) => theme.theme.color.text.default};
` `
export default () => { export default () => {
const [howMany, setHowMany] = useState(5) const [howMany, setHowMany] = useState(5)
const [postsLength, setPostsLength] = useState(0) const [postsLength, setPostsLength] = useState(0)
const [postCards, setPostCards] = useState<JSX.Element[]>([]) const [postCards, setPostCards] = useState<JSX.Element[]>([])
const loadPostCards = useCallback(() => { const loadPostCards = useCallback(() => {
let postCount = 0 let postCount = 0
const postCards = [] as JSX.Element[] const postCards = [] as JSX.Element[]
for (const date of Object.keys(contentMap.date).reverse()) { for (const date of Object.keys(contentMap.date).reverse()) {
if (postCount >= howMany) break if (postCount >= howMany) break
const length = contentMap.date[date].length const length = contentMap.date[date].length
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
if (postCount >= howMany) break if (postCount >= howMany) break
postCount++ postCount++
const content_id = contentMap.date[date][length - i - 1] const content_id = contentMap.date[date][length - i - 1]
postCards.push( postCards.push(
<PostCard <PostCard
key={content_id} key={content_id}
postData={{ postData={{
content_id: content_id, content_id: content_id,
...contentMap.posts[content_id], ...contentMap.posts[content_id],
}} }}
/> />
) )
} }
} }
setPostCards(postCards) setPostCards(postCards)
}, [howMany, postCards]) }, [howMany, postCards])
useEffect(() => { useEffect(() => {
loadPostCards() loadPostCards()
setPostsLength(Object.keys(contentMap.posts).length) setPostsLength(Object.keys(contentMap.posts).length)
}, [howMany]) }, [howMany])
return ( return (
<> <>
<Helmet> <Helmet>
<title>pomp | Home</title> <title>pomp | Home</title>
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content="/icon/icon.svg" /> <meta property="og:image" content="/icon/icon.svg" />
</Helmet> </Helmet>
<PostList> <PostList>
<h1>Recent Posts</h1> <h1>Recent Posts</h1>
{postCards} {postCards}
{postsLength > howMany && ( {postsLength > howMany && (
<ShowMoreButton <ShowMoreButton
action={() => { action={() => {
setHowMany((prev) => prev + 5) setHowMany((prev) => prev + 5)
}} }}
/> />
)} )}
</PostList> </PostList>
</> </>
) )
} }

View file

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

View file

@ -4,36 +4,36 @@ import { Helmet } from "react-helmet-async"
import MainContent from "../components/MainContent" import MainContent from "../components/MainContent"
const StyledNotFound = styled(MainContent)` const StyledNotFound = styled(MainContent)`
text-align: center; text-align: center;
` `
const Styled404 = styled.h1` const Styled404 = styled.h1`
font-size: 5rem; font-size: 5rem;
` `
const NotFound = () => { const NotFound = () => {
return ( return (
<> <>
<Helmet> <Helmet>
<title>pomp | 404</title> <title>pomp | 404</title>
<meta property="og:title" content="Page Not Found" /> <meta property="og:title" content="Page Not Found" />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:url" content="http://blog.developomp.com" /> <meta property="og:url" content="http://blog.developomp.com" />
<meta <meta
property="og:image" property="og:image"
content="http://blog.developomp.com/icon/icon.svg" content="http://blog.developomp.com/icon/icon.svg"
/> />
<meta property="og:description" content="Page does not exist" /> <meta property="og:description" content="Page does not exist" />
</Helmet> </Helmet>
<StyledNotFound> <StyledNotFound>
<Styled404>404</Styled404> <Styled404>404</Styled404>
<br /> <br />
Page was not found :( Page was not found :(
</StyledNotFound> </StyledNotFound>
</> </>
) )
} }
export default NotFound export default NotFound

View file

@ -1,52 +1,53 @@
import styled from "styled-components" import styled from "styled-components"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { import {
faBook, faBook,
faCalendar, faCalendar,
faFile, faFile,
faHourglass, faHourglass,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
import { PageData } from "@developomp-site/blog-content/src/types/types" import { PageData } from "@developomp-site/blog-content/src/types/types"
const StyledMetaContainer = styled.div` const StyledMetaContainer = styled.div`
color: ${({ theme }) => theme.theme.color.text.gray}; color: ${({ theme }) => theme.theme.color.text.gray};
` `
const Meta = (props: { fetchedPage: PageData }) => { const Meta = (props: { fetchedPage: PageData }) => {
return ( return (
<StyledMetaContainer> <StyledMetaContainer>
{/* posts count */} {/* posts count */}
{props.fetchedPage.length > 0 && ( {props.fetchedPage.length > 0 && (
<> <>
<FontAwesomeIcon icon={faFile} /> <FontAwesomeIcon icon={faFile} />
&nbsp;&nbsp; &nbsp;&nbsp;
{props.fetchedPage.length} post {props.fetchedPage.length} post
{props.fetchedPage.length > 1 && "s"} &nbsp;&nbsp;&nbsp;&nbsp; {props.fetchedPage.length > 1 && "s"}{" "}
</> &nbsp;&nbsp;&nbsp;&nbsp;
)} </>
{/* date */} )}
<FontAwesomeIcon icon={faCalendar} /> {/* date */}
&nbsp;&nbsp; <FontAwesomeIcon icon={faCalendar} />
{props.fetchedPage.date || "Unknown date"} &nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp; {props.fetchedPage.date || "Unknown date"}
{/* read time */} &nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faHourglass} /> {/* read time */}
&nbsp;&nbsp; <FontAwesomeIcon icon={faHourglass} />
{props.fetchedPage.readTime &nbsp;&nbsp;
? props.fetchedPage.readTime + " read" {props.fetchedPage.readTime
: "unknown length"} ? props.fetchedPage.readTime + " read"
&nbsp;&nbsp;&nbsp;&nbsp; : "unknown length"}
{/* word count */} &nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faBook} /> {/* word count */}
&nbsp;&nbsp; <FontAwesomeIcon icon={faBook} />
{props.fetchedPage.wordCount &nbsp;&nbsp;
? props.fetchedPage.wordCount + {props.fetchedPage.wordCount
" word" + ? props.fetchedPage.wordCount +
(props.fetchedPage.wordCount > 1 && "s") " word" +
: "unknown words"} (props.fetchedPage.wordCount > 1 && "s")
</StyledMetaContainer> : "unknown words"}
) </StyledMetaContainer>
)
} }
export default Meta export default Meta

View file

@ -12,10 +12,10 @@ import NotFound from "../NotFound"
import SeriesControlButtons from "./SeriesControlButtons" import SeriesControlButtons from "./SeriesControlButtons"
import { import {
categorizePageType, categorizePageType,
fetchContent, fetchContent,
PageType, PageType,
parsePageData, parsePageData,
} from "./helper" } from "./helper"
import Meta from "./Meta" import Meta from "./Meta"
import Toc from "./Toc" import Toc from "./Toc"
@ -25,112 +25,114 @@ import type { PageData } from "@developomp-site/blog-content/src/types/types"
import contentMap from "../../contentMap" import contentMap from "../../contentMap"
const StyledTitle = styled.h1` const StyledTitle = styled.h1`
margin-bottom: 1rem; margin-bottom: 1rem;
word-wrap: break-word; word-wrap: break-word;
` `
export default function Page() { export default function Page() {
const { pathname } = useLocation() const { pathname } = useLocation()
const [pageData, setPageData] = useState<PageData | undefined>(undefined) const [pageData, setPageData] = useState<PageData | undefined>(undefined)
const [pageType, setPageType] = useState<PageType>(PageType.POST) const [pageType, setPageType] = useState<PageType>(PageType.POST)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
// this code runs if either the url or the locale changes // this code runs if either the url or the locale changes
useEffect(() => { useEffect(() => {
const content_id = pathname.replace(/\/$/, "") // remove trailing slash const content_id = pathname.replace(/\/$/, "") // remove trailing slash
const pageType = categorizePageType(content_id) const pageType = categorizePageType(content_id)
fetchContent(pageType, content_id).then((fetched_content) => { fetchContent(pageType, content_id).then((fetched_content) => {
if (!fetched_content) { if (!fetched_content) {
// stop loading without fetching pageData so 404 page will display // stop loading without fetching pageData so 404 page will display
setIsLoading(false) setIsLoading(false)
return return
} }
setPageData(parsePageData(fetched_content, pageType, content_id)) setPageData(parsePageData(fetched_content, pageType, content_id))
setPageType(pageType) setPageType(pageType)
setIsLoading(false) setIsLoading(false)
}) })
}, [pathname]) }, [pathname])
if (isLoading) return <Loading /> if (isLoading) return <Loading />
if (!pageData) return <NotFound /> if (!pageData) return <NotFound />
return ( return (
<> <>
<Helmet> <Helmet>
<title>pomp | {pageData.title}</title> <title>pomp | {pageData.title}</title>
<meta property="og:title" content={pageData.title} /> <meta property="og:title" content={pageData.title} />
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:image" content="/icon/icon.svg" /> <meta property="og:image" content="/icon/icon.svg" />
</Helmet> </Helmet>
<MainContent> <MainContent>
{/* next/previous series post buttons */} {/* next/previous series post buttons */}
{pageType == PageType.SERIES && ( {pageType == PageType.SERIES && (
<SeriesControlButtons <SeriesControlButtons
seriesHome={pageData.seriesHome} seriesHome={pageData.seriesHome}
prevURL={pageData.prev} prevURL={pageData.prev}
nextURL={pageData.next} nextURL={pageData.next}
/> />
)} )}
<StyledTitle>{pageData.title}</StyledTitle> <StyledTitle>{pageData.title}</StyledTitle>
<small> <small>
{/* Post tags */} {/* Post tags */}
{pageData.tags.length > 0 && ( {pageData.tags.length > 0 && (
<TagList direction="left"> <TagList direction="left">
{pageData.tags.map((tag) => { {pageData.tags.map((tag) => {
return ( return (
<div key={pageData?.title + tag}> <div key={pageData?.title + tag}>
<Tag text={tag} /> <Tag text={tag} />
</div> </div>
) )
})} })}
</TagList> </TagList>
)} )}
<br /> <br />
{/* Post metadata */} {/* Post metadata */}
{[PageType.POST, PageType.SERIES, PageType.SERIES_HOME].includes( {[
pageType PageType.POST,
) && <Meta fetchedPage={pageData} />} PageType.SERIES,
</small> PageType.SERIES_HOME,
].includes(pageType) && <Meta fetchedPage={pageData} />}
</small>
<hr /> <hr />
{/* add table of contents if it exists */} {/* add table of contents if it exists */}
<Toc data={pageData.toc} /> <Toc data={pageData.toc} />
{/* page content */} {/* page content */}
<div <div
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: pageData.content, __html: pageData.content,
}} }}
/> />
</MainContent> </MainContent>
{/* series post list */} {/* series post list */}
{pageType == PageType.SERIES_HOME && {pageType == PageType.SERIES_HOME &&
pageData.order.map((post) => { pageData.order.map((post) => {
return ( return (
<PostCard <PostCard
key={post} key={post}
postData={{ postData={{
content_id: post, content_id: post,
...contentMap.posts[post], ...contentMap.posts[post],
}} }}
/> />
) )
})} })}
</> </>
) )
} }

View file

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

View file

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

View file

@ -5,163 +5,165 @@ import type { PageData } from "@developomp-site/blog-content/src/types/types"
import contentMap from "../../contentMap" import contentMap from "../../contentMap"
export enum PageType { export enum PageType {
POST, POST,
SERIES, SERIES,
SERIES_HOME, SERIES_HOME,
PORTFOLIO_PROJECT, PORTFOLIO_PROJECT,
UNSEARCHABLE, UNSEARCHABLE,
} }
export async function fetchContent(pageType: PageType, url: string) { export async function fetchContent(pageType: PageType, url: string) {
try { try {
if (pageType == PageType.UNSEARCHABLE) { if (pageType == PageType.UNSEARCHABLE) {
return await import( return await import(
`@developomp-site/blog-content/dist/content/unsearchable${url}.json` `@developomp-site/blog-content/dist/content/unsearchable${url}.json`
) )
} else { } else {
return await import( return await import(
`@developomp-site/blog-content/dist/content${url}.json` `@developomp-site/blog-content/dist/content${url}.json`
) )
} }
} catch (err) { } catch (err) {
return return
} }
} }
export function categorizePageType(content_id: string): PageType { export function categorizePageType(content_id: string): PageType {
if (content_id.startsWith("/post")) return PageType.POST if (content_id.startsWith("/post")) return PageType.POST
if (content_id.startsWith("/portfolio")) return PageType.PORTFOLIO_PROJECT if (content_id.startsWith("/portfolio")) return PageType.PORTFOLIO_PROJECT
if (content_id.startsWith("/series")) { if (content_id.startsWith("/series")) {
// if the URL looks like /series/series-title (if the url has two slashes) // if the URL looks like /series/series-title (if the url has two slashes)
if ([...(content_id.match(/\//g) || [])].length == 2) if ([...(content_id.match(/\//g) || [])].length == 2)
return PageType.SERIES_HOME return PageType.SERIES_HOME
// if the URL looks like /series/series-title/post-title (if the url does not have 2 slashes) // if the URL looks like /series/series-title/post-title (if the url does not have 2 slashes)
return PageType.SERIES return PageType.SERIES
} }
return PageType.UNSEARCHABLE return PageType.UNSEARCHABLE
} }
export function parsePageData( export function parsePageData(
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
fetched_content: any, fetched_content: any,
pageType: PageType, pageType: PageType,
content_id: string content_id: string
): PageData { ): PageData {
// page date to be saved as a react state // page date to be saved as a react state
const pageData: PageData = { const pageData: PageData = {
title: "No title", title: "No title",
date: "Unknown date", date: "Unknown date",
readTime: "Unknown read time", readTime: "Unknown read time",
wordCount: 0, wordCount: 0,
tags: [], tags: [],
toc: undefined, toc: undefined,
content: "No content", content: "No content",
// series // series
seriesHome: "", seriesHome: "",
prev: "", prev: "",
next: "", next: "",
// series home // series home
order: [], order: [],
length: 0, length: 0,
// portfolio // portfolio
image: "", image: "",
overview: "", overview: "",
badges: [], badges: [],
repo: "", repo: "",
} }
// load and parse content differently depending on the content type // load and parse content differently depending on the content type
switch (pageType) { switch (pageType) {
case PageType.POST: { case PageType.POST: {
const post = contentMap.posts[content_id] const post = contentMap.posts[content_id]
pageData.content = fetched_content.content pageData.content = fetched_content.content
pageData.toc = fetched_content.toc pageData.toc = fetched_content.toc
pageData.title = post.title pageData.title = post.title
pageData.date = post.date pageData.date = post.date
pageData.readTime = post.readTime pageData.readTime = post.readTime
pageData.wordCount = post.wordCount pageData.wordCount = post.wordCount
pageData.tags = post.tags || [] pageData.tags = post.tags || []
break break
} }
case PageType.SERIES: { case PageType.SERIES: {
const seriesURL = content_id.slice(0, content_id.lastIndexOf("/")) const seriesURL = content_id.slice(0, content_id.lastIndexOf("/"))
const curr = contentMap.series[seriesURL].order.indexOf(content_id) const curr = contentMap.series[seriesURL].order.indexOf(content_id)
const prev = curr - 1 const prev = curr - 1
const next = curr + 1 const next = curr + 1
const post = contentMap.posts[content_id] const post = contentMap.posts[content_id]
pageData.content = fetched_content.content pageData.content = fetched_content.content
pageData.toc = fetched_content.toc pageData.toc = fetched_content.toc
pageData.title = post.title pageData.title = post.title
pageData.date = post.date pageData.date = post.date
pageData.readTime = post.readTime pageData.readTime = post.readTime
pageData.wordCount = post.wordCount pageData.wordCount = post.wordCount
pageData.tags = post.tags || [] pageData.tags = post.tags || []
pageData.seriesHome = seriesURL pageData.seriesHome = seriesURL
pageData.prev = pageData.prev =
prev >= 0 ? contentMap.series[seriesURL].order[prev] : undefined prev >= 0 ? contentMap.series[seriesURL].order[prev] : undefined
pageData.next = pageData.next =
next < contentMap.series[seriesURL].order.length next < contentMap.series[seriesURL].order.length
? contentMap.series[seriesURL].order[next] ? contentMap.series[seriesURL].order[next]
: undefined : undefined
break break
} }
case PageType.SERIES_HOME: { case PageType.SERIES_HOME: {
const seriesData = contentMap.series[content_id] const seriesData = contentMap.series[content_id]
pageData.title = seriesData.title pageData.title = seriesData.title
pageData.content = fetched_content.content pageData.content = fetched_content.content
pageData.date = seriesData.date pageData.date = seriesData.date
pageData.readTime = seriesData.readTime pageData.readTime = seriesData.readTime
pageData.wordCount = seriesData.wordCount pageData.wordCount = seriesData.wordCount
pageData.order = seriesData.order pageData.order = seriesData.order
pageData.length = seriesData.length pageData.length = seriesData.length
break break
} }
case PageType.PORTFOLIO_PROJECT: { case PageType.PORTFOLIO_PROJECT: {
const data = const data =
portfolio.projects[content_id as keyof typeof portfolio.projects] portfolio.projects[
content_id as keyof typeof portfolio.projects
]
pageData.content = fetched_content.content pageData.content = fetched_content.content
pageData.toc = fetched_content.toc pageData.toc = fetched_content.toc
pageData.title = data.name pageData.title = data.name
pageData.image = data.image pageData.image = data.image
pageData.overview = data.overview pageData.overview = data.overview
pageData.badges = data.badges pageData.badges = data.badges
pageData.repo = data.repo pageData.repo = data.repo
break break
} }
case PageType.UNSEARCHABLE: { case PageType.UNSEARCHABLE: {
pageData.title = contentMap.unsearchable[content_id].title pageData.title = contentMap.unsearchable[content_id].title
pageData.content = fetched_content.content pageData.content = fetched_content.content
break break
} }
} }
return pageData return pageData
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -14,44 +14,44 @@ import markCSS from "./mark"
import katexCSS from "./katex" import katexCSS from "./katex"
const globalCSS = css` const globalCSS = css`
body { body {
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
} }
html, html,
body, body,
#root { #root {
/* size */ /* size */
min-height: 100vh; min-height: 100vh;
margin: 0; margin: 0;
/* style */ /* style */
display: flex; display: flex;
flex-flow: column; flex-flow: column;
/* text */ /* text */
line-height: 2rem; line-height: 2rem;
font-size: 1rem; font-size: 1rem;
font-family: ${({ theme }) => theme.theme.font.sansSerif}; font-family: ${({ theme }) => theme.theme.font.sansSerif};
font-weight: 400; font-weight: 400;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
/* color */ /* color */
background-color: ${({ theme }) => theme.theme.color.background}; background-color: ${({ theme }) => theme.theme.color.background};
color: ${({ theme }) => theme.theme.color.text.default}; color: ${({ theme }) => theme.theme.color.text.default};
} }
* { * {
transition: color 0.1s linear; transition: color 0.1s linear;
scroll-behavior: smooth; scroll-behavior: smooth;
scroll-margin: 4rem; scroll-margin: 4rem;
} }
` `
/** /**

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,28 +1,28 @@
{ {
"compilerOptions": { "compilerOptions": {
"plugins": [ "plugins": [
{ {
"name": "@styled/typescript-styled-plugin", "name": "@styled/typescript-styled-plugin",
"validate": false "validate": false
} }
], ],
"target": "es5", "target": "es5",
"module": "esnext", "module": "esnext",
"lib": ["dom", "dom.iterable", "esnext"], "lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"strict": true, "strict": true,
"downlevelIteration": true, "downlevelIteration": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": ["src/**/*", "types/**/*"] "include": ["src/**/*", "types/**/*"]
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -1,40 +1,40 @@
{ {
"hosting": [ "hosting": [
{ {
"target": "main", "target": "main",
"cleanUrls": true, "cleanUrls": true,
"public": "apps/main/build", "public": "apps/main/build",
"rewrites": [ "rewrites": [
{ {
"source": "**", "source": "**",
"destination": "/index.html" "destination": "/index.html"
} }
], ],
"ignore": ["**/.*"] "ignore": ["**/.*"]
}, },
{ {
"target": "blog", "target": "blog",
"cleanUrls": true, "cleanUrls": true,
"public": "apps/blog/build", "public": "apps/blog/build",
"rewrites": [ "rewrites": [
{ {
"source": "**", "source": "**",
"destination": "/index.html" "destination": "/index.html"
} }
], ],
"ignore": ["**/.*"] "ignore": ["**/.*"]
}, },
{ {
"target": "portfolio", "target": "portfolio",
"cleanUrls": true, "cleanUrls": true,
"public": "apps/portfolio/dist", "public": "apps/portfolio/dist",
"rewrites": [ "rewrites": [
{ {
"source": "**", "source": "**",
"destination": "/index.html" "destination": "/index.html"
} }
], ],
"ignore": ["**/.*"] "ignore": ["**/.*"]
} }
] ]
} }

View file

@ -1,18 +1,18 @@
{ {
"private": true, "private": true,
"packageManager": "^pnpm@7.0.0", "packageManager": "^pnpm@7.0.0",
"scripts": { "scripts": {
"build": "turbo run build", "build": "turbo run build",
"dev": "turbo run dev --no-cache --parallel --continue", "dev": "turbo run dev --no-cache --parallel --continue",
"lint": "turbo run lint", "lint": "turbo run lint",
"clean": "turbo run clean && rm -rf node_modules", "clean": "turbo run clean && rm -rf node_modules",
"format": "prettier --write \"**/*.{ts,tsx,md}\"" "format": "prettier --write \"**/*.{ts,tsx,md}\""
}, },
"devDependencies": { "devDependencies": {
"@developomp-site/eslint-config": "workspace:*", "@developomp-site/eslint-config": "workspace:*",
"eslint": "^8.29.0", "eslint": "^8.29.0",
"prettier": "^2.8.1", "prettier": "^2.8.1",
"prettier-plugin-tailwindcss": "^0.2.0", "prettier-plugin-tailwindcss": "^0.2.0",
"turbo": "^1.10.6" "turbo": "^1.10.6"
} }
} }

View file

@ -1,41 +1,41 @@
{ {
"name": "@developomp-site/blog-content", "name": "@developomp-site/blog-content",
"version": "0.0.0", "version": "0.0.0",
"license": "MIT", "license": "MIT",
"files": [ "files": [
"dist/**" "dist/**"
], ],
"scripts": { "scripts": {
"build": "ts-node --experimental-specifier-resolution=node ./src", "build": "ts-node --experimental-specifier-resolution=node ./src",
"clean": "rm -rf .turbo node_modules dist" "clean": "rm -rf .turbo node_modules dist"
}, },
"dependencies": { "dependencies": {
"@developomp-site/tsconfig": "workspace:*", "@developomp-site/tsconfig": "workspace:*",
"@types/ejs": "^3.1.1", "@types/ejs": "^3.1.1",
"@types/katex": "^0.14.0", "@types/katex": "^0.14.0",
"@types/markdown-it": "^12.2.3", "@types/markdown-it": "^12.2.3",
"@types/read-time-estimate": "^0.0.0", "@types/read-time-estimate": "^0.0.0",
"@types/svgo": "^3.0.0", "@types/svgo": "^3.0.0",
"@types/tinycolor2": "^1.4.3", "@types/tinycolor2": "^1.4.3",
"canvas": "^2.11.2", "canvas": "^2.11.2",
"ejs": "^3.1.8", "ejs": "^3.1.8",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"markdown-it": "^13.0.1", "markdown-it": "^13.0.1",
"markdown-it-anchor": "^8.6.5", "markdown-it-anchor": "^8.6.5",
"markdown-it-attrs": "^4.1.4", "markdown-it-attrs": "^4.1.4",
"markdown-it-footnote": "^3.0.3", "markdown-it-footnote": "^3.0.3",
"markdown-it-highlight-lines": "^1.0.2", "markdown-it-highlight-lines": "^1.0.2",
"markdown-it-mark": "^3.0.1", "markdown-it-mark": "^3.0.1",
"markdown-it-sub": "^1.0.0", "markdown-it-sub": "^1.0.0",
"markdown-it-sup": "^1.0.0", "markdown-it-sup": "^1.0.0",
"markdown-it-task-checkbox": "^1.0.6", "markdown-it-task-checkbox": "^1.0.6",
"markdown-it-texmath": "^1.0.0", "markdown-it-texmath": "^1.0.0",
"markdown-toc": "^1.2.0", "markdown-toc": "^1.2.0",
"read-time-estimate": "^0.0.3", "read-time-estimate": "^0.0.3",
"simple-icons": "^7.21.0", "simple-icons": "^7.21.0",
"slugify": "^1.6.6", "slugify": "^1.6.6",
"svgo": "^3.0.2", "svgo": "^3.0.2",
"tinycolor2": "^1.4.2", "tinycolor2": "^1.4.2",
"typescript": "^4.9.4" "typescript": "^4.9.4"
} }
} }

View file

@ -16,19 +16,19 @@ import postProcess from "./postProcess"
import { ContentMap, ParseMode, PortfolioData, SeriesMap } from "./types/types" import { ContentMap, ParseMode, PortfolioData, SeriesMap } from "./types/types"
export const contentMap: ContentMap = { export const contentMap: ContentMap = {
date: {}, date: {},
tags: {}, tags: {},
meta: { meta: {
tags: [], tags: [],
}, },
posts: {}, posts: {},
series: {}, series: {},
unsearchable: {}, unsearchable: {},
} }
export const seriesMap: SeriesMap = {} export const seriesMap: SeriesMap = {}
export const portfolioData: PortfolioData = { export const portfolioData: PortfolioData = {
skills: new Set(), skills: new Set(),
projects: {}, projects: {},
} }
/** /**
@ -36,8 +36,8 @@ export const portfolioData: PortfolioData = {
*/ */
try { try {
fs.rmSync("dist", { recursive: true }) fs.rmSync("dist", { recursive: true })
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (err) {} } catch (err) {}
/** /**
@ -45,16 +45,16 @@ try {
*/ */
if (!fs.lstatSync(markdownPath).isDirectory()) if (!fs.lstatSync(markdownPath).isDirectory())
throw Error("Invalid markdown path") throw Error("Invalid markdown path")
if (!fs.lstatSync(markdownPath + "/posts").isDirectory()) if (!fs.lstatSync(markdownPath + "/posts").isDirectory())
throw Error(`Cannot find directory: ${markdownPath + "/posts"}`) throw Error(`Cannot find directory: ${markdownPath + "/posts"}`)
if (!fs.lstatSync(markdownPath + "/unsearchable").isDirectory()) if (!fs.lstatSync(markdownPath + "/unsearchable").isDirectory())
throw Error(`Cannot find directory: ${markdownPath + "/posts"}`) throw Error(`Cannot find directory: ${markdownPath + "/posts"}`)
if (!fs.lstatSync(markdownPath + "/series").isDirectory()) if (!fs.lstatSync(markdownPath + "/series").isDirectory())
throw Error(`Cannot find directory: ${markdownPath + "/posts"}`) throw Error(`Cannot find directory: ${markdownPath + "/posts"}`)
/** /**
* Parse * Parse
@ -77,11 +77,11 @@ postProcess()
fs.writeFileSync(mapFilePath, JSON.stringify(contentMap)) fs.writeFileSync(mapFilePath, JSON.stringify(contentMap))
fs.writeFileSync( fs.writeFileSync(
portfolioFilePath, portfolioFilePath,
JSON.stringify({ JSON.stringify({
...portfolioData, ...portfolioData,
skills: Array.from(portfolioData.skills), skills: Array.from(portfolioData.skills),
}) })
) )
saveIndex() saveIndex()

View file

@ -24,37 +24,37 @@ import { MarkdownData, ParseMode } from "./types/types"
const slugifyIt = (s: string) => slugify(s, { lower: true, strict: true }) const slugifyIt = (s: string) => slugify(s, { lower: true, strict: true })
const md = markdownIt({ const md = markdownIt({
// https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md // https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md
highlight: (str, lang) => { highlight: (str, lang) => {
if (lang && hljs.getLanguage(lang)) { if (lang && hljs.getLanguage(lang)) {
try { try {
return hljs.highlight(str, { language: lang }).value return hljs.highlight(str, { language: lang }).value
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (error) {} } catch (error) {}
} }
return "" // use external default escaping return "" // use external default escaping
}, },
html: true, html: true,
}) })
.use(markdownItTexMath, { .use(markdownItTexMath, {
engine: katex, engine: katex,
delimiters: "dollars", delimiters: "dollars",
}) })
.use(markdownItAnchor, { .use(markdownItAnchor, {
permalink: markdownItAnchor.permalink.ariaHidden({ permalink: markdownItAnchor.permalink.ariaHidden({
placement: "before", placement: "before",
symbol: "#", symbol: "#",
renderHref: (s) => `#${slugifyIt(s)}`, renderHref: (s) => `#${slugifyIt(s)}`,
}), }),
slugify: slugifyIt, slugify: slugifyIt,
}) })
.use(markdownItTaskCheckbox) .use(markdownItTaskCheckbox)
.use(markDownItMark) .use(markDownItMark)
.use(markdownItSub) .use(markdownItSub)
.use(markdownItSup) .use(markdownItSup)
.use(highlightLines) .use(highlightLines)
.use(markdownItFootnote) .use(markdownItFootnote)
/** /**
* parse the front matter if it exists * parse the front matter if it exists
@ -64,70 +64,70 @@ const md = markdownIt({
* @param {ParseMode} mode * @param {ParseMode} mode
*/ */
export default function parseMarkdown( export default function parseMarkdown(
markdownRaw: string, markdownRaw: string,
path: string, path: string,
mode: ParseMode mode: ParseMode
): MarkdownData { ): MarkdownData {
const fileHasFrontMatter = markdownRaw.startsWith("---") const fileHasFrontMatter = markdownRaw.startsWith("---")
const frontMatter = fileHasFrontMatter const frontMatter = fileHasFrontMatter
? matter(markdownRaw.slice(0, nthIndex(markdownRaw, "---", 2) + 3)).data ? matter(markdownRaw.slice(0, nthIndex(markdownRaw, "---", 2) + 3)).data
: {} : {}
if (fileHasFrontMatter) { if (fileHasFrontMatter) {
if (mode != ParseMode.PORTFOLIO) { if (mode != ParseMode.PORTFOLIO) {
if (!frontMatter.title) if (!frontMatter.title)
throw Error(`Title is not defined in file: ${path}`) throw Error(`Title is not defined in file: ${path}`)
if (mode != ParseMode.UNSEARCHABLE && !frontMatter.date) if (mode != ParseMode.UNSEARCHABLE && !frontMatter.date)
throw Error(`Date is not defined in file: ${path}`) throw Error(`Date is not defined in file: ${path}`)
} }
if (mode === ParseMode.PORTFOLIO) { if (mode === ParseMode.PORTFOLIO) {
if (frontMatter.overview) { if (frontMatter.overview) {
frontMatter.overview = md.render(frontMatter.overview) frontMatter.overview = md.render(frontMatter.overview)
} }
} }
} }
// //
// work with rendered DOM // work with rendered DOM
// //
const dom = new JSDOM( const dom = new JSDOM(
md.render( md.render(
fileHasFrontMatter fileHasFrontMatter
? markdownRaw.slice(nthIndex(markdownRaw, "---", 2) + 3) ? markdownRaw.slice(nthIndex(markdownRaw, "---", 2) + 3)
: markdownRaw : markdownRaw
) || "" ) || ""
) )
// add .hljs class to all block codes // add .hljs class to all block codes
dom.window.document.querySelectorAll("pre > code").forEach((item) => { dom.window.document.querySelectorAll("pre > code").forEach((item) => {
item.classList.add("hljs") item.classList.add("hljs")
}) })
// add parent div to tables (horizontally scroll table on small displays) // add parent div to tables (horizontally scroll table on small displays)
dom.window.document.querySelectorAll("table").forEach((item) => { dom.window.document.querySelectorAll("table").forEach((item) => {
// `element` is the element you want to wrap // `element` is the element you want to wrap
const parent = item.parentNode const parent = item.parentNode
if (!parent) return // stop if table doesn't have a parent node if (!parent) return // stop if table doesn't have a parent node
const wrapper = dom.window.document.createElement("div") const wrapper = dom.window.document.createElement("div")
wrapper.style.overflowX = "auto" wrapper.style.overflowX = "auto"
parent.replaceChild(wrapper, item) parent.replaceChild(wrapper, item)
wrapper.appendChild(item) wrapper.appendChild(item)
}) })
frontMatter.content = dom.window.document.documentElement.innerHTML frontMatter.content = dom.window.document.documentElement.innerHTML
return frontMatter as MarkdownData return frontMatter as MarkdownData
} }
export function generateToc(markdownRaw: string): string { export function generateToc(markdownRaw: string): string {
return md.render(toc(markdownRaw).content, { return md.render(toc(markdownRaw).content, {
slugify: slugifyIt, slugify: slugifyIt,
}) })
} }

View file

@ -1,21 +1,21 @@
{ {
"Programming Languages": [ "Programming Languages": [
"javascript", "javascript",
"typescript", "typescript",
"python", "python",
"rust", "rust",
"csharp C#" "csharp C#"
], ],
"Web Front End": ["react", "svelte", "tailwindcss Tailwind"], "Web Front End": ["react", "svelte", "tailwindcss Tailwind"],
"Desktop Front End": ["gtk", "electron", "tauri"], "Desktop Front End": ["gtk", "electron", "tauri"],
"Back End": ["firebase"], "Back End": ["firebase"],
"DevOps": ["docker", "githubactions GH Actions"], "DevOps": ["docker", "githubactions GH Actions"],
"Game Development": ["unity"], "Game Development": ["unity"],
"Etc": [ "Etc": [
"figma", "figma",
"markdown", "markdown",
"notion", "notion",
"google Google-Fu", "google Google-Fu",
"discord Discord Bot" "discord Discord Bot"
] ]
} }

View file

@ -1,9 +1,9 @@
svg { svg {
/* from github */ /* from github */
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial,
sans-serif, Apple Color Emoji, Segoe UI Emoji; sans-serif, Apple Color Emoji, Segoe UI Emoji;
font-size: 14px; font-size: 14px;
color: #777777; color: #777777;
} }
h1, h1,
@ -12,50 +12,50 @@ h3,
h4, h4,
h5, h5,
h6 { h6 {
text-align: center; text-align: center;
} }
.items-wrapper { .items-wrapper {
display: grid; display: grid;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(5, 1fr);
column-gap: 10px; column-gap: 10px;
row-gap: 15px; row-gap: 15px;
} }
.badge { .badge {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
text-align: center; text-align: center;
gap: 5px; gap: 5px;
} }
.badge-box { .badge-box {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border-radius: 7px; border-radius: 7px;
width: 70px; width: 70px;
height: 70px; height: 70px;
} }
.icon-container > svg { .icon-container > svg {
height: 40px !important; height: 40px !important;
} }
.white { .white {
color: white; color: white;
fill: white; fill: white;
} }
.black { .black {
color: black; color: black;
fill: black; fill: black;
} }

View file

@ -12,127 +12,127 @@ import skills from "./portfolio/skills.json"
import { writeToFile } from "./util" import { writeToFile } from "./util"
export default function postProcess() { export default function postProcess() {
sortDates() sortDates()
fillTags() fillTags()
parseSeries() parseSeries()
generatePortfolioSVGs() generatePortfolioSVGs()
} }
function sortDates() { function sortDates() {
const TmpDate = contentMap.date const TmpDate = contentMap.date
contentMap.date = {} contentMap.date = {}
Object.keys(TmpDate) Object.keys(TmpDate)
.sort() .sort()
.forEach((sortedDateKey) => { .forEach((sortedDateKey) => {
contentMap.date[sortedDateKey] = TmpDate[sortedDateKey] contentMap.date[sortedDateKey] = TmpDate[sortedDateKey]
}) })
} }
function fillTags() { function fillTags() {
contentMap.meta.tags = Object.keys(contentMap.tags) contentMap.meta.tags = Object.keys(contentMap.tags)
} }
function parseSeries() { function parseSeries() {
// sort series map // sort series map
for (const seriesURL in seriesMap) { for (const seriesURL in seriesMap) {
seriesMap[seriesURL].sort((a, b) => { seriesMap[seriesURL].sort((a, b) => {
if (a.index < b.index) return -1 if (a.index < b.index) return -1
if (a.index > b.index) return 1 if (a.index > b.index) return 1
return 0 return 0
}) })
} }
// series length and order // series length and order
for (const seriesURL in seriesMap) { for (const seriesURL in seriesMap) {
contentMap.series[seriesURL].length = seriesMap[seriesURL].length contentMap.series[seriesURL].length = seriesMap[seriesURL].length
contentMap.series[seriesURL].order = seriesMap[seriesURL].map( contentMap.series[seriesURL].order = seriesMap[seriesURL].map(
(item) => item.url (item) => item.url
) )
} }
} }
function generatePortfolioSVGs() { function generatePortfolioSVGs() {
/** /**
* render skills.svg * render skills.svg
*/ */
// todo: wait add ejs once it's available // todo: wait add ejs once it's available
const style = readFileSync("./src/portfolio/style.css", "utf-8") const style = readFileSync("./src/portfolio/style.css", "utf-8")
const data: { const data: {
[key: string]: Badge[] | { [key: string]: Badge[] } [key: string]: Badge[] | { [key: string]: Badge[] }
} = {} } = {}
// C O G N I T O - H A Z A R D // C O G N I T O - H A Z A R D
// THIS PART OF THE CODE WAS WRITTEN IN 3 AM // THIS PART OF THE CODE WAS WRITTEN IN 3 AM
// C O G N I T O - H A Z A R D // C O G N I T O - H A Z A R D
for (const key in skills) { for (const key in skills) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
if (skills[key] instanceof Array) { if (skills[key] instanceof Array) {
if (!data[key]) { if (!data[key]) {
data[key] = [] data[key] = []
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
;(skills[key] as string[]).forEach((badge) => ;(skills[key] as string[]).forEach((badge) =>
(data[key] as Badge[]).push(parseBadge(badge)) (data[key] as Badge[]).push(parseBadge(badge))
) )
} else { } else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
for (const subKey in skills[key]) { for (const subKey in skills[key]) {
if (!data[key]) data[key] = {} if (!data[key]) data[key] = {}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
if (!data[key][subKey]) { if (!data[key][subKey]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
data[key][subKey] = [] data[key][subKey] = []
} }
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
skills[key][subKey].forEach((badge: string) => skills[key][subKey].forEach((badge: string) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
(data[key][subKey] as Badge[]).push(parseBadge(badge)) (data[key][subKey] as Badge[]).push(parseBadge(badge))
) )
} }
} }
} }
const renderedSVG = ejs.render( const renderedSVG = ejs.render(
readFileSync("./src/portfolio/skills.ejs", "utf-8"), readFileSync("./src/portfolio/skills.ejs", "utf-8"),
{ style, data }, { style, data },
{ views: ["./src/portfolio"] } { views: ["./src/portfolio"] }
) )
writeToFile( writeToFile(
"./dist/public/img/skills.svg", "./dist/public/img/skills.svg",
optimize(renderedSVG, { multipass: true }).data optimize(renderedSVG, { multipass: true }).data
) )
} }
function parseBadge(badgeRaw: string): Badge { function parseBadge(badgeRaw: string): Badge {
const isMultiWord = badgeRaw.includes(" ") const isMultiWord = badgeRaw.includes(" ")
const words = badgeRaw.split(" ") const words = badgeRaw.split(" ")
const slug = words[0] const slug = words[0]
// @ts-ignore // @ts-ignore
const icon = icons["si" + slug[0].toUpperCase() + slug.slice(1)] const icon = icons["si" + slug[0].toUpperCase() + slug.slice(1)]
const color = tinycolor(icon.hex).lighten(5).desaturate(5) const color = tinycolor(icon.hex).lighten(5).desaturate(5)
return { return {
svg: icon.svg, svg: icon.svg,
hex: color.toHexString(), hex: color.toHexString(),
isDark: color.isDark(), isDark: color.isDark(),
title: isMultiWord ? words.slice(1).join(" ") : icon.title, title: isMultiWord ? words.slice(1).join(" ") : icon.title,
} }
} }

View file

@ -15,15 +15,15 @@ import { ParseMode } from "../types/types"
* Data that's passed from {@link parseFile} to other function * Data that's passed from {@link parseFile} to other function
*/ */
export interface DataToPass { export interface DataToPass {
path: string path: string
urlPath: string urlPath: string
markdownRaw: string markdownRaw: string
markdownData: { markdownData: {
content: string content: string
[key: string]: unknown [key: string]: unknown
} }
humanizedDuration: string humanizedDuration: string
totalWords: number totalWords: number
} }
/** /**
@ -33,23 +33,23 @@ export interface DataToPass {
* @param {string} path - path of file or folder * @param {string} path - path of file or folder
*/ */
export function recursiveParse(mode: ParseMode, path: string): void { export function recursiveParse(mode: ParseMode, path: string): void {
// get name of the file or folder that's currently being parsed // get name of the file or folder that's currently being parsed
const fileOrFolderName = path2FileOrFolderName(path) const fileOrFolderName = path2FileOrFolderName(path)
// stop if the file or folder starts with a underscore // stop if the file or folder starts with a underscore
if (fileOrFolderName.startsWith("_")) return if (fileOrFolderName.startsWith("_")) return
const stats = fs.lstatSync(path) const stats = fs.lstatSync(path)
// if it's a directory, call this function to every files/directories in it // if it's a directory, call this function to every files/directories in it
// if it's a file, parse it and then save it to file // if it's a file, parse it and then save it to file
if (stats.isDirectory()) { if (stats.isDirectory()) {
fs.readdirSync(path).map((childPath) => { fs.readdirSync(path).map((childPath) => {
recursiveParse(mode, `${path}/${childPath}`) recursiveParse(mode, `${path}/${childPath}`)
}) })
} else if (stats.isFile()) { } else if (stats.isFile()) {
parseFile(mode, path) parseFile(mode, path)
} }
} }
/** /**
@ -59,50 +59,50 @@ export function recursiveParse(mode: ParseMode, path: string): void {
* @param {string} path - path of the markdown file * @param {string} path - path of the markdown file
*/ */
function parseFile(mode: ParseMode, path: string): void { function parseFile(mode: ParseMode, path: string): void {
// stop if it is not a markdown file // stop if it is not a markdown file
if (!path.endsWith(".md")) { if (!path.endsWith(".md")) {
console.log(`Ignoring non markdown file at: ${path}`) console.log(`Ignoring non markdown file at: ${path}`)
return return
} }
/** /**
* Parse markdown * Parse markdown
*/ */
const markdownRaw = fs.readFileSync(path, "utf8") const markdownRaw = fs.readFileSync(path, "utf8")
const markdownData = parseMarkdown(markdownRaw, path, mode) const markdownData = parseMarkdown(markdownRaw, path, mode)
const { humanizedDuration, totalWords } = readTimeEstimate( const { humanizedDuration, totalWords } = readTimeEstimate(
markdownData.content, markdownData.content,
275, 275,
12, 12,
500, 500,
["img", "Image"] ["img", "Image"]
) )
const dataToPass: DataToPass = { const dataToPass: DataToPass = {
path, path,
urlPath: path2URL(path), urlPath: path2URL(path),
markdownRaw, markdownRaw,
markdownData, markdownData,
humanizedDuration, humanizedDuration,
totalWords, totalWords,
} }
switch (mode) { switch (mode) {
case ParseMode.POSTS: case ParseMode.POSTS:
parsePost(dataToPass) parsePost(dataToPass)
break break
case ParseMode.SERIES: case ParseMode.SERIES:
parseSeries(dataToPass) parseSeries(dataToPass)
break break
case ParseMode.UNSEARCHABLE: case ParseMode.UNSEARCHABLE:
parseUnsearchable(dataToPass) parseUnsearchable(dataToPass)
break break
case ParseMode.PORTFOLIO: case ParseMode.PORTFOLIO:
parseProjects(dataToPass) parseProjects(dataToPass)
break break
} }
} }

View file

@ -8,65 +8,70 @@ import { DataToPass } from "."
import { PostData } from "../types/types" import { PostData } from "../types/types"
export default function parsePost(data: DataToPass): void { export default function parsePost(data: DataToPass): void {
const { urlPath, markdownRaw, markdownData, humanizedDuration, totalWords } = const {
data urlPath,
markdownRaw,
markdownData,
humanizedDuration,
totalWords,
} = data
const postData: PostData = { const postData: PostData = {
title: markdownData.title as string, title: markdownData.title as string,
date: "", date: "",
readTime: humanizedDuration, readTime: humanizedDuration,
wordCount: totalWords, wordCount: totalWords,
tags: [], tags: [],
} }
/** /**
* Dates * Dates
*/ */
const postDate = new Date(markdownData.date as string) const postDate = new Date(markdownData.date as string)
postData.date = postDate.toLocaleString("default", { postData.date = postDate.toLocaleString("default", {
month: "short", month: "short",
day: "numeric", day: "numeric",
year: "numeric", year: "numeric",
}) })
const YYYY_MM_DD = postDate.toISOString().split("T")[0] const YYYY_MM_DD = postDate.toISOString().split("T")[0]
if (contentMap.date[YYYY_MM_DD]) { if (contentMap.date[YYYY_MM_DD]) {
contentMap.date[YYYY_MM_DD].push(urlPath) contentMap.date[YYYY_MM_DD].push(urlPath)
} else { } else {
contentMap.date[YYYY_MM_DD] = [urlPath] contentMap.date[YYYY_MM_DD] = [urlPath]
} }
/** /**
* Tags * Tags
*/ */
postData.tags = markdownData.tags as string[] postData.tags = markdownData.tags as string[]
if (postData.tags) { if (postData.tags) {
postData.tags.forEach((tag) => { postData.tags.forEach((tag) => {
if (contentMap.tags[tag]) { if (contentMap.tags[tag]) {
contentMap.tags[tag].push(urlPath) contentMap.tags[tag].push(urlPath)
} else { } else {
contentMap.tags[tag] = [urlPath] contentMap.tags[tag] = [urlPath]
} }
}) })
} }
/** /**
* *
*/ */
contentMap.posts[urlPath] = postData contentMap.posts[urlPath] = postData
addDocument({ addDocument({
title: markdownData.title, title: markdownData.title,
body: markdownData.content, body: markdownData.content,
url: urlPath, url: urlPath,
}) })
writeToFile( writeToFile(
`${contentDirectoryPath}${urlPath}.json`, `${contentDirectoryPath}${urlPath}.json`,
JSON.stringify({ JSON.stringify({
content: markdownData.content, content: markdownData.content,
toc: generateToc(markdownRaw), toc: generateToc(markdownRaw),
}) })
) )
} }

View file

@ -9,46 +9,46 @@ import { portfolioData } from ".."
import { DataToPass } from "." import { DataToPass } from "."
export default function parseProjects(data: DataToPass): void { export default function parseProjects(data: DataToPass): void {
const { urlPath, markdownRaw, markdownData } = data const { urlPath, markdownRaw, markdownData } = data
if (markdownData.badges) { if (markdownData.badges) {
;(markdownData.badges as string[]).forEach((slug) => { ;(markdownData.badges as string[]).forEach((slug) => {
// todo: handle cases when icon is not on simple-icons // todo: handle cases when icon is not on simple-icons
const icon: SimpleIcon = const icon: SimpleIcon =
// @ts-ignore // @ts-ignore
icons["si" + slug[0].toUpperCase() + slug.slice(1)] icons["si" + slug[0].toUpperCase() + slug.slice(1)]
portfolioData.skills.add(slug) portfolioData.skills.add(slug)
const color = tinycolor(icon.hex).lighten(5).desaturate(5) const color = tinycolor(icon.hex).lighten(5).desaturate(5)
// save svg icon // save svg icon
writeToFile( writeToFile(
`${iconsDirectoryPath}/${icon.slug}.json`, `${iconsDirectoryPath}/${icon.slug}.json`,
JSON.stringify({ JSON.stringify({
svg: icon.svg, svg: icon.svg,
hex: color.toHexString(), hex: color.toHexString(),
isDark: color.isDark(), isDark: color.isDark(),
title: icon.title, title: icon.title,
}) })
) )
}) })
} }
// remove /projects/ prefix // remove /projects/ prefix
portfolioData.projects[urlPath.replace("/projects/", "")] = { portfolioData.projects[urlPath.replace("/projects/", "")] = {
name: markdownData.name as string, name: markdownData.name as string,
image: markdownData.image as string, image: markdownData.image as string,
overview: markdownData.overview as string, overview: markdownData.overview as string,
badges: (markdownData.badges as string[]) || [], badges: (markdownData.badges as string[]) || [],
repo: (markdownData.repo as string) || "", repo: (markdownData.repo as string) || "",
} }
writeToFile( writeToFile(
`${contentDirectoryPath}${urlPath}.json`, `${contentDirectoryPath}${urlPath}.json`,
JSON.stringify({ JSON.stringify({
content: markdownData.content, content: markdownData.content,
toc: generateToc(markdownRaw), toc: generateToc(markdownRaw),
}) })
) )
} }

View file

@ -8,140 +8,141 @@ import { DataToPass } from "."
import { PostData } from "../types/types" import { PostData } from "../types/types"
export default function parseSeries(data: DataToPass): void { export default function parseSeries(data: DataToPass): void {
const { const {
path, path,
urlPath: _urlPath, urlPath: _urlPath,
markdownRaw, markdownRaw,
markdownData, markdownData,
humanizedDuration, humanizedDuration,
totalWords, totalWords,
} = data } = data
// last part of the url without the slash // last part of the url without the slash
let lastPath = _urlPath.slice(_urlPath.lastIndexOf("/") + 1) let lastPath = _urlPath.slice(_urlPath.lastIndexOf("/") + 1)
if (!lastPath.includes("_") && !lastPath.startsWith("0")) if (!lastPath.includes("_") && !lastPath.startsWith("0"))
throw Error(`Invalid series file name at: "${path}"`) throw Error(`Invalid series file name at: "${path}"`)
// if file is a series descriptor or not (not = regular series post) // if file is a series descriptor or not (not = regular series post)
const isFileDescriptor = lastPath.startsWith("0") && !lastPath.includes("_") const isFileDescriptor = lastPath.startsWith("0") && !lastPath.includes("_")
// series post url // series post url
if (isFileDescriptor) { if (isFileDescriptor) {
lastPath = "" lastPath = ""
} else { } else {
lastPath = lastPath lastPath = lastPath
.slice(lastPath.indexOf("_") + 1) // get string after the series index .slice(lastPath.indexOf("_") + 1) // get string after the series index
.replace(/\/$/, "") // remove trailing slash .replace(/\/$/, "") // remove trailing slash
} }
// get url until right before the lastPath // get url until right before the lastPath
const urlUntilLastPath = _urlPath.slice(0, _urlPath.lastIndexOf("/") + 1) const urlUntilLastPath = _urlPath.slice(0, _urlPath.lastIndexOf("/") + 1)
// remove trailing slash if it's a regular series post // remove trailing slash if it's a regular series post
const urlPath = const urlPath =
(isFileDescriptor (isFileDescriptor
? urlUntilLastPath.replace(/\/$/, "") ? urlUntilLastPath.replace(/\/$/, "")
: urlUntilLastPath) + lastPath : urlUntilLastPath) + lastPath
// todo: separate interface for series descriptor (no word count and read time) // todo: separate interface for series descriptor (no word count and read time)
const postData: PostData = { const postData: PostData = {
title: markdownData.title as string, title: markdownData.title as string,
date: "", date: "",
readTime: humanizedDuration, readTime: humanizedDuration,
wordCount: totalWords, wordCount: totalWords,
tags: [], tags: [],
} }
/** /**
* Date * Date
*/ */
const postDate = new Date(markdownData.date as string) const postDate = new Date(markdownData.date as string)
postData.date = postDate.toLocaleString("default", { postData.date = postDate.toLocaleString("default", {
month: "short", month: "short",
day: "numeric", day: "numeric",
year: "numeric", year: "numeric",
}) })
const YYYY_MM_DD = postDate.toISOString().split("T")[0] const YYYY_MM_DD = postDate.toISOString().split("T")[0]
if (contentMap.date[YYYY_MM_DD]) { if (contentMap.date[YYYY_MM_DD]) {
contentMap.date[YYYY_MM_DD].push(urlPath) contentMap.date[YYYY_MM_DD].push(urlPath)
} else { } else {
contentMap.date[YYYY_MM_DD] = [urlPath] contentMap.date[YYYY_MM_DD] = [urlPath]
} }
/** /**
* Tags * Tags
*/ */
postData.tags = markdownData.tags as string[] postData.tags = markdownData.tags as string[]
if (postData.tags) { if (postData.tags) {
postData.tags.forEach((tag) => { postData.tags.forEach((tag) => {
if (contentMap.tags[tag]) { if (contentMap.tags[tag]) {
contentMap.tags[tag].push(urlPath) contentMap.tags[tag].push(urlPath)
} else { } else {
contentMap.tags[tag] = [urlPath] contentMap.tags[tag] = [urlPath]
} }
}) })
} }
/** /**
* *
*/ */
addDocument({ addDocument({
title: markdownData.title, title: markdownData.title,
body: markdownData.content, body: markdownData.content,
url: urlPath, url: urlPath,
}) })
contentMap.posts[urlPath] = postData contentMap.posts[urlPath] = postData
// series markdown starting with 0 is a series descriptor // series markdown starting with 0 is a series descriptor
if (isFileDescriptor) { if (isFileDescriptor) {
contentMap.series[urlPath] = { contentMap.series[urlPath] = {
...postData, ...postData,
order: [], order: [],
length: 0, length: 0,
} }
} else { } else {
// put series post in appropriate series // put series post in appropriate series
for (const key of Object.keys(contentMap.series)) { for (const key of Object.keys(contentMap.series)) {
if (urlPath.includes(key)) { if (urlPath.includes(key)) {
const index = parseInt( const index = parseInt(
_urlPath.slice( _urlPath.slice(
_urlPath.lastIndexOf("/") + 1, _urlPath.lastIndexOf("/") + 1,
_urlPath.lastIndexOf("_") _urlPath.lastIndexOf("_")
) )
) )
if (isNaN(index)) throw Error(`Invalid series index at: ${path}`) if (isNaN(index))
throw Error(`Invalid series index at: ${path}`)
const itemToPush = { const itemToPush = {
index: index, index: index,
url: urlPath, url: urlPath,
} }
if (seriesMap[key]) { if (seriesMap[key]) {
seriesMap[key].push(itemToPush) seriesMap[key].push(itemToPush)
} else { } else {
seriesMap[key] = [itemToPush] seriesMap[key] = [itemToPush]
} }
break break
} }
} }
} }
/** /**
* Save content * Save content
*/ */
writeToFile( writeToFile(
`${contentDirectoryPath}${urlPath}.json`, `${contentDirectoryPath}${urlPath}.json`,
JSON.stringify({ JSON.stringify({
content: markdownData.content, content: markdownData.content,
toc: generateToc(markdownRaw), toc: generateToc(markdownRaw),
}) })
) )
} }

View file

@ -5,30 +5,30 @@ import { contentMap } from ".."
import { DataToPass } from "." import { DataToPass } from "."
export default function parseUnsearchable(data: DataToPass): void { export default function parseUnsearchable(data: DataToPass): void {
const { urlPath: _urlPath, markdownData } = data const { urlPath: _urlPath, markdownData } = data
// convert path like /XXX/YYY/ZZZ to /YYY/ZZZ // convert path like /XXX/YYY/ZZZ to /YYY/ZZZ
const urlPath = _urlPath.slice(_urlPath.slice(1).indexOf("/") + 1) const urlPath = _urlPath.slice(_urlPath.slice(1).indexOf("/") + 1)
addDocument({ addDocument({
title: markdownData.title, title: markdownData.title,
body: markdownData.content, body: markdownData.content,
url: urlPath, url: urlPath,
}) })
// Parse data that will be written to map.js // Parse data that will be written to map.js
contentMap.unsearchable[urlPath] = { contentMap.unsearchable[urlPath] = {
title: markdownData.title as string, title: markdownData.title as string,
} }
/** /**
* Save content * Save content
*/ */
writeToFile( writeToFile(
`${contentDirectoryPath}/unsearchable${urlPath}.json`, `${contentDirectoryPath}/unsearchable${urlPath}.json`,
JSON.stringify({ JSON.stringify({
content: markdownData.content, content: markdownData.content,
}) })
) )
} }

View file

@ -8,19 +8,19 @@ import elasticlunr from "elasticlunr"
import { searchIndexFilePath } from "./config" import { searchIndexFilePath } from "./config"
const elasticlunrIndex = elasticlunr(function () { const elasticlunrIndex = elasticlunr(function () {
this.addField("title" as never) this.addField("title" as never)
this.addField("body" as never) this.addField("body" as never)
this.setRef("url" as never) this.setRef("url" as never)
}) })
export function addDocument(doc: { export function addDocument(doc: {
title?: unknown title?: unknown
body?: string body?: string
url?: string url?: string
}) { }) {
elasticlunrIndex.addDoc(doc) elasticlunrIndex.addDoc(doc)
} }
export function saveIndex() { export function saveIndex() {
fs.writeFileSync(searchIndexFilePath, JSON.stringify(elasticlunrIndex)) fs.writeFileSync(searchIndexFilePath, JSON.stringify(elasticlunrIndex))
} }

View file

@ -1,4 +1,4 @@
declare module "markdown-it-texmath" { declare module "markdown-it-texmath" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function texmath(md: MarkdownIt, ...params: any[]): void export default function texmath(md: MarkdownIt, ...params: any[]): void
} }

View file

@ -1,6 +1,6 @@
declare module "markdown-toc" { declare module "markdown-toc" {
export default function toc(str: string): { export default function toc(str: string): {
json: JSON json: JSON
content: string content: string
} }
} }

View file

@ -1,32 +1,32 @@
export interface ContentMap { export interface ContentMap {
// key: YYYY-MM-DD // key: YYYY-MM-DD
// value: url // value: url
date: { [key: string]: string[] } date: { [key: string]: string[] }
// key: tag name // key: tag name
// value: url // value: url
tags: { tags: {
[key: string]: string[] [key: string]: string[]
} }
// list of all meta data // list of all meta data
meta: { meta: {
tags: string[] tags: string[]
} }
// searchable, non-series posts // searchable, non-series posts
// must have a post date // must have a post date
// tag is not required // tag is not required
posts: { posts: {
[key: string]: PostData [key: string]: PostData
} }
// series posts have "previous post" and "next post" button so they need to be ordered // series posts have "previous post" and "next post" button so they need to be ordered
series: { [key: string]: Series } series: { [key: string]: Series }
// urls of unsearchable posts // urls of unsearchable posts
// it is here to quickly check if a post exists or not // it is here to quickly check if a post exists or not
unsearchable: { [key: string]: { title: string } } unsearchable: { [key: string]: { title: string } }
} }
/** /**
@ -34,58 +34,58 @@ export interface ContentMap {
*/ */
export enum ParseMode { export enum ParseMode {
POSTS, POSTS,
SERIES, SERIES,
UNSEARCHABLE, UNSEARCHABLE,
PORTFOLIO, PORTFOLIO,
} }
export interface MarkdownData { export interface MarkdownData {
content: string content: string
[key: string]: unknown [key: string]: unknown
} }
export interface PostData { export interface PostData {
title: string title: string
date: string date: string
readTime: string readTime: string
wordCount: number wordCount: number
tags?: string[] tags?: string[]
} }
export interface PageData { export interface PageData {
title: string title: string
date: string date: string
readTime: string readTime: string
wordCount: number wordCount: number
tags: string[] tags: string[]
toc?: string toc?: string
content: string content: string
// series // series
seriesHome: string seriesHome: string
prev?: string prev?: string
next?: string next?: string
// series home // series home
order: string[] order: string[]
length: number length: number
// portfolio // portfolio
image: string // image url image: string // image url
overview: string overview: string
badges: string[] badges: string[]
repo: string repo: string
} }
export interface Badge { export interface Badge {
svg: string svg: string
hex: string hex: string
isDark: boolean isDark: boolean
title: string title: string
} }
/** /**
@ -93,23 +93,23 @@ export interface Badge {
*/ */
export interface Series { export interface Series {
title: string title: string
date: string date: string
readTime: string readTime: string
wordCount: number wordCount: number
order: string[] order: string[]
length: number length: number
tags?: string[] tags?: string[]
} }
export interface SeriesMap { export interface SeriesMap {
// key: url // key: url
[key: string]: SeriesEntry[] [key: string]: SeriesEntry[]
} }
export interface SeriesEntry { export interface SeriesEntry {
index: number index: number
url: string url: string
} }
/** /**
@ -117,25 +117,25 @@ export interface SeriesEntry {
*/ */
export interface PortfolioData { export interface PortfolioData {
// a set of valid simple icons slug // a set of valid simple icons slug
skills: Set<string> skills: Set<string>
// key: url // key: url
projects: { projects: {
[key: string]: PortfolioProject [key: string]: PortfolioProject
} }
} }
export interface PortfolioOverview { export interface PortfolioOverview {
// link to my github // link to my github
github: string github: string
description: string description: string
} }
export interface PortfolioProject { export interface PortfolioProject {
name: string name: string
image: string // url to the image image: string // url to the image
overview: string overview: string
badges: string[] // array of valid simpleIcons slug badges: string[] // array of valid simpleIcons slug
repo: string // url of the git repository repo: string // url of the git repository
} }

View file

@ -9,9 +9,9 @@ import { markdownPath } from "./config"
* @param {string} pathToConvert * @param {string} pathToConvert
*/ */
export function path2URL(pathToConvert: string): string { export function path2URL(pathToConvert: string): string {
return `/${relative(markdownPath, pathToConvert)}` return `/${relative(markdownPath, pathToConvert)}`
.replace(/\.[^/.]+$/, "") // remove the file extension .replace(/\.[^/.]+$/, "") // remove the file extension
.replace(/ /g, "-") // replace all space with a dash .replace(/ /g, "-") // replace all space with a dash
} }
/** /**
@ -20,33 +20,34 @@ export function path2URL(pathToConvert: string): string {
* @param {string} inputPath - path to parse * @param {string} inputPath - path to parse
*/ */
export function path2FileOrFolderName(inputPath: string): string { export function path2FileOrFolderName(inputPath: string): string {
// remove trailing slash // remove trailing slash
if (inputPath[-1] == "/") inputPath = inputPath.slice(0, inputPath.length - 1) if (inputPath[-1] == "/")
inputPath = inputPath.slice(0, inputPath.length - 1)
// get the last section // get the last section
return inputPath.slice(inputPath.lastIndexOf("/") + 1) return inputPath.slice(inputPath.lastIndexOf("/") + 1)
} }
// gets the nth occurance of a pattern in string // gets the nth occurance of a pattern in string
// returns -1 if nothing is found // returns -1 if nothing is found
// https://stackoverflow.com/a/14482123/12979111 // https://stackoverflow.com/a/14482123/12979111
export function nthIndex(str: string, pat: string, n: number) { export function nthIndex(str: string, pat: string, n: number) {
let i = -1 let i = -1
while (n-- && i++ < str.length) { while (n-- && i++ < str.length) {
i = str.indexOf(pat, i) i = str.indexOf(pat, i)
if (i < 0) break if (i < 0) break
} }
return i return i
} }
export function writeToFile(filePath: string, dataToWrite: string) { export function writeToFile(filePath: string, dataToWrite: string) {
// create directory to put the files // create directory to put the files
fs.mkdirSync(filePath.slice(0, filePath.lastIndexOf("/")), { fs.mkdirSync(filePath.slice(0, filePath.lastIndexOf("/")), {
recursive: true, recursive: true,
}) })
// write content to the file // write content to the file
fs.writeFileSync(filePath, dataToWrite) fs.writeFileSync(filePath, dataToWrite)
} }

View file

@ -1,16 +1,16 @@
{ {
"extends": "@developomp-site/tsconfig/node16.json", "extends": "@developomp-site/tsconfig/node16.json",
"include": ["src"], "include": ["src"],
"ts-node": { "ts-node": {
"esm": true "esm": true
}, },
"compilerOptions": { "compilerOptions": {
"moduleResolution": "Node", "moduleResolution": "Node",
"isolatedModules": false, "isolatedModules": false,
"noImplicitAny": false, "noImplicitAny": false,
"esModuleInterop": true, "esModuleInterop": true,
"allowJs": true, "allowJs": true,
"resolveJsonModule": true "resolveJsonModule": true
}, },
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]
} }

View file

@ -1,17 +1,20 @@
/** @type {import("eslint").Linter.Config} */
module.exports = { module.exports = {
extends: [ root: true,
"plugin:@typescript-eslint/recommended", extends: [
"plugin:json/recommended", "eslint:recommended",
"eslint:recommended", "plugin:@typescript-eslint/recommended",
"prettier", "prettier",
"turbo", "turbo",
], ],
parser: "@typescript-eslint/parser", parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"], plugins: ["@typescript-eslint", "simple-import-sort"],
rules: { rules: {
"@next/next/no-html-link-for-pages": "off", // import related
"simple-import-sort/imports": "error",
"@typescript-eslint/explicit-module-boundary-types": "off", "simple-import-sort/exports": "error",
"@typescript-eslint/no-empty-interface": "off", "import/first": "error",
}, "import/newline-after-import": "error",
"import/no-duplicates": "error",
},
} }

View file

@ -1,16 +1,18 @@
{ {
"name": "@developomp-site/eslint-config", "name": "@developomp-site/eslint-config",
"version": "0.0.0", "version": "0.0.0",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"clean": "rm -rf node_modules" "clean": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.46.0", "@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.46.0", "@typescript-eslint/parser": "^5.60.1",
"eslint-config-next": "^12.3.4", "eslint": "^8.43.0",
"eslint-config-prettier": "^8.5.0", "eslint-config-prettier": "^8.8.0",
"eslint-config-turbo": "latest", "eslint-config-turbo": "latest",
"eslint-plugin-json": "^3.1.0" "eslint-plugin-json": "^3.1.0",
} "eslint-plugin-simple-import-sort": "^10.0.0",
"typescript": "^5.1.6"
}
} }

View file

@ -1,12 +1,12 @@
{ {
"name": "@developomp-site/tailwind-config", "name": "@developomp-site/tailwind-config",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"clean": "rm -rf node_modules" "clean": "rm -rf node_modules"
}, },
"devDependencies": { "devDependencies": {
"tailwindcss": "^3.2.4" "tailwindcss": "^3.2.4"
} }
} }

View file

@ -1,8 +1,8 @@
module.exports = { module.exports = {
content: [ content: [
// app content // app content
`src/**/*.{js,ts,jsx,tsx}`, `src/**/*.{js,ts,jsx,tsx}`,
// include packages if not transpiling // include packages if not transpiling
// "../../packages/**/*.{js,ts,jsx,tsx}", // "../../packages/**/*.{js,ts,jsx,tsx}",
], ],
} }

View file

@ -1,5 +1,5 @@
{ {
"env": { "env": {
"node": true "node": true
} }
} }

View file

@ -1,143 +1,143 @@
export interface Theme { export interface Theme {
font: { font: {
sansSerif: string sansSerif: string
monospace: string monospace: string
} }
color: { color: {
text: { text: {
highContrast: string highContrast: string
default: string default: string
gray: string gray: string
} }
background: string background: string
} }
maxDisplayWidth: { maxDisplayWidth: {
mobile: string mobile: string
desktop: string desktop: string
} }
component: { component: {
anchor: { anchor: {
color: { color: {
default: string default: string
hover: string hover: string
active: string active: string
header: string header: string
} }
} }
blockQuote: { blockQuote: {
color: { color: {
background: string background: string
borderLeft: string borderLeft: string
} }
} }
card: { card: {
color: { color: {
background: string background: string
hoverGlow: string hoverGlow: string
} }
} }
code: { code: {
inline: { inline: {
color: { color: {
text: string text: string
background: string background: string
border: string border: string
} }
} }
block: { block: {
color: { color: {
border: string border: string
highlight: string highlight: string
} }
style: string style: string
} }
} }
footer: { footer: {
color: { color: {
background: string background: string
text: string text: string
} }
} }
header: { header: {
color: { color: {
background: string background: string
hover: string hover: string
text: string text: string
} }
height: string height: string
} }
input: { input: {
color: { color: {
background: { background: {
default: string default: string
itemHover: string itemHover: string
} }
border: { border: {
default: string default: string
hover: string hover: string
focus: string focus: string
} }
placeHolder: string placeHolder: string
} }
} }
kbd: { kbd: {
color: { color: {
text: string text: string
border: string border: string
outerShadow: string outerShadow: string
innerShadow: string innerShadow: string
background: string background: string
} }
} }
mark: { mark: {
color: { color: {
text: string text: string
background: string background: string
} }
} }
scrollbar: { scrollbar: {
color: { color: {
track: string track: string
thumb: string thumb: string
} }
width: string width: string
borderRadius: string borderRadius: string
} }
scrollProgressBar: { scrollProgressBar: {
color: { color: {
background: string background: string
foreground: string foreground: string
} }
} }
table: { table: {
color: { color: {
border: string border: string
even: string even: string
} }
} }
ui: { ui: {
color: { color: {
background: { background: {
default: string default: string
hover: string hover: string
} }
border: string border: string
} }
} }
} }
} }

View file

@ -1,22 +1,22 @@
{ {
"name": "@developomp-site/theme", "name": "@developomp-site/theme",
"version": "0.0.0", "version": "0.0.0",
"types": "./index.d.ts", "types": "./index.d.ts",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "nodemon --ignore dist/ --exec pnpm build", "dev": "nodemon --ignore dist/ --exec pnpm build",
"build": "npx ts-node ./build.ts", "build": "npx ts-node ./build.ts",
"clean": "rm -rf .turbo node_modules dist" "clean": "rm -rf .turbo node_modules dist"
}, },
"devDependencies": { "devDependencies": {
"@types/merge-deep": "^3.0.0", "@types/merge-deep": "^3.0.0",
"@types/node": "^18.11.11", "@types/node": "^18.11.11",
"merge-deep": "^3.0.3", "merge-deep": "^3.0.3",
"nodemon": "^2.0.20", "nodemon": "^2.0.20",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsup": "^5.12.9", "tsup": "^5.12.9",
"utility-types": "^3.10.0" "utility-types": "^3.10.0"
} }
} }

View file

@ -1,71 +1,71 @@
/* from highlight.js/styles/atom-one-dark-reasonable.css */ /* from highlight.js/styles/atom-one-dark-reasonable.css */
pre code.hljs { pre code.hljs {
display: block; display: block;
overflow-x: auto; overflow-x: auto;
padding: 1em; padding: 1em;
} }
code.hljs { code.hljs {
padding: 3px 5px; padding: 3px 5px;
} }
.hljs { .hljs {
color: #abb2bf; color: #abb2bf;
background: #282c34; background: #282c34;
} }
.hljs-keyword, .hljs-keyword,
.hljs-operator, .hljs-operator,
.hljs-pattern-match { .hljs-pattern-match {
color: #f92672; color: #f92672;
} }
.hljs-function, .hljs-function,
.hljs-pattern-match .hljs-constructor { .hljs-pattern-match .hljs-constructor {
color: #61aeee; color: #61aeee;
} }
.hljs-function .hljs-params { .hljs-function .hljs-params {
color: #a6e22e; color: #a6e22e;
} }
.hljs-function .hljs-params .hljs-typing { .hljs-function .hljs-params .hljs-typing {
color: #fd971f; color: #fd971f;
} }
.hljs-module-access .hljs-module { .hljs-module-access .hljs-module {
color: #7e57c2; color: #7e57c2;
} }
.hljs-constructor { .hljs-constructor {
color: #e2b93d; color: #e2b93d;
} }
.hljs-constructor .hljs-string { .hljs-constructor .hljs-string {
color: #9ccc65; color: #9ccc65;
} }
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: #b18eb1; color: #b18eb1;
font-style: italic; font-style: italic;
} }
.hljs-doctag, .hljs-doctag,
.hljs-formula { .hljs-formula {
color: #c678dd; color: #c678dd;
} }
.hljs-deletion, .hljs-deletion,
.hljs-name, .hljs-name,
.hljs-section, .hljs-section,
.hljs-selector-tag, .hljs-selector-tag,
.hljs-subst { .hljs-subst {
color: #e06c75; color: #e06c75;
} }
.hljs-literal { .hljs-literal {
color: #56b6c2; color: #56b6c2;
} }
.hljs-addition, .hljs-addition,
.hljs-attribute, .hljs-attribute,
.hljs-meta .hljs-string, .hljs-meta .hljs-string,
.hljs-regexp, .hljs-regexp,
.hljs-string { .hljs-string {
color: #98c379; color: #98c379;
} }
.hljs-built_in, .hljs-built_in,
.hljs-class .hljs-title, .hljs-class .hljs-title,
.hljs-title.class_ { .hljs-title.class_ {
color: #e6c07b; color: #e6c07b;
} }
.hljs-attr, .hljs-attr,
.hljs-number, .hljs-number,
@ -75,7 +75,7 @@ code.hljs {
.hljs-template-variable, .hljs-template-variable,
.hljs-type, .hljs-type,
.hljs-variable { .hljs-variable {
color: #d19a66; color: #d19a66;
} }
.hljs-bullet, .hljs-bullet,
.hljs-link, .hljs-link,
@ -83,14 +83,14 @@ code.hljs {
.hljs-selector-id, .hljs-selector-id,
.hljs-symbol, .hljs-symbol,
.hljs-title { .hljs-title {
color: #61aeee; color: #61aeee;
} }
.hljs-emphasis { .hljs-emphasis {
font-style: italic; font-style: italic;
} }
.hljs-strong { .hljs-strong {
font-weight: 700; font-weight: 700;
} }
.hljs-link { .hljs-link {
text-decoration: underline; text-decoration: underline;
} }

View file

@ -3,165 +3,145 @@ import type { Theme } from "../.."
import { readFileSync } from "fs" import { readFileSync } from "fs"
export default { export default {
font: { font: {
sansSerif: "'Noto Sans KR', sans-serif", // https://fonts.google.com/noto/specimen/Noto+Sans+KR sansSerif: "'Noto Sans KR', sans-serif", // https://fonts.google.com/noto/specimen/Noto+Sans+KR
monospace: "'Source Code Pro', monospace", monospace: "'Source Code Pro', monospace",
}, },
color: { color: {
text: { text: {
highContrast: "#FFFFFF", highContrast: "#FFFFFF",
default: "#EEEEEE", default: "#EEEEEE",
gray: "#CCC", gray: "#CCC",
}, },
background: "#36393F", background: "#36393F",
}, },
maxDisplayWidth: { maxDisplayWidth: {
mobile: "1024px", // max-w-screen-lg mobile: "1024px", // max-w-screen-lg
desktop: "1536px", // max-w-screen-2xl desktop: "1536px", // max-w-screen-2xl
}, },
component: { component: {
anchor: { anchor: {
color: { color: {
default: "#66AAFF", default: "#66AAFF",
hover: "#4592F7", hover: "#4592F7",
active: "#4592F7", active: "#4592F7",
header: "#778899", header: "#778899",
}, },
}, },
blockQuote: { blockQuote: {
color: { color: {
background: "#FFFFFF12", background: "#FFFFFF12",
borderLeft: "#FFFFFF4D", borderLeft: "#FFFFFF4D",
}, },
}, },
card: { card: {
color: { color: {
background: "#2F3136", background: "#2F3136",
hoverGlow: "#FFFFFF33", hoverGlow: "#FFFFFF33",
}, },
}, },
code: { code: {
inline: { inline: {
color: { color: {
text: "#FFFFFF", text: "#FFFFFF",
background: "#444", background: "#444",
border: "#666", border: "#666",
}, },
}, },
block: { block: {
color: { color: {
border: "#555", border: "#555",
highlight: "#14161A", highlight: "#14161A",
}, },
style: readFileSync(__dirname + "/codeblock.css", "utf-8"), style: readFileSync(__dirname + "/codeblock.css", "utf-8"),
}, },
}, },
footer: { footer: {
color: { color: {
background: "#000000", background: "#000000",
text: "", text: "",
}, },
}, },
header: { header: {
color: { color: {
background: "#202225", // custom background: "#202225", // custom
hover: "#3F3F46", // zinc-700 hover: "#3F3F46", // zinc-700
text: "#D4D4D8", // zinc-300 text: "#D4D4D8", // zinc-300
}, },
height: "16px", // h-4 height: "16px", // h-4
}, },
input: { input: {
color: { color: {
background: { background: {
default: "#36393f", default: "#36393f",
itemHover: "#202225", itemHover: "#202225",
}, },
border: { border: {
default: "#555555", default: "#555555",
hover: "#808080", hover: "#808080",
focus: "#a3a3a3", // neutral-400 focus: "#a3a3a3", // neutral-400
}, },
placeHolder: "#A9A9A9", placeHolder: "#A9A9A9",
}, },
}, },
kbd: { kbd: {
color: { color: {
text: "#FFFFFF", text: "#FFFFFF",
border: "#555555", border: "#555555",
outerShadow: "#FFFFFF4D", outerShadow: "#FFFFFF4D",
innerShadow: "#000000", innerShadow: "#000000",
background: "#000000", background: "#000000",
}, },
}, },
mark: { mark: {
color: { color: {
text: "#FFFFFF", text: "#FFFFFF",
background: "#FFFF0080", background: "#FFFF0080",
}, },
}, },
scrollbar: { scrollbar: {
color: { color: {
track: "#18181B", track: "#18181B",
thumb: "#888888", thumb: "#888888",
}, },
width: "8px", // w-2 width: "8px", // w-2
borderRadius: "4px", // rounded borderRadius: "4px", // rounded
}, },
scrollProgressBar: { scrollProgressBar: {
color: { color: {
background: "#52525B", // zinc 600 background: "#52525B", // zinc 600
foreground: "#D4D4D8", // zinc-300 foreground: "#D4D4D8", // zinc-300
}, },
}, },
table: { table: {
color: { color: {
border: "#777777", border: "#777777",
even: "#21272E", even: "#21272E",
}, },
}, },
ui: { ui: {
color: { color: {
background: { background: {
default: "#202225", default: "#202225",
hover: "#3F3F46", // zinc-700 hover: "#3F3F46", // zinc-700
}, },
border: "#555", border: "#555",
}, },
}, },
}, },
} as Theme } as Theme
/*
dark: {
backgroundColor0: "#18181b",
backgroundColor1: "#36393F",
backgroundColor2: "#2F3136",
color0: "#FFFFFF",
color1: "#EEEEEE",
color2: "#CCC",
}
light: {
backgroundColor0: "#FFFFFF",
backgroundColor1: "#F7F7F7",
backgroundColor2: "#DDDDDD",
color0: "#000000",
color1: "#111111",
color2: "#555",
}
*/

View file

@ -1,27 +1,27 @@
/* from highlight.js/styles/default.css */ /* from highlight.js/styles/default.css */
pre code.hljs { pre code.hljs {
display: block; display: block;
overflow-x: auto; overflow-x: auto;
padding: 1em; padding: 1em;
} }
code.hljs { code.hljs {
padding: 3px 5px; padding: 3px 5px;
} }
.hljs { .hljs {
background: #f0f0f0; background: #f0f0f0;
color: #444; color: #444;
} }
.hljs-comment { .hljs-comment {
color: #888; color: #888;
} }
.hljs-punctuation, .hljs-punctuation,
.hljs-tag { .hljs-tag {
color: #444a; color: #444a;
} }
.hljs-tag .hljs-attr, .hljs-tag .hljs-attr,
.hljs-tag .hljs-name { .hljs-tag .hljs-name {
color: #444; color: #444;
} }
.hljs-attribute, .hljs-attribute,
.hljs-doctag, .hljs-doctag,
@ -29,7 +29,7 @@ code.hljs {
.hljs-meta .hljs-keyword, .hljs-meta .hljs-keyword,
.hljs-name, .hljs-name,
.hljs-selector-tag { .hljs-selector-tag {
font-weight: 700; font-weight: 700;
} }
.hljs-deletion, .hljs-deletion,
.hljs-number, .hljs-number,
@ -39,12 +39,12 @@ code.hljs {
.hljs-string, .hljs-string,
.hljs-template-tag, .hljs-template-tag,
.hljs-type { .hljs-type {
color: #800; color: #800;
} }
.hljs-section, .hljs-section,
.hljs-title { .hljs-title {
color: #800; color: #800;
font-weight: 700; font-weight: 700;
} }
.hljs-link, .hljs-link,
.hljs-operator, .hljs-operator,
@ -54,26 +54,26 @@ code.hljs {
.hljs-symbol, .hljs-symbol,
.hljs-template-variable, .hljs-template-variable,
.hljs-variable { .hljs-variable {
color: #bc6060; color: #bc6060;
} }
.hljs-literal { .hljs-literal {
color: #78a960; color: #78a960;
} }
.hljs-addition, .hljs-addition,
.hljs-built_in, .hljs-built_in,
.hljs-bullet, .hljs-bullet,
.hljs-code { .hljs-code {
color: #397300; color: #397300;
} }
.hljs-meta { .hljs-meta {
color: #1f7199; color: #1f7199;
} }
.hljs-meta .hljs-string { .hljs-meta .hljs-string {
color: #4d99bf; color: #4d99bf;
} }
.hljs-emphasis { .hljs-emphasis {
font-style: italic; font-style: italic;
} }
.hljs-strong { .hljs-strong {
font-weight: 700; font-weight: 700;
} }

View file

@ -7,120 +7,120 @@ import { DeepPartial } from "utility-types"
import BaseTheme from "../dark" import BaseTheme from "../dark"
export default merge<Theme, DeepPartial<Theme>>(BaseTheme, { export default merge<Theme, DeepPartial<Theme>>(BaseTheme, {
color: { color: {
text: { text: {
highContrast: "#000000", highContrast: "#000000",
default: "#111111", default: "#111111",
gray: "#555", gray: "#555",
}, },
background: "#F7F7F7", background: "#F7F7F7",
}, },
component: { component: {
anchor: { anchor: {
color: { color: {
header: "#D3D3D3", header: "#D3D3D3",
}, },
}, },
blockQuote: { blockQuote: {
color: { color: {
background: "#0000000D", background: "#0000000D",
borderLeft: "#0000001A", borderLeft: "#0000001A",
}, },
}, },
card: { card: {
color: { color: {
background: "#FFFFFF", background: "#FFFFFF",
hoverGlow: "#00000040", hoverGlow: "#00000040",
}, },
}, },
code: { code: {
inline: { inline: {
color: { color: {
text: "#000000", text: "#000000",
background: "#EEE", background: "#EEE",
border: "#BBB", border: "#BBB",
}, },
}, },
block: { block: {
color: { color: {
border: "#BBB", border: "#BBB",
highlight: "#DDDDDD", highlight: "#DDDDDD",
}, },
style: readFileSync(__dirname + "/codeblock.css", "utf-8"), style: readFileSync(__dirname + "/codeblock.css", "utf-8"),
}, },
}, },
footer: { footer: {
color: { color: {
background: "#FFFFFF", background: "#FFFFFF",
text: "", text: "",
}, },
}, },
input: { input: {
color: { color: {
background: { background: {
default: "#EEEEEE", default: "#EEEEEE",
itemHover: "#FFFFFF", itemHover: "#FFFFFF",
}, },
border: { border: {
default: "#CCCCCC", default: "#CCCCCC",
hover: "#808080", hover: "#808080",
focus: "#000000", focus: "#000000",
}, },
placeHolder: "#777777", placeHolder: "#777777",
}, },
}, },
kbd: { kbd: {
color: { color: {
text: "#333333", text: "#333333",
border: "#CCCCCC", border: "#CCCCCC",
outerShadow: "#00000033", outerShadow: "#00000033",
innerShadow: "#FFFFFF", innerShadow: "#FFFFFF",
background: "#F7F7F7", background: "#F7F7F7",
}, },
}, },
mark: { mark: {
color: { color: {
text: "#000000", text: "#000000",
background: "#FFFF00BF", background: "#FFFF00BF",
}, },
}, },
scrollbar: { scrollbar: {
color: { color: {
track: "#FFFFFF", track: "#FFFFFF",
thumb: "#DDDDDD", thumb: "#DDDDDD",
}, },
}, },
scrollProgressBar: { scrollProgressBar: {
color: { color: {
background: "#d4d4d8", // zinc-300 background: "#d4d4d8", // zinc-300
foreground: "#52525b", // zinc-600 foreground: "#52525b", // zinc-600
}, },
}, },
table: { table: {
color: { color: {
border: "#DDD", border: "#DDD",
even: "#F2F2F2", even: "#F2F2F2",
}, },
}, },
ui: { ui: {
color: { color: {
background: { background: {
default: "#FFFFFF", default: "#FFFFFF",
hover: "#EEEEEE", hover: "#EEEEEE",
}, },
border: "#CCC", border: "#CCC",
}, },
}, },
}, },
}) as Theme }) as Theme

View file

@ -1,9 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"types": ["node"], "types": ["node"],
"moduleResolution": "Node", "moduleResolution": "Node",
"esModuleInterop": true "esModuleInterop": true
}, },
"include": ["src"], "include": ["src"],
"exclude": ["dist", "node_modules"] "exclude": ["dist", "node_modules"]
} }

View file

@ -1,20 +1,20 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"display": "Default", "display": "Default",
"compilerOptions": { "compilerOptions": {
"composite": false, "composite": false,
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"esModuleInterop": true, "esModuleInterop": true,
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"inlineSources": false, "inlineSources": false,
"isolatedModules": true, "isolatedModules": true,
"moduleResolution": "node", "moduleResolution": "node",
"noUnusedLocals": false, "noUnusedLocals": false,
"noUnusedParameters": false, "noUnusedParameters": false,
"preserveWatchOutput": true, "preserveWatchOutput": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": true "strict": true
}, },
"exclude": ["node_modules"] "exclude": ["node_modules"]
} }

View file

@ -1,10 +1,10 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"display": "Node 16", "display": "Node 16",
"extends": "./base.json", "extends": "./base.json",
"compilerOptions": { "compilerOptions": {
"lib": ["ES2020"], "lib": ["ES2020"],
"module": "commonjs", "module": "commonjs",
"target": "ES2020" "target": "ES2020"
} }
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@developomp-site/tsconfig", "name": "@developomp-site/tsconfig",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"license": "MIT" "license": "MIT"
} }

View file

@ -1,11 +1,11 @@
{ {
"$schema": "https://json.schemastore.org/tsconfig", "$schema": "https://json.schemastore.org/tsconfig",
"display": "React Library", "display": "React Library",
"extends": "./base.json", "extends": "./base.json",
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
"lib": ["dom", "ES2015"], "lib": ["dom", "ES2015"],
"module": "ESNext", "module": "ESNext",
"target": "es6" "target": "es6"
} }
} }

View file

@ -1,4 +1,4 @@
module.exports = { module.exports = {
root: true, root: true,
extends: ["developomp-site"], extends: ["developomp-site"],
}; }

View file

@ -1,28 +1,28 @@
{ {
"name": "@developomp-site/utils", "name": "@developomp-site/utils",
"version": "0.0.0", "version": "0.0.0",
"main": "./dist/index.js", "main": "./dist/index.js",
"module": "./dist/index.mjs", "module": "./dist/index.mjs",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"sideEffects": false, "sideEffects": false,
"license": "MIT", "license": "MIT",
"files": [ "files": [
"dist/**" "dist/**"
], ],
"scripts": { "scripts": {
"build": "tsup src/index.tsx --format esm,cjs --dts --external react", "build": "tsup src/index.tsx --format esm,cjs --dts --external react",
"dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react", "dev": "tsup src/index.tsx --format esm,cjs --watch --dts --external react",
"lint": "TIMING=1 eslint \"src/**/*.ts*\"", "lint": "TIMING=1 eslint \"src/**/*.ts*\"",
"clean": "rm -rf .turbo node_modules dist" "clean": "rm -rf .turbo node_modules dist"
}, },
"devDependencies": { "devDependencies": {
"@developomp-site/eslint-config": "workspace:*", "@developomp-site/eslint-config": "workspace:*",
"@developomp-site/tsconfig": "workspace:*", "@developomp-site/tsconfig": "workspace:*",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.9", "@types/react-dom": "^18.0.9",
"eslint": "^8.29.0", "eslint": "^8.29.0",
"react": "^18.2.0", "react": "^18.2.0",
"tsup": "^5.12.9", "tsup": "^5.12.9",
"typescript": "^4.9.4" "typescript": "^4.9.4"
} }
} }

View file

@ -1,5 +1,5 @@
{ {
"extends": "@developomp-site/tsconfig/react-library.json", "extends": "@developomp-site/tsconfig/react-library.json",
"include": ["."], "include": ["."],
"exclude": ["dist", "build", "node_modules"] "exclude": ["dist", "build", "node_modules"]
} }

Some files were not shown because too many files have changed in this diff Show more