minimized use of null, made Page.tsx functional component, removed unused import from NotFound.tsx, and moved HelmetProvider to index.tsx

This commit is contained in:
Kim, Jimin 2021-12-15 10:14:19 +09:00
parent 49fd2b2900
commit d86e5ae002
6 changed files with 251 additions and 284 deletions

View file

@ -5,7 +5,7 @@ import styled, {
createGlobalStyle, createGlobalStyle,
css, css,
} from "styled-components" } from "styled-components"
import { HelmetProvider, Helmet } from "react-helmet-async" import { Helmet } from "react-helmet-async"
import storage from "local-storage-fallback" import storage from "local-storage-fallback"
import { isIE } from "react-device-detect" import { isIE } from "react-device-detect"
@ -303,7 +303,6 @@ export default class App extends React.Component<AppProps, AppState> {
) )
return ( return (
<HelmetProvider>
<ThemeProvider <ThemeProvider
theme={{ theme={{
currentTheme: this.state.currentTheme, currentTheme: this.state.currentTheme,
@ -321,13 +320,11 @@ export default class App extends React.Component<AppProps, AppState> {
content="developomp's blog" content="developomp's blog"
/> />
<meta <meta property="og:url" content={process.env.PUBLIC_URL} />
property="og:url"
content={process.env.PUBLIC_URL}
/>
</Helmet> </Helmet>
<GlobalStyle /> <GlobalStyle />
<Navbar /> <Navbar />
<StyledContentContainer> <StyledContentContainer>
{this.state.isLoading ? ( {this.state.isLoading ? (
@ -336,24 +333,17 @@ export default class App extends React.Component<AppProps, AppState> {
<Routes> <Routes>
<Route <Route
path="/" path="/"
element={ element={<PostList howMany={5} title="Home" />}
<PostList howMany={5} title="Home" />
}
/> />
<Route path="/loading" element={<Loading />} /> <Route path="/loading" element={<Loading />} />
<Route path="/search" element={<Search />} /> <Route path="/search" element={<Search />} />
<Route path="/404" element={<NotFound />} /> <Route path="/404" element={<NotFound />} />
<Route path="/:path*" element={<Page />} /> <Route path="/:path*" element={<Page />} />
</Routes> </Routes>
)} )}
</StyledContentContainer> </StyledContentContainer>
<Footer /> <Footer />
</ThemeProvider> </ThemeProvider>
</HelmetProvider>
) )
} }
} }

View file

@ -65,7 +65,7 @@ const SubMenu = (props: Props) => {
? props.item.iconOpened ? props.item.iconOpened
: props.item.subNav : props.item.subNav
? props.item.iconClosed ? props.item.iconClosed
: null} : undefined}
</div> </div>
</SidebarLink> </SidebarLink>

View file

@ -1,5 +1,6 @@
import React from "react" import React from "react"
import ReactDOM from "react-dom" import ReactDOM from "react-dom"
import { HelmetProvider } from "react-helmet-async"
import { BrowserRouter } from "react-router-dom" import { BrowserRouter } from "react-router-dom"
import App from "./App" import App from "./App"
@ -8,7 +9,9 @@ import reportWebVitals from "./reportWebVitals"
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<HelmetProvider>
<App /> <App />
</HelmetProvider>
</BrowserRouter> </BrowserRouter>
</React.StrictMode>, </React.StrictMode>,
document.getElementById("root") document.getElementById("root")

View file

@ -1,4 +1,3 @@
import React from "react"
import styled from "styled-components" import styled from "styled-components"
import { Helmet } from "react-helmet-async" import { Helmet } from "react-helmet-async"

View file

@ -1,19 +1,13 @@
import React from "react" import { useState } from "react"
import { Helmet } from "react-helmet-async" import { Helmet } from "react-helmet-async"
import { Link } from "react-router-dom" import { Link, useLocation } from "react-router-dom"
import styled from "styled-components" import styled from "styled-components"
import { HashLink } from "react-router-hash-link" import { HashLink } from "react-router-hash-link"
import { Collapse } from "react-collapse" import { Collapse } from "react-collapse"
import storage from "local-storage-fallback" import storage from "local-storage-fallback"
import theming from "../theming" import { TocElement, FetchedPage, Map } from "../types/typings"
import Tag from "../components/Tag"
import TagList from "../components/TagList"
import NotFound from "./NotFound"
import Loading from "../components/Loading"
import _map from "../data/map.json"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { import {
faBook, faBook,
@ -23,7 +17,15 @@ import {
faHourglass, faHourglass,
} from "@fortawesome/free-solid-svg-icons" } from "@fortawesome/free-solid-svg-icons"
import { TocElement, FetchedPage, Map } from "../types/typings" import Tag from "../components/Tag"
import TagList from "../components/TagList"
import NotFound from "./NotFound"
import Loading from "../components/Loading"
import theming from "../theming"
import _map from "../data/map.json"
import { useEffect } from "react"
const map: Map = _map const map: Map = _map
@ -119,95 +121,127 @@ function parseToc(tocData: TocElement[]) {
) )
} }
interface PageProps {} const NextPrevButton = (props: { prevURL?: string; nextURL?: string }) => {
interface PageState {
fetchedPage?: FetchedPage
isUnsearchable: boolean
isSeries: boolean
seriesData: {
seriesHome: string
prev: string | null
next: string | null
} | null
isTocOpened: boolean
loading: boolean
}
interface NextPrevProps {
prevURL: string | null
nextURL: string | null
}
class NextPrev extends React.Component<NextPrevProps> {
render() {
return ( return (
<StyledNextPrevContainer> <StyledNextPrevContainer>
{this.props.prevURL ? ( {props.prevURL ? (
<StyledLink to={this.props.prevURL}>prev</StyledLink> <StyledLink to={props.prevURL}>prev</StyledLink>
) : ( ) : (
<StyledDisabledLink>prev</StyledDisabledLink> <StyledDisabledLink>prev</StyledDisabledLink>
)} )}
{this.props.nextURL ? ( {props.nextURL ? (
<StyledLink to={this.props.nextURL}>next</StyledLink> <StyledLink to={props.nextURL}>next</StyledLink>
) : ( ) : (
<StyledDisabledLink>next</StyledDisabledLink> <StyledDisabledLink>next</StyledDisabledLink>
)} )}
</StyledNextPrevContainer> </StyledNextPrevContainer>
) )
} }
const PostMeta = (props: { fetchedPage: FetchedPage }) => {
return (
<StyledMetaContainer>
<FontAwesomeIcon icon={faCalendar} />
&nbsp;&nbsp;&nbsp;
{props.fetchedPage.date || "Unknown date"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faHourglass} />
&nbsp;&nbsp;&nbsp;
{props.fetchedPage.readTime
? props.fetchedPage.readTime + " read"
: "unknown length"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faBook} />
&nbsp;&nbsp;&nbsp;
{props.fetchedPage.wordCount
? props.fetchedPage.wordCount + " words"
: "unknown words"}
</StyledMetaContainer>
)
} }
export default class Page extends React.Component<PageProps, PageState> { const PageTOC = (props: { fetchedPage: FetchedPage }) => {
constructor(props: PageProps) { const [isTocOpened, setIsTocOpened] = useState(
super(props) storage.getItem("isTocOpened") == "true"
)
this.state = { useEffect(() => {
fetchedPage: undefined, storage.setItem("isTocOpened", isTocOpened.toString())
isUnsearchable: false, }, [isTocOpened])
isSeries: false,
seriesData: null, return (
isTocOpened: storage.getItem("isTocOpened") == "true", <>
loading: true, <StyledTocToggleButton
} onClick={() => {
setIsTocOpened((prev) => !prev)
}}
>
<strong>Table of Content </strong>
{isTocOpened ? (
<FontAwesomeIcon icon={faCaretUp} />
) : (
<FontAwesomeIcon icon={faCaretDown} />
)}
</StyledTocToggleButton>
<StyledCollapseContainer>
<Collapse isOpened={isTocOpened}>
<div className="white-link">{props.fetchedPage.toc}</div>
</Collapse>
</StyledCollapseContainer>
<hr />
</>
)
} }
componentDidUpdate(_: PageProps, prevState: PageState) { interface SeriesData {
if (this.state.isTocOpened !== prevState.isTocOpened) { seriesHome: string
storage.setItem("isTocOpened", this.state.isTocOpened.toString()) prev?: string
} next?: string
} }
async componentDidMount() { const Page = () => {
let _isUnsearchable = false const [fetchedPage, setFetchPage] = useState<FetchedPage | undefined>(
undefined
)
const [isPageUnsearchable, setIsPageUnsearchable] = useState(false)
const [isSeries, setIsSeries] = useState(false)
const [seriesData, setSeriesData] = useState<SeriesData | undefined>(
undefined
)
const [isLoading, setIsLoading] = useState(true)
const location = useLocation()
const fetchContent = async (
isContentUnsearchable: boolean,
url: string
) => {
return isContentUnsearchable
? await import(`../data/content/unsearchable${url}.json`)
: await import(`../data/content${url}.json`)
}
useEffect(() => {
let _isSeries = false let _isSeries = false
const url = location.pathname.replace(/\/$/, "") // remove trailing slash const url = location.pathname.replace(/\/$/, "") // remove trailing slash
if (url.startsWith("/series")) _isSeries = true if (url.startsWith("/series")) _isSeries = true
if (_isSeries) { if (_isSeries) {
const seriesURL = url.slice(0, url.lastIndexOf("/")) const seriesURL = url.slice(0, url.lastIndexOf("/"))
if (seriesURL in map.series) { if (seriesURL in map.series) {
const _curr: number = map.series[seriesURL].order.indexOf(url) const _curr: number = map.series[seriesURL].order.indexOf(url)
let _prev: number | null = _curr - 1 const _prev = _curr - 1
let _next: number | null = _curr + 1 const _next = _curr + 1
if (_prev < 0) _prev = null setSeriesData({
if (_next > map.series[seriesURL].order.length - 1) _next = null
this.setState({
seriesData: {
seriesHome: seriesURL, seriesHome: seriesURL,
prev: prev:
_prev != null _prev >= 0
? map.series[seriesURL].order[_prev] ? map.series[seriesURL].order[_prev]
: null, : undefined,
next: next:
_next != null _next < map.series[seriesURL].order.length
? map.series[seriesURL].order[_next] ? map.series[seriesURL].order[_next]
: null, : undefined,
},
}) })
} }
} }
@ -221,20 +255,18 @@ export default class Page extends React.Component<PageProps, PageState> {
tags: [] as string[], tags: [] as string[],
} }
let _isUnsearchable = false
if (!MapPost) { if (!MapPost) {
_isUnsearchable = true _isUnsearchable = true
this.setState({ isUnsearchable: _isUnsearchable }) setIsPageUnsearchable(_isUnsearchable)
if (!map.unsearchable[url]) { if (!map.unsearchable[url]) {
this.setState({ loading: false }) setIsLoading(false)
return return
} }
} }
const fetched_content = _isUnsearchable fetchContent(_isUnsearchable, url).then((fetched_content) => {
? await import(`../data/content/unsearchable${url}.json`)
: await import(`../data/content${url}.json`)
fetchedPage.content = fetched_content.content fetchedPage.content = fetched_content.content
? fetched_content.content ? fetched_content.content
: "No content" : "No content"
@ -246,32 +278,29 @@ export default class Page extends React.Component<PageProps, PageState> {
: fetchedPage?.title : fetchedPage?.title
? fetchedPage.title ? fetchedPage.title
: "No title" : "No title"
if (!_isUnsearchable) { if (!_isUnsearchable) {
fetchedPage.date = fetchedPage?.date fetchedPage.date = fetchedPage?.date
? fetchedPage.date ? fetchedPage.date
: "Unknown date" : "Unknown date"
} }
this.setState({ setIsSeries(_isSeries)
isSeries: _isSeries, setFetchPage(fetchedPage)
fetchedPage: fetchedPage, setIsLoading(false)
loading: false,
}) })
} }, [location])
render() { if (isLoading) return <Loading />
if (this.state.loading) return <Loading />
if (!this.state.fetchedPage) return <NotFound /> if (!fetchedPage) return <NotFound />
return ( return (
<> <>
<Helmet> <Helmet>
<title>pomp | {this.state.fetchedPage.title}</title> <title>pomp | {fetchedPage.title}</title>
<meta <meta property="og:title" content={fetchedPage.title} />
property="og:title"
content={this.state.fetchedPage.title}
/>
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta <meta
property="og:image" property="og:image"
@ -279,109 +308,56 @@ export default class Page extends React.Component<PageProps, PageState> {
/> />
</Helmet> </Helmet>
<div <div className="card main-content" style={{ paddingTop: 0 }}>
className="card main-content" {isSeries ? (
style={{ <NextPrevButton
paddingTop: 0, prevURL={seriesData?.prev}
}} nextURL={seriesData?.next}
>
{this.state.isSeries ? (
<NextPrev
prevURL={this.state.seriesData?.prev || null}
nextURL={this.state.seriesData?.next || null}
/> />
) : ( ) : (
<br /> <br />
)} )}
<StyledTitle>{this.state.fetchedPage.title}</StyledTitle> <StyledTitle>{fetchedPage.title}</StyledTitle>
{/* Post tags */}
<small> <small>
{/* Post tags */}
{!!fetchedPage.tags.length && (
<TagList direction="left"> <TagList direction="left">
{this.state.fetchedPage.tags ? ( {fetchedPage.tags.map((tag) => {
this.state.fetchedPage.tags.map((tag) => {
return ( return (
<td <td key={fetchedPage?.title + tag}>
key={
this.state.fetchedPage?.title +
tag
}
>
<Tag text={tag} /> <Tag text={tag} />
</td> </td>
) )
}) })}
) : (
<></>
)}
</TagList> </TagList>
)}
<br /> <br />
{!this.state.isUnsearchable && ( {/* Post metadata */}
<StyledMetaContainer> {!isPageUnsearchable && (
<FontAwesomeIcon icon={faCalendar} /> <PostMeta fetchedPage={fetchedPage} />
&nbsp;&nbsp;&nbsp;
{this.state.fetchedPage?.date || "Unknown date"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faHourglass} />
&nbsp;&nbsp;&nbsp;
{this.state.fetchedPage?.readTime
? this.state.fetchedPage?.readTime + " read"
: "unknown length"}
&nbsp;&nbsp;&nbsp;&nbsp;
<FontAwesomeIcon icon={faBook} />
&nbsp;&nbsp;&nbsp;
{this.state.fetchedPage?.wordCount
? this.state.fetchedPage.wordCount +
" words"
: "unknown words"}
</StyledMetaContainer>
)} )}
</small> </small>
<hr /> <hr />
{ {/* add table of contents if it exists */}
// add table of contents if it exists {!!fetchedPage.toc?.props.children.length && (
!!this.state.fetchedPage?.toc?.props.children <PageTOC fetchedPage={fetchedPage} />
.length && (
<>
<StyledTocToggleButton
onClick={() => {
this.setState({
isTocOpened:
!this.state.isTocOpened,
})
}}
>
<strong>Table of Content </strong>
{this.state.isTocOpened ? (
<FontAwesomeIcon icon={faCaretUp} />
) : (
<FontAwesomeIcon icon={faCaretDown} />
)} )}
</StyledTocToggleButton>
<StyledCollapseContainer>
<Collapse isOpened={this.state.isTocOpened}>
<div className="white-link">
{this.state.fetchedPage.toc}
</div>
</Collapse>
</StyledCollapseContainer>
<hr />
</>
)
}
{/* page content */}
<div <div
className="white-link" className="white-link"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: this.state.fetchedPage.content, __html: fetchedPage.content,
}} }}
/> />
</div> </div>
</> </>
) )
} }
}
export default Page

View file

@ -133,16 +133,11 @@ function isDateInRange(
return Date.parse(from) < compareDate && compareDate < Date.parse(to) return Date.parse(from) < compareDate && compareDate < Date.parse(to)
} }
function isSelectedTagsInPost( function isSelectedTagsInPost(selectedTags?: TagsData[], postTags?: string[]) {
selectedTags: TagsData[] | null, if (!selectedTags || selectedTags.length <= 0) return true
postTags: string[] | undefined if (!postTags || postTags.length <= 0) return false
) {
if (selectedTags == null) return true
if (selectedTags.length == 0) return true
if (postTags == undefined) return false // if tag is empty or undefined
// if tag is empty or null
const tagValues = selectedTags.map((value) => value.value) const tagValues = selectedTags.map((value) => value.value)
if (!postTags.every((val) => tagValues.includes(val))) return false if (!postTags.every((val) => tagValues.includes(val))) return false
@ -150,8 +145,8 @@ function isSelectedTagsInPost(
} }
// Search doesn't work on url change if component is not wrapped // Search doesn't work on url change if component is not wrapped
export default () => { const Search = () => {
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement | null>(null)
const navigate = useNavigate() const navigate = useNavigate()
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
@ -179,7 +174,7 @@ export default () => {
const [postCards, setPostCards] = useState<unknown[]>([]) const [postCards, setPostCards] = useState<unknown[]>([])
const [dateRange, setDateRange] = useState<Array<Range>>(defaultDateRange) const [dateRange, setDateRange] = useState<Array<Range>>(defaultDateRange)
const [searchInput, setSearchInput] = useState(query.query) const [searchInput, setSearchInput] = useState(query.query)
const [selectedTags, setSelectedOption] = useState<TagsData[] | null>( const [selectedTags, setSelectedOption] = useState<TagsData[] | undefined>(
query.tags.map((elem) => ({ label: elem, value: elem })) query.tags.map((elem) => ({ label: elem, value: elem }))
) )
@ -273,12 +268,12 @@ export default () => {
} }
// convert Date to YYYY-MM-DD string if it exists // convert Date to YYYY-MM-DD string if it exists
if (item.selection.startDate != null) if (item.selection.startDate)
historyToPush.from = item.selection.startDate historyToPush.from = item.selection.startDate
.toISOString() .toISOString()
.split("T")[0] .split("T")[0]
if (item.selection.endDate != null) if (item.selection.endDate)
historyToPush.to = item.selection.endDate historyToPush.to = item.selection.endDate
.toISOString() .toISOString()
.split("T")[0] .split("T")[0]
@ -348,8 +343,10 @@ export default () => {
interface TagSelectProps { interface TagSelectProps {
query: Query query: Query
selectedTags: TagsData[] | null selectedTags?: TagsData[]
setSelectedOption: React.Dispatch<React.SetStateAction<TagsData[] | null>> setSelectedOption: React.Dispatch<
React.SetStateAction<TagsData[] | undefined>
>
} }
const TagSelect: React.FC<TagSelectProps> = ({ const TagSelect: React.FC<TagSelectProps> = ({
@ -487,3 +484,5 @@ const TagSelect: React.FC<TagSelectProps> = ({
</StyledReactTagsContainer> </StyledReactTagsContainer>
) )
} }
export default Search