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 = {
root: true,
extends: ["@developomp-site/eslint-config"],
settings: {
next: {
rootDir: ["apps/*/"],
},
},
root: true,
extends: ["@developomp-site/eslint-config"],
settings: {
next: {
rootDir: ["apps/*/"],
},
},
}

View file

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

View file

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

4
.gitignore vendored
View file

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

View file

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

View file

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

148
.vscode/settings.json vendored
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,26 +3,26 @@ import styled, { css } from "styled-components"
import Card from "./Card"
export const mainContentCSS = css`
margin-top: 1rem;
width: 50%;
margin-top: 1rem;
width: 50%;
img {
max-width: 100%;
}
img {
max-width: 100%;
}
table img {
max-width: fit-content;
}
table img {
max-width: fit-content;
}
@media screen and (max-width: ${({ theme }) =>
theme.theme.maxDisplayWidth.mobile}) {
width: auto;
margin: 1rem;
}
@media screen and (max-width: ${({ theme }) =>
theme.theme.maxDisplayWidth.mobile}) {
width: auto;
margin: 1rem;
}
`
const MainContent = styled(Card)`
${mainContentCSS}
${mainContentCSS}
`
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 {
faBook,
faCalendar,
faHourglass,
faBook,
faCalendar,
faHourglass,
} from "@fortawesome/free-solid-svg-icons"
import Tag from "./Tag"
@ -15,85 +15,85 @@ import TagList from "./TagList"
import MainContent from "./MainContent"
const PostCard = styled(MainContent)`
box-shadow: 0 4px 10px rgb(0 0 0 / 10%);
text-align: left;
margin-bottom: 2rem;
box-shadow: 0 4px 10px rgb(0 0 0 / 10%);
text-align: left;
margin-bottom: 2rem;
:hover {
cursor: pointer;
box-shadow: 0 4px 10px
${({ theme }) => theme.theme.component.card.color.hoverGlow};
}
:hover {
cursor: pointer;
box-shadow: 0 4px 10px
${({ theme }) => theme.theme.component.card.color.hoverGlow};
}
`
const PostCardContainer = styled(Link)`
display: block;
padding: 2rem;
text-decoration: none;
padding: 0;
display: block;
padding: 2rem;
text-decoration: none;
padding: 0;
/* override link color */
color: ${({ theme }) => theme.theme.color.text.gray};
&:hover {
color: ${({ theme }) => theme.theme.color.text.gray};
}
/* override link color */
color: ${({ theme }) => theme.theme.color.text.gray};
&:hover {
color: ${({ theme }) => theme.theme.color.text.gray};
}
`
const Title = styled.h1`
font-size: 2rem;
font-style: bold;
margin: 0;
margin-bottom: 1rem;
font-size: 2rem;
font-style: bold;
margin: 0;
margin-bottom: 1rem;
`
const MetaContainer = styled.small``
interface PostCardData extends PostData {
content_id: string
content_id: string
}
interface Props {
postData: PostCardData
postData: PostCardData
}
export default (props: Props) => {
const { postData } = props
const { content_id, wordCount, date, readTime, title, tags } = postData
const { postData } = props
const { content_id, wordCount, date, readTime, title, tags } = postData
return (
<PostCard>
<PostCardContainer to={content_id}>
<Title>
{title || "No title"}
{/* show "(series)" for urls that matches regex "/series/<series-title>" */}
{/\/series\/[^/]*$/.test(content_id) && " (series)"}
</Title>
return (
<PostCard>
<PostCardContainer to={content_id}>
<Title>
{title || "No title"}
{/* show "(series)" for urls that matches regex "/series/<series-title>" */}
{/\/series\/[^/]*$/.test(content_id) && " (series)"}
</Title>
<br />
<br />
<MetaContainer>
<TagList direction="left">
{tags &&
tags.map((tag) => {
return <Tag key={title + tag} text={tag} />
})}
</TagList>
<hr />
<FontAwesomeIcon icon={faCalendar} />
&nbsp;&nbsp;&nbsp;
{date || "Unknown date"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faHourglass} />
&nbsp;&nbsp;&nbsp;
{readTime ? readTime + " read" : "unknown read time"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faBook} />
&nbsp;&nbsp;&nbsp;
{typeof wordCount === "number"
? wordCount + " words"
: "unknown length"}
</MetaContainer>
</PostCardContainer>
</PostCard>
)
<MetaContainer>
<TagList direction="left">
{tags &&
tags.map((tag) => {
return <Tag key={title + tag} text={tag} />
})}
</TagList>
<hr />
<FontAwesomeIcon icon={faCalendar} />
&nbsp;&nbsp;&nbsp;
{date || "Unknown date"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faHourglass} />
&nbsp;&nbsp;&nbsp;
{readTime ? readTime + " read" : "unknown read time"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faBook} />
&nbsp;&nbsp;&nbsp;
{typeof wordCount === "number"
? wordCount + " words"
: "unknown length"}
</MetaContainer>
</PostCardContainer>
</PostCard>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,16 +3,16 @@ import styled from "styled-components"
import buttonStyle from "../../styles/button"
const Button = styled.button`
${buttonStyle}
${buttonStyle}
/* center div */
/* center div */
margin: 0 auto;
`
interface Props {
action(): void
action(): void
}
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"
const StyledNotFound = styled(MainContent)`
text-align: center;
text-align: center;
`
const Styled404 = styled.h1`
font-size: 5rem;
font-size: 5rem;
`
const NotFound = () => {
return (
<>
<Helmet>
<title>pomp | 404</title>
return (
<>
<Helmet>
<title>pomp | 404</title>
<meta property="og:title" content="Page Not Found" />
<meta property="og:type" content="website" />
<meta property="og:url" content="http://blog.developomp.com" />
<meta
property="og:image"
content="http://blog.developomp.com/icon/icon.svg"
/>
<meta property="og:description" content="Page does not exist" />
</Helmet>
<meta property="og:title" content="Page Not Found" />
<meta property="og:type" content="website" />
<meta property="og:url" content="http://blog.developomp.com" />
<meta
property="og:image"
content="http://blog.developomp.com/icon/icon.svg"
/>
<meta property="og:description" content="Page does not exist" />
</Helmet>
<StyledNotFound>
<Styled404>404</Styled404>
<br />
Page was not found :(
</StyledNotFound>
</>
)
<StyledNotFound>
<Styled404>404</Styled404>
<br />
Page was not found :(
</StyledNotFound>
</>
)
}
export default NotFound

View file

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

View file

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

View file

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

View file

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

View file

@ -5,163 +5,165 @@ import type { PageData } from "@developomp-site/blog-content/src/types/types"
import contentMap from "../../contentMap"
export enum PageType {
POST,
SERIES,
SERIES_HOME,
PORTFOLIO_PROJECT,
UNSEARCHABLE,
POST,
SERIES,
SERIES_HOME,
PORTFOLIO_PROJECT,
UNSEARCHABLE,
}
export async function fetchContent(pageType: PageType, url: string) {
try {
if (pageType == PageType.UNSEARCHABLE) {
return await import(
`@developomp-site/blog-content/dist/content/unsearchable${url}.json`
)
} else {
return await import(
`@developomp-site/blog-content/dist/content${url}.json`
)
}
} catch (err) {
return
}
try {
if (pageType == PageType.UNSEARCHABLE) {
return await import(
`@developomp-site/blog-content/dist/content/unsearchable${url}.json`
)
} else {
return await import(
`@developomp-site/blog-content/dist/content${url}.json`
)
}
} catch (err) {
return
}
}
export function categorizePageType(content_id: string): PageType {
if (content_id.startsWith("/post")) return PageType.POST
if (content_id.startsWith("/portfolio")) return PageType.PORTFOLIO_PROJECT
if (content_id.startsWith("/series")) {
// if the URL looks like /series/series-title (if the url has two slashes)
if ([...(content_id.match(/\//g) || [])].length == 2)
return PageType.SERIES_HOME
if (content_id.startsWith("/post")) return PageType.POST
if (content_id.startsWith("/portfolio")) return PageType.PORTFOLIO_PROJECT
if (content_id.startsWith("/series")) {
// if the URL looks like /series/series-title (if the url has two slashes)
if ([...(content_id.match(/\//g) || [])].length == 2)
return PageType.SERIES_HOME
// if the URL looks like /series/series-title/post-title (if the url does not have 2 slashes)
return PageType.SERIES
}
// if the URL looks like /series/series-title/post-title (if the url does not have 2 slashes)
return PageType.SERIES
}
return PageType.UNSEARCHABLE
return PageType.UNSEARCHABLE
}
export function parsePageData(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetched_content: any,
pageType: PageType,
content_id: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetched_content: any,
pageType: PageType,
content_id: string
): PageData {
// page date to be saved as a react state
const pageData: PageData = {
title: "No title",
date: "Unknown date",
readTime: "Unknown read time",
wordCount: 0,
tags: [],
toc: undefined,
content: "No content",
// page date to be saved as a react state
const pageData: PageData = {
title: "No title",
date: "Unknown date",
readTime: "Unknown read time",
wordCount: 0,
tags: [],
toc: undefined,
content: "No content",
// series
// series
seriesHome: "",
prev: "",
next: "",
seriesHome: "",
prev: "",
next: "",
// series home
// series home
order: [],
length: 0,
order: [],
length: 0,
// portfolio
// portfolio
image: "",
overview: "",
badges: [],
repo: "",
}
image: "",
overview: "",
badges: [],
repo: "",
}
// load and parse content differently depending on the content type
switch (pageType) {
case PageType.POST: {
const post = contentMap.posts[content_id]
// load and parse content differently depending on the content type
switch (pageType) {
case PageType.POST: {
const post = contentMap.posts[content_id]
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
pageData.title = post.title
pageData.date = post.date
pageData.readTime = post.readTime
pageData.wordCount = post.wordCount
pageData.tags = post.tags || []
pageData.title = post.title
pageData.date = post.date
pageData.readTime = post.readTime
pageData.wordCount = post.wordCount
pageData.tags = post.tags || []
break
}
break
}
case PageType.SERIES: {
const seriesURL = content_id.slice(0, content_id.lastIndexOf("/"))
case PageType.SERIES: {
const seriesURL = content_id.slice(0, content_id.lastIndexOf("/"))
const curr = contentMap.series[seriesURL].order.indexOf(content_id)
const prev = curr - 1
const next = curr + 1
const curr = contentMap.series[seriesURL].order.indexOf(content_id)
const prev = curr - 1
const next = curr + 1
const post = contentMap.posts[content_id]
const post = contentMap.posts[content_id]
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
pageData.title = post.title
pageData.date = post.date
pageData.readTime = post.readTime
pageData.wordCount = post.wordCount
pageData.tags = post.tags || []
pageData.title = post.title
pageData.date = post.date
pageData.readTime = post.readTime
pageData.wordCount = post.wordCount
pageData.tags = post.tags || []
pageData.seriesHome = seriesURL
pageData.prev =
prev >= 0 ? contentMap.series[seriesURL].order[prev] : undefined
pageData.next =
next < contentMap.series[seriesURL].order.length
? contentMap.series[seriesURL].order[next]
: undefined
pageData.seriesHome = seriesURL
pageData.prev =
prev >= 0 ? contentMap.series[seriesURL].order[prev] : undefined
pageData.next =
next < contentMap.series[seriesURL].order.length
? contentMap.series[seriesURL].order[next]
: undefined
break
}
break
}
case PageType.SERIES_HOME: {
const seriesData = contentMap.series[content_id]
case PageType.SERIES_HOME: {
const seriesData = contentMap.series[content_id]
pageData.title = seriesData.title
pageData.content = fetched_content.content
pageData.title = seriesData.title
pageData.content = fetched_content.content
pageData.date = seriesData.date
pageData.readTime = seriesData.readTime
pageData.wordCount = seriesData.wordCount
pageData.order = seriesData.order
pageData.length = seriesData.length
pageData.date = seriesData.date
pageData.readTime = seriesData.readTime
pageData.wordCount = seriesData.wordCount
pageData.order = seriesData.order
pageData.length = seriesData.length
break
}
break
}
case PageType.PORTFOLIO_PROJECT: {
const data =
portfolio.projects[content_id as keyof typeof portfolio.projects]
case PageType.PORTFOLIO_PROJECT: {
const data =
portfolio.projects[
content_id as keyof typeof portfolio.projects
]
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
pageData.content = fetched_content.content
pageData.toc = fetched_content.toc
pageData.title = data.name
pageData.image = data.image
pageData.overview = data.overview
pageData.badges = data.badges
pageData.repo = data.repo
pageData.title = data.name
pageData.image = data.image
pageData.overview = data.overview
pageData.badges = data.badges
pageData.repo = data.repo
break
}
break
}
case PageType.UNSEARCHABLE: {
pageData.title = contentMap.unsearchable[content_id].title
pageData.content = fetched_content.content
case PageType.UNSEARCHABLE: {
pageData.title = contentMap.unsearchable[content_id].title
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"
export const DateRangeControl = styled.div`
width: 350px;
width: 350px;
@media screen and (max-width: ${(props) =>
props.theme.theme.maxDisplayWidth.mobile}) {
margin-top: 2rem;
}
@media screen and (max-width: ${(props) =>
props.theme.theme.maxDisplayWidth.mobile}) {
margin-top: 2rem;
}
`
export const ClearDateButton = styled.button`
width: 100%;
height: 2.5rem;
width: 100%;
height: 2.5rem;
border: none;
cursor: pointer;
border: none;
cursor: pointer;
background-color: tomato; /* 🍅 mmm tomato 🍅 */
color: white;
font-weight: bold;
background-color: tomato; /* 🍅 mmm tomato 🍅 */
color: white;
font-weight: bold;
`
export const StyledDateRange = styled(DateRange)`
width: 100%;
height: 350px;
width: 100%;
height: 350px;
`

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,127 +12,127 @@ import skills from "./portfolio/skills.json"
import { writeToFile } from "./util"
export default function postProcess() {
sortDates()
fillTags()
parseSeries()
generatePortfolioSVGs()
sortDates()
fillTags()
parseSeries()
generatePortfolioSVGs()
}
function sortDates() {
const TmpDate = contentMap.date
contentMap.date = {}
Object.keys(TmpDate)
.sort()
.forEach((sortedDateKey) => {
contentMap.date[sortedDateKey] = TmpDate[sortedDateKey]
})
const TmpDate = contentMap.date
contentMap.date = {}
Object.keys(TmpDate)
.sort()
.forEach((sortedDateKey) => {
contentMap.date[sortedDateKey] = TmpDate[sortedDateKey]
})
}
function fillTags() {
contentMap.meta.tags = Object.keys(contentMap.tags)
contentMap.meta.tags = Object.keys(contentMap.tags)
}
function parseSeries() {
// sort series map
for (const seriesURL in seriesMap) {
seriesMap[seriesURL].sort((a, b) => {
if (a.index < b.index) return -1
if (a.index > b.index) return 1
// sort series map
for (const seriesURL in seriesMap) {
seriesMap[seriesURL].sort((a, b) => {
if (a.index < b.index) return -1
if (a.index > b.index) return 1
return 0
})
}
return 0
})
}
// series length and order
for (const seriesURL in seriesMap) {
contentMap.series[seriesURL].length = seriesMap[seriesURL].length
contentMap.series[seriesURL].order = seriesMap[seriesURL].map(
(item) => item.url
)
}
// series length and order
for (const seriesURL in seriesMap) {
contentMap.series[seriesURL].length = seriesMap[seriesURL].length
contentMap.series[seriesURL].order = seriesMap[seriesURL].map(
(item) => item.url
)
}
}
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: {
[key: string]: Badge[] | { [key: string]: Badge[] }
} = {}
const data: {
[key: string]: Badge[] | { [key: string]: Badge[] }
} = {}
// C O G N I T O - H A Z A R D
// 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
// THIS PART OF THE CODE WAS WRITTEN IN 3 AM
// C O G N I T O - H A Z A R D
for (const key in skills) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (skills[key] instanceof Array) {
if (!data[key]) {
data[key] = []
}
for (const key in skills) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (skills[key] instanceof Array) {
if (!data[key]) {
data[key] = []
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
;(skills[key] as string[]).forEach((badge) =>
(data[key] as Badge[]).push(parseBadge(badge))
)
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for (const subKey in skills[key]) {
if (!data[key]) data[key] = {}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
;(skills[key] as string[]).forEach((badge) =>
(data[key] as Badge[]).push(parseBadge(badge))
)
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
for (const subKey in skills[key]) {
if (!data[key]) data[key] = {}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!data[key][subKey]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
data[key][subKey] = []
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (!data[key][subKey]) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
data[key][subKey] = []
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
skills[key][subKey].forEach((badge: string) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(data[key][subKey] as Badge[]).push(parseBadge(badge))
)
}
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
skills[key][subKey].forEach((badge: string) =>
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
(data[key][subKey] as Badge[]).push(parseBadge(badge))
)
}
}
}
const renderedSVG = ejs.render(
readFileSync("./src/portfolio/skills.ejs", "utf-8"),
{ style, data },
{ views: ["./src/portfolio"] }
)
const renderedSVG = ejs.render(
readFileSync("./src/portfolio/skills.ejs", "utf-8"),
{ style, data },
{ views: ["./src/portfolio"] }
)
writeToFile(
"./dist/public/img/skills.svg",
optimize(renderedSVG, { multipass: true }).data
)
writeToFile(
"./dist/public/img/skills.svg",
optimize(renderedSVG, { multipass: true }).data
)
}
function parseBadge(badgeRaw: string): Badge {
const isMultiWord = badgeRaw.includes(" ")
const words = badgeRaw.split(" ")
const slug = words[0]
const isMultiWord = badgeRaw.includes(" ")
const words = badgeRaw.split(" ")
const slug = words[0]
// @ts-ignore
const icon = icons["si" + slug[0].toUpperCase() + slug.slice(1)]
// @ts-ignore
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 {
svg: icon.svg,
hex: color.toHexString(),
isDark: color.isDark(),
title: isMultiWord ? words.slice(1).join(" ") : icon.title,
}
return {
svg: icon.svg,
hex: color.toHexString(),
isDark: color.isDark(),
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
*/
export interface DataToPass {
path: string
urlPath: string
markdownRaw: string
markdownData: {
content: string
[key: string]: unknown
}
humanizedDuration: string
totalWords: number
path: string
urlPath: string
markdownRaw: string
markdownData: {
content: string
[key: string]: unknown
}
humanizedDuration: string
totalWords: number
}
/**
@ -33,23 +33,23 @@ export interface DataToPass {
* @param {string} path - path of file or folder
*/
export function recursiveParse(mode: ParseMode, path: string): void {
// get name of the file or folder that's currently being parsed
const fileOrFolderName = path2FileOrFolderName(path)
// get name of the file or folder that's currently being parsed
const fileOrFolderName = path2FileOrFolderName(path)
// stop if the file or folder starts with a underscore
if (fileOrFolderName.startsWith("_")) return
// stop if the file or folder starts with a underscore
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 file, parse it and then save it to file
if (stats.isDirectory()) {
fs.readdirSync(path).map((childPath) => {
recursiveParse(mode, `${path}/${childPath}`)
})
} else if (stats.isFile()) {
parseFile(mode, path)
}
// 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 (stats.isDirectory()) {
fs.readdirSync(path).map((childPath) => {
recursiveParse(mode, `${path}/${childPath}`)
})
} else if (stats.isFile()) {
parseFile(mode, path)
}
}
/**
@ -59,50 +59,50 @@ export function recursiveParse(mode: ParseMode, path: string): void {
* @param {string} path - path of the markdown file
*/
function parseFile(mode: ParseMode, path: string): void {
// stop if it is not a markdown file
if (!path.endsWith(".md")) {
console.log(`Ignoring non markdown file at: ${path}`)
return
}
// stop if it is not a markdown file
if (!path.endsWith(".md")) {
console.log(`Ignoring non markdown file at: ${path}`)
return
}
/**
* Parse markdown
*/
/**
* Parse markdown
*/
const markdownRaw = fs.readFileSync(path, "utf8")
const markdownData = parseMarkdown(markdownRaw, path, mode)
const { humanizedDuration, totalWords } = readTimeEstimate(
markdownData.content,
275,
12,
500,
["img", "Image"]
)
const markdownRaw = fs.readFileSync(path, "utf8")
const markdownData = parseMarkdown(markdownRaw, path, mode)
const { humanizedDuration, totalWords } = readTimeEstimate(
markdownData.content,
275,
12,
500,
["img", "Image"]
)
const dataToPass: DataToPass = {
path,
urlPath: path2URL(path),
markdownRaw,
markdownData,
humanizedDuration,
totalWords,
}
const dataToPass: DataToPass = {
path,
urlPath: path2URL(path),
markdownRaw,
markdownData,
humanizedDuration,
totalWords,
}
switch (mode) {
case ParseMode.POSTS:
parsePost(dataToPass)
break
switch (mode) {
case ParseMode.POSTS:
parsePost(dataToPass)
break
case ParseMode.SERIES:
parseSeries(dataToPass)
break
case ParseMode.SERIES:
parseSeries(dataToPass)
break
case ParseMode.UNSEARCHABLE:
parseUnsearchable(dataToPass)
break
case ParseMode.UNSEARCHABLE:
parseUnsearchable(dataToPass)
break
case ParseMode.PORTFOLIO:
parseProjects(dataToPass)
break
}
case ParseMode.PORTFOLIO:
parseProjects(dataToPass)
break
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -8,19 +8,19 @@ import elasticlunr from "elasticlunr"
import { searchIndexFilePath } from "./config"
const elasticlunrIndex = elasticlunr(function () {
this.addField("title" as never)
this.addField("body" as never)
this.setRef("url" as never)
this.addField("title" as never)
this.addField("body" as never)
this.setRef("url" as never)
})
export function addDocument(doc: {
title?: unknown
body?: string
url?: string
title?: unknown
body?: string
url?: string
}) {
elasticlunrIndex.addDoc(doc)
elasticlunrIndex.addDoc(doc)
}
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" {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function texmath(md: MarkdownIt, ...params: any[]): void
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function texmath(md: MarkdownIt, ...params: any[]): void
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,16 +1,18 @@
{
"name": "@developomp-site/eslint-config",
"version": "0.0.0",
"main": "index.js",
"scripts": {
"clean": "rm -rf node_modules"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.46.0",
"@typescript-eslint/parser": "^5.46.0",
"eslint-config-next": "^12.3.4",
"eslint-config-prettier": "^8.5.0",
"eslint-config-turbo": "latest",
"eslint-plugin-json": "^3.1.0"
}
"name": "@developomp-site/eslint-config",
"version": "0.0.0",
"main": "index.js",
"scripts": {
"clean": "rm -rf node_modules"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-turbo": "latest",
"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",
"version": "0.0.0",
"private": true,
"main": "index.js",
"scripts": {
"clean": "rm -rf node_modules"
},
"devDependencies": {
"tailwindcss": "^3.2.4"
}
"name": "@developomp-site/tailwind-config",
"version": "0.0.0",
"private": true,
"main": "index.js",
"scripts": {
"clean": "rm -rf node_modules"
},
"devDependencies": {
"tailwindcss": "^3.2.4"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -3,165 +3,145 @@ import type { Theme } from "../.."
import { readFileSync } from "fs"
export default {
font: {
sansSerif: "'Noto Sans KR', sans-serif", // https://fonts.google.com/noto/specimen/Noto+Sans+KR
monospace: "'Source Code Pro', monospace",
},
font: {
sansSerif: "'Noto Sans KR', sans-serif", // https://fonts.google.com/noto/specimen/Noto+Sans+KR
monospace: "'Source Code Pro', monospace",
},
color: {
text: {
highContrast: "#FFFFFF",
default: "#EEEEEE",
gray: "#CCC",
},
background: "#36393F",
},
color: {
text: {
highContrast: "#FFFFFF",
default: "#EEEEEE",
gray: "#CCC",
},
background: "#36393F",
},
maxDisplayWidth: {
mobile: "1024px", // max-w-screen-lg
desktop: "1536px", // max-w-screen-2xl
},
maxDisplayWidth: {
mobile: "1024px", // max-w-screen-lg
desktop: "1536px", // max-w-screen-2xl
},
component: {
anchor: {
color: {
default: "#66AAFF",
hover: "#4592F7",
active: "#4592F7",
header: "#778899",
},
},
component: {
anchor: {
color: {
default: "#66AAFF",
hover: "#4592F7",
active: "#4592F7",
header: "#778899",
},
},
blockQuote: {
color: {
background: "#FFFFFF12",
borderLeft: "#FFFFFF4D",
},
},
blockQuote: {
color: {
background: "#FFFFFF12",
borderLeft: "#FFFFFF4D",
},
},
card: {
color: {
background: "#2F3136",
hoverGlow: "#FFFFFF33",
},
},
card: {
color: {
background: "#2F3136",
hoverGlow: "#FFFFFF33",
},
},
code: {
inline: {
color: {
text: "#FFFFFF",
background: "#444",
border: "#666",
},
},
block: {
color: {
border: "#555",
highlight: "#14161A",
},
style: readFileSync(__dirname + "/codeblock.css", "utf-8"),
},
},
code: {
inline: {
color: {
text: "#FFFFFF",
background: "#444",
border: "#666",
},
},
block: {
color: {
border: "#555",
highlight: "#14161A",
},
style: readFileSync(__dirname + "/codeblock.css", "utf-8"),
},
},
footer: {
color: {
background: "#000000",
text: "",
},
},
footer: {
color: {
background: "#000000",
text: "",
},
},
header: {
color: {
background: "#202225", // custom
hover: "#3F3F46", // zinc-700
text: "#D4D4D8", // zinc-300
},
height: "16px", // h-4
},
header: {
color: {
background: "#202225", // custom
hover: "#3F3F46", // zinc-700
text: "#D4D4D8", // zinc-300
},
height: "16px", // h-4
},
input: {
color: {
background: {
default: "#36393f",
itemHover: "#202225",
},
border: {
default: "#555555",
hover: "#808080",
focus: "#a3a3a3", // neutral-400
},
placeHolder: "#A9A9A9",
},
},
input: {
color: {
background: {
default: "#36393f",
itemHover: "#202225",
},
border: {
default: "#555555",
hover: "#808080",
focus: "#a3a3a3", // neutral-400
},
placeHolder: "#A9A9A9",
},
},
kbd: {
color: {
text: "#FFFFFF",
border: "#555555",
outerShadow: "#FFFFFF4D",
innerShadow: "#000000",
background: "#000000",
},
},
kbd: {
color: {
text: "#FFFFFF",
border: "#555555",
outerShadow: "#FFFFFF4D",
innerShadow: "#000000",
background: "#000000",
},
},
mark: {
color: {
text: "#FFFFFF",
background: "#FFFF0080",
},
},
mark: {
color: {
text: "#FFFFFF",
background: "#FFFF0080",
},
},
scrollbar: {
color: {
track: "#18181B",
thumb: "#888888",
},
width: "8px", // w-2
borderRadius: "4px", // rounded
},
scrollbar: {
color: {
track: "#18181B",
thumb: "#888888",
},
width: "8px", // w-2
borderRadius: "4px", // rounded
},
scrollProgressBar: {
color: {
background: "#52525B", // zinc 600
foreground: "#D4D4D8", // zinc-300
},
},
scrollProgressBar: {
color: {
background: "#52525B", // zinc 600
foreground: "#D4D4D8", // zinc-300
},
},
table: {
color: {
border: "#777777",
even: "#21272E",
},
},
table: {
color: {
border: "#777777",
even: "#21272E",
},
},
ui: {
color: {
background: {
default: "#202225",
hover: "#3F3F46", // zinc-700
},
border: "#555",
},
},
},
ui: {
color: {
background: {
default: "#202225",
hover: "#3F3F46", // zinc-700
},
border: "#555",
},
},
},
} 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 */
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
padding: 3px 5px;
}
.hljs {
background: #f0f0f0;
color: #444;
background: #f0f0f0;
color: #444;
}
.hljs-comment {
color: #888;
color: #888;
}
.hljs-punctuation,
.hljs-tag {
color: #444a;
color: #444a;
}
.hljs-tag .hljs-attr,
.hljs-tag .hljs-name {
color: #444;
color: #444;
}
.hljs-attribute,
.hljs-doctag,
@ -29,7 +29,7 @@ code.hljs {
.hljs-meta .hljs-keyword,
.hljs-name,
.hljs-selector-tag {
font-weight: 700;
font-weight: 700;
}
.hljs-deletion,
.hljs-number,
@ -39,12 +39,12 @@ code.hljs {
.hljs-string,
.hljs-template-tag,
.hljs-type {
color: #800;
color: #800;
}
.hljs-section,
.hljs-title {
color: #800;
font-weight: 700;
color: #800;
font-weight: 700;
}
.hljs-link,
.hljs-operator,
@ -54,26 +54,26 @@ code.hljs {
.hljs-symbol,
.hljs-template-variable,
.hljs-variable {
color: #bc6060;
color: #bc6060;
}
.hljs-literal {
color: #78a960;
color: #78a960;
}
.hljs-addition,
.hljs-built_in,
.hljs-bullet,
.hljs-code {
color: #397300;
color: #397300;
}
.hljs-meta {
color: #1f7199;
color: #1f7199;
}
.hljs-meta .hljs-string {
color: #4d99bf;
color: #4d99bf;
}
.hljs-emphasis {
font-style: italic;
font-style: italic;
}
.hljs-strong {
font-weight: 700;
font-weight: 700;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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