chore: change eslint & prettier config

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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