chore: change eslint & prettier config
This commit is contained in:
parent
fc827d74fe
commit
b43871c516
103 changed files with 3581 additions and 3543 deletions
|
@ -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"]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
`
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
`
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />
|
||||
|
||||
{date || "Unknown date"}
|
||||
|
||||
<FontAwesomeIcon icon={faHourglass} />
|
||||
|
||||
{readTime ? readTime + " read" : "unknown read time"}
|
||||
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
|
||||
{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} />
|
||||
|
||||
{date || "Unknown date"}
|
||||
|
||||
<FontAwesomeIcon icon={faHourglass} />
|
||||
|
||||
{readTime ? readTime + " read" : "unknown read time"}
|
||||
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
|
||||
{typeof wordCount === "number"
|
||||
? wordCount + " words"
|
||||
: "unknown length"}
|
||||
</MetaContainer>
|
||||
</PostCardContainer>
|
||||
</PostCard>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} /> {props.text}
|
||||
</Tag>
|
||||
)
|
||||
return (
|
||||
<Tag onClick={props.onClick || undefined}>
|
||||
<FontAwesomeIcon icon={faHashtag} /> {props.text}
|
||||
</Tag>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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} />
|
||||
|
||||
{props.fetchedPage.length} post
|
||||
{props.fetchedPage.length > 1 && "s"}
|
||||
</>
|
||||
)}
|
||||
{/* date */}
|
||||
<FontAwesomeIcon icon={faCalendar} />
|
||||
|
||||
{props.fetchedPage.date || "Unknown date"}
|
||||
|
||||
{/* read time */}
|
||||
<FontAwesomeIcon icon={faHourglass} />
|
||||
|
||||
{props.fetchedPage.readTime
|
||||
? props.fetchedPage.readTime + " read"
|
||||
: "unknown length"}
|
||||
|
||||
{/* word count */}
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
|
||||
{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} />
|
||||
|
||||
{props.fetchedPage.length} post
|
||||
{props.fetchedPage.length > 1 && "s"}{" "}
|
||||
|
||||
</>
|
||||
)}
|
||||
{/* date */}
|
||||
<FontAwesomeIcon icon={faCalendar} />
|
||||
|
||||
{props.fetchedPage.date || "Unknown date"}
|
||||
|
||||
{/* read time */}
|
||||
<FontAwesomeIcon icon={faHourglass} />
|
||||
|
||||
{props.fetchedPage.readTime
|
||||
? props.fetchedPage.readTime + " read"
|
||||
: "unknown length"}
|
||||
|
||||
{/* word count */}
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
|
||||
{props.fetchedPage.wordCount
|
||||
? props.fetchedPage.wordCount +
|
||||
" word" +
|
||||
(props.fetchedPage.wordCount > 1 && "s")
|
||||
: "unknown words"}
|
||||
</StyledMetaContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default Meta
|
||||
|
|
|
@ -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],
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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;
|
||||
`
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
`
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/**/*"]
|
||||
}
|
||||
|
|
6
apps/blog/types/react-date-range.d.ts
vendored
6
apps/blog/types/react-date-range.d.ts
vendored
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
34
apps/blog/types/read-time-estimate.d.ts
vendored
34
apps/blog/types/read-time-estimate.d.ts
vendored
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue