many many updates (check commit detail)
- updated dependencies - bumped react version 17 -> 18 - changed navbar button tag from `a` to `button` - added locale info to url (made sure same url = same content) - fixed 0 gettingconsidered as "unknown length" for word count in `PostCard` - moved functions from `Page.tsx` to `helper.ts` - added "translation not available" page
This commit is contained in:
parent
56bf555bd7
commit
d1a33ccf1e
17 changed files with 2569 additions and 2383 deletions
|
@ -11,7 +11,7 @@
|
||||||
"tryExtensions": [".js", ".jsx", ".json"]
|
"tryExtensions": [".js", ".jsx", ".json"]
|
||||||
},
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"version": "17.0"
|
"version": "18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
},
|
},
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": ["react", "@typescript-eslint"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"@typescript-eslint/no-empty-interface": "off",
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import ejs from "ejs"
|
import ejs from "ejs"
|
||||||
import { optimize } from "svgo"
|
import { optimize, OptimizedSvg } from "svgo"
|
||||||
import { readFileSync, writeFileSync } from "fs"
|
import { readFileSync, writeFileSync } from "fs"
|
||||||
import simpleIcon from "simple-icons"
|
import simpleIcon from "simple-icons"
|
||||||
import tinycolor from "tinycolor2"
|
import tinycolor from "tinycolor2"
|
||||||
|
@ -78,7 +78,12 @@ function generatePortfolioSVGs() {
|
||||||
|
|
||||||
const optimizedSVG = optimize(renderedSVG, { multipass: true })
|
const optimizedSVG = optimize(renderedSVG, { multipass: true })
|
||||||
|
|
||||||
writeFileSync("./public/img/skills.svg", optimizedSVG.data)
|
if (optimizedSVG.error) {
|
||||||
|
console.error("Failed to generate optimized skills.svg")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync("./public/img/skills.svg", (optimizedSVG as OptimizedSvg).data)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseBadge(badgeRaw: string): Badge {
|
function parseBadge(badgeRaw: string): Badge {
|
||||||
|
|
65
package.json
65
package.json
|
@ -6,6 +6,9 @@
|
||||||
"quick-start": "react-scripts start",
|
"quick-start": "react-scripts start",
|
||||||
"build": "yarn generate && react-scripts build"
|
"build": "yarn generate && react-scripts build"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@types/react": "18.0.5"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||||
"@fortawesome/free-brands-svg-icons": "^6.1.1",
|
"@fortawesome/free-brands-svg-icons": "^6.1.1",
|
||||||
|
@ -13,52 +16,50 @@
|
||||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||||
"elasticlunr": "^0.9.5",
|
"elasticlunr": "^0.9.5",
|
||||||
"highlight.js": "^11.3.1",
|
"highlight.js": "^11.5.1",
|
||||||
"katex": "^0.15.1",
|
"katex": "^0.15.3",
|
||||||
"local-storage-fallback": "^4.1.2",
|
"local-storage-fallback": "^4.1.2",
|
||||||
"react": "^17.0.2",
|
"react": "^18.0.0",
|
||||||
"react-collapse": "^5.1.1",
|
"react-collapse": "^5.1.1",
|
||||||
"react-date-range": "^1.4.0",
|
"react-date-range": "^1.4.0",
|
||||||
"react-device-detect": "^2.1.2",
|
"react-device-detect": "^2.2.2",
|
||||||
"react-dnd": "^14.0.5",
|
"react-dnd": "^16.0.0",
|
||||||
"react-dnd-html5-backend": "^14.1.0",
|
"react-dnd-html5-backend": "^16.0.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.0.0",
|
||||||
"react-helmet-async": "^1.2.2",
|
"react-helmet-async": "^1.3.0",
|
||||||
"react-router-dom": "^6.2.1",
|
"react-router-dom": "^6.3.0",
|
||||||
"react-router-hash-link": "^2.4.3",
|
"react-scripts": "^5.0.1",
|
||||||
"react-scripts": "^5.0.0",
|
"react-select": "^5.3.0",
|
||||||
"react-select": "^5.2.1",
|
|
||||||
"react-tooltip": "^4.2.21",
|
"react-tooltip": "^4.2.21",
|
||||||
"styled-components": "^5.3.3"
|
"styled-components": "^5.3.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ejs": "^3.1.0",
|
"@types/ejs": "^3.1.0",
|
||||||
"@types/elasticlunr": "^0.9.4",
|
"@types/elasticlunr": "^0.9.4",
|
||||||
"@types/highlight.js": "^10.1.0",
|
"@types/highlight.js": "^10.1.0",
|
||||||
"@types/jsdom": "^16.2.14",
|
"@types/jsdom": "^16.2.14",
|
||||||
"@types/katex": "^0.11.1",
|
"@types/katex": "^0.14.0",
|
||||||
"@types/markdown-it": "^12.2.3",
|
"@types/markdown-it": "^12.2.3",
|
||||||
"@types/node": "^17.0.8",
|
"@types/node": "^17.0.24",
|
||||||
"@types/react": "^17.0.38",
|
"@types/react": "^18.0.5",
|
||||||
"@types/react-collapse": "^5.0.1",
|
"@types/react-collapse": "^5.0.1",
|
||||||
"@types/react-date-range": "^1.4.2",
|
"@types/react-date-range": "^1.4.3",
|
||||||
"@types/react-dom": "^17.0.11",
|
"@types/react-dom": "^18.0.1",
|
||||||
"@types/react-router-hash-link": "^2.4.4",
|
|
||||||
"@types/react-select": "^5.0.1",
|
"@types/react-select": "^5.0.1",
|
||||||
"@types/styled-components": "^5.1.19",
|
"@types/styled-components": "^5.1.25",
|
||||||
"@types/svgo": "^2.6.0",
|
"@types/svgo": "^2.6.3",
|
||||||
"@types/tinycolor2": "^1.4.3",
|
"@types/tinycolor2": "^1.4.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.9.0",
|
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||||
"@typescript-eslint/parser": "^5.9.0",
|
"@typescript-eslint/parser": "^5.19.0",
|
||||||
"ejs": "^3.1.6",
|
"ejs": "^3.1.6",
|
||||||
"eslint-config-prettier": "^8.3.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-plugin-json": "^3.1.0",
|
"eslint-plugin-json": "^3.1.0",
|
||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.29.4",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"jsdom": "^19.0.0",
|
"jsdom": "^19.0.0",
|
||||||
"jspdf": "^2.5.0",
|
"jspdf": "^2.5.1",
|
||||||
"markdown-it": "^12.3.0",
|
"markdown-it": "^12.3.2",
|
||||||
"markdown-it-anchor": "^8.4.1",
|
"markdown-it-anchor": "^8.6.2",
|
||||||
"markdown-it-attrs": "^4.1.3",
|
"markdown-it-attrs": "^4.1.3",
|
||||||
"markdown-it-footnote": "^3.0.3",
|
"markdown-it-footnote": "^3.0.3",
|
||||||
"markdown-it-highlight-lines": "^1.0.2",
|
"markdown-it-highlight-lines": "^1.0.2",
|
||||||
|
@ -68,14 +69,14 @@
|
||||||
"markdown-it-task-checkbox": "^1.0.6",
|
"markdown-it-task-checkbox": "^1.0.6",
|
||||||
"markdown-it-texmath": "^0.9.7",
|
"markdown-it-texmath": "^0.9.7",
|
||||||
"markdown-toc": "^1.2.0",
|
"markdown-toc": "^1.2.0",
|
||||||
"prettier": "^2.5.1",
|
"prettier": "^2.6.2",
|
||||||
"read-time-estimate": "^0.0.3",
|
"read-time-estimate": "^0.0.3",
|
||||||
"simple-icons": "^6.9.0",
|
"simple-icons": "^6.19.0",
|
||||||
"svgo": "^2.8.0",
|
"svgo": "^2.8.0",
|
||||||
"tinycolor2": "^1.4.2",
|
"tinycolor2": "^1.4.2",
|
||||||
"ts-node": "^10.4.0",
|
"ts-node": "^10.7.0",
|
||||||
"tslint-config-prettier": "^1.18.0",
|
"tslint-config-prettier": "^1.18.0",
|
||||||
"typescript": "^4.5.4"
|
"typescript": "^4.6.3"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|
47
src/App.tsx
47
src/App.tsx
|
@ -1,5 +1,5 @@
|
||||||
import { useContext, useEffect, useState } from "react"
|
import { useContext, useEffect, useState } from "react"
|
||||||
import { Routes, Route } from "react-router-dom"
|
import { Routes, Route, useNavigate, useLocation } from "react-router-dom"
|
||||||
import styled, { ThemeProvider } from "styled-components"
|
import styled, { ThemeProvider } from "styled-components"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
import { isIE } from "react-device-detect"
|
import { isIE } from "react-device-detect"
|
||||||
|
@ -35,10 +35,20 @@ const StyledContentContainer = styled.div`
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const { globalState, dispatch } = useContext(globalContext)
|
const { globalState, dispatch } = useContext(globalContext)
|
||||||
|
const { locale } = globalState
|
||||||
|
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
|
||||||
// set loading to false if all fonts are loaded
|
// update url on locale change
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
navigate(locale + pathname.slice(3))
|
||||||
|
}, [locale])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// set loading to false if all fonts are loaded
|
||||||
// checks if document.fonts.onloadingdone is supported on the browser
|
// checks if document.fonts.onloadingdone is supported on the browser
|
||||||
if (typeof document.fonts.onloadingdone != undefined) {
|
if (typeof document.fonts.onloadingdone != undefined) {
|
||||||
document.fonts.onloadingdone = () => {
|
document.fonts.onloadingdone = () => {
|
||||||
|
@ -47,6 +57,10 @@ export default function App() {
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// automatically add locale prefix if it's not specified
|
||||||
|
if (!pathname.startsWith("/en") && !pathname.startsWith("/kr"))
|
||||||
|
navigate(`/${globalState.locale}${pathname}`)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (isIE)
|
if (isIE)
|
||||||
|
@ -80,12 +94,29 @@ export default function App() {
|
||||||
<Loading />
|
<Loading />
|
||||||
) : (
|
) : (
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
{/*
|
||||||
<Route path="/search" element={<Search />} />
|
Using this ugly code because the developers of react-router-dom decided that
|
||||||
<Route path="/portfolio" element={<Portfolio />} />
|
removing regex support was a good idea.
|
||||||
<Route path="/404" element={<NotFound />} />
|
https://github.com/remix-run/react-router/issues/7285
|
||||||
<Route path="/loading" element={<Loading />} />
|
*/}
|
||||||
<Route path="/*" element={<Page />} />
|
|
||||||
|
<Route path="en">
|
||||||
|
<Route index element={<Home />} />
|
||||||
|
<Route path="search" element={<Search />} />
|
||||||
|
<Route path="portfolio" element={<Portfolio />} />
|
||||||
|
<Route path="404" element={<NotFound />} />
|
||||||
|
<Route path="loading" element={<Loading />} />
|
||||||
|
<Route path="*" element={<Page />} />
|
||||||
|
</Route>
|
||||||
|
|
||||||
|
<Route path="kr">
|
||||||
|
<Route index element={<Home />} />
|
||||||
|
<Route path="search" element={<Search />} />
|
||||||
|
<Route path="portfolio" element={<Portfolio />} />
|
||||||
|
<Route path="404" element={<NotFound />} />
|
||||||
|
<Route path="loading" element={<Loading />} />
|
||||||
|
<Route path="*" element={<Page />} />
|
||||||
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
)}
|
)}
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useContext } from "react"
|
import { useContext } from "react"
|
||||||
import ReactTooltip from "react-tooltip"
|
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
|
import ReactTooltip from "react-tooltip"
|
||||||
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
import { faLanguage } from "@fortawesome/free-solid-svg-icons"
|
import { faLanguage } from "@fortawesome/free-solid-svg-icons"
|
||||||
|
@ -14,18 +14,20 @@ interface StyledLocaleToggleButtonProps {
|
||||||
locale: SiteLocale
|
locale: SiteLocale
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledLocaleToggleButton = styled.div<StyledLocaleToggleButtonProps>`
|
const StyledLocaleToggleButton = styled.button<StyledLocaleToggleButtonProps>`
|
||||||
${theming.styles.navbarButtonStyle}
|
${theming.styles.navbarButtonStyle}
|
||||||
|
border: none;
|
||||||
|
width: 72px;
|
||||||
|
|
||||||
${(props) => (props.locale == "en" ? "" : "transform: scaleX(-1);")};
|
${(props) => (props.locale == "en" ? "" : "transform: scaleX(-1);")};
|
||||||
`
|
`
|
||||||
|
|
||||||
const LocaleToggleButton = () => {
|
function LocaleToggleButton() {
|
||||||
const { globalState, dispatch } = useContext(globalContext)
|
const { globalState, dispatch } = useContext(globalContext)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledLocaleToggleButton
|
<StyledLocaleToggleButton
|
||||||
locale={globalState.locale}
|
|
||||||
data-tip
|
data-tip
|
||||||
data-for="locale"
|
data-for="locale"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -34,6 +36,7 @@ const LocaleToggleButton = () => {
|
||||||
payload: globalState.locale == "en" ? "kr" : "en",
|
payload: globalState.locale == "en" ? "kr" : "en",
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
locale={globalState.locale}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faLanguage} />
|
<FontAwesomeIcon icon={faLanguage} />
|
||||||
</StyledLocaleToggleButton>
|
</StyledLocaleToggleButton>
|
||||||
|
|
|
@ -23,7 +23,7 @@ const NavLinks = () => {
|
||||||
return (
|
return (
|
||||||
<StyledNavLinks>
|
<StyledNavLinks>
|
||||||
{NavbarData.map((item, index) => (
|
{NavbarData.map((item, index) => (
|
||||||
<Link key={index} to={item.path}>
|
<Link key={index} to={globalState.locale + item.path}>
|
||||||
<StyledLink>
|
<StyledLink>
|
||||||
{globalState.locale == "en" ? item.title_en : item.title_kr}
|
{globalState.locale == "en" ? item.title_en : item.title_kr}
|
||||||
</StyledLink>
|
</StyledLink>
|
||||||
|
|
|
@ -9,8 +9,11 @@ import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons"
|
||||||
import theming from "../../styles/theming"
|
import theming from "../../styles/theming"
|
||||||
import { ActionsEnum, globalContext } from "../../globalContext"
|
import { ActionsEnum, globalContext } from "../../globalContext"
|
||||||
|
|
||||||
const StyledThemeButton = styled.div`
|
const StyledThemeButton = styled.button`
|
||||||
${theming.styles.navbarButtonStyle}
|
${theming.styles.navbarButtonStyle}
|
||||||
|
border: none;
|
||||||
|
width: 72px;
|
||||||
|
|
||||||
${(props) =>
|
${(props) =>
|
||||||
theming.theme(props.theme.currentTheme, {
|
theming.theme(props.theme.currentTheme, {
|
||||||
light: "",
|
light: "",
|
||||||
|
|
|
@ -78,7 +78,7 @@ const PostCard = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledPostCard>
|
<StyledPostCard>
|
||||||
<PostCardContainer to={process.env.PUBLIC_URL + postData.url}>
|
<PostCardContainer to={postData.url}>
|
||||||
<StyledTitle>
|
<StyledTitle>
|
||||||
{postData.title || "No title"}
|
{postData.title || "No title"}
|
||||||
{/* show "(series)" for urls that matches regex "/series/<series-title>" */}
|
{/* show "(series)" for urls that matches regex "/series/<series-title>" */}
|
||||||
|
@ -107,7 +107,7 @@ const PostCard = (props: Props) => {
|
||||||
|
|
||||||
<FontAwesomeIcon icon={faBook} />
|
<FontAwesomeIcon icon={faBook} />
|
||||||
|
|
||||||
{postData.wordCount
|
{typeof postData.wordCount === "number"
|
||||||
? postData.wordCount + " words"
|
? postData.wordCount + " words"
|
||||||
: "unknown length"}
|
: "unknown length"}
|
||||||
</StyledMetaContainer>
|
</StyledMetaContainer>
|
||||||
|
|
|
@ -49,7 +49,10 @@ const SubMenu = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SidebarLink to={props.item.path} onClick={handleSidebarLinkClick}>
|
<SidebarLink
|
||||||
|
to={globalState.locale + props.item.path}
|
||||||
|
onClick={handleSidebarLinkClick}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{props.item.icon}
|
{props.item.icon}
|
||||||
<SidebarLabel>
|
<SidebarLabel>
|
||||||
|
|
|
@ -31,8 +31,15 @@ export interface IGlobalContext {
|
||||||
dispatch: Dispatch<GlobalAction>
|
dispatch: Dispatch<GlobalAction>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDefaultLocale(): SiteLocale {
|
||||||
|
if (window.location.pathname.startsWith("/en")) return "en"
|
||||||
|
if (window.location.pathname.startsWith("/kr")) return "kr"
|
||||||
|
|
||||||
|
return (storage.getItem("locale") as SiteLocale) || "en"
|
||||||
|
}
|
||||||
|
|
||||||
const defaultState: IGlobalState = {
|
const defaultState: IGlobalState = {
|
||||||
locale: (storage.getItem("locale") || "en") as SiteLocale,
|
locale: getDefaultLocale(),
|
||||||
theme: (storage.getItem("theme") || "dark") as SiteTheme,
|
theme: (storage.getItem("theme") || "dark") as SiteTheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,20 +1,18 @@
|
||||||
import React from "react"
|
import { createRoot } from "react-dom/client"
|
||||||
import ReactDOM from "react-dom"
|
|
||||||
import { HelmetProvider } from "react-helmet-async"
|
import { HelmetProvider } from "react-helmet-async"
|
||||||
import { BrowserRouter } from "react-router-dom"
|
import { BrowserRouter } from "react-router-dom"
|
||||||
import { GlobalStore } from "./globalContext"
|
import { GlobalStore } from "./globalContext"
|
||||||
|
|
||||||
import App from "./App"
|
import App from "./App"
|
||||||
|
|
||||||
ReactDOM.render(
|
const container = document.getElementById("root") as HTMLElement
|
||||||
<React.StrictMode>
|
const root = createRoot(container)
|
||||||
<GlobalStore>
|
root.render(
|
||||||
<BrowserRouter>
|
<GlobalStore>
|
||||||
<HelmetProvider>
|
<BrowserRouter>
|
||||||
<App />
|
<HelmetProvider>
|
||||||
</HelmetProvider>
|
<App />
|
||||||
</BrowserRouter>
|
</HelmetProvider>
|
||||||
</GlobalStore>
|
</BrowserRouter>
|
||||||
</React.StrictMode>,
|
</GlobalStore>
|
||||||
document.getElementById("root")
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -31,6 +31,8 @@ const StyledPostList = styled.div`
|
||||||
|
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
const { globalState } = useContext(globalContext)
|
const { globalState } = useContext(globalContext)
|
||||||
|
const { locale } = globalState
|
||||||
|
|
||||||
const [howMany, setHowMany] = useState(5)
|
const [howMany, setHowMany] = useState(5)
|
||||||
const [postsLength, setPostsLength] = useState(0)
|
const [postsLength, setPostsLength] = useState(0)
|
||||||
const [postCards, setPostCards] = useState<JSX.Element[]>([])
|
const [postCards, setPostCards] = useState<JSX.Element[]>([])
|
||||||
|
@ -48,9 +50,17 @@ const Home = () => {
|
||||||
if (postCount >= howMany) break
|
if (postCount >= howMany) break
|
||||||
|
|
||||||
postCount++
|
postCount++
|
||||||
const url: string = map.date[date][length - i - 1]
|
const content_id = map.date[date][length - i - 1]
|
||||||
|
|
||||||
postCards.push(
|
postCards.push(
|
||||||
<PostCard key={url} postData={{ url: url, ...map.posts[url] }} />
|
<PostCard
|
||||||
|
key={content_id}
|
||||||
|
postData={{
|
||||||
|
// /<locale>/<content id without locale suffix>
|
||||||
|
url: `/${locale}${content_id.replace(/(.kr)$/g, "")}`,
|
||||||
|
...map.posts[content_id],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +76,7 @@ const Home = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>pomp | {globalState.locale == "en" ? "Home" : "홈"}</title>
|
<title>pomp | {locale == "en" ? "Home" : "홈"}</title>
|
||||||
|
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta
|
<meta
|
||||||
|
@ -76,7 +86,7 @@ const Home = () => {
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<StyledPostList>
|
<StyledPostList>
|
||||||
<h1>{globalState.locale == "en" ? "Recent Posts" : "최근 포스트"}</h1>
|
<h1>{locale == "en" ? "Recent Posts" : "최근 포스트"}</h1>
|
||||||
{postCards}
|
{postCards}
|
||||||
{postsLength > howMany && (
|
{postsLength > howMany && (
|
||||||
<ShowMoreButton
|
<ShowMoreButton
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useContext, useState } from "react"
|
import { useContext, useState, useEffect } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
import { useLocation } from "react-router-dom"
|
import { useLocation } from "react-router-dom"
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
|
@ -12,16 +12,23 @@ import Badge from "../../components/Badge"
|
||||||
import Tag from "../../components/Tag"
|
import Tag from "../../components/Tag"
|
||||||
import NotFound from "../NotFound"
|
import NotFound from "../NotFound"
|
||||||
|
|
||||||
|
import TranslationNotAvailable from "./TranslationNotAvailable"
|
||||||
import SeriesControlButtons from "./SeriesControlButtons"
|
import SeriesControlButtons from "./SeriesControlButtons"
|
||||||
|
import {
|
||||||
|
categorizePageType,
|
||||||
|
checkURLValidity,
|
||||||
|
fetchContent,
|
||||||
|
PageType,
|
||||||
|
URLValidity,
|
||||||
|
parsePageData,
|
||||||
|
} from "./helper"
|
||||||
import Meta from "./Meta"
|
import Meta from "./Meta"
|
||||||
import Toc from "./Toc"
|
import Toc from "./Toc"
|
||||||
|
|
||||||
import portfolio from "../../data/portfolio.json"
|
import { globalContext } from "../../globalContext"
|
||||||
import _map from "../../data/map.json"
|
|
||||||
import { useEffect } from "react"
|
|
||||||
|
|
||||||
import type { PageData, Map } from "../../../types/types"
|
import type { PageData, Map } from "../../../types/types"
|
||||||
import { globalContext, SiteLocale } from "../../globalContext"
|
|
||||||
|
import _map from "../../data/map.json"
|
||||||
|
|
||||||
const map: Map = _map
|
const map: Map = _map
|
||||||
|
|
||||||
|
@ -46,253 +53,72 @@ const ProjectImage = styled.img`
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
`
|
`
|
||||||
|
|
||||||
enum PageType {
|
export default function Page() {
|
||||||
POST,
|
|
||||||
SERIES,
|
|
||||||
SERIES_HOME,
|
|
||||||
PORTFOLIO_PROJECT,
|
|
||||||
UNSEARCHABLE,
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchContent = async (
|
|
||||||
pageType: PageType,
|
|
||||||
url: string,
|
|
||||||
locale: SiteLocale
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
if (pageType == PageType.UNSEARCHABLE) {
|
|
||||||
if (locale == "en") {
|
|
||||||
return await import(`../../data/content/unsearchable${url}.json`)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return await import(
|
|
||||||
`../../data/content/unsearchable${url}.${locale}.json`
|
|
||||||
)
|
|
||||||
} catch {
|
|
||||||
return await import(`../../data/content/unsearchable${url}.json`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (locale == "en") {
|
|
||||||
return await import(`../../data/content${url}.json`)
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
return await import(`../../data/content${url}.${locale}.json`)
|
|
||||||
} catch {
|
|
||||||
return await import(`../../data/content${url}.json`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const categorizePageType = (url: string): PageType => {
|
|
||||||
if (url.startsWith("/post")) return PageType.POST
|
|
||||||
if (url.startsWith("/series")) {
|
|
||||||
if ([...(url.match(/\//g) || [])].length == 2) {
|
|
||||||
// url: /series/series-title
|
|
||||||
return PageType.SERIES_HOME
|
|
||||||
} else {
|
|
||||||
// url: /series/series-title/post-title
|
|
||||||
return PageType.SERIES
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (url.startsWith("/portfolio")) return PageType.PORTFOLIO_PROJECT
|
|
||||||
|
|
||||||
return PageType.UNSEARCHABLE
|
|
||||||
}
|
|
||||||
|
|
||||||
const Page = () => {
|
|
||||||
const { globalState } = useContext(globalContext)
|
const { globalState } = useContext(globalContext)
|
||||||
const location = useLocation()
|
const { locale } = globalState
|
||||||
|
const { pathname } = useLocation()
|
||||||
|
|
||||||
const [pageData, setPageData] = useState<PageData | undefined>(undefined)
|
const [pageData, setPageData] = useState<PageData | undefined>(undefined)
|
||||||
const [pageType, setPageType] = useState<PageType>(PageType.POST)
|
const [pageType, setPageType] = useState<PageType>(PageType.POST)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isTranslationAvailable, setIsTranslationAvailable] = useState(true)
|
||||||
|
|
||||||
|
// this code runs if either the url or the locale changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = location.pathname.replace(/\/$/, "") // remove trailing slash
|
const content_id =
|
||||||
const pageType = categorizePageType(url)
|
pathname
|
||||||
|
.replace(/^\/kr/, "") // remove /kr prefix
|
||||||
|
.replace(/^\/en/, "") // remove /en prefix
|
||||||
|
.replace(/\/$/, "") + // remove trailing slash
|
||||||
|
(locale == "en" ? "" : ".kr")
|
||||||
|
|
||||||
/**
|
const pageType = categorizePageType(content_id)
|
||||||
* Test if url is a valid one
|
|
||||||
*/
|
|
||||||
|
|
||||||
let show404 = false
|
|
||||||
switch (pageType) {
|
|
||||||
case PageType.POST: {
|
|
||||||
if (!map.posts[url]) show404 = true
|
|
||||||
|
|
||||||
|
switch (checkURLValidity(content_id, pageType)) {
|
||||||
|
case URLValidity.VALID: {
|
||||||
|
// continue if the URL is valid
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
case PageType.SERIES_HOME:
|
case URLValidity.VALID_BUT_IN_OTHER_LOCALE: {
|
||||||
case PageType.SERIES: {
|
// stop loading and set isTranslationAvailable to true so translation not available page will display
|
||||||
show404 = !Object.keys(map.series).some((seriesHomeURL) =>
|
setIsTranslationAvailable(false)
|
||||||
url.startsWith(seriesHomeURL)
|
setIsLoading(false)
|
||||||
)
|
|
||||||
|
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
case PageType.PORTFOLIO_PROJECT: {
|
case URLValidity.NOT_VALID: {
|
||||||
if (!(url in portfolio.projects)) show404 = true
|
// stop loading without fetching pageData so 404 page will display
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
break
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
case PageType.UNSEARCHABLE: {
|
|
||||||
if (!map.unsearchable[url]) show404 = true
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show404) {
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get page data
|
* Get page data
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const pageData: PageData = {
|
fetchContent(pageType, content_id, locale).then((fetched_content) => {
|
||||||
title: "No title",
|
|
||||||
date: "Unknown date",
|
|
||||||
readTime: "Unknown read time",
|
|
||||||
wordCount: 0,
|
|
||||||
tags: [],
|
|
||||||
toc: undefined,
|
|
||||||
content: "No content",
|
|
||||||
|
|
||||||
// series
|
|
||||||
|
|
||||||
seriesHome: "",
|
|
||||||
prev: "",
|
|
||||||
next: "",
|
|
||||||
|
|
||||||
// series home
|
|
||||||
|
|
||||||
order: [],
|
|
||||||
length: 0,
|
|
||||||
|
|
||||||
// portfolio
|
|
||||||
|
|
||||||
image: "",
|
|
||||||
overview: "",
|
|
||||||
badges: [],
|
|
||||||
repo: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchContent(pageType, url, globalState.locale).then((fetched_content) => {
|
|
||||||
if (!fetched_content) {
|
if (!fetched_content) {
|
||||||
|
// stop loading without fetching pageData so 404 page will display
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (pageType) {
|
setPageData(parsePageData(fetched_content, pageType, content_id, locale))
|
||||||
case PageType.POST: {
|
setIsTranslationAvailable(true)
|
||||||
const post = map.posts[url]
|
|
||||||
|
|
||||||
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 || []
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case PageType.SERIES: {
|
|
||||||
const seriesURL = url.slice(0, url.lastIndexOf("/"))
|
|
||||||
|
|
||||||
const curr = map.series[seriesURL].order.indexOf(url)
|
|
||||||
const prev = curr - 1
|
|
||||||
const next = curr + 1
|
|
||||||
|
|
||||||
const post = map.posts[url]
|
|
||||||
|
|
||||||
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.seriesHome = seriesURL
|
|
||||||
pageData.prev =
|
|
||||||
prev >= 0 ? map.series[seriesURL].order[prev] : undefined
|
|
||||||
pageData.next =
|
|
||||||
next < map.series[seriesURL].order.length
|
|
||||||
? map.series[seriesURL].order[next]
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case PageType.SERIES_HOME: {
|
|
||||||
const seriesData = map.series[url]
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case PageType.PORTFOLIO_PROJECT: {
|
|
||||||
const data =
|
|
||||||
portfolio.projects[url as keyof typeof portfolio.projects]
|
|
||||||
|
|
||||||
pageData.content = fetched_content.content
|
|
||||||
pageData.toc = fetched_content.toc
|
|
||||||
|
|
||||||
pageData.title = data.name
|
|
||||||
pageData.image = data.image
|
|
||||||
pageData.overview = data.overview
|
|
||||||
pageData.badges = data.badges
|
|
||||||
pageData.repo = data.repo
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
case PageType.UNSEARCHABLE: {
|
|
||||||
pageData.title = (
|
|
||||||
map.unsearchable[`${url}.${globalState.locale}`] ||
|
|
||||||
map.unsearchable[url]
|
|
||||||
).title
|
|
||||||
pageData.content = fetched_content.content
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply result
|
|
||||||
*/
|
|
||||||
|
|
||||||
setPageType(pageType)
|
setPageType(pageType)
|
||||||
setPageData(pageData)
|
|
||||||
|
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
})
|
})
|
||||||
}, [location, globalState.locale])
|
}, [pathname, locale])
|
||||||
|
|
||||||
if (isLoading) return <Loading />
|
if (isLoading) return <Loading />
|
||||||
|
|
||||||
|
if (!isTranslationAvailable) return <TranslationNotAvailable />
|
||||||
|
|
||||||
if (!pageData) return <NotFound />
|
if (!pageData) return <NotFound />
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -386,5 +212,3 @@ const Page = () => {
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page
|
|
||||||
|
|
61
src/pages/Page/TranslationNotAvailable.tsx
Normal file
61
src/pages/Page/TranslationNotAvailable.tsx
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import { useContext } from "react"
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { Helmet } from "react-helmet-async"
|
||||||
|
|
||||||
|
import MainContent from "../../components/MainContent"
|
||||||
|
|
||||||
|
import { globalContext } from "../../globalContext"
|
||||||
|
|
||||||
|
const Card = styled(MainContent)`
|
||||||
|
text-align: center;
|
||||||
|
`
|
||||||
|
|
||||||
|
const Title = styled.h1`
|
||||||
|
font-size: 3rem;
|
||||||
|
`
|
||||||
|
|
||||||
|
const TranslationNotAvailable = () => {
|
||||||
|
const { globalState } = useContext(globalContext)
|
||||||
|
const { locale } = globalState
|
||||||
|
|
||||||
|
const localized_title =
|
||||||
|
locale == "en" ? "Translation not found" : "번역이 존재하지 않습니다"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Helmet>
|
||||||
|
<title>pomp | {localized_title}</title>
|
||||||
|
|
||||||
|
<meta property="og:title" content={localized_title} />
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="http://developomp.com/icon/icon.svg"
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content={
|
||||||
|
locale == "en"
|
||||||
|
? "This content is not available in English."
|
||||||
|
: "본 내용의 한국어 번역이 존재하지 않습니다"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Helmet>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Title>{localized_title}</Title>
|
||||||
|
<br />
|
||||||
|
{locale == "en" ? (
|
||||||
|
<>
|
||||||
|
This post is only available in <b>Korean (한국어)</b>.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
본 내용은 <b>영어(English)</b> 로만 제공됩니다.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TranslationNotAvailable
|
258
src/pages/Page/helper.ts
Normal file
258
src/pages/Page/helper.ts
Normal file
|
@ -0,0 +1,258 @@
|
||||||
|
import portfolio from "../../data/portfolio.json"
|
||||||
|
import _map from "../../data/map.json"
|
||||||
|
|
||||||
|
import type { SiteLocale } from "../../globalContext"
|
||||||
|
import type { Map, PageData } from "../../../types/types"
|
||||||
|
|
||||||
|
const map: Map = _map
|
||||||
|
|
||||||
|
export enum PageType {
|
||||||
|
POST,
|
||||||
|
SERIES,
|
||||||
|
SERIES_HOME,
|
||||||
|
PORTFOLIO_PROJECT,
|
||||||
|
UNSEARCHABLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum URLValidity {
|
||||||
|
VALID, // page does exist in selected language
|
||||||
|
VALID_BUT_IN_OTHER_LOCALE, // page exists but only in another language
|
||||||
|
NOT_VALID, // page does not exist
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchContent(
|
||||||
|
pageType: PageType,
|
||||||
|
url: string,
|
||||||
|
locale: SiteLocale
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
if (pageType == PageType.UNSEARCHABLE) {
|
||||||
|
if (locale == "en") {
|
||||||
|
return await import(`../../data/content/unsearchable${url}.json`)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return await import(
|
||||||
|
`../../data/content/unsearchable${url}.${locale}.json`
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return await import(`../../data/content/unsearchable${url}.json`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locale == "en") {
|
||||||
|
return await import(`../../data/content${url}.json`)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
return await import(`../../data/content${url}.${locale}.json`)
|
||||||
|
} catch {
|
||||||
|
return await import(`../../data/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 the URL looks like /series/series-title/post-title (if the url does not have 2 slashes)
|
||||||
|
return PageType.SERIES
|
||||||
|
}
|
||||||
|
|
||||||
|
return PageType.UNSEARCHABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkURLValidity(
|
||||||
|
content_id: string,
|
||||||
|
pageType: PageType
|
||||||
|
): URLValidity {
|
||||||
|
// content ID of other language
|
||||||
|
const alt_content_id = content_id.endsWith(".kr")
|
||||||
|
? content_id.replace(/\.kr$/, "") // remove .kr suffix
|
||||||
|
: content_id + ".kr" // add .kr suffix
|
||||||
|
|
||||||
|
switch (pageType) {
|
||||||
|
case PageType.POST: {
|
||||||
|
if (map.posts[content_id]) return URLValidity.VALID
|
||||||
|
|
||||||
|
if (map.posts[alt_content_id])
|
||||||
|
return URLValidity.VALID_BUT_IN_OTHER_LOCALE
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case PageType.SERIES_HOME:
|
||||||
|
case PageType.SERIES: {
|
||||||
|
const series_keys = Object.keys(map.series)
|
||||||
|
|
||||||
|
if (
|
||||||
|
series_keys.some((seriesHomeURL) =>
|
||||||
|
content_id.startsWith(seriesHomeURL)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return URLValidity.VALID
|
||||||
|
|
||||||
|
if (
|
||||||
|
series_keys.some((seriesHomeURL) =>
|
||||||
|
alt_content_id.startsWith(seriesHomeURL)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return URLValidity.VALID_BUT_IN_OTHER_LOCALE
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case PageType.PORTFOLIO_PROJECT: {
|
||||||
|
if (content_id in portfolio.projects) return URLValidity.VALID
|
||||||
|
|
||||||
|
if (alt_content_id in portfolio.projects)
|
||||||
|
return URLValidity.VALID_BUT_IN_OTHER_LOCALE
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case PageType.UNSEARCHABLE: {
|
||||||
|
if (map.unsearchable[content_id]) return URLValidity.VALID
|
||||||
|
|
||||||
|
if (map.unsearchable[alt_content_id])
|
||||||
|
return URLValidity.VALID_BUT_IN_OTHER_LOCALE
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return URLValidity.NOT_VALID
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePageData(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
fetched_content: any,
|
||||||
|
pageType: PageType,
|
||||||
|
content_id: string,
|
||||||
|
locale: SiteLocale
|
||||||
|
): 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",
|
||||||
|
|
||||||
|
// series
|
||||||
|
|
||||||
|
seriesHome: "",
|
||||||
|
prev: "",
|
||||||
|
next: "",
|
||||||
|
|
||||||
|
// series home
|
||||||
|
|
||||||
|
order: [],
|
||||||
|
length: 0,
|
||||||
|
|
||||||
|
// portfolio
|
||||||
|
|
||||||
|
image: "",
|
||||||
|
overview: "",
|
||||||
|
badges: [],
|
||||||
|
repo: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
// load and parse content differently depending on the content type
|
||||||
|
switch (pageType) {
|
||||||
|
case PageType.POST: {
|
||||||
|
const post = map.posts[content_id]
|
||||||
|
|
||||||
|
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 || []
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case PageType.SERIES: {
|
||||||
|
const seriesURL = content_id.slice(0, content_id.lastIndexOf("/"))
|
||||||
|
|
||||||
|
const curr = map.series[seriesURL].order.indexOf(content_id)
|
||||||
|
const prev = curr - 1
|
||||||
|
const next = curr + 1
|
||||||
|
|
||||||
|
const post = map.posts[content_id]
|
||||||
|
|
||||||
|
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.seriesHome = seriesURL
|
||||||
|
pageData.prev = prev >= 0 ? map.series[seriesURL].order[prev] : undefined
|
||||||
|
pageData.next =
|
||||||
|
next < map.series[seriesURL].order.length
|
||||||
|
? map.series[seriesURL].order[next]
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case PageType.SERIES_HOME: {
|
||||||
|
const seriesData = map.series[content_id]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case PageType.PORTFOLIO_PROJECT: {
|
||||||
|
const data =
|
||||||
|
portfolio.projects[content_id as keyof typeof portfolio.projects]
|
||||||
|
|
||||||
|
pageData.content = fetched_content.content
|
||||||
|
pageData.toc = fetched_content.toc
|
||||||
|
|
||||||
|
pageData.title = data.name
|
||||||
|
pageData.image = data.image
|
||||||
|
pageData.overview = data.overview
|
||||||
|
pageData.badges = data.badges
|
||||||
|
pageData.repo = data.repo
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
case PageType.UNSEARCHABLE: {
|
||||||
|
pageData.title = (
|
||||||
|
map.unsearchable[`${content_id}.${locale}`] ||
|
||||||
|
map.unsearchable[content_id]
|
||||||
|
).title
|
||||||
|
pageData.content = fetched_content.content
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return pageData
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react"
|
import { useContext, useEffect, useState } from "react"
|
||||||
import styled from "styled-components"
|
import styled from "styled-components"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import { cardCSS } from "../../components/Card"
|
||||||
|
|
||||||
import { PortfolioProject } from "../../../types/types"
|
import { PortfolioProject } from "../../../types/types"
|
||||||
import theming from "../../styles/theming"
|
import theming from "../../styles/theming"
|
||||||
|
import { globalContext } from "../../globalContext"
|
||||||
|
|
||||||
const StyledProjectCard = styled.div`
|
const StyledProjectCard = styled.div`
|
||||||
${cardCSS}
|
${cardCSS}
|
||||||
|
@ -37,6 +38,7 @@ interface ProjectCardProps {
|
||||||
const ProjectCard = (props: ProjectCardProps) => {
|
const ProjectCard = (props: ProjectCardProps) => {
|
||||||
const { projectID, project } = props
|
const { projectID, project } = props
|
||||||
|
|
||||||
|
const { globalState } = useContext(globalContext)
|
||||||
const [badges, setBadges] = useState<JSX.Element[]>([])
|
const [badges, setBadges] = useState<JSX.Element[]>([])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -44,7 +46,7 @@ const ProjectCard = (props: ProjectCardProps) => {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link to={process.env.PUBLIC_URL + projectID}>
|
<Link to={`/${globalState.locale}${projectID}`}>
|
||||||
<StyledProjectCard>
|
<StyledProjectCard>
|
||||||
<h1>{project.name}</h1>
|
<h1>{project.name}</h1>
|
||||||
<StyledImg src={project.image} />
|
<StyledImg src={project.image} />
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue