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:
Kim, Jimin 2022-04-17 21:49:54 +09:00
parent 56bf555bd7
commit d1a33ccf1e
17 changed files with 2569 additions and 2383 deletions

View file

@ -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",

View file

@ -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 {

View file

@ -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": [

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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: "",

View file

@ -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) => {
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faBook} />
&nbsp;&nbsp;&nbsp;
{postData.wordCount
{typeof postData.wordCount === "number"
? postData.wordCount + " words"
: "unknown length"}
</StyledMetaContainer>

View file

@ -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>

View file

@ -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,
}

View file

@ -1,20 +1,18 @@
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>
<GlobalStore>
<BrowserRouter>
<HelmetProvider>
<App />
</HelmetProvider>
</BrowserRouter>
</GlobalStore>
</React.StrictMode>,
document.getElementById("root")
const container = document.getElementById("root") as HTMLElement
const root = createRoot(container)
root.render(
<GlobalStore>
<BrowserRouter>
<HelmetProvider>
<App />
</HelmetProvider>
</BrowserRouter>
</GlobalStore>
)

View file

@ -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

View file

@ -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)
)
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)
break
return
}
case PageType.PORTFOLIO_PROJECT: {
if (!(url in portfolio.projects)) show404 = true
case URLValidity.NOT_VALID: {
// 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
*/
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

View 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
View 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
}

View file

@ -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} />

4158
yarn.lock

File diff suppressed because it is too large Load diff