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"]
|
||||
},
|
||||
"react": {
|
||||
"version": "17.0"
|
||||
"version": "18.0"
|
||||
}
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
|
@ -21,7 +21,7 @@
|
|||
},
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["react", "@typescript-eslint"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import ejs from "ejs"
|
||||
import { optimize } from "svgo"
|
||||
import { optimize, OptimizedSvg } from "svgo"
|
||||
import { readFileSync, writeFileSync } from "fs"
|
||||
import simpleIcon from "simple-icons"
|
||||
import tinycolor from "tinycolor2"
|
||||
|
@ -78,7 +78,12 @@ function generatePortfolioSVGs() {
|
|||
|
||||
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 {
|
||||
|
|
65
package.json
65
package.json
|
@ -6,6 +6,9 @@
|
|||
"quick-start": "react-scripts start",
|
||||
"build": "yarn generate && react-scripts build"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "18.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.1.1",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.1.1",
|
||||
|
@ -13,52 +16,50 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-fontawesome": "^0.1.18",
|
||||
"elasticlunr": "^0.9.5",
|
||||
"highlight.js": "^11.3.1",
|
||||
"katex": "^0.15.1",
|
||||
"highlight.js": "^11.5.1",
|
||||
"katex": "^0.15.3",
|
||||
"local-storage-fallback": "^4.1.2",
|
||||
"react": "^17.0.2",
|
||||
"react": "^18.0.0",
|
||||
"react-collapse": "^5.1.1",
|
||||
"react-date-range": "^1.4.0",
|
||||
"react-device-detect": "^2.1.2",
|
||||
"react-dnd": "^14.0.5",
|
||||
"react-dnd-html5-backend": "^14.1.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet-async": "^1.2.2",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-router-hash-link": "^2.4.3",
|
||||
"react-scripts": "^5.0.0",
|
||||
"react-select": "^5.2.1",
|
||||
"react-device-detect": "^2.2.2",
|
||||
"react-dnd": "^16.0.0",
|
||||
"react-dnd-html5-backend": "^16.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "^5.0.1",
|
||||
"react-select": "^5.3.0",
|
||||
"react-tooltip": "^4.2.21",
|
||||
"styled-components": "^5.3.3"
|
||||
"styled-components": "^5.3.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ejs": "^3.1.0",
|
||||
"@types/elasticlunr": "^0.9.4",
|
||||
"@types/highlight.js": "^10.1.0",
|
||||
"@types/jsdom": "^16.2.14",
|
||||
"@types/katex": "^0.11.1",
|
||||
"@types/katex": "^0.14.0",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/node": "^17.0.8",
|
||||
"@types/react": "^17.0.38",
|
||||
"@types/node": "^17.0.24",
|
||||
"@types/react": "^18.0.5",
|
||||
"@types/react-collapse": "^5.0.1",
|
||||
"@types/react-date-range": "^1.4.2",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-router-hash-link": "^2.4.4",
|
||||
"@types/react-date-range": "^1.4.3",
|
||||
"@types/react-dom": "^18.0.1",
|
||||
"@types/react-select": "^5.0.1",
|
||||
"@types/styled-components": "^5.1.19",
|
||||
"@types/svgo": "^2.6.0",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@types/svgo": "^2.6.3",
|
||||
"@types/tinycolor2": "^1.4.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.9.0",
|
||||
"@typescript-eslint/parser": "^5.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.19.0",
|
||||
"@typescript-eslint/parser": "^5.19.0",
|
||||
"ejs": "^3.1.6",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-json": "^3.1.0",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react": "^7.29.4",
|
||||
"gray-matter": "^4.0.3",
|
||||
"jsdom": "^19.0.0",
|
||||
"jspdf": "^2.5.0",
|
||||
"markdown-it": "^12.3.0",
|
||||
"markdown-it-anchor": "^8.4.1",
|
||||
"jspdf": "^2.5.1",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-anchor": "^8.6.2",
|
||||
"markdown-it-attrs": "^4.1.3",
|
||||
"markdown-it-footnote": "^3.0.3",
|
||||
"markdown-it-highlight-lines": "^1.0.2",
|
||||
|
@ -68,14 +69,14 @@
|
|||
"markdown-it-task-checkbox": "^1.0.6",
|
||||
"markdown-it-texmath": "^0.9.7",
|
||||
"markdown-toc": "^1.2.0",
|
||||
"prettier": "^2.5.1",
|
||||
"prettier": "^2.6.2",
|
||||
"read-time-estimate": "^0.0.3",
|
||||
"simple-icons": "^6.9.0",
|
||||
"simple-icons": "^6.19.0",
|
||||
"svgo": "^2.8.0",
|
||||
"tinycolor2": "^1.4.2",
|
||||
"ts-node": "^10.4.0",
|
||||
"ts-node": "^10.7.0",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typescript": "^4.5.4"
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
|
47
src/App.tsx
47
src/App.tsx
|
@ -1,5 +1,5 @@
|
|||
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 { Helmet } from "react-helmet-async"
|
||||
import { isIE } from "react-device-detect"
|
||||
|
@ -35,10 +35,20 @@ const StyledContentContainer = styled.div`
|
|||
|
||||
export default function App() {
|
||||
const { globalState, dispatch } = useContext(globalContext)
|
||||
const { locale } = globalState
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { pathname } = useLocation()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
||||
// set loading to false if all fonts are loaded
|
||||
// update url on locale change
|
||||
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
|
||||
if (typeof document.fonts.onloadingdone != undefined) {
|
||||
document.fonts.onloadingdone = () => {
|
||||
|
@ -47,6 +57,10 @@ export default function App() {
|
|||
} else {
|
||||
setIsLoading(false)
|
||||
}
|
||||
|
||||
// automatically add locale prefix if it's not specified
|
||||
if (!pathname.startsWith("/en") && !pathname.startsWith("/kr"))
|
||||
navigate(`/${globalState.locale}${pathname}`)
|
||||
}, [])
|
||||
|
||||
if (isIE)
|
||||
|
@ -80,12 +94,29 @@ export default function App() {
|
|||
<Loading />
|
||||
) : (
|
||||
<Routes>
|
||||
<Route path="/" 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 />} />
|
||||
{/*
|
||||
Using this ugly code because the developers of react-router-dom decided that
|
||||
removing regex support was a good idea.
|
||||
https://github.com/remix-run/react-router/issues/7285
|
||||
*/}
|
||||
|
||||
<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>
|
||||
)}
|
||||
</StyledContentContainer>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useContext } from "react"
|
||||
import ReactTooltip from "react-tooltip"
|
||||
import styled from "styled-components"
|
||||
import ReactTooltip from "react-tooltip"
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faLanguage } from "@fortawesome/free-solid-svg-icons"
|
||||
|
@ -14,18 +14,20 @@ interface StyledLocaleToggleButtonProps {
|
|||
locale: SiteLocale
|
||||
}
|
||||
|
||||
const StyledLocaleToggleButton = styled.div<StyledLocaleToggleButtonProps>`
|
||||
const StyledLocaleToggleButton = styled.button<StyledLocaleToggleButtonProps>`
|
||||
${theming.styles.navbarButtonStyle}
|
||||
border: none;
|
||||
width: 72px;
|
||||
|
||||
${(props) => (props.locale == "en" ? "" : "transform: scaleX(-1);")};
|
||||
`
|
||||
|
||||
const LocaleToggleButton = () => {
|
||||
function LocaleToggleButton() {
|
||||
const { globalState, dispatch } = useContext(globalContext)
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledLocaleToggleButton
|
||||
locale={globalState.locale}
|
||||
data-tip
|
||||
data-for="locale"
|
||||
onClick={() => {
|
||||
|
@ -34,6 +36,7 @@ const LocaleToggleButton = () => {
|
|||
payload: globalState.locale == "en" ? "kr" : "en",
|
||||
})
|
||||
}}
|
||||
locale={globalState.locale}
|
||||
>
|
||||
<FontAwesomeIcon icon={faLanguage} />
|
||||
</StyledLocaleToggleButton>
|
||||
|
|
|
@ -23,7 +23,7 @@ const NavLinks = () => {
|
|||
return (
|
||||
<StyledNavLinks>
|
||||
{NavbarData.map((item, index) => (
|
||||
<Link key={index} to={item.path}>
|
||||
<Link key={index} to={globalState.locale + item.path}>
|
||||
<StyledLink>
|
||||
{globalState.locale == "en" ? item.title_en : item.title_kr}
|
||||
</StyledLink>
|
||||
|
|
|
@ -9,8 +9,11 @@ import { faMoon, faSun } from "@fortawesome/free-solid-svg-icons"
|
|||
import theming from "../../styles/theming"
|
||||
import { ActionsEnum, globalContext } from "../../globalContext"
|
||||
|
||||
const StyledThemeButton = styled.div`
|
||||
const StyledThemeButton = styled.button`
|
||||
${theming.styles.navbarButtonStyle}
|
||||
border: none;
|
||||
width: 72px;
|
||||
|
||||
${(props) =>
|
||||
theming.theme(props.theme.currentTheme, {
|
||||
light: "",
|
||||
|
|
|
@ -78,7 +78,7 @@ const PostCard = (props: Props) => {
|
|||
|
||||
return (
|
||||
<StyledPostCard>
|
||||
<PostCardContainer to={process.env.PUBLIC_URL + postData.url}>
|
||||
<PostCardContainer to={postData.url}>
|
||||
<StyledTitle>
|
||||
{postData.title || "No title"}
|
||||
{/* show "(series)" for urls that matches regex "/series/<series-title>" */}
|
||||
|
@ -107,7 +107,7 @@ const PostCard = (props: Props) => {
|
|||
|
||||
<FontAwesomeIcon icon={faBook} />
|
||||
|
||||
{postData.wordCount
|
||||
{typeof postData.wordCount === "number"
|
||||
? postData.wordCount + " words"
|
||||
: "unknown length"}
|
||||
</StyledMetaContainer>
|
||||
|
|
|
@ -49,7 +49,10 @@ const SubMenu = (props: Props) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<SidebarLink to={props.item.path} onClick={handleSidebarLinkClick}>
|
||||
<SidebarLink
|
||||
to={globalState.locale + props.item.path}
|
||||
onClick={handleSidebarLinkClick}
|
||||
>
|
||||
<div>
|
||||
{props.item.icon}
|
||||
<SidebarLabel>
|
||||
|
|
|
@ -31,8 +31,15 @@ export interface IGlobalContext {
|
|||
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 = {
|
||||
locale: (storage.getItem("locale") || "en") as SiteLocale,
|
||||
locale: getDefaultLocale(),
|
||||
theme: (storage.getItem("theme") || "dark") as SiteTheme,
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import React from "react"
|
||||
import ReactDOM from "react-dom"
|
||||
import { createRoot } from "react-dom/client"
|
||||
import { HelmetProvider } from "react-helmet-async"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
import { GlobalStore } from "./globalContext"
|
||||
|
||||
import App from "./App"
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
const container = document.getElementById("root") as HTMLElement
|
||||
const root = createRoot(container)
|
||||
root.render(
|
||||
<GlobalStore>
|
||||
<BrowserRouter>
|
||||
<HelmetProvider>
|
||||
|
@ -15,6 +15,4 @@ ReactDOM.render(
|
|||
</HelmetProvider>
|
||||
</BrowserRouter>
|
||||
</GlobalStore>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
)
|
||||
|
|
|
@ -31,6 +31,8 @@ const StyledPostList = styled.div`
|
|||
|
||||
const Home = () => {
|
||||
const { globalState } = useContext(globalContext)
|
||||
const { locale } = globalState
|
||||
|
||||
const [howMany, setHowMany] = useState(5)
|
||||
const [postsLength, setPostsLength] = useState(0)
|
||||
const [postCards, setPostCards] = useState<JSX.Element[]>([])
|
||||
|
@ -48,9 +50,17 @@ const Home = () => {
|
|||
if (postCount >= howMany) break
|
||||
|
||||
postCount++
|
||||
const url: string = map.date[date][length - i - 1]
|
||||
const content_id = map.date[date][length - i - 1]
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>pomp | {globalState.locale == "en" ? "Home" : "홈"}</title>
|
||||
<title>pomp | {locale == "en" ? "Home" : "홈"}</title>
|
||||
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
|
@ -76,7 +86,7 @@ const Home = () => {
|
|||
</Helmet>
|
||||
|
||||
<StyledPostList>
|
||||
<h1>{globalState.locale == "en" ? "Recent Posts" : "최근 포스트"}</h1>
|
||||
<h1>{locale == "en" ? "Recent Posts" : "최근 포스트"}</h1>
|
||||
{postCards}
|
||||
{postsLength > howMany && (
|
||||
<ShowMoreButton
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useContext, useState } from "react"
|
||||
import { useContext, useState, useEffect } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
import { useLocation } from "react-router-dom"
|
||||
import styled from "styled-components"
|
||||
|
@ -12,16 +12,23 @@ import Badge from "../../components/Badge"
|
|||
import Tag from "../../components/Tag"
|
||||
import NotFound from "../NotFound"
|
||||
|
||||
import TranslationNotAvailable from "./TranslationNotAvailable"
|
||||
import SeriesControlButtons from "./SeriesControlButtons"
|
||||
import {
|
||||
categorizePageType,
|
||||
checkURLValidity,
|
||||
fetchContent,
|
||||
PageType,
|
||||
URLValidity,
|
||||
parsePageData,
|
||||
} from "./helper"
|
||||
import Meta from "./Meta"
|
||||
import Toc from "./Toc"
|
||||
|
||||
import portfolio from "../../data/portfolio.json"
|
||||
import _map from "../../data/map.json"
|
||||
import { useEffect } from "react"
|
||||
|
||||
import { globalContext } from "../../globalContext"
|
||||
import type { PageData, Map } from "../../../types/types"
|
||||
import { globalContext, SiteLocale } from "../../globalContext"
|
||||
|
||||
import _map from "../../data/map.json"
|
||||
|
||||
const map: Map = _map
|
||||
|
||||
|
@ -46,253 +53,72 @@ const ProjectImage = styled.img`
|
|||
max-width: 100%;
|
||||
`
|
||||
|
||||
enum PageType {
|
||||
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 = () => {
|
||||
export default function Page() {
|
||||
const { globalState } = useContext(globalContext)
|
||||
const location = useLocation()
|
||||
const { locale } = globalState
|
||||
const { pathname } = useLocation()
|
||||
|
||||
const [pageData, setPageData] = useState<PageData | undefined>(undefined)
|
||||
const [pageType, setPageType] = useState<PageType>(PageType.POST)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isTranslationAvailable, setIsTranslationAvailable] = useState(true)
|
||||
|
||||
// this code runs if either the url or the locale changes
|
||||
useEffect(() => {
|
||||
const url = location.pathname.replace(/\/$/, "") // remove trailing slash
|
||||
const pageType = categorizePageType(url)
|
||||
const content_id =
|
||||
pathname
|
||||
.replace(/^\/kr/, "") // remove /kr prefix
|
||||
.replace(/^\/en/, "") // remove /en prefix
|
||||
.replace(/\/$/, "") + // remove trailing slash
|
||||
(locale == "en" ? "" : ".kr")
|
||||
|
||||
/**
|
||||
* Test if url is a valid one
|
||||
*/
|
||||
|
||||
let show404 = false
|
||||
switch (pageType) {
|
||||
case PageType.POST: {
|
||||
if (!map.posts[url]) show404 = true
|
||||
const pageType = categorizePageType(content_id)
|
||||
|
||||
switch (checkURLValidity(content_id, pageType)) {
|
||||
case URLValidity.VALID: {
|
||||
// continue if the URL is valid
|
||||
break
|
||||
}
|
||||
|
||||
case PageType.SERIES_HOME:
|
||||
case PageType.SERIES: {
|
||||
show404 = !Object.keys(map.series).some((seriesHomeURL) =>
|
||||
url.startsWith(seriesHomeURL)
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case PageType.PORTFOLIO_PROJECT: {
|
||||
if (!(url in portfolio.projects)) show404 = true
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
case PageType.UNSEARCHABLE: {
|
||||
if (!map.unsearchable[url]) show404 = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (show404) {
|
||||
case URLValidity.VALID_BUT_IN_OTHER_LOCALE: {
|
||||
// stop loading and set isTranslationAvailable to true so translation not available page will display
|
||||
setIsTranslationAvailable(false)
|
||||
setIsLoading(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
case URLValidity.NOT_VALID: {
|
||||
// stop loading without fetching pageData so 404 page will display
|
||||
setIsLoading(false)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get page data
|
||||
*/
|
||||
|
||||
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: "",
|
||||
}
|
||||
|
||||
fetchContent(pageType, url, globalState.locale).then((fetched_content) => {
|
||||
fetchContent(pageType, content_id, locale).then((fetched_content) => {
|
||||
if (!fetched_content) {
|
||||
// stop loading without fetching pageData so 404 page will display
|
||||
setIsLoading(false)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
switch (pageType) {
|
||||
case PageType.POST: {
|
||||
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
|
||||
*/
|
||||
|
||||
setPageData(parsePageData(fetched_content, pageType, content_id, locale))
|
||||
setIsTranslationAvailable(true)
|
||||
setPageType(pageType)
|
||||
setPageData(pageData)
|
||||
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [location, globalState.locale])
|
||||
}, [pathname, locale])
|
||||
|
||||
if (isLoading) return <Loading />
|
||||
|
||||
if (!isTranslationAvailable) return <TranslationNotAvailable />
|
||||
|
||||
if (!pageData) return <NotFound />
|
||||
|
||||
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 { Link } from "react-router-dom"
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { cardCSS } from "../../components/Card"
|
|||
|
||||
import { PortfolioProject } from "../../../types/types"
|
||||
import theming from "../../styles/theming"
|
||||
import { globalContext } from "../../globalContext"
|
||||
|
||||
const StyledProjectCard = styled.div`
|
||||
${cardCSS}
|
||||
|
@ -37,6 +38,7 @@ interface ProjectCardProps {
|
|||
const ProjectCard = (props: ProjectCardProps) => {
|
||||
const { projectID, project } = props
|
||||
|
||||
const { globalState } = useContext(globalContext)
|
||||
const [badges, setBadges] = useState<JSX.Element[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -44,7 +46,7 @@ const ProjectCard = (props: ProjectCardProps) => {
|
|||
}, [])
|
||||
|
||||
return (
|
||||
<Link to={process.env.PUBLIC_URL + projectID}>
|
||||
<Link to={`/${globalState.locale}${projectID}`}>
|
||||
<StyledProjectCard>
|
||||
<h1>{project.name}</h1>
|
||||
<StyledImg src={project.image} />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue